diff --git a/README.md b/README.md index e057e4c..94704be 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,21 @@ -# Membrane Template Plugin +# ExHLS -[![Hex.pm](https://img.shields.io/hexpm/v/membrane_template_plugin.svg)](https://hex.pm/packages/membrane_template_plugin) -[![API Docs](https://img.shields.io/badge/api-docs-yellow.svg?style=flat)](https://hexdocs.pm/membrane_template_plugin) -[![CircleCI](https://circleci.com/gh/membraneframework/membrane_template_plugin.svg?style=svg)](https://circleci.com/gh/membraneframework/membrane_template_plugin) +[![Hex.pm](https://img.shields.io/hexpm/v/ex_hls.svg)](https://hex.pm/packages/ex_hls) +[![API Docs](https://img.shields.io/badge/api-docs-yellow.svg?style=flat)](https://hexdocs.pm/ex_hls) +[![CircleCI](https://circleci.com/gh/membraneframework/ex_hls.svg?style=svg)](https://circleci.com/gh/membraneframework/ex_hls) -This repository contains a template for new plugins. - -Check out different branches for other flavors of this template. +This repository contains ExHLS - an Elixir package for handling HLS streams It's a part of the [Membrane Framework](https://membrane.stream). ## Installation -The package can be installed by adding `membrane_template_plugin` to your list of dependencies in `mix.exs`: +The package can be installed by adding `ex_hls` to your list of dependencies in `mix.exs`: ```elixir def deps do [ - {:membrane_template_plugin, "~> 0.1.0"} + {:ex_hls, "~> 0.1.0"} ] end ``` @@ -28,8 +26,8 @@ TODO ## Copyright and License -Copyright 2020, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane_template_plugin) +Copyright 2025, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=ex_hls) -[![Software Mansion](https://logo.swmansion.com/logo?color=white&variant=desktop&width=200&tag=membrane-github)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=membrane_template_plugin) +[![Software Mansion](https://logo.swmansion.com/logo?color=white&variant=desktop&width=200&tag=membrane-github)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=ex_hls) Licensed under the [Apache License, Version 2.0](LICENSE) diff --git a/fixture/fileSequence0.m4s b/fixture/fileSequence0.m4s new file mode 100644 index 0000000..f8e131f Binary files /dev/null and b/fixture/fileSequence0.m4s differ diff --git a/fixture/init.mp4 b/fixture/init.mp4 new file mode 100644 index 0000000..4eca140 Binary files /dev/null and b/fixture/init.mp4 differ diff --git a/fixture/output.m3u8 b/fixture/output.m3u8 new file mode 100644 index 0000000..ad28290 --- /dev/null +++ b/fixture/output.m3u8 @@ -0,0 +1,9 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:10 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-MAP:URI="init.mp4" +#EXTINF:10.001628, +fileSequence0.m4s +#EXT-X-ENDLIST diff --git a/lib/ex_hls/chunk.ex b/lib/ex_hls/chunk.ex new file mode 100644 index 0000000..16fb5ef --- /dev/null +++ b/lib/ex_hls/chunk.ex @@ -0,0 +1,21 @@ +defmodule ExHLS.Chunk do + @moduledoc """ + A struct representing a media chunk in the ExHLS demuxing engine. + """ + @enforce_keys [:payload, :pts_ms, :dts_ms, :track_id] + defstruct @enforce_keys ++ [metadata: %{}] + + @type t :: %__MODULE__{ + payload: binary(), + pts_ms: integer(), + dts_ms: integer(), + track_id: term(), + metadata: map() + } + + # timestamps need to be represented in milliseconds + @time_base 1000 + + @spec time_base() :: integer() + def time_base(), do: @time_base +end diff --git a/lib/ex_hls/client.ex b/lib/ex_hls/client.ex new file mode 100644 index 0000000..9ce506d --- /dev/null +++ b/lib/ex_hls/client.ex @@ -0,0 +1,286 @@ +defmodule ExHLS.Client do + @moduledoc """ + Module providing functionality to read and demux HLS streams. + It allows reading chunks from the stream, choosing variants, and managing media playlists. + """ + + alias ExHLS.DemuxingEngine + alias Membrane.{AAC, H264, RemoteStream} + + @opaque client :: map() + @type chunk :: any() + + @type variant_description :: %{ + id: integer(), + name: String.t() | nil, + frame_rate: number() | nil, + resolution: {integer(), integer()} | nil, + codecs: String.t() | nil, + bandwidth: integer() | nil, + uri: String.t() | nil + } + + @doc """ + Starts the ExHLS client with the given URL and demuxing engine implementation. + + By default, it uses `DemuxingEngine.MPEGTS` as the demuxing engine implementation. + """ + + @spec new(String.t()) :: client() + def new(url) do + %{status: 200, body: request_body} = Req.get!(url) + multivariant_playlist = request_body |> ExM3U8.deserialize_multivariant_playlist!([]) + + %{ + media_playlist: nil, + media_base_url: nil, + multivariant_playlist: multivariant_playlist, + base_url: Path.dirname(url), + video_chunks: [], + demuxing_engine_impl: nil, + demuxing_engine: nil, + queues: %{audio: Qex.new(), video: Qex.new()}, + timestamp_offsets: %{audio: nil, video: nil}, + last_timestamps: %{audio: nil, video: nil} + } + end + + defp ensure_media_playlist_loaded(%{media_playlist: nil} = client) do + get_variants(client) + |> Map.to_list() + |> case do + [] -> + read_media_playlist_without_variant(client) + + [{variant_id, _variant}] -> + choose_variant(client, variant_id) + + _many_variants -> + raise """ + If there are available variants, you have to choose one of them using \ + `choose_variant/2` function before reading chunks. Available variants: + #{get_variants(client) |> inspect(limit: :infinity, pretty: true)} + """ + end + end + + 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 + | media_playlist: deserialized_media_playlist, + media_base_url: client.base_url + } + end + + @spec get_variants(client()) :: %{optional(integer()) => variant_description()} + def get_variants(client) do + client.multivariant_playlist.items + |> Enum.filter(&match?(%ExM3U8.Tags.Stream{}, &1)) + |> Enum.with_index(fn variant, index -> + variant_description = + variant + |> Map.take([:name, :frame_rate, :resolution, :codecs, :bandwidth, :uri]) + |> Map.put(:id, index) + + {index, variant_description} + end) + |> Map.new() + end + + @spec choose_variant(client(), String.t()) :: client() + def choose_variant(client, variant_id) do + chosen_variant = + get_variants(client) + |> Map.fetch!(variant_id) + + media_playlist = Path.join(client.base_url, chosen_variant.uri) |> Req.get!() + + deserialized_media_playlist = + ExM3U8.deserialize_media_playlist!(media_playlist.body, []) + + media_base_url = Path.join(client.base_url, Path.dirname(chosen_variant.uri)) + + %{ + client + | media_playlist: deserialized_media_playlist, + media_base_url: media_base_url + } + end + + @spec read_video_chunk(client()) :: chunk() | :end_of_stream + def read_video_chunk(client), do: pop_queue_or_do_read_chunk(client, :video) + + @spec read_audio_chunk(client()) :: chunk() | :end_of_stream + def read_audio_chunk(client), do: pop_queue_or_do_read_chunk(client, :audio) + + defp pop_queue_or_do_read_chunk(client, media_type) do + client.queues[media_type] + |> Qex.pop() + |> case do + {{:value, chunk}, queue} -> + client = client |> put_in([:queues, media_type], queue) + {chunk, client} + + {:empty, _queue} -> + do_read_chunk(client, media_type) + end + end + + @spec do_read_chunk(client(), :audio | :video) :: {chunk() | :end_of_stream, 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, chunk, demuxing_engine} <- client.demuxing_engine |> impl.pop_chunk(track_id) do + client = + with %{timestamp_offsets: %{^media_type => nil}} <- client do + client |> put_in([:timestamp_offsets, media_type], chunk.dts_ms) + end + |> put_in([:last_timestamps, media_type], chunk.dts_ms) + |> put_in([:demuxing_engine], demuxing_engine) + + {chunk, client} + else + other -> + case other do + {:error, _reason, demuxing_engine} -> %{client | demuxing_engine: demuxing_engine} + nil -> client + end + |> download_chunk() + |> case do + {:ok, client} -> do_read_chunk(client, media_type) + {:end_of_stream, client} -> {:end_of_stream, client} + end + end + end + + @spec get_tracks_info(client()) :: + {:ok, %{optional(integer()) => struct()}, client()} + | {:error, reason :: any(), client()} + def get_tracks_info(client) do + with impl when impl != nil <- client.demuxing_engine_impl, + {:ok, tracks_info} <- client.demuxing_engine |> impl.get_tracks_info() do + {:ok, tracks_info, client} + else + _other -> + media_type = media_type_with_lower_ts(client) + {chunk_or_eos, client} = do_read_chunk(client, media_type) + + with %ExHLS.Chunk{} <- chunk_or_eos do + client + |> update_in([:queues, media_type], &Qex.push(&1, chunk_or_eos)) + |> get_tracks_info() + else + :end_of_stream -> + {:error, "end of stream reached, but tracks info is not available", client} + end + end + end + + defp media_type_with_lower_ts(client) do + cond do + client.timestamp_offsets.audio == nil -> + :audio + + client.timestamp_offsets.video == nil -> + :video + + true -> + [:audio, :video] + |> Enum.min_by(fn media_type -> + client.last_timestamps[media_type] - client.timestamp_offsets[media_type] + end) + end + end + + defp download_chunk(client) do + client = ensure_media_playlist_loaded(client) + + case client.media_playlist.timeline do + [%{uri: segment_uri} | rest] -> + client = + with %{demuxing_engine: nil} <- client do + resolve_demuxing_engine(segment_uri, client) + end + + request_result = + Path.join(client.media_base_url, segment_uri) + |> Req.get!() + + demuxing_engine = + client.demuxing_engine + |> client.demuxing_engine_impl.feed!(request_result.body) + + client = + %{ + client + | demuxing_engine: demuxing_engine, + media_playlist: %{client.media_playlist | timeline: rest} + } + + {:ok, client} + + [_other_tag | rest] -> + %{client | media_playlist: %{client.media_playlist | timeline: rest}} + |> download_chunk() + + [] -> + client = + client + |> Map.update!(:demuxing_engine, &client.demuxing_engine_impl.end_stream/1) + + {:end_of_stream, client} + end + end + + defp resolve_demuxing_engine(segment_uri, %{demuxing_engine: nil} = client) do + demuxing_engine_impl = + case Path.extname(segment_uri) do + ".ts" -> DemuxingEngine.MPEGTS + ".m4s" -> DemuxingEngine.CMAF + ".mp4" -> DemuxingEngine.CMAF + _other -> raise "Unsupported segment URI extension: #{segment_uri |> inspect()}" + end + + %{ + client + | demuxing_engine_impl: demuxing_engine_impl, + demuxing_engine: demuxing_engine_impl.new() + } + 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 + + with {:ok, tracks_info} <- client.demuxing_engine |> impl.get_tracks_info() do + tracks_info + |> Enum.find_value(:error, fn + {id, %AAC{}} when type == :audio -> {:ok, id} + {id, %RemoteStream{content_format: AAC}} when type == :audio -> {:ok, id} + {id, %H264{}} when type == :video -> {:ok, id} + {id, %RemoteStream{content_format: H264}} when type == :video -> {:ok, id} + _different_type -> false + end) + else + {:error, _reason} -> :error + end + end +end diff --git a/lib/ex_hls/demuxing_engine.ex b/lib/ex_hls/demuxing_engine.ex new file mode 100644 index 0000000..d63ab3c --- /dev/null +++ b/lib/ex_hls/demuxing_engine.ex @@ -0,0 +1,16 @@ +defmodule ExHLS.DemuxingEngine do + @moduledoc false + + @type t :: any() + + @callback new() :: t() + + @callback feed!(t(), binary()) :: t() + + @callback get_tracks_info(t()) :: {:ok, %{optional(integer()) => struct()}} | {:error, any()} + + @callback pop_chunk(t(), track_id :: any()) :: + {:ok, ExHLS.Chunk.t(), t()} | {:error, :empty_track_data, t()} + + @callback end_stream(t()) :: t() +end diff --git a/lib/ex_hls/demuxing_engine/cmaf.ex b/lib/ex_hls/demuxing_engine/cmaf.ex new file mode 100644 index 0000000..4311ced --- /dev/null +++ b/lib/ex_hls/demuxing_engine/cmaf.ex @@ -0,0 +1,78 @@ +defmodule ExHLS.DemuxingEngine.CMAF do + @moduledoc false + @behaviour ExHLS.DemuxingEngine + + alias Membrane.MP4.Demuxer.CMAF + + @enforce_keys [:demuxer] + defstruct @enforce_keys ++ [tracks_to_chunks: %{}] + + @type t :: %__MODULE__{ + demuxer: CMAF.Engine.t(), + tracks_to_chunks: map() + } + + @impl true + def new() do + %__MODULE__{ + demuxer: CMAF.Engine.new() + } + end + + @impl true + def feed!(%__MODULE__{} = demuxing_engine, binary) do + {:ok, chunks, demuxer} = + demuxing_engine.demuxer + |> CMAF.Engine.feed!(binary) + |> CMAF.Engine.pop_samples() + + new_tracks_to_chunks = + chunks + |> Enum.group_by( + fn chunk -> chunk.track_id end, + fn %CMAF.Engine.Sample{} = chunk -> + %ExHLS.Chunk{ + payload: chunk.payload, + pts_ms: chunk.pts, + dts_ms: chunk.dts, + track_id: chunk.track_id + } + end + ) + + tracks_to_chunks = + new_tracks_to_chunks + |> Enum.reduce( + demuxing_engine.tracks_to_chunks, + fn {track_id, new_chunks}, tracks_to_chunks -> + tracks_to_chunks + |> Map.put_new_lazy(track_id, &Qex.new/0) + |> Map.update!(track_id, fn track_qex -> + new_chunks |> Enum.reduce(track_qex, &Qex.push(&2, &1)) + end) + end + ) + + %__MODULE__{demuxing_engine | demuxer: demuxer, tracks_to_chunks: tracks_to_chunks} + end + + @impl true + def get_tracks_info(demuxing_engine) do + CMAF.Engine.get_tracks_info(demuxing_engine.demuxer) + end + + @impl true + def pop_chunk(demuxing_engine, track_id) do + with qex when qex != nil <- demuxing_engine.tracks_to_chunks[track_id], + {{:value, chunk}, popped_qex} <- Qex.pop(qex) do + demuxing_engine = put_in(demuxing_engine.tracks_to_chunks[track_id], popped_qex) + {:ok, chunk, demuxing_engine} + else + nil -> {:error, :unknown_track, demuxing_engine} + {:empty, _qex} -> {:error, :empty_track_data, demuxing_engine} + end + end + + @impl true + def end_stream(demuxing_engine), do: demuxing_engine +end diff --git a/lib/ex_hls/demuxing_engine/mpeg_ts.ex b/lib/ex_hls/demuxing_engine/mpeg_ts.ex new file mode 100644 index 0000000..4b41349 --- /dev/null +++ b/lib/ex_hls/demuxing_engine/mpeg_ts.ex @@ -0,0 +1,92 @@ +defmodule ExHLS.DemuxingEngine.MPEGTS do + @moduledoc false + @behaviour ExHLS.DemuxingEngine + + require Logger + alias Membrane.{AAC, H264, RemoteStream} + alias MPEG.TS.Demuxer + + @enforce_keys [:demuxer] + defstruct @enforce_keys + + @type t :: %__MODULE__{ + demuxer: Demuxer.t() + } + + @impl true + def new() do + demuxer = Demuxer.new() + + # we need to explicitly override that `waiting_random_access_indicator` as otherwise Demuxer + # discards all the input data + # TODO - figure out how to do it properly + demuxer = %{demuxer | waiting_random_access_indicator: false} + + %__MODULE__{demuxer: demuxer} + end + + @impl true + def feed!(%__MODULE__{} = demuxing_engine, binary) do + demuxing_engine + |> Map.update!(:demuxer, &Demuxer.push_buffer(&1, binary)) + end + + @impl true + def get_tracks_info(%__MODULE__{} = demuxing_engine) do + with %{streams: streams} <- demuxing_engine.demuxer.pmt do + tracks_info = + streams + |> Enum.flat_map(fn + {id, %{stream_type: :AAC}} -> + [{id, %RemoteStream{content_format: AAC}}] + + {id, %{stream_type: :H264}} -> + [{id, %RemoteStream{content_format: H264}}] + + {id, unsupported_stream_info} -> + Logger.warning(""" + #{__MODULE__ |> inspect()}: dropping unsupported stream with id #{id |> inspect()}.\ + Stream info: #{unsupported_stream_info |> inspect(pretty: true)} + """) + + [] + end) + |> Map.new() + + {:ok, tracks_info} + else + nil -> {:error, :tracks_info_not_available} + end + end + + @impl true + def pop_chunk(%__MODULE__{} = demuxing_engine, track_id) do + with {[packet], demuxer} <- Demuxer.take(demuxing_engine.demuxer, track_id) do + chunk = %ExHLS.Chunk{ + payload: packet.data, + pts_ms: packet.pts |> packet_ts_to_millis(), + dts_ms: packet.dts |> packet_ts_to_millis(), + track_id: track_id, + metadata: %{ + discontinuity: packet.discontinuity, + is_aligned: packet.is_aligned + } + } + + {:ok, chunk, %{demuxing_engine | demuxer: demuxer}} + else + {[], demuxer} -> + {:error, :empty_track_data, %{demuxing_engine | demuxer: demuxer}} + end + end + + @mpegts_clock_rate 90 + defp packet_ts_to_millis(ts), do: div(ts, @mpegts_clock_rate) + + @impl true + def end_stream(%__MODULE__{} = demuxing_engine) do + demuxer = Demuxer.end_of_stream(demuxing_engine.demuxer) + + %{demuxing_engine | demuxer: demuxer} + end +end diff --git a/lib/membrane_template.ex b/lib/membrane_template.ex deleted file mode 100644 index c6882fb..0000000 --- a/lib/membrane_template.ex +++ /dev/null @@ -1,2 +0,0 @@ -defmodule Membrane.Template do -end diff --git a/mix.exs b/mix.exs index 611ad1c..8a46eaa 100644 --- a/mix.exs +++ b/mix.exs @@ -1,12 +1,12 @@ -defmodule Membrane.Template.Mixfile do +defmodule ExHLS.Mixfile do use Mix.Project @version "0.1.0" - @github_url "https://github.com/membraneframework/membrane_template_plugin" + @github_url "https://github.com/membraneframework-labs/ex_hls" def project do [ - app: :membrane_template_plugin, + app: :ex_hls, version: @version, elixir: "~> 1.13", elixirc_paths: elixirc_paths(Mix.env()), @@ -15,11 +15,11 @@ defmodule Membrane.Template.Mixfile do dialyzer: dialyzer(), # hex - description: "Template Plugin for Membrane Framework", + description: "Elixir package for handling HLS streams", package: package(), # docs - name: "Membrane Template plugin", + name: "ExHLS", source_url: @github_url, docs: docs(), homepage_url: "https://membrane.stream" @@ -37,7 +37,13 @@ defmodule Membrane.Template.Mixfile do defp deps do [ - {:membrane_core, "~> 1.0"}, + {:ex_m3u8, "~> 0.15.2"}, + {:req, "~> 0.5.10"}, + {:qex, "~> 0.5.1"}, + {:membrane_mp4_plugin, "~> 0.35.3"}, + {:membrane_h26x_plugin, "~> 0.10.2"}, + # {:mpeg_ts, github: "kim-company/kim_mpeg_ts"}, + {:mpeg_ts, github: "membraneframework-labs/kim_mpeg_ts", branch: "backport-v1.0.3"}, {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, {:dialyxir, ">= 0.0.0", only: :dev, runtime: false}, {:credo, ">= 0.0.0", only: :dev, runtime: false} @@ -74,7 +80,7 @@ defmodule Membrane.Template.Mixfile do extras: ["README.md", "LICENSE"], formatters: ["html"], source_ref: "v#{@version}", - nest_modules_by_prefix: [Membrane.Template] + nest_modules_by_prefix: [ExHLS] ] end end diff --git a/mix.lock b/mix.lock index 8d15140..cc9586c 100644 --- a/mix.lock +++ b/mix.lock @@ -1,21 +1,44 @@ %{ - "bunch": {:hex, :bunch, "1.6.0", "4775f8cdf5e801c06beed3913b0bd53fceec9d63380cdcccbda6be125a6cfd54", [:mix], [], "hexpm", "ef4e9abf83f0299d599daed3764d19e8eac5d27a5237e5e4d5e2c129cfeb9a22"}, - "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, + "bimap": {:hex, :bimap, "1.3.0", "3ea4832e58dc83a9b5b407c6731e7bae87458aa618e6d11d8e12114a17afa4b3", [:mix], [], "hexpm", "bf5a2b078528465aa705f405a5c638becd63e41d280ada41e0f77e6d255a10b4"}, + "bunch": {:hex, :bunch, "1.6.1", "5393d827a64d5f846092703441ea50e65bc09f37fd8e320878f13e63d410aec7", [:mix], [], "hexpm", "286cc3add551628b30605efbe2fca4e38cc1bea89bcd0a1a7226920b3364fe4a"}, + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"}, - "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, - "dialyxir": {:hex, :dialyxir, "1.4.1", "a22ed1e7bd3a3e3f197b68d806ef66acb61ee8f57b3ac85fc5d57354c5482a93", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "84b795d6d7796297cca5a3118444b80c7d94f7ce247d49886e7c291e1ae49801"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.35", "437773ca9384edf69830e26e9e7b2e0d22d2596c4a6b17094a3b29f01ea65bb8", [:mix], [], "hexpm", "8652ba3cb85608d0d7aa2d21b45c6fad4ddc9a1f9a1f1b30ca3a246f0acc33f6"}, - "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "ex_doc": {:hex, :ex_doc, "0.30.6", "5f8b54854b240a2b55c9734c4b1d0dd7bdd41f71a095d42a70445c03cf05a281", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bd48f2ddacf4e482c727f9293d9498e0881597eae6ddc3d9562bd7923375109f"}, - "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, - "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, - "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, - "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, - "membrane_core": {:hex, :membrane_core, "1.0.0", "1b543aefd952283be1f2a215a1db213aa4d91222722ba03cd35280622f1905ee", [:mix], [{:bunch, "~> 1.6", [hex: :bunch, repo: "hexpm", optional: false]}, {:qex, "~> 0.3", [hex: :qex, repo: "hexpm", optional: false]}, {:ratio, "~> 3.0", [hex: :ratio, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "352c90fd0a29942143c4bf7a727cc05c632e323f50a1a4e99321b1e8982f1533"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, + "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, + "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, + "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.2", "504d25eef296b4dec3b8e33e810bc8b5344d565998cd83914ffe1b8503737c02", [: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", "732f2d972e42c116a70802f9898c51b54916e542cc50968ac6980512ec90f42b"}, + "ex_m3u8": {:hex, :ex_m3u8, "0.15.2", "7b13d5c719fa6bd58e1fd54d3d17b27e4c2a21fa10ad74c39e7ec36be165d58a", [: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", "160d31f132f15856fcd4a4c40e448784f3105e5fcd05ac39de6a4ccc8aa4a697"}, + "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, + "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"}, + "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "logger_backends": {:hex, :logger_backends, "1.0.0", "09c4fad6202e08cb0fbd37f328282f16539aca380f512523ce9472b28edc6bdf", [:mix], [], "hexpm", "1faceb3e7ec3ef66a8f5746c5afd020e63996df6fd4eb8cdb789e5665ae6c9ce"}, + "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, + "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, + "membrane_aac_format": {:hex, :membrane_aac_format, "0.8.0", "515631eabd6e584e0e9af2cea80471fee6246484dbbefc4726c1d93ece8e0838", [:mix], [{:bimap, "~> 1.1", [hex: :bimap, repo: "hexpm", optional: false]}], "hexpm", "a30176a94491033ed32be45e51d509fc70a5ee6e751f12fd6c0d60bd637013f6"}, + "membrane_cmaf_format": {:hex, :membrane_cmaf_format, "0.7.1", "9ea858faefdcb181cdfa8001be827c35c5f854e9809ad57d7062cff1f0f703fd", [:mix], [], "hexpm", "3c7b4ed2a986e27f6f336d2f19e9442cb31d93b3142fc024c019572faca54a73"}, + "membrane_core": {:hex, :membrane_core, "1.2.3", "0e23f50b2e7dfe95dd6047cc341807991f9d0349cd98455cc5cbfab41ba5233c", [:mix], [{:bunch, "~> 1.6", [hex: :bunch, repo: "hexpm", optional: false]}, {:qex, "~> 0.3", [hex: :qex, repo: "hexpm", optional: false]}, {:ratio, "~> 3.0 or ~> 4.0", [hex: :ratio, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e3099dcb52d136a4aef84b6fbb20905ea55d9f0d2d6726f7b589e8d169a55cd"}, + "membrane_file_plugin": {:hex, :membrane_file_plugin, "0.17.2", "650e134c2345d946f930082fac8bac9f5aba785a7817d38a9a9da41ffc56fa92", [:mix], [{:logger_backends, "~> 1.0", [hex: :logger_backends, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "df50c6040004cd7b901cf057bd7e99c875bbbd6ae574efc93b2c753c96f43b9d"}, + "membrane_h264_format": {:hex, :membrane_h264_format, "0.6.1", "44836cd9de0abe989b146df1e114507787efc0cf0da2368f17a10c47b4e0738c", [:mix], [], "hexpm", "4b79be56465a876d2eac2c3af99e115374bbdc03eb1dea4f696ee9a8033cd4b0"}, + "membrane_h265_format": {:hex, :membrane_h265_format, "0.2.0", "1903c072cf7b0980c4d0c117ab61a2cd33e88782b696290de29570a7fab34819", [:mix], [], "hexpm", "6df418bdf242c0d9f7dbf2e5aea4c2d182e34ac9ad5a8b8cef2610c290002e83"}, + "membrane_h26x_plugin": {:hex, :membrane_h26x_plugin, "0.10.5", "e9fa1ee9cda944259c4d2728c8b279bfe0152a3a6c1af187b07fa8411ca4e25e", [:mix], [{:bunch, "~> 1.4", [hex: :bunch, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_h264_format, "~> 0.6.0", [hex: :membrane_h264_format, repo: "hexpm", optional: false]}, {:membrane_h265_format, "~> 0.2.0", [hex: :membrane_h265_format, repo: "hexpm", optional: false]}], "hexpm", "dd0287a6b6223e47bba30a8952d6ec53db35f6a3e33203b7ad786e995711f098"}, + "membrane_mp4_format": {:hex, :membrane_mp4_format, "0.8.0", "8c6e7d68829228117d333b4fbb030e7be829aab49dd8cb047fdc664db1812e6a", [:mix], [], "hexpm", "148dea678a1f82ccfd44dbde6f936d2f21255f496cb45a22cc6eec427f025522"}, + "membrane_mp4_plugin": {:hex, :membrane_mp4_plugin, "0.35.3", "80228f4332eeef4fce4d90184a82bd5869d184f78438419660da7dc91871a238", [:mix], [{:bunch, "~> 1.5", [hex: :bunch, repo: "hexpm", optional: false]}, {:membrane_aac_format, "~> 0.8.0", [hex: :membrane_aac_format, repo: "hexpm", optional: false]}, {:membrane_cmaf_format, "~> 0.7.0", [hex: :membrane_cmaf_format, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_file_plugin, "~> 0.17.0", [hex: :membrane_file_plugin, repo: "hexpm", optional: false]}, {:membrane_h264_format, "~> 0.6.1", [hex: :membrane_h264_format, repo: "hexpm", optional: false]}, {:membrane_h265_format, "~> 0.2.0", [hex: :membrane_h265_format, repo: "hexpm", optional: false]}, {:membrane_mp4_format, "~> 0.8.0", [hex: :membrane_mp4_format, repo: "hexpm", optional: false]}, {:membrane_opus_format, "~> 0.3.0", [hex: :membrane_opus_format, repo: "hexpm", optional: false]}, {:membrane_timestamp_queue, "~> 0.2.1", [hex: :membrane_timestamp_queue, repo: "hexpm", optional: false]}], "hexpm", "c6d8b20e49540329f246e9a9c69adae330d424802fdfa1e6485d76a5257e6169"}, + "membrane_opus_format": {:hex, :membrane_opus_format, "0.3.0", "3804d9916058b7cfa2baa0131a644d8186198d64f52d592ae09e0942513cb4c2", [:mix], [], "hexpm", "8fc89c97be50de23ded15f2050fe603dcce732566fe6fdd15a2de01cb6b81afe"}, + "membrane_timestamp_queue": {:hex, :membrane_timestamp_queue, "0.2.2", "1c831b2273d018a6548654aa9f7fa7c4b683f71d96ffe164934ef55f9d11f693", [:mix], [{:heap, "~> 2.0", [hex: :heap, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "7c830e760baaced0988421671cd2c83c7cda8d1bd2b61fd05332711675d1204f"}, + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, + "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, + "mpeg_ts": {:git, "https://github.com/membraneframework-labs/kim_mpeg_ts.git", "8c036fca6558a4339033a5a8697ebf147728f36b", [branch: "backport-v1.0.3"]}, + "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"}, "qex": {:hex, :qex, "0.5.1", "0d82c0f008551d24fffb99d97f8299afcb8ea9cf99582b770bd004ed5af63fd6", [:mix], [], "hexpm", "935a39fdaf2445834b95951456559e9dc2063d0a055742c558a99987b38d6bab"}, - "ratio": {:hex, :ratio, "3.0.2", "60a5976872a4dc3d873ecc57eed1738589e99d1094834b9c935b118231297cfb", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:numbers, "~> 5.2.0", [hex: :numbers, repo: "hexpm", optional: false]}], "hexpm", "3a13ed5a30ad0bfd7e4a86bf86d93d2b5a06f5904417d38d3f3ea6406cdfc7bb"}, - "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "ratio": {:hex, :ratio, "4.0.1", "3044166f2fc6890aa53d3aef0c336f84b2bebb889dc57d5f95cc540daa1912f8", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:numbers, "~> 5.2.0", [hex: :numbers, repo: "hexpm", optional: false]}], "hexpm", "c60cbb3ccdff9ffa56e7d6d1654b5c70d9f90f4d753ab3a43a6bf40855b881ce"}, + "req": {:hex, :req, "0.5.15", "662020efb6ea60b9f0e0fac9be88cd7558b53fe51155a2d9899de594f9906ba9", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "a6513a35fad65467893ced9785457e91693352c70b58bbc045b47e5eb2ef0c53"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"}, } diff --git a/test/client_test.exs b/test/client_test.exs new file mode 100644 index 0000000..f496ac2 --- /dev/null +++ b/test/client_test.exs @@ -0,0 +1,100 @@ +defmodule Client.Test do + use ExUnit.Case, async: true + + alias ExHLS.Client + + alias Membrane.{AAC, H264, RemoteStream} + + @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) + + variant_720 = + Client.get_variants(client) + |> Map.values() + |> Enum.find(&(&1.resolution == {1280, 720})) + + assert variant_720 != nil + + client = client |> Client.choose_variant(variant_720.id) + {:ok, tracks_info, client} = Client.get_tracks_info(client) + + tracks_info = tracks_info |> Map.values() + + assert tracks_info |> length() == 2 + assert %RemoteStream{content_format: AAC, type: :bytestream} in tracks_info + assert %RemoteStream{content_format: H264, type: :bytestream} in tracks_info + + {video_chunk, client} = client |> Client.read_video_chunk() + + assert %{pts_ms: 10_033, dts_ms: 10_000} = video_chunk + assert byte_size(video_chunk.payload) == 1048 + + assert <<0, 0, 0, 1, 9, 240, 0, 0, 0, 1, 103, 100, 0, 31, 172, 217, 128, 80, 5, 187, 1, 16, + 0, 0, 3, 0, 16, 0, 0, 7, 128, 241, 131, 25, 160, 0, 0, 0, + 1>> <> _rest = video_chunk.payload + + {audio_chunk, _client} = Client.read_audio_chunk(client) + + assert %{pts_ms: 10_010, dts_ms: 10_010} = audio_chunk + assert byte_size(audio_chunk.payload) == 6154 + + assert <<255, 241, 80, 128, 4, 63, 252, 222, 4, 0, 0, 108, 105, 98, 102, 97, 97, 99, 32, 49, + 46, 50, 56, 0, 0, 66, 64, 147, 32, 4, 50, 0, 71, 255, 241, 80, 128, 10, 255, 252, + 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) + + assert Client.get_variants(client) == %{} + assert {:ok, tracks_info, client} = Client.get_tracks_info(client) + tracks_info = tracks_info |> Map.values() + + assert tracks_info |> length() == 2 + + assert %H264{ + width: 480, + height: 270, + alignment: :au, + nalu_in_metadata?: false, + stream_structure: {:avc1, _binary} + } = tracks_info |> Enum.find(&match?(%H264{}, &1)) + + assert %Membrane.AAC{ + sample_rate: 44_100, + channels: 2, + mpeg_version: 2, + samples_per_frame: 1024, + frames_per_buffer: 1, + encapsulation: :none, + config: {:esds, _binary} + } = tracks_info |> Enum.find(&match?(%AAC{}, &1)) + + {video_chunk, client} = Client.read_video_chunk(client) + + assert %{pts_ms: 0, dts_ms: 0} = video_chunk + assert byte_size(video_chunk.payload) == 775 + + assert <<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, 51, 49, 48, 56, 32, 51, 49, 101>> <> _rest = video_chunk.payload + + {first_audio_chunk, client} = Client.read_audio_chunk(client) + + assert %{pts_ms: 0, dts_ms: 0} = first_audio_chunk + + assert first_audio_chunk.payload == + <<220, 0, 76, 97, 118, 99, 54, 49, 46, 51, 46, 49, 48, 48, 0, 66, 32, 8, 193, 24, + 56>> + + {second_audio_chunk, _client} = Client.read_audio_chunk(client) + + assert %{pts_ms: 23, dts_ms: 23} = second_audio_chunk + assert second_audio_chunk.payload == <<33, 16, 4, 96, 140, 28>> + end + end +end