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盒子。

ffmpeg architecture(中)的更多相关文章

  1. ffmpeg architecture(下)

    ffmpeg architecture(下) 第3章-转码 TLDR:给我看代码和执行. $ make run_transcoding 我们将跳过一些细节,但是请放心:源代码可在github上找到. ...

  2. ffmpeg architecture(上)

    ffmpeg architecture(上) 目录 介绍 视频-您看到的是什么! 音频-您在听什么! 编解码器-缩小数据 容器-音频和视频的舒适场所 FFmpeg-命令行 FFmpeg命令行工具101 ...

  3. FFmpeg: FFmepg中的sws_scale() 函数分析

    FFmpeg中的 sws_scale() 函数主要是用来做视频像素格式和分辨率的转换,其优势在于:可以在同一个函数里实现:1.图像色彩空间转换, 2:分辨率缩放,3:前后图像滤波处理.不足之处在于:效 ...

  4. ffmpeg 频中分离 video audio 截取片断

    1.获取视频的信息    ffmpeg -i video.avi 2,将图片序列分解合成视频    ffmpeg -i src.mpg image%d.jpg ffmpeg -f image2 -i ...

  5. ffmpeg xcode 中的使用

    最近比较闲,苦于ios设备上没有直接播放torrent 文件的软件,开始折腾了.找了不少资料有了思路.但是其中用到了ffmpeg 这个东西. ffmpeg 是通用的一个视频解决框架,用C语言编写,通用 ...

  6. ffmpeg的中文文档

    1. 概要 ffmpeg [global_options] {[input_file_options] -i INPUT_FILE} ... {[output_file_options] OUTPUT ...

  7. FFmpeg——AVFrame中 的 data

    AVFrame中 的 data 的定义如下: typedef struct AVFrame { #define AV_NUM_DATA_POINTERS 8 /** * pointer to the ...

  8. ffmpeg编码中的二阻塞一延迟

    1. avformat_find_stream_info接口延迟 不论是减少预读的数据量,还是设置flag不写缓存,我这边都不实用,前者有风险,后者会丢帧,可能我还没找到好姿势,记录在此,参考:htt ...

  9. ffmpeg中swscale 的用法

    移植ffmpeg过程中,遇到swscale的用法问题,所以查到这篇文章.文章虽然已经过去很长时间,但是还有颇多可以借鉴之处.谢谢“咕咕鐘". 转自:http://guguclock.blog ...

随机推荐

  1. 书评第001篇:《C++黑客编程揭秘与防范》

    本书基本信息 作者:冀云(编著) 出版社:人民邮电出版社 出版时间:2012-6-1 ISBN:9787115280640 版次:1 页数:265 字数:406000 印刷时间:2012-6-1 开本 ...

  2. LA3989女士的选择

    题意:       给你n个男士n个女士,然后给你每个男士中女士的排名,和每个女士中每个男士在他们心中的排名,问你是否可以组成稳定的舞伴,如果存在以下情况(1)男生u和女生v不是舞伴,他们喜欢对方的程 ...

  3. android中Stub Proxy答疑

    在上篇添加账户源码解析的博文中,我们发现功能是由AccountManager的mService成员来实现.而mService其实是AccountManagerService,如果对android系统有 ...

  4. 什么?这么精髓的View的Measure流程源码全解析,你确定不看看?

    前言 Android开发中我们平时接触最多的是各种View, View是一个比较大的体系,包含了绘制流程.事件分发.各种动画.自定义View 等等.前几天我写了一篇事件分发源码解析的文章, 今天我们来 ...

  5. 【pytest系列】- pytest测试框架介绍与运行

    如果想从头学起pytest,可以去看看这个系列的文章! https://www.cnblogs.com/miki-peng/category/1960108.html 前言​ ​ 目前有两种纯测试的测 ...

  6. 每天一道面试题LeetCode 80--删除排序数组中的重复项 II(python实现)

    LeetCode 80--删除排序数组中的重复项 II 给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素最多出现两次,返回移除后数组的新长度. 不要使用额外的数组空间,你必须在原地修改输 ...

  7. Java发送邮件报错:com.sun.mail.util.LineOutputStream.<init>(Ljava/io/OutputStream;Z)V

    在练习使用Java程序发送邮件的代码 运行出现了com.sun.mail.util.LineOutputStream.<init>(Ljava/io/OutputStream;Z)V报错信 ...

  8. 一文读懂 SuperEdge 云边隧道

    作者 李腾飞,腾讯容器技术研发工程师,腾讯云TKE后台研发,SuperEdge核心开发成员. 杜杨浩,腾讯云高级工程师,热衷于开源.容器和Kubernetes.目前主要从事镜像仓库,Kubernete ...

  9. 浅尝js垃圾回收机制

    局部作用域内的变量,在函数执行结束之后就会被js的垃圾回收机制销毁   为什么要销毁局部变量? => 为了释放内存   js垃圾回收机制何时会销毁局部变量 : 如果局部变量无法再得到访问,就会被 ...

  10. 关于Annotation注解的理解

    在编Java程序的时候,我们经常会碰到annotation.比如:@Override 我们在子类继承父类的时候,会经常用到这个annotation.它告诉编译器这个方法是override父类的方法的. ...