51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

FFmpeg AVPacket

本篇简单介绍下,如何从输入多媒体中读取或者写入一帧数据。由于我们并没有进行编解码操作,这里的读写操作都是编码后的数据。在 FFmpeg 中每一帧数据是由 AVPacket 来表示。读操作需要用到的函数有:

  1. avformat_open_input
  2. av_packet_alloc
  3. av_read_frame
  4. av_packet_unref
  5. avformat_close_input

写操作需要用到的函数有:

  1. avformat_alloc_output_context2

  2. avformat_new_stream

  3. av_interleaved_write_frame

  4. AVPacket 读操作 {#title-0} ==========================

从多媒体文件中读取一帧数据的过程如下:

  1. 首先,使用 av_packet_alloc 函数初始化一个 AVPacket 对象,用于存储读取到的数据
  2. 接着,调用 av_read_frame 从多媒体文件中读取一帧数据,存储到 AVPacket 对象中
  3. 然后,由于多媒体文件中存在多帧数据,我们肯定需要循环来读取,为了能够复用 AVPacket 对象,我们需要每次处理完帧数据之后,就需要使用 av_packet_unref 函数对 AVPacket 对象进行重置
  4. 最后,当所有帧数据都读取完毕之后,调用 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!
  1. AVPacket 写操作 {#title-1} ==========================

一个 AVPacket 数据可以写到一个文件中,也可以写到缓冲区中,我们这里演示下,将 AVPacket 从输入多媒体文件中读取出来,再重新写入到另外一个多媒体文件中。这个过程,我们并不对读取到每一帧数据进行重新解码和编码操作。过程如下:

  1. 使用 avformat_open_input 函数打开多媒体文件,获得一个 AVFormatContext 类型的输入文件对象
  2. 使用 avformat_alloc_output_context2 函数再创建一个用于输出的 AVFormatContext 类型文件对象
  3. 使用 avformat_new_stream 向新创建的 AVFormatContext 对象中添加视频、音频 AVStream 对象
  4. 使用 avio_open 函数打开一个空的本地多媒体文件(该函数会自动创建该文件)
  5. 使用 avformat_write_header 函数向新的多媒体文件中写入头信息
  6. 循环从输入多媒体文件中读取 AVPacket 数据,并使用 av_interleaved_write_frame 函数将其写入到新创建的多媒体文件中
  7. 使用 av_write_trailer 函数向多媒体文件中写入尾信息
  8. 释放相关资源

上面步骤中,大多数步骤实现起来还是比较简单的,有些步骤就要麻烦一些,我们分开来实现每一步的代码。

2.1 打开多媒体文件 {#title-2}

这一步比较简单,前面已经讲解过了。

AVFormatContext* pInFormatContex = nullptr;
avformat_open_input(&pInFormatContex, "demo.mp4", nullptr, nullptr);
avformat_find_stream_info(pInFormatContex, nullptr);(pOutFormatContex, packet);

2.2 创建输出文件对象 {#title-3}

输入和输出使用的都是 AVFormatContext 类型的对象,不同的是:

  1. 初始化的方式不一样。输出对象使用的是 avformat_alloc_output_context2 函数进行初始化。
  2. 资源释放的方式不一样。输出对象使用的是 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 创建基本流对象 {#title-4}

这一步稍微复杂一些,主要要实现在新创建的多媒体对象中,添加两个基本流,一个是视频流,一个是音频流。当我们从其他输入多媒体文件中读取到 AVPacket 数据之后,需要根据 AVPacket 的类型将其存储到视频流或者音频流中。

创建完成之后,我们需要对视频流和音频流中的关键信息进行初始化,否则是无法进行下一步操作。初始化内容就是分别是:

  1. 给视频流、音频流设置编码器参数
  2. 给视频流、音频流设置时间基

示例代码如下:

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 打开输出文件 {#title-5}

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 写入头信息 {#title-6}

这一步需要注意,在写入头文件之前,一定要确保新创建的视频流、音频流完成必要的初始化,并且打开了相应的目标设备,才能正常执行下面的代码。

一般的头信息可能包含以下信息:

  1. 文件格式信息:描述音视频数据流的封装格式、文件头大小等信息。
  2. 视频流信息:包括视频编码格式、分辨率、帧率、比特率、像素格式等信息。
  3. 音频流信息:包括音频编码格式、采样率、声道数、比特率、样本格式等信息。
  4. 其他信息:如文件创建时间、时长、元数据(如标题、作者、关键字等)等。
avformat_write_header(pOutFormatContex, nullptr);

函数声明如下:

int avformat_write_header(AVFormatContext *s, AVDictionary **options);

2.6 写入 AVPacket 数据 {#title-7}

这一步使用 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 写入尾部信息 {#title-8}

av_write_trailer(pOutFormatContex);

函数声明如下:

int av_write_trailer(AVFormatContext *s);

2.8 释放相关资源 {#title-9}

// 释放 AVPacket 资源
av_packet_free(&packet);
// 释放 IO 资源
avio_close(pOutFormatContex->pb);
// 释放输入和输出上下文资源
avformat_close_input(&pInFormatContex);
avformat_free_context(pOutFormatContex);    

赞(1)
未经允许不得转载:工具盒子 » FFmpeg AVPacket