快速了解 MP4
参考资料和资源
什么是MP4?我们都知道它是一种用于播放带声音的视频的文件格式。Netflix、YouTube、Instagram等平台都使用MP4进行视频流传输,iPhone也用它来录制视频。但是,它是如何工作的?如何使用?它的字节结构是什么?什么是容器?
本指南是对 MP4 文件格式的介绍和快速入门,MP4 文件格式也称为 ISO 基本媒体文件格式 ( ISO-BMFF MPEG-4 Part 14 )。我知道,这名字听起来很专业。
本指南不会详细介绍播放细节,但会更多地介绍通常被称为 MP4 盒结构的 MP4 字节格式。
介绍
MPEG-4 Part 14 (MP4) 是最常见的视频容器格式之一,其扩展名为 .mp4 .mp4。您可能已经了解其他容器格式,例如wav.cd mov、.mdmp3或最近出现的webm.md。容器“包含”视频或音频轨道,或两者兼有。它还可以支持嵌入式字幕轨道。
MP4 是 ISO 基本媒体文件格式 ( ISOBMFF,MPEG-4 第 12 部分)的扩展,该格式旨在包含定时媒体信息。
ISO-BMFF 格式直接基于QuickTime,因此 MP4 本质上与 QuickTime 文件格式相同。
MPEG-4 Part 14 - MP4 File Format
┌──────────────────────────────────────┐
├────────────────────┐ │
│ ISO Base Media │ │
│ File Format │ MP4 Extension │
│ (MPEG-4 Part 12) │ │
└────────────────────┴─────────────────┘
为了充分了解 MP4 的结构,您需要获取一份ISO文档:
- 14496-12 – MPEG-4 第 12 部分
- 14496-14 - MPEG-4 第 14 部分
通过谷歌搜索应该可以找到一些资源,获取PDF文件。
容器里装的是什么?
由于 MP4 是一种容器格式,它实际上并不处理视频和音频流的解码,它只是将它们作为轨道及其元数据包含在内。
容器可以存储以下部分信息:
- 通用元数据,例如文件类型和兼容性
- 视频、音频和字幕轨道以及编解码器详情
- 元数据:时长、时间刻度、比特率、宽度/高度等
- 渐进式和碎片化的元数据细节
- 一系列视频帧或音频样本,称为“样本数据”。
这是玩家解码和播放内容所需的所有信息。
从宏观层面来看,MP4 文件的结构通常如下所示:
video.mp4
├───general file metadata
├───movie data
├───tracks
│ ├───video
│ │ ├───video metadata
│ │ └───video sample data
│ └───audio
│ ├───audio metadata
│ └───audio sample data
└───more metadata
电影盒子
根据 QuickTime 规范,MP4 字节结构由一系列称为“原子”的“盒子”组成。每个盒子描述并包含用于构建 MP4 容器格式的数据。
盒子通常有一个四字母名称,也称为FourCC。这是完整盒子名称的缩写形式,足以容纳在 4 个字节中。这对于以字节格式读取和写入盒子数据至关重要。
在深入探讨字节结构细节之前,这里提供一个比上面的高级视图更技术性的 MP4 盒树视图:
video.mp4
├───ftyp -------------------> FileType Box
├───mdat -------------------> Movie Data Box
├───moov -------------------> Movie Boxes
│ ├───trak ---------------> Track Box
│ │ ├─── tkhd ----------> Track Header
│ │ └─── mdia ----------> Media Box
│ │ └─── ...
│ └───trak
│ │ ├─── tkhd ----------> Track Header
│ │ └─── mdia ----------> Media Box
│ │ └─── ...
└───udta -------------------> Userdata Box
这只是一个简化的视图。然而,MP4规范中定义了更多类型的框。
盒子里装的是什么?
MP4“盒子”包含的信息刚好足以读取和解析盒子名称、大小和数据。
每个方框都有不同的用途,包含特定数据的详细信息。有些方框描述文件类型,有些则会描述编解码器细节、图像分辨率、帧速率、时长、采样大小等等。此外,还有一些方框包含编码后的视频和音频数据。
一个盒子通常包含以下基本信息:
- 盒子大小(以字节为单位)
- 盒子名称(FourCC)
- 盒子数据
┌─────────────────────┐
| Box Header |
| Size (4) | Type (4) | Box Header = 8 Bytes
| --------------------|
| Box Data (N) | Box Data = N Bytes
└─────────────────────┘
└─────────── Box Size = 8 + N bytes
这些信息足以让我们知道如何解析一个视频框,再加上 MP4 规范文档,就能理解视频框的各个字段。
解析一个盒子
那么,让我们来解析第一个盒子吧!
如上所述,每个方框的前 8 个字节称为“方框头”,其中前 4 个字节表示方框的大小,后 4 个字节表示方框的名称。您需要知道这两个值才能逐字节地遍历和解析每个方框。
以下是一个盒子头部结构示例:
type Box struct {
Size int32
Name string
}
读取每个原子中的盒子数据需要知道每个待解析盒子的大小、名称和字节结构。您可以参考 MPEG-4 第 14 部分规范来获取每个已知盒子的字节结构,或者直接参考一些现有的 MP4 解析开源代码。
根据规格说明,该ftyp包装盒具有以下结构:
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
}
major_brand– 是品牌标识符minor_version– 是一个信息丰富的整数,表示主品牌的次要版本。compatible_brands——是方框末尾列出的品牌列表。
例如,读取 FileTypeBox( ftyp) 函数的代码如下所示(使用 Golang):
type FtypBox struct {
*Box
MajorBrand string
MinorVersion uint32
CompatibleBrands []string
}
func (b *FtypBox) parse() {
data := b.ReadBoxData() // Read box header.
b.MajorBrand = string(data[0:4])
b.MinorVersion = binary.BigEndian.Uint32(data[4:8])
if len(data) > 8 {
for i := 8; i < len(data); i += 4 {
b.CompatibleBrands = append(b.CompatibleBrands, string(data[i:i+4]))
}
}
}
回顾以上内容:
- 将盒子头部的前 4 个字节读取为无符号 32 位整数(大端序),即可得到盒子大小:
32 bytes。 - 接下来的 4 个字节给出:
0x66747970十六进制值,或ftyp字符串值。 - 接下来的 4 个字节给出了主要品牌:
0x69736F6D以十六进制或isom字符串形式表示。 - 接下来的 4 个字节表示次版本号:
512 - 接下来的 16 个字节,一次读取一个
uint32be字符串,得到一个兼容品牌数组:isom,,,和。iso2avc1mp41 32 bytes我们已读取了框标题中定义的总数。
请参阅以下最小示例:
https://gist.github.com/alfg/7375aee32fda490de4bf62fbced49d2e#file-mp4_example-go
$ go run mp4.go tears-of-steel.mp4
ftyp.name: ftyp
ftyp.major_brand: isom
ftyp.minor_version: 512
ftyp.compatible_brands: [isom iso2 avc1 mp41]
如果用十六进制编辑器打开mp4文件,你会看到类似这样的方框内容ftyp:
0x00 00 00 00 20 66 74 79 70 | 69 73 6F 6D 00 00 02 00 ... ftypisom....
0x10 69 73 6F 6D 69 73 6F 32 | 61 76 63 31 6D 70 34 31 isomiso2avc1mp41
下一个盒子!
现在我们已经阅读完了这个ftyp框,可以继续阅读下一个框的标题,也就是这个moov框:
type MoovBox struct {
*Box
Mvhd *MvhdBox
}
func (b *MoovBox) parse() {
boxes := readBoxes(b.Reader, b.Start+BoxHeaderSize, b.Size-BoxHeaderSize)
for _, box := range boxes {
switch box.Name {
case "mvhd":
b.Mvhd = &MvhdBox{Box: box}
b.Mvhd.parse()
}
}
这个moov盒子里包含一个嵌套的mvhd盒子,所以我们还需要定义mvhd:
type MvhdBox struct {
*Box
Flags uint32
Version uint8
CreationTime uint32
ModificationTime uint32
Timescale uint32
Duration uint32
Rate Fixed32
Volume Fixed16
}
func (b *MvhdBox) parse() {
data := b.ReadBoxData()
b.Version = data[0]
b.Timescale = binary.BigEndian.Uint32(data[12:16])
b.Duration = binary.BigEndian.Uint32(data[16:20])
b.Rate = fixed32(data[20:24])
b.Volume = fixed16(data[24:26])
}
moov请查看包含方框的示例mvhd:
https://gist.github.com/alfg/7375aee32fda490de4bf62fbced49d2e#file-mp4_example_2-go
$ go run mp4.go tears-of-steel.mp4
ftyp.name: ftyp
ftyp.major_brand: isom
ftyp.minor_version: 512
ftyp.compatible_brands: [isom iso2 avc1 mp41]
moov.name: moov 3170
moov.mvhd.name: mvhd
moov.mvhd.version: 0
moov.mvhd.volume: 1
EOF
现在我们已经解析了 3 个方框,希望您能对如何实现更多解析有所了解。使用读取器时,这个过程是迭代的:
- 阅读盒子标题,其中包含盒子尺寸和名称。
- 请参考规范以阅读和/或跳过字段。
- 跳过盒子大小中剩余的字节。
- 阅读下一个方框(或跳过)。
需要注意以下几点:
- 有些盒子有多个版本,因此盒子的结构和整体尺寸可能有所不同。
- 您可以跳过属性,但读者必须知道要跳过多少字节。
MPEG-4 Part 14随着这些年来越来越多的设备被加入到MP4产品中,MP4的规格也出现了多种变化。- 分段式 MP4 文件 (fMP4) 被分割成一系列的
moof方框mdat。这种方式更常见,也更适合流媒体传输。我将在以后的文章中详细介绍。
感谢阅读!
想要查看更完整的 Go 语言读取 MP4 盒子的示例,请访问:
https://github.com/alfg/mp4
我还有一个用 Rust 编写的更高级的 MP4 读取器和写入器:
https://github.com/alfg/mp4rs
我强烈推荐使用以下一些工具来检查 MP4 文件:
我的GitHub地址是:https://github.com/alfg
祝您编程愉快!🎥
参考资料和资源
- https://developer.apple.com/library/archive/documentation/QuickTime/QTFF
- https://en.wikipedia.org/wiki/MPEG-4_Part_14
- https://en.wikipedia.org/wiki/ISO/IEC_base_media_file_format
- https://en.wikipedia.org/wiki/Comparison_of_video_container_formats
- https://en.wikipedia.org/wiki/FourCC
- https://gist.github.com/alfg/7375aee32fda490de4bf62fbced49d2e
- https://github.com/alfg