手动将MJPEG图片,转成MP4文件格式
MP4 容器格式
对于MP4,其box类型种类很多,此处只针对使用ffmpeg将MJPEG指定mjpeg编解码器生产的MP4文件内所有的box:
File Type Box(ftyp)
ftyp box 放在文件的最开始,描述的文件的版本、兼容协议等。
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 长度 | 0X0000001C |
box type | 4 字节 | box 类型,固定值:ftyp | ftyp |
major_brand | 4 字节 | 主版本 | isom |
minor_version | 4 字节 | 次版本 | 0x00000200 |
compatible_brands[] | n 字节 | 指定兼容的版本,可以包含多个 | isomiso2mp41 |
- major_brand:比如常见的 isom、mp41、mp42、avc1、qt等。它表示“最好”基于哪种格式来解析当前的文件。举例,major_brand 是 A,compatible_brands 是 A1,当解码器同时支持 A、A1 规范时,最好使用A规范来解码当前媒体文件,如果不支持A规范,但支持A1规范,那么,可以使用A1规范来解码;
- minor_version:提供 major_brand 的说明信息,比如版本号,不得用来判断媒体文件是否符合某个标准/规范;
- compatible_brands:文件兼容的brand列表。比如 mp41 的兼容 brand 为 isom。通过兼容列表里的 brand 规范,可以将文件 部分(或全部)解码出来;
使用十六进制查看工具打开MP4文件,查看对应内容:
标准文档定义:
aligned(8) class FileTypeBox extends Box(‘ftyp’) {
unsigned int(32) major_brand;
unsigned int(32) minor_version;
unsigned int(32) compatible_brands[]; // to end of the box
}
代码实现:
void write_ftyp(FILE* file) {
uint32_t size = 0X1C;
write_uint32(file, size); // box length
fwrite("ftyp", 1, 4, file); // box type
fwrite("isom", 1, 4, file); // major_brand
fwrite("\x00\x00\x02\x00", 1, 4, file); // minor_version
fwrite("isomiso2mp41", 1, 12, file); // compatible_brands
}
Free Space Box(free)
free box 可选,用于存储一些不必要的信息,比如版权声明等,不填写该 box 也不影响封装成视频文件。
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 长度 | 0X00000008 |
box type | 4 字节 | box 类型,固定值:free | free |
使用十六进制查看工具打开MP4文件,查看对应内容:
代码实现:
void write_free(FILE* file) {
uint32_t size = 0X08;
write_uint32(file, size); // box length
fwrite("free", 1, 4, file); // box type
}
Movie Data Box(mdat)
mdat box 存放媒体数据,mdat box 可选,但一个媒体文件中必须存在一个 mdat box。对于目前项目的实际运用之中,一般只有一个,存储 MJPEG 数据。该 box 可以位于 moov box 前,也可以位于 moov box 后。但必须和 stbl 中的信息保持一致。
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 长度 | 不定长 |
box type | 4 字节 | box 类型,固定值:mdat | mdat |
box data | n 字节 | 图像数据 | MJPEG文件数据内容 |
mdat box
存放每一帧的图像数据,一般是先填写 box length 为 0,再填写完后再回来填写长度。对于其他不定长的 box,基本上都采取这种方法。
对于MJPEG的图像数据,只用把MJPEG的文件头(FF D8
)和文件尾(FF D9
)所包含的数据依次全部写入进去即可。
使用十六进制查看工具打开MP4文件,查看对应内容:
标准文档定义:
aligned(8) class MeidaDataBox extends Box(‘mdat’) {
bit(8) data[]; // to end of the box
}
代码实现:
// 将 JPEG 文件写入 mdat 并记录偏移和大小
void write_mdat(FILE* mp4_file, const char** jpeg_files, size_t jpeg_count, uint32_t* frame_offsets, uint32_t* frame_sizes, uint32_t position) {
// 写入 mdat box 的头部
uint32_t mdat_size = 8; // 初始化为 header 的大小
for (size_t i = 0; i < jpeg_count; i++) {
FILE* jpeg_file = fopen(jpeg_files[i], "rb");
if (!jpeg_file) {
perror("Failed to open JPEG file");
exit(EXIT_FAILURE);
}
uint8_t data;
while (1)
{
fread(&data, 1, 1, jpeg_file);
if (data == 0XFF)
{
fread(&data, 1, 1, jpeg_file);
if (data == 0XD9)
{
break;
}
}
}
// 获取文件大小
uint32_t frame_size = ftell(jpeg_file);
rewind(jpeg_file);
frame_offsets[i] = mdat_size; // 记录当前帧偏移
frame_sizes[i] = frame_size; // 记录当前帧大小
mdat_size += frame_size; // 更新 mdat 总大小
// 写入 JPEG 数据
uint8_t* buffer = (uint8_t*)malloc(frame_size);
fread(buffer, 1, frame_size, jpeg_file);
fwrite(buffer, 1, frame_size, mp4_file);
free(buffer);
fclose(jpeg_file);
}
// 回到文件开头写入 mdat box 长度和类型
fseek(mp4_file, position, SEEK_SET);
write_uint32(mp4_file, mdat_size); // 写入 mdat 长度
fwrite("mdat", 1, 4, mp4_file);
fseek(mp4_file, 0, SEEK_END);
}
Movie Box(moov)
moov box 存放媒体文件的大多数信息,moov box 可选,但一个媒体文件中必须存在一个 moov box。
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 长度 | 不定长 |
box type | 4 字节 | box 类型,固定值:moov | moov |
box data | n 字节 | box 数据 | 子box内容,包括:mvhd、trak等 |
使用十六进制查看工具打开MP4文件,查看对应内容:
代码实现:
uint32_t position_moov_size = ftell(mp4_file);
write_uint32(mp4_file, 0); // 占位 moov 长度
fwrite("moov", 1, 4, mp4_file); // 写入 moov 类型
对于box data,具体内容如下:
Movie Header Box(mvhd)
mvhd box 存放媒体文件的信息,比如创建时间、修改时间、时间标尺、播放时间、时长等。
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 长度 | 0X0000006C |
box type | 4 字节 | box 类型,固定值:mvhd | mvhd |
version | 1 字节 | 版本,取0或者1,一般取0 | 0X00 |
flags | 3 字节 | 标志 | 0X000000 |
creation_time | 8/4 字节 | 创建时间,当version=0时,取4字节 | 0X00000000 |
modification_time | 8/4 字节 | 修改时间,当version=0时,取4字节 | 0X00000000 |
timescale | 4 字节 | 时间标尺,用于计算时间 | 0X000003E8(十进制1000) |
duration | 4 字节 | 时长,单位为timescale | frame_count * timescale / fps |
rate | 4 字节 | 播放速率,高16位为小数点后2位,低16位为整数 | 0X00010000(1.0) |
volume | 2 字节 | 音量,0为静音,1为最大音量 | 0X0100(1.0) |
reserved | 10 字节 | 保留字段 | 0 |
matrix | 36 字节 | 矩阵数据 | 0 |
pre_defined | 24 字节 | 预定义字段 | 0 |
next_track_ID | 4 字节 | 下一个track的ID,一般为track_count+1 | 0X00000002 |
对于创建时间、修改时间不影响MP4的播放,所以可以都设置为0。视频帧率设置的是30帧。
frame_count
:视频帧数,也可以理解成图像个数,一个图像就是一个frame。
timescale
:时间标尺/时间基数,在此次封装中默认为1000
。
fps
:视频帧数,在此次封装中默认为30
帧。
使用十六进制查看工具打开MP4文件,查看对应内容:
标准文档定义:
aligned(8) class MovieHeaderBox extends FullBox(‘mvhd’, version, 0) {
if (version==1) {
unsigned int(64) creation_time;
unsigned int(64) modification_time;
unsigned int(32) timescale;
unsigned int(64) duration;
} else { // version==0
unsigned int(32) creation_time;
unsigned int(32) modification_time;
unsigned int(32) timescale;
unsigned int(32) duration;
}
template int(32) rate = 0x00010000; // typically 1.0
template int(16) volume = 0x0100; // typically, full volume
const bit(16) reserved = 0;
const unsigned int(32)[2] reserved = 0;
template int(32)[9] matrix =
{ 0x00010000,0,0,0,0x00010000,0,0,0,0x40000000 };
// Unity matrix
bit(32)[6] pre_defined = 0;
unsigned int(32) next_track_ID;
}
代码实现:
void write_mvhd(FILE* file, uint32_t frame_count, uint32_t timescale)
{
uint32_t size = 0X6C;
write_uint32(file, size);
fwrite("mvhd", 1, 4, file);
write_uint32(file, 0); //version and flags
write_uint32(file, 0); // Creation time
write_uint32(file, 0); // Modification time
write_uint32(file, timescale); // Timescale
write_uint32(file, frame_count * timescale / 30); // Duration
fwrite("\x00\x01\x00\x00", 1, 4, file); // Rate
fwrite("\x01\x00", 1, 2, file); // Volume
fwrite("\x00\x00", 1, 2, file); // Reserved
fwrite("\x00\x00\x00\x00", 1, 4, file); // Reserved
fwrite("\x00\x00\x00\x00", 1, 4, file); // Reserved
fwrite("\x00\x00\x00\x00", 1, 4, file); // Matrix
fwrite("\x00\x00\x00\x00", 1, 4, file); // Matrix
fwrite("\x00\x00\x00\x00", 1, 4, file); // Matrix
fwrite("\x00\x00\x00\x00", 1, 4, file); // Matrix
fwrite("\x00\x00\x00\x00", 1, 4, file); // Matrix
fwrite("\x00\x00\x00\x00", 1, 4, file); // Matrix
fwrite("\x00\x00\x00\x00", 1, 4, file); // Matrix
fwrite("\x00\x00\x00\x00", 1, 4, file); // Matrix
fwrite("\x00\x00\x00\x00", 1, 4, file); // Matrix
fwrite("\x00\x00\x00\x00", 1, 4, file); // Pre_defined
fwrite("\x00\x00\x00\x00", 1, 4, file); // Pre_defined
fwrite("\x00\x00\x00\x00", 1, 4, file); // Pre_defined
fwrite("\x00\x00\x00\x00", 1, 4, file); // Pre_defined
fwrite("\x00\x00\x00\x00", 1, 4, file); // Pre_defined
fwrite("\x00\x00\x00\x00", 1, 4, file); // Pre_defined
write_uint32(file, 2); // Next track ID
}
Track Box(trak)
trak box 记录媒体流信息,文件中可以存在一个或多个 track,它们之间是相互独立的。一般的包含一个视频track和一个音频track。
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 长度 | 不定长 |
box type | 4 字节 | box 类型,固定值:trak | trak |
box data | n 字节 | box 数据 | 子box内容,包括tkhd、edts、mdia等 |
此次封装只涉及将MJPEG封装为MP4,不涉及音频,所以只有一个trak box。
使用十六进制查看工具打开MP4文件,查看对应内容:
代码实现:
uint32_t position_trak_size = ftell(mp4_file);
write_uint32(mp4_file, 0); // 占位 trak 长度
fwrite("trak", 1, 4, mp4_file);
对于box data,具体内容如下:
Track Header Box(tkhd)
tkhd box 存放track的信息,如果是视频会有宽、高信息、 还有文件创建时间、修改时间等。
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 长度 | 0X0000005C |
box type | 4 字节 | box 类型,固定值:tkhd | tkhd |
version | 1 字节 | box 版本,取0或1,一般取0 | 0X00 |
flags | 3 字节 | box 标志 | 0X000003 |
creation_time | 8/4 字节 | 创建时间,当version=0时,取4字节 | 0X00000000 |
modification_time | 8/4 字节 | 修改时间,当version=0时,取4字节 | 0X00000000 |
track_ID | 4 字节 | 本track ID | 0X00000001 |
reserved | 4 字节 | 保留字段 | 0 |
duration | 4 字节 | 时长 | frame_count * timescale / fps |
reserved | 8 字节 | 保留字段 | 0 |
layer | 2 字节 | 图层 | 0X0000 |
alternate_group | 2 字节 | 替代track组 | 0X0000 |
volume | 2 字节 | 音量(如果是audio trak,则为0X0100,否则为0) | 0X0000 |
reserved | 2 字节 | 保留字段 | 0X0000 |
matrix | 36 字节 | 矩阵 | 0 |
width | 4 字节 | 宽度 [16.16]格式 | width << 10 |
height | 4 字节 | 高度 [16.16]格式 | height << 10 |
使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:
标准文档定义:
aligned(8) class TrackHeaderBox extends FullBox(‘tkhd’, version, flags){
if (version==1) {
unsigned int(64) creation_time;
unsigned int(64) modification_time;
unsigned int(32) track_ID;
const unsigned int(32) reserved = 0;
unsigned int(64) duration;
} else { // version==0
unsigned int(32) creation_time;
unsigned int(32) modification_time;
unsigned int(32) track_ID;
const unsigned int(32) reserved = 0;
unsigned int(32) duration;
}
const unsigned int(32)[2] reserved = 0;
template int(16) layer = 0;
template int(16) alternate_group = 0;
template int(16) volume = {if track_is_audio 0x0100 else 0};
const unsigned int(16) reserved = 0;
template int(32)[9] matrix=
{ 0x00010000,0,0,0,0x00010000,0,0,0,0x40000000 };
// unity matrix
unsigned int(32) width;
unsigned int(32) height;
}
代码实现:
void write_tkhd(FILE* file, uint32_t frame_count, uint32_t timescale, int width, int height) {
uint32_t size = 0X5C;
write_uint32(file, size);
fwrite("tkhd", 1, 4, file);
write_uint32(file, 3); //version and flag
write_uint32(file, 0); //creation_time
write_uint32(file, 0); //modification_time
write_uint32(file, 1); //track id
write_uint32(file, 0); //reserved
write_uint32(file, frame_count * timescale / 30); //duration
write_uint32(file, 0); //reserved
write_uint32(file, 0); //reserved
fwrite("\x00\x00", 1, 2, file); //layer
fwrite("\x00\x00", 1, 2, file); //alternate_group
fwrite("\x00\x00", 1, 2, file); //volume
fwrite("\x00\x00", 1, 2, file); //reserved
write_uint32(file, 0); //matrix
write_uint32(file, 0); //matrix
write_uint32(file, 0); //matrix
write_uint32(file, 0); //matrix
write_uint32(file, 0); //matrix
write_uint32(file, 0); //matrix
write_uint32(file, 0); //matrix
write_uint32(file, 0); //matrix
write_uint32(file, 0); //matrix
write_uint32(file, width << 10); //width
write_uint32(file, height << 10); //height
}
Edit Box(edts)
edts box 存放track的编辑信息,比如track在媒体文件中的起始时间、时长等。
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 长度 | 0X00000024 |
box type | 4 字节 | box 类型 | edts |
box data | n 字节 | box 数据 | 子box内容,包括elst等 |
使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:
代码实现:
uint32_t size = 0X24;
write_uint32(file, size);
fwrite("edts", 1, 4, file);
对于box data,具体内容如下:
Edit List Box(elst)
elst box 存放track的编辑信息,比如track在媒体文件中的起始时间、时长等。
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 长度 | 0X0000001C |
box type | 4 字节 | box 类型 | elst |
version | 1 字节 | box 版本,取0或1,一般取0 | 0X00 |
flags | 3 字节 | box 标志 | 0X000000 |
entry_count | 4 字节 | entry 数量 | 0X00000001 |
segment_duration | 4 字节 | 片段时长 | frame_count * timescale / fps |
media_time | 4 字节 | 媒体开始时间 | 0X00000000 |
media_rate | 4 字节 | 媒体播放速率 | 0X00010000 |
使用十六进制查看工具打开MP4文件,查看对应内容:
标准文档定义:
aligned(8) class EditListBox extends Ful1Box('elst' , version, 0) {
unsigned int (32) entry_count;
for (i=1; i <= entry_count; i++) {
if (version==1) {
unsigned int(64) segment_duration;
int(64) media_ time ;
} else { // version==0
unsigned int(32) segment_duration;
int(32) media_time ;
}
int(16) media_rate_integer;
int(16) media_rate_fraction = 0;
}
}
代码实现:
write_uint32(file, 0X1C);
fwrite("elst", 1, 4, file);
write_uint32(file, 0); //version and flag
write_uint32(file, 1); //entry_count
write_uint32(file, frame_count * timescale / 30); //segment_duration
write_uint32(file, 0); //start time
fwrite("\x00\x01\x00\x00", 1, 4, file); //rate
Media Box(mdia)
mdia box 描述了这条音视频轨/流(trak)的媒体数据样本的主要信息,对播放器来说是一个很重要的 box。
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 长度 | 不定长 |
box type | 4 字节 | box 类型 | mdia |
box data | n 字节 | box 数据 | 子box内容,包括mdhd等 |
使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:
代码实现:
uint32_t position_mdia_size = ftell(mp4_file);
write_uint32(mp4_file, 0); // 占位 mdia 长度
fwrite("mdia", 1, 4, mp4_file);
对于box data,具体内容如下:
Media Header Box(mdhd)
mdhd box 当前音/视频轨/流(trak)的总体信息。
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 长度 | 0X00000020 |
box type | 4 字节 | box 类型 | mdhd |
version | 1 字节 | box 版本,取0或1,一般取0 | 0X00 |
flags | 3 字节 | box 标志 | 0X000000 |
creation_time | 8/4 字节 | 创建时间 | 0X00000000 |
modification_time | 8/4 字节 | 修改时间 | 0X00000000 |
timescale | 4 字节 | 时间标尺,同mvhd中的timescale | 0X00000001 |
duration | 8/4 字节 | 本track时长 | 0X00000000 |
pad | 1 位 | 取0 | 0B |
language | 3 * 5 位 | 语言,最高位为0,后面15位为三个字符 | 101010111000100B |
pre_defined | 2 字节 | 保留 | 0X0000 |
对于language
,见ISO 639-2/T,如中文为chi
,英文为eng
。
使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:
标准文档定义:
aligned(8) class MediaHeaderBox extends FullBox(‘mdhd’, version, 0) {
if (version==1) {
unsigned int(64) creation_time;
unsigned int(64) modification_time;
unsigned int(32) timescale;
unsigned int(64) duration;
} else { // version==0
unsigned int(32) creation_time;
unsigned int(32) modification_time;
unsigned int(32) timescale;
unsigned int(32) duration;
}
bit(1) pad = 0;
unsigned int(5)[3] language; // ISO-639-2/T language code
unsigned int(16) pre_defined = 0;
}
代码实现:
void write_mdhd(FILE* file, uint32_t frame_count, uint32_t timescale) {
uint32_t size = 0X20;
write_uint32(file, size);
fwrite("mdhd", 1, 4, file);
write_uint32(file, 0); //version and flag
write_uint32(file, 0); //creation_time
write_uint32(file, 0); //modification_time
write_uint32(file, timescale); //timescale
write_uint32(file, frame_count * timescale / 30); //duration
fwrite("\x55\xC4\x00\x00", 1, 4, file); //pad and language and pre_defined
}
Media Handler Reference Box(hdlr)
hdlr box 解释了媒体的播放过程信息。
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 长度 | 0x0000002D |
box type | 4 字节 | box 类型 | hdlr |
version | 1 字节 | box 版本 | 0X00 |
flags | 3 字节 | box 标志 | 0X000000 |
pre_defined | 4 字节 | 保留 | 0X00000000 |
handler_type | 4 字节 | 类型 | 0X6D646976 |
reserved | 12 字节 | 保留 | 0 |
name | n 字节 | 自定义名称,以’\0’结尾 | VideoHandler |
handler_type
:
取值为
vide:Video track,soun:Audio track,
hint:Hint track,
meta:Timed Metadata track,
auxv:Auxiliary Video track
在此处,0X6D646976,将每一个字节转成ASCII字符,根据大端序排序,即vide
。
使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:
标准文档定义:
aligned(8) class HandlerBox extends FullBox(‘hdlr’, version = 0, 0) {
unsigned int(32) pre_defined = 0;
unsigned int(32) handler_type;
const unsigned int(32)[3] reserved = 0;
string name;
}
代码实现:
void write_hdlr(FILE* file) {
uint32_t size = 0X2D;
write_uint32(file, size);
fwrite("hdlr", 1, 4, file);
write_uint32(file, 0); // version and flag
write_uint32(file, 0); // pre_defined
fwrite("vide", 1, 4, file); // handler_type
write_uint32(file, 0); // reserved
write_uint32(file, 0); // reserved
write_uint32(file, 0); // reserved
fwrite("VideoHandler", 1, 12, file); // name
fwrite("\x00", 1, 1, file);
}
Midia Information Box(minf)
minf box 存放track的媒体信息,比如track的duration、timescale等。
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 长度 | 不定长 |
box type | 4 字节 | box 类型 | minf |
box data | n 字节 | box 数据 | 子box内容,包括vmhd、dinf等 |
使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:
代码实现:
uint32_t position_minf_size = ftell(mp4_file);
write_uint32(mp4_file, 0); // 占位 minf 长度
fwrite("minf", 1, 4, mp4_file);
对于box data,具体内容如下:
Video Media Handler Box(vmhd)
vmhd box 存放track的媒体信息,比如track的duration、timescale等。
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 长度 | 0x00000014 |
box type | 4 字节 | box 类型 | vmhd |
version | 1 字节 | box 版本 | 0X00 |
flags | 3 字节 | box 标志 | 0X000001 |
graphics mode | 2 字节 | 视频合成模式,为0时拷贝原始图像,否则与opcolor进行合成 | 0X0000 |
opcolor | 6 字节 | 操作颜色{red, green, blue} | 0X000000000000 |
使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:
标准文档定义:
aligned(8) class VideoMediaHeaderBox extends FullBox(‘vmhd’, version = 0, 1) {
template unsigned int(16) graphicsmode = 0; // copy, see below
template unsigned int(16)[3] opcolor = {0, 0, 0};
}
代码实现:
void write_vmhd(FILE* file) {
uint32_t size = 0X14;
write_uint32(file, size);
fwrite("vmhd", 1, 4, file);
write_uint32(file, 1); // version and flag
fwrite("\x00\x00", 1, 2, file); // graphicsmode
fwrite("\x00\x00\x00\x00\x00\x00", 1, 6, file); // opcolor
}
Data Information Box(dinf)
dinf 解释如何定位媒体信息,是一个 container box。dinf 一般包含一个 dref,即 data reference box; dref 下会包含若干个 url 或 urn ,这些box组成一个表,用来定位track数据。简单的说,track可以被分成若干段,每一段都可以根据 url 或 urn 指向的地址来获取数据,sample描述中会用这些片段的序号将这些片段组成一个完整的track。
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 长度 | 不定长(0X00000024) |
box type | 4 字节 | box 类型 | dinf |
box data | n 字节 | box 数据 | 子box内容,包括dref等 |
使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:
代码实现:
uint32_t size = 0X24;
write_uint32(file, size);
fwrite("dinf", 1, 4, file);
对于box data,具体内容如下:
Data Reference Box(dref)
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 长度 | 不定长(0X0000001C) |
box type | 4 字节 | box 类型 | dref |
verion | 1 字节 | box 版本 | 0X00 |
flags | 3 字节 | box 标志 | 0X000000 |
entry count | 4 字节 | 条目数量 | 0X00000001 |
url/urn | n 字节 | url/urn 列表 | url/urn box |
url/urn box:
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 长度 | 0X0000000C |
box type | 4 字节 | box 类型 | url |
box flag | 4 字节 | box 标志 | 0X00000001 |
一般情况下,当数据被完全包含在文件中时,url或urn中的定位字符串是空的。
url或urn都是box,url的内容为字符串,urn的内容为一对字符串。当url或urn的box flag为1时,字符串均为空。
使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:
标准文档定义:
aligned(8) class DataEntryUrlBox (bit(24) flags)
extends FullBox(‘url ’, version = 0, flags) {
string location;
}
aligned(8) class DataEntryUrnBox (bit(24) flags)
extends FullBox(‘urn ’, version = 0, flags) {
string name;
string location;
}
aligned(8) class DataReferenceBox
extends FullBox(‘dref’, version = 0, 0) {
unsigned int(32) entry_count;
for (i=1; i <= entry_count; i++) {
DataEntryBox(entry_version, entry_flags) data_entry;
}
}
代码实现:
write_uint32(file, 0X1C);
fwrite("dref", 1, 4, file);
write_uint32(file, 0); // version and flags
write_uint32(file, 1); // entry count
write_uint32(file, 0X0C); // url box length
fwrite("url ", 1, 4, file); // url box type
write_uint32(file, 1); // url box flag
由绿色部分我们知道包含的 url 或 urn 个数为1,绿色后面为 url box 的内容。紫色为 url 的 box header(根据box type我们知道是个 url ),粉色为 box flag,值为 1,说明 url 中的字符串为空,表示 track 数据已包含在文件中。
注意:虽然写入内容应该是不定长,但是实际写入时,为固定值。
Sample Description Box(stbl)
stbl 解释如何解码媒体信息,是一个 container box。stbl 包含了编码信息、解码参数、时间戳等信息,是媒体文件中最重要的部分。stbl 中包含的各个 box 的含义如下:
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 长度 | 不定长 |
box type | 4 字节 | box 类型 | stbl |
box data | n 字节 | box 数据 | 子box内容,包括stsd、stts等 |
使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:
代码实现:
uint32_t position_stbl_size = ftell(mp4_file);
write_uint32(mp4_file, 0); // 占位 stbl 长度
fwrite("stbl", 1, 4, mp4_file);
对于box data,具体内容如下:
Sample Description Entry(stsd)
stsd 描述了媒体数据的编码信息,是一个 container box。stsd 中包含一个或多个 sample description box,每个 sample description box 描述了一种编码格式。stsd 的 box data 中包含一个 sample description entry,每个 sample description entry 描述了一种编码格式。
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 长度 | 0X000000C0 |
box type | 4 字节 | box 类型 | stsd |
version | 1 字节 | box 版本 | 0X00 |
flags | 3 字节 | box 标志 | 0X000000 |
entry count | 4 字节 | 条目数量 | 0X00000001 |
box data | n 字节 | box 数据 | 子box内容,包括mp4v box等 |
使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:
标准文档定义:
aligned(8) class SampleDescriptionBox (unsigned int(32) handler_type)
extends FullBox('stsd', 0, 0){
int i ;
unsigned int(32) entry_count;
for (i = 1 ; i <= entry_count ; i++){
switch (handler_type){
case ‘soun’: // for audio tracks
AudioSampleEntry();
break;
case ‘vide’: // for video tracks
VisualSampleEntry();
break;
case ‘hint’: // Hint track
HintSampleEntry();
break;
case ‘meta’: // Metadata track
MetadataSampleEntry();
break;
}
}
}
}
代码实现:
uint32_t size = 0XC0;
write_uint32(file, size);
fwrite("stsd", 1, 4, file);
write_uint32(file, 0); // version and flags
write_uint32(file, 1); // entry count
对于box data,具体内容如下:
Sample Description Entry(sample entry)
mp4v box:
对于video track,使用“VisualSampleEntry”类型信息。
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 类型 | 0X000000B0 |
box type | 4 字节 | box 类型 | mp4v |
reserved | 6 字节 | 保留 | 0 |
data_reference_index | 2 字节 | 数据索引 | 0X0001 |
pre_defined | 2 字节 | 保留 | 0 |
reserved | 2 字节 | 保留 | 0 |
pre_defined | 12 字节 | 保留 | 0 |
width | 2 字节 | 像素宽度 | 0X0500 |
height | 2 字节 | 像素高度 | 0X02D0 |
horizresolution | 4 字节 | 水平,每英尺的像素值 | 0X00480000 |
vertresolution | 4 字节 | 垂直,每英尺的像素值 | 0X00480000 |
reserved | 4 字节 | 保留 | 0 |
frame_count | 2 字节 | 每个sample中的视频帧数,固定为1 | 0X0001 |
compressor | 32 字节 | 压缩器标识符 | \x13Lavc61.19.100 mjpeg |
depth | 2 字节 | 深度 | 0X0018 |
pre_defined | 2 字节 | 保留,以0XFFFF填充 | 0XFFFF |
使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:
标准文档定义:
aligned(8) class VisualSampleEntry(codingname) extends SampleEntry (codingname){
unsigned int(16) pre_defined = 0;
const unsigned int(16) reserved = 0;
unsigned int(32)[3] pre_defined = 0;
unsigned int(16) width;
unsigned int(16) height;
template unsigned int(32) horizresolution = 0x00480000; // 72 dpi
template unsigned int(32) vertresolution = 0x00480000; // 72 dpi
const unsigned int(32) reserved = 0;
template unsigned int(16) frame_count = 1;
string[32] compressorname;
template unsigned int(16) depth = 0x0018;
int(16) pre_defined = -1;
// other boxes from derived specifications
CleanApertureBox clap; // optional
PixelAspectRatioBox pasp; // optional
}
代码实现:
write_uint32(file, 0XB0);
fwrite("mp4v", 1, 4, file);
fwrite("\x00\x00\x00\x00\x00\x00", 1, 6, file); // reserved
fwrite("\x00\x01", 1, 2, file); // data_reference_index
fwrite("\x00\x00", 1, 2, file); // pre_defined
fwrite("\x00\x00", 1, 2, file); // reserved
write_uint32(file, 0X0); // pre_defined
write_uint32(file, 0X0); // pre_defined
write_uint32(file, 0X0); // pre_defined
write_uint32(file, 0X0); // pre_defined
fwrite("\x05\x00\x02\xD0", 1, 4, file); // width and height
fwrite("\x00\x48\x00\xD0", 1, 4, file); // horizresolution
fwrite("\x00\x48\x00\xD0", 1, 4, file); // vertresolution
write_uint32(file, 0X0); // reserved
fwrite("\x00\x01", 1, 2, file); // frame_count
fwrite("\x13Lavc61.19.100 mjpeg", 1, 20, file); // compressorname
write_uint32(file, 0X0);
write_uint32(file, 0X0);
write_uint32(file, 0X0);
fwrite("\x00\x18\xFF\xFF", 1, 4, file); // depth and pre_defined
esds box:
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 类型 | 0X0000001C |
box type | 4 字节 | box 类型 | esds |
version | 1 字节 | box 版本 | 0X00 |
flags | 3 字节 | box 标志 | 0X000000 |
ESDescriptor | n 字节 | 嵌套的 ESDescriptor 数据,描述流信息 | 子box内容 |
使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:
代码实现:
write_uint32(file, 0X2C);
fwrite("esds", 1, 4, file);
write_uint32(file, 0X0); // version and flags
ESDescriptor:
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
tag | 1 字节 | 描述符类型,固定为 0X03 | 0X03 |
length | 4 字节 | 描述符长度,包含接下来的所有字段 | 0X8080801B |
ES_ID | 2 字节 | ES 标识符,唯一标识此流 | 0X0001 |
streamDependenceFlag | 1 位 | 是否依赖其他流,通常为 0 | 0 |
URL_Flag | 1 位 | 是否包含 URL 描述符,通常为 0 | 0 |
OCRstreamFlag | 1 位 | 是否有 OCR 流标识符,通常为 0 | 0 |
streamPriority | 5 位 | 流的优先级,通常为 0 | 00000 |
DependsOn_ES_ID | 可选 | 如果 streamDependenceFlag=1,则填写此字段 | 此处不填写 |
URLstring | 可选 | 如果 URL_Flag=1,则填写此字段 | 此处不填写 |
OCR_ES_Id | 可选 | 如果 OCRstreamFlag=1,则填写此字段 | 此处不填写 |
DecoderConfigDescriptor | n 字节 | 嵌套的 DecoderConfigDescriptor 数据 | 子box内容 |
使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:
代码实现:
//该字段内容一般不会变,直接写入即可
fwrite("\x03\x80\x80\x80\x1B\x00\x01\x00", 1, 8, file);
DecoderConfigDescriptor:
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
tag | 1 字节 | 描述符类型,固定为 0X04 | 0X04 |
length | 4 字节 | 描述符长度,包含接下来的所有字段 | 0X8080800D |
objectTypeIndication | 1 字节 | 编码对象类型,标识流的编码格式 | 0X6C |
streamType | 6 位 | 流类型,通常为 0x04(视频)或 0x05(音频) | 000100 |
upstreamFlag | 1 位 | 是否为上行流,通常为 0 | 0 |
reserved | 1 位 | 保留位,固定为 1 | 1 |
bufferSizeDB | 3 字节 | 解码器的缓冲区大小 | 0X000000 |
maxBitrate | 4 字节 | 最大比特率 | 0X00BA0E28 |
avgBitrate | 4 字节 | 平均比特率 | 0X00BA0E28 |
SLConfigDescrTag | n 字节 | 嵌套的 SLConfigDescrTag 数据 | 子box内容 |
使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:
代码实现:
//该字段内容一般不会变,直接写入即可
fwrite("\x04\x80\x80\x80\x0D\x6C\x11\x00", 1, 8, file);
fwrite("\x00\x00\x00\xBA\x0E\x28\x00\xBA\x0E\x28", 1, 10, file);
objectTypeIndication:0X6C
对应了一种编码:AV_CODEC_ID_MJPEG。FFmpeg isom.c中定义了MP4容器支持的所有编码格式:
/* http://www.mp4ra.org */
/* ordered by muxing preference */
const AVCodecTag ff_mp4_obj_type[] = {
{ AV_CODEC_ID_MOV_TEXT , 0x08 },
{ AV_CODEC_ID_MPEG4 , 0x20 },
{ AV_CODEC_ID_H264 , 0x21 },
{ AV_CODEC_ID_HEVC , 0x23 },
{ AV_CODEC_ID_AAC , 0x40 },
{ AV_CODEC_ID_MP4ALS , 0x40 }, /* 14496-3 ALS */
{ AV_CODEC_ID_MPEG2VIDEO , 0x61 }, /* MPEG-2 Main */
{ AV_CODEC_ID_MPEG2VIDEO , 0x60 }, /* MPEG-2 Simple */
{ AV_CODEC_ID_MPEG2VIDEO , 0x62 }, /* MPEG-2 SNR */
{ AV_CODEC_ID_MPEG2VIDEO , 0x63 }, /* MPEG-2 Spatial */
{ AV_CODEC_ID_MPEG2VIDEO , 0x64 }, /* MPEG-2 High */
{ AV_CODEC_ID_MPEG2VIDEO , 0x65 }, /* MPEG-2 422 */
{ AV_CODEC_ID_AAC , 0x66 }, /* MPEG-2 AAC Main */
{ AV_CODEC_ID_AAC , 0x67 }, /* MPEG-2 AAC Low */
{ AV_CODEC_ID_AAC , 0x68 }, /* MPEG-2 AAC SSR */
{ AV_CODEC_ID_MP3 , 0x69 }, /* 13818-3 */
{ AV_CODEC_ID_MP2 , 0x69 }, /* 11172-3 */
{ AV_CODEC_ID_MPEG1VIDEO , 0x6A }, /* 11172-2 */
{ AV_CODEC_ID_MP3 , 0x6B }, /* 11172-3 */
{ AV_CODEC_ID_MJPEG , 0x6C }, /* 10918-1 */
{ AV_CODEC_ID_PNG , 0x6D },
{ AV_CODEC_ID_JPEG2000 , 0x6E }, /* 15444-1 */
{ AV_CODEC_ID_VC1 , 0xA3 },
{ AV_CODEC_ID_DIRAC , 0xA4 },
{ AV_CODEC_ID_AC3 , 0xA5 },
{ AV_CODEC_ID_EAC3 , 0xA6 },
{ AV_CODEC_ID_DTS , 0xA9 }, /* mp4ra.org */
{ AV_CODEC_ID_VP9 , 0xC0 }, /* nonstandard, update when there is a standard value */
{ AV_CODEC_ID_FLAC , 0xC1 }, /* nonstandard, update when there is a standard value */
{ AV_CODEC_ID_TSCC2 , 0xD0 }, /* nonstandard, camtasia uses it */
{ AV_CODEC_ID_EVRC , 0xD1 }, /* nonstandard, pvAuthor uses it */
{ AV_CODEC_ID_VORBIS , 0xDD }, /* nonstandard, gpac uses it */
{ AV_CODEC_ID_DVD_SUBTITLE, 0xE0 }, /* nonstandard, see unsupported-embedded-subs-2.mp4 */
{ AV_CODEC_ID_QCELP , 0xE1 },
{ AV_CODEC_ID_MPEG4SYSTEMS, 0x01 },
{ AV_CODEC_ID_MPEG4SYSTEMS, 0x02 },
{ AV_CODEC_ID_NONE , 0 },
};
SLConfigDescrTag:
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
tag | 1 字节 | 描述符类型,固定为 0X06 | 0X06 |
length | 4 字节 | 描述符长度,包含接下来的所有字段 | 0X80808001 |
predefined | 1 字节 | 预定义 | 0X02 |
使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:
代码实现:
//该字段内容一般不会变,直接写入即可
fwrite("\x06\x80\x80\x80\x01\x02", 1, 6, file);
对于上述ESDescriptor
表、DecoderConfigDescriptor
、SLConfigDescrTag
,其中的 length 采用 VLVC (Variable Length Value Coding) 格式编码。每个字节的最高位是延续位,用来指示是否还有后续字节:
- 1 表示还有后续字节
- 0 表示没有后续字节,当前字节是最后一个字节
其余七位表示实际的数值部分。
例如:0X8080800D
是 4 个字节,其中每个字节表示如下:
- 0x80 的二进制是 10000000:延续位为 1,表示后面还有字节。
- 0x80 的二进制是 10000000:延续位为 1,表示后面还有字节。
- 0x80 的二进制是 10000000:延续位为 1,表示后面还有字节。
- 0x1B 的二进制是 00011011:延续位为 0,表示这是最后一个字节。
移除每个字节的最高位(延续位),只保留数值部分:
- 第 1 字节:0x80 -> 0x00
- 第 2 字节:0x80 -> 0x00
- 第 3 字节:0x80 -> 0x00
- 第 4 字节:0x1B -> 0x1B
将这些数值部分按照顺序拼接起来:
拼接后得到:0x0000001B(十六进制)或 27(十进制)。
fiel box:
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 长度 | 0X0000000A |
box type | 4 字节 | box 类型 | fiel |
field_count | 1 字节 | 扫描模式,1 表示逐行扫描,2 表示隔行扫描 | 0X01 |
field_order | 1 字节 | 场序,仅在隔行扫描模式时有效 | 0X00 |
使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:
代码实现:
write_uint32(file, 0X0A);
fwrite("fiel", 1, 4, file);
fwrite("\x01\x00", 1, 2, file); // field_count 和 field_order
对于 field_order
的取值说明如下:
- 0: 无场序或未定义
- 1: 上场(Top Field First)
- 6: 下场(Bottom Field First)
pasp box:
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 长度 | 0X00000010 |
box type | 4 字节 | box 类型 | pasp |
hSpacing | 4 字节 | 水平方向像素的比例因子 | 0X00000001 |
vSpacing | 4 字节 | 垂直方向像素的比例因子 | 0X00000001 |
使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:
代码实现:
write_uint32(file, 0X10);
fwrite("pasp", 1, 4, file);
write_uint32(file, 1); // hSpacing
write_uint32(file, 1); // vSpacing
像素长宽比 = vSpacing / hSpacing
如果 hSpacing 为 1 且 vSpacing 为 1,则表示像素是正方形。
btrt box:
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 长度 | 0X00000014 |
box type | 4 字节 | box 类型 | btrt |
bufferSizeDB | 4 字节 | 解码器所需的缓冲区大小,单位为字节 | 0X00000000 |
maxBitrate | 4 字节 | 流的最大比特率,单位为比特每秒 | 0X00BA0E28 |
avgBitrate | 4 字节 | 流的平均比特率,单位为比特每秒 | 0X00BA0E28 |
使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:
代码实现:
write_uint32(file, 0X14);
fwrite("btrt", 1, 4, file);
write_uint32(file, 0);
fwrite("\x00\xBA\x0E\x28", 1, 4, file); // maxBitrate
fwrite("\x00\xBA\x0E\x28", 1, 4, file); // avgBitrate
Decoding Time to Sample Box(stts)
stts 包含了一个压缩版本的表,通过这个表可以从解码时间映射到sample序号。表中的每一项是连续相同的编码时间增量(Decode Delta)的个数和编码时间增量。通过把时间增量累加就可以建立一个完整的time to sample表。
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 类型 | 0X00000018 |
box type | 4 字节 | box 类型 | stts |
verion | 1 字节 | 取 0 或 1,一般取 0 | 0X00 |
flags | 3 字节 | box 标志位 | 0X000000 |
entry_count | 4 字节 | 条目数量 | 0X00000001 |
sample_count | 4 字节 | sample数量 | 图片数量(0X00000008) |
sample_delta | 4 字节 | sample间隔 | 0X00000021 |
在 stts 中,sample_count 和 sample_delta 成对出现的次数为 entry_count。
使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:
标准文档定义:
aligned(8) class TimeToSampleBox extends FullBox(’stts’, version = 0, 0) {
unsigned int(32) entry_count;
int i;
for (i=0; i < entry_count; i++) {
unsigned int(32) sample_count;
unsigned int(32) sample_delta;
}
}
代码实现:
void write_stts(FILE* file, uint32_t frame_count, uint32_t timescale) {
uint32_t size = 0X18;
write_uint32(file, size);
fwrite("stts", 1, 4, file);
write_uint32(file, 0); //version and flag
write_uint32(file, 1); //entry count
write_uint32(file, frame_count); //sample count
write_uint32(file, 33); //sample delta
}
Sample To Chunk Box(stsc)
media中的sample被分为组成chunk。chunk可以有不同的大小,chunk内的sample可以有不同的大小。
通过stsc中的sample-chunk映射表可以找到包含指定sample的chunk,从而找到这个sample。结构相同的chunk可以聚集在一起形成一个entry,这个entry就是stsc映射表的表项。
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 类型 | 0X0000001C |
box type | 4 字节 | box 类型 | stsc |
verion | 1 字节 | 取 0 或 1,一般取 0 | 0X00 |
flags | 3 字节 | box 标志位 | 0X000000 |
entry_count | 4 字节 | 条目数量 | 0X01 |
first_chunk | 4 字节 | 一组 chunk 的第一个 chunk 的序号,chunk 的编号从 1 开始 | 0X01 |
samples_per_chunk | 4 字节 | 每个 chunk 有多少个 sample | 图片数量(0X08) |
sample_description_index | 4 字节 | stsd box 中 sample desc 信息的索引 | 0X01 |
在 stsc 中 first_chunk 、 samples_per_chunk 和 sample_description_index 成对出现的次数为 entry_count。
使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:
标准文档定义:
aligned(8) class SampleToChunkBoxextends FullBox(‘stsc’, version = 0, 0) {
unsigned int(32) entry_count;
for (i=1; i <= entry_count; i++) {
unsigned int(32) first_chunk;
unsigned int(32) samples_per_chunk;
unsigned int(32) sample_description_index;
}
}
代码实现:
void write_stsc(FILE* file, uint32_t frame_count, uint32_t timescale) {
uint32_t size = 0X1C;
write_uint32(file, size);
fwrite("stsc", 1, 4, file);
write_uint32(file, 0); // version and flag
write_uint32(file, 1); // entry count
write_uint32(file, 1); // first_chunk
write_uint32(file, frame_count); // samples_per_chunk
write_uint32(file, 1); // sample_description_index
}
Sample Size Box(stsz)
stsz 包含了每个 sample 的大小,每个 sample 的大小都存储在 stsz 中。
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 类型 | 20 + 图片数量 * 4 |
box type | 4 字节 | box 类型 | stsz |
verion | 1 字节 | 取 0 或 1,一般取 0 | 0X00 |
flags | 3 字节 | box 标志位 | 0X000000 |
sample_size | 4 字节 | 指定默认的 sample 字节大小,如果所有 sample 的大小不一样,这个字段为 0 | 0X00000000 |
sample_count | 4 字节 | trak 中 sample数量 | 图片数量(0X08) |
entry_size | 4 字节 | 每个 sample 的字节大小 | 0X00000000 |
在 stsz 中,entry_size 出现的次数为 sample_count。
使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:
标准文档定义:
aligned(8) class SampleSizeBox extends FullBox(‘stsz’, version = 0, 0) {
unsigned int(32) sample_size;
unsigned int(32) sample_count;
if (sample_size==0) {
for (i=1; i <= sample_count; i++) {
unsigned int(32) entry_size;
}
}
}
代码实现:
void write_stsz(FILE* file, uint32_t frame_count, uint32_t* frame_sizes) {
uint32_t size = 20 + frame_count * 4;;
write_uint32(file, size);
fwrite("stsz", 1, 4, file);
write_uint32(file, 0); //version and flag
write_uint32(file, 0); //sample_size
write_uint32(file, frame_count); //sample_count
for (int i = 0; i < frame_count; i++)
{
write_uint32(file, frame_sizes[i]); //entry_size
}
}
Chunk Offset Box(stco)
Chunk Offset 表存储了每个chunk在文件中的位置,这样就可以直接在文件中找到媒体数据,而不用解析box。
字段 | 大小 | 含义 | 写入内容 |
---|---|---|---|
box length | 4 字节 | box 类型 | 0X00000014 |
box type | 4 字节 | box 类型 | stco |
verion | 1 字节 | 取 0 或 1,一般取 0 | 0X00 |
flags | 3 字节 | box 标志位 | 0X000000 |
entry_count | 4 字节 | 条目数量 | 0X00000001 |
chunk_offset | 4 字节 | chunk 在文件中的偏移 | mdat字符位置 |
在 stco 中,chunk_offset 出现的次数为 entry_count。
其中,chunk_offset 的值是开始填入 mdat box 的位置。也就是写入 box type 为 mdat
后,获取文件指针位置,然后写入 chunk_offset。
使用十六进制查看工具打开MP4文件,查看对应内容,各字段已用颜色区分开:
标准文档定义:
aligned(8) class ChunkOffsetBox extends FullBox(‘stco’, version = 0, 0) {
unsigned int(32) entry_count;
for (i=1; i <= entry_count; i++) {
unsigned int(32) chunk_offset;
}
}
代码实现:
void write_stco(FILE* file, uint32_t position) {
uint32_t size = 0X14;
write_uint32(file, size);
fwrite("stco", 1, 4, file);
write_uint32(file, 0); // version and flag
write_uint32(file, 1); // entry_count
write_uint32(file, position); // chunk_offset
}
提示
1、实际上,在 moov box 中还有一个 udat box,这个 box 是用来存储用户自定义信息的,这个 box 不是必须的,所以这里没有写入。
2、代码目前只支持在代码内更改需要转换的MJPEG图片,如果需要支持不定长图片数量进行转换,需要修改代码,添加文件读取功能。
3、分辨率也是写死的,如果需要支持不同的分辨率,请修改代码,可以通过读取图片文件信息获取分辨率,然后写入到代码中。
4、代码中输出的MP4文件名字是固定的,如果需要支持不同的文件名,请修改代码。
5、目前输出的MP4文件在PC端只能在VLC播放器中播放,经过测试,其他播放器播放该MP4文件会报错,具体原因未知,可能是因为MP4文件格式不标准。
6、在手机端,目前使用了OnePlus 一加手机进行了测试,将文件保存至相册,在相册内打开是可以正常播放的。
7、需要代码请私信我获取。