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
46 changes: 25 additions & 21 deletions lib/ex_hls/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ defmodule ExHLS.Client do
media_playlist: nil,
media_base_url: nil,
multivariant_playlist: multivariant_playlist,
root_playlist_string: request_body,
base_url: Path.dirname(url),
video_chunks: [],
demuxing_engine_impl: nil,
demuxing_engine: nil,
media_types: [:audio, :video],
queues: %{audio: Qex.new(), video: Qex.new()},
timestamp_offsets: %{audio: nil, video: nil},
last_timestamps: %{audio: nil, video: nil}
Expand Down Expand Up @@ -67,13 +69,9 @@ defmodule ExHLS.Client do
defp ensure_media_playlist_loaded(client), do: client

defp read_media_playlist_without_variant(%{media_playlist: nil} = client) do
media_playlist =
client.base_url
|> Path.join("output.m3u8")
|> Req.get!()

deserialized_media_playlist =
ExM3U8.deserialize_media_playlist!(media_playlist.body, [])
client.root_playlist_string
|> ExM3U8.deserialize_media_playlist!([])

%{
client
Expand Down Expand Up @@ -136,12 +134,13 @@ defmodule ExHLS.Client do
end
end

@spec do_read_chunk(client(), :audio | :video) :: {chunk() | :end_of_stream, client()}
@spec do_read_chunk(client(), :audio | :video) ::
{chunk() | :end_of_stream | {:error, atom()}, client()}
defp do_read_chunk(client, media_type) do
client = ensure_media_playlist_loaded(client)

with impl when impl != nil <- client.demuxing_engine_impl,
track_id <- get_track_id!(client, media_type),
{:ok, track_id} <- get_track_id(client, media_type),
{:ok, chunk, demuxing_engine} <- client.demuxing_engine |> impl.pop_chunk(track_id) do
client =
with %{timestamp_offsets: %{^media_type => nil}} <- client do
Expand All @@ -152,6 +151,12 @@ defmodule ExHLS.Client do

{chunk, client}
else
# returned from the second match
:error ->
client = %{client | media_types: client.media_types -- [media_type]}
{{:error, :no_track_for_media_type}, client}

# returned from the first or the third match
other ->
case other do
{:error, _reason, demuxing_engine} -> %{client | demuxing_engine: demuxing_engine}
Expand All @@ -175,29 +180,35 @@ defmodule ExHLS.Client do
else
_other ->
media_type = media_type_with_lower_ts(client)
{chunk_or_eos, client} = do_read_chunk(client, media_type)
{chunk_eos_or_error, client} = do_read_chunk(client, media_type)

with %ExHLS.Chunk{} <- chunk_or_eos do
with %ExHLS.Chunk{} = chunk <- chunk_eos_or_error do
client
|> update_in([:queues, media_type], &Qex.push(&1, chunk_or_eos))
|> update_in([:queues, media_type], &Qex.push(&1, chunk))
|> get_tracks_info()
else
:end_of_stream ->
{:error, "end of stream reached, but tracks info is not available", client}

{:error, :no_track_for_media_type} when client.media_types != [] ->
client |> get_tracks_info()

{:error, :no_track_for_media_type} when client.media_types == [] ->
{:error, "no supported media types in HLS stream", client}
end
end
end

defp media_type_with_lower_ts(client) do
cond do
client.timestamp_offsets.audio == nil ->
client.timestamp_offsets.audio == nil and :audio in client.media_types ->
:audio

client.timestamp_offsets.video == nil ->
client.timestamp_offsets.video == nil and :video in client.media_types ->
:video

true ->
[:audio, :video]
client.media_types
|> Enum.min_by(fn media_type ->
client.last_timestamps[media_type] - client.timestamp_offsets[media_type]
end)
Expand Down Expand Up @@ -260,13 +271,6 @@ defmodule ExHLS.Client do
}
end

defp get_track_id!(client, type) when type in [:audio, :video] do
case get_track_id(client, type) do
{:ok, track_id} -> track_id
:error -> raise "Track ID for #{type} not found in client #{inspect(client, pretty: true)}"
end
end

defp get_track_id(client, type) when type in [:audio, :video] do
impl = client.demuxing_engine_impl

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.2"},
{:ex_m3u8, "~> 0.15.3"},
{:req, "~> 0.5.10"},
{:qex, "~> 0.5.1"},
{:membrane_mp4_plugin, "~> 0.35.3"},
Expand Down
55 changes: 52 additions & 3 deletions test/client_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ defmodule Client.Test do
use ExUnit.Case, async: true

alias ExHLS.Client

alias Membrane.{AAC, H264, RemoteStream}

@fixtures "https://raw.githubusercontent.com/membraneframework-labs/ex_hls/refs/heads/support-one-media-type/test/fixtures/"
@fmp4_url @fixtures <> "fmp4/output.m3u8"
@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"
@fmp4_url "https://raw.githubusercontent.com/membraneframework-labs/ex_hls/refs/heads/plug-demuxing-engine-into-client/fixture/output.m3u8"

describe "if client reads video and audio chunks of the HLS" do
test "(MPEGTS) stream" do
client = Client.new(@mpegts_url)
Expand Down Expand Up @@ -46,7 +49,6 @@ defmodule Client.Test do
33, 70, 254, 208, 221, 101, 200, 21, 97, 0>> <> _rest = audio_chunk.payload
end

@tag :a
test "(fMP4) stream" do
client = Client.new(@fmp4_url)

Expand Down Expand Up @@ -97,4 +99,51 @@ defmodule Client.Test do
assert second_audio_chunk.payload == <<33, 16, 4, 96, 140, 28>>
end
end

test "(MPEGTS) stream with only video" do
client = Client.new(@mpegts_only_video_url)

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()

{video_chunk, _client} = Client.read_video_chunk(client)

assert %{pts_ms: 1480, dts_ms: 1400} = video_chunk
assert byte_size(video_chunk.payload) == 822

assert <<0, 0, 0, 1, 9, 240, 0, 0, 0, 1, 6, 5, 255, 255, 167, 220, 69, 233, 189, 230, 217, 72,
183, 150, 44, 216, 32, 217, 35, 238, 239, 120, 50, 54, 52, 32, 45, 32, 99, 111, 114,
101, 32, 49, 54, 52, 32, 114>> <> _rest = video_chunk.payload

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

test "(fMP4) stream with only video" do
client = Client.new(@fmp4_only_video_url)

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

assert [
%H264{
width: 480,
height: 270,
alignment: :au,
nalu_in_metadata?: false,
stream_structure: {:avc1, _binary}
}
] = tracks_info |> Map.values()

{video_chunk, _client} = Client.read_video_chunk(client)

assert %{pts_ms: 0, dts_ms: 0} = video_chunk
assert byte_size(video_chunk.payload) == 823

assert <<0, 0, 0, 2, 9, 240, 0, 0, 2, 171, 6, 5, 255, 255, 167, 220, 69, 233, 189, 230, 217,
72, 183, 150, 44, 216, 32, 217, 35, 238, 239, 120, 50, 54, 52, 32, 45, 32, 99, 111,
114, 101, 32, 49, 54, 52, 32, 114>> <> _rest = video_chunk.payload
end
end
File renamed without changes.
File renamed without changes.
File renamed without changes.
22 changes: 22 additions & 0 deletions test/fixtures/fmp4_only_video/output.m3u8
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#EXTM3U
#EXT-X-VERSION:7
#EXT-X-TARGETDURATION:4
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MAP:URI="output.m4s",BYTERANGE="848@0"
#EXTINF:3.840625,
#EXT-X-BYTERANGE:93742@848
output.m4s
#EXTINF:1.920313,
#EXT-X-BYTERANGE:219741@94590
output.m4s
#EXTINF:1.920313,
#EXT-X-BYTERANGE:148297@314331
output.m4s
#EXTINF:1.920313,
#EXT-X-BYTERANGE:45690@462628
output.m4s
#EXTINF:0.400065,
#EXT-X-BYTERANGE:34649@508318
output.m4s
#EXT-X-ENDLIST
Binary file added test/fixtures/fmp4_only_video/output.m4s
Binary file not shown.
12 changes: 12 additions & 0 deletions test/fixtures/mpeg_ts_only_video/output_playlist.m3u8
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:5.760933,
video_segment_000.ts
#EXTINF:3.840633,
video_segment_001.ts
#EXTINF:0.400111,
video_segment_002.ts
#EXT-X-ENDLIST
Binary file not shown.
Binary file not shown.
Binary file not shown.