# `Fmp4`
[🔗](https://github.com/alexdont/fmp4/blob/v0.1.0/lib/fmp4.ex#L1)

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.

# `fragment`

```elixir
@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`

```elixir
@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).

# `bitrate`

```elixir
@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`

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

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

# `playlist`

```elixir
@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`

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

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

# `scan`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
