WebRTC Loopback
A WebRTC data-channel loopback: two RTCPeerConnection instances sit in the same process, exchange SDP offer / answer + ICE candidates, open an RTCDataChannel, and echo whatever the user types back through it. No signaling server, no remote peer — but the entire RTCPeerConnection state machine runs through GStreamer’s webrtcbin on GJS.
Live demo
Section titled “Live demo”What it exercises
Section titled “What it exercises”| Pillar | What it needs |
|---|---|
@gjsify/webrtc | RTCPeerConnection, createOffer / createAnswer / setLocalDescription / setRemoteDescription, ICE event firing, RTCDataChannel with send + onmessage |
@gjsify/webrtc-native | Vala signal bridges that re-emit GStreamer’s streaming-thread callbacks onto the GLib main context (otherwise they’d fire from a wrong thread and crash SpiderMonkey) |
gst-1.0 + gst-webrtc-1.0 | The actual WebRTC implementation (DTLS, SCTP, ICE) on GJS |
| Adwaita widgets (GJS) | Adw.PreferencesGroup for the message log and input |
Run it on GJS
Section titled “Run it on GJS”gjsify showcase webrtc-loopbackA GTK4 window opens. Click “Connect” — two RTCPeerConnection instances are created, both oniceconnectionstatechange handlers wire up, the offer / answer dance happens internally, and the data channel reaches 'open'. Type into the input → echoes back.
Run it in the browser
Section titled “Run it in the browser”import { mount } from '@gjsify/example-dom-webrtc-loopback/browser';mount(document.getElementById('app')!);Exact same flow — uses the browser’s native RTCPeerConnection. The contract is whether @gjsify/webrtc’s GJS implementation behaves identically.
Source
Section titled “Source”showcases/dom/webrtc-loopback/ — entry points: src/gjs/gjs.ts (GJS shell, GLib MainLoop), src/browser/browser.ts (mount), src/browser/browser-main.ts (standalone fullscreen browser entry), src/loopback-demo.ts (the loopback dance — peer setup, signaling, data-channel wiring, runs unchanged in both targets).
The shared module imports only RTCPeerConnection + RTCSessionDescription + RTCIceCandidate from the global namespace. That surface is the contract between the browser’s native WebRTC and @gjsify/webrtc — every method it uses must hold up identically in both runtimes.
Why this showcase is non-trivial
Section titled “Why this showcase is non-trivial”WebRTC is the second-broadest API surface in the Web platform (after the DOM itself). To match it on GJS, @gjsify/webrtc has to:
- Translate every
RTCSessionDescription/RTCIceCandidateJSON object into the GstWebRTC equivalent + back - Bridge GStreamer’s streaming threads to the GLib main context so JS callbacks fire safely
- Wrap
RTCDataChannel’s'open'/'close'/'message'events without dropping any (GstWebRTCDataChannel fires from yet another thread) - Keep the
Gst.Promise.new_with_change_funclifecycle aligned with the JSPromiselifecycle so SDP / ICE async calls don’t leak
If any of those layers misfires, the channel never reaches 'open' and the showcase hangs at “Connecting…”. A successful round-trip echo is the end-to-end proof that the full stack holds.
Related
Section titled “Related”refs/node-gst-webrtc/— the GStreamer-WebRTC reference implrefs/webrtc-samples/— MDN / Google’s reference samples for browser-side behavior@gjsify/webrtc-native— the Vala bridge package