ffmpeg architecture(中)
ffmpeg architecture(中)
艰苦学习FFmpeg libav
您是否不奇怪有时会发出声音和视觉?
由于FFmpeg作为命令行工具非常有用,可以对媒体文件执行基本任务,因此如何在程序中使用它?
FFmpeg 由几个库组成,这些库可以集成到我们自己的程序中。通常,当您安装FFmpeg时,它将自动安装所有这些库。我将这些库的集合称为FFmpeg libav。
此标题是对Zed Shaw的系列“ Learn X the Hard Way”(特别是他的书“ Learn C the Hard Way” )的致敬。
第0章-臭名昭著的你好世界
您好世界实际上不会"hello world"
在终端中显示消息👅 相反,我们将打印出有关视频的信息,例如其格式(容器),时长,分辨率,音频通道之类的信息,最后,我们将解码一些帧并将其保存为图像文件。
FFmpeg libav体系结构
但是在开始编码之前,让我们学习FFmpeg libav架构如何工作以及其组件如何与其他组件通信。
这是解码视频的过程:
首先,您需要将媒体文件加载到名为AVFormatContext
(视频容器也称为格式)的组件中。实际上,它并未完全加载整个文件:它通常仅读取标头。
加载容器的最小标头后,就可以访问其流(将其视为基本的音频和视频数据)。每个流都可以在名为的组件中使用AVStream
。
流是连续数据流的奇特名称。
假设我们的视频有两个流:用AAC CODEC编码的音频和用H264(AVC)CODEC编码的视频。从每个流中,我们可以提取称为数据包的数据片段(切片),这些数据将加载到名为的组件中AVPacket
。
该包内的数据仍然编码(压缩),并以数据包进行解码,我们需要将它们传递给特定的AVCodec
。
在AVCodec
将它们解码成AVFrame
最后,该组件为我们提供了非压缩帧。注意,音频和视频流使用相同的术语/过程。
要求
由于有些人在编译或运行 我们将Docker
用作开发/ 运行器环境的示例时遇到问题,因此,我们还将使用大型的兔子视频,因此,如果您在本地没有该视频,请运行命令make fetch_small_bunny_video
。
第0章-代码演练
TLDR;给我看代码和执行。
$ make run_hello
我们将跳过一些细节,但是请放心:源代码可在github上找到。
我们将分配内存给AVFormatContext
将保存有关格式(容器)信息的组件。
AVFormatContext * pFormatContext = avformat_alloc_context();
现在,我们将打开文件并读取其标头,并AVFormatContext
使用有关该格式的最少信息填充(注意,通常不会打开编解码器)。用于执行此操作的函数是avformat_open_input
。它需要一个AVFormatContext
,一个filename
和两个可选参数:(AVInputFormat
如果通过NULL
,则FFmpeg会猜测格式)和AVDictionary
(这是解复用器的选项)。
avformat_open_input(&pFormatContext,filename,NULL,NULL);
我们可以打印格式名称和媒体持续时间:
printf(“格式%s,持续时间%lld us ”,pFormatContext-> iformat-> long_name,pFormatContext-> duration);
要访问streams
,我们需要从媒体读取数据。该功能可以avformat_find_stream_info
做到这一点。现在,pFormatContext->nb_streams
将保留流的数量,并且pFormatContext->streams[i]
将为我们提供i
流(an AVStream
)。
avformat_find_stream_info(pFormatContext, NULL);
现在,我们将遍历所有流。
对于(int i = 0 ; i <pFormatContext-> nb_streams; i ++)
{
//
}
对于每个流,我们将保留AVCodecParameters
,它描述了该流使用的编解码器的属性i
。
AVCodecParameters * pLocalCodecParameters = pFormatContext-> streams [i]-> codecpar;
随着编解码器的属性,我们可以看一下正确的CODEC查询功能avcodec_find_decoder
,并找到注册解码器编解码器ID并返回AVCodec
,知道如何连接部件有限公司德和DEC ODE流。
AVCodec * pLocalCodec = avcodec_find_decoder(pLocalCodecParameters-> codec_id);
现在我们可以打印有关编解码器的信息。
//特定视频和音频
如果(pLocalCodecParameters-> codec_type == AVMEDIA_TYPE_VIDEO){
printf的( “视频编解码器:分辨率%d X %d ”,pLocalCodecParameters->宽度,pLocalCodecParameters->高度);
} 否则 如果(pLocalCodecParameters-> codec_type == AVMEDIA_TYPE_AUDIO){
printf的(“音频编解码器:%d通道,采样率%d ”,pLocalCodecParameters-> 通道,pLocalCodecParameters-> SAMPLE_RATE);
}
// //常规
printf( “ \ t编解码器%s ID %d bit_rate %lld ”,pLocalCodec-> long_name,pLocalCodec-> id,pCodecParameters-> bit_rate);
使用编解码器,我们可以为分配内存,该内存AVCodecContext
将保存我们的解码/编码过程的上下文,但是随后我们需要使用CODEC参数填充此编解码器上下文;我们这样做avcodec_parameters_to_context
。
填充编解码器上下文后,我们需要打开编解码器。我们调用该函数avcodec_open2
,然后就可以使用它了。
AVCodecContext * pCodecContext = avcodec_alloc_context3(pCodec);
avcodec_parameters_to_context(pCodecContext,pCodecParameters);
avcodec_open2(pCodecContext,pCodec,NULL);
现在,我们打算从流中读取数据包,并将其解码为帧,但首先,我们需要为这两个组件的分配内存AVPacket
和AVFrame
。
AVPacket * pPacket = av_packet_alloc();
AVFrame * pFrame = av_frame_alloc();
让我们在函数av_read_frame
有数据包时从流中提供数据包。
while(av_read_frame(pFormatContext,pPacket)> = 0){
// ...
}
让我们使用函数通过编解码器上下文将原始数据包(压缩帧)发送到解码器avcodec_send_packet
。
avcodec_send_packet(pCodecContext,pPacket);
然后,我们使用function通过相同的编解码器上下文从解码器接收原始数据帧(未压缩的帧)avcodec_receive_frame
。
avcodec_receive_frame(pCodecContext,pFrame);
printf(
“帧%c(%d)点%d dts %d key_frame %d [coded_picture_number %d,display_picture_number %d ] ”,
av_get_picture_type_char(pFrame-> pict_type),
pCodecContext-> frame_number,
pFrame-> pts,
pFrame-> pkt_dts,
pFrame-> key_frame,
pFrame-> coded_picture_number,
pFrame-> display_picture_number
);
最后,我们可以将解码后的帧保存为简单的灰度图像。该过程非常简单,我们将使用pFrame->data
索引与平面Y,Cb和Cr相关的位置,我们刚刚选择0
(Y)保存灰度图像。
save_gray_frame(pFrame-> data [ 0 ],pFrame-> linesize [ 0 ],pFrame-> width,pFrame-> height,frame_filename);
static void save_gray_frame(unsigned char * buf,int wrap,int xsize,int ysize,char * filename)
{
文件 * f;
诠释 I;
f = fopen(文件名,“ w ”);
//编写pgm文件格式所需的最小标头
//便携式灰度图格式-> https://en.wikipedia.org/wiki/Netpbm_format#PGM_example
fprintf(f,“ P5 \ n %d %d \ n %d \ n “,xsize,ysize,255);
//
为(i = 0 ; i <ysize; i ++)
逐行编写fwrite(buf + i * wrap, 1,xsize,f);
fclose(f);
}
第1章-同步音频和视频
成为播放器 -一个年轻的JS开发人员,编写新的MSE视频播放器。
在开始编写转码示例代码之前,我们先谈一下定时,或者视频播放器如何知道正确的时间播放帧。
在上一个示例中,我们保存了一些可以在此处看到的帧:
在设计视频播放器时,我们需要以给定的速度播放每一帧,否则,由于播放的速度太快或太慢,很难令人愉快地观看视频。
因此,我们需要引入一些逻辑来平稳地播放每个帧。为此,每个帧具有表示时间戳(PTS),其是在时基中分解的递增数字,该时基是可被帧速率(fps)整除的有理数(其中分母称为时间标度)。
当我们看一些示例时,更容易理解,让我们模拟一些场景。
对于fps=60/1
,timebase=1/60000
每个PTS都会增加,timescale / fps = 1000
因此每个帧的PTS实时可能是(假设从0开始):
frame=0, PTS = 0, PTS_TIME = 0
frame=1, PTS = 1000, PTS_TIME = PTS * timebase = 0.016
frame=2, PTS = 2000, PTS_TIME = PTS * timebase = 0.033
对于几乎相同的情况,但时基等于1/60
。
frame=0, PTS = 0, PTS_TIME = 0
frame=1, PTS = 1, PTS_TIME = PTS * timebase = 0.016
frame=2, PTS = 2, PTS_TIME = PTS * timebase = 0.033
frame=3, PTS = 3, PTS_TIME = PTS * timebase = 0.050
对于fps=25/1
和timebase=1/75
每个PTS将增加timescale / fps = 3
和PTS时间可能是:
frame=0, PTS = 0, PTS_TIME = 0
frame=1, PTS = 3, PTS_TIME = PTS * timebase = 0.04
frame=2, PTS = 6, PTS_TIME = PTS * timebase = 0.08
frame=3, PTS = 9, PTS_TIME = PTS * timebase = 0.12
- ...
frame=24, PTS = 72, PTS_TIME = PTS * timebase = 0.96
- ...
frame=4064, PTS = 12192, PTS_TIME = PTS * timebase = 162.56
现在,借助,pts_time
我们可以找到一种方法来呈现与音频pts_time
或系统时钟同步的同步。FFmpeg libav通过其API提供以下信息:
- fps =
AVStream->avg_frame_rate
- tbr =
AVStream->r_frame_rate
- tbn =
AVStream->time_base
出于好奇,我们保存的帧以DTS顺序发送(帧:1、6、4、2、3、5),但以PTS顺序播放(帧:1、2、3、4、5)。另外,请注意,B帧与P帧或I帧相比价格便宜。
LOG: AVStream->r_frame_rate 60/1
LOG: AVStream->time_base 1/60000
...
LOG: Frame 1 (type=I, size=153797 bytes) pts 6000 key_frame 1 [DTS 0]
LOG: Frame 2 (type=B, size=8117 bytes) pts 7000 key_frame 0 [DTS 3]
LOG: Frame 3 (type=B, size=8226 bytes) pts 8000 key_frame 0 [DTS 4]
LOG: Frame 4 (type=B, size=17699 bytes) pts 9000 key_frame 0 [DTS 2]
LOG: Frame 5 (type=B, size=6253 bytes) pts 10000 key_frame 0 [DTS 5]
LOG: Frame 6 (type=P, size=34992 bytes) pts 11000 key_frame 0 [DTS 1]
第2章-重新混合
重塑是将一种格式(容器)更改为另一种格式的行为,例如,我们可以使用FFmpeg 轻松地将MPEG-4视频更改为MPEG-TS:
ffmpeg input.mp4 -c复制output.ts
它将对mp4进行解复用,但不会对其进行解码或编码(-c copy
),最后,会将其复用为mpegts
文件。如果您不提供格式,-f
则ffmpeg会尝试根据文件扩展名猜测它。
FFmpeg或libav的一般用法遵循模式/体系结构或工作流程:
- 协议层 -接受
input
(file
例如,但也可以是rtmp
或HTTP
输入) - 格式层 -它
demuxes
的内容,主要显示元数据及其流 - 编解码器层 -
decodes
压缩流数据可选 - 像素层 -也可以将其应用于
filters
原始帧(如调整大小)可选 - 然后它做反向路径
- 编解码器层 -它
encodes
(或re-encodes
什至transcodes
)原始帧是可选的 - 格式层 -它
muxes
(或remuxes
)原始流(压缩数据) - 协议层 -最终将多路复用的数据发送到
output
(另一个文件或网络远程服务器)
现在,让我们使用libav编写示例,以提供与中相同的效果ffmpeg input.mp4 -c copy output.ts
。
我们将从一个输入(input_format_context
)读取并将其更改为另一个输出(output_format_context
)。
AVFormatContext * input_format_context = NULL ;
AVFormatContext * output_format_context = NULL ;
我们开始进行通常的分配内存并打开输入格式。对于这种特定情况,我们将打开一个输入文件并为输出文件分配内存。
if((ret = avformat_open_input(&input_format_context,in_filename,NULL,NULL))< 0){
fprintf(stderr,“无法打开输入文件' %s ' ”,in_filename);
转到结尾
}
if((ret = avformat_find_stream_info(input_format_context,NULL))< 0){
fprintf(stderr,“无法检索输入流信息”);
转到结尾
}
avformat_alloc_output_context2(&output_format_context,NULL,NULL,out_filename);
if(!output_format_context){
fprintf(stderr,“无法创建输出上下文\ n ”);
ret = AVERROR_UNKNOWN;
转到结尾
}
我们将只重新混合流的视频,音频和字幕类型,因此我们将要使用的流保留到索引数组中。
number_of_streams = input_format_context-> nb_streams;
stream_list = av_mallocz_array(stream_numbers,sizeof(* streams_list));
分配完所需的内存后,我们将遍历所有流,并需要使用avformat_new_stream函数为每个流在输出格式上下文中创建新的输出流。请注意,我们标记的不是视频,音频或字幕的所有流,因此我们可以在以后跳过它们。
对于(i = 0 ; i <input_format_context-> nb_streams; i ++){
AVStream * out_stream;
AVStream * in_stream = input_format_context-> 流 [i];
AVCodecParameters * in_codecpar = in_stream-> codecpar ;
如果(in_codecpar-> codec_type!= AVMEDIA_TYPE_AUDIO &&
in_codecpar-> codec_type!= AVMEDIA_TYPE_VIDEO &&
in_codecpar-> codec_type!= AVMEDIA_TYPE_SUBTITLE){
stream_list [i] = -1 ;
继续 ;
}
stream_list [i] = stream_index ++;
out_stream = avformat_new_stream(output_format_context,NULL);
if(!out_stream){
fprintf(stderr,“无法分配输出流\ n ”);
ret = AVERROR_UNKNOWN;
转到结尾
}
ret = avcodec_parameters_copy(out_stream-> codecpar,in_codecpar);
if(ret < 0){
fprintf(stderr,“复制编解码器参数失败\ n ”);
转到结尾
}
}
现在我们可以创建输出文件了。
如果(!(output_format_context-> oformat-> flags和AVFMT_NOFILE)){
ret = avio_open(&output_format_context-> pb,out_filename,AVIO_FLAG_WRITE);
if(ret < 0){
fprintf(stderr,“无法打开输出文件' %s ' ”,out_filename);
转到结尾
}
}
ret = avformat_write_header(output_format_context,NULL);
if(ret < 0){
fprintf(stderr,“打开输出文件时发生错误\ n ”);
转到结尾
}
之后,我们可以逐个数据包地将流从输入复制到输出流。我们将在它有数据包(av_read_frame
)时循环播放,对于每个数据包,我们需要重新计算PTS和DTS以最终将其(av_interleaved_write_frame
)写入输出格式上下文。
而(1){
AVStream * in_stream,* out_stream;
ret = av_read_frame(input_format_context,&packet);
如果(ret < 0)
中断 ;
in_stream = input_format_context-> 流 [数据包。stream_index ];
如果(分组。stream_index > = number_of_streams || streams_list [数据包。stream_index ] < 0){
av_packet_unref(包);
继续 ;
}
包。stream_index = stream_list [数据包。stream_index ];
out_stream = output_format_context-> 流 [数据包。stream_index ];
/ *复制数据包* /
数据包。pts = av_rescale_q_rnd(数据包pts,in_stream-> time_base,out_stream-> time_base,AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
包。dts = av_rescale_q_rnd(数据包dts,in_stream-> time_base,out_stream-> time_base,AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
包。持续时间 = av_rescale_q(数据包duration,in_stream-> time_base,out_stream-> time_base);
// https://ffmpeg.org/doxygen/trunk/structAVPacket.html#ab5793d8195cf4789dfb3913b7a693903
数据包。pos = -1 ;
// https://ffmpeg.org/doxygen/trunk/group__lavf__encoding.html#ga37352ed2c63493c38219d935e71db6c1
ret = av_interleaved_write_frame(output_format_context,&packet);
if(ret < 0){
fprintf(stderr, “错误合并数据包\ n ”);
休息 ;
}
av_packet_unref(&packet);
}
最后,我们需要使用av_write_trailer函数将流预告片写入输出媒体文件。
av_write_trailer(output_format_context);
现在我们准备对其进行测试,并且第一个测试将是从MP4到MPEG-TS视频文件的格式(视频容器)转换。我们基本上是ffmpeg input.mp4 -c copy output.ts
使用libav 制作命令行。
使run_remuxing_ts
工作正常!!!可以通过以下方法进行检查ffprobe
:
ffprobe -i remuxed_small_bunny_1080p_60fps.ts
从'remuxed_small_bunny_1080p_60fps.ts'
输入# 0,mpegts:
持续时间:00:00:10.03,开始:0.000000,比特率:2751 kb / s
程序1
元数据:
service_name :服务 01
service_provider:FFmpeg
流# 0:0 [0x100]:视频:h264(高)([27] [0] [0] [0] / 0x001B),yuv420p(逐行),1920x1080 [SAR 1:1 DAR 16:9],60 fps,60 tbr,90k tbn,120 tbc
流# 0:1 [0x101]:音频:ac3([129] [0] [0] [0] / 0x0081),48000 Hz,5.1(侧面),fltp,320 kb /秒
总结一下我们在图中所做的事情,我们可以回顾一下关于libav如何工作的最初想法,但表明我们跳过了编解码器部分。
在结束本章之前,我想展示重混合过程的重要部分,您可以将选项传递给多路复用器。假设我们要为此提供MPEG-DASH格式,我们需要使用分段的mp4(有时称为fmp4
)代替MPEG-TS或纯MPEG-4。
ffmpeg -i non_fragmented.mp4 -movflags frag_keyframe+empty_moov+default_base_moof fragmented.mp4
由于命令行是libav版本,因此几乎同样容易,我们只需要在复制数据包之前在写入输出标头时传递选项即可。
AVDictionary * opts = NULL ;
av_dict_set(&opts,“ movflags ”,“ frag_keyframe + empty_moov + default_base_moof ”,0);
ret = avformat_write_header(output_format_context,&opts);
现在,我们可以生成此分段的mp4文件:
制作run_remuxing_fragmented_mp4
但是要确保我没有对你说谎。您可以使用令人惊叹的site / tool gpac / mp4box.js或网站http://mp4parser.com/来查看差异,首先加载“常用” mp4。
如您所见,它只有一个mdat
原子/盒子,这是视频和音频帧所在的位置。现在加载零碎的mp4,以查看它如何散布mdat
盒子。