ffplay播放器-音视频解码线程

解码线程

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

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

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

类型PacketQueueFrameQueuevidck解码线程
视频videoqpictqvidcllkvideo_thread
⾳频audioqsampqaudclkaudio_thread
字幕subtitleqsubpqsubtitle_thread

其中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()

视频解码线程

数据来源:从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;
    // 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_m ove_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()

主要流程:

  1. 调⽤ decoder_decode_frame 解码并获取解码后的视频帧;
  2. 分析如果获取到帧是否需要drop掉(逻辑就是如果刚解出来就落后主时钟,那就没有必要放⼊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掉
    // 2. 分析获取到的该帧是否要drop掉, 该机制的⽬的是在放⼊帧队列前先drop掉过时 的视频帧
        double dpts = NAN;

        if (frame->pts != AV_NOPTS_VALUE)
        	//计算出秒为单位的pts
            dpts = av_q2d(is->video_st->time_base) * frame->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++;
                    av_frame_unref(frame);
                    got_picture = 0;
                }
            }
        }
    }

    return got_picture;
}

先确定进⼊丢帧检测流程,控制是否进⼊丢帧检测有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:正常解码获取到帧
static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
    int ret = AVERROR(EAGAIN);

    for (;;) { // ⼤循环
        AVPacket pkt;
		//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) {
                    d->finished = d->pkt_serial;
                    avcodec_flush_buffers(d->avctx);
                    return 0;// 解码器已完全冲刷,没有帧可读了
                }
                if (ret >= 0)
                    return 1;// 读取到解码帧
            } while (ret != AVERROR(EAGAIN));
        }
		//2. 获取⼀个packet,如果播放序列不⼀致(数据不连续)则过滤掉“过时”的pac ket
        do {
            if (d->queue->nb_packets == 0)//如果没有数据可读则唤醒read_th read
                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。

d->queue 就是video PacketQueue(videoq)
d->pkt_serial 是最近⼀次取的packet的序列号。在判断完 d->queue->serial == d- >pkt_serial 确保流连续后,循环调⽤ avcodec_receive_frame ,有取到帧就返回。(即使还没送⼊新的Packet,这是为了兼容⼀个Packet可以解出多个Frame的情况)

  1. **获取⼀个packet,如果播放序列不⼀致(数据不连续)则过滤掉“过时”的packet。**主要阻塞调⽤ packet_queue_get()。另外,会在PacketQueue为空时,发送 empty_queue_cond 条件信 号,通知读线程继续读数据(empty_queue_cond 就是 continue_read_thread ,可以参考read线程的分析,查看读线程何时会等待该条件量。
  2. 将packet送⼊解码器。
1.同⼀播放序列流连续的情况下,不断调⽤avcodec_receive_frame获取解码后的frame。

我们先看avcodec_receive_frame的具体流程,这⾥先省略Audio的case:

        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);
                        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;
                    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。
  1. avcodec_flush_buffers
    使⽤“空包”冲刷解码器后,如果要再次解码则需要调avcodec_flush_buffers(),之所以在这个节点调⽤ avcodec_flush_buffers(),主要是让我们在循环播放码流的时候可以继续正常解码
2.获取⼀个packet,如果播放序列不⼀致(数据不连续)则过滤掉“过时”的packet
// 2 获取⼀个packet,如果播放序列不⼀致(数据不连续)则过滤掉“过时”的packet
        do {
        	// 2.1 如果没有数据可读则唤醒read_thread, 实际是continue_read_thread S DL_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;
            }
        } while (d->queue->serial != d->pkt_serial);// 如果不是同⼀播放序列(流不连续)则继续读取

重点:
(1)如果还有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

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, 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);
#endif

    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_thread()

我们先看audio_thraed(),对于滤镜部分(CONFIG_AVFILTER定义部分),这⾥不做分析 ,简化后的代 码如下:

static int audio_thread(void *arg)
{
    VideoState *is = arg;
    AVFrame *frame = av_frame_alloc();// 分配解码帧
    Frame *af;
#if CONFIG_AVFILTER
    int last_serial = -1;
    int64_t dec_channel_layout;
    int reconfigure;
#endif
    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;
                            }
                        }
                        break;
                }
                ...
            } while (ret != AVERROR(EAGAIN));// 1.5 没帧可读时ret返 回EAGIN,需要继续送packet
        }
        ...
    }
}

从上可以看出来,将audio frame从decoder_decode_frame取出来后,已由stream->time_base转成了 {1, frame->sample_rate}作为time_base。

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

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
您可以从FFmpeg项目的官方网站(https://ffmpeg.org/)下载ffplay播放器ffplay是FFmpeg的一部分,因此您可以在FFmpeg的下载页面上找到相关的二进制文件或源代码。请注意,根据您的操作系统和需求,提供了不同版本的FFmpeg和ffplay播放器。下载后,您可以按照安装说明进行安装并开始使用ffplay播放器。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [ffplay 万能播放器](https://download.csdn.net/download/w13511069150/10970923)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [ffmpeg的下载及安装](https://blog.csdn.net/qq_33697094/article/details/112718101)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [ffmpeg下载安装教程及介绍](https://blog.csdn.net/yinshipin007/article/details/130650738)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值