【FFmpeg】FLV 格式分析 ④ ( 解析 FLV 视频文件 - 代码示例 )
FLV文件结构解析摘要(149字) FLV文件由文件头(9字节)和文件体组成,文件体包含多个"Previous Tag Size + Tag数据块"结构。Tag数据块分为3种类型:音频数据、视频数据和元数据,每种类型有独特的结构。解析程序通过读取文件到内存缓冲区,分离处理完整数据与不完整数据,并最终将音视频数据单独输出。程序使用C++的fstream以二进制模式读取FLV文件,
FLV 文件 的总体结构 由 File Header 文件头、File Body 文件体 组成 ;
- File Header 文件头 固定为 9 字节 ,
- File Body 文件体 由 若干 " Previous Tag Size 前一个标签大小 + Tag 数据块 " 组成 ;
- Tag 数据块 由 Tag Header 数据块头、Tag Body 数据块体 组成 ;
- Tag Header 数据块头 由 固定 11 字节组成 ;
- Tag Body 需要根据 三种不同的 Tag 类型 各自有不同的结构 , 如 :
- 音频数据 Audio Data、
- 视频数据 Video Data、
- 元数据 Script Data ;
在博客 【FFmpeg】FLV 格式分析 ① ( File Header 文件头 | File Body 文件体 | Tag Header 数据块头结构 | Script Data 元数据结构 ) 中 , 讲解了 Script Data 元数据 的 Tag Body 结构 ;
在博客 【FFmpeg】FLV 格式分析 ② ( Tag Body 数据块体结构 - Audio Data 音频数据 | AAC 序列头 AudioSpecificConfig 结构分析 ) 中 介绍了 音频数据 Audio Data 的 Tag Body 结构 ;
在博客 【FFmpeg】FLV 格式分析 ③ ( Tag Body 数据块体结构 - Vedio Data 视频数据 ) 中 介绍 视频数据 Video Data 的 Tag Body 结构 ;
本篇博客 参考 FlvParser 源码 , 分析 FLV 文件的解析过程 ;
博客源码地址 : https://download.csdn.net/download/han1202012/92155449
- 该源码中有 完整的 Qt 项目源码 , 可以直接在 Qt 中运行 ;
一、FLV 解析需求分析
本篇博客中实现一个 FLV 解析程序 ,
- 首先 , 解析 FLV 格式的 File Header 文件头、File Body 文件体 信息 , 其中 文件体 由 若干 Previous Tag Size 字段 + Tag 数据块 组成 ;
- 然后 , 解析 文件体 中的 Tag 数据块 , 每个 Tag 块都有 Tag Header 和 Tag Body 构成 ;
- Tag 数据块有 3 种类型 : 音频数据 Audio Data、视频数据 Video Data、元数据 Script Data , 每种类型都有对应的 独特的 Tag 格式 ;
- 再后 , 将解析出的 音视频 格式信息 打印出来 ;
- 最后 , 将 音频数据、视频数据 单独剥离出来 , 输出到独立的 文件中 ;
二、函数入口分析
1、main 函数功能分析
在 main 函数中 , 主要执行如下逻辑 ,
首先 , 打开 FLV 视频文件 , 如果打开失败直接退出 ;
然后 , 解析 FLV 视频文件 ;
最后 , 关闭 文件输入流 , 退出程序 ;
2、main 函数代码分析
① 解析文件准备
打开输入文件 , 该文件被拷贝到了 Qt 程序编译目录中 , 项目名称是 FlvParser , 程序的编译输出目录是 build-FlvParser-Desktop_Qt_5_14_2_MinGW_32_bit-Debug ,

将 FLV 视频文件 input.flv 拷贝到上述目录中 , 这是本程序要解析的 FLV 视频文件 ;

② fstream 打开 FLV 文件输入流
fstream 是 C++ 标准库中用于文件输入输出的类 , 下面是以 二进制 ( ios_base::binary ) 模式 和 只读 ( ios_base::in ) 模式 打开名为 input.flv 的 FLV 格式文件 , 如果打开文件失败 , 直接返回 ;
fstream fin;
// 1. 打开输入的 FLV 源文件
fin.open("input.flv", ios_base::in | ios_base::binary);
if (!fin)
return 0;
打开文件后 , 调用 Process 函数 解析 FLV 文件 , 该函数在之前提前声明 ;
// 处理 FLV 文件的函数声明
void Process(fstream &fin, const char *filename);
// 2. 调用处理函数处理 FLV 文件
Process(fin, "dump.flv");
③ 关闭 fstream 输入流
处理 FLV 文件后 , 关闭 FLV 文件输入流 , 并退出程序 ;
// 3. 处理完毕后 , 关闭文件 , 并退出程序
fin.close(); // 关闭文件
3、main 函数入口代码示例
main 函数 入口 代码示例 :
int main(int argc, char *argv[])
{
fstream fin;
// 1. 打开输入的 FLV 源文件
fin.open("input.flv", ios_base::in | ios_base::binary);
if (!fin)
return 0;
// 2. 调用处理函数处理 FLV 文件
Process(fin, "dump.flv");
// 3. 处理完毕后 , 关闭文件 , 并退出程序
fin.close(); // 关闭文件
return 1;
}
三、FLV 文件解析主体框架代码分析
1、主体解析框架
在上述 main 函数中 , 调用了 Process 函数 执行 函数解析 操作 ;
void Process(fstream &fin, const char *filename)
在 Process 函数中 , 执行如下代码逻辑 :
首先 , 将文件中的数据 读取到 内存缓冲区 中 ;
然后 , 解析 内存缓冲区 中的数据 ;
再后 , 将 解析结果 打印到 命令行终端 中 ;
最后 , 将 FLV 视频解析后的 视频画面 和 音频采样 输出到 独立文件中 ;
2、文件解析框架代码分析
① 内存缓冲区读取
将文件中的数据 读取到 内存缓冲区 中 ;
在 堆内存 中 , 设置 2 个内存缓冲区 , 主缓冲区 *pBuf 和 备份缓冲区 *pBak , 两个缓冲区的默认大小都是 2MB ;
主缓冲区 存放的是 本次要解析的 FLV 文件数据 ,
备份缓冲区 存放的是 本次没有解析完毕的数据 ;
// 设置每次读取 FLV 文件的缓冲区大小为 2MB
int nBufSize = 2 * 1024 * 1024;
int nFlvPos = 0; // 当前解析的位置
uint8_t *pBuf, *pBak;
pBuf = new uint8_t[nBufSize]; // 分配主缓冲区
pBak = new uint8_t[nBufSize]; // 分配备份缓冲区 , 用于存储每次未解析的不完整数据
调用 FLV 文件的 文件输入流 fstream fin 对象 的 read 函数 , 将文件数据读取到 主缓冲区 *pBuf 中 ;
一次读取 2MB 数据 , 解析前面的完整内容 , 最后可能会有几百字节的 不完整数据 , 这些 不完整数据 最后复制出来 放在下次缓冲区的首部 , 使用 nFlvPos 记录上一次没有解析完毕的数据字节数 ,
下一次读取的时候 读取 nBufSize - nFlvPos 字节数据 , 存放到 主缓冲区 中的 第 nFlvPos 字节位置 , 正好把 主缓冲区 *pBuf 填满 ;
nFlvPos 用于记录 当前的解析位置 , 注意该解析位置指的是在 主缓冲区 中的 0 ~ 2 * 1024 * 1024 之间的解析位置 ;
读取 nReadNum 字节数据 , 该读取的字节数据 一般都是 2 * 1024 * 1024 , 最后一波读取数据不足 2 * 1024 * 1024 , 读取多少是多少 ;
// ① 读取文件内容 到 uint8_t *pBuf 缓冲区 中
int nReadNum = 0; // 实际读取到的数据量
int nUsedLen = 0; // 已解析的数据长度
// 一次读取 2MB 数据 , 解析前面的完整内容
// 最后可能会有几百字节的不完整数据
// 这些不完整数据最后复制出来 放在下次缓冲区的首部
fin.read((char *)pBuf + nFlvPos, nBufSize - nFlvPos); // 读取数据到缓冲区
nReadNum = fin.gcount(); // 获取读取到的数据量
if (nReadNum == 0) // 如果没有更多数据,则退出循环
break;
nFlvPos += nReadNum; // 更新当前数据位置
② 解析内存缓冲区数据
将 nBufSize - nFlvPos 字节的 FLV 视频数据读取到 uint8_t *pBuf 主缓冲区 中 ,
调用 FLV 文件解析器对象 CFlvParser parser 的 Parse 函数 , 解析 主缓冲区 中 前 nFlvPos 字节数据 ,
解析前面的数据 nFlvPos 值都是 int nBufSize = 2 * 1024 * 1024 ,
解析最后一波数据 , 得到的值是最后一次读取的数据字节数 加上 倒数第二次没有解析的字节数 ;
CFlvParser parser; // FLV 文件解析器对象
// ② 解析读取到 uint8_t *pBuf 缓冲区 中的 FLV 文件数据
// ☆ 核心函数入口
parser.Parse(pBuf, nFlvPos, nUsedLen); // 调用解析函数解析缓冲区数据
CFlvParser::Parse 解析函数 , 最后一个参数是引用类型 , 可以作为返回值使用 , 返回的是 解析的 字节个数 ;
int &nUsedLen 参数 是 引用传递 ,
函数调用时 , 形参 nUsedLen 是外部 实参的 引用 , 它与 实参 指向同一块内存空间 , 不产生副本 ,
函数内部对 nUsedLen 的修改 , 会 直接作用于 外部的实参 ;
int CFlvParser::Parse(uint8_t *pBuf, int nBufSize, int &nUsedLen)
③ 解析内存缓冲区数据
调用 CFlvParser::Parse 解析函数 解析 uint8_t *pBuf 主缓冲区 中的 FLV 数据后 , 校对解析结果 ;
nFlvPos 是传入的 数据字节数 , nUsedLen 是解析成功的字节数 , nFlvPos - nUsedLen 是 未解析的字节数 ;
将 *pBuf 主缓冲区 中 第 nUsedLen 字节开始的 nFlvPos - nUsedLen 未解析字节数的 数据 拷贝到 备份缓冲区 *pBak 中 , 然后再将这些数据 拷贝回 *pBuf 主缓冲区 中 ;
// ③ 处理缓冲区中未被解析的数据 , 将这些数据放入到 *pBak 备份缓冲区中 , 之后再将备份缓冲区的数据移动到 *pBuf 主缓冲区
if (nFlvPos != nUsedLen) // 如果有未解析的数据
{
// 将 未解析的数据 复制到 备份缓冲区,然后 移动到 主缓冲区 前端
memcpy(pBak, pBuf + nUsedLen, nFlvPos - nUsedLen);
// 将 未解析的数据 移动到 主缓冲区 前端
memcpy(pBuf, pBak, nFlvPos - nUsedLen);
}
nFlvPos -= nUsedLen; // 更新缓冲区数据的有效长度
④ 打印参数信息并输出流数据
FLV 文件解析器对象 CFlvParser parser 解析完毕 FLV 文件数据之后 ,
调用 CFlvParser::PrintInfo 函数 , 将解析后的 FLV 文件的 音视频参数信息 输出到命令行中 ;
调用 CFlvParser::DumpH264 函数 , 将 H264 视频数据 导出 到指定的文件中 ;
调用 CFlvParser::DumpAAC 函数 , 将 AAC 音频数据 导出 到指定的文件中 ;
// 2. 打印解析后的 FLV 信息
parser.PrintInfo();
// 3. 将 FLV 的 视频/音频 分别输出到不同的文件中
// 导出 H.264 视频流到文件
parser.DumpH264("parser.264");
// 导出 AAC 音频流到文件
parser.DumpAAC("parser.aac");
// 导出完整的 FLV 文件到指定文件
parser.DumpFlv(filename);
// 释放分配的缓冲区内存
delete[] pBak;
delete[] pBuf;
3、文件解析框架代码示例
void Process(fstream &fin, const char *filename)
{
CFlvParser parser; // FLV 文件解析器对象
// 设置每次读取 FLV 文件的缓冲区大小为 2MB
int nBufSize = 2 * 1024 * 1024;
int nFlvPos = 0; // 当前解析的位置
uint8_t *pBuf, *pBak;
pBuf = new uint8_t[nBufSize]; // 分配主缓冲区
pBak = new uint8_t[nBufSize]; // 分配备份缓冲区 , 用于存储每次未解析的不完整数据
// 1. 循环读取并解析 FLV 数据
while (1)
{
// ① 读取文件内容 到 uint8_t *pBuf 缓冲区 中
int nReadNum = 0; // 实际读取到的数据量
int nUsedLen = 0; // 已解析的数据长度
// 一次读取 2MB 数据 , 解析前面的完整内容
// 最后可能会有几百字节的不完整数据
// 这些不完整数据最后复制出来 放在下次缓冲区的首部
fin.read((char *)pBuf + nFlvPos, nBufSize - nFlvPos); // 读取数据到缓冲区
nReadNum = fin.gcount(); // 获取读取到的数据量
if (nReadNum == 0) // 如果没有更多数据,则退出循环
break;
nFlvPos += nReadNum; // 更新当前数据位置
// ② 解析读取到 uint8_t *pBuf 缓冲区 中的 FLV 文件数据
// ☆ 核心函数入口
parser.Parse(pBuf, nFlvPos, nUsedLen); // 调用解析函数解析缓冲区数据
// ③ 处理缓冲区中未被解析的数据 , 将这些数据放入到 *pBak 备份缓冲区中 , 之后再将备份缓冲区的数据移动到 *pBuf 主缓冲区
if (nFlvPos != nUsedLen) // 如果有未解析的数据
{
// 将 未解析的数据 复制到 备份缓冲区,然后 移动到 主缓冲区 前端
memcpy(pBak, pBuf + nUsedLen, nFlvPos - nUsedLen);
// 将 未解析的数据 移动到 主缓冲区 前端
memcpy(pBuf, pBak, nFlvPos - nUsedLen);
}
nFlvPos -= nUsedLen; // 更新缓冲区数据的有效长度
}
// 2. 打印解析后的 FLV 信息
parser.PrintInfo();
// 3. 将 FLV 的 视频/音频 分别输出到不同的文件中
// 导出 H.264 视频流到文件
parser.DumpH264("parser.264");
// 导出 AAC 音频流到文件
parser.DumpAAC("parser.aac");
// 导出完整的 FLV 文件到指定文件
parser.DumpFlv(filename);
// 释放分配的缓冲区内存
delete[] pBak;
delete[] pBuf;
}
四、FLV 解析相关 结构体字段 分析
1、FLV 文件头结构体
FLV 文件头 由 9 字节组成 , 参考 【FFmpeg】FLV 格式分析 ① ( File Header 文件头 | File Body 文件体 | Tag Header 数据块头结构 | Script Data 元数据结构 ) - 2、File Header 文件头 章节内容 ;

下面的结构体 typedef struct FlvHeader_s 的字段就是 FLV 文件头 包含的信息 ;
将解析出的 FLV 版本号 , 是否包含 音频 / 视频 , FLV 头部长度 赋值给 FlvHeader_s 结构体对象 ;
// FLV 文件头
typedef struct FlvHeader_s
{
int nVersion; // 版本
int bHaveVideo; // 是否包含视频
int bHaveAudio; // 是否包含音频
int nHeadSize; // FLV头部长度
/*
* 指向存放FLV头部的buffer
* 上面的三个成员指明了FLV头部的信息,是从FLV的头部中“翻译”得到的,
* 真实的FLV头部是一个二进制比特串,放在一个buffer中,由pFlvHeader成员指明
*/
uint8_t *pFlvHeader;
} FlvHeader;
2、Tag 标签头结构体
Tag 标签头 参考 【FFmpeg】FLV 格式分析 ① ( File Header 文件头 | File Body 文件体 | Tag Header 数据块头结构 | Script Data 元数据结构 ) - 1、Tag Header 数据块头 内容 ;
下面的代码 是 Tag 标签头 的 结构体信息 , 将 Tag 类型 , 标签 body 大小 , 时间戳 , 时间戳扩展字段 , 流 ID 等数据解析出来 , 赋值给 TagHeader 结构体字段 ;

// Tag 标签头部信息
struct TagHeader
{
int nType; // Tag 标签类型, 视频标签/音频标签/元数据标签
int nDataSize; // 标签 body 的大小
int nTimeStamp; // 时间戳
int nTSEx; // 时间戳的扩展字节
int nStreamID; // 流的ID,总是0
uint32_t nTotalTS; // 完整的时间戳nTimeStamp和nTSEx拼装
TagHeader() : nType(0), nDataSize(0), nTimeStamp(0), nTSEx(0), nStreamID(0), nTotalTS(0) {}
~TagHeader() {}
};
3、Tag 标签类
① Tag 标签类 - 基类
下面的 Tag 类 是 FLV 标签类 的 基类 , TagHeader _header 成员 是 对应的该标签的 标签头 结构体 对象 ;
这只是 Tag 标签 的 通用结构 , 不同的标签类型 , 如 : 视频标签 , 音频标签 , 元数据标签 , 有 其它 不同 参数 , 对应的 不同类型的 标签参数 , 一般都在 Tag 标签的 Body 数据中 进行解析 ;
class Tag
{
public:
Tag() : _pTagHeader(NULL), _pTagData(NULL), _pMedia(NULL), _nMediaLen(0) {}
void Init(TagHeader *pHeader, uint8_t *pBuf, int nLeftLen);
TagHeader _header;
uint8_t *_pTagHeader; // 指向标签头部
uint8_t *_pTagData; // 指向标签body,原始的tag data数据
uint8_t *_pMedia; // 指向标签的元数据,改造后的数据
int _nMediaLen; // 数据长度
};
② Tag 标签类 - 视频标签类
下面的 CVideoTag 类 是 视频标签类 信息 , 可参考 【FFmpeg】FLV 格式分析 ③ ( Tag Body 数据块体结构 - Vedio Data 视频数据 ) 博客 内容 ;
基础的 标签类 信息 , 标签类型是 0x09 ;

如果是 视频标签 , 则继续解析 Tag Body 数据块体 , 参考 下图 进行解析 ,
- 第 1 字节 : 表示 帧类型 和 编码格式 ;
- 第 2 字节 : 表示 包类型 , 仅适用于 AVC 格式 ;
- 第 3 ~ 5 字节 : 表示 显示时间戳 PTS 与 解码时间戳 DTS 的 偏移量 ;
- 第 6 字节开始 : 是 视频标签 的 实际数据内容 ;

// 定义 视频标签类 , 继承自 基类 Tag
class CVideoTag : public Tag
{
public:
/**
* 创建视频标签对象
* @brief CVideoTag构造函数
* @param pHeader 标签头指针
* @param pBuf 整个标签数据的起始地址
* @param nLeftLen 剩余可解析数据长度
* @param pParser FLV解析器指针
*/
CVideoTag(TagHeader *pHeader, uint8_t *pBuf, int nLeftLen, CFlvParser *pParser);
int _nFrameType; // 帧类型(如关键帧/非关键帧)
int _nCodecID; // 视频编解码器类型(如H.264)
int ParseH264Tag(CFlvParser *pParser); // 解析H264视频标签
int ParseH264Configuration(CFlvParser *pParser, uint8_t *pTagData); // 解析H264配置信息
int ParseNalu(CFlvParser *pParser, uint8_t *pTagData); // 解析NALU单元
};
③ Tag 标签类 - 音频标签类
下面的 CAudioTag 类 是 音频标签类 , 继承了 标签类基类 Tag 类 ;
音频标签类 结构解析 参考 【FFmpeg】FLV 格式分析 ② ( Tag Body 数据块体结构 - Audio Data 音频数据 | AAC 序列头 AudioSpecificConfig 结构分析 ) 博客内容 ;
基础标签头 的 标签类型是 0x08 音频数据 ,
音频数据 的 Tag 数据块 的 Tag Body 结构 ,
- 第 1 字节 : 包含 音频格式、采样率、采样精度、声道类型 参数信息 ;
- 第 2 字节 :
- 假如是 AAC 类型音频数据 , 该字节是 AAC 包类型 参数 , 从 第 3 字节开始 是 音频采样数据 ;
- 假如不是 AAC 类型的音频数据 , 从 第 2 字节开始 , 就是 音频采样数据 ;

// 定义 音频标签类 , 继承自 基类 Tag
class CAudioTag : public Tag
{
public:
// 创建 音频标签对象
CAudioTag(TagHeader *pHeader, uint8_t *pBuf, int nLeftLen, CFlvParser *pParser);
int _nSoundFormat; // 音频编码格式(如AAC/MP3)
int _nSoundRate; // 采样率(如44.1kHz)
int _nSoundSize; // 采样精度(如16bit)
int _nSoundType; // 声道类型(单声道/立体声)
// AAC音频相关静态成员
static int _aacProfile; // AAC规格配置(如LC/HE-AAC)
static int _sampleRateIndex; // 采样率索引值
static int _channelConfig; // 声道配置(如双声道)
int ParseAACTag(CFlvParser *pParser); // 解析AAC音频标签
int ParseAudioSpecificConfig(CFlvParser *pParser, uint8_t *pTagData); // 解析AAC配置信息
int ParseRawAAC(CFlvParser *pParser, uint8_t *pTagData); // 解析原始AAC数据
};
④ Tag 标签类 - 元数据标签类
下面的 CMetaDataTag 元数据标签 继承了 Tag 标签类 , 其中封装了 元数据标签 的 相关参数信息 ;
基础标签头 的 标签类型是 0x12 元数据标签 , 则 Tag Body 的 标签体数据 如下 :
元数据标签 的 结构 可参考 【FFmpeg】FLV 格式分析 ① ( File Header 文件头 | File Body 文件体 | Tag Header 数据块头结构 | Script Data 元数据结构 ) - 四、Tag Body 数据块体结构 - Script Data 元数据 博客章节 ;
元数据标签 的 Tag Body 标签体 使用 AMF 编码存储数据 , AMF 数据块结构如下 :

每个 AMF数据类型 都有一个 键值对 数据 , 每个 键 都有一个键名 以及 对应的 数据类型 , 键名 和 数据类型如下图所示 :

// 定义 元数据标签类 , 继承自 基类Tag
class CMetaDataTag : public Tag
{
public:
CMetaDataTag(TagHeader *pHeader, uint8_t *pBuf, int nLeftLen, CFlvParser *pParser);
double hexStr2double(const unsigned char* hex, const unsigned int length); // 十六进制转double
int parseMeta(CFlvParser *pParser); // 解析元数据
void printMeta(); // 打印元数据信息
// AMF格式元数据解析相关字段
uint8_t m_amf1_type; // 第一个AMF数据类型
uint32_t m_amf1_size; // 第一个AMF数据大小
uint8_t m_amf2_type; // 第二个AMF数据类型
unsigned char *m_meta; // 元数据存储指针
unsigned int m_length; // 元数据长度
// 视频相关元数据
double m_duration; // 总时长(秒)
double m_width; // 视频宽度(像素)
double m_height; // 视频高度(像素)
double m_videodatarate; // 视频码率(kbps)
double m_framerate; // 帧率(fps)
double m_videocodecid; // 视频编码ID
// 音频相关元数据
double m_audiodatarate; // 音频码率(kbps)
double m_audiosamplerate; // 音频采样率(Hz)
double m_audiosamplesize; // 音频采样大小(bit)
bool m_stereo; // 是否立体声
double m_audiocodecid; // 音频编码ID
// 文件格式信息
string m_major_brand; // 主要品牌(如mp42)
string m_minor_version; // 次要版本号
string m_compatible_brands; // 兼容品牌列表
string m_encoder; // 编码器信息
double m_filesize; // 文件总大小(字节)
};
五、解析 FLV 文件头 和 FLV 文件体
1、解析 FLV 文件头
CFlvParser::Parse 函数 是 解析 FLV 数据 的 函数入口 , 在该函数中 解析 FLV 文件头 和 文件体 数据 ;
FLV 文件头 数据 是 9 字节 , 参考下图 进行解析即可 ;

在 CFlvParser::Parse 函数中 , 如果是刚开始解析 , 还没有解析 FLV 头部,先解析 FLV 的 9 字节文件头 , 核心是调用 CreateFlvHeader 函数 解析 文件头数据 ;
// 解析 FLV 数据
// FLV 文件由 FLV 文件头 和 FLV 文件体 构成
// FLV 文件头 由若干 Previous Tag Size 前一个标签大小 + Tag 数据块 组成
// 解析 FLV Tag 有不同的类型 , 分别是 音频数据 / 视频数据 / 元数据
// 本函数中主要执行两个操作 : ① 解析 FLV 文件头 , ② 解析 FLV Tag 标签
int CFlvParser::Parse(uint8_t *pBuf, int nBufSize, int &nUsedLen)
{
int nOffset = 0; // 当前解析的偏移量
// 1. 解析 FLV 文件头
// 如果还没有解析 FLV 头部,先解析 FLV 的 9 字节文件头
if (_pFlvHeader == 0)
{
CheckBuffer(9); // 检查缓冲区是否至少有 9 字节
// ☆ 核心函数 : 创建 FLV 头部对象 , CFlvParser::FlvHeader 结构体对象
_pFlvHeader = CreateFlvHeader(pBuf+nOffset);
nOffset += _pFlvHeader->nHeadSize; // 更新偏移量
}
在 上述 CFlvParser::Parse 函数中 , 调用了 CFlvParser::CreateFlvHeader 函数 , 在该函数中 创建了 FlvHeader 结构体对象 , 在该对象中封装了 FLV 文件头参数 ;
下面是 创建 FlvHeader 并为其填充参数 的具体过程 ;
/**
* 创建 FLV 头部对象 FlvHeader, 这是一个结构体对象 , 其中封装了 版本 , 是否包含视频 / 音频 , 头部长度
* @brief CFlvParser::CreateFlvHeader
* @param pBuf
* @return
*/
CFlvParser::FlvHeader *CFlvParser::CreateFlvHeader(uint8_t *pBuf)
{
// 创建 FlvHeader 结构体对象
FlvHeader *pHeader = new FlvHeader;
// 为头部数据填充数据
pHeader->nVersion = pBuf[3]; // 版本号 Signature
pHeader->bHaveAudio = (pBuf[4] >> 2) & 0x01; // 是否有音频
pHeader->bHaveVideo = (pBuf[4] >> 0) & 0x01; // 是否有视频
pHeader->nHeadSize = ShowU32(pBuf + 5); // 头部长度
// 根据头部长度 创建 头部数据 内存空间
pHeader->pFlvHeader = new uint8_t[pHeader->nHeadSize];
// 拷贝头部数据到 FlvHeader 结构体中
memcpy(pHeader->pFlvHeader, pBuf, pHeader->nHeadSize);
return pHeader;
}
2、解析 FLV 文件体
FLV 文件体 就是 若干 FLV Tag 标签 数据 ,
解析完 FLV 文件的 前 9 个字节之后 , 就是 若干 FLV 的 Tag 标签数据 ;
① 解析 Previous Tag Size 前一个标签大小 + Tag Header 标签头
每组 Tag 标签数据由 " Previous Tag Size 前一个标签大小 + Tag 数据块 " 组成 ,
这里使用 while (1) 开启一个 无限循环 , 解析 FLV Tag 数据 ,
在每个循环中 :
- 首先 , 解析 Previous Tag Size 前一个标签大小 ( 4 字节 ) 和 当前 Tag 的头部 ( 11 字节 ) , 确保缓冲区有足够的 15 字节 用于解析 , 如果剩余数据不够 15 字节 , 则无法解析出完整的 TAG 数据 ;
- 然后 , 解析 Tag 数据 , 调用 解析 Tag 数据的核心函数 CreateTag 函数 , 在该函数中 , 根据不同的 Tag 标签类型 , 如 : 视频标签 , 音频标签 , 元数据标签 , 进行不同的解析操作 , 创建不同的 Tag 类型对象 ;
// 解析 FLV 数据
// FLV 文件由 FLV 文件头 和 FLV 文件体 构成
// FLV 文件体 由若干 Previous Tag Size 前一个标签大小 + Tag 数据块 组成
// 解析 FLV Tag 有不同的类型 , 分别是 音频数据 / 视频数据 / 元数据
// 本函数中主要执行两个操作 : ① 解析 FLV 文件头 , ② 解析 FLV Tag 标签
int CFlvParser::Parse(uint8_t *pBuf, int nBufSize, int &nUsedLen)
{
int nOffset = 0; // 当前解析的偏移量
// ... 省略 解析文件头逻辑
// 2. 解析 FLV Tag 数据
// 循环解析 FLV Tag 数据
while (1)
{
// ① 解析 Previous Tag Size 前一个标签大小 ( 4 字节 ) 和 当前 Tag 的头部 ( 11 字节 )
// 如果剩余数据不够 15 字节 , 则无法解析出完整的 TAG 数据
CheckBuffer(15); // 确保缓冲区有足够的字节用于解析
// 包括前一个 Tag 的大小 (4 字节) 和当前 Tag 的头部 (11 字节)
int nPrevSize = ShowU32(pBuf + nOffset); // 读取前一个 Tag 的大小
nOffset += 4; // 偏移量增加 4 字节
// 解析 Tag 数据的核心函数
Tag *pTag = CreateTag(pBuf + nOffset, nBufSize-nOffset); // 创建当前 Tag 对象
if (pTag == NULL)
{
nOffset -= 4; // 回退 4 字节偏移量
break; // 无法创建 Tag 时退出循环
}
nOffset += (11 + pTag->_header.nDataSize); // 更新偏移量,跳过 Tag 数据
// 将解析出的 Tag 添加到列表中
// 该列表中包含了 FLV 文件中的所有 TAG 标签数据 , 包括 音频标签 / 视频标签 / 元数据标签
_vpTag.push_back(pTag);
}
nUsedLen = nOffset; // 设置已使用的长度
return 0;
}
② 根据 Tag Header 不同的标签类型创建不同的标签
在 CFlvParser::CreateTag 函数中 , 解析 Tag Header 标签头信息 ,
标签头 Tag Header 中 包含 标签类型 , 标签体 Tag Body 长度 , 时间戳信息 , 流 ID 信息 参数 ;
根据 标签的类型 , 调用不同的函数 进行解析 ,
- 如果是 视频标签 , 则调用 CVideoTag 构造函数 创建 视频标签对象 , 并填充 标签的具体参数和数据 ;
- 如果是 音频标签 , 则调用 CAudioTag 构造函数 创建 音频标签对象 , 并填充 标签的具体参数和数据 ;
- 如果是 元数据标签 , 则调用 CMetaDataTag 构造函数 创建 元数据标签对象 , 并填充 标签的具体参数和数据 ;
// 创建 FLV 标签对象
CFlvParser::Tag *CFlvParser::CreateTag(uint8_t *pBuf, int nLeftLen)
{
// 开始解析 标签头部
TagHeader header;
// 读取 pBuf 数组中的第 0 个字节开始 的 1 字节数据
header.nType = ShowU8(pBuf + 0); // 类型,标识当前标签是视频、音频还是脚本数据
// 读取 pBuf 数组中的第 1 个字节开始 的 3 字节数据
header.nDataSize = ShowU24(pBuf + 1); // 标签body的长度
// 读取 pBuf 数组中的第 4 个字节开始 的 3 字节数据
header.nTimeStamp = ShowU24(pBuf + 4); // 时间戳(低24位)
header.nTSEx = ShowU8(pBuf + 7); // 时间戳的扩展字段(高8位)
header.nStreamID = ShowU24(pBuf + 8); // 流的ID,通常为0
header.nTotalTS = (uint32_t)((header.nTSEx << 24)) + header.nTimeStamp; // 组合完整的时间戳
// 标签头部解析结束
// 检查剩余数据是否足够包含标签头部和数据
if ((header.nDataSize + 11) > nLeftLen)
{
return NULL; // 如果数据不足,则返回NULL
}
Tag *pTag; // 创建对应类型的Tag对象
switch (header.nType) {
case 0x09: // 视频类型的Tag
pTag = new CVideoTag(&header, pBuf, nLeftLen, this);
break;
case 0x08: // 音频类型的Tag
pTag = new CAudioTag(&header, pBuf, nLeftLen, this);
break;
case 0x12: // 脚本类型的Tag(如元数据)
pTag = new CMetaDataTag(&header, pBuf, nLeftLen, this);
break;
default: // 其他类型的Tag
pTag = new Tag();
pTag->Init(&header, pBuf, nLeftLen); // 初始化默认的Tag
}
return pTag; // 返回创建的Tag对象
}
③ 创建视频标签
创建视频标签 , 主要是 解析 视频标签 的 各种参数信息 , 以及 将视频标签 的 实际数据 解析出来 ;

参考上图 , 以及下面的源码 , 可以看到 视频标签的 Tag Data 第一个字节 分为两部分 ,
- 高 4 字节是 帧类型, 1 关键帧 , 2 普通帧
- 低 4 字节是 视频编码类型 , 1 JPEG , 2 H.263 , 7 AVC
将上述参数解析出来 , 赋值给 CVideoTag 对象的相应字段 即可 , 然后继续调用 ParseH264Tag 函数 继续解析 AVC 视频标签内容 ;
// 创建 视频标签 对象
CFlvParser::CVideoTag::CVideoTag(TagHeader *pHeader, uint8_t *pBuf, int nLeftLen, CFlvParser *pParser)
{
// 从主缓冲区中获取 Tag 的 header 和 body 数据
Init(pHeader, pBuf, nLeftLen);
// 视频标签 的 Tag Data 数据内容
uint8_t *pd = _pTagData;
// 视频标签的 Tag Data 第一个字节 分为两部分
// 高 4 字节是 帧类型, 1 关键帧 , 2 普通帧
// 低 4 字节是 视频编码类型 , 1 JPEG , 2 H.263 , 7 AVC
_nFrameType = (pd[0] & 0xf0) >> 4; // 帧类型
_nCodecID = pd[0] & 0x0f; // 视频编码类型
// 这里只处理 H.264 视频 , 即视频编码类型为 AVC 格式的情况 , 其它情况不处理
if (_header.nType == 0x09 && _nCodecID == 7)
{
// 解析 H.264 格式的 视频标签数据
ParseH264Tag(pParser); // 解析 H.264 视频标签
}
}
④ 创建音频标签
/**
* @brief 解析音频标签数据
* 音频标签数据中包含音频格式、采样率、采样精度和声道信息。
* 对于 AAC 音频数据,需要进一步判断是配置信息还是原始数据。
* @param pHeader 标签头部
* @param pBuf 数据缓冲区
* @param nLeftLen 剩余数据长度
* @param pParser FLV 解析器实例
*/
CFlvParser::CAudioTag::CAudioTag(TagHeader *pHeader, uint8_t *pBuf, int nLeftLen, CFlvParser *pParser)
{
// 从主缓冲区中获取 Tag 的 header 和 body 数据
Init(pHeader, pBuf, nLeftLen);
uint8_t *pd = _pTagData;
// 解析第 1 个字节数据
// 这些数据 仅供参考 , 实际的音频格式数据在 AAC 配置 数据包信息中
_nSoundFormat = (pd[0] & 0xf0) >> 4; // 音频格式
_nSoundRate = (pd[0] & 0x0c) >> 2; // 采样率
_nSoundSize = (pd[0] & 0x02) >> 1; // 采样精度
_nSoundType = (pd[0] & 0x01); // 是否为立体声
if (_nSoundFormat == 10) // AAC 格式
{
ParseAACTag(pParser); // 解析 AAC 标签
}
}
⑤ 创建元数据标签
CFlvParser::CMetaDataTag::CMetaDataTag(TagHeader *pHeader, uint8_t *pBuf, int nLeftLen, CFlvParser *pParser)
{
// 从主缓冲区中获取 Tag 的 header 和 body 数据
Init(pHeader, pBuf, nLeftLen); // 初始化元数据标签
uint8_t *pd = _pTagData;
// 读取 AMF 数据类型 , 标签数据的 第 0 字节开始的 1 字节数据 是 数据类型信息
m_amf1_type = ShowU8(pd + 0);
// // 读取 AMF 数据大小 , 第 1 字节开始的 2 字节数据是 数据大小
m_amf1_size = ShowU16(pd + 1);
if (m_amf1_type != 2) // 判断是否为AMF字符串类型
{
printf("no metadata\n"); // 如果不是,输出无元数据信息
return;
}
// 解析 Script 数据
if (strncmp((const char *)"onMetaData", (const char *)(pd + 3), 10) == 0)
parseMeta(pParser); // 如果是“onMetaData”,调用parseMeta解析元数据
}
六、完整代码示例
1、main 函数入口代码 - main.cpp
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <fstream>
#include "FlvParser.h"
using namespace std;
// 处理 FLV 文件的函数声明
void Process(fstream &fin, const char *filename);
int main(int argc, char *argv[])
{
fstream fin;
// 1. 打开输入的 FLV 源文件
fin.open("input.flv", ios_base::in | ios_base::binary);
if (!fin)
return 0;
// 2. 调用处理函数处理 FLV 文件
Process(fin, "dump.flv");
// 3. 处理完毕后 , 关闭文件 , 并退出程序
fin.close(); // 关闭文件
return 1;
}
void Process(fstream &fin, const char *filename)
{
CFlvParser parser; // FLV 文件解析器对象
// 设置每次读取 FLV 文件的缓冲区大小为 2MB
int nBufSize = 2 * 1024 * 1024;
int nFlvPos = 0; // 当前解析的位置
uint8_t *pBuf, *pBak;
pBuf = new uint8_t[nBufSize]; // 分配主缓冲区
pBak = new uint8_t[nBufSize]; // 分配备份缓冲区 , 用于存储每次未解析的不完整数据
// 1. 循环读取并解析 FLV 数据
while (1)
{
// ① 读取文件内容 到 uint8_t *pBuf 缓冲区 中
int nReadNum = 0; // 实际读取到的数据量
int nUsedLen = 0; // 已解析的数据长度
// 一次读取 2MB 数据 , 解析前面的完整内容
// 最后可能会有几百字节的不完整数据
// 这些不完整数据最后复制出来 放在下次缓冲区的首部
fin.read((char *)pBuf + nFlvPos, nBufSize - nFlvPos); // 读取数据到缓冲区
nReadNum = fin.gcount(); // 获取读取到的数据量
if (nReadNum == 0) // 如果没有更多数据,则退出循环
break;
nFlvPos += nReadNum; // 更新当前数据位置
// ② 解析读取到 uint8_t *pBuf 缓冲区 中的 FLV 文件数据
// ☆ 核心函数入口
parser.Parse(pBuf, nFlvPos, nUsedLen); // 调用解析函数解析缓冲区数据
// ③ 处理缓冲区中未被解析的数据 , 将这些数据放入到 *pBak 备份缓冲区中 , 之后再将备份缓冲区的数据移动到 *pBuf 主缓冲区
if (nFlvPos != nUsedLen) // 如果有未解析的数据
{
// 将 未解析的数据 复制到 备份缓冲区,然后 移动到 主缓冲区 前端
memcpy(pBak, pBuf + nUsedLen, nFlvPos - nUsedLen);
// 将 未解析的数据 移动到 主缓冲区 前端
memcpy(pBuf, pBak, nFlvPos - nUsedLen);
}
nFlvPos -= nUsedLen; // 更新缓冲区数据的有效长度
}
// 2. 打印解析后的 FLV 信息
parser.PrintInfo();
// 3. 将 FLV 的 视频/音频 分别输出到不同的文件中
// 导出 H.264 视频流到文件
parser.DumpH264("parser.264");
// 导出 AAC 音频流到文件
parser.DumpAAC("parser.aac");
// 导出完整的 FLV 文件到指定文件
parser.DumpFlv(filename);
// 释放分配的缓冲区内存
delete[] pBak;
delete[] pBuf;
}
2、FLV 文件解析核心代码 - FlvParser.h 头文件
#ifndef FLVPARSER_H
#define FLVPARSER_H
#include <iostream>
#include <vector>
#include <stdint.h>
#include "Videojj.h"
using namespace std;
typedef unsigned long long uint64_t;
class CFlvParser
{
public:
CFlvParser();
virtual ~CFlvParser();
int Parse(uint8_t *pBuf, int nBufSize, int &nUsedLen);
int PrintInfo();
int DumpH264(const std::string &path);
int DumpAAC(const std::string &path);
int DumpFlv(const std::string &path);
private:
// FLV 文件头
typedef struct FlvHeader_s
{
int nVersion; // 版本
int bHaveVideo; // 是否包含视频
int bHaveAudio; // 是否包含音频
int nHeadSize; // FLV头部长度
/*
* 指向存放FLV头部的buffer
* 上面的三个成员指明了FLV头部的信息,是从FLV的头部中“翻译”得到的,
* 真实的FLV头部是一个二进制比特串,放在一个buffer中,由pFlvHeader成员指明
*/
uint8_t *pFlvHeader;
} FlvHeader;
// Tag 标签头部信息
struct TagHeader
{
int nType; // Tag 标签类型, 视频标签/音频标签/元数据标签
int nDataSize; // 标签 body 的大小
int nTimeStamp; // 时间戳
int nTSEx; // 时间戳的扩展字节
int nStreamID; // 流的ID,总是0
uint32_t nTotalTS; // 完整的时间戳nTimeStamp和nTSEx拼装
TagHeader() : nType(0), nDataSize(0), nTimeStamp(0), nTSEx(0), nStreamID(0), nTotalTS(0) {}
~TagHeader() {}
};
class Tag
{
public:
Tag() : _pTagHeader(NULL), _pTagData(NULL), _pMedia(NULL), _nMediaLen(0) {}
void Init(TagHeader *pHeader, uint8_t *pBuf, int nLeftLen);
TagHeader _header;
uint8_t *_pTagHeader; // 指向标签头部
uint8_t *_pTagData; // 指向标签body,原始的tag data数据
uint8_t *_pMedia; // 指向标签的元数据,改造后的数据
int _nMediaLen; // 数据长度
};
// 定义 视频标签类 , 继承自 基类 Tag
class CVideoTag : public Tag
{
public:
/**
* 创建视频标签对象
* @brief CVideoTag构造函数
* @param pHeader 标签头指针
* @param pBuf 整个标签数据的起始地址
* @param nLeftLen 剩余可解析数据长度
* @param pParser FLV解析器指针
*/
CVideoTag(TagHeader *pHeader, uint8_t *pBuf, int nLeftLen, CFlvParser *pParser);
int _nFrameType; // 帧类型(如关键帧/非关键帧)
int _nCodecID; // 视频编解码器类型(如H.264)
int ParseH264Tag(CFlvParser *pParser); // 解析H264视频标签
int ParseH264Configuration(CFlvParser *pParser, uint8_t *pTagData); // 解析H264配置信息
int ParseNalu(CFlvParser *pParser, uint8_t *pTagData); // 解析NALU单元
};
// 定义 音频标签类 , 继承自 基类 Tag
class CAudioTag : public Tag
{
public:
// 创建 音频标签对象
CAudioTag(TagHeader *pHeader, uint8_t *pBuf, int nLeftLen, CFlvParser *pParser);
int _nSoundFormat; // 音频编码格式(如AAC/MP3)
int _nSoundRate; // 采样率(如44.1kHz)
int _nSoundSize; // 采样精度(如16bit)
int _nSoundType; // 声道类型(单声道/立体声)
// AAC音频相关静态成员
static int _aacProfile; // AAC规格配置(如LC/HE-AAC)
static int _sampleRateIndex; // 采样率索引值
static int _channelConfig; // 声道配置(如双声道)
int ParseAACTag(CFlvParser *pParser); // 解析AAC音频标签
int ParseAudioSpecificConfig(CFlvParser *pParser, uint8_t *pTagData); // 解析AAC配置信息
int ParseRawAAC(CFlvParser *pParser, uint8_t *pTagData); // 解析原始AAC数据
};
// 定义 元数据标签类 , 继承自 基类Tag
class CMetaDataTag : public Tag
{
public:
CMetaDataTag(TagHeader *pHeader, uint8_t *pBuf, int nLeftLen, CFlvParser *pParser);
double hexStr2double(const unsigned char* hex, const unsigned int length); // 十六进制转double
int parseMeta(CFlvParser *pParser); // 解析元数据
void printMeta(); // 打印元数据信息
// AMF格式元数据解析相关字段
uint8_t m_amf1_type; // 第一个AMF数据类型
uint32_t m_amf1_size; // 第一个AMF数据大小
uint8_t m_amf2_type; // 第二个AMF数据类型
unsigned char *m_meta; // 元数据存储指针
unsigned int m_length; // 元数据长度
// 视频相关元数据
double m_duration; // 总时长(秒)
double m_width; // 视频宽度(像素)
double m_height; // 视频高度(像素)
double m_videodatarate; // 视频码率(kbps)
double m_framerate; // 帧率(fps)
double m_videocodecid; // 视频编码ID
// 音频相关元数据
double m_audiodatarate; // 音频码率(kbps)
double m_audiosamplerate; // 音频采样率(Hz)
double m_audiosamplesize; // 音频采样大小(bit)
bool m_stereo; // 是否立体声
double m_audiocodecid; // 音频编码ID
// 文件格式信息
string m_major_brand; // 主要品牌(如mp42)
string m_minor_version; // 次要版本号
string m_compatible_brands; // 兼容品牌列表
string m_encoder; // 编码器信息
double m_filesize; // 文件总大小(字节)
};
// FLV文件统计信息结构体
struct FlvStat
{
int nMetaNum; // 元数据标签数量
int nVideoNum; // 视频标签数量
int nAudioNum; // 音频标签数量
int nMaxTimeStamp; // 最大时间戳值
int nLengthSize; // NALU长度字段字节数(H.264特有)
FlvStat() : nMetaNum(0), nVideoNum(0), nAudioNum(0), nMaxTimeStamp(0), nLengthSize(0) {}
~FlvStat() {}
};
/******************** 字节操作工具函数 ********************/
// 从缓冲区读取大端序32位无符号整数
static uint32_t ShowU32(uint8_t *pBuf) { return (pBuf[0] << 24) | (pBuf[1] << 16) | (pBuf[2] << 8) | pBuf[3]; }
// 从缓冲区读取大端序24位无符号整数
static uint32_t ShowU24(uint8_t *pBuf) { return (pBuf[0] << 16) | (pBuf[1] << 8) | (pBuf[2]); }
// 从缓冲区读取大端序16位无符号整数
static uint32_t ShowU16(uint8_t *pBuf) { return (pBuf[0] << 8) | (pBuf[1]); }
// 从缓冲区读取8位无符号整数
static uint32_t ShowU8(uint8_t *pBuf) { return (pBuf[0]); }
// 向64位整数写入指定位数的值(用于位操作)
static void WriteU64(uint64_t & x, int length, int value) {
uint64_t mask = 0xFFFFFFFFFFFFFFFF >> (64 - length);
x = (x << length) | ((uint64_t)value & mask);
}
// 将32位整数转换为大端序字节表示
static uint32_t WriteU32(uint32_t n) {
uint32_t nn = 0;
uint8_t *p = (uint8_t *)&n;
uint8_t *pp = (uint8_t *)&nn;
pp[0] = p[3]; // 高位字节在前
pp[1] = p[2];
pp[2] = p[1];
pp[3] = p[0];
return nn;
}
// 声明友元类(允许Tag类访问私有成员)
friend class Tag;
private:
// 创建FLV头部对象
FlvHeader *CreateFlvHeader(uint8_t *pBuf);
// 销毁FLV头部对象
int DestroyFlvHeader(FlvHeader *pHeader);
// 创建标签对象
Tag *CreateTag(uint8_t *pBuf, int nLeftLen);
// 销毁标签对象
int DestroyTag(Tag *pTag);
// 统计FLV文件信息
int Stat();
// 统计视频标签信息
int StatVideo(Tag *pTag);
// 判断是否为用户数据标签
int IsUserDataTag(Tag *pTag);
private:
FlvHeader* _pFlvHeader; // FLV头部指针
vector<Tag *> _vpTag; // 存储所有标签的容器
FlvStat _sStat; // FLV统计信息
CVideojj *_vjj; // 视频处理相关对象(疑似拼写错误)
// H.264相关参数
int _nNalUnitLength; // NALU长度字段字节数(通常为4)
};
#endif // FLVPARSER_H
3、FLV 文件解析核心代码 - FlvParser.cpp
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <iostream>
#include <fstream>
#include "FlvParser.h"
using namespace std;
// 检查 缓冲区 是否有 足够的剩余数据 , 缓冲区的数据是否大于等于 x 字节 , 如果不足 ( 缓冲区数据小于等于 x ) 则返回 0
#define CheckBuffer(x) { if ((nBufSize-nOffset)<(x)) { nUsedLen = nOffset; return 0;} }
// 初始化静态成员变量
int CFlvParser::CAudioTag::_aacProfile;
int CFlvParser::CAudioTag::_sampleRateIndex;
int CFlvParser::CAudioTag::_channelConfig;
// 定义 H264 起始码
static const uint32_t nH264StartCode = 0x01000000;
// 构造函数,初始化成员变量
CFlvParser::CFlvParser()
{
_pFlvHeader = NULL;
_vjj = new CVideojj();
}
// 析构函数,释放分配的内存
CFlvParser::~CFlvParser()
{
for (int i = 0; i < _vpTag.size(); i++)
{
DestroyTag(_vpTag[i]); // 销毁 Tag 对象
delete _vpTag[i]; // 释放内存
}
if (_vjj != NULL)
delete _vjj; // 释放 CVideojj 对象
}
// 解析 FLV 数据
// FLV 文件由 FLV 文件头 和 FLV 文件体 构成
// FLV 文件头 由若干 Previous Tag Size 前一个标签大小 + Tag 数据块 组成
// 解析 FLV Tag 有不同的类型 , 分别是 音频数据 / 视频数据 / 元数据
// 本函数中主要执行两个操作 : ① 解析 FLV 文件头 , ② 解析 FLV Tag 标签
int CFlvParser::Parse(uint8_t *pBuf, int nBufSize, int &nUsedLen)
{
int nOffset = 0; // 当前解析的偏移量
// 1. 解析 FLV 文件头
// 如果还没有解析 FLV 头部,先解析 FLV 的 9 字节文件头
if (_pFlvHeader == 0)
{
CheckBuffer(9); // 检查缓冲区是否至少有 9 字节
// ☆ 核心函数 : 创建 FLV 头部对象 , CFlvParser::FlvHeader 结构体对象
_pFlvHeader = CreateFlvHeader(pBuf+nOffset);
nOffset += _pFlvHeader->nHeadSize; // 更新偏移量
}
// 2. 解析 FLV Tag 数据
// 循环解析 FLV Tag 数据
while (1)
{
// ① 解析 Previous Tag Size 前一个标签大小 ( 4 字节 ) 和 当前 Tag 的头部 ( 11 字节 )
// 如果剩余数据不够 15 字节 , 则无法解析出完整的 TAG 数据
CheckBuffer(15); // 确保缓冲区有足够的字节用于解析
// 包括前一个 Tag 的大小 (4 字节) 和当前 Tag 的头部 (11 字节)
int nPrevSize = ShowU32(pBuf + nOffset); // 读取前一个 Tag 的大小
nOffset += 4; // 偏移量增加 4 字节
// 解析 Tag 数据的核心函数
Tag *pTag = CreateTag(pBuf + nOffset, nBufSize-nOffset); // 创建当前 Tag 对象
if (pTag == NULL)
{
nOffset -= 4; // 回退 4 字节偏移量
break; // 无法创建 Tag 时退出循环
}
nOffset += (11 + pTag->_header.nDataSize); // 更新偏移量,跳过 Tag 数据
// 将解析出的 Tag 添加到列表中
// 该列表中包含了 FLV 文件中的所有 TAG 标签数据 , 包括 音频标签 / 视频标签 / 元数据标签
_vpTag.push_back(pTag);
}
nUsedLen = nOffset; // 设置已使用的长度
return 0;
}
// 打印解析出的信息
int CFlvParser::PrintInfo()
{
Stat(); // 统计 Tag 信息
// 打印统计结果
cout << "vnum: " << _sStat.nVideoNum << " , anum: " << _sStat.nAudioNum << " , mnum: " << _sStat.nMetaNum << endl;
cout << "maxTimeStamp: " << _sStat.nMaxTimeStamp << " ,nLengthSize: " << _sStat.nLengthSize << endl;
cout << "Vjj SEI num: " << _vjj->_vVjjSEI.size() << endl;
for (int i = 0; i < _vjj->_vVjjSEI.size(); i++)
cout << "SEI time : " << _vjj->_vVjjSEI[i].nTimeStamp << endl;
return 1;
}
// 导出 H264 数据到指定文件路径
int CFlvParser::DumpH264(const std::string &path)
{
fstream f;
f.open(path.c_str(), ios_base::out | ios_base::binary); // 以二进制方式打开文件
vector<Tag *>::iterator it_tag;
for (it_tag = _vpTag.begin(); it_tag != _vpTag.end(); it_tag++)
{
if ((*it_tag)->_header.nType != 0x09) // 只处理视频 Tag (类型为 0x09)
continue;
f.write((char *)(*it_tag)->_pMedia, (*it_tag)->_nMediaLen); // 写入媒体数据
}
f.close(); // 关闭文件
return 1;
}
// 导出 AAC 数据到指定文件路径
int CFlvParser::DumpAAC(const std::string &path)
{
fstream f;
f.open(path.c_str(), ios_base::out | ios_base::binary); // 以二进制方式打开文件
vector<Tag *>::iterator it_tag;
for (it_tag = _vpTag.begin(); it_tag != _vpTag.end(); it_tag++)
{
if ((*it_tag)->_header.nType != 0x08) // 只处理音频 Tag (类型为 0x08)
continue;
CAudioTag *pAudioTag = (CAudioTag *)(*it_tag); // 转换为音频 Tag
if (pAudioTag->_nSoundFormat != 10) // 只处理 AAC 格式的音频
continue;
if (pAudioTag->_nMediaLen != 0)
f.write((char *)(*it_tag)->_pMedia, (*it_tag)->_nMediaLen); // 写入媒体数据
}
f.close(); // 关闭文件
return 1;
}
int CFlvParser::DumpFlv(const std::string &path)
{
fstream f;
f.open(path.c_str(), ios_base::out | ios_base::binary);
// 写入 FLV 文件头部
f.write((char *)_pFlvHeader->pFlvHeader, _pFlvHeader->nHeadSize);
uint32_t nLastTagSize = 0;
// 写入 FLV 标签
vector<Tag *>::iterator it_tag;
for (it_tag = _vpTag.begin(); it_tag < _vpTag.end(); it_tag++)
{
uint32_t nn = WriteU32(nLastTagSize); // 写入上一个标签的大小
f.write((char *)&nn, 4);
// 检查是否存在重复的起始码
if ((*it_tag)->_header.nType == 0x09 && *((*it_tag)->_pTagData + 1) == 0x01) {
bool duplicate = false;
uint8_t *pStartCode = (*it_tag)->_pTagData + 5 + _nNalUnitLength; // 获取起始码指针
unsigned nalu_len = 0; // 存储 NALU 的长度
uint8_t *p_nalu_len=(uint8_t *)&nalu_len;
switch (_nNalUnitLength) {
case 4:
nalu_len = ShowU32((*it_tag)->_pTagData + 5);
break;
case 3:
nalu_len = ShowU24((*it_tag)->_pTagData + 5);
break;
case 2:
nalu_len = ShowU16((*it_tag)->_pTagData + 5);
break;
default:
nalu_len = ShowU8((*it_tag)->_pTagData + 5);
break;
}
uint8_t *pStartCodeRecord = pStartCode;
int i;
// 遍历标签数据,检查重复的 SPS、PPS 或 SEI 起始码
for (i = 0; i < (*it_tag)->_header.nDataSize - 5 - _nNalUnitLength - 4; ++i) {
if (pStartCode[i] == 0x00 && pStartCode[i+1] == 0x00 && pStartCode[i+2] == 0x00 &&
pStartCode[i+3] == 0x01) {
if (pStartCode[i+4] == 0x67) {
i += 4; // 跳过 SPS
continue;
}
else if (pStartCode[i+4] == 0x68) {
i += 4; // 跳过 PPS
continue;
}
else if (pStartCode[i+4] == 0x06) {
i += 4; // 跳过 SEI
continue;
}
else {
i += 4;
duplicate = true; // 标记为重复
break;
}
}
}
if (duplicate) {
nalu_len -= i; // 调整 NALU 的长度
(*it_tag)->_header.nDataSize -= i; // 调整标签数据大小
uint8_t *p = (uint8_t *)&((*it_tag)->_header.nDataSize);
(*it_tag)->_pTagHeader[1] = p[2];
(*it_tag)->_pTagHeader[2] = p[1];
(*it_tag)->_pTagHeader[3] = p[0];
f.write((char *)(*it_tag)->_pTagHeader, 11); // 写入调整后的标签头部
switch (_nNalUnitLength) {
case 4:
*((*it_tag)->_pTagData + 5) = p_nalu_len[3];
*((*it_tag)->_pTagData + 6) = p_nalu_len[2];
*((*it_tag)->_pTagData + 7) = p_nalu_len[1];
*((*it_tag)->_pTagData + 8) = p_nalu_len[0];
break;
case 3:
*((*it_tag)->_pTagData + 5) = p_nalu_len[2];
*((*it_tag)->_pTagData + 6) = p_nalu_len[1];
*((*it_tag)->_pTagData + 7) = p_nalu_len[0];
break;
case 2:
*((*it_tag)->_pTagData + 5) = p_nalu_len[1];
*((*it_tag)->_pTagData + 6) = p_nalu_len[0];
break;
default:
*((*it_tag)->_pTagData + 5) = p_nalu_len[0];
break;
}
f.write((char *)(*it_tag)->_pTagData, pStartCode - (*it_tag)->_pTagData);
f.write((char *)pStartCode + i, (*it_tag)->_header.nDataSize - (pStartCode - (*it_tag)->_pTagData));
} else {
f.write((char *)(*it_tag)->_pTagHeader, 11);
f.write((char *)(*it_tag)->_pTagData, (*it_tag)->_header.nDataSize);
}
} else {
f.write((char *)(*it_tag)->_pTagHeader, 11);
f.write((char *)(*it_tag)->_pTagData, (*it_tag)->_header.nDataSize);
}
nLastTagSize = 11 + (*it_tag)->_header.nDataSize; // 更新最后一个标签的大小
}
uint32_t nn = WriteU32(nLastTagSize);
f.write((char *)&nn, 4); // 写入最后一个标签大小
f.close();
return 1;
}
int CFlvParser::Stat()
{
for (int i = 0; i < _vpTag.size(); i++)
{
switch (_vpTag[i]->_header.nType)
{
case 0x08:
_sStat.nAudioNum++; // 音频标签计数
break;
case 0x09:
StatVideo(_vpTag[i]); // 统计视频信息
break;
case 0x12:
_sStat.nMetaNum++; // 元数据标签计数
break;
default:
;
}
}
return 1;
}
int CFlvParser::StatVideo(Tag *pTag)
{
_sStat.nVideoNum++; // 视频标签计数
_sStat.nMaxTimeStamp = pTag->_header.nTimeStamp; // 更新最大时间戳
if (pTag->_pTagData[0] == 0x17 && pTag->_pTagData[1] == 0x00)
{
_sStat.nLengthSize = (pTag->_pTagData[9] & 0x03) + 1; // 记录 NALU 长度字节数
}
return 1;
}
/**
* 创建 FLV 头部对象 FlvHeader, 这是一个结构体对象 , 其中封装了 版本 , 是否包含视频 / 音频 , 头部长度
* @brief CFlvParser::CreateFlvHeader
* @param pBuf
* @return
*/
CFlvParser::FlvHeader *CFlvParser::CreateFlvHeader(uint8_t *pBuf)
{
// 创建 FlvHeader 结构体对象
FlvHeader *pHeader = new FlvHeader;
// 为头部数据填充数据
pHeader->nVersion = pBuf[3]; // 版本号 Signature
pHeader->bHaveAudio = (pBuf[4] >> 2) & 0x01; // 是否有音频
pHeader->bHaveVideo = (pBuf[4] >> 0) & 0x01; // 是否有视频
pHeader->nHeadSize = ShowU32(pBuf + 5); // 头部长度
// 根据头部长度 创建 头部数据 内存空间
pHeader->pFlvHeader = new uint8_t[pHeader->nHeadSize];
// 拷贝头部数据到 FlvHeader 结构体中
memcpy(pHeader->pFlvHeader, pBuf, pHeader->nHeadSize);
return pHeader;
}
int CFlvParser::DestroyFlvHeader(FlvHeader *pHeader)
{
if (pHeader == NULL)
return 0;
delete pHeader->pFlvHeader;
return 1;
}
/**
* @brief 从主缓冲区中获取 Tag 的 header 和 body 数据
* @param pHeader 标签头部
* @param pBuf 数据缓冲区
* @param nLeftLen 剩余数据长度
*/
void CFlvParser::Tag::Init(TagHeader *pHeader, uint8_t *pBuf, int nLeftLen)
{
memcpy(&_header, pHeader, sizeof(TagHeader)); // 复制标签头部
_pTagHeader = new uint8_t[11];
memcpy(_pTagHeader, pBuf, 11); // 复制标签头部
_pTagData = new uint8_t[_header.nDataSize];
memcpy(_pTagData, pBuf + 11, _header.nDataSize); // 复制标签数据部分
}
// 创建 视频标签 对象
CFlvParser::CVideoTag::CVideoTag(TagHeader *pHeader, uint8_t *pBuf, int nLeftLen, CFlvParser *pParser)
{
// 从主缓冲区中获取 Tag 的 header 和 body 数据
Init(pHeader, pBuf, nLeftLen);
// 视频标签 的 Tag Data 数据内容
uint8_t *pd = _pTagData;
// 视频标签的 Tag Data 第一个字节 分为两部分
// 高 4 字节是 帧类型, 1 关键帧 , 2 普通帧
// 低 4 字节是 视频编码类型 , 1 JPEG , 2 H.263 , 7 AVC
_nFrameType = (pd[0] & 0xf0) >> 4; // 帧类型
_nCodecID = pd[0] & 0x0f; // 视频编码类型
// 这里只处理 H.264 视频 , 即视频编码类型为 AVC 格式的情况 , 其它情况不处理
if (_header.nType == 0x09 && _nCodecID == 7)
{
// 解析 H.264 格式的 视频标签数据
ParseH264Tag(pParser); // 解析 H.264 视频标签
}
}
/**
* @brief 解析音频标签数据
* 音频标签数据中包含音频格式、采样率、采样精度和声道信息。
* 对于 AAC 音频数据,需要进一步判断是配置信息还是原始数据。
* @param pHeader 标签头部
* @param pBuf 数据缓冲区
* @param nLeftLen 剩余数据长度
* @param pParser FLV 解析器实例
*/
CFlvParser::CAudioTag::CAudioTag(TagHeader *pHeader, uint8_t *pBuf, int nLeftLen, CFlvParser *pParser)
{
// 从主缓冲区中获取 Tag 的 header 和 body 数据
Init(pHeader, pBuf, nLeftLen);
uint8_t *pd = _pTagData;
// 解析第 1 个字节数据
// 这些数据 仅供参考 , 实际的音频格式数据在 AAC 配置 数据包信息中
_nSoundFormat = (pd[0] & 0xf0) >> 4; // 音频格式
_nSoundRate = (pd[0] & 0x0c) >> 2; // 采样率
_nSoundSize = (pd[0] & 0x02) >> 1; // 采样精度
_nSoundType = (pd[0] & 0x01); // 是否为立体声
if (_nSoundFormat == 10) // AAC 格式
{
ParseAACTag(pParser); // 解析 AAC 标签
}
}
// 解析 AAC 标签
int CFlvParser::CAudioTag::ParseAACTag(CFlvParser *pParser)
{
uint8_t *pd = _pTagData;
// 数据包类型
// 0 AAC 配置信息
// 1 AAC 原始数据
int nAACPacketType = pd[1]; // 数据包类型
if (nAACPacketType == 0) // 如果是AAC配置信息
{
ParseAudioSpecificConfig(pParser, pd); // 解析AAC配置信息
}
else if (nAACPacketType == 1) // 如果是AAC原始数据
{
ParseRawAAC(pParser, pd); // 解析AAC原始数据
}
return 1; // 返回成功
}
// 解析 AAC 配置信息
int CFlvParser::CAudioTag::ParseAudioSpecificConfig(CFlvParser *pParser, uint8_t *pTagData)
{
uint8_t *pd = _pTagData;
_aacProfile = ((pd[2] & 0xf8) >> 3); // 提取5位AAC编码级别
_sampleRateIndex = ((pd[2] & 0x07) << 1) | (pd[3] >> 7); // 提取4位采样率索引
_channelConfig = (pd[3] >> 3) & 0x0f; // 提取4位通道配置
// 打印提取的AAC配置信息
printf("----- AAC ------\n");
printf("profile:%d\n", _aacProfile);
printf("sample rate index:%d\n", _sampleRateIndex);
printf("channel config:%d\n", _channelConfig);
_pMedia = NULL; // 初始化媒体数据指针
_nMediaLen = 0; // 初始化媒体数据长度
return 1; // 返回成功
}
// 解析 AAC 原始音频数据
int CFlvParser::CAudioTag::ParseRawAAC(CFlvParser *pParser, uint8_t *pTagData)
{
uint64_t bits = 0; // 占用8字节,用于存储ADTS头
// 跳过音频标签数据部分的 前两个字节
// 第一个字节是 音频数据 参数信息
// 第二个字节开始时 音频数据流 , 音频数据流的第一个字节是 AAC 数据包类型 字段
// 从第三个字节开始才是真正的 原始音频采样数据
int dataSize = _header.nDataSize - 2;
// 制作ADTS头部元数据
WriteU64(bits, 12, 0xFFF); // 设置同步字
WriteU64(bits, 1, 0); // 设置ID
WriteU64(bits, 2, 0); // 设置Layer
WriteU64(bits, 1, 1); // 设置保护位
WriteU64(bits, 2, _aacProfile - 1); // 设置配置文件
WriteU64(bits, 4, _sampleRateIndex); // 设置采样率索引
WriteU64(bits, 1, 0); // 设置私有位
WriteU64(bits, 3, _channelConfig); // 设置声道配置
WriteU64(bits, 1, 0); // 设置原始位
WriteU64(bits, 1, 0); // 设置首页位
WriteU64(bits, 1, 0); // 设置其他位
WriteU64(bits, 1, 0); // 设置ADTS其余位
WriteU64(bits, 13, 7 + dataSize); // 设置帧长度
WriteU64(bits, 11, 0x7FF); // 设置缓冲区满度
WriteU64(bits, 2, 0); // 设置帧编号
// 计算媒体数据总长度(ADTS头+AAC数据)
_nMediaLen = 7 + dataSize;
_pMedia = new uint8_t[_nMediaLen];
uint8_t p64[8];
p64[0] = (uint8_t)(bits >> 56); // 提取最高8位(实际上为0)
p64[1] = (uint8_t)(bits >> 48); // 提取同步字高8位
p64[2] = (uint8_t)(bits >> 40); // 提取后续位
p64[3] = (uint8_t)(bits >> 32);
p64[4] = (uint8_t)(bits >> 24);
p64[5] = (uint8_t)(bits >> 16);
p64[6] = (uint8_t)(bits >> 8);
p64[7] = (uint8_t)(bits);
memcpy(_pMedia, p64 + 1, 7); // 将ADTS头拷贝到媒体数据缓冲区
memcpy(_pMedia + 7, pTagData + 2, dataSize); // 将AAC数据拷贝到媒体数据缓冲区
return 1; // 返回成功
}
CFlvParser::CMetaDataTag::CMetaDataTag(TagHeader *pHeader, uint8_t *pBuf, int nLeftLen, CFlvParser *pParser)
{
// 从主缓冲区中获取 Tag 的 header 和 body 数据
Init(pHeader, pBuf, nLeftLen); // 初始化元数据标签
uint8_t *pd = _pTagData;
// 读取 AMF 数据类型 , 标签数据的 第 0 字节开始的 1 字节数据 是 数据类型信息
m_amf1_type = ShowU8(pd + 0);
// // 读取 AMF 数据大小 , 第 1 字节开始的 2 字节数据是 数据大小
m_amf1_size = ShowU16(pd + 1);
if (m_amf1_type != 2) // 判断是否为AMF字符串类型
{
printf("no metadata\n"); // 如果不是,输出无元数据信息
return;
}
// 解析 Script 数据
if (strncmp((const char *)"onMetaData", (const char *)(pd + 3), 10) == 0)
parseMeta(pParser); // 如果是“onMetaData”,调用parseMeta解析元数据
}
double CFlvParser::CMetaDataTag::hexStr2double(const uint8_t *hex, const uint32_t length)
{
double ret = 0;
char hexstr[length * 2];
memset(hexstr, 0, sizeof(hexstr));
for (uint32_t i = 0; i < length; i++)
{
sprintf(hexstr + i * 2, "%02x", hex[i]); // 将十六进制转换为字符串
}
sscanf(hexstr, "%llx", (unsigned long long *)&ret); // 将字符串转换为双精度浮点数
return ret; // 返回结果
}
// 解析 元数据 标签
int CFlvParser::CMetaDataTag::parseMeta(CFlvParser *pParser)
{
uint8_t *pd = _pTagData;
int dataSize = _header.nDataSize; // 获取元数据大小
uint32_t arrayLen = 0;
uint32_t offset = 13; // Type + Value_Size + Value占用13字节
uint32_t nameLen = 0; // 字段名称长度
double doubleValue = 0; // 存储数值类型的字段值
string strValue = ""; // 存储字符串类型的字段值
bool boolValue = false; // 存储布尔类型的字段值
uint32_t valueLen = 0; // 字符串类型值的长度
uint8_t u8Value = 0; // 临时存储布尔值的字节
if (pd[offset++] == 0x08) // 检测是否是onMetaData类型(0x08表示ECMA数组)
{
arrayLen = ShowU32(pd + offset); // 读取ECMA数组的长度
offset += 4; // 跳过[ECMAArrayLength]占用的字节
printf("ArrayLen = %d\n", arrayLen); // 输出数组长度
}
else
{
printf("metadata format error!!!"); // 如果类型错误,输出错误信息
return -1;
}
for (uint32_t i = 0; i < arrayLen; i++) // 遍历ECMA数组
{
doubleValue = 0;
boolValue = false;
strValue = "";
// 读取字段名称长度
nameLen = ShowU16(pd + offset);
offset += 2; // 跳过2字节字段长度
char name[nameLen + 1]; // 创建缓冲区以存储字段名称
memset(name, 0, sizeof(name));
memcpy(name, &pd[offset], nameLen); // 复制字段名称
name[nameLen + 1] = '\0'; // 添加字符串结束符
offset += nameLen; // 跳过字段名称占用的字节
uint8_t amfType = pd[offset++]; // 读取AMF值的类型
switch (amfType) // 根据AMF类型解析字段值
{
case 0x0: // 数值类型(double),占用8字节
doubleValue = hexStr2double(&pd[offset], 8); // 解析为double值
offset += 8; // 跳过8字节
break;
case 0x1: // 布尔类型,占用1字节
u8Value = ShowU8(pd + offset); // 读取布尔值
offset += 1; // 跳过1字节
boolValue = (u8Value != 0x00); // 转换为布尔类型
break;
case 0x2: // 字符串类型
valueLen = ShowU16(pd + offset); // 获取字符串长度
offset += 2; // 跳过2字节的长度信息
strValue.append(pd + offset, pd + offset + valueLen); // 读取字符串值
offset += valueLen; // 跳过字段值的长度
break;
default: // 未处理的AMF类型
printf("un handle amfType:%d\n", amfType); // 输出未处理类型的信息
break;
}
// 根据字段名称设置对应的成员变量值
if (strncmp(name, "duration", 8) == 0)
{
m_duration = doubleValue;
}
else if (strncmp(name, "width", 5) == 0)
{
m_width = doubleValue;
}
else if (strncmp(name, "height", 6) == 0)
{
m_height = doubleValue;
}
else if (strncmp(name, "videodatarate", 13) == 0)
{
m_videodatarate = doubleValue;
}
else if (strncmp(name, "framerate", 9) == 0)
{
m_framerate = doubleValue;
}
else if (strncmp(name, "videocodecid", 12) == 0)
{
m_videocodecid = doubleValue;
}
else if (strncmp(name, "audiodatarate", 13) == 0)
{
m_audiodatarate = doubleValue;
}
else if (strncmp(name, "audiosamplerate", 15) == 0)
{
m_audiosamplerate = doubleValue;
}
else if (strncmp(name, "audiosamplesize", 15) == 0)
{
m_audiosamplesize = doubleValue;
}
else if (strncmp(name, "stereo", 6) == 0)
{
m_stereo = boolValue;
}
else if (strncmp(name, "audiocodecid", 12) == 0)
{
m_audiocodecid = doubleValue;
}
else if (strncmp(name, "major_brand", 11) == 0)
{
m_major_brand = strValue;
}
else if (strncmp(name, "minor_version", 13) == 0)
{
m_minor_version = strValue;
}
else if (strncmp(name, "compatible_brands", 17) == 0)
{
m_compatible_brands = strValue;
}
else if (strncmp(name, "encoder", 7) == 0)
{
m_encoder = strValue;
}
else if (strncmp(name, "filesize", 8) == 0)
{
m_filesize = doubleValue;
}
}
printMeta(); // 输出元数据信息
return 1; // 返回成功
}
void CFlvParser::CMetaDataTag::printMeta()
{
// 打印提取的元数据信息
printf("\nduration: %0.2lfs, filesize: %.0lfbytes\n", m_duration, m_filesize);
printf("width: %0.0lf, height: %0.0lf\n", m_width, m_height);
printf("videodatarate: %0.2lfkbps, framerate: %0.0lffps\n", m_videodatarate, m_framerate);
printf("videocodecid: %0.0lf\n", m_videocodecid);
printf("audiodatarate: %0.2lfkbps, audiosamplerate: %0.0lfKhz\n",
m_audiodatarate, m_audiosamplerate);
printf("audiosamplesize: %0.0lfbit, stereo: %d\n", m_audiosamplesize, m_stereo);
printf("audiocodecid: %0.0lf\n", m_audiocodecid);
printf("major_brand: %s, minor_version: %s\n", m_major_brand.c_str(), m_minor_version.c_str());
printf("compatible_brands: %s, encoder: %s\n\n", m_compatible_brands.c_str(), m_encoder.c_str());
}
// 创建 FLV 标签对象
CFlvParser::Tag *CFlvParser::CreateTag(uint8_t *pBuf, int nLeftLen)
{
// 开始解析 标签头部
TagHeader header;
// 读取 pBuf 数组中的第 0 个字节开始 的 1 字节数据
header.nType = ShowU8(pBuf + 0); // 类型,标识当前标签是视频、音频还是脚本数据
// 读取 pBuf 数组中的第 1 个字节开始 的 3 字节数据
header.nDataSize = ShowU24(pBuf + 1); // 标签body的长度
// 读取 pBuf 数组中的第 4 个字节开始 的 3 字节数据
header.nTimeStamp = ShowU24(pBuf + 4); // 时间戳(低24位)
header.nTSEx = ShowU8(pBuf + 7); // 时间戳的扩展字段(高8位)
header.nStreamID = ShowU24(pBuf + 8); // 流的ID,通常为0
header.nTotalTS = (uint32_t)((header.nTSEx << 24)) + header.nTimeStamp; // 组合完整的时间戳
// 标签头部解析结束
// 检查剩余数据是否足够包含标签头部和数据
if ((header.nDataSize + 11) > nLeftLen)
{
return NULL; // 如果数据不足,则返回NULL
}
Tag *pTag; // 创建对应类型的Tag对象
switch (header.nType) {
case 0x09: // 视频类型的Tag
pTag = new CVideoTag(&header, pBuf, nLeftLen, this);
break;
case 0x08: // 音频类型的Tag
pTag = new CAudioTag(&header, pBuf, nLeftLen, this);
break;
case 0x12: // 脚本类型的Tag(如元数据)
pTag = new CMetaDataTag(&header, pBuf, nLeftLen, this);
break;
default: // 其他类型的Tag
pTag = new Tag();
pTag->Init(&header, pBuf, nLeftLen); // 初始化默认的Tag
}
return pTag; // 返回创建的Tag对象
}
int CFlvParser::DestroyTag(Tag *pTag)
{
// 释放Tag中分配的内存
if (pTag->_pMedia != NULL)
delete []pTag->_pMedia; // 释放媒体数据
if (pTag->_pTagData != NULL)
delete []pTag->_pTagData; // 释放Tag数据
if (pTag->_pTagHeader != NULL)
delete []pTag->_pTagHeader; // 释放Tag头部
return 1; // 返回成功
}
// 解析 H.264 格式的 视频标签数据
int CFlvParser::CVideoTag::ParseH264Tag(CFlvParser *pParser)
{
uint8_t *pd = _pTagData;
// *pd 指针指向的数据 是 视频标签 的 数据
// 第一个字节 前四位 是 帧类型 , 后四位 是 视频编码类型
// 第二个字节 开始就是 实际的视频数据 , 下面的 pd[1] 开始就是视频数据
/*
** 数据包的类型
** 视频数据被压缩之后被打包成数据包在网上传输
** 包括两种类型的数据包:
** 1. 视频信息包(如SPS、PPS等)
** 2. 视频数据包(如视频的压缩数据)
*/
int nAVCPacketType = pd[1]; // 获取AVC数据包类型
int nCompositionTime = CFlvParser::ShowU24(pd + 2); // 获取视频时间偏移量
// H.264 的 AVC 视频编码 分别为两部分 , 分别是 :
// AVC 序列头 视频配置信息
// AVC NALU 视频数据
// 如果是视频配置信息
if (nAVCPacketType == 0) // AVC sequence header
{
// 解析 AVC 序列头 视频配置信息
ParseH264Configuration(pParser, pd); // 解析视频配置信息(SPS、PPS等)
}
// 如果是视频数据
else if (nAVCPacketType == 1) // AVC NALU
{
// 解析 AVC NALU 视频数据
ParseNalu(pParser, pd); // 解析视频的压缩数据
}
else
{
// 其他类型不处理
}
return 1; // 返回成功
}
/**
* @brief
AVCDecoderConfigurationRecord {
uint32_t(8) configurationVersion = 1; [0]
uint32_t(8) AVCProfileIndication; [1]
uint32_t(8) profile_compatibility; [2]
uint32_t(8) AVCLevelIndication; [3]
bit(6) reserved = ‘111111’b; [4]
uint32_t(2) lengthSizeMinusOne; [4] 计算方法是 1 + (lengthSizeMinusOne & 3),实际计算结果一直是4
bit(3) reserved = ‘111’b; [5]
uint32_t(5) numOfSequenceParameterSets; [5] SPS 的个数,计算方法是 numOfSequenceParameterSets & 0x1F,实际计算结果一直为1
for (i=0; i< numOfSequenceParameterSets; i++) {
uint32_t(16) sequenceParameterSetLength ; [6,7]
bit(8*sequenceParameterSetLength) sequenceParameterSetNALUnit;
}
uint32_t(8) numOfPictureParameterSets; PPS 的个数,一直为1
for (i=0; i< numOfPictureParameterSets; i++) {
uint32_t(16) pictureParameterSetLength;
bit(8*pictureParameterSetLength) pictureParameterSetNALUnit;
}
}
_nNalUnitLength 这个变量告诉我们用几个字节来存储NALU的长度,如果NALULengthSizeMinusOne是0,
那么每个NALU使用一个字节的前缀来指定长度,那么每个NALU包的最大长度是255字节,
这个明显太小了,使用2个字节的前缀来指定长度,那么每个NALU包的最大长度是64K字节,
也不一定够,一般分辨率达到1280*720 的图像编码出的I帧,可能大于64K;3字节是比较完美的,
但是因为一些原因(例如对齐)没有被广泛支持;因此4字节长度的前缀是目前使用最多的方式
* @param pParser
* @param pTagData
* @return
*/
int CFlvParser::CVideoTag::ParseH264Configuration(CFlvParser *pParser, uint8_t *pTagData)
{
uint8_t *pd = pTagData;
// 跨过 Tag Data 的 VIDEODATA(1字节) AVCVIDEOPACKET(AVCPacketType(1字节) 和CompositionTime(3字节) 4字节)
// 总共跨过 5 个字节 才能看到 视频配置信息
// Tag 数据从第 5 个字节开始才是 视频配置信息
// NalUnit 长度表示占用的字节
// NALU 单元的长度 用多少个字节表示 , 该数据是 视频配置信息的第四个字节
// pd[9] 的 9 是 跳过的 5 个字节 加上 视频配置信息的第四个字节的 后 2 位 , 与 0x03 也就是二进制的 0b11 , 只取后 2 位
// 参考 bit(6) reserved = ‘111111’b; [4]
pParser->_nNalUnitLength = (pd[9] & 0x03) + 1; // lengthSizeMinusOne 9 = 5 + 4
int sps_size, pps_size;
// sps(序列参数集)的长度
// 读取 pd[11] 位置的 2 字节数据 , 这是 Tag 数据跳过的 5 字节 , 加上 6 , 视频配置的 第 6 和 7 字节是 sps 值
// 参考 uint32_t(16) sequenceParameterSetLength ; [6,7] 是 sps 数据
sps_size = CFlvParser::ShowU16(pd + 11); // sequenceParameterSetLength 11 = 5 + 6
// pps(图像参数集)的长度
pps_size = CFlvParser::ShowU16(pd + 11 + (2 + sps_size) + 1); // pictureParameterSetLength
// 元数据的长度
_nMediaLen = 4 + sps_size + 4 + pps_size; // 添加start code
// _pMedia 数据 是在 pTagData 基础上改造后的数据
// pTagData 是 原始的 FLV 数据
// _pMedia 数据 是 视频 的元数据 参数 , 如果要导出为独立的可播放的 h264 视频文件 或 aac 音频文件
// 必须在 pTagData 数据基础上 同时导出 配套的 _pMedia 元数据
// 导出后的 音视频文件 即可进行播放
_pMedia = new uint8_t[_nMediaLen];
// 保存元数据
memcpy(_pMedia, &nH264StartCode, 4);
memcpy(_pMedia + 4, pd + 11 + 2, sps_size);
memcpy(_pMedia + 4 + sps_size, &nH264StartCode, 4);
memcpy(_pMedia + 4 + sps_size + 4, pd + 11 + 2 + sps_size + 2 + 1, pps_size);
return 1;
}
// 解析 AVC NALU 视频数据
// NALU 是 视频在 网络上传输的 最小单元
int CFlvParser::CVideoTag::ParseNalu(CFlvParser *pParser, uint8_t *pTagData)
{
uint8_t *pd = pTagData;
int nOffset = 0;
_pMedia = new uint8_t[_header.nDataSize+10];
_nMediaLen = 0;
// 跨过 Tag Data的VIDEODATA(1字节) AVCVIDEOPACKET(AVCPacketType和CompositionTime 4字节)
nOffset = 5; // 总共跨过5个字节 132 - 5 = 127 = _nNalUnitLength(4字节) + NALU(123字节)
// startcode(4字节) + NALU(123字节) = 127
// NALU 数据也跳过了 5 个字节 , 才是真正的 NALU 数据 ;
// 每个 视频 Tag 标签可能包含多个 NALU 数据
// 下面的每次循环 , 都处理了一个 NALU 数据
// 每个 NALU 的 前 4 字节是 NALU 的长度 , 从第 5 个字节开始就是 NALU 的数据
while (1)
{
// 如果解析完了一个Tag,那么就跳出循环
if (nOffset >= _header.nDataSize)
break;
// 计算 NALU(视频数据被包装成NALU在网上传输)的长度,
// 一个tag可能包含多个nalu, 所以每个nalu前面有NalUnitLength字节表示每个nalu的长度
// NALU 的 前 4 字节是 NALU 的 长度
int nNaluLen;
switch (pParser->_nNalUnitLength)
{
case 4:
nNaluLen = CFlvParser::ShowU32(pd + nOffset);
break;
case 3:
nNaluLen = CFlvParser::ShowU24(pd + nOffset);
break;
case 2:
nNaluLen = CFlvParser::ShowU16(pd + nOffset);
break;
default:
nNaluLen = CFlvParser::ShowU8(pd + nOffset);
}
// 获取 NALU 的 起始码 , 添加到 _pMedia 中
memcpy(_pMedia + _nMediaLen, &nH264StartCode, 4);
// 复制 NALU 的数据 到 _pMedia 中, 后面的 4 是上面的 起始码
memcpy(_pMedia + _nMediaLen + 4, pd + nOffset + pParser->_nNalUnitLength, nNaluLen);
// 解析 NALU , 需要查看解析结果 可以打开该注释
// pParser->_vjj->Process(_pMedia+_nMediaLen, 4+nNaluLen, _header.nTotalTS);
// 最终的 _pMedia 长度 , 添加了 可播放的元数据 之后的 NALU 数据长
// _pMedia 可以直接导出为可播放的视频文件
_nMediaLen += (4 + nNaluLen);
// 主缓冲区 中 解析数据的偏移
nOffset += (pParser->_nNalUnitLength + nNaluLen);
}
return 1;
}
4、执行结果
在 build-FlvParser-Desktop_Qt_5_14_2_MinGW_32_bit-Debug 目录中 , 拷贝入要解析的 FLV 文件数据 , input.flv ;

执行程序 , 命令行打印如下结果 :
ArrayLen = 16
duration: 20.72s, filesize: 1534951bytes
width: 496, height: 500
videodatarate: 195.31kbps, framerate: 30fps
videocodecid: 2
audiodatarate: 0.00kbps, audiosamplerate: 48000Khz
audiosamplesize: 16bit, stereo: 1
audiocodecid: 2
major_brand: isom, minor_version: 512
compatible_brands: mp41, encoder: Lavf58.76.100
vnum: 604 , anum: 863 , mnum: 1
maxTimeStamp: 20690 ,nLengthSize: 0
Vjj SEI num: 0

最终导出的 视频文件 dump.flv , H.264 视频数据 parser.264 , H.264 音频数据 parser.aac ;

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)