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

fragment()

@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.

info()

@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

bitrate(arg1)

@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.

duration(map)

@spec duration(info()) :: float()

The seconds of film the fragments hold, from the first one.

playlist(map, uri, opts \\ [])

@spec playlist(info(), String.t(), keyword()) :: String.t()

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).

reach(map)

@spec reach(info()) :: float()

The seconds of film written so far: the first fragment's time plus every fragment's length.

scan(path)

@spec scan(Path.t()) :: {:ok, info()} | {:error, term()}

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: []}}.