timing-provider
Version:
An implementation of the timing provider specification.
308 lines • 14 kB
JavaScript
import { EMPTY, Observable, Subject, concat, count, defer, finalize, from, ignoreElements, iif, interval, merge, mergeMap, of, retry, switchMap, take, takeUntil, tap, throwError, timer, zip } from 'rxjs';
import { inexorably } from 'rxjs-etc/operators';
import { on } from 'subscribable-things';
import { createBackoff } from '../functions/create-backoff';
import { echo } from './echo';
import { ignoreLateResult } from './ignore-late-result';
export const negotiateDataChannels = (createPeerConnection, sendSignalingEvent) => (source) => source.pipe(mergeMap(([clientId, isActive, observable]) => new Observable((observer) => {
const errorEvents = [];
const errorSubject = new Subject();
const receivedCandidates = [];
const resetSubject = new Subject();
const createAndSendOffer = () => {
isFresh = false;
return ignoreLateResult(peerConnection.setLocalDescription()).pipe(tap(() => {
const { localDescription } = peerConnection;
if (localDescription === null) {
throw new Error('The local description is not set.');
}
sendSignalingEvent({
...jsonifyDescription(localDescription),
client: { id: clientId },
version
});
}));
};
const subscribeToCandidates = () => on(peerConnection, 'icecandidate')(({ candidate }) => {
if (candidate === null) {
sendSignalingEvent({
client: { id: clientId },
numberOfGatheredCandidates,
type: 'summary',
version
});
}
else if (candidate.port !== 9 && candidate.protocol !== 'tcp') {
sendSignalingEvent({
...candidate.toJSON(),
client: { id: clientId },
type: 'candidate',
version
});
numberOfGatheredCandidates += 1;
}
});
const subscribeToDataChannels = () => {
const subscriptions = [
zip([on(reliableDataChannel, 'open'), on(unreliableDataChannel, 'open')])
.pipe(take(1))
.subscribe(() => {
send = (event) => {
const dataChannel = event.type === 'update' ? reliableDataChannel : unreliableDataChannel;
try {
dataChannel.send(JSON.stringify(event));
}
catch (err) {
errorSubject.next(err);
return false;
}
return true;
};
observer.next([clientId, send]);
}),
merge(...[reliableDataChannel, unreliableDataChannel]
.map((dataChannel) => ['close', 'closing', 'error'].map((type) => on(dataChannel, type)))
.flat()).subscribe(({ type }) => errorSubject.next(new Error(`RTCDataChannel fired unexpected event of type "${type}".`)))
];
const unsubscribeFunctions = [
() => subscriptions.forEach((subscription) => subscription.unsubscribe()),
on(reliableDataChannel, 'message')(({ data }) => {
const event = JSON.parse(data);
observer.next([clientId, event]);
}),
on(unreliableDataChannel, 'message')(({ data, timeStamp }) => {
const event = {
...JSON.parse(data),
timestamp: timeStamp !== null && timeStamp !== void 0 ? timeStamp : performance.now()
};
observer.next([clientId, event]);
})
];
return () => unsubscribeFunctions.forEach((unsubscribeFunction) => unsubscribeFunction());
};
const [getBackoff, incrementBackoff] = createBackoff(1);
const subscribeToPeerConnection = () => {
const subscription = merge(on(peerConnection, 'icecandidate'), on(peerConnection, 'icegatheringstatechange'))
.pipe(switchMap(() => iif(() => peerConnection.iceGatheringState === 'gathering', defer(() => timer(10000 * getBackoff())), EMPTY)))
.subscribe(() => {
incrementBackoff();
errorSubject.next(new Error('RTCPeerConnection seems to be stuck at iceGatheringState "gathering".'));
});
const unsubscribeFunctions = [
() => subscription.unsubscribe(),
on(peerConnection, 'connectionstatechange')(() => {
const connectionState = peerConnection.connectionState;
if (['closed', 'disconnected', 'failed'].includes(connectionState)) {
errorSubject.next(new Error(`RTCPeerConnection transitioned to unexpected connectionState "${connectionState}".`));
}
}),
on(peerConnection, 'icecandidateerror')(({ address, errorCode, errorText, port, url }) => sendSignalingEvent({
address,
errorCode,
errorText,
port,
type: 'icecandidateerror',
url
})),
on(peerConnection, 'iceconnectionstatechange')(() => {
const iceConnectionState = peerConnection.iceConnectionState;
if (['closed', 'disconnected', 'failed'].includes(iceConnectionState)) {
errorSubject.next(new Error(`RTCPeerConnection transitioned to unexpected iceConnectionState "${iceConnectionState}".`));
}
}),
on(peerConnection, 'signalingstatechange')(() => {
if (peerConnection.signalingState === 'closed') {
errorSubject.next(new Error(`RTCPeerConnection transitioned to unexpected signalingState "closed".`));
}
})
];
return () => unsubscribeFunctions.forEach((unsubscribeFunction) => unsubscribeFunction());
};
const resetState = (newVersion) => {
resetSubject.next(null);
unsubscribeFromCandidates();
unsubscribeFromDataChannels();
unsubscribeFromPeerConnection();
reliableDataChannel.close();
unreliableDataChannel.close();
peerConnection.close();
if (send !== null) {
observer.next([clientId, true]);
}
isFresh = true;
numberOfAppliedCandidates = 0;
numberOfExpectedCandidates = version === newVersion ? numberOfExpectedCandidates : Infinity;
numberOfGatheredCandidates = 0;
peerConnection = createPeerConnection();
receivedCandidates.length = version === newVersion ? receivedCandidates.length : 0;
reliableDataChannel = peerConnection.createDataChannel('', { id: 0, negotiated: true, ordered: true });
send = null;
unreliableDataChannel = peerConnection.createDataChannel('', {
id: 1,
maxRetransmits: 0,
negotiated: true,
ordered: false
});
unsubscribeFromCandidates = subscribeToCandidates();
unsubscribeFromDataChannels = subscribeToDataChannels();
unsubscribeFromPeerConnection = subscribeToPeerConnection();
version = newVersion;
};
let isFresh = true;
let numberOfAppliedCandidates = 0;
let numberOfExpectedCandidates = Infinity;
let numberOfGatheredCandidates = 0;
let peerConnection = createPeerConnection();
let reliableDataChannel = peerConnection.createDataChannel('', { id: 0, negotiated: true, ordered: true });
let send = null;
let unrecoverableError = null;
let unreliableDataChannel = peerConnection.createDataChannel('', {
id: 1,
maxRetransmits: 0,
negotiated: true,
ordered: false
});
let unsubscribeFromCandidates = subscribeToCandidates();
let unsubscribeFromDataChannels = subscribeToDataChannels();
let unsubscribeFromPeerConnection = subscribeToPeerConnection();
let version = 0;
const addFinalCandidate = async (numberOfNewlyAppliedCandidates) => {
numberOfAppliedCandidates += numberOfNewlyAppliedCandidates;
if (numberOfAppliedCandidates === numberOfExpectedCandidates) {
await peerConnection.addIceCandidate();
}
};
const jsonifyDescription = (description) => (description instanceof RTCSessionDescription ? description.toJSON() : description);
const processEvent = (event) => {
const { type } = event;
if (type === 'answer' && isActive) {
if (version > event.version) {
return EMPTY;
}
if (version === event.version && !isFresh) {
return ignoreLateResult(peerConnection.setRemoteDescription(event)).pipe(mergeMap(() => from(receivedCandidates)), mergeMap((receivedCandidate) => ignoreLateResult(peerConnection.addIceCandidate(receivedCandidate))), count(), mergeMap((numberOfNewlyAppliedCandidates) => ignoreLateResult(addFinalCandidate(numberOfNewlyAppliedCandidates))));
}
}
if (type === 'candidate') {
if (version > event.version) {
return EMPTY;
}
if (version < event.version && !isActive) {
resetState(event.version);
}
if (version === event.version) {
if (peerConnection.remoteDescription === null) {
receivedCandidates.push(event);
return EMPTY;
}
return ignoreLateResult(peerConnection.addIceCandidate(event)).pipe(mergeMap(() => ignoreLateResult(addFinalCandidate(1))));
}
}
if (type === 'error' && isActive) {
if (version > event.version) {
return EMPTY;
}
resetState(event.version + 1);
return createAndSendOffer();
}
if (type === 'notice' && !isActive) {
return EMPTY;
}
if (type === 'offer' && !isActive) {
if (version > event.version) {
return EMPTY;
}
if (version < event.version) {
resetState(event.version);
}
isFresh = false;
return ignoreLateResult(peerConnection.setRemoteDescription(event)).pipe(mergeMap(() => merge(ignoreLateResult(peerConnection.setLocalDescription()).pipe(tap(() => {
const { localDescription } = peerConnection;
if (localDescription === null) {
throw new Error('The local description is not set.');
}
sendSignalingEvent({
...jsonifyDescription(localDescription),
client: { id: clientId },
version
});
})), from(receivedCandidates).pipe(mergeMap((receivedCandidate) => ignoreLateResult(peerConnection.addIceCandidate(receivedCandidate))), count(), mergeMap((numberOfNewlyAppliedCandidates) => ignoreLateResult(addFinalCandidate(numberOfNewlyAppliedCandidates)))))));
}
if (type === 'request' && isActive) {
if (version === 0 && isFresh) {
return createAndSendOffer();
}
return EMPTY;
}
if (type === 'summary') {
if (version > event.version) {
return EMPTY;
}
if (version < event.version && !isActive) {
resetState(event.version);
}
if (version === event.version) {
numberOfExpectedCandidates = event.numberOfGatheredCandidates;
return ignoreLateResult(addFinalCandidate(0));
}
}
unrecoverableError = new Error(`The current event of type "${type}" can't be processed.`);
// tslint:disable-next-line:rxjs-throw-error
return throwError(() => unrecoverableError);
};
observer.next([clientId, true]);
return merge(defer(() => from(errorEvents)),
// tslint:disable-next-line:rxjs-throw-error
errorSubject.pipe(mergeMap((err) => throwError(() => err))), observable.pipe(echo(() => sendSignalingEvent({
client: { id: clientId },
type: 'check'
}), () => reliableDataChannel.readyState !== 'open' || unreliableDataChannel.readyState !== 'open', interval(5000)), inexorably((notification) => {
if (notification !== undefined) {
errorSubject.complete();
}
})))
.pipe(mergeMap((event) => processEvent(event).pipe(takeUntil(resetSubject))), retry({
delay: (err) => {
if (err === unrecoverableError) {
// tslint:disable-next-line:rxjs-throw-error
return throwError(() => err);
}
errorEvents.length = 0;
if (isFresh) {
resetState(version);
}
else {
const errorEvent = {
client: { id: clientId },
message: err.message,
name: err.name,
type: 'error',
version
};
if (isActive) {
errorEvents.push(errorEvent);
}
else {
resetState(version + 1);
sendSignalingEvent(errorEvent);
}
}
return of(null);
}
}), takeUntil(concat(observable.pipe(ignoreElements()), of(null))), finalize(() => {
unsubscribeFromCandidates();
unsubscribeFromDataChannels();
unsubscribeFromPeerConnection();
reliableDataChannel.close();
unreliableDataChannel.close();
peerConnection.close();
}))
.subscribe({
complete: () => {
observer.next([clientId, false]);
observer.complete();
},
error: (err) => observer.error(err)
});
})));
//# sourceMappingURL=negotiate-data-channels.js.map