ffmpeg作为图书馆:libav教程

2021-05-13 17:10:37

我正在寻找一个教程/书,教我如何开始使用ffmpeg作为库(a.k.a.ibav),然后我找到了"如何在少于1k行和#34中编写视频播放器;教程。不幸的是,它被推翻了,所以我决定写这个。

这里的大多数代码都将在c但不用担心:您可以轻松理解并将其应用于您的首选语言.FFFMPEG Libav对Python这样的许多语言有很多绑定,即使您的语言也不是您的语言&# 39; t拥有它,您仍然可以通过FFI(这里的一个例子)来支持它。

我们' ll从一个快速课程开始,关于视频,音频,编解码器和容器,然后我们' ll转到如何使用ffmpeg命令行的崩溃课程,最后我们' ll写代码,感觉自由跳过直接跳过该节,学习ffmpeg libav的硬路。

有些人常常说互联网视频流是传统电视的未来,无论如何,FFMPEG是值得学习的。

如果您有一个序列系列图像并以给定的频率更改它们(让' s每秒表示24张图片),您将创建一个幻觉。总结这是视频背后的非常基本的想法:一个系列以给定速率运行的图片/框架。

虽然静音视频可以表达各种各样的感受,但为其提供了更多的乐趣。

声音是作为压力波的振动,通过空气或任何其他传动介质,例如气体,液体或固体。

在数字音频系统中,麦克风将声音转换为模拟电信号,然后是模数转换器(ADC) - 通常使用脉冲码调制(PCM) - 将模拟信号转换为数字信号。

Codec是一种压缩或解压缩数字音频/视频的电子电路或软件。它将原始(未压缩的)数字音频/视频转换为压缩格式,反之亦然。 https://en.wikipedia.org/wiki/video_codec.

但是如果我们选择在一个文件中收拾数百万图像并将其称为电影,我们可能会结束一个巨大的文件。让' s做数学:

假设我们正在创建一个具有1080 x 1920(高度x宽度)的视频,并且我们' ll每像素(屏幕上的最小点)花3个字节以编码颜色(或24位颜色,给出什么US 16,777,216不同颜色),此视频运行每秒24帧,长30分钟。

toppf = 1080 * 1920 // total_of_pixels_per_framecpp = 3 // cost_per_pixeltis = 30 * 60 // time_in_secondsfps = 24 // frames_per_secondrequired_storage = tis * fps * toppf * cpp

此视频需要大约250.28GB的存储或1.11Gbps的带宽!那个'为什么我们需要使用编解码器。

容器或包装格式是一种元文件格式,其规范描述了数据和元数据在计算机文件中的不同元素。 https://en.wikipedia.org/wiki/digital_container_format.

包含所有流的单个文件(主要是音频和视频),它还提供同步和常规元数据,例如标题,分辨率等。

通常,我们可以通过查看其扩展来推断文件的格式:例如,视频.Webm可能是使用容器Webm的视频。

使用多媒体工作,我们可以使用名为ffmpeg的惊人工具/库。您已经直接或间接地或间接地或使用它(使用Chrome?)。

它有一个名为ffmpeg的命令行程序,一个非常简单而强大的二进制文件。对于实例,可以通过键入power命令从mp4转换为容器avi:

我们刚刚在这里进行了复制,它正在将一个容器从一个容器转换为另一个容器。FFMPEG也可以进行代码转换,但我们稍后会谈论这一点。

FFMPEG确实有一个文档,可以做出很大的工作解释它是如何运作的。

要使事物简短,FFMPEG命令行程序期望以下参数格式执行其操作ffmpeg {1} {2} -2} -i {3} {4} {5},其中:

第2,3,4和5部分可以与您的需要一样多。&#39更容易理解此参数格式的操作:

#警告:此文件约为300MB $ WGet -o bunny_1080p_60fps.mp4 http://distribution.bbb3d.renderfarming.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_1080p_60fps_normal.mp4。ffmpeg \ -y \#global选项-c:a libfdk_aac -c :v libx264 \ #inpection options-i bunny_1080p_60fps.mp4 \ #inping url-c:v libvpx-vp9 -c:一个libvorbis \ #pourst optionsbunny_1080p_60fps_vp9.webm#输出URL

该命令采用包含两个流的输入文件MP4(使用H264编解码器和使用H264编解码器编码的视频编码的音频)并将其转换为Webm,也改变其音频和视频编解码器。

我们可以简化上面的命令,但请注意,FFMPEG将采用或猜测您的默认值。如果只需键入ffmpeg -i input.avi output.mp4时,它会使用哪些音频/视频编解码器来产生输出。 mp4?

什么?将一条流(音频或视频)从一个编解码器转换为另一个编解码器的行为。

为什么?有时某些设备(电视,智能手机,控制台等)并不支持x,但y和较新的编解码器提供更好的压缩率。

为什么?有时某些设备(电视,智能手机,控制台等)并不支持x,但y有时更新的容器提供现代必修功能。

$ ffmpeg \ -i bunny_1080p_60fps.mp4 \ -c copy \#只是对ffmpeg跳过encodingbunny_1080p_60fps.webm

为什么?人们将尝试使用更强大的智能手机或在其4克电视上的光纤互联网连接中使用2G(边缘)连接,因此您应该提供多个具有不同比特率的相同视频的再出现。

通常是我们' LL使用转频与变化。 Werner Robitza写了另一种必须读/执行关于FFMPEG速率控制的帖子。

什么?将一个分辨率转换为另一个分辨率的行为。如前所述,在经常发生之前通常与转置一起使用。

什么?产生许多分辨率(比特率)并将媒体拆分为块并通过HTTP提供服务的行为。

为什么?提供一种可在低端智能手机或4K电视上观看的灵活媒体,它也可以轻松划衡和部署,但它可以添加延迟。

#视频流$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 160x90-b:v 250k -keyint_min 150-g 150-an -f webm -dash 1 video_160x90_250k.webm $ ffmpeg -i bunny_1080p_60fps.mp4 - C:v libvpx-vp9-s 320x180 -b:v 500k -keyint_min 150-g 150-an -f webm -dash 1 video_320x180_500k.webm $ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 640x360 -b :v 750k -keyint_min 150-g 150-an -f webm -dash 1 video_640x360_750k.webm $ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 640x360 -b:v 1000k -keyint_min 150-g 150-an -f webm -dash 1 video_640x360_1000k.webm $ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 1280x720 -b:v 1500k -keyint_min 150-g 150-an -f webm -dash 1 video_1280x720_1500k.webm#音频Streams $ ffmpeg -i bunny_1080p_60fps.mp4 -c:一个libvorbis -b:一个128k -vn -f webm -dash 1 audio_128k.webm#dash manifest $ ffmpeg \ -f webm_dash_manifest -i video_160x90_250k.webm \ -f webm_dash_manifest -i Video_320x180_500k.webm \ -f webm_dash_manifest -i video_640x360_750k.webm \ - f webm_dash_manifest -i video_640x360_1000k.webm \ -f webm_dash_manifest -i video_1280x720_500k.webm \ -f webm_dash_manifest -i audio_128k.webm \ -c copy -map 0 -map 1 -map 2 -map 3 -map 4 -map 5 \ -f webm_dash_manifest \ -adaptation_sets" id = 0,streams = 0,1,2,3,4 id = 1,流= 5" \ \ insmest.mpd.

FFMPEG有许多和许多其他用法.I与iMovie一起使用它来为YouTube生产/编辑一些视频,您可以专业地使用它。

由于FFMPEG作为指令行工具如媒体文件进行必要的任务,因此我们如何在我们的程序中使用它?

ffmpeg由几个可以集成到我们自己的程序中的图书馆组成。通常,安装FFMPEG时,它会自动安装所有这些库。我是指作为ffmpeg libav的这些库的集合。

这个标题是ZED Shaw' S系列学习x的斗争,特别是他的书学习了艰难的方式。

这个你好世界实际上是赢得' t显示消息"你好世界"在终端中👅👅👅'重新打印有关视频的信息,类似于它的格式(容器),持续时间,分辨率,音频通道等的东西,最终,我们' ll解码一些框架并保存它们作为图像文件。

但在我们开始代码之前,让我们了解FFMPEG Libav架构的工作方式以及其组件如何与他人通信。

您' ll首先需要将媒体文件加载到名为avformatcontext的组件(视频容器也被称为格式)。它实际上并不完全加载整个文件:它通常只读取标题。

一旦我们加载了我们的容器的最小标题,我们就可以访问其流(将其视为基本的音频和视频数据).Ecte流将在称为AVStream的组件中使用。

假设我们的视频有两个流:用AAC编解码器编码的音频和使用H264(AVC)编解码器编码的视频。从每个流中,我们可以提取名为数据包的数据的片段(切片),该数据包将被加载到名为avpacket的组件中。

数据包内的数据仍然被编码(压缩),并且为了解码数据包,我们需要将它们传递给特定的avcodec。

avcodec将它们解码为avframe,最后,此组件为我们提供了未压缩的帧。注意到,使用音频和视频流使用相同的术语/过程。

由于一些人在编译或运营的例子时面临问题,我们'重新使用Docker作为我们的开发/跑步者环境,我们' LL也使用大量的Bunny视频,所以如果你没有'它在本地运行命令make fetch_small_bunny_video。

我们'重新将内存分配给Component AvformatContext,该组件将保存有关格式的信息(容器)。

现在我们'重新打开文件并读取其标题并填充有关格式的最少信息的AvformatContext(请注意通常编解码器未打开)。用于执行此操作的函数是Avformat_open_Input。它期望AvformatContext,文件名和两个可选参数:avinputformat(如果通过null,ffmpeg将猜测格式)和avdictionary(这是调查器的选项)。

要访问流,我们需要从媒体读取数据。函数avformat_find_stream_info确实如此.now,pformatcontext-> nb_streams将保存流量和pformatcontext-> streams [i]将给我们我流(avstream)。

对于每个流,我们将保留avcodecparameters,它描述了流i的编解码器的属性。

使用编解码器属性,我们可以查找查询函数avcodec_find_decoder的正确编解码器,并找到编解码器ID的已注册的解码器并返回avcodec,知道如何en en代码和解码流的组件。

//视频和音频特定(PlocalcodeCparameters-> codec_type == avmedia_type_video){printf("视频编解码器:分辨率%dx%d",plocalcodecparameters->宽度,plocalcodecparameters->高度) }}否则if(plocalcodecparameters-> codec_type == avmedia_type_audio){printf("音频编解码器:%d频道,样品率%d",plocalcodecparameters-> plocalcodecparameters-> sample_rate); } //常规printf(" \ tcodec%s id%dit_rate%lld" plocalcodec-> long_name,plocalcodec-> id,plocalcodecparameters-> bit_rate);

使用编解码器,我们可以为avcodeccontext分配内存,这将为我们的解码/编码过程保持上下文,但是我们需要使用编解码器参数填充此编解码器上下文;我们用avcodec_parameters_to_context这样做。

填写编解码器上下文后,我们需要打开编解码器。我们调用函数avcodec_open2,然后我们可以使用它。

现在我们将从流中读取数据包并将它们解码为帧,但首先,我们需要为两个组件,avpacket和avframe分配内存。

让' s从具有数据包的函数av_read_frame中从流中馈送我们的数据包。

使用函数avcodec_send_packet,将原始数据包(压缩帧)发送到解码器,通过编解码器上下文发送到解码器。

使用函数avcodec_receive_frame,通过相同的编解码器上下文从解码器接收来自解码器的原始数据帧(未压缩帧)。

printf("帧%c(%d)pts%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);

最后,我们可以将解码的帧保存到简单的灰色图像中。这个过程非常简单,我们' ll使用pframe->索引与平面y,cb和cr相关的数据,我们刚刚选择0(y)以保存我们的灰色图像。

save_gray_frame(pframe->数据[0],pframe-> Lineize [0],Pframe->宽度,pframe->高度,Frame_FileName);静态void save_gray_frame(无符号char * buf,int wrap,int xsize,int ysize,char * filename){file * f; INT I; f = fopen(文件名," w"); //以PGM文件格式编写最小所需的标题// Portable GrayMap格式 - > 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 *包装,1,xsize,f); fclose(f);}

在我们转到代码之前,转码示例让'谈论时间,或者视频播放器如何知道播放帧的正确时间。

当我们设计一个视频播放器时,我们需要在给定的速度下播放每个帧,否则会很难看到视频,因为它' s如此迅速或慢。

因此,我们需要介绍一些逻辑以平稳地播放每个帧。如此,每个帧具有呈现时间戳(PTS),该时间戳(PTS)是在时基中的越来越多的因子,其是由帧速率(FPS)可分割的Rational数(其中称为时间尺寸)。

对于FPS = 60/1和TimeBase = 1/60000,每个PTS将增加时间尺寸/ FPS = 1000因此,每个帧的PTS实时可能(假设它开始于0):

对于FPS = 25/1和TimeBase = 1/75,每个PTS将增加时间尺度/ FPS = 3并且PTS时间可能是:

现在使用PTS_Time,我们可以找到一种方法来将此与音频PTS_Time或系统时钟同步。 ffmpeg libav通过其API提供这些信息:

刚出于好奇,我们保存的框架以DTS订单(帧:1,6,4,2,3,5)发送,但以PTS订单(框架:1,2,3,4,5)播放。此外,请注意与P或I帧相比,B帧有多便宜。

日志:avstream-> r_frame_rate 60 / 1log:avstream-> time_base 1/60000 ...日志:框架1(类型= i,size = 153797字节)pts 6000 key_frame 1 [dts 0]日志:框架2(类型= B,大小= 8117字节)PTS 7000键键0 [DTS 3]日志:帧3(类型= B,大小= 8226字节)PTS 8000键_Frame 0 [DTS 4]日志:第4帧(类型= B,size = 17699字节)PTS 9000 KEY_FRAME 0 [DTS 2]日志:第5帧(类型= B,大小= 6253字节)PTS 10000 KEY_FRAME 0 [DTS 5]日志:第6帧(类型= P,大小= 34992字节)PTS 11000键_FRAME 0 [DTS 1]

Remuxing是从一种格式(容器)转换为另一个格式(容器)的行为,例如,我们可以使用ffmpeg疼痛将MPEG-4视频更改为MPEG-TS: 它' ll demux mp4,但它赢得了' t解码或编码它(-c copy)和结束,它'将mux in'将它置于mpegts文件中。 如果您不提供格式 - FFMPEG将尝试基于文件&#39的扩展名。 协议层 - 它接受输入(例如,文件,但它也可能是RTMP或HTTP输入) 像素层 - 它也可以将一些过滤器应用于原始帧(如调整大小)可选 协议层 - 并且最后将Muxed数据发送到输出(另一个文件或网络远程服务器) 现在让' s代码使用libav提供与ffmpeg input.mp4 -c复制输出中相同的效果。 我们'重新从输入(input_format_context)读取并将其更改为另一个输出(output_format_context)。

我们开始执行通常分配内存并打开输入格式。对于此特定情况,我们'重新打开输入文件并为输出文件分配内存。

if((ret = avformat_open_input(& input_format_context,in_filename,null,null))< 0){fprintf(stderr,"无法打开输入文件'%s'&#34 ;, in_filename);转到结束;如果((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 = verror_unknown;转到结束;}

我们'重新remux只能remux的reglys的流,所以我们'重新举行什么流我们' ll使用索引数组。

就在我们分配所需的内存之后,我们'重新循环在整个流中,每个我们需要使用avformat_new_stream函数创建新的输出格式上下文。请注意,我们标记aren' t视频,音频或字幕的所有流,所以我们可以跳过它们。

for(i = 0; i&lt; input_format_context-&gt; nb_streams; i ++){avstream * out_stream; avstream * in_stream = input_format_context-&gt;溪流[i]; avcodecparameters * in_codecpar = in_stream-&gt; Codecpar; if(in_codecpar-&gt; codec_type!= avmedia_type_audio&amp;&amp; in_codecpar-&gt; codec_type!= avmedia_type_video&amp;&amp; in_codecpar-&gt; codec_type!= avmedia_type_subtitle){streams_list [i] = - 1;继续; streams_list [i] = stream_index ++; OUT_STREAM = AVFORMAT_NEW_STREAM(output_format_context,null); if(!out_stream){fprintf(stderr,&#34;失败的分配输出流\ n&#34;); ret = verror_unknown;转到结束; } ret = avcodec_parameters_copy(out_stream-&gt; codecpar,in_codecpar); if(ret <0){fprintf(stderr,&#34;未能复制编解码器参数\ n&#34;);转到结束; }}

if(!(output_format_context-&gt; format-&gt; flags&amp; avfmt_nofile)){ret = avio_open(&amp; output_format_context-&gt; pb,out_filename,avio_flag_write); if(ret&lt; 0){fprintf(stderr,&#34;无法打开输出文件&#39;%s&#39;&#34;,out_filename);转到结束; RET = avformat_write_header(output_format_context,null); if(ret&lt; 0){fprintf(stderr,&#34;打开输出文件时发生错误\ n&#34;);转到结束;}

之后,我们可以从我们的输出流中的输入中复制流,数据包。我们&#39; LL循环在它具有数据包(AV_Read_Frame)时,对于每个数据包,我们需要重新计算PTS和DTS,以最终将其写入(AV_Interleaved_Write_Frite_Frame)到我们的输出格式上下文。

虽然(1){avstream * in_stream,* out_stream; RET = AV_READ_FRAME(INPUT_FORMAT_CONTEXT,&amp;数据包); if(et&lt; 0)破裂; in_stream = input_format_context-&gt; 流[数据包。 stream_index]; if(数据包。stream_index&gt; = number_of_streams || streams_list [数据包。stall_index]&lt; 0){av_packet_unref(&amp; packet); 继续; }数据包。 stream_index = streams_list [数据包。 stream_index]; OUT_STREAM = OUTPUT_FORMAT_CONTEXT-&gt; 流[数据包。 stream_index]; / *复制数据包* /包。 pts = av_rescale_q_r. ......