MP4文件是基于ISO Base Media File Format标准的容器格式,所有的数据都存储在称为Box的单元中,视频的Metadata信息也分散存储在不同的Box结构里,要提取这些信息首先需要理解Box的基本结构。

MP4文件Box结构基础
每个Box由头部和数据部分组成,头部包含Box的大小和类型两个核心字段。Box的大小字段占4字节,表示整个Box的字节数,类型字段占4字节,用四个ASCII字符标识Box的类型,比如ftyp表示文件类型Box,moov表示媒体信息容器Box。
常见的存储视频Metadata的Box包括:
- mvhd:存储整个媒体的基础信息,比如时长、创建时间
- trak:存储单个媒体轨道的信息,视频轨道通常包含分辨率相关参数
- tkhd:存储单个轨道的具体属性,比如视频的宽度和高度
- stsd:存储媒体采样描述信息,包含编码格式等参数
解析前的准备工作
首先需要定义基础的数据结构来映射Box的头部信息,同时需要注意MP4文件采用大端字节序存储数据,在读取多字节数值时需要进行字节序转换。
#include <iostream>
#include <fstream>
#include <cstdint>
#include <cstring>
#include <string>
#include <arpa/inet.h> // 用于大端转小端函数
// Box头部结构
struct BoxHeader {
uint32_t size; // Box总大小
char type[5]; // Box类型,多留1位存字符串结束符
};
// 转换大端字节序到主机字节序
uint32_t big_endian_to_host(uint32_t val) {
return ntohl(val);
}
uint16_t big_endian_to_host(uint16_t val) {
return ntohs(val);
}
// 读取Box头部的函数
bool read_box_header(std::ifstream& file, BoxHeader& header) {
// 读取size字段
if (!file.read(reinterpret_cast<char*>(&header.size), 4)) {
return false;
}
header.size = big_endian_to_host(header.size);
// 读取type字段
if (!file.read(header.type, 4)) {
return false;
}
header.type[4] = ' ';
return true;
}
核心Metadata提取实现
提取视频时长
视频总时长存储在moov容器下的mvhd Box中,mvhd的结构中,前4字节是版本信息,版本0和版本1的时长字段偏移位置不同,需要分别处理。
// 解析mvhd获取时长,返回时长(秒),失败返回-1
double parse_mvhd(std::ifstream& file, uint32_t box_size) {
// 跳过版本和标志字段,共8字节
file.seekg(8, std::ios::cur);
uint8_t version;
file.read(reinterpret_cast<char*>(&version), 1);
file.seekg(-9, std::ios::cur); // 回到mvhd数据起始位置
uint32_t timescale;
uint32_t duration;
if (version == 0) {
// 版本0:跳过创建时间、修改时间,各4字节,共8字节
file.seekg(8, std::ios::cur);
// 读取时间刻度
if (!file.read(reinterpret_cast<char*>(×cale), 4)) return -1;
timescale = big_endian_to_host(timescale);
// 读取时长
if (!file.read(reinterpret_cast<char*>(&duration), 4)) return -1;
duration = big_endian_to_host(duration);
} else {
// 版本1:跳过创建时间、修改时间,各8字节,共16字节
file.seekg(16, std::ios::cur);
// 读取时间刻度
if (!file.read(reinterpret_cast<char*>(×cale), 4)) return -1;
timescale = big_endian_to_host(timescale);
// 跳过8字节的时长(版本1时长是64位)
uint64_t duration_64;
if (!file.read(reinterpret_cast<char*>(&duration_64), 8)) return -1;
duration = static_cast<uint32_t>(big_endian_to_host(duration_64) & 0xFFFFFFFF);
}
if (timescale == 0) return -1;
return static_cast<double>(duration) / timescale;
}
提取视频分辨率
视频的宽度和高度存储在trak下的tkhd Box中,同样需要根据tkhd的版本判断字段偏移位置。
// 解析tkhd获取分辨率,返回是否成功
bool parse_tkhd(std::ifstream& file, uint32_t box_size, uint32_t& width, uint32_t& height) {
// 跳过版本和标志,共8字节
file.seekg(8, std::ios::cur);
uint8_t version;
file.read(reinterpret_cast<char*>(&version), 1);
file.seekg(-9, std::ios::cur); // 回到tkhd起始位置
if (version == 0) {
// 版本0:跳过创建时间、修改时间、track ID、保留字段,共16字节
file.seekg(16, std::ios::cur);
// 跳过时长字段4字节
file.seekg(4, std::ios::cur);
// 跳过保留字段8字节
file.seekg(8, std::ios::cur);
// 跳过矩阵结构36字节
file.seekg(36, std::ios::cur);
// 读取宽度和高度,各4字节,是16.16定点数,取整数部分
uint32_t width_raw, height_raw;
if (!file.read(reinterpret_cast<char*>(&width_raw), 4)) return false;
if (!file.read(reinterpret_cast<char*>(&height_raw), 4)) return false;
width = big_endian_to_host(width_raw) >> 16;
height = big_endian_to_host(height_raw) >> 16;
} else {
// 版本1:跳过创建时间、修改时间、track ID、保留字段,共24字节
file.seekg(24, std::ios::cur);
// 跳过时长字段8字节
file.seekg(8, std::ios::cur);
// 跳过保留字段8字节
file.seekg(8, std::ios::cur);
// 跳过矩阵结构36字节
file.seekg(36, std::ios::cur);
// 读取宽度和高度
uint32_t width_raw, height_raw;
if (!file.read(reinterpret_cast<char*>(&width_raw), 4)) return false;
if (!file.read(reinterpret_cast<char*>(&height_raw), 4)) return false;
width = big_endian_to_host(width_raw) >> 16;
height = big_endian_to_host(height_raw) >> 16;
}
return true;
}
提取编码格式
编码格式存储在stsd Box下的子Box中,子Box的类型就是编码格式标识,比如avc1代表H.264编码,mp4a代表AAC音频编码。
// 解析stsd获取视频编码格式
std::string parse_stsd(std::ifstream& file, uint32_t box_size) {
// 跳过版本、标志、条目数量,共8字节
file.seekg(8, std::ios::cur);
// 读取第一个子Box的类型,就是编码格式
char codec[5];
// 跳过子Box的size字段4字节
file.seekg(4, std::ios::cur);
if (!file.read(codec, 4)) return "";
codec[4] = ' ';
return std::string(codec);
}
完整解析流程示例
将上面的功能组合起来,实现完整的MP4 Metadata解析流程:
int main() {
std::string file_path = "test.mp4";
std::ifstream file(file_path, std::ios::binary);
if (!file.is_open()) {
std::cerr << "无法打开文件" << std::endl;
return -1;
}
double video_duration = -1;
uint32_t video_width = 0, video_height = 0;
std::string video_codec = "";
// 遍历所有Box
while (file.peek() != EOF) {
BoxHeader header;
if (!read_box_header(file, header)) break;
// 处理moov容器
if (strcmp(header.type, "moov") == 0) {
uint32_t moov_size = header.size - 8;
uint32_t read_moov = 0;
while (read_moov < moov_size) {
BoxHeader sub_header;
if (!read_box_header(file, sub_header)) break;
// 处理mvhd获取时长
if (strcmp(sub_header.type, "mvhd") == 0) {
video_duration = parse_mvhd(file, sub_header.size - 8);
}
// 处理trak获取分辨率
else if (strcmp(sub_header.type, "trak") == 0) {
uint32_t trak_size = sub_header.size - 8;
uint32_t read_trak = 0;
while (read_trak < trak_size) {
BoxHeader trak_sub_header;
if (!read_box_header(file, trak_sub_header)) break;
if (strcmp(trak_sub_header.type, "tkhd") == 0) {
parse_tkhd(file, trak_sub_header.size - 8, video_width, video_height);
} else if (strcmp(trak_sub_header.type, "mdia") == 0) {
// 进入mdia查找stsd获取编码格式
uint32_t mdia_size = trak_sub_header.size - 8;
uint32_t read_mdia = 0;
while (read_mdia < mdia_size) {
BoxHeader mdia_sub_header;
if (!read_box_header(file, mdia_sub_header)) break;
if (strcmp(mdia_sub_header.type, "minf") == 0) {
uint32_t minf_size = mdia_sub_header.size - 8;
uint32_t read_minf = 0;
while (read_minf < minf_size) {
BoxHeader minf_sub_header;
if (!read_box_header(file, minf_sub_header)) break;
if (strcmp(minf_sub_header.type, "stbl") == 0) {
uint32_t stbl_size = minf_sub_header.size - 8;
uint32_t read_stbl = 0;
while (read_stbl < stbl_size) {
BoxHeader stbl_sub_header;
if (!read_box_header(file, stbl_sub_header)) break;
if (strcmp(stbl_sub_header.type, "stsd") == 0) {
video_codec = parse_stsd(file, stbl_sub_header.size - 8);
}
// 跳过当前stbl子Box
file.seekg(stbl_sub_header.size - 8 - (file.tellg() - (std::istream::pos_type)&stbl_sub_header), std::ios::cur);
read_stbl += stbl_sub_header.size;
}
} else {
file.seekg(minf_sub_header.size - 8, std::ios::cur);
}
read_minf += minf_sub_header.size;
}
} else {
file.seekg(mdia_sub_header.size - 8, std::ios::cur);
}
read_mdia += mdia_sub_header.size;
}
} else {
file.seekg(trak_sub_header.size - 8, std::ios::cur);
}
read_trak += trak_sub_header.size;
}
} else {
file.seekg(sub_header.size - 8, std::ios::cur);
}
read_moov += sub_header.size;
}
} else {
// 跳过非moov的Box
file.seekg(header.size - 8, std::ios::cur);
}
}
// 输出解析结果
std::cout << "视频时长: " << video_duration << " 秒" << std::endl;
std::cout << "视频分辨率: " << video_width << "x" << video_height << std::endl;
std::cout << "视频编码格式: " << video_codec << std::endl;
file.close();
return 0;
}
注意事项
实际开发中需要注意几个问题,首先是MP4文件可能存在Box嵌套多层的情况,上面的示例只处理了常见的层级结构,复杂文件可能需要递归遍历所有Box。其次部分MP4文件可能采用碎片化的结构,Metadata可能存储在moof等Box中,需要额外适配。另外读取文件时要注意边界判断,避免读取超出文件范围导致程序崩溃。
C++MP4_metadata视频信息提取文件解析修改时间:2026-07-12 10:57:46