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

A conversion that leaves the video alone.

A browser that can decode a release's video itself (H.264 everywhere, HEVC
on anything Apple makes and on a desktop with a hardware decoder, AV1 on
most) only needs the container changed and the audio put into AAC. So
ffmpeg copies the video into one **fragmented MP4 that grows while it
plays**, and the HLS playlist served for it names byte ranges of that file
(`Fmp4`). That costs a fraction of a core instead of the several a
re-encode takes — and it is the only form of HEVC Safari plays through
HLS: it refuses the segments ffmpeg's own HLS muxer makes of the same
video and plays the MP4 muxer's fragments.

    # the browser said what it decodes when it connected
    caps = Direct.Caps.from_params(params)

    case Direct.start(url, id: id, caps: caps, from: resume_seconds) do
      {:ok, id, part} -> serve the playlist at /stream/id/playlist.m3u8?from=part
      :skip -> this browser cannot decode the video; re-encode instead
      {:error, :unreachable} -> the file could not be read
    end

Add `Direct.Supervisor` to your supervision tree, and tell it where files
go (`config :direct, root: "/var/cache/video"`, or `dir: fn id -> path end`).

## What is written

A release with one audio track becomes one file, `media.mp4`. One with
several becomes a video-only `media.mp4` and one `audioN.mp4` per track,
and the playlist is then a master naming each track as a rendition the
player switches between (`video.m3u8`, `audioN.m3u8` are the media
playlists). Text subtitles inside the release are written out as WebVTT
(`subN.vtt`), listed in `subs.json`. `streams.json` keeps the probe,
`audio.json` the audio tracks, and `done` marks a finished conversion.

## Parts

Coming back to a film half-way through should not mean converting the
half already seen: a conversion can begin at a position (a *part*, named
by its second: `media-1800.mp4`) and keeps the film's own clock, so its
fragments carry the real times and a subtitle written for the whole film
still fits. The playlist of a part lists only what the part has; the
player learns where it starts (`start_at/2`) and asks for another part
when the viewer seeks before it. A part from the beginning keeps the
plain names. Parts closer than `:min_part` seconds (default 60) to the
beginning are not worth it and start from 0.

## Serving

Everything is files in `dir/1`: serve `media.mp4`/`audioN.mp4` with
`Fmp4.RangeFile`, `playlist.m3u8` from `playlist/3`, `video.m3u8` and
`audioN.m3u8` from `media_playlist/3`, and call `touch/1` on every fetch
— a conversion nobody has fetched for `:idle_ms` (default ten minutes)
stops, and what it wrote stays as a cache for you to sweep.

# `dir`

```elixir
@spec dir(String.t()) :: Path.t()
```

Where a conversion's files are: `config :direct, dir: fn id -> path end`
(or `{module, function}`), else `config :direct, root: path` joined with
the id (default `direct` under the system's temporary directory).

# `direct?`

Whether a conversion of this kind exists under `id` (started, at least).

# `file`

A file of a conversion, by name.

# `id_for`

```elixir
@spec id_for(String.t(), term()) :: String.t()
```

A stable id for a release, from its name and size: 16 lowercase hex characters.

# `media`

The video file of a part (`media.mp4`, `media-1800.mp4`).

# `media_playlist`

The HLS media playlist of what is written of one file of a part so far
(`base` is `"media"` or `"audio1"`): one byte range per fragment
(`Fmp4.playlist/3`), ended once ffmpeg has finished. It counts from the
part's start: what the fragments carry says where that is in the film.

# `part_name`

A part's file name: `media.mp4` for the whole file, `media-1800.mp4` for a part.

# `part_query`

The query a part's playlists carry (`?from=1800`); nothing for the whole file.

# `parts`

The starts of the parts on disk, in seconds, earliest first.

# `playlist`

What the player loads for a part: the media playlist of `media.mp4` when
the audio is in it, else a master playlist with the video and one
rendition per audio track. Options: `:prefer` (languages in order of
preference, `["en", "ko"]`: the first the release has is the default
track), `:subs` (subtitle renditions to list, `[%{file: "sub0", lang:
"fre", name: "French"}]`, each served as `file.m3u8`).

# `ready?`

Enough of a part is written to start playing: two fragments of every file, or all of it.

# `running`

Ids converting right now.

# `start`

```elixir
@spec start(String.t(), keyword()) ::
  {:ok, String.t(), non_neg_integer()} | :skip | {:error, term()}
```

Start converting (or join the conversion of) the file at `url`, from
`:from` seconds in: `{:ok, id, part}` when its video is one the browser
with `:caps` decodes — `part` is where the conversion serving it begins
(0 for the whole file, which is reused when it is there already) —
`:skip` when it is not, `{:error, :unreachable}` when the file could not
be probed.

Options:

  * `:id` — what to file the conversion under (16 lowercase hex characters
    are a good shape for an address). Without one it is derived from
    `:name` and `:size` (`id_for/2`), so the same release maps to the same
    directory.
  * `:caps` — what the browser decodes (`Direct.Caps`); default nothing
    beyond H.264.
  * `:from` — seconds into the film to begin at (default 0).
  * `:streams` — the probe, if you already have it (`Direct.Probe.streams/1`).

# `start_at`

Where a part begins, in seconds of the film: the first fragment's time (0 for the whole file).

# `stop`

End the conversion, every part of it (what is written stays).

# `subtitles`

The subtitle tracks written out of the release: `[%{file: "sub0", lang: "fre", index: 2}]`.

# `touch`

The stream is being watched: keep converting.

# `tracks`

The audio tracks of a conversion, as written at its start: `[%{file:
"audio0", lang: "kor", name: "Korean"}]`. One entry (or none) means the
audio is inside `media.mp4`.

---

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