rtc-link
Version:
A library for managing WebRTC connections with multiplexed streams, anti-glare handling, and streamlined data channel setup.
113 lines (101 loc) • 4.28 kB
JavaScript
import {asPlex} from "rxprotoplex";
import {createStreamSwitcher} from "stream-switcher";
import {fromEvent, Subject} from "rxjs";
import {closeRTCListeners} from "./closeRTCListeners.js";
import {iceCandidateHandling} from "./iceCandidateHandling.js";
import {makeOffer} from "./makeOffer.js";
import {listenForOffer} from "./listenForOffer.js";
import {duplexFromRtcDataChannel} from "./duplexFromRtcDataChannel.js";
/**
* Establishes a WebRTC data channel stream over a given transport stream, handling peer connection
* creation, offer/answer exchange, and ICE candidate management with anti-glare protection.
*
* @param {boolean} isInitiator - Indicates if the current peer is the initiator of the WebRTC connection.
* @param {Stream} stream - The transport stream to establish WebRTC signaling over.
* @param {Object} [config={}] - Optional configuration object for the WebRTC connection.
* @param {Array<Object>} [config.iceServers=[{ urls: 'stun:stun.l.google.com:19302' }]] - ICE servers used by WebRTC for NAT traversal.
*
* @returns {Object} - A data channel that acts as a stream switcher, enabling duplex communication
* over the WebRTC data channel.
*
* @description
* The `establishWrtcStream` function sets up a WebRTC peer-to-peer connection using the provided transport
* stream for signaling. It configures the connection as an initiator or responder based on the `isInitiator`
* parameter and establishes a data channel for bidirectional communication. The connection listens for
* WebRTC offers, answers, and ICE candidates, allowing peers to establish and maintain a connection
* even behind NATs.
*
* - **Anti-Glare Mechanism**:
* - Prevents two initiators from conflicting when both peers attempt to initiate a connection.
* - Ensures only one peer continues with the connection setup if both try to send an offer simultaneously.
*
* - **Event Handling**:
* - Automatically regenerates the peer connection and data channel on errors or disconnects.
* - Listens for incoming offers and ICE candidates from the signaling channel.
*
* - **Error Handling**: Emits any errors on the `dataChannel`.
*
* @example
* const isInitiator = true;
* const config = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] };
* const wrtcStream = establishWrtcStream(isInitiator, myStream, config);
*
* @function
* @private
*/
const establishWrtcStream = (isInitiator, stream, config = {}) => {
config.iceServers ||= [{urls: 'stun:stun.l.google.com:19302'}];
const plex = asPlex(stream);
const dataChannel = createStreamSwitcher();
let close$ = new Subject();
let rtc;
let closed = false;
makeNewPeer(isInitiator);
dataChannel.once("close", () => {
if (rtc) {
closeRTCListeners(rtc);
rtc.close();
rtc = undefined;
closed = true;
}
});
return dataChannel;
function onError(e) {
dataChannel.emit("error", e);
}
function makeNewPeer(isInitiator) {
if (closed) {
throw new Error("Cannot make new peer because stream is closed.");
}
if (rtc) {
rtc.close();
closeRTCListeners(rtc);
}
close$.next?.();
close$.complete?.();
close$ = new Subject();
fromEvent(stream, "close").subscribe(o => close$.next(o));
rtc = new RTCPeerConnection(config);
rtc.makingOffer = false;
rtc.localFingerprint = null; // Store fingerprint on rtc
createDataChannel(rtc, isInitiator);
iceCandidateHandling(plex, rtc, close$);
// Initiator tries to make an offer but also listens for incoming offers
if (isInitiator) {
makeOffer(plex, rtc, close$).catch(onError);
}
// Always listen for incoming offers, even if we start by making an offer
listenForOffer(plex, rtc, close$, makeNewPeer).catch(onError);
}
function createDataChannel(rtc, isInitiator) {
const stream = duplexFromRtcDataChannel(
isInitiator,
rtc.createDataChannel("wire", {
negotiated: true,
id: 0
})
);
dataChannel.switch(stream);
}
}
export {establishWrtcStream};