Skip to content

Commit 641a8c5

Browse files
authored
Bump ex_m3u8 to v0.15.4. Properly handle absolute URIs for variant pl… (#11) (#15)
* Bump ex_m3u8 to v0.15.4. Properly handle absolute URIs for variant playlists and segments. Fallback to MPEG-TS demuxer * Add segment_format option to the client to override segments demuxer resolving * Improve warning message * Pass segment_format to reader
1 parent bbc5949 commit 641a8c5

File tree

6 files changed

+86
-31
lines changed

6 files changed

+86
-31
lines changed

lib/ex_hls/client.ex

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ defmodule ExHLS.Client do
2222
:vod_client,
2323
:live_reader,
2424
:live_forwarder,
25-
:how_much_to_skip_ms
25+
:how_much_to_skip_ms,
26+
:segment_format
2627
]
2728

2829
defstruct @enforce_keys
@@ -50,16 +51,29 @@ defmodule ExHLS.Client do
5051
of the beginning of the stream should be skipped. This option is only supported
5152
when the HLS stream is in the VoD mode. Defaults to `0`.
5253
54+
Apart from that you can also pass `:segment_format` to force treating HLS segments
55+
as either `MPEG-TS` or `CMAF` container files. If you don't provide this option,
56+
the client will treat HLS segments based on the extension in their name,
57+
falling back `MPEG-TS` if the cannot recognize the extension.
58+
5359
Note that there is no guarantee that exactly the specified amount of time will be skipped.
5460
The actual skipped duration may be slightly shorter, depending on the HLS segments durations.
5561
To get the actual skipped duration, you can use `get_skipped_segments_cumulative_duration_ms/1`
5662
function.
5763
"""
58-
@spec new(String.t(), parent_process: pid(), how_much_to_skip_ms: non_neg_integer()) :: client()
64+
@spec new(String.t(),
65+
parent_process: pid(),
66+
how_much_to_skip_ms: non_neg_integer(),
67+
segment_format: :ts | :cmaf
68+
) :: client()
5969
def new(url, opts \\ []) do
60-
%{parent_process: parent_process, how_much_to_skip_ms: how_much_to_skip_ms} =
70+
%{
71+
parent_process: parent_process,
72+
how_much_to_skip_ms: how_much_to_skip_ms,
73+
segment_format: segment_format
74+
} =
6175
opts
62-
|> Keyword.validate!(parent_process: self(), how_much_to_skip_ms: 0)
76+
|> Keyword.validate!(parent_process: self(), how_much_to_skip_ms: 0, segment_format: nil)
6377
|> Map.new()
6478

6579
root_playlist_raw_content = Utils.download_or_read_file!(url)
@@ -79,7 +93,8 @@ defmodule ExHLS.Client do
7993
vod_client: nil,
8094
live_reader: nil,
8195
live_forwarder: nil,
82-
how_much_to_skip_ms: how_much_to_skip_ms
96+
how_much_to_skip_ms: how_much_to_skip_ms,
97+
segment_format: segment_format
8398
}
8499
|> maybe_resolve_media_playlist()
85100
end
@@ -109,7 +124,8 @@ defmodule ExHLS.Client do
109124
ExHLS.Client.VOD.new(
110125
client.media_playlist_url,
111126
client.media_playlist,
112-
client.how_much_to_skip_ms
127+
client.how_much_to_skip_ms,
128+
client.segment_format
113129
)
114130

115131
%{client | vod_client: vod_client, hls_mode: :vod}
@@ -128,7 +144,14 @@ defmodule ExHLS.Client do
128144
end
129145

130146
{:ok, forwarder} = ExHLS.Client.Live.Forwarder.start_link(client.parent_process)
131-
{:ok, reader} = ExHLS.Client.Live.Reader.start_link(client.media_playlist_url, forwarder)
147+
148+
{:ok, reader} =
149+
ExHLS.Client.Live.Reader.start_link(
150+
client.media_playlist_url,
151+
forwarder,
152+
client.segment_format
153+
)
154+
132155
%{client | live_reader: reader, live_forwarder: forwarder, hls_mode: :live}
133156
end
134157
end
@@ -185,7 +208,12 @@ defmodule ExHLS.Client do
185208

186209
defp do_choose_variant(%__MODULE__{} = client, variant_id) do
187210
chosen_variant = get_variants(client) |> Map.fetch!(variant_id)
188-
media_playlist_url = Path.join(client.base_url, chosen_variant.uri)
211+
212+
media_playlist_url =
213+
case URI.new!(chosen_variant.uri).host do
214+
nil -> Path.join(client.base_url, chosen_variant.uri)
215+
_some_host -> chosen_variant.uri
216+
end
189217

190218
media_playlist =
191219
media_playlist_url

lib/ex_hls/client/live/reader.ex

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,21 @@ defmodule ExHLS.Client.Live.Reader do
1010

1111
alias ExM3U8.Tags.{MediaInit, Segment}
1212

13-
@spec start_link(String.t(), Forwarder.t()) :: {:ok, pid()} | {:error, any()}
14-
def start_link(media_playlist_url, forwarder) do
13+
@spec start_link(String.t(), Forwarder.t(), :ts | :cmaf | nil) :: {:ok, pid()} | {:error, any()}
14+
def start_link(media_playlist_url, forwarder, segment_format) do
1515
GenServer.start_link(__MODULE__, %{
1616
media_playlist_url: media_playlist_url,
17-
forwarder: forwarder
17+
forwarder: forwarder,
18+
segment_format: segment_format
1819
})
1920
end
2021

2122
@impl true
22-
def init(%{media_playlist_url: media_playlist_url, forwarder: forwarder}) do
23+
def init(%{
24+
media_playlist_url: media_playlist_url,
25+
forwarder: forwarder,
26+
segment_format: segment_format
27+
}) do
2328
state = %{
2429
forwarder: forwarder,
2530
tracks_data: nil,
@@ -34,7 +39,8 @@ defmodule ExHLS.Client.Live.Reader do
3439
max_downloaded_seq_num: nil,
3540
playlist_check_scheduled?: false,
3641
timestamp_offset: nil,
37-
playing_started?: false
42+
playing_started?: false,
43+
segment_format: segment_format
3844
}
3945

4046
{:ok, state, {:continue, :setup}}
@@ -212,7 +218,12 @@ defmodule ExHLS.Client.Live.Reader do
212218
end
213219

214220
defp download_and_consume_segment(segment, state) do
215-
uri = Path.join(state.media_base_url, segment.uri)
221+
uri =
222+
case URI.new!(segment.uri).host do
223+
nil -> Path.join(state.media_base_url, segment.uri)
224+
_some_host -> segment.uri
225+
end
226+
216227
Logger.debug("[ExHLS.Client] Downloading segment: #{uri}")
217228

218229
segment_content = Utils.download_or_read_file!(uri)
@@ -390,7 +401,7 @@ defmodule ExHLS.Client.Live.Reader do
390401
defp doesnt_exist_or_empty?([track_data]), do: track_data.empty?
391402

392403
defp maybe_resolve_demuxing_engine(segment_uri, %{demuxing_engine: nil} = state) do
393-
demuxing_engine_impl = Utils.resolve_demuxing_engine_impl(segment_uri)
404+
demuxing_engine_impl = Utils.resolve_demuxing_engine_impl(segment_uri, state.segment_format)
394405

395406
%{
396407
state

lib/ex_hls/client/utils.ex

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,28 @@ defmodule ExHLS.Client.Utils do
4242
def stream_format_to_media_type(%RemoteStream{content_format: H264}), do: :video
4343
def stream_format_to_media_type(%RemoteStream{content_format: AAC}), do: :audio
4444

45-
@spec resolve_demuxing_engine_impl(String.t()) :: atom()
46-
def resolve_demuxing_engine_impl(segment_uri) do
47-
URI.parse(segment_uri).path
48-
|> Path.extname()
49-
|> case do
50-
".ts" -> DemuxingEngine.MPEGTS
51-
".m4s" -> DemuxingEngine.CMAF
52-
".mp4" -> DemuxingEngine.CMAF
53-
_other -> raise "Unsupported segment URI extension: #{segment_uri |> inspect()}"
45+
@spec resolve_demuxing_engine_impl(String.t(), :ts | :cmaf | nil) :: atom()
46+
def resolve_demuxing_engine_impl(segment_uri, nil) do
47+
case Path.extname(segment_uri) do
48+
".ts" <> _id ->
49+
DemuxingEngine.MPEGTS
50+
51+
".m4s" <> _id ->
52+
DemuxingEngine.CMAF
53+
54+
".mp4" <> _id ->
55+
DemuxingEngine.CMAF
56+
57+
_other ->
58+
Logger.warning("""
59+
Unsupported segment URI extension: #{segment_uri |> inspect()}
60+
Falling back to recognizing segment as MPEG-TS container file.
61+
You can force recognizing segment as CMAF container file
62+
by providing `segment_format: :cmaf` to `ExHLS.Client/2`.
63+
""")
5464
end
5565
end
66+
67+
def resolve_demuxing_engine_impl(_segment_uri, :ts), do: DemuxingEngine.MPEGTS
68+
def resolve_demuxing_engine_impl(_segment_uri, :cmaf), do: DemuxingEngine.CMAF
5669
end

lib/ex_hls/client/vod.ex

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ defmodule ExHLS.Client.VOD do
2424
:end_stream_executed?,
2525
:stream_ended_by_media_type,
2626
:how_much_to_skip_ms,
27-
:skipped_segments_cumulative_duration_ms
27+
:skipped_segments_cumulative_duration_ms,
28+
:segment_format
2829
]
2930

3031
defstruct @enforce_keys
@@ -37,8 +38,9 @@ defmodule ExHLS.Client.VOD do
3738
By default, it uses `DemuxingEngine.MPEGTS` as the demuxing engine implementation.
3839
"""
3940

40-
@spec new(String.t(), ExM3U8.MediaPlaylist.t(), non_neg_integer()) :: client()
41-
def new(media_playlist_url, media_playlist, how_much_to_skip_ms) do
41+
@spec new(String.t(), ExM3U8.MediaPlaylist.t(), non_neg_integer(), :ts | :cmaf | nil) ::
42+
client()
43+
def new(media_playlist_url, media_playlist, how_much_to_skip_ms, segment_format) do
4244
:ok = generate_discontinuity_warnings(media_playlist)
4345

4446
last_timestamps = %{audio: %{returned: nil, read: nil}, video: %{returned: nil, read: nil}}
@@ -54,7 +56,8 @@ defmodule ExHLS.Client.VOD do
5456
end_stream_executed?: false,
5557
stream_ended_by_media_type: %{audio: false, video: false},
5658
how_much_to_skip_ms: how_much_to_skip_ms,
57-
skipped_segments_cumulative_duration_ms: nil
59+
skipped_segments_cumulative_duration_ms: nil,
60+
segment_format: segment_format
5861
}
5962
|> skip_segments()
6063
end
@@ -279,7 +282,7 @@ defmodule ExHLS.Client.VOD do
279282
end
280283

281284
defp ensure_demuxing_engine_resolved(%{demuxing_engine: nil} = client, segment_uri) do
282-
demuxing_engine_impl = Utils.resolve_demuxing_engine_impl(segment_uri)
285+
demuxing_engine_impl = Utils.resolve_demuxing_engine_impl(segment_uri, client.segment_format)
283286

284287
%{
285288
client

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ defmodule ExHLS.Mixfile do
3737

3838
defp deps do
3939
[
40-
{:ex_m3u8, "~> 0.15.3"},
40+
{:ex_m3u8, "~> 0.15.4"},
4141
{:req, "~> 0.5.10"},
4242
{:qex, "~> 0.5.1"},
4343
{:membrane_mp4_plugin, "~> 0.36.0"},

mix.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
99
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
1010
"ex_doc": {:hex, :ex_doc, "0.38.4", "ab48dff7a8af84226bf23baddcdda329f467255d924380a0cf0cee97bb9a9ede", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "f7b62346408a83911c2580154e35613eb314e0278aeea72ed7fedef9c1f165b2"},
11-
"ex_m3u8": {:hex, :ex_m3u8, "0.15.3", "c10427f450b2ed7bfd85808d8dce21214f1fe9fa18927591cbbf96fea0a6a8aa", [:mix], [{:nimble_parsec, "~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "99f20c0b44bab130dc6aca71fefe0d1a174413ae9ac2763220994b29bd310939"},
11+
"ex_m3u8": {:hex, :ex_m3u8, "0.15.4", "66f6ec7e4fb7372c48032db1c2d4a3e6c2bbbde2d1d9a1098986e3caa0ab7a55", [:mix], [{:nimble_parsec, "~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "ec03aa516919e0c8ec202da55f609b763bd7960195a3388900090fcad270c873"},
1212
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
1313
"finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"},
1414
"heap": {:hex, :heap, "2.0.2", "d98cb178286cfeb5edbcf17785e2d20af73ca57b5a2cf4af584118afbcf917eb", [:mix], [], "hexpm", "ba9ea2fe99eb4bcbd9a8a28eaf71cbcac449ca1d8e71731596aace9028c9d429"},

0 commit comments

Comments
 (0)