一、网络抽象层单元 - NALU ( Network Abstraction Layer Unit )




1、NALU 简介


NALU ( Network Abstract Layer Unit , 网络抽象层单元 ) 是 H.264 视频编码 中 用于 封装 编码后数据 的 基本传输单元 , 用于将 视频数据 以适合 网络传输的格式 组织起来 ;

NALU 是 对 原始视频帧 进行编码后的数据单元 , 它们 包含了 视频数据 的 编码信息 , 如 图像帧的编码数据、控制信息 等 ;


2、NALU 常见的数据类型


NALU 类型 通过其头部的 nal_unit_type 类型字段 定义 数据类型 , 主要有以下几种常见类型 :

  • SPS , Sequence Parameter Set , 序列参数集 , 保存一组 编码视频序列 的 参数 ;
  • PPS , Picture Parameter Set , 图像参数集 , 保存 一个序列中 某一个 或 某几个 图像帧 的 参数 ;
  • I 帧 , Intra-coded Frame , 帧内编码帧 , 可独立解码成一张完整的画面帧 ;
  • P 帧 , Predictive-coded Frame , 前向预测帧 , 参考前面的 I 帧 或 P 帧 解码出完整 画面帧 ;
  • B 帧 , Bidirectional Predictive-coded Frame , 双向预测帧 , 参考前后的 I 帧 或 P 帧 解码出完整 画面帧 ;

在这里插入图片描述


3、NALU 传输注意事项


传输 H.264 裸流的时候 , 发送 I 帧之前 , 至少要向解码器发送一次 SPS 和 PPS , 如果 解码器 收不到 SPS 和 PPS 数据 , 解码器解码会失败 ;

注意 : 不是每个 I 帧都要发送 SPS 和 PPS 数据 , 在单个 H.264 视频流中 , 至少发一次 ;

SPS 和 PPS 后面的 I 帧 一般都是 IDR 帧 ;

在这里插入图片描述

一张 图像帧 可能对应有多个 NALU , 如 上图所示 , 一帧 I 帧 被分成两个 NALU 发送 ;


4、NALU 功能结构


NALU , Network Abstraction Layer Unit , 网络抽象层单元 , 是视频数据的基本组成单元 , 其功能分为两层 , 分别是 视频编码层 ( VCL ) 网络提取层 ( NAL ) ;


① 视频编码层 ( VCL : Video Coding Layer )

视频编码层 VCL , 全称 Video Coding Layer , 负责实际的 视频数据 编码 和 压缩 , 是编码器的核心部分 , 处理视频内容的分析、压缩和优化 ;

视频编码层 中 包括 编码块、宏块 Macroblock 、帧 Frame 、片 Slice 等图像数据相关的语法级定义 ;


VCL(Video Coding Layer)相关的类型 : 这些类型与视频的核心编码数据有关 , 主要是帧数据和分片传输 ;

  • 类型 1 : 非 IDR 帧 , 包含普通的编码片数据 , 如 : P 帧 或 B 帧 ;
  • 类型 2、3、4 : 数据分片 A / B /C , 将大的视频帧分片为更小的单元 , 用于网络传输或码流分割 ;
  • 类型 5 : 即时解码刷新帧 ( IDR 帧 ) 和 关键帧 ( I 帧 ) 的编码数据 , 是解码器重置参考帧的起始点 ;

这些类型是视频数据的核心 , 包含实际的帧编码内容 ;


② 网络提取层 ( NAL : Network Abstraction Layer )

网络提取层 NAL , 全称 Network Abstraction Layer , 负责将 VCL 产生的 压缩比特流 适配到不同的 传输协议 和 环境中 , 使 视频数据 能够 在网络中 传输或存储 ,


NAL(Network Abstraction Layer)相关的类型 : 这些类型为网络传输提供支持 , 主要涉及参数集、辅助信息和码流管理 ;

  • 类型 6 : SEI , Supplemental Enhancement Information , 包含附加信息 , 如 : 时间戳、字幕、场景切换标志 等 ;
  • 类型 7 : SPS , Sequence Parameter Set , 定义全局的序列参数 , 如 : 分辨率、帧率、编码模式 等 ;
  • 类型 8 : PPS , Picture Parameter Set , 定义图像级别的参数 , 如 : 分片布局、熵编码模式 ;
  • 类型 9 : 访问单元边界符 , 用于标记 访问单元(AU)的边界 , 方便解码器同步解码 ;
  • 类型 10 : 序列结束 , 用于指示序列的结束 ;
  • 类型 11 : 流结束 , 用于指示整个视频流的结束 ;
  • 类型 12 : 填充数据 , 用于填充码率、对齐数据流或占位 ;

这些类型是辅助层 , 提供 元信息 和 流控制 ;


③ NALU 的 VCL 和 NAL 对应的 nal_unit_type 类型

nal_unit_type描述归属层级备注
0未定义-保留,不使用。
1非 IDR 帧(编码后的片数据)VCL普通帧,如 P 帧、B 帧等。
2数据分片 AVCL用于分片传输的 A 类数据片。
3数据分片 BVCL用于分片传输的 B 类数据片。
4数据分片 CVCL用于分片传输的 C 类数据片。
5IDR 帧(关键帧)VCLI 帧,随机访问点,重置解码器参考帧。
6补充增强信息(SEI)NAL包含附加信息(如时间戳、场景切换标记等)。
7序列参数集(SPS)NAL定义视频序列的全局参数(如分辨率、帧率等)。
8图像参数集(PPS)NAL定义单个图像的参数(如熵编码模式、分片信息等)。
9边界符(Access Unit Delimiter)NAL标记访问单元边界,辅助解析器解析视频流。
10结束序列(End of Sequence)NAL指示序列结束。
11结束流(End of Stream)NAL指示整个码流结束。
12填充数据(Filler Data)NAL占位符,用于填充码率或对齐数据流。
13保留NAL当前未使用,保留以备未来扩展。
14-23未定义-预留类型,具体使用取决于扩展协议。

5、NALU 数据结构


H.264 视频流 NALU 单元 组成 :

  • Start Code 起始标志位 , 使解码器能够正确地定位每个 NALU 单元的开始 ;
  • NALU Header 头部 , 提供该 NALU 单元的基本信息 , 包括 NALU 类型、参考级别等 ;
  • NALU Payload 负载 , 包含实际的视频数据 或 参数集 ;
+-----------------------------------------------------------+
|             Start Code (4 bytes) 00 00 00 01              |
|-----------------------------------------------------------|
|                    NALU Header (1 byte)                   |
|  +-------------------+-----------------+----------------+ |
|  | forbidden_zero_bit | nal_ref_idc    | nal_unit_type  | |
|  +-------------------+-----------------+----------------+ |
|                                                           |
|                    NALU Payload (variable size)           |
|-----------------------------------------------------------|
|           Encoded Video Data or SPS/PPS/SEI Data          |
+-----------------------------------------------------------+

① Start Code 起始标志位

Start Code 是用来标识 NALU 单元的开始位置 , 确保编码数据流可以被正确地识别和分割 ;

Start Code 是一个 固定的 4 字节序列 : 0x00 0x00 0x00 0x01 , 是 NALU 单元的开始标记 , 帮助解码器识别每个 NALU 单元的起始位置 ;

特殊情况 : 在特定的传输方式中 , 如 : RTP 包 , Start Code 是 3 字节序列 0x00 0x00 0x01 ;


② NALU Header 头

NALU Header 用于 说明 当前 NALU 的类型 , 以及 控制信息 , 如 : 是否是参考帧、NALU 的优先级等 ;

I . NALU Header 组成

NALU Header 三个组成部分 :
在这里插入图片描述

  • F 禁止位 : forbidden_zero_bit , 必须是 0 ;
  • R 重要性指示位 : nal_ref_idc , 取值范围 00 ~ 11 , 值越大越重要 , 越需要保护 ;
    • 设置为 00 说明可以丢弃 , 不影响视频播放 ;
    • 如果画面帧是参考帧 , 该值必须大于 0 ;
  • T 负荷数据类型 : nal_unit_type , 取值范围 1 ~ 12 ;
字段长度描述
forbidden_zero_bit1 bit固定为 0,表示该 NALU 是有效的。
nal_ref_idc2 bits表示当前 NALU 的参考级别,表明它是否可以作为参考帧。
nal_unit_type5 bits表示当前 NALU 的类型(如 I 帧、P 帧、SPS、PPS 等)。
II . NALU Header 示例

NALU Header 示例 : 假设有一个 I 帧 NALU , 类型值为 5 , 重要性级别为 3 ;

  • forbidden_zero_bit = 0
  • nal_ref_idc = 3 , 表示该 NALU 可以作为参考帧 ;
  • nal_unit_type = 5 , 表示 I 帧 ;
  • NALU 头部的二进制表示为:0 11 00101 ;
III . 常见的 NALU 的 nal_unit_type 类型
NALU 类型值描述
0未定义
1非 IDR 帧(I帧,P帧等)
2视频补充数据(SEI,Supplemental Enhancement Information)
3片段(Slice)
4一般的帧内预测数据
5IDR 帧(关键帧)
6补充增强信息(SEI)
7序列参数集(SPS,Sequence Parameter Set)
8图像参数集(PPS,Picture Parameter Set)
9补充填充数据(填充用数据)
10增强编码数据(无特定解释)
11系统参数集(用于容器或传输中的数据)
12胶片补充信息(Film Buffers)
13其他类型数据

③ NALU Payload 载荷

NALU Payload 包含了 NALU 的 实际编码数据 , 数据的具体内容取决于 NALU 的 nal_unit_type 类型 ;

  • VCL 数据 : nal_unit_type 类型 取值 1 ~ 5 , 则该 NALU 是视频帧数据 , 如 : I 帧、P 帧、B 帧 , 负载部分会包实际的帧编码内容 ;
  • NAL 数据 : nal_unit_type 类型 取值 6 ~ 13 , 这些类型是辅助层 , 提供 元信息 和 流控制 ;

参考上面的 视频编码层 ( VCL : Video Coding Layer ) 和 网络提取层 ( NAL : Network Abstraction Layer ) 两个章节 ;





二、H.264 封装模式 - annexb模式 / MP4 模式



H.264 视频流 有 2 种 不同的 封装模式 , 以适应 不同的 传输和存储场景 ,

  • Annex B 模式 : H.264 标准定义的裸流封装方式 , 常用于实时传输 , 就是本博客讲解的 NALU 裸流封装 , 结构为 [Start Code] [NALU Header] [NALU Payload] ;
  • MP4 模式 : 基于 容器格式 的封装方式 , 主要用于存储 , 如 : MP4 / MKV 等容器格式 ;

1、Annex B 模式


Annex B 封装模式 是 H.264 标准定义的 裸流 封装方式 , 常用于 网络实时传输 , 其特点是通过 0x00000001 起始码 分割每个 NAL 单元 , 用于 RTSP / RTMP / UDP / RTP 实时流媒体传输 ;

Annex B 封装模式 特点 :

  • 起始码分隔 : 每个 NAL 单元 以固定的 0x0000010x00000001 起始码 开头 , 用于分隔 NALU 单元 ;
  • 实时解码 : 视频流 中不包含 时间戳 等 元信息 , 适合实时解码 ;
  • 适合传输 : 适合实时流媒体传输 , 不适用于存储 , 因为存储时信息不够完整 ;

2、MP4 模式


MP4 封装模式 是基于 文件容器格式 的封装方式 , 主要用于 本地存储 , NALU 单元 被封装到 容器文件中 , 配合 元信息 进行管理 ; 如 : MP4 文件 , MKV 文件 等 ;

MP4 封装模式 特点 :

  • 元信息丰富 : 包含 时间戳、帧偏移、关键帧索引等信息,便于随机访问和播放。
  • 长度前缀 : 每个 NAL 单元前使用 4 字节 长度前缀 代替起始码,表示后续 NAL 单元的大小。
  • 适合存储: 由于元信息完善,适用于文件存储和编辑。

MP4 封装模式 用途:

  • 视频文件存储
  • 视频编辑和点播场景。

3、Annex B 和 MP4 模式的对比


Annex B 和 MP4 模式的对比 :

  • Annex B 封装模式 适合实时流媒体传输 , 封装简单 , 但缺少元信息 , 依赖起始码分割 NAL 单元 ;
  • MP4 封装模式 将 H.264 视频 封装在 MP4 容器中 , 支持时间戳等丰富的元信息 , 适合存储和点播场景 , 但复杂度较高 ;
特性Annex B 模式MP4 模式
分隔方式起始码(0x0000010x00000001长度前缀(4 字节)
元信息支持无元信息,流中仅包含裸 NAL 单元包含丰富元信息,如时间戳和索引
适用场景实时流媒体传输(如 RTSP、RTP 等)文件存储和点播(如 .mp4.m4v
复杂度简单直接,解码时可逐个读取 NAL 单元较高,需解析 MP4 容器结构和时间戳信息
容器支持无容器,裸流形式基于 MP4 容器封装
随机访问能力较弱,依赖特定 IDR 帧标识强,具备快速定位关键帧和时间戳的能力
封装效率较低,起始码占用额外空间较高,节省起始码空间
典型用途流媒体实时传输、直播视频存储、编辑、点播




三、将 MP4 模式 转为 Annex B 模式



有的 解码器 只⽀持 H.264 的 Annex B 封装模式 , 不支持 MP4 封装模式 , 这种情况下需要将MP4 封装模式 转为 Annex B 封装模式 ;


1、转换核心代码示例


先给出代码示例 , 然后讲解代码中的核心要点 ;

在下面的代码中 ,

  • 首先 , 查找 比特流过滤器 AVBitStreamFilter ;

  • 然后 , 定义 比特流过滤器上下文 AVBSFContext , 并进行初始化操作 ;

  • 再后 , 将编解码器参数拷贝到 比特流过滤器上下文 AVBSFContext 中 , 用于初始化 比特流过滤器 AVBitStreamFilter ;

  • 最后 , 使用 比特流过滤器上下文 AVBSFContext , 将 AVPacket 由 MP4 封装模式 转为 Annex B 封装模式 ;


简要的核心代码如下 : 只有核心代码 , 省略了很多其它代码 , 如 : 解码 MP4 文件 , AVPacket 初始化释放操作等 ;

// 获取指定名称的 比特流过滤器 Bitstream Filter ( BSF )
// 在此代码中,我们使用 "h264_mp4toannexb" 过滤器,
// 该 过滤器 用于将 H.264 的 MP4 封装格式 转换为 Annex B 封装格式。
const AVBitStreamFilter *bsfilter = av_bsf_get_by_name("h264_mp4toannexb");

// 定义一个比特流过滤器上下文指针,用于管理过滤器的配置和操作。
AVBSFContext *bsf_ctx = NULL; 

// 初始化 比特流过滤器 上下文
// av_bsf_alloc 函数 根据 指定的过滤器 分配一个上下文结构 ( AVBSFContext )
// 参数:
//   bsfilter : 要使用的过滤器 , 这里是 h264_mp4toannexb 
//   &bsf_ctx : 用来存储分配的过滤器上下文的指针 
av_bsf_alloc(bsfilter, &bsf_ctx);

// 配置比特流过滤器的输入参数
// 通过将输入流的 编解码参数 ( codec parameters ) 复制到过滤器上下文中,
// 让过滤器知道如何处理输入数据。
// 参数:
//   bsf_ctx->par_in:比特流过滤器的输入参数结构。
//   ifmt_ctx->streams[videoindex]->codecpar:输入流中视频的编解码参数。
// ifmt_ctx 是输入格式上下文,videoindex 是视频流索引。
avcodec_parameters_copy(bsf_ctx->par_in, ifmt_ctx->streams[videoindex]->codecpar);

// 初始化 比特流过滤器
// av_bsf_init 函数会根据前面配置的输入参数和过滤器类型初始化过滤器上下文。
// 初始化后,过滤器就可以用于处理数据了。
av_bsf_init(bsf_ctx);

// AVPacket 数据包 , 省略之后的初始化以及读取数据的过程 
AVPacket *pkt = NULL;
// ... 省略之后的 AVPacket 初始化以及读取数据的过程 

// 将一个编码数据包发送到比特流过滤器进行处理
av_bsf_send_packet(bsf_ctx, pkt);
/*
 * av_bsf_send_packet:
 * - 参数 bsf_ctx:比特流过滤器上下文,必须先初始化(通过 av_bsf_alloc 和 av_bsf_init)。
 * - 参数 pkt:指向要发送的 AVPacket 数据包。
 * - 功能:将数据包发送到比特流过滤器内部进行处理。
 * - 注意:此函数会接管 pkt 数据的管理,成功调用后无需再手动释放 pkt 的数据。
 */

// 释放 AVPacket 中的动态内存,并重置数据指针和大小
av_packet_unref(pkt);
/*
 * av_packet_unref:
 * - 参数 pkt:需要释放资源的 AVPacket。
 * - 功能:释放 pkt 内部分配的所有动态内存,例如数据缓冲区(data)和大小信息(size)。
 * - 注意:pkt 本身不会被释放,只是其内容被重置为未初始化状态(如 data = NULL, size = 0)。
 */

// 从比特流过滤器中提取一个处理后的编码数据包
av_bsf_receive_packet(bsf_ctx, pkt);
/*
 * av_bsf_receive_packet:
 * - 参数 bsf_ctx:比特流过滤器上下文,存储过滤器的状态和配置。
 * - 参数 pkt:指向一个 AVPacket,用于接收过滤后的输出数据。
 * - 功能:从比特流过滤器的内部缓冲区中提取处理后的数据包。
 * - 返回值:
 *   - 0:成功提取到一个数据包。
 *   - AVERROR(EAGAIN):当前没有可用输出数据包,需要更多输入数据。
 *   - AVERROR_EOF:数据流结束,没有更多数据输出。
 * - 注意:
 *   - 提取的 pkt 数据包需要在使用后调用 av_packet_unref 释放。
 *   - 在处理过程中需要检查返回值,区分是否需要提供更多输入数据。
 */

2、核心要点 - av_bsf_get_by_name 函数 : 根据名称获取 比特流过滤器


av_bsf_get_by_name 函数 用于 根据 过滤器名称 查找并获取对应的 过滤器结构体 AVBitStreamFilter , 通常在进行比特流处理 时使用 ; 这里用于 将 H.264 视频流 的 MP4 封装模式 转为 Annex B 封装模式 ;


av_bsf_get_by_name 函数原型 :

const AVBitStreamFilter *av_bsf_get_by_name(const char *name);
  • const char *name 参数 : 指定要获取的 比特流过滤器 的名称(字符串形式)。常见的过滤器名称如 h264_mp4toannexb 等。
  • 返回值 :
    • 获取成功 : 返回一个指向 AVBitStreamFilter 结构体的指针 , 表示与指定名称对应的比特流过滤器 ;
    • 获取失败 : 如果没有找到对应名称的 比特流过滤器 , 返回 NULL ;

常用的 比特流过滤器 AVBitStreamFilter 名称 :

  • h264_mp4toannexb : 将 H.264 MP4 封装格式 转换为 Annex B 格式 ;
  • hevc_mp4toannexb : 将 HEVC MP4 封装格式 转换为 Annex B 格式 ;
  • aac_adtstoasc : 将 AAC 的 ADTS 格式转换为 Audio Specific Config 格式 ;

3、核心要点 - av_bsf_alloc 函数 : 分配比特流过滤器上下文


av_bsf_alloc 函数 的 作用是 为 指定的 比特流过滤器 AVBitStreamFilter 分配一个新的 比特流过滤器上下文 AVBSFContext , 用于后续的初始化和操作 ;


av_bsf_alloc 函数原型 :

int av_bsf_alloc(const AVBitStreamFilter *filter, AVBSFContext **bsf);
  • 参数解析 :

    • const AVBitStreamFilter *filter 参数 : 指向一个 AVBitStreamFilter 结构的指针 , 表示要分配的比特流过滤器 , 该参数 一般 通过 av_bsf_get_by_name 函数 获取 ;
    • AVBSFContext **bsf 参数 : 指向一个 比特流过滤器上下文 AVBSFContext 的指针,用于接收分配的比特流过滤器上下文(AVBSFContext)
  • 返回值解析 : 返回 整型值 , 表示函数的执行结果 ;

    • 返回 0 表示成功 ;
    • 返回 负数 , 表示失败 , 可根据负数值获取对应的错误描述 ;

4、核心要点 - avcodec_parameters_copy 函数 : 拷贝 AVCodecParameters 结构体


avcodec_parameters_copy 函数 的作用是 将一个 AVCodecParameters 结构中 的 编码参数 复制到另一个 AVCodecParameters 结构体 中 ;

该函数常用于在初始化 AVBSFContext 或 重新配置解码器 时传递编码参数 ;


avcodec_parameters_copy 函数原型 :

int avcodec_parameters_copy(AVCodecParameters *dst, const AVCodecParameters *src);
  • 参数说明 :
    • AVCodecParameters *dst 参数 : 拷贝目标 AVCodecParameters 结构 , 用于存储复制后的编码参数 ;
    • const AVCodecParameters *src 参数 : 拷贝源 AVCodecParameters 结构 , 包含需要复制的编码参数 ;
  • 返回值说明 :
    • 返回值为 0 表示 参数复制成功 ;
    • 返回值为 负值 表示 复制出错 ;

5、核心要点 - av_bsf_init 函数 : 初始化 比特流过滤器上下文 AVBSFContext


av_bsf_init 函数 用于 初始化 比特流过滤器的上下文 AVBSFContext 结构体 , 完成 比特流过滤器 的准备工作 , 一旦调用此函数 , 过滤器即可处理输入数据包 ;

该函数通常用于在配置完 比特流过滤器上下文 AVBSFContext 的 输入参数后 , 正式启用 比特流过滤器 ;


av_bsf_init 函数原型 :

int av_bsf_init(AVBSFContext *ctx);
  • AVBSFContext *ctx 参数 : 指向 比特流过滤器上下文 AVBSFContext 的指针 , 该 上下文 需要在调用此函数前由 av_bsf_alloc 函数 初始化 内存 , 并且调用 avcodec_parameters_copy 函数 配置好输入参数 ;
  • 返回值说明 :
    • 返回值为 0 表示 初始化 成功 ;
    • 返回值为 负值 表示 初始化 出错 ;

6、核心要点 - av_bsf_send_packet 函数 : 将数据包发送到比特流过滤器


av_bsf_send_packet 函数 的 作用是 将一个编码后的 数据包 AVPacket 发送到 比特流过滤器 AVBitStreamFilter 进行处理 , 过滤后的输出的 AVPacket 数据包 可以通过 av_bsf_receive_packet 函数获取 ;

av_bsf_send_packet 函数原型 :

int av_bsf_send_packet(AVBSFContext *ctx, AVPacket *pkt);
  • 参数说明 :
    • AVBSFContext *ctx 参数 : 指向 比特流过滤器上下文 AVBSFContext 的指针 , 比特流过滤器的上下文 , 必须在调用此函数之前通过 av_bsf_init 初始化 ;
    • AVPacket *pkt 参数 : 指向 AVPacket 的指针 , 表示需要发送到比特流过滤器进行处理的数据包 ;
  • 返回值说明 :
    • 返回值为 0 表示 发送 AVPacket 数据包 成功 ;
    • 返回值为 负值 表示 发送 AVPacket 数据包 出错 ;

7、核心要点 - av_packet_unref 函数 : 释放 AVPacket 内部资源


av_packet_unref 函数 释放 AVPacket 结构体中 分配 的 所有动态内存 , 并重置其内容 , 使其处于未初始化状态 , 如 : 数据指针 data 和 大小 size ;

该函数 不会释放 AVPacket 结构体本身的内存 , AVPacket * 指针变量 依然有效 , 仅释放其内部关联的资源 ;

解码或编码过程中 , 当一个 AVPacket 的数据已不再需要时 , 使用 av_packet_unref 释放内存 , 避免内存泄漏 ;

这里 在读取一个 AVPacket 后 , 这是 MP4 封装模式 的 H.264 视频数据 , 将其发送到 比特流过滤器上下文 AVBSFContext 之后 , 该原始 AVPacket 就没用了 , 直接调用该函数 释放 AVPacket 的内部资源 , 该 AVPacket 继续使用 , 传入 av_bsf_receive_packet 函数 , 用于接收 转换后的 Annex B 封装模式的 H.264 视频数据 ;


av_packet_unref 函数原型 :

void av_packet_unref(AVPacket *pkt);
  • AVPacket *pkt 参数 : 指向需要释放资源的 AVPacket 结构体指针 , 这个 AVPacket 通常用于存储 编码后的视频或音频数据 ;

可参考 【FFmpeg】FFmpeg 内存结构 ④ ( AVPacket 函数简介 | av_packet_unref 函数 | av_packet_move_ref 函数 ) 博客 ;


8、核心要点 - av_bsf_receive_packet 函数 : 从 比特流过滤器 接收修改后的 AVPacket 数据包


av_bsf_receive_packet 函数 的 作用是 从 比特流过滤器 中提取一个处理后的 AVPacket 数据包 , 该函数通常与 av_bsf_send_packet 配合使用 , 用于逐一获取比特流过滤器处理后的输出包 ;


av_bsf_receive_packet 函数原型 :

int av_bsf_receive_packet(AVBSFContext *ctx, AVPacket *pkt);
  • 参数说明 :
    • AVBSFContext *ctx 参数 : 指向 比特流过滤器上下文 AVBSFContext 的指针 , 比特流过滤器的上下文 , 必须在调用此函数之前通过 av_bsf_init 初始化 , 以及调用 av_bsf_send_packet 完成数据准备 ;
    • AVPacket *pkt 参数 : 指向 AVPacket 的指针 , 用于存储从 比特流过滤器 输出的处理后的数据包 ;
  • 返回值说明 :
    • 返回值为 0 表示 接收 AVPacket 数据包 成功 ;
    • 返回值为 负值 表示 接收 AVPacket 数据包 出错 ;

9、完整代码示例


#include <stdio.h>
#include <libavutil/avutil.h>
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>

int main(int argc, char **argv)
{
    // 打印 FFmpeg 版本号
    printf("FFmpeg version is %s\n", av_version_info());


    // 1. 文件准备
    // 要解封装的 视频文件 的 相对路径
    char *file_name = "test.mp4";
    // 要输出的 文件路径
    char *file_name_out = "out.mp4";

    // 打开输出文件 , 用于保存 Annex B 封装模式的 H.264 视频数据
    FILE *outfp = fopen(file_name_out, "wb");


    // 2. 打开视频文件
    // 描述 媒体文件 或 媒体流 的构成和基本信息
    AVFormatContext *fmt_ctx = NULL;

    // 打开文件 , 探测协议类型 , 将获取的参数信息 填充到 AVFormatContext 结构体中
    int ret = avformat_open_input(&fmt_ctx, file_name, NULL, NULL);
    // avformat_open_input 函数 : 执行成功返回 0 , 执行失败返回负数 ( 错误码 )
    if (ret < 0)
    {
        // 用于接收错误信息的字符串
        char buf[1024] = { 0 };
        // 获取 负数错误码 对应的 错误日志信息
        av_strerror(ret, buf, sizeof(buf) - 1);
        // 打印错误信息
        printf("avformat_open_input failed : %s\n", file_name, buf);
        return -1;
    }


    // 3. 获取 媒体流 详细信息
    ret = avformat_find_stream_info(fmt_ctx, NULL);
    // 获取 码流 信息失败 , 返回 负数错误码
    if (ret < 0)
    {
        // 用于接收错误信息的字符串
        char buf[1024] = { 0 };
        // 获取 负数错误码 对应的 错误日志信息
        av_strerror(ret, buf, sizeof(buf) - 1);
        // 打印错误信息
        printf("avformat_find_stream_info failed:%s\n", file_name, buf);
        return -1;
    }


    // 4. 打印 视频文件 的 参数信息 ☆☆☆
    printf_s("\n==== av_dump_format start ===\n\n");
    av_dump_format(fmt_ctx, 0, file_name, 0);
    printf_s("\n==== av_dump_format end ===\n");


    // 5. 获取视频流索引
    int videoindex = -1;        // 视频索引
    // 查找最佳的视频流索引
    videoindex = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
    // 查找 视频流 失败
    if(videoindex == -1)
    {
        printf("find video stream failed\n");
        avformat_close_input(&fmt_ctx);
        return -1;
    }


    // 6. 获取 比特流过滤器 并 初始化 比特流过滤器上下文

    // 获取指定名称的 比特流过滤器 Bitstream Filter ( BSF )
    // 在此代码中,我们使用 "h264_mp4toannexb" 过滤器,
    // 该 过滤器 用于将 H.264 的 MP4 封装格式 转换为 Annex B 封装格式。
    const AVBitStreamFilter *bsfilter = av_bsf_get_by_name("h264_mp4toannexb");

    // 定义一个比特流过滤器上下文指针,用于管理过滤器的配置和操作。
    AVBSFContext *bsf_ctx = NULL;

    // 初始化 比特流过滤器 上下文
    // av_bsf_alloc 函数 根据 指定的过滤器 分配一个上下文结构 ( AVBSFContext )
    // 参数:
    //   bsfilter : 要使用的过滤器 , 这里是 h264_mp4toannexb
    //   &bsf_ctx : 用来存储分配的过滤器上下文的指针
    av_bsf_alloc(bsfilter, &bsf_ctx);

    // 配置比特流过滤器的输入参数
    // 通过将输入流的 编解码参数 ( codec parameters ) 复制到过滤器上下文中,
    // 让过滤器知道如何处理输入数据。
    // 参数:
    //   bsf_ctx->par_in:比特流过滤器的输入参数结构。
    //   ifmt_ctx->streams[videoindex]->codecpar:输入流中视频的编解码参数。
    // ifmt_ctx 是输入格式上下文,videoindex 是视频流索引。
    avcodec_parameters_copy(bsf_ctx->par_in, fmt_ctx->streams[videoindex]->codecpar);

    // 初始化 比特流过滤器
    // av_bsf_init 函数会根据前面配置的输入参数和过滤器类型初始化过滤器上下文。
    // 初始化后,过滤器就可以用于处理数据了。
    av_bsf_init(bsf_ctx);


    // 7. 开始解析 数据包 , 并将 MP4 模式 转为 Annex B 模式 输出到本地文件中
    // 为 AVPacket 分配内存空间,用于存储视频数据包
    AVPacket *pkt = av_packet_alloc();
    // 初始化 AVPacket,清空相关字段
    av_init_packet(pkt);

    // 文件读取结束标志,初始为 0,表示文件未读取结束
    int file_end = 0;

    // 循环读取文件,直到文件结束
    while (0 == file_end)
    {
        // 读取一帧数据包
        if((ret = av_read_frame(fmt_ctx, pkt)) < 0)
        {
            // 如果没有更多数据包可读,表示文件已读完
            file_end = 1;
            printf("read file end: ret:%d\n", ret); // 输出读取结束信息
        }

        // 如果读取到的数据包是视频流
        if(ret == 0 && pkt->stream_index == videoindex)
        {
#if 1
            int input_size = pkt->size;  // 获取输入数据包的大小
            int out_pkt_count = 0;       // 统计输出数据包的数量

            // 将数据包送入比特流过滤器(bitstream filter),进行编码格式转换
            if (av_bsf_send_packet(bsf_ctx, pkt) != 0)
            {
                av_packet_unref(pkt);   // 如果过滤器处理失败,释放资源
                continue;               // 继续读取下一个数据包
            }

            av_packet_unref(pkt);    // 释放资源(pkt 内存由过滤器管理)

            // 从比特流过滤器接收处理后的数据包
            while(av_bsf_receive_packet(bsf_ctx, pkt) == 0)
            {
                out_pkt_count++;  // 每成功接收一个处理后的数据包,计数增加
                size_t size = fwrite(pkt->data, 1, pkt->size, outfp);  // 将处理后的数据包写入输出文件
                if(size != pkt->size)
                {
                    printf("fwrite failed-> write:%u, pkt_size:%u\n", size, pkt->size); // 写入失败提示
                }
                av_packet_unref(pkt);  // 处理完一个包后释放其内存
            }

            // 如果处理后的数据包数量超过 2 个,输出警告信息
            if(out_pkt_count >= 2)
            {
                printf("cur pkt(size:%d) only get 1 out pkt, it get %d pkts\n",
                       input_size, out_pkt_count);
            }
#else       // TS 格式的文件已经是 Annex B 格式 , 不需要进行转换 , 可以直接写入
            size_t size = fwrite(pkt->data, 1, pkt->size, outfp); // 直接写入输出文件
            if(size != pkt->size)
            {
                printf("fwrite failed-> write:%u, pkt_size:%u\n", size, pkt->size); // 写入失败提示
            }
            av_packet_unref(pkt);  // 释放资源
#endif
        }
        else
        {
            // 如果数据包不是视频流或读取出错 , 直接释放内存 , 继续进行下一次循环
            if(ret == 0)
            {
                av_packet_unref(pkt);        // 释放内存
            }
        }
    }

    // 如果输出文件存在,关闭文件
    if(outfp)
    {
        fclose(outfp);
    }

    // 如果比特流过滤器上下文存在,释放资源
    if(bsf_ctx)
    {
        av_bsf_free(&bsf_ctx);
    }

    // 如果数据包指针存在,释放 AVPacket 的内存
    if(pkt)
    {
        av_packet_free(&pkt);
    }

    // 如果格式上下文存在,关闭输入流并释放资源
    if(fmt_ctx){
        avformat_close_input(&fmt_ctx); // 与 avformat_open_input 配对使用,防止内存泄漏
    }


    // 执行结束
    printf("\nFFmpeg End\n");
    return 0;
}

10、代码执行结果


代码执行结果 :

FFmpeg version is 4.2.1

==== av_dump_format start ===

Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'test.mp4':
  Metadata:
    major_brand     : isom
    minor_version   : 512
    compatible_brands: mp41
    creation_time   : 2024-04-21T12:09:43.000000Z
    encoder         : Bandicam 5.2.0.1855 / GDI
    encoder-eng     : Bandicam 5.2.0.1855 / GDI
  Duration: 00:01:05.77, start: 0.000000, bitrate: 7572 kb/s
    Stream #0:0(eng): Video: h264 (Constrained Baseline) (avc1 / 0x31637661), yuv420p, 848x480, 7370 kb/s, 29.29 fps, 30 tbr, 30k tbn, 60k tbc (default)
    Metadata:
      creation_time   : 2024-04-21T12:09:43.000000Z
      handler_name    : VideoHandler
    Stream #0:1(eng): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 194 kb/s (default)
    Metadata:
      creation_time   : 2024-04-21T12:09:43.000000Z
      handler_name    : SoundHandler

==== av_dump_format end ===
read file end: ret:-541478725

FFmpeg End

在这里插入图片描述

进入 项目编译 输出 目录 , 使用

ffplay out.mp4

命令 , 播放 输出 的 annexb 封装模式 out.mp4 文件 , 播放效果如下 :

在这里插入图片描述

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐