ffplay源码分析__视频解码线程video_thread

部署运行你感兴趣的模型镜像

前言

video_thread线程主要负责解码视频数据。本文是把CONFIG_AVFILTER设置为0去掉滤镜功能后分析的。

一、video_thread

video_thread线程简化后代码如下:

static int video_thread(void *arg)
{
    VideoState *is = arg;
    AVFrame *frame = av_frame_alloc();
    double pts;
    double duration;
    int ret;
    AVRational tb = is->video_st->time_base;
    AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);

    if (!frame)
        return AVERROR(ENOMEM);

    for (;;) {
        ret = get_video_frame(is, frame);
        if (ret < 0)
            goto the_end;
        if (!ret)
            continue;

        duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
        pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
        ret = queue_picture(is, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);
        av_frame_unref(frame);

        if (ret < 0)
            goto the_end;
    }
 the_end:
    av_frame_free(&frame);
    return 0;
}

video_thread线程的主要代码是for循环,在for循环中调用get_video_frame()获取解码数据,再调用queue_picture()把解码数据更新到FrameQueue的过程。 

1、首先通过av_frame_alloc()函数创建AVFrame

AVFrame在调用解码函数avcodec_receive_frame()时用到,用于保存解码后的数据

AVFrame *frame = av_frame_alloc();

2、获取视频流的时间基

AVRational tb = is->video_st->time_base;

从视频流AVStream中获取时间基,ffmpeg的时间基是{1,90000},即把1秒分成了90000份,每一份代表1/90000秒。AVRational{num,den}是个结构体,用于表示分数,前面的数字num是分子,后面den是分母。ffmpeg为了保持数据的精度,所以使用AVRational结构体而不是浮点型,因为1/3不能用0.3或0.33表示,这样会损失精度。AVRational结构体如下

/**
 * Rational number (pair of numerator and denominator).
 */
typedef struct AVRational{
    int num; ///< Numerator
    int den; ///< Denominator
} AVRational;

3、获取帧率 

AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);

用av_guess_frame_rate()函数,根据媒体格式上下文和媒体流的信息,来计算帧率。获取的帧率也是AVRational结构体。例如帧率是24,返回的AVRational结构是{24,1},分子分母是反的。

二、get_video_frame函数

static int get_video_frame(VideoState *is, AVFrame *frame)
{
    int got_picture;

    if ((got_picture = decoder_decode_frame(&is->viddec, frame, NULL)) < 0)
        return -1;

    if (got_picture) {
        double dpts = NAN;

        if (frame->pts != AV_NOPTS_VALUE)
            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);
        //framedrop初始值-1
        if (framedrop>0 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) {
            if (frame->pts != AV_NOPTS_VALUE) {
                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) {
                    is->frame_drops_early++;
                    av_frame_unref(frame);
                    got_picture = 0;
                }
            }
        }
    }

    return got_picture;
}

 get_video_frame()有两部分功能,第一部分调用decoder_decode_frame()解码,第二部分判断解码后的AVFrame是否有效。

1、decoder_decode_frame()解码

详见链接:

ffplay源码分析__解码函数decoder_decode_frame-优快云博客

decoder_decode_frame()解码成功后返回1,解码到文件末尾返回0,解码错误返回负数。

2、判断解码后的AVFrame是否有效

if (frame->pts != AV_NOPTS_VALUE)
    dpts = av_q2d(is->video_st->time_base) * frame->pts;

frame->pts是该帧的显示时间戳,单位是AVStream的时间基,需要用av_q2d函数转换成秒,代表该帧的显示时刻。例如frame->pts为9000,那么转成秒后为9000*(1/90000)=0.1秒,即该帧在0.1秒的时刻显示。

接下来判断该帧是否有效。

  //framedrop初始值-1    
  if (framedrop>0 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) {
        if (frame->pts != AV_NOPTS_VALUE) {
            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) {
                is->frame_drops_early++;
                av_frame_unref(frame);
                got_picture = 0;
            }
        }
    }

这串代码是判断解码出的AVFrame,是否已经过了应该显示的时间。

diff,代表该frame的显示时钟和主时钟相差多少秒,在去掉滤镜的情况下,is->frame_last_filter_delay始终为0。所以判断语句简化为:           

    if (!isnan(diff)     // diff有值
        && fabs(diff) < AV_NOSYNC_THRESHOLD &&  // diff的绝对值小于10
        diff  < 0 && // diff<0,代表该帧过时了
        is->viddec.pkt_serial == is->vidclk.serial &&  //序列号一致
        is->videoq.nb_packets)  // 视频PacketQueue还有数据包

最重要的判断语句是diff<0,代表该帧的播放时刻已经晚于当前音频帧的播放时刻,那么就丢弃。

其余的条件还有时钟差绝对值小于10,序列号一致以及视频PacketQueue中还有数据。

如果视频帧过时,那么got_picture返回0。如果视频帧有效,got_picture则返回1。接下来又返回到video_thread线程中。

三、queue_picture函数

1、计算视频帧的持续时长

duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);

av_q2d()的计算方式:num / (double) den,注意此时frame_rate.den对应AVRational的num(1),frame_rate.num对应AVRational的den(24),所以duration=1/24=0.041,即1帧持续0.041秒。

pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);

 同样计算pts,tb是AVStream的时间基

2、解码后的数据更新到FrameQueue

ret = queue_picture(is, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);

调用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;

    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);
    frame_queue_push(&is->pictq);
    return 0;
}

在queue_picture()中,主要是frame_queue_peek_writable()和frame_queue_push()。首先判断队列是否可写,不可写返回-1,可写的情况下对Frame *vp做相应的赋值工作。

3、frame_queue_peek_writable()

static Frame *frame_queue_peek_writable(FrameQueue *f)
{
    /* wait until we have space to put a new frame */
    SDL_LockMutex(f->mutex);
    while (f->size >= f->max_size &&
           !f->pktq->abort_request) {
        SDL_CondWait(f->cond, f->mutex);
    }
    SDL_UnlockMutex(f->mutex);

    if (f->pktq->abort_request)
        return NULL;

    return &f->queue[f->windex];
}

在frame_queue_peek_writable()中,如果 f->size >= f->max_size && !f->pktq->abort_request为true,说明FrameQueue队列满了,程序会一直阻塞在SDL_CondWait(f->cond, f->mutex)处等待,直到消费线程(视频的消费线程是主线程)调用SDL_CondSignal()发送信号为止。调用SDL_CondSignal()的代码在video_refresh()-->frame_queue_next()中,当frame_queue_next()消费掉视频数据时调用SDL_CondSignal,说明队列有可写空间了,通知生产线程(解码线程video_thread)可以继续解码了,下面是SDL_CondSignal()发送信号的代码。

如果可写的情况下,frame_queue_peek_writable()返回&f->queue[f->windex],即FrameQueue中Frame数组的指针。接下来通过对Frame *vp变量赋值

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

Frame数组在初始化时已经分配好了内存,所以对Frame *vp赋值相当于把数据更新到队列中。

其中vp的序列号被赋值为is->viddec.pkt_serial,即视频Decoder的pkt_serial,Decoder的pkt_serial等于PacketQueue的序列号,也等于MyAVPacketList的序列号,所以Frame的序列号 ,Decoder的序列号,PacketQueue的序列号,以及MyAVPacketList的序列号此时都是相等的。

通过av_frame_move_ref(),把AVFrame的解码数据拷贝到vp->frame,这样解码数据也插入到队列中了。

4、frame_queue_push()分析

static void frame_queue_push(FrameQueue *f)
{
    if (++f->windex == f->max_size)
        f->windex = 0;
    SDL_LockMutex(f->mutex);
    f->size++;
    SDL_CondSignal(f->cond);
    SDL_UnlockMutex(f->mutex);
}

在frame_queue_push()中了环形缓冲区的处理方式,对写指针windex累加。如果到了最大值写指针windex重新从0开始,并同时更新队列的大小。 

从以上代码看,frame_queue_peek_writable()返回数组元素的指针

queue_picture()负责更新数据

frame_queue_push()负责更新队列的写指针和队列大小

四、总结

video_thread线程是在read_thread线程中创建的,主要作用是从视频PacketQueue队列中获取AVPacket数据用于解码,并把解码后的数据封装成Frame,保存到视频的FrameQueue中。真正的解码是在decoder_decode_frame()中完成。

把解码数据插入到队列是用三个函数完成,分别是frame_queue_peek_writable(),queue_picture(),frame_queue_push()。frame_queue_peek_writable()返回可写数组,queue_picture()负责插入数据,frame_queue_push()负责更新队列的写索引和大小。

ffmpeg设置视频参数DAR,SAR_ffmpeg sar-优快云博客

您可能感兴趣的与本文相关的镜像

Seed-Coder-8B-Base

Seed-Coder-8B-Base

文本生成
Seed-Coder

Seed-Coder是一个功能强大、透明、参数高效的 8B 级开源代码模型系列,包括基础变体、指导变体和推理变体,由字节团队开源

<think> 嗯,用户这次想要的是FFmpeg从命令行到数据流的源码分析内容。看起来ta是个开发者或者音视频技术研究者,需要深入理解FFmpeg的内部工作机制。从问题表述来看,ta应该已经具备一定FFmpeg使用基础,现在想深入源码层。 我注意到用户引用了四篇FFmpeg相关的技术文档片段:包括结构体分析、ffmpeg.c源码解析、架构设计和ffplay参数解析。这些引用很有价值,特别是引用[2]直接提到了ffmpeg.c的主函数流程,这正是命令行处理的核心入口。 用户上次的问题也是关于FFmpeg源码架构的,这次聚焦在“命令行到数据流”这个具体路径上。需要重点解释从用户输入命令到音视频数据流被处理的完整过程。 考虑从这几个层面展开: 1. 命令解析阶段:ffmpeg_parse_options()如何拆解命令行参数,这是用户最直接的操作入口 2. 初始化阶段:编解码器注册、数据结构准备 3. 核心流程:transcode()函数里的输入输出流处理闭环 4. 数据流转:解复用、解码、滤镜、编码、复用的完整流水线 5. 关键数据结构:AVFormatContext、AVStream这些容器如何承载数据流 特别要注意引用[3]提到的模块化设计思想,在解释流程时要突出各模块的协作关系。比如解复用器(libavformat)和编解码器(libavcodec)如何通过AVPacket和AVFrame交互。 用户可能还想知道数据流经过的每个模块的可扩展性,比如如何通过AVFilter实现自定义处理。这正好对应引用[3]的滤镜系统描述。 在回答中会避免陷入太细节的函数调用,而是把握主干流程。最后考虑附上几个进阶问题,引导用户深入理解性能优化和定制开发方向。 </think> 以下是FFmpeg从命令行输入到数据流处理的源码分析全流程解析,结合核心源码结构(基于FFmpeg 4.2.2版本): --- ### **一、命令行入口与初始化** **源码文件**:`fftools/ffmpeg.c` **主函数调用栈**: ```c main(int argc, char **argv) ├─ avdevice_register_all() // 注册所有输入输出设备 ├─ show_banner() // 打印版本信息 ├─ ffmpeg_parse_options() // 关键:解析命令行参数 └─ transcode() // 核心转码入口 ``` --- ### **二、命令行参数解析流程** **函数**:`ffmpeg_parse_options()` **关键步骤**: 1. **参数分类处理** ```c while ((opt = getopt(argc, argv, "i:y:f:...")) != -1) { switch (opt) { case 'i': input_files = optarg; // 输入文件 case 'y': force_height = atoi(optarg); // 强制设置分辨率(如用户示例) case 'f': format = optarg; // 强制封装格式 ... } } ``` 2. **构建过滤器图**(Filtergraph) 通过`-vf`/`-af`参数解析为`FilterGraph`结构体[^3] --- ### **三、核心转码流程:`transcode()`** **函数调用栈**: ```c transcode() ├─ open_input_file() // 打开输入文件 │ └─ avformat_open_input() // 解复用器初始化 ├─ open_output_file() // 打开输出文件 │ └─ avformat_alloc_output_context2() ├─ init_input_stream() // 初始化输入流 │ ├─ avcodec_find_decoder() // 查找解码器 │ └─ avcodec_open2() // 打开解码器 ├─ transcode_init() // 初始化转码器 ├─ transcode_step() // 执行转码循环 └─ close_files() // 资源释放 ``` --- ### **四、数据流处理关键路径** #### 1. **解复用(Demux)** - **数据结构**:`AVFormatContext` (包含`AVStream`数组) - **源码逻辑**: ```c while (av_read_frame(ifmt_ctx, &pkt) >= 0) { // 读取数据包 AVStream *in_stream = ifmt_ctx->streams[pkt.stream_index]; // 将pkt送入解码队列 } ``` #### 2. **解码(Decode)** - **函数**:`decode_video()`/`decode_audio()` - **关键调用**: ```c ret = avcodec_send_packet(dec_ctx, pkt); // 发送数据包到解码器 while (ret >= 0) { ret = avcodec_receive_frame(dec_ctx, frame); // 获取解码后的帧 if (ret == AVERROR_EOF) break; // 将frame送入滤镜或编码器 } ``` #### 3. **滤镜处理(Filtering)** - **数据结构**:`FilterGraph`、`AVFilterContext` - **流程**: ```c av_buffersrc_add_frame(filter_ctx, frame); // 帧输入滤镜 while (av_buffersink_get_frame_flags(sink_ctx, frame) >= 0) { // 处理过滤后的帧 } ``` #### 4. **编码(Encode)与复用(Mux)** - **编码**: ```c avcodec_send_frame(enc_ctx, frame); // 发送帧到编码器 avcodec_receive_packet(enc_ctx, pkt); // 获取编码后的包 ``` - **复用**: ```c av_interleaved_write_frame(ofmt_ctx, pkt); // 写入输出文件 ``` --- ### **五、关键数据结构关系** ```mermaid graph TD A[命令行参数] --> B[AVFormatContext] B --> C[AVStream] C --> D[AVCodecContext] D --> E[AVPacket/AVFrame] E --> F[AVFilterGraph] F --> G[编码器AVCodecContext] G --> H[输出AVFormatContext] ``` --- ### **六、性能优化关键点** 1. **异步处理**:通过多线程解码/编码(`avcodec_open2`时设置`thread_count`) 2. **零拷贝**:使用`hwaccel`硬件加速(如`-hwaccel cuda`)[^1] 3. **内存优化**:复用`AVFrame`和`AVPacket`对象池
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值