第三章 FFmpeg的介绍与使用
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第三章 FFmpeg的介绍与使用相关的知识,希望对你有一定的参考价值。
参考技术AFFmpeg名称中的mpeg来自视频编码标准MPEG,而前缀FF是Fast Forward的首字母缩写。
目录
默认的编译会生成 4 个可执行文件和 8 个静态库。可执行文件包括用于 转码 、 推流 、Dump媒体文件的 ffmpeg 、用于播放媒体文件的 ffplay 、 用于获取媒体文件信息的 ffprobe ,以及作为简单流媒体服务器的 ffserver 。
8个静态库其实就是FFmpeg的8个模块,具体包括如下内容。
比如AAC编码,常见的有两种封装格式
AAC 的 bit stream filter 常常应用在 编码 的过程中。
与音频的AAC编码格式相对应的是视频中的 H264编码 ,它也有两种封装格式
FFmpeg中也提供了对应的 bit stream filter ,称 H264_mp4toannexb ,可以将MP4封装格式的H264数据包转换为annexb封装格式的H264数据 (其实就是裸的H264的数据)包。
H264 的 bit stream filter 常常应用于视频解码过程中。
ffmpeg 是进行媒体文件转码的命令行工具
ffprobe 是用于查看媒体 文件头信息的工具
ffplay 则是用于播放媒体文件的工具
1.首先用ffprobe查看一个音频的文件
2.输出格式信息format_name、时间长度duration、文件 大小size、比特率bit_rate、流的数目nb_streams等。
3.以JSON格式的形式输出具体每一个流 最详细 的信息
4.显示帧信息的命令如下:
5.查看包信息的命令如下:
ffplay是以FFmpeg框架为基础,外加渲染音视频 的库libSDL来构建的媒体文件播放器。
业界内开源的 ijkPlayer 其实就是基于 ffplay 进行改造的播放器,当然其做了硬件解码以及很多兼容性的工作。
在 ffplay中音画同步的实现方式其实有三种。分别是
并且在 ffplay 中默认的对齐方式也是以 音频 为基准进行对齐的。
首先要声明的是,播放器接收到的视频帧或者音频帧,内部都会有 时间戳(PTS时钟) 来标识它实际应该在什么时刻进行展示。
实际的对齐策略如下:比较视频当前的播放时间和音频当前的播放时间
关键就在于音视频时间的比较以及延迟的计算,当然在比较的过程中会设 置一个 阈值(Threshold) ,若超过预设的阈值就应该做调整(丢帧渲染 或者重复渲染),这就是整个对齐策略。
ffmpeg 就是强大的媒体文件转换工具。它可以转换任何格式的媒体文件,并且还可以用自己的 AudioFilter 以及 VideoFilter 进行处理和编辑。
接下来介绍一个解码的实例,该实例实现的功能非常单一,就是把一个视频文件解码成单独的音频PCM文件和视频YUV文件。
AVFormatContext是API层直接接触到的结构体,它会进行格式的封 装与解封装。
该结构体包含的就是与实际的 编解码 有关的部分。
3.3.1 av_register_all
所以该函数的内部实现会先调用 avcodec_register_all 来注册所有config.h里面开放的编解码器,然后会注册所有的 Muxer 和 Demuxer (也就是封装格式),最后注册所有的 Protocol (即协议层的东西)。
3.3.2 av_find_codec
这里面其实包含了两部分的内容:一部分是寻找 解码器 ,一部分是寻找 编码器 。
3.3.3 avcodec_open2
该函数是打开编解码器(Codec)的函数,无论是编码过程还是解码过程,都会用到该函数。
avformat_open_input
根据所提供的文件路径判断文件的格 式,其实就是通过这一步来决定使用的到底是哪一个 Demuxer 。
avformat_find_stream_info
该方法的作用就是把所有 Stream 的 MetaData 信息填充好。
av_read_frame
使用该方法读取出来的数据是 AVPacket 。
对于 音频流 ,一个 AVPacket 可能包含 多 个 AVFrame ,但是对于 视频流 ,一个 AVPacket 只包含 一 个 AVFrame ,该函数最终只会返回一个 AVPacket 结构体。
avcodec_decode
该方法包含了两部分内容:一部分是 解码视频 ,一部分是 解码音频 , 解码 是会委托给对应的解码器来实施的。
avformat_close_input
该函数负责释放对应的资源。
avformat_alloc_output_context2
该函数内部需要调用方法avformat_alloc_context来分配一个 AVFormatContext 结构体。
avio_open2
编码的阶段了,开发者需要将手动封装好的 AVFrame 结构体,作为 avcodec_encode_video 方法的输入,将其编码成为 AVPacket ,然后调用 av_write_frame 方法输出到媒体文件中。
本文参考 音视频开发进阶指南
项目源码地址 - FFmpegDecoder
FFmpeg的帧
之前小程介绍了FFmpeg的一些应用,并没有涉及到FFmpeg本身的结构或流程,都只是把FFmpeg当作一个黑盒子来使用。
就“能用即可”的角度,能把FFmpeg这个黑盒子用好,就已经是很好的成绩了。
但追求理解甚至完善FFmpeg的读者,应该会关心FFmpeg本身的结构与处理流程。
于是,小程准备用几篇文章来介绍FFmpeg的结构与流程。
在介绍过程中,小程尽量引用具体的数值,让读者对结构有个直观的感知。为了拿到具体的数据,需要调试FFmpeg的代码,这部分的内容(包括gdb的使用)小程已经在前面的章节介绍过了,读者可以关注“广州小程”微信公众号,并找到相应的菜单进行查阅。
本文介绍FFmpeg的帧的结构。
FFmpeg的“帧”涉及到两个结构,即AVPacket,以及AVFrame。
(一)AVPacket
AVPacket,是压缩数据的结构体,也就是解码前或编码后的数据的载体。
为了查看AVPacket结构体的变量的值,小程写了一段调用FFmpeg的代码:
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
void show_frame(const char* filepath) {
av_register_all();
av_log_set_level(AV_LOG_DEBUG);
AVFormatContext* formatContext = avformat_alloc_context();
int status = 0;
int success = 0;
int videostreamidx = -1;
AVCodecContext* codecContext = NULL;
status = avformat_open_input(&formatContext, filepath, NULL, NULL);
if (status == 0) {
status = avformat_find_stream_info(formatContext, NULL);
if (status >= 0) {
for (int i = 0; i < formatContext->nb_streams; i ++) {
if (formatContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
videostreamidx = i;
break;
}
}
if (videostreamidx > -1) {
AVStream* avstream = formatContext->streams[videostreamidx];
codecContext = avstream->codec;
AVCodec* codec = avcodec_find_decoder(codecContext->codec_id);
if (codec) {
status = avcodec_open2(codecContext, codec, NULL);
if (status == 0) {
success = 1;
}
}
}
}
else {
av_log(NULL, AV_LOG_DEBUG, "avformat_find_stream_info error\n");
}
if (success) {
av_dump_format(formatContext, 0, filepath, 0);
int gotframe = 0;
AVFrame* frame = av_frame_alloc();
int decodelen = 0;
int limitcount = 10;
int pcindex = 0;
while (pcindex < limitcount) {
AVPacket packet;
av_init_packet( &packet );
status = av_read_frame(formatContext, &packet);
if (status < 0) {
if (status == AVERROR_EOF) {
av_log(NULL, AV_LOG_DEBUG, "read end for file\n");
}
else {
av_log(NULL, AV_LOG_DEBUG, "av_read_frame error\n");
}
av_packet_unref(&packet);
break;
}
else {
if (packet.stream_index == videostreamidx) {
decodelen = avcodec_decode_video2(codecContext, frame, &gotframe, &packet);
if (decodelen > 0 && gotframe) {
av_log(NULL, AV_LOG_DEBUG, "got one avframe, pcindex=%d\n", pcindex);
}
}
}
av_packet_unref(&packet);
pcindex ++;
}
av_frame_free(&frame);
}
avformat_close_input(&formatContext);
}
avformat_free_context(formatContext);
}
int main(int argc, char *argv[])
{
show_frame("moments.mp4");
return 0;
}
从上面的代码可以看到,调用av_read_frame可以取得一个AVPacket,所以在调用这个函数的地方下个断点,看一下AVPacet长什么样子。
小程先说一下怎么编译上面这段代码。小程使用的是mac电脑。
把上面的代码保存成show_frame.c文件,然后写一个makefile编译脚本(保存成makefile文件,与show_frame.c在同一目录),内容如下:
exe=showframe
srcs=show_frame.c
$(exe):$(srcs)
gcc -o $(exe) $(srcs) -Iffmpeg/include/ -Lffmpeg -lffmpeg -liconv -lz -g
clean:
rm -f $(exe) *.o
整个项目的代码目录结构是这样的:
注意,上图的show_avcodec.c要换成show_frame.c,小程沿用了另一个例子的截图,并且没有更改:)。
另外,FFmpeg是事先就编译了的,对于FFmpeg的编译,小程在前面有介绍。
准备好环境好,就可以编译这段代码了:
make
编译代码后,使用gdb启动调试:
gdb showframe
b 38
r
单步调试时,可以看到,在没有调用av_read_frame前,AVPacket中的变量值:
调用av_read_frame后,AVPacket中的变量:
再一次av_read_frame后:
av_read_frame后,AVPacket也可能没有数据:
小程简单介绍下AVPacket中变量的含义。
AVPacket是压缩数据,一个AVPacket,最多包含一帧视频(NALU)数据,但可以包括多帧音频数据。
AVPacket中的变量含义:
pts/dts,显示/解码时间戵,以packet所在的流的time_base为单位。
stream_index,所在流的索引。
data,avpacket拥有的数据。
size,avpacket的数据长度。
duration,avpacket的时长,同样以time_base为单位。
AVPacket结构,在libavcodec/avcodec.h中定义,读者可以详细看下这个头文件的说明。
(二)AVFrame
AVFrame,是原始数据的结构体,也就是解码后或编码前的数据的载体。
可以简单理解为,AVFrame就是原始的音频或视频数据。有了它,可以做一些处理,比如音效或图效处理、特征提取、特征图绘制,等等。
为了看AVFrame的数据,使用调试AVPacket的代码即可,部分代码如截图:
然后在avcodec_decode_video2的调用处下个断点,使用gdb进行单步调试。
可以看到,在解码前,avframe是这样的:
解码后,并且保证有解码到一帧数据时,avframe是这样的:
以下对AVFrame的一些变量作一些解释:
data,指针数组(最多8个指针),每个指针指向不同维度的byte数据,比如对于视频来说,如果是planar的,则data[0]可能指向Y维度的数据,data[1]可能指向U维度的数据。对于音频来说,如果声道是平面组织的(planar),则data[0]指向一个声道,data[1]指向另一个声道...;如果声音是打包形式的(packed,即左右不分开),则只有data[0]。
linesize,长度的数组。对于视频,如果是planar数据,则linesize[i]是某个维度的一行的长度;如果是packet数据,则只有linesize[0],而且表示所有数据的长度。对于音频,只有linesize[0]可用;如果是planar数据,则linesize[0]对应data[i]的长度(每个data[i]是一样的长度);如果是packed数据,则linesize[0]表示data[0]的长度。对于音频,没有“一行”的概念,linesize[0]表示的是整个长度。对于视频,注意linesize[i]表示一行的长度时,可能比实际的数据的长度(宽)要大。
extern_data,对于视频,等同于data。对于音频,经常用于packed数据。
width/height,视频宽高。
nb_samples,一个声道的样本数。
format,视频的颜色空间,或音频的样本格式。
key_frame,是否为关键帧。
pict_type,视频帧的类型(ipb帧等)。
sample_aspect_ratio,宽高比例。
pts,表现时间戵。
pkt_pts,对应的AVPacket的pts。
quality,质量系数。
sample_rate,音频采样率。
channels,声道数。
AVFrame结构,在libavutil/frame.h中定义,读者可以详细阅读里面的说明。
至此,FFmpeg的帧结构就介绍完毕了。
总结一下,本文介绍了FFmpeg的帧的结构,包括AVFrame与AVPacket,并且通过调试查看了结构中变量的值的变化。
以上是关于第三章 FFmpeg的介绍与使用的主要内容,如果未能解决你的问题,请参考以下文章
[ffmpeg 扩展第三方库编译系列] 关于libopenjpeg mingw32编译问题