Skip to content

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.

PillarWhat it needs
@gjsify/webrtcRTCPeerConnection, createOffer / createAnswer / setLocalDescription / setRemoteDescription, ICE event firing, RTCDataChannel with send + onmessage
@gjsify/webrtc-nativeVala 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.0The actual WebRTC implementation (DTLS, SCTP, ICE) on GJS
Adwaita widgets (GJS)Adw.PreferencesGroup for the message log and input
gjsify showcase webrtc-loopback

A 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.

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.

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.

WebRTC is the second-broadest API surface in the Web platform (after the DOM itself). To match it on GJS, @gjsify/webrtc has to:

  1. Translate every RTCSessionDescription / RTCIceCandidate JSON object into the GstWebRTC equivalent + back
  2. Bridge GStreamer’s streaming threads to the GLib main context so JS callbacks fire safely
  3. Wrap RTCDataChannel’s 'open' / 'close' / 'message' events without dropping any (GstWebRTCDataChannel fires from yet another thread)
  4. Keep the Gst.Promise.new_with_change_func lifecycle aligned with the JS Promise lifecycle 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.