Current section

Files

Jump to
membrane_hls_plugin lib membrane hls source.ex
Raw

lib/membrane/hls/source.ex

defmodule Membrane.HLS.Source do
@moduledoc """
Source element that reads a single media playlist and emits segment payloads.
## Operation
- For VOD playlists (`#EXT-X-ENDLIST` or `:vod` type), segments are served
on demand until the playlist is exhausted, then EOS is emitted.
- For live/event playlists, the playlist is polled and new segments are
enqueued as they appear. Demand still controls when segments are downloaded
and sent downstream.
## Stream format
This element does not parse media; it forwards raw segment payloads.
Callers must provide `stream_format` describing the segment container:
- `%Membrane.HLS.Format.MPEG{}` for MPEG-TS
- `%Membrane.HLS.Format.PackedAudio{}` for packed AAC
- `%Membrane.HLS.Format.WebVTT{}` for WebVTT
## Notes
- Segment metadata (`HLS.Segment`) is attached to each buffer.
- `poll_interval_ms` is intended for tests; production should rely on
playlist target duration.
"""
use Membrane.Source
alias HLS.Playlist
alias HLS.Playlist.Media
alias HLS.Storage
alias Membrane.Buffer
alias Membrane.HLS.Format
alias Membrane.HLS.TaskSupervisor
@playlist_retry_interval_ms 1_000
@poll_min_interval_ms 500
require Membrane.Logger
def_output_pad(:output,
flow_control: :manual,
availability: :always,
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 contents"
],
media_playlist_uri: [
spec: URI.t(),
description: "URI of the media playlist"
],
stream_format: [
spec: struct(),
description: "Stream format describing the media playlist contents (`%Membrane.HLS.Format.PackedAudio{}`, `%Membrane.HLS.Format.WebVTT{}`, or `%Membrane.HLS.Format.MPEG{}`)"
],
poll_interval_ms: [
spec: non_neg_integer() | nil,
default: nil,
description: """
Optional poll interval override (milliseconds) for live playlists.
When nil, the target duration derived from the playlist is used.
"""
]
)
@impl true
def handle_init(_ctx, options) do
{[],
%{
storage: options.storage,
media_playlist_uri: options.media_playlist_uri,
stream_format: options.stream_format,
poll_interval_ms: options.poll_interval_ms,
mode: nil,
target_duration_ms: nil,
next_sequence: nil,
pending: :queue.new(),
ready: :queue.new(),
download: nil,
finished: false
}}
end
@impl true
def handle_playing(_ctx, state) do
actions = [
{:stream_format, {:output, %Membrane.RemoteStream{content_format: state.stream_format}}}
]
send(self(), :refresh_playlist)
{actions, state}
end
@impl true
def handle_demand(:output, _size, :buffers, _ctx, state) do
{actions, state} =
get_and_update_in(state, [:ready], fn q ->
case :queue.out(q) do
{{:value, action}, q} -> {[action], q}
{:empty, q} -> {[], q}
end
end)
{actions, state} =
cond do
not is_nil(state.download) ->
{actions, state}
not :queue.is_empty(state.pending) ->
{actions, schedule_download!(state, state.storage)}
not :queue.is_empty(state.ready) ->
{actions ++ [{:redemand, :output}], state}
state.finished ->
{actions ++ [{:end_of_stream, :output}], state}
true ->
{actions, state}
end
{actions, state}
end
@impl true
def handle_info(:refresh_playlist, _ctx, state) do
case Storage.get(state.storage, state.media_playlist_uri) do
{:ok, data} ->
playlist = Playlist.unmarshal(data, %Media{uri: state.media_playlist_uri})
{actions, state} = handle_playlist_update(playlist, state)
{actions, schedule_next_refresh(state)}
{:error, reason} ->
Membrane.Logger.warning("Media playlist check failed: #{inspect(reason)}")
Membrane.Logger.warning(
"Media playlist check attempt scheduled in #{@playlist_retry_interval_ms}ms"
)
Process.send_after(self(), :refresh_playlist, @playlist_retry_interval_ms)
{[], state}
end
end
def handle_info({task_ref, {:segment, {:data, binary}}}, _ctx, state) do
Process.demonitor(task_ref, [:flush])
action = {:buffer, {:output, %Buffer{payload: binary, metadata: state.download.segment}}}
state =
state
|> update_in([:ready], fn q -> :queue.in(action, q) end)
|> put_in([:download], nil)
{[{:redemand, :output}], state}
end
def handle_info({:DOWN, _task_ref, _, _, reason}, _ctx, state) do
Membrane.Logger.warning("HLS could not get next segment: #{inspect(reason)}")
state = put_in(state, [:download], nil)
{[{:redemand, :output}], state}
end
defp schedule_download!(%{download: nil, media_playlist_uri: media_uri} = state, storage) do
{{:value, segment}, queue} = :queue.out(state.pending)
task =
Task.Supervisor.async_nolink(TaskSupervisor, fn ->
uri = Media.build_segment_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, "Storage.get failed: #{inspect(reason)}"
end
end)
%{state | pending: queue, download: %{task_ref: task.ref, segment: segment}}
end
defp handle_playlist_update(%Media{} = playlist, state) do
mode =
state.mode ||
if playlist.finished or playlist.type == :vod do
:vod
else
:live
end
next_sequence = state.next_sequence || playlist.media_sequence_number
start_sequence = max(next_sequence, playlist.media_sequence_number)
new_segments =
playlist.segments
|> Enum.filter(&(&1.absolute_sequence >= start_sequence))
next_sequence =
case List.last(new_segments) do
nil -> start_sequence
segment -> segment.absolute_sequence + 1
end
state =
state
|> update_in([:pending], fn q ->
Enum.reduce(new_segments, q, fn segment, acc -> :queue.in(segment, acc) end)
end)
|> Map.merge(%{
mode: mode,
finished: playlist.finished,
next_sequence: next_sequence,
target_duration_ms: playlist.target_segment_duration * 1_000
})
actions = if new_segments == [], do: [], else: [{:redemand, :output}]
{actions, state}
end
defp schedule_next_refresh(state) do
case {state.mode, state.finished} do
{:live, false} ->
interval =
state.poll_interval_ms ||
max(state.target_duration_ms || @poll_min_interval_ms, @poll_min_interval_ms)
_ref = Process.send_after(self(), :refresh_playlist, interval)
state
_other ->
state
end
end
end