// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// The samples one track contributes to a fragment.
pub(all) struct Run {
track_id : Int32
base_decode_time : Int64
samples : Array[Sample]
} derive(Eq, Debug)
///|
pub extend Run with Eq::{equal, not_equal}
///|
pub extend Run with @moonbitlang/core/debug.Debug::{to_repr}
///|
/// One movie fragment: the `moof` that describes it and the media data that
/// follows, up to the next fragment.
pub(all) struct Fragment {
sequence : Int32
/// Offset of the `moof` in the file.
start : Int32
/// Offset just past the fragment's last byte.
end : Int32
runs : Array[Run]
} derive(Eq, Debug)
///|
pub extend Fragment with Eq::{equal, not_equal}
///|
pub extend Fragment with @moonbitlang/core/debug.Debug::{to_repr}
///|
/// The fragments of a fragmented file, in order.
///
/// A fragment runs from its `moof` to the next one, so a segment cut on a
/// fragment boundary is a run of whole boxes.
pub fn Mp4::fragments(self : Mp4) -> Array[Fragment] raise Mp4Error {
let out : Array[Fragment] = []
// Offsets are not carried by the tree, so they are recovered by walking the
// top-level boxes in file order. A fragment runs to the next `moof`, which
// means it takes the media data box after it: a fragment cut anywhere else
// would need two requests to play.
let starts = top_level_offsets(self.boxes)
let moofs : Array[Int32] = []
for i = 0; i < self.boxes.length(); i = i + 1 {
if self.boxes[i].is_type("moof") {
moofs.push(i)
}
}
for k = 0; k < moofs.length(); k = k + 1 {
let i = moofs[k]
let box = self.boxes[i]
let start = starts[i]
let end = if k + 1 < moofs.length() {
starts[moofs[k + 1]]
} else {
self.data.length()
}
let sequence = match box.child("mfhd") {
Some(mfhd) => read_u32(mfhd.raw()[:], 4)
None => 0
}
let runs : Array[Run] = []
for traf in box.children_of("traf") {
runs.push(traf_runs(traf, start, end))
}
out.push({ sequence, start, end, runs, })
}
out
}
///|
/// Where each top-level box starts in the file.
fn top_level_offsets(boxes : Array[Box]) -> Array[Int32] {
let out : Array[Int32] = []
let mut at = 0
for box in boxes {
out.push(at)
at = at + box.byte_length()
}
out
}
///|
/// How many bytes this box occupies, header included.
pub fn Box::byte_length(self : Box) -> Int32 {
let payload = self.raw().length()
let children = match self.content {
Children(boxes~, ..) => {
let mut total = 0
for child in boxes {
total = total + child.byte_length()
}
total
}
Bytes(_) => 0
}
let usertype = match self.usertype {
Some(types) => types.length()
None => 0
}
let size = payload + children + 8 + usertype
if self.large || (size + 8).to_int64() > 0xFFFF_FFFFL {
size + 8
} else {
size
}
}
///|
/// The samples one `traf` describes.
fn traf_runs(
traf : Box,
moof_start : Int32,
fragment_end : Int32,
) -> Run raise Mp4Error {
let tfhd = match traf.child("tfhd") {
Some(box) => box.raw()[:]
None => raise Malformed(at=moof_start, want="tfhd", got="none")
}
let flags = read_u24(tfhd, 1)
let track_id = read_u32(tfhd, 4)
let mut cursor = 8
let base_data_offset = if (flags & 0x000001) != 0 {
let value = read_u64(tfhd, cursor)
cursor = cursor + 8
Some(value.to_int())
} else {
None
}
if (flags & 0x000002) != 0 {
cursor = cursor + 4
}
let default_duration = if (flags & 0x000008) != 0 {
let value = read_u32(tfhd, cursor).to_int64()
cursor = cursor + 4
Some(value)
} else {
None
}
let default_size = if (flags & 0x000010) != 0 {
let value = read_u32(tfhd, cursor)
cursor = cursor + 4
Some(value)
} else {
None
}
let default_flags = if (flags & 0x000020) != 0 {
let value = read_u32(tfhd, cursor)
cursor = cursor + 4
Some(value)
} else {
None
}
// Without an explicit base, sample data sits in the movie fragment that
// describes it; the older rule points at the start of the file instead.
let base = match base_data_offset {
Some(offset) => offset
None => moof_start
}
let base_decode_time = match traf.child("tfdt") {
Some(tfdt) => {
let raw = tfdt.raw()[:]
if read_u8(raw, 0) == 1 {
read_u64(raw, 4)
} else {
read_u32(raw, 4).to_int64()
}
}
None => 0L
}
let samples : Array[Sample] = []
let mut decode_time = base_decode_time
for trun in traf.children_of("trun") {
let raw = trun.raw()[:]
let flags = read_u24(raw, 1)
let count = read_u32(raw, 4)
let mut cursor = 8
let data_offset = if (flags & 0x000001) != 0 {
let value = read_u32(raw, cursor)
cursor = cursor + 4
Some(value)
} else {
None
}
let first_flags = if (flags & 0x000004) != 0 {
let value = read_u32(raw, cursor)
cursor = cursor + 4
Some(value)
} else {
None
}
let mut offset = match data_offset {
Some(value) => base + value
None => base
}
if offset < 0 || offset > fragment_end {
// A run that points outside its fragment cannot be read, and pretending
// otherwise would hand a caller bytes from the wrong stream.
raise Malformed(
at=moof_start,
want="sample data inside the fragment",
got=offset.to_string(),
)
}
for i = 0; i < count; i = i + 1 {
let duration = if (flags & 0x000100) != 0 {
let value = read_u32(raw, cursor).to_int64()
cursor = cursor + 4
value
} else {
default_duration.unwrap_or(0L)
}
let size = if (flags & 0x000200) != 0 {
let value = read_u32(raw, cursor)
cursor = cursor + 4
value
} else {
default_size.unwrap_or(0)
}
let sample_flags = if (flags & 0x000400) != 0 {
let value = read_u32(raw, cursor)
cursor = cursor + 4
value
} else if i == 0 {
match first_flags {
Some(value) => value
None => default_flags.unwrap_or(0)
}
} else {
default_flags.unwrap_or(0)
}
let composition = if (flags & 0x000800) != 0 {
let value = read_u32_unsigned(raw, cursor)
cursor = cursor + 4
value
} else {
0L
}
samples.push({
offset,
size,
dts: decode_time,
duration,
composition_offset: composition,
// The non-sync bit is the sixteenth from the bottom of the flags.
sync: (sample_flags & 0x0001_0000) == 0,
})
offset = offset + size
decode_time = decode_time + duration
}
}
{ track_id, base_decode_time, samples, }
}
///|
/// A segment index, `sidx` (ISO/IEC 14496-12 ยง8.16.3).
///
/// It says where the pieces of a fragmented stream are, in a form a player can
/// read without downloading them, which is how a DASH client knows what to
/// fetch next.
pub(all) struct SegmentIndex {
reference_id : Int32
timescale : Int64
earliest_presentation_time : Int64
first_offset : Int64
references : Array[IndexReference]
} derive(Eq, Debug)
///|
pub extend SegmentIndex with Eq::{equal, not_equal}
///|
pub extend SegmentIndex with @moonbitlang/core/debug.Debug::{to_repr}
///|
/// One piece of a segment index: how big the next piece is and how long it
/// plays, or a pointer to another index.
pub(all) struct IndexReference {
/// Whether this points at another index rather than at media.
reference_type : Bool
/// How many bytes the piece takes, or how large the index it points at is.
referenced_size : Int64
subsegment_duration : Int64
starts_with_sap : Bool
sap_type : Int32
sap_delta_time : Int64
} derive(Eq, Debug)
///|
pub extend IndexReference with Eq::{equal, not_equal}
///|
pub extend IndexReference with @moonbitlang/core/debug.Debug::{to_repr}
///|
/// Read a `sidx` box.
pub fn segment_index(box : Box) -> SegmentIndex raise Mp4Error {
let raw = box.raw()[:]
let version = read_u8(raw, 0)
let reference_id = read_u32(raw, 4)
let mut at = 8
// Between the last field and the count there are two reserved bytes, which is
// where a reader that counted from the wrong end of the box goes wrong.
let (timescale, earliest, first_offset) = if version == 0 {
let timescale = read_u32(raw, 8).to_int64()
let earliest = read_u32(raw, 12).to_int64()
at = 22
(timescale, earliest, read_u32(raw, 16).to_int64())
} else {
let timescale = read_u32(raw, 8).to_int64()
let earliest = read_u64(raw, 12)
at = 30
(timescale, earliest, read_u64(raw, 20))
}
let count = read_u16(raw, at)
at = at + 2
let references : Array[IndexReference] = []
for i = 0; i < count; i = i + 1 {
let word = read_u32(raw, at + i * 12)
let duration = read_u32(raw, at + i * 12 + 4).to_int64()
let sap = read_u32(raw, at + i * 12 + 8)
references.push({
reference_type: (word & 0x8000_0000) != 0,
referenced_size: (word & 0x7FFF_FFFF).to_int64(),
subsegment_duration: duration,
starts_with_sap: (sap & 0x8000_0000) != 0,
sap_type: (sap >> 28) & 0x7,
sap_delta_time: (sap & 0x0FFF_FFFF).to_int64(),
})
}
{
reference_id,
timescale,
earliest_presentation_time: earliest,
first_offset,
references,
}
}
///|
/// Write a segment index back as a `sidx` box.
///
/// The version is chosen from the values: a presentation time or an offset past
/// what 32 bits hold needs the wide form, and a reader that assumed the narrow
/// one would run into the next field.
pub fn SegmentIndex::encode(self : SegmentIndex) -> Bytes {
let wide = self.earliest_presentation_time > 0xFFFF_FFFFL ||
self.first_offset > 0xFFFF_FFFFL
let buf = @buffer.Buffer()
buf.write_byte(if wide { b'\x01' } else { b'\x00' })
buf.write_byte(b'\x00')
buf.write_byte(b'\x00')
buf.write_byte(b'\x00')
write_u32(buf, self.reference_id)
write_u32(buf, self.timescale.to_int())
if wide {
write_u64(buf, self.earliest_presentation_time)
write_u64(buf, self.first_offset)
} else {
write_u32(buf, self.earliest_presentation_time.to_int())
write_u32(buf, self.first_offset.to_int())
}
// Two reserved bytes, then the count of references.
buf.write_byte(b'\x00')
buf.write_byte(b'\x00')
buf.write_byte(((self.references.length() >> 8) & 0xFF).to_byte())
buf.write_byte((self.references.length() & 0xFF).to_byte())
for reference in self.references {
let kind = if reference.reference_type { 0x8000_0000 } else { 0 }
write_u32(buf, kind | (reference.referenced_size.to_int() & 0x7FFF_FFFF))
write_u32(buf, reference.subsegment_duration.to_int())
let sap = (if reference.starts_with_sap { 0x8000_0000 } else { 0 }) |
((reference.sap_type & 0x7) << 28) |
(reference.sap_delta_time.to_int() & 0x0FFF_FFFF)
write_u32(buf, sap)
}
Box::bytes("sidx", buf.contents()).encode()
}