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
endAdd 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.
Summary
Functions
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).
Whether a conversion of this kind exists under id (started, at least).
A file of a conversion, by name.
A stable id for a release, from its name and size: 16 lowercase hex characters.
The video file of a part (media.mp4, media-1800.mp4).
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.
A part's file name: media.mp4 for the whole file, media-1800.mp4 for a part.
The query a part's playlists carry (?from=1800); nothing for the whole file.
The starts of the parts on disk, in seconds, earliest first.
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).
Enough of a part is written to start playing: two fragments of every file, or all of it.
Ids converting right now.
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.
Where a part begins, in seconds of the film: the first fragment's time (0 for the whole file).
End the conversion, every part of it (what is written stays).
The subtitle tracks written out of the release: [%{file: "sub0", lang: "fre", index: 2}].
The stream is being watched: keep converting.
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.
Functions
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).
Whether a conversion of this kind exists under id (started, at least).
A file of a conversion, by name.
A stable id for a release, from its name and size: 16 lowercase hex characters.
The video file of a part (media.mp4, media-1800.mp4).
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.
A part's file name: media.mp4 for the whole file, media-1800.mp4 for a part.
The query a part's playlists carry (?from=1800); nothing for the whole file.
The starts of the parts on disk, in seconds, earliest first.
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).
Enough of a part is written to start playing: two fragments of every file, or all of it.
Ids converting right now.
@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:nameand: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).
Where a part begins, in seconds of the film: the first fragment's time (0 for the whole file).
End the conversion, every part of it (what is written stays).
The subtitle tracks written out of the release: [%{file: "sub0", lang: "fre", index: 2}].
The stream is being watched: keep converting.
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.