MPEG-4文件的核心组织形式是Atom树结构,每个Atom作为独立的数据单元,包含自身的元数据信息和可能的子Atom,整个文件由多层嵌套的Atom构成。解析该结构需要先明确Atom的基础格式,再通过二进制读取和递归遍历完成树构建。
Atom基础结构定义
标准MPEG-4 Atom由三部分组成,前8字节为头部信息:前4字节是Atom的总大小(包含头部和内容),后4字节是Atom的类型标识(4字符编码)。如果头部的大小值为1,说明后续8字节是扩展大小字段,此时头部总长度变为16字节。容器型Atom的内容部分全部是子Atom,非容器型Atom的内容是具体的数据。
首先定义Atom的基础结构体,存储解析后的信息:
#include <fstream>
#include <string>
#include <vector>
#include <cstdint>
#include <stdexcept>
// Atom结构体,存储解析后的单个Atom信息
struct Mp4Atom {
uint64_t size; // Atom总大小
std::string type; // Atom类型标识,4字符
uint64_t data_offset; // Atom内容起始偏移量(相对于文件开头)
std::vector<Mp4Atom> children; // 子Atom列表,容器型Atom有效
};
字节序转换与基础读取函数
MPEG-4文件采用大端字节序存储多字节整数,而x86架构CPU通常采用小端字节序,因此需要先实现大端到小端的转换函数,同时封装基础的文件读取函数。
// 大端字节序转小端字节序,处理32位整数
uint32_t big_endian_to_host_32(uint32_t val) {
return ((val & 0x000000FF) << 24) |
((val & 0x0000FF00) << 8) |
((val & 0x00FF0000) >> 8) |
((val & 0xFF000000) >> 24);
}
// 大端字节序转小端字节序,处理64位整数
uint64_t big_endian_to_host_64(uint64_t val) {
uint32_t high = big_endian_to_host_32(val & 0xFFFFFFFF);
uint32_t low = big_endian_to_host_32((val >> 32) & 0xFFFFFFFF);
return ((uint64_t)high << 32) | low;
}
// 从文件读取指定字节数的数据
bool read_bytes(std::ifstream& file, char* buffer, size_t len) {
file.read(buffer, len);
return file.gcount() == len;
}
// 读取32位大端整数并转换为本地字节序
uint32_t read_u32(std::ifstream& file) {
uint32_t val;
if (!read_bytes(file, reinterpret_cast<char*>(&val), sizeof(val))) {
throw std::runtime_error("读取32位整数失败");
}
return big_endian_to_host_32(val);
}
// 读取64位大端整数并转换为本地字节序
uint64_t read_u64(std::ifstream& file) {
uint64_t val;
if (!read_bytes(file, reinterpret_cast<char*>(&val), sizeof(val))) {
throw std::runtime_error("读取64位整数失败");
}
return big_endian_to_host_64(val);
}
单个Atom解析实现
解析单个Atom需要先读取头部的大小和类型,判断是否为扩展大小,再记录内容偏移量,最后返回解析后的Atom结构体。
// 解析单个Atom,返回解析后的Atom,更新文件偏移量
Mp4Atom parse_single_atom(std::ifstream& file) {
Mp4Atom atom;
uint32_t header_size = 8; // 基础头部大小:4字节大小 + 4字节类型
uint32_t size_32 = read_u32(file);
// 读取4字节类型标识
char type_buf[4];
if (!read_bytes(file, type_buf, 4)) {
throw std::runtime_error("读取Atom类型失败");
}
atom.type.assign(type_buf, 4);
// 处理大小字段
if (size_32 == 1) {
// 扩展大小,基础头部后接8字节扩展大小
header_size = 16;
atom.size = read_u64(file);
} else {
atom.size = size_32;
}
// 记录内容起始偏移量:当前文件位置即为内容起始
atom.data_offset = file.tellg();
// 跳过当前Atom的内容部分,移动到下一个Atom起始位置
if (atom.size > header_size) {
file.seekg(atom.data_offset + (atom.size - header_size));
}
return atom;
}
Atom树递归解析逻辑
容器型Atom(如moov、trak、mdia等)的内容全部是子Atom,需要递归解析子Atom,直到遍历完所有嵌套结构。首先需要定义容器型Atom的类型列表,用于判断是否需要递归解析。
// 判断是否为容器型Atom,需要递归解析子Atom
bool is_container_atom(const std::string& type) {
static const std::vector<std::string> container_types = {
"moov", "trak", "mdia", "minf", "stbl", "mvex", "moof", "traf", "mfra", "skip", "free", "wide"
};
for (const auto& t : container_types) {
if (t == type) {
return true;
}
}
return false;
}
// 递归解析Atom树,传入文件流和当前剩余可解析的字节数(如果为0则解析到文件末尾)
std::vector<Mp4Atom> parse_atom_tree(std::ifstream& file, uint64_t remaining_bytes = 0) {
std::vector<Mp4Atom> atoms;
std::streampos start_pos = file.tellg();
while (true) {
// 检查是否到达解析边界
if (remaining_bytes > 0) {
std::streampos current_pos = file.tellg();
if (current_pos - start_pos >= remaining_bytes) {
break;
}
}
// 检查是否到文件末尾
if (file.eof()) {
break;
}
// 解析单个Atom
Mp4Atom atom = parse_single_atom(file);
// 如果是容器型Atom,递归解析子Atom
if (is_container_atom(atom.type)) {
// 计算子Atom的总大小:Atom总大小 - 头部大小
uint64_t header_size = (atom.size == 1) ? 16 : 8;
uint64_t children_size = atom.size - header_size;
// 保存当前文件位置,递归解析子Atom
std::streampos children_start = file.tellg();
atom.children = parse_atom_tree(file, children_size);
// 递归完成后不需要手动移动文件指针,parse_single_atom已经跳过了整个Atom
}
atoms.push_back(atom);
}
return atoms;
}
完整解析示例与结果输出
编写主函数调用解析逻辑,打开MPEG-4文件并输出解析后的Atom树结构,验证解析逻辑的正确性。
// 递归打印Atom树,缩进表示层级
void print_atom_tree(const std::vector<Mp4Atom>& atoms, int indent = 0) {
for (const auto& atom : atoms) {
for (int i = 0; i < indent; i++) {
std::cout << " ";
}
std::cout << "Atom类型: " << atom.type
<< ", 大小: " << atom.size
<< ", 内容偏移: " << atom.data_offset
<< std::endl;
if (!atom.children.empty()) {
print_atom_tree(atom.children, indent + 1);
}
}
}
int main() {
std::string file_path = "test.mp4"; // 待解析的MPEG-4文件路径
std::ifstream file(file_path, std::ios::binary);
if (!file.is_open()) {
std::cerr << "无法打开文件: " << file_path << std::endl;
return 1;
}
try {
std::vector<Mp4Atom> root_atoms = parse_atom_tree(file);
std::cout << "MPEG-4文件Atom树结构:" << std::endl;
print_atom_tree(root_atoms);
} catch (const std::exception& e) {
std::cerr << "解析失败: " << e.what() << std::endl;
}
file.close();
return 0;
}
解析注意事项
实际解析过程中需要注意几个常见问题:一是文件读取时要以二进制模式打开,避免换行符转换导致读取错误;二是部分Atom的大小可能等于头部大小,此时该Atom没有内容部分;三是如果遇到未知类型的Atom,按照非容器型Atom处理即可,直接跳过内容部分;四是文件损坏时可能出现大小字段异常,需要添加边界检查,避免文件指针越界。
以上逻辑完整实现了C++解析MPEG-4 Atom树结构的核心流程,开发者可以在此基础上扩展对不同类型Atom内容的解析,比如解析mvhd Atom获取视频时长,解析stsd Atom获取编码信息等。