基础概念
缩写 | 编码标准 | 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 为 avc1
或 hvc1
。
c++
#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;
}