FFmpeg视频编解码

文章目录

  • 前言
  • 一、FFmpeg常用结构体和函数
    • [1.1 AVFrame](#1.1 AVFrame)
    • [1.2 SwsContext](#1.2 SwsContext)
    • [1.3 AVPacket](#1.3 AVPacket)
    • [1.4 AVFormatContext](#1.4 AVFormatContext)
    • [1.5 AVStream](#1.5 AVStream)
    • [1.6 AVCodecParameters](#1.6 AVCodecParameters)
  • 二、视频编码与解码
    • [2.1 视频编码](#2.1 视频编码)
      • [2.1.1 选择编码器](#2.1.1 选择编码器)
      • [2.1.2 创建编码器上下文](#2.1.2 创建编码器上下文)
      • [2.1.3 设置编码器参数](#2.1.3 设置编码器参数)
      • [2.1.4 打开编码器](#2.1.4 打开编码器)
      • [2.1.5 编码数据](#2.1.5 编码数据)
      • [2.1.6 写入输出](#2.1.6 写入输出)
      • [2.1.7 关闭编码器](#2.1.7 关闭编码器)
    • [2.2 视频解码](#2.2 视频解码)
      • [2.2.1 查找解码器](#2.2.1 查找解码器)
      • [2.2.2 分配解码上下文](#2.2.2 分配解码上下文)
      • [2.2.3 打开解码器](#2.2.3 打开解码器)
      • [2.2.4 解码数据](#2.2.4 解码数据)
      • [2.2.5 H264 帧分割](#2.2.5 H264 帧分割)
      • [2.2.6 解码硬件加速 DXVA2](#2.2.6 解码硬件加速 DXVA2)
  • 三、视频封装与解封装
    • [3.1 视频封装](#3.1 视频封装)
      • [3.1.1 创建封装上下文](#3.1.1 创建封装上下文)
      • [3.1.2 创建媒体流](#3.1.2 创建媒体流)
      • [3.1.3 打开输出 I/O](#3.1.3 打开输出 I/O)
      • [3.1.4 写入文件头](#3.1.4 写入文件头)
      • [3.1.5 写入帧数据](#3.1.5 写入帧数据)
      • [3.1.6 写入尾部数据](#3.1.6 写入尾部数据)
      • [3.1.7 其它相关函数](#3.1.7 其它相关函数)
    • [3.2 视频解封装](#3.2 视频解封装)
      • [3.2.1 打开媒体文件](#3.2.1 打开媒体文件)
      • [3.2.2 获取流信息](#3.2.2 获取流信息)
      • [3.2.3 查找音视频流索引](#3.2.3 查找音视频流索引)
      • [3.2.4 读取数据包](#3.2.4 读取数据包)
      • [3.2.5 关闭文件释放资源](#3.2.5 关闭文件释放资源)
      • [3.2.6 其它相关函数](#3.2.6 其它相关函数)
  • 四、RTSP
    • [4.1 VLC模拟RTSP服务器](#4.1 VLC模拟RTSP服务器)
  • 结束语

  • 💂 个人主页 :风间琉璃
  • 🤟 版权 : 本文由【风间琉璃】原创、在CSDN首发、需要转载请联系博主
  • 💬 如果文章对你有帮助欢迎关注点赞收藏(一键三连)订阅专栏

前言

提示:这里可以添加本文要记录的大概内容:


必看:通过思维导图快速了解FFmpeg源码整体结构体

以FFmpeg最为常用的两个场景(转码和播放)为例,其路径以及使用的库的关系如下图。

该图展示了 FFmpeg 处理音视频的整体流程,分为多个阶段和组件。

  1. Source(数据源)
  • 文件(File):输入的数据可以来自本地文件,例如.mp4, .avi等格式的视频文件。
  • 网络流(Network Stream):输入数据可以来自网络流,比如 RTSP 流、HTTP 流等实时传输的音视频数据。
  • 设备(Device):输入数据还可以来自设备,如摄像头、麦克风等硬件设备。
  1. Demux(解封装)
  • 该阶段主要涉及从源文件中解封装音视频数据 。FFmpeg 使用 libavformat 库来处理文件的解析,提取音频和视频流。解封装后的音视频流将分别传递到后续处理模块。
  1. Decoder(解码器)
  • 视频解码(Video ES 1, Video ES 2) :解封装后的音视频流(视频流)经过解码器(libavcodec)进行解码,转化为未压缩的格式,便于后续处理。
  • 音频解码(Audio ES 1, Audio ES 2):类似地,音频流也会被解码为可处理的未压缩音频数据。
  1. Filter(滤镜处理)
  • 视频滤镜(Video Filter):可以对视频流进行一系列的操作,比如缩放(scale)、帧率转换(frc)、裁剪(crop)等。
  • 音频滤镜(Audio Filter):音频数据也可以经过处理,如增益调整、均衡、效果添加等。
  1. Encoder(编码器)
  • 视频编码(Video Encoder) :处理后的视频流会通过编码器(libavcodec)进行编码,转化为压缩格式(如 H.264)。
  • 音频编码(Audio Encoder):同样,音频流也会通过编码器进行编码(如 AAC 编码)。
  1. Mux(封装)
  • Muxer(封装器) :经过编码后的音频和视频流会被封装成目标文件格式,如 .avi, .mp4, .ts, .flv 等。这个过程使用 libavformat 中的封装工具,最终生成一个多媒体文件。
  1. Renderer(渲染器)
  • 视频渲染(Video Renderer):渲染处理后的解码视频流,以便显示。
  • 音频渲染(Audio Renderer):渲染音频流,通常指播放音频。
  • 音视频同步(A/V sync):确保音视频的同步播放,保证音频和视频的播放保持一致,避免视频卡顿或音频延迟。

一、FFmpeg常用结构体和函数

1.1 AVFrame

AVFrame主要用于存储未压缩的音频或视频的帧数据。它包含了大量字段,用于描述帧的像素格式、分辨率、数据指针、时间戳等信息。

c 复制代码
struct AVFrame {
	uint8_t *data[AV_NUM_DATA_POINTERS];
	int linesize[AV_NUM_DATA_POINTERS]; //字节对齐
	int width, height;
	int format;  //AVPixelFormat
	AVBufferRef *buf[AV_NUM_DATA_POINTERS];
	int64_t pts;
}
  • uint8_t *data[AV_NUM_DATA_POINTERS]:数据指针数组,每个指针指向一个数据平面。

    • 对于视频
      • YUV420P:data[0] -> Y 平面;data[1] -> U 平面;data[2] -> V 平面。
      • RGB24:data[0] -> RGB 数据按行存储,无分平面。
    • 对于音频:存储多通道数据。
  • int linesize[AV_NUM_DATA_POINTERS]:每个数据平面的行字节数(步幅)

    • 视频:每一行数据所占的字节数,可能比实际图像宽度大,用于对齐。
    • 音频:每个音频通道的步幅(以字节为单位)。
  • width, height:视频帧的宽度和高度。

  • format:像素格式,例如 AV_PIX_FMT_YUV420P、AV_PIX_FMT_RGB24 等。

  • AVBufferRef *buf[AV_NUM_DATA_POINTERS]:数据缓冲区的引用计数指针,用于内存管理

  • int64_t pts:时间戳(Presentation Timestamp),用于同步音视频。

在使用 FFmpeg 处理音视频时,AVFrame相关函数主要用于内存管理、数据拷贝、引用计数、初始化与释放等操作。这些函数分布在 FFmpeg 的多个模块中,例如 libavutil/frame.hlibavcodec/avcodec.h

  1. 内存管理

(1)分配内存

cpp 复制代码
AVFrame *av_frame_alloc(void);

分配一个新的 AVFrame 结构体,并将其字段初始化为默认值 ,直接使用时需要根据具体需求设置字段(如数据缓冲区和格式等)。返回的AVFrame可以用来存储音频或视频帧数据,也可以用作中间处理的缓冲区。av_frame_alloc是线程安全的,可以在多线程环境中调用。

(2)释放内存

在帧使用完毕后,调用 av_frame_free 释放内存,避免内存泄漏。

cpp 复制代码
void av_frame_free(AVFrame **frame);
  • 功能:释放AVFrame的内存。
  • 参数:传入指向AVFrame的指针地址,释放后指针会被设置为NULL。

释放一个AVFrame结构体及其分配的内存(包括数据缓冲区和元数据)。将指针 *frame 设置为 NULL,防止后续使用无效的指针。它会销毁AVFrame的数据缓冲区和结构体本身,并将指针置为 NULL,以避免悬空指针的出现。

(3)清空数据但不释放内容

cpp 复制代码
int av_frame_unref(AVFrame *frame);

清空AVFrame的数据内容 ,包括引用的缓冲区(例如图像数据、音频数据)和元数据(如时间戳、标志等),但保留结构体本身的内存 。这样可以重复使用该**AVFrame结构体,而无需重新分配内存。**调用后,AVFrame变为一个干净的状态,可以重新赋值或重新填充数据。

(4)重置为初始状态

cpp 复制代码
void av_frame_reset(AVFrame *frame);

将AVFrame重置为初始状态,清除所有字段。

注意:av_frame_unref只清空帧内容,保留AVFrame的结构体本身以便复用。而av_frame_free会释放整个AVFrame结构体。在需要频繁处理帧的场景下,可以通过av_frame_unref避免重复分配 AVFrame带来的性能开销。

  1. 数据管理

(1)克隆AVFrame

cpp 复制代码
AVFrame *av_frame_clone(const AVFrame *src);

创建源帧的深拷贝。

(2)数据引用

cpp 复制代码
int av_frame_ref(AVFrame *dst, const AVFrame *src);
  • AVFrame *dst:用于保存对源AVFrame的引用。需要确保已分配内存(通常使用 av_frame_alloc)。如果目标帧之前已经持有数据,应先调用 av_frame_unref清空。

  • AVFrame *src:源AVFrame,需要被引用的帧, 必须是有效的且已初始化的帧。

将源 AVFrame src的内容和数据缓冲区引用赋值给目标AVFrame dst。增加源AVFrame 数据缓冲区的引用计数。目标AVFrame(dst) 会持有源AVFrame数据的引用,而不是拷贝。目标AVFrame和源AVFrame的数据内容相同 ,但可以独立管理其元数据(如时间戳、标志等)。这种机制可以有效避免数据的深拷贝,提升内存使用效率。

(3)释放数据引用

cpp 复制代码
void av_frame_unref(AVFrame *frame);

释放 AVFrame 中的所有引用数据(如图像缓冲区、音频缓冲区等) 。清除与帧相关的元数据 (如时间戳、标志等)。保留AVFrame结构体,使其可以重复用于新的数据填充。如果没有其他引用,释放实际内存。

av_frame_unref只清空AVFrame的内容,不释放AVFrame本身。如果需要释放整个AVFrame,请使用 av_frame_free。

如果多个AVFrame引用了相同的缓冲区,调用av_frame_unref会减少引用计数**,直到所有引用被释放,缓冲区才会真正被释放。**清空后,AVFrame可以重新设置属性,用于处理新的数据。av_frame_ref 创建帧引用,av_frame_unref 用于清空引用并减少引用计数。

(4)转移引用

cpp 复制代码
void av_frame_move_ref(AVFrame *dst, AVFrame *src);

将源帧 src 的引用移动到目标帧 dst,源帧的数据引用清空。

  1. 缓冲区管理

(1)分配图像缓冲区

cpp 复制代码
int av_frame_get_buffer(AVFrame *frame, int align);
  • AVFrame *frame:指向需要分配缓冲区的AVFrame。该帧必须提前设置好基本属性,如宽度、高度、像素格式(视频)或样本格式、通道布局(音频)。

  • int align:缓冲区的对齐字节数(通常为 32 或更高的值,具体取决于硬件要求)。对齐值越大,分配的内存地址更容易满足硬件对齐需求,从而提高处理性能。

AVFrame分配数据缓冲区的函数。它会根据AVFrame的属性(如宽度、高度和像素格式)分配适当的内存,以存储图像或音频数据。分配的缓冲区会自动与AVFrame关联,并在av_frame_unref或av_frame_free 时自动释放。

(2)分配音频缓冲区

cpp 复制代码
int av_samples_fill_arrays(uint8_t **audio_data, int *linesize,
                  const uint8_t *buf, int channels,
                  int nb_samples, enum AVSampleFormat sample_fmt, int align);

为音频帧分配缓冲区。通常与音频采样数据的管理相关。

以下示例演示如何使用AVFrame处理视频帧:

cpp 复制代码
AVFrame *frame = av_frame_alloc();
if (!frame) {
    fprintf(stderr, "Could not allocate AVFrame\n");
    return -1;
}

frame->width = 1920;
frame->height = 1080;
frame->format = AV_PIX_FMT_YUV420P;

// 分配图像缓冲区
if (av_frame_get_buffer(frame, 32) < 0) {
    fprintf(stderr, "Could not allocate frame buffer\n");
    av_frame_free(&frame);
    return -1;
}

// 处理帧数据(略)

// 释放帧
av_frame_free(&frame);

1.2 SwsContext

SwsContext 是 FFmpeg 中用于图像转换的结构体,主要用于视频缩放格式转换色彩空间转换等操作。SwsContext不直接定义在 FFmpeg 的头文件中,而是通过函数 sws_getContext() 创建的。该结构体包含了图像转换的相关上下文和状态信息,用于执行具体的图像转换操作。

1.SwsContext的创建

①为了使用SwsContext进行图像转换,需要调用sws_getContext()来初始化这个结构体SwsContext本身并不包含图像数据,而是提供了一个转换上下文,该上下文包含了如何将一种图像格式转换为另一种格式的所有信息。SwsContext用于初始化一个用于像素格式和尺寸转换上下文

c 复制代码
struct SwsContext *sws_getContext(
    int srcW, int srcH, enum AVPixelFormat srcFormat, // 源图像的宽度、高度和像素格式
    int dstW, int dstH, enum AVPixelFormat dstFormat, // 目标图像的宽度、高度和像素格式
    int flags,                           // 缩放算法
    SwsFilter *srcFilter, SwsFilter *dstFilter, const double *param
);
  • srcW, srcH:输入图像的宽度和高度。

  • srcFormat:输入图像的像素格式。

  • dstW, dstH:输出图像的宽度和高度。

  • dstFormat:输出图像的像素格式。

  • flags:转换的算法标志。

    • SWS_FAST_BILINEAR:快速的双线性插值,适用于需要快速处理但不关心图像质量的场景。

    • SWS_BILINEAR:标准的双线性插值,适用于普通的图像缩放。

    • SWS_BICUBIC:双立方插值,适用于高质量图像缩放,尤其适合细节较为丰富的图像。

    • SWS_LANCZOS:Lanczos 算法,适用于高质量缩放,能提供比双立方更高的质量,但计算量也较大。

  • srcFilter, dstFilter:可选的过滤器参数(通常为NULL)。

  • param:可选的参数数组(通常为 NULL)。

  • 返回值:返回一个 SwsContext上下文指针。

sws_getCachedContext也用于获取(或创建)一个像素格式和尺寸转换的上下文(SwsContext)。与 sws_getContext 不同的是,该函数会检查是否已经存在符合要求的上下文,并重用它从而避免频繁分配和释放资源。

cpp 复制代码
struct SwsContext *sws_getCachedContext(
    struct SwsContext *context,
    int srcW, int srcH, enum AVPixelFormat srcFormat,
    int dstW, int dstH, enum AVPixelFormat dstFormat,
    int flags, SwsFilter *srcFilter, SwsFilter *dstFilter, const double *param
);

对于参数context, 如果已存在的 SwsContext,传入指针以重用。如果为 NULL,则创建一个新的上下文。其余参数和sws_getContext 都是相同的。

2.sws_scale图像转换操作

一旦初始化了SwsContext,可以通过sws_scale()函数进行实际的图像转换。这个函数用于将源图像按照设定的转换上下文进行缩放和像素格式转换

c 复制代码
int sws_scale(
    struct SwsContext *c, const uint8_t *const srcSlice[],
    const int srcStride[], int srcSliceY, int srcSliceH,
    uint8_t *const dst[], const int dstStride[]
);
  • c:上面创建的SwsContext。

  • srcSlice:源图像的指针数组(例如,YUV 的 Y、U、V 平面)。

  • srcStride:源图像每一行的字节数。

  • srcSliceY:起始行号 (通常为 0)。

  • srcSliceH:转换的行数。

  • dst:目标图像的指针数组。

  • dstStride:目标图像每一行的字节数。

  • 返回值:返回处理的输出图像的高度(行数)。


3.SwsContext的释放

使用完SwsContext后,必须释放该上下文所占用的资源,调用sws_freeContext()。

c 复制代码
void sws_freeContext(struct SwsContext *swsContext);

以下示例展示如何将一幅 YUV420图像转换为RGB像素格式,并调整尺寸。

cpp 复制代码
#include <iostream>
#include <fstream>

extern "C" 
{
    #include <libswscale/swscale.h>
}

#define YUV_INPUT_FILE "400_300_25.yuv"
#define RGBA_OUTPUT_FILE "800_600_25.rgba"

using namespace std;
int main(int argc, char* argv[]) 
{
     cout << "Start YUV to RGBA conversion using FFmpeg" << endl;
    // 原始 YUV 文件的宽高和目标 RGBA 文件的宽高
    const int yuv_width = 400;
    const int yuv_height = 300;
    const int rgba_width = 800;
    const int rgba_height = 600;

    // 分配 YUV 数据缓冲区 (YUV420P 格式)
    // YUV420P 格式存储:Y 平面宽高与图像相同,U/V 平面宽高为原始的 1/2
    unsigned char* yuv_planes[3] = {nullptr};
    int yuv_line_size[3] = {yuv_width, yuv_width / 2, yuv_width / 2};
    yuv_planes[0] = new unsigned char[yuv_width * yuv_height];         // Y 平面
    yuv_planes[1] = new unsigned char[yuv_width * yuv_height / 4];     // U 平面
    yuv_planes[2] = new unsigned char[yuv_width * yuv_height / 4];     // V 平面

    // 分配 RGBA 数据缓冲区 RGBA 格式存储:每像素 4 字节(R、G、B、A)
    unsigned char* rgba_buffer = new unsigned char[rgba_width * rgba_height * 4];
    int rgba_line_size = rgba_width * 4;

    // 打开输入 YUV 文件和输出 RGBA 文件
    ifstream yuv_file(YUV_INPUT_FILE, ios::binary);
    ofstream rgba_file(RGBA_OUTPUT_FILE, ios::binary);

    // 定义用于像素格式和尺寸转换的上下文
    SwsContext* sws_context = nullptr;
    // 循环读取 YUV 数据并转换为 RGBA
    while (true) {
        // 读取 YUV 帧数据
        yuv_file.read((char*)yuv_planes[0], yuv_width * yuv_height);     // Y 平面
        yuv_file.read((char*)yuv_planes[1], yuv_width * yuv_height / 4);  // U 平面
        yuv_file.read((char*)yuv_planes[2], yuv_width * yuv_height / 4);  // V 平面

        // 如果读取不到更多数据,退出循环
        if (yuv_file.gcount() == 0) 
        {
            break;
        }

        // 创建或更新 SwsContext,用于 YUV 转换为 RGBA
        sws_context = sws_getCachedContext(
            sws_context,             // 上下文,若为 NULL 则创建新上下文
            yuv_width, yuv_height,      // 输入图像的宽高
            AV_PIX_FMT_YUV420P,        // 输入图像的像素格式
            rgba_width, rgba_height,     // 输出图像的宽高
            AV_PIX_FMT_RGBA,          // 输出图像的像素格式
            SWS_BILINEAR,            // 转换算法(双线性插值)
            nullptr, nullptr, nullptr    // 过滤器参数,通常为 NULL
        );

        // 检查上下文是否创建成功
        if (!sws_context) 
        {
            cerr << "Failed to create SwsContext for YUV to RGBA conversion" << endl;
            return -1;
        }

        // 执行像素格式和尺寸转换
        unsigned char* output_planes[1] = {rgba_buffer}; // 输出缓冲区
        int output_line_sizes[1] = {rgba_line_size};     // 输出行字节数
        int converted_height = sws_scale(
            sws_context,                // 上下文
            yuv_planes,                // 输入 YUV 数据
            yuv_line_size,              // 输入行字节数
            0,                      // 输入起始行
            yuv_height,                // 输入图像高度
            output_planes,              // 输出 RGBA 数据
            output_line_sizes            // 输出行字节数
        );

        // 打印转换行数,验证转换是否成功
        cout << "Converted height: " << converted_height << " rows" << endl;
        // 将 RGBA 数据写入输出文件
        rgba_file.write((char*)rgba_buffer, rgba_width * rgba_height * 4);
    }

    // 清理资源
    delete[] yuv_planes[0];
    delete[] yuv_planes[1];
    delete[] yuv_planes[2];
    delete[] rgba_buffer;

    if (sws_context) 
    {
        sws_freeContext(sws_context);
    }
    cout << "YUV to RGBA conversion completed successfully!" << endl;
    return 0;
}

1.3 AVPacket

AVPacket结构体存储压缩后的数据,通常有两种情况出现:一是由Demuxer(解封装)导出,然后作为输入传给解码器;二是作为编码器的输出,然后传给Muxer(封装)。对于视频来说,它通常应包含一个压缩帧;对于音频来说,它可能包含几个压缩帧。另外,编码器也允许输出空包,而没有压缩数据,只包含side data(例如,在编码结束后更新一些流相关的参数)。AVPacket重要的字段如下图所示。

AVPacket.data的生命周期取决于buf字段。如果它被设置,数据包是动态分配的,并且一直有效,**直到调用av_packet_unref()将引用计数减少到0。**如果buf字段没有被设置,av_packet_ ref()将进行复制而不是增加引用计数。side data总是由av_malloc()分配,由av_packet_ ref()复制,由av_packet_unref()释放。。

AVPacket是FFmpeg库中用于存储编码后数据的结构体,通常用于音视频数据流的处理。它包含了媒体数据的原始数据、时间戳、大小、格式等信息。

c++ 复制代码
typedef struct AVPacket {
    uint8_t *data;      ///< 指向数据缓冲区的指针
    int size;           ///< 数据的大小
    int64_t pts;        ///< 解码时间戳
    int64_t dts;        ///< 显示时间戳
    int64_t duration;   ///< 帧的持续时间
    int flags;          ///< 包的标志
    int stream_index;   ///< 流的索引
    void *opaque;       ///< 额外的用户数据
} AVPacket;

data: 指向包中数据的指针,通常是音频或视频数据。

size: 包中数据的大小(字节数)。

pts (Presentation Time Stamp) : 解码后帧的显示时间戳 。通常是解码的时间戳,用于决定媒体数据的播放顺序。

dts (Decoding Time Stamp): 解码前的时间戳,表示帧的解码顺序。

duration: 当前数据包的持续时间。

flags : 该数据包的一些标志位。常见的标志位包括 AV_PKT_FLAG_KEY(表示关键帧)。

stream_index: 当前包所属流的索引,用于区分视频、音频或字幕流。

opaque: 一个额外的用户自定义数据指针。

FFmpeg 提供了大量的 API 用于处理 AVPacket,例如从文件或流中读取数据、将数据发送到解码器、将数据推送到编码器等。

(1)av_init_packet()初始化AVPacket结构体。

c 复制代码
void av_init_packet(AVPacket *pkt);

(2)av_packet_alloc()动态分配一个AVPacket实例。

c 复制代码
AVPacket *av_packet_alloc(void);

(3)av_packet_free()释放通过 av_packet_alloc分配的AVPacket。

c 复制代码
void av_packet_free(AVPacket **pkt);

(4)av_packet_ref()引用计数增加,增加对AVPacket的引用,通常在需要多个地方使用同一个包时使用。

c 复制代码
int av_packet_ref(AVPacket *dst, const AVPacket *src);

(5)av_packet_rescale_ts()重新调整时间戳,使得 AVPacket在不同流之间的时间戳转换正常。

c 复制代码
int av_packet_rescale_ts(AVPacket *pkt, AVRational src_tb, AVRational dst_tb);

(6)av_packet_unref()释放 AVPacket 中的内存数据,但不释放结构体本身。

c 复制代码
void av_packet_unref(AVPacket *pkt);

(7)av_packet_move_ref将一个 AVPacket 的数据转移到另一个包中。

c 复制代码
int av_packet_move_ref(AVPacket *dst, AVPacket *src);

(8)av_read_frame()读取一个数据包,通常用于从多媒体文件中提取音视频流的数据。

c 复制代码
int av_read_frame(AVFormatContext *s, AVPacket *pkt);

1.4 AVFormatContext

AVFormatContext 用于处理多媒体文件的输入和输出 。它是 FFmpeg 中处理流(audio/video stream)和媒体文件的上下文,负责存储关于文件格式和流的各种信息。通过AVFormatContext,可以方便地读取多种格式的视频和音频数据,或者写入多种格式的媒体文件。

c 复制代码
typedef struct AVFormatContext {
    ...
    unsigned int nb_streams;                         
    AVStream **streams;                                               
    void *priv_data;                             
    AVIOContext *pb;   
    int64_t duration; 
    int64_t bit_rate; 
    char *url; 
    ...
} AVFormatContext;
  • nb_streams:文件或流的数量 ,表示媒体文件中包含的音视频流总数 。例如,一个文件可能包含多个视频流、音频流和字幕流。一个包含视频和音频的 MP4 文件,nb_streams 的值可能为 2。

  • streams:指向流的指针数组**,数组大小为 nb_streams**。每个 AVStream 表示一个具体的媒体流(如音频流或视频流) 。可通过 streams[index] 获取对应的 AVStream。

  • priv_data:私有数据指针,通常用于存储与特定输入或输出格式相关的上下文数据。使用时通过 AVOptions 设置或获取选项。

  • pb: 输入/输出上下文,用于文件或流的 I/O 操作。在解复用(demuxing)时,它表示输入文件。 在复用(muxing)时,它表示输出文件。

  • duration: 媒体文件的总时长,单位为微秒(us)。仅在解复用(demuxing)时有效。某些流可能不提供此信息。

  • bit_rate: 媒体文件的总比特率,单位为比特每秒(bps)。如果文件头中没有明确的比特率信息,可能会估算此值。

  • url:指向媒体文件或流的 URL 地址。 例如,文件路径、HTTP 地址或 RTSP 流 URL。解码器或编码器使用此 URL 定位源文件或目标文件。url的值可以是 "file.mp4"、"http://example.com/video.mp4" 或 "rtsp://192.168.0.1/stream"。

1.5 AVStream

AVStream 包含了一个媒体文件 中的音频流视频流字幕流的相关信息。在 FFmpeg 中,一个 AVFormatContext通常包含多个AVStream,每个AVStream代表一个媒体流。AVStream包含了描述媒体流的元数据(如时间基、编码参数、解码参数等)。它提供了解码器所需的参数,帮助处理器正确解码媒体流。通过time_base字段处理不同时间基的时间戳转换。

c 复制代码
typedef struct AVStream {
    int index; 
    AVCodecParameters *codecpar;      
    AVRational time_base;             
    int64_t duration;   // 流的时长,单位为 time_base。       
    int64_t nb_frames;                
    int64_t start_time;  // 流的起始时间戳,单位为 time_base。用于确定流的起始位置。
    void *priv_data;   / 流的私有数据,通常与特定的解复用器或复用器相关。
    ...
} AVStream;
  • index:流的索引 ,从 0 开始,标识该流在媒体文件中的位置。媒体文件中第一个流是视频流,其索引为 0第二个流是音频流,其索引为 1
  • codecpar(AVCodecParameters) :指向编解码器参数的指针,存储该流的编解码器相关信息。包括编码类型(音频、视频或字幕)、分辨率、采样率等。
  • time_base:流的时间基准,用于时间戳和实际时间的转换。time_base = {1, 25} 表示每秒 25 帧。
  • nb_frames:流中帧的总数,通常在视频流中有效。对于直播流或未提供帧数信息的文件,此值可能为 0。
  • avg_frame_rate:平均帧率,用于描述视频流的帧率。avg_frame_rate = {30, 1} 表示每秒 30 帧。

假设一个 MP4 文件包含以下流信息:视频流(h.264 编码,1920x1080,30fps)。音频流(AAC 编码,采样率 44100Hz)。使用 AVStream`的相关信息可以解析为:

c 复制代码
AVFormatContext *fmt_ctx = NULL;
avformat_open_input(&fmt_ctx, "input.mp4", NULL, NULL);
avformat_find_stream_info(fmt_ctx, NULL);

AVStream *video_stream = format_ctx->streams[0];
printf("视频流分辨率:%dx%d\n", video_stream->codecpar->width, video_stream->codecpar->height);
printf("视频流帧率:%d/%d fps\n", video_stream->avg_frame_rate.num, video_stream->avg_frame_rate.den);

AVStream *audio_stream = format_ctx->streams[1];
printf("音频采样率:%d Hz\n", audio_stream->codecpar->sample_rate);

1.6 AVCodecParameters

AVCodecParameters 是 FFmpeg 中用于描述流的编码参数 的结构体。它包含了视频或音频流的编码信息 ,例如编码格式、比特率、分辨率等。该结构体通常与 AVStream 一起使用,表示流的编解码参数,供编解码器(codec)使用。

c 复制代码
typedef struct AVCodecParameters {
    AVMediaType codec_type;     
    AVCodecID codec_id;      
    int format;           
    int64_t bit_rate;              // 流的比特率,单位为比特每秒(bps)。
    int width;                   // 视频流的宽度,单位为像素。
    int height;                  // 视频流的高度,单位为像素。

    // 音频流相关字段
    uint64_t channel_layout;      
    int channels;    
    int sample_rate;               
    int frame_size;              
} AVCodecParameters;
  • codec_type: 媒体类型,表示该流是视频流还是音频流。AVMEDIA_TYPE_VIDEO 表示视频流,AVMEDIA_TYPE_AUDIO 表示音频流。

  • codec_id:编解码器ID,表示该流使用的编解码器。例如,AV_CODEC_ID_H264 代表 H.264 编解码器,AV_CODEC_ID_AAC 代表 AAC 编解码器。

  • format:媒体数据格式。对于视频流,表示像素格式(如 AV_PIX_FMT_YUV420P)。对于音频流,表示采样格式(如 AV_SAMPLE_FMT_FLTP)。

  • channel_layout: 音频通道布局,表示音频的声道配置。例如,AV_CH_LAYOUT_STEREO 表示立体声,AV_CH_LAYOUT_MONO 表示单声道。

  • channels: 音频流的声道数。 例如,对于立体声,channels 的值为 2;对于单声道,值为 1。

  • sample_rate:音频流的采样率,单位为赫兹(Hz)。表示每秒钟采样的次数,例如 44100 表示每秒采样 44100 次。

  • frame_size:每帧的采样数,表示音频流中每一帧包含的采样点数。对于音频流,每帧的大小可以根据采样率、声道数等参数计算得到。

AVCodecParameters结构体通常与AVStream一起使用,用来描述流的编解码参数 。在解封装(Demuxing)过程中,**AVFormatContext中的每个AVStream会包含一个AVCodecParameters,该参数描述了流的编码信息。**可以通过 AVStream 获取 AVCodecParameters,并使用它来选择适当的解码器(或编码器)进行解码(或编码)。

c 复制代码
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <stdio.h>

int main() {
    AVFormatContext *format_ctx = NULL;
    AVCodecParameters *codec_params = NULL;

    // 打开媒体文件
    if (avformat_open_input(&format_ctx, "input.mp4", NULL, NULL) < 0) {
        printf("Error opening file\n");
        return -1;
    }

    // 查找流信息
    if (avformat_find_stream_info(format_ctx, NULL) < 0) {
        printf("Error finding stream info\n");
        avformat_close_input(&format_ctx);
        return -1;
    }

    // 遍历流
    for (int i = 0; i < format_ctx->nb_streams; i++) {
        codec_params = format_ctx->streams[i]->codecpar;

        // 打印流的编码参数
        if (codec_params->codec_type == AVMEDIA_TYPE_VIDEO) {
            printf("Video stream:\n");
            printf("Codec ID: %d\n", codec_params->codec_id);
            printf("Width: %d, Height: %d\n", codec_params->width, codec_params->height);
            printf("Bitrate: %d\n", codec_params->bitrate);
        } else if (codec_params->codec_type == AVMEDIA_TYPE_AUDIO) {
            printf("Audio stream:\n");
            printf("Codec ID: %d\n", codec_params->codec_id);
            printf("Sample Rate: %d\n", codec_params->sample_rate);
            printf("Channels: %d\n", codec_params->channels);
        }
    }

    // 清理
    avformat_close_input(&format_ctx);

    return 0;
}

小结:

AVFormatContext :存储媒体文件的格式信息,并包含多个 AVStream

AVCodecParametersAVStream 的一部分,用于存储编码参数。

AVPacket :表示流中的一个数据包,与 AVStream 一起使用,用于解码。

AVFrame:解码后的帧数据,包含时间戳等信息。

二、视频编码与解码

libavcodec为音视频的编解码提供了通用的框架,它包含了大量的编码器和解码器,使用libavcodec库需要了解两个重要的结构体,分别是AVCodecAVCodecContext ,前者主要表征编解码器的实现 ,后者则是表征编解码器的运行时信息即程序运行时当前Codec使用的上下文,着重于所有Codec共有的属性(并且是在程序运行时才能确定的值),其中的codec字段和具体的AVCodec相关联,而priv_data关联具体AVCodec实例的私有运行时的信息。

2.1 视频编码

FFmpeg 提供了一套丰富的编码接口,用于视频或音频数据的编码。这些接口使得开发者能够将原始的音频或视频帧转换为压缩后的编码格式,常用于将媒体数据保存为视频文件或流传输

编码视频或音频数据的过程大致包括以下几个步骤:

  1. 初始化编码器:选择并初始化适当的编码器。
  2. 准备输入数据 :输入数据通常是未压缩的音视频帧
  3. 将数据传递给编码器 :将音频/视频帧数据传递给编码器进行编码。
  4. 获取编码后的数据 :从编码器获取编码后的数据(通常是 AVPacket)。
  5. 写入输出文件/流:将编码后的数据写入文件或通过网络发送。
  6. 释放资源:编码完成后,释放所有资源。

2.1.1 选择编码器

avcodec_find_encoder()是 FFmpeg 中用于查找并返回一个编码器的函数 。它根据传入的编码器 ID(例如 H.264、H.265 等)查找相应的编码器并返回该编码器的指针。如果找不到指定的编码器,函数将返回 NULL

c 复制代码
AVCodec *avcodec_find_encoder(enum AVCodecID id);

id是一个枚举类型,表示要查找的编码器的 ID。FFmpeg 中定义了多个编码器 ID,每个编码器 ID 对应一个特定的视频或音频编码格式,比如压缩效率、速度、支持的格式等。常见的编码器 ID 如下:

  • AV_CODEC_ID_H264:H.264 编码器。
  • AV_CODEC_ID_HEVC:H.265 编码器。
  • AV_CODEC_ID_AAC:AAC 音频编码器。
  • AV_CODEC_ID_MP3:MP3 音频编码器。
  • AV_CODEC_ID_VP8:VP8 视频编码器。
  • AV_CODEC_ID_VP9:VP9 视频编码器。
c 复制代码
AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!codec) {
    fprintf(stderr, "Codec not found\n");
    exit(1);
}

2.1.2 创建编码器上下文

一旦找到了编码器,需要创建编码上下文 AVCodecContext,它存储了与编码器相关的配置信息 。创建编码器上下文通常是通过 avcodec_alloc_context3()来进行的。AVCodecContext 是 **FFmpeg 用于存储编码器或解码器状态的结构体,包含了许多与视频/音频编码或解码过程相关的参数和设置,**如视频分辨率、比特率、时间基、像素格式等参数。

c 复制代码
AVCodecContext *avcodec_alloc_context3(const AVCodec *codec); // codec使用的编码器或解码器
c 复制代码
AVCodecContext *codec_context = avcodec_alloc_context3(codec);
if (!codec_context) {
    fprintf(stderr, "Could not allocate video codec context\n");
    exit(1);
}

2.1.3 设置编码器参数

通过调用avcodec_alloc_context3(),为编码器或解码器提供了一个独立的上下文,这样可以针对不同的编解码器配置不同的参数,而不会发生冲突。在调用avcodec_alloc_context3()之后,通常需要设置 AVCodecContext 结构体中的各个字段,尤其是与编码/解码过程相关的参数(如 bit_ratewidthheighttime_basethread_count等)。

c 复制代码
codec_context->bit_rate = 400000; // 设置比特率
codec_context->width = 1920;      // 设置宽度
codec_context->height = 1080;     // 设置高度
codec_context->time_base = (AVRational){1, 25}; // 设置时间基(帧率)
codec_context->pix_fmt = AV_PIX_FMT_YUV420P;    // 设置像素格式
codec_context->thread_count = 32;      // 设置编码线程数

比特率 决定了编码视频的压缩程度和视频质量。较高的比特率通常会带来更好的视频质量,但文件大小也会增大。较低的比特率会压缩文件,但可能会导致视频质量下降,出现马赛克、模糊等现象。

②设置视频帧的宽度和高度,即视频的分辨率。高分辨率的视频会带来更清晰的画面,但也会增加编码和解码的计算复杂度以及文件大小。常见的视频分辨率有:

  • 1280x720(HD)
  • 1920x1080(Full HD)
  • 3840x2160(4K)

③设置时间基(time base) ,它表示一个时间单位对应多少个刻度。time_base通常与视频的帧率相关,表示每一帧显示的时间长度 。AVRational是一个结构体,包含两个整数(分子和分母),用来表示一个分数值。(AVRational){1, 25}表示每一帧的时间长度是 1/25 秒,即每秒 25 帧,这就是视频的帧率(Frame Rate)。常见值:

  • 1/30:30fps(通常用于高清视频)
  • 1/60:60fps(用于高帧率视频)
  • 1/25:25fps(常见于 PAL 制式的视频)

作用:时间基决定了视频帧的时间戳(PTS 和 DTS)如何计算。它直接影响视频流的播放时间和同步问题。

④像素格式决定了每个像素如何存储在内存中,影响图像的色彩空间、色度子采样以及每个像素的数据量。像素格式决定了视频图像的色彩质量与存储效率。选择合适的像素格式,可以平衡图像质量和压缩效率。

⑤设置编码时使用的线程数。FFmpeg 支持多线程编码,可以在多核 CPU 上利用并行处理来加速编码过程。线程数通常选择为系统 CPU 核数的倍数,或者直接设置为 0(让 FFmpeg 自行决定)。

在 FFmpeg 中,av_dict_set()av_opt_set() 是两个用于设置选项的函数,它们分别用于不同的配置场景。

(1)av_dict_set()

用于设置 AVDictionary中的键值对的函数。AVDictionary是FFmpeg用于存储键值对参数的结构体,通常用于传递额外的配置选项,例如在编码、解码、过滤等过程中设置不同的参数。

c 复制代码
int av_dict_set(AVDictionary **pm, const char *key, const char *value, int flags);
  • pm:指向AVDictionary结构体的指针。AVDictionary用于存储键值对,它的作用类似于其他语言中的字典或映射。**pm 是一个指向字典指针的指针,因为 av_dict_set() 可能会修改字典本身(例如分配内存或者扩展字典)。
  • key:键名用于指定字典中的键(key="bit_rate")。它是一个字符串,用于标识该参数的名称。
  • value:键对应的值(value = "400000")。它也是一个字符串,表示设置给该键的具体值。
  • flags:设置选项的标志,控制字典项的行为。通常可以是:
    • 0:标准方式设置键值对。
    • AV_DICT_DONT_OVERWRITE:如果字典中已存在该键,则不会覆盖现有值。
    • AV_DICT_APPEND:如果字典中已存在该键,则会在其现有值后追加新值。
    • AV_DICT_MERGE:将新值合并到现有值(只适用于列表类型值)。

(2)av_opt_set()

用于设置 FFmpeg 对象(如编解码器、过滤器等)选项的函数。av_opt_set()与 AVDictionary不同,它是针对对象内部的选项进行设置。常用于设置编解码器、过滤器或其他 FFmpeg 组件的各种参数。

c 复制代码
int av_opt_set(void *obj, const char *key, const char *value, int search_flags);
  • obj:这是一个指向对象的指针,表示想设置选项的 FFmpeg 对象。它可以是 AVCodecContext(编码器/解码器上下文)、AVFormatContext(格式上下文)、AVFilterContext(滤镜上下文)等。

  • key:要设置的选项的名称(例如 "bit_rate""preset" 等)。

  • value:设置给key的值。它通常是一个字符串,表示该选项的具体值。

  • search_flags:查找选项时的标志。通常是 0

对于av_opt_set_int()专门用于设置整数类型的参数

c 复制代码
int av_opt_set_int(void *obj, const char *key, int64_t value, int search_flags);

value:设置给选项的值,这里是一个 整数值 。注意,这里是 int64_t 类型,可以传递较大的整数值。

三种设置方法比较:

直接设置结构体成员(如 codec_context->time_base和 codec_context->pix_fmt)是一种最常见且直接的方式,通常适用于大多数场景。而 av_opt_set() 和 av_dict_set()提供了更灵活的方式,适用于需要动态设置、批量配置或者通过字典传递参数的场景。

2.1.4 打开编码器

在设置了所有参数后,可以通过 avcodec_open2() 打开编码器。它的主要作用是初始化和配置 AVCodecContext结构体,使其与指定的编码器或解码器连接,从而能够进行编码或解码操作

c 复制代码
int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
  • AVCodecContext *avctx:编解码器的上下文结构。

  • const AVCodec *codec:要使用的具体编码器或解码器。

  • AVDictionary **options:用于传递额外的配置选项 (如编码器特定的设置)。这些选项将会被传递到编码器或解码器中。如果没有特定的设置,可以传入NULL。AVDictionary是一个键值对结构,用于传递参数。例如,某些编码器可能允许设置比特率、压缩等级等参数。

c 复制代码
if (avcodec_open2(codec_context, codec, NULL) < 0) {
    fprintf(stderr, "Could not open codec\n");
    exit(1);
}

avcodec_open2() 是一个核心函数,它将编码器或解码器与给定的 AVCodecContext 绑定,并初始化它的内部状态,准备好进行数据的编码或解码。这个过程包括:

  • 设置编码器或解码器的内部参数。
  • 为编码器或解码器分配资源,初始化内部缓存。
  • 配置解码器(或编码器)使用的输入输出格式。

在调用avcodec_open2()之前,通常已经选择了一个编码器或解码器,并为 AVCodecContext配置了相关的参数(例如分辨率、帧率等)。调用avcodec_open2()会根据这些设置打开并初始化编码器/解码器。如果打开失败,可能是因为配置不正确,或者编码器/解码器本身不支持某些特性。

2.1.5 编码数据

准备好 AVFrame(即原始数据),然后使用avcodec_receive_packet()avcodec_send_frame() 来编码数据。

avcodec_send_frame()用于将一帧图像(AVFrame)发送到编码器进行编码 。它是发送/接收 模型的一部分,允许将数据逐帧地发送到编码器,而不是像旧版接口那样一次性传递帧并获取包,使得 FFmpeg 支持 流式编码,也就是说可以逐帧地将图像帧送入编码器,而不是一次性处理所有帧。

c 复制代码
int avcodec_send_frame(AVCodecContext *avctx, const AVFrame *frame);
  • avctx:编码器的上下文,包含编码器的设置和当前状态。

  • frame:输入的图像帧(AVFrame),是需要编码的原始图像数据。

avcodec_send_frame()负责将数据帧传递给编码器。在使用这个函数时,需要确保编码器已经准备好接收新的帧。

在调用avcodec_send_frame()发送帧之后,编码器可能已经生成了一个或多个编码后的数据包(视频流) 。avcodec_receive_packet()就是用来接收这些编码后的数据包的

c 复制代码
int avcodec_receive_packet(AVCodecContext *avctx, AVPacket *pkt);
  • avctx:编码器的上下文,包含编码器的设置和当前状态。

  • pkt:输出的编码数据包,用于存储编码后的数据。

  • 返回值

    • 成功:返回 0,表示成功接收编码后的数据包。
    • 无数据可用 :返回 AVERROR(EAGAIN),表示编码器没有生成新的数据包(可以稍后再次调用)。
    • 错误:返回负数,表示发生了错误。

avcodec_receive_packet()用于从编码器中获取编码后的数据包,会返回一个包含编码数据的 AVPacket,然后可以将该数据包保存或进一步处理。在 send/receive 模型下,通常的工作流是:先调用 avcodec_send_frame() 传入原始图像帧,然后通过多次调用 avcodec_receive_packet() 获取编码后的数据包,直到编码器没有数据可返回。

c 复制代码
int ret = avcodec_send_frame(codec_context, frame);
if (ret < 0) {
    // 处理错误
}

ret = avcodec_receive_packet(codec_context, &pkt);
if (ret == 0) {
    // 成功接收到数据包,保存或进一步处理
} else if (ret == AVERROR(EAGAIN)) {
    // 没有数据包可接收,可以稍后再次尝试
} else {
    // 处理错误
}

2.1.6 写入输出

获取到编码后的 AVPacket 后,可以将其写入输出文件或网络流中。通常使用 av_write_frame() 或其他封装器接口(如 avio_write())。也可以直接使用C++的文件操作直接写入文件。

c 复制代码
av_write_frame(output_format_context, &pkt);
av_packet_unref(&pkt);

2.1.7 关闭编码器

编码完成后,释放编码器资源。

c 复制代码
avcodec_close(codec_context);
avcodec_free_context(&codec_context);

以下是一个简单的视频编码示例:

c 复制代码
#include <iostream>
#include <fstream>
using namespace std;

extern "C" { 

    #include <libavcodec/avcodec.h>
}

int main(int argc, char* argv[])
{
    // 设置默认的文件名和编码格式
    string filename = "400_300_25";
    AVCodecID codec_id = AV_CODEC_ID_H264;

    // 根据选择的编码器设置文件扩展名
    if (codec_id == AV_CODEC_ID_H264)
    {
        filename += ".h264";
    }
    else if (codec_id == AV_CODEC_ID_HEVC)
    {
        filename += ".h265";
    }

    // 打开文件输出流,二进制模式
    ofstream ofs;
    ofs.open(filename, ios::binary);

    // 1. 找到指定编码器(H264 或 H265)
    auto codec = avcodec_find_encoder(codec_id);
    if (!codec)
    {
        cerr << "codec not found!" << endl;
        return -1;
    }

    // 2. 为编码器分配编码上下文
    auto c = avcodec_alloc_context3(codec);
    if (!c)
    {
        cerr << "avcodec_alloc_context3 failed!" << endl;
        return -1;
    }

    // 3. 设置编码上下文参数
    c->width = 400; // 视频的宽度
    c->height = 300; // 视频的高度

    // 设置帧时间戳的单位,时间基为 1/25,即每秒 25 帧
    c->time_base = { 1, 25 };

    // 设置像素格式为 YUV420P,这种格式适用于 H264 和 HEVC 编码
    c->pix_fmt = AV_PIX_FMT_YUV420P;

    // 设置编码器线程数,可以通过调用系统接口来获取 CPU 核心数
    c->thread_count = 16;

    // 4. 打开编码器上下文
    int re = avcodec_open2(c, codec, NULL);
    if (re != 0)
    {
        // 获取错误信息并输出
        char buf[1024] = { 0 };
        av_strerror(re, buf, sizeof(buf) - 1);
        cerr << "avcodec_open2 failed!" << buf << endl;
        return -1;
    }
    cout << "avcodec_open2 success!" << endl;

    // 创建 AVFrame,用于存储未压缩的视频数据
    auto frame = av_frame_alloc();
    frame->width = c->width;
    frame->height = c->height;
    frame->format = c->pix_fmt;

    // 为 AVFrame 分配内存(存储未压缩的视频帧)
    re = av_frame_get_buffer(frame, 0);
    if (re != 0)
    {
        // 获取错误信息并输出
        char buf[1024] = { 0 };
        av_strerror(re, buf, sizeof(buf) - 1);
        cerr << "avcodec_open2 failed!" << buf << endl;
        return -1;
    }

    // 创建 AVPacket,用于存储压缩后的视频数据
    auto pkt = av_packet_alloc();

    // 假设要生成 10 秒的视频,每秒 25 帧,250 帧数据
    for (int i = 0; i < 250; i++)
    {
        // 生成未压缩的 AVFrame 数据,每一帧的像素值不同
        // 填充 Y 分量(亮度)
        for (int y = 0; y < c->height; y++)
        {
            for (int x = 0; x < c->width; x++)
            {
                // 填充 Y 分量,简单的示例数据:x + y + 当前帧索引 * 3
                frame->data[0][y * frame->linesize[0] + x] = x + y + i * 3;
            }
        }

        // 填充 UV 分量(色度)
        for (int y = 0; y < c->height / 2; y++)
        {
            for (int x = 0; x < c->width / 2; x++)
            {
                // 填充 U 和 V 分量,简单的示例数据:128 + y + i * 2 和 64 + x + i * 5
                frame->data[1][y * frame->linesize[1] + x] = 128 + y + i * 2;
                frame->data[2][y * frame->linesize[2] + x] = 64 + x + i * 5;
            }
        }

        // 设置帧的显示时间戳
        frame->pts = i;

        // 将未压缩的帧发送到编码器进行编码
        re = avcodec_send_frame(c, frame);
        if (re != 0)
        {
            // 如果返回值不为 0,则编码失败
            break;
        }

        // 循环接收编码后的数据包(一个数据包可能包含多个帧)
        while (re >= 0)
        {
            // 调用 avcodec_receive_packet() 获取压缩后的数据包
            re = avcodec_receive_packet(c, pkt);
            // 如果没有足够的帧进行编码,返回 EAGAIN 或 EOF 错误码
            if (re == AVERROR(EAGAIN) || re == AVERROR_EOF)
                break;

            // 如果发生其他错误,输出错误信息
            if (re < 0)
            {
                char buf[1024] = { 0 };
                av_strerror(re, buf, sizeof(buf) - 1);
                cerr << "avcodec_receive_packet failed!" << buf << endl;
                break;
            }

            // 输出当前压缩帧的大小(调试信息)
            cout << pkt->size << " " << flush;

            // 将压缩后的数据写入文件
            ofs.write((char*)pkt->data, pkt->size);

            // 释放 AVPacket 占用的空间
            av_packet_unref(pkt);
        }
    }

    // 关闭输出文件
    ofs.close();

    // 释放 AVPacket 和 AVFrame 的内存
    av_packet_free(&pkt);
    av_frame_free(&frame);

    // 释放编码器上下文的内存
    avcodec_free_context(&c);

    return 0;
}

总帧数 = I 帧数 + P 帧数 + B 帧数,总共190帧,没有达到程序设置中的250帧,这是因为在视频编码结束时,编码器仍可能有一些未输出的编码数据(例如,帧之间的依赖关系处理或B帧的依赖),这部分数据通常会保存在编码器的内部缓冲区,直到发送 NULL 帧,要求编码器输出所有缓存中的数据。

解决:通过向编码器发送 NULL 帧(结束帧)来确保所有编码的内容都被处理,并接收缓存中的所有编码结果。

2.2 视频解码

FFmpeg 解码过程包括:

  1. 查找解码器(avcodec_find_decoder
  2. 分配解码上下文(avcodec_alloc_context3
  3. 打开解码器(avcodec_open2
  4. 发送压缩数据包给解码器(avcodec_send_packet
  5. 接收解码后的帧(avcodec_receive_frame

这个过程需要反复执行,直到所有数据包都被处理完。

2.2.1 查找解码器

avcodec_find_decoder用于查找支持指定解码器的AVCodec对象。此函数的作用是根据给定的解码器 ID(AVCodecID),返回一个相应的解码器对象,该结构体包含了解码器的相关信息。这个解码器对象可以用于解码多种不同格式的视频或音频数据。

c 复制代码
AVCodec *avcodec_find_decoder(enum AVCodecID id);

id:这是一个枚举类型AVCodecID,表示解码器的 ID。AVCodecID定义了各种音视频编码格式的标识符,如 AV_CODEC_ID_H264AV_CODEC_ID_MP3 等。与前面编码器类似,这里就不过多赘述。

c 复制代码
AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (codec == NULL) {
    printf("Codec not found\n");
} else {
    printf("Codec found: %s\n", codec->name);
}

2.2.2 分配解码上下文

avcodec_alloc_context3用于为解码器或编码器分配并初始化一个AVCodecContext结构体。AVCodecContext是与特定编解码器相关的上下文结构,包含了编解码器的状态、参数等信息。参照编码部分的介绍。

2.2.3 打开解码器

avcodec_open2用于打开并初始化一个指定的编解码器。**它将AVCodecContext结构体与实际的编解码器绑定,并进行相关的解码或编码设置。**该函数通常在分配并配置了AVCodecContext后调用,用来启动编解码过程。参照编码部分的介绍。

2.2.4 解码数据

avcodec_send_packetavcodec_receive_frame 用于解码数据流 。解码操作采用发送-接收模式,其中avcodec_send_packet用于将数据包传递给解码器,而 avcodec_receive_frame用于从解码器获取解码后的帧。这个模式在处理视频流时非常常见,可以有效地分离输入和输出操作。

①avcodec_send_packet

avcodec_send_packet函数用于将一个数据包(通常是编码的数据)发送给解码器进行解码。调用这个函数会将一个数据包传入解码器,然后解码器会处理这个数据包,直到它有足够的信息来输出一个解码帧。

c 复制代码
int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *pkt);
  • avctx:指向AVCodecContext的指针,表示解码器上下文。它包含了解码器的配置信息。

  • pkt:指向AVPacket的指针,表示待解码的数据包。

  • 返回值

    • 返回 0 表示成功。
    • 返回负值表示错误。常见的错误包括:
      • AVERROR(EAGAIN):表示解码器的内部缓冲区已满,尚未准备好接收更多数据。
      • AVERROR_EOF:表示数据包已经结束。
      • AVERROR(EINVAL):表示参数无效。

②avcodec_receive_frame

avcodec_receive_frame函数用于从解码器中获取解码后的帧 。解码器在接收到一个数据包并成功解码后,帧会被存储在输出缓冲区中,调用该函数即可获取解码后的帧数据

c 复制代码
int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);
  • avctx:指向AVCodecContext的指针,表示解码器上下文。

  • frame:指向AVFrame的指针,表示解码后的帧。

  • 返回值:

    • 返回 0 表示成功并且成功地接收到了一个帧。
    • 返回负值表示错误,常见的错误包括:
      • AVERROR(EAGAIN):表示解码器没有足够的数据来生成一个完整的帧。需要更多数据包来继续解码。
      • AVERROR_EOF:表示没有更多的帧可用,通常在流的末尾时发生。
      • AVERROR(EINVAL):表示参数无效。

2.2.5 H264 帧分割

av_parser_init 用于初始化一个编解码器解析器上下文(AVCodecParserContext)。解析器用于从视频或音频流中提取数据包(例如,H.264、H.265、MP3 等格式)。它通过对**输入数据进行逐字节解析,**帮助 FFmpeg 库在解码前提取出完整的数据包。av_parser_init函数将为给定的编解码器 ID 创建一个新的解析器上下文。

c 复制代码
AVCodecParserContext *av_parser_init(int codec_id);
  • codec_id:表示要初始化的编解码器类型。通常是AVCodecID枚举值之一,表示特定的编解码器(例如,AV_CODEC_ID_H264、AV_CODEC_ID_MP3等)。

  • 返回值:返回一个指向AVCodecParserContext结构体的指针,表示成功初始化的解析器上下文。如果解析器无法初始化,返回 NULL

AVCodecParserContext结构体用于管理解析器的状态,并保存输入数据的解析信息。通过该上下文,解码器可以从原始流中提取一个完整的数据包,以便后续的解码处理。该函数通常与av_parser_parse2配合使用。

av_parser_parse2用于将原始流中的数据传递给解析器,以便提取出完整的数据包。

c 复制代码
int av_parser_parse2(AVCodecParserContext *s, AVCodecContext *avctx,
                     uint8_t **poutbuf, int *poutbuf_size,
                     uint8_t *buf, int buf_size);
  • s:表示解析器上下文,是在av_parser_init函数中初始化的,它会保持解析过程的状态。

  • avctx:表示编解码器上下文。这个参数通常在解码或编码时需要传递,用于解码或编码的设置。如果仅进行解析,可以传递 NULL

  • poutbuf:指向uint8_t指针,返回提取的有效数据包。当函数成功解析数据时,poutbuf将指向解码器所需的完整数据包。

  • poutbuf_size:返回poutbuf指向的有效数据包的大小

  • buf:指向输入数据缓冲区的指针,这些数据是未解压缩的流数据(例如压缩的视频或音频数据)。该缓冲区包含了来自原始数据流的部分数据包。

  • buf_size:输入数据缓冲区的大小。

  • 返回值:

    • 返回0表示成功并且已经提取了一个完整的数据包。
    • 如果输入数据不完整,返回 AVERROR(EAGAIN),表示需要更多数据才能提取出有效数据包。
    • 如果发生错误,返回负值,通常是 AVERROR(EINVAL),表示无效参数。

av_parser_parse2是一个数据包解析函数。它会读取输入数据缓冲区中的数据,并将其解析成一个完整的编解码数据包。解析器根据数据流的格式(如 H.264、H.265、MP3 等)来判断如何从中提取有效的数据包。解析后的数据包会存储在输出缓冲区(poutbuf)中,供后续的解码或其他处理使用。

在处理流式数据时尤其有用,比如从网络或文件中逐块读取数据并进行解码。av_parser_parse2会不断接收数据块,直到它能够从中提取出一个完整的、可用于解码的数据包。

av_parser_close用于释放与解析器上下文相关的资源

c 复制代码
void av_parser_close(AVCodecParserContext *s); //s:表示解析器上下文。

av_parser_close会释放与解析器上下文AVCodecParserContext相关的资源。它是 FFmpeg 库中的清理函数,确保在不再需要解析器时正确释放内存,避免内存泄漏。

2.2.6 解码硬件加速 DXVA2

实现了一个简单的 H.264 解码流程,通过 FFmpeg 解码视频文件,并在自定义窗口上显示解码后的视频帧。同时,计算并显示帧率。

c 复制代码
    // 创建一个 XVideoView 对象,用于显示解码后的帧
    auto view = XVideoView::Create();
	// 1. 读取 H.264 流文件
    string filename = "/home/xiaochao/RTSP/test.h264";  // 需要解码的 H.264 文件
    ifstream ifs(filename, ios::binary);  // 以二进制方式打开文件

    unsigned char inbuf[4096] = { 0 };  // 用于存储读取的数据
    AVCodecID codec_id = AV_CODEC_ID_H264;  // 设置解码器为 H.264

    // 2. 查找 H.264 解码器
    auto codec = avcodec_find_decoder(codec_id);
    if (!codec) {
        cerr << "Codec not found!" << endl;
    }

    // 3. 创建解码器上下文
    auto c = avcodec_alloc_context3(codec);
    if (!c) {
        cerr << "Could not allocate codec context!" << endl;
    }

    // 配置解码器线程数为 16
    c->thread_count = 16;

    // 4. 打开解码器上下文
    if (avcodec_open2(c, NULL, NULL) < 0) {
        cerr << "Could not open codec!" << endl;
    }

    // 5. 创建一个解析器上下文,用于解析 H.264 流中的数据包
    auto parser = av_parser_init(codec_id);

    // 6. 创建用于存储数据包和帧的对象
    auto pkt = av_packet_alloc();  // AVPacket 用于存储数据包
    auto frame = av_frame_alloc();  // AVFrame 用于存储解码后的帧

    auto begin = NowMs();  // 获取当前时间(毫秒)
    int count = 0;  // 解码的帧计数
    bool is_init_win = false;  // 是否初始化窗口

    // 7. 逐块读取 H.264 数据并解码
    while (!ifs.eof())
    {
        ifs.read((char*)inbuf, sizeof(inbuf));  // 读取数据块
        int data_size = ifs.gcount();  // 获取读取的字节数
        if (data_size <= 0) break;  // 如果没有数据,跳出循环
        auto data = inbuf;

        while (data_size > 0)  // 一次可能有多帧数据
        {
            // 使用解析器解析数据包,直到能够提取一个完整的帧
            int ret = av_parser_parse2(parser, c, &pkt->data, &pkt->size, data, data_size, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
            data += ret;  // 更新数据指针
            data_size -= ret;  // 剩余数据大小

            if (pkt->size)  // 如果提取到了一个有效的数据包
            {
                // 将数据包发送到解码器
                ret = avcodec_send_packet(c, pkt);
                if (ret < 0) break;

                // 获取解码后的帧
                while (ret >= 0)
                {
                    ret = avcodec_receive_frame(c, frame);
                    if (ret < 0) break;

                    // 输出帧的格式
                    cout << frame->format << " " << flush;

                    // 初始化显示窗口
                    if (!is_init_win)
                    {
                        is_init_win = true;
                        view->Init(frame->width, frame->height, (XVideoView::Format)frame->format);  // 初始化窗口
                    }
                    view->DrawFrame(frame);  // 绘制当前帧

                    count++;  // 增加解码帧计数
                    auto cur = NowMs();  // 获取当前时间
                    if (cur - begin >= 100)  // 每 1/10 秒更新一次 FPS
                    {
                        cout << "\nfps = " << count * 10 << endl;
                        count = 0;  // 重置帧数
                        begin = cur;  // 更新时间
                    }
                }
            }
        }
    }

    // 8. 发送空包,处理可能残留的数据
    int ret = avcodec_send_packet(c, NULL);
    while (ret >= 0)
    {
        ret = avcodec_receive_frame(c, frame);
        if (ret < 0) break;
        cout << frame->format << "-" << flush;
    }

    // 9. 清理资源
    av_parser_close(parser);  // 关闭解析器上下文
    avcodec_free_context(&c);  // 释放解码器上下文
    av_frame_free(&frame);  // 释放帧内存
    av_packet_free(&pkt);  // 释放数据包内存

inbuf 设置为 4096 字节是为了保证每次读取的数据量足够大,可以容纳多个 NALU 或帧的数据,且不会因为每次读取的数据过小而造成频繁的读取操作。同时,FFmpeg 的解析器会根据实际读取的数据内容进行解析,将其切分成完整的帧,并送到解码器进行解码。

三、视频封装与解封装

libavformat 是 FFmpeg 中处理音频、视频和字幕等封装和解封装的通用框架,内置了大量多媒体格式的Muxer和Demuxer,它支持AVInputFormat输入容器和AVOutputFormat输出容器,同时也支持基于网络的一些流媒体协议,如HTTP、RTSP、RTMP等。

3.1 视频封装

视频封装 的目的是将已经编码的视频、音频和其他多媒体数据(如字幕、章节信息等)组合成一个文件。

这是为了便于存储、传输和播放,封装的目的有几个方面:

  • 多媒体数据的组合和统一管理

    封装将编码后的视频流、音频流、字幕流以及其他辅助数据(如元数据、章节信息 等)合并在一个文件中,方便管理和播放。封装文件中的时间戳确保音频、视频和字幕同步播放,避免不同流出现错位的问题。

  • 指定编码格式、帧率、时长等参数

    通过封装,文件会记录视频和音频的关键参数 (如编码格式、分辨率、帧率、比特率、时长等),使播放器能够正确解码和播放。某些封装格式(如 MP4、MKV)被广泛支持,通过封装,可以确保文件能够在多种设备和操作系统中正常播放。

  • 支持视频帧索引存储

    封装格式通常会存储关键帧的索引信息,播放器可以根据索引快速定位到特定帧,实现快速跳转(如进度条拖动时无延迟)。支持非线性播放,可以方便地实现功能如快进、倒退、循环播放。

虽然编码和封装经常一起出现,但它们是两个不同的过程:

  1. 编码 (Compression/Encoding):
    • 编码是将原始的视频、音频数据(通常是未压缩的)转化为压缩格式 的过程。编码的目的是减少文件的大小,便于存储和传输 。常见的视频编码格式有 H.264、H.265 、VP8 等;音频编码格式有 AAC、MP3、Opus 等。
    • 编码过程中使用压缩算法(如 H.264、HEVC)将数据压缩去掉冗余信息,以减小数据量
  2. 封装 (Muxing/Packaging):
    • 封装是将编码后的视频流、音频流、字幕等多媒体数据组合成一个文件的过程。封装不涉及数据的压缩,而是组织和结构化存储。封装后的文件可以包括多个不同的媒体流。
    • 常见的封装格式有 MP4、MKV、AVI、MOV、WebM 等。

编码是数据压缩的过程 ,它将未压缩的音视频数据(通常非常庞大)转换成一种特定的格式,减小数据体积。封装是容器化的过程 ,它将编码后的音视频数据以及其他相关内容(如字幕、元数据等)整合到一个文件或流中,以便于存储、传输和播放。编码后,得到的是一堆压缩的字节数据,但它们还不能直接播放。封装将这些压缩的字节数据组织成一个可播放的文件,例如将视频、音频和字幕合并到一个 MP4 文件中。

MP4(MPEG-4 Part 14)是一种广泛使用的视频和音频容器格式 ,它支持包括视频、音频、字幕等多种多媒体数据。MP4 格式采用ISO Base Media File Format(ISO 14496-12)作为其基础结构,并通过不同的 Box(盒子) 来组织和存储多种信息。每个 Box 中包含了不同的元数据,帮助播放器理解如何处理视频、音频等流。

  • File Type Box (ftyp) :用于文件格式识别,标识该文件为 MP4 格式及其支持的版本。

  • Movie Box (moov):这是 MP4 文件中的核心部分,包含了整个电影的所有描述信息,如时长、帧率、音轨信息等。

    • Movie Header Box (mvhd):存储视频的总时长、时间基准、帧率等元数据。

      • duration:视频的总时长。
      • timescale:时间基准,表示时间单位的数量(如每秒多少帧)。
      • rate:视频的播放速率,通常为 1.0 表示正常播放。
      • volume:最大音量。
    • Track Box (trak) :一个 MP4 文件可以包含多个轨道 (视频、音频、字幕等),每个轨道会有一个 trak Box。

      • Track Header Box (tkhd):描述轨道的基本信息,如视频的宽高、时长、音频的最大音量等。
      • Sample Table Box (stbl):存储有关样本(视频帧、音频帧等)相关的详细信息,包括样本的大小、时间戳、帧偏移量等。
      • Sample Size Box (stsz/stz2):
        • stsz:记录每个样本的大小信息,对于不均匀大小的样本,存储每个样本的字节数。
        • stz2:提供更加高效的样本大小存储方法,通常在视频编码中使用

媒体流封装(Muxing)过程主要指以AVPackets的形式获取编码后的数据后,以指定的容器格式将其写入文件或以其他方式输出到字节流中。Muxing实际执行的主要API调用流程如下:

  • 初始化,avformat_alloc_output_context2()

  • 创建媒体流(如果有的话),avformat_new_stream()

  • 写文件头,avformat_write_header()

  • 写数据包,av_write_frame()/av_interleaved_write_frame()

  • 写文件尾部信息并释放内部资源,av_write_trailer()

3.1.1 创建封装上下文

封装上下文AVFormatContext是封装文件的基本结构体,它包含了多媒体文件的各种信息,包括文件格式、流信息、输出文件的元数据等。使用avformat_alloc_output_context2创建一个新的输出格式上下文。

avformat_alloc_output_context2用于创建输出格式上下文 (AVFormatContext) ,主要用于为视频封装创建一个新的格式上下文。在使用该函数时,可以指定输出文件的格式、文件路径以及其他输出相关的选项。

c 复制代码
int avformat_alloc_output_context2(AVFormatContext **ctx, const AVOutputFormat *oformat, const char *format_name, const char *filename);
  • ctx:指向要分配并初始化的格式上下文指针。该函数会分配一个新的AVFormatContext结构体,并将其赋值给该指针。

  • oformat:可选参数,指定输出格式 。通常,这个参数是 NULL,FFmpeg 会根据文件扩展名自动推断。

  • format_name:可选参数,指定容器格式的名称 (如 mp4movmkv)。通常情况下,设置为 NULL 即可,FFmpeg 会自动根据文件扩展名来选择格式。

  • filename: 输出文件的名称或路径。可以是一个绝对路径或相对路径,或者是一个 RTMP 或 RTSP URL。

c 复制代码
AVFormatContext *output_ctx = NULL;
if (avformat_alloc_output_context2(&output_ctx, NULL, NULL, "output.mp4") < 0) {
    // 错误处理
}

3.1.2 创建媒体流

添加音频流和视频流(AVStream)。封装视频时,需要将音视频流添加到封装上下文中 。音视频流是 AVFormatContext 中的核心元素,每个流包含了编码信息、解码参数、时间戳等。使用avformat_new_stream向封装格式上下文中添加新的流(音频流或视频流);avcodec_parameters_copy复制音视频流的编解码参数到AVStream中。

avformat_new_stream用于向 AVFormatContext添加新的流(视频流、音频流等)。每个流代表着多媒体内容中的一种数据类型(例如视频、音频、字幕等)。使用此函数可以在封装输出文件时添加流,并为其分配一个新的AVStream结构体。

c 复制代码
AVStream *avformat_new_stream(AVFormatContext *s, const AVCodec *c);
  • s:指向已初始化的AVFormatContext的指针,表示输出封装的上下文。该上下文用于管理所有的流信息、输出格式等。

  • c:可选参数,指向AVCodec的指针,用于指定流的编解码器。如果该参数为 NULL,则默认使用封装格式所需要的编解码器。如果是创建视频流,通常会传入 AVCodec对象,表示视频流的编码方式。如果是创建音频流,传入相应的音频编码器。

  • 返回值是指向新创建的AVStream的指针,表示成功创建了一个新的流。如果返回 NULL,表示流创建失败。

avcodec_parameters_copy 用于复制 AVCodecParameters 结构体数据。它主要用于将一个流的编码参数复制到另一个流,通常用于在解封装时将输入流的编码参数传递到输出流,或者用于在处理流时共享编码参数。

c 复制代码
int avcodec_parameters_copy(AVCodecParameters *dst, const AVCodecParameters *src);
  • dst: 复制的数据会被填充到这个结构体中。通常是目标流的编码参数。

  • src:数据将从该结构体复制到目标结构体。通常是来源流的编码参数。

c 复制代码
AVStream *video_stream = avformat_new_stream(output_ctx, NULL);  // 创建视频流
AVStream *audio_stream = avformat_new_stream(output_ctx, NULL);  // 创建音频流

AVCodecParameters *video_par = input_video_ctx->streams[0]->codecpar;
avcodec_parameters_copy(video_stream->codecpar, video_par);

AVCodecParameters *audio_par = input_audio_ctx->streams[0]->codecpar;
avcodec_parameters_copy(audio_stream->codecpar, audio_par);

3.1.3 打开输出 I/O

打开输出 I/O(AVIOContext),打开输出文件并初始化输出流 。FFmpeg 需要一个输出 I/O 上下文来写入数据。如果封装格式没有内嵌 I/O,必须通过 avio_open 手动打开输出文件。

avio_open用于打开指定的 I/O 流(文件或其他类型的流) ,此函数与 AVIOContext 结构体紧密结合,AVIOContext 是用于表示一个 I/O 操作上下文,提供了对文件或其他数据流的读写支持

c 复制代码
int avio_open(AVIOContext **s, const char *url, int flags);
  • s: 指向AVIOContext指针的指针,这个指针将在函数执行完后指向一个新的AVIOContext。AVIOContext用来进行文件或其他 I/O 操作

  • url:输入的 URL,指定要打开的文件或网络流的路径。URL 可以是本地文件路径,也可以是网络地址(例如 RTSP 流、HTTP 流等)。

  • flags:该参数用于指定打开文件的模式,类似于fopen中的文件访问模式。常用的标志如下:

    • AVIO_FLAG_READ:以读取模式打开文件。
    • AVIO_FLAG_WRITE:以写入模式打开文件。
    • AVIO_FLAG_READ_WRITE:以读写模式打开文件。
    • AVIO_FLAG_DIRECT:启用直接 I/O 操作(绕过系统缓冲区,直接从磁盘读取)。
c 复制代码
if (avio_open(&output_ctx->pb, "output.mp4", AVIO_FLAG_WRITE) < 0) {
    // 错误处理
}

3.1.4 写入文件头

在开始写入数据前,必须先写入文件的头部信息 。FFmpeg 会根据流的信息生成文件头,并写入封装文件。avformat_write_header写入文件头。此函数会创建文件的结构,初始化文件中的多媒体流,并做必要的准备工作。

avformat_write_header用于写入媒体文件头 。它用于封装操作的输出文件中,在实际写入视频和音频帧数据之前,必须调用该函数来写入必要的头部信息,如流的格式、编解码器参数等。此时会写入必要的封装格式信息,为后续的数据帧写入做准备。

c 复制代码
int avformat_write_header(AVFormatContext *s, AVDictionary **options);
  • s: 指向已初始化的 AVFormatContext结构体,表示封装文件的输出上下文

  • options:用于设置一些写入头部时的选项。这个参数是一个字典,可以包含一些特定的参数。通常为 NULL,但是在某些情况下,可以提供特定的选项(如指定一些额外的参数,例如 GOP 大小、码率限制等)。

c 复制代码
if (avformat_write_header(output_ctx, NULL) < 0) {
    // 错误处理
}

3.1.5 写入帧数据

写入帧数据 (需要考虑写入次序,av_write_frame),写入音频和视频的每一帧数据。封装时需要注意数据包(AVPacket)的顺序以及时间戳等信息。必须根据流的顺序先写入视频流,然后写入音频流,确保数据的同步。**av_read_frame从输入文件中读取数据包。av_write_frame将读取的数据包写入输出封装文件。**注意:每次读取帧数据后,需要清理AVPacket,防止内存泄漏。使用av_packet_unref来释放资源。

av_read_frame 用于从输入流中读取一帧数据。它可以用于解封装操作,从媒体文件或流中读取音视频帧。它的主要作用是将数据包(AVPacket)从输入流中提取出来,供解码器或其他后续操作使用。

c 复制代码
int av_read_frame(AVFormatContext *s, AVPacket *pkt);
  • s:表示封装文件或流的上下文,它包含了与流相关的所有信息(例如流的数量、解封装器的状态等)。

  • pkt: 指向AVPacket的指针,该结构体将被填充为从流中读取的帧数据AVPacket 包含了压缩后的音视频数据(即数据包)。

av_write_frame用于将数据包写入到输出流。它的主要作用是将已经编码好的音视频数(AVPacket)写入到封装文件或网络流中。该函数通常用于封装音视频数据并写入到输出文件(例如 MP4、MKV 等格式)。

c 复制代码
int av_write_frame(AVFormatContext *s, AVPacket *pkt);
  • s:封装文件或流的上下文。

  • pkt:表示需要写入的数据包(音频帧或视频帧)。通常这是已经通过解码或其他方式准备好的编码后的音视频数据包。

av_write_frame直接将AVPacket写入输出文件(如.mp4、.ts、.mkv等格式)。不保证音视频流交错存储 (即不会主动处理音视频帧的同步问题)。适用于不需要严格交错的格式(如音频流)

要保证保证音视频交错存储 (即按时间戳顺序排列),使用av_interleaved_write_frame。该函数适用于需要严格同步音视频的容器格式 (如 .mp4.mkv),适用于 多流文件(音频 + 视频)编码 ,通常比 av_write_frame() 更常用。

c 复制代码
int av_interleaved_write_frame(AVFormatContext *s, AVPacket *pkt);

推荐使用av_interleaved_write_frame(),除非明确知道 av_write_frame() 更适合你的应用场景

c 复制代码
AVPacket packet;
while (av_read_frame(input_video_ctx, &packet) >= 0) {
    if (packet.stream_index == video_stream->index) {
        packet.stream_index = video_stream->index;
        av_interleaved_write_frame(output_ctx, &packet);
    }
    av_packet_unref(&packet);
}

3.1.6 写入尾部数据

写入尾部数据。尾部数据主要包含文件的结束信息,指示文件已经写入完成 。在视频封装过程中,PTS(显示时间戳)索引也会在此时进行更新。av_write_trailer写入文件尾部信息

av_write_trailer 用于写入封装文件尾部数据 。它通常在封装过程的最后调用,用来写入文件尾部的必要信息,例如流的索引、时间戳等,以确保封装文件的完整性和正确性。

c 复制代码
int av_write_trailer(AVFormatContext *s);

av_write_trailer用于封装文件的结尾部分。在封装过程中,通常会先写入文件头(通过 avformat_write_header),然后逐帧写入音视频数据(通过av_write_frame),最后使用 av_write_trailer写入尾部数据。它确保所有流的索引和时间戳等信息都被正确写入文件的尾部。该函数通常不需要频繁调用,仅在封装操作结束时调用一次。

c 复制代码
av_write_trailer(output_ctx);

在 FFmpeg 中,封装视频的步骤主要包括创建封装上下文、添加音视频流、打开输出文件、写入文件头、逐帧写入数据,最后写入尾部数据。通过正确的函数调用,可以轻松将编码后的音视频流封装为一个目标格式的文件。

3.1.7 其它相关函数

(1)av_seek_frame控制播放进度

av_seek_frame 是 FFmpeg 中用于在流媒体文件中进行随机访问的函数。**它允许用户根据给定的时间戳(或帧索引)定位到文件的某个特定位置,通常用于快速跳转到指定的帧,避免从头开始解码或解封装数据。**这个函数常用于视频播放器、编辑器等应用中,让用户能够在视频流中跳转到特定的时间点。

c 复制代码
int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp, int flags);
  • s:封装文件的上下文。

  • stream_index:表示需要定位的流的索引 。如果该值为 -1,则表示搜索所有流。常见的流索引包括音频流、视频流等。

  • timestamp:表示要定位到的时间戳 (单位为微秒),它指示了所需跳转的时间点 。如果设置为 AV_NOPTS_VALUE,则表示按帧索引进行定位。

  • flags:表示跳转操作的选项,通常可以是以下几种值的组合:

    • AVSEEK_FLAG_BACKWARD:向前查找 ,跳转到指定时间戳之前的最近关键帧
    • AVSEEK_FLAG_ANY:寻找任何帧(不一定是关键帧),这意味着可以跳到指定时间戳附近的任何帧。
    • AVSEEK_FLAG_FRAME:按帧进行查找,跳转到指定帧的准确位置(如果支持的话)
    • AVSEEK_FLAG_BYTE:按字节进行查找。

av_seek_frame可以用于视频的跳转操作,使得播放器可以快速定位到某个时间点。这对于大视频文件尤为重要,避免了重新从头开始解码整个文件。

timestamp是基于AVStream.time_base的单位(一般是微秒)。通过该时间戳可以快速跳转到某个时间点的位置。

函数会自动定位到最近的关键帧 (如果使用了 AVSEEK_FLAG_BACKWARD 标志)。如果跳转到的时间点不是关键帧,解码器会要求从该位置重新解码

(2)av_rescale_q_rndPTS计算

PTS计算公式
P T S = a ∗ b q / c q PTS = a * bq / cq PTS=a∗bq/cq

其中:

  • a是待计算的时间戳或某个值(通常是帧的显示时间)。
  • bq是源时间基(比如编码器的时间基)。
  • cq是目标时间基(通常是解码器的时间基)。

由于在处理视频或音频时,不同的编码格式可能使用不同的时间基,所以需要通过此公式来调整时间基,确保正确的时间戳计算。

av_rescale_q_rnd用于对时间戳、帧率等值进行缩放转换。这个函数主要的作用是将一个时间值从一种单位转换为另一种单位,并且根据需要进行四舍五入。

c 复制代码
int64_t av_rescale_q_rnd(int64_t a, AVRational b, AVRational c, int rnd);
  • a: 要转换的值 ,通常是一个时间戳或帧数等数值。

  • b: 当前值的单位表示,通常是一个AVRational结构体,表示分子和分母。

  • c: 目标单位的表示,也是一个AVRational结构体。

  • rnd: 指定如何进行四舍五入的方式

    • AV_ROUND_ZERO = 0: 向零舍入。

    • AV_ROUND_INF = 1: 远离向零舍入,表示向绝对值更大的方向舍入。

    • AV_ROUND_DOWN = 2: 向下舍入。

    • AV_ROUND_UP = 3: 向上舍入。

    • AV_ROUND_NEAR_INF = 5: 最近邻舍入(四舍五入)。

    • AV_ROUND_PASS_MINMAX = 8192:传递INT64_MIN和 NT64_MAX,而不是进行重缩放。这通常用于处理特殊值AV_NOPTS_VALUE(如时间戳无效时)。

  • 返回值是转换后的值,单位为目标单位 c。

3.2 视频解封装

在视频播放、转码及转封装中,视频媒体文件的解封装(Demuxing)是最基本的操作。一般而言,**解封装过程需要读取一个媒体文件并将其分割成若干数据块(数据包,在FFmpeg中使用AVPacket表示),一个数据包包含一个或多个属于一个基本流的编码帧。**在lavf的API中,这个过程先使用函数avformat_open_input()打开一个文件,然后使用av_read_frame()循环读取每一个数据包,最后由函数avformat_close_input()执行清理操作。其基本步骤如下:

3.2.1 打开媒体文件

使用 avformat_open_input() 打开输入文件,并读取文件格式信息。avformat_open_input用于**打开媒体文件(如视频文件、音频文件等)并初始化与该文件相关的AVFormatContext。**它通常是解封装(Demuxing)操作的第一步,目的是打开输入文件并为其分配必要的内存。

c 复制代码
int avformat_open_input(AVFormatContext **ps, const char *filename, AVInputFormat *fmt, AVDictionary **options);
  • ps:指向AVFormatContext指针的指针。在函数调用之后,AVFormatContext对象将被创建并填充数据,指向该对象的指针将被返回。

  • filename:需要打开的输入文件的文件路径或 URL。

  • fmt:指定输入文件的格式。如果为NULL,FFmpeg 会自动检测文件格式并选择合适的解封装器。

  • options:用于设置一些额外的参数,通常与输入文件的解码相关。如果没有额外设置,可以将其设置为 NULL

c 复制代码
AVFormatContext *fmt_ctx = NULL;
if (avformat_open_input(&fmt_ctx, "input.mp4", NULL, NULL) < 0) {
    fprintf(stderr, "Could not open input file\n");
    return -1;
}

3.2.2 获取流信息

使用 avformat_find_stream_info() 解析媒体流信息(音视频流)。avformat_find_stream_info是用于查找输入文件或流信息 的函数。它通常在打开媒体文件(如视频、音频等)后,调用此函数来获取文件中的流(视频流、音频流等)的详细信息

c 复制代码
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);

在调用avformat_open_input打开文件之后,avformat_find_stream_info会从媒体文件中提取流信息并填充到 AVFormatContext中。流信息包括视频流、音频流、字幕流等的编码信息、时长、比特率等。

c 复制代码
if (avformat_find_stream_info(fmt_ctx, NULL) < 0) {
    fprintf(stderr, "Failed to retrieve stream info\n");
    return -1;
}

该函数解析 视频流、音频流、字幕流 等信息,存储在 fmt_ctx->streams[] 数组中。该步骤有助于 FFmpeg 自动检测帧率、分辨率、编解码器,并为解码器提供更精准的参数。

3.2.3 查找音视频流索引

查找音视频流索引 是确定文件中视频流音频流AVFormatContext 结构体中的索引(stream_index),以便后续 提取、解码和处理 正确的数据流。一个多媒体文件(如 .mp4、.mkv)通常包含多个 流(Stream),包括:

  • 视频流AVMEDIA_TYPE_VIDEO
  • 音频流AVMEDIA_TYPE_AUDIO
  • 字幕流AVMEDIA_TYPE_SUBTITLE

FFmpeg需要区分不同类型的数据 ,正确读取音频或视频数据,因此必须 确定流的索引(stream_index 。在AVFormatContext结构体中,streams[] 数组存储了所有的流信息 。可以遍历 streams[] 来找到视频流的索引。

c 复制代码
int video_stream_index = -1;
for (int i = 0; i < fmt_ctx->nb_streams; i++) {
    if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
        video_stream_index = i;
        break;
    }
}
if (video_stream_index == -1) {
    fprintf(stderr, "No video stream found\n");
    return -1;
}

3.2.4 读取数据包

使用 av_read_frame() 循环读取 压缩数据包(AVPacket) 。av_read_frame是用于从已打开的媒体文件或流中读取一帧数据的函数。它通常用于解封装(demuxing)过程,即从容器中提取音视频数据帧。该函数会把读取到的帧数据存储在AVPacket结构体中,供后续解码使用。

c 复制代码
int av_read_frame(AVFormatContext *s, AVPacket *pkt);

av_read_frame主要用于读取输入流中的一帧数据(无论是音频帧还是视频帧)。该函数会将读取到的数据填充到AVPacket中,这个数据包可以被后续的解码器解码为实际的音频或视频帧,并根据文件的不同格式(如 MP4、AVI、MKV 等)将不同的流(视频流、音频流)分离开来。

在使用av_read_frame读取一帧数据后,pkt中会填充以下重要字段:

  • pkt->pts:表示帧的显示时间 (呈现时间戳),单位通常是流的时间基(AVStream.time_base)。pts用于决定帧的显示顺序。
  • pkt->dts:表示解码时间戳,指示该帧的解码顺序。在解码时,dts用来确定正确的解码顺序,尤其在 GOP(图像组)结构的编码中,dts和 pts可能不同。
  • pkt->duration:表示当前帧的持续时间,单位是流的时间基准。对于视频流,每个帧的持续时间通常是由帧率计算得出的;对于音频流,它通常是由采样率和帧的大小计算得出的。
  • pkt->stream_index:这是一个整数,指示该数据包属于哪个流 。在AVFormatContext中,streams数组存储了所有的流信息pkt->stream_index 对应 streams 数组中的下标。通过这个字段,可以知道数据包属于视频流、音频流或字幕流。每个流(AVStream)通常有不同的编码器,pkt->stream_index允许你在解码时区分这些流。

av_read_frame会为每一帧分配内存,所以在处理完每一帧后,务必使用av_packet_unref()来释放 AVPacket内部的数据。

c 复制代码
AVPacket pkt;
av_init_packet(&pkt);

while (av_read_frame(fmt_ctx, &pkt) >= 0) {
    if (pkt.stream_index == video_stream_index) {
        // 处理视频数据包
        process_packet(&pkt);
    }
    av_packet_unref(&pkt); // 释放数据包
}

3.2.5 关闭文件释放资源

avformat_close_input用于关闭输入媒体文件并释放相关资源的函数。该函数通常在处理完输入文件之后调用,用于清理和释放内存,确保系统资源得到释放。

c 复制代码
void avformat_close_input(AVFormatContext **s);

当完成了对输入文件的处理后,必须释放由avformat_open_input和其他 FFmpeg 函数分配的内存,避免内存泄漏。AVFormatContext会包含多个流(视频流、音频流等)及其相应的 I/O 缓冲区。调用 avformat_close_input会释放所有与文件相关的资源。关闭输入后,AVFormatContext指针被置为 NULL,这有助于避免后续访问已释放资源时出现错误。

复制代码
avformat_close_input(&fmt_ctx); // 释放 AVFormatContext,关闭文件,清理资源。

📌小结:

3.2.6 其它相关函数

(1)avcodec_parameters_to_context用于将一个AVCodecParameters结构中的参数复制到 AVCodecContext结构中 。这两个结构分别代表了编码/解码的参数和上下文信息。它将封装格式中的流参数传递到解码器或编码器的上下文中,以确保解码器/编码器能够正确处理数据。

c 复制代码
int avcodec_parameters_to_context(AVCodecContext *codec_ctx, const AVCodecParameters *codec_par);
  • codec_ctx:解码器或编码器的上下文指针,表示解码或编码操作所需的配置信息。
  • codec_par:传入的流的编码参数,通常是通过AVStream->codecpar获取的。AVCodecParameters是从封装格式中提取出来的流的编码信息(如视频流或音频流的格式、码率、分辨率、采样率等),这些参数必须转移到解码器或编码器的上下文中。

avcodec_parameters_to_context的主要作用是将AVCodecParameters中的流参数复制到 AVCodecContext 中。

示例🚀:

c 复制代码
	// 打开媒体文件路径
    const char* url = "/home/xiaochao/RTSP/v1080.mp4";  // 视频文件路径

    // 解封装输入上下文,初始化 AVFormatContext
    AVFormatContext* ic = nullptr;
    auto re = avformat_open_input(&ic, url,
        NULL,       // 封装器格式,NULL 表示自动探测(根据文件后缀名或文件头判断)
        NULL        // 参数设置,通常 RTSP 流需要设置一些额外的参数
    );

    if (re != 0) {
        char buf[1024] = { 0 };
        av_strerror(re, buf, sizeof(buf) - 1);
        cerr << "Error opening file: " << buf << endl;
    }

    // 获取媒体信息,不涉及头部格式
    re = avformat_find_stream_info(ic, NULL);
    if (re != 0) {
        char buf[1024] = { 0 };
        av_strerror(re, buf, sizeof(buf) - 1);
        cerr << "Error finding stream info: " << buf << endl;
        avformat_close_input(&ic);  // 关闭文件
    }

    // 打印封装文件信息,包括视频、音频流等
    av_dump_format(ic, 0, url, 0);  // 0 表示输入上下文,1 表示输出上下文

    // 初始化音频流和视频流的指针
    AVStream* as = nullptr; // 音频流
    AVStream* vs = nullptr; // 视频流

    // 遍历流信息,获取音视频流
    for (int i = 0; i < ic->nb_streams; i++)
    {
        // 音频流
        if (ic->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
        {
            as = ic->streams[i];
            cout << "=====音频=====" << endl;
            cout << "sample_rate: " << as->codecpar->sample_rate << endl;  // 输出音频采样率
        }
        // 视频流
        else if (ic->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
        {
            vs = ic->streams[i];
            cout << "=========视频=========" << endl;
            cout << "width: " << vs->codecpar->width << endl;   // 输出视频宽度
            cout << "height: " << vs->codecpar->height << endl; // 输出视频高度
        }
    }

    // 获取视频编码器 ID 和视频流的编码参数
    auto video_codec_id = vs->codecpar->codec_id;  // 视频编码器 ID
    auto video_par = vs->codecpar;                  // 视频编码参数

    // 视频解码初始化
    XDecode decode;
    auto decode_c = XCodec::Create(video_codec_id, false);  // 创建解码器
    // 将解封装的视频编码参数传递给解码上下文
    avcodec_parameters_to_context(decode_c, video_par);

    // 设置解码器,线程安全地使用解码器,不再在外部使用 decode_c
    decode.set_c(decode_c);
    if (!decode.Open())
    {
        cout << "decode Open failed!" << endl;

    }

    // 创建解码输出的空间(解码后的视频帧)
    auto frame = decode.CreateFrame();
    // 渲染初始化
    auto view = XVideoView::Create();  // 创建渲染对象
    // 初始化渲染视图,设置视频的宽度、高度和格式
    view->Init(video_par->width, video_par->height, (XVideoView::Format)video_par->format);

    AVPacket pkt;  // 定义解封装数据包

    // 无限循环,逐帧读取媒体数据
    for (;;)
    {
        // 读取一帧数据包
        re = av_read_frame(ic, &pkt);
        if (re < 0) {
            char buf[1024] = { 0 };
            av_strerror(re, buf, sizeof(buf) - 1);
            cerr << "Error reading frame: " << buf << endl;
            break;  // 读取失败时退出
        }

        // 如果数据包是视频流
        if (vs && pkt.stream_index == vs->index)
        {
            // 解码视频帧
            if (decode.Send(&pkt))
            {
                // 循环接收解码后的帧
                while (decode.Recv(frame))
                {
                    cout << frame->pts << " " << flush;  // 输出解码帧的时间戳
                    view->DrawFrame(frame);              // 渲染视频帧
                }
            }
        }
        else if (as && pkt.stream_index == as->index)
        {
            // 如果是音频流,可以在此处理音频数据
        }

        // 打印数据包的时间戳等信息,调试时可以使用
        // cout << pkt.pts << " : " << pkt.dts << " :" << pkt.size << endl;

        // 释放数据包引用,避免内存泄漏
        av_packet_unref(&pkt);
    }

    // 释放帧的内存
    av_frame_free(&frame);
    // 关闭输入文件,释放上下文
    avformat_close_input(&ic);

四、RTSP

4.1 VLC模拟RTSP服务器

VLC(VideoLAN Client )不仅是一个强大的媒体播放器,还可以用作 RTSP 服务器 来推送视频流。使用 VLC 模拟 RTSP 服务器,可以方便地进行视频流测试,或者作为流媒体服务器提供播放服务。

(1)打开 VLC ,进入 "媒体" -> "流"。

在 "文件" 选项卡中 添加本地视频文件。点击 "串流" 按钮。

(2)在 "目标"选项中,选择 "RTSP",然后点击 "添加"。

路径 中输入test,/test 是 RTSP 流路径,这样RTSP地址是127.0.0.1。也可以按如下设置,在路径中输入 rtsp://0.0.0.0:8554/test

  • 0.0.0.0 表示监听所有 IP 地址。
  • 8554 是 RTSP 端口(默认 RTSP 端口为 554,但 8554 避免了权限问题)。
  • /test 是 RTSP 流路径

点击下一步。

(3)设置转码,这里暂不进行设置,点击下一步。

(4)启动 RTSP 服务器

  1. 勾上串流所有基本流,点击 "流" 按钮,VLC 将开始作为 RTSP 服务器运行。
  2. 客户端可以连接 rtsp://<服务器IP>:8554/test 进行播放

然后重新打开一个VLC,选择打开网络流,输入RTSP地址rtsp://127.0.0.1:8554/test。

结束语

感谢阅读吾之文章,今已至此次旅程之终站 🛬。

吾望斯文献能供尔以宝贵之信息与知识也 🎉。

学习者之途,若藏于天际之星辰🍥,吾等皆当努力熠熠生辉,持续前行。

然而,如若斯文献有益于尔,何不以三连为礼?点赞、留言、收藏 - 此等皆以证尔对作者之支持与鼓励也 💞。

相关推荐
彷徨而立4 小时前
【win32】ffmpeg 解码器
ffmpeg
彷徨而立7 小时前
【win32】ffmpeg 解码器2
ffmpeg
沃达德软件8 小时前
AI数字人视频图像音频生成服务
图像处理·人工智能·计算机视觉·ai作画·音视频·实时音视频·视频编解码
Everbrilliant8910 小时前
Android音视频编解码全流程之Muxer
视频编解码·ffmpeg帧写入·ndkmediamuxer·muxer·muxer复用器·amediamuxer·音视频编解码全流程
喝呜昂_黄10 小时前
【 嵌入式Linux应用开发项目 | Rockit + FFmpeg+ Nginx】基于泰山派的IPC网络摄像头
linux·c语言·nginx·ffmpeg
陈旭金-小金子10 小时前
FFmpeg 5.x 编译 so 文件的记录
ffmpeg
huluang1 天前
ppt视频极致压缩参数
ffmpeg·powerpoint·音视频
在狂风暴雨中奔跑5 天前
厌倦了复杂的编译?一键集成 AeroFFmpeg,让Android音视频开发更简单!
ffmpeg·开源
Java陈序员6 天前
直播录制神器!一款多平台直播流自动录制客户端!
python·docker·ffmpeg