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

现在,我们打算从流中读取数据包,并将其解码为帧,但首先,我们需要为这两个组件的分配内存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);

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

avcodec_receive_frame(pCodecContext,pFrame);

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

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/1timebase=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/1timebase=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提供以下信息:

出于好奇,我们保存的帧以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的一般用法遵循模式/体系结构或工作流程:

  • 协议层 -接受inputfile例如,但也可以是rtmpHTTP输入)
  • 格式层 -它demuxes的内容,主要显示元数据及其流
  • 编解码器层 -decodes压缩流数据可选
  • 像素层 -也可以将其应用于filters原始帧(如调整大小)可选
  • 然后它做反向路径
  • 编解码器层 -它encodes(或re-encodes什至transcodes)原始帧是可选的
  • 格式层 -它muxes(或remuxes)原始流(压缩数据)
  • 协议层 -最终将多路复用的数据发送到output(另一个文件或网络远程服务器)

此图受到雷小华Slhck的作品的强烈启发。

现在,让我们使用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盒子。

 

posted @ 2020-07-13 06:56  吴建明wujianming  阅读(277)  评论(0编辑  收藏  举报