diff --git a/README.md b/README.md index e9cd0a28..ee10639a 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ For more examples, see [examples.livemd](examples.livemd). |---|---|---| | MP4 | `"*.mp4"` | `"*.mp4"` | | WebRTC | `{:webrtc, signaling}` | `{:webrtc, signaling}` | +| WHIP | `{:whip, "http://*", token: "token"}` | `{:whip, "http://*", token: "token"}` | | RTMP | `"rtmp://*"` | _not supported_ | | RTSP | `"rtsp://*"` | _not supported_ | | RTP | `{:rtp, opts}` | _not yet supported_ | @@ -112,7 +113,7 @@ The CLI API is a direct mapping of the Elixir API: For example: ```elixir -Boombox.run(input: "file.mp4", output: {:webrtc, "ws://localhost:8830"}) +Boombox.run(input: "file.mp4", output: {:whip, "http://localhost:3721", token: "token"}) Boombox.run( input: {:rtp, @@ -127,7 +128,7 @@ Boombox.run( are equivalent to: ```sh -./boombox -i file.mp4 -o --webrtc ws://localhost:8830 +./boombox -i file.mp4 -o --whip http://localhost:3721 --token token ./boombox -i --rtp --port 50001 --audio-encoding AAC --audio-specific-config a13f --aac-bitrate-mode hbr -o index.m3u8 ``` diff --git a/bin/boombox_local b/bin/boombox_local new file mode 100755 index 00000000..efd13d2a --- /dev/null +++ b/bin/boombox_local @@ -0,0 +1,2 @@ +#/bin/sh +mix run -e 'Logger.configure(level: :info);Boombox.run_cli()' -- $@ \ No newline at end of file diff --git a/boombox_examples_data/webrtc_from_browser.html b/boombox_examples_data/webrtc_from_browser.html index 908a75e0..1d2299dc 100644 --- a/boombox_examples_data/webrtc_from_browser.html +++ b/boombox_examples_data/webrtc_from_browser.html @@ -32,6 +32,7 @@

Boombox stream WebRTC from browser example

const localStream = await navigator.mediaDevices.getUserMedia(mediaConstraints); preview.srcObject = localStream; const pc = new RTCPeerConnection(pcConfig); + window.pc = pc; // for debugging purposes pc.onicecandidate = event => { if (event.candidate === null) return; diff --git a/boombox_examples_data/webrtc_to_browser.html b/boombox_examples_data/webrtc_to_browser.html index 028c7b6a..48add967 100644 --- a/boombox_examples_data/webrtc_to_browser.html +++ b/boombox_examples_data/webrtc_to_browser.html @@ -12,10 +12,10 @@ style="background-color: black; color: white; font-family: Arial, Helvetica, sans-serif; min-height: 100vh; margin: 0px; padding: 5px 0px 5px 0px">

Boombox stream WebRTC to browser example

-
Boombox URL:
+

@@ -30,6 +30,7 @@

Boombox stream WebRTC to browser example

videoPlayer.srcObject = new MediaStream(); const pc = new RTCPeerConnection(pcConfig); + window.pc = pc; // for debugging purposes pc.ontrack = event => videoPlayer.srcObject.addTrack(event.track); videoPlayer.play(); pc.onicecandidate = event => { @@ -42,6 +43,7 @@

Boombox stream WebRTC to browser example

pc.onconnectionstatechange = () => { if (pc.connectionState == "connected") { connStatus.innerHTML = "Connected"; + button.innerHTML = "Disconnect"; } } @@ -67,7 +69,11 @@

Boombox stream WebRTC to browser example

const connect = () => { const ws = new WebSocket(url.value); ws.onopen = () => connectRTC(ws); - ws.onclose = event => console.log("WebSocket connection was terminated:", event); + ws.onclose = event => { + console.log("WebSocket connection was terminated:", event); + connStatus.innerHTML = "Disconnected"; + button.innerHTML = "Connect"; + } } button.onclick = connect; diff --git a/boombox_examples_data/whip.html b/boombox_examples_data/whip.html new file mode 100644 index 00000000..7392839a --- /dev/null +++ b/boombox_examples_data/whip.html @@ -0,0 +1,56 @@ + + + + + + + + Membrane WebRTC WHIP/WHEP Example + + + +

Boombox WHIP Example

+
+ Boombox URL: + Token: + +
+
+
+ + + + + \ No newline at end of file diff --git a/examples.livemd b/examples.livemd index 6c5ca662..33a8f454 100644 --- a/examples.livemd +++ b/examples.livemd @@ -84,14 +84,14 @@ Boombox.run(input: {:webrtc, "ws://localhost:8829"}, output: {:webrtc, "ws://loc -## Record WebRTC to MP4 +## Record WebRTC via WHIP to MP4 -To send the stream, visit http://localhost:1234/webrtc_from_browser.html. +To send the stream, visit http://localhost:1234/whip.html. Note: don't stop this cell to finish recording - click 'disconnect' or close the browser tab instead, so the recording is finalized properly. ```elixir -Boombox.run(input: {:webrtc, "ws://localhost:8829"}, output: "#{out_dir}/webrtc_to_mp4.mp4") +Boombox.run(input: {:whip, "http://localhost:8829", token: "whip_it!"}, output: "#{out_dir}/webrtc_to_mp4.mp4") ``` ```elixir diff --git a/lib/boombox.ex b/lib/boombox.ex index 8db7d255..c9872199 100644 --- a/lib/boombox.ex +++ b/lib/boombox.ex @@ -69,6 +69,7 @@ defmodule Boombox do (path_or_uri :: String.t()) | {:mp4, location :: String.t(), transport: :file | :http} | {:webrtc, webrtc_signaling()} + | {:whip, uri :: String.t(), token: String.t()} | {:rtmp, (uri :: String.t()) | (client_handler :: pid)} | {:rtsp, url :: String.t()} | {:rtp, in_rtp_opts()} @@ -78,6 +79,7 @@ defmodule Boombox do (path_or_uri :: String.t()) | {:mp4, location :: String.t()} | {:webrtc, webrtc_signaling()} + | {:whip, uri :: String.t(), [{:token, String.t()} | {bandit_option :: atom(), term()}]} | {:hls, location :: String.t()} | {:rtp, out_rtp_opts()} | {:stream, out_stream_opts()} @@ -190,6 +192,14 @@ defmodule Boombox do {:webrtc, uri} when is_binary(uri) -> value + {:whip, uri} when is_binary(uri) -> + parse_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 + {:rtmp, arg} when direction == :input and (is_binary(arg) or is_pid(arg)) -> value diff --git a/lib/boombox/utils/cli.ex b/lib/boombox/utils/cli.ex index b9e2b4c2..a1f033ae 100644 --- a/lib/boombox/utils/cli.ex +++ b/lib/boombox/utils/cli.ex @@ -22,7 +22,9 @@ defmodule Boombox.Utils.CLI do audio_specific_config: {:string, :binary}, pps: {:string, :binary}, sps: {:string, :binary}, - vps: {:string, :binary} + vps: {:string, :binary}, + whip: {:string, :string}, + token: {:string, :string} ] @spec parse_argv([String.t()]) :: diff --git a/lib/boombox/webrtc.ex b/lib/boombox/webrtc.ex index 88d1596a..3235a3f5 100644 --- a/lib/boombox/webrtc.ex +++ b/lib/boombox/webrtc.ex @@ -3,11 +3,9 @@ defmodule Boombox.WebRTC do import Membrane.ChildrenSpec require Membrane.Pad, as: Pad - alias Membrane.WebRTC.SimpleWebSocketServer alias Boombox.Pipeline.{Ready, State, Wait} - alias Membrane.{H264, RemoteStream, VP8} + alias Membrane.{H264, RemoteStream, VP8, WebRTC} alias Membrane.Pipeline.CallbackContext - alias Membrane.WebRTC.SignalingChannel @type output_webrtc_state :: %{negotiated_video_codecs: [:vp8 | :h264] | nil} @type webrtc_sink_new_tracks :: [%{id: term, kind: :audio | :video}] @@ -15,7 +13,7 @@ defmodule Boombox.WebRTC do @spec create_input(Boombox.webrtc_signaling(), Boombox.output(), CallbackContext.t(), State.t()) :: Wait.t() def create_input(signaling, output, ctx, state) do - signaling = resolve_signaling(signaling, ctx.utility_supervisor) + signaling = resolve_signaling(signaling, :input, ctx.utility_supervisor) keyframe_interval = case output do @@ -40,7 +38,7 @@ defmodule Boombox.WebRTC do end spec = - child(:webrtc_input, %Membrane.WebRTC.Source{ + child(:webrtc_input, %WebRTC.Source{ signaling: signaling, preferred_video_codec: preferred_video_codec, allowed_video_codecs: allowed_video_codecs, @@ -50,7 +48,7 @@ defmodule Boombox.WebRTC do %Wait{actions: [spec: spec]} end - @spec handle_input_tracks(Membrane.WebRTC.Source.new_tracks()) :: Ready.t() + @spec handle_input_tracks(WebRTC.Source.new_tracks()) :: Ready.t() def handle_input_tracks(tracks) do track_builders = Map.new(tracks, fn @@ -75,11 +73,11 @@ defmodule Boombox.WebRTC do @spec create_output(Boombox.webrtc_signaling(), CallbackContext.t(), State.t()) :: {Ready.t() | Wait.t(), State.t()} def create_output(signaling, ctx, state) do - signaling = resolve_signaling(signaling, ctx.utility_supervisor) + signaling = resolve_signaling(signaling, :output, ctx.utility_supervisor) startup_tracks = if webrtc_input?(state), do: [:audio, :video], else: [] spec = - child(:webrtc_output, %Membrane.WebRTC.Sink{ + child(:webrtc_output, %WebRTC.Sink{ signaling: signaling, tracks: startup_tracks, video_codec: [:vp8, :h264] @@ -91,7 +89,7 @@ defmodule Boombox.WebRTC do if webrtc_input?(state) do # let's spawn websocket server for webrtc source before the source starts {:webrtc, input_signaling} = state.input - signaling_channel = resolve_signaling(input_signaling, ctx.utility_supervisor) + signaling_channel = resolve_signaling(input_signaling, :input, ctx.utility_supervisor) state = %{state | input: {:webrtc, signaling_channel}} {%Wait{actions: [spec: spec]}, state} @@ -189,16 +187,58 @@ defmodule Boombox.WebRTC do %Ready{actions: [spec: spec], eos_info: Map.values(tracks)} end - defp resolve_signaling(%SignalingChannel{} = signaling, _utility_supervisor) do + defp resolve_signaling( + %WebRTC.SignalingChannel{} = signaling, + _direction, + _utility_supervisor + ) do signaling end - defp resolve_signaling(uri, utility_supervisor) when is_binary(uri) do + defp resolve_signaling({:whip, uri, opts}, :input, utility_supervisor) do + uri = URI.new!(uri) + {:ok, ip} = :inet.getaddr(~c"#{uri.host}", :inet) + setup_whip_server([ip: ip, port: uri.port] ++ opts, utility_supervisor) + end + + defp resolve_signaling({:whip, uri, opts}, :output, utility_supervisor) do + signaling = WebRTC.SignalingChannel.new() + + Membrane.UtilitySupervisor.start_link_child( + utility_supervisor, + {WebRTC.WhipClient, [signaling: signaling, uri: uri] ++ opts} + ) + + signaling + end + + defp resolve_signaling(uri, _direction, utility_supervisor) when is_binary(uri) do uri = URI.new!(uri) {:ok, ip} = :inet.getaddr(~c"#{uri.host}", :inet) opts = [ip: ip, port: uri.port] - SimpleWebSocketServer.start_link_supervised(utility_supervisor, opts) + WebRTC.SimpleWebSocketServer.start_link_supervised(utility_supervisor, opts) + end + + defp setup_whip_server(opts, utility_supervisor) do + signaling = WebRTC.SignalingChannel.new() + clients_cnt = :atomics.new(1, []) + {valid_token, opts} = Keyword.pop(opts, :token) + + handle_new_client = fn token -> + cond do + valid_token not in [nil, token] -> {:error, :invalid_token} + :atomics.add_get(clients_cnt, 1, 1) > 1 -> {:error, :already_connected} + true -> {:ok, signaling} + end + end + + Membrane.UtilitySupervisor.start_child(utility_supervisor, { + WebRTC.WhipServer, + [handle_new_client: handle_new_client] ++ opts + }) + + signaling end defp webrtc_input?(%{input: {:webrtc, _signalling}}), do: true diff --git a/mix.exs b/mix.exs index b222d4c6..29899e6a 100644 --- a/mix.exs +++ b/mix.exs @@ -46,7 +46,8 @@ defmodule Boombox.Mixfile do defp deps do [ {:membrane_core, "~> 1.1"}, - {:membrane_webrtc_plugin, "~> 0.23.2"}, + # {:membrane_webrtc_plugin, "~> 0.23.2"}, + {:membrane_webrtc_plugin, github: "membraneframework/membrane_webrtc_plugin"}, {:membrane_opus_plugin, "~> 0.20.3"}, {:membrane_aac_plugin, "~> 0.19.0"}, {:membrane_aac_fdk_plugin, "~> 0.18.0"}, diff --git a/mix.lock b/mix.lock index abc02ec1..f6f72245 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,5 @@ %{ - "bandit": {:hex, :bandit, "1.6.5", "24096d6232e0d050096acec96a0a382c44de026f9b591b883ed45497e1ef4916", [: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", "b6b91f630699c8b41f3f0184bd4f60b281e19a336ad9dc1a0da90637b6688332"}, + "bandit": {:hex, :bandit, "1.6.6", "f2019a95261d400579075df5bc15641ba8e446cc4777ede6b4ec19e434c3340d", [: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", "ceb19bf154bc2c07ee0c9addf407d817c48107e36a66351500846fc325451bf9"}, "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"}, @@ -88,7 +88,7 @@ "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"}, "membrane_vpx_plugin": {:hex, :membrane_vpx_plugin, "0.3.0", "60404d1b1511b4c62ba6bbf7b6212570f1732ba477015c4072e0aa33e18a8809", [:mix], [{: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_video_format, "~> 0.4.0", [hex: :membrane_raw_video_format, repo: "hexpm", optional: false]}, {:membrane_vp8_format, "~> 0.5.0", [hex: :membrane_vp8_format, repo: "hexpm", optional: false]}, {:membrane_vp9_format, "~> 0.5.0", [hex: :membrane_vp9_format, repo: "hexpm", optional: false]}, {:unifex, "~> 1.2", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "effa7762bbf73efd8d21d0978bce79538414719284194db97672afbce665b56a"}, - "membrane_webrtc_plugin": {:hex, :membrane_webrtc_plugin, "0.23.2", "5f3aa18d54d808fcefc89f0047300d840eccc49af87a729b76907df987dd9074", [:mix], [{:bandit, "~> 1.2", [hex: :bandit, repo: "hexpm", optional: false]}, {:corsica, "~> 2.0", [hex: :corsica, repo: "hexpm", optional: false]}, {:ex_webrtc, "~> 0.4.0", [hex: :ex_webrtc, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.1", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_rtp_h264_plugin, "~> 0.20.1", [hex: :membrane_rtp_h264_plugin, repo: "hexpm", optional: false]}, {:membrane_rtp_opus_plugin, "~> 0.10.0", [hex: :membrane_rtp_opus_plugin, repo: "hexpm", optional: false]}, {:membrane_rtp_plugin, "~> 0.30.0", [hex: :membrane_rtp_plugin, repo: "hexpm", optional: false]}, {:membrane_rtp_vp8_plugin, "~> 0.9.4", [hex: :membrane_rtp_vp8_plugin, repo: "hexpm", optional: false]}, {:membrane_timestamp_queue, "~> 0.2.0", [hex: :membrane_timestamp_queue, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.0", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "22aad6b69a94ced429091bd24887d54ff908dc61945dfd427c57a7b3b9ed1ac3"}, + "membrane_webrtc_plugin": {:git, "https://github.com/membraneframework/membrane_webrtc_plugin.git", "8c1567c212f6ce4dfb06b6d5ed183b37d435336f", []}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, @@ -111,7 +111,7 @@ "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "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.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"}, + "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.9", "095db3e2650819443e33237891271943fad3b7f9ba341073947581362582ab5a", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "25ab4c07badadf7f87adb4ab414e0ed374e5f19e72503aa85132caa25776e54f"}, "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"}, diff --git a/test/assets/webrtc_from_browser.html b/test/assets/webrtc_from_browser.html deleted file mode 100644 index c7ff3073..00000000 --- a/test/assets/webrtc_from_browser.html +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - - - Boombox stream WebRTC from browser example - - - -
-

Boombox stream WebRTC from browser example

-
- Boombox URL: -
-
-
-
- -
- - - - - - \ No newline at end of file diff --git a/test/assets/webrtc_to_browser.html b/test/assets/webrtc_to_browser.html deleted file mode 100644 index 134c5949..00000000 --- a/test/assets/webrtc_to_browser.html +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - Boombox stream WebRTC to browser example -
-
- - - -
-

Boombox stream WebRTC to browser example

-
- Boombox URL: -
-
- -
- - - - \ No newline at end of file diff --git a/test/boombox_test.exs b/test/boombox_test.exs index ff81b257..10c8fe49 100644 --- a/test/boombox_test.exs +++ b/test/boombox_test.exs @@ -73,6 +73,20 @@ defmodule BoomboxTest do Compare.compare(output, "test/fixtures/ref_bun10s_opus_aac.mp4") end + @tag :file_whip + async_test "mp4 file -> webrtc/whip -> mp4 file", %{tmp_dir: tmp} do + output = Path.join(tmp, "output.mp4") + + t = + Task.async(fn -> + Boombox.run(input: @bbb_mp4, output: {:whip, "http://127.0.0.1:3721"}) + end) + + Boombox.run(input: {:whip, "http://127.0.0.1:3721"}, output: output) + Task.await(t) + Compare.compare(output, "test/fixtures/ref_bun10s_opus_aac.mp4") + end + @tag :http_webrtc async_test "http mp4 -> webrtc -> mp4 file", %{tmp_dir: tmp} do output = Path.join(tmp, "output.mp4") diff --git a/test/browser_test.exs b/test/browser_test.exs index 410e3ba7..c2cf394c 100644 --- a/test/browser_test.exs +++ b/test/browser_test.exs @@ -1,4 +1,4 @@ -defmodule BrowserTest do +defmodule Boombox.BrowserTest do use ExUnit.Case, async: false # Tests from this module are currently switched off on the CI because @@ -10,6 +10,8 @@ defmodule BrowserTest do @port 1235 + @moduletag :browser + setup_all do browser_launch_opts = %{ args: [ @@ -29,7 +31,7 @@ defmodule BrowserTest do :inets.start(:httpd, bind_address: ~c"localhost", port: @port, - document_root: ~c"#{__DIR__}/assets", + document_root: ~c"boombox_examples_data", server_name: ~c"assets_server", server_root: ~c"/tmp", erl_script_nocache: true @@ -48,7 +50,6 @@ defmodule BrowserTest do [browser: browser] end - @tag :browser @tag :tmp_dir test "browser -> boombox -> mp4", %{browser: browser, tmp_dir: tmp_dir} do output_path = Path.join(tmp_dir, "/webrtc_to_mp4.mp4") @@ -61,15 +62,43 @@ defmodule BrowserTest do ) end) - ingress_page = start_ingress_page(browser) + ingress_page = start_page(browser, "webrtc_from_browser") + + seconds = 10 + Process.sleep(seconds * 1000) + + assert_page_connected(ingress_page) + assert_frames_encoded(ingress_page, seconds) + + close_page(ingress_page) + + Task.await(boombox_task) + + assert %{size: size} = File.stat!(output_path) + # if things work fine, the size should be around ~850_000 + assert size > 400_000 + end + + @tag :tmp_dir + test "browser -> (whip) boombox -> mp4", %{browser: browser, tmp_dir: tmp_dir} do + output_path = Path.join(tmp_dir, "/webrtc_to_mp4.mp4") + + boombox_task = + Task.async(fn -> + Boombox.run( + input: {:whip, "http://localhost:8829", token: "whip_it!"}, + output: output_path + ) + end) + ingress_page = start_page(browser, "whip") seconds = 10 Process.sleep(seconds * 1000) assert_page_connected(ingress_page) - assert_frames_on_ingress_page(ingress_page, seconds) + assert_frames_encoded(ingress_page, seconds) - Playwright.Page.close(ingress_page) + close_page(ingress_page) Task.await(boombox_task) @@ -79,7 +108,6 @@ defmodule BrowserTest do end for first <- [:ingress, :egress] do - @tag :browser test "browser -> boombox -> browser, but #{first} browser page connects first", %{ browser: browser } do @@ -94,15 +122,15 @@ defmodule BrowserTest do {ingress_page, egress_page} = case unquote(first) do :ingress -> - ingress_page = start_ingress_page(browser) + ingress_page = start_page(browser, "webrtc_from_browser") Process.sleep(500) - egress_page = start_egress_page(browser) + egress_page = start_page(browser, "webrtc_to_browser") {ingress_page, egress_page} :egress -> - egress_page = start_egress_page(browser) + egress_page = start_page(browser, "webrtc_to_browser") Process.sleep(500) - ingress_page = start_ingress_page(browser) + ingress_page = start_page(browser, "webrtc_from_browser") {ingress_page, egress_page} end @@ -112,27 +140,22 @@ defmodule BrowserTest do [ingress_page, egress_page] |> Enum.each(&assert_page_connected/1) - assert_frames_on_ingress_page(ingress_page, seconds) - assert_frames_on_egress_page(egress_page, seconds) + assert_frames_encoded(ingress_page, seconds) + assert_frames_decoded(egress_page, seconds) [ingress_page, egress_page] - |> Enum.each(&Playwright.Page.close/1) + |> Enum.each(&close_page/1) Task.await(boombox_task) end end - defp start_ingress_page(browser) do - url = "http://localhost:#{inspect(@port)}/webrtc_from_browser.html" - start_page(browser, url) + defp start_page(browser, page) do + url = "http://localhost:#{@port}/#{page}.html" + do_start_page(browser, url) end - defp start_egress_page(browser) do - url = "http://localhost:#{inspect(@port)}/webrtc_to_browser.html" - start_page(browser, url) - end - - defp start_page(browser, url) do + defp do_start_page(browser, url) do page = Playwright.Browser.new_page(browser) response = Playwright.Page.goto(page, url) @@ -143,31 +166,35 @@ defmodule BrowserTest do page end - defp assert_frames_on_ingress_page(page, seconds_since_launch) do - div_id = "outbound-rtp-frames-encoded" - assert_frames_on_page(page, div_id, seconds_since_launch) + defp close_page(page) do + Playwright.Page.click(page, "button[id=\"button\"]") + Playwright.Page.close(page) end - defp assert_frames_on_egress_page(page, seconds_since_launch) do - div_id = "inbound-rtp-frames-decoded" - assert_frames_on_page(page, div_id, seconds_since_launch) + defp assert_page_connected(page) do + assert page + |> Playwright.Page.text_content("[id=\"status\"]") + |> String.contains?("Connected") end - defp assert_frames_on_page(page, id, seconds_since_launch) do - frames_number = - page - |> Playwright.Page.text_content("[id=\"#{id}\"]") - |> String.to_integer() - - frames_per_second_lowerbound = 12 - frames_number_lowerbound = frames_per_second_lowerbound * seconds_since_launch + defp assert_frames_encoded(page, time_seconds) do + fps_lowerbound = 12 + frames_encoded = get_webrtc_stats(page, type: "outbound-rtp", kind: "video").framesEncoded + assert frames_encoded >= time_seconds * fps_lowerbound + end - assert frames_number >= frames_number_lowerbound + defp assert_frames_decoded(page, time_seconds) do + fps_lowerbound = 12 + frames_decoded = get_webrtc_stats(page, type: "inbound-rtp", kind: "video").framesDecoded + assert frames_decoded >= time_seconds * fps_lowerbound end - defp assert_page_connected(page) do - assert page - |> Playwright.Page.text_content("[id=\"status\"]") - |> String.contains?("Connected") + defp get_webrtc_stats(page, constraints) do + js_fuj = + "async () => {const stats = await window.pc.getStats(null); return Array.from(stats)}" + + 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) end end