保姆级教程:用C语言和mp4v2库手动封装H.264裸流为MP4(附完整代码)
深入解析H.264裸流封装MP4的底层实现与技术细节在音视频开发领域H.264裸流封装为MP4是一个基础但至关重要的技术环节。许多开发者习惯使用FFmpeg等现成工具完成这一转换但理解底层实现原理对于解决复杂问题、优化性能至关重要。本文将带你从零开始用C语言和mp4v2库手动实现H.264到MP4的完整封装过程。1. H.264裸流结构与NALU解析基础H.264裸流由一系列网络抽象层单元(NALU)组成每个NALU包含视频编码数据或控制信息。理解NALU结构是封装过程的第一步。1.1 NALU的组成与类型每个NALU以起始码0x00000001开头后跟NALU头和数据负载。NALU头中的类型字段决定了单元的作用// NALU类型掩码 #define NALU_TYPE_MASK 0x1F // 常见NALU类型 enum { NALU_TYPE_SPS 7, // 序列参数集 NALU_TYPE_PPS 8, // 图像参数集 NALU_TYPE_IDR 5, // 关键帧 NALU_TYPE_SLICE 1 // 非关键帧 };关键NALU类型说明SPS(Sequence Parameter Set)包含全局编码参数如分辨率、帧率等PPS(Picture Parameter Set)包含帧级编码参数IDR帧可独立解码的关键帧普通Slice非关键帧数据1.2 NALU读取的实现细节读取NALU时需要注意几个关键点起始码检测必须准确避免误判文件指针操作要谨慎特别是回退操作内存管理要合理避免缓冲区溢出int read_nalu(FILE* fp, uint8_t* buffer, size_t buf_size) { static const uint8_t start_code[4] {0x00, 0x00, 0x00, 0x01}; size_t pos 0; // 读取并验证起始码 if(fread(buffer, 1, 4, fp) ! 4) return -1; if(memcmp(buffer, start_code, 4) ! 0) return -1; // 读取NALU数据直到下一个起始码或文件结束 while(pos buf_size - 1) { if(fread(buffer[pos], 1, 1, fp) ! 1) break; pos; // 检查是否遇到下一个起始码 if(pos 4 buffer[pos-4] 0x00 buffer[pos-3] 0x00 buffer[pos-2] 0x00 buffer[pos-1] 0x01) { fseek(fp, -4, SEEK_CUR); pos - 4; break; } } return pos; }2. MP4容器格式与mp4v2库基础MP4是基于ISO基础媒体文件格式(ISO BMFF)的容器格式理解其结构有助于正确使用mp4v2库。2.1 MP4文件基本结构MP4文件由多个box(或称atom)组成每个box包含特定类型的数据Box类型作用是否必需ftyp文件类型声明是moov元数据容器是mdat媒体数据是free空闲空间否2.2 mp4v2库核心APImp4v2库提供了一系列API用于创建和操作MP4文件// 创建MP4文件 MP4FileHandle MP4Create(const char* fileName, uint32_t flags); // 添加H.264视频轨道 MP4TrackId MP4AddH264VideoTrack( MP4FileHandle hFile, uint32_t timeScale, MP4Duration sampleDuration, uint16_t width, uint16_t height, uint8_t AVCProfileIndication, uint8_t profile_compat, uint8_t AVCLevelIndication, uint8_t sampleLenFieldSizeMinusOne); // 写入样本数据 bool MP4WriteSample( MP4FileHandle hFile, MP4TrackId trackId, const uint8_t* pBytes, uint32_t numBytes, MP4Duration duration, MP4Duration renderingOffset, bool isSyncSample);3. 完整封装流程实现3.1 初始化MP4文件与轨道创建创建MP4文件并设置基本参数是封装过程的第一步MP4FileHandle mp4File MP4Create(output.mp4, 0); if(mp4File MP4_INVALID_FILE_HANDLE) { fprintf(stderr, Failed to create MP4 file\n); return -1; } // 设置时间基准(通常使用90000) MP4SetTimeScale(mp4File, 90000);3.2 处理SPS和PPSSPS和PPS包含了H.264流的解码配置信息必须正确提取并写入MP4文件uint8_t sps[256], pps[256]; size_t sps_size 0, pps_size 0; MP4TrackId videoTrack MP4_INVALID_TRACK_ID; while((nalu_size read_nalu(h264_file, nalu_buffer, sizeof(nalu_buffer))) 0) { uint8_t nalu_type nalu_buffer[4] NALU_TYPE_MASK; if(nalu_type NALU_TYPE_SPS) { memcpy(sps, nalu_buffer[4], nalu_size - 4); sps_size nalu_size - 4; // 从SPS中提取视频宽度和高度 parse_sps(sps, sps_size, width, height); } else if(nalu_type NALU_TYPE_PPS) { memcpy(pps, nalu_buffer[4], nalu_size - 4); pps_size nalu_size - 4; // 创建视频轨道 videoTrack MP4AddH264VideoTrack( mp4File, 90000, 90000/25, // 假设帧率为25fps width, height, sps[1], // AVCProfileIndication sps[2], // profile_compat sps[3], // AVCLevelIndication 3); // NALU长度字段大小-1 if(videoTrack MP4_INVALID_TRACK_ID) { fprintf(stderr, Failed to add video track\n); break; } // 添加SPS和PPS MP4AddH264SequenceParameterSet(mp4File, videoTrack, sps, sps_size); MP4AddH264PictureParameterSet(mp4File, videoTrack, pps, pps_size); } }3.3 写入视频帧数据对于非SPS/PPS的NALU我们需要将其作为视频帧写入MP4文件// 准备写入样本数据 uint32_t sample_size nalu_size; uint8_t* sample_data malloc(sample_size 4); // 添加NALU长度前缀(4字节大端序) sample_data[0] (nalu_size 24) 0xFF; sample_data[1] (nalu_size 16) 0xFF; sample_data[2] (nalu_size 8) 0xFF; sample_data[3] nalu_size 0xFF; // 复制NALU数据 memcpy(sample_data[4], nalu_buffer, nalu_size); // 写入样本 MP4WriteSample( mp4File, videoTrack, sample_data, sample_size 4, MP4_INVALID_DURATION, // 使用默认持续时间 0, // 渲染偏移 (nalu_type NALU_TYPE_IDR)); // 是否为关键帧 free(sample_data);4. 高级话题与性能优化4.1 时间戳处理与同步正确处理时间戳对于视频播放的流畅性至关重要// 计算时间戳增量(基于帧率) MP4Duration sample_duration 90000 / frame_rate; uint64_t current_timestamp 0; // 写入样本时指定时间戳 MP4WriteSample( mp4File, videoTrack, sample_data, sample_size, sample_duration, // 样本持续时间 current_timestamp, // 解码时间戳 is_sync_sample); current_timestamp sample_duration;4.2 内存管理与性能优化处理大视频文件时内存管理和I/O性能变得尤为重要缓冲区管理使用固定大小的环形缓冲区避免频繁的内存分配/释放I/O优化使用更大的读取块(如64KB)考虑内存映射文件#define BUFFER_SIZE (1024 * 1024) // 1MB缓冲区 uint8_t* file_buffer malloc(BUFFER_SIZE); size_t bytes_read fread(file_buffer, 1, BUFFER_SIZE, h264_file); // 处理缓冲区中的数据 process_buffer(file_buffer, bytes_read);4.3 错误处理与健壮性完善的错误处理能提高程序的稳定性// 检查文件是否有效 if(access(input_file, R_OK) ! 0) { perror(Input file access error); return -1; } // 检查内存分配 uint8_t* buffer malloc(LARGE_SIZE); if(!buffer) { fprintf(stderr, Memory allocation failed\n); return -1; } // 检查API调用结果 if(!MP4WriteSample(...)) { fprintf(stderr, Failed to write sample\n); break; }5. 完整实现代码示例以下是整合了上述所有概念的完整实现#include stdio.h #include stdlib.h #include string.h #include unistd.h #include mp4v2/mp4v2.h #define NALU_TYPE_SPS 7 #define NALU_TYPE_PPS 8 #define NALU_TYPE_IDR 5 #define NALU_TYPE_SLICE 1 typedef struct { uint16_t width; uint16_t height; uint8_t profile; uint8_t level; } VideoInfo; int parse_sps(const uint8_t* sps, size_t size, VideoInfo* info) { // 简化的SPS解析实现 // 实际实现应按照H.264规范完整解析 if(size 4) return -1; info-profile sps[1]; info-level sps[3]; // 假设分辨率信息在固定位置(实际应按照指数哥伦布编码解析) info-width (sps[4] 8) | sps[5]; info-height (sps[6] 8) | sps[7]; return 0; } int read_nalu(FILE* fp, uint8_t* buffer, size_t buf_size) { // 实现同前文... } int h264_to_mp4(const char* input, const char* output, int frame_rate) { FILE* h264_file fopen(input, rb); if(!h264_file) { perror(Failed to open input file); return -1; } MP4FileHandle mp4_file MP4Create(output, 0); if(mp4_file MP4_INVALID_FILE_HANDLE) { fprintf(stderr, Failed to create MP4 file\n); fclose(h264_file); return -1; } MP4SetTimeScale(mp4_file, 90000); uint8_t nalu_buffer[1024 * 1024]; VideoInfo video_info {0}; MP4TrackId video_track MP4_INVALID_TRACK_ID; uint8_t sps[256], pps[256]; size_t sps_size 0, pps_size 0; uint64_t current_ts 0; MP4Duration sample_duration 90000 / frame_rate; while(1) { int nalu_size read_nalu(h264_file, nalu_buffer, sizeof(nalu_buffer)); if(nalu_size 0) break; uint8_t nalu_type nalu_buffer[4] 0x1F; uint32_t nalu_data_size nalu_size - 4; uint8_t* nalu_data nalu_buffer[4]; switch(nalu_type) { case NALU_TYPE_SPS: memcpy(sps, nalu_data, nalu_data_size); sps_size nalu_data_size; parse_sps(sps, sps_size, video_info); break; case NALU_TYPE_PPS: memcpy(pps, nalu_data, nalu_data_size); pps_size nalu_data_size; if(video_track MP4_INVALID_TRACK_ID sps_size 0) { video_track MP4AddH264VideoTrack( mp4_file, 90000, sample_duration, video_info.width, video_info.height, video_info.profile, 0, // profile compat video_info.level, 3); if(video_track MP4_INVALID_TRACK_ID) { fprintf(stderr, Failed to add video track\n); goto cleanup; } MP4AddH264SequenceParameterSet(mp4_file, video_track, sps, sps_size); MP4AddH264PictureParameterSet(mp4_file, video_track, pps, pps_size); } break; default: { uint32_t sample_size nalu_size; uint8_t* sample_data malloc(sample_size 4); // 添加NALU长度前缀 sample_data[0] (nalu_size 24) 0xFF; sample_data[1] (nalu_size 16) 0xFF; sample_data[2] (nalu_size 8) 0xFF; sample_data[3] nalu_size 0xFF; memcpy(sample_data[4], nalu_buffer, nalu_size); // 写入样本 bool is_sync (nalu_type NALU_TYPE_IDR); if(!MP4WriteSample( mp4_file, video_track, sample_data, sample_size 4, sample_duration, current_ts, is_sync)) { fprintf(stderr, Failed to write sample\n); free(sample_data); goto cleanup; } current_ts sample_duration; free(sample_data); break; } } } cleanup: fclose(h264_file); MP4Close(mp4_file, 0); return 0; } int main(int argc, char** argv) { if(argc 3) { printf(Usage: %s input.h264 output.mp4 [frame_rate]\n, argv[0]); return 1; } int frame_rate (argc 3) ? atoi(argv[3]) : 25; return h264_to_mp4(argv[1], argv[2], frame_rate); }6. 常见问题与调试技巧在实际开发中你可能会遇到以下典型问题文件无法播放检查SPS/PPS是否正确写入验证NALU长度前缀是否正确确保时间戳连续递增视频花屏或卡顿确认关键帧标记是否正确检查帧率设置是否合理验证时间戳计算是否正确内存泄漏使用valgrind等工具检测确保所有分配的内存都被释放特别注意文件句柄和MP4句柄的关闭调试时可以添加详细的日志输出printf([DEBUG] NALU type: %d, size: %d\n, nalu_type, nalu_size); printf([DEBUG] Video info: %dx%d, profile: %d, level: %d\n, video_info.width, video_info.height, video_info.profile, video_info.level);对于更复杂的调试可以考虑将中间数据写入文件void dump_to_file(const char* filename, const uint8_t* data, size_t size) { FILE* f fopen(filename, wb); if(f) { fwrite(data, 1, size, f); fclose(f); } }