admin 管理员组

文章数量: 1184232

C++实现H264文件以及一段H264码流解析,源码如下:

h264Parse.h:

#ifndef _H264PARSE_H_
#define _H264PARSE_H_
#include <fstream>
class H264Parse
{
public:
    int open_file(const std::string &filename);
    /**
     * @brief 从文件中读取一个NALU,包含起始码
     * @param buf 存放nalu的缓冲区
     * @param size 缓冲区大小
     * @param len nalu的长度
     * @param n 每次读取多少个字节
     * @return -1 失败  0 已到文件末尾  1 成功获取到一个nalu
     */
    int read_nalu(uint8_t *buf, uint32_t size, uint32_t &len, uint32_t n);
    /**
     * @brief 获取sps和pps,包含起始码
     * @param spsbuf 存放sps的缓冲区
     * @param spsbuf_size 缓冲区大小
     * @param spslen sps的长度
     * @param ppsbuf 存放pps的缓冲区
     * @param ppsbuf_size 缓冲区大小
     * @param ppslen pps的长度
     * @return -1 失败  0 成功
     */
    int get_sps_pps(uint8_t *spsbuf, uint32_t spsbuf_size, uint32_t &spslen,
                    uint8_t *ppsbuf, uint32_t ppsbuf_size, uint32_t &ppslen);
    // 获取起始码长度
    static int get_startCode_len(const uint8_t *ptr);
    void close_file();
    /**
     * @brief 从一段h264码流中分割NALU,包含起始码
     * @param stream h264码流
     * @param streamLen 码流大小
     * @param nalu 分割出的NALU
     * @param naluLen NALU的长度
     * @param record 用于记录状态,第一次分割时把 *record 赋值为NULL
     * @return -1 失败  0 已分割完  1 成功获取到一个NALU
     */
    static int nalu_tok(const uint8_t *stream, uint32_t streamLen, const uint8_t **nalu,
                        uint32_t &naluLen, const uint8_t **record);
private:
    std::fstream h264File;
    static const uint8_t *find_startCode_pos(const uint8_t *buf, uint32_t len);
    int read_start_code(uint8_t *buf);
    int adjust_filePointer_pos(uint32_t totalRead, uint32_t naluLen);
};
#endif // _H264PARSE_H_

h264Parse.cpp:

#include "h264Parse.h"
#include <iostream>
#include <cstring>
int H264Parse::open_file(const std::string &filename)
{
    h264File.open(filename, std::ios::in | std::ios::binary);
    if (!h264File.is_open())
    {
        std::cout << "Failed to open the H.264 file. filename: " << filename << std::endl;
        return -1;
    }
    return 0;
}
int H264Parse::get_startCode_len(const uint8_t *ptr)
{
    if (ptr[0] == 0x00 && ptr[1] == 0x00)
    {
        if (ptr[2] == 0x01)
            return 3;
        else if (ptr[2] == 0x00 && ptr[3] == 0x01)
            return 4;
    }
    return -1; // 无效的起始码
}
// 读取起始码,并返回其长度
int H264Parse::read_start_code(uint8_t *buf)
{
    // 读取前4个字节来判断起始码长度
    h264File.read(reinterpret_cast<char *>(buf), 4);
    if (h264File.gcount() < 4)
    {
        return -1;
    }
    return get_startCode_len(buf);
}
// 寻找NALU的起始码位置
const uint8_t *H264Parse::find_startCode_pos(const uint8_t *buf, uint32_t len)
{
    const uint8_t *p = buf;
    if (len < 3)
        return NULL;
    for (uint32_t i = 0; i < len - 3; ++i)
    {
        if ((p[0] == 0x00 && p[1] == 0x00 && p[2] == 0x01) ||
            (p[0] == 0x00 && p[1] == 0x00 && p[2] == 0x00 && p[3] == 0x01))
        {
            return p;
        }
        p++;
    }
    // 检查最后3字节是不是起始码
    if (p[0] == 0x00 && p[1] == 0x00 && p[2] == 0x01)
        return p;
    return NULL;
}
// 调整文件指针位置
int H264Parse::adjust_filePointer_pos(uint32_t totalRead, uint32_t naluLen)
{
    int offset = -(totalRead - naluLen);
    if (!h264File.eof())
    {
        h264File.seekg(offset, std::ios::cur);
    }
    else
    {
        h264File.clear(); // 达到文件末尾了要先清除 eof 标志
        h264File.seekg(offset, std::ios::end);
    }
    if (h264File.fail())
    {
        std::cout << "seekg failed!" << std::endl;
        return -1;
    }
    return 0;
}
int H264Parse::read_nalu(uint8_t *buf, uint32_t size, uint32_t &len, uint32_t n)
{
    if (size <= 4)
    {
        std::cout << "nalu buffer size is too small." << std::endl;
        return -1;
    }
    int startCodeLength = read_start_code(buf);
    if (startCodeLength == -1)
    {
        std::cout << "read_start_code failed." << std::endl;
        return -1;
    }
    uint32_t totalRead = 4; // 已经读取了4字节的长度
    while (true)
    {
        if (size < totalRead + n)
        {
            std::cout << "Buffer size is too small: size=" << size
                      << ", needed=" << totalRead + n << std::endl;
            return -1;
        }
        h264File.read(reinterpret_cast<char *>(buf + totalRead), n);
        std::streamsize bytesRead = h264File.gcount();
        if (bytesRead <= 0)
        {
            std::cout << "Failed to read from file!" << std::endl;
            return -1;
        }
        uint32_t searchStart = (totalRead > 4) ? totalRead - 3 : startCodeLength;
        const uint8_t *naluEnd = find_startCode_pos(buf + searchStart,
                                                    bytesRead + (totalRead > 4 ? 3 : 0));
        totalRead += bytesRead;
        if (naluEnd != nullptr)
        {
            len = naluEnd - buf;
            if (adjust_filePointer_pos(totalRead, len) < 0)
                return -1;
            break;
        }
        // 是否读取到文件末尾
        if (h264File.peek() == std::char_traits<char>::eof())
        {
            len = totalRead;
            return 0; // NALU完整读取
        }
    }
    memset(buf + len, 0, size - len); // 清空剩余部分
    return 1;                         // 成功读取
}
int H264Parse::get_sps_pps(uint8_t *spsbuf, uint32_t spsbuf_size, uint32_t &spslen,
                           uint8_t *ppsbuf, uint32_t ppsbuf_size, uint32_t &ppslen)
{
    int ret = -1;
    // 检查是否到文件末尾
    if (h264File.eof())
        h264File.clear(); // 达到文件末尾了要先清除 eof 标志
    // 保存当前文件指针位置
    std::streampos original_pos = h264File.tellg();
    if (h264File.fail())
    {
        std::cout << "Failed to get the current file position." << std::endl;
        return -1;
    }
    // 将文件指针移动到文件开头
    h264File.seekg(0, std::ios::beg);
    if (h264File.fail())
    {
        std::cout << "seekg to beginning failed!" << std::endl;
        goto restore_position;
    }
    int result;
    uint8_t buffer[1028];
    uint32_t len;
    spslen = 0;
    ppslen = 0;
    while ((result = read_nalu(buffer, sizeof(buffer), len, 1024)) != -1)
    {
        uint32_t startCode_len = (buffer[2] == 0x01) ? 3 : 4;
        uint8_t nalu_type = buffer[startCode_len] & 0x1F;
        if (nalu_type == 7) // SPS
        {
            if (len > spsbuf_size)
            {
                std::cout << "SPS buffer size is too small." << std::endl;
                break;
            }
            spslen = len;
            std::memcpy(spsbuf, buffer, spslen);
        }
        else if (nalu_type == 8) // PPS
        {
            if (len > ppsbuf_size)
            {
                std::cout << "PPS buffer size is too small." << std::endl;
                break;
            }
            ppslen = len;
            std::memcpy(ppsbuf, buffer, ppslen);
        }
        // 如果已经找到了 SPS 和 PPS,就退出循环
        if (spslen > 0 && ppslen > 0)
        {
            ret = 0;
            break;
        }
        // 检查是否已经读取到文件末尾
        if (result == 0)
        {
            h264File.clear();
            break;
        }
    }
restore_position:
    // 恢复文件指针位置
    h264File.seekg(original_pos);
    if (h264File.fail())
    {
        std::cout << "Failed to restore file position!" << std::endl;
        return -1;
    }
    return ret;
}
void H264Parse::close_file()
{
    h264File.close();
}
int H264Parse::nalu_tok(const uint8_t *stream, uint32_t streamLen, const uint8_t **nalu,
                        uint32_t &naluLen, const uint8_t **record)
{
    const uint8_t *current = (record && *record) ? *record : stream;
    uint32_t offset = static_cast<uint32_t>(current - stream);
    if (offset >= streamLen)
    {
        return -1; // 当前记录位置超出缓冲区
    }
    int scLen = get_startCode_len(current);
    if (scLen == -1 || (current + scLen) > (stream + streamLen))
    {
        return -1; // 无效的起始码或起始码长度超出缓冲区
    }
    // 查找下一个起始码的位置
    const uint8_t *next_start = find_startCode_pos(current + scLen, streamLen - offset - scLen);
    if (next_start)
    {
        *nalu = current;
        naluLen = static_cast<uint32_t>(next_start - current);
        *record = next_start;
        return 1; // 成功获取到一个 NALU
    }
    else
    {
        // 最后一个 NALU
        *nalu = current;
        naluLen = streamLen - offset;
        *record = NULL; // 重置记录指针
        return 0;       // 分割完毕
    }
}

测试:

#include <iostream>
#include <vector>
#include "h264Parse.h"
void test1()
{
    int ret;
    int number = 0;
    H264Parse h264;
    uint8_t buf[1024 * 1024];
    uint32_t len = 0;
    uint8_t sps[1024], pps[1024];
    uint32_t spslen, ppslen;
    h264.open_file("/home/tl/work/app/res/output.h264");
    while ((ret = h264.read_nalu(buf, sizeof(buf), len, 1024 * 2)) != -1)
    {
        printf("number: %d nalu len: %u\n", number, len - h264.get_startCode_len(buf));
        number++;
        if (ret == 0)
            break;
    }
    if (ret == -1)
    {
        std::cout << "read_nalu failed." << std::endl;
    }
    h264.get_sps_pps(sps, sizeof(sps), spslen, pps, sizeof(pps), ppslen);
    std::cout << "spslen: " << spslen << " ppslen: " << ppslen << std::endl;
    h264.close_file();
}
// 辅助函数:打印 NALU 信息
void print_nalu(const uint8_t *nalu, uint32_t len, int index)
{
    std::cout << "NALU " << index << ": Length = " << len << " bytes, Data = ";
    for (uint32_t i = 0; i < len; ++i)
    {
        printf("%02X ", nalu[i]);
    }
    std::cout << std::endl;
}
void test2()
{
    // 构造一个模拟的 H.264 码流缓冲区,包含多个 NALU
    // 起始码格式:0x000001 (3 字节) 和 0x00000001 (4 字节)
    // NALU 内容:随机填充的字节数据
    std::vector<uint8_t> buffer;
    // NALU 1: 3 字节起始码 + 5 字节数据
    std::vector<uint8_t> nalu1 = {0x00, 0x00, 0x01, 0x65, 0x88, 0x84, 0x21, 0xA0};
    buffer.insert(buffer.end(), nalu1.begin(), nalu1.end());
    // NALU 2: 4 字节起始码 + 6 字节数据
    std::vector<uint8_t> nalu2 = {0x00, 0x00, 0x00, 0x01, 0x41, 0x9A, 0x5C, 0xD4, 0x00, 0x11};
    buffer.insert(buffer.end(), nalu2.begin(), nalu2.end());
    // NALU 3: 3 字节起始码 + 4 字节数据
    std::vector<uint8_t> nalu3 = {0x00, 0x00, 0x01, 0x06, 0x05, 0xFF, 0xEE};
    buffer.insert(buffer.end(), nalu3.begin(), nalu3.end());
    // NALU 4: 3 字节起始码 + 3 字节数据 (测试末尾)
    std::vector<uint8_t> nalu4 = {0x00, 0x00, 0x01, 0x07, 0xAD, 0xBE};
    buffer.insert(buffer.end(), nalu4.begin(), nalu4.end());
    // 输出构建的缓冲区
    std::cout << "Constructed H.264 Buffer: ";
    for (size_t i = 0; i < buffer.size(); ++i)
    {
        printf("%02X ", buffer[i]);
    }
    std::cout << "\n\n";
    const uint8_t *pnalu = nullptr;
    uint32_t nale_len = 0;
    const uint8_t *pRecord = NULL; // 初始时为 NULL
    int ret;
    int nalu_index = 1;
    // 循环分割并打印每个 NALU
    while ((ret = H264Parse::nalu_tok(buffer.data(), buffer.size(), &pnalu, nale_len, &pRecord)) != -1)
    {
        print_nalu(pnalu, nale_len, nalu_index);
        nalu_index++;
        if (ret == 0)
            break;
    }
    if (ret == -1)
    {
        std::cout << "Error occurred during NALU tokenization." << std::endl;
    }
}
// 主函数
int main()
{
    test1();
    // test2();
    return 0;
}

相关文章链接:

本文标签: 字节起始 的长度 的缓冲区