天天看点

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

本系列 以 ffmpeg4.2 源码为准,下载地址:链接:百度网盘 提取码:g3k8

本系列主要分析各种 ffmpeg 命令 在代码里是如何实现的。a.mp4下载链接:百度网盘,提取码:nl0s 。

1、ffmpeg命令分析-vf

命令如下:

./ffmpeg -i a.mp4 -vf "split[main][tmp];[tmp]crop=iw:ih/2:0:0,vflip[flip];[main][flip]overlay=0:H/2" OUTPUT

上这行命令是一个simple filter 处理,在 ffmpeg 工程的代码实现如下。

"split[main][tmp];[tmp]crop=iw:ih/2:0:0,vflip[flip];[main][flip]overlay=0:H/2" 这一大串filter 是作用于输出文件的,所以他被绑在 OutputStream 结构体上。

图一,把 OptionsContext 的 filter 赋值给 ost->filters。

图二跟三,调用 get_ost_filters(),把 ost->filters 赋值给 ost->avfilter,

图四,把那一大串字符串filter 传递给 avfilter_graph_parse2()

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

相关视频推荐

FFmpeg最佳学习方法,只讲一次!/FFmpeg/webRTC/rtmp/hls/rtsp/ffplay/srs

2、ffmpeg命令分析-ac

命令如下:

./ffmpeg -i a.mp4 -ac 1 output.flv,这里的输入文件是双声道,这条命令是把双声道转成单声道。

在 ffmpeg 中,双声道转单声道是用 aformat filter 实现的, 在 ffmpeg 工程的代码实现如下:

解析命令行参数到 编码器context 的channel,也就是 audio_enc->channels

根据编码器的 channels 设置 OutputFilter::channel_layout,也就是下面的第三张图代码。

然后,OutputFilter::channel_layout 会取出来赋值给 aformat filter 的 args 变量。

最后会再设置一下编码器的 channels 跟 channel_layout ,虽然命令行参数已经设置了 channels,不过最后还是以 buffersink 的为准。

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

 分享一个音视频高级开发交流群,需要C/C++ Linux服务器架构师学习资料加企鹅群:788280672获取。资料包括(C/C++,Linux,FFmpeg  webRTC  rtmp  hls rtsp ffplay  srs 等等),免费分享。

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe
ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

3、ffmpeg命令分析-b:v

命令如下:

./ffmpeg -i a.mp4 -b:v 200k output.flv 。

这个命令是改变文件的视频码率, 在 ffmpeg 工程的代码实现如下:

图一,调用 opt_bitrate 设置采样率参数进 o->g->codec_opts

图二,codec_opts 转移给 OutputStream::encoder_opts

OutputStream::encoder_opts 被丢进去 avcodec_open2(),设置编码器的码率。

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

4、ffmpeg命令分析-r

本系列 以 ffmpeg4.2 源码为准,下载地址:链接:百度网盘 提取码:g3k8

之前的文章分析 FFMpeg 工程的 do_video_out() 函数的时候,建议不关注 delta0,delta,nb0_frames,nb_frames 等变量。

因为在之前的命令没有用帧率变换参数,-r 。所以上面这些变量赋值,有跟没有是一样的。

现在来补一下之前缺失的内容。命令行指定 -r 之后,delta0,delta,nb0_frames,nb_frames 的变化。

本文章主要讲解 FFMpeg 里面是如何实现帧率变换的,例如 24fps 是如何转成 8fps的,缩小了3倍的帧率。

./ffmpeg -i a.mp4 -r 8 output.flv 本文以此命令讲解,原始视频是24fps,转成 8fps。

a.mp4 下载链接:百度网盘 ,提取码:nl0s

如果没指定 -r ,就会从 buffersink 获取输出的帧率,帧率跟输入文件帧率一样,代码如下:

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

如果命令行指定了 -r ,就会用命令行参数赋值 给 

ost->frame_rate

。,代码如下:

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

上面是 命令行参数 -r 赋值给 

ost->frame_rate

。然后 

ost->frame_rate

 会作为时间基赋值给 编码器的time_base,见下图代码:

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

所以,综上所述,-r 最后主要影响的是 编码器的time_base,那编码器的time_base是怎么影响帧率变换的呢?请看下面。

在讲之前,需要普及一个知识点,请看下面代码

int a = av_rescale_q(1,  (AVRational){1, 24}, (AVRational){1, 12});
int b = av_rescale_q(2,  (AVRational){1, 24}, (AVRational){1, 12});
           

这里 a 跟 b 都是1,a不会是0.5。av_rescale_q() 返回的整数,所以精度丢失了。这也是为什么 reap_filtes() 内部会搞一个 float_pts 出来,这个float_pts 是有精度的。请看下图,编码器的time_base 的影响就在这里。

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

可以看到,上图代码把原始视频帧 的pts 的时间基转成 编码器的time_base了。这里是一个什么概念呢。

假如原始视频帧率是每秒24帧,他AVFrame的pts的时间基也是 {1,24}。那这时候这个 filtered_frame->pts 肯定是1,2,3,4 这样递增下去的。

把 filtered_frame->pts 转成编码器时间基 {1,8} 会怎样?

播放时间pts不变,1/24 = 0.041s ,第一帧在 0.041s 播放。然后 x/8 = 0.041,这个x等于多少呢?x = 0.333。

也就是说,第二帧的 filtered_frame->pts 原本是 1,时间基从 {1,24} 转成 {1,8} 之后,pts 从1 变成了 0.333。

类推,第三帧pts变成了 0.666,第四种变成0.999。

然后,这些跟帧率变换有什么关系?请继续看下面分析。

因为帧率变换涉及到好几个场景,这里只介绍 format_video_sync = VSYNC_VFR 的场景

接着分析,虽然上面说到,原始pts转换之后变成了 0.33,0.66 之类的小数。但是对于输入文件,对于编码器来说,编码器的帧率是 8fps,{1,8},传递给编码器的frame的pts,肯定也必须是 1,2,3,4 这样的整数递增的。这里实现这个功能的是 ost->sync_opts 变量。

要好好分析一下 ost->sync_opts 这个变量,这个变量的初始值是 0,每输入一个frame给编码器,ost->sync_opts 就会+1。

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

如上图所示,do_video_out() 的 in_picture 帧的pts会被 ost->sync_opts 替换,达到 输入给编码器的frame pts 都是 1,2,3,递增的目的。

接下来继续讲,上面那些0.33,0.66小数是用来干嘛的?其实这些小数就是用来丢帧,实现帧率缩小的功能,或者重复上一帧,实现帧率变大功能。

如下图所示,nb0_frames 这个变量不用管,在format_video_sync = VSYNC_VFR 的场景 下,nb0_frames 总是0。

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

第一帧全是0,看不出上面的逻辑,直接从第二帧开始分析。第二帧 do_video_out() 传递的 sync_ipts 是0.333。但是此时 sync_opts 是 1。

大家可以仔细琢磨一下那句英文注释,老外写注释都比较简洁。

delta0 is the "drift" between the input frame (next_picture) and where it would fall in the output.

delta0 = 0.333 - 1 = -0.666,delta0 代表当前输入帧与输出给编码器的时间差距,在第二帧的时候,时间差是 0.666。解析到这里,应该有点眉目,缩小帧率,肯定要根据时间差距来丢弃某些帧。

说实话,他这个算法有点复杂,我也不太明白他为什么不 sync_ipts < sync_opts 就直接丢弃,这样不是更简单?这个问题先不纠结,接着分析ffmpeg 是如何用delta0 ,delta 实现丢帧的。补充:直接 sync_ipts < sync_opts 不行,会出错。

这个算法涉及到不同刻度表的转换,用两个刻度表来解释这个算法会更明白。

delta0 = sync_ipts - ost->sync_opts; 

delta  = sync_ipts - ost->sync_opts + duration;

可以看到,因为 ost->sync_opts 每次都会 +1 地递增,而 sync_ipts 每次只能 +0.3 递增。所以delta会负得越来越大,duration是固定是 0.33。所以delta也会负得越来越大,然后 delta <= -0.6 就会把 nb_frames 置为 0 ,导致后面的for循环没执行,实现丢帧。至于为什么是0.6我也不知道。实在没想好怎么表达他这个算法逻辑,反正代码逻辑它就是这么跑的,delta越来越大,就丢帧。丢帧后,ost->sync_opts 不会+1,sync_ipts 就会慢慢赶上 sync_opts

case VSYNC_VFR:
    if (delta <= -0.6){
        //丢弃 frame
        nb_frames = 0;
    }
    else if (delta > 0.6)
        ost->sync_opts = lrint(sync_ipts);
    break;
           

实际丢帧情况如下。

0 0.33 0.66(x) 1(x) 1.33 1.66(x) 2(x) 2.33 2.66(x) 3(x)

只有 .33 后缀的帧才会保留,确实是缩小了3倍帧率。

实际上,他这个算法应该是一个数学公式,sync_ipts + duration + 60% 刻度 > sync_opts,如果大于 sync_opts就可以 输出给解码器,小于就丢弃。这里的刻度是1,所以60%刻度是0.6。sync_ipts + duration 是因为只要这个frame的区间跨过 sync_opts 刻度,哪怕跨过一点点,都可以输出。

如果不改变帧率,sync_opts 跟 sync_ipts 是同步+1的,duration也是1,然后 delta0 一直是一个非常小的接近0的数字,delta 一直是接近1的数字。

所以不改变帧率,delta0 跟delta 这些变量是没有作用的。

接下来继续分析 帧率放大算法是如何实现的。

把 帧率 {1,24} 转成 {1,48},实际上就是把 pts 乘以 2 。

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

注意,有些MP4 是 VSYNC_CFR,有些是 VSYNC_VFR。

CFR 的帧率翻倍,会插入新帧,文件大小也会翻倍。

VFR 的帧率翻倍,不会插入新帧,文件大小不变。

case VSYNC_CFR:
            // FIXME set to 0.5 after we fix some dts/pts bugs like in avidec.c
            if (frame_drop_threshold && delta < frame_drop_threshold && ost->frame_number) {
                nb_frames = 0;
            } else if (delta < -1.1)
                nb_frames = 0;
            else if (delta > 1.1) {
                nb_frames = lrintf(delta);
                if (delta0 > 1.1)
                    nb0_frames = lrintf(delta0 - 0.6);
            }
            break;
case VSYNC_VFR:
            if (delta <= -0.6){
                //丢弃 frame
                nb_frames = 0;
            }
            else if (delta > 0.6)
                ost->sync_opts = lrint(sync_ipts);
            break;
           

实际上,我个人认为,命令行 参数 -r 在 ffmpeg.c 里面的实现是一个历史遗留问题,这种实现在 ffmpeg.c 里面暴露了太多的复杂性,实际上新版本的ffmpeg,例如 4.4 版本,已经有 fps,framerate 两个新的滤镜来实现帧率转换。

所以,调 API 函数实现帧率转换,推荐使用 fps,framerate 滤镜,就没有这么多 delta 变量之类的。

5、ffmpeg命令分析-re

命令如下:

ffmpeg -re -i a.mp4 a.flv

-re 参数控制读取 AVpacket 的速度,按照帧率速度读取文件 AVpacket。如果有多个流,以最慢的帧率为准。

命令行参数 -re 定义如下:

ffmpeg_opt.c 3453行
{ "re",OPT_BOOL | OPT_EXPERT | OPT_OFFSET |  OPT_INPUT,  { .off =  OFFSET(rate_emu) },"read input at native frame rate", "" },
           

命令行参数 -re 赋值给 InputFile 结构, 如下:

ffmpeg_opt.c 1209行
f->rate_emu   = o->rate_emu;
           

可以看到,在 ffmpeg_opt.c 1209行 把 OptionsContext 里面的 rate_emu 赋值给了 InputFile 的 rate_emu。

InputFile 的 rate_emu 赋值给 InputStream 的start, 如下:

ffmpeg.c 4134行
 /* init framerate emulation */
 for (i = 0; i < nb_input_files; i++) {
    InputFile *ifile = input_files[i];
    if (ifile->rate_emu) //注意这里
        for (j = 0; j < ifile->nb_streams; j++)
            input_streams[j + ifile->ist_index]->start = av_gettime_relative(); //注意这里
 }
           

InputStream 的start 的用途,如下:

fmpeg.c 4134行
static int get_input_packet(InputFile *f, AVPacket *pkt)
{
    if (f->rate_emu) { //注意这里
        int i;
        for (i = 0; i < f->nb_streams; i++) {
            InputStream *ist = input_streams[f->ist_index + i];
            int64_t pts = av_rescale(ist->dts, 1000000, AV_TIME_BASE);
            int64_t now = av_gettime_relative() - ist->start;
            if (pts > now)
                return AVERROR(EAGAIN);
        }
    }
​
    return av_read_frame(f->ctx, pkt);
}
           

从上面的逻辑可以看到,InputStream 的start 实际上就是存一个启动时间,然后在 get_input_packet() 里面根据 now 跟 ist->dts 控制频率 读取 AVpacket 的数据。

如果有多个流,以最慢的帧率为准。

分析到这里初学者学ffmpeg的源码,不需要一开始就去看多余的分支逻辑,因为 ffmpeg.c 里面实现了非常多的命令参数功能。

如果一开始就把 ffmpeg 的所有参数的结构讲完,没有意义。就好像初学者一开始看 这个 InputStream 的start字段,根本不可能猜出来 start 这个字段的真正用途。我一开始还以为start 是流的第一个帧的pts。

所以,学习ffmpeg源码,只需要学会用 Qt 来断点调试,了解了主干逻辑就行。其他细枝末节的逻辑,你后面用到了,断点调试即可了解ffmpeg是如何实现这些功能的,这样会影响更加深刻。

6、ffmpeg命令分析-segment

命令如下:

ffmpeg -i a.mp4 -c copy -f segment test_output-%d.mp4

-f segment ,ffmpeg 切分视频文件,前面加 -c copy 是为了避免重新编解码,加快切分速度。

命令行参数 -f 定义如下:

ffmpeg_opt.c 3371行
{"f", HAS_ARG | OPT_STRING | OPT_OFFSET | OPT_INPUT | OPT_OUTPUT, { .off = OFFSET(format) }, "force format", "fmt" },
           

命令行参数 -f segment 会赋值给 OptionsContext 的 format,想知道怎么解析然后赋值的,请看《FFmpeg 源码解析-命令行》的文章。

OptionsContext 的 format 最后用在哪里,请看下面代码:

ffmpeg_opt.c 2151行
err = avformat_alloc_output_context2(&oc, NULL, o->format, filename); // 注意 o->foramt
           

如上图,在申请 ouput_context (输出上下文)的时候,传递了 o->format 进去。

不得不说,ffmpeg 封装得真好,真智能,直接定义一个 segment 的 AVFormatContext,把分片逻辑都封装在 format 里面。API 调用层完全不用管,调 api函数 av_write_frame() 往 segment 的 AVFormatContext 写数据就能自动分片好了。

ffmpeg -i a.mp4 a.m3u8 这条命令 hls m3u8 的分片也是用 AVFormatContext 的方式来实现的,是 HLSContext。

7、ffmpeg命令分析-t

命令如下:

ffmpeg -i a.mp4 -t 10 a2.mp4

-t 10 ,从开头截取10秒的视频数据。

命令行参数 -t 定义如下:

ffmpeg_opt.c 3404行
{ "t", HAS_ARG | OPT_TIME | OPT_OFFSET | OPT_INPUT | OPT_OUTPUT,{.off = OFFSET(recording_time) },
        "record or transcode \"duration\" seconds of audio/video",
        "duration" },
           

从上面定义可以看出,-t 10 会解析赋值 给 OptionsContext 的 recording_time 字段。

我刚开始搜索的时候,以为是 check_recording_time() 实现了上面那条命令的时间限制,断点调试下来发现不是,根本没跑进去 check_recording_time()。

一步一步加断点,发现到最后一帧的时候 av_buffersrc_add_frame_flags() 函数执行之后,avfilter_graph_request_oldest() 立即返回了 AVERROR_EOF,这个很奇怪。如图:

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

看ffmpeg的源码,函数说明,只有传递进 av_buffersrc_add_frame_flags() 函数 的AVFrame 是NULL,下次调 avfilter_graph_request_oldest() 才会返回 AVERROR_EOF。

最后一帧AVFrame 明明不是NULL,却也导致 avfilter_graph_request_oldest() 返回了 AVERROR_EOF。非常奇怪。

能让 filter 链返回结束,输入的AVFrame 又是正常的,我思索了30分钟,那只能在 初始化 filter那里搞了点事情。

重新看回 configure_input_video_filter() 函数,发现里面果然用了 recording_time 这个变量,请看下图。

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

总结:

能让 avfilter_graph_request_oldest() 返回了 AVERROR_EOF,有两种情况。

1,传递进 av_buffersrc_add_frame_flags() 函数 的AVFrame 是NULL。

2,初始化filter 的时候用了 trim filter,时间裁剪滤镜,到时间了,avfilter_graph_request_oldest() 也会返回了 AVERROR_EOF。

8、ffmpeg命令分析-ss

命令如下:

ffmpeg -ss 3 -i a.mp4 a2.mp4

-ss 3 ,seek 到输入文件的第三秒开始提取视频

命令行参数 -ss 定义如下:

ffmpeg_opt.c 3412行
{ "ss",HAS_ARG | OPT_TIME | OPT_OFFSET | OPT_INPUT | OPT_OUTPUT,{ .off = OFFSET(start_time) }, "set the start time offset", "time_off" },
           

从上面定义可以看出,-ss 3 会解析赋值 给 OptionsContext 的 start_time 字段。

在前面文章《FFmpeg源码分析-命令行》,分析过命令行参数的类别,主要有3类。

1,作用于 输入文件 的参数。

2,作用于 输出文件 的参数。

3,全局参数。

因为 -ss 是在 -i 前面,所以 现在的 -ss 是作用于输入文件的参数。

下面就来看看 OptionsContext 的 start_time 最后用在哪里,如下截图

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

在 ffmpeg_opt.c 1161行使用了 o->start_time。然后判断如果文件封装格式 AVFMT_SEEK_TO_PTS 是否置位,如果没置位 seek_timestamp 会减少 3*AV_TIME_BASE / 23 的值。

AVFMT_SEEK_TO_PTS 在源码的注释是 Seeking is based on PTS,是否基于 PTS 进行seek。

可以看到 FFmpeg 里面其实处理了非常多的兼容性问题。对不同的格式都进行了处理,自身项目如果要调 avformat_seek_file 这个函数,最好把上面的AVFMT_SEEK_TO_PTS 判断 也照抄进去项目。

9、ffmpeg命令分析-output_ts_offset

命令如下:ffmpeg -i a.mp4 -t 5 -output_ts_offset 5 a2.flv

-output_ts_offset 5 ,设置输出文件的所有流的第一帧位5秒,后续帧从5秒开始递增。

命令行参数 -output_ts_offset 定义如下:

options_table.h 92行
{"output_ts_offset", "set output timestamp offset", OFFSET(output_ts_offset), AV_OPT_TYPE_DURATION, {.i64 = 0}, -INT64_MAX, INT64_MAX, E}
           

注意,本次的命令行参数不在 ffmpeg_opt.c 里面定义了,而是在 libavformat 目录下的 options_table.h 文件里面。

在前面文章《FFmpeg源码分析-命令行》,分析过 命令行参数查找 option 的逻辑,如下:

1,在 cmdutil.c 第 803 行,调用 find_option(options, opt),优选查找 ffmepg_opt.c 里面定义的 options 选项。

2,在 ffmepg_opt.c 定义的 options 里找不到 output_ts_offset ,就会调 opt_default(NULL, opt, argv[optindex]),从AVClass 里面查找,也就是 从 options_table.h 的选项里查找。

查找过程源码如下图所示:

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

从 opt_default() 函数可以看出,*fc = avformat_get_class() ,AVFormat 对应的 AVClass 能找到 output_ts_offset 这个参数的定义。代码如下:

cmdutil.c 578行
if ((o = opt_find(&fc, opt, NULL, 0,
                     AV_OPT_SEARCH_CHILDREN | AV_OPT_SEARCH_FAKE_OBJ))) {
    av_dict_set(&format_opts, opt, arg, FLAGS); //注意这里
    if (consumed)
        av_log(NULL, AV_LOG_VERBOSE, "Routing option %s to both codec and muxer layer\n", opt);
    consumed = 1;
}
           

上面的重点代码是 av_dict_set(&format_opts, opt, arg, FLAGS),把 -output_ts_offset 60 的值设置进去 format_opts 这个全局变量里面了。

接下来再看看 format_opts 这个全局变量在哪里使用了,

因为 -output_ts_offset 60 是作用于输出文件的参数,所以基本可以猜到,就在 open_output_file() 函数里面用了 format_opts ,打开输出文件的时候,把format_opts 传递进去。

果然,在 ffmpeg_opt.c 2146行,open_output_file() 函数里面有这样一行代码。

ffmpeg_opt.c 2146行
av_dict_copy(&of->opts, o->g->format_opts, 0);
           

把 o->g->format_opts 赋值 给 of->opts。

读者可能会疑惑, 全局变量 format_opts 是怎么转移到 o->g->format_opts 的,请看之前的文章 《FFmpeg源码分析-命令行》,这里不再讲述。

再接着找 of->opts 用在哪里。

ffmpeg_opt.c 2559行
/* open the file */
if ((err = avio_open2(&oc->pb, filename, AVIO_FLAG_WRITE, &oc->interrupt_callback, &of->opts)) < 0) { //注意这里
        print_error(filename, err);
        exit_program(1);
}
           

可以看到 of->opts 作为一个参数,丢进去 avio_open2() api函数里面了。

总结:

-output_ts_offset 是AVFormat 库里面的一个公共参数,所以在 调用 avio_open2() 的时候,把 output_ts_offset 作为一个 AVDictionary 传递给 avio_open2() 即可。

本文命令生成的 a2.flv ,如果用迅雷播放器播放,可以播10s,当时用ffpaly播放只能播5s,视频本身是5s的,只是第一帧pts 设置成了 5s。

个人思考:

记得我刚开始学 FFmpeg 的时候,看到AVFrame有个 pts,我就在想第一帧的pts 是不是都是0呢?因为你打开视频肯定要立即播放,但是这个第一帧的pts 又是可以手动改的,因为视频文件在你的手里,改数据很容易。

那时候我就在想,如果一个视频文件,他的第一帧的pts是 60 s,是不是说明这个视频打开之后,要过 60s 才能播放第一帧,60s 之前播放器要一直黑屏。标准实现是不是应该这样子。

今天看到 ffmpeg 的命令参数 output_ ts_ offset 可以指定 第一帧pts,实际测试了下。其实原来根本就没有什么标准的播放器实现,一定要如何如何实现。

在迅雷播放器下,我那个第一帧是60s的视频,前面60s确实是黑屏的。我的视频只有10s,所以迅雷播放器进度条是 60+ 10s,一共70s。

但是我用 ffplay 播放,直接开始播放了,根本没有黑屏。

所以虽然有 pts 这个一个东西,但他的值是可以随便改,而且对于播放器而言,播放器想怎么解释就怎么解释。没什么标准不标准。

10、ffmpeg命令分析-vn

命令如下:

ffmpeg -i a.mp4 -vn -acodec copy output.mp4

上面的命令是单独抽取文件中的音频流到 输出文件

命令行参数 -vn 定义如下:

ffmpeg_opt.h 3569行
{ "vn",OPT_VIDEO | OPT_BOOL  | OPT_OFFSET | OPT_INPUT | OPT_OUTPUT,{ .off = OFFSET(video_disable) },"disable video" },
           

从上面定义可以看出,-vn 会解析赋值 给 OptionsContext 的 video_disable字段。

OptionsContext 的 video_disable字段 会在哪里使用呢?请看下图:

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

可以看到,o->video_disable 控制了 new_video_stream() 没有执行,导致输出 AVFormatContext没有 video 流。

虽然AVFormatContext没有 video 流,但是视频流的AVPacket 还是会从输入文件读取出来,那读取出来的 AVPacket 会如何处理呢?

在《FFmpeg 源码分析》中讲解过,有一个全局变量数组 input_streams[] ,数组里存储所有的输入流。

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

在 add_input_stream() 里面 创建输入流的时候, ist->discard 默认是 1 (默认读出来的AVPacket全部丢弃),只有执行了 new_video_stream() -> new_output_stream() 的时候才会把 ist->discard 设置成 0。

new_output_stream() 函数的代码如下:

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

可以看到 在 ffmpeg_opt.c 1559行,根据输出流来决定输入流的 discard 的值要不要开启。

输入流的 discard 如果是 1,会导致什么结果呢,再请看下图。

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

-vn 命令分析完毕。

总结:

vn 导致没有执行 new_video_stream(),导致 视频输入流的 ist->discard 一直是1 ,ist->discard等于1 再导致 process_input() 的时候丢弃读取出来的AVPacket。

扩展知识:

Qt creator 里面的 data breakpoit 特别好用,想知道某个变量在哪里被修改了,用 data breakpoit 断点变量的地址,下次变量被修改就可以自动停在相应的代码位置。

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

11、ffmpeg命令分析-acc

命令如下:ffmpeg -i a.mp4 -vn -acodec copy output.aac

FFmpeg 抽取音视频文件 中的 AAC 音频 流,与上一条命令 ffmpeg -i a.mp4 -vn -acodec copy output.mp4 相比。

只是封装格式不同,这篇文章就来讲解,如果输出文件不是 mp4,而是aac的封装格式,ffmpeg是如何处理。

命令行参数 -vn 上篇文章已经讲解过它的实现原理,就不再重复讲解了。

输出文件 output.aac 跟 output.mp4 ,格式虽然不同,但在 ffmpeg.c 这个工程里面其实并没有做任何特殊的处理,你调 ffmpeg 的api函数 avio_open2(),只要传递不同后缀的文件名,avio_open2 内部就会根据不同的文件名后缀,生成不同的 AVOutputFormat 放进去 AVFormatContext 的 oformat 字段里。

请看下图,在 avio_open2() 后面打了断点。

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

然后,在ffmpeg 4.2 的源码里搜索 "ADTS AAC (Advanced Audio Coding)" 字符串,可以发现 aac 的封装格式是在 libavformat 目录的 adtsenc.c 里面,如下:

libavformat/adtsenc.c 229行
AVOutputFormat ff_adts_muxer = {
    .name              = "adts",
    .long_name         = NULL_IF_CONFIG_SMALL("ADTS AAC (Advanced Audio Coding)"),
    .mime_type         = "audio/aac",
    .extensions        = "aac,adts",
    .priv_data_size    = sizeof(ADTSContext),
    .audio_codec       = AV_CODEC_ID_AAC,
    .video_codec       = AV_CODEC_ID_NONE,
    .init              = adts_init,
    .write_header      = adts_write_header,
    .write_packet      = adts_write_packet,
    .write_trailer     = adts_write_trailer,
    .priv_class        = &adts_muxer_class,
    .flags             = AVFMT_NOTIMESTAMPS,
};
           

MP4 后缀的文件名,原理类似,mp4 的封装格式文件是 libavformat/movenc.c,具体自行查看。

虽然 aac 是编码层的封装格式,mp4 是容器层的格式,但是ffmpeg把他们都放到了 libavformat 下,个人觉得比较新奇。

总结:

ffmpeg 实现不同封装格式,是通过一种多态的方式实现的。不同后缀,就用不同的AVOutputFormat来实现。

在使用 avio_open2() API函数的时候,可以不管底层实现,直接传递不同后缀的文件名 即可完成不同格式的封装。

12、ffmpeg命令分析-print

命令如下:ffmpeg -i a.mp4 -vcodec libx264 -preset placebo output.mp4

本文不讲上面的命令行参数实现,而是打算讲解,ffmpeg 转码过程中输出的 frame ,fps,q,size, bitrate ,speed 的意义,如下图。

最重要的是speed,转码速度

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

 从ffmpeg.c 里可以看出,上面的红色圈出来的是 print_report() 函数打印的,下面就来分析 print_report() 的实现,流程图如下:

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

 print_report() 入口会做一个时间判断,每 隔0.5 秒才打印一次日志,代码如下:

if (!is_last_report) {
    if (last_time == -1) {
        last_time = cur_time;
        return;
    }
    //50万微妙,0.5秒统计一次。
    if ((cur_time - last_time) < 500000) //注意这里
        return;
    last_time = cur_time;
}
           

后续的代码没有什么好分析,逻辑比较简单,自行查看即可。

frame= 160 fps= 37 q=28.0 size= 0kB time=00:00:07.23 bitrate= 0.1kbits/s speed=1.68x

打印日志的参数解析如下:

1,frame ,用的是 ost->frame_number 赋值,代表当前编码到视频流的第几帧。

2,fps,用上面的 frame 除以时间得到,代表一秒编码了多少个视频帧。

3,q,编码质量,也是针对视频的。q 的计算方式如下:

q = ost->quality / (float) FF_QP2LAMBDA;

可以看到,q 是 从ost->quality 来的,那 ost->quality 的值是在哪里赋值的呢?代码如下:

ffmpeg.c 742行
uint8_t *sd = av_packet_get_side_data(pkt, AV_PKT_DATA_QUALITY_STATS,NULL);
ost->quality = sd ? AV_RL32(sd) : -1;
           

可以看到,流的质量quality,应该是一个累计的值。我个人猜测编码器会根据以往编码的Frame 不断计算这个值,然后放进去 AVPacket 的side_data 里面。

可以 av_packet_get_side_data() 获取到 side_data,side_data 的前32位就是 quality,AV_PKT_DATA_QUALITY_STATS 这个side_data的定义如下。

libavcodec\avcodec.h 1266行
 /**
     * This side data contains quality related information from the encoder.
     * @code
     * u32le quality factor of the compressed frame. Allowed range is between 1 (good) and FF_LAMBDA_MAX (bad).
     * u8    picture type
     * u8    error count
     * u16   reserved
     * u64le[error count] sum of squared differences between encoder in and output
     * @endcode
     */
 AV_PKT_DATA_QUALITY_STATS,
           

前面 3个参数都是针对 视频流 的分析

4,size,写入文件的数据大小,通过 avio_size(oc->pb) 获得。

5,bitrate,比特率,这个bitrate 的单位是 kbits/s ,不是 kb/s ,要注意。 kbits/s 是指每秒传输多少千比特,一个字节有8个比特。

所以源码里 bitrate 的计算方式如下:

ffmpeg.c 1769行
bitrate = pts && total_size >= 0 ? total_size * 8 / (pts / 1000.0) : -1;
           

total_size 是avio_size() 返回的,单位是字节,所以乘以 8,pts 的单位是微妙,除以1000后等于 千分之一秒。千分之一秒能传输 ( total_size * 8) 大小的比特。那1秒就能够传输 ( total_size * 8) * 1000 大小的比特。但是输出的单位不是比特,而是千比特,kbits,前面有个k,所以这个乘以1000就可以抵消掉。就形成源码的计算方式。

6,speed,编码速度。用输出流的最后一个AVPacket的pts 除以程序运行时间。哪个流的最后AVPacket的pts最大就以哪个流的pts为准,输出流最后一个AVPacket的pts通过函数 av_stream_get_end_pts() 获得。

这样就比较好理解了,如果现在已经编码到视频流的第10秒的数据帧,但是转码程序只运行了2秒,那speed转码速度就是 10 /2 ,speed等于5.

ffmpeg.c 1770行
speed = t != 0.0 ? (double)pts / AV_TIME_BASE / t : -1;
           

print_report 里面 的qp_hist 跟 AV_CODEC_FLAG_PSNR 本次不讲解,因为我也不知道是做什么的

扩展知识:

print_report 里面有个非常有用的函数,avio_size(oc->pb), 可以很方便得获取 ffmpeg 已经往文件写了多少数据进去。

13、ffmpeg命令分析-yuv420p

命令如下:ffmpeg -i a.mp4 -pix_fmt yuv420p a.yuv

上面的命令是转成 yuv 的封装格式,封装格式前面《ffmpeg命令分析-acc》已经讲过了,本文主要讲解,a.mp4 原本是有音频的,转成yuv之后音频没有,所以本文主要讲解音频是如何丢包的。如下图

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

在 open_output_file() 函数里面,用 av_guess_codec() 函数知道,yuv的封装格式不支持音频 codec , av_guess_codec() 返回了 AV_CODEC_ID_NONE,所以不会执行 new_audio_stream()。所以音频流会丢弃包。

为什么不执行 new_audio_stream() 就会丢弃读取出来的AVPakcet?请阅读 《ffmpeg命令分析-vn》

14、ffmpeg命令分析-yuv封装mp4

命令如下:ffmpeg -s 720*404 -pix_fmt yuv420p -i a.yuv -vcodec libx264 a-666.mp4

上面的命令是 把 yuv 数据 编码成 H264,然后封装进 MP4 格式里面。

yuv文件本身没有 宽高信息,像素格式信息,所以需要命令行指定 -s 720*404 ,否则会报错。

下面就来讲解 ffmpeg 工程 是如何读取 yuv 数据的。

先看 open_input_file() 函数,打开输入文件的逻辑都在这个函数里面。

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

如上图所示,在 avformat_open_input() 函数前面打了一个 断点,运行后可以发现 ic->iformat->long_name 等于 "raw video",直接在 FFmpeg 4.2 的源码中搜索 "raw video" 这个关键词,可以发现,yuv 的解析是在 libavformat\rawvideodec.c 里面实现的,请看下面代码

libavformat\rawvideodec.c
AVInputFormat ff_rawvideo_demuxer = {
    .name           = "rawvideo",
    .long_name      = NULL_IF_CONFIG_SMALL("raw video"),
    .priv_data_size = sizeof(RawVideoDemuxerContext),
    .read_header    = rawvideo_read_header,
    .read_packet    = rawvideo_read_packet,
    .flags          = AVFMT_GENERIC_INDEX,
    .extensions     = "yuv,cif,qcif,rgb",
    .raw_codec_id   = AV_CODEC_ID_RAWVIDEO,
    .priv_class     = &rawvideo_demuxer_class,
};
           

我们知道, yuv 是原始数据,不需要解码的,在这种情况,ffmpeg.c 工程中的 struct InputStream 的 dec_ctx 解码器上下文会初始化成什么样呢?继续分析ffmpeg.c 的实现。

在《FFMpeg 源码分析-命令行》的时候讲解过, 初始化解码器是在 add_input_streams() 函数里面完成的,如图:

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

在 add_input_streams() 函数里面的 avcodec_alloc_context3 附近打一个断点,看看数据。

可以看到 即使是 yuv 这种原始数据格式,不需要进行解码操作,ffmpeg也声明了一个解码类。也就是 libavcodec\rawdec.c 这个文件。

应该是为了api 函数的通用性,即使是原始数据yuv,也可以 调用 av_read_frame() 把数据读取进 AVPacket,然后丢给 解码器,再从 AVPacket 转成 AVFrame。

总结:

虽然 yuv可以直接读取数据,然后自己拼接YUV数据到 AVFrame 的data 等等字段,但是最好还是调 ffmpeg 的api 函数 av_read_frame() 读出 AVPacket,再转成 AVFrame,自己写一套 yuv 的解释器会比较容易出错,调 ffmpeg 写好的东西比较稳定。

15、FFmpeg硬件加速

ffmpeg4.4.1 源码为准,用以下命令分析 ffmpeg.c 里面的硬件加速逻辑实现。

命令如下:

ffmpeg.exe -hwaccel cuvid -vcodec h264_cuvid -i juren_10s.mp4 -vcodec h264_nvenc -acodec copy juren_h264_nvenc_10s.mp4 -y
           

以上命令使用 h264_cuvid 硬件解码 MP4,然后再使用 h264_nvenc 硬件编码成 MP4。juren_10s.mp4 下载地址,百度网盘,提取码:3khn

CUDA 硬件加速的代码,貌似不是ABI 兼容的,所以只能用 MSVC 编译出 DLL。然后 qt creator 里面也必须使用 msvc 编译调试,不能用 MinGW ,会报错。

完整项目下载:百度网盘,提取码:9yeu,qt creator 编译 Kits 请选择 MSVC 2019 64 bits ,调试环境如图:

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

其实ffmpeg.c 工程的硬件加速代码在3地方都有分布,解码,filter,编码。本文分开讲述。

硬件加速,解码的流程图如下:

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

首先,在 ffmpeg_opt.c 的 add_input_streams() 添加输入流的时候,初始化硬件解码相关变量参数,如下:

ffmpeg_opt.c 
            if (hwaccel) {
                // The NVDEC hwaccels use a CUDA device, so remap the name here.
                if (!strcmp(hwaccel, "nvdec") || !strcmp(hwaccel, "cuvid"))
                    hwaccel = "cuda";
​
                if (!strcmp(hwaccel, "none"))
                    ist->hwaccel_id = HWACCEL_NONE;
                else if (!strcmp(hwaccel, "auto"))
                    ist->hwaccel_id = HWACCEL_AUTO;
                else {
                    enum AVHWDeviceType type;
                    int i;
                    for (i = 0; hwaccels[i].name; i++) {
                        if (!strcmp(hwaccels[i].name, hwaccel)) {
                            ist->hwaccel_id = hwaccels[i].id;
                            break;
                        }
                    }
​
                    if (!ist->hwaccel_id) {
                        type = av_hwdevice_find_type_by_name(hwaccel);
                        if (type != AV_HWDEVICE_TYPE_NONE) {
                            ist->hwaccel_id = HWACCEL_GENERIC;
                            ist->hwaccel_device_type = type;
                        }
                    }
​
                    if (!ist->hwaccel_id) {
                        av_log(NULL, AV_LOG_FATAL, "Unrecognized hwaccel: %s.\n",
                               hwaccel);
                        av_log(NULL, AV_LOG_FATAL, "Supported hwaccels: ");
                        type = AV_HWDEVICE_TYPE_NONE;
                        while ((type = av_hwdevice_iterate_types(type)) !=
                               AV_HWDEVICE_TYPE_NONE)
                            av_log(NULL, AV_LOG_FATAL, "%s ",
                                   av_hwdevice_get_type_name(type));
                        av_log(NULL, AV_LOG_FATAL, "\n");
                        exit_program(1);
                    }
                }
            }
           

上面这段代码主要有以下重点:

解析命令行参数 -hwaccel cuvid 到 hwaccel 变量,所以上图中的 hwaccel 等于 cuvid,后续被合并修改为 cuda。

设置 ist->hwaccel_id ,在本环境中,被设置为 HWACCEL_GENERIC。

设置 ist->hwaccel_device_type,在本环境中,被设置为 AV_HWDEVICE_TYPE_CUDA

命令行没指定 -hwaccel cuvid 会导致 ist->hwaccel_id 没设置,会影响 get_format() 里面的逻辑

然后在 ffmpeg.c 的 init_input_stream() 函数里面,初始化输入流的时候,也有一部分硬件解码相关代码 ,如下:

ffmpeg.c 
static int init_input_stream(int ist_index, char *error, int error_len)
{
    //省略代码...
    if (ist->decoding_needed) {
        ist->dec_ctx->opaque                = ist;
        //注意 get_format
        ist->dec_ctx->get_format            = get_format;
        ist->dec_ctx->get_buffer2           = get_buffer;
        省略代码...
    }
    
    ret = hw_device_setup_for_decode(ist);
    if (ret < 0) {
        snprintf(error, error_len, "Device setup failed for "
             "decoder on input stream #%d:%d : %s",
             ist->file_index, ist->st->index, av_err2str(ret));
        return ret;
    }
    if ((ret = avcodec_open2(ist->dec_ctx, codec, &ist->decoder_opts)) < 0) {
        //省略代码...
    }
    
    return 0;
}
           

上面代码,有两个重点。

1,hw_device_setup_for_decode() 初始化硬件解码设备

2,get_format() ,get_format() 这是一个回调函数,在 avcodec_open2() 打开的解码器的时候会调用 get_format(),根据 get_format 的返回值决定解码器输出哪种 像素格式,一般解码器支持输出的像素格式有限,例如 h264_cuvid 只支持输出 NV12 跟 CUDA 两种像素格式。

先讲 hw_device_setup_for_decode() 函数,主要代码如下:

int hw_device_setup_for_decode(InputStream *ist)
{
    const AVCodecHWConfig *config;
    enum AVHWDeviceType type;
    HWDevice *dev = NULL;
    int err, auto_device = 0;
​
    if (ist->hwaccel_device) {
        //省略代码...
        //命令行没指定 -hwaccel_device,这里逻辑没执行。
    } else {
        if (ist->hwaccel_id == HWACCEL_AUTO) {
            auto_device = 1;
        } else if (ist->hwaccel_id == HWACCEL_GENERIC) {
            type = ist->hwaccel_device_type;
            dev = hw_device_get_by_type(type);
            if (!dev){
                //重点代码
                err = hw_device_init_from_type(type, NULL, &dev);
            }
        } else {
            //省略代码.,逻辑没有执行
        }
    }
​
    if (auto_device) {
        //省略代码.,逻辑没有执行
    }
​
    if (!dev) {
        av_log(ist->dec_ctx, AV_LOG_ERROR, "No device available "
               "for decoder: device type %s needed for codec %s.\n",
               av_hwdevice_get_type_name(type), ist->dec->name);
        return err;
    }
​    //重点代码
    ist->dec_ctx->hw_device_ctx = av_buffer_ref(dev->device_ref);
    if (!ist->dec_ctx->hw_device_ctx)
        return AVERROR(ENOMEM);
​
    return 0;
}
           

由于我们命令行没使用 -hwaccel_device 指定硬件加速设备,所以 if (ist->hwaccel_device) {xxx} 的条件并没有跑进去。

以上代码都是经过删减的代码,有以下重点。

1,调用 hw_device_init_from_type(type, NULL, &dev); 初始化 dev 变量。

2,ist->dec_ctx->hw_device_ctx 初始化,用了 av_buffer_ref() 函数,AVBuffer 是ffmpeg的一个通用结构,很多字段都是 AVBuffer。C语言就是用一块void *内存来实现泛型,然后做指针强制转换,这块内存就会被解析成相应的类型(struct)。

接着分析 get_format 函数,get_format 是用来给调用层 决定解码出来什么样的 pixel format 的。get_format() 的定义如下:

/**
* callback to negotiate the pixelFormat
* @param fmt is the list of formats which are supported by the codec,
* it is terminated by -1 as 0 is a valid format, the formats are ordered by quality.
* The first is always the native one.
* @note The callback may be called again immediately if initialization for
* the selected (hardware-accelerated) pixel format failed.
* @warning Behavior is undefined if the callback returns a value not
* in the fmt list of formats.
* @return the chosen format
* - encoding: unused
* - decoding: Set by user, if not set the native format will be chosen.
*/
enum AVPixelFormat (*get_format)(struct AVCodecContext *s, const enum AVPixelFormat * fmt);
           

第二个参数 const enum AVPixelFormat * fmt 是解码器支持的 像素格式。本命令使用的解码器是 h264_cuvid ,只支持 NV12,CUDA 两种像素格式。

get_format 函数的实现在 ffmpeg.c 里面:

static enum AVPixelFormat get_format(AVCodecContext *s, const enum AVPixelFormat *pix_fmts)
{
    InputStream *ist = s->opaque;
    const enum AVPixelFormat *p;
    int ret;
​
    省略代码...
​
    return *p;
}
           

主要有以下重点:

1,非硬件加速的解码器 (NV12 像素格式是非硬件加速的),默认取第一个支持的像素格式作为解码输出。可以看到这里直接 break ,跳过循环。

if (!(desc->flags & AV_PIX_FMT_FLAG_HWACCEL))
    break;
           

2,如果是硬件加速的解码 (CUDA 像素格式是硬件加速的),就会继续执行,用 avcodec_get_hw_config() 找出一个 config 是支持 AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX 的。

if (ist->hwaccel_id == HWACCEL_GENERIC ||
    ist->hwaccel_id == HWACCEL_AUTO) {
    for (i = 0;; i++) {
        config = avcodec_get_hw_config(s->codec, i);
        if (!config)
            break;
        if (!(config->methods &
              AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX))
            continue;
        if (config->pix_fmt == *p)
            break;
    }
}
           

3,尝试初始化硬件解码器。

ret = hwaccel_decode_init(s);
 if (ret < 0) {
    if (ist->hwaccel_id == HWACCEL_GENERIC) {
        av_log(NULL, AV_LOG_FATAL,
            "   %s hwaccel requested for input stream #%d:%d, "
                "but cannot be initialized.\n",
                av_hwdevice_get_type_name(config->device_type),
        ist->file_index, ist->st->index);
        return AV_PIX_FMT_NONE;
    }
    continue;
 }
           

4,设置 硬件解码输出的 格式 为 CUDA 格式,break,然后会 return。

ist->hwaccel_pix_fmt = *p;
break;
           

以上就是 ffmpeg.c 里 get_foramt() 对于普通的解码跟硬件解码的区别处理,主要重点如下:

1,普通解码直接返回第一个解码器支持的像素格式。

2,硬件解码会多做一些检测,跟变量初始化。

硬件解码还有一个函数 get_buffer(),也是在 ffmpeg.c 里面,代码如下:

static int get_buffer(AVCodecContext *s, AVFrame *frame, int flags)
{
    InputStream *ist = s->opaque;
​
    if (ist->hwaccel_get_buffer && frame->format == ist->hwaccel_pix_fmt)
        return ist->hwaccel_get_buffer(s, frame, flags);
​
    return avcodec_default_get_buffer2(s, frame, flags);
}
           

这里面其实是对 qsv 硬件解码做了特殊处理,ist->hwaccel_get_buffer 这个只会在 qsv_init() 里面被初始化赋值。

我们用的是 cuda,会直接走默认的 get_buffer 函数,就是 avcodec_default_get_buffer2()。

至此 ,ffmpeg 的硬件解码已经分析完毕。

硬件加速 filter的处理如下:

ffmpeg_filter.c 1037行
ret = hw_device_setup_for_filter(fg);
           
int hw_device_setup_for_filter(FilterGraph *fg)
{
    HWDevice *dev;
    int i;
​
    // If the user has supplied exactly one hardware device then just
    // give it straight to every filter for convenience.  If more than
    // one device is available then the user needs to pick one explcitly
    // with the filter_hw_device option.
    if (filter_hw_device)
        dev = filter_hw_device;
    else if (nb_hw_devices == 1)
        dev = hw_devices[0];
    else
        dev = NULL;
​
    if (dev) {
        for (i = 0; i < fg->graph->nb_filters; i++) {
            fg->graph->filters[i]->hw_device_ctx =
                av_buffer_ref(dev->device_ref);
            if (!fg->graph->filters[i]->hw_device_ctx)
                return AVERROR(ENOMEM);
        }
    }
​
    return 0;
}
           

hw_device_setup_for_filter() 重点就是设置了 filter里面的 hw_device_ctx 变量,估计是用来处理 硬件像素格式的 filter 逻辑。

硬件加速,编码流程图如下:

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

hw_device_setup_for_encode() 函数里的代码就不粘贴了,比较容易理解,在本文命令里主要就设置了一个变量 

ost->enc_ctx->hw_frames_ctx

hw_device_setup_for_encode()
ost->enc_ctx->hw_frames_ctx = av_buffer_ref(frames_ref);
           

命令行参数中,有个奇怪的地方, -hwaccel cuvid,我个人比较疑惑,这个参数起到什么样的作用,硬件编解码应该只需要指定解码器是什么就行了,为什么还要多此一举指定 -hwaccel cuvid 呢?带着这个疑问继续研究。接下来分析如果没有指定 -hwaccel cuvid 这个会有何影响,命令如下:

ffmpeg.exe -vcodec h264_cuvid -i juren_10s.mp4 -vcodec h264_nvenc -acodec copy juren_h264_nvenc_10s.mp4 -y

没设置 -hwaccel cuvid 会导致以下变化:

1,导致 add_input_streams() 里面的以下逻辑不会执行,导致 ist->hwaccel_id 没有值 。

add_input_streams()
if( hwaccel ){ 
    设置 ist->hwaccel_id 
    设置 ist->hwaccel_device_type
}
           

2,ist->hwaccel_id 没有值,就会导致 get_format() 函数返回的 AVPixelFormat *p 是 NV12,而不是 CUDA。这里 NV12 是没有 AV_PIX_FMT_FLAG_HWACCEL 这个标记的,CUDA有这个标记。所以会导致 h264_cuvid 这个解码器输出的 AVFrame 是 NV12 格式的,不是原来的 CUDA 格式。但 h264_cuvid 依然是一个硬件解码器。

3,影响 hw_device_setup_for_decode() 函数的逻辑,导致 ist->dec_ctx->hw_device_ctx 没有值。

4,影响 hw_device_setup_for_decode() 函数的逻辑,导致 hw_device_init_from_type() 没有执行,所以变量 nb_hw_devices 等于 0,应该是没有硬件设备的意思。

4,变量 nb_hw_devices 等于 0 会影响 hw_device_setup_for_filter() 函数的逻辑,导致 fg->graph->filters[i]->hw_device_ctx 没有赋值,hw_device_setup_for_filte() 函数的代码上面有,不贴了。

5,fg->graph->filters[i]->hw_device_ctx 没有赋值,会导致 hw_device_setup_for_encode() 里面的 av_buffersink_get_hw_frames_ctx() 函数拿不到值,进而导致 ost->enc_ctx->hw_frames_ctx 没有被设置,代码如下:

hw_device_setup_for_encode()
frames_ref = av_buffersink_get_hw_frames_ctx(ost->filter->filter);
ost->enc_ctx->hw_frames_ctx = av_buffer_ref(frames_ref); //没有执行
           

做下总结, -hwaccel cuvid 没设置,所以

ist->hwaccel_id 没有值

ist->hwaccel_device_type 没有值

ist->dec_ctx->hw_device_ctx 没有值

nb_hw_devices 等于 0

fg->graph->filters[i]->hw_device_ctx 没有值

ost->enc_ctx->hw_frames_ctx 没有值

重点:解码的时候用的是 dec_ctx->hw_device_ctx ,编码的时候设置的 enc_ctx->hw_frames_ctx,hw_device_ctx 跟 hw_frames_ctx 应该是两个不同的东西,这里埋个坑,后续讲解。

虽然没设置 -hwaccel cuvid 导致这么多变量没有值,但是我看我的GPU,却实实在在跑满了,这个问题,我也百思不得其解,如下图:

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

从上面的分析看起来,-hwaccel cuvid 貌似并不会影响到使用GPU编解码

讨论补充:

CUDA 跟 CUVID 是 ffmpeg 实现的两种使用硬件加速的方式,主要区别是 frame 怎么解码,然后内存数据怎么转发。

网址:HWAccelIntro – FFmpeg

ffmpeg命令分析【详细分析合集】ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe

 还有最后一个分析,h264_cuvid 解码器解码出来 CUDA 格式的 AVFrame,因为某些编码器只支持NV12格式,我们想转成 NV12 的AVFrame,再传递给 编码器如何操作。可以指定 

-hwaccel_output_format nv12

 ,命令如下:

ffmpeg.exe -hwaccel cuvid  -hwaccel_output_format nv12 -vcodec h264_cuvid -i juren_10s.mp4 -vcodec h264_nvenc -acodec copy juren_h264_nvenc_10s.mp4 -y
           

这个功能是由 hwaccel_retrieve_data() 函数实现的,在 hwaccel_retrieve_data() 内部 如果 ist->hwaccel_pix_fmt 跟 ist->hwaccel_output_format 不一致,就会进行硬件格式转换。

这里的像素格式转换跟 《ffmpeg命令分析-pix_fmt》 不太一样,-pix_fmt 是通过 format filter 来实现的,针对的是非硬件像素格式,如果 format filter 的输入是 cuda 像素格式,输出是 nv12 之类的非硬件像素格式,format filter会报错。

总结:

1,-pix_fmt ,通过 format filter 来实现,用于非硬件像素格式的转换。

2,-hwaccel_output_format,通过 hwaccel_retrieve_data() 来实现,用于硬件像素格式的转换。

ffmpeg cuda 硬件加速 分析完毕。

分享一个音视频高级开发交流群,需要C/C++ Linux服务器架构师学习资料加企鹅群:788280672获取。资料包括(C/C++,Linux,FFmpeg  webRTC  rtmp  hls rtsp ffplay  srs 等等),免费分享。

没讲完的请看下篇

《ffmpeg命令分析下【详细分析合集】》