Reads the shape of a fragmented MP4 as ffmpeg writes it: the header (ftyp +
moov) and then moof + mdat pairs, one per fragment. A file still being
written is read as far as it is complete, so a playlist can be made of it
while it grows — which is how a conversion plays as it runs, with the server
spending a fraction of a core instead of an HLS segmenter's worth.
{:ok, info} = Fmp4.scan("media.mp4")
info.fragments
#=> [%{offset: 1255, size: 20569, time: 0.0, duration: 2.028}, …]
Fmp4.playlist(info, "media.mp4", done: false)
#=> "#EXTM3U\n#EXT-X-VERSION:7\n…#EXT-X-MAP:URI=\"media.mp4\",BYTERANGE=\"1255@0\"\n…"Only box sizes, the video track's sample durations and each fragment's
decode time are read; nothing is decoded. Written for ffmpeg's output
(-movflags frag_keyframe+empty_moov+default_base_moof, with +frag_discont
and -copyts for a file begun part-way through a longer one), and read
with :file.pread, so a file of any size costs only its boxes.
Fmp4.RangeFile serves such a file (or any file) to a <video> element
with HTTP range requests, including one that is still growing.
Summary
Types
One complete fragment: where it is in the file and when it is in the film, in seconds.
The file's shape so far: init is the byte length of the header (ftyp +
moov, what EXT-X-MAP points at), timescale the video track's,
start the first fragment's time in seconds (zero for a file written from
the beginning, the position in the film for one begun part-way through).
Functions
The bits per second the fragments were written at, for a master playlist's
BANDWIDTH — nil with no fragment yet.
The seconds of film the fragments hold, from the first one.
The HLS media playlist of what is written so far: the header as the
EXT-X-MAP, then one byte range per fragment, all pointing at uri (the
file's address relative to the playlist's). While the file grows the
playlist is EVENT type and open-ended; a player reloads it and finds more.
done: true closes it with EXT-X-ENDLIST.
The seconds of film written so far: the first fragment's time plus every fragment's length.
The complete fragments of the file. {:error, :no_header} while ftyp and
moov are not both on disk yet (a conversion just started), or a file
error. A file with its header and no fragment yet is {:ok, %{fragments: []}}.
Types
@type fragment() :: %{ offset: non_neg_integer(), size: pos_integer(), duration: float(), time: float() }
One complete fragment: where it is in the file and when it is in the film, in seconds.
@type info() :: %{ init: pos_integer(), timescale: pos_integer(), start: float(), fragments: [fragment()] }
The file's shape so far: init is the byte length of the header (ftyp +
moov, what EXT-X-MAP points at), timescale the video track's,
start the first fragment's time in seconds (zero for a file written from
the beginning, the position in the film for one begun part-way through).
Functions
@spec bitrate(info()) :: pos_integer() | nil
The bits per second the fragments were written at, for a master playlist's
BANDWIDTH — nil with no fragment yet.
The seconds of film the fragments hold, from the first one.
The HLS media playlist of what is written so far: the header as the
EXT-X-MAP, then one byte range per fragment, all pointing at uri (the
file's address relative to the playlist's). While the file grows the
playlist is EVENT type and open-ended; a player reloads it and finds more.
done: true closes it with EXT-X-ENDLIST.
Fragments end at keyframes, so their length is the release's own; the
target duration is the longest fragment, at least :target (default 10),
kept high so it seldom changes once a player has read it. :sequence is
the EXT-X-MEDIA-SEQUENCE (default 0).
The seconds of film written so far: the first fragment's time plus every fragment's length.
The complete fragments of the file. {:error, :no_header} while ftyp and
moov are not both on disk yet (a conversion just started), or a file
error. A file with its header and no fragment yet is {:ok, %{fragments: []}}.