diff --git a/lib/boombox.ex b/lib/boombox.ex index eeb740d..25c6615 100644 --- a/lib/boombox.ex +++ b/lib/boombox.ex @@ -4,10 +4,13 @@ defmodule Boombox do See `run/1` for details and [examples.livemd](examples.livemd) for examples. """ + require Logger require Membrane.Time alias Membrane.RTP + @type force_transcoding() :: {:force_transcoding, boolean() | :audio | :video} + @type webrtc_signaling :: Membrane.WebRTC.Signaling.t() | String.t() @type in_stream_opts :: [ {:audio, :binary | boolean()} @@ -65,6 +68,7 @@ defmodule Boombox do | {:address, :inet.ip_address() | String.t()} | {:port, :inet.port_number()} | {:target, String.t()} + | force_transcoding() ] @type input :: @@ -79,15 +83,22 @@ defmodule Boombox do @type output :: (path_or_uri :: String.t()) + | {path_or_uri :: String.t(), [force_transcoding()]} | {:mp4, location :: String.t()} + | {:mp4, location :: String.t(), [force_transcoding()]} | {:webrtc, webrtc_signaling()} + | {:webrtc, webrtc_signaling(), [force_transcoding()]} | {:whip, uri :: String.t(), [{:token, String.t()} | {bandit_option :: atom(), term()}]} | {:hls, location :: String.t()} + | {:hls, location :: String.t(), [force_transcoding()]} | {:rtp, out_rtp_opts()} | {:stream, out_stream_opts()} @typep procs :: %{pipeline: pid(), supervisor: pid()} - @typep opts_map :: %{input: input(), output: output()} + @typep opts_map :: %{ + input: input(), + output: output() + } @doc """ Runs boombox with given input and output. @@ -103,13 +114,27 @@ defmodule Boombox do If the input is `{:stream, opts}`, a `Stream` or other `Enumerable` is expected as the first argument. + ``` + Boombox.run( + input: "path/to/file.mp4", + output: {:webrtc, "ws://0.0.0.0:1234"} + ) + ``` """ - @spec run(Enumerable.t() | nil, input: input(), output: output()) :: :ok | Enumerable.t() + @spec run(Enumerable.t() | nil, + input: input(), + output: output() + ) :: :ok | Enumerable.t() + @endpoint_opts [:input, :output] def run(stream \\ nil, opts) do - opts_keys = [:input, :output] - opts = Keyword.validate!(opts, opts_keys) |> Map.new(fn {k, v} -> {k, parse_opt!(k, v)} end) + opts = + opts + |> Keyword.validate!(@endpoint_opts) + |> Map.new(fn {key, value} -> {key, parse_endpoint_opt!(key, value)} end) + + :ok = maybe_log_transcoding_related_warning(opts) - if key = Enum.find(opts_keys, fn k -> not is_map_key(opts, k) end) do + if key = Enum.find(@endpoint_opts, fn k -> not is_map_key(opts, k) end) do raise "#{key} is not provided" end @@ -157,55 +182,80 @@ defmodule Boombox do end end - @spec parse_opt!(:input, input()) :: input() - @spec parse_opt!(:output, output()) :: output() - defp parse_opt!(direction, value) when is_binary(value) do + @spec parse_endpoint_opt!(:input, input()) :: input() + @spec parse_endpoint_opt!(:output, output()) :: output() + defp parse_endpoint_opt!(direction, value) when is_binary(value) do + parse_endpoint_opt!(direction, {value, []}) + end + + defp parse_endpoint_opt!(direction, {value, opts}) when is_binary(value) do uri = URI.parse(value) scheme = uri.scheme extension = if uri.path, do: Path.extname(uri.path) case {scheme, extension, direction} do - {scheme, ".mp4", :input} when scheme in [nil, "http", "https"] -> {:mp4, value} - {nil, ".mp4", :output} -> {:mp4, value} + {scheme, ".mp4", :input} when scheme in [nil, "http", "https"] -> {:mp4, value, opts} + {nil, ".mp4", :output} -> {:mp4, value, opts} {scheme, _ext, :input} when scheme in ["rtmp", "rtmps"] -> {:rtmp, value} {"rtsp", _ext, :input} -> {:rtsp, value} - {nil, ".m3u8", :output} -> {:hls, value} + {nil, ".m3u8", :output} -> {:hls, value, opts} _other -> raise ArgumentError, "Unsupported URI: #{value} for direction: #{direction}" end - |> then(&parse_opt!(direction, &1)) + |> then(&parse_endpoint_opt!(direction, &1)) end # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity - defp parse_opt!(direction, value) when is_tuple(value) do + defp parse_endpoint_opt!(direction, value) when is_tuple(value) do case value do {:mp4, location} when is_binary(location) and direction == :input -> - parse_opt!(:input, {:mp4, location, []}) + parse_endpoint_opt!(:input, {:mp4, location, []}) {:mp4, location, opts} when is_binary(location) and direction == :input -> - if Keyword.keyword?(opts), - do: {:mp4, location, transport: resolve_transport(location, opts)} + opts = opts |> Keyword.put(:transport, resolve_transport(location, opts)) + {:mp4, location, opts} {:mp4, location} when is_binary(location) and direction == :output -> - {:mp4, location} + {:mp4, location, []} - {:webrtc, %Membrane.WebRTC.Signaling{}} -> + {:mp4, location, _opts} when is_binary(location) and direction == :output -> value - {:webrtc, uri} when is_binary(uri) -> + {:webrtc, %Membrane.WebRTC.Signaling{}} when direction == :input -> + value + + {:webrtc, %Membrane.WebRTC.Signaling{} = signaling} -> + {:webrtc, signaling, []} + + {:webrtc, %Membrane.WebRTC.Signaling{}, _opts} when direction == :output -> + value + + {:webrtc, uri} when is_binary(uri) and direction == :input -> + value + + {:webrtc, uri} when is_binary(uri) and direction == :output -> + {:webrtc, uri, []} + + {:webrtc, uri, _opts} when is_binary(uri) and direction == :output -> value {:whip, uri} when is_binary(uri) -> - parse_opt!(direction, {:whip, uri, []}) + parse_endpoint_opt!(direction, {:whip, uri, []}) - {:whip, uri, opts} when is_binary(uri) and is_list(opts) -> - if Keyword.keyword?(opts) do - {:webrtc, {:whip, uri, opts}} - end + {:whip, uri, opts} when is_binary(uri) and is_list(opts) and direction == :input -> + if Keyword.keyword?(opts), do: {:webrtc, value} + + {:whip, uri, opts} when is_binary(uri) and is_list(opts) and direction == :output -> + {webrtc_opts, whip_opts} = split_webrtc_and_whip_opts(opts) + if Keyword.keyword?(opts), do: {:webrtc, {:whip, uri, whip_opts}, webrtc_opts} {:rtmp, arg} when direction == :input and (is_binary(arg) or is_pid(arg)) -> value {:hls, location} when direction == :output and is_binary(location) -> + {:hls, location, []} + + {:hls, location, opts} + when direction == :output and is_binary(location) and is_list(opts) -> value {:rtsp, location} when direction == :input and is_binary(location) -> @@ -226,6 +276,27 @@ defmodule Boombox do end end + defguardp is_webrtc_endpoint(endpoint) + when is_tuple(endpoint) and elem(endpoint, 0) in [:webrtc, :whip] + + @spec maybe_log_transcoding_related_warning(opts_map()) :: :ok + def maybe_log_transcoding_related_warning(opts) do + if is_webrtc_endpoint(opts.output) and not is_webrtc_endpoint(opts.input) and + webrtc_output_force_transcoding(opts) not in [true, :video] do + Logger.warning(""" + Boombox output protocol is WebRTC, while Boombox input doesn't support keyframe requests. This \ + might lead to issues with the output video if the output stream isn't sent only by localhost. You \ + can solve this by setting `:force_transcoding` output option to `true` or `:video`, but be aware \ + that it will increase Boombox CPU usage. + """) + end + + :ok + end + + defp webrtc_output_force_transcoding(%{output: {:webrtc, _singaling, opts}}), + do: Keyword.get(opts, :force_transcoding) + @spec consume_stream(Enumerable.t(), opts_map()) :: term() defp consume_stream(stream, opts) do procs = start_pipeline(opts) @@ -341,7 +412,7 @@ defmodule Boombox do @spec resolve_transport(String.t(), [{:transport, :file | :http}]) :: :file | :http defp resolve_transport(location, opts) do - case Keyword.validate!(opts, transport: nil)[:transport] do + case Keyword.validate!(opts, transport: nil, force_transcoding: false)[:transport] do nil -> uri = URI.parse(location) @@ -359,4 +430,9 @@ defmodule Boombox do raise ArgumentError, "Invalid transport: #{inspect(transport)}" end end + + defp split_webrtc_and_whip_opts(opts) do + opts + |> Enum.split_with(fn {key, _value} -> key == :force_transcoding end) + end end diff --git a/lib/boombox/hls.ex b/lib/boombox/hls.ex index 7a84ba5..1b4aeb2 100644 --- a/lib/boombox/hls.ex +++ b/lib/boombox/hls.ex @@ -10,10 +10,13 @@ defmodule Boombox.HLS do @spec link_output( Path.t(), + [Boombox.force_transcoding()], Boombox.Pipeline.track_builders(), Membrane.ChildrenSpec.t() ) :: Ready.t() - def link_output(location, track_builders, spec_builder) do + def link_output(location, opts, track_builders, spec_builder) do + force_transcoding = opts |> Keyword.get(:force_transcoding, false) + {directory, manifest_name} = if Path.extname(location) == ".m3u8" do {Path.dirname(location), Path.basename(location, ".m3u8")} @@ -44,7 +47,8 @@ defmodule Boombox.HLS do {:audio, builder} -> builder |> child(:hls_audio_transcoder, %Membrane.Transcoder{ - output_stream_format: Membrane.AAC + output_stream_format: Membrane.AAC, + force_transcoding?: force_transcoding in [true, :audio] }) |> via_in(Pad.ref(:input, :audio), options: [encoding: :AAC, segment_duration: Time.milliseconds(2000)] @@ -54,7 +58,8 @@ defmodule Boombox.HLS do {:video, builder} -> builder |> child(:hls_video_transcoder, %Membrane.Transcoder{ - output_stream_format: %H264{alignment: :au, stream_structure: :avc3} + output_stream_format: %H264{alignment: :au, stream_structure: :avc3}, + force_transcoding?: force_transcoding in [true, :video] }) |> via_in(Pad.ref(:input, :video), options: [encoding: :H264, segment_duration: Time.milliseconds(2000)] diff --git a/lib/boombox/mp4.ex b/lib/boombox/mp4.ex index aa4c1c2..6459e87 100644 --- a/lib/boombox/mp4.ex +++ b/lib/boombox/mp4.ex @@ -9,7 +9,8 @@ defmodule Boombox.MP4 do defguardp is_h26x(format) when is_struct(format) and format.__struct__ in [H264, H265] - @spec create_input(String.t(), transport: :file | :http) :: Wait.t() + @spec create_input(String.t(), [Boombox.force_transcoding() | {:transport, :file | :http}]) :: + Wait.t() def create_input(location, opts) do spec = case opts[:transport] do @@ -53,10 +54,13 @@ defmodule Boombox.MP4 do @spec link_output( String.t(), + [Boombox.force_transcoding()], Boombox.Pipeline.track_builders(), Membrane.ChildrenSpec.t() ) :: Ready.t() - def link_output(location, track_builders, spec_builder) do + def link_output(location, opts, track_builders, spec_builder) do + force_transcoding = opts |> Keyword.get(:force_transcoding, false) + spec = [ spec_builder, @@ -66,7 +70,8 @@ defmodule Boombox.MP4 do {:audio, builder} -> builder |> child(:mp4_audio_transcoder, %Membrane.Transcoder{ - output_stream_format: Membrane.AAC + output_stream_format: Membrane.AAC, + force_transcoding?: force_transcoding in [true, :audio] }) |> child(:mp4_out_aac_parser, %Membrane.AAC.Parser{ out_encapsulation: :none, @@ -90,7 +95,8 @@ defmodule Boombox.MP4 do _not_h26x -> %H264{stream_structure: :avc3, alignment: :au} - end + end, + force_transcoding?: force_transcoding in [true, :video] }) |> via_in(Pad.ref(:input, :video)) |> get_child(:mp4_muxer) diff --git a/lib/boombox/pipeline.ex b/lib/boombox/pipeline.ex index aa0d495..d548352 100644 --- a/lib/boombox/pipeline.ex +++ b/lib/boombox/pipeline.ex @@ -53,7 +53,13 @@ defmodule Boombox.Pipeline do defmodule State do @moduledoc false - @enforce_keys [:status, :input, :output, :parent] + @enforce_keys [ + :status, + :input, + :output, + :parent + # :force_transcoding + ] defstruct @enforce_keys ++ [ @@ -150,7 +156,10 @@ defmodule Boombox.Pipeline do """ end + {:webrtc, _signaling, webrtc_opts} = state.output + Boombox.WebRTC.handle_output_tracks_negotiated( + webrtc_opts, state.track_builders, state.spec_builder, tracks, @@ -331,7 +340,7 @@ defmodule Boombox.Pipeline do @spec create_output(Boombox.output(), Membrane.Pipeline.CallbackContext.t(), State.t()) :: {Ready.t() | Wait.t(), State.t()} - defp create_output({:webrtc, signaling}, ctx, state) do + defp create_output({:webrtc, signaling, _opts}, ctx, state) do Boombox.WebRTC.create_output(signaling, ctx, state) end @@ -347,21 +356,21 @@ defmodule Boombox.Pipeline do State.t() ) :: Ready.t() | Wait.t() - defp link_output({:webrtc, _signaling}, track_builders, spec_builder, _ctx, state) do + defp link_output({:webrtc, _signaling, opts}, track_builders, spec_builder, _ctx, state) do tracks = [ %{kind: :audio, id: :audio_track}, %{kind: :video, id: :video_tracks} ] - Boombox.WebRTC.link_output(track_builders, spec_builder, tracks, state) + Boombox.WebRTC.link_output(opts, track_builders, spec_builder, tracks, state) end - defp link_output({:mp4, location}, track_builders, spec_builder, _ctx, _state) do - Boombox.MP4.link_output(location, track_builders, spec_builder) + defp link_output({:mp4, location, opts}, track_builders, spec_builder, _ctx, _state) do + Boombox.MP4.link_output(location, opts, track_builders, spec_builder) end - defp link_output({:hls, location}, track_builders, spec_builder, _ctx, _state) do - Boombox.HLS.link_output(location, track_builders, spec_builder) + defp link_output({:hls, location, opts}, track_builders, spec_builder, _ctx, _state) do + Boombox.HLS.link_output(location, opts, track_builders, spec_builder) end defp link_output({:rtp, opts}, track_builders, spec_builder, _ctx, _state) do diff --git a/lib/boombox/rtp.ex b/lib/boombox/rtp.ex index 2827e19..3a57761 100644 --- a/lib/boombox/rtp.ex +++ b/lib/boombox/rtp.ex @@ -62,7 +62,8 @@ defmodule Boombox.RTP do track_configs: %{ optional(:audio) => parsed_output_track_config(), optional(:video) => parsed_output_track_config() - } + }, + force_transcoding: boolean() | :video | :audio } @type parsed_track_config :: parsed_input_track_config() | parsed_output_track_config() @@ -137,12 +138,12 @@ defmodule Boombox.RTP do Enum.map(track_builders, fn {media_type, builder} -> track_config = parsed_opts.track_configs[media_type] - {output_stream_format, parser, payloader} = + {output_stream_format, parser, payloader, force_transcoding?} = case track_config.encoding_name do :H264 -> {%Membrane.H264{stream_structure: :annexb, alignment: :nalu}, %Membrane.H264.Parser{output_stream_structure: :annexb, output_alignment: :nalu}, - Membrane.RTP.H264.Payloader} + Membrane.RTP.H264.Payloader, parsed_opts.force_transcoding in [true, :video]} :AAC -> {%Membrane.AAC{encapsulation: :none}, @@ -150,21 +151,22 @@ defmodule Boombox.RTP do %Membrane.RTP.AAC.Payloader{ mode: track_config.encoding_specific_params.aac_bitrate_mode, frames_per_packet: 1 - }} + }, parsed_opts.force_transcoding in [true, :video]} :OPUS -> {Membrane.Opus, %Membrane.Opus.Parser{delimitation: :undelimit}, - Membrane.RTP.Opus.Payloader} + Membrane.RTP.Opus.Payloader, parsed_opts.force_transcoding in [true, :audio]} :H265 -> {%Membrane.H265{stream_structure: :annexb, alignment: :nalu}, %Membrane.H265.Parser{output_stream_structure: :annexb, output_alignment: :nalu}, - Membrane.RTP.H265.Payloader} + Membrane.RTP.H265.Payloader, parsed_opts.force_transcoding in [true, :audio]} end builder |> child({:rtp_transcoder, media_type}, %Membrane.Transcoder{ - output_stream_format: output_stream_format + output_stream_format: output_stream_format, + force_transcoding?: force_transcoding? }) |> child({:rtp_out_parser, media_type}, parser) |> child({:rtp_payloader, media_type}, payloader) @@ -213,7 +215,16 @@ defmodule Boombox.RTP do raise "No RTP media configured" end - Map.put(transport_opts, :track_configs, parsed_track_configs) + parsed_opts = transport_opts |> Map.put(:track_configs, parsed_track_configs) + + case direction do + :input -> + parsed_opts + + :output -> + force_transcoding = opts |> Keyword.get(:force_transcoding, false) + parsed_opts |> Map.put(:force_transcoding, force_transcoding) + end end @spec parse_output_target( diff --git a/lib/boombox/utils/cli.ex b/lib/boombox/utils/cli.ex index a1f033a..9533df9 100644 --- a/lib/boombox/utils/cli.ex +++ b/lib/boombox/utils/cli.ex @@ -24,7 +24,8 @@ defmodule Boombox.Utils.CLI do sps: {:string, :binary}, vps: {:string, :binary}, whip: {:string, :string}, - token: {:string, :string} + token: {:string, :string}, + force_transcoding: {:boolean, :boolean} ] @spec parse_argv([String.t()]) :: @@ -98,6 +99,9 @@ defmodule Boombox.Utils.CLI do [{direction, value}] -> {direction, value} + [{direction, value} | opts] -> + {direction, {value, translate_opts(opts)}} + _other -> cli_exit_error() end diff --git a/lib/boombox/webrtc.ex b/lib/boombox/webrtc.ex index 463d01e..83d097e 100644 --- a/lib/boombox/webrtc.ex +++ b/lib/boombox/webrtc.ex @@ -33,8 +33,8 @@ defmodule Boombox.WebRTC do %{negotiated_video_codecs: [codec]} -> {codec, [:h264, :vp8]} - %{negotiated_video_codecs: both} when is_list(both) -> - {:vp8, both} + %{negotiated_video_codecs: [_codec_1, _codec_2] = codecs} -> + {:vp8, codecs} end spec = @@ -109,14 +109,15 @@ defmodule Boombox.WebRTC do end @spec link_output( + [Boombox.force_transcoding()], Boombox.Pipeline.track_builders(), Membrane.ChildrenSpec.t(), webrtc_sink_new_tracks(), State.t() ) :: Ready.t() | Wait.t() - def link_output(track_builders, spec_builder, tracks, state) do + def link_output(opts, track_builders, spec_builder, tracks, state) do if webrtc_input?(state) do - do_link_output(track_builders, spec_builder, tracks, state) + do_link_output(opts, track_builders, spec_builder, tracks, state) else tracks = Bunch.KVEnum.keys(track_builders) %Wait{actions: [notify_child: {:webrtc_output, {:add_tracks, tracks}}]} @@ -124,22 +125,24 @@ defmodule Boombox.WebRTC do end @spec handle_output_tracks_negotiated( + [Boombox.force_transcoding()], Boombox.Pipeline.track_builders(), Membrane.ChildrenSpec.t(), webrtc_sink_new_tracks(), State.t() ) :: Ready.t() | no_return() - def handle_output_tracks_negotiated(track_builders, spec_builder, tracks, state) do + def handle_output_tracks_negotiated(opts, track_builders, spec_builder, tracks, state) do if webrtc_input?(state) do raise """ Currently ICE restart is not supported in Boombox instances having WebRTC input and output. """ end - do_link_output(track_builders, spec_builder, tracks, state) + do_link_output(opts, track_builders, spec_builder, tracks, state) end - defp do_link_output(track_builders, spec_builder, tracks, state) do + defp do_link_output(opts, track_builders, spec_builder, tracks, state) do + force_transcoding = opts |> Keyword.get(:force_transcoding, false) tracks = Map.new(tracks, &{&1.kind, &1.id}) spec = [ @@ -148,7 +151,8 @@ defmodule Boombox.WebRTC do {:audio, builder} -> builder |> child(:mp4_audio_transcoder, %Membrane.Transcoder{ - output_stream_format: Membrane.Opus + output_stream_format: Membrane.Opus, + force_transcoding?: force_transcoding in [true, :audio] }) |> child(:webrtc_out_audio_realtimer, Membrane.Realtimer) |> via_in(Pad.ref(:input, tracks.audio), options: [kind: :audio]) @@ -156,28 +160,20 @@ defmodule Boombox.WebRTC do {:video, builder} -> negotiated_codecs = state.output_webrtc_state.negotiated_video_codecs - vp8_negotiated? = :vp8 in negotiated_codecs - h264_negotiated? = :h264 in negotiated_codecs + force_video_transcoding? = force_transcoding in [true, :video] builder |> child(:webrtc_out_video_realtimer, Membrane.Realtimer) |> child(:webrtc_video_transcoder, %Membrane.Transcoder{ - output_stream_format: fn - %H264{} = h264 when h264_negotiated? -> - %H264{h264 | alignment: :nalu, stream_structure: :annexb} - - %VP8{} = vp8 when vp8_negotiated? -> - vp8 - - %RemoteStream{content_format: VP8, type: :packetized} when vp8_negotiated? -> - VP8 - - _format when h264_negotiated? -> - %H264{alignment: :nalu, stream_structure: :annexb} - - _format when vp8_negotiated? -> - VP8 - end + output_stream_format: fn input_format -> + resolve_output_video_stream_format( + input_format, + :vp8 in negotiated_codecs, + :h264 in negotiated_codecs, + force_video_transcoding? + ) + end, + force_transcoding?: force_video_transcoding? }) |> via_in(Pad.ref(:input, tracks.video), options: [kind: :video]) |> get_child(:webrtc_output) @@ -187,6 +183,45 @@ defmodule Boombox.WebRTC do %Ready{actions: [spec: spec], eos_info: Map.values(tracks)} end + defp resolve_output_video_stream_format( + input_stream_format, + vp8_negotiated?, + h264_negotiated?, + false = _force_transcoding? + ) do + case input_stream_format do + %H264{} = h264 when h264_negotiated? -> + %H264{h264 | alignment: :nalu, stream_structure: :annexb} + + %VP8{} = vp8 when vp8_negotiated? -> + vp8 + + %RemoteStream{content_format: VP8, type: :packetized} = remote_stream + when vp8_negotiated? -> + remote_stream + + _format when h264_negotiated? -> + %H264{alignment: :nalu, stream_structure: :annexb} + + _format when vp8_negotiated? -> + VP8 + end + end + + defp resolve_output_video_stream_format( + _input_stream_format, + vp8_negotiated?, + h264_negotiated?, + true = _force_transcoding? + ) do + # if we have to perform transcoding one way or another, we always choose H264 if it is possilbe, + # because H264 Encoder comsumes less CPU than VP8 Encoder + cond do + h264_negotiated? -> %H264{alignment: :nalu, stream_structure: :annexb} + vp8_negotiated? -> VP8 + end + end + defp resolve_signaling( %WebRTC.Signaling{} = signaling, _direction, diff --git a/mix.exs b/mix.exs index 20504fa..ce69bbe 100644 --- a/mix.exs +++ b/mix.exs @@ -46,8 +46,8 @@ defmodule Boombox.Mixfile do defp deps do [ - {:membrane_core, "~> 1.1"}, - {:membrane_transcoder_plugin, "~> 0.1.2"}, + {:membrane_core, "~> 1.2"}, + {:membrane_transcoder_plugin, "~> 0.2.1"}, {:membrane_webrtc_plugin, "~> 0.24.0"}, {:membrane_mp4_plugin, "~> 0.35.2"}, {:membrane_realtimer_plugin, "~> 0.9.0"}, diff --git a/mix.lock b/mix.lock index e628cb9..3a3b457 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,5 @@ %{ - "bandit": {:hex, :bandit, "1.6.7", "42f30e37a1c89a2a12943c5dca76f731a2313e8a2e21c1a95dc8241893e922d1", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "551ba8ff5e4fc908cbeb8c9f0697775fb6813a96d9de5f7fe02e34e76fd7d184"}, + "bandit": {:hex, :bandit, "1.6.8", "be6fcbe01a74e6cba42ae35f4085acaeae9b2d8d360c0908d0b9addbc2811e47", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "4fc08c8d4733735d175a007ecb25895e84d09292b0180a2e9f16948182c88b6e"}, "bimap": {:hex, :bimap, "1.3.0", "3ea4832e58dc83a9b5b407c6731e7bae87458aa618e6d11d8e12114a17afa4b3", [:mix], [], "hexpm", "bf5a2b078528465aa705f405a5c638becd63e41d280ada41e0f77e6d255a10b4"}, "bunch": {:hex, :bunch, "1.6.1", "5393d827a64d5f846092703441ea50e65bc09f37fd8e320878f13e63d410aec7", [:mix], [], "hexpm", "286cc3add551628b30605efbe2fca4e38cc1bea89bcd0a1a7226920b3364fe4a"}, "bunch_native": {:hex, :bunch_native, "0.5.0", "8ac1536789a597599c10b652e0b526d8833348c19e4739a0759a2bedfd924e63", [:mix], [{:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "24190c760e32b23b36edeb2dc4852515c7c5b3b8675b1a864e0715bdd1c8f80d"}, @@ -8,19 +8,19 @@ "burrito": {:hex, :burrito, "1.2.0", "88f973469edcb96bd984498fb639d3fc4dbf01b52baab072b40229f03a396789", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:req, ">= 0.4.0", [hex: :req, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.2.0 or ~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "7e22158023c6558de615795ab135d27f0cbd9a0602834e3e474fe41b448afba9"}, "castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, - "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, + "certifi": {:hex, :certifi, "2.14.0", "ed3bef654e69cde5e6c022df8070a579a79e8ba2368a00acf3d75b82d9aceeed", [:rebar3], [], "hexpm", "ea59d87ef89da429b8e905264fdec3419f84f2215bb3d81e07a18aac919026c3"}, "coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"}, "corsica": {:hex, :corsica, "2.1.3", "dccd094ffce38178acead9ae743180cdaffa388f35f0461ba1e8151d32e190e6", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "616c08f61a345780c2cf662ff226816f04d8868e12054e68963e95285b5be8bc"}, "cowlib": {:hex, :cowlib, "2.7.3", "a7ffcd0917e6d50b4d5fb28e9e2085a0ceb3c97dea310505f7460ff5ed764ce9", [:rebar3], [], "hexpm", "1e1a3d176d52daebbecbbcdfd27c27726076567905c2a9d7398c54da9d225761"}, "crc": {:hex, :crc, "0.10.5", "ee12a7c056ac498ef2ea985ecdc9fa53c1bfb4e53a484d9f17ff94803707dfd8", [:mix, :rebar3], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3e673b6495a9525c5c641585af1accba59a1eb33de697bedf341e247012c2c7f"}, "credo": {:hex, :credo, "1.7.11", "d3e805f7ddf6c9c854fd36f089649d7cf6ba74c42bc3795d587814e3c9847102", [: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", "56826b4306843253a66e47ae45e98e7d284ee1f95d53d1612bb483f88a8cf219"}, "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.43", "34b2f401fe473080e39ff2b90feb8ddfeef7639f8ee0bbf71bb41911831d77c5", [:mix], [], "hexpm", "970a3cd19503f5e8e527a190662be2cee5d98eed1ff72ed9b3d1a3d466692de8"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"}, "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "esbuild": {:hex, :esbuild, "0.8.2", "5f379dfa383ef482b738e7771daf238b2d1cfb0222bef9d3b20d4c8f06c7a7ac", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "558a8a08ed78eb820efbfda1de196569d8bfa9b51e8371a1934fbb31345feda7"}, - "ex_doc": {:hex, :ex_doc, "0.37.2", "2a3aa7014094f0e4e286a82aa5194a34dd17057160988b8509b15aa6c292720c", [:mix], [{:earmark_parser, "~> 1.4.42", [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", "4dfa56075ce4887e4e8b1dcc121cd5fcb0f02b00391fd367ff5336d98fa49049"}, + "ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [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", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"}, "ex_dtls": {:hex, :ex_dtls, "0.16.0", "3ae38025ccc77f6db573e2e391602fa9bbc02253c137d8d2d59469a66cbe806b", [:mix], [{:bundlex, "~> 1.5.3", [hex: :bundlex, repo: "hexpm", optional: false]}, {:unifex, "~> 1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "2a4e30d74c6ddf95cc5b796423293c06a0da295454c3823819808ff031b4b361"}, "ex_ice": {:hex, :ex_ice, "0.9.4", "793121989164e49d8dc64b82bcb7842a4c2e0d224a2f00379ab415293a78c8e7", [:mix], [{:elixir_uuid, "~> 1.0", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:ex_stun, "~> 0.2.0", [hex: :ex_stun, repo: "hexpm", optional: false]}, {:ex_turn, "~> 0.2.0", [hex: :ex_turn, repo: "hexpm", optional: false]}], "hexpm", "fc328ed721c558440266def81a2cd5138d163164218ebe449fa9a10fcda72574"}, "ex_libsrtp": {:hex, :ex_libsrtp, "0.7.2", "211bd89c08026943ce71f3e2c0231795b99cee748808ed3ae7b97cd8d2450b6b", [:mix], [{:bunch, "~> 1.6", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.3", [hex: :bundlex, repo: "hexpm", optional: false]}, {:membrane_precompiled_dependency_provider, "~> 0.1.0", [hex: :membrane_precompiled_dependency_provider, repo: "hexpm", optional: false]}, {:unifex, "~> 1.1", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "2e20645d0d739a4ecdcf8d4810a0c198120c8a2f617f2b75b2e2e704d59f492a"}, @@ -33,7 +33,7 @@ "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [: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", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, "gun": {:hex, :gun, "1.3.3", "cf8b51beb36c22b9c8df1921e3f2bc4d2b1f68b49ad4fbc64e91875aa14e16b4", [:rebar3], [{:cowlib, "~> 2.7.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "3106ce167f9c9723f849e4fb54ea4a4d814e3996ae243a1c828b256e749041e0"}, - "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, + "hackney": {:hex, :hackney, "1.23.0", "55cc09077112bcb4a69e54be46ed9bc55537763a96cd4a80a221663a7eafd767", [:rebar3], [{:certifi, "~> 2.14.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "6cd1c04cd15c81e5a493f167b226a15f0938a84fc8f0736ebe4ddcab65c0b44e"}, "heap": {:hex, :heap, "2.0.2", "d98cb178286cfeb5edbcf17785e2d20af73ca57b5a2cf4af584118afbcf917eb", [:mix], [], "hexpm", "ba9ea2fe99eb4bcbd9a8a28eaf71cbcac449ca1d8e71731596aace9028c9d429"}, "hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, @@ -45,10 +45,10 @@ "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, "membrane_aac_fdk_plugin": {:hex, :membrane_aac_fdk_plugin, "0.18.11", "6bdcde2eaeafcce136f21fb17effdcdaf9c73107771c8d2a661ca5d0b616ea12", [:mix], [{:bunch, "~> 1.4", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.3", [hex: :bundlex, repo: "hexpm", optional: false]}, {:membrane_aac_format, "~> 0.8.0", [hex: :membrane_aac_format, repo: "hexpm", optional: false]}, {:membrane_common_c, "~> 0.16.0", [hex: :membrane_common_c, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_precompiled_dependency_provider, "~> 0.1.0", [hex: :membrane_precompiled_dependency_provider, repo: "hexpm", optional: false]}, {:membrane_raw_audio_format, "~> 0.12.0", [hex: :membrane_raw_audio_format, repo: "hexpm", optional: false]}, {:unifex, "~> 1.1", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "dac3f0c839f33603dc2fdad8b9fbebd5ca578c9ca28ea65249ebb15c96c0fd31"}, "membrane_aac_format": {:hex, :membrane_aac_format, "0.8.0", "515631eabd6e584e0e9af2cea80471fee6246484dbbefc4726c1d93ece8e0838", [:mix], [{:bimap, "~> 1.1", [hex: :bimap, repo: "hexpm", optional: false]}], "hexpm", "a30176a94491033ed32be45e51d509fc70a5ee6e751f12fd6c0d60bd637013f6"}, - "membrane_aac_plugin": {:hex, :membrane_aac_plugin, "0.19.0", "58a15efaaa4f2cc91b968464cfd269244e035efdd983aac2e3ddeb176fcf0585", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:membrane_aac_format, "~> 0.8.0", [hex: :membrane_aac_format, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "eb7e786e650608ee205f4eebff4c1df3677e545acf09802458f77f64f9942fe9"}, + "membrane_aac_plugin": {:hex, :membrane_aac_plugin, "0.19.1", "29b9eecf75e3d60b16e7aac0861a501caa16f6d34f88e6d1d4140fdd4926292b", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:membrane_aac_format, "~> 0.8.0", [hex: :membrane_aac_format, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "21158745f4d748eb15dd63e872d21a7deacb055294c0efb24b31960ad0400171"}, "membrane_cmaf_format": {:hex, :membrane_cmaf_format, "0.7.1", "9ea858faefdcb181cdfa8001be827c35c5f854e9809ad57d7062cff1f0f703fd", [:mix], [], "hexpm", "3c7b4ed2a986e27f6f336d2f19e9442cb31d93b3142fc024c019572faca54a73"}, "membrane_common_c": {:hex, :membrane_common_c, "0.16.0", "caf3f29d2f5a1d32d8c2c122866110775866db2726e4272be58e66dfdf4bce40", [:mix], [{:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:shmex, "~> 0.5.0", [hex: :shmex, repo: "hexpm", optional: false]}, {:unifex, "~> 1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "a3c7e91de1ce1f8b23b9823188a5d13654d317235ea0ca781c05353ed3be9b1c"}, - "membrane_core": {:hex, :membrane_core, "1.2.0", "c74ef043791e11d149a01e344d9973de34d6dd2e3b8a6bdc79ff5b183a74b243", [: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", "0133e3478af608b1749729d82bc747a0357e207f5819a6bd4d31614ce7d0a463"}, + "membrane_core": {:hex, :membrane_core, "1.2.1", "96cd9c8a255585c0ed2906d093a6c6e5e4198f1b23cfe5e62a613673fcb1da30", [: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", "becc0e6fabe8732f360c91607b708f196dda1ab5ab1f15fc4a979909cc76d49d"}, "membrane_ffmpeg_swresample_plugin": {:hex, :membrane_ffmpeg_swresample_plugin, "0.20.2", "2e669f0b25418d10b51a73bc52d2e12e4a3a26b416c5c1199d852c3f781a18b3", [:mix], [{:bunch, "~> 1.6", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.2", [hex: :bundlex, repo: "hexpm", optional: false]}, {:membrane_common_c, "~> 0.16.0", [hex: :membrane_common_c, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_precompiled_dependency_provider, "~> 0.1.0", [hex: :membrane_precompiled_dependency_provider, repo: "hexpm", optional: false]}, {:membrane_raw_audio_format, "~> 0.12.0", [hex: :membrane_raw_audio_format, repo: "hexpm", optional: false]}, {:mockery, "~> 2.1", [hex: :mockery, repo: "hexpm", optional: false]}, {:unifex, "~> 1.1", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "6c8d3bcd61d568dd94cabb9b45f29e8926e0076e4432d8f419378e004e02147c"}, "membrane_ffmpeg_swscale_plugin": {:hex, :membrane_ffmpeg_swscale_plugin, "0.16.2", "581909312d6d12ed560ee99caa1b1674a339760ab2ad6835d243326806c23da1", [:mix], [{:bundlex, "~> 1.2", [hex: :bundlex, repo: "hexpm", optional: false]}, {:membrane_common_c, "~> 0.16.0", [hex: :membrane_common_c, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.1", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_precompiled_dependency_provider, "~> 0.1.0", [hex: :membrane_precompiled_dependency_provider, repo: "hexpm", optional: false]}, {:membrane_raw_video_format, "~> 0.4.1", [hex: :membrane_raw_video_format, repo: "hexpm", optional: false]}], "hexpm", "46c185dacff1e1b404d0ceb74d0d5224f0931fe1e8b951cc3776ebd099e39afc"}, "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"}, @@ -84,7 +84,7 @@ "membrane_tee_plugin": {:hex, :membrane_tee_plugin, "0.12.0", "f94989b4080ef4b7937d74c1a14d3379577c7bd4c6d06e5a2bb41c351ad604d4", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "0d61c9ed5e68e5a75d54200e1c6df5739c0bcb52fee0974183ad72446a179887"}, "membrane_telemetry_metrics": {:hex, :membrane_telemetry_metrics, "0.1.1", "57917e72012f9ebe124eab54f29ca74c9d9eb3ae2207f55c95618ee51280eb4f", [:mix], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.1 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "69392966e6bd51937244758c2b3d835c5ff47d8953d25431a9d37059737afc11"}, "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"}, - "membrane_transcoder_plugin": {:hex, :membrane_transcoder_plugin, "0.1.4", "f933c6cd5f3ed3166827759c409d34401bcbbfbb2599fd45e83f170a50c40579", [:mix], [{:membrane_aac_fdk_plugin, "~> 0.18.0", [hex: :membrane_aac_fdk_plugin, repo: "hexpm", optional: false]}, {:membrane_aac_format, "~> 0.8.0", [hex: :membrane_aac_format, repo: "hexpm", optional: false]}, {:membrane_aac_plugin, "~> 0.19.0", [hex: :membrane_aac_plugin, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.1", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_ffmpeg_swresample_plugin, "~> 0.20.0", [hex: :membrane_ffmpeg_swresample_plugin, repo: "hexpm", optional: false]}, {:membrane_funnel_plugin, "~> 0.9.1", [hex: :membrane_funnel_plugin, repo: "hexpm", optional: false]}, {:membrane_h264_ffmpeg_plugin, "~> 0.32.0", [hex: :membrane_h264_ffmpeg_plugin, repo: "hexpm", optional: false]}, {:membrane_h264_format, "~> 0.6.1", [hex: :membrane_h264_format, repo: "hexpm", optional: false]}, {:membrane_h265_ffmpeg_plugin, "~> 0.4.2", [hex: :membrane_h265_ffmpeg_plugin, repo: "hexpm", optional: false]}, {:membrane_h265_format, "~> 0.2.0", [hex: :membrane_h265_format, repo: "hexpm", optional: false]}, {:membrane_h26x_plugin, "~> 0.10.0", [hex: :membrane_h26x_plugin, repo: "hexpm", optional: false]}, {:membrane_opus_format, "~> 0.3.0", [hex: :membrane_opus_format, repo: "hexpm", optional: false]}, {:membrane_opus_plugin, "~> 0.20.3", [hex: :membrane_opus_plugin, repo: "hexpm", optional: false]}, {:membrane_timestamp_queue, "~> 0.2.2", [hex: :membrane_timestamp_queue, repo: "hexpm", optional: false]}, {:membrane_vp8_format, "~> 0.5.0", [hex: :membrane_vp8_format, repo: "hexpm", optional: false]}, {:membrane_vpx_plugin, "~> 0.4.0", [hex: :membrane_vpx_plugin, repo: "hexpm", optional: false]}], "hexpm", "bb6f9ed84ec9f594cf6af38336657395e24748e975dd79dbe94a06702df59ae8"}, + "membrane_transcoder_plugin": {:hex, :membrane_transcoder_plugin, "0.2.1", "aea6145435c98310b06f7451e8899a5d926b143418d10b1d9f31b8c67a15dff8", [:mix], [{:membrane_aac_fdk_plugin, "~> 0.18.0", [hex: :membrane_aac_fdk_plugin, repo: "hexpm", optional: false]}, {:membrane_aac_format, "~> 0.8.0", [hex: :membrane_aac_format, repo: "hexpm", optional: false]}, {:membrane_aac_plugin, "~> 0.19.0", [hex: :membrane_aac_plugin, repo: "hexpm", optional: false]}, {:membrane_core, ">= 1.2.1 and < 2.0.0-0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_ffmpeg_swresample_plugin, "~> 0.20.0", [hex: :membrane_ffmpeg_swresample_plugin, repo: "hexpm", optional: false]}, {:membrane_funnel_plugin, "~> 0.9.1", [hex: :membrane_funnel_plugin, repo: "hexpm", optional: false]}, {:membrane_h264_ffmpeg_plugin, "~> 0.32.0", [hex: :membrane_h264_ffmpeg_plugin, repo: "hexpm", optional: false]}, {:membrane_h264_format, "~> 0.6.1", [hex: :membrane_h264_format, repo: "hexpm", optional: false]}, {:membrane_h265_ffmpeg_plugin, "~> 0.4.2", [hex: :membrane_h265_ffmpeg_plugin, repo: "hexpm", optional: false]}, {:membrane_h265_format, "~> 0.2.0", [hex: :membrane_h265_format, repo: "hexpm", optional: false]}, {:membrane_h26x_plugin, "~> 0.10.0", [hex: :membrane_h26x_plugin, repo: "hexpm", optional: false]}, {:membrane_opus_format, "~> 0.3.0", [hex: :membrane_opus_format, repo: "hexpm", optional: false]}, {:membrane_opus_plugin, "~> 0.20.3", [hex: :membrane_opus_plugin, repo: "hexpm", optional: false]}, {:membrane_timestamp_queue, "~> 0.2.2", [hex: :membrane_timestamp_queue, repo: "hexpm", optional: false]}, {:membrane_vp8_format, "~> 0.5.0", [hex: :membrane_vp8_format, repo: "hexpm", optional: false]}, {:membrane_vpx_plugin, "~> 0.4.0", [hex: :membrane_vpx_plugin, repo: "hexpm", optional: false]}], "hexpm", "1af035369642dc92726b1258ce816589b3775eb1cb69249c78fa556a15d28373"}, "membrane_udp_plugin": {:hex, :membrane_udp_plugin, "0.14.0", "d533ee5f6fcdd0551ad690045cdb6c1a76307a155d9255cc4a4606f85774bc37", [:mix], [{:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:mockery, "~> 2.3.0", [hex: :mockery, repo: "hexpm", optional: false]}], "hexpm", "902d1a7aa228ec377482d53a605b100e20e0b6e59196f94f94147bb62b23c47e"}, "membrane_vp8_format": {:hex, :membrane_vp8_format, "0.5.0", "a589c20bb9d97ddc9b717684d00cefc84e2500ce63a0c33c4b9618d9b2f9b2ea", [:mix], [], "hexpm", "d29e0dae4bebc6838e82e031c181fe626d168c687e4bc617c1d0772bdeed19d5"}, "membrane_vp9_format": {:hex, :membrane_vp9_format, "0.5.0", "c6a4f2cbfc39dba5d80ad8287162c52b5cf6488676bd64435c1ac957bd16e66f", [:mix], [], "hexpm", "68752d8cbe7270ec222fc84a7d1553499f0d8ff86ef9d9e89f8955d49e20278e"}, @@ -100,7 +100,7 @@ "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"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, - "phoenix_html": {:hex, :phoenix_html, "4.2.0", "83a4d351b66f472ebcce242e4ae48af1b781866f00ef0eb34c15030d4e2069ac", [:mix], [], "hexpm", "9713b3f238d07043583a94296cc4bbdceacd3b3a6c74667f4df13971e7866ec8"}, + "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"}, "playwright": {:git, "https://github.com/membraneframework-labs/playwright-elixir.git", "5c02249512fa543f5e619a69b7e5c9e046605fe5", [ref: "5c02249512fa543f5e619a69b7e5c9e046605fe5"]}, "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, @@ -113,7 +113,7 @@ "sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, - "thousand_island": {:hex, :thousand_island, "1.3.10", "a9971ebab1dfb36e2710a86b37c3f54973fbc9470d892035334415521fb53328", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17ab1f1b13aadb1f4b4c8e5b59c06874d701119fed082884c9c6d38addad254f"}, + "thousand_island": {:hex, :thousand_island, "1.3.11", "b68f3e91f74d564ae20b70d981bbf7097dde084343c14ae8a33e5b5fbb3d6f37", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "555c18c62027f45d9c80df389c3d01d86ba11014652c00be26e33b1b64e98d29"}, "typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "unifex": {:hex, :unifex, "1.2.1", "6841c170a6e16509fac30b19e4e0a19937c33155a59088b50c15fc2c36251b6b", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.4", [hex: :bundlex, repo: "hexpm", optional: false]}, {:shmex, "~> 0.5.0", [hex: :shmex, repo: "hexpm", optional: false]}], "hexpm", "8c9d2e3c48df031e9995dd16865bab3df402c0295ba3a31f38274bb5314c7d37"}, diff --git a/test/browser_test.exs b/test/browser_test.exs index 33d285a..053fbba 100644 --- a/test/browser_test.exs +++ b/test/browser_test.exs @@ -107,15 +107,18 @@ defmodule Boombox.BrowserTest do assert size > 400_000 end - for first <- [:ingress, :egress] do - test "browser -> boombox -> browser, but #{first} browser page connects first", %{ - browser: browser - } do + for first <- [:ingress, :egress], transcoding? <- [true, false] do + test "browser -> boombox -> browser, but #{first} browser page connects first and :enforce_audio/video_transcoding? is set to #{transcoding?}", + %{ + browser: browser + } do + transcoding? = unquote(transcoding?) + boombox_task = Task.async(fn -> Boombox.run( input: {:webrtc, "ws://localhost:8829"}, - output: {:webrtc, "ws://localhost:8830"} + output: {:webrtc, "ws://localhost:8830", force_transcoding: transcoding?} ) end) @@ -143,6 +146,10 @@ defmodule Boombox.BrowserTest do assert_frames_encoded(ingress_page, seconds) assert_frames_decoded(egress_page, seconds) + assert_page_codecs(ingress_page, [:vp8, :opus]) + egress_video_codec = if unquote(transcoding?), do: :h264, else: :vp8 + assert_page_codecs(egress_page, [egress_video_codec, :opus]) + [ingress_page, egress_page] |> Enum.each(&close_page/1) @@ -220,15 +227,37 @@ defmodule Boombox.BrowserTest do |> String.contains?("Connected") end + defp assert_page_codecs(page, codecs) do + page_codecs = + get_webrtc_stats(page, type: "codec") + |> MapSet.new(& &1.mimeType) + + expected_codecs = + codecs + |> MapSet.new(fn + :h264 -> "video/H264" + :vp8 -> "video/VP8" + :opus -> "audio/opus" + end) + + assert page_codecs == expected_codecs + end + defp assert_frames_encoded(page, time_seconds) do fps_lowerbound = 12 - frames_encoded = get_webrtc_stats(page, type: "outbound-rtp", kind: "video").framesEncoded + + [%{framesEncoded: frames_encoded}] = + get_webrtc_stats(page, type: "outbound-rtp", kind: "video") + assert frames_encoded >= time_seconds * fps_lowerbound end defp assert_frames_decoded(page, time_seconds) do fps_lowerbound = 12 - frames_decoded = get_webrtc_stats(page, type: "inbound-rtp", kind: "video").framesDecoded + + [%{framesDecoded: frames_decoded}] = + get_webrtc_stats(page, type: "inbound-rtp", kind: "video") + assert frames_decoded >= time_seconds * fps_lowerbound end @@ -238,6 +267,6 @@ defmodule Boombox.BrowserTest do Playwright.Page.evaluate(page, js_fuj) |> Enum.map(fn [_id, data] -> data end) - |> Enum.find(fn stat -> Enum.all?(constraints, fn {k, v} -> stat[k] == v end) end) + |> Enum.filter(fn stat -> Enum.all?(constraints, fn {k, v} -> stat[k] == v end) end) end end