音视频开发—FFmpeg处理流数据的基本概念详解

时间:2024-07-12 22:04:45

文章目录

    • 多媒体文件的基本概念
    • 相关重要的结构体
    • 操作数据流的基本步骤
      • 1.解复用(Demuxing)
      • 2.获取流(Stream)
      • 3. 读取数据包(Packet)
      • 4. 释放资源(Free Resources)
      • 完整示例

多媒体文件的基本概念

  • 多媒体文件其实是个容器

多媒体文件(如MP4、MKV、AVI等)实际上是一个容器格式。容器的作用是将不同类型的数据(如视频、音频、字幕等)封装在一个文件中,方便管理和播放。每种容器格式都有自己的规范,定义了如何组织和存储这些不同类型的数据。

  • 在容器里有很多流

在多媒体容器文件中,可以包含多个不同的流。每个流代表一种媒体数据,例如视频流、音频流、字幕流等。一个典型的多媒体文件通常至少包含一个视频流和一个音频流,但也可以包含多个视频流、多个音频流和其他类型的流(如字幕、章节信息、元数据等)。

  • 每种流是由不同的编码器编码实现的

每个流的数据在存储之前需要经过编码。编码器(如H.264、AAC、MP3等)将原始的多媒体数据(如未压缩的视频和音频)转换成压缩格式,以减少存储空间和传输带宽。不同的编码器适用于不同类型的数据和使用场景。例如,视频流可能使用H.264编码器,音频流可能使用AAC编码器。

  • 从流中读出的数据叫做包

在流中,数据被分成一个个的数据包(packet)。每个包包含一段编码后的多媒体数据,以及一些元数据(如时间戳、流的标识等)。在解码和播放时,播放器会从容器文件中读取这些数据包,并将其传递给相应的解码器进行解码。

  • 在一个包中包含多个帧

数据包中的内容进一步细分为帧。帧是视频或音频数据的最小单位。例如,在视频流中,每一帧代表一个静止的图像,连续播放这些图像可以形成视频。在音频流中,每一帧代表一段音频采样数据。帧的数量和类型(如关键帧、预测帧等)取决于编码器的工作方式和编码参数。

相关重要的结构体

  • AVFormatContext 结构体

AVFormatContext 是 FFmpeg 中用于描述多媒体文件或流的上下文结构体。它包含了文件格式、输入输出协议、文件信息以及多个流等信息。

  • AVStream 结构体

AVStream 是 FFmpeg 中用于描述多媒体文件中的一个流(如视频流、音频流、字幕流等)的结构体。每个 AVStream 包含了流的编解码信息、时间基准等。

  • AVPacket

AVPacket 是 FFmpeg 中用于描述存储在容器中的多媒体数据包的结构体。数据包是编码后的数据,包含一组帧。

操作数据流的基本步骤

在这里插入图片描述

1.解复用(Demuxing)

解复用是指从多媒体容器中提取出独立的音频、视频和其他流的过程。在FFmpeg中,解复用通过打开文件并解析文件头部信息来实现。

主要步骤

  • 注册所有格式和编解码器: 使用 av_register_all() 注册FFmpeg支持的所有格式和编解码器(FFmpeg 4.x及以前版本需要,FFmpeg 5.0及以后版本不需要)。
  • 打开输入文件: 使用 avformat_open_input() 打开输入文件。
  • 读取文件头部信息: 使用 avformat_find_stream_info() 读取文件头部信息。

示例代码

AVFormatContext *fmt_ctx = NULL;
int ret;

// 注册所有格式和编解码器(FFmpeg 4.x及以前版本需要)
av_register_all();

// 打开输入文件
if ((ret = avformat_open_input(&fmt_ctx, "input.mp4", NULL, NULL)) < 0) {
    av_log(NULL, AV_LOG_ERROR, "Cannot open input file: %s\n", av_err2str(ret));
    return ret;
}

// 读取文件头部信息
if ((ret = avformat_find_stream_info(fmt_ctx, NULL)) < 0) {
    av_log(NULL, AV_LOG_ERROR, "Cannot find stream information: %s\n", av_err2str(ret));
    avformat_close_input(&fmt_ctx);
    return ret;
}

// 打印输入文件的信息
av_dump_format(fmt_ctx, 0, "input.mp4", 0);

2.获取流(Stream)

获取流是指从多媒体文件中提取出各个独立的流,例如音频流和视频流。每个流包含了相关的编解码信息。

主要步骤

  • 查找音频和视频流: 遍历 AVFormatContext 中的流,查找音频和视频流。
  • 打印流信息: 打印每个流的信息。

示例代码

AVStream *video_stream = NULL;
AVStream *audio_stream = NULL;

for (unsigned int i = 0; i < fmt_ctx->nb_streams; i++) {
    AVStream *stream = fmt_ctx->streams[i];
    if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
        video_stream = stream;
    } else if (stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
        audio_stream = stream;
    }
}

if (!video_stream && !audio_stream) {
    av_log(NULL, AV_LOG_ERROR, "No video or audio stream found\n");
    avformat_close_input(&fmt_ctx);
    return -1;
}

3. 读取数据包(Packet)

读取数据包是指从文件中逐个读取编码后的数据包。数据包可以包含音频、视频或其他类型的数据。

主要步骤

  • 初始化数据包: 使用 av_init_packet() 初始化数据包。
  • 读取数据包: 使用 av_read_frame() 读取数据包。
  • 处理数据包: 根据数据包所属的流进行相应处理。
  • 释放数据包: 使用 av_packet_unref() 释放数据包。

示例代码

AVPacket pkt;
av_init_packet(&pkt);
pkt.data = NULL;
pkt.size = 0;

while (av_read_frame(fmt_ctx, &pkt) >= 0) {
    if (pkt.stream_index == video_stream->index) {
        // 处理视频数据包
        av_log(NULL, AV_LOG_INFO, "Video Packet: PTS=%" PRId64 ", DTS=%" PRId64 ", size=%d\n",
               pkt.pts, pkt.dts, pkt.size);
    } else if (pkt.stream_index == audio_stream->index) {
        // 处理音频数据包
        av_log(NULL, AV_LOG_INFO, "Audio Packet: PTS=%" PRId64 ", DTS=%" PRId64 ", size=%d\n",
               pkt.pts, pkt.dts, pkt.size);
    }
    av_packet_unref(&pkt);
}

4. 释放资源(Free Resources)

释放资源是指在完成数据流操作后,释放分配的所有内存和资源,以避免内存泄漏。

主要步骤

  • 释放数据包: 使用 av_packet_unref() 释放每个数据包。
  • 关闭输入文件: 使用 avformat_close_input() 关闭输入文件并释放 AVFormatContext
  • 释放其他资源: 释放任何其他分配的资源。

示例代码

// 释放数据包
av_packet_unref(&pkt);

// 关闭输入文件并释放AVFormatContext
avformat_close_input(&fmt_ctx);

完整示例

#include <libavformat/avformat.h>
#include <libavutil/log.h>

int main(int argc, char *argv[]) {
    AVFormatContext *fmt_ctx = NULL;
    AVPacket pkt;
    AVStream *video_stream = NULL;
    AVStream *audio_stream = NULL;
    int ret;

    if (argc < 2) {
        fprintf(stderr, "Usage: %s <input file>\n", argv[0]);
        return 1;
    }

    // 注册所有格式和编解码器(FFmpeg 4.x及以前版本需要)
    av_register_all();

    // 打开输入文件
    if ((ret = avformat_open_input(&fmt_ctx, argv[1], NULL, NULL)) < 0) {
        av_log(NULL, AV_LOG_ERROR, "Cannot open input file: %s\n", av_err2str(ret));
        return ret;
    }

    // 读取文件头部信息
    if ((ret = avformat_find_stream_info(fmt_ctx, NULL)) < 0) {
        av_log(NULL, AV_LOG_ERROR, "Cannot find stream information: %s\n", av_err2str(ret));
        avformat_close_input(&fmt_ctx);
        return ret;
    }

    // 打印输入文件的信息
    av_dump_format(fmt_ctx, 0, argv[1], 0);

    // 查找音频和视频流
    for (unsigned int i = 0; i < fmt_ctx->nb_streams; i++) {
        AVStream *stream = fmt_ctx->streams[i];
        if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            video_stream = stream;
        } else if (stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
            audio_stream = stream;
        }
    }

    if (!video_stream && !audio_stream) {
        av_log(NULL, AV_LOG_ERROR, "No video or audio stream found\n");
        avformat_close_input(&fmt_ctx);
        return -1;
    }

    // 初始化数据包
    av_init_packet(&pkt);
    pkt.data = NULL;
    pkt.size = 0;

    // 读取数据包
    while (av_read_frame(fmt_ctx, &pkt) >= 0) {
        if (pkt.stream_index == video_stream->index) {
            // 处理视频数据包
            av_log(NULL, AV_LOG_INFO, "Video Packet: PTS=%" PRId64 ", DTS=%" PRId64 ", size=%d\n",
                   pkt.pts, pkt.dts, pkt.size);
        } else if (pkt.stream_index == audio_stream->index) {
            // 处理音频数据包
            av_log(NULL, AV_LOG_INFO, "Audio Packet: PTS=%" PRId64 ", DTS=%" PRId64 ", size=%d\n",
                   pkt.pts, pkt.dts, pkt.size);
        }
        av_packet_unref(&pkt);
    }

    // 关闭输入文件并释放AVFormatContext
    avformat_close_input(&fmt_ctx);

    return 0;
}