Skip to content

Commit e5d7eba

Browse files
committed
add webrtc wrapper around gstreamer, web client
1 parent 9d40814 commit e5d7eba

File tree

4 files changed

+277
-0
lines changed

4 files changed

+277
-0
lines changed

test/webrtc.tcl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
loadVirtualPrograms [list "virtual-programs/web/webrtc.folk" "virtual-programs/web/web-keyboards.folk" "virtual-programs/keyboard.folk" "virtual-programs/gstreamer.folk" "virtual-programs/images.folk" "virtual-programs/web/new-program-web-editor.folk"]
2+
Step
3+
4+
Assert <unknown> wishes the-moon receives webrtc video earth
5+
6+
When the-moon has webrtc video earth frame /image/ at /ts/ {
7+
puts "have feed"
8+
Wish the web server handles route "/rtc-image/$" with handler [list apply {{im} {
9+
set filename "/tmp/web-image-frame.png"
10+
image saveAsPng $im $filename
11+
set fsize [file size $filename]
12+
set fd [open $filename r]
13+
fconfigure $fd -encoding binary -translation binary
14+
set body [read $fd $fsize]
15+
close $fd
16+
dict create statusAndHeaders "HTTP/1.1 200 OK\nConnection: close\nContent-Type: image/png\nContent-Length: $fsize\n\n" body $body
17+
}} $image]
18+
}
19+
20+
forever { Step }

vendor/gstwebrtc/gstwebrtc-api-2.0.0.min.js

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

virtual-programs/web/webrtc.folk

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
try {
2+
exec gst-webrtc-signalling-server &
3+
} on error {e} {
4+
error "gst-webrtc-signalling-server not found, you probably need to install gst-plugins-rs"
5+
}
6+
7+
try {
8+
exec gst-inspect-1.0 rswebrtc
9+
} on error {e} {
10+
error "gstreamer plugin 'rswebrtc' not found, you probably need to install gst-plugins-rs"
11+
}
12+
13+
try {
14+
exec gst-inspect-1.0 webrtc
15+
} on error {e} {
16+
error "gstreamer plugin 'webrtc' not found, you probably need to install gst-plugins-bad"
17+
}
18+
19+
When /someone/ wishes /p/ receives webrtc video /feed/ &\
20+
/someone/ claims webrtc video /feed/ streams from peer /peer-id/ {
21+
set uri gstwebrtc://127.0.0.1:8443?peer-id=${peer-id}
22+
When the gstreamer pipeline "uridecodebin uri=$uri" frame is /image/ at /ts/ {
23+
Claim $p has webrtc video $feed frame $image at $ts
24+
}
25+
}
26+
27+
When when /p/ has webrtc video /feed/ frame /image/ at /ts/ /lambda/ with environment /e/ {
28+
Wish $p receives webrtc video $feed
29+
}
30+
31+
Wish the web server handles route "/webrtc$" with handler {
32+
html {
33+
<!DOCTYPE html>
34+
<html>
35+
<body>
36+
<span id="status">Status</span>
37+
<h1>active feeds</h1>
38+
<div id="feeds">
39+
</div>
40+
41+
<script src="/lib/folk.js"></script>
42+
<script>
43+
function wsConnect() {
44+
ws = new WebSocket(window.location.origin.replace("http", "ws") + "/ws");
45+
interval = null;
46+
47+
ws.onopen = () => {
48+
document.getElementById('status').innerHTML = "<span style=background-color:seagreen;color:white;>Connnected</span>";
49+
ws.send(`Commit {
50+
When /someone/ wishes /someone/ receives webrtc video /feed/ {
51+
::websocket::send $this text "add $feed"
52+
On unmatch { ::websocket::send $this text "del $feed" }
53+
}
54+
}`);
55+
};
56+
ws.onclose = window.onbeforeunload = () => {
57+
document.getElementById('status').innerHTML = "<span style=background-color:red;color:white;>Disconnnected</span>";
58+
feeds.innerHTML = '';
59+
setTimeout(() => { wsConnect(); }, 1000);
60+
};
61+
ws.onerror = (err) => {
62+
document.getElementById('status').innerText = "Error";
63+
console.error('Socket encountered error: ', err.message, 'Closing socket');
64+
ws.close();
65+
}
66+
ws.onmessage = (e) => {
67+
const [action, feed] = loadList(e.data);
68+
if (action === "add") {
69+
const link = document.createElement('a');
70+
link.id = `feed-${feed}`;
71+
link.style = "display: block";
72+
link.innerText = `stream to ${feed}`;
73+
link.href = `/webrtc/${encodeURIComponent(feed)}/send`;
74+
feeds.append(link);
75+
} else if (action === "del") {
76+
document.getElementById(`feed-${feed}`).remove();
77+
}
78+
}
79+
};
80+
wsConnect();
81+
</script>
82+
</body>
83+
</html>
84+
}
85+
}
86+
87+
Wish the web server handles route {/webrtc/(.*)/send$} with handler {
88+
regexp {/webrtc/(.*)/send$} $path -> feed
89+
html [format {
90+
<!DOCTYPE html>
91+
<html style="display: flex; min-height: 100vh">
92+
<head>
93+
</head>
94+
<body style="display: flex; flex-direction: column; flex: 1;">
95+
<span id="status">Status</span>
96+
<h1>share video to feed %s</h1>
97+
<div>
98+
<button id="camera-button">start camera</button>
99+
<button id="screen-button">start screenshare</button>
100+
<button id="stop-button">stop</button>
101+
</div>
102+
<video style="flex: 1; background: #000; margin: auto; max-width: 100%;" id="preview"></video>
103+
104+
<script src="/lib/folk.js"></script>
105+
<script src="/vendor/gstwebrtc/gstwebrtc-api-2.0.0.min.js"></script>
106+
<script>
107+
const protocol = window.location.protocol.replace("http", "ws");
108+
const api = new GstWebRTCAPI({
109+
meta: { name: `WebClient-${Date.now()}` },
110+
signalingServerUrl: `${protocol}//${window.location.hostname}:8443/webrtc`,
111+
});
112+
113+
const feed = '%s';
114+
let peerId = null;
115+
let session;
116+
117+
const videoElement = document.getElementById("preview");
118+
119+
api.registerConnectionListener({
120+
connected: (clientId) => { peerId = clientId; },
121+
disconnected: () => { peerId = null; }
122+
});
123+
124+
const handleStream = (stream) => {
125+
const sess = api.createProducerSession(stream);
126+
127+
if (!sess) {
128+
for (const track of stream.getTracks()) {
129+
track.stop();
130+
}
131+
// captureSection.classList.remove("starting");
132+
return;
133+
}
134+
135+
sess.addEventListener("error", (event) => console.error(event.message, event.error));
136+
137+
sess.addEventListener("closed", () => {
138+
videoElement.pause();
139+
videoElement.srcObject = null;
140+
ws.send(`Commit {}`);
141+
session = null;
142+
// captureSection.classList.remove("has-sess", "starting");
143+
});
144+
145+
sess.addEventListener("stateChanged", (event) => {
146+
if ((event.target.state === GstWebRTCAPI.SessionState.streaming)) {
147+
videoElement.srcObject = stream;
148+
videoElement.play().catch(() => {});
149+
// captureSection.classList.remove("starting");
150+
151+
// sess ready, announce to folk consumers!
152+
ws.send(`Commit {
153+
Claim webrtc video ${feed} streams from peer ${peerId}
154+
}`);
155+
}
156+
});
157+
158+
/*
159+
sess.addEventListener("clientConsumerAdded", (event) => {
160+
if (captureSection._producerSession === sess) {
161+
console.info(`client consumer added: ${event.detail.peerId}`);
162+
}
163+
});
164+
165+
sess.addEventListener("clientConsumerRemoved", (event) => {
166+
if (captureSection._producerSession === sess) {
167+
console.info(`client consumer removed: ${event.detail.peerId}`);
168+
}
169+
});
170+
*/
171+
172+
// captureSection.classList.add("has-sess");
173+
sess.start();
174+
return sess;
175+
};
176+
177+
document.getElementById("stop-button").onclick = (event) => {
178+
event.preventDefault();
179+
if (session) {
180+
session.close();
181+
}
182+
};
183+
184+
document.getElementById("camera-button").onclick = (event) => {
185+
event.preventDefault();
186+
if (session) return;
187+
188+
session = true;
189+
190+
const constraints = { video: true, audio: false };
191+
navigator.mediaDevices.getUserMedia(constraints)
192+
.then(handleStream)
193+
.then(
194+
(s) => { session = s; },
195+
(error) => {
196+
console.error("cannot have access to webcam and microphone", error);
197+
session = null;
198+
}
199+
);
200+
};
201+
202+
document.getElementById("screen-button").onclick = (event) => {
203+
event.preventDefault();
204+
if (session) return;
205+
206+
session = true;
207+
const constraints = { video: true, audio: false };
208+
navigator.mediaDevices.getDisplayMedia(constraints)
209+
.then(handleStream)
210+
.then(
211+
(s) => { session = s; },
212+
(error) => {
213+
console.error("cannot have access to screen", error);
214+
session = null;
215+
}
216+
);
217+
};
218+
</script>
219+
<script>
220+
function wsConnect() {
221+
ws = new WebSocket(window.location.origin.replace("http", "ws") + "/ws");
222+
interval = null;
223+
224+
ws.onopen = () => {
225+
document.getElementById('status').innerHTML = "<span style=background-color:seagreen;color:white;>Connnected</span>";
226+
};
227+
ws.onclose = window.onbeforeunload = () => {
228+
document.getElementById('status').innerHTML = "<span style=background-color:red;color:white;>Disconnnected</span>";
229+
setTimeout(() => { wsConnect(); }, 1000);
230+
};
231+
ws.onerror = (err) => {
232+
document.getElementById('status').innerText = "Error";
233+
console.error('Socket encountered error: ', err.message, 'Closing socket');
234+
ws.close();
235+
}
236+
ws.onmessage = (e) => {
237+
}
238+
};
239+
wsConnect();
240+
</script>
241+
</body>
242+
</html>
243+
} $feed $feed]
244+
}

web.tcl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ proc handlePage {path httpStatusVar contentTypeVar} {
128128
"/statements.pdf" {
129129
getDotAsPdf [Statements::dot] contentType
130130
}
131+
"/lib/folk.js" {
132+
set contentType "text/javascript"
133+
readFile "lib/folk.js" contentType
134+
}
135+
"/vendor/gstwebrtc/gstwebrtc-api-2.0.0.min.js" {
136+
set contentType "text/javascript"
137+
readFile "vendor/gstwebrtc/gstwebrtc-api-2.0.0.min.js" contentType
138+
}
131139
default {
132140
upvar $httpStatusVar httpStatus
133141
set httpStatus "HTTP/1.1 404 Not Found"

0 commit comments

Comments
 (0)