video_thread视频解码线程分析

FFmpeg 的社群来了,想加入微信社群的朋友请购买《FFmpeg原理》VIP版 电子书,里有更高级的内容与答疑服务。


之前在 stream_component_open() 里面的 decode_start() 函数开启了 video_thread 线程,如下:

video_thread 线程主要是负责 解码 PacketQueue 队列里面的 AVPacket 的,解码出来 AVFrame,然后丢给入口滤镜,再从出口滤镜把 AVFrame 读出来,再插入 FrameQueue 队列。流程图如下:

video_thread() 函数里面有几个 CONFIG_AVFILTER 的宏判断,这是判断编译的时候是否启用滤镜模块。默认都是启用滤镜模块的。

下面来分析一下 video_thread() 函数的重点逻辑,如下:

video_thread() 函数里面比较重要的局部变量如下:

1,AVFilterGraph *graph,滤镜容器

2,AVFilterContext *filt_in,入口滤镜指针,指向滤镜容器的输入

3,AVFilterContext *filt_out,出口滤镜指针,指向滤镜容器的输出

4,int last_w ,上一次解码出来的 AVFrame 的宽度,初始值为 0

5,int last_h ,上一次解码出来的 AVFrame 的高度,初始值为 0

6,enum AVPixelFormat last_format ,上一次解码出来的 AVFrame 的像素格式,初始值为 -2

7,int last_serial,上一次解码出来的 AVFrame 的序列号,初始值为 -1

8,int last_vfilter_idx,上一次使用的视频滤镜的索引,ffplay 播放器的命令行是可以指定多个视频滤镜,然后按 w 键切换查看效果的

声明初始化完一些局部变量之后,video_thread() 线程就会进入 for 死循环不断处理任务。

get_video_frame() 函数主要是从解码器读取 AVFrame,里面有一个视频同步的逻辑,同步的逻辑稍微复杂,推荐阅读《FFplay视频同步分析


需要注意的是,last_w 一开始是赋值为 0 的,所以必然不等于解码出来的 frame->width,所以一开始肯定是会调进入那个 if 判断,然后调 configure_video_filters() 函数创建滤镜。

总结一下,释放旧滤镜,重新创建新的滤镜有3种情况:

1,后面解码出来的 AVFrame 如果跟上一个 AVFrame 的宽高或者格式不一致。

2,按了 w 键,last_vfilter_idx != is->vfilter_idxffplay 播放器的命令行是可以指定多个视频滤镜,然后按 w 键切换查看效果的,推荐阅读《FFplay视频滤镜分析》。

3,进行了快进快退操作,因为快进快退会导致 is->viddec.pkt_serial 递增。详情请阅读《FFplay序列号分析》。我也不知道为什么序列号变了要重建滤镜。

这3种情况,ffplay 都会处理,只要解码出来的 AVFrame 跟之前的格式不一致,都会重建滤镜,然后更新 last_xxx 变量,这样滤镜处理才不会出错。

由于每次读取出口滤镜的数据,都会用 while 循环把缓存刷完,不会留数据在滤镜容器里面,所以重建滤镜不会导致数据丢失。


video_thread 线程的逻辑比较简单,复杂的地方都封装在它调用的子函数里面,所以本文简单讲解一下,video_thread() 里面调用的各个函数的作用。

1,get_video_frame(),实际上就是对 decoder_decode_frame() 函数进行了封装,加入了视频同步逻辑。返回值如下:

  • 返回 1,获取到 AVFrame 。
  • 返回 0 ,获取不到 AVFrame 。有3种情况会获取不到 AVFrame,一是MP4文件播放完毕,二是解码速度太慢无数据可读,三是视频比音频播放慢了导致丢帧
  • 返回 -1,代表 PacketQueue 队列关闭了(abort_request)。返回 -1 会导致 video_thread() 线程用 goto the_end 跳出 for(;;) 循环,跳出循环之后,video_thread 线程就会自己结束了。返回 -1 通常是因为关闭了 ffplay 播放器。

更详细的分析请阅读《FFplay视频同步分析


2,configure_video_filters(),创建视频滤镜函数,推荐阅读《FFplay视频滤镜分析》。

3,av_buffersrc_add_frame(),往入口滤镜发送 AVFrame

4,av_buffersink_get_frame_flags(),从出口滤镜读取 AVFrame

滤镜相关的函数推荐阅读 FFmpeg实战之路 一章的 《FFmpeg的scale滤镜介绍


5,queue_picture(),此函数可能会阻塞。只是对 frame_queue_peek_writable() 跟 frame_queue_push() 两个函数进行了封装。

在 audio_thread() 音频线程里面是用 frame_queue_peek_writable() 跟 frame_queue_push() 两个函数来插入 FrameQueue 队列的。

在 video_thread() 视频线程里面是用 queue_picture() 函数来插入 FrameQueue 队列的。

音频解码线程 跟 视频解码线程,有很多类似的地方,跟 FrameQueue 队列相关的函数都在 《FrameQueue队列分析》一文中。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
以下是一个使用FFmpeg进行多线程解码的示例代码: ```c #include <stdio.h> #include <string.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> #include <libavcodec/avcodec.h> #include <libavutil/frame.h> #include <libavutil/hwcontext.h> #include <libavutil/pixdesc.h> #define THREAD_COUNT 4 typedef struct DecoderContext { AVCodecContext *codec_ctx; AVBufferRef *hw_device_ctx; AVFrame *decoded_frame; int video_stream_index; } DecoderContext; void *decode_thread(void *arg) { DecoderContext *ctx = (DecoderContext *)arg; AVPacket packet; int ret; int got_frame; AVFrame *frame = av_frame_alloc(); while (1) { ret = av_read_frame(ctx->codec_ctx->pb, &packet); if (ret < 0) { break; } if (packet.stream_index == ctx->video_stream_index) { ret = avcodec_send_packet(ctx->codec_ctx, &packet); if (ret < 0) { av_packet_unref(&packet); break; } while (1) { ret = avcodec_receive_frame(ctx->codec_ctx, frame); if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { break; } else if (ret < 0) { av_packet_unref(&packet); av_frame_unref(frame); goto fail; } if (frame->format == AV_PIX_FMT_CUDA) { printf("Thread %d decoded a CUDA frame\n", (int)pthread_self()); } av_frame_unref(frame); } } av_packet_unref(&packet); } fail: av_frame_free(&frame); return NULL; } int main(int argc, char **argv) { av_register_all(); avcodec_register_all(); AVFormatContext *fmt_ctx = NULL; int ret = avformat_open_input(&fmt_ctx, argv[1], NULL, NULL); if (ret < 0) { printf("Error opening input file: %s\n", av_err2str(ret)); return ret; } ret = avformat_find_stream_info(fmt_ctx, NULL); if (ret < 0) { printf("Error finding stream information: %s\n", av_err2str(ret)); avformat_close_input(&fmt_ctx); return ret; } int video_stream_index = -1; AVCodec *codec = NULL; AVCodecContext *codec_ctx = NULL; AVDictionary *opts = NULL; for (int i = 0; i < fmt_ctx->nb_streams; i++) { if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { video_stream_index = i; codec = avcodec_find_decoder_by_name("h264_cuvid"); if (!codec) { printf("Error creating decoder\n"); avformat_close_input(&fmt_ctx); return -1; } codec_ctx = avcodec_alloc_context3(codec); if (!codec_ctx) { printf("Error allocating codec context\n"); avformat_close_input(&fmt_ctx); return -1; } avcodec_parameters_to_context(codec_ctx, fmt_ctx->streams[i]->codecpar); codec_ctx->thread_count = THREAD_COUNT; codec_ctx->thread_type = FF_THREAD_FRAME | FF_THREAD_SLICE; av_dict_set(&opts, "threads", "auto", 0); av_dict_set(&opts, "refcounted_frames", "1", 0); if (avcodec_open2(codec_ctx, codec, &opts) < 0) { printf("Error opening codec\n"); avcodec_free_context(&codec_ctx); avformat_close_input(&fmt_ctx); return -1; } break; } } if (video_stream_index == -1) { printf("No video stream found\n"); avcodec_free_context(&codec_ctx); avformat_close_input(&fmt_ctx); return -1; } AVBufferRef *hw_device_ctx = NULL; AVHWDeviceType hw_device_type = av_hwdevice_find_type_by_name("cuda"); if (hw_device_type == AV_HWDEVICE_TYPE_NONE) { printf("No CUDA device found\n"); avcodec_free_context(&codec_ctx); avformat_close_input(&fmt_ctx); return -1; } ret = av_hwdevice_ctx_create(&hw_device_ctx, hw_device_type, NULL, NULL, 0); if (ret < 0) { printf("Error creating CUDA device context: %s\n", av_err2str(ret)); avcodec_free_context(&codec_ctx); avformat_close_input(&fmt_ctx); return ret; } DecoderContext *ctx = (DecoderContext *)malloc(sizeof(DecoderContext) * THREAD_COUNT); pthread_t *threads = (pthread_t *)malloc(sizeof(pthread_t) * THREAD_COUNT); for (int i = 0; i < THREAD_COUNT; i++) { ctx[i].codec_ctx = avcodec_alloc_context3(codec); avcodec_parameters_to_context(ctx[i].codec_ctx, fmt_ctx->streams[video_stream_index]->codecpar); ctx[i].hw_device_ctx = av_buffer_ref(hw_device_ctx); ctx[i].codec_ctx->hw_device_ctx = av_buffer_ref(ctx[i].hw_device_ctx); ctx[i].video_stream_index = video_stream_index; ctx[i].decoded_frame = av_frame_alloc(); if (pthread_create(&threads[i], NULL, decode_thread, &ctx[i]) != 0) { printf("Error creating thread\n"); avcodec_free_context(&codec_ctx); avformat_close_input(&fmt_ctx); return -1; } } for (int i = 0; i < THREAD_COUNT; i++) { pthread_join(threads[i], NULL); } for (int i = 0; i < THREAD_COUNT; i++) { avcodec_free_context(&ctx[i].codec_ctx); av_buffer_unref(&ctx[i].hw_device_ctx); av_frame_free(&ctx[i].decoded_frame); } av_buffer_unref(&hw_device_ctx); avcodec_free_context(&codec_ctx); avformat_close_input(&fmt_ctx); return 0; } ``` 这个示例代码使用了4个线程进行解码。每个线程都有一个`DecoderContext`结构体,其中包含了一个`AVCodecContext`结构体,一个硬件设备上下文`AVBufferRef`和一个解码后的帧`AVFrame`。在主线程中,我们为每个线程分配一个`DecoderContext`,然后启动线程并等待线程结束。在线程中,我们使用`avcodec_send_packet`和`avcodec_receive_frame`函数来进行硬解码,并在解码成功后打印一条消息。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Loken2020

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值