Packages
membrane_hls_plugin
2.0.3
3.0.10
3.0.9
3.0.8
3.0.7
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
2.1.9
2.1.8
2.1.7
2.1.6
2.1.5
2.1.4
2.1.3
2.1.2
2.1.1
2.1.0
2.0.16
2.0.15
2.0.14
2.0.13
2.0.12
2.0.11
2.0.10
2.0.9
2.0.8
2.0.7
2.0.6
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
1.1.12
1.1.11
1.1.10
1.1.9
1.1.8
1.1.7
1.1.6
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.0.0
Adaptive live streaming (HLS) plugin for the Membrane Framework.
Current section
Files
Jump to
Current section
Files
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