Current section
Files
Jump to
Current section
Files
lib/membrane_http_adaptive_stream/sink.ex
defmodule Membrane.HTTPAdaptiveStream.Sink do
@moduledoc """
Sink for generating HTTP streaming manifests.
Uses `Membrane.HTTPAdaptiveStream.Manifest` for manifest serialization
and `Membrane.HTTPAdaptiveStream.Storage` for saving files.
## Notifications
- `{:track_playable, input_pad_id}` - sent when the first segment of a track is
stored, and thus the track is ready to be played
- `{:cleanup, cleanup_function :: (()-> :ok)}` - sent when playback changes
from playing to prepared. Invoking `cleanup_function` lambda results in removing
all the files that remain after the streaming
## Examples
The following configuration:
%#{inspect(__MODULE__)}{
manifest_name: "manifest",
manifest_module: Membrane.HTTPAdaptiveStream.HLS,
storage: %Membrane.HTTPAdaptiveStream.Storages.FileStorage{directory: "output"}
}
will generate a HLS manifest in the `output` directory, playable from
`output/manifest.m3u8` file.
"""
use Bunch
use Membrane.Sink
alias Membrane.CMAF
alias Membrane.HTTPAdaptiveStream.{Manifest, Storage}
def_input_pad :input,
availability: :on_request,
demand_unit: :buffers,
caps: CMAF.Track
def_options manifest_name: [
type: :string,
spec: String.t(),
default: "index",
description: "Name of the main manifest file"
],
manifest_module: [
type: :atom,
spec: module,
description: """
Implementation of the `Membrane.HTTPAdaptiveStream.Manifest`
behaviour.
"""
],
storage: [
type: :struct,
spec: Storage.config_t(),
description: """
Storage configuration. May be one of `Membrane.HTTPAdaptiveStream.Storages.*`.
See `Membrane.HTTPAdaptiveStream.Storage` behaviour.
"""
],
target_window_duration: [
spec: pos_integer | :infinity,
type: :time,
default: Membrane.Time.seconds(5),
description: """
Manifest duration is keept above that time, while the oldest segments
are removed whenever possible.
"""
],
persist?: [
type: :bool,
default: false,
description: """
If true, stale segments are removed from the manifest only. Once
playback finishes, they are put back into the manifest.
"""
],
target_segment_duration: [
type: :time,
default: 0,
description: """
Expected length of each segment. Setting it is not necessary, but
may help players achieve better UX.
"""
]
@impl true
def handle_init(options) do
options
|> Map.from_struct()
|> Map.delete(:manifest_name)
|> Map.delete(:manifest_module)
|> Map.merge(%{
storage: Storage.new(options.storage),
manifest: %Manifest{name: options.manifest_name, module: options.manifest_module},
awaiting_first_segment: MapSet.new()
})
~> {:ok, &1}
end
@impl true
def handle_caps(Pad.ref(:input, id), %CMAF.Track{} = caps, _ctx, state) do
{header_name, manifest} =
Manifest.add_track(
state.manifest,
%Manifest.Track.Config{
id: id,
content_type: caps.content_type,
header_extension: ".mp4",
segment_extension: ".m4s",
target_window_duration: state.target_window_duration,
target_segment_duration: state.target_segment_duration,
persist?: state.persist?
}
)
state = %{state | manifest: manifest}
{result, storage} = Storage.store_header(state.storage, header_name, caps.header)
{result, %{state | storage: storage}}
end
@impl true
def handle_prepared_to_playing(ctx, state) do
demands = ctx.pads |> Map.keys() |> Enum.map(&{:demand, &1})
{{:ok, demands}, state}
end
@impl true
def handle_pad_added(pad, %{playback_state: :playing}, state) do
{{:ok, demand: pad}, state}
end
@impl true
def handle_pad_added(_pad, _ctx, state) do
{:ok, state}
end
@impl true
def handle_start_of_stream(Pad.ref(:input, id), _ctx, state) do
awaiting_first_segment = MapSet.put(state.awaiting_first_segment, id)
{:ok, %{state | awaiting_first_segment: awaiting_first_segment}}
end
@impl true
def handle_write(Pad.ref(:input, id) = pad, buffer, _ctx, state) do
%{storage: storage, manifest: manifest} = state
duration = buffer.metadata.duration
{changeset, manifest} = Manifest.add_segment(manifest, id, duration)
state = %{state | manifest: manifest}
with {:ok, storage} <- Storage.apply_segment_changeset(storage, changeset, buffer.payload),
{:ok, storage} <- serialize_and_store_manifest(manifest, storage) do
{notify, state} = maybe_notify_playable(id, state)
{{:ok, notify ++ [demand: pad]}, %{state | storage: storage}}
else
{error, storage} -> {error, %{state | storage: storage}}
end
end
@impl true
def handle_end_of_stream(Pad.ref(:input, id), _ctx, state) do
%{manifest: manifest, storage: storage} = state
manifest = Manifest.finish(manifest, id)
{store_result, storage} = serialize_and_store_manifest(manifest, storage)
storage = Storage.clear_cache(storage)
state = %{state | storage: storage, manifest: manifest}
{store_result, state}
end
@impl true
def handle_playing_to_prepared(_ctx, state) do
%{
manifest: manifest,
storage: storage,
persist?: persist?
} = state
to_remove = Manifest.all_segments(manifest)
cleanup = fn ->
{result, _storage} = Storage.cleanup(storage, to_remove)
result
end
result =
if persist? do
{result, storage} =
manifest |> Manifest.from_beginning() |> serialize_and_store_manifest(storage)
{result, %{state | storage: storage}}
else
{:ok, state}
end
with {:ok, state} <- result do
{{:ok, notify: {:cleanup, cleanup}}, state}
end
end
defp maybe_notify_playable(id, %{awaiting_first_segment: awaiting_first_segment} = state) do
if MapSet.member?(awaiting_first_segment, id) do
{[notify: {:track_playable, id}],
%{state | awaiting_first_segment: MapSet.delete(awaiting_first_segment, id)}}
else
{[], state}
end
end
defp serialize_and_store_manifest(manifest, storage) do
manifest_files = Manifest.serialize(manifest)
Storage.store_manifests(storage, manifest_files)
end
end