FFmpeg libav手册


很久以来,我一直在搜寻一本可以使用类似FFmpeg的库libav的书(该的名称代表lib rary a udio v ideo)。找到一本教科书“ 如何编写视频播放器并适合少于一千行。”不幸的是,那里的信息已经过时,所以我不得不自己创建一个手册。

大多数代码将使用C语言,但是请放心:您将轻松理解所有内容,并可以用自己喜欢的语言来应用它。 FFmpeg libav与许多语言(包括Python和Go)有很多绑定。但是,即使您的语言不具有直接兼容性,您仍然可以通过ffi进行附加(这是一个示例,其中包含Lua)。

让我们从视频,音频,编解码器和容器的简短内容入手。然后,我们继续使用FFmpeg命令行进入速成课程,最后编写代码。随意直接进入“学习FFmpeg libav的棘手路径”部分。

有一种观点(不仅是我的观点)认为,流媒体互联网视频已经从传统电视中夺取了接力棒。尽管如此,FFmpeg libav绝对值得探索。

目录


EDISON软件-网络开发
EDISON.

, , .

! ;-)


— , !


如果以给定的频率(例如每秒24张图像)更改图像序列,则会产生运动的错觉。这是视频的主要思想:一系列以给定速度运动的图像(帧)。

1886年插图。

音频就是您所听到的!


尽管无声视频会引起各种各样的感觉,但是添加声音会极大地增加乐趣。

声音是在空气或任何其他传输介质(例如气体,液体或固体)中传播的振动波。

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


编解码器-数据压缩


编解码器是一种压缩或解压缩数字音频/视频的电子电路或软件。它将原始(未压缩的)数字音频/视频转换为压缩格式(反之亦然)。

但是,如果我们决定将数百万个图像打包到一个文件中并称为电影,那么我们可以获得一个巨大的文件。我们来计算

一下假设我们创建了一个分辨率为1080×1920(高×宽)的视频。我们每个像素花费3个字节(屏幕上的最小点)进行颜色编码(24位颜色,这提供了16,777,216种不同的颜色)。该视频以每秒24帧的速度工作,总持续时间为30分钟。

toppf = 1080 * 1920 //    
cpp = 3 //  
tis = 30 * 60 //   
fps = 24 //   

required_storage = tis * fps * toppf * cpp

此视频大约需要250.28 GB的内存,即1.11 Gb / s!这就是为什么您必须使用编解码器。

容器是存储音频/视频的便捷方式


容器(包装器)格式是图元文件格式,其规范描述了各种数据和元数据元素如何在计算机文件中共存。

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

通常,文件格式由其扩展名确定:例如,video.webm最有可能是使用webm容器的视频。


命令行FFmpeg


独立的跨平台解决方案,用于记录,转换和流传输音频/视频。

对于使用多媒体,我们有一个了不起的工具-名为FFmpeg的库即使您没有在程序代码中使用它,您仍然会使用它(您使用的是Chrome吗?)。

该库具有一个控制台程序,用于输入一个名为ffmpeg的命令行(与库本身的名称相反,用小写字母表示)。这是一个简单而强大的二进制文件。例如,您只需输入以下命令即可将mp4转换为avi:

$ ffmpeg -i input.mp4 output.avi

我们只是重新混合 -从一个容器转换为另一个容器。从技术上讲,FFmpeg也可以转码,但稍后会介绍更多。

命令行工具FFmpeg 101


FFmpeg拥有文档,其中所有内容都完美地解释了其工作原理。

从原理上讲,FFmpeg命令行程序希望使用以下参数格式来完成其工作- ffmpeg {1} {2} -i {3} {4} {5}其中:

{1} -全局参数
{2} -输入文件的参数
{3} -传入URL
{4} -输出文件
{5}的参数 -传出URL

部分{2},{3},{4},{5}指定所需数量的参数。使用示例更容易理解传递参数的格式:

警告:按引用的文件重300 MB

$ wget -O bunny_1080p_60fps.mp4 http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_1080p_60fps_normal.mp4

$ ffmpeg \
-y \ #  
-c: libfdk_aac -c: v libx264 \ #  
-i bunny_1080p_60fps.mp4 \ #  URL
-c: v libvpx-vp9 -c: libvorbis \ #  
bunny_1080p_60fps_vp9.webm #  URL

此命令获取包含两个流(使用aac编解码器编码的音频和使用h264编解码器编码的视频)的传入mp4文件,并将其转换为webm,同时还更改音频和视频编解码器。

如果简化上述命令,则应考虑FFmpeg代替您接受默认值。例如,如果您只是键入

ffmpeg -i input.avi output.mp4

它使用哪个音频/视频编解码器创建output.mp4?

Werner Robitz编写了用于使用FFmpeg读取/执行编码和编辑指南

基本视频操作


在处理音频/视频时,我们通常会执行许多与多媒体相关的任务。

转码(转码)




它是什么?将流或音频或视频(或同时转换)从一种编解码器转换到另一种编解码器的过程。文件格式(容器)不变。

为了什么?碰巧某些设备(电视,智能手机,控制台等)不支持音频/视频格式X,但支持音频/视频格式Y。或者,较新的编解码器是可取的,因为它们提供了更好的压缩率。

怎么样?例如,将视频H264(AVC)转换为H265(HEVC):

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-c:v libx265 \
bunny_1080p_60fps_h265.mp4

复用



它是什么?从一种格式(容器)转换为另一种格式。

为了什么?碰巧某些设备(电视,智能手机,控制台等)不支持X文件格式,但支持Y文件格式;或者,较新的容器(与旧的容器不同)提供了现代必需的功能。

怎么样?将mp4转换为webm:

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-c copy \ # just saying to ffmpeg to skip encoding
bunny_1080p_60fps.webm

转换



它是什么?更改数据速率或创建另一个视图。

为了什么?用户既可以在低功率智能手机上的2G网络上观看视频,也可以在4K电视上通过光纤互联网连接观看视频。因此,您应该提供多个选项来播放具有不同数据速率的同一视频。

怎么样?以3856K和2000K之间的比特率播放。

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-minrate 964K -maxrate 3856K -bufsize 2000K \
bunny_1080p_60fps_transrating_964_3856.mp4

通常,转换与重新校准一起完成。Werner Robitz写了另一篇关于速度控制FFmpeg的强制性文章

转换(重新校准)



它是什么?分辨率变化。如上所述,转换通常与转换同时进行。

为了什么?出于与转换相同的原因。

怎么样?将1080的分辨率降低到480:

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-vf scale=480:-1 \
bunny_1080p_60fps_transsizing_480.mp4

奖励:自适应流



它是什么?创建许多权限(比特率)并将媒体分成多个部分,然后通过http协议进行传输。

为了什么?为了提供灵活的多媒体,即使在廉价的智能手机上,甚至在4K等离子上也可以观看,因此可以轻松缩放和部署它(但这会增加延迟)。

怎么样?使用DASH创建响应式WebM:

# video streams
$ 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

# audio streams
$ ffmpeg -i bunny_1080p_60fps.mp4 -c:a libvorbis -b:a 128k -vn -f webm -dash 1 audio_128k.webm

# the 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,streams=5" \
 manifest.mpd

PS:我从使用DASH播放Adaptive WebM说明中提取了此示例

超越


FFmpeg没有其他用途。我将它与iMovie结合使用来创建/编辑一些YouTube视频。而且,当然,没有什么可以阻止您专业地使用它。

学习FFmpeg libav的棘手路径

通过听觉和视觉可以时不时地感到惊奇吗?

生物学家大卫·罗伯·琼斯
FFmpeg作为用于对多媒体文件执行重要操作的命令行工具非常有用。也许它也可以在程序中使用?

FFmpeg由几个库组成,这些库可以集成到我们自己的程序中。通常,当您安装FFmpeg时,所有这些库都会自动安装。我将这些库的集合称为FFmpeg libav

本节的标题是对Zed Shaw的系列《学习棘手之路》的致敬,尤其是他的书《学习C的棘手之路》。

第0章-简单的世界你好


在我们的Hello World中,您真的不会欢迎使用控制台语言的世界。而是打印有关视频的以下信息:格式(容器),持续时间,分辨率,音频通道,最后,解密一些帧并将其保存为图像文件。

FFmpeg libav体系结构


但是在开始编写代码之前,让我们看一下FFmpeg libav架构的总体工作方式以及其组件如何与其他组件交互。

这是视频解码过程的示意图:

首先,将媒体文件加载到名为AVFormatContext的组件中(视频容器也是一种格式)。实际上,它不能完全下载整个文件:通常只读取标头。

下载完我们容器最小标头后,就可以访问其流(它们可以表示为基本音频和视频数据)。每个流都将在AVStream组件中可用

假设我们的视频有两个流:使用AAC编解码器编码的音频和使用H264编解码器AVC编码的视频。我们可以从每个流中提取称为数据包的数据已加载到称为AVPacket的组件中

数据包内部的数据仍被编码(压缩),要解码数据包,我们需要将其传递给特定的AVCodec

AVCodec将它们解码为一个AVFrame,其结果是该组件为我们提供了未压缩的帧。请注意,音频和视频流的术语和过程相同。

要求


由于有时在编译或运行示例时会遇到问题,因此我们将Docker用作开发/运行时环境。我们还将使用带有一只大兔子视频,因此,如果您在本地计算机上没有视频,只需在控制台中运行命令make fetch_small_bunny_video即可

实际上,代码


TLDR 给我看一个可执行代码的例子,兄弟:

$ make run_hello

我们将省略一些细节,但是请不要担心:源代码在github 上可用

我们将为AVFormatContext组件分配内存,该组件将包含有关格式(容器)的信息。

AVFormatContext *pFormatContext = avformat_alloc_context();

现在,我们将打开文件,读取其标题,并使用最少的格式信息填充AVFormatContext(请注意,编解码器通常不会打开)。为此,请使用avformat_open_input函数它需要AVFormatContext,一个文件名和两个可选参数:AVInputFormat(如果传递NULL,则FFmpeg将确定格式)和AVDictionary(这是解复用器选项)。

avformat_open_input(&pFormatContext, filename, NULL, NULL);

您还可以打印格式名称和介质的持续时间:

printf("Format %s, duration %lld us", pFormatContext->iformat->long_name, pFormatContext->duration);

要访问流,我们需要从媒体读取数据。这是通过avformat_find_stream_info函数完成的现在pFormatContext-> nb_streams将包含线程数,而pFormatContext->流[i]将使我们连续执行第i个流(AVStream)。

avformat_find_stream_info(pFormatContext,  NULL);

让我们遍历所有线程的循环:

for(int i = 0; i < pFormatContext->nb_streams; i++) {
  //
}

对于每个流,我们将保存AVCodecParameters,它描述了第i个流使用的编解码器的属性

AVCodecParameters *pLocalCodecParameters = pFormatContext->streams[i]->codecpar;


使用编解码器的属性,我们可以通过请求avcodec_find_decoder函数找到相应的解码器,还可以找到已注册的解码器以获取编解码器标识符,并返回AVCodec-一个知道如何对流进行编码和解码的组件:

AVCodec *pLocalCodec = avcodec_find_decoder(pLocalCodecParameters->codec_id);

现在我们可以打印编解码器信息:

// specific for video and audio
if (pLocalCodecParameters->codec_type == AVMEDIA_TYPE_VIDEO) {
  printf("Video Codec: resolution %d x %d", pLocalCodecParameters->width, pLocalCodecParameters->height);
} else if (pLocalCodecParameters->codec_type == AVMEDIA_TYPE_AUDIO) {
  printf("Audio Codec: %d channels, sample rate %d", pLocalCodecParameters->channels, pLocalCodecParameters->sample_rate);
}
// general
printf("\tCodec %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);

现在,我们将从流中读取数据包并将其解码为帧,但是首先我们需要为两个组件(AVPacketAVFrame分配内存

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);

然后,我们使用avcodec_receive_frame函数通过相同的编解码器上下文从解码器获取原始数据帧(未压缩的帧)

avcodec_receive_frame(pCodecContext, pFrame);

我们可以打印帧号,PTS,DTS,帧类型等:

printf(
    "Frame %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
);

最后,我们可以将解码后的帧保存为简单的灰度图像。这个过程非常简单:我们将使用pFrame-> data,其中索引与颜色空间YCbCr关联只需选择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)
{
    FILE *f;
    int i;
    f = fopen(filename,"w");
    // writing the minimal required header for a pgm file format
    // portable graymap format -> https://en.wikipedia.org/wiki/Netpbm_format#PGM_example
    fprintf(f, "P5\n%d %d\n%d\n", xsize, ysize, 255);

    // writing line by line
    for (i = 0; i < ysize; i++)
        fwrite(buf + i * wrap, 1, xsize, f);
    fclose(f);
}

瞧!现在我们有一个2MB的灰度图像:


第1章-同步音频和视频

参与其中是年轻的JS开发人员编写新的MSE视频播放器。
在开始编写转码代码之前,让我们谈谈同步或视频播放器如何找到合适的时间播放帧。

在上一个示例中,我们保存了几帧:


设计视频播放器时,我们需要以一定的速度播放每一帧,否则,由于播放速度太快或太慢,很难欣赏到视频。

因此,我们需要定义一些逻辑来平滑播放每个帧。在这方面,每一帧具有时间表示标志(PTS -从p resentation IME 小号夯实),这是在可变考虑越来越多的timebase,它是一个有理数(分母称为时间标度-timescale)除以帧频fps)。

通过示例更容易理解。让我们模拟一些场景。

对于fps = 60/1timebase = 1/60000,每个PTS将增加时标/ 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/1timebase = 1/75,每个PTS都会增加时标/ 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)。另请注意,与PI相比,B便宜多少

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 copy output.ts

MP4文件将被多路分解,而该文件将不会被解码或编码(-c copy),最后,我们得到了mpegts文件。如果您未指定-f格式,则ffmpeg将尝试根据文件扩展名猜测它。

FFmpeg或libav的一般用法遵循以下模式/架构或工作流程:

  • 协议级别 -接受输入数据(例如文件,但也可以是rtmp或HTTP下载)
  • — , , ,
  • — (, ),
  • … :
  • — ( )
  • — ( ) ( )
  • — , , ( , , )


(此图强烈地受到LeixiaohuaSlhck的启发)。

现在让我们使用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, "Could not open input file '%s'", in_filename);
  goto end;
}
if ((ret = avformat_find_stream_info(input_format_context, NULL)) < 0) {
  fprintf(stderr, "Failed to retrieve input stream information");
  goto end;
}

avformat_alloc_output_context2(&output_format_context, NULL, NULL, out_filename);
if (!output_format_context) {
  fprintf(stderr, "Could not create output context\n");
  ret = AVERROR_UNKNOWN;
  goto end;
}

我们将仅对视频,音频和字幕流进行多路复用。因此,我们确定将在索引数组中使用哪些流:

number_of_streams = input_format_context->nb_streams;
streams_list = av_mallocz_array(number_of_streams, sizeof(*streams_list));

分配必要的内存后,我们需要立即遍历所有流,并需要针对每个流使用avformat_new_stream函数在输出格式的上下文中创建一个新的输出流请注意,我们标记了所有非视频,音频或字幕的流,以便我们可以跳过它们。

for (i = 0; i < input_format_context->nb_streams; i++) {
  AVStream *out_stream;
  AVStream *in_stream = input_format_context->streams[i];
  AVCodecParameters *in_codecpar = in_stream->codecpar;
  if (in_codecpar->codec_type != AVMEDIA_TYPE_AUDIO &&
      in_codecpar->codec_type != AVMEDIA_TYPE_VIDEO &&
      in_codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE) {
    streams_list[i] = -1;
    continue;
  }
  streams_list[i] = stream_index++;
  out_stream = avformat_new_stream(output_format_context, NULL);
  if (!out_stream) {
    fprintf(stderr, "Failed allocating output stream\n");
    ret = AVERROR_UNKNOWN;
    goto end;
  }
  ret = avcodec_parameters_copy(out_stream->codecpar, in_codecpar);
  if (ret < 0) {
    fprintf(stderr, "Failed to copy codec parameters\n");
    goto end;
  }
}

现在创建输出文件:

if (!(output_format_context->oformat->flags & AVFMT_NOFILE)) {
  ret = avio_open(&output_format_context->pb, out_filename, AVIO_FLAG_WRITE);
  if (ret < 0) {
    fprintf(stderr, "Could not open output file '%s'", out_filename);
    goto end;
  }
}

ret = avformat_write_header(output_format_context, NULL);
if (ret < 0) {
  fprintf(stderr, "Error occurred when opening output file\n");
  goto end;
}

之后,您可以逐包将流从输入复制到输出流。只要有包(av_read_frame),这种情况就会循环发生,对于每个包,您都需要重新计算PTSDTS才能最终将它们(av_interleaved_write_frame)写入输出格式的上下文中。

while (1) {
  AVStream *in_stream, *out_stream;
  ret = av_read_frame(input_format_context, &packet);
  if (ret < 0)
    break;
  in_stream  = input_format_context->streams[packet.stream_index];
  if (packet.stream_index >= number_of_streams || streams_list[packet.stream_index] < 0) {
    av_packet_unref(&packet);
    continue;
  }
  packet.stream_index = streams_list[packet.stream_index];
  out_stream = output_format_context->streams[packet.stream_index];
  /* copy packet */
  packet.pts = av_rescale_q_rnd(packet.pts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
  packet.dts = av_rescale_q_rnd(packet.dts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
  packet.duration = av_rescale_q(packet.duration, in_stream->time_base, out_stream->time_base);
  // https://ffmpeg.org/doxygen/trunk/structAVPacket.html#ab5793d8195cf4789dfb3913b7a693903
  packet.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, "Error muxing packet\n");
    break;
  }
  av_packet_unref(&packet);
}

要完成此操作,我们需要使用av_write_trailer函数将流预告片写入输出媒体文件

av_write_trailer(output_format_context);

现在我们准备测试代码。第一个测试是将格式(视频容器)从MP4转换为MPEG-TS视频文件。基本上,我们创建命令行ffmpeg input.mp4 -c来使用libav复制output.ts。

make run_remuxing_ts

有用!不相信我 ?!ffprobe检查

ffprobe -i remuxed_small_bunny_1080p_60fps.ts

Input #0, mpegts, from 'remuxed_small_bunny_1080p_60fps.ts':
  Duration: 00:00:10.03, start: 0.000000, bitrate: 2751 kb/s
  Program 1
    Metadata:
      service_name    : Service01
      service_provider: FFmpeg
    Stream #0:0[0x100]: Video: h264 (High) ([27][0][0][0] / 0x001B), yuv420p(progressive), 1920x1080 [SAR 1:1 DAR 16:9], 60 fps, 60 tbr, 90k tbn, 120 tbc
    Stream #0:1[0x101]: Audio: ac3 ([129][0][0][0] / 0x0081), 48000 Hz, 5.1(side), fltp, 320 kb/s

总结一下我们所做的事情,现在我们可以回到关于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文件:

make run_remuxing_fragmented_mp4

为确保一切正常,您可以使用令人惊叹的gpac / mp4box.js工具网站或http://mp4parser.com/来查看差异-首先下载mp4。

如您所见,它有一个不可分割的mdat-这是视频和音频帧所在的位置。现在下载片段化的mp4,以了解它如何扩展mdat块:

第3章-转码


TLDR 给我看代码和执行:

$ make run_transcoding

我们将跳过一些细节,但是请不要担心:源代码在github上找到。

在本章中,我们将创建一个用C编写的极简代码转换器,它可以使用FFmpeg libav库(尤其是libavcodeclibavformatlibavutil)将视频从H264转换为H265


AVFormatContext是媒体文件格式的抽象,即 对于容器(MKV,MP4,Webm,TS),
AVStream代表给定格式的每种数据类型(例如:音频,视频,字幕,元数据)
AVPacket是从AVStream接收的压缩数据的片段,可以使用AVCodec对其进行解码(例如:av1,h264,vp9,hevc)生成称为AVFrame的原始数据

复用


让我们从简单的转换开始,然后加载输入文件。

// Allocate an AVFormatContext
avfc = avformat_alloc_context();
// Open an input stream and read the header.
avformat_open_input(avfc, in_filename, NULL, NULL);
// Read packets of a media file to get stream information.
avformat_find_stream_info(avfc, NULL);

现在配置解码器。AVFormatContext将使我们能够访问AVStream的所有组件,对于每个组件,我们都可以获取其AVCodec并创建特定的AVCodecContext。最后,我们可以打开此编解码器进行解码。

AVCodecContext包含媒体配置数据,例如数据速率,帧速率,采样率,通道,音高等

for(int i = 0; i < avfc->nb_streams; i++) {
  AVStream *avs = avfc->streams[i];
  AVCodec *avc = avcodec_find_decoder(avs->codecpar->codec_id);
  AVCodecContext *avcc = avcodec_alloc_context3(*avc);
  avcodec_parameters_to_context(*avcc, avs->codecpar);
  avcodec_open2(*avcc, *avc, NULL);
}

您还需要准备输出媒体文件以进行转换。首先,为输出AVFormatContext分配内存以输出格式创建每个流。要正确打包流,请从解码器复制编解码器参数。

设置标志AV_CODEC_FLAG_GLOBAL_HEADER,告诉编码器他可以使用全局头,最后打开输出文件进行写入并保存头:

avformat_alloc_output_context2(&encoder_avfc, NULL, NULL, out_filename);

AVStream *avs = avformat_new_stream(encoder_avfc, NULL);
avcodec_parameters_copy(avs->codecpar, decoder_avs->codecpar);

if (encoder_avfc->oformat->flags & AVFMT_GLOBALHEADER)
  encoder_avfc->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

avio_open(&encoder_avfc->pb, encoder->filename, AVIO_FLAG_WRITE);
avformat_write_header(encoder->avfc, &muxer_opts);

我们从解码器获取AVPacket,调整时间戳并将数据包正确写入输出文件。尽管av_interleaved_write_frame函数报告“ write frame ”,我们还是保存了程序包。我们通过将流预告片写入文件来完成排列过程。

AVFrame *input_frame = av_frame_alloc();
AVPacket *input_packet = av_packet_alloc();

while(av_read_frame(decoder_avfc, input_packet) >= 0) {
  av_packet_rescale_ts(input_packet, decoder_video_avs->time_base, encoder_video_avs->time_base);
  av_interleaved_write_frame(*avfc, input_packet) < 0));
}

av_write_trailer(encoder_avfc);

转码


在上一节中,有一个简单的转换程序,现在我们将添加对文件进行编码的功能,尤其是将视频从h264转换h265的功能

准备好解码器之后,但在组织输出媒体文件之前,请配置编码器。

  • 创建视频AV流的编码器avformat_new_stream
  • 我们使用avcodec中的名称libx265avcodec_find_encoder_by_name
  • 创建AVCodecContext基础上,创建avcodec_alloc_context3编解码器
  • 设置代码转换会话的基本属性并...
  • ...打开编解码器并将参数从上下文复制到流(avcodec_open2avcodec_parameters_from_context)。

AVRational input_framerate = av_guess_frame_rate(decoder_avfc, decoder_video_avs, NULL);
AVStream *video_avs = avformat_new_stream(encoder_avfc, NULL);

char *codec_name = "libx265";
char *codec_priv_key = "x265-params";
// we're going to use internal options for the x265
// it disables the scene change detection and fix then
// GOP on 60 frames.
char *codec_priv_value = "keyint=60:min-keyint=60:scenecut=0";

AVCodec *video_avc = avcodec_find_encoder_by_name(codec_name);
AVCodecContext *video_avcc = avcodec_alloc_context3(video_avc);
// encoder codec params
av_opt_set(sc->video_avcc->priv_data, codec_priv_key, codec_priv_value, 0);
video_avcc->height = decoder_ctx->height;
video_avcc->width = decoder_ctx->width;
video_avcc->pix_fmt = video_avc->pix_fmts[0];
// control rate
video_avcc->bit_rate = 2 * 1000 * 1000;
video_avcc->rc_buffer_size = 4 * 1000 * 1000;
video_avcc->rc_max_rate = 2 * 1000 * 1000;
video_avcc->rc_min_rate = 2.5 * 1000 * 1000;
// time base
video_avcc->time_base = av_inv_q(input_framerate);
video_avs->time_base = sc->video_avcc->time_base;

avcodec_open2(sc->video_avcc, sc->video_avc, NULL);
avcodec_parameters_from_context(sc->video_avs->codecpar, sc->video_avcc);

必须扩展解码周期以对视频流进行转码:

  • 我们将一个空的AVPacket发送解码器(avcodec_send_packet)。
  • 获取未压缩的AVFrameavcodec_receive_frame)。
  • 我们开始重新编码原始帧。
  • 我们发送原始帧(avcodec_send_frame)。
  • 我们基于AVPacket编解码器avcodec_receive_packet)进行压缩
  • 设置时间戳(av_packet_rescale_ts)。
  • 我们写入输出文件(av_interleaved_write_frame)。

AVFrame *input_frame = av_frame_alloc();
AVPacket *input_packet = av_packet_alloc();

while (av_read_frame(decoder_avfc, input_packet) >= 0)
{
  int response = avcodec_send_packet(decoder_video_avcc, input_packet);
  while (response >= 0) {
    response = avcodec_receive_frame(decoder_video_avcc, input_frame);
    if (response == AVERROR(EAGAIN) || response == AVERROR_EOF) {
      break;
    } else if (response < 0) {
      return response;
    }
    if (response >= 0) {
      encode(encoder_avfc, decoder_video_avs, encoder_video_avs, decoder_video_avcc, input_packet->stream_index);
    }
    av_frame_unref(input_frame);
  }
  av_packet_unref(input_packet);
}
av_write_trailer(encoder_avfc);

// used function
int encode(AVFormatContext *avfc, AVStream *dec_video_avs, AVStream *enc_video_avs, AVCodecContext video_avcc int index) {
  AVPacket *output_packet = av_packet_alloc();
  int response = avcodec_send_frame(video_avcc, input_frame);

  while (response >= 0) {
    response = avcodec_receive_packet(video_avcc, output_packet);
    if (response == AVERROR(EAGAIN) || response == AVERROR_EOF) {
      break;
    } else if (response < 0) {
      return -1;
    }

    output_packet->stream_index = index;
    output_packet->duration = enc_video_avs->time_base.den / enc_video_avs->time_base.num / dec_video_avs->avg_frame_rate.num * dec_video_avs->avg_frame_rate.den;

    av_packet_rescale_ts(output_packet, dec_video_avs->time_base, enc_video_avs->time_base);
    response = av_interleaved_write_frame(avfc, output_packet);
  }
  av_packet_unref(output_packet);
  av_packet_free(&output_packet);
  return 0;
}

我们将媒体流从h264转换h265正如预期的那样,h265媒体文件的版本小于h264,而该程序有很多机会:

  /*
   * H264 -> H265
   * Audio -> remuxed (untouched)
   * MP4 - MP4
   */
  StreamingParams sp = {0};
  sp.copy_audio = 1;
  sp.copy_video = 0;
  sp.video_codec = "libx265";
  sp.codec_priv_key = "x265-params";
  sp.codec_priv_value = "keyint=60:min-keyint=60:scenecut=0";

  /*
   * H264 -> H264 (fixed gop)
   * Audio -> remuxed (untouched)
   * MP4 - MP4
   */
  StreamingParams sp = {0};
  sp.copy_audio = 1;
  sp.copy_video = 0;
  sp.video_codec = "libx264";
  sp.codec_priv_key = "x264-params";
  sp.codec_priv_value = "keyint=60:min-keyint=60:scenecut=0:force-cfr=1";

  /*
   * H264 -> H264 (fixed gop)
   * Audio -> remuxed (untouched)
   * MP4 - fragmented MP4
   */
  StreamingParams sp = {0};
  sp.copy_audio = 1;
  sp.copy_video = 0;
  sp.video_codec = "libx264";
  sp.codec_priv_key = "x264-params";
  sp.codec_priv_value = "keyint=60:min-keyint=60:scenecut=0:force-cfr=1";
  sp.muxer_opt_key = "movflags";
  sp.muxer_opt_value = "frag_keyframe+empty_moov+default_base_moof";

  /*
   * H264 -> H264 (fixed gop)
   * Audio -> AAC
   * MP4 - MPEG-TS
   */
  StreamingParams sp = {0};
  sp.copy_audio = 0;
  sp.copy_video = 0;
  sp.video_codec = "libx264";
  sp.codec_priv_key = "x264-params";
  sp.codec_priv_value = "keyint=60:min-keyint=60:scenecut=0:force-cfr=1";
  sp.audio_codec = "aac";
  sp.output_extension = ".ts";

  /* WIP :P  -> it's not playing on VLC, the final bit rate is huge
   * H264 -> VP9
   * Audio -> Vorbis
   * MP4 - WebM
   */
  //StreamingParams sp = {0};
  //sp.copy_audio = 0;
  //sp.copy_video = 0;
  //sp.video_codec = "libvpx-vp9";
  //sp.audio_codec = "libvorbis";
  //sp.output_extension = ".webm";

坦白地说,它比起初看起来要复杂一些。我必须选择FFmpeg命令行源代码并进行了大量测试。可能我错过了某些地方,因为我必须h264使用force-cfr,并且仍然弹出一些警告消息,例如,帧类型(5)被强制更改为帧类型(3)。

爱迪生博客上的翻译:


All Articles