本篇简单介绍下,如何从输入多媒体中读取或者写入一帧数据。由于我们并没有进行编解码操作,这里的读写操作都是编码后的数据。在 FFmpeg 中每一帧数据是由 AVPacket 来表示。读操作需要用到的函数有:
- avformat_open_input
- av_packet_alloc
- av_read_frame
- av_packet_unref
- avformat_close_input
写操作需要用到的函数有:
- avformat_alloc_output_context2
- avformat_new_stream
- av_interleaved_write_frame
1. AVPacket 读操作
从多媒体文件中读取一帧数据的过程如下:
- 首先,使用 av_packet_alloc 函数初始化一个 AVPacket 对象,用于存储读取到的数据
- 接着,调用 av_read_frame 从多媒体文件中读取一帧数据,存储到 AVPacket 对象中
- 然后,由于多媒体文件中存在多帧数据,我们肯定需要循环来读取,为了能够复用 AVPacket 对象,我们需要每次处理完帧数据之后,就需要使用 av_packet_unref 函数对 AVPacket 对象进行重置
- 最后,当所有帧数据都读取完毕之后,调用 avformat_close_input 函数释放 APacket 的内存
示例代码:
#include <iostream> extern "C" { #include <libavformat/avformat.h> } void test() { AVFormatContext* pFormatContex = nullptr; avformat_open_input(&pFormatContex, "demo.mp4", nullptr, nullptr); avformat_find_stream_info(pFormatContex, nullptr); /*******************************************************/ // 初始化 AVPacket 对象 AVPacket* packet = av_packet_alloc(); int ret = -1; while (true) { // return 0 if OK, < 0 on error or end of file. ret = av_read_frame(pFormatContex, packet); if (ret < 0) { fprintf(stderr, "av_read_frame error!\n"); break; } // 表示当前帧为视频流数据 if (packet->stream_index == AVMEDIA_TYPE_VIDEO) { printf("video ==> pts: %9lld dts: %9lld pos: %9lld size: %8d stream_index: %8d\n", packet->pts, // 多媒体帧的显示时间戳 packet->dts, // 多媒体帧的解码时间戳 packet->pos, // 数据在文件中的位置(字节) packet->size, // 当前数据帧的大小 packet->stream_index); // 当前是音频流、视频流、字母流的数据帧的标识 } // 表示当前帧为音频流数据 if (packet->stream_index == AVMEDIA_TYPE_AUDIO) { printf("audio ==> pts: %9lld dts: %9lld pos: %9lld size: %8d stream_index: %8d\n", packet->pts, // 多媒体帧的显示时间戳 packet->dts, // 多媒体帧的解码时间戳 packet->pos, // 数据在文件中的位置(字节) packet->size, // 当前数据帧的大小 packet->stream_index); // 当前是音频流、视频流、字母流的数据帧的标识 } // 取消对数据的引用,并将其余字段重置为默认值,使得 packet 复用 av_packet_unref(packet); } // 释放 packet 资源 av_packet_free(&packet); /*******************************************************/ avformat_close_input(&pFormatContex); } int main() { test(); return 0; }
程序执行结果:
audio ==> pts: 2956945 dts: 2956945 pos: 341207270 size: 540 stream_index: 1 audio ==> pts: 2957724 dts: 2957724 pos: 341207810 size: 493 stream_index: 1 ...省略 video ==> pts: 5566332 dts: 5566332 pos: 341587541 size: 103518 stream_index: 0 video ==> pts: 5567832 dts: 5567832 pos: 341691059 size: 61536 stream_index: 0 video ==> pts: 5569332 dts: 5569332 pos: 341752595 size: 60801 stream_index: 0 ...省略 audio ==> pts: 3094162 dts: 3094162 pos: 352981871 size: 544 stream_index: 1 video ==> pts: 5740698 dts: 5740698 pos: 352982415 size: 68850 stream_index: 0 ...省略 video ==> pts: 5808993 dts: 5808993 pos: 358110174 size: 47064 stream_index: 0 av_read_frame error!
2. AVPacket 写操作
一个 AVPacket 数据可以写到一个文件中,也可以写到缓冲区中,我们这里演示下,将 AVPacket 从输入多媒体文件中读取出来,再重新写入到另外一个多媒体文件中。这个过程,我们并不对读取到每一帧数据进行重新解码和编码操作。过程如下:
- 使用 avformat_open_input 函数打开多媒体文件,获得一个 AVFormatContext 类型的输入文件对象
- 使用 avformat_alloc_output_context2 函数再创建一个用于输出的 AVFormatContext 类型文件对象
- 使用 avformat_new_stream 向新创建的 AVFormatContext 对象中添加视频、音频 AVStream 对象
- 使用 avio_open 函数打开一个空的本地多媒体文件(该函数会自动创建该文件)
- 使用 avformat_write_header 函数向新的多媒体文件中写入头信息
- 循环从输入多媒体文件中读取 AVPacket 数据,并使用 av_interleaved_write_frame 函数将其写入到新创建的多媒体文件中
- 使用 av_write_trailer 函数向多媒体文件中写入尾信息
- 释放相关资源
上面步骤中,大多数步骤实现起来还是比较简单的,有些步骤就要麻烦一些,我们分开来实现每一步的代码。
2.1 打开多媒体文件
这一步比较简单,前面已经讲解过了。
AVFormatContext* pInFormatContex = nullptr; avformat_open_input(&pInFormatContex, "demo.mp4", nullptr, nullptr); avformat_find_stream_info(pInFormatContex, nullptr);(pOutFormatContex, packet);
2.2 创建输出文件对象
输入和输出使用的都是 AVFormatContext 类型的对象,不同的是:
- 初始化的方式不一样。输出对象使用的是 avformat_alloc_output_context2 函数进行初始化。
- 资源释放的方式不一样。输出对象使用的是 avformat_free_context 函数进行对象销毁。、
这一步的实现代码如下:
AVFormatContext* pOutFormatContex = nullptr; avformat_alloc_output_context2(&pOutFormatContex, nullptr, "mp4", nullptr);
函数声明如下:
int avformat_alloc_output_context2(AVFormatContext **ctx, const AVOutputFormat *oformat, const char *format_name, const char *filename);
ctx
:用于存储创建的输出文件上下文(AVFormatContext)的指针oformat
:指定要使用的输出格式(AVOutputFormat),如果为 NULL,由 FFmpeg 自动选择输出格式format_name
:输出格式的名称。如果为 NULL,FFmpeg 将自动选择输出格式filename
:输出文件的文件名。如果为 NULL,FFmpeg 将不会打开文件
函数的返回值为 0 表示成功,否则表示失败。
2.3 创建基本流对象
这一步稍微复杂一些,主要要实现在新创建的多媒体对象中,添加两个基本流,一个是视频流,一个是音频流。当我们从其他输入多媒体文件中读取到 AVPacket 数据之后,需要根据 AVPacket 的类型将其存储到视频流或者音频流中。
创建完成之后,我们需要对视频流和音频流中的关键信息进行初始化,否则是无法进行下一步操作。初始化内容就是分别是:
- 给视频流、音频流设置编码器参数
- 给视频流、音频流设置时间基
示例代码如下:
for (size_t i = 0; i < pInFormatContex->nb_streams; ++i) { // 向 pOutFormatContex 添加基本流 AVStream* pNewStream = avformat_new_stream(pOutFormatContex, nullptr); // 使用输入文件的基本流的编码器信息初始化新的基本流的编码器 // 创建辅助变量 AVCodecContext* pCodecContex = avcodec_alloc_context3(nullptr); // 将当前流的编码器信息拷贝到新创建的基本流中,即:初始化基本流的必要参数 avcodec_parameters_to_context(pCodecContex, pInFormatContex->streams[i]->codecpar); avcodec_parameters_from_context(pNewStream->codecpar, pCodecContex); // 销毁辅助变量 avcodec_free_context(&pCodecContex); // 初始化新基本流的时间基 pNewStream->time_base = pInFormatContex->streams[i]->time_base; }
上面代码中,我们在将输入多媒体流的编码信息拷贝到输出多媒体流中时,使用了两个函数:
int avcodec_parameters_to_context(AVCodecContext *codec, const AVCodecParameters *par); int avcodec_parameters_from_context(AVCodecParameters *par, const AVCodecContext *codec);
我们的目的是将输入流中的 codecpar 信息(编码器的信息)拷贝到输出流的 codecpar 中,但是没有相关的 api 函数直接可以做到这一点,所以先把输入流的 codecpar 拷贝到一个中间变量 AVCodecContex 中,再从 AVCodecContex 中拷贝输出流的 codecpar 中。
编码器参数初始化完成之后,再初始化下时间基 time_base 参数即可,直接从输入流中复制即可。最后别忘了使用 avcodec_free_context 函数把中间变量资源释放了。
2.4 打开输出文件
AVFormatContext 中的 pb 参数专门用于 IO 操作的数据结构,我们使用 avio_open 函数将其初始化(也可以理解打开),并设置 IO 模式为 AVIO_FLAG_WRITE 写模式即可。
avio_open(&pOutFormatContex->pb, "test.mp4", AVIO_FLAG_WRITE);
执行完该代码之后,会在指定路径下创建一个多媒体文件。函数声明如下:
int avio_open(AVIOContext **s, const char *url, int flags);
2.5 写入头信息
这一步需要注意,在写入头文件之前,一定要确保新创建的视频流、音频流完成必要的初始化,并且打开了相应的目标设备,才能正常执行下面的代码。
一般的头信息可能包含以下信息:
- 文件格式信息:描述音视频数据流的封装格式、文件头大小等信息。
- 视频流信息:包括视频编码格式、分辨率、帧率、比特率、像素格式等信息。
- 音频流信息:包括音频编码格式、采样率、声道数、比特率、样本格式等信息。
- 其他信息:如文件创建时间、时长、元数据(如标题、作者、关键字等)等。
avformat_write_header(pOutFormatContex, nullptr);
函数声明如下:
int avformat_write_header(AVFormatContext *s, AVDictionary **options);
2.6 写入 AVPacket 数据
这一步使用 av_interleaved_write_frame 函数完成 AVPacket 的写入工作。
AVPacket* packet = av_packet_alloc(); while (av_read_frame(pInFormatContex, packet) >= 0) { // 将 packet 写入到文件中 av_interleaved_write_frame(pOutFormatContex, packet); }
函数声明如下:
int av_interleaved_write_frame(AVFormatContext *s, AVPacket *pkt);
2.7 写入尾部信息
av_write_trailer(pOutFormatContex);
函数声明如下:
int av_write_trailer(AVFormatContext *s);
2.8 释放相关资源
// 释放 AVPacket 资源 av_packet_free(&packet); // 释放 IO 资源 avio_close(pOutFormatContex->pb); // 释放输入和输出上下文资源 avformat_close_input(&pInFormatContex); avformat_free_context(pOutFormatContex);