6 数据格式
简介
本篇章收集一些文件数据的详细格式格式,为程序开发提供参考。
本篇章收集一些文件数据的详细格式格式,为程序开发提供参考。
二进制文件的格式一般采用结构 文件头 + 核心内容。
描述了文件的整体信息,常见的字段有魔数、检验码、版本号、文件大小等。
顾名思义,即是将文件的整体信息通常放在文件的开头。
在少数情况下,也会将文件头放在文件的尾部,也就是‘文件尾’,但是一般还是叫做 文件头。
魔数作为文件格式的标识,一般用于识别文件是否程序所能处理的文件。其值可以随意选取,主要看设计者自身的喜好。
例如 zip 格式的魔数就是 “PK\x03\x04”,其中 PK 就是设计者 Philip Katz 的名字首字母。
一般程序在分析文件时,会先判断魔数的值是否匹配,不匹配就表明文件格式不正确。
需要注意的是,假如魔数正确,文件格式并非一定能够读取正确,还需要进一步判断。
文件头通常还会有个检验码,用于检验文件是否完整并且没有经过修改的。这个检验码可以使用 crc, 可以使用 md5,也可以使用其它算法。
这也是检查文件格式的正确性中的重要一步,因为即便是魔数相同,校验码所采用的算法不同,和校验码所作的位置不同,所计算的校验码,基本上是不一的,所以进一步就,能区分出文件格式是否正确。
文件头通常还会包含版本号。
版本号不同,就表明文件格式发生变化,可能变化很小,也可能变化很大;可能是某些字段的值在解释上发生变化,也可能是直接添加了一些结构。所以导致文件的读取方式可能会有所不同。
版本号有时只是单独一个数字,不断往上递增。有时也会拆分成两个数字,为主版本号和次版本号。主版本号修改,通常表示文件格式发生大变动。而次版本号修改,通常只是表示添加了一些小功能。
字节顺序在文件格式设计中至关重要。
字节顺序分为大端字节序和小端字节序。不同的机器字节序有可能不同,设计文件格式时需要考虑文件用什么字节序保存数据的。
读取和存储的字节序不一致就会导致程序出错。
通用结构定义如下:
struct FileHeader{
uint8 mMagic[4]; //魔数
uint8 mHash[16]; //检验码
uint32 mEndian; //字节序
uint32 mVersion; //文件版本
...
};
分段(或叫分节,后续默认叫法为分段)的二进制文件格式是比较常见的,比如编译器 GCC 输出的目标文件一般为 ELF 的分段文件。
这类文件一般采用 文件头 + 分段 的结构:
file header
section 0
section 1
....
section N
文件头设计方法在 文件头 中介绍,本章节主要描述分段式存储数据的方法。
一般有两种结构:
分散式分结构在分区的头部的描述分段的大小等信息,然后各个分段首尾相连的,分段写入。
分段的每一个段的结构通常是:
tag + length
section data
tag 和 length 合起来是分区头部,描述分段中的数据标识,后面紧跟着体数据。
tag 一般是一个整数,用来标识分区的类型。不同的分区用不同的 tag 值表示,不同的分区种类可以使用不同的读取方式。
整体结构如下:
file header
section 0
tag //section 0 的标识
length //section 0 的数据长度
data[] //section 0 的数据
section 1
....
section N
段表式的结构是有一个描述分段信息的段表,后面存储各个分区的数据。
主要优点就是,可以通过段描述表(简称段表)快速的访问分段的内容,其格式如下。
section table
section 0
section 1
....
section N
段表一般描述分段的类型、数据长度、相对偏移等,结构如下。
kind
length
offset
C 语言定义如下:
struct SegTabItem{
uint32 kind; //类型
uint32 length; //数据长度
uint32 offset; //相对偏移
};
整体结构如下:
file header
section table
kind //section 0 的类型
length //section 0 的数据长度
offset //section 0 的相对偏移
kind //section 1 的类型
length //section 1 的数据长度
offset //section 1 的相对偏移
...
kind //section N 的类型
length //section N 的数据长度
offset //section N 的相对偏移
section 0
section 1
....
section N
TLV 是一种可变的格式,由三个域构成:标识域(Tag)+长度域(Length)+值域(Value),简称TLV格式。
其中:
T 和 L 的长度固定,一般是2或4个字节,V 的长度由 Length 指定。
data 0
T = 1
L = 1
V = 1
data 1
T = 1
L = 1
V = 2
...
data N
T = 1
L = 1
V = n
当然V中的数据也是可以嵌套,至于嵌套几层看设计者的规定的。
结构如下:
data 0
T = 1
L = 3
V =[
data x
T = 1
L = 1
V = 1
]
data 1
T = 1
L = 1
V = 2
...
data N
T = 1
L = 1
V = n
颜色常用颜色空间来表示。颜色空间是用一种数学方法形象化表示颜色,人们用它来指定和产生颜色。例如,
颜色空间有设备相关和设备无关之分。
设备相关的颜色空间是指颜色空间指定生成的颜色与生成颜色的设备有关。例如,RGB颜色空间是与显示系统相关的颜色空间,计算机显示器使用RGB来显示颜色,用像素值(例如,R=250,G=123,B=23)生成的颜色将随显示器的亮度和对比度的改变而改变。
设备无关的颜色空间是指颜色空间指定生成的颜色与生成颜色的设备无关,例如,CIE Lab*颜色空间就是设备无关的颜色空间,它构建在HSV(hue, saturation and value)颜色空间的基础上,用该空间指定的颜色无论在什么设备上生成的颜色都相同。
RGB 代表红(Red)、绿(Green)、蓝(Blue)。这三种颜色被称为光的三原色。在 RGB 色彩模式下,通过不同强度比例的红、绿、蓝三种光的混合,可以产生出各种各样的颜色。
从物理学角度来看,光是一种电磁波,而人眼能够感知到的可见光波段内,红、绿、蓝这三种颜色的光具有独特的波长范围。红色光的波长大约在 620 - 750 纳米之间,绿色光波长约为 495 - 570 纳米,蓝色光波长则在 450 - 480 纳米左右。当这三种颜色的光以不同的能量强度同时作用于人眼时,大脑就会感知到不同的色彩。 例如,当红色光以最强的强度发出,而绿色光和蓝色光强度为零时,我们看到的就是纯粹的红色;同理,单独最强强度的绿色光呈现绿色,单独最强强度的蓝色光呈现蓝色。而当三种光强度相等时,就会产生白色光。
其中 RGB、RGBA是比较常用的。
RGB24 是一种 24 位的像素格式,它使用 8 位来表示红、绿和蓝的颜色分量。也被称作 RGB888。
typedef struct
{
unsigned char r;
unsigned char g;
unsigned char b;
}RGB,RGB24,RGB888;
#define RGB(r,g,b) ((unsigned int)(((unsigned char)(r)|(((unsigned char)(g))<<8))|(((unsigned char)(b))<<16)))
注:windows 平台下,一般使用BGR。颜色分量顺序为 BGR。
RGBA32 是一种 32 位的像素格式,它使用 8 位来表示红、绿、蓝和透明度的颜色分量。也被称作 RGB8888。
typedef union
{
unsigned int rgba;
struct
{
unsigned char r;
unsigned char g;
unsigned char b;
unsigned char a;
};
}RGBA,RGBA32,RGBA8888;
#define RGBA(r,g,b,a) ((unsigned int)(((unsigned char)(r)|(((unsigned char)(g))<<8))|(((unsigned char)(b))<<16)|(((unsigned char)(a))<<24)))
RGB555 是一种 16 位的像素格式,它使用 5 位来表示红、绿和蓝的颜色分量。其中的1位作为透明分量(可以不使用),共 16 位。
typedef struct
{
unsigned short r : 5;
unsigned short g : 5;
unsigned short b : 5;
unsigned short a : 1;
}RGBA555;
//各分量取值:
#define RGB555_MASK_RED 0x7C00
#define RGB555_MASK_GREEN 0x03E0
#define RGB555_MASK_BLUE 0x001F
#define RGB555_MASK_ALPHA 0x8000
R = (wPixel & RGB555_MASK_RED) >> 10; // 取值范围0-31
G = (wPixel & RGB555_MASK_GREEN) >> 5; // 取值范围0-31
B = wPixel & RGB555_MASK_BLUE; // 取值范围0-31
A = (wPixel & RGB555_MASK_ALPHA) >> 15;
#define RGB555(r,g,b,a) (unsigned short)( (r|0x08 << 10) | (g|0x08 << 5) | b|0x08 | (a << 15) )
RGB565使用16位表示一个像素,这16位中的5位用于R,6位用于G,5位用于B。程序中通常使用一个字(WORD,一个字等于两个字节)来操作一个像素。当读出一个像素后,这个字的各个位意义如下:
typedef struct
{
unsigned short r : 5;
unsigned short g : 6;
unsigned short b : 5;
}RGB565;
//各分量取值:
#define RGB565_MASK_RED 0xF800
#define RGB565_MASK_GREEN 0x07E0
#define RGB565_MASK_BLUE 0x001F
R = (wPixel & RGB565_MASK_RED) >> 11;
G = (wPixel & RGB565_MASK_GREEN) >> 5;
B = wPixel & RGB565_MASK_BLUE;
#define RGB(r,g,b) (unsigned int)( (r|0x08 << 11) | (g|0x08 << 6) | b|0x08 )
HSL是一种将RGB色彩模型中的点在圆柱坐标系中的表示法。这两种表示法试图做到比基于笛卡尔坐标系的几何结构RGB更加直观。是运用最广的颜色系统之一
HSL即色相、饱和度、亮度(英语:Hue, Saturation, Lightness)。 色相(H)是色彩的基本属性,就是平常所说的颜色名称,如红色、黄色等。 饱和度(S)是指色彩的纯度,越高色彩越纯,低则逐渐变灰,取0-100%的数值。 明度(V),亮度(L),取0-100%。
HSV即色相、饱和度、明度(英语:Hue, Saturation, Value),又称HSB,其中B即英语:Brightness。 HSL和HSV二者都把颜色描述在圆柱坐标系内的点,这个圆柱的中心轴取值为自底部的黑色到顶部的白色而在它们中间的是灰色,绕这个轴的角度对应于“色相”,到这个轴的距离对应于“饱和度”,而沿着这个轴的高度对应于“亮度”、“色调”或“明度”。 这两种表示在目的上类似,但在方法上有区别。二者在数学上都是圆柱,但HSV(色相、饱和度、明度)在概念上可以被认为是颜色的倒圆锥体(黑点在下顶点,白色在上底面圆心),HSL在概念上表示了一个双圆锥体和圆球体(白色在上顶点,黑色在下顶点,最大横切面的圆心是半程灰色)。注意尽管在HSL和HSV中“色相”指称相同的性质,它们的“饱和度”的定义是明显不同的。 因为HSL和HSV是设备依赖的RGB的简单变换,(h,s,l)或 (h,s,v)三元组定义的颜色依赖于所使用的特定RGB“加法原色”。每个独特的RGB设备都伴随着一个独特的HSL和HSV空间。但是 (h,s,l)或 (h,s,v)三元组在被约束于特定RGB空间比如sRGB的时候就更明确了。
HSL和HSV在数学上定义为在RGB空间中的颜色的R,G和B的坐标的变换。
// 定义结构体表示 HSL 颜色
typedef struct {
float h; // 色调,范围 [0, 360]
float s; // 饱和度,范围 [0, 1]
float l; // 亮度,范围 [0, 1]
} HSL;
// 将 RGB 转换为 HSL
HSL rgb_to_hsl(int r, int g, int b) {
// 归一化 RGB 值
float rf = r / 255.0;
float gf = g / 255.0;
float bf = b / 255.0;
// 计算最大值和最小值
float max = fmaxf(fmaxf(rf, gf), bf);
float min = fminf(fminf(rf, gf), bf);
float delta = max - min;
HSL hsl = {0, 0, 0};
// 计算亮度 L
hsl.l = (max + min) / 2.0;
if (delta == 0) {
hsl.h = 0; // 色调不确定,设为 0
hsl.s = 0; // 饱和度为 0
} else {
// 计算饱和度 S
if (hsl.l < 0.5) {
hsl.s = delta / (max + min);
} else {
hsl.s = delta / (2.0 - max - min);
}
// 计算色调 H
if (rf == max) {
hsl.h = (gf - bf) / delta;
} else if (gf == max) {
hsl.h = 2 + (bf - rf) / delta;
} else {
hsl.h = 4 + (rf - gf) / delta;
}
hsl.h *= 60.0; // 转换为度数
if (hsl.h < 0) {
hsl.h += 360.0;
}
}
return hsl;
}
// 将 HSL 转换为 RGB
RGB hsl_to_rgb(HSL hsl) {
RGB rgb = {0, 0, 0};
if (hsl.s == 0) {
// 如果饱和度为 0,则是灰度颜色
rgb.r = rgb.g = rgb.b = (int)(hsl.l * 255);
return rgb;
}
float c = (1 - fabs(2 * hsl.l - 1)) * hsl.s;
float x = c * (1 - fabs(fmod(hsl.h / 60.0, 2) - 1));
float m = hsl.l - c / 2;
float r, g, b;
if (hsl.h >= 0 && hsl.h < 60) {
r = c; g = x; b = 0;
} else if (hsl.h >= 60 && hsl.h < 120) {
r = x; g = c; b = 0;
} else if (hsl.h >= 120 && hsl.h < 180) {
r = 0; g = c; b = x;
} else if (hsl.h >= 180 && hsl.h < 240) {
r = 0; g = x; b = c;
} else if (hsl.h >= 240 && hsl.h < 300) {
r = x; g = 0; b = c;
} else if (hsl.h >= 300 && hsl.h < 360) {
r = c; g = 0; b = x;
}
rgb.r = (int)((r + m) * 255);
rgb.g = (int)((g + m) * 255);
rgb.b = (int)((b + m) * 255);
return rgb;
}
代码解释:
// 定义结构体表示 RGB 颜色
typedef struct {
int r; // 红色分量,范围 [0, 255]
int g; // 绿色分量,范围 [0, 255]
int b; // 蓝色分量,范围 [0, 255]
} RGB;
// 定义结构体表示 HSV 颜色
typedef struct {
float h; // 色调,范围 [0, 360)
float s; // 饱和度,范围 [0, 1]
float v; // 明度,范围 [0, 1]
} HSV;
// 将 HSV 转换为 RGB
RGB hsv_to_rgb(HSV hsv) {
RGB rgb = {0, 0, 0};
float c = hsv.v * hsv.s;
float x = c * (1 - fabs(fmod(hsv.h / 60.0, 2) - 1));
float m = hsv.v - c;
if (hsv.h >= 0 && hsv.h < 60) {
rgb.r = (int)((c + m) * 255);
rgb.g = (int)((x + m) * 255);
rgb.b = (int)(m * 255);
} else if (hsv.h >= 60 && hsv.h < 120) {
rgb.r = (int)((x + m) * 255);
rgb.g = (int)((c + m) * 255);
rgb.b = (int)(m * 255);
} else if (hsv.h >= 120 && hsv.h < 180) {
rgb.r = (int)(m * 255);
rgb.g = (int)((c + m) * 255);
rgb.b = (int)((x + m) * 255);
} else if (hsv.h >= 180 && hsv.h < 240) {
rgb.r = (int)(m * 255);
rgb.g = (int)((x + m) * 255);
rgb.b = (int)((c + m) * 255);
} else if (hsv.h >= 240 && hsv.h < 300) {
rgb.r = (int)((x + m) * 255);
rgb.g = (int)(m * 255);
rgb.b = (int)((c + m) * 255);
} else if (hsv.h >= 300 && hsv.h < 360) {
rgb.r = (int)((c + m) * 255);
rgb.g = (int)(m * 255);
rgb.b = (int)((x + m) * 255);
}
return rgb;
}
// 将 HSV 转换为 RGB
RGB hsv_to_rgb(HSV hsv) {
RGB rgb = {0, 0, 0};
float c = hsv.v * hsv.s;
float x = c * (1 - fabs(fmod(hsv.h / 60.0, 2) - 1));
float m = hsv.v - c;
if (hsv.h >= 0 && hsv.h < 60) {
rgb.r = (int)((c + m) * 255);
rgb.g = (int)((x + m) * 255);
rgb.b = (int)(m * 255);
} else if (hsv.h >= 60 && hsv.h < 120) {
rgb.r = (int)((x + m) * 255);
rgb.g = (int)((c + m) * 255);
rgb.b = (int)(m * 255);
} else if (hsv.h >= 120 && hsv.h < 180) {
rgb.r = (int)(m * 255);
rgb.g = (int)((c + m) * 255);
rgb.b = (int)((x + m) * 255);
} else if (hsv.h >= 180 && hsv.h < 240) {
rgb.r = (int)(m * 255);
rgb.g = (int)((x + m) * 255);
rgb.b = (int)((c + m) * 255);
} else if (hsv.h >= 240 && hsv.h < 300) {
rgb.r = (int)((x + m) * 255);
rgb.g = (int)(m * 255);
rgb.b = (int)((c + m) * 255);
} else if (hsv.h >= 300 && hsv.h < 360) {
rgb.r = (int)((c + m) * 255);
rgb.g = (int)(m * 255);
rgb.b = (int)((x + m) * 255);
}
return rgb;
}
代码解释:
YUV,是一种颜色编码方法。常使用在各个视频处理组件中。 YUV在对照片或视频编码时,考虑到人类的感知能力,允许降低色度的带宽。 YUV是编译true-color颜色空间(color space)的种类,Y’UV, YUV, YCbCr,YPbPr等专有名词都可以称为YUV,彼此有重叠。“Y”表示明亮度(Luminance或Luma),也就是灰阶值,“U”和“V”表示的则是色度(Chrominance或Chroma),作用是描述影像色彩及饱和度,用于指定像素的颜色。
Y’代表明亮度(luma;brightness)而U与V存储色度(色讯;chrominance;color)部分;亮度(luminance)记作Y,而Y’的prime符号记作伽玛校正。 YUVFormats分成两个格式: 紧缩格式(packedformats):将Y、U、V值存储成MacroPixels数组,和RGB的存放方式类似。 平面格式(planarformats):将Y、U、V的三个分量分别存放在不同的矩阵中。 紧缩格式(packedformat)中的YUV是混合在一起的,对于YUV常见格式有AYUV格式(4:4:4采样、打包格式);YUY2、UYVY(采样、打包格式),有UYVY、YUYV等。平面格式(planarformats)是指每Y分量,U分量和V分量都是以独立的平面组织的,也就是说所有的U分量必须在Y分量后面,而V分量在所有的U分量后面,此一格式适用于采样(subsample)。平面格式(planarformat)有I420(4:2:0)、YV12、IYUV等。
为节省带宽起见,大多数YUV格式平均使用的每像素位数都少于24位。主要的抽样(subsample)格式有YCbCr4:2:0、YCbCr4:2:2、YCbCr4:1:1和YCbCr4:4:4。YUV的表示法称为A:B:C表示法:
最常用Y:UV记录的比重通常1:1或2:1,DVD-Video是以YUV4:2:0的方式记录,也就是我们俗称的I420,YUV4:2:0并不是说只有U(即Cb),V(即Cr)一定为0,而是指U:V互相援引,时见时隐,也就是说对于每一个行,只有一个U或者V分量,如果一行是4:2:0的话,下一行就是4:0:2,再下一行是4:2:0…以此类推。
至于其他常见的YUV格式有YUY2、YUYV、YVYU、UYVY、AYUV、Y41P、Y411、Y211、IF09、IYUV、YV12、YVU9、YUV411、YUV420等。
YUY2(和YUYV)格式为像素保留Y,而UV在水平空间上相隔二个像素采样一次(Y0U0Y1V0),(Y2U2Y3V2)…其中,(Y0U0Y1V0)就是一个macro-pixel(宏像素),它表示了2个像素,(Y2U2Y3V2)是另外的2个像素。以此类推,再如:Y41P(和Y411)格式为每个像素保留Y分量,而UV分量在水平方向上每4个像素采样一次。一个宏像素为12个字节,实际表示8个像素。
图像数据中YUV分量排列顺序如下:(U0Y0V0Y1U4Y2V4Y3Y4Y5Y6Y7)
YVYU,UYVY格式跟YUY2类似,只是排列顺序有所不同。Y211格式是Y每2个像素采样一次,而UV每4个像素采样一次。AYUV格式则有一Alpha通道。
YV12格式与IYUV类似,每个像素都提取Y,在UV提取时,将图像2x2的矩阵,每个矩阵提取一个U和一个V。YV12格式和I420格式的不同处在V平面和U平面的位置不同。在YV12格式中,V平面紧跟在Y平面之后,然后才是U平面(即:YVU);但I420则是相反(即:YUV)。NV12与YV12类似,效果一样,YV12中U和V是连续排列的,而在NV12中,U和V就交错排列的。
CIE 1931 XYZ色彩空间(也叫做CIE 1931色彩空间)是其中一个最先采用数学方式来定义的色彩空间,它由国际照明委员会(CIE)于1931年创立。
XYZ 色彩空间是为了解决更精确地定义色彩而提出来的, XYZ 三个分量中, XY代表的是色度, 其中Y分量既可以代表亮度也可以代表色度, 三个分量的单位都是 cd/m2 , (或者叫做nit)。我们无法用RGB来精确定义颜色, 因为,不同的设备显示的RGB都是不一样的,不同的设备, 显示同一个RGB, 在人眼看出来是千差万别的, 如果我们用XYZ定义一个设备的色彩空间, 这样就精确多了!
R = 3.2406 * X + -1.5372 * Y + -0.4986 * Z
G = -0.9689 * X + 1.8758 * Y + 0.0415 * Z
B = 0.0557 * X + -0.2040 * Y + 1.0570 * Z
X = 0.4124 * R + 0.3576 * G + 0.1805 * B
Y = 0.2126 * R + 0.7152 * G + 0.0722 * B
Z = 0.0193 * R + 0.1192 * G + 0.9505 * B
Lab色彩模型是由照度(L)和有关色彩的a, b三个要素组成。L表示照度(Luminosity),相当于亮度,a表示从红色至绿色的范围,b表示从蓝色至黄色的范围。L的值域由0到100,L=50时,就相当于50%的黑;a和b的值域都是由+120至-120,其中+120 a就是红色,渐渐过渡到-120 a的时候就变成绿色;同样原理,+120 b是黄色,-120 b是蓝色。所有的颜色就以这三个值交互变化所组成。
详情请在网络上搜索。
CMYK代表印刷上用的四种颜色,C代表青色(Cyan),M代表洋红色或者品红(Magenta),Y代表黄色(Yellow),K代表黑色(Black)。因为在实际应用中,青色、洋红色和黄色很难叠加形成真正的黑色,最多不过是褐色而已。因此才引入了K——黑色。黑色的作用是强化暗调,加深暗部色彩。
详情请在网络上搜索。
本文由 简悦 SimpRead 转码, 原文地址 zhuanlan.zhihu.com
作者:张武星
审核:泰一
MP4 文件由 box 组成,每个 box 分为 Header 和 Data。其中 Header 部分包含了 box 的类型和大小,Data 包含了子 box 或者数据,box 可以嵌套子 box。
下图是一个典型 MP4 文件的基本结构:
图中看到 MP4 文件有几个主要组成部分:
fytp
File Type Box,一般在文件的开始位置,描述的文件的版本、兼容协议等。
moov
Movie Box,包含本文件中所有媒体数据的宏观描述信息以及每路媒体轨道的具体信息。一般位于 ftyp 之后,也有的视频放在文件末尾。注意,当改变 moov 位置时,内部一些值需要重新计算。
mdat
Media Data Box,存放具体的媒体数据。
MP4 的媒体数据信息主要存放在 Moov Box 中,是我们需要分析的重点。moov 的主要组成部分如下:
mvhd
Movie Header Box,记录整个媒体文件的描述信息,如创建时间、修改时间、时间度量标尺、可播放时长等。
下图示例中,可以获取文件信息如时长为 3.637 秒。
udta
User Data Box,自定义数据。
track
Track Box,记录媒体流信息,文件中可以存在一个或多个 track,它们之间是相互独立的。每个 track 包含以下几个组成部分:
1. tkhd
Track Header Box,包含关于媒体流的头信息。
下图示例中,可以看到流信息如视频流宽度 720,长度 1280。
2. mdia
Media Box,这是一个包含 track 媒体数据信息的 container box。子 box 包括:
stbl 包含了媒体流每一个 sample 在文件中的 offset,pts,duration 等信息。想要播放一个 MP4 文件,必须根据 stbl 正确找到每个 sample 并送给解码器。mdia 展开如下图所示:
Sample Table Box,上文提到 mdia 中最主要的部分是存放文件中每个 Sample 信息的 stbl。在解析 stbl 前,我们需要区分 Chunk 和 Sample 这两个概念。在 MP4 文件中,Sample 是一个媒体流的基本单元,例如视频流的一个 Sample 代表实际的 nal 数据。Chunk 是数据存储的基本单位,它是一系列 Sample 数据的集合,一个 Chunk 中可以包含一个或多的 Sample。
stbl 用来描述每个 Sample 的信息,包含以下几个主要的子 box:
stsd
Sample Description Box,存放解码必须的描述信息。
下图示例中,对于 h264 的视频流,其具体类型为 avc1
,extensions 中存放有 sps,pps 等解码必要信息。
stts
Time-to-Sample Box,定义每个 Sample 时长。Time To Sample 的 table entry 布局如下:
持续时间相同的连续的 Sample 可以放到一个 entry 里面,以达到节省空间的目的。
下图示例中,第 1 个 Sample 时间为 33362 微秒,第 2-11 个 Sample 时间为 33363 微秒:
stss
Sync Sample Box,同步 Sample 表,存放关键帧列表,关键帧是为了支持随机访问。stss 的 table entry 布局如下:
下图示例中,该视频 track 只有一个关键帧即第 1 帧:
stsc
Sample-To-Chunk Box,Sample-Chunk 映射表。上文提到 MP4 通常把 Sample 封装到 Chunk 中,一个 Chunk 可能会包含一个或者几个 Sample。Sample-To-Chunk Atom 的 table entry 布局如下图所示:
下图示例中,可以看到该视频 track 一共有两个 stsc 表项,Chunk 序列 1-108,每个 Chunk 包含一个 sample,Chunk 序列 109 开始,每个 Chunk 包含两个 Sample。
stsz
Sample Size Box,指定了每个 Sample 的 size。Sample Size Atom 包含两 Sample 总数和一张包含了每个 Sample Size 的表。Sample Size 表的 entry 布局如下图:
下图示例中,该视频流一共有 110 个 Sample,第 1 个 Sample 大小为 42072 字节,第 2 个 Sample 大小为 7354 个字节。
stco
Chunk Offset Box,指定了每个 Chunk 在文件中的位置,这个表是确定每个 Sample 在文件中位置的关键。该表包含了 Chunk 个数和一个包含每个 Chunk 在文件中偏移位置的表。每个表项的内存布局如下:
需要注意,这里 stco 只是指定的每个 Chunk 在文件中的偏移位置,并没有给出每个 Sample 在文件中的偏移。想要获得每个 Sample 的偏移位置,需要结合 Sample Size box 和 Sample-To-Chunk 计算后取得。
下图示例中,该视频流第 1 个 Chunk 在文件中的偏移为 4750,第 1 个 Chunk 在文件中的偏移为 47007。
上文提到通过 stco 并不能直接获取某个 Sample 的偏移位置,下面举例说明如何获取某一个 pts 对应的 Sample 在文件中的位置。大体需要以下步骤:
将 pts 转换到媒体对应的时间坐标系。
根据 stts 计算某个 pts 对应的 Sample 序号。
根据 stsc 计算 Sample 序号存放在哪个 Chunk 中。
根据 stco 获取对应 Chunk 在文件中的偏移位置。
根据 stsz 获取 Sample 在 Chunk 内的偏移位置并加上第 4 步获取的偏移,计算出 Sample 在文件中的偏移。
例如,想要获取 3.64 秒视频 Sample 数据在文件中的位置:
根据 time scale 参数,将 3.64 秒转换为视频时间轴对应的 3640000。
遍历累加下表所示 stts 所有项目,计算得到 3640000 位于第 110 个 Sample。
type stts
size 224
flags 0
version 0
sample_counts 1,10,1,1,11,1,1,2,1,25,1,1,1,17,1,10,1,1,1,7,1,1,1,1,10,1
sample_deltas 33362,33363,33362,33364,33363,33362,33364,33363,33362,33363,33362,33364,33362,33363,33362,33363,33362,33364,33362,33363,33362,33364,33363,33362,33363,0
type stsc
size 40
flags 0
version 0
first_chunk 1,109
samples_per_chunk 1,2
sample_description_index 1,1
Property name Property value
type stco
size 452
flags 0
version 0
chunk_offsets 4750,47007,54865,61967,75519,88424,105222,117892,133730,149529,165568,182034,194595,210776,225470,240756,255358,270711,285459,300135,315217,330899,347372,363196,376409,394509,407767,424615,438037,455603,469784,487287,505197,519638,536714,553893,567187,584744,599907,615298,630669,645918,662605,678655,693510,708980,724061,738946,754170,771520,787233,800847,816997,832490,847814,862559,877929,898379,911054,925810,943883,956497,974403,991527,1009478,1025198,1041806,1062609,1078401,1091360,1105142,1118748,1132815,1145281,1156966,1171871,1186742,1202760,1218235,1236688,1249330,1263163,1280880,1297903,1313162,1332885,1345726,1359017,1376283,1391401,1405512,1419550,1433644,1452103,1475241,1492689,1511291,1522606,1535368,1559413,1575331,1588853,1609829,1626623,1642798,1658640,1674160,1693972,1710064
offset:1710064 + 14808 = 1724872
size:17930
type stsz
size 460
flags 0
version 0
sample_sizes 42072,7354,6858,13110,12684,16416,12490,15497,15630,15865,16116,12387,15775,14519,14929,14433,15181,14390,14496,14717,15507,16101,15643,12843,17911,13070,16455,13221,17186,14002,17139,17737,14251,16708,16999,12911,17356,14801,15213,15016,15062,16505,15689,14657,15053,14907,14527,15048,17161,15308,13432,15777,15307,14971,14568,14987,20264,12494,14382,17873,12235,17718,16770,17766,15366,16420,20623,15403,12761,13394,13390,13714,12295,11505,14541,14689,15635,15291,18091,12458,13645,17346,16847,14902,19530,12446,13105,16872,14937,13944,13657,13908,18092,22959,17080,18421,11129,12400,23844,15564,13340,20603,16609,15984,15474,15339,19451,15719,14808,17930
sample_size 0
sample_count 110
QuickTime File Format Specification
「视频云技术」公众号,每周阿里云一线专家分享,音视频领域最佳技术干货,欢迎关注。
本文由 简悦 SimpRead 转码, 原文地址 zhuanlan.zhihu.com
MP4 或称 MPEG-4 第 14 部分(MPEG-4 Part 14)是一种标准的数字多媒体容器格式。扩展名为. mp4。虽然被官方标准定义的唯一扩展名是. mp4,但第三方通常会使用各种扩展名来指示文件的内容:
大部分数据可以通过专用数据流嵌入到 MP4 文件中,因此 MP4 文件中包含了一个单独的用于存储流信息的轨道。目前得到广泛支持的编解码器或数据流格式有:
为了后面能比较规范的了解这种文件格式,这里需要了解下面几个概念和术语,这些概念和术语是理解好 MP4 媒体封装格式和其操作算法的关键。
(1)Box
这个概念起源于 QuickTime 中的 atom,也就是说 MP4 文件就是由一个个 Box 组成的,可以将其理解为一个数据块,它由 Header+Data 组成,Data 可以存储媒体元数据和实际的音视频码流数据。Box 里面可以直接存储数据块但是也可以包含其它类型的 Box,我们把这种 Box 又称为 container box。
(2)Sample
简单理解为采样,对于视频可以理解为一帧数据,音频一帧数据就是一段固定时间的音频数据,可以由多个 Sample 数据组成,简而言之:存储媒体数据的单位是 sample。
(3)Track
表示一些 sample 的集合,对于媒体数据而言就是一个视频序列或者音频序列,我们常说的音频轨和视频轨可以对照到这个概念上。当然除了 Video Track 和 Audio Track 还可以有非媒体数据,比如 Hint Track,这种类型的 Track 就不包含媒体数据,可以包含一些将其他数据打包成媒体数据的指示信息或者字幕信息。简单来说:Track 就是电影中可以独立操作的媒体单位。
(4)Chunk
一个 track 的连续几个 sample 组成的单元被称为 chunk,每个 chunk 在文件中有一个偏移量,整个偏移量从文件头算起,在这个 chunk 内,sample 是连续存储的。
这样就可以理解为 MP4 文件里面有多个 Track, 一个 Track 又是由多个 Chunk 组成,每个 Chunk 里面包含着一组连续的 Sample,正是因为定义了上述几个概念,MP4 这种封装格式才容易实现灵活、高效、开放的特性,所以要仔细理解。
MP4 格式是一个 box 的格式,box 容器套 box 子容器,box 子容器再套 box 子容器。
一个 box 由两部分组成:box header、box body。
完整的 Box 结构:
每个 Box 承载的数据内容如下:
mp4 封装格式采用称为 box 的结构来组织数据。结构如下:
+-+-+-+-+-+-+-+-+-+-+
| header | body |
+-+-+-+-+-+-+-+-+-+-+
其它所有 box 都在语法上继承自此基本 box 结构。
box 分为普通 box 和 fullbox。
(1)普通 box header 结构如下:
字段 | 类型 | 描述 |
size | 4 Bytes | 包含 box header 的整个 box 的大小 |
type | 4 Bytes | 4 个 ascii 值,如果是 "uuid",则表示此 box 为用户自定义,可忽略 |
large size | 8 Bytes | size=1 时才有的字段,用于扩展,例如 mdat box 会需要此字段 |
(2)fullbox 在上面的基础上新增了 2 个字段:
字段 | 类型 | 描述 |
version | 1 Bytes | 版本号 |
flags | 3 Bytes | 标识 |
一个 box 可能会包含其它多个 box,此种 box 称为 container box。因此 box body 可能是一种具体 box 类型,也有可能是其它 box。
虽然 Box 的类型非常多,大概有 70 多种,但是并不是都是必须的,一般的 MP4 文件都是含有必须的 Box 和个别非必须 Box,我用 MP4info 这种工具分析了一首 MP4 的文件,具体的 Box 显示如下:
通过上述工具分析出来的结果,我们大概可以总结出 MP4 以下几个特点:
ftyp 一般出现在文件的开头,用来指示该 mp4 文件使用的标准规范:
字段 | 类型 | 描述 |
major_brand | 4 bytes | 主版本 |
minor_version | 4 bytes | 次版本 |
compatible_brands[] | 4 bytes | 指定兼容的版本,注意此字段是一个 list,即可以包含多个 4 bytes 版本号 |
一个示例如下:
子 Box:
一个示例如下:
(1)结构
(2)数据
(3)成分
子 Box:mvhd
用于简单描述一些所有媒体共享的信息。
子 Box:trak
track,轨道,用于描述音频流或视频流信息,可以有多个轨道,如上出现了 2 次,分别表示一路音频和一路视频流。
mvhd 作为媒体信息的 header 出现 (注意此 header 不是 box header,而是 moov 媒体信息的 header),用于描述一些所有媒体共享的基本信息。
mvhd 语法继承自 fullbox,注意下述示例出现的 version 和 flags 字段属于 fullbox header。
Box Body:
2.6 trak(track)
一个示例如下:
其中:
2.8 edts(edit Box)
它下边有一个 elst(Edit List Box),它的作用是使某个 track 的时间戳产生偏移。看一下一些字段:
为使 PTS 从 0 开始,media_time 字段一般设置为第一个 CTTS 的值,计算 PTS 和 DTS 的时候,他们分别都减去 media_time 字段的值就可以将 PTS 调整为从 0 开始的值。
如果 media_time 是从一个比较大的值,则表示要求 PTS 值大于该值时画面才进行显示,这时应该将第一个大于或等于该值的 PTS 设置为 0,其他的 PTS 和 DTS 也相应做调整。
2.9 mdia(media box)
一个示例如下:
其中:
注:timescale 同 mvhd 中的 timescale,但是需要注意虽然意义相同,但是值有可能不同,下边 stts,ctts 等时间戳的计算都是以 mdhd 中的 timescale。
2.12 minf(Media Information box)
可分为 “vmhd”、“smhd”、“hmhd” 和“nmhd”,比如视频类型则为 vmhd,音频类型为 smhd。
(1)vmhd
在介绍 stbl box 之前,需要先介绍一下 mp4 中定义的 sample 与 chunk:
stbl box 是一个 container box,是整个 track 中最重要的一个 box,其子 box 描述了该路媒体流的解码相关信息、音视频位置信息、时间戳信息等。
MP4 文件的媒体数据部分在 mdat box 里,而 stbl 则包含了这些媒体数据的索引以及时间信息。
一个示例如下:
其中:
主要存储了编码类型和初始化解码器需要的信息。这里以视频为例,包含子 box:avc1,表示是 H264 的视频。
对于 h264 视频,典型结构如下:
其上 (只列出 avc1 与 avcC,其余 box 可忽略):
avcC(AVC Video Stream Definition Box),存储 sps && pps,即在 ISO/IEC 14496-15 中定义的 AVCDecoderConfigurationRecord 结构
注:在 srs 中,解析 avcc/AVCDecoderConfigurationRecord 结构解析参见 srs/trunk/src/kernel/srs_kerner_codec.cpp::SrsFormat::avc_demux_sps_pps() 函数。
对于 aac 音频,典型结构如下:
可以看到,aac stsd 结构比较复杂,box 众多。实际上,在 ISO/IEC 14496-3 中,定义了 AudioSpecificConfig 类型,aac stsd 结构主要信息就来自 AudioSpecificConfig。
具体不做分析,可以参看 srs 中:
这里为了节约条目的个数,采用了压缩存储的方式,即 sample_count 个连续的 sample 如果 sample_delta 时长一样,那么用一个条目就能表示了。
一个音频 track 的示例如下:
一个视频 track 的示例如下:
注意:
它包含 media 中的关键帧的 sample 表。关键帧是为了支持随机访问。如果此表不存在,说明每一个 sample 都是一个关键帧。
一个视频示例如下:
存储了该 track 中每个 sample 与 chunk 的映射关系。
一个音频示例如下:
包含 sample 的数量和每个 sample 的字节大小,这个 box 相对来说体积比较大的。表明视频帧或者音频帧大小,FFmpeg 里面的 AVPacket 的 size 数据大小,就是从这个 box 中来的。
stco 有两种形式,如果你的视频过大的话,就有可能造成 chunkoffset 超过 32bit 的限制。所以,这里针对大 Video 额外创建了一个 co64 的 Box。它的功效等价于 stco,也是用来表示 sample 在 mdat box 中的位置。只是,里面 chunk_offset 是 64bit 的。
用户自定义数据。