
Module mp4ff implements MP4 media file parsing and writing for AVC and HEVC video, AAC and AC-3 audio, stpp and wvtt subtitles, and timed metadata tracks. It is focused on fragmented files as used for streaming in MPEG-DASH, MSS and HLS fMP4, but can also decode and encode all boxes needed for progressive MP4 files.
Some useful command line tools are available in cmd directory.
You can install these tools by going to their respective directory and run go install . or directly from the repo with
go install github.com/Eyevinn/mp4ff/cmd/mp4ff-info@latest
go install github.com/Eyevinn/mp4ff/cmd/mp4ff-encrypt@latest
...
for each individual tool.
This repo is focused on the file format, but goes beyond the base file format and supports codec-specific boxes. The codecs and their boxes are
| Type | Codec | Sample Entry | Config Box | Other Boxes |
|---|---|---|---|---|
| Video | AVC/H.264 | avc1, avc3 | avcC | btrt, pasp, colr |
| Video | HEVC/H.265 | hvc1, hev1 | hvcC | btrt, pasp, colr |
| Video | AV1 | av01 | av1C | btrt, pasp, colr |
| Video | AVS3 | avs3 | av3c | btrt, pasp, colr |
| Video | VP8/VP9 | vp08, vp09 | vpcC | btrt, pasp, colr |
| Video | VVC/H.266 | vvc1, vvi1 | vvcC | btrt, pasp, colr |
| Video | Encrypted | encv | sinf | btrt |
| Audio | AAC | mp4a | esds | btrt |
| Audio | AC-3 | ac-3 | dac3 | btrt |
| Audio | E-AC-3 | ec-3 | dec3 | btrt |
| Audio | AC-4 | ac-4 | dac4 | btrt |
| Audio | Opus | Opus | dOps | btrt |
| Audio | IAMF | iamf | iacb | btrt |
| Audio | MPEG-H 3D Audio | mha1, mha2, mhm1, mhm2 | mhaC | btrt |
| Audio | Encrypted | enca | sinf | btrt |
| Subtitles | WebVTT | wvtt | vttC, vlab | vttc, vtte, vtta, vsid, ctim, iden, sttg, payl, btrt |
| Subtitles | TTML | stpp | - | btrt |
| Subtitles | Generic | evte | - | btrt |
You can also run the tools as a job in Eyevinn Open Source Cloud. Here is an example using the mp4ff-crop command and the Open Source Cloud CLI.
% export OSC_ACCESS_TOKEN=<your-personal-access-token>
% npx -y @osaas/cli@latest create eyevinn-mp4ff test \
-o awsAccessKeyId=<s3-access-key-id> \
-o awsSecretAccessKey=<s3-secret-key> \
-o s3EndpointUrl=https://eyevinnlab-birme.minio-minio.auto.prod.osaas.io \
-o cmdLineArgs="mp4ff-crop s3://input/VINN.mp4 s3://output/VINN-crop2.mp4"
The file VINN.mp4 on the bucket called "input" on the MinIO server at https://eyevinnlab-birme.minio-minio.auto.prod.osaas.io is processed and output uploaded to bucket "output" on the same MinIO server.
Example code for some common use cases is available in the examples directory. The examples and their functions are:
FullSamplemdat in lazy modeThe top-level packages in the mp4ff module are
mp4ff/avc package including parsing of SPS and PPS,
and finding start-codes in Annex B byte streams.The top level structure for both non-fragmented and fragmented mp4 files is mp4.File.
In a progressive (non-fragmented) mp4.File, the top-level attributes Ftyp, Moov, and Mdat point to the corresponding boxes.
A fragmented mp4.File can be more or less complete, like a single init segment,
one or more media segments, or a combination of both, like a CMAF track which renders
into a playable one-track asset. It can also have multiple tracks.
For fragmented files, the following high-level attributes are used:
Init contains a ftyp and a moov box and provides the general metadata for a fragmented file.
It corresponds to a CMAF header. It can also contain one or more sidx boxes.Segments is a slice of MediaSegment which start with an optional styp box, possibly one or more sidx
boxes and then one or moreFragments.Fragment is a mp4 fragment with exactly one moof box followed by a mdat box where the latter
contains the media data. It can have one or more trun boxes containing the metadata
for the samples. The fragment can start with one or more emsg boxes.It should be noted that it is sometimes hard to decide what should belong to a Segment or Fragment.
All child boxes of container boxes such as MoovBox are listed in the Children attribute, but the
most prominent child boxes have direct links with names which makes it possible to write a path such
as
fragment.Moof.Traf.Trun
to access the (only) trun box in a fragment with only one traf box, or
fragment.Moof.Trafs[1].Trun[1]
to get the second trun of the second traf box (provided that they exist). Care must be
taken to assert that none of the intermediate pointers are nil to avoid panic.
A typical use case is to generate a fragmented file consisting of an init segment followed by a series of media segments.
The first step is to create the init segment. This is done in three steps as can be seen in
examples/initcreator:
init := mp4.CreateEmptyInit()
init.AddEmptyTrack(timescale, mediatype, language)
init.Moov.Trak.SetHEVCDescriptor("hvc1", vpsNALUs, spsNALUs, ppsNALUs)
Here the third step fills in codec-specific parameters into the sample descriptor of the single track.
Multiple tracks are also available via the slice attribute Traks instead of Trak.
The second step is to start producing media segments. They should use the timescale that was set when creating the init segment. Generally, that timescale should be chosen so that the sample durations have exact values without rounding errors, e.g. 48000 for 48kHz audio.
A media segment contains one or more fragments, where each fragment has a moof and a mdat box.
If all samples are available before the segment is created, one can use a single
fragment in each segment. Example code for this can be found in examples/segmenter.
For low-latency MPEG-DASH generation, short-duration fragments are added to the segment as the
corresponding media samples become available.
A simple, but not optimal, way of creating a media segment is to first create a slice of FullSample with the data needed.
The definition of mp4.FullSample is
mp4.FullSample{
Sample: mp4.Sample{
Flags uint32 // Flag sync sample etc
Dur uint32 // Sample duration in mdhd timescale
Size uint32 // Size of sample data
Cto int32 // Signed composition time offset
},
DecodeTime uint64 // Absolute decode time (offset + accumulated sample Dur)
Data []byte // Sample data
}
The mp4.Sample part is what will be written into the trun box.
DecodeTime is the media timeline accumulated time.
The DecodeTime value of the first sample of a fragment, will
be set as the BaseMediaDecodeTime in the tfdt box.
Once a number of such full samples are available, they can be added to a media segment like
seg := mp4.NewMediaSegment()
frag := mp4.CreateFragment(uint32(segNr), mp4.DefaultTrakID)
seg.AddFragment(frag)
for _, sample := range samples {
frag.AddFullSample(sample)
}
This segment can finally be output to a w io.Writer as
err := seg.Encode(w)
or to a sw bits.SliceWriter as
err := seg.EncodeSW(sw)
For multi-track segments, the code is a bit more involved. Please have a look at examples/segmenter
to see how it is done. A more optimal way of handling media sample is
to handle them lazily, or using intervals, as explained next.
For video and audio, the dominating part of a mp4 file is the media data which is stored
in one or more mdat boxes. In some cases, for example when segmenting large progressive
files, it is much more memory efficient to just read the movie or fragment metadata
from the moov or moof box and defer the reading of the media data from the mdat box
to later.
For decoding, this is supported by running mp4.DecodeFile() in lazy mode as
parsedMp4, err = mp4.DecodeFile(ifd, mp4.WithDecodeMode(mp4.DecModeLazyMdat))
In this case, the media data of the mdat box will not be read, but only its size is being saved.
To read or copy the actual data corresponding to a sample, one must calculate the
corresponding byte range and either call
func (m *MdatBox) ReadData(start, size int64, rs io.ReadSeeker) ([]byte, error)
or
func (m *MdatBox) CopyData(start, size int64, rs io.ReadSeeker, w io.Writer) (nrWritten int64, err error)
Example code for this, including lazy writing of mdat, can be found in examples/segmenter
with the lazy mode set.
The use of the interfaces io.Reader and io.Writer for reading and writing boxes gives a lot of
flexibility, but is not optimal when it comes to memory allocation. In particular, the
Read(p []byte) method needs a slice p of the proper size to read data, which leads to a
lot of allocations and copying of data.
In order to achieve better performance, it is advantageous to read the full top level boxes into
one, or a few, slices and decode these.
To enable that mode, version 0.27 of the code introduced Decode<X>SR(sr bits.SliceReader)
methods to every box <X> where mp4ff.bits.SliceReader is an interface.
For example, the TrunBox gets the method DecodeTrunSR(sr bits.SliceReader) in addition to its old
DecodeTrun(r io.Reader) method. The bits.SliceReader interface provides methods to read all kinds
of data structures from an underlying slice of bytes. It has an implementation bits.FixedSliceReader
which uses a fixed-size slice as underlying slice, but one could consider implementing a growing version
which would get its data from some external source.
The memory allocation and speed improvements achieved by this may vary, but should be substantial, especially compared to versions before 0.27 which used an extra `io.Lim
$ claude mcp add mp4ff \
-- python -m otcore.mcp_server <graph>