【FFmpeg】H.264 格式分析 ② ( 网络抽象层单元 NALU NALU 功能结构 VCL 视频编码层 NAL 网络提取层 H.264 封装模式 - annexb 模式 )

文章目录

一、网络抽象层单元 - 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

数据分片 A

VCL

用于分片传输的 A 类数据片。

3

数据分片 B

VCL

用于分片传输的 B 类数据片。

4

数据分片 C

VCL

用于分片传输的 C 类数据片。

5

IDR 帧(关键帧)

VCL

I 帧,随机访问点,重置解码器参考帧。

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)
    ±------------------±----------------±---------------+
    ±------------------±----------------±---------------+
    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_bit

1 bit

固定为 0,表示该 NALU 是有效的。

nal_ref_idc

2 bits

表示当前 NALU 的参考级别,表明它是否可以作为参考帧。

nal_unit_type

5 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

一般的帧内预测数据

5

IDR 帧(关键帧)

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
", 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
", 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
", file_name, buf);
        return -1;
    }


    // 4. 打印 视频文件 的 参数信息 ☆☆☆
    printf_s("
==== av_dump_format start ===

");
    av_dump_format(fmt_ctx, 0, file_name, 0);
    printf_s("
==== av_dump_format end ===
");


    // 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
");
        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
", 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
", 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
",
                       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
", 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("
FFmpeg End
");
    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 文件 , 播放效果如下 :

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值