音视频学习(六十四):avc1 hvc1和hev1

发布于:2025-09-12 ⋅ 阅读:(14) ⋅ 点赞:(0)

基础概念

缩写 编码标准 FourCC 说明
AVC/H.264 Advanced Video Coding avc1 最常用的 H.264 编码标识符,兼容 MP4/MOV/FMP4 等容器。
HEVC/H.265 High Efficiency Video Coding hvc1 HEVC 视频流在 MP4/FMP4 中常用标识符,要求存储 NALU 的 VPS/SPS/PPS(类似 H.264 SPS/PPS)信息在 track header 内。
HEVC/H.265 High Efficiency Video Coding hev1 另一个 HEVC 标识符,与 hvc1 区别在于封装方式:hev1 视频帧中只保留原始 NALU,SPS/PPS/VPS 信息不一定放在 track header 内,而是随帧存储。

小结:FourCC 是用于 MP4/FMP4/QuickTime 等容器标识视频编码类型的 4 字节字符码。

avc1(H.264)

  • 用途:用于 MP4/FMP4/MOV 文件中标识 H.264 视频流。
  • NALU 类型
    • SPS (Sequence Parameter Set)
    • PPS (Picture Parameter Set)
    • IDR/I帧、P帧、B帧
  • 封装特点
    • avcC box 中存储 SPS/PPS 信息(解码初始化参数)。
    • 视频帧按长度前缀或 Annex-B 封装。
  • 兼容性
    • 几乎所有现代播放器和浏览器均支持。
    • 广泛用于 Web 视频(MP4/HLS/DASH)。

hvc1 与 hev1 (H.265)

特性 hvc1 hev1
SPS/PPS/VPS 存储在 track header(init segment)中 可随帧存储,也可在 init segment
主要用途 WebM/MP4/FMP4 等 MP4 封装 MP4/FMP4、部分硬件加速播放器
播放兼容性 现代硬件/软件 HEVC 解码器 软件解码器可能需要解析每帧 NALU
注释 hvc1 适合片头集中存储参数集 hev1 更接近原始编码流(流媒体或片段式播放)

注意

  • 对于 hvc1,播放器在播放前可直接读取 init segment 中的 VPS/SPS/PPS 初始化解码器。
  • 对于 hev1,播放器可能需要在每个片段或每帧中解析 NALU 以获取参数集,适合动态流式场景。

示例

C++ + FFmpeg 将 H.264/H.265 裸流封装成 FMP4(Fragmented MP4),并设置 FourCC 为 avc1hvc1

#include <iostream>
#include <fstream>
#include <vector>
#include <cstdint>
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/opt.h>
#include <libavutil/mathematics.h>
#include <libavutil/mem.h>
#include <libavutil/error.h>
}

// 从裸流中提取 SPS/PPS/VPS 并生成 extradata
std::vector<uint8_t> extract_extradata(const std::vector<uint8_t>& stream_data, bool is_h265) {
    std::vector<uint8_t> extradata;
    size_t pos = 0;
    while (pos + 4 < stream_data.size()) {
        // 查找起始码 0x00000001 或 0x000001
        size_t start = stream_data.find("\x00\x00\x00\x01", pos);
        if (start == std::string::npos) start = stream_data.find("\x00\x00\x01", pos);
        if (start == std::string::npos) break;

        size_t next_start = stream_data.find("\x00\x00\x00\x01", start + 4);
        if (next_start == std::string::npos) next_start = stream_data.size();

        uint8_t nal_unit_type = stream_data[start + 4] & 0x1F; // H.264
        if (is_h265) {
            nal_unit_type = (stream_data[start + 4] >> 1) & 0x3F; // H.265
        }

        // H.264: SPS=7, PPS=8; H.265: VPS=32, SPS=33, PPS=34
        if ((!is_h265 && (nal_unit_type == 7 || nal_unit_type == 8)) ||
            (is_h265 && (nal_unit_type == 32 || nal_unit_type == 33 || nal_unit_type == 34))) {
            extradata.insert(extradata.end(), stream_data.begin() + start, stream_data.begin() + next_start);
        }
        pos = next_start;
    }
    return extradata;
}

// 将裸流封装成 fMP4
int mux_fmp4(const char* input_file, const char* output_file, bool is_h265) {
    av_register_all();
    avformat_network_init();

    int ret;
    AVFormatContext* ofmt_ctx = nullptr;
    AVStream* out_stream = nullptr;

    // 创建输出上下文
    ret = avformat_alloc_output_context2(&ofmt_ctx, nullptr, "mp4", output_file);
    if (ret < 0 || !ofmt_ctx) {
        std::cerr << "Could not create output context\n";
        return -1;
    }

    // 打开输入裸流文件
    std::ifstream infile(input_file, std::ios::binary | std::ios::ate);
    if (!infile.is_open()) {
        std::cerr << "Failed to open input file\n";
        return -1;
    }

    auto file_size = infile.tellg();
    infile.seekg(0);
    std::vector<uint8_t> stream_data(file_size);
    infile.read(reinterpret_cast<char*>(stream_data.data()), file_size);

    // 提取 extradata
    std::vector<uint8_t> extradata = extract_extradata(stream_data, is_h265);

    // 创建视频流
    out_stream = avformat_new_stream(ofmt_ctx, nullptr);
    if (!out_stream) {
        std::cerr << "Failed to create stream\n";
        return -1;
    }

    AVCodecParameters* codecpar = out_stream->codecpar;
    codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
    codecpar->codec_id = is_h265 ? AV_CODEC_ID_HEVC : AV_CODEC_ID_H264;
    codecpar->width = 1920;
    codecpar->height = 1080;
    out_stream->time_base = AVRational{1, 25};

    if (!extradata.empty()) {
        codecpar->extradata_size = extradata.size();
        codecpar->extradata = (uint8_t*)av_malloc(extradata.size() + AV_INPUT_BUFFER_PADDING_SIZE);
        memcpy(codecpar->extradata, extradata.data(), extradata.size());
    }

    // 设置 FMP4 flags
    av_opt_set(ofmt_ctx->priv_data, "movflags", "frag_keyframe+empty_moov+default_base_moof", 0);

    // 打开输出文件
    if (!(ofmt_ctx->oformat->flags & AVFMT_NOFILE)) {
        ret = avio_open(&ofmt_ctx->pb, output_file, AVIO_FLAG_WRITE);
        if (ret < 0) {
            std::cerr << "Could not open output file\n";
            return -1;
        }
    }

    // 写文件头
    ret = avformat_write_header(ofmt_ctx, nullptr);
    if (ret < 0) {
        std::cerr << "Error writing header\n";
        return -1;
    }

    // 分帧写入
    size_t pos = 0;
    int64_t pts = 0;
    while (pos + 4 < stream_data.size()) {
        // 查找起始码
        size_t start = stream_data.find("\x00\x00\x00\x01", pos);
        if (start == std::string::npos) start = stream_data.find("\x00\x00\x01", pos);
        if (start == std::string::npos) break;

        size_t next_start = stream_data.find("\x00\x00\x00\x01", start + 4);
        if (next_start == std::string::npos) next_start = stream_data.size();

        AVPacket pkt;
        av_init_packet(&pkt);
        pkt.data = stream_data.data() + start;
        pkt.size = next_start - start;
        pkt.stream_index = out_stream->index;
        pkt.pts = pts;
        pkt.dts = pts;
        pkt.duration = 1;
        pkt.flags |= AV_PKT_FLAG_KEY; // 假设每帧都是关键帧,可根据 nal_unit_type 判断

        ret = av_interleaved_write_frame(ofmt_ctx, &pkt);
        if (ret < 0) {
            std::cerr << "Error muxing packet\n";
            break;
        }

        pos = next_start;
        pts++;
    }

    av_write_trailer(ofmt_ctx);

    if (!(ofmt_ctx->oformat->flags & AVFMT_NOFILE)) {
        avio_close(ofmt_ctx->pb);
    }
    avformat_free_context(ofmt_ctx);

    std::cout << "FMP4 muxing done.\n";
    return 0;
}

int main(int argc, char* argv[]) {
    if (argc < 4) {
        std::cerr << "Usage: " << argv[0] << " input.264|265 output.mp4 is_h265(0|1)\n";
        return -1;
    }

    const char* input = argv[1];
    const char* output = argv[2];
    bool is_h265 = std::atoi(argv[3]) != 0;

    mux_fmp4(input, output, is_h265);

    return 0;
}

网站公告

今日签到

点亮在社区的每一天
去签到