ISO/IEC 14496
ISO/IEC 14496 是MPEG专家组制定的MPEG-4标准,于2000年年初正式成为国际标准。MPEG-4与MPEG-1和MPEG-2有很大的不同。MPEG-4不只是具体压缩算法,它是针对数字电视、交互式绘图应用(影音合成内容)、交互式多媒体(WWW、资料撷取与分散)等整合及压缩技术的需求而制定的国际标准。
MPEG-4由一系列的字标准组成,包含以下的部分:
- ISO/IEC 14496-1:系统 - 描述视频和音频数据流的控制、同步以及混合方式(即混流Multiplexing,简写为MUX)。
- ISO/IEC 14496-2:视频 - 定义一个对各种视觉信息(包括自然视频、静止纹理、计算机合成图形等等)的编解码器。例如,XviD编码就属于MPEG -4 Part 2。
- ISO/IEC 14496-3:音频 - 定义一个对各种音频信号进行编码的编解码器的集合,包括高级音频编码(Advanced Audio Coding,缩写为AAC)的若干变形和其他一些音频/语音编码工具。
- ISO/IEC 14496-4:一致性 - 定义对本标准其他的部分进行一致性测试的程序。
- ISO/IEC 14496-5:参考软件 - 提供用于演示功能和说明本标准其他部分功能的软件。
- ISO/IEC 14496-6:多媒体传输集成框架 - DMIF for Delivery Multimedia Integration Framework。
- ISO/IEC 14496-7:优化的参考软件 - 提供对实现(ISO/IEC 14496-5)进行优化的例子。
- ISO/IEC 14496-8:在IP网络上传输 - 定义在IP网络上传输MPEG-4内容的方式。
- ISO/IEC 14496-9:参考硬件 - 提供用于演示怎样在硬件上实现本标准其他部分功能的硬件设计方案。
- ISO/IEC 14496-10:高级视频编码 - Advanced Video Coding,缩写为AVC,定义一个视频编解码器codec。AVC和XviD都属于MPEG-4编码,但由于AVC属于MPEG-4 Part 10,在技术特性上比属于MPEG-4 Part 2的XviD要先进。另外,它和ITU-T H.264标准是一致的,故又称为H.264。
- ISO/IEC 14496-11:场景描述和应用引擎 - 被称作 BIFS(Binary Format for Scene),XMT,MPEG-J。它被设计的用处是实现动态显示和交互显示的有效展现,构建2D和3D的图像、画面、文本,以及试听材料。这种显示的展现包括对不同场景组件(scene component)的时空组织性,以及用户交互性和动画进行描述。
- ISO/IEC 14496-12:基于ISO的媒体文件格式 - 定义一个存储媒体内容的文件格式。mp4文件的解析参考的就是该部分。
- ISO/IEC 14496-13:知识产权管理和保护拓展 - IPMP for Intellectual Property Management and Protection。
- ISO/IEC 14496-14:MPEG-4文件格式 - 定义基于 ISO/IEC 14496-12 的用于存储MPEG-4内容的视频文件格式。
- ISO/IEC 14496-15:AVC文件格式 - 定义基于 ISO/IEC 14496-12 的用于存储 ISO/IEC 14496-10 的视频内容的文件格式。
- ISO/IEC 14496-16:动画扩展框架 - AFX,Animation Framework eXtension。
- ISO/IEC 14496-17:同步文本字幕格式。
- ISO/IEC 14496-18:字体压缩和流式传输 - 针对开放字体格式Open Font Format。
- ISO/IEC 14496-19:合成材质流 - Synthesized Texture Strean。
- ISO/IEC 14496-20:简单场景表示 - LASeR for Lightweight Scene Representation。
- ISO/IEC 14496-21:用于描绘(Rendering)的MPEG-J拓展。
- ISO/IEC 14496-22:开放字体格式 - Open Font Format。
- ISO/IEC 14496-23:符号化音乐表示 - Symbolic Music Representation。
- ISO/IEC 14496-24:音频与系统交互作用 - Audio and systems interaction。
- ISO/IEC 14496-25:3D图形压缩模型 - 3D Graphics Compression Model。
- ISO/IEC 14496-26:音频一致性检查 - 定义测试音频数据与 ISO/IEC 14496-3 是否一致的方法,Audio comformance。
- ISO/IEC 14496-27:3D图形一致性检查 - 定义测试3D图形数据与 ISO/IEC 14496-11:2005,ISO/IEC 14496-16:2006,ISO/IEC 14496-21:2006 和 ISO/IEC 14496-25:2009 是否一致的方法,3D Graphics conformance。
- ISO/IEC 14496-28:复合字体表示法 - Composite font representation。
- ISO/IEC 14496-29:网络视频编码 - Web video coding。
- ISO/IEC 14496-30:ISO基本媒体文件格式中的定时文本和其他可视覆盖层 - Timed text and other visual overlays in ISO base media file format。
- ISO/IEC 14496-31:浏览器视频编码 - Video coding for browsers。
- ISO/IEC 14496-33:互联网视频编码 - Internet video coding。
mp4文件格式
MP4是在 ISO/IEC 14496-14 标准文件中定义的一种多媒体容器格式,是 ISO/IEC 14496-12 标准中所定义的媒体格式的一种实现。
参考标准
术语和定义
- box:由唯一类型标识符和长度定义的面向对象构建块。
- container box:用于包含和分组一组相关box的box。
- sample:一个时间戳的所有数据。一个音轨中的sample的时间戳不能重复。在非hint track中,一个sample是一个单独的视频帧,或一组连续的视频帧,或是一段连续的压缩音频;在hint track中,一个sample定义一个或多个流数据包的形成。
- chunk:一个音轨的连续sample集合。
- track:相关sample的时序。对于media data,track对应于一系列图像或采样的音频;对于hint track,一个轨道对应于一个流信道。
- hint track:一种特殊的音轨,它不包含媒体数据,而是包含将一个或多个音轨打包成一个流媒体信道的方法。
- presentation:一个或多个运动序列,可能与音频结合。表示一个视频文件。
- media data box:为一个presentation保存实际媒体数据的box,类型标识符为mdat。
- movie box:一个container box,其子box定义了一个presentation的metadata,类型标识符为moov。
- sample description:定义并描述一个track中多个sample格式的box,类型标识符为stsd。
- sample table:一个track中sample的时序和物理布局,类型标识符为stbl。
- sync sample:描述关键帧,类型标识符为stss。
文件组成
MP4文件由一系列的box构成,box中可以套box,所有的数据都保存在box中。每个文件主要由以下部分构成:
- File Type Box:ftyp,文件类型
- Media Data Box:mdat,实际媒体数据
- Free Space Box:free或skip,无关紧要的内容,可忽略
- Meta Box:meta,包含描述或注释信息
- Movie Box:moov,实际媒体数据的metadata
- Movie Header Box:mvhd,文件总体信息,如时长和创建时间等
- Track Box:trak,一个视频或音频序列
- Track Header Box:tkhd,track的特性和总体信息,如时长和宽高等
- Media Box:mdia,包含整个track的媒体信息,比如媒体类型和sample信息
- Media Header Box:mdhd,包含track总体信息,内容和track header大致一样
- Handler Reference Box:hdlr,解释媒体的播放过程信息
- Media Information Box:minf,包含所有描述该track的媒体信息的对象,信息存储在子box中
- Video Media Header Box / Sound Media Header Box:vmhd/smhd,包含当前track的视频/音频描述信息
- Data Infomation Box:dinf,解释如何定位媒体信息
- Data Reference Box:dref,用来设置当前box描述信息的data entry
- Data Entry Url Box:url,用来定位track数据
- Data Entry Urn Box:urn,用来定位track数据
- Data Reference Box:dref,用来设置当前box描述信息的data entry
- Sample Table Box:stbl,sample时序和物理布局表
- Sample Description Box:stsd,定义和描述轨中的采样格式结构
- Time To Sample Box:stts,存储sample的duration,描述sample时序的映射方法
- Sync Sample Box:stss,确定media中的关键帧
- Sample Size Box:stsz或stz2,定义每个sample的大小
- Sample To Chunk Box:stsc,描述sample与chunk的映射关系
- Chunk Offset Box:stco(32位)或co64(64位),定义每个chunk在媒体流中的位置
- Composition Time To Sample Box:ctts,解码时间和合成时间的偏移量
C#实现
本文使用c#解析MP4文件,后面将展示部分box的结构,逐步补充。
Mp4File
public class Mp4File
{
private string filePath;
private string fileName;
/// <summary>
/// File Type
/// </summary>
public FileTypeBox Ftyp = new FileTypeBox();
/// <summary>
/// Media Data
/// </summary>
public List<MediaDataBox> Mdats = new List<MediaDataBox>();
/// <summary>
/// Movie
/// </summary>
public MovieBox Moov = new MovieBox();
/// <summary>
/// Free
/// </summary>
public FreeSpaceBox Free;
/// <summary>
/// Meta
/// </summary>
public MetaBox Meta;
/// <summary>
/// 其他
/// </summary>
public List<Box> Boxs = new List<Box>();
public bool Open(string file)
{
this.filePath = file;
this.fileName = Path.GetFileNameWithoutExtension(fileName);
FileStream fs = new FileStream(this.filePath, FileMode.Open, FileAccess.Read);
BinaryReader br = new BinaryReader(fs);
br.BaseStream.Seek(0, SeekOrigin.Begin);
while (br.PeekChar() > -1)
{
Box box = new Box();
box.ReadHeader(br);
switch (box.Type)
{
case "ftyp":
Ftyp.Copy(box);
Ftyp.ReadContent(br);
break;
case "mdat":
MediaDataBox mdat = new MediaDataBox();
mdat.Copy(box);
mdat.ReadContent(br);
Mdats.Add(mdat);
break;
case "free":
case "skip":
Free = new FreeSpaceBox();
Free.Copy(box);
Free.ReadContent(br);
break;
case "meta":
Meta = new MetaBox();
Meta.Copy(box);
Meta.ReadFullHeader(br);
Meta.ReadContent(br);
break;
case "moov":
Moov.Copy(box);
Moov.ReadContent(br);
break;
default:
box.ReadContent(br);
Boxs.Add(box);
break;
}
}
br.Close();
fs.Close();
return true;
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(Ftyp.ToString());
for (int i = 0; i < Mdats.Count; i++)
{
str.Append(Mdats[i].ToString());
}
if (Free != null)
{
str.Append(Free.ToString());
}
if (Meta != null)
{
str.Append(Meta.ToString());
}
str.Append(Moov.ToString());
// 其他
for (int i = 0; i < Boxs.Count; i++)
{
str.Append(Boxs[i].ToString());
}
return str.ToString();
}
}
Box
box以box header开头,header中包含box的大小和类型。box中的字节序为网络字节序,也就是大端字节序(Big-Endian).
/// <summary>
/// 由唯一类型标识符和长度定义的面向对象的构建;
/// box中的字节序为网络字节序,也就是大端字节序(Big-Endian);
/// </summary>
public class Box
{
/// <summary>
/// box前四个字节,uint32类型,标识整个box所占用的大小,包括header部分;
/// 如果box很大(例如存放具体视频数据的mdat box),超过了uint32的最大数值,size就被设置为1,并用type后面的8位uint64的largesize来存放大小;
/// </summary>
public ulong Size;
/// <summary>
/// box类型,uint类型,占四个字节
/// </summary>
public string Type;
/// <summary>
/// 若Type为uuid,则header中包含该字段,类型为unsigned int(8)[16],占16个字节
/// </summary>
public string UserType;
/// <summary>
/// box header长度
/// </summary>
protected int headerLength;
/// <summary>
/// 设置父级路径
/// </summary>
protected string parentPath = string.Empty;
/// <summary>
/// 设置父级路径
/// </summary>
/// <returns></returns>
public string SetParentPath(string value)
{
return parentPath = value;
}
/// <summary>
/// 获取路径
/// </summary>
/// <returns></returns>
public string GetPath()
{
return Path.Combine(parentPath, this.GetType().Name);
}
/// <summary>
/// 读取box header,获取大小和类型
/// </summary>
/// <param name="br"></param>
/// <returns>box header的长度</returns>
public virtual void ReadHeader(BinaryReader br)
{
Size = GetUint32(br);
Type = GetString(br, 4).Trim();
if (Size == 1)
{
Size = GetUint64(br);
headerLength = 16;
}
else
{
headerLength = 8;
}
if (Type.Equals("uuid"))
{
UserType = GetString(br, 16).Trim();
headerLength += 16;
}
}
/// <summary>
/// 根据长度跳过box内容
/// </summary>
/// <param name="br"></param>
public virtual void ReadContent(BinaryReader br)
{
ulong contentLength = Size - (ulong)headerLength;
ulong i = 0;
while (i < contentLength)
{
try
{
br.ReadByte();
}
catch (Exception e)
{
break;
}
i++;
}
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.AppendLine(GetPath());
if (Size > uint.MaxValue)
{
str.AppendLine(" Size : " + 1);
str.AppendLine(" Type : " + Type);
str.AppendLine(" Largesize : " + Size);
}
else
{
str.AppendLine(" Size : " + Size);
str.AppendLine(" Type : " + Type);
}
//str.AppendLine(" ContentLength : " + (Size - (ulong)headerLength));
return str.ToString();
}
public virtual void Copy(Box box)
{
Size = box.Size;
Type = box.Type;
UserType = box.UserType;
headerLength = box.headerLength;
parentPath = box.parentPath;
}
public static short GetInt16(BinaryReader br)
{
byte[] bytes = br.ReadBytes(2);
Array.Reverse(bytes);
return BitConverter.ToInt16(bytes, 0);
}
public static ushort GetUint16(BinaryReader br)
{
byte[] bytes = br.ReadBytes(2);
Array.Reverse(bytes);
return BitConverter.ToUInt16(bytes, 0);
}
public static int GetInt32(BinaryReader br)
{
byte[] bytes = br.ReadBytes(4);
Array.Reverse(bytes);
return BitConverter.ToInt32(bytes, 0);
}
public static uint GetUint32(BinaryReader br)
{
byte[] bytes = br.ReadBytes(4);
Array.Reverse(bytes);
return BitConverter.ToUInt32(bytes, 0);
}
public static ulong GetUint64(BinaryReader br)
{
byte[] bytes = br.ReadBytes(8);
Array.Reverse(bytes);
return BitConverter.ToUInt64(bytes, 0);
}
public static int[] GetInt32Array(BinaryReader br, int length)
{
int[] array = new int[length];
for (int i = 0; i < length; i++)
{
array[i] = GetInt32(br);
}
return array;
}
public static uint[] GetUint32Array(BinaryReader br, int length)
{
uint[] array = new uint[length];
for (int i = 0; i < length; i++)
{
array[i] = GetUint32(br);
}
return array;
}
public static ushort[] GetUint16Array(BinaryReader br, int length)
{
ushort[] array = new ushort[length];
for (int i = 0; i < length; i++)
{
array[i] = GetUint16(br);
}
return array;
}
public static string GetString(BinaryReader br, int length)
{
byte[] bytes = br.ReadBytes(length);
return Encoding.ASCII.GetString(bytes).Trim('\0');
}
public static byte GetByte(BitArray bitArray, int startIndex, int length)
{
byte data = 0;
for (int i = 0; i < length; i++)
{
if (bitArray[startIndex + i])
{
data |= (byte)(1 << 8 - (startIndex + i));
}
}
return data;
}
}
Full Box
full box的header中除了size、type外,还包含version和flags字段。
/// <summary>
/// box header相对box多了version和flags字段
/// </summary>
public class FullBox : Box
{
/// <summary>
/// 用来指定该box的文件的格式
/// full box的特有字段
/// </summary>
public byte Version;
/// <summary>
/// 标志图
/// full box的特有字段
/// </summary>
public byte[] Flags = new byte[3];
public override void ReadHeader(BinaryReader br)
{
base.ReadHeader(br);
ReadFullHeader(br);
}
/// <summary>
/// 读取fullbox相对box额外的信息
/// </summary>
/// <param name="br"></param>
public void ReadFullHeader(BinaryReader br)
{
Version = br.ReadByte();
Flags = br.ReadBytes(3);
headerLength += 4;
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
str.AppendLine(" Version : " + Version);
str.AppendLine(" Flags : " + BitConverter.ToString(Flags));
return str.ToString();
}
public virtual void Copy(FullBox box)
{
base.Copy(box);
Version = box.Version;
Flags = box.Flags;
}
}
File Type Box
文件类型,包含在文件中,有且仅有一个。
/// <summary>
/// 文件类型,占24个字节,type域为ftyp
/// </summary>
public class FileTypeBox : Box
{
/// <summary>
/// 文件类型标识符,例如mp42,占四个字节(int)
/// </summary>
public string MajorBrand;
/// <summary>
/// major brand的次版本标识,占四个字节(int)
/// </summary>
public string MinorVersion;
/// <summary>
/// 兼容类型,占八个字节
/// </summary>
public string CompatibleBrands;
public override void ReadContent(BinaryReader br)
{
MajorBrand = GetString(br, 4);
MinorVersion = GetString(br, 4);
CompatibleBrands = GetString(br, 8);
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
str.AppendLine(" MajorBrand : " + MajorBrand);
str.AppendLine(" MinorVersion : " + MinorVersion);
str.AppendLine(" CompatibleBrands : " + CompatibleBrands);
return str.ToString();
}
}
Media Data Box
在视频track中,包含多个视频帧。一个文件可能包含0个或多个Medai Data Box。实际媒体数据紧跟在type字段后。
/// <summary>
/// 用来容纳实体数据的box,type域为mdat;
/// 该box包含于文件层,可以有多个,也可以没有(当媒体数据全部为外部文件引用时),用来存储媒体数据。
/// 数据直接跟在box type字段后面,它的结构是由metadata来描述的,metadata通过文件中的绝对偏移来引用媒体数据。
/// </summary>
public class MediaDataBox : Box
{
/// <summary>
/// 实体数据
/// </summary>
public byte[] Datas;
public override void ReadContent(BinaryReader br)
{
ulong contentLength = Size - (ulong)headerLength;
Datas = new byte[contentLength];
ulong i = 0;
while (i < contentLength)
{
try
{
Datas[i] = br.ReadByte();
}
catch (Exception e)
{
break;
}
i++;
}
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
str.AppendLine(" ContentLength : " + Datas.LongLength);
return str.ToString();
}
}
Free Space Box
0个或多个,可包含在文件或其他box中。
/// <summary>
/// free space box中的内容是无关紧要的,可以被忽略。
/// 该box被删除后,不会对播放产生任何影响,它的type域可以是free或skip
/// </summary>
public class FreeSpaceBox : Box
{
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
str.AppendLine(" ContentLength : " + (Size - (ulong)headerLength));
return str.ToString();
}
}
Meta Box
可包含在文件中,也可包含在Movie Box、Track Box、Additional Metadata Container Box、Movie Fragment Box、Track Fragment Box。在文件、Movie Box、Track Box中有0个或1个,在Additional Metadata Container Box中有一个或多个。
/// <summary>
/// type域为meta,包含描述或注释信息
/// </summary>
public class MetaBox : FullBox
{
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
str.AppendLine(" ContentLength : " + (Size - (ulong)headerLength));
return str.ToString();
}
}
Movie Box
包含在文件中,有且仅有一个。包含多个box。
/// <summary>
/// 该box用来存放媒体的metadata信息,其内容信息由子box诠释,type域为moov;
/// 该box有且只有一个并且包含在文件层,一般情况下moov box会紧随ftyp box出现,但也有放在文件末尾的。
/// </summary>
public class MovieBox : Box
{
/// <summary>
/// 用来存放文件的总体信息,如时长和创建时间等,它是独立于媒体的并且与整个播放相关
/// </summary>
public MovieHeaderBox MovieHeader = new MovieHeaderBox();
/// <summary>
/// 注释或描述信息
/// </summary>
public MetaBox Meta = null;
/// <summary>
/// track box序列
/// </summary>
public List<TrackBox> Tracks = new List<TrackBox>();
/// <summary>
/// 其他
/// </summary>
public List<Box> Boxs = new List<Box>();
public override void ReadContent(BinaryReader br)
{
ulong i = (ulong)headerLength;
while (i < Size)
{
Box box = new Box();
box.SetParentPath(GetPath());
box.ReadHeader(br);
switch (box.Type)
{
case "mvhd":
MovieHeader.Copy(box);
MovieHeader.ReadFullHeader(br);
MovieHeader.ReadContent(br);
break;
case "meta":
Meta = new MetaBox();
Meta.Copy(box);
Meta.ReadFullHeader(br);
Meta.ReadContent(br);
break;
case "trak":
TrackBox track = new TrackBox();
track.Copy(box);
track.ReadContent(br);
Tracks.Add(track);
break;
default:
box.ReadContent(br);
Boxs.Add(box);
break;
}
i += box.Size;
}
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
str.Append(MovieHeader.ToString());
if (Meta != null)
{
str.Append(Meta.ToString());
}
for (int i = 0; i < Tracks.Count; i++)
{
str.Append(Tracks[i].ToString());
}
for (int i = 0; i < Boxs.Count; i++)
{
str.Append(Boxs[i].ToString());
}
return str.ToString();
}
}
Movie Header Box
包含在Movie Box中,有且仅有一个。
/// <summary>
/// 用来存放文件的总体信息,如时长和创建时间等,它是独立于媒体的并且与整个播放相关,type域为mvhd;
/// </summary>
public class MovieHeaderBox : FullBox
{
/// <summary>
/// 创建时间,表示相对于UTC时间1904-01-01零点的秒数;
/// 若fullbox中的version为0占四个字节,若version为1占8个字节;
/// </summary>
public DateTime CreateTime;
/// <summary>
/// 最后修改时间,表示相对于UTC时间1904-01-01零点的秒数;
/// 若fullbox中的version为0占四个字节,若version为1占8个字节;
/// </summary>
public DateTime ModificationTime;
/// <summary>
/// 文件媒体在1秒时间内的刻度值,即1秒长度的时间单元数,占四个字节
/// </summary>
public uint TimeScale;
/// <summary>
/// 该track的时间长度;
/// 若fullbox中的version为0占四个字节,若version为1占8个字节;
/// 用duration和time scale可以计算track时长;
/// 比如audio track的time scale = 8000, duration = 560128,时长为70.016秒;
/// video track的time scale = 600, duration = 42000,时长为70秒;
/// </summary>
public ulong Duration;
/// <summary>
/// 视频时长(秒)=Duration/TimeScale,不占字节,是计算出来的
/// </summary>
public float TimeLength;
/// <summary>
/// 推荐播放速率,占四个字节;
/// 高16位和低16位分别为小数点整数部分和小数部分;
/// 该值为1.0(0x00010000)表示正常前向播放
/// </summary>
public float Rate;
/// <summary>
/// 推荐音量,占两个字节;
/// 高8位和低8位分别为小数点整数部分和小数部分;
/// 1.0(0x0100)表示最大音量
/// </summary>
public float Volume;
/// <summary>
/// 保留字节,占10个字节:bit(16)+int(32)[2]
/// </summary>
public byte[] Reserved;
/// <summary>
/// 视频变换矩阵,3*3,占36个字节
/// </summary>
public int[] Matrix;
/// <summary>
/// 预定义,占24个字节:bit(32)[6]
/// </summary>
public byte[] PreDefined;
/// <summary>
/// 下一个track使用的id号,占4个字节
/// </summary>
public uint NextTrackId;
public override void ReadContent(BinaryReader br)
{
if (Version == 1)
{
ulong seconds = GetUint64(br);
CreateTime = new DateTime(1904, 1, 1).AddSeconds(seconds);
seconds = GetUint64(br);
ModificationTime = new DateTime(1904, 1, 1).AddSeconds(seconds);
TimeScale = GetUint32(br);
Duration = GetUint64(br);
}
else
{
uint seconds = GetUint32(br);
CreateTime = new DateTime(1904, 1, 1).AddSeconds(seconds).ToLocalTime();
seconds = GetUint32(br);
ModificationTime = new DateTime(1904, 1, 1).AddSeconds(seconds).ToLocalTime();
TimeScale = GetUint32(br);
Duration = GetUint32(br);
}
TimeLength = (float)Duration / TimeScale;
Rate = GetUint16(br) + GetUint16(br) / 100.0f;
Volume = br.ReadByte() + br.ReadByte() / 10.0f;
Reserved = br.ReadBytes(10);
Matrix = GetInt32Array(br, 9);
PreDefined = br.ReadBytes(24);
NextTrackId = GetUint32(br);
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
str.AppendLine(" CreateTime : " + CreateTime.ToString());
str.AppendLine(" ModificationTime : " + ModificationTime.ToString());
str.AppendLine(" TimeScale : " + TimeScale);
str.AppendLine(" Duration : " + Duration);
str.AppendLine(" TimeLength : " + TimeLength + " s");
str.AppendLine(" Rate : " + Rate);
str.AppendLine(" Volume : " + Volume);
str.AppendLine(" Reserved : " + BitConverter.ToString(Reserved));
str.AppendLine(" Matrix : " + string.Join(",", Matrix));
str.AppendLine(" PreDefined : " + BitConverter.ToString(PreDefined));
str.AppendLine(" NextTrackId : " + NextTrackId);
return str.ToString();
}
}
Track Box
包含在Movie Box中,有一个或多个。
/// <summary>
/// 按时间排序的相关的采样,对于媒体数据来说,track表示一个视频或音频序列,type域为trak;
/// tack box是一个container box,其子box包含了该track的媒体数据引用和描述(hint track除外)。
/// 一个MP4文件中的媒体可以包含多个track,且至少有一个track,这些track之间彼此独立,有自己的时间和空间信息。
/// trak box必须包含一个tkhd box和一个mdia box
/// </summary>
public class TrackBox : Box
{
/// <summary>
/// 该track的特性和总体信息,如时长、宽高等
/// </summary>
public TrackHeaderBox TrackHeader = new TrackHeaderBox();
/// <summary>
/// 注释或描述信息
/// </summary>
public MetaBox Meta = null;
/// <summary>
///
/// </summary>
public MediaBox Media = new MediaBox();
/// <summary>
/// 其他
/// </summary>
public List<Box> Boxs = new List<Box>();
public override void ReadContent(BinaryReader br)
{
ulong i = (ulong)headerLength;
while (i < Size)
{
Box box = new Box();
box.SetParentPath(GetPath());
box.ReadHeader(br);
switch (box.Type)
{
case "tkhd":
TrackHeader.Copy(box);
TrackHeader.ReadFullHeader(br);
TrackHeader.ReadContent(br);
break;
case "meta":
Meta = new MetaBox();
Meta.Copy(box);
Meta.ReadFullHeader(br);
Meta.ReadContent(br);
break;
case "mdia":
Media.Copy(box);
Media.ReadContent(br);
break;
default:
box.ReadContent(br);
Boxs.Add(box);
break;
}
i += box.Size;
}
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
str.Append(TrackHeader.ToString());
if (Meta != null)
{
str.Append(Meta.ToString());
}
str.Append(Media.ToString());
for (int i = 0; i < Boxs.Count; i++)
{
str.Append(Boxs[i].ToString());
}
return str.ToString();
}
}
Track Header Box
包含在Track Box中,有且仅有一个。
/// <summary>
/// 该track的特性和总体信息,如时长、宽高等,type域为tkhd;
/// </summary>
public class TrackHeaderBox : FullBox
{
/// <summary>
/// 创建时间,表示相对于UTC时间1904-01-01零点的秒数;
/// 若fullbox中的version为0占四个字节,若version为1占8个字节;
/// </summary>
public DateTime CreateTime;
/// <summary>
/// 最后修改时间,表示相对于UTC时间1904-01-01零点的秒数;
/// 若fullbox中的version为0占四个字节,若version为1占8个字节;
/// </summary>
public DateTime ModificationTime;
/// <summary>
/// track的id号,不能重复且不能为0,占四个字节
/// </summary>
public uint TrackID;
/// <summary>
/// 保留位,占四个字节
/// </summary>
public uint Reserved1;
/// <summary>
/// 该track的时长;
/// 若fullbox中的version为0占四个字节,若version为1占8个字节;
/// </summary>
public ulong Duration;
/// <summary>
/// 保留位,占8个字节
/// </summary>
public uint[] Reserved2;
/// <summary>
/// 指定视频层,默认为0,值小的在上层,占两个字节;
/// </summary>
public short Layer;
/// <summary>
/// 指定track分组信息,默认为0表示该track未与其他track有群组关系,占两个字节;
/// </summary>
public short AlternateGroup;
/// <summary>
/// 音量,占两个字节;
/// 高8位和低8位分别为小数点整数部分和小数部分;
/// 如果为音频track,1.0(0x0100)表示最大音量;否则为0;
/// </summary>
public float Volume;
/// <summary>
/// 保留位,占两个字节
/// </summary>
public ushort Reserved3;
/// <summary>
/// 视频变换矩阵,3*3,占36个字节
/// </summary>
public int[] Matrix;
/// <summary>
/// 宽,与sample描述中的实际画面大小比值,用于播放时的展示宽高,占四个字节;
/// 高16位和低16位分别为小数点整数部分和小数部分;
/// </summary>
public float Width;
/// <summary>
/// 高,与sample描述中的实际画面大小比值,用于播放时的展示宽高,占四个字节;
/// 高16位和低16位分别为小数点整数部分和小数部分;
/// </summary>
public float Height;
public override void ReadContent(BinaryReader br)
{
if (Version == 1)
{
ulong seconds = GetUint64(br);
CreateTime = new DateTime(1904, 1, 1).AddSeconds(seconds);
seconds = GetUint64(br);
ModificationTime = new DateTime(1904, 1, 1).AddSeconds(seconds);
TrackID = GetUint32(br);
Reserved1 = GetUint32(br);
Duration = GetUint64(br);
}
else
{
uint seconds = GetUint32(br);
CreateTime = new DateTime(1904, 1, 1).AddSeconds(seconds).ToLocalTime();
seconds = GetUint32(br);
ModificationTime = new DateTime(1904, 1, 1).AddSeconds(seconds).ToLocalTime();
TrackID = GetUint32(br);
Reserved1 = GetUint32(br);
Duration = GetUint32(br);
}
Reserved2 = GetUint32Array(br, 2);
Layer = GetInt16(br);
AlternateGroup = GetInt16(br);
Volume = br.ReadByte() + br.ReadByte() / 10.0f;
Reserved3 = GetUint16(br);
Matrix = GetInt32Array(br, 9);
Width = GetUint16(br) + GetUint16(br) / 100.0f;
Height = GetUint16(br) + GetUint16(br) / 100.0f;
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
str.AppendLine(" CreateTime : " + CreateTime.ToString());
str.AppendLine(" ModificationTime : " + ModificationTime.ToString());
str.AppendLine(" TrackID : " + TrackID);
str.AppendLine(" Reserved : " + Reserved1);
str.AppendLine(" Duration : " + Duration);
str.AppendLine(" Reserved : " + string.Join(",", Reserved2));
str.AppendLine(" Layer : " + Layer);
str.AppendLine(" AlternateGroup : " + AlternateGroup);
str.AppendLine(" Volume : " + Volume);
str.AppendLine(" Reserved : " + Reserved3);
str.AppendLine(" Matrix : " + string.Join(",", Matrix));
str.AppendLine(" Width : " + Width);
str.AppendLine(" Height : " + Height);
return str.ToString();
}
}
Media Box
包含在Track Box中,有且仅有一个。
/// <summary>
/// 包含整个track的媒体信息,比如媒体类型和sample信息,type域为mdia
/// </summary>
public class MediaBox : Box
{
/// <summary>
/// 包含该track的总体信息,针对media来设置,一般与track header一致
/// </summary>
public MediaHeaderBox MediaHeader = new MediaHeaderBox();
/// <summary>
/// 解释媒体的播放过程信息
/// </summary>
public HandlerReferenceBox Handler = new HandlerReferenceBox();
/// <summary>
/// 包含所有描述该track中的媒体信息的对象
/// </summary>
public MediaInformationBox MediaInformation = new MediaInformationBox();
/// <summary>
/// 其他
/// </summary>
public List<Box> Boxs = new List<Box>();
public override void ReadContent(BinaryReader br)
{
ulong i = (ulong)headerLength;
while (i < Size)
{
Box box = new Box();
box.SetParentPath(GetPath());
box.ReadHeader(br);
switch (box.Type)
{
case "mdhd":
MediaHeader.Copy(box);
MediaHeader.ReadFullHeader(br);
MediaHeader.ReadContent(br);
break;
case "hdlr":
Handler.Copy(box);
Handler.ReadFullHeader(br);
Handler.ReadContent(br);
break;
case "minf":
MediaInformation.Copy(box);
MediaInformation.ReadContent(br);
break;
default:
box.ReadContent(br);
Boxs.Add(box);
break;
}
i += box.Size;
}
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
str.Append(MediaHeader.ToString());
str.Append(Handler.ToString());
str.Append(MediaInformation.ToString());
for (int i = 0; i < Boxs.Count; i++)
{
str.Append(Boxs[i].ToString());
}
return str.ToString();
}
}
Media Header Box
包含在Media Box中,有且仅有一个。
/// <summary>
/// 包含该track的总体信息,内容和track header的内容大致一样,type域为mdhd;
/// track header通常是对指定的track设定相关属性和内容。
/// media header是针对独立的media来设置,一般情况下二者相同。
/// </summary>
public class MediaHeaderBox : FullBox
{
/// <summary>
/// 创建时间,表示相对于UTC时间1904-01-01零点的秒数;
/// 若fullbox中的version为0占四个字节,若version为1占8个字节;
/// </summary>
public DateTime CreateTime;
/// <summary>
/// 最后修改时间,表示相对于UTC时间1904-01-01零点的秒数;
/// 若fullbox中的version为0占四个字节,若version为1占8个字节;
/// </summary>
public DateTime ModificationTime;
/// <summary>
/// 文件媒体在1秒时间内的刻度值,即1秒长度的时间单元数,占四个字节
/// </summary>
public uint TimeScale;
/// <summary>
/// 该track的时间长度;
/// 若fullbox中的version为0占四个字节,若version为1占8个字节;
/// 用duration和time scale可以计算track时长;
/// 比如audio track的time scale = 8000, duration = 560128,时长为70.016秒;
/// video track的time scale = 600, duration = 42000,时长为70秒;
/// </summary>
public ulong Duration;
/// <summary>
/// 媒体语言码,占2个字节;
/// 最高位为0,后面15位为3个字符(见ISO 639-2/T标准中定义)
/// </summary>
public string Language;
/// <summary>
/// 预留位,占2个字节
/// </summary>
public ushort PreDefined;
public override void ReadContent(BinaryReader br)
{
if (Version == 1)
{
ulong seconds = GetUint64(br);
CreateTime = new DateTime(1904, 1, 1).AddSeconds(seconds);
seconds = GetUint64(br);
ModificationTime = new DateTime(1904, 1, 1).AddSeconds(seconds);
TimeScale = GetUint32(br);
Duration = GetUint64(br);
}
else
{
uint seconds = GetUint32(br);
CreateTime = new DateTime(1904, 1, 1).AddSeconds(seconds).ToLocalTime();
seconds = GetUint32(br);
ModificationTime = new DateTime(1904, 1, 1).AddSeconds(seconds).ToLocalTime();
TimeScale = GetUint32(br);
Duration = GetUint32(br);
}
byte[] arr = br.ReadBytes(2);
BitArray bitArray = new BitArray(arr);
// 后面15位为3个字符
//Language = ("" + (char)GetByte(bitArray, 1, 5) + (char)GetByte(bitArray, 6, 5) + (char)GetByte(bitArray, 11, 5)).Trim('\0');
// 以二进制输出
Language = Convert.ToString(arr[0], 2).PadLeft(8, '0') + " " + Convert.ToString(arr[1], 2).PadLeft(8, '0');
PreDefined = GetUint16(br);
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
str.AppendLine(" CreateTime : " + CreateTime.ToString());
str.AppendLine(" ModificationTime : " + ModificationTime.ToString());
str.AppendLine(" TimeScale : " + TimeScale);
str.AppendLine(" Duration : " + Duration);
str.AppendLine(" Language : " + Language);
str.AppendLine(" PreDefined : " + PreDefined);
return str.ToString();
}
}
Handler Reference Box
包含在Media Box或Meta Box中,有且仅有一个。
/// <summary>
/// 解释媒体的播放过程信息,该box也可被包含在meta box中,type域为hdlr
/// </summary>
public class HandlerReferenceBox : FullBox
{
/// <summary>
/// 预定义,占四个字节
/// </summary>
public uint PreDefined;
/// <summary>
/// 在media box中,该值为4个字符,占四个字节;
/// vide:video track
/// soun:audio track
/// hint:hint track
/// </summary>
public string HandlerType;
/// <summary>
/// 预留位,uint[3],占12个字节
/// </summary>
public uint[] Reserved;
/// <summary>
/// human‐readable name for the track type
/// </summary>
public string Name;
public override void ReadContent(BinaryReader br)
{
PreDefined = GetUint32(br);
HandlerType = GetString(br, 4);
Reserved = GetUint32Array(br, 3);
Name = GetString(br, (int)Size - headerLength - 20);
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
str.AppendLine(" PreDefined : " + PreDefined);
str.AppendLine(" HandlerType : " + HandlerType);
str.AppendLine(" Reserved : " + string.Join(",", Reserved));
str.AppendLine(" Name : " + Name);
return str.ToString();
}
}
Media Information Box
包含在Media Box中,有且仅有一个。
/// <summary>
/// 包含了所有描述该track中的媒体信息的对象,信息存储在其子box中,type域为minf;
/// </summary>
public class MediaInformationBox : Box
{
/// <summary>
/// 用在视频track中,包含当前track的视频描述信息(如视频编码等信息)
/// </summary>
public VideoMediaHeaderBox VideoMediaHeader;
/// <summary>
/// 用在音频track中,包含当前track的音频描述信息(如编码格式等信息)
/// </summary>
public SoundMediaHeaderBox SoundMediaHeader;
/// <summary>
///
/// </summary>
public DataInformationBox DataInformation = new DataInformationBox();
/// <summary>
///
/// </summary>
public SampleTableBox SampleTable = new SampleTableBox();
/// <summary>
/// 其他
/// </summary>
public List<Box> Boxs = new List<Box>();
public override void ReadContent(BinaryReader br)
{
ulong i = (ulong)headerLength;
while (i < Size)
{
Box box = new Box();
box.SetParentPath(GetPath());
box.ReadHeader(br);
switch (box.Type)
{
case "vmhd":
VideoMediaHeader = new VideoMediaHeaderBox();
VideoMediaHeader.Copy(box);
VideoMediaHeader.ReadFullHeader(br);
VideoMediaHeader.ReadContent(br);
break;
case "smhd":
SoundMediaHeader = new SoundMediaHeaderBox();
SoundMediaHeader.Copy(box);
SoundMediaHeader.ReadFullHeader(br);
SoundMediaHeader.ReadContent(br);
break;
case "dinf":
DataInformation.Copy(box);
DataInformation.ReadContent(br);
break;
case "stbl":
SampleTable.Copy(box);
SampleTable.ReadContent(br);
break;
default:
box.ReadContent(br);
Boxs.Add(box);
break;
}
i += box.Size;
}
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
if (VideoMediaHeader != null)
{
str.Append(VideoMediaHeader.ToString());
}
if (SoundMediaHeader != null)
{
str.Append(SoundMediaHeader.ToString());
}
str.Append(DataInformation.ToString());
str.Append(SampleTable.ToString());
for (int i = 0; i < Boxs.Count; i++)
{
str.Append(Boxs[i].ToString());
}
return str.ToString();
}
}
Video Media Header Box
包含在视频track的Media Information Box中,有且仅有一个。
/// <summary>
/// 用在视频track中,包含当前track的视频描述信息(如视频编码等信息),type域为vmhd
/// </summary>
public class VideoMediaHeaderBox : FullBox
{
/// <summary>
/// 视频合成模式,为0时为拷贝原始图像,否则与opcolor进行合成,占两个字节
/// </summary>
public ushort GraphicsMode;
/// <summary>
/// {red, green, blue},占6个字节
/// </summary>
public ushort[] OpColor;
public override void ReadContent(BinaryReader br)
{
GraphicsMode = GetUint16(br);
OpColor = GetUint16Array(br, 3);
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
str.AppendLine(" GraphicsMode : " + GraphicsMode);
str.AppendLine(" OpColor : " + string.Join(",", OpColor));
return str.ToString();
}
}
Sound Media Header Box
包含在音频track的Media Information Box中,有且仅有一个。
/// <summary>
/// 用在音频track中,包含当前track的音频描述信息(如编码格式等信息),type域为smhd
/// </summary>
public class SoundMediaHeaderBox : FullBox
{
/// <summary>
/// 立体声平衡,占2个字节,高8位和低8位分别为小数点整数部分和小数部分;
/// 一般为0,-1.0表示全部左声道,1.0表示全部右声道;
/// </summary>
public float Balance;
/// <summary>
/// 保留位,占2个字节
/// </summary>
public ushort Reserved;
public override void ReadContent(BinaryReader br)
{
Balance = br.ReadByte() + br.ReadByte() / 10.0f;
Reserved = GetUint16(br);
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
str.AppendLine(" Balance : " + Balance);
str.AppendLine(" Reserved : " + Reserved);
return str.ToString();
}
}
Data Information Box
包含在Media Information Box或Meta Box中,在Media Information Box中是必须的,有且仅有一个,在Meta Box中是可选的。
/// <summary>
/// 解释如何定位媒体信息,是一个container box。type域为dinf;
/// 一般包含一个dref box,即data reference box。
/// </summary>
public class DataInformationBox : Box
{
/// <summary>
/// 用来设置当前box描述信息的data_entry
/// </summary>
public DataReferenceBox DataReference = new DataReferenceBox();
/// <summary>
/// 其他
/// </summary>
public List<Box> Boxs = new List<Box>();
public override void ReadContent(BinaryReader br)
{
ulong i = (ulong)headerLength;
while (i < Size)
{
Box box = new Box();
box.SetParentPath(GetPath());
box.ReadHeader(br);
if (box.Type.Equals("dref"))
{
DataReference.Copy(box);
DataReference.ReadFullHeader(br);
DataReference.ReadContent(br);
}
else
{
box.ReadContent(br);
Boxs.Add(box);
}
i += box.Size;
}
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
str.Append(DataReference.ToString());
for (int i = 0; i < Boxs.Count; i++)
{
str.Append(Boxs[i].ToString());
}
return str.ToString();
}
}
Data Reference Box
包含在Data Information Box中,有且仅有一个。
/// <summary>
/// 用来设置当前Box描述信息的data_entry,type域为dref;
/// 包含若干个url或urn,这些box组成一个表,用来定位track数据。
/// 简单的说,track可以被分为若干段,每一段都可以根据url或urn指向的地址来获取数据,
/// sample描述中会用这些片段的序号将这些片段组成一个完成的track。
/// 一般情况下,当数据被完全包含在文件中时,url或urn中的定位字符串是空的。
/// </summary>
public class DataReferenceBox : FullBox
{
/// <summary>
/// entry个数
/// </summary>
public uint EntryCount;
public List<DataEntryUrlBox> DataEntryUrls = new List<DataEntryUrlBox>();
public List<DataEntryUrnBox> DataEntryUrns = new List<DataEntryUrnBox>();
public override void ReadContent(BinaryReader br)
{
EntryCount = GetUint32(br);
ulong i = (ulong)headerLength + 4;
while (i < Size)
{
FullBox box = new FullBox();
box.SetParentPath(GetPath());
box.ReadHeader(br);
if (box.Type.Equals("url"))
{
DataEntryUrlBox dataUrl = new DataEntryUrlBox();
dataUrl.Copy(box);
dataUrl.ReadContent(br);
DataEntryUrls.Add(dataUrl);
}
else if (box.Type.Equals("urn"))
{
DataEntryUrnBox dataUrn = new DataEntryUrnBox();
dataUrn.Copy(box);
dataUrn.ReadContent(br);
DataEntryUrns.Add(dataUrn);
}
else
{
box.ReadContent(br);
}
i += box.Size;
}
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
str.AppendLine(" EntryCount : " + EntryCount);
for (int i = 0; i < DataEntryUrls.Count; i++)
{
str.Append(DataEntryUrls[i].ToString());
}
for (int i = 0; i < DataEntryUrns.Count; i++)
{
str.Append(DataEntryUrns[i].ToString());
}
return str.ToString();
}
}
Data Entry Url Box
包含在Data Reference Box中,至少有一个。
/// <summary>
/// data reference box下会包含若干个ur或urn,这些box组成一个表,用来定位track数据。type域为url;
/// flags值不是固定的,但是有一个特殊的值,0x000001用来表示当前media的数据和moov包含的数据一致;
/// </summary>
public class DataEntryUrlBox : FullBox
{
/// <summary>
///
/// </summary>
public string Location = string.Empty;
public override void ReadContent(BinaryReader br)
{
int length = (int)Size - headerLength;
if (length > 0)
{
Location = GetString(br, length);
}
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
str.AppendLine(" Location : " + Location);
return str.ToString();
}
}
Data Entry Urn Box
包含在Data Reference Box中,至少有一个。
/// <summary>
/// data reference box下会包含若干个ur或urn,这些box组成一个表,用来定位track数据。type域为urn;
/// </summary>
public class DataEntryUrnBox : FullBox
{
/// <summary>
/// 必须
/// </summary>
public string Name = string.Empty;
/// <summary>
/// 可选
/// </summary>
public string Location = string.Empty;
public override void ReadContent(BinaryReader br)
{
int length = (int)Size - headerLength;
if (length > 0)
{
Name = GetString(br, length);
}
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
str.AppendLine(" Name : " + Name);
str.AppendLine(" Location : " + Location);
return str.ToString();
}
}
Sample Table Box
包含在Media Information Box中,有且仅有一个。
/// <summary>
/// 指明sample时序和物理布局的表,type域为stbl;
/// sample是媒体数据存储的单位,存储在media的chunk中,chunk和sample的长度均可互不相同;
/// stbl包含了关于track中sample所有时间和位置的信息,以及sample的编解码等信息。
/// 利用这个表,可以解释sample的时序、类型、大小以及在各自存储容器中的位置。
/// stbl是一个container box,其子box包括:sample description box(stsd)、time to sample box(stts)、
/// sample size box(stsz或stz2)、sample to chunk box(stsc)、chunk offset box(stco或co64)、
/// composition time to sample box(ctts)、sync sample box(stss)等;
/// </summary>
public class SampleTableBox : Box
{
/// <summary>
/// stsd必不可少,该box包含了data reference box进行sample数据检索的信息。
/// </summary>
public SampleDescriptionBox SampleDescription = new SampleDescriptionBox();
/// <summary>
/// 时间戳到sample序号的映射表
/// </summary>
public TimeToSampleBox TimeToSample = new TimeToSampleBox();
/// <summary>
/// 可选
/// </summary>
public CompositionOffsetBox CompositionOffset;
/// <summary>
/// 定义了每个sample的大小
/// </summary>
public SampleSizeBox SampleSize = new SampleSizeBox();
/// <summary>
/// 定义sample与chunk的映射关系
/// </summary>
public SampleToChunkBox SampleToChunk = new SampleToChunkBox();
/// <summary>
/// 描述哪一个sample是关键帧
/// </summary>
public SyncSampleBox SyncSample;
/// <summary>
/// 描述chunk在媒体流中的位置
/// </summary>
public ChunkOffsetBox ChunkOffset = new ChunkOffsetBox();
/// <summary>
/// 其他
/// </summary>
public List<Box> Boxs = new List<Box>();
public override void ReadContent(BinaryReader br)
{
ulong i = (ulong)headerLength;
while (i < Size)
{
Box box = new Box();
box.SetParentPath(GetPath());
box.ReadHeader(br);
switch (box.Type)
{
case "stsd":
SampleDescription.Copy(box);
SampleDescription.ReadFullHeader(br);
SampleDescription.ReadContent(br);
break;
case "stts":
TimeToSample.Copy(box);
TimeToSample.ReadFullHeader(br);
TimeToSample.ReadContent(br);
break;
case "stss":
SyncSample = new SyncSampleBox();
SyncSample.Copy(box);
SyncSample.ReadFullHeader(br);
SyncSample.ReadContent(br);
break;
case "stsz":
SampleSize.Copy(box);
SampleSize.ReadFullHeader(br);
SampleSize.ReadContent(br);
break;
case "stsc":
SampleToChunk.Copy(box);
SampleToChunk.ReadFullHeader(br);
SampleToChunk.ReadContent(br);
break;
case "stco":
case "co64":
ChunkOffset.Copy(box);
ChunkOffset.ReadFullHeader(br);
ChunkOffset.ReadContent(br);
break;
case "ctts":
CompositionOffset = new CompositionOffsetBox();
CompositionOffset.Copy(box);
CompositionOffset.ReadFullHeader(br);
CompositionOffset.ReadContent(br);
break;
default:
box.ReadContent(br);
Boxs.Add(box);
break;
}
i += box.Size;
}
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
str.Append(SampleDescription.ToString());
str.Append(TimeToSample.ToString());
if (CompositionOffset != null)
{
str.Append(CompositionOffset.ToString());
}
str.Append(SampleSize.ToString());
str.Append(SampleToChunk.ToString());
if (SyncSample != null)
{
str.Append(SyncSample.ToString());
}
str.Append(ChunkOffset.ToString());
for (int i = 0; i < Boxs.Count; i++)
{
str.Append(Boxs[i].ToString());
}
return str.ToString();
}
}
Sample Description Box
包含在Sample Table Box中,有且仅有一个。
/// <summary>
/// 定义和描述轨中的采样的格式的结构,type域为stsd;
/// stsd必不可少,且至少包含一个条目,该box包含了data reference box进行sample数据检索的信息。
/// 没有stsd就无法计算media sample的存储位置。
/// sdsd包含了编码的信息,其存储的信息随媒体类型不同而不同。
/// </summary>
public class SampleDescriptionBox : FullBox
{
/// <summary>
/// entry数目,占四个字节
/// </summary>
public uint EntryCount;
/// <summary>
/// stsd在box header和version字段后会有一个entry count字段,
/// 根据entry的个数,每个entry会有type信息,如“vide”、“sund”等,
/// 根据type不同sample description会提供不同的信息,例如对于video track,
/// 会有video sample entry类型信息,对于audio track会有audio sample entry类型信息。
/// 视频的编码类型、宽高、长度、音频的声道、采样等信息都会出现在这个box中。
/// </summary>
public List<SampleEntry> SampleEntrys = new List<SampleEntry>();
public override void ReadContent(BinaryReader br)
{
EntryCount = GetUint32(br);
ulong i = (ulong)headerLength + 4;
while (i < Size)
{
SampleEntry box = new SampleEntry();
box.SetParentPath(GetPath());
box.ReadHeader(br);
box.ReadContent(br);
SampleEntrys.Add(box);
i += box.Size;
}
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
str.AppendLine(" EntryCount : " + EntryCount);
for (int i = 0; i < SampleEntrys.Count; i++)
{
str.Append(SampleEntrys[i].ToString());
}
return str.ToString();
}
}
/// <summary>
/// 分为video sample entry和audio sample entry。
/// video track中为video sample entry,type域为vide;
/// audio track中为audio sample entry,type域为sund;
/// </summary>
public class SampleEntry : Box
{
/// <summary>
/// 保留位,占6个字节
/// </summary>
public byte[] Reserved;
/// <summary>
/// 占2个字节
/// </summary>
public ushort DataReferenceIndex;
public override void ReadContent(BinaryReader br)
{
Reserved = br.ReadBytes(6);
DataReferenceIndex = GetUint16(br);
ulong i = (ulong)headerLength + 8;
while (i < Size)
{
br.ReadByte();
i++;
}
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
str.AppendLine(" Reserved : " + string.Join(",", Reserved));
str.AppendLine(" DataReferenceIndex : " + DataReferenceIndex);
return str.ToString();
}
}
Time To Sample Box
包含在Sample Table Box中,有且仅有一个。
/// <summary>
/// type域为stts;
/// stts box存储了sample的duration,描述了sample时序的映射方法,我们通过它可以找到任何时间的sample。
/// stts box可以包含一个压缩的表来映射时间和sample序号,用其他的表来提供每个sample的长度和指针。
/// 表中每个条目提供了在同一时间偏移量里面连续的sample序号,以及sample的偏移量。
/// 递增这些偏移量,就可以建立一个完整的time to sample表(时间戳到sample序号的映射表)。
/// </summary>
public class TimeToSampleBox : FullBox
{
/// <summary>
/// 个数,占四个字节
/// </summary>
public uint EntryCount;
/// <summary>
/// 占EntryCount*4个字节
/// </summary>
public List<uint> SampleCounts = new List<uint>();
/// <summary>
/// 占EntryCount*4个字节
/// </summary>
public List<uint> SampleDeltas = new List<uint>();
public override void ReadContent(BinaryReader br)
{
EntryCount = GetUint32(br);
for (int i = 0; i < EntryCount; i++)
{
uint sampleCount = GetUint32(br);
SampleCounts.Add(sampleCount);
uint sampleDelta = GetUint32(br);
SampleDeltas.Add(sampleDelta);
}
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
str.AppendLine(" EntryCount : " + EntryCount);
str.AppendLine(" SampleCount : " + string.Join(",", SampleCounts));
str.AppendLine(" SampleDeltas : " + string.Join(",", SampleDeltas));
//// 计算Sample总数
//uint sampleCount = 0;
//for (int i = 0; i < SampleCounts.Count; i++)
//{
// sampleCount += SampleCounts[i];
//}
//str.AppendLine(" Sample总数 : " + sampleCount);
//// 计算每个sample的时间戳
//List<uint> timestamps = new List<uint>();
//for (int i = 0; i < EntryCount; i++)
//{
// uint j = 0;
// while (j < SampleCounts[i])
// {
// if (timestamps.Count < 1)
// {
// timestamps.Add(SampleDeltas[i]);
// }
// else
// {
// timestamps.Add(timestamps[timestamps.Count - 1] + SampleDeltas[i]);
// }
// j++;
// }
//}
//str.AppendLine(" Sample时间戳 : " + string.Join(",", timestamps));
return str.ToString();
}
}
Sync Sample Box
包含在Sample Table Box中,有0个或一个。
/// <summary>
/// type域为stss;
/// stss确定media中的关键帧。对于压缩媒体数据,关键帧是一系列压缩序列的开始帧,其解压缩时不依赖以前的帧,
/// 而后续帧解压缩将依赖于这个关键帧。stss可以非常紧凑的标记媒体内的随机存取点,它包含一个sample序号表,
/// 表内的每一项严格按照sample的序号排列,说明了媒体中的那个sample是关键帧。如果此表不存在,
/// 说明每一个sample都是关键帧,是一个随机存取点。
/// </summary>
public class SyncSampleBox : FullBox
{
/// <summary>
/// 占4个字节
/// </summary>
public uint EntryCount;
/// <summary>
/// 占EntryCount*4个字节
/// </summary>
public List<uint> SampleNumbers = new List<uint>();
public override void ReadContent(BinaryReader br)
{
EntryCount = GetUint32(br);
for (int i = 0; i < EntryCount; i++)
{
SampleNumbers.Add(GetUint32(br));
}
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
str.AppendLine(" EntrySize : " + EntryCount);
str.AppendLine(" SampleNumbers : " + string.Join(",", SampleNumbers));
return str.ToString();
}
}
Sample Size Box
包含在Sample Table Box中,有且仅有一个。
/// <summary>
/// type域为stsz或stz2;
/// stsz定义了每个sample的大小,包含了媒体中全部sample的数目和一张给出每个sample大小的表。
/// 这个box相对来说体积是比较大的。
/// </summary>
public class SampleSizeBox : FullBox
{
/// <summary>
/// 占四个字节
/// </summary>
public uint SampleSize;
/// <summary>
/// 占四个字节
/// </summary>
public uint SampleCount;
/// <summary>
/// 若SampleSize=0,则占SampleCount*4个字节
/// </summary>
public List<uint> EntrySizes = new List<uint>();
public override void ReadContent(BinaryReader br)
{
SampleSize = GetUint32(br);
SampleCount = GetUint32(br);
if (SampleSize == 0)
{
for (int i = 0; i < SampleCount; i++)
{
EntrySizes.Add(GetUint32(br));
}
}
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
str.AppendLine(" SampleSize : " + SampleSize);
str.AppendLine(" SampleCount : " + SampleCount);
str.AppendLine(" EntrySize : " + string.Join(",", EntrySizes));
//// 计算sample总大小
//uint sampleSizeSum = 0;
//for (int i = 0; i < SampleCount; i++)
//{
// sampleSizeSum += EntrySizes[i];
//}
//str.AppendLine(" Sample总大小 : " + sampleSizeSum);
return str.ToString();
}
}
Sample To Chunk Box
包含在Smaple Table Box中,有且仅有一个。
/// <summary>
/// type域为stsc;
/// 用chunk组织sample可以方便优化数据获取,一个chunk包含一个或多个sample。
/// stsc中用一个表描述了sample与chunk的映射关系,查看这张表就可以找到包含指定sample的chunk,从而找到这个sample。
/// </summary>
public class SampleToChunkBox : FullBox
{
/// <summary>
/// 占4个字节
/// </summary>
public uint EntryCount;
/// <summary>
/// 占EntryCount*4个字节
/// </summary>
public List<uint> FirstChunks = new List<uint>();
/// <summary>
/// 占EntryCount*4个字节
/// </summary>
public List<uint> SamplesPerChunks = new List<uint>();
/// <summary>
/// 占EntryCount*4个字节
/// </summary>
public List<uint> SmapleDescriptionIndexs = new List<uint>();
public override void ReadContent(BinaryReader br)
{
EntryCount = GetUint32(br);
for (int i = 0; i < EntryCount; i++)
{
FirstChunks.Add(GetUint32(br));
SamplesPerChunks.Add(GetUint32(br));
SmapleDescriptionIndexs.Add(GetUint32(br));
}
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
str.AppendLine(" EntrySize : " + EntryCount);
str.AppendLine(" FirstChunks : " + string.Join(",", FirstChunks));
str.AppendLine(" SamplesPerChunks : " + string.Join(",", SamplesPerChunks));
str.AppendLine(" SmapleDescriptionIndexs : " + string.Join(",", SmapleDescriptionIndexs));
return str.ToString();
}
}
Chunk Offset Box
包含在Sample Table Box中,有且仅有一个。
/// <summary>
/// type域为stco表示32位;type域为co64表示64位;
/// stco定义了每个chunk在媒体流中的位置,sample的偏移可以根据其他box推算出来。
/// 位置有两种可能,32位和64位的,在一个表中只会有一种可能,这个位置是在整个文件中的,
/// 而不是在任何box中的,这样做就可以直接在文件中找到媒体位置,而不用解释box。
/// 需要注意的是一旦前面的box有了任何改变,这张表都要重新建立,因为位置信息已经改变了。
/// </summary>
public class ChunkOffsetBox : FullBox
{
/// <summary>
/// 占四个字节
/// </summary>
public uint EntryCount;
/// <summary>
/// 若type为stco使用,占EntryCount*4个字节
/// </summary>
public List<uint> ChunkOffset32;
/// <summary>
/// 若type为co64使用,占EntryCount*8个字节
/// </summary>
public List<ulong> ChunkOffset64;
public override void ReadContent(BinaryReader br)
{
EntryCount = GetUint32(br);
if (Type.Equals("stco"))
{
ChunkOffset32 = new List<uint>();
for (int i = 0; i < EntryCount; i++)
{
ChunkOffset32.Add(GetUint32(br));
}
}
else
{
ChunkOffset64 = new List<ulong>();
for (int i = 0; i < EntryCount; i++)
{
ChunkOffset64.Add(GetUint64(br));
}
}
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
str.AppendLine(" EntrySize : " + EntryCount);
if (Type.Equals("stco"))
{
str.AppendLine(" ChunkOffset : " + string.Join(",", ChunkOffset32));
}
else
{
str.AppendLine(" ChunkOffset : " + string.Join(",", ChunkOffset64));
}
return str.ToString();
}
}
Composition Time To Sample Box
包含在Sample Table Box中,有0个或1个。
/// <summary>
/// type域为ctts
/// </summary>
public class CompositionOffsetBox : FullBox
{
/// <summary>
/// 占四个字节
/// </summary>
public uint EntryCount;
/// <summary>
/// 占EntryCount*4个字节
/// </summary>
public List<uint> SampleCounts = new List<uint>();
/// <summary>
/// 占EntryCount*4个字节
/// version = 0时,sample offset类型为uint32;
/// version = 1时,sample offset类型为int32;
/// </summary>
public List<uint> SampleOffsetUint32 = new List<uint>();
/// <summary>
/// 占EntryCount*4个字节
/// version = 0时,sample offset类型为uint32;
/// version = 1时,sample offset类型为int32;
/// </summary>
public List<int> SampleOffsetInt32 = new List<int>();
public override void ReadContent(BinaryReader br)
{
EntryCount = GetUint32(br);
for (int i = 0; i < EntryCount; i++)
{
uint sampleCount = GetUint32(br);
SampleCounts.Add(sampleCount);
if (Version == 0)
{
uint offsetUint = GetUint32(br);
SampleOffsetUint32.Add(offsetUint);
}
else
{
int offsetInt = GetInt32(br);
SampleOffsetInt32.Add(offsetInt);
}
}
}
public override string ToString()
{
StringBuilder str = new StringBuilder();
str.Append(base.ToString());
str.AppendLine(" EntryCount : " + EntryCount);
str.AppendLine(" SampleCount : " + string.Join(",", SampleCounts));
str.AppendLine(" SampleOffset : " + (Version == 0 ? string.Join(",", SampleOffsetUint32) : string.Join(",", SampleOffsetInt32)));
return str.ToString();
}
}
若要获取指定track下指定sample的原始数据:
- 通过 TimeToSampleBox 可获取指定sample的时间偏移
- 通过 SampleToChunkBox 可获取sample所在的chunk
- 通过 ChunkOffsetBox 可获取chunk的偏移量
- 通过 SampleSizeBox 可获取chunk下sample的大小
- 根据chunk偏移量及其中sample的大小可获取指定sample的偏移量
- 根据指定sample的偏移量及大小可在 MediaDataBox 中获取sample的数据
解析输出
FileTypeBox
Size : 24
Type : ftyp
MajorBrand : mp42
MinorVersion :
CompatibleBrands : isommp42
MediaDataBox
Size : 2473246
Type : mdat
ContentLength : 2473238
FreeSpaceBox
Size : 4520
Type : free
ContentLength : 4512
MovieBox
Size : 1870
Type : moov
MovieBox\MovieHeaderBox
Size : 108
Type : mvhd
Version : 0
Flags : 00-00-00
CreateTime : 2019/12/17 17:13:43
ModificationTime : 2019/12/17 17:13:43
TimeScale : 1000
Duration : 3772
TimeLength : 3.772 s
Rate : 1
Volume : 1
Reserved : 00-00-00-00-00-00-00-00-00-00
Matrix : 65536,0,0,0,65536,0,0,0,1073741824
PreDefined : 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
NextTrackId : 3
MovieBox\MetaBox
Size : 121
Type : meta
Version : 0
Flags : 00-00-21
ContentLength : 109
MovieBox\TrackBox
Size : 1038
Type : trak
MovieBox\TrackBox\TrackHeaderBox
Size : 92
Type : tkhd
Version : 0
Flags : 00-00-07
CreateTime : 2019/12/17 17:13:43
ModificationTime : 2019/12/17 17:13:43
TrackID : 1
Reserved : 0
Duration : 3772
Reserved : 0,0
Layer : 0
AlternateGroup : 0
Volume : 0
Reserved : 0
Matrix : 65536,0,0,0,65536,0,0,0,1073741824
Width : 640
Height : 480
MovieBox\TrackBox\MediaBox
Size : 938
Type : mdia
MovieBox\TrackBox\MediaBox\MediaHeaderBox
Size : 32
Type : mdhd
Version : 0
Flags : 00-00-00
CreateTime : 2019/12/17 17:13:43
ModificationTime : 2019/12/17 17:13:43
TimeScale : 90000
Duration : 339467
Language : 00000000 00000000
PreDefined : 0
MovieBox\TrackBox\MediaBox\HandlerReferenceBox
Size : 44
Type : hdlr
Version : 0
Flags : 00-00-00
PreDefined : 0
HandlerType : vide
Reserved : 0,0,0
Name : VideoHandle
MovieBox\TrackBox\MediaBox\MediaInformationBox
Size : 854
Type : minf
MovieBox\TrackBox\MediaBox\MediaInformationBox\VideoMediaHeaderBox
Size : 20
Type : vmhd
Version : 0
Flags : 00-00-01
GraphicsMode : 0
OpColor : 0,0,0
MovieBox\TrackBox\MediaBox\MediaInformationBox\DataInformationBox
Size : 36
Type : dinf
MovieBox\TrackBox\MediaBox\MediaInformationBox\DataInformationBox\DataReferenceBox
Size : 28
Type : dref
Version : 0
Flags : 00-00-00
EntryCount : 1
MovieBox\TrackBox\MediaBox\MediaInformationBox\DataInformationBox\DataReferenceBox\DataEntryUrlBox
Size : 12
Type : url
Version : 0
Flags : 00-00-01
Location :
MovieBox\TrackBox\MediaBox\MediaInformationBox\SampleTableBox
Size : 790
Type : stbl
MovieBox\TrackBox\MediaBox\MediaInformationBox\SampleTableBox\SampleDescriptionBox
Size : 178
Type : stsd
Version : 0
Flags : 00-00-00
EntryCount : 1
MovieBox\TrackBox\MediaBox\MediaInformationBox\SampleTableBox\SampleDescriptionBox\SampleEntry
Size : 162
Type : avc1
Reserved : 0,0,0,0,0,0
DataReferenceIndex : 1
MovieBox\TrackBox\MediaBox\MediaInformationBox\SampleTableBox\TimeToSampleBox
Size : 168
Type : stts
Version : 0
Flags : 00-00-00
EntryCount : 19
SampleCount : 1,14,1,21,1,6,2,2,2,2,2,2,2,2,2,2,2,1,8
SampleDeltas : 6493,4499,4509,4500,4490,4501,4492,4507,4492,4507,4493,4506,4493,4507,4492,4507,4492,4508,4499
MovieBox\TrackBox\MediaBox\MediaInformationBox\SampleTableBox\SampleSizeBox
Size : 320
Type : stsz
Version : 0
Flags : 00-00-00
SampleSize : 0
SampleCount : 75
EntrySize : 28384,20762,32986,37220,31942,35826,35359,34416,33380,35938,32239,34326,35127,33610,36638,33771,35001,32392,29924,68881,39600,36986,34851,42119,30216,33564,27977,26736,29889,29433,31844,27765,27300,21469,28343,25686,29222,27196,68539,42875,38770,32598,36496,31465,34818,33051,27156,28712,25484,28587,27679,29602,31300,24207,24289,25392,26633,60898,40983,41188,34165,33186,32904,36045,31128,30491,34074,30812,26314,25990,28260,27787,30652,33405,23297
MovieBox\TrackBox\MediaBox\MediaInformationBox\SampleTableBox\SampleToChunkBox
Size : 52
Type : stsc
Version : 0
Flags : 00-00-00
EntrySize : 3
FirstChunks : 1,2,4
SamplesPerChunks : 23,21,10
SmapleDescriptionIndexs : 1,1,1
MovieBox\TrackBox\MediaBox\MediaInformationBox\SampleTableBox\SyncSampleBox
Size : 32
Type : stss
Version : 0
Flags : 00-00-00
EntrySize : 4
SampleNumbers : 1,20,39,58
MovieBox\TrackBox\MediaBox\MediaInformationBox\SampleTableBox\ChunkOffsetBox
Size : 32
Type : stco
Version : 0
Flags : 00-00-00
EntrySize : 4
ChunkOffset : 6422,818110,1509107,2188005
MovieBox\TrackBox
Size : 595
Type : trak
MovieBox\TrackBox\TrackHeaderBox
Size : 92
Type : tkhd
Version : 0
Flags : 00-00-07
CreateTime : 2019/12/17 17:13:43
ModificationTime : 2019/12/17 17:13:43
TrackID : 2
Reserved : 0
Duration : 3712
Reserved : 0,0
Layer : 0
AlternateGroup : 0
Volume : 1
Reserved : 0
Matrix : 65536,0,0,0,65536,0,0,0,1073741824
Width : 0
Height : 0
MovieBox\TrackBox\MediaBox
Size : 495
Type : mdia
MovieBox\TrackBox\MediaBox\MediaHeaderBox
Size : 32
Type : mdhd
Version : 0
Flags : 00-00-00
CreateTime : 2019/12/17 17:13:43
ModificationTime : 2019/12/17 17:13:43
TimeScale : 8000
Duration : 29696
Language : 00000000 00000000
PreDefined : 0
MovieBox\TrackBox\MediaBox\HandlerReferenceBox
Size : 44
Type : hdlr
Version : 0
Flags : 00-00-00
PreDefined : 0
HandlerType : soun
Reserved : 0,0,0
Name : SoundHandle
MovieBox\TrackBox\MediaBox\MediaInformationBox
Size : 411
Type : minf
MovieBox\TrackBox\MediaBox\MediaInformationBox\SoundMediaHeaderBox
Size : 16
Type : smhd
Version : 0
Flags : 00-00-00
Balance : 0
Reserved : 0
MovieBox\TrackBox\MediaBox\MediaInformationBox\DataInformationBox
Size : 36
Type : dinf
MovieBox\TrackBox\MediaBox\MediaInformationBox\DataInformationBox\DataReferenceBox
Size : 28
Type : dref
Version : 0
Flags : 00-00-00
EntryCount : 1
MovieBox\TrackBox\MediaBox\MediaInformationBox\DataInformationBox\DataReferenceBox\DataEntryUrlBox
Size : 12
Type : url
Version : 0
Flags : 00-00-01
Location :
MovieBox\TrackBox\MediaBox\MediaInformationBox\SampleTableBox
Size : 351
Type : stbl
MovieBox\TrackBox\MediaBox\MediaInformationBox\SampleTableBox\SampleDescriptionBox
Size : 91
Type : stsd
Version : 0
Flags : 00-00-00
EntryCount : 1
MovieBox\TrackBox\MediaBox\MediaInformationBox\SampleTableBox\SampleDescriptionBox\SampleEntry
Size : 75
Type : mp4a
Reserved : 0,0,0,0,0,0
DataReferenceIndex : 1
MovieBox\TrackBox\MediaBox\MediaInformationBox\SampleTableBox\TimeToSampleBox
Size : 32
Type : stts
Version : 0
Flags : 00-00-00
EntryCount : 2
SampleCount : 1,28
SampleDeltas : 1024,1024
MovieBox\TrackBox\MediaBox\MediaInformationBox\SampleTableBox\SampleSizeBox
Size : 136
Type : stsz
Version : 0
Flags : 00-00-00
SampleSize : 0
SampleCount : 29
EntrySize : 195,225,272,239,229,207,209,199,168,186,169,207,181,181,169,186,187,215,187,197,196,178,178,172,192,191,198,191,184
MovieBox\TrackBox\MediaBox\MediaInformationBox\SampleTableBox\SampleToChunkBox
Size : 52
Type : stsc
Version : 0
Flags : 00-00-00
EntrySize : 3
FirstChunks : 1,2,4
SamplesPerChunks : 10,8,3
SmapleDescriptionIndexs : 1,1,1
MovieBox\TrackBox\MediaBox\MediaInformationBox\SampleTableBox\ChunkOffsetBox
Size : 32
Type : stco
Version : 0
Flags : 00-00-00
EntrySize : 4
ChunkOffset : 815981,1507612,2186514,2479087
参考资料
来源:CSDN
作者:doris_d
链接:https://blog.csdn.net/doris_d/article/details/103762379