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 ;

在这里插入图片描述

Logo

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

更多推荐