ffplay.c学习-3-音视频解码线程


目录

  1. 解码线程
  2. 视频解码线程
    1. video_thread()
    2. get_video_frame()
      1. 同⼀播放序列流连续的情况下,不断调⽤avcodec_receive_frame获取解码后的frame。
      2. 获取⼀个packet,如果播放序列不⼀致(数据不连续)则过滤掉“过时”的packet
      3. 将packet送⼊解码器
    3. queue_picture()
  3. 音频解码线程
类型 PacketQueue FrameQueue vidck 解码线程
视频 videoq pictq vidcllk video_thread
⾳频 audioq sampq audclk audio_thread
字幕 subtitleq subpq subtitle_thread
  1. 其中PacketQueue⽤于存放从read_thread取到的各⾃播放时间内的AVPacket。FrameQueue⽤于存放各⾃解码后的AVFrame。Clock⽤于同步⾳视频。解码线程负责将PacketQueue数据解码为AVFrame,并存⼊FrameQueue。
  2. 对于不同流,其解码过程⼤同⼩异。
/*** 解码器封装*/
typedef struct Decoder {AVPacket pkt;PacketQueue *queue;         // 数据包队列AVCodecContext *avctx;     // 解码器上下文int pkt_serial;         // 包序列int finished;           // =0,解码器处于工作状态;=非0,解码器处于空闲状态int packet_pending;     // =0,解码器处于异常状态,需要考虑重置解码器;=1,解码器处于正常状态SDL_cond *empty_queue_cond;  // 检查到packet队列空时发送 signal缓存read_thread读取数据int64_t start_pts;          // 初始化时是stream的start timeAVRational start_pts_tb;       // 初始化时是stream的time_baseint64_t next_pts;           // 记录最近一次解码后的frame的pts,当解出来的部分帧没有有效的pts时则使用next_pts进行推算AVRational next_pts_tb;        // next_pts的单位SDL_Thread *decoder_tid;       // 线程句柄
} Decoder;

1. 解码器相关的函数(decoder是ffplay⾃定义,重新封装的。 avcodec才是ffmpeg的提供的)

  1. 初始化解码器
static void decoder_init(Decoder *d, AVCodecContext *avctx, PacketQueue *queue, SDL_cond *empty_queue_cond)
  1. 启动解码器
/*** 创建解码线程, audio/video有独立的线程*/
static int decoder_start(Decoder *d, int (*fn)(void *), const char *thread_name, void *arg) 
  1. 解帧
static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub)
  1. 终⽌解码器
void decoder_abort(Decoder *d, FrameQueue *fq);
  1. 销毁解码器
void decoder_destroy(Decoder *d);

2. 使⽤⽅法

  1. 启动解码线程

    1. decoder_init()
    2. decoder_start()
  2. 解码线程具体流程
    1. decoder_decode_frame()
  3. 退出解码线程
    1. decoder_abort()
    2. decoder_destroy()

1. 解码线程

  1. ffplay的解码线程独⽴于数据读线程,并且每种类型的流(AVStream)都有其各⾃的解码线程,如:

    1. video_thread⽤于解码video stream;
    2. audio_thread⽤于解码audio stream;
    3. subtitle_thread⽤于解码subtitle stream。
  2. 为⽅便阅读,先列⼀张表格,梳理各个变量、函数名称。

2. 视频解码线程

  1. 数据来源:从read_thread线程⽽来
  2. 数据处理:在video_thread进⾏解码,具体调⽤get_video_frame
  3. 数据出⼝:在video_refresh读取frame进⾏显示

1. video_thread()

  1. 我们先看video_thead,对于滤镜部分(CONFIG_AVFILTER定义部分),这⾥不做分析 ,简化后的代码如下:
// 视频解码线程
static int video_thread(void *arg) {VideoState *is = arg;AVFrame *frame = av_frame_alloc();  // 分配解码帧double pts;                 // ptsdouble duration;            // 帧持续时间int ret;//1 获取stream timebaseAVRational tb = is->video_st->time_base; // 获取stream timebase//2 获取帧率,以便计算每帧picture的durationAVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);#if CONFIG_AVFILTERAVFilterGraph *graph = NULL;AVFilterContext *filt_out = NULL, *filt_in = NULL;int last_w = 0;int last_h = 0;enum AVPixelFormat last_format = -2;int last_serial = -1;int last_vfilter_idx = 0;#endifif (!frame)return AVERROR(ENOMEM);for (;;) {  // 循环取出视频解码的帧数据// 3 获取解码后的视频帧ret = get_video_frame(is, frame);if (ret < 0)goto the_end;   //解码结束, 什么时候会结束if (!ret)           //没有解码得到画面, 什么情况下会得不到解后的帧continue;// 4 计算帧持续时间和换算pts值为秒// 1/帧率 = duration 单位秒, 没有帧率时则设置为0, 有帧率帧计算出帧间隔duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational) {frame_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->viddec.pkt_serial);// 6 释放frame对应的数据av_frame_unref(frame);
#if CONFIG_AVFILTERif (is->videoq.serial != is->viddec.pkt_serial)break;}
#endifif (ret < 0) // 返回值小于0则退出线程goto the_end;}the_end:
#if CONFIG_AVFILTERavfilter_graph_free(&graph);
#endifav_frame_free(&frame);return 0;
}
  1. 在该流程中,当调⽤函数返回值⼩于<0时则退出线程。
  2. 线程的总体流程很清晰:
    1. 获取stream timebase,以便将frame的pts转成秒为单位
    2. 获取帧率,以便计算每帧picture的duration
    3. 获取解码后的视频帧,具体调⽤get_video_frame()实现
    4. 计算帧持续时间和换算pts值为秒
    5. 将解码后的视频帧插⼊队列,具体调⽤queue_picture()实现
    6. 释放frame对应的数据
  3. 重点是get_video_frame()和queue_picture()

2. get_video_frame()

  1. get_video_frame 简化如下:
/*** @brief 获取视频帧* @param is* @param frame 指向获取的视频帧* @return*/
static int get_video_frame(VideoState *is, AVFrame *frame) {int got_picture;// 1. 获取解码后的视频帧if ((got_picture = decoder_decode_frame(&is->viddec, frame, NULL)) < 0) {return -1; // 返回-1意味着要退出解码线程, 所以要分析decoder_decode_frame什么情况下返回-1}if (got_picture) {// 2. 分析获取到的该帧是否要drop掉, 该机制的目的是在放入帧队列前先drop掉过时的视频帧...}return got_picture;
}
  1. 主要流程:

    1. 调⽤ decoder_decode_frame 解码并获取解码后的视频帧;
    2. 分析如果获取到帧是否需要drop掉(逻辑就是如果刚解出来就落后主时钟,那就没有必要放⼊Frame队列,再拿去播放,但是也是有⼀定的条件的,⻅下⾯分析)
  2. 被简化的部分主要是针对丢帧的⼀个处理

if (got_picture) {// 2. 分析获取到的该帧是否要drop掉, 该机制的目的是在放入帧队列前先drop掉过时的视频帧double dpts = NAN;if (frame->pts != AV_NOPTS_VALUE)dpts = av_q2d(is->video_st->time_base) * frame->pts;    //计算出秒为单位的ptsframe->sample_aspect_ratio = av_guess_sample_aspect_ratio(is->ic, is->video_st, frame);if (framedrop > 0 || // 允许drop帧(framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER))//非视频同步模式{if (frame->pts != AV_NOPTS_VALUE) { // pts值有效double diff = dpts - get_master_clock(is);if (!isnan(diff) &&     // 差值有效fabs(diff) < AV_NOSYNC_THRESHOLD && // 差值在可同步范围呢diff - is->frame_last_filter_delay < 0 && // 和过滤器有关系is->viddec.pkt_serial == is->vidclk.serial && // 同一序列的包is->videoq.nb_packets) { // 帧队列至少有1帧数据is->frame_drops_early++;printf("%s(%d) diff:%lfs, drop frame, drops:%d\n",__FUNCTION__, __LINE__, diff, is->frame_drops_early);av_frame_unref(frame);got_picture = 0;}}}}
  1. 先确定进⼊丢帧检测流程,控制是否进⼊丢帧检测有3种情况

    1. 控制是否丢帧的开关变量是 framedrop ,为1,则始终判断是否丢帧;
    2. framedrop 为0,则始终不丢帧;
    3. framedrop 为-1(默认值),则在主时钟不是video的时候,判断是否丢帧。
  2. 如果进⼊丢帧检测流程,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个包。
  3. 接下来看下真正解码的过程—— decoder_decode_frame ,这个函数也包含了对audio和subtitle的解码,其返回值:
    -1:请求退出解码器线程
    0:解码器已经完全冲刷,没有帧可读,这⾥也说明对应码流播放结束
    1:正常解码获取到帧

  4. 先看简化后的主⼲代码(注意for(;;)这个⼤循环):

    static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle * sub) {for (;;) { // ⼤循环//1. 流连续情况下获取解码后的帧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_threadSDL_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);}}
  1. decoder_decode_frame 的主⼲代码是⼀个循环,要拿到⼀帧解码数据,或解码出错、⽂件结束,才会返回。
  2. 循环内可以分解为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送⼊解码器。
1. 同⼀播放序列流连续的情况下,不断调⽤avcodec_receive_frame获取解码后的frame。
  1. 我们先看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, frame->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);if (ret >= 0) {AVRational tb = (AVRational) {1, frame->sample_rate};if (frame->pts != AV_NOPTS_VALUE)frame->pts = av_rescale_q(frame->pts, d->avctx->pkt_timebase, tb);else if (d->next_pts != AV_NOPTS_VALUE)frame->pts = av_rescale_q(d->next_pts, d->next_pts_tb, tb);if (frame->pts != AV_NOPTS_VALUE) {d->next_pts = frame->pts + frame->nb_samples;d->next_pts_tb = tb;}}break;}// 1.3. 检查解码是否已经结束,解码结束返回0if (ret == AVERROR_EOF) {d->finished = d->pkt_serial;printf("avcodec_flush_buffers %s(%d)\n", __FUNCTION__, __LINE__);avcodec_flush_buffers(d->avctx);return 0;}// 1.4. 正常解码返回1if (ret >= 0)return 1;} while (ret != AVERROR(EAGAIN));   // 1.5 没帧可读时ret返回EAGIN,需要继续送packet}
  1. 注意返回值:
    -1:请求退出解码器线程
    0:解码器已经完全冲刷,没有帧可读,这⾥也说明对应码流播放结束
    1:正常解码获取到帧
1. 重点分析decoder_reorder_pts
                    ret = avcodec_receive_frame(d->avctx, frame);//printf("frame pts:%ld, dts:%ld\n", frame->pts, frame->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;}}
  1. decoder_reorder_pts:让ffmpeg排序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
  1. 使⽤“空包”冲刷解码器后,如果要再次解码则需要调⽤avcodec_flush_buffers(),之所以在这个节点调⽤avcodec_flush_buffers(),主要是让我们在循环播放码流的时候可以继续正常解码。
2. 获取⼀个packet,如果播放序列不⼀致(数据不连续)则过滤掉“过时”的packet
        // 2 获取一个packet,如果播放序列不一致(数据不连续)则过滤掉“过时”的packetdo {// 2.1 如果没有数据可读则唤醒read_thread, 实际是continue_read_thread SDL_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 阻塞式读取packetif (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial) < 0)return -1;}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);}} while (d->queue->serial != d->pkt_serial);// 如果不是同一播放序列(流不连续)则继续读取
1. 重点:如果还有pending的packet则使⽤它
             // 2.2 如果还有pending的packet则使用它if (d->packet_pending) {av_packet_move_ref(&pkt, &d->pkt);d->packet_pending = 0;}
  1. 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 both returned EAGAIN, which is an API violation.\n");d->packet_pending = 1;av_packet_move_ref(&d->pkt, &pkt);}
    
  2. 如果 avcodec_send_packet 返回 EAGAIN ,则把当前 pkt 存⼊ d->pkt ,然后置标志位packet_pending 为1
2. 重点:do {} while (d->queue->serial != d->pkt_serial);// 如果不是同⼀播放序列(流不连续)则继续读取
  1. 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
}
3. 将packet送⼊解码器
        // 3 将packet送入解码器if (pkt.data == 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, &pkt);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) : AVERROR_EOF);}} else {if (avcodec_send_packet(d->avctx, &pkt) == AVERROR(EAGAIN)) {av_log(d->avctx, AV_LOG_ERROR,"Receive_frame and send_packet both returned EAGAIN, which is an API violation.\n");d->packet_pending = 1;av_packet_move_ref(&d->pkt, &pkt);}}av_packet_unref(&pkt);    // 一定要自己去释放音视频数据}}
1. 重点:有针对 flush_pkt 的处理
if (pkt.data == 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}
  1. 了解过PacketQueue的代码,我们知道在往PacketQueue送⼊⼀个flush_pkt后,PacketQueue的serial值会加1,⽽送⼊的flush_pkt和PacketQueue的新serial值保持⼀致。所以如果有“过时(旧serial)”Packet,过滤后,取到新的播放序列第⼀个pkt将是flush_pkt。
  2. 根据api要求,此时需要调⽤ avcodec_flush_buffers
  3. 也要注意d->finished = 0; 的重置。
2. 重点:avcodec_send_packet后出现AVERROR(EAGAIN),则说明我们要继续调⽤avcodec_receive_frame()将frame读取,再调⽤avcodec_send_packet发packet。
  1. 由于出现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 both returned EAGAIN, which is an API violation.\n");d->packet_pending = 1;av_packet_move_ref(&d->pkt, &pkt);}

3. queue_picture()

  1. 上⾯,我们就分析完video_thread中关键的 get_video_frame 函数,根据所分析的代码,已经可以取到正确解码后的⼀帧数据。接下来就要把这⼀帧放⼊FrameQueue:
            // 4 计算帧持续时间和换算pts值为秒// 1/帧率 = duration 单位秒, 没有帧率时则设置为0, 有帧率帧计算出帧间隔duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational) {frame_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->viddec.pkt_serial);// 6 释放frame对应的数据av_frame_unref(frame);
#if CONFIG_AVFILTERif (is->videoq.serial != is->viddec.pkt_serial)break;}
  1. 主要调⽤ queue_picture :
static int queue_picture(VideoState *is, AVFrame *src_frame, double pts,double duration, int64_t pos, int serial) {Frame *vp;#if defined(DEBUG_SYNC)printf("frame_type=%c pts=%0.3f\n",av_get_picture_type_char(src_frame->pict_type), pts);
#endifif (!(vp = frame_queue_peek_writable(&is->pictq))) // 检测队列是否有可写空间return -1;      // Frame队列满了则返回-1// 执行到这步说已经获取到了可写入的Framevp->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;
}
  1. queue_picture 的代码很直观:
  2. ⾸先 frame_queue_peek_writable 取FrameQueue的当前写节点;
  3. 然后把该拷⻉的拷⻉给节点(struct Frame)保存再 frame_queue_push ,“push”节点到队列中。唯⼀需要关注的是,AVFrame的拷⻉是通过av_frame_move_ref 实现的,所以拷⻉后 src_frame 就是⽆效的了

3. 音频解码线程

  1. 数据来源:从read_thread()线程⽽来
  2. 数据处理:在audio_thread()进⾏解码,具体调⽤decoder_decode_frame()
  3. 数据出⼝:在sdl_audio_callback()->audio_decode_frame()读取frame进⾏播放

1. audio_thread()

// ⾳频解码线程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, NULL)) < 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并放⼊FrameQueueaf->pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->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_EOF);the_end:av_frame_free(&frame);return ret;}
  1. 从简化后的代码来看,逻辑和video_thread()基本是类似的且更简单,这⾥主要重点讲解为什么video_thread()是tb是采⽤了stream->base_base,这⾥却不是,这个时候就要回到decoder_decode_frame()函数,我们主要是重点看audio部分
static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *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->sample_rate}; //if (frame->pts != AV_NOPTS_VALUE) {// 如果frame->pts正常则先将其从pkt_timebase转成{1, frame->sample_rate}// pkt_timebase实质就是stream->time_baseframe->pts = av_rescale_q(frame->pts, d->avctx->pkt_timebase, tb);}else if (d->next_pts != AV_NOPTS_VALUE) {// 如果frame->pts不正常则使⽤上⼀帧更新的next_s和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) {tb = (AVRational){1, frame->sample_rate}; // 设置为sample_rate为timebase18// 根据当前帧的pts和nb_samples预估下⼀帧的ptsd->next_pts = frame->pts + frame->nb_samples;d->next_pts_tb = tb; // 设置timebase}}break;}....} while (ret != AVERROR(EAGAIN)); // 1.5 没帧可读时ret返回EAGIN,需要继续送packet}....}
  1. 从上可以看出来,将audio frame从decoder_decode_frame取出来后,已由stream->time_base转成了{1, frame->sample_rate}作为time_base。

ffplay.c学习-3-音视频解码线程相关推荐

  1. ffplay.c学习-2-数据读取线程

    ffplay.c学习-2-数据读取线程 目录 准备⼯作 avformat_alloc_context 创建上下⽂ ic->interrupt_callback avformat_open_inp ...

  2. ffplay分析(视频解码线程的操作)

    <ffplay的数据结构分析> <ffplay分析(从启动到读取线程的操作)> <ffplay分析(音频解码线程的操作)> <ffplay 分析(音频从Fra ...

  3. ffplay.c学习-1-框架及数据结构

    ffplay.c学习-1-框架及数据结构 目录 ffplay.c的意义 FFplay框架分析 数据结构分析 struct VideoState 播放器封装 struct Clock 时钟封装 stru ...

  4. ffplay.c学习-5-视频输出和尺⼨变换

    ffplay.c学习-5-视频输出和尺⼨变换 目录 视频输出模块 视频输出初始化 视频输出初始化主要流程 初始化窗⼝显示⼤⼩ 视频输出逻辑 video_refresh 计算上⼀帧应显示的时⻓,判断是否 ...

  5. FFmpeg源码分析:avcodec_send_packet()与avcodec_receive_frame()音视频解码

    FFmpeg在libavcodec模块,旧版本提供avcodec_decode_video2()作为视频解码函数,avcodec_decode_audio4()作为音频解码函数.在FFmpeg 3.1 ...

  6. FFplay源码分析-音视频同步1

    本系列 以 ffmpeg4.2 源码为准,下载地址:链接:百度网盘 提取码:g3k8 FFplay 源码分析系列以一条简单的命令开始,ffplay -i a.mp4.a.mp4下载链接:百度网盘,提取 ...

  7. ffplay.c学习-6-⾳视频同步基础

    ffplay.c学习-6-⾳视频同步基础 目录 ⾳视频同步策略 ⾳视频同步概念 FFmpeg中的时间单位 ⾳视频时间换算的问题 不同结构体的time_base/duration分析 不同结构体的PTS ...

  8. ffplay.c学习-4-⾳频输出和⾳频重采样

    ffplay.c学习-4-⾳频输出和⾳频重采样 目录 ⾳频输出模块 打开SDL⾳频设备 打开⾳频设备audio_open 回调函数逻辑sdl_audio_callback 回调函数读取数据 ⾳频重采样 ...

  9. FFmpeg音视频解码同步播放流程

    FFmpeg音视频解码同步播放流程 数据接收后处理流程及阶段示意图: 1.接收导数据(Trans Stage) 2.新建音.视频解码线程(Parse Stage) 3.将解码好的数据分别放入队列(St ...

最新文章

  1. 小鱼提问1 类中嵌套public修饰的枚举,外部访问的时候却只能Class.Enum这样访问,这是为何?...
  2. python变量需要声明吗_python中可以声明变量类型吗
  3. Ubuntu 16.04 设置MySQL远程访问权限
  4. 【项目管理】合同和采购
  5. SetTimeout(延迟计时器)
  6. 任正非:将来华为岗位分三类 职员类岗位不涉及末位淘汰
  7. linux中下载迅雷链接
  8. params.c:Parameter() - Ignoring badly formed line in configuration file: ignore errors 解决方法
  9. steam自定义信息框_如何设置和自定义Steam控制器
  10. Android事件分发理解
  11. Adjoin the Networks Gym - 100781A
  12. uniapp导航切换(一个页面可以切换3个子页面)
  13. 2020Cfa最新mock下载和使用
  14. 数字经济时代文化消费新特征
  15. 导体接地时的静电平衡问题
  16. 小米官网——简单产品模块布局实现
  17. 无线传感器网络及应用
  18. zebra 的Thread机制
  19. 机器学习笔记之十七——VC dimension
  20. 赖世雄老师的音标课,旋元佑老师的语法书

热门文章

  1. 远程管理,无需在机房来回穿梭
  2. swfheader 0.10 Released(已更正下载地址)
  3. 极客班C++ STL(容器)第二周笔记
  4. Linux驱动中获取系统时间
  5. 免费好用的web应用托管平台
  6. arm-linux-gdb正确无错误安装
  7. CodeForces - 993C Careful Maneuvering(几何+暴力+状态压缩)
  8. HDU - 2825 Wireless Password(AC自动机+状压dp)
  9. HDU - 2819 Swap(二分图完备匹配+路径输出)
  10. 2019ICPC(南昌) - Hello 2019(动态规划+线段树维护矩阵)