Documentation
¶
Overview ¶
Package mp4 demuxes ISO base media files (ISO/IEC 14496-12) and their QuickTime kin: the .m4a/.m4b/.mp4 family carrying AAC-LC or ALAC audio. It parses the moov box tree into per-track sample tables, selects the audio track, and hands format.Media codec packets read straight from mdat, with sample-exact seeking over the sample-to-chunk mapping.
Gapless trims come from the iTunes iTunSMPB tag or an edit list, mapped onto Track.Delay/Padding so format.Media delivers the trimmed timeline. Nero (chpl) and QuickTime text chapter tracks are parsed and surfaced through Chapters for probe.
The box parser is the service's widest attack surface, so it holds to the hostile-input invariants strictly: bounded nesting depth, every size validated against remaining input before any allocation, table and sample-count caps, and a progress guarantee on every parse loop. The whole moov is read into a bounded buffer and parsed from memory; mdat stays on disk and only sampled bytes are read.
Index ¶
- Constants
- func InitSegment(t container.Track) ([]byte, error)
- func Match(head []byte) bool
- func PatchFreeform(f FreeformPatcher, key, placeholder, value string) error
- type Chapter
- type Demuxer
- func (d *Demuxer) Brands() []string
- func (d *Demuxer) Chapters() []Chapter
- func (d *Demuxer) ReadPacket(pkt *container.Packet) error
- func (d *Demuxer) SeekSample(track int, sample int64) (int64, error)
- func (d *Demuxer) Tags() map[string][]string
- func (d *Demuxer) Tracks() []container.Track
- func (d *Demuxer) Warnings() []container.Warning
- type DemuxerOptions
- type FreeformPatcher
- type Muxer
- type MuxerOptions
- type ProgressiveMuxer
- type Segment
- type Segmenter
- type SegmenterOptions
Constants ¶
const MatchNeed = 8
MatchNeed is how many leading bytes Match inspects.
const SegmenterVersion = "mp4-seg-3"
SegmenterVersion identifies the segment and init-header box layout for the ADR-0004 cache key: cached segments regenerate when this bumps, so a box-layout fix can never serve stale segments next to fresh ones.
Variables ¶
This section is empty.
Functions ¶
func InitSegment ¶
InitSegment builds the CMAF init header for the track: ftyp plus a moov whose track carries the codec's sample entry, an empty sample table, the movie-extends defaults, and, when the track declares an encoder delay or a known length, an edit list mapping the decode timeline onto the presentation one (the fMP4 gapless convention: the delay is known up front and rides in the init header; end padding is trimmed by the same edit when the length is known). Deterministic: equal tracks yield identical bytes.
func Match ¶
Match reports whether head is an ISO base media file: a leading box whose type is ftyp. It is the format sniff-table entry. Some muxers emit a leading styp or a free/skip box before ftyp; the sniffer only needs the common case, and the ext hint covers the rest.
func PatchFreeform ¶
func PatchFreeform(f FreeformPatcher, key, placeholder, value string) error
PatchFreeform replaces the value of the freeform ilst tag key in a finished file, in place. placeholder must be the exact value the muxer wrote at Begin and value must be the same length, so no byte moves: this is how measured ReplayGain values land in a fragmented MP4 after the encode, which no tag rewriter can restructure. The written atom bytes are matched whole, so a payload that happens to spell the placeholder can never redirect the patch.
Types ¶
type Chapter ¶
Chapter is one parsed chapter marker, timed in the movie timeline. It aliases the container-level type so demuxer chapters feed muxer options (and the metadata mapper) without conversion.
type Demuxer ¶
type Demuxer struct {
// contains filtered or unexported fields
}
Demuxer reads one audio track from an ISO base media file. It selects the sound track, exposes it as a single track (ID 0), and reads sample packets from mdat on demand.
func NewDemuxer ¶
func NewDemuxer(src container.Source, opts *DemuxerOptions) (*Demuxer, error)
NewDemuxer parses the movie header and positions on the first sample. The returned Demuxer implements container.Seeker and container.Warner.
func NewFragmentedDemuxer ¶
NewFragmentedDemuxer reads a bare CMAF/HLS media segment (moof+mdat with no ftyp/moov) using an out-of-band init segment for the codec config, sample entry, mvex defaults, and edit list. The HLS client calls it with the init it fetched from the playlist's EXT-X-MAP; the media Source holds one or more concatenated media segments. A bare segment has no magic to sniff, so it is not in the drivers table and `probe segment.m4s` is not expected to work.
func (*Demuxer) ReadPacket ¶
ReadPacket yields the next sample as a codec packet. Packet data aliases the read window and is reused across calls.
func (*Demuxer) SeekSample ¶
SeekSample lands on a sync sample at or before the target in the raw decoder timeline, backed off by seekPreroll samples so the decoder's inter-frame state converges. format.Media pre-rolls the remainder for a sample-exact landing.
func (*Demuxer) Tags ¶
Tags returns the ilst tags, nil when the file carries none. The map is the demuxer's own and must not be mutated; format.Info hands it on to read-only consumers.
type DemuxerOptions ¶
type DemuxerOptions struct {
// Strict turns tolerated damage (the Warnings list) into errors.
Strict bool
}
DemuxerOptions configures parsing.
type FreeformPatcher ¶
FreeformPatcher is the file access PatchFreeform needs (os.File).
type Muxer ¶
type Muxer struct {
// contains filtered or unexported fields
}
Muxer writes one audio track as a progressive fragmented MP4 (fMP4): an ftyp+moov init header declaring an empty sample table plus a movie-extends (mvex) box, then a moof+mdat fragment per bounded run of samples. Nothing is back-patched, so NeedsSeek reports false and a plain io.Writer streams live. The design is the CMAF/DASH shape the HLS segments reuse.
The muxer carries ALAC and AAC-LC. ALAC is lossless and signals no gapless trims, so a nonzero Delay/Padding is rejected rather than silently dropped. AAC's encoder priming rides in the init header's edit list (delay is known up front; the length joins it when the engine projects one), and when the writer can seek, End patches the edit's duration with the encoder's exact trailer, upgrading a projected or unknown length to the exact one. On a pure stream the init bytes stand as written: full gapless when the length was known, delay-only otherwise (the capability matrix's live fMP4 cell).
func NewMuxer ¶
func NewMuxer(w io.Writer, opts *MuxerOptions) *Muxer
NewMuxer returns a fragmented MP4 muxer writing to w.
func (*Muxer) End ¶
End flushes the final fragment. ALAC carries no trailing gapless padding, so a nonzero trailer trim is rejected.
type MuxerOptions ¶
type MuxerOptions struct {
// FragmentSamples is the target sample count per fragment. Zero selects
// roughly defaultFragmentSeconds of audio at the track rate. A fragment
// closes once it reaches the target (the final one may be shorter).
FragmentSamples int
// Tags embeds canonical metadata fields as iTunes ilst atoms in the
// moov (the keys ilstText and ilstFreeform map; others are skipped).
Tags []container.Tag
// Chapters embeds Nero chpl chapter markers in the moov's udta.
Chapters []container.Chapter
// Art embeds cover art as the ilst covr atom. Init headers are written
// before the first audio byte, so large art delays first audio on a
// live stream; the caller decides (jobs pass it, live streams do not).
Art *container.Picture
}
MuxerOptions configures the fragmented writer.
type ProgressiveMuxer ¶
type ProgressiveMuxer struct {
// contains filtered or unexported fields
}
ProgressiveMuxer writes one audio track as a flat (non-fragmented) MP4: an ftyp, an mdat holding every sample, then a moov whose stbl carries the full sample tables (stsd/stts/stsc/stsz/stco). This is the .m4a form most players and editors expect, the read-side counterpart of the progressive demuxer, and the other direction of the fragmented muxer's symmetry.
It needs a seekable destination (NeedsSeek true): the mdat is written first with a placeholder size and streamed, then the size is back-patched and the moov appended once every sample's size and duration is known. Only the per-sample metadata is buffered, not the audio, so memory stays bounded by the sample count.
It carries the same codecs as the segmenter (Opus, FLAC, AAC-LC, ALAC), reusing seg.go's sample-entry builders so read and write share box construction; gapless rides in the moov edit list, which the demuxer's parseElst reads back.
Chapters, when the options carry any, are written twice: as a QuickTime text track beside the audio (unbounded, the form readers prefer) and as the Nero chpl list in the udta (capped at 255, for the readers that know only that). The text track is the muxer's own, synthesized here rather than accepted as an input track, which is why the single-track contract still holds.
func NewProgressiveMuxer ¶
func NewProgressiveMuxer(w io.Writer, opts *MuxerOptions) *ProgressiveMuxer
NewProgressiveMuxer returns a progressive MP4 muxer writing to w, which must be a destination it can seek (NeedsSeek is true).
func (*ProgressiveMuxer) Begin ¶
func (m *ProgressiveMuxer) Begin(tracks []container.Track) error
Begin validates the track, writes the ftyp, and opens the mdat.
func (*ProgressiveMuxer) End ¶
func (m *ProgressiveMuxer) End(trailer codec.Trailer) error
End back-patches the mdat size and writes the moov with the full sample tables and the gapless edit list.
func (*ProgressiveMuxer) NeedsSeek ¶
func (m *ProgressiveMuxer) NeedsSeek() bool
NeedsSeek reports true: the mdat size is back-patched and the moov is written after the samples.
func (*ProgressiveMuxer) WritePacket ¶
func (m *ProgressiveMuxer) WritePacket(pkt container.Packet) error
WritePacket streams one sample into the mdat and records its size and duration.
type Segment ¶
type Segment struct {
// Index is the segment number, counting the whole stream's segments
// from zero (a segmenter started mid-stream begins at its
// StartSegment).
Index int64
// Data is the segment's bytes. Freshly allocated per segment; the
// caller owns it.
Data []byte
// Samples is the segment's decode duration in track samples.
Samples int64
}
Segment is one emitted media segment: an styp plus one moof+mdat pair, self-contained and independently decodable.
type Segmenter ¶
type Segmenter struct {
// contains filtered or unexported fields
}
Segmenter packs codec packets into numbered CMAF media segments for HLS: each segment is styp plus exactly one moof+mdat pair whose tfdt carries the decode time in track samples (the media timescale is the sample rate). The matching init header comes from InitSegment. Boundaries are sample-counted, so the packet stream must arrive frame-aligned: every packet whole, segment length a frame multiple.
One fragment per segment is load-bearing, not just simple: it makes the mfhd sequence_number (index+1) and the tfdt (index*SegmentSamples) pure functions of the segment index, so a worker restarted mid-stream reproduces a continuous run's bytes exactly. Splitting large segments into several fragments would decouple sequence numbers from segment indexes and break that guarantee (and buys nothing here: a segment is buffered whole before it is emitted either way).
Codecs: Opus, FLAC, and ALAC (the fMP4-capable encoders). Every frame of each is independently decodable, so every sample is a sync sample and segments can begin anywhere on a frame boundary.
func NewSegmenter ¶
func NewSegmenter(t container.Track, opts *SegmenterOptions) (*Segmenter, error)
NewSegmenter validates the track and options and returns a Segmenter. The same track must produce the init header (InitSegment); validation is shared so a track that plans here cannot fail there.
func (*Segmenter) WritePacket ¶
WritePacket appends one packet to the current segment, emitting the segment once it reaches its target length. Packets must not straddle a segment boundary (the caller feeds frame-aligned packets and the target is a frame multiple); one that would is a wiring bug and errors.
type SegmenterOptions ¶
type SegmenterOptions struct {
// SegmentSamples is the decode duration of every segment but the last,
// in track samples. It must be a positive multiple of the codec frame
// so segment boundaries land exactly between packets.
SegmentSamples int
// StartSegment is the index of the first emitted segment; the base
// decode time follows as StartSegment * SegmentSamples. Zero is the
// stream's top.
StartSegment int64
}
SegmenterOptions configures a Segmenter.