Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions lib/ex_hls/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ defmodule ExHLS.Client do
:live_reader,
:live_forwarder,
:how_much_to_skip_ms,
:segment_format
:segment_format,
:live_edge_mode?
]

defstruct @enforce_keys
Expand Down Expand Up @@ -57,6 +58,13 @@ defmodule ExHLS.Client do
the client will treat HLS segments based on the extension in their name,
falling back `MPEG-TS` if the cannot recognize the extension.

Passing `live_edge_mode?: true` option turns on live edge mode of the client (please do not
confuse it with the Low Latency HLS extension which is not supported by the client!).
In this mode the client starts playing the playlist as fast as possible, and skips to the most
recent segment.
Please note that this is not compliant with the HLS specification and might cause playback stalls.
The live edge mode is turned off by default.

Note that there is no guarantee that exactly the specified amount of time will be skipped.
The actual skipped duration may be slightly shorter, depending on the HLS segments durations.
To get the actual skipped duration, you can use `get_skipped_segments_cumulative_duration_ms/1`
Expand All @@ -71,10 +79,16 @@ defmodule ExHLS.Client do
%{
parent_process: parent_process,
how_much_to_skip_ms: how_much_to_skip_ms,
segment_format: segment_format
segment_format: segment_format,
live_edge_mode?: live_edge_mode?
} =
opts
|> Keyword.validate!(parent_process: self(), how_much_to_skip_ms: 0, segment_format: nil)
|> Keyword.validate!(
parent_process: self(),
how_much_to_skip_ms: 0,
segment_format: nil,
live_edge_mode?: false
)
|> Map.new()

root_playlist_raw_content = Utils.download_or_read_file!(url)
Expand All @@ -100,7 +114,8 @@ defmodule ExHLS.Client do
live_reader: nil,
live_forwarder: nil,
how_much_to_skip_ms: how_much_to_skip_ms,
segment_format: segment_format
segment_format: segment_format,
live_edge_mode?: live_edge_mode?
}
|> maybe_resolve_media_playlist()
end
Expand Down Expand Up @@ -155,7 +170,8 @@ defmodule ExHLS.Client do
ExHLS.Client.Live.Reader.start_link(
client.media_playlist_url,
forwarder,
client.segment_format
client.segment_format,
client.live_edge_mode?
)

%{client | live_reader: reader, live_forwarder: forwarder, hls_mode: :live}
Expand Down
31 changes: 24 additions & 7 deletions lib/ex_hls/client/live/reader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,23 @@ defmodule ExHLS.Client.Live.Reader do
alias ExHLS.Client.Live.Forwarder
alias ExM3U8.Tags.{MediaInit, Segment}

@spec start_link(String.t(), Forwarder.t(), :ts | :cmaf | nil) :: {:ok, pid()} | {:error, any()}
def start_link(media_playlist_url, forwarder, segment_format) do
@spec start_link(String.t(), Forwarder.t(), :ts | :cmaf | nil, boolean()) ::
{:ok, pid()} | {:error, any()}
def start_link(media_playlist_url, forwarder, segment_format, live_edge_mode?) do
GenServer.start_link(__MODULE__, %{
media_playlist_url: media_playlist_url,
forwarder: forwarder,
segment_format: segment_format
segment_format: segment_format,
live_edge_mode?: live_edge_mode?
})
end

@impl true
def init(%{
media_playlist_url: media_playlist_url,
forwarder: forwarder,
segment_format: segment_format
segment_format: segment_format,
live_edge_mode?: live_edge_mode?
}) do
state = %{
forwarder: forwarder,
Expand All @@ -39,7 +42,8 @@ defmodule ExHLS.Client.Live.Reader do
playlist_check_scheduled?: false,
timestamp_offset: nil,
playing_started?: false,
segment_format: segment_format
segment_format: segment_format,
live_edge_mode?: live_edge_mode?
}

{:ok, state, {:continue, :setup}}
Expand Down Expand Up @@ -112,7 +116,7 @@ defmodule ExHLS.Client.Live.Reader do
end_list?: end_list?
} = state.media_playlist.info

end_list? or
end_list? or state.live_edge_mode? or
(is_number(media_sequence) and media_sequence >= 1) or
segments_duration_sum(state) >= 2 * target_duration
end
Expand Down Expand Up @@ -165,7 +169,20 @@ defmodule ExHLS.Client.Live.Reader do
{media_inits, %{state | media_init_downloaded?: true}}
end

defp next_segment_to_download_seq_num(%{max_downloaded_seq_num: nil} = state) do
# in the live edge mode it skips to the most recent segment
defp next_segment_to_download_seq_num(
%{max_downloaded_seq_num: nil, live_edge_mode?: true} = state
) do
how_many_segments =
state.media_playlist.timeline
|> Enum.count(&match?(%Segment{}, &1))

state.media_playlist.info.media_sequence + how_many_segments - 1
end

defp next_segment_to_download_seq_num(
%{max_downloaded_seq_num: nil, live_edge_mode?: false} = state
) do
{segments_with_end_times, duration_sum} =
state.media_playlist.timeline
|> Enum.flat_map_reduce(0, fn
Expand Down
23 changes: 23 additions & 0 deletions test/client_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ defmodule ExHLS.Client.Test do
@fmp4_only_video_url @fixtures <> "fmp4_only_video/output.m3u8"
@mpegts_only_video_url @fixtures <> "mpeg_ts_only_video/output_playlist.m3u8"
@mpegts_url "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8"
@mpegts_live_url "./test/fixtures/mpeg_ts_live/output_playlist.m3u8"

describe "if client reads video and audio chunks of the HLS" do
test "(MPEGTS) stream" do
Expand Down Expand Up @@ -203,6 +204,28 @@ defmodule ExHLS.Client.Test do
215, 198, 77, 184, 229, 170, 157, 115, 169, 223>> <> _rest = audio_chunk.payload
end

test "(MPEGTS) stream with live edge mode" do
client = Client.new(@mpegts_live_url, live_edge_mode?: true)

assert Client.get_variants(client) == %{}
assert {:ok, tracks_info, client} = Client.get_tracks_info(client)

assert [%Membrane.RemoteStream{content_format: Membrane.H264, type: :bytestream}] =
tracks_info |> Map.values()

chunks = Client.generate_stream(client) |> Enum.take(1)
[video_chunk | _rest_video_chunks] = chunks

assert %{pts_ms: 11_081, dts_ms: 11_001} = video_chunk
assert byte_size(video_chunk.payload) == 28_699

assert <<0, 0, 0, 1, 9, 240, 0, 0, 0, 1, 103, 100, 0, 21, 172, 217, 65, 224, 143, 235, 1, 106,
12, 2, 13, 110, 0, 0, 9, 154, 0, 1, 224, 0, 30, 44, 91, 44, 0, 0, 0, 1, 104, 234,
225, 178, 200, 176, 0, 0>> <> _rest = video_chunk.payload

assert video_chunk.metadata == %{discontinuity: false, is_aligned: false}
end

defp assert_chunks_are_in_proper_order(chunks) do
iteration_state = %{
last_dts: %{audio: nil, video: nil},
Expand Down
10 changes: 10 additions & 0 deletions test/fixtures/mpeg_ts_live/output_playlist.m3u8
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:5.760933,
video_segment_000.ts
#EXTINF:3.840633,
video_segment_001.ts
#EXTINF:0.400111,
video_segment_002.ts
Binary file added test/fixtures/mpeg_ts_live/video_segment_000.ts
Binary file not shown.
Binary file added test/fixtures/mpeg_ts_live/video_segment_001.ts
Binary file not shown.
Binary file added test/fixtures/mpeg_ts_live/video_segment_002.ts
Binary file not shown.