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
44 changes: 36 additions & 8 deletions lib/ex_hls/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ defmodule ExHLS.Client do
:vod_client,
:live_reader,
:live_forwarder,
:how_much_to_skip_ms
:how_much_to_skip_ms,
:segment_format
]

defstruct @enforce_keys
Expand Down Expand Up @@ -50,16 +51,29 @@ defmodule ExHLS.Client do
of the beginning of the stream should be skipped. This option is only supported
when the HLS stream is in the VoD mode. Defaults to `0`.

Apart from that you can also pass `:segment_format` to force treating HLS segments
as either `MPEG-TS` or `CMAF` container files. If you don't provide this option,
the client will treat HLS segments based on the extension in their name,
falling back `MPEG-TS` if the cannot recognize the extension.

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`
function.
"""
@spec new(String.t(), parent_process: pid(), how_much_to_skip_ms: non_neg_integer()) :: client()
@spec new(String.t(),
parent_process: pid(),
how_much_to_skip_ms: non_neg_integer(),
segment_format: :ts | :cmaf
) :: client()
def new(url, opts \\ []) do
%{parent_process: parent_process, how_much_to_skip_ms: how_much_to_skip_ms} =
%{
parent_process: parent_process,
how_much_to_skip_ms: how_much_to_skip_ms,
segment_format: segment_format
} =
opts
|> Keyword.validate!(parent_process: self(), how_much_to_skip_ms: 0)
|> Keyword.validate!(parent_process: self(), how_much_to_skip_ms: 0, segment_format: nil)
|> Map.new()

root_playlist_raw_content = Utils.download_or_read_file!(url)
Expand All @@ -79,7 +93,8 @@ defmodule ExHLS.Client do
vod_client: nil,
live_reader: nil,
live_forwarder: nil,
how_much_to_skip_ms: how_much_to_skip_ms
how_much_to_skip_ms: how_much_to_skip_ms,
segment_format: segment_format
}
|> maybe_resolve_media_playlist()
end
Expand Down Expand Up @@ -109,7 +124,8 @@ defmodule ExHLS.Client do
ExHLS.Client.VOD.new(
client.media_playlist_url,
client.media_playlist,
client.how_much_to_skip_ms
client.how_much_to_skip_ms,
client.segment_format
)

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

{:ok, forwarder} = ExHLS.Client.Live.Forwarder.start_link(client.parent_process)
{:ok, reader} = ExHLS.Client.Live.Reader.start_link(client.media_playlist_url, forwarder)

{:ok, reader} =
ExHLS.Client.Live.Reader.start_link(
client.media_playlist_url,
forwarder,
client.segment_format
)

%{client | live_reader: reader, live_forwarder: forwarder, hls_mode: :live}
end
end
Expand Down Expand Up @@ -185,7 +208,12 @@ defmodule ExHLS.Client do

defp do_choose_variant(%__MODULE__{} = client, variant_id) do
chosen_variant = get_variants(client) |> Map.fetch!(variant_id)
media_playlist_url = Path.join(client.base_url, chosen_variant.uri)

media_playlist_url =
case URI.new!(chosen_variant.uri).host do
nil -> Path.join(client.base_url, chosen_variant.uri)
_some_host -> chosen_variant.uri
end

media_playlist =
media_playlist_url
Expand Down
25 changes: 18 additions & 7 deletions lib/ex_hls/client/live/reader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,21 @@ defmodule ExHLS.Client.Live.Reader do

alias ExM3U8.Tags.{MediaInit, Segment}

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

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

{:ok, state, {:continue, :setup}}
Expand Down Expand Up @@ -212,7 +218,12 @@ defmodule ExHLS.Client.Live.Reader do
end

defp download_and_consume_segment(segment, state) do
uri = Path.join(state.media_base_url, segment.uri)
uri =
case URI.new!(segment.uri).host do
nil -> Path.join(state.media_base_url, segment.uri)
_some_host -> segment.uri
end

Logger.debug("[ExHLS.Client] Downloading segment: #{uri}")

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

defp maybe_resolve_demuxing_engine(segment_uri, %{demuxing_engine: nil} = state) do
demuxing_engine_impl = Utils.resolve_demuxing_engine_impl(segment_uri)
demuxing_engine_impl = Utils.resolve_demuxing_engine_impl(segment_uri, state.segment_format)

%{
state
Expand Down
31 changes: 22 additions & 9 deletions lib/ex_hls/client/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,28 @@ defmodule ExHLS.Client.Utils do
def stream_format_to_media_type(%RemoteStream{content_format: H264}), do: :video
def stream_format_to_media_type(%RemoteStream{content_format: AAC}), do: :audio

@spec resolve_demuxing_engine_impl(String.t()) :: atom()
def resolve_demuxing_engine_impl(segment_uri) do
URI.parse(segment_uri).path
|> Path.extname()
|> case do
".ts" -> DemuxingEngine.MPEGTS
".m4s" -> DemuxingEngine.CMAF
".mp4" -> DemuxingEngine.CMAF
_other -> raise "Unsupported segment URI extension: #{segment_uri |> inspect()}"
@spec resolve_demuxing_engine_impl(String.t(), :ts | :cmaf | nil) :: atom()
def resolve_demuxing_engine_impl(segment_uri, nil) do
case Path.extname(segment_uri) do
".ts" <> _id ->
DemuxingEngine.MPEGTS

".m4s" <> _id ->
DemuxingEngine.CMAF

".mp4" <> _id ->
DemuxingEngine.CMAF

_other ->
Logger.warning("""
Unsupported segment URI extension: #{segment_uri |> inspect()}
Falling back to recognizing segment as MPEG-TS container file.
You can force recognizing segment as CMAF container file
by providing `segment_format: :cmaf` to `ExHLS.Client/2`.
""")
end
end

def resolve_demuxing_engine_impl(_segment_uri, :ts), do: DemuxingEngine.MPEGTS
def resolve_demuxing_engine_impl(_segment_uri, :cmaf), do: DemuxingEngine.CMAF
end
13 changes: 8 additions & 5 deletions lib/ex_hls/client/vod.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ defmodule ExHLS.Client.VOD do
:end_stream_executed?,
:stream_ended_by_media_type,
:how_much_to_skip_ms,
:skipped_segments_cumulative_duration_ms
:skipped_segments_cumulative_duration_ms,
:segment_format
]

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

@spec new(String.t(), ExM3U8.MediaPlaylist.t(), non_neg_integer()) :: client()
def new(media_playlist_url, media_playlist, how_much_to_skip_ms) do
@spec new(String.t(), ExM3U8.MediaPlaylist.t(), non_neg_integer(), :ts | :cmaf | nil) ::
client()
def new(media_playlist_url, media_playlist, how_much_to_skip_ms, segment_format) do
:ok = generate_discontinuity_warnings(media_playlist)

last_timestamps = %{audio: %{returned: nil, read: nil}, video: %{returned: nil, read: nil}}
Expand All @@ -54,7 +56,8 @@ defmodule ExHLS.Client.VOD do
end_stream_executed?: false,
stream_ended_by_media_type: %{audio: false, video: false},
how_much_to_skip_ms: how_much_to_skip_ms,
skipped_segments_cumulative_duration_ms: nil
skipped_segments_cumulative_duration_ms: nil,
segment_format: segment_format
}
|> skip_segments()
end
Expand Down Expand Up @@ -279,7 +282,7 @@ defmodule ExHLS.Client.VOD do
end

defp ensure_demuxing_engine_resolved(%{demuxing_engine: nil} = client, segment_uri) do
demuxing_engine_impl = Utils.resolve_demuxing_engine_impl(segment_uri)
demuxing_engine_impl = Utils.resolve_demuxing_engine_impl(segment_uri, client.segment_format)

%{
client
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ defmodule ExHLS.Mixfile do

defp deps do
[
{:ex_m3u8, "~> 0.15.3"},
{:ex_m3u8, "~> 0.15.4"},
{:req, "~> 0.5.10"},
{:qex, "~> 0.5.1"},
{:membrane_mp4_plugin, "~> 0.36.0"},
Expand Down
2 changes: 1 addition & 1 deletion mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
"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"},
"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"},
"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"},
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
"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"},
"heap": {:hex, :heap, "2.0.2", "d98cb178286cfeb5edbcf17785e2d20af73ca57b5a2cf4af584118afbcf917eb", [:mix], [], "hexpm", "ba9ea2fe99eb4bcbd9a8a28eaf71cbcac449ca1d8e71731596aace9028c9d429"},
Expand Down