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
盒子。
ffmpeg architecture(中)的更多相关文章
- ffmpeg architecture(下)
ffmpeg architecture(下) 第3章-转码 TLDR:给我看代码和执行. $ make run_transcoding 我们将跳过一些细节,但是请放心:源代码可在github上找到. ...
- ffmpeg architecture(上)
ffmpeg architecture(上) 目录 介绍 视频-您看到的是什么! 音频-您在听什么! 编解码器-缩小数据 容器-音频和视频的舒适场所 FFmpeg-命令行 FFmpeg命令行工具101 ...
- FFmpeg: FFmepg中的sws_scale() 函数分析
FFmpeg中的 sws_scale() 函数主要是用来做视频像素格式和分辨率的转换,其优势在于:可以在同一个函数里实现:1.图像色彩空间转换, 2:分辨率缩放,3:前后图像滤波处理.不足之处在于:效 ...
- ffmpeg 频中分离 video audio 截取片断
1.获取视频的信息 ffmpeg -i video.avi 2,将图片序列分解合成视频 ffmpeg -i src.mpg image%d.jpg ffmpeg -f image2 -i ...
- ffmpeg xcode 中的使用
最近比较闲,苦于ios设备上没有直接播放torrent 文件的软件,开始折腾了.找了不少资料有了思路.但是其中用到了ffmpeg 这个东西. ffmpeg 是通用的一个视频解决框架,用C语言编写,通用 ...
- ffmpeg的中文文档
1. 概要 ffmpeg [global_options] {[input_file_options] -i INPUT_FILE} ... {[output_file_options] OUTPUT ...
- FFmpeg——AVFrame中 的 data
AVFrame中 的 data 的定义如下: typedef struct AVFrame { #define AV_NUM_DATA_POINTERS 8 /** * pointer to the ...
- ffmpeg编码中的二阻塞一延迟
1. avformat_find_stream_info接口延迟 不论是减少预读的数据量,还是设置flag不写缓存,我这边都不实用,前者有风险,后者会丢帧,可能我还没找到好姿势,记录在此,参考:htt ...
- ffmpeg中swscale 的用法
移植ffmpeg过程中,遇到swscale的用法问题,所以查到这篇文章.文章虽然已经过去很长时间,但是还有颇多可以借鉴之处.谢谢“咕咕鐘". 转自:http://guguclock.blog ...
随机推荐
- 书评第001篇:《C++黑客编程揭秘与防范》
本书基本信息 作者:冀云(编著) 出版社:人民邮电出版社 出版时间:2012-6-1 ISBN:9787115280640 版次:1 页数:265 字数:406000 印刷时间:2012-6-1 开本 ...
- LA3989女士的选择
题意: 给你n个男士n个女士,然后给你每个男士中女士的排名,和每个女士中每个男士在他们心中的排名,问你是否可以组成稳定的舞伴,如果存在以下情况(1)男生u和女生v不是舞伴,他们喜欢对方的程 ...
- android中Stub Proxy答疑
在上篇添加账户源码解析的博文中,我们发现功能是由AccountManager的mService成员来实现.而mService其实是AccountManagerService,如果对android系统有 ...
- 什么?这么精髓的View的Measure流程源码全解析,你确定不看看?
前言 Android开发中我们平时接触最多的是各种View, View是一个比较大的体系,包含了绘制流程.事件分发.各种动画.自定义View 等等.前几天我写了一篇事件分发源码解析的文章, 今天我们来 ...
- 【pytest系列】- pytest测试框架介绍与运行
如果想从头学起pytest,可以去看看这个系列的文章! https://www.cnblogs.com/miki-peng/category/1960108.html 前言 目前有两种纯测试的测 ...
- 每天一道面试题LeetCode 80--删除排序数组中的重复项 II(python实现)
LeetCode 80--删除排序数组中的重复项 II 给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素最多出现两次,返回移除后数组的新长度. 不要使用额外的数组空间,你必须在原地修改输 ...
- Java发送邮件报错:com.sun.mail.util.LineOutputStream.<init>(Ljava/io/OutputStream;Z)V
在练习使用Java程序发送邮件的代码 运行出现了com.sun.mail.util.LineOutputStream.<init>(Ljava/io/OutputStream;Z)V报错信 ...
- 一文读懂 SuperEdge 云边隧道
作者 李腾飞,腾讯容器技术研发工程师,腾讯云TKE后台研发,SuperEdge核心开发成员. 杜杨浩,腾讯云高级工程师,热衷于开源.容器和Kubernetes.目前主要从事镜像仓库,Kubernete ...
- 浅尝js垃圾回收机制
局部作用域内的变量,在函数执行结束之后就会被js的垃圾回收机制销毁 为什么要销毁局部变量? => 为了释放内存 js垃圾回收机制何时会销毁局部变量 : 如果局部变量无法再得到访问,就会被 ...
- 关于Annotation注解的理解
在编Java程序的时候,我们经常会碰到annotation.比如:@Override 我们在子类继承父类的时候,会经常用到这个annotation.它告诉编译器这个方法是override父类的方法的. ...