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