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/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 的为准。
分享一个音视频高级开发交流群,需要C/C++ Linux服务器架构师学习资料加企鹅群:788280672获取。资料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等),免费分享。
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(),设置编码器的码率。
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 获取输出的帧率,帧率跟输入文件帧率一样,代码如下:
如果命令行指定了 -r ,就会用命令行参数赋值 给
ost->frame_rate
。,代码如下:
上面是 命令行参数 -r 赋值给
ost->frame_rate
。然后
ost->frame_rate
会作为时间基赋值给 编码器的time_base,见下图代码:
所以,综上所述,-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 的影响就在这里。
可以看到,上图代码把原始视频帧 的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。
如上图所示,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。
第一帧全是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 。
注意,有些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的源码,函数说明,只有传递进 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 这个变量,请看下图。
总结:
能让 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_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 的选项里查找。
查找过程源码如下图所示:
从 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字段 会在哪里使用呢?请看下图:
可以看到,o->video_disable 控制了 new_video_stream() 没有执行,导致输出 AVFormatContext没有 video 流。
虽然AVFormatContext没有 video 流,但是视频流的AVPacket 还是会从输入文件读取出来,那读取出来的 AVPacket 会如何处理呢?
在《FFmpeg 源码分析》中讲解过,有一个全局变量数组 input_streams[] ,数组里存储所有的输入流。
在 add_input_stream() 里面 创建输入流的时候, ist->discard 默认是 1 (默认读出来的AVPacket全部丢弃),只有执行了 new_video_stream() -> new_output_stream() 的时候才会把 ist->discard 设置成 0。
new_output_stream() 函数的代码如下:
可以看到 在 ffmpeg_opt.c 1559行,根据输出流来决定输入流的 discard 的值要不要开启。
输入流的 discard 如果是 1,会导致什么结果呢,再请看下图。
-vn 命令分析完毕。
总结:
vn 导致没有执行 new_video_stream(),导致 视频输入流的 ist->discard 一直是1 ,ist->discard等于1 再导致 process_input() 的时候丢弃读取出来的AVPacket。
扩展知识:
Qt creator 里面的 data breakpoit 特别好用,想知道某个变量在哪里被修改了,用 data breakpoit 断点变量的地址,下次变量被修改就可以自动停在相应的代码位置。
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 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.c 里可以看出,上面的红色圈出来的是 print_report() 函数打印的,下面就来分析 print_report() 的实现,流程图如下:
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之后音频没有,所以本文主要讲解音频是如何丢包的。如下图
在 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() 函数,打开输入文件的逻辑都在这个函数里面。
如上图所示,在 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() 函数里面完成的,如图:
在 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.c 工程的硬件加速代码在3地方都有分布,解码,filter,编码。本文分开讲述。
硬件加速,解码的流程图如下:
首先,在 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 逻辑。
硬件加速,编码流程图如下:
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,却实实在在跑满了,这个问题,我也百思不得其解,如下图:
从上面的分析看起来,-hwaccel cuvid 貌似并不会影响到使用GPU编解码
讨论补充:
CUDA 跟 CUVID 是 ffmpeg 实现的两种使用硬件加速的方式,主要区别是 frame 怎么解码,然后内存数据怎么转发。
网址:HWAccelIntro – FFmpeg
还有最后一个分析,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命令分析下【详细分析合集】》