Skip to content

Commit 5859bd1

Browse files
authored
Merge pull request #57 from membraneframework/fix-bugs
Fix bugs in related to browser connecting via WebRTC
2 parents 069e8c6 + 69e4e81 commit 5859bd1

16 files changed

+452
-50
lines changed

.circleci/config.yml

+19-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@ version: 2.1
22
orbs:
33
elixir: membraneframework/elixir@1
44

5+
jobs:
6+
test_exclude_browser:
7+
docker:
8+
- image: membraneframeworklabs/docker_membrane
9+
environment:
10+
MIX_ENV: test
11+
12+
working_directory: ~/app
13+
steps:
14+
- attach_workspace:
15+
at: .
16+
- checkout
17+
- run: mix deps.get
18+
- run: mix compile
19+
- run: mix test --exclude browser
20+
521
workflows:
622
version: 2
723
build:
@@ -10,16 +26,16 @@ workflows:
1026
filters: &filters
1127
tags:
1228
only: /v.*/
13-
- elixir/test:
29+
- elixir/lint:
1430
filters:
1531
<<: *filters
16-
- elixir/lint:
32+
- test_exclude_browser:
1733
filters:
1834
<<: *filters
1935
- elixir/hex_publish:
2036
requires:
2137
- elixir/build_test
22-
- elixir/test
38+
- test_exclude_browser
2339
- elixir/lint
2440
context:
2541
- Deployment

README.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ For more examples, see [examples.livemd](examples.livemd).
7070
|---|---|---|
7171
| MP4 | `"*.mp4"` | `"*.mp4"` |
7272
| WebRTC | `{:webrtc, signaling}` | `{:webrtc, signaling}` |
73+
| WHIP | `{:whip, "http://*", token: "token"}` | `{:whip, "http://*", token: "token"}` |
7374
| RTMP | `"rtmp://*"` | _not supported_ |
7475
| RTSP | `"rtsp://*"` | _not supported_ |
7576
| RTP | `{:rtp, opts}` | _not yet supported_ |
@@ -112,7 +113,7 @@ The CLI API is a direct mapping of the Elixir API:
112113
For example:
113114

114115
```elixir
115-
Boombox.run(input: "file.mp4", output: {:webrtc, "ws://localhost:8830"})
116+
Boombox.run(input: "file.mp4", output: {:whip, "http://localhost:3721", token: "token"})
116117
Boombox.run(
117118
input:
118119
{:rtp,
@@ -127,7 +128,7 @@ Boombox.run(
127128
are equivalent to:
128129

129130
```sh
130-
./boombox -i file.mp4 -o --webrtc ws://localhost:8830
131+
./boombox -i file.mp4 -o --whip http://localhost:3721 --token token
131132
./boombox -i --rtp --port 50001 --audio-encoding AAC --audio-specific-config a13f --aac-bitrate-mode hbr -o index.m3u8
132133
```
133134

bin/boombox_local

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#/bin/sh
2+
mix run -e 'Logger.configure(level: :info);Boombox.run_cli()' -- $@

boombox_examples_data/webrtc_from_browser.html

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ <h1>Boombox stream WebRTC from browser example</h1>
3232
const localStream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
3333
preview.srcObject = localStream;
3434
const pc = new RTCPeerConnection(pcConfig);
35+
window.pc = pc; // for debugging purposes
3536

3637
pc.onicecandidate = event => {
3738
if (event.candidate === null) return;

boombox_examples_data/webrtc_to_browser.html

+14-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ <h1>Boombox stream WebRTC to browser example</h1>
1515
<div>
1616
Boombox URL: <input type="text" value="ws://localhost:8830" id="url" /> <button id="button">Connect</button>
1717
</div>
18+
<div id="status"></div>
1819
<br>
1920
<video id="videoPlayer" controls muted autoplay></video>
2021
</main>
@@ -29,6 +30,7 @@ <h1>Boombox stream WebRTC to browser example</h1>
2930
videoPlayer.srcObject = new MediaStream();
3031

3132
const pc = new RTCPeerConnection(pcConfig);
33+
window.pc = pc; // for debugging purposes
3234
pc.ontrack = event => videoPlayer.srcObject.addTrack(event.track);
3335
videoPlayer.play();
3436
pc.onicecandidate = event => {
@@ -38,6 +40,13 @@ <h1>Boombox stream WebRTC to browser example</h1>
3840
ws.send(JSON.stringify({ type: "ice_candidate", data: event.candidate }));
3941
};
4042

43+
pc.onconnectionstatechange = () => {
44+
if (pc.connectionState == "connected") {
45+
connStatus.innerHTML = "Connected";
46+
button.innerHTML = "Disconnect";
47+
}
48+
}
49+
4150
ws.onmessage = async event => {
4251
const { type, data } = JSON.parse(event.data);
4352

@@ -60,7 +69,11 @@ <h1>Boombox stream WebRTC to browser example</h1>
6069
const connect = () => {
6170
const ws = new WebSocket(url.value);
6271
ws.onopen = () => connectRTC(ws);
63-
ws.onclose = event => console.log("WebSocket connection was terminated:", event);
72+
ws.onclose = event => {
73+
console.log("WebSocket connection was terminated:", event);
74+
connStatus.innerHTML = "Disconnected";
75+
button.innerHTML = "Connect";
76+
}
6477
}
6578

6679
button.onclick = connect;

boombox_examples_data/whip.html

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<meta http-equiv="X-UA-Compatible" content="ie=edge">
8+
<title>Membrane WebRTC WHIP/WHEP Example</title>
9+
</head>
10+
11+
<body
12+
style="background-color: black; color: white; font-family: Arial, Helvetica, sans-serif; min-height: 100vh; margin: 0px; padding: 5px 0px 5px 0px">
13+
<h1>Boombox WHIP Example</h1>
14+
<div>
15+
Boombox URL: <input type="text" value="http://localhost:8829" id="url" />
16+
Token: <input type="text" value="whip_it!" id="token" />
17+
<button id="button">Connect</button>
18+
</div>
19+
<div id="status"></div>
20+
<br>
21+
<video id="preview" autoplay muted></video>
22+
<script type="module">
23+
import { WHIPClient } from 'https://cdn.jsdelivr.net/npm/[email protected]/whip.js'
24+
25+
const button = document.getElementById("button");
26+
const connStatus = document.getElementById("status");
27+
const preview = document.getElementById("preview");
28+
const url = document.getElementById("url");
29+
const status = document.getElementById("status");
30+
const token = document.getElementById("token");
31+
const pcConfig = { iceServers: [{ urls: "stun:stun.l.google.com:19302" }] };
32+
const mediaConstraints = { video: true, audio: true };
33+
34+
const connect = async () => {
35+
connStatus.innerHTML = "Connecting..."
36+
const localStream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
37+
preview.srcObject = localStream;
38+
const pc = new RTCPeerConnection(pcConfig);
39+
window.pc = pc; // for debugging purposes
40+
for (const track of localStream.getTracks()) { pc.addTransceiver(track, { 'direction': 'sendonly' }) }
41+
const whip = new WHIPClient();
42+
await whip.publish(pc, url.value, token.value);
43+
connStatus.innerHTML = "Connected";
44+
button.innerHTML = "Disconnect";
45+
button.onclick = async () => {
46+
await whip.stop();
47+
status.innerHTML = "Disconnected";
48+
button.onclick = connect;
49+
}
50+
}
51+
52+
button.onclick = connect;
53+
</script>
54+
</body>
55+
56+
</html>

examples.livemd

+14-6
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,15 @@ System.put_env("PATH", "/opt/homebrew/bin:#{System.get_env("PATH")}")
99
# In case of problems installing Nx/EXLA/Bumblebee,
1010
# you can remove them and the Nx backend config below.
1111
# Examples that don't mention them should still work.
12-
Mix.install([:boombox, :kino, :nx, :exla, :bumblebee, :websockex, :membrane_simple_rtsp_server])
12+
Mix.install([
13+
{:boombox, path: __DIR__},
14+
:kino,
15+
:nx,
16+
:exla,
17+
:bumblebee,
18+
:websockex,
19+
:membrane_simple_rtsp_server
20+
])
1321

1422
Nx.global_default_backend(EXLA.Backend)
1523
```
@@ -27,8 +35,7 @@ File.mkdir_p!(input_dir)
2735
out_dir = "#{data_dir}/output"
2836
File.mkdir_p!(out_dir)
2937

30-
samples_url =
31-
"https://raw.githubusercontent.com/membraneframework/static/gh-pages/samples"
38+
samples_url = "https://raw.githubusercontent.com/membraneframework/static/gh-pages/samples"
3239

3340
unless File.exists?("#{input_dir}/bun.mp4") do
3441
%{status: 200, body: bbb_mp4} = Req.get!("#{samples_url}/big-buck-bunny/bun33s.mp4")
@@ -77,14 +84,14 @@ Boombox.run(input: {:webrtc, "ws://localhost:8829"}, output: {:webrtc, "ws://loc
7784

7885
<!-- livebook:{"branch_parent_index":0} -->
7986

80-
## Record WebRTC to MP4
87+
## Record WebRTC via WHIP to MP4
8188

82-
To send the stream, visit http://localhost:1234/webrtc_from_browser.html.
89+
To send the stream, visit http://localhost:1234/whip.html.
8390

8491
Note: don't stop this cell to finish recording - click 'disconnect' or close the browser tab instead, so the recording is finalized properly.
8592

8693
```elixir
87-
Boombox.run(input: {:webrtc, "ws://localhost:8829"}, output: "#{out_dir}/webrtc_to_mp4.mp4")
94+
Boombox.run(input: {:whip, "http://localhost:8829", token: "whip_it!"}, output: "#{out_dir}/webrtc_to_mp4.mp4")
8895
```
8996

9097
```elixir
@@ -539,6 +546,7 @@ To receive the stream, visit http://localhost:1234/hls.html after running the ce
539546

540547
```elixir
541548
rtp_port = 50001
549+
542550
t =
543551
Task.async(fn ->
544552
Boombox.run(

lib/boombox.ex

+10
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ defmodule Boombox do
6969
(path_or_uri :: String.t())
7070
| {:mp4, location :: String.t(), transport: :file | :http}
7171
| {:webrtc, webrtc_signaling()}
72+
| {:whip, uri :: String.t(), token: String.t()}
7273
| {:rtmp, (uri :: String.t()) | (client_handler :: pid)}
7374
| {:rtsp, url :: String.t()}
7475
| {:rtp, in_rtp_opts()}
@@ -78,6 +79,7 @@ defmodule Boombox do
7879
(path_or_uri :: String.t())
7980
| {:mp4, location :: String.t()}
8081
| {:webrtc, webrtc_signaling()}
82+
| {:whip, uri :: String.t(), [{:token, String.t()} | {bandit_option :: atom(), term()}]}
8183
| {:hls, location :: String.t()}
8284
| {:rtp, out_rtp_opts()}
8385
| {:stream, out_stream_opts()}
@@ -190,6 +192,14 @@ defmodule Boombox do
190192
{:webrtc, uri} when is_binary(uri) ->
191193
value
192194

195+
{:whip, uri} when is_binary(uri) ->
196+
parse_opt!(direction, {:whip, uri, []})
197+
198+
{:whip, uri, opts} when is_binary(uri) and is_list(opts) ->
199+
if Keyword.keyword?(opts) do
200+
{:webrtc, {:whip, uri, opts}}
201+
end
202+
193203
{:rtmp, arg} when direction == :input and (is_binary(arg) or is_pid(arg)) ->
194204
value
195205

lib/boombox/pipeline.ex

+4-4
Original file line numberDiff line numberDiff line change
@@ -305,8 +305,8 @@ defmodule Boombox.Pipeline do
305305

306306
@spec create_input(Boombox.input(), Membrane.Pipeline.CallbackContext.t(), State.t()) ::
307307
Ready.t() | Wait.t()
308-
defp create_input({:webrtc, signaling}, _ctx, state) do
309-
Boombox.WebRTC.create_input(signaling, state.output, state)
308+
defp create_input({:webrtc, signaling}, ctx, state) do
309+
Boombox.WebRTC.create_input(signaling, state.output, ctx, state)
310310
end
311311

312312
defp create_input({:mp4, location, opts}, _ctx, _state) do
@@ -331,8 +331,8 @@ defmodule Boombox.Pipeline do
331331

332332
@spec create_output(Boombox.output(), Membrane.Pipeline.CallbackContext.t(), State.t()) ::
333333
{Ready.t() | Wait.t(), State.t()}
334-
defp create_output({:webrtc, signaling}, _ctx, state) do
335-
Boombox.WebRTC.create_output(signaling, state)
334+
defp create_output({:webrtc, signaling}, ctx, state) do
335+
Boombox.WebRTC.create_output(signaling, ctx, state)
336336
end
337337

338338
defp create_output(_output, _ctx, state) do

lib/boombox/transcoder/video.ex

+12-1
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@ defmodule Boombox.Transcoder.Video do
33

44
import Membrane.ChildrenSpec
55
require Membrane.Logger
6+
alias Membrane.RemoteStream
67
alias Membrane.{ChildrenSpec, H264, H265, RawVideo, VP8}
78

89
@type video_stream_format :: VP8.t() | H264.t() | H265.t() | RawVideo.t()
910

1011
defguard is_video_format(format)
11-
when is_struct(format) and format.__struct__ in [VP8, H264, H265, RawVideo]
12+
when is_struct(format) and
13+
(format.__struct__ in [VP8, H264, H265, RawVideo] or
14+
(format.__struct__ == RemoteStream and format.content_format == VP8 and
15+
format.type == :packetized))
1216

1317
@spec plug_video_transcoding(
1418
ChildrenSpec.builder(),
@@ -70,6 +74,13 @@ defmodule Boombox.Transcoder.Video do
7074
builder |> child(:vp8_decoder, %VP8.Decoder{})
7175
end
7276

77+
defp maybe_plug_parser_and_decoder(builder, %RemoteStream{
78+
content_format: VP8,
79+
type: :packetized
80+
}) do
81+
builder |> child(:vp8_decoder, %VP8.Decoder{})
82+
end
83+
7384
defp maybe_plug_parser_and_decoder(builder, %RawVideo{}) do
7485
builder
7586
end

lib/boombox/utils/cli.ex

+3-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ defmodule Boombox.Utils.CLI do
2222
audio_specific_config: {:string, :binary},
2323
pps: {:string, :binary},
2424
sps: {:string, :binary},
25-
vps: {:string, :binary}
25+
vps: {:string, :binary},
26+
whip: {:string, :string},
27+
token: {:string, :string}
2628
]
2729

2830
@spec parse_argv([String.t()]) ::

0 commit comments

Comments
 (0)