mp4ff 包实现了 MP4 媒体文件的解析和写入,支持 AVC 和 HEVC 视频、AAC 和 AC-3 音频以及 stpp 和 wvtt 字幕。它主要针对 DASH、MSS 和 HLS fMP4 流媒体所使用的分段文件,但也可以解码和编码渐进式 MP4 文件所需的所有盒子。特别是,mp4ff-crop
工具可用于裁剪渐进式文件。
命令行工具
cmd
目录中提供了一些有用的命令行工具。
mp4ff-info
打印 mp4 文件的盒子层级树结构,并提供盒子信息。可以使用-l
选项增加详细程度,如-l all:1
适用于所有盒子,或-l trun:1,stss:1
适用于特定盒子。mp4ff-pslister
从 mp4 或字节流(Annex B)文件中提取并显示 AVC 或 HEVC 的 SPS 和 PPS。对于 HEVC,会打印部分信息。mp4ff-nallister
列出渐进式或分段文件中视频的 NALU 和图像类型。mp4ff-subslister
列出 wvtt 或 stpp(ISOBMFF 中的 WebVTT 或 TTML)字幕样本的详细信息。mp4ff-crop
将渐进式 mp4 文件裁剪到指定时长。mp4ff-encrypt
使用 cenc 或 cbcs 通用加密方案加密分段文件。mp4ff-decrypt
解密使用 cenc 或 cbcs 通用加密方案加密的分段文件。
你可以通过进入各个工具的目录并运行 go install .
来安装这些工具,或直接从仓库安装:
go install github.com/Eyevinn/mp4ff/cmd/mp4ff-info@latest
示例代码
examples
目录中提供了示例代码。
这些示例及其功能如下:
initcreator
为视频和音频创建典型的初始化段(ftyp + moov)。resegmenter
读取分段文件(CMAF 轨道)并使用fullSample
以其他段持续时间重新分段。segmenter
接受渐进式 mp4 文件并从中创建初始化段和媒体段。该工具已扩展以支持生成多轨道段,以及以惰性模式读写mdat
。multitrack
解析具有多个轨道的分段文件。combine-segs
将单轨道初始化段和媒体段合并为多轨道段。
库
该库在 mp4ff/mp4
包中提供了解析(称为 Decode)和写入(Encode)的功能。它还包含 mp4ff.avc
包中的 AVC/H.264 编解码器特定解析,包括完整的 SPS 和 PPS 解析。HEVC/H.265 解析不太完整,可在 mp4ff.hevc
中使用。补充增强信息可以使用 mp4ff.sei
包进行解析和写入。
可以解析和解码传统的非分段多路复用 mp4 文件,但重点是 DASH、HLS 和 CMAF 中使用的分段 mp4 文件。
除了单轨道分段文件外,还增加了对解析和生成多轨道分段文件的支持,可以在 examples/segment
和 examples/multitrack
中看到。
非分段和分段 mp4 文件的顶层结构都是 mp4.File
。
在渐进式(非分段)mp4.File
中,顶层属性 Ftyp、Moov 和 Mdat 指向相应的盒子。
分段 mp4.File
可以或多或少地完整,如单个初始化段、一个或多个媒体段,或两者的组合,如可渲染成可播放单轨道资产的 CMAF 轨道。它也可以有多个轨道。
对于分段文件,使用以下高级属性:
Init
包含ftyp
和moov
盒子,提供分段文件的一般元数据。它对应于 CMAF 头部。它还可以包含一个或多个sidx
盒子。Segments
是MediaSegment
的切片,以可选的styp
盒子开始,可能有一个或多个sidx
盒子,然后是一个或多个Fragment
。Fragment
是一个 mp4 片段,恰好有一个moof
盒子,后跟一个包含媒体数据的mdat
盒子。它可以有一个或多个trun
盒子,包含样本的元数据。
容器盒子(如 MoovBox
)的所有子盒子都列在 Children
属性中,但最重要的子盒子有直接链接的名称,这使得可以写出如下路径:
fragment.Moof.Traf.Trun
以访问只有一个 traf
盒子的片段中的(唯一)trun
盒子,或
fragment.Moof.Trafs[1].Trun[1]
以获取第二个 traf
盒子的第二个 trun
(如果存在)。必须注意确保中间指针都不为 nil,以避免 panic
。
创建新的分段文件
一个典型的用例是创建一个由初始化段后跟一系列媒体段组成的片段。
第一步是创建初始化段。这可以通过三个步骤完成,如 examples/initcreator
中所示:
init := mp4.CreateEmptyInit()
init.AddEmptyTrack(timescale, mediatype, language)
init.Moov.Trak.SetHEVCDescriptor("hvc1", vpsNALUs, spsNALUs, ppsNALUs)
这里第三步将编解码器特定参数填入单个轨道的样本描述符中。多个轨道也可以通过 Traks
切片属性而不是 Trak
来使用。
第二步是开始生成媒体段。它们应该使用创建初始化段时设置的时间刻度。通常,应选择时间刻度使样本持续时间具有精确值,无需四舍五入。
媒体段包含一个或多个片段,每个片段都有一个 moof
和一个 mdat
盒子。如果在创建段之前所有样本都可用,可以在每个段中使用单个片段。这方面的示例代码可以在 examples/segmenter
中找到。
创建媒体段的一种简单但不优化的方法是首先创建一个包含所需数据的 FullSample
切片。mp4.FullSample
的定义如下:
mp4.FullSample{
Sample: mp4.Sample{
Flags uint32 // 标记同步样本等
Dur uint32 // 样本持续时间(以 mdhd 时间刻度为单位)
Size uint32 // 样本数据大小
Cto int32 // 有符号合成时间偏移
},
DecodeTime uint64 // 绝对解码时间(偏移量 + 累积样本 Dur)
Data []byte // 样本数据
}
mp4.Sample
部分是将写入 trun
盒子的内容。DecodeTime
是累积的媒体时间线时间。片段第一个样本的 DecodeTime
值将被设置为 tfdt
盒子中的 BaseMediaDecodeTime
。
一旦有了一定数量的这样的完整样本,就可以将它们添加到媒体段中
seg := mp4.NewMediaSegment()
frag := mp4.CreateFragment(uint32(segNr), mp4.DefaultTrakID)
seg.AddFragment(frag)
for _, sample := range samples {
frag.AddFullSample(sample)
}
最后可以将该片段输出到一个 w io.Writer
中:
err := seg.Encode(w)
对于多轨道片段,代码会稍微复杂一些。请查看 examples/segmenter
了解具体实现。处理媒体样本的更优方式是延迟处理,下面将对此进行解释。
mdat 数据的延迟解码和写入
对于视频和音频,mp4 文件的主要部分是存储在一个或多个 mdat
box 中的媒体数据。在某些情况下,例如对大型渐进式文件进行分段时,只读取 moov
或 moof
box 中的电影或片段数据,并将 mdat
box 中的媒体数据读取推迟到后面,这种方式在内存使用上会更加高效。
要以延迟模式进行解码,可以如下运行 mp4.DecodeFile()
:
parsedMp4, err = mp4.DecodeFile(ifd, mp4.WithDecodeMode(mp4.DecModeLazyMdat))
在这种情况下,不会读取 mdat
box 的媒体数据,只会设置其大小。要读取或复制与样本对应的实际数据,必须计算相应的字节范围,然后调用:
func (m *MdatBox) ReadData(start, size int64, rs io.ReadSeeker) ([]byte, error)
或
func (m *MdatBox) CopyData(start, size int64, rs io.ReadSeeker, w io.Writer) (nrWritten int64, err error)
包括延迟写入 mdat
在内的示例代码可以在 examples/segmenter
中找到,将 lazy
模式设置为开启。
使用 SliceReader 和 SliceWriter 实现更高效的 I/O
使用 io.Reader
和 io.Writer
接口来读写 box 提供了很大的灵活性,但在内存分配方面并不是最佳选择。特别是 Read(p []byte)
方法需要一个适当大小的切片 p
来读取数据,这会导致大量的内存分配和数据复制。
为了获得更好的性能,将顶层 box 完整读入一个或几个切片中进行解码是更有利的。
为了启用该模式,代码的 0.27 版本为每个 box X 引入了 DecodeX(sr bits.SliceReader)
方法,其中 mp4ff.bits.SliceReader
是一个接口。
例如,TrunBox
除了原有的 DecodeTrun(r io.Reader)
方法外,还新增了 DecodeTrunSR(sr bits.SliceReader)
方法。bits.SliceReader
接口提供了从底层字节切片中读取各种数据结构的方法。它有一个实现 bits.FixedSliceReader
,使用固定大小的切片作为底层切片,但也可以考虑实现一个可增长的版本,从某个外部源获取数据。
通过这种方式实现的内存分配和速度改进可能会有所不同,但应该是显著的,特别是与 0.27 版本之前使用额外的 io.LimitReader
层相比。
为了进一步减少读取渐进式文件的 mdat
数据时的内存分配,应该使用某种缓冲读取器。
基准测试
为了研究新的 SliceReader 和 SliceWriter 方法的效率,进行了基准测试。
基准测试定义在 mp4/benchmarks_test.go
和 mp4/benchmarks_srw_test.go
文件中。
对于 DecodeFile
,可以看到从 0.26 版本到 0.27 版本有很大的改进,这两个版本都使用 io.Reader
接口,
但使用 SliceReader
源又有了很大的提升。
后者的基准测试称为 BenchmarkDecodeFileSR
,但在这里为了便于比较,使用了相同的名称。
请注意,这里的分配是指在基准测试循环内进行的堆分配。在循环外,
会分配一个切片来保存输入数据。
对于 EncodeFile
,可以看到使用 io.Writer
接口时,v0.27 实际上比 v0.26 更差。
这是因为代码被重构,所有写入都通过 SliceWriter
层进行,以减少代码重复。
然而,如果直接使用 SliceWriter
方法,在分配方面可以看到很大的相对增益,
如最后一列所示。
名称 \ 操作时间 | v0.26 | v0.27 | v0.27-srw |
---|---|---|---|
DecodeFile/1.m4s-16 | 21.9µs | 6.7µs | 2.6µs |
DecodeFile/prog_8s.mp4-16 | 143µs | 48µs | 16µs |
EncodeFile/1.m4s-16 | 1.70µs | 2.14µs | 1.50µs |
EncodeFile/prog_8s.mp4-16 | 15.7µs | 18.4µs | 12.9µs |
名称 \ 分配操作 | v0.26 | v0.27 | v0.27-srw |
---|---|---|---|
DecodeFile/1.m4s-16 | 120kB | 28kB | 2kB |
DecodeFile/prog_8s.mp4-16 | 906kB | 207kB | 12kB |
EncodeFile/1.m4s-16 | 1.16kB | 1.39kB | 0.08kB |
EncodeFile/prog_8s.mp4-16 | 6.84kB | 8.30kB | 0.05kB |
名称 \ 分配次数 | v0.26 | v0.27 | v0.27-srw |
---|---|---|---|
DecodeFile/1.m4s-16 | 98.0 | 42.0 | 34.0 |
DecodeFile/prog_8s.mp4-16 | 454 | 180 | 169 |
EncodeFile/1.m4s-16 | 15.0 | 15.0 | 3.0 |
EncodeFile/prog_8s.mp4-16 | 101 | 86 | 1 |
Box 结构和接口
大多数 box 都有自己的以 box 命名的文件,但在某些情况下,可能有多个 box 具有相同的内容,这时代码文件会有一个通用名称,如 mp4/visualsampleentry.go
。
Box 接口在 mp4/box.go
中指定。它不包含解码(解析)方法,这些方法对每种 box 类型都有不同的名称并被分发。
解码分发的映射在 mp4.decoders
表中给出,用于 io.Reader
方法,在 mp4.decodersSR
中给出,用于 mp4ff.bits.SliceReader
方法。
如何实现新的 box
要实现一个新的 box fooo
,需要以下步骤:
创建一个文件 fooo.go
并创建一个结构体类型 FoooBox
。
FoooBox
必须实现 Box 接口方法:
Type()
Size()
Encode(w io.Writer)
EncodeSW(sw bits.SliceWriter) // v0.27.0 新增
Info()
它还需要自己的解码方法 DecodeFooo
,必须将其添加到 box.go
中的 decoders
映射中,
以及 v0.27.0 新增的 DecodeFoooSR
方法添加到 decodersSR
中。
一个简单的例子,可以查看 prft.go
中的 PrftBox
。
还应该有一个测试文件 fooo_test.go
,其中包含使用 boxDiffAfterEncodeAndDecode
方法的测试,以检查编码和解码后 box 信息是否相同。
直接更改属性
许多属性是公开的,因此可以自由更改。
这种方式的优点是可以编写代码以多种方式操作 box,
但必须谨慎,避免破坏与子 box 的链接或在 box 中创建不一致的状态。
例如,容器盒子如TrafBox
有一个AddChild
方法,该方法不仅将盒子添加到其子盒子切片Children
中,还会设置一个特定的成员引用(如Tfdt
)指向该盒子。如果直接操作Children
,该链接可能会失效。
编码模式和优化
对于分片文件,可以选择编码mp4.File
中的所有盒子,或只编码包含在初始化段和媒体段中的盒子。控制这一行为的属性称为FragEncMode
。
另一个属性EncOptimize
控制文件编码过程的可能优化。
目前,只有一种可能的优化,称为OptimizeTrun
。
它可以通过在TfhdBox
中查找并写入默认值,并从TrunBox
中省略相应的值来减小TrunBox
的大小。
请注意,这可能会改变trun
所有祖先盒子的大小。
样本编号偏移
按照ISOBMFF标准,样本编号和其他编号从1开始(基于1)。 这适用于函数和方法的参数。 在切片中的实际存储是基于0的,所以 样本编号1在相应的切片中索引为0。
稳定性
API应该相当稳定,但在版本1之前可能会发生轻微的不向后兼容的变化。
规范
MP4文件格式的主要规范是ISO基本媒体文件格式(ISOBMFF)标准 ISO/IEC 14496-12第6版2020。一些盒子在其他标准中有规定,应在代码中注释说明。
许可证
MIT,请参见LICENSE。
pkg/mp4中的一些代码来自或基于https://github.com/jfbus/mp4,其版权为
Copyright (c) 2015 Jean-François Bustarret
。
pkg/bits中的一些代码来自或基于https://github.com/tcnksm/go-casper/tree/master/internal/bits
Copyright (c) 2017 Taichi Nakashima
。
更新日志和版本
请参见CHANGELOG.md。
支持
加入我们的Slack社区,您可以在那里提出任何关于我们开源项目的问题。Eyevinn的咨询业务还可以为您提供:
- 该组件的进一步开发
- 该组件在您平台上的定制和集成
- 支持和维护协议
如果您感兴趣,请联系sales@eyevinn.se。
关于Eyevinn Technology
Eyevinn Technology是一家独立的咨询公司,专门从事视频和流媒体领域。我们独立的方式是不与任何平台或技术供应商有商业关系。作为推动行业前进的创新方式,我们开发概念验证和工具。我们学到的知识和编写的代码,我们通过博客与行业分享,并开源我们编写的代码。
想了解更多关于Eyevinn的信息以及在这里工作的感受吗?请联系我们:work@eyevinn.se!