Skip to content

WebRTC Loopback

Two RTCPeerConnection instances in a single process exchange an SDP offer and answer, trickle ICE candidates at each other, open an RTCDataChannel, and echo a string and an ArrayBuffer back and forth. No signaling server and no remote peer.

The W3C WebRTC API works on GJS, backed by GStreamer’s webrtcbin. If your app needs peer-to-peer data or media, you write the code you would write for a browser.

Press Restart to run the handshake again from scratch.

WebRTC on GJS needs GStreamer 1.20 or newer with the WebRTC plugin and libnice:

Terminal
# Fedora
sudo dnf install gstreamer1-plugins-bad-free gstreamer1-plugins-bad-free-extras libnice-gstreamer1
# Ubuntu / Debian
sudo apt install gstreamer1.0-plugins-bad gstreamer1.0-nice

Check that both are found:

Terminal
gst-inspect-1.0 webrtcbin
gst-inspect-1.0 nicesrc
gjsify system-check # reports GJS plus the optional GStreamer pieces

This is one of two showcases that declare ["gjs"] rather than all four runtimes. Its native side is a GStreamer pipeline reached through a typelib, so it ships no --app node bundle and --runtime node tells you so instead of crashing:

Terminal
gjsify showcase webrtc-loopback

This one is a console program rather than a window. It prints the handshake as it happens and exits when the round trip completes:

Code
[A] createOffer
[A] signalingState → have-local-offer
[B] signalingState → have-remote-offer
[B] createAnswer
[B] signalingState → stable
[A] iceConnectionState → checking
[A] iceConnectionState → connected
[B] ondatachannel "chat"
[A] received: echo: hello from peer A
[A] received ArrayBuffer(4): [4, 3, 2, 1]

If it stops at “checking” and never reaches “connected”, one of the two GStreamer plugins above is missing.

TypeScript
import { mount } from '@gjsify/example-dom-webrtc-loopback/browser';
const handle = mount(document.getElementById('app')!, {
title: 'WebRTC Data-Channel Loopback',
});
await handle.restart();

mount() renders a heading, a log and a Restart button, then runs the same runLoopback(log) against the browser’s own WebRTC stack. Comparing the two logs line by line is how you check that the GJS side behaves the same.

PackageWhat it needs
@gjsify/webrtcRTCPeerConnection, createOffer, createAnswer, setLocalDescription, setRemoteDescription, addIceCandidate, and RTCDataChannel with string and ArrayBuffer payloads
@gjsify/webrtc-nativeThe Vala bridge that moves webrtcbin’s callbacks off GStreamer’s streaming threads onto the GLib main context, so your handlers run on the main loop
gst-1.0, gst-webrtc-1.0, libniceDTLS, SCTP and ICE, the actual WebRTC implementation

The shared demo module imports nothing from @gjsify/*. It reaches for RTCPeerConnection, RTCSessionDescription and RTCIceCandidate as globals, exactly as browser code does, and the build supplies them.

showcases/dom/webrtc-loopback/

  • src/loopback-demo.ts is the whole handshake and echo, behind one runLoopback(log) function.
  • src/gjs/gjs.ts runs it on a GLib.MainLoop and prints to stdout.
  • src/browser/browser.ts exports mount(); browser-main.ts runs it full-page.

The GJS entry is a useful template on its own: WebRTC needs no GTK window, only a running main loop.

  • webrtc-video is the media half of the same stack, with getUserMedia and a GTK video sink.
  • Web API packages lists what @gjsify/webrtc covers and which GNOME libraries back it.