带问题重读ijkPlayer

问题

  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是接受者,它知道需要一个什么样的结果。
  • 这样在执行流程上一样的,能保持流程的稳定性;而实际执行时,在某些地方又有不同,从而又可以适应各种独特的需求。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 教程一:视频截图(Tutorial 01: Making Screencaps) 首先我们需要了解视频文件的一些基...
    90后的思维阅读 4,648评论 0 3
  • 在上一篇笔记中我们已经完成了使用SDL播放声音和视频,声音播放没有什么问题,而视频播放太快,很明显视频没有同步。在...
    762683ff5d3d阅读 1,299评论 0 1
  • 这是一个跨平台的播放器ijkplayer,iOS上集成看【如何快速的开发一个完整的iOS直播app】(原理篇)。 ...
    FindCrt阅读 7,041评论 2 45
  • Linear PCM 在介绍Core Audio之前,先介绍一下最常用的非压缩数字音频格式Linear PCM(线...
    huangjun0阅读 4,306评论 0 2
  • 根据ffmpeg官方网站上的例子程序开始学习ffmpeg和SDL编程。 SDL是一个跨平台的多媒体开发包。适用于游...
    762683ff5d3d阅读 1,779评论 0 2