带问题重读ijkPlayer

作者: FindCrt | 来源:发表于2018-10-25 12:24 被阅读11次

    问题

    1. 主流程上的区别
    2. 缓冲区的设计
    3. 内存管理的逻辑
    4. 音视频播放方式
    5. 音视频同步
    6. seek的问题:缓冲区flush、播放时间显示、k帧间距大时定位不准问题...
    7. stop时怎么释放资源,是否切换到副线程?
    8. 网络不好时的处理,如获取frame速度慢于消耗速度时,如果不暂停,会一致卡顿,是否会主动暂停?
    9. VTB的解码和ffmpeg的解码怎么统一的?架构上怎么设计的?

    数据流向

    主流程更详细看ijkPlayer主流程分析

    音频
    • av_read_frame
    • packet_queue_put
    • audio_thread+decoder_decode_frame+packet_queue_get_or_buffering
    • frame_queue_peek_writable+frame_queue_push
    • audio_decode_frame+frame_queue_peek_readable,数据到is->audio_buf
    • sdl_audio_callback,数据导入到参数stream里。这个函数是上层的音频播放库的buffer填充函数,如iOS里使用audioQueue,回调函数IJKSDLAudioQueueOuptutCallback调用到这里,然后把数据传入到audioQueue.
    视频

    读取packet部分一样

    • video_thread,然后ffpipenode_run_sync里硬解码定位到videotoolbox_video_thread,然后ffp_packet_queue_get_or_buffering读取。
    • VTDecoderCallback解码完成回调里,SortQueuePush(ctx, newFrame);把解码后的pixelBuffer装入到一个有序的队列里。
    • GetVTBPicture从有序队列里把frame的封装拿出来,也就是这个有序队列只是一个临时的用来排序的工具罢了,这个思想是可以吸收的queue_picture里,把解码的frame放入frame缓冲区
    • 显示video_refresh+video_image_display2+[IJKSDLGLView display:]
    • 最后的纹理生成放在了render里,对vtb的pixelBuffer,在yuv420sp_vtb_uploadTexture。使用render这个角色,渲染的部分都抽象出来了。shader在IJK_GLES2_getFragmentShader_yuv420sp

    结论:主流程上没有大的差别。

    缓冲区的设计

    packetQueue:

    1. 数据机构设计

    packetQueue采用两条链表,一个是保存数据的链表,一个是复用节点链表,保存没有数据的那些节点。数据链表从first_pktlast_pkt,插入数据接到last_pkt的后面,取数据从first_pkt拿。复用链表的开头是recycle_pkt,取完数据后的空节点,放到空链表recycle_pkt的头部,然后这个空节点成为新的recycle_pkt。存数据时,也从recycle_pkt复用一个节点。

    链表的节点像是包装盒,装载数据的时候放到数据链表,数据取出后回归到复用链表。

    1. 进出的阻塞控制

    取数据的时候可能没有,那么就有几种处理:直接返回、阻塞等待。它这里的处理是阻塞等待,并且会把视频播放暂停。所以这个回答了问题8,外面看到的效果就是:网络卡的时候,会停止播放然后流畅的播放一会,然后又继续卡顿,播放和卡顿是清晰分隔的。

    进数据的时候并没有做阻塞控制,为什么数据不会无限扩大?

    是有阻塞的,但阻塞不是在packetQueue里面,而是在readFrame函数里:

    if (ffp->infinite_buffer<1 && !is->seek_req &&
                 (is->audioq.size + is->videoq.size + is->subtitleq.size > ffp->dcc.max_buffer_size
               || (   stream_has_enough_packets(is->audio_st, is->audio_stream, &is->audioq, MIN_FRAMES)
                   && stream_has_enough_packets(is->video_st, is->video_stream, &is->videoq, MIN_FRAMES)
                   && stream_has_enough_packets(is->subtitle_st, is->subtitle_stream, &is->subtitleq, MIN_FRAMES)))) {
               if (!is->eof) {
                   ffp_toggle_buffering(ffp, 0);
               }
               /* wait 10 ms */
               SDL_LockMutex(wait_mutex);
               SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
               SDL_UnlockMutex(wait_mutex);
               continue;
           }
    

    简化来看就是:

    • infinite_buffer不是无限的缓冲
    • is->audioq.size + is->videoq.size + is->subtitleq.size > ffp->dcc.max_buffer_size,使用数据大小做限制
    • stream_has_enough_packets使用数据的个数做限制

    因为个数设置到了50000,一般达不到,而是数据大小做了限制,在15M左右。

    这里精髓的地方有两点:

    • 采用了数据大小做限制,因为对于不同的视频,分辨率的问题会导致同一个packet差距巨大,而我们实际关心的其实就是内存问题。
    • 暂停10ms,而不是无限暂停等待条件锁的signal。从设计上说会更简单,而且可以避免频繁的wait+signal。这个问题还需仔细思考,但直觉上觉得这样的操作非常好。

    frameQueue:

    数据使用一个简单的数组保存,可以把这个数据看成是环形的,然后也是其中一段有数据,另一段没有数据。rindex表示数据开头的index,也是读取数据的index,即read index,windex表示空数据开头的index,是写入数据的index,即write index。

    也是不断循环重用,然后size表示当前数据大小,max_size表示最大的槽位数,写入的时候如果size满了,就会阻塞等待;读取的时候size为空,也会阻塞等待。

    有个奇怪的东西是rindex_shown,读取的时候不是读的rindex位置的数据,而是rindex+rindex_shown,需要结合后面的使用情况再看这个的作用。后面再看。

    还有serial没有明白什么意思

    结论:缓冲区的设计和我的完全不同,但都使用重用的概念,而且节点都是包装盒,数据包装在节点里面。性能上不好比较,但我的设计更完善,frame和packet使用统一设计,还包含了排序功能。

    内存管理
    1. packet的管理
    • av_read_frame得到初始值,这个时候引用数为1,packet是使用一个临时变量去接的,也就是栈内存。
    • 然后加入队列时,pkt1->pkt = *pkt;使用值拷贝的方式把packet存入,这样缓冲区的数据和外面的临时变量就分离了。
    • packet_queue_get_or_buffering把packet取出来,同样使用值复制的方式。
    • 最后使用av_packet_unref把packet关联的buf释放掉,而临时变量的packet可以继续使用。

    需要注意的一点是:avcodec_send_packet返回EAGAIN表示当前还无法接受新的packet,还有frame没有取出来,所以有了:

    d->packet_pending = 1;
    av_packet_move_ref(&d->pkt, &pkt);
    

    把这个packet存到d->pkt,在下一个循环里,先取frame,再把packet接回来,接着上面的操作:

    if (d->packet_pending) {
        av_packet_move_ref(&pkt, &d->pkt);
        d->packet_pending = 0;
    }
    

    可能是存在B帧的时候会这样,因为B帧需要依赖后面的帧,所以不会解码出来,等到后面的帧传入后,就会有多个帧需要读取。这时解码器应该就不接受新的packet。但ijkplayer这里的代码似乎不会出现这样的情况,因为读取frame不是一次一个,而是一次性读到报EAGAIN错误未知。待考察。

    另,av_packet_move_ref这个函数就是完全的只复制,source的值完全的搬到destination,并且把source重置掉。其实就是搬了个位置,buf的引用数不改变。

    1. 视频frame的内存管理
    • ffplay_video_thread里,frame是一个对内存,使用get_video_frame从解码器读取到frame。这时frame的引用为1
    • 过程中出错,使用av_frame_unref释放frame的buf的内存,但frame本身还可以继续使用。不出错,也会调用av_frame_unref,这样保证每个读取的frame都会unref,这个unref跟初始化是对应的。使用引用指数来管理内存,重要的原则就是一一对应。

    因为这里只是拿到frame,然后存入缓冲区,还没有到使用的时候,如果buf被释放了,那么到播放的时候,数据就丢失了,所以是怎么处理的呢?

    存入缓冲区在queue_picture里,再到SDL_VoutFillFrameYUVOverlay,这个函数会到上层,根据解码器不同做不同处理,以ijksdl_vout_overlay_ffmpeg.cfunc_fill_frame为例。

    有两种处理:

    • 一种是overlay和frame共享内存,就显示的直接使用frame的内存,格式是YUV420p的就是这样,因为OpenGL可以直接显示这种颜色空间的图像。这种就只需要对frame加一个引用,保证不会被释放掉就好了。关键就是这句:av_frame_ref(opaque->linked_frame, frame);
    • 另一种是不共享,因为要转格式,另建一个frame,即这里的opaque->managed_frame,然后转格式。数据到了新地方,原frame也就没用了。不做ref操作,它自然的就会释放了。
    1. 音频frame的处理

    audio_thread里,不断通过decoder_decode_frame获取到新的frame。和视频一样,这里的frame也是对内存,读到解码后的frame后,引用为1。音频的格式转换放在了播放阶段,所以这里只是单纯的把frame存入:av_frame_move_ref(af->frame, frame);。做了一个复制,把读取的frame搬运到缓冲区里了。

    在frame的缓冲区取数据的时候,frame_queue_next里包含了av_frame_unref把frame释放。这个视频也是一样

    有一个问题是,上层播放器的读取音频数据的时候,frame必须是活的,因为如果音频不转换格式,是直接读取了frame里的数据。所以也就是需要在填充播放器数据结束后,才可以释放frame。

    unref是在frame_queue_next,而这个函数是在下一次读取frame的时候才发生,下一次读取frame又是在当前的数据读完后,所以读完了数据后,才会释放frame,这样就没错了。

    //数据读完才会去拉取下一个frame
    if (is->audio_buf_index >= is->audio_buf_size) {
        audio_size = audio_decode_frame(ffp);
    
    音视频的播放方式

    音频播放使用AudioQueue:

    • 构建AudioQueue:AudioQueueNewOutput
    • 开始AudioQueueStart,暂停AudioQueuePause,结束AudioQueueStop
    • 在回调函数IJKSDLAudioQueueOuptutCallback里,调用下层的填充函数来填充AudioQueue的buffer。
    • 使用AudioQueueEnqueueBuffer把装配完的AudioQueue Buffer入队,进入播放。

    上面这些都是AudioQueue的标准操作,特别的是构建AudioStreamBasicDescription的时候,也就是指定音频播放的格式。格式是由音频源的格式决定的,在IJKSDLGetAudioStreamBasicDescriptionFromSpec里看,除了格式固定为pcm之外,其他的都是从底层给的格式复制过来。这样就有了很大的自由,音频源只需要解码成pcm就可以了。

    而底层的格式是在audio_open里决定的,逻辑是:

    • 根据源文件,构建一个期望的格式wanted_spec,然后把这个期望的格式提供给上层,最后把上层的实际格式拿到作为结果返回。一个类似沟通的操作,这种思维很值得借鉴
    • 如果上传不接受这种格式,返回错误,底层修改channel数、采样率然后再继续沟通。
    • 但是样本格式是固定为s16,即signed integer 16,有符号的int类型,位深为16比特的格式。位深指每个样本存储的内存大小,16个比特,加上有符号,所以范围是[-2^15, 215-1],215为32768,变化性足够了。

    因为都是pcm,是不压缩的音频,所以决定性的因素就只有:采样率、通道数和样本格式。样本格式固定s16,和上层沟通就是决定采样率和通道数。

    这里是一个很好的分层架构的例子,底层通用,上层根据平台各有不同。

    视频的播放:

    播放都是使用OpenGL ES,使用IJKSDLGLView,重写了layerClass,把layer类型修改为CAEAGLLayer可以显示OpenGL ES的渲染内容。所有类型的画面都使用这个显示,有区别的地方都抽象到Render这个角色里了,相关的方法有:

    • setupRenderer 构建一个render
    • IJK_GLES2_Renderer_renderOverlay 绘制overlay。

    render的构建包括:

    • 使用不同的fragmnt shader和共通的vertex shader构建program
    • 提供mvp矩阵
    • 设置顶点和纹理坐标数据

    render的绘制包括:

    • func_uploadTexture定位到不同的render,执行不同的纹理上传操作
    • 绘制图形使用glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);,使用了图元GL_TRIANGLE_STRIP而不是GL_TRIANGLE,可以节省顶点。

    提供纹理的方法也是重点,区别在于颜色空间以及元素的排列方式:

    • rgb类型的提供了3种:565、888和8888。rgb类型的元素都是混合在一起的,也就是只有一个层(plane),565指是rgb3个元素分别占用的比特位数,同理888,8888是另外包含了alpha元素。所以每个像素565占2个字节,888占3个字节,8888占4个字节。
    glTexImage2D(GL_TEXTURE_2D,
                        0,
                        GL_RGBA,
                        widths[plane],
                        heights[plane],
                        0,
                        GL_RGBA,
                        GL_UNSIGNED_BYTE,
                        pixels[plane]);
    

    构建纹理的时候区别就在format跟type参数上。

    • yuv420p的,这种指的是最常用的y、u、v3个元素全部开,分3层,然后数量比是4:1:1,所以u v的纹理大小高和宽都是y纹理的一半。然后因为每个分量各自一个纹理,所以每个纹理都是单通道的,使用的format为GL_LUMINANCE
    • yuv420sp的,这种yuv的比例也是4:1:1,区别在于u v不是分开两层,而是混合在同一层里,分层是uuuuvvvv,混合是uvuvuvuv。所以构建两个纹理,y的纹理不变,uv的纹理使用双通道的格式GL_RG_EXT,大小也是y的1/4(高宽都为1/2)。这种在fragment shader里取值的时候会有区别:
    //3层的
    yuv.y = (texture2D(us2_SamplerY, vv2_Texcoord).r - 0.5);
           yuv.z = (texture2D(us2_SamplerZ, vv2_Texcoord).r - 0.5);
    //双层的
    yuv.yz = (texture2D(us2_SamplerY,  vv2_Texcoord).rg - vec2(0.5, 0.5));
    

    uv在同一个纹理里,texture2D直接取了rg两个分量。

    • yuv444p的不是很懂,看fragment shader貌似每个像素有两个版本的yuv,然后做了一个插值。
    • 最后是yuv420p_vtb,这个是VideoToolBox硬解出来的数据的显示,因为数据存储在CVPixelBuffer里,所以直接使用了iOS系统的纹理构建方法。

    ijkplayer里的的OpenGL ES是2.0版本,如果使用3.0版本,双通道可以使用GL_LUMINANCE_ALPHA

    音视频同步

    首先看音频,音频并没有做阻塞控制,上层的的播放器要需要数据都会填充,没有看到时间不到不做填充的操作。所以应该是默认了音频钟做主控制,所以音频没做处理。

    1. 视频显示时的时间控制

    视频的控制在video_refresh里,播放函数是video_display2,进入这里代表时间到了、该播了,这是一个检测点。

    有几个参数需要了解:

    • is->frame_timer,这个时间代表上一帧播放的时间
    • delay表示这一帧到下一帧的时间差
    if (isnan(is->frame_timer) || time < is->frame_timer){
        is->frame_timer = time;
    }
    

    上一帧的播放时间在当前时间后面,说明数据错误,调整到当期时间

    if (time < is->frame_timer + delay) {
        *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
        goto display;
    }
    

    is->frame_timer + delay就表示当前帧播放的时间,这个时间晚于当前时间,就表示还没到播放的时候。

    这个有个坑goto display并不是去播放了,因为display代码块里还有一个判断,判断里有个is->force_refresh。这个值默认是false,所以直接跳去display,实际的意义是啥也不干,结束这次判断。

    反之,如果播放时间早于当前时间,那就要马上播放了。所以更新上一帧的播放时间:is->frame_timer += delay;

    然后一直到后面,有个is->force_refresh = 1;,这时才是真的播放。

    从上面两段就可以看出基本的流程了:

    一开始当前帧播放时间没到,goto display等待下次循环,循环多次,时间不段后移,终于播放时间到了,播放当前帧,frame_timer更新为当前帧的时间。然后又重复上面的过程,去播放下一帧。

    然后有个问题是:为什么frame_timer的更新是加上delay,而不是直接等于当前时间?

    如果直接等于当前时间,因为time>= frame_timer+delay,那么frame_timer是相对更大了一些,那么在计算下一帧时间,也就是frame_timer+delay的时候,也就会大一点。而且每一帧都会是这个情况,最后每一帧都会大那么一点,整体而言可能会有有比较大的差别。

    if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX){
        is->frame_timer = time;
    }
    

    在frame_timer比较落后的时候,直接提到当前time上,就可以直接把状态修正,之后的播放又会走上正轨。

    2. 同步钟以及钟时间的修正

    同步钟的概念: 音频或者视频,如果把内容正确的完整的播放,某个内容和一个时间是一一对应的,当前的音频或者视频播放到哪个位置,它就有一个时间来表示,这个时间就是同步钟的时间。所以音频钟的时间表示音频播放到哪个位置,视频钟表示播放到哪个位置。

    因为音频和视频是分开表现的,就可能会出现音频和视频的进度不一致,在同步钟上就表现为两个同步钟的值不同,如果让两者统一,就是音视频同步的问题。

    因为有了同步钟的概念,音视频内容上的同步就可以简化为更准确的:音频钟和视频钟时间相同。

    这时会有一个同步钟作为主钟,也就是其他的同步钟根据这个主钟来调整自己的时间。满了就调快、快了就调慢。

    compute_target_delay里的逻辑就是这样,diff = get_clock(&is->vidclk) - get_master_clock(is);这个是视频钟和主钟的差距:

    //视频落后超过临界值,缩短下一帧时间
    if (diff <= -sync_threshold)
        delay = FFMAX(0, delay + diff);
    //视频超前,且超过临界值,延长下一帧时间
    else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)
        delay = delay + diff;
    else if (diff >= sync_threshold)
        delay = 2 * delay;
    

    至于为什么不都是delay + diff,即为什么还有第3种1情况,我的猜测是:

    延时直接加上diff,那么下一帧就直接修正了视频种和主钟的差异,但有可能这个差异已经比较大了,直接一步到位的修正导致的效果就是:画面有明显的停顿,然后声音继续播,等到同步了视频再恢复正常。

    而如果采用2*delay的方式,是每一次修正delay,多次逐步修正差异,可能变化上会更平滑一些。效果上就是画面和声音都是正常的,然后声音逐渐的追上声音,最后同步。

    至于为什么第2种情况选择一步到位的修正,第3种情况选择逐步修正,这个很难说。因为AV_SYNC_FRAMEDUP_THRESHOLD值为0.15,对应的帧率是7左右,到这个程度,视频基本都是幻灯片了,我猜想这时逐步修正也没意义了。

    3. 同步钟时间获取的实现

    再看同步钟时间的实现:get_clock获取时间, set_clock_at更新时间。

    解析一下:return c->pts_drift + time - (time - c->last_updated) * (1.0 - c->speed);,为啥这么写?

    上一次显示的时候,更新了同步钟,调用set_clock_at,上次的时间为c->last_updated,则:

    c->pts_drift + time = (c->pts - c->last_updated)+time;

    假设距离上次的时间差time_diff = time - c->last_updated,则表达式整体可以变为:

    c->pts+time_diff+(c->speed - 1)*time_diff,合并后两项变为:
    c->pts+c->speed*time_diff.

    我们要求得就是当前时间时的媒体内容位置,上次的位置是c->pts,而中间过去了time_diff这么多时间,媒体内容过去的时间就是:播放速度x现实时间,也就是c->speed*time_diff。举例:现实里过去10s,如果你2倍速的播放,那视频就过去了20s。所以这个表达式就很清晰了。

    set_clock_speed里同时调用了set_clock,这是为了保证从上次更新时间以来,速度是没变的,否则计算就没有意义了。

    到这差不多了,还有一点是在seek时候同步钟的处理,到seek问题的时候再看。

    seek的处理

    seek就是调整进度条到新的地方开始播,这个操作会打乱原本的数据流,一些播放秩序要重新建立。需要处理的问题包括:

    • 缓冲区数据的释放,而且要重头到位全部释放干净
    • 播放时间显示
    • “加载中”的状态的维护,这个影响着用户界面的显示问题
    • 剔除错误帧的问题

    流程

    1. 外界seek调用到ijkmp_seek_to_l,然后发送消息ffp_notify_msg2(mp->ffplayer, FFP_REQ_SEEK, (int)msec);,消息捕获到后调用到stream_seek,然后设置seek_req为1,记录seek目标到seek_pos
    2. 在读取函数read_thread里,在is->seek_req为true时,进入seek处理,几个核心处理:
    • ffp_toggle_buffering关闭解码,packet缓冲区静止
    • 调用avformat_seek_file进行seek
    • 成功之后用packet_queue_flush清空缓冲区,并且把flush_pkt插入进去,这时一个标记数据
    • 把当前的serial记录下来

    到这里值得学习的点是:

    • 我在处理seek的时候,是另开一个线程调用了ffmpeg的seek方法,而这里是直接在读取线程里,这样就不用等待读取流程的结束了
    • seek成功之后再flush缓冲区

    因为

    if (pkt == &flush_pkt)
            q->serial++;
    

    所以serial的意义就体现出来了,每次seek,serial+1,也就是serial作为一个标记,相同代表是同一次seek里的。

    1. decoder_decode_frame里:
    • 因为seek的修改是在读取线程里,和这里的解码线程不是一个,所以seek的修改可以在这里代码的任何位置出现。
    • if (d->queue->serial == d->pkt_serial)这个判断里面为代码块1,while (d->queue->serial != d->pkt_serial)这个循环为代码块2,if (pkt.data == flush_pkt.data)这个判断为true为代码块3,false为代码块4.
    • 如果seek修改出现在代码块2之前,那么就一定会进代码块2,因为packet_queue_get_or_buffering会一直读取到flush_pkt,所以也就会一定进代码块3,会执行avcodec_flush_buffers清空解码器的缓存。
    • 如果seek在代码块2之后,那么就只会进代码块4,但是再循环回去时,会进代码块2、代码块3,然后avcodec_flush_buffers把这个就得packet清掉了。
    • 综合上面两种情况,只有seek之后的packet才会得到解码,牛逼!

    这一段厉害在:

    • seek的修改在任何时候,它都不会出错
    • seek的处理是在解码线程里做的,省去了条件锁等线程间通信的处理,更简单稳定。如果整个数据流是一条河流,那flush_pkt就像一个这个河流的一个浮标,遇到这个浮标,后面水流的颜色都变了。有一种自己升级自己的这种意思,而不是由一个第三方来做辅助的升级。对于流水线式的程序逻辑,这样做更好。
    1. 播放处

    视频video_refresh里:

       if (vp->serial != is->videoq.serial) {
           frame_queue_next(&is->pictq);
           goto retry;
       }
    

    音频audio_decode_frame里:

        do {
           if (!(af = frame_queue_peek_readable(&is->sampq)))
               return -1;
           frame_queue_next(&is->sampq);
        } while (af->serial != is->audioq.serial);
    

    都根据serial把旧数据略过了。

    所以整体看下来,seek体系里最厉害的东西的东西就是使用了serial来标记数据,从而可以很明确的知道哪些是就数据,哪些是新数据。然后处理都是在原线程里做的处理,而不是在另外的线程里来修改相关的数据,省去了线程控制、线程通讯的麻烦的操作,稳定性也提高了。

    播放时间获取

    ijkmp_get_current_position,seek时,返回seek的时间,播放时看ffp_get_current_position_l,核心就是内容时间get_master_clock减去开始时间is->ic->start_time

    seek的时候,内容位置发生了一个巨大的跳跃,所以要怎么维持同步钟的正确?

    • 音频和视频数据里的pts都是frame->pts * av_q2d(tb),也就是内容时间,但是转成了现实时间单位。
    • 然后is->audio_clock = af->pts + (double) af->frame->nb_samples / af->frame->sample_rate;,所以is->audio_clock是最新的一帧音频的数据播完时内容时间
    • 在音频的填充方法里,设置音频钟的代码是:
    set_clock_at(&is->audclk, 
    is->audio_clock - (double)(is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec - SDL_AoutGetLatencySeconds(ffp->aout), 
    is->audio_clock_serial, 
    ffp->audio_callback_time / 1000000.0);
    

    因为is->audio_write_buf_size = is->audio_buf_size - is->audio_buf_index;,所以audio_write_buf_size就是当前帧还没读完剩余的大小,所以(double)(is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec就标识剩余的数据播放完的时间。

    SDL_AoutGetLatencySeconds(ffp->aout)是上层的缓冲区的数据的时间,对iOS的AudioQueue而言,有多个AudioBuffer等待播放,这个时间就是它们播放完要花的时间。

    时间轴上是这样的:

    [帧结束点][剩余buf时间][上层的buf时间][刚结束播放的点]

    所以第二个参数的时间是:当前帧结束时的内容时间-剩余buf的时间-上层播放器buf的时间,也就是刚结束播放的内容时间。

    ffp->audio_callback_time是填充方法调用时的时间,这里存在一个假设,就是上层播放器播完了一个buffer,立马调用了填充函数,所以ffp->audio_callback_time就是刚结束播放的现实时间。

    这样第2个参数和第4个参数的意义就匹配上了。

    回到seek,在seek完成后,会有第一个新的frame进入播放,它会把同步钟的pts,也就是媒体的内容时间调整到seek后的位置,那么还有一个问题:mp->seek_req这个标识重置回0的时间点必须比第一个新frame的set_clock_at要晚,否则同步钟的时间还没调到新的,seek的标识就结束了,然后根据同步钟去计算当前的播放时间,就出错了(界面上应该是进度条闪回seek之前)。

    而事实上并没有这样,因为在同步钟的get_clock,还有一个

    if (*c->queue_serial != c->serial)
            return NAN;
    

    这个serial真是神操作,太好用了!

    音频钟和视频钟的serial都是在播放时更新的,也就是第一帧新数据播放时更新到seek以后的serial,而c->queue_serial是一个指针:init_clock(&is->vidclk, &is->videoq.serial);,和packetQueue的serial共享内存的。

    所以也就是到第一帧新数据播放后,c->queue_serial != c->serial这个才不成立。也就是即使mp->seek_req重置回0,取得值还是seek的目标值,还不是根据pts计算的,所以也不会闪回了。

    关于seek的东西太复杂了。

    stop时的资源释放

    从方法shutdown到核心释放方法stream_close。操作的流程如下:

    1. 停掉读取线程:
    • packet_queue_abort把音视频的packetQueue停止读取
    • abort_request标识为1,然后SDL_WaitThread等待线程结束
    1. 停掉解码器部分stream_component_close
    • decoder_abort停掉packetQueue,放开framequeue的阻塞,等待解码线程结束,然后清空packetQueue。
    • decoder_destroy 销毁解码器
    • 重置流数据为空
    1. 停掉显示线程:在显示线程里有判断数据流,视频is->video_st,音频is->audio_st,在上一步里把流重置为空,显示线程会结束。这里同样使用SDL_WaitThread等待线程结束。
    2. 清空缓冲区数据:packet_queue_destroy销毁packetQueue,frame_queue_destory销毁frameQueue。

    对比我写的,需要修改的地方:

    • 结束线程使用pthread_join的方式,而不是用锁
    • 解码器、缓冲区等全部摧毁,下次播放再重建,不要重用
    • 音频的停止通过停掉上层播放器,底层是被动的,而且没有循环线程;视频的停止也只需要等待线程结束。

    核心就是第一点,使用pthread_join等待线程结束。

    网络不好处理

    会自动暂停,等待。内部可以控制播放或暂停。

    使用VTB时架构的统一
    1. frame缓冲区使用自定义的数据结构Frame,通过他可以把各种样式进行统一。
    2. 下层拥有了Frame数据,上层的对接对象时Vout,边界就在这里。然后上层要的是overlay,所以问题就是怎么由frame转化成overlay,以及如何显示overlay。这两个操作由Vout提供的create_overlaydisplay_overlay来完成。
    3. 使用VTB之后,数据存在解码后获得的pixelBuffer里,而ffmpeg解码后的数据在AVFrame里,这个转化的区别就在不同的overlay创建函数里。

    总结:

    • 对于两个模块的连接处,为了统一,两边都需要封装统一的模型;
    • 在统一的模型内,又具有不同的操作细分;
    • 输入数据从A到B,那么细分操作由B来提供,应为B是接受者,它知道需要一个什么样的结果。
    • 这样在执行流程上一样的,能保持流程的稳定性;而实际执行时,在某些地方又有不同,从而又可以适应各种独特的需求。

    相关文章

      网友评论

      本文标题:带问题重读ijkPlayer

      本文链接:https://www.haomeiwen.com/subject/ccbqtqtx.html