Current section

Files

Jump to
membrane_hls_plugin lib membrane hls source.ex
Raw

lib/membrane/hls/source.ex

defmodule Membrane.HLS.Source do
use Membrane.Source
alias HLS.Tracker
alias HLS.Playlist
alias HLS.Playlist.Master
alias HLS.Storage
alias Membrane.Buffer
alias Membrane.HLS.Format
alias Membrane.HLS.TaskSupervisor
alias Membrane.HLS.TrackerSupervisor
@master_check_retry_interval_ms 1_000
require Membrane.Logger
def_output_pad(:output,
flow_control: :manual,
availability: :on_request,
accepted_format:
%Membrane.RemoteStream{content_format: %format{}}
when format in [Format.PackedAudio, Format.WebVTT, Format.MPEG]
)
def_options(
storage: [
spec: Storage.t(),
description: "HLS.Storage implementation used to obtain playlist's contents"
],
master_playlist_uri: [
spec: URI.t(),
description: "URI of the master playlist"
]
)
@impl true
def handle_init(_ctx, options) do
{[],
%{
storage: options.storage,
master_playlist_uri: options.master_playlist_uri,
pad_to_tracker: %{},
ref_to_pad: %{},
monitor_to_pad: %{}
}}
end
@impl true
def handle_pad_added(pad = {Membrane.Pad, :output, {:rendition, rendition}}, _, state) do
uri = Playlist.build_absolute_uri(state.master_playlist_uri, extract_uri(rendition))
ref = make_ref()
{:ok, pid} =
DynamicSupervisor.start_child(
TrackerSupervisor,
{Tracker,
[
media_playlist_uri: uri,
storage: state.storage,
ref: ref,
owner: self()
]}
)
monitor_ref = Process.monitor(pid)
config = %{
media_uri: uri,
monitor_ref: monitor_ref,
tracking: ref,
tracker: pid,
ready: :queue.new(),
pending: :queue.new(),
download: nil,
closed: false
}
state =
state
|> put_in([:pad_to_tracker, pad], config)
# When the tracker sends us messages it forwards ref. We use this mapping
# to retrieve the tracker.
|> put_in([:ref_to_pad, ref], pad)
# In case the tracker exits.
|> put_in([:monitor_to_pad, monitor_ref], pad)
{[
{:stream_format,
{pad, %Membrane.RemoteStream{content_format: build_stream_format(rendition)}}}
], state}
end
@impl true
def handle_playing(_ctx, state) do
send(self(), :check_master_playlist)
{[], state}
end
@impl true
def handle_demand(pad, _size, :buffers, _ctx, state) do
tracker = get_in(state, [:pad_to_tracker, pad])
# First take one ready if available.
{actions, tracker} =
get_and_update_in(tracker, [:ready], fn q ->
case :queue.out(q) do
{{:value, action}, q} -> {[action], q}
{:empty, q} -> {[], q}
end
end)
{actions, tracker} =
cond do
not is_nil(tracker.download) ->
# We're already downloading another segment. We can chill out.
{actions, tracker}
not :queue.is_empty(tracker.pending) ->
# The pending queue is not empty. Schedule a download.
{actions, schedule_download!(tracker, state.storage)}
not :queue.is_empty(tracker.ready) ->
# We have other ready segments.
{actions ++ [{:redemand, pad}], tracker}
tracker.closed ->
# Everything is out and the tracker is not going to provide
# more segments. Time to close.
{actions ++ [{:end_of_stream, pad}], tracker}
true ->
# We're waiting for more segments from the tracker.
{actions, tracker}
end
state = put_in(state, [:pad_to_tracker, pad], tracker)
{actions, state}
end
@impl true
def handle_info(
:check_master_playlist,
_ctx,
state = %{storage: storage, master_playlist_uri: uri}
) do
case Storage.get(storage, uri) do
{:ok, data} ->
playlist = Playlist.unmarshal(data, %Master{uri: uri})
{[{:notify_parent, {:hls_master_playlist, playlist}}], state}
{:error, reason} ->
Membrane.Logger.warning("Master playlist check failed: #{inspect(reason)}")
Membrane.Logger.warning(
"Master playlist check attempt scheduled in #{@master_check_retry_interval_ms}ms"
)
Process.send_after(self(), :check_master_playlist, @master_check_retry_interval_ms)
{[], state}
end
end
def handle_info({:segment, ref, segment}, _ctx, state) do
Membrane.Logger.debug("HLS segment received on #{inspect(ref)}: #{to_string(segment.uri)}")
pad = Map.fetch!(state.ref_to_pad, ref)
# We're not downloading the segment, we're only putting it into the pending
# queue. Download will be triggered when the segment is demanded. This allows
# to download a VOD playlist in a controlled fashion.
state =
update_in(state, [:pad_to_tracker, pad, :pending], fn q ->
:queue.in(segment, q)
end)
{[{:redemand, pad}], state}
end
def handle_info({task_ref, {:segment, {:data, binary}}}, _ctx, state) do
# The task succeed so we can cancel the monitoring and discard the DOWN message
Process.demonitor(task_ref, [:flush])
pad = find_pad_by_download_ref(task_ref, state)
tracker = get_in(state, [:pad_to_tracker, pad])
action = {:buffer, {pad, %Buffer{payload: binary, metadata: tracker.download.segment}}}
state =
state
|> update_in([:pad_to_tracker, pad, :ready], fn q -> :queue.in(action, q) end)
|> put_in([:pad_to_tracker, pad, :download], nil)
{[{:redemand, pad}], state}
end
def handle_info({:DOWN, task_ref, _, _, reason}, _ctx, state) do
# This could either be a download message or a tracker.
cond do
Map.has_key?(state.monitor_to_pad, task_ref) ->
pad = get_in(state, [:monitor_to_pad, task_ref])
raise RuntimeError,
"Tracker for pad #{inspect(pad)} exited with reason: #{inspect(reason)}"
true ->
# In this case, is is a download task.
pad = find_pad_by_download_ref(task_ref, state)
Membrane.Logger.warning(
"HLS could not get next segment for pad #{inspect(pad)}: #{inspect(reason)}"
)
state = put_in(state, [:pad_to_tracker, pad, :download], nil)
{[{:redemand, pad}], state}
end
end
def handle_info({:start_of_track, _ref, _next_sequence}, _ctx, state) do
{[], state}
end
def handle_info({:end_of_track, ref}, _ctx, state) do
# Note that the tracker process is going to exit at this point.
Membrane.Logger.debug("HLS end_of_track received on #{inspect(ref)}")
pad = Map.fetch!(state.ref_to_pad, ref)
monitor_ref = get_in(state, [:pad_to_tracker, pad, :monitor_ref])
# Avoid receiving the exit message, even though we should not receive
# it anyway in case it goes down with :normal reason.
Process.demonitor(monitor_ref, [:flush])
state = put_in(state, [:pad_to_tracker, pad, :closed], true)
{[{:redemand, pad}], state}
end
defp find_pad_by_download_ref(task_ref, state) do
{pad, _tracker} =
Enum.find(
state.pad_to_tracker,
{nil, nil},
fn {_pad, tracker} ->
tracker.download != nil and tracker.download.task_ref == task_ref
end
)
pad
end
defp schedule_download!(%{download: nil, media_uri: media_uri} = tracker, storage) do
{{:value, segment}, queue} = :queue.out(tracker.pending)
task =
Task.Supervisor.async_nolink(TaskSupervisor, fn ->
uri = Playlist.build_absolute_uri(media_uri, segment.uri)
Membrane.Logger.debug("Getting segment: #{to_string(uri)}")
case Storage.get(storage, uri) do
{:ok, data} -> {:segment, {:data, data}}
{:error, reason} -> raise RuntimeError, reason
end
end)
%{tracker | pending: queue, download: %{task_ref: task.ref, segment: segment}}
end
defp extract_uri(%HLS.AlternativeRendition{uri: uri}), do: uri
defp extract_uri(%HLS.VariantStream{uri: uri}), do: uri
defp build_stream_format(%HLS.VariantStream{codecs: codecs}), do: %Format.MPEG{codecs: codecs}
defp build_stream_format(%HLS.AlternativeRendition{type: :subtitles, language: lang}),
do: %Format.WebVTT{language: lang}
defp build_stream_format(%HLS.AlternativeRendition{type: :audio}), do: %Format.PackedAudio{}
defp build_stream_format(rendition),
do: raise(ArgumentError, "Unable to provide a proper cap for rendition #{inspect(rendition)}")
end