@sigx/lynx-webrtc
v0.30.0
Published
W3C-shaped WebRTC for sigx-lynx: peer connections, audio tracks, data channels
Maintainers
Readme
@sigx/lynx-webrtc
W3C-shaped WebRTC for sigx-lynx: peer connections, microphone audio tracks, and data channels, backed by libwebrtc (webrtc-sdk builds). Code written against browser WebRTC ports nearly unchanged.
Platforms: Android + iOS. Not available on the web runtime (the background thread has no
RTCPeerConnection) — gate withisWebRTCAvailable().
📚 Documentation
Full guides, API reference and live examples → https://sigx.dev/lynx/modules/webrtc/overview/
Install
pnpm add @sigx/lynx-webrtc
sigx prebuildAutolinking adds the native SDK dependency (gradle dep / WebRTC-SDK pod),
the Android RECORD_AUDIO / MODIFY_AUDIO_SETTINGS permissions, the iOS
microphone usage description, and the iOS audio background mode (so calls
survive backgrounding). Note the native SDK adds roughly 5–10 MB per ABI on
Android and ~10 MB on iOS.
Usage
import { RTCPeerConnection, mediaDevices } from '@sigx/lynx-webrtc';
// 1. Capture the mic (shows the OS permission prompt when needed).
const stream = await mediaDevices.getUserMedia({ audio: true });
const [micTrack] = stream.getAudioTracks();
// 2. Wire up a peer connection.
const peer = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.example.com' }] });
peer.addTrack(micTrack, stream);
const events = peer.createDataChannel('events');
events.onmessage = e => console.log('peer says', e.data);
// 3. Negotiate. For an HTTP (non-trickle) SDP exchange, wait for ICE
// gathering to complete so localDescription carries every candidate.
const offer = await peer.createOffer();
await peer.setLocalDescription(offer);
await new Promise<void>(resolve => {
if (peer.iceGatheringState === 'complete') return resolve();
peer.onicegatheringstatechange = () => {
if (peer.iceGatheringState === 'complete') resolve();
};
});
const answerSdp = await postOfferSomewhere(peer.localDescription!.sdp);
await peer.setRemoteDescription({ type: 'answer', sdp: answerSdp });
// 4. Remote audio plays automatically — no element to attach.
peer.ontrack = e => console.log('remote track', e.track.id);
// Mute / hang up.
micTrack.enabled = false;
peer.close();
micTrack.stop();API
mediaDevices.getUserMedia(constraints)
Audio only ({ audio: true }). Rejects with NotAllowedError when the
microphone permission is denied and NotSupportedError for video constraints.
Errors
Failures throw a SigxError (from @sigx/lynx-core) whose name is the
DOMException name a browser would use — InvalidStateError,
InvalidAccessError, NotAllowedError, NotSupportedError, or
OperationError for a native failure with no W3C equivalent. The same string
is the machine-readable code, so both of these work:
import { isSigxError } from '@sigx/lynx-core';
try {
await peer.setRemoteDescription(answer);
} catch (e) {
if ((e as Error).name === 'InvalidStateError') { /* ported browser code */ }
if (isSigxError(e) && e.code === 'InvalidStateError') { /* sigx code */ }
}Messages are scoped — [@sigx/lynx-webrtc] <action> failed: <cause> — and
carry the native cause; branch on name/code, never on the message. Bad
arguments still throw a plain TypeError, as the W3C algorithms specify.
RTCPeerConnection
| Member | Notes |
|---|---|
| new RTCPeerConnection(config?) | config.iceServers supported (urls string or array, username, credential) |
| createOffer(options?) / createAnswer() | resolves { type, sdp } |
| setLocalDescription(desc?) / setRemoteDescription(desc) | no-arg implicit form supported |
| addIceCandidate(candidate?) | null/omitted = end-of-candidates |
| addTrack(track, ...streams) → RTCRtpSender | minimal sender ({ track }) |
| removeTrack(sender) | |
| createDataChannel(label, init?) | synchronous, like W3C |
| close() | also ends remote tracks and closes child channels; final — no further state events fire, including from calls still in flight |
| connectionState / iceConnectionState / iceGatheringState / signalingState | sync getters |
| localDescription / remoteDescription | localDescription is refreshed on every icegatheringstatechange, so it carries the full SDP once gathering completes |
| onconnectionstatechange / oniceconnectionstatechange / onicegatheringstatechange / onsignalingstatechange | |
| onicecandidate | ev.candidate is null at end-of-candidates |
| ontrack | ev.track, ev.streams (no receiver/transceiver) |
| ondatachannel | ev.channel |
RTCDataChannel
label, ordered, protocol, id (SCTP stream id, null until open),
readyState, bufferedAmount (write-through approximation),
binaryType: 'arraybuffer', send(string | ArrayBuffer | view), close(),
onopen / onmessage / onclose / onerror.
MediaStreamTrack / MediaStream
track.enabled = false mutes without stopping capture; track.stop() releases
the capturer. stream.getAudioTracks() etc. are JS-side bookkeeping; the
stream's id is what appears as the msid in SDP.
Extras (non-W3C)
import { WebRTC, isWebRTCAvailable } from '@sigx/lynx-webrtc';
import type { PermissionResponse } from '@sigx/lynx-webrtc';
await WebRTC.setAudioOutput('earpiece'); // default route is 'speaker'
const { status } = await WebRTC.requestPermission(); // pre-prompt explainer flows
await WebRTC.getPermissionStatus();
isWebRTCAvailable(); // false on web / when not linkedBoth permission methods resolve the shared
PermissionResponse — { status: 'granted' | 'denied' | 'undetermined' |
'blocked', canAskAgain: boolean } — re-exported from this barrel so your own
permission state types without a second import from @sigx/lynx-core.
Deviations from W3C
- Remote audio tracks render automatically through the platform audio device —
there is no
<audio>element. Guard any element-attach code with a platform check when sharing a store with web. localDescription/remoteDescriptionreturn plain frozen{ type, sdp }objects withtoJSON(); there are noRTCSessionDescription/RTCIceCandidateconstructors (init dictionaries are accepted everywhere).addTrackreturns a minimal sender — noreplaceTrack/getParameters.- No
getStats(), transceivers, video tracks, or renderer views in v1.
Web
Not supported on web. This package ships no .web.ts shim and
@sigx/lynx-web-host has no sigx.webrtc.* handler, so the WebRTC module is
not registered in a sigx run:web build: isWebRTCAvailable() returns false,
and mediaDevices.getUserMedia() / new RTCPeerConnection() throw the
"module not available" error — gate on isWebRTCAvailable() before touching
either. The browser's own RTCPeerConnection is a page API while the app's
background code runs in a Web Worker, so a web implementation would have to
drive it over the @sigx/lynx-web-host bridge; nothing does that today.
Platform notes
- Call audio session: while at least one peer connection is live, the
module holds the platform in call mode (Android
MODE_IN_COMMUNICATION; iOSAVAudioSession.playAndRecord/.voiceChatvia WebRTC's manual-audio mode) and routes output to the loudspeaker by default; the previous state is restored when the last peer closes. On iOS the configuration is re-asserted after route changes and interruptions. - Backgrounding (iOS): the manifest contributes the
audioUIBackgroundModesentry, so an active call keeps streaming when the app is backgrounded. - Bluetooth headsets: Android SCO routing is not wired up yet — calls
use the built-in speaker or earpiece (iOS allows Bluetooth via
.allowBluetooth). @sigx/lynx-audiointerplay: avoid starting/stopping its recorders or players during an active call; both packages manage the audio session.- iOS simulator: WebRTC's audio unit is unreliable there — SDP and data channels work, but verify audio on a physical device.
- Android R8: the manifest contributes
-keep class org.webrtc.** { *; }viaandroid.proguardRules, whichsigx prebuildwrites intoapp/proguard-rules-generated.pro. It is not optional: theio.github.webrtc-sdkAAR ships no consumer proguard rules, and libwebrtc resolves its Java surface from native by exact name — includingorg.webrtc.WebRtcClassLoader, which has no Java callers at all. Without the rule R8 deletes it and the library aborts the process insideJNI_OnLoadon the firstgetUserMedia. Release builds only; debug is unminified.
License
MIT
