音视频流媒体开发【二十五】ffplay播放器-音视频解码线程

音视频流媒体开发-目录

5 6解码线程

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

  • video_thread⽤于解码video stream;
  • audio_thread⽤于解码audio stream;
  • subtitle_thread⽤于解码subtitle stream。

为⽅便阅读,先列⼀张表格,梳理各个变量、函数名称。


其中PacketQueue⽤于存放从read_thread取到的各⾃播放时间内的AVPacket。FrameQueue⽤于存放各⾃解码后的AVFrame。Clock⽤于同步⾳视频。解码线程负责将PacketQueue数据解码为AVFrame,并存⼊FrameQueue。

对于不同流,其解码过程⼤同⼩异。

/**
* 解码器封装
*/
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 time
    AVRational start_pts_tb; // 初始化时是stream的time_base
    int64_t next_pts; // 记录最近⼀次解码后的frame的pts,当解出来的部分帧没有有效的pts时则使⽤next_pts进⾏推算
    AVRational next_pts_tb; // next_pts的单位
    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);
使⽤⽅法
  • 启动解码线程
    decoder_init()
    decoder_start()
  • 解码线程具体流程
    decoder_decode_frame()
  • 退出解码线程
    decoder_abort()
    decoder_destroy()

5 视频解码线程

数据来源:从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(); // 分配解码帧
    double pts; // pts
    double duration; // 帧持续时间
    int ret;
    // 1 获取stream timebase
    AVRational tb = is->video_st->time_base; // 获取stream timebase
    // 2 获取帧率,以便计算每帧picture的duration
    AVRational 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值为秒
        // 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); // 正常情况下frame对应的buf以被av_frame_move_ref

        if (ret < 0) // 返回值⼩于0则退出线程
            goto the_end;
    }
    the_end:
    av_frame_free(&frame); // 释放frame
    return 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()

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) {
        return -1; // 返回-1意味着要退出解码线程, 所以要分析decoder_decode_frame什么情况下返回-1
    }

    if (got_picture) {
        // 2. 分析获取到的该帧是否要drop掉
        .....
    }
    return got_picture;
}

主要流程:

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

被简化的部分主要是针对丢帧的⼀个处理:

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; //计算出秒为单位的pts

    frame->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) { // packet队列⾄少有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;
            }
        }
    }
}

先确定进⼊丢帧检测流程,控制是否进⼊丢帧检测有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, 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,如果播放序列不⼀致(数据不连续)则过滤掉“过时”的packet
        do {
            if (d->queue->nb_packets == 0)//如果没有数据可读则唤醒read_thread
                SDL_CondSignal(d->empty_queue_cond);
            if (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial) < 0)// 阻塞⽅式读packet
                return -1;
        } while (d->queue->serial != d->pkt_serial); // 播放序列的判断
        //3. 将packet送⼊解码器
        avcodec_send_packet(d->avctx, &pkt);
    }
}

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送⼊解码器。

1 同⼀播放序列流连续的情况下,不断调⽤avcodec_receive_frame获取解码后的frame。

我们先看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);
                ....
                break;
        }

        // 1.3. 检查解码是否已经结束,解码结束返回0
        if (ret == AVERROR_EOF) {
            d->finished = d->pkt_serial;
            printf("avcodec_flush_buffers %s(%d)\n", __FUNCTION__, __LINE__);
            avcodec_flush_buffers(d->avctx); // 调⽤该函数后可以再次解码,只要有数据packet进⼊
            return 0;
        }
        // 1.4. 正常解码返回1
        if (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 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(),主要是让我们在循环播放码流的时候可以继续正常解码。

2 获取⼀个packet,如果播放序列不⼀致(数据不连续)则过滤掉“过时”的packet

// 2 获取⼀个packet,如果播放序列不⼀致(数据不连续)则过滤掉“过时”的packet
do {
    // 2.1 如果没有数据可读则唤醒read_thread, 实际是continue_read_thread SDL_cond
    if (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并获取serial
        if (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_serial);
        av_packet_unref(&pkt); // fixed me? 释放要过滤的packet
    }
} 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;
}

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);
}

如果 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) {
    // darren⾃⼰的代码
    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 stream
    avcodec_flush_buffers(d->avctx); //清空⾥⾯的缓存帧
    d->finished = 0; // 重置为0
    d->next_pts = d->start_pts; // 主要⽤在了audio
    d->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 stream
    avcodec_flush_buffers(d->avctx); //清空⾥⾯的缓存帧
    d->finished = 0; // 重置为0
    d->next_pts = d->start_pts; // 主要⽤在了audio
    d->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 。

也要注意d->finished = 0; 的重置。

(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 both 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){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);

主要调⽤ queue_picture :

static int queue_picture(VideoState *is, AVFrame *src_frame, doublepts,
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 就是⽆效的了。
思考下⾯流程
  • flush_pkt的作⽤
  • Decoder的packet_pending和pkt的作⽤
  • 解码流程:avcodec_receive_frame-> packet_queue_get-> avcodec_send_packet

6 ⾳频解码线程

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

audio_thread()

我们先看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; // timebase
    int 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. 获取可写Frame
            if (!(af = frame_queue_peek_writable(&is->sampq)))// 获取可写帧
                goto the_end;
            // 3. 设置Frame并放⼊FrameQueue
            af->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;
}

从简化后的代码来看,逻辑和video_thread()基本是类似的且更简单,这⾥主要重点讲解

tb = (AVRational){1, frame->sample_rate}; // 设置为sample_rate为timebase

为什么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_base
                                frame->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预估下⼀帧的pts
                                d->next_pts = frame->pts + frame->nb_samples;
                                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。

⾳频解码线程主要是讲解了和视频解码线程差异化部分,其他共同部分参考视频解码线程的讲解。

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

推荐阅读更多精彩内容