mp4
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.
| Function | Does |
|---|---|
demuxer() -> Demuxer | A demuxer at the start of a file |
d.need() -> ?Range | The 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 Track | The tracks, once the moov is read |
d.next_sample(track) -> ?Sample | The 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) -> i64 | Go 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
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.
| Function | Does |
|---|---|
writer_aac(rate, channels, config) -> Writer | A 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_u8 | The 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: