mp4

tape
import mp4 from "mp4";

var d: mp4.Demuxer = mp4.demuxer();
defer d.free();
// read until the tracks are known
while (true) {
    let r: ?mp4.Range = d.need();
    if (r == null) { break; }
    d.give(r!.at, read_bytes(file, r!.at, r!.len)) or {
        io.println("cannot read it: {d.last_error()}");
        return 1;
    };
}
let video: i64 = 0;                           // an index into d.tracks()
while (true) {
    let s: ?mp4.Sample = d.next_sample(video);
    if (s == null) {
        let r: ?mp4.Range = d.need();         // a fragmented file: the next fragment
        if (r == null) { break; }             // the end of the track
        d.give(r!.at, read_bytes(file, r!.at, r!.len)) or { return 1; };
        continue;
    }
    decode(read_bytes(file, s!.offset, s!.size), s!.pts_us);
}

mp4 reads ISO base media files (.mp4, .m4a, .m4v): which tracks they hold, each one’s codec and configuration, and its samples in decoding order, each an offset and size in the file with its times. It is pure and sans-I/O: it never reads a file itself but says which bytes it wants (need), and you hand them over (give), from a file, a part of one or an HTTP range. Progressive files (with the moov first or last) and fragmented ones (moof) are read; sidx makes seeking in a fragmented file reach any time.

FunctionDoes
demuxer() -> DemuxerA demuxer at the start of a file
d.need() -> ?RangeThe bytes it wants next (at, len), or null
d.give(at, data)Those bytes; fewer than asked means the file ends there
d.tracks() -> []const TrackThe tracks, once the moov is read
d.next_sample(track) -> ?SampleThe next sample of a track, or null (its end, or read on: see need). track is an index into tracks(), not a Track’s id
d.seek(track, time_us) -> i64Go to the key frame at or before a time; the time it lands on (track as for next_sample)
d.last_error(), d.free()What went wrong; give the memory back

A Track has kind (Video, Audio, Other), codec (Avc for H.264, Aac, Other with its fourcc), config (the avcC record h264.Decoder.configure_avcc takes, or AAC’s AudioSpecificConfig), timescale, duration_us, width and height or sample_rate and channels, language, end_us (where the edit list ends the presentation: an AAC track’s padding past it is not to be played; 0 when it does not say). A Sample has offset, size, dts and pts in the track’s ticks and dts_us, pts_us in microseconds (the edit list applied: a file with B frames starts presenting at 0), duration and key.

Damaged files give Corrupt with where, never a crash; a box over 256 MB or a track over 16M samples is Unsupported.

Writing

tape
var w: mp4.Writer = mp4.writer_aac(48000, 1, enc.config());   // one AAC track
defer w.free();
w.add(block);                                  // each block from aac's encoder
let file: collections.Vec_u8 = w.finish(enc.priming(), enc.length());
fs.write_file("note.m4a", file.slice()) or { return 1; };

mp4.writer_aac(rate, channels, config) writes an .m4a with one AAC track: the blocks aac.Encoder gives (see aac’s Encoding), in memory. finish(priming, length) returns the whole file: ftyp (M4A ), the moov first so players can start before the file has fully arrived, then the mdat. Its edit list cuts the encoder’s priming and the padding after the input to the sample, so the file plays exactly length frames (length -1: everything after the priming). This is the shape iOS and Android record voice notes in, and both play it.

FunctionDoes
writer_aac(rate, channels, config) -> WriterA writer; config is the encoder’s AudioSpecificConfig
w.add(frame, frames = 1024)One block and its duration in samples
w.finish(priming, length = -1, large = false) -> Vec_u8The file (yours to free); large forces 64-bit offsets
w.free()Give its memory back

The writer keeps the blocks until finish (an hour of a 64 kbit/s voice note is 29 MB); past 4 GiB it writes 64-bit offsets by itself. finish leaves the writer as it was, so you can add more and finish again. The demuxer reads the file back: the track’s end_us is where the edit list ends it, and its first sample’s pts is the negative priming.

Last modified: