陆小曼|超详细解析FFplay之音视频解码线程


1.解码线程
ffplay的解码线程独?于数据读线程 , 并且每种类型的流(AVStream)都有其各?的解码线程 , 如:
(1)video_thread?于解码video stream 。
(2)audio_thread?于解码audio stream 。
(3)subtitle_thread?于解码subtitle stream 。
为?便阅读 , 先列?张表格 , 梳理各个变量、函数名称 。
陆小曼|超详细解析FFplay之音视频解码线程
本文插图

其中PacketQueue?于存放从read_thread取到的各?播放时间内的AVPacket 。 FrameQueue?于存放各?解码后的AVFrame 。 Clock?于同步?视频 。 解码线程负责将PacketQueue数据解码为AVFrame , 并存?FrameQueue 。 对于不同流 , 其解码过程?同?异 。

/** * 解码器封装 */typedef struct Decoder {AVPacket pkt;// 数据包队列PacketQueue *queue;// 解码器上下?AVCodecContext *avctx;// 包序列int pkt_serial;// =0 , 解码器处于?作状态;=?0 , 解码器处于空闲状态int finished;// =0 , 解码器处于异常状态 , 需要考虑重置解码器;=1 , 解码器处于正常状 态int packet_pending;// 检查到packet队列空时发送 signal缓存 , read_thread读取数据SDL_cond *empty_queue_cond;// 初始化时是stream的start timeint64_t start_pts;// 初始化时是stream的time_baseAVRational start_pts_tb;// 记录最近?次解码后的frame的pts , 当解出来的部分帧没有有效的pts 时则使?next_pts进?推算int64_t next_pts;// next_pts的单位AVRational next_pts_tb;// 线程句柄SDL_Thread *decoder_tid; } Decoder;解码器相关的函数
decoder我们ffplay?定义 , 重新封装的 。avcodec才是ffmpeg的提供 。
初始化解码器 。
void decoder_init(Decoder *d, AVCodecContext *avctx, PacketQueue *queue,SDL_cond *empty_queue_cond);
启动解码器
int decoder_start(Decoder *d, int (*fn)(void *), const char *thread_name, void* arg)
解帧
int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub);

终?解码器
void decoder_abort(Decoder *d, FrameQueue *fq);
销毁解码器
void decoder_destroy(Decoder *d);
使??法如下:
(1)启动解码线程
decoder_init() 。
decoder_start() 。
(2)解码线程具体流程
decoder_decode_frame()
(3)退出解码线程
decoder_abort()
decoder_destroy()
2.视频解码线程
数据来源:从read_thread线程?来 。
数据处理:在video_thread进?解码 , 具体调?get_video_frame 。
数据出?:在video_refresh读取frame进?显示 。
video_thread() , 先看video_thead , 对于滤镜部分(CONFIG_AVFILTER定义部分) , 暂时不做分析, 需要知道有这个功能就可以 , 简化后的代码如下:

static int video_thread(void *arg){VideoState *is = arg;// 分配解码帧AVFrame *frame = av_frame_alloc();// ptsdouble pts;// 帧持续时间double duration;int ret;// 1 获取stream timebaseAVRational tb = is->video_st->time_base;// 2 获取帧率 , 以便计算每帧picture的durationAVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st , NULL);if (!frame)return AVERROR(ENOMEM);for (;;) {// 循环取出视频解码的帧数据// 3 解码获取?帧视频画?ret = get_video_frame(is, frame);if (ret < 0)goto the_end; //解码结束, 什么时候会结束if (!ret) //没有解码得到画?, 什么情况下会得不到解后的帧continue;// 4 计算帧持续时间和换算pts值为秒//没有帧率时则设置为0, 有帧率帧计算出帧间隔duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRat ional){frame_rate.den, 27 frame_r ate.num}) : 0);// 根据AVStream timebase计算出pts值, 单位为秒pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av _q2d(tb);// 5 将解码后的视频帧插?队列ret = queue_picture(is, frame, pts, duration, frame->pkt_pos , is->viddec.pkt_serial);// 6 释放frame对应的数据// 正常情况下frame对应的buf以被av_frame_move_refav_frame_unref(frame);if (ret < 0) // 返回值?于0则退出线程goto the_end;}the_end:av_frame_free(&frame); // 释放framereturn 0; }在该流程中 , 当调?函数返回值?于<0时则退出线程 。 线程的步骤如下:
(1). 获取stream timebase , 以便将frame的pts转成秒为单位
(2). 获取帧率 , 以便计算每帧picture的duration
(3). 获取解码后的视频帧 , 具体调?get_video_frame()实现
(4). 计算帧持续时间和换算pts值为秒
(5). 将解码后的视频帧插?队列 , 具体调?queue_picture()实现
(6). 释放frame对应的数据
下面重点说说get_video_frame()和queue_picture() 。
get_video_frame 简化如下:

static int get_video_frame(VideoState *is, AVFrame *frame) {int got_picture;// 1. 获取解码后的视频帧if ((got_picture = decoder_decode_frame(&is->viddec, frame, NULL )) < 0) {// 返回-1意味着要退出解码线程return -1;}if (got_picture) {// 2. 分析获取到的该帧是否要drop掉.....}return got_picture; }主要流程:
(1)调? decoder_decode_frame 解码并获取解码后的视频帧 。
(2)分析如果获取到帧是否需要drop掉 。 逻辑就是如果刚解出来就落后主时钟 , 那就没有必要放?Frame队列 , 再拿去播放 , 但是也是有?定的条件的 , ?下?分析 。
被简化的部分主要是针对丢帧的?个处理:
if (got_picture) {2 // 2. 分析获取到的该帧是否要drop掉, 该机制的?的是在放?帧队列前先drop掉过时 的视频帧3 double dpts = NAN;45 if (frame->pts != AV_NOPTS_VALUE)6 dpts = av_q2d(is->video_st->time_base) * frame->pts;//计 算出秒为单位的pts78 frame->sample_aspect_ratio = av_guess_sample_aspect_ratio(is->ic , is->video_st, frame);910 if (framedrop>0 ||// 允许drop帧11 (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MAST ER))//?视频同步模式12 { 13 if (frame->pts != AV_NOPTS_VALUE) { // pts值有效
先确定进?丢帧检测流程 , 控制是否进?丢帧检测有3种情况:
(1)控制是否丢帧的开关变量是 framedrop, 为1 , 则始终判断是丢帧 。
(2)framedrop 为0 , 则始终不丢帧 。
(3)framedrop 为-1(默认值) , 则在主时钟不是video的时候 , 判断是否丢帧 。
如果进?丢帧检测流程 , drop帧需要下列因素都成?:
(1)!isnan(diff):当前pts和主时钟的差值是有效值 。
(2)fabs(diff) < AV_NOSYNC_THRESHOLD:差值在可同步范围内 , 这?设置的是10秒 , 意思是如果差值太?这?就不管了了 , 可能流本身录制的时候就有问题 , 这?不能随便把帧都drop掉 。
(3)diff - is->frame_last_filter_delay < 0:和过滤器有关系 , 不设置过滤器时简化为 diff < 0 。
(4)is->viddec.pkt_serial == is->vidclk.serial:解码器的serial和时钟的serial相同 , 即是?少显示了?帧图像 , 因为只有显示的时候才调?update_video_pts()设置到video clk的serial 。
(5)is->videoq.nb_packets:?少packetqueue有1个包 。
接下来看下真正解码的过程—— decoder_decode_frame, 这个函数也包含了对audio和subtitle的解码 , 其返回值:
-1:请求退出解码器线程

0:解码器已经完全冲刷 , 没有帧可读 , 这?也说明对应码流播放结束 。
1:正常解码获取到帧 。
先看简化后的主?代码(注意for(;;)这个?循环):

static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtit le *sub) {for (;;) {// ?循环//1. 流连续情况下获取解码后的帧4 if (d->queue->serial == d->pkt_serial) {do {if (d->queue->abort_request)return -1; // 是否请求退出ret = avcodec_receive_frame(d->avctx, frame);if (ret == AVERROR_EOF) {return 0; // 解码器已完全冲刷 , 没有帧可读了}if (ret >= 0)return 1; // 读取到解码帧} while (ret != AVERROR(EAGAIN));}//2. 获取?个packet , 如果播放序列不?致(数据不连续)则过滤掉“过时”的packetdo {if (d->queue->nb_packets == 0)//如果没有数据可读则唤醒read_th readSDL_CondSignal(d->empty_queue_cond);if (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial) < 0)// 阻塞?式读packetreturn -1;} while (d->queue->serial != d->pkt_serial); // 播放序列的判断//3. 将packet送?解码器avcodec_send_packet(d->avctx, &pkt);}}decoder_decode_frame 的主?代码是?个循环 , 要拿到?帧解码数据 , 或解码出错、?件结束 , 才会 返回 。 decoder_decode_frame 的主?代码是?个循环 , 要拿到?帧解码数据 , 或解码出错、?件结束 , 才会返回 。

循环内可以分解为3个步骤:
(1)同?播放序列流连续的情况下 , 不断调?avcodec_receive_frame获取解码后的frame 。
a. d->queue 就是video PacketQueue(videoq)
b. d->pkt_serial 是最近?次取的packet的序列号 。 在判断完 d->queue->serial == d->pkt_serial 确保流连续后 , 循环调? avcodec_receive_frame, 有取到帧就返回 。 使还没送?新的Packet , 这是为了兼容?个Packet可以解出多个Frame的情况 。
(2)获取?个packet , 如果播放序列不?致(数据不连续)则过滤掉“过时”的packet 。 主要阻塞调?packet_queue_get () 。 另外 , 会在PacketQueue为空时 , 发送 empty_queue_cond 条件信号 , 通知读线程继续读数据 。 ( empty_queue_cond 就是 continue_read_thread, 可以参考read线程的分析 , 查看读线程何时会等待该条件量 。
(3)将packet送?解码器 。
先看avcodec_receive_frame的具体流程 , 这?先省略Audio的case:

// 1. 流连续情况下获取解码后的帧if (d->queue->serial == d->pkt_serial) {// 1.1 先判断是否是同?播放序列 的数据do {if (d->queue->abort_request)return -1; // 是否请求退出// 1.2. 获取解码帧switch (d->avctx->codec_type) {case AVMEDIA_TYPE_VIDEO:ret = avcodec_receive_frame(d->avctx, frame);//printf("frame pts:%ld, dts:%ld\n", frame->pts, fra me->pkt_dts);if (ret >= 0) {if (decoder_reorder_pts == -1) {frame->pts = frame->best_effort_timestamp;} else if (!decoder_reorder_pts) {frame->pts = frame->pkt_dts;}}break;case AVMEDIA_TYPE_AUDIO:ret = avcodec_receive_frame(d->avctx, frame);....break;}// 1.3. 检查解码是否已经结束 , 解码结束返回0if (ret == AVERROR_EOF) {d->finished = d->pkt_serial;printf("avcodec_flush_buffers %s(%d)\n", __FUNCTION__, _ _LINE__);// 调?该函数后可以再次解 码 , 只要有数据packet进?, 取出缓存avcodec_flush_buffers(d->avctx);return 0; }// 1.4. 正常解码返回1if (ret >= 0)return 1;} while (ret != AVERROR(EAGAIN));// 1.5 没帧可读时ret返回EAGIN ,需要继续送packet}注意返回值:注意返回值:

-1:请求退出解码器线程 。
0:解码器已经完全冲刷 , 没有帧可读 , 这?也说明对应码流播放结束 。
1:正常解码获取到帧 。
重点分析以下:
(1)decoder_reorder_pts
ret = avcodec_receive_frame(d->avctx, frame);if (ret >= 0) {if (decoder_reorder_pts == -1) {frame->pts = frame->best_effort_timestamp;} else if (!decoder_reorder_pts) {frame->pts = frame->pkt_dts;}}decoder_reorder_pts:让ffmpeg内部排序pts 。 这里表示解码后的frame的pts , 该用何值 。
0=off, 1=on ,-1=auto 。 默认为-1 。 (ffplay配置 -drp value进?设置) 。
0:frame的pts使?pkt_dts , 这种情况很少(涉及到时间基的变化) 。
1:frame保留??的pts 。
-1:frame的pts使?frame->best_effort_timestamp , best_effort_timestamp是经过算法计算出来的值 , 主要是“尝试为可能有错误的时间戳猜测出适当单调的时间戳” , ?部分情况下还是frame->pts , 或者就是frame->pkt_dts 。
(2)avcodec_flush_buffers

使?“空包”冲刷解码器后 , 如果要再次解码则需要调?avcodec_flush_buffers() , 之所以在这个节点调?avcodec_flush_buffers() , 主要是让我们在循环播放码流的时候可以继续正常解码 , 不刷空包 , 再次循环播放 , 解码时 , 有可能出错 。
获取?个packet , 如果播放序列不?致(数据不连续)则过滤掉“过时”的packet , 怎么理解呢?

// 2 获取?个packet , 如果播放序列不?致(数据不连续)则过滤掉“过时”的packetdo {// 2.1 如果没有数据可读则唤醒read_thread, 实际是continue_read_thread S DL_condif (d->queue->nb_packets == 0) // 没有数据可读SDL_CondSignal(d->empty_queue_cond);// 通知read_thread放?packet// 2.2 如果还有pending的packet则使?它if (d->packet_pending) {av_packet_move_ref(&pkt, &d->pkt);d->packet_pending = 0;} else {// 2.3 阻塞式读取packet , 这?好理解 , 就是读packet并获取serialif (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial) < 0)return -1;}if(d->queue->serial != d->pkt_serial) {// darren??的代码printf("%s(%d) discontinue:queue->serial:%d,pkt_serial:%d\n" ,__FUNCTION__, __LINE__, d->queue->serial, d->pkt_seri al);av_packet_unref(&pkt); // fixed me? 释放要过滤的packet}} while (d->queue->serial != d->pkt_serial);// 如果不是同?播放序列(流不连续)则继续读取(1)如果还有pending的packet则使?它 , pending就表示Packet还没发送完 。
// 2.2 如果还有pending的packet则使?它if (d->packet_pending) {av_packet_move_ref(&pkt, &d->pkt);d->packet_pending = 0;}
pending包packet和 packet_pending 的概念的来源 , 来?send失败时重新发送:
if (avcodec_send_packet(d->avctx, &pkt) == AVERROR(EAGAIN)) {av_log(d->avctx, AV_LOG_ERROR, "Receive_frame and send_packet bot h returned EAGAIN, which is an API violation.\n");d->packet_pending = 1;av_packet_move_ref(&d->pkt, &pkt);}如果 avcodec_send_packet 返回 EAGAIN, 则把当前 pkt 存? d->pkt, 然后置标志位packet_pending 为1 。
(2)do {} while (d->queue->serial != d->pkt_serial);这个意思就是说 , 如果不是同?播放序列(流不连续)则继续读取 。
d->queue->serial是最新的播放序列 , 当读取出来的packet的serial和最新的serial不同时则过滤掉 , 继续读取packet , 但检测到不是同?serial , 是不是应该释放掉packet的数据??如下列代码:
if(d->queue->serial != d->pkt_serial) {printf("%s(%d) discontinue:queue->serial:%d,pkt_serial:%d\n",__FUNCTION__, __LINE__, d->queue->serial, d->pkt_serial);av_packet_unref(&pkt); // fixed me? 释放要过滤的packet}将packet送?解码器

// 3 将packet送?解码器if (pkt.data =http://news.hoteastday.com/a/= flush_pkt.data) {//// when seeking or when switching to a different streamavcodec_flush_buffers(d->avctx); //清空??的缓存帧d->finished = 0; // 重置为0d->next_pts = d->start_pts; // 主要?在了audiod->next_pts_tb = d->start_pts_tb;// 主要?在了audio} else {if (d->avctx->codec_type == AVMEDIA_TYPE_SUBTITLE) {int got_frame = 0;ret = avcodec_decode_subtitle2(d->avctx, sub, &got_frame, &p kt);if (ret < 0) {ret = AVERROR(EAGAIN);} else {if (got_frame && !pkt.data) {d->packet_pending = 1;av_packet_move_ref(&d->pkt, &pkt);}ret = got_frame ? 0 : (pkt.data ? AVERROR(EAGAIN) : AVER ROR_EOF);}} else {//将packet送?解码器if (avcodec_send_packet(d->avctx, &pkt) == AVERROR(EAGAIN)) {av_log(d->avctx, AV_LOG_ERROR,"Receive_frame and send_p acket both returned EAGAIN, which is an API violation.\n");d->packet_pending = 1;25 av_packet_move_ref(&d->pkt, &pkt);}}av_packet_unref(&pkt); // ?定要??去释放?视频 字幕数据}重点:
(1)有针对 flush_pkt 的处理

if (pkt.data =http://news.hoteastday.com/a/= flush_pkt.data) {//// when seeking or when switching to a different streamavcodec_flush_buffers(d->avctx); //清空??的缓存帧d->finished = 0; // 重置为0d->next_pts = d->start_pts; // 主要?在了audiod->next_pts_tb = d->start_pts_tb;// 主要?在了audio}了解过PacketQueue的代码 , 我们知道在往PacketQueue送??个flush_pkt后 , PacketQueue的serial值会加1 , ?送?的flush_pkt和PacketQueue的新serial值保持?致 。 所以如果有“过时(旧serial)”Packet , 过滤后 , 取到新的播放序列第?个pkt将是flush_pkt 。 起到了一个分割作用 。 根据api要求 , 此时需要调? avcodec_flush_buffers。
(2)avcodec_send_packet后出现AVERROR(EAGAIN) , 则说明我们要继续调?avcodec_receive_frame()将frame读取 , 再调?avcodec_send_packet发packet 。 由于出现AVERROR(EAGAIN)返回值解码器内部没有接收传?的packet , 但?没法放回PacketQueue , 所以我们就缓存到了?封装的Decoder的pkt(即是d->pkt)(暂存) , 并将 d->packet_pending = 1 , 以备下次继续使?该packet 。

if (avcodec_send_packet(d->avctx, &pkt) == AVERROR(EAGAIN)) {av_log(d->avctx, AV_LOG_ERROR, "Receive_frame and send_packet bot h returned EAGAIN, which is an API violation.\n");d->packet_pending = 1;av_packet_move_ref(&d->pkt, &pkt);}queue_picture()
上? , 我们就分析完video_thread中关键的 get_video_frame 函数 , 根据所分析的代码 , 已经可以取到正确解码后的?帧数据 。 接下来就要把这?帧放?FrameQueue:
// 4 计算帧持续时间和换算pts值为秒 // 1/帧率 = duration 单位秒, 没有帧率时则设置为0, 有帧率帧计算出帧间隔duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){fr ame_rate.den, frame_rate.num}) : 0);// 根据AVStream timebase计算出pts值, 单位为秒 pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);// 5 将解码后的视频帧插?队列ret = queue_picture(is, frame, pts, duration, frame->pkt_pos, is->vid dec.pkt_serial);// 6 释放frame对应的数据av_frame_unref(frame);主要调? queue_picture, 看看他做了什么:

static int queue_picture(VideoState *is, AVFrame *src_frame, double pts,double duration, int64_t pos, int serial){Frame *vp;if (!(vp = frame_queue_peek_writable(&is->pictq))) // 检测队列是否 有可写空间return -1; // Frame队列满了则返回-1// 执?到这步说已经获取到了可写?的Frame//进行填充vp->sar = src_frame->sample_aspect_ratio;vp->uploaded = 0;vp->width = src_frame->width;vp->height = src_frame->height;vp->format = src_frame->format;vp->pts = pts;vp->duration = duration;vp->pos = pos;vp->serial = serial;set_default_window_size(vp->width, vp->height, vp->sar);av_frame_move_ref(vp->frame, src_frame); // 将src中所有数据拷?到dst 中 , 并复位src 。frame_queue_push(&is->pictq); // 更新写索引位置return 0; }queue_picture 的代码很直观 。
?先 frame_queue_peek_writable 取FrameQueue的当前写节点 。
然后把该拷?的拷?给节点(struct Frame)保存 。
再 frame_queue_push, “push”节点到队列中 。 唯?需要关注的是 , AVFrame的拷?是通过av_frame_move_ref 实现的 , 所以拷?后 src_frame 就是?效的了 。
?频解码线程

数据来源:从read_thread()线程?来 。
数据处理:在audio_thread()进?解码 , 具体调?decoder_decode_frame() 。
数据出?:在sdl_audio_callback()->audio_decode_frame()读取frame进?播放 。
先看audio_thraed() , 对于滤镜部分(CONFIG_AVFILTER定义部分) , 这?不做分析, 简化后的代码如下:

// ?频解码线程static int audio_thread(void *arg){VideoState *is = arg;AVFrame *frame = av_frame_alloc(); // 分配解码帧Frame *af;int got_frame = 0; // 是否读取到帧AVRational tb; // timebaseint ret = 0;if (!frame)return AVERROR(ENOMEM);do {// 1. 读取解码帧if ((got_frame = decoder_decode_frame(&is->auddec, frame, NU LL)) < 0)goto the_end;if (got_frame) {tb = (AVRational){1, frame->sample_rate};// 设置为 sample_rate为timebase// 2. 获取可写Frameif (!(af = frame_queue_peek_writable(&is->sampq))) // 获取可写帧goto the_end;// 3. 设置Frame并放?FrameQueue//填充复制帧 , 线程安全af->pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : fra me->pts * av_q2d(tb);af->pos = frame->pkt_pos;af->serial = is->auddec.pkt_serial;af->duration = av_q2d((AVRational){frame->nb_samples , frame->sample_rate});av_frame_move_ref(af->frame, frame); //转移frame_queue_push(&is->sampq); // 更新写索引}} while (ret >= 0 || ret == AVERROR(EAGAIN) || ret == AVERROR_EO F);the_end:av_frame_free(&frame);return ret;}从简化后的代码来看 , 逻辑和video_thread()基本是类似的且更简单 , 这?主要重点讲解 。
【陆小曼|超详细解析FFplay之音视频解码线程】
陆小曼|超详细解析FFplay之音视频解码线程
本文插图

为什么video_thread()是tb是采?了stream->base_base , audio这?却不是?

static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtit le *sub) {...for (;;) {AVPacket pkt;// 1. 流连续情况下获取解码后的帧if (d->queue->serial == d->pkt_serial) {// 1.1 先判断是否是同 ?播放序列的数据do {.........switch (d->avctx->codec_type) {case AVMEDIA_TYPE_VIDEO:....break;case AVMEDIA_TYPE_AUDIO:ret = avcodec_receive_frame(d->avctx, frame);if (ret >= 0) {AVRational tb = (AVRational){1, frame->sampl e_rate}; //if (frame->pts != AV_NOPTS_VALUE) {// 如果frame->pts正常则先将其从pkt_timebase 转成{1, frame->sample_rate}// pkt_timebase实质就是stream->time_base 2frame->pts = av_rescale_q(frame->pts, d- >avctx->pkt_timebase, tb);}else if (d->next_pts != AV_NOPTS_VALUE) {// 如果frame->pts不正常则使?上?帧更新的next_ pts和next_pts_tb// 转成{1, frame->sample_rate}frame->pts = av_rescale_q(d->next_pts, d ->next_pts_tb, tb);}if (frame->pts != AV_NOPTS_VALUE) {// 根据当前帧的pts和nb_samples预估下?帧的ptsd->next_pts = frame->pts + frame->nb_sam ples;d->next_pts_tb = tb; // 设置timebase}}break;}....} while (ret != AVERROR(EAGAIN)); // 1.5 没帧可读时ret返 回EAGIN , 需要继续送packet}....}从上可以看出来 , 将audio frame从decoder_decode_frame取出来后 , 已由stream->time_base转成了{1, frame->sample_rate}作为time_base 。
本篇文章就分享到这里 , 欢迎关注 , 点赞 , 收藏 , 转发 。


    推荐阅读