Current section
Files
Jump to
Current section
Files
lib/hls/tracker.ex
defmodule HLS.Tracker do
use GenServer
alias HLS.Playlist.Media
alias HLS.{Segment, Playlist, Storage}
@max_initial_live_segments 3
defstruct [
:ref,
:owner,
:monitor,
:storage,
:uri,
:timer,
started: false,
next_seq: 0
]
def initial_live_buffer_size(), do: @max_initial_live_segments
@spec start_link(Keyword.t()) :: GenServer.on_start()
def start_link(opts \\ []) do
opts = Keyword.validate!(opts, [:name, :media_playlist_uri, :storage, :owner, :ref])
{server_opts, opts} = Keyword.split(opts, [:name])
GenServer.start_link(__MODULE__, opts, server_opts)
end
@impl true
def init(opts) do
uri = Keyword.fetch!(opts, :media_playlist_uri)
storage = Keyword.fetch!(opts, :storage)
owner = Keyword.fetch!(opts, :owner)
if Process.alive?(owner) do
ref = Keyword.fetch!(opts, :ref)
state = %__MODULE__{
uri: uri,
storage: storage,
owner: owner,
ref: ref,
monitor: Process.monitor(owner)
}
{:ok, state, {:continue, :refresh}}
else
:ignore
end
end
@impl true
def handle_continue(:refresh, state) do
handle_refresh(state)
end
@impl true
def handle_info(:refresh, state) do
handle_refresh(state)
end
def handle_info({:DOWN, ref, :process, _pid, reason}, state = %__MODULE__{monitor: ref}) do
{:stop, reason, state}
end
defp read_media_playlist(storage, uri) do
raw_playlist = read!(storage, uri)
Playlist.unmarshal(raw_playlist, %Media{uri: uri})
end
defp handle_refresh(state) do
playlist = read_media_playlist(state.storage, state.uri)
segs = Media.segments(playlist)
if length(segs) < @max_initial_live_segments and !playlist.finished do
# wait for some more segments to be listed in the playlist to
# avoid playback stalls.
wait = playlist.target_segment_duration * 1_000
{:noreply, schedule_refresh_after(state, wait)}
else
send_updates(state, playlist)
end
end
defp send_updates(state, playlist) do
segs = Media.segments(playlist)
# Determine initial sequence number, sending the start_of_track message if
# needed.
state =
if state.started do
state
else
next_seq =
if !playlist.finished && length(segs) > @max_initial_live_segments do
playlist.media_sequence_number + (length(segs) - @max_initial_live_segments)
else
playlist.media_sequence_number
end
send(state.owner, {:start_of_track, state.ref, next_seq})
state
|> put_in([Access.key!(:next_seq)], next_seq)
|> put_in([Access.key!(:started)], true)
end
{segs, last_seq} =
segs
|> Enum.map_reduce(0, fn seg, _ ->
seg = Segment.update_absolute_sequence(seg, playlist.media_sequence_number)
{seg, seg.absolute_sequence}
end)
# Send new segments only
segs
|> Enum.filter(fn seg -> seg.absolute_sequence >= state.next_seq end)
|> Enum.map(fn seg ->
{:segment, state.ref, seg}
end)
|> Enum.each(&send(state.owner, &1))
# Schedule a new refresh if needed
state =
state
|> put_in([Access.key!(:next_seq)], last_seq + 1)
if playlist.finished do
send(state.owner, {:end_of_track, state.ref})
{:stop, :normal, state}
else
wait = playlist.target_segment_duration * 1_000
{:noreply, schedule_refresh_after(state, wait)}
end
end
defp schedule_refresh_after(state, wait) do
timer = Process.send_after(self(), :refresh, wait)
put_in(state, [Access.key!(:timer)], timer)
end
defp read!(storage, uri) do
case Storage.get(storage, uri) do
{:ok, binary} -> binary
{:error, :not_found} -> raise RuntimeError, "Read error on #{inspect(uri)}: not found"
{:error, error} -> raise RuntimeError, "Read error on #{inspect(uri)}: #{inspect(error)}"
end
end
end