diff --git a/examples.livemd b/examples.livemd index 1e560e2..5c6e68e 100644 --- a/examples.livemd +++ b/examples.livemd @@ -60,6 +60,11 @@ unless File.exists?("#{input_dir}/ffmpeg-testsrc.h264") do File.write!("#{input_dir}/ffmpeg-testsrc.h264", testsrc_h264) end +unless File.exists?("#{input_dir}/ffmpeg-testsrc.mp4") do + %{status: 200, body: testsrc_mp4} = Req.get!("#{samples_url}/ffmpeg-testsrc-480x270.mp4") + File.write!("#{input_dir}/ffmpeg-testsrc.mp4", testsrc_mp4) +end + unless File.exists?("#{input_dir}/test-audio.aac") do %{status: 200, body: test_audio} = Req.get!("#{samples_url}/test-audio.aac") File.write!("#{input_dir}/test-audio.aac", test_audio) @@ -548,6 +553,137 @@ end) +## Compose two streams side by side, broadcast via HLS + +To receive the stream, visit http://localhost:1234/hls.html after running the cells below + +The first cell uses `:reader` and `:writer` endpoints to communicate with boombox. In this +configuration the process calling `Boombox.read/1` controls when packets are being provided. + +```elixir +input1 = "#{input_dir}/bun.mp4" +input2 = "#{input_dir}/ffmpeg-testsrc.mp4" +output = "#{out_dir}/index.m3u8" + +reader1 = + Boombox.run(input: input1, output: {:reader, video: :image, audio: false}) + +reader2 = + Boombox.run(input: input2, output: {:reader, video: :image, audio: false}) + +writer = Boombox.run(input: {:writer, video: :image, audio: false}, output: output) + +Stream.unfold(%{}, fn _state -> + {result1, packet1} = Boombox.read(reader1) + {result2, packet2} = Boombox.read(reader2) + + joined_image = + Vix.Vips.Operation.join!(packet1.payload, packet2.payload, :VIPS_DIRECTION_HORIZONTAL) + + packet = %Boombox.Packet{ + pts: max(packet1.pts, packet2.pts), + payload: joined_image, + kind: :video + } + + Boombox.write(writer, packet) + + if :finished in [result1, result2] do + if result1 == :ok, do: + Boombox.close(reader1) + if result2 == :ok, do: + Boombox.close(reader2) + nil + else + {nil, %{}} + end +end) +|> Stream.run() + +Boombox.close(writer) +``` + +The second cell uses `:message` endpoints, meaning that the server communicates with boomboxes by +exchanging messages. A consequence of this is that the inputting boomboxes will control the +pace of providing the packets to the server, what can be useful in some circumstances: + +```elixir +defmodule MyServer do + use GenServer + + def start(args) do + GenServer.start(__MODULE__, args) + end + + @impl true + def init(args) do + bb1 = Boombox.run(input: args.input1, output: {:message, video: :image, audio: false}) + bb2 = Boombox.run(input: args.input2, output: {:message, video: :image, audio: false}) + output_writer = + Boombox.run(input: {:writer, video: :image, audio: false}, output: args.output) + + {:ok, + %{ + input_boomboxes_states: %{ + bb1: %{last_packet: nil, eos: false}, + bb2: %{last_packet: nil, eos: false} + }, + input_boomboxes: %{bb1 => :bb1, bb2 => :bb2}, + output_writer: output_writer + }} + end + + @impl true + def handle_info({:boombox_packet, bb, packet}, state) do + boombox_id = state.input_boomboxes[bb] + state = put_in(state.input_boomboxes_states[boombox_id].last_packet, packet) + + if Enum.all?(Map.values(state.input_boomboxes_states), &(&1.last_packet != nil)) do + joined_image = + Vix.Vips.Operation.join!( + state.input_boomboxes_states.bb1.last_packet.payload, + state.input_boomboxes_states.bb2.last_packet.payload, + :VIPS_DIRECTION_HORIZONTAL + ) + + packet = %Boombox.Packet{packet | payload: joined_image} + + Boombox.write(state.output_writer, packet) + end + + {:noreply, state} + end + + @impl true + def handle_info({:boombox_finished, bb}, state) do + boombox_id = state.input_boomboxes[bb] + state = put_in(state.input_boomboxes_states[boombox_id].eos, true) + + if Enum.all?(Map.values(state.input_boomboxes_states), & &1.eos) do + Boombox.close(state.output_writer) + {:stop, :normal, state} + else + {:noreply, state} + end + end +end + +input1 = "#{input_dir}/bun.mp4" +input2 = "#{input_dir}/ffmpeg-testsrc.mp4" +output = "#{out_dir}/index.m3u8" + +{:ok, server} = MyServer.start(%{input1: input, input2: input, output: output}) +monitor = Process.monitor(server) + +receive do + {:DOWN, ^monitor, :process, ^server, reason} -> + IO.inspect(reason) + :ok +end +``` + + + ## Forward RTMP via WebRTC To receive the stream, visit http://localhost:1234/webrtc_to_browser.html diff --git a/lib/boombox.ex b/lib/boombox.ex index 9fb6e34..292b070 100644 --- a/lib/boombox.ex +++ b/lib/boombox.ex @@ -16,9 +16,9 @@ defmodule Boombox do @moduledoc """ Defines a struct to be used when interacting with boombox when using `:writer` endpoint. """ - @opaque t :: %__MODULE__{ - server_reference: GenServer.server() - } + @type t :: %__MODULE__{ + server_reference: GenServer.server() + } @enforce_keys [:server_reference] defstruct @enforce_keys @@ -28,9 +28,9 @@ defmodule Boombox do @moduledoc """ Defines a struct to be used when interacting with boombox when using `:reader` endpoint. """ - @opaque t :: %__MODULE__{ - server_reference: GenServer.server() - } + @type t :: %__MODULE__{ + server_reference: GenServer.server() + } @enforce_keys [:server_reference] defstruct @enforce_keys @@ -137,7 +137,7 @@ defmodule Boombox do | {:srt, url :: String.t(), srt_auth_opts()} | {:srt, server_awaiting_accept :: ExLibSRT.Server.t()} - @type elixir_input :: {:stream | :writer, in_raw_data_opts()} + @type elixir_input :: {:stream | :writer | :message, in_raw_data_opts()} @type output :: (path_or_uri :: String.t()) @@ -156,7 +156,7 @@ defmodule Boombox do | {:srt, url :: String.t(), srt_auth_opts()} | :player - @type elixir_output :: {:stream | :reader, out_raw_data_opts()} + @type elixir_output :: {:stream | :reader | :message, out_raw_data_opts()} @typep procs :: %{pipeline: pid(), supervisor: pid()} @typep opts_map :: %{ @@ -177,14 +177,29 @@ defmodule Boombox do See `t:input/0` and `t:output/0` for available inputs and outputs and [examples.livemd](examples.livemd) for examples. - If the input is a `:stream` endpoint, a `Stream` or other `Enumerable` is expected - as the first argument. - - If the input is a `:writer` endpoint this function will return a `Boombox.Writer` struct, - which is used to write media packets to boombox with `write/2` and to finish writing with `close/1`. - - If the output is a `:reader` endpoint this function will return a `Boombox.Reader` struct, - which is used to read media packets from boombox with `read/1`. + Input endpoints with special behaviours: + * `:stream` - a `Stream` or other `Enumerable` containing `Boombox.Packet`s is expected as the first argument. + * `:writer` - this function will return a `Boombox.Writer` struct, which is used to + write media packets to boombox with `write/2` and to finish writing with `close/1`. + * `:message` - this function returns a PID of a process to communicate with. The process accepts + the following types of messages: + - `{:boombox_packet, sender_pid :: pid(), packet :: Boombox.Packet.t()}` - provides boombox + with a media packet. The process will a `{:boombox_finished, boombox_pid :: pid()}` message to + `sender_pid` if it has finished processing packets and should not be provided any more. + - `{:boombox_close, sender_pid :: pid()}` - tells boombox that no more packets will be + provided and that it should terminate. The process will reply by sending + `{:boombox_finished, boombox_pid :: pid()}` to `sender_pid` + + Output endpoints with special behaviours: + * `:stream` - this function will return a `Stream` that contains `Boombox.Packet`s + * `:reader` - this function will return a `Boombox.Reader` struct, which is used to read media packets from + boombox with `read/1` and to stop reading with `close/1`. + * `:message` - this function returns a PID of a process to communicate with. The process will + send the following types of messages to the process that called this function: + - `{:boombox_packet, boombox_pid :: pid(), packet :: Boombox.Packet.t()}` - contains a packet + produced by boombox. + - `{:boombox_finished, boombox_pid :: pid()}` - informs that boombox has finished producing + packets and will begin terminating. No more messages will be sent. ``` Boombox.run( @@ -212,13 +227,19 @@ defmodule Boombox do produce_stream(sink, procs) %{input: {:writer, _writer_opts}} -> - pid = start_server(opts) + pid = start_server(opts, :calls) %Writer{server_reference: pid} + %{input: {:message, _message_opts}} -> + start_server(opts, :messages) + %{output: {:reader, _reader_opts}} -> - pid = start_server(opts) + pid = start_server(opts, :calls) %Reader{server_reference: pid} + %{output: {:message, _message_opts}} -> + start_server(opts, :messages) + opts -> opts |> start_pipeline() @@ -249,8 +270,8 @@ defmodule Boombox do It returns a `Task.t()` that can be awaited later. - If the output is a `:stream` or `:reader` endpoint, or the input is a `:writer` endpoint, - the behaviour is identical to `run/2`. + If the output is a `:stream`, `:reader` or `:message` endpoint, or the input + is a `:writer` or `:message` endpoint, the behaviour is identical to `run/2`. """ @spec async(Enumerable.t() | nil, input: input(), @@ -275,13 +296,19 @@ defmodule Boombox do produce_stream(sink, procs) %{input: {:writer, _writer_opts}} -> - pid = start_server(opts) + pid = start_server(opts, :calls) %Writer{server_reference: pid} + %{input: {:message, _message_opts}} -> + start_server(opts, :messages) + %{output: {:reader, _reader_opts}} -> - pid = start_server(opts) + pid = start_server(opts, :calls) %Reader{server_reference: pid} + %{output: {:message, _message_opts}} -> + start_server(opts, :messages) + # In case of rtmp, rtmps, rtp, rtsp, we need to wait for the tcp/udp server to be ready # before returning from async/2. %{input: {protocol, _opts}} when protocol in [:rtmp, :rtmps, :rtp, :rtsp, :srt] -> @@ -359,16 +386,27 @@ defmodule Boombox do end @doc """ - Informs Boombox that it will not be provided any more packets with `write/2` and should terminate - accordingly. + Gracefully terminates Boombox when using `:reader` or `:writer` endpoints before a response + of type `:finished` has been received. + + When using `:reader` endpoint on output informs Boombox that no more packets will be read + from it with `read/1` and that it should terminate accordingly. This function will then + return one last packet. + + When using `:writer` endpoint on input informs Boombox that it will not be provided + any more packets with `write/2` and should terminate accordingly. - Can be called only when using `:writer` endpoint on input. """ @spec close(Writer.t()) :: :finished | {:error, :incompatible_mode} - def close(writer) do + @spec close(Reader.t()) :: {:finished, Boombox.Packet.t()} | {:error, :incompatible_mode} + def close(%Writer{} = writer) do Boombox.Server.finish_consuming(writer.server_reference) end + def close(%Reader{} = reader) do + Boombox.Server.finish_producing(reader.server_reference) + end + @endpoint_opts [:input, :output] defp validate_opts!(stream, opts) do opts = opts |> Keyword.validate!(@endpoint_opts) |> Map.new() @@ -384,21 +422,27 @@ defmodule Boombox do elixir_endpoint?(opts.input) and elixir_endpoint?(opts.output) -> raise ArgumentError, - ":stream, :writer or :reader on both input and output is not supported" + "Using an elixir endpoint (:reader, :writer, :message, :stream) on both input and output is not supported" true -> opts end end - defp elixir_endpoint?({:reader, _opts}), do: true - defp elixir_endpoint?({:writer, _opts}), do: true - defp elixir_endpoint?({:stream, _opts}), do: true + defp elixir_endpoint?({type, _opts}) when type in [:reader, :writer, :stream, :message], + do: true + defp elixir_endpoint?(_io), do: false - @spec start_server(opts_map()) :: boombox_server() - defp start_server(opts) do - {:ok, pid} = Boombox.Server.start(packet_serialization: false, stop_application: false) + @spec start_server(opts_map(), :messages | :calls) :: boombox_server() + defp start_server(opts, server_communication_medium) do + {:ok, pid} = + Boombox.Server.start( + packet_serialization: false, + stop_application: false, + communication_medium: server_communication_medium + ) + Boombox.Server.run(pid, Map.to_list(opts)) pid end diff --git a/lib/boombox/packet.ex b/lib/boombox/packet.ex index bc1d920..9347087 100644 --- a/lib/boombox/packet.ex +++ b/lib/boombox/packet.ex @@ -25,7 +25,7 @@ defmodule Boombox.Packet do defstruct @enforce_keys ++ [format: %{}] @spec update_payload(t(), (payload() -> payload())) :: t() - def update_payload(packet, fun) do + def update_payload(%__MODULE__{} = packet, fun) do %__MODULE__{packet | payload: fun.(packet.payload)} end end diff --git a/lib/boombox/server.ex b/lib/boombox/server.ex index f951ed7..d6b4c27 100644 --- a/lib/boombox/server.ex +++ b/lib/boombox/server.ex @@ -23,12 +23,15 @@ defmodule Boombox.Server do alias Boombox.Packet - @type t :: pid() + @type t :: GenServer.server() + + @type communication_medium :: :calls | :messages @type opts :: [ name: GenServer.name(), packet_serialization: boolean(), - stop_application: boolean() + stop_application: boolean(), + communication_medium: communication_medium() ] @type boombox_opts :: [ @@ -92,10 +95,12 @@ defmodule Boombox.Server do packet_serialization: boolean(), stop_application: boolean(), boombox_pid: pid() | nil, - boombox_mode: Boombox.Server.boombox_mode() | nil + boombox_mode: Boombox.Server.boombox_mode() | nil, + communication_medium: Boombox.Server.communication_medium(), + parent_pid: pid() } - @enforce_keys [:packet_serialization, :stop_application] + @enforce_keys [:packet_serialization, :stop_application, :communication_medium, :parent_pid] defstruct @enforce_keys ++ [boombox_pid: nil, boombox_mode: nil] end @@ -106,7 +111,7 @@ defmodule Boombox.Server do @spec start_link(opts()) :: {:ok, t()} | {:error, {:already_started, t()}} def start_link(opts) do genserver_opts = Keyword.take(opts, [:name]) - + opts = Keyword.put(opts, :parent_pid, self()) GenServer.start_link(__MODULE__, opts, genserver_opts) end @@ -116,7 +121,7 @@ defmodule Boombox.Server do @spec start(opts()) :: {:ok, t()} | {:error, {:already_started, t()}} def start(opts) do genserver_opts = Keyword.take(opts, [:name]) - + opts = Keyword.put(opts, :parent_pid, self()) GenServer.start(__MODULE__, opts, genserver_opts) end @@ -170,12 +175,25 @@ defmodule Boombox.Server do GenServer.call(server, :produce_packet) end + @doc """ + Informs Boombox that no more packets will be read and shouldn't be produced. + Can be called only when Boombox is in `:producing` mode. + """ + @spec finish_producing(t()) :: + {:finished, serialized_boombox_packet() | Boombox.Packet.t()} + | {:error, :incompatible_mode} + def finish_producing(server) do + GenServer.call(server, :finish_producing) + end + @impl true def init(opts) do {:ok, %State{ packet_serialization: Keyword.get(opts, :packet_serialization, false), - stop_application: Keyword.get(opts, :stop_application, false) + stop_application: Keyword.get(opts, :stop_application, false), + communication_medium: Keyword.get(opts, :communication_medium, :calls), + parent_pid: Keyword.fetch!(opts, :parent_pid) }} end @@ -192,6 +210,42 @@ defmodule Boombox.Server do {:noreply, state} end + @impl true + def handle_info( + {:boombox_packet, sender_pid, %Boombox.Packet{} = packet}, + %State{communication_medium: :messages} = state + ) do + {response, state} = handle_request({:consume_packet, packet}, state) + if response == :finished, do: send(sender_pid, :boombox_finished) + {:noreply, state} + end + + @impl true + def handle_info({:boombox_close, sender_pid}, %State{communication_medium: :messages} = state) do + handle_request(:finish_consuming, state) + send(sender_pid, {:boombox_finished, self()}) + {:noreply, state} + end + + @impl true + def handle_info( + {:packet_produced, packet, boombox_pid}, + %State{communication_medium: :messages, boombox_pid: boombox_pid} = state + ) do + send(state.parent_pid, {:boombox_packet, self(), packet}) + {:noreply, state} + end + + @impl true + def handle_info( + {:finished, packet, boombox_pid}, + %State{communication_medium: :messages, boombox_pid: boombox_pid} = state + ) do + send(state.parent_pid, {:boombox_packet, self(), packet}) + send(state.parent_pid, {:boombox_finished, self()}) + {:noreply, state} + end + @impl true def handle_info({:DOWN, _ref, :process, pid, reason}, %State{boombox_pid: pid} = state) do reason = @@ -228,10 +282,11 @@ defmodule Boombox.Server do end @spec handle_request({:run, boombox_opts()}, State.t()) :: {boombox_mode(), State.t()} - defp handle_request({:run, boombox_opts}, state) do + defp handle_request({:run, boombox_opts}, %State{} = state) do boombox_opts = boombox_opts |> Enum.map(fn + {direction, {:message, opts}} -> {direction, {:stream, opts}} {direction, {:writer, opts}} -> {direction, {:stream, opts}} {direction, {:reader, opts}} -> {direction, {:stream, opts}} other -> other @@ -243,9 +298,14 @@ defmodule Boombox.Server do boombox_process_fun = case boombox_mode do - :consuming -> fn -> consuming_boombox_run(boombox_opts, server_pid) end - :producing -> fn -> producing_boombox_run(boombox_opts, server_pid) end - :standalone -> fn -> standalone_boombox_run(boombox_opts) end + :consuming -> + fn -> consuming_boombox_run(boombox_opts, server_pid) end + + :producing -> + fn -> producing_boombox_run(boombox_opts, server_pid, state.communication_medium) end + + :standalone -> + fn -> standalone_boombox_run(boombox_opts) end end boombox_pid = spawn(boombox_process_fun) @@ -341,6 +401,25 @@ defmodule Boombox.Server do {{:error, :incompatible_mode}, state} end + @spec handle_request(:finish_producing, State.t()) :: + {{:finished, serialized_boombox_packet() | Boombox.Packet.t()} + | {:error, :incompatible_mode}, State.t()} + defp handle_request( + :finish_producing, + %State{boombox_mode: :producing, boombox_pid: boombox_pid} = state + ) do + send(boombox_pid, :finish_producing) + + receive do + {:finished, packet, ^boombox_pid} -> + {{:finished, packet}, state} + end + end + + defp handle_request(:finish_producing, %State{boombox_mode: _other_mode} = state) do + {{:error, :incompatible_mode}, state} + end + @spec handle_request(term(), State.t()) :: {{:error, :invalid_request}, State.t()} defp handle_request(_invalid_request, state) do {{:error, :invalid_request}, state} @@ -358,7 +437,7 @@ defmodule Boombox.Server do %{output: {:stream, _output_opts}} -> :producing - _neither_input_or_output -> + _other -> :standalone end end @@ -385,20 +464,26 @@ defmodule Boombox.Server do |> Boombox.run(boombox_opts) end - @spec producing_boombox_run(boombox_opts(), pid()) :: :ok - defp producing_boombox_run(boombox_opts, server_pid) do + @spec producing_boombox_run(boombox_opts(), pid(), communication_medium()) :: :ok + defp producing_boombox_run(boombox_opts, server_pid, communication_medium) do last_packet = Boombox.run(boombox_opts) - |> Enum.reduce(nil, fn new_packet, last_produced_packet -> + |> Enum.reduce_while(nil, fn new_packet, last_produced_packet -> if last_produced_packet != nil do send(server_pid, {:packet_produced, last_produced_packet, self()}) end - receive do - :produce_packet -> :ok - end - - new_packet + action = + if communication_medium == :calls do + receive do + :produce_packet -> :cont + :finish_producing -> :halt + end + else + :cont + end + + {action, new_packet} end) send(server_pid, {:finished, last_packet, self()}) diff --git a/mix.exs b/mix.exs index 70ee357..aa8a22a 100644 --- a/mix.exs +++ b/mix.exs @@ -75,6 +75,7 @@ defmodule Boombox.Mixfile do {:image, "~> 0.54.0"}, {:async_test, github: "software-mansion-labs/elixir_async_test", only: :test}, {:playwright, "~> 1.49.1-alpha.2", only: :test}, + {:cowlib, "~> 2.16", override: true, only: :test}, {:burrito, "~> 1.0", runtime: burrito?(), optional: true}, {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, {:dialyxir, ">= 0.0.0", only: :dev, runtime: false}, diff --git a/mix.lock b/mix.lock index 66bd09c..39fa3d4 100644 --- a/mix.lock +++ b/mix.lock @@ -6,22 +6,22 @@ "bunch_native": {:hex, :bunch_native, "0.5.0", "8ac1536789a597599c10b652e0b526d8833348c19e4739a0759a2bedfd924e63", [:mix], [{:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "24190c760e32b23b36edeb2dc4852515c7c5b3b8675b1a864e0715bdd1c8f80d"}, "bundlex": {:hex, :bundlex, "1.5.4", "3726acd463f4d31894a59bbc177c17f3b574634a524212f13469f41c4834a1d9", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:elixir_uuid, "~> 1.2", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:qex, "~> 0.5", [hex: :qex, repo: "hexpm", optional: false]}, {:req, ">= 0.4.0", [hex: :req, repo: "hexpm", optional: false]}, {:zarex, "~> 1.0", [hex: :zarex, repo: "hexpm", optional: false]}], "hexpm", "e745726606a560275182a8ac1c8ebd5e11a659bb7460d8abf30f397e59b4c5d2"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, - "burrito": {:hex, :burrito, "1.4.0", "f94fa1c3f174575bc4cad887a2940fd77469e1985c3a6633fcdcfa72f915caf2", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:req, ">= 0.5.0", [hex: :req, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.2.0 or ~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "0fa052e6f446cd3e5ff7e00813452b09eeadeddb5ec5174c2976eb0e4ad88765"}, - "castore": {:hex, :castore, "1.0.16", "8a4f9a7c8b81cda88231a08fe69e3254f16833053b23fa63274b05cbc61d2a1e", [:mix], [], "hexpm", "33689203a0eaaf02fcd0e86eadfbcf1bd636100455350592e7e2628564022aaf"}, + "burrito": {:hex, :burrito, "1.5.0", "d68ec01df2871f1d5bc603b883a78546c75761ac73c1bec1b7ae2cc74790fcd1", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:req, ">= 0.5.0", [hex: :req, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.2.0 or ~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "3861abda7bffa733862b48da3e03df0b4cd41abf6fd24b91745f5c16d971e5fa"}, + "castore": {:hex, :castore, "1.0.17", "4f9770d2d45fbd91dcf6bd404cf64e7e58fed04fadda0923dc32acca0badffa2", [:mix], [], "hexpm", "12d24b9d80b910dd3953e165636d68f147a31db945d2dcb9365e441f8b5351e5"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, - "coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"}, + "coerce": {:hex, :coerce, "1.0.2", "5ef791040c92baaa5dd344887563faaeac6e6742573a167493294f8af3672bbe", [:mix], [], "hexpm", "0b3451c729571234fdac478636c298e71d1f2ce1243abed5fa43fa3181b980eb"}, "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.13", "126a0697df6b7b71cd18c81bc92335297839a806b6f62b61d417500d1070ff4e", [: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", "47641e6d2bbff1e241e87695b29f617f1a8f912adea34296fb10ecc3d7e9e84f"}, - "dialyxir": {:hex, :dialyxir, "1.4.6", "7cca478334bf8307e968664343cbdb432ee95b4b68a9cba95bdabb0ad5bdfd9a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "8cf5615c5cd4c2da6c501faae642839c8405b49f8aa057ad4ae401cb808ef64d"}, + "cowlib": {:hex, :cowlib, "2.16.0", "54592074ebbbb92ee4746c8a8846e5605052f29309d3a873468d76cdf932076f", [:make, :rebar3], [], "hexpm", "7f478d80d66b747344f0ea7708c187645cfcc08b11aa424632f78e25bf05db51"}, + "crc": {:hex, :crc, "0.10.6", "a52243715da06265399ade929b12e6807a82ddbd04231d8bd3069480aa890f01", [:mix, :rebar3], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "9e832833d48a5fff03cb7488f8aa5c08adda0a5fa8188bbe124cb17c4e39a00d"}, + "credo": {:hex, :credo, "1.7.14", "c7e75216cea8d978ba8c60ed9dede4cc79a1c99a266c34b3600dd2c33b96bc92", [: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", "12a97d6bb98c277e4fb1dff45aaf5c137287416009d214fb46e68147bd9e0203"}, + "dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"}, "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"}, + "erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"}, "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.38.4", "ab48dff7a8af84226bf23baddcdda329f467255d924380a0cf0cee97bb9a9ede", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "f7b62346408a83911c2580154e35613eb314e0278aeea72ed7fedef9c1f165b2"}, + "ex_doc": {:hex, :ex_doc, "0.39.3", "519c6bc7e84a2918b737aec7ef48b96aa4698342927d080437f61395d361dcee", [: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", "0590955cf7ad3b625780ee1c1ea627c28a78948c6c0a9b0322bd976a079996e1"}, "ex_dtls": {:hex, :ex_dtls, "0.18.0", "0815e3384bb0c1e6c06559012479cf9a94a501ddf46c3df54dc2d1b169e29d5c", [:mix], [{:bundlex, "~> 1.5.3", [hex: :bundlex, repo: "hexpm", optional: false]}, {:unifex, "~> 1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "562eda1815eeaed8360b2b5c34d4db5b453794bc096404a4c64f193fa7b18bf2"}, "ex_hls": {:hex, :ex_hls, "0.1.5", "b761b4ec0e5324a13b5e2663ca05a532246895b119843d0138d2448c37d2e2ca", [:mix], [{:ex_m3u8, "~> 0.15.4", [hex: :ex_m3u8, repo: "hexpm", optional: false]}, {:membrane_h26x_plugin, "~> 0.10.2", [hex: :membrane_h26x_plugin, repo: "hexpm", optional: false]}, {:membrane_mp4_plugin, "~> 0.36.0", [hex: :membrane_mp4_plugin, repo: "hexpm", optional: false]}, {:mpeg_ts, "~> 2.0.0", [hex: :mpeg_ts, repo: "hexpm", optional: false]}, {:qex, "~> 0.5.1", [hex: :qex, repo: "hexpm", optional: false]}, {:req, "~> 0.5.10", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "144b35920846db02af5212f0dcd2a11d87a2745f1d4307aa20a93c0323da8764"}, "ex_ice": {:hex, :ex_ice, "0.13.0", "13a6ae106b26bb5f2957a586bf20d4031299e5b968533828e637bb4ac7645d31", [: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", "0d65afa15e36b5610d0f51e72e4c25b22346caa9a6d7d2f6f1cfd8db94bd494e"}, @@ -79,7 +79,7 @@ "membrane_portaudio_plugin": {:hex, :membrane_portaudio_plugin, "0.19.4", "775996a14b8a2ecc2561d4caffe3312b0aae73f1fa332bc4267ff575952a749c", [:mix], [{:bunch, "~> 1.5", [hex: :bunch, 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.2.1", [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.2", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "c1d7643590b777012b9f73147b14236ccab1c477ba1dfb2b3f271915798162c8"}, "membrane_precompiled_dependency_provider": {:hex, :membrane_precompiled_dependency_provider, "0.2.2", "0fbff1eb651619ce95abd7f9d19dd636ce460adc01bea36a440c48d1a6572a95", [:mix], [{:bundlex, "~> 1.4", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "60296232d613856d22494303b64487bfa141666544f2e83a97f1d2dd28c34453"}, "membrane_raw_audio_format": {:hex, :membrane_raw_audio_format, "0.12.0", "b574cd90f69ce2a8b6201b0ccf0826ca28b0fbc8245b8078d9f11cef65f7d5d5", [:mix], [{:bimap, "~> 1.1", [hex: :bimap, repo: "hexpm", optional: false]}, {:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "6e6c98e3622a2b9df19eab50ba65d7eb45949b1ba306fa8423df6cdb12fd0b44"}, - "membrane_raw_video_format": {:hex, :membrane_raw_video_format, "0.4.1", "d7344499c2d80f236a7ef962b5490c651341a501052ee43dec56cf0319fa3936", [:mix], [], "hexpm", "9920b7d445b5357608a364fec5685acdfce85334c647f745045237a0d296c442"}, + "membrane_raw_video_format": {:hex, :membrane_raw_video_format, "0.4.3", "61b2f6afdffa43c25de5a433c3a3bed933144be0f753b6fc8c6a9f255382eaff", [:mix], [{:image, ">= 0.54.0", [hex: :image, repo: "hexpm", optional: true]}], "hexpm", "11739a7d956d037f3ee109f06f075f1a99fea000c778628ac58ed28637e4c637"}, "membrane_realtimer_plugin": {:hex, :membrane_realtimer_plugin, "0.9.0", "27210d5e32a5e8bfd101c41e4d8c1876e873a52cc129ebfbee4d0ccbea1cbd21", [:mix], [{:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "b2e96d62135ee57ef9a5fdea94b3a9ab1198e5ea8ee248391b89c671125d1b51"}, "membrane_rtmp_plugin": {:hex, :membrane_rtmp_plugin, "0.29.1", "ad3f9c937aa28eaa6b837a250367cb1ad4d600aa0c38ddcc7f2f2f2721cdaeaf", [:mix], [{:membrane_aac_plugin, "~> 0.19.0", [hex: :membrane_aac_plugin, 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_flv_plugin, "~> 0.12.0", [hex: :membrane_flv_plugin, repo: "hexpm", optional: false]}, {:membrane_funnel_plugin, "~> 0.9.0", [hex: :membrane_funnel_plugin, repo: "hexpm", optional: false]}, {:membrane_h264_format, "~> 0.6.1", [hex: :membrane_h264_format, repo: "hexpm", optional: false]}, {:membrane_h26x_plugin, "~> 0.10.0", [hex: :membrane_h26x_plugin, repo: "hexpm", optional: false]}, {:membrane_precompiled_dependency_provider, "~> 0.2.1", [hex: :membrane_precompiled_dependency_provider, repo: "hexpm", optional: false]}, {:unifex, "~> 1.2", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "8d0c70899627ba00e3d524945715971d8626a636e80eb76d4d63bbd1a88a3895"}, "membrane_rtp_aac_plugin": {:hex, :membrane_rtp_aac_plugin, "0.9.4", "355efe237151b304a479a8f0db12043aea2528718d045cb596cbfb85f64ff20a", [:mix], [{: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]}, {:membrane_rtp_format, "~> 0.10.0", [hex: :membrane_rtp_format, repo: "hexpm", optional: false]}], "hexpm", "f72d12b88b57a3c93eeea19c02c95c878c4b09883dbec703ae3d1557d3af44c0"}, @@ -119,12 +119,12 @@ "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, "playwright": {:hex, :playwright, "1.49.1-alpha.2", "911e7771f51874cca6d075b2c73e7a32e3d7e9b29dc38732f5121ac916cae35d", [:mix], [{:cowlib, "~> 2.7.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:elixir_uuid, "~> 1.2", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:esbuild, "~> 0.8.1", [hex: :esbuild, repo: "hexpm", optional: false]}, {:gun, "~> 1.3.3", [hex: :gun, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:recase, "~> 0.7", [hex: :recase, repo: "hexpm", optional: false]}], "hexpm", "492386163d6fd65967c39e4822478639e198bcf45e527253825f57d72867af52"}, - "plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [: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", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"}, + "plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [: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", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "qex": {:hex, :qex, "0.5.1", "0d82c0f008551d24fffb99d97f8299afcb8ea9cf99582b770bd004ed5af63fd6", [:mix], [], "hexpm", "935a39fdaf2445834b95951456559e9dc2063d0a055742c558a99987b38d6bab"}, "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"}, - "recase": {:hex, :recase, "0.9.0", "437982693fdfbec125f11c8868eb3b4d32e9aa6995d3a68ac8686f3e2bf5d8d1", [:mix], [], "hexpm", "efa7549ebd128988d1723037a6f6a61948055aec107db6288f1c52830cb6501c"}, - "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"}, + "recase": {:hex, :recase, "0.9.1", "82d2e2e2d4f9e92da1ce5db338ede2e4f15a50ac1141fc082b80050b9f49d96e", [:mix], [], "hexpm", "19ba03ceb811750e6bec4a015a9f9e45d16a8b9e09187f6d72c3798f454710f3"}, + "req": {:hex, :req, "0.5.16", "99ba6a36b014458e52a8b9a0543bfa752cb0344b2a9d756651db1281d4ba4450", [: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", "974a7a27982b9b791df84e8f6687d21483795882a7840e8309abdbe08bb06f09"}, "shmex": {:hex, :shmex, "0.5.1", "81dd209093416bf6608e66882cb7e676089307448a1afd4fc906c1f7e5b94cf4", [:mix], [{:bunch_native, "~> 0.5.0", [hex: :bunch_native, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "c29f8286891252f64c4e1dac40b217d960f7d58def597c4e606ff8fbe71ceb80"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "stream_split": {:hex, :stream_split, "0.1.7", "2d3fd1fd21697da7f91926768d65f79409086052c9ec7ae593987388f52425f8", [:mix], [], "hexpm", "1dc072ff507a64404a0ad7af90df97096183fee8eeac7b300320cea7c4679147"}, @@ -137,6 +137,6 @@ "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"}, "vix": {:hex, :vix, "0.35.0", "f6319b715e3b072e53eba456a21af5f2ff010a7a7b19b884600ea98a0609b18c", [:make, :mix], [{:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "a3e80067a89d0631b6cf2b93594e03c1b303a2c7cddbbdd28040750d521984e5"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, - "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"}, "zarex": {:hex, :zarex, "1.0.6", "f657ed1187e6e90472e24c92b1fd5bf3f846e74bd240bd77276c13f336a8d168", [:mix], [], "hexpm", "b628a9b0bc312f278af2c288078c31fd4757224b82d768e91bcf3bedbe3a50e7"}, } diff --git a/test/boombox_test.exs b/test/boombox_test.exs index 0db788b..15f04c8 100644 --- a/test/boombox_test.exs +++ b/test/boombox_test.exs @@ -472,7 +472,7 @@ defmodule BoomboxTest do Compare.compare("#{tmp}/output.mp4", "test/fixtures/ref_bun_rotated.mp4") end - [:stream, :writer] + [:stream, :writer, :message] |> Enum.each(fn elixir_endpoint -> @tag String.to_atom("bouncing_bubble_#{elixir_endpoint}_webrtc_mp4") async_test "bouncing bubble -> #{elixir_endpoint} -> webrtc -> mp4", %{tmp_dir: tmp} do @@ -506,6 +506,23 @@ defmodule BoomboxTest do Enum.each(stream, &Boombox.write(writer, &1)) Boombox.close(writer) end + + :message -> + server = + Boombox.run( + input: {:message, video: :image, audio: false}, + output: {:webrtc, signaling} + ) + + fn stream -> + Enum.each(stream, &send(server, {:boombox_packet, self(), &1})) + send(server, {:boombox_close, self()}) + + receive do + {:boombox_finished, ^server} -> + :ok + end + end end Task.async(fn -> @@ -529,7 +546,7 @@ defmodule BoomboxTest do end end) - [:stream, :reader] + [:stream, :reader, :message] |> Enum.each(fn elixir_endpoint -> @tag String.to_atom("mp4_#{elixir_endpoint}_resampled_pcm") async_test "mp4 -> #{elixir_endpoint} -> resampled PCM" do @@ -559,6 +576,17 @@ defmodule BoomboxTest do :finished -> nil end) + + :message -> + Stream.unfold(:ok, fn :ok -> + receive do + {:boombox_packet, ^boombox, packet} -> + {packet, :ok} + + {:boombox_finished, ^boombox} -> + nil + end + end) end |> Enum.map_join(& &1.payload)