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 = 0frame=1, PTS = 1000, PTS_TIME = PTS * timebase = 0.016frame=2, PTS = 2000, PTS_TIME = PTS * timebase = 0.033
对于几乎相同的情况,但时基等于1/60。
frame=0, PTS = 0, PTS_TIME = 0frame=1, PTS = 1, PTS_TIME = PTS * timebase = 0.016frame=2, PTS = 2, PTS_TIME = PTS * timebase = 0.033frame=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 = 0frame=1, PTS = 3, PTS_TIME = PTS * timebase = 0.04frame=2, PTS = 6, PTS_TIME = PTS * timebase = 0.08frame=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 ...
随机推荐
- hdu4403暴力搜索
题意: 给你一个数字串,让你在里面添加一个=和若干个+,使等式成立. 思路: lmax最大是15,直接暴搜,无压力,关键是判重,要在答案的时候判重,一开始在进队列之前判的,各种wa ...
- 编译android4.4刷到nexus 5
操作系统:ubuntu14.4tls android源码版本:4.4 手机:nexus5 1 获得手机的驱动程序(跟硬件平台有关):Binaries for Nexus Device 查到nexus ...
- UVA11624大火蔓延的迷宫
题意: 给1个n*m的网格,上面有的点能走,有的点不能走(墙),然后有的点是火源,火源和人一样,每次都是上下左右四个方向蔓延,速度一样是1,火也不可以从墙上跨过去,给你人的起点,终点是只要走到 ...
- DLL内存加载
动态加载dll 功能: 把一个处于内存里的dll直接加载并且使用. 用途: 免杀(静态文件查杀),外挂(防止游戏自己hook了loadlibrary等函数),以及其他. 原理: ...
- iwrite复制攻略
打开iwrite,一提交作业,发现: 这可咋办啊! 那就跟着步骤来呗: 按F12打开元素审查 点一下左上角 再点一下文本框,就能定位到HTML中的位置 在文本框中写几个字母,康康具体位置: 那就复制进 ...
- 【Unity】实验二 游戏场景搭建
实验要求 实验二 游戏场景搭建 实验目的:掌握游戏场景搭建. 实验要求:能够使用Unity的地形引擎创建地形,熟悉场景中的光照与阴影,掌握天空盒和雾化效果等. 实验内容: 地形的绘制:使用高度图绘制: ...
- C# 变体(variance)
上节讲到了泛型,这节延申一下,讲一下变体. 变体(variance)是协变(convariance)和抗变(也说逆变contravariance)的统称.这个概念在.net 4中引入,在.net 2. ...
- 上手 WebRTC DTLS 遇到很多 BUG?浅谈 DTLS Fragment
上一篇<详解 WebRTC 传输安全机制:一文读懂 DTLS 协议>详细阐述了 DTLS.本文将结合 DTLS 开发中遇到的问题,详细解读 DTLS 的一些基础概念以及 Fragment ...
- 将mysql数据同步到ES6.4(全量+增量)
下载安装包时注意下载到指定文件夹 这里我放在OPT文件夹下一:安装logstash进入到opt文件夹打开终端 执行以下命令wget -c https://artifacts.elastic.co/do ...
- 16.分类和static
1.案例驱动模式 1.1案例驱动模式概述 (理解) 通过我们已掌握的知识点,先实现一个案例,然后找出这个案例中,存在的一些问题,在通过新知识点解决问题 1.2案例驱动模式的好处 (理解) 解决重复代码 ...