2018-06-11 13:08:30 +02:00
|
|
|
/*
|
2022-01-13 11:18:47 +01:00
|
|
|
* (C) Copyright 2017-2022 OpenVidu (https://openvidu.io)
|
2018-06-11 13:08:30 +02:00
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
2022-01-26 17:59:15 +01:00
|
|
|
import freeice = require('freeice');
|
2021-03-23 10:51:14 +01:00
|
|
|
import { v4 as uuidv4 } from 'uuid';
|
2022-01-27 14:42:01 +01:00
|
|
|
import { TypeOfVideo } from '../Enums/TypeOfVideo';
|
2021-03-22 19:23:03 +01:00
|
|
|
import { ExceptionEventName } from '../Events/ExceptionEvent';
|
2020-05-04 20:01:56 +02:00
|
|
|
import { OpenViduLogger } from '../Logger/OpenViduLogger';
|
2020-10-13 16:13:37 +02:00
|
|
|
import { PlatformUtils } from '../Utils/Platform';
|
|
|
|
|
2020-05-04 20:01:56 +02:00
|
|
|
/**
|
|
|
|
* @hidden
|
|
|
|
*/
|
|
|
|
const logger: OpenViduLogger = OpenViduLogger.getInstance();
|
2020-10-13 16:13:37 +02:00
|
|
|
/**
|
|
|
|
* @hidden
|
|
|
|
*/
|
2020-11-26 13:17:55 +01:00
|
|
|
let platform: PlatformUtils;
|
2020-05-04 20:01:56 +02:00
|
|
|
|
2018-06-11 13:08:30 +02:00
|
|
|
export interface WebRtcPeerConfiguration {
|
|
|
|
mediaConstraints: {
|
2022-01-28 14:03:26 +01:00
|
|
|
audio: boolean;
|
|
|
|
video: boolean;
|
2018-06-11 13:08:30 +02:00
|
|
|
};
|
|
|
|
simulcast: boolean;
|
2021-07-08 12:39:08 +02:00
|
|
|
mediaServer: string;
|
2021-05-27 21:28:17 +02:00
|
|
|
onIceCandidate: (event: RTCIceCandidate) => void;
|
|
|
|
onIceConnectionStateException: (exceptionName: ExceptionEventName, message: string, data?: any) => void;
|
2021-06-11 17:33:35 +02:00
|
|
|
iceServers?: RTCIceServer[];
|
|
|
|
mediaStream?: MediaStream | null;
|
2018-11-21 12:03:14 +01:00
|
|
|
mode?: 'sendonly' | 'recvonly' | 'sendrecv';
|
2018-06-11 13:08:30 +02:00
|
|
|
id?: string;
|
2022-01-28 14:03:26 +01:00
|
|
|
typeOfVideo: TypeOfVideo | undefined;
|
2018-06-11 13:08:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export class WebRtcPeer {
|
|
|
|
pc: RTCPeerConnection;
|
2018-06-19 09:52:04 +02:00
|
|
|
remoteCandidatesQueue: RTCIceCandidate[] = [];
|
|
|
|
localCandidatesQueue: RTCIceCandidate[] = [];
|
2018-06-11 13:08:30 +02:00
|
|
|
|
2021-06-11 17:33:35 +02:00
|
|
|
// Same as WebRtcPeerConfiguration but without optional fields.
|
|
|
|
protected configuration: Required<WebRtcPeerConfiguration>;
|
|
|
|
|
|
|
|
private iceCandidateList: RTCIceCandidate[] = [];
|
|
|
|
private candidategatheringdone = false;
|
2018-06-19 14:32:23 +02:00
|
|
|
|
2021-06-11 17:33:35 +02:00
|
|
|
constructor(configuration: WebRtcPeerConfiguration) {
|
2020-11-26 13:17:55 +01:00
|
|
|
platform = PlatformUtils.getInstance();
|
2021-06-11 17:33:35 +02:00
|
|
|
|
|
|
|
this.configuration = {
|
|
|
|
...configuration,
|
|
|
|
iceServers:
|
|
|
|
!!configuration.iceServers &&
|
2021-06-28 11:37:00 +02:00
|
|
|
configuration.iceServers.length > 0
|
2021-06-11 17:33:35 +02:00
|
|
|
? configuration.iceServers
|
|
|
|
: freeice(),
|
|
|
|
mediaStream:
|
|
|
|
configuration.mediaStream !== undefined
|
|
|
|
? configuration.mediaStream
|
|
|
|
: null,
|
|
|
|
mode: !!configuration.mode ? configuration.mode : "sendrecv",
|
|
|
|
id: !!configuration.id ? configuration.id : this.generateUniqueId(),
|
|
|
|
};
|
2022-01-28 14:03:26 +01:00
|
|
|
// prettier-ignore
|
|
|
|
logger.debug(`[WebRtcPeer] configuration:\n${JSON.stringify(this.configuration, null, 2)}`);
|
2018-06-11 13:08:30 +02:00
|
|
|
|
|
|
|
this.pc = new RTCPeerConnection({ iceServers: this.configuration.iceServers });
|
|
|
|
|
2021-03-22 11:46:52 +01:00
|
|
|
this.pc.addEventListener('icecandidate', (event: RTCPeerConnectionIceEvent) => {
|
2021-03-16 10:26:39 +01:00
|
|
|
if (event.candidate != null) {
|
2018-08-31 15:07:34 +02:00
|
|
|
const candidate: RTCIceCandidate = event.candidate;
|
2021-05-27 21:28:17 +02:00
|
|
|
this.configuration.onIceCandidate(candidate);
|
2021-03-16 10:26:39 +01:00
|
|
|
if (candidate.candidate !== '') {
|
2018-08-31 15:07:34 +02:00
|
|
|
this.localCandidatesQueue.push(<RTCIceCandidate>{ candidate: candidate.candidate });
|
|
|
|
}
|
2018-06-11 13:08:30 +02:00
|
|
|
}
|
2021-03-22 11:46:52 +01:00
|
|
|
});
|
2018-06-11 13:08:30 +02:00
|
|
|
|
2021-03-22 11:46:52 +01:00
|
|
|
this.pc.addEventListener('signalingstatechange', () => {
|
2018-06-11 13:08:30 +02:00
|
|
|
if (this.pc.signalingState === 'stable') {
|
2021-11-05 15:50:13 +01:00
|
|
|
// SDP Offer/Answer finished. Add stored remote candidates.
|
2018-06-19 14:32:23 +02:00
|
|
|
while (this.iceCandidateList.length > 0) {
|
2020-06-30 10:48:55 +02:00
|
|
|
let candidate = this.iceCandidateList.shift();
|
|
|
|
this.pc.addIceCandidate(<RTCIceCandidate>candidate);
|
2018-06-11 13:08:30 +02:00
|
|
|
}
|
|
|
|
}
|
2021-03-22 11:46:52 +01:00
|
|
|
});
|
2018-06-11 13:08:30 +02:00
|
|
|
}
|
|
|
|
|
2021-06-11 17:33:35 +02:00
|
|
|
getId(): string {
|
|
|
|
return this.configuration.id;
|
2018-06-11 13:08:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This method frees the resources used by WebRtcPeer
|
|
|
|
*/
|
2020-02-14 20:51:52 +01:00
|
|
|
dispose() {
|
2020-05-04 20:01:56 +02:00
|
|
|
logger.debug('Disposing WebRtcPeer');
|
2020-02-14 20:51:52 +01:00
|
|
|
if (this.pc) {
|
|
|
|
if (this.pc.signalingState === 'closed') {
|
|
|
|
return;
|
2018-06-11 13:08:30 +02:00
|
|
|
}
|
2020-02-14 20:51:52 +01:00
|
|
|
this.pc.close();
|
|
|
|
this.remoteCandidatesQueue = [];
|
|
|
|
this.localCandidatesQueue = [];
|
2018-06-11 13:08:30 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-03 16:28:14 +01:00
|
|
|
// DEPRECATED LEGACY METHOD: Old WebRTC versions don't implement
|
|
|
|
// Transceivers, and instead depend on the deprecated
|
|
|
|
// "offerToReceiveAudio" and "offerToReceiveVideo".
|
|
|
|
private createOfferLegacy(): Promise<RTCSessionDescriptionInit> {
|
|
|
|
if (!!this.configuration.mediaStream) {
|
|
|
|
this.deprecatedPeerConnectionTrackApi();
|
|
|
|
}
|
|
|
|
|
|
|
|
const hasAudio = this.configuration.mediaConstraints.audio;
|
|
|
|
const hasVideo = this.configuration.mediaConstraints.video;
|
|
|
|
|
|
|
|
const options: RTCOfferOptions = {
|
|
|
|
offerToReceiveAudio: this.configuration.mode !== "sendonly" && hasAudio,
|
|
|
|
offerToReceiveVideo: this.configuration.mode !== "sendonly" && hasVideo,
|
|
|
|
};
|
|
|
|
|
|
|
|
logger.debug("[createOfferLegacy] RTCPeerConnection.createOffer() options:", JSON.stringify(options));
|
|
|
|
|
|
|
|
return this.pc.createOffer(options);
|
|
|
|
}
|
|
|
|
|
2018-06-11 13:08:30 +02:00
|
|
|
/**
|
2022-02-03 16:28:14 +01:00
|
|
|
* Creates an SDP offer from the local RTCPeerConnection to send to the other peer.
|
|
|
|
* Only if the negotiation was initiated by this peer.
|
2018-06-11 13:08:30 +02:00
|
|
|
*/
|
2022-02-03 16:28:14 +01:00
|
|
|
async createOffer(): Promise<RTCSessionDescriptionInit> {
|
|
|
|
// TODO: Delete this conditional when all supported browsers are
|
|
|
|
// modern enough to implement the Transceiver methods.
|
|
|
|
if (!("addTransceiver" in this.pc)) {
|
|
|
|
logger.warn(
|
|
|
|
"[createOffer] Method RTCPeerConnection.addTransceiver() is NOT available; using LEGACY offerToReceive{Audio,Video}"
|
|
|
|
);
|
|
|
|
return this.createOfferLegacy();
|
|
|
|
} else {
|
|
|
|
logger.debug("[createOffer] Method RTCPeerConnection.addTransceiver() is available; using it");
|
|
|
|
}
|
2018-11-21 12:03:14 +01:00
|
|
|
|
2022-02-03 16:28:14 +01:00
|
|
|
// Spec doc: https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-addtransceiver
|
2022-01-28 14:03:26 +01:00
|
|
|
|
2022-02-03 16:28:14 +01:00
|
|
|
if (this.configuration.mode !== "recvonly") {
|
|
|
|
// To send media, assume that all desired media tracks have been
|
|
|
|
// already added by higher level code to our MediaStream.
|
2022-01-28 14:03:26 +01:00
|
|
|
|
2022-02-03 16:28:14 +01:00
|
|
|
if (!this.configuration.mediaStream) {
|
|
|
|
throw new Error(
|
|
|
|
`[WebRtcPeer.createOffer] Direction is '${this.configuration.mode}', but no stream was configured to be sent`
|
|
|
|
);
|
|
|
|
}
|
2021-06-30 19:24:23 +02:00
|
|
|
|
2022-02-03 16:28:14 +01:00
|
|
|
for (const track of this.configuration.mediaStream.getTracks()) {
|
|
|
|
const tcInit: RTCRtpTransceiverInit = {
|
|
|
|
direction: this.configuration.mode,
|
|
|
|
streams: [this.configuration.mediaStream],
|
|
|
|
};
|
2022-01-28 14:03:26 +01:00
|
|
|
|
2022-02-03 16:49:01 +01:00
|
|
|
if (track.kind === "video" && this.configuration.simulcast) {
|
|
|
|
// Check if the requested size is enough to ask for 3 layers.
|
|
|
|
const trackSettings = track.getSettings();
|
|
|
|
const trackConsts = track.getConstraints();
|
|
|
|
|
|
|
|
const trackWidth: number =
|
|
|
|
trackSettings.width ??
|
|
|
|
(trackConsts.width as ConstrainULongRange).ideal ??
|
|
|
|
(trackConsts.width as number) ??
|
|
|
|
0;
|
|
|
|
const trackHeight: number =
|
|
|
|
trackSettings.height ??
|
|
|
|
(trackConsts.height as ConstrainULongRange).ideal ??
|
|
|
|
(trackConsts.height as number) ??
|
|
|
|
0;
|
|
|
|
logger.info(`[createOffer] Video track dimensions: ${trackWidth}x${trackHeight}`);
|
|
|
|
|
|
|
|
const trackPixels = trackWidth * trackHeight;
|
|
|
|
let maxLayers = 0;
|
|
|
|
if (trackPixels >= 960 * 540) {
|
|
|
|
maxLayers = 3;
|
|
|
|
} else if (trackPixels >= 480 * 270) {
|
|
|
|
maxLayers = 2;
|
|
|
|
} else {
|
|
|
|
maxLayers = 1;
|
2022-02-03 16:28:14 +01:00
|
|
|
}
|
2022-01-28 14:03:26 +01:00
|
|
|
|
2022-02-03 16:49:01 +01:00
|
|
|
tcInit.sendEncodings = [];
|
|
|
|
for (let l = 0; l < maxLayers; l++) {
|
|
|
|
const layerDiv = 2 ** (maxLayers - l - 1);
|
2022-01-28 14:03:26 +01:00
|
|
|
|
2022-02-03 16:49:01 +01:00
|
|
|
const encoding: RTCRtpEncodingParameters = {
|
|
|
|
rid: "rDiv" + layerDiv.toString(),
|
2022-01-28 14:03:26 +01:00
|
|
|
|
2022-02-03 16:49:01 +01:00
|
|
|
// @ts-ignore -- Property missing from DOM types.
|
|
|
|
scalabilityMode: "L1T1",
|
|
|
|
};
|
2022-01-28 14:03:26 +01:00
|
|
|
|
2022-02-03 16:49:01 +01:00
|
|
|
if (["detail", "text"].includes(track.contentHint)) {
|
|
|
|
// Prioritize best resolution, for maximum picture detail.
|
|
|
|
encoding.scaleResolutionDownBy = 1.0;
|
2022-01-28 14:03:26 +01:00
|
|
|
|
2022-02-03 16:49:01 +01:00
|
|
|
// @ts-ignore -- Property missing from DOM types.
|
|
|
|
encoding.maxFramerate = Math.floor(30 / layerDiv);
|
|
|
|
} else {
|
|
|
|
encoding.scaleResolutionDownBy = layerDiv;
|
2021-06-11 17:33:35 +02:00
|
|
|
}
|
2022-02-03 16:49:01 +01:00
|
|
|
|
|
|
|
tcInit.sendEncodings.push(encoding);
|
2021-06-11 17:33:35 +02:00
|
|
|
}
|
2018-11-21 12:03:14 +01:00
|
|
|
}
|
|
|
|
|
2022-02-03 16:28:14 +01:00
|
|
|
const tc = this.pc.addTransceiver(track, tcInit);
|
2021-06-11 17:33:35 +02:00
|
|
|
|
2022-02-03 16:28:14 +01:00
|
|
|
if (track.kind === "video") {
|
|
|
|
let sendParams = tc.sender.getParameters();
|
|
|
|
let needSetParams = false;
|
2018-06-11 13:08:30 +02:00
|
|
|
|
2022-02-03 16:49:01 +01:00
|
|
|
if (!sendParams.degradationPreference?.length) {
|
|
|
|
// degradationPreference for video: "balanced", "maintain-framerate", "maintain-resolution".
|
|
|
|
// https://www.w3.org/TR/2018/CR-webrtc-20180927/#dom-rtcdegradationpreference
|
|
|
|
if (["detail", "text"].includes(track.contentHint)) {
|
2022-02-03 16:28:14 +01:00
|
|
|
sendParams.degradationPreference = "maintain-resolution";
|
|
|
|
} else {
|
|
|
|
sendParams.degradationPreference = "balanced";
|
|
|
|
}
|
2021-05-04 10:14:29 +02:00
|
|
|
|
2022-02-03 16:49:01 +01:00
|
|
|
logger.info(
|
2022-02-03 16:28:14 +01:00
|
|
|
`[createOffer] Video sender Degradation Preference set: ${sendParams.degradationPreference}`
|
|
|
|
);
|
|
|
|
|
|
|
|
// FIXME: Firefox implements degradationPreference on each individual encoding!
|
|
|
|
// (set it on every element of the sendParams.encodings array)
|
2021-05-04 10:14:29 +02:00
|
|
|
|
2022-02-03 16:28:14 +01:00
|
|
|
needSetParams = true;
|
|
|
|
}
|
2021-05-04 10:14:29 +02:00
|
|
|
|
2022-02-03 16:28:14 +01:00
|
|
|
// FIXME: Check that the simulcast encodings were applied.
|
|
|
|
// Firefox doesn't implement `RTCRtpTransceiverInit.sendEncodings`
|
|
|
|
// so the only way to enable simulcast is with `RTCRtpSender.setParameters()`.
|
|
|
|
//
|
|
|
|
// This next block can be deleted when Firefox fixes bug #1396918:
|
|
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1396918
|
|
|
|
//
|
|
|
|
// NOTE: This is done in a way that is compatible with all browsers, to save on
|
|
|
|
// browser-conditional code. The idea comes from WebRTC Adapter.js:
|
|
|
|
// * https://github.com/webrtcHacks/adapter/issues/998
|
|
|
|
// * https://github.com/webrtcHacks/adapter/blob/v7.7.0/src/js/firefox/firefox_shim.js#L231-L255
|
|
|
|
if (this.configuration.simulcast) {
|
|
|
|
if (
|
|
|
|
!("encodings" in sendParams) ||
|
|
|
|
sendParams.encodings.length !== tcInit.sendEncodings!.length
|
|
|
|
) {
|
|
|
|
sendParams.encodings = tcInit.sendEncodings!;
|
|
|
|
|
|
|
|
needSetParams = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (needSetParams) {
|
|
|
|
logger.debug(`[createOffer] Setting new RTCRtpSendParameters`);
|
|
|
|
await tc.sender.setParameters(sendParams);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// DEBUG: Uncomment for details.
|
|
|
|
// if (track.kind === "video" && this.configuration.simulcast) {
|
|
|
|
// // Print browser capabilities.
|
|
|
|
// // prettier-ignore
|
|
|
|
// logger.error(`[createOffer] Transceiver send capabilities (static):\n${JSON.stringify(RTCRtpSender.getCapabilities?.("video"), null, 2)}`);
|
|
|
|
// // prettier-ignore
|
|
|
|
// logger.error(`[createOffer] Transceiver recv capabilities (static):\n${JSON.stringify(RTCRtpReceiver.getCapabilities?.("video"), null, 2)}`);
|
|
|
|
|
|
|
|
// // Print requested Transceiver encodings and parameters.
|
|
|
|
// // prettier-ignore
|
|
|
|
// logger.error(`[createOffer] Transceiver send encodings (requested):\n${JSON.stringify(tcInit.sendEncodings, null, 2)}`);
|
|
|
|
// // prettier-ignore
|
|
|
|
// logger.error(`[createOffer] Transceiver send parameters (requested):\n${JSON.stringify(tc.sender.getParameters(), null, 2)}`);
|
|
|
|
// }
|
2019-05-10 10:36:10 +02:00
|
|
|
}
|
2022-02-03 16:28:14 +01:00
|
|
|
} else {
|
|
|
|
// To just receive media, create new recvonly transceivers.
|
|
|
|
for (const kind of ["audio", "video"]) {
|
|
|
|
// Check if the media kind should be used.
|
|
|
|
if (!this.configuration.mediaConstraints[kind]) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.configuration.mediaStream = new MediaStream();
|
|
|
|
this.pc.addTransceiver(kind, {
|
|
|
|
direction: this.configuration.mode,
|
|
|
|
streams: [this.configuration.mediaStream],
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.pc.createOffer();
|
2018-06-11 13:08:30 +02:00
|
|
|
}
|
|
|
|
|
2021-07-07 12:53:20 +02:00
|
|
|
deprecatedPeerConnectionTrackApi() {
|
|
|
|
for (const track of this.configuration.mediaStream!.getTracks()) {
|
|
|
|
this.pc.addTrack(track, this.configuration.mediaStream!);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-11 13:08:30 +02:00
|
|
|
/**
|
2021-03-16 10:26:39 +01:00
|
|
|
* Creates an SDP answer from the local RTCPeerConnection to send to the other peer
|
|
|
|
* Only if the negotiation was initiated by the other peer
|
|
|
|
*/
|
|
|
|
createAnswer(): Promise<RTCSessionDescriptionInit> {
|
|
|
|
return new Promise((resolve, reject) => {
|
2021-06-11 17:33:35 +02:00
|
|
|
// TODO: Delete this conditional when all supported browsers are
|
|
|
|
// modern enough to implement the Transceiver methods.
|
|
|
|
if ("getTransceivers" in this.pc) {
|
|
|
|
logger.debug("[createAnswer] Method RTCPeerConnection.getTransceivers() is available; using it");
|
|
|
|
|
|
|
|
// Ensure that the PeerConnection already contains one Transceiver
|
|
|
|
// for each kind of media.
|
|
|
|
// The Transceivers should have been already created internally by
|
|
|
|
// the PC itself, when `pc.setRemoteDescription(sdpOffer)` was called.
|
|
|
|
|
|
|
|
for (const kind of ["audio", "video"]) {
|
|
|
|
// Check if the media kind should be used.
|
|
|
|
if (!this.configuration.mediaConstraints[kind]) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
let tc = this.pc
|
|
|
|
.getTransceivers()
|
|
|
|
.find((tc) => tc.receiver.track.kind === kind);
|
|
|
|
|
|
|
|
if (tc) {
|
|
|
|
// Enforce our desired direction.
|
|
|
|
tc.direction = this.configuration.mode;
|
|
|
|
} else {
|
2022-01-26 12:17:31 +01:00
|
|
|
return reject(new Error(`${kind} requested, but no transceiver was created from remote description`));
|
2021-06-11 17:33:35 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.pc
|
|
|
|
.createAnswer()
|
|
|
|
.then((sdpAnswer) => resolve(sdpAnswer))
|
|
|
|
.catch((error) => reject(error));
|
2021-07-07 12:53:20 +02:00
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
// TODO: Delete else branch when all supported browsers are
|
|
|
|
// modern enough to implement the Transceiver methods
|
|
|
|
|
|
|
|
let offerAudio, offerVideo = true;
|
|
|
|
if (!!this.configuration.mediaConstraints) {
|
|
|
|
offerAudio = (typeof this.configuration.mediaConstraints.audio === 'boolean') ?
|
|
|
|
this.configuration.mediaConstraints.audio : true;
|
|
|
|
offerVideo = (typeof this.configuration.mediaConstraints.video === 'boolean') ?
|
|
|
|
this.configuration.mediaConstraints.video : true;
|
|
|
|
const constraints: RTCOfferOptions = {
|
|
|
|
offerToReceiveAudio: offerAudio,
|
|
|
|
offerToReceiveVideo: offerVideo
|
|
|
|
};
|
2022-01-26 12:17:31 +01:00
|
|
|
this.pc!.createAnswer(constraints)
|
|
|
|
.then(sdpAnswer => resolve(sdpAnswer))
|
|
|
|
.catch(error => reject(error));
|
2021-07-07 12:53:20 +02:00
|
|
|
}
|
|
|
|
|
2021-03-16 10:26:39 +01:00
|
|
|
}
|
2021-06-11 17:33:35 +02:00
|
|
|
|
|
|
|
// else, there is nothing to do; the legacy createAnswer() options do
|
|
|
|
// not offer any control over which tracks are included in the answer.
|
2021-03-16 10:26:39 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This peer initiated negotiation. Step 1/4 of SDP offer-answer protocol
|
2018-06-11 13:08:30 +02:00
|
|
|
*/
|
2021-03-16 10:26:39 +01:00
|
|
|
processLocalOffer(offer: RTCSessionDescriptionInit): Promise<void> {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
this.pc.setLocalDescription(offer)
|
|
|
|
.then(() => {
|
|
|
|
const localDescription = this.pc.localDescription;
|
|
|
|
if (!!localDescription) {
|
|
|
|
logger.debug('Local description set', localDescription.sdp);
|
2022-01-26 12:17:31 +01:00
|
|
|
return resolve();
|
2021-03-16 10:26:39 +01:00
|
|
|
} else {
|
2022-01-26 12:17:31 +01:00
|
|
|
return reject('Local description is not defined');
|
2021-03-16 10:26:39 +01:00
|
|
|
}
|
|
|
|
})
|
2022-01-26 12:17:31 +01:00
|
|
|
.catch(error => reject(error));
|
2021-03-16 10:26:39 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Other peer initiated negotiation. Step 2/4 of SDP offer-answer protocol
|
|
|
|
*/
|
|
|
|
processRemoteOffer(sdpOffer: string): Promise<void> {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const offer: RTCSessionDescriptionInit = {
|
|
|
|
type: 'offer',
|
|
|
|
sdp: sdpOffer
|
|
|
|
};
|
|
|
|
logger.debug('SDP offer received, setting remote description', offer);
|
|
|
|
|
|
|
|
if (this.pc.signalingState === 'closed') {
|
2022-01-26 12:17:31 +01:00
|
|
|
return reject('RTCPeerConnection is closed when trying to set remote description');
|
2021-03-16 10:26:39 +01:00
|
|
|
}
|
|
|
|
this.setRemoteDescription(offer)
|
2022-01-26 12:17:31 +01:00
|
|
|
.then(() => resolve())
|
|
|
|
.catch(error => reject(error));
|
2021-03-16 10:26:39 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Other peer initiated negotiation. Step 3/4 of SDP offer-answer protocol
|
|
|
|
*/
|
|
|
|
processLocalAnswer(answer: RTCSessionDescriptionInit): Promise<void> {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
logger.debug('SDP answer created, setting local description');
|
|
|
|
if (this.pc.signalingState === 'closed') {
|
2022-01-26 12:17:31 +01:00
|
|
|
return reject('RTCPeerConnection is closed when trying to set local description');
|
2021-03-16 10:26:39 +01:00
|
|
|
}
|
|
|
|
this.pc.setLocalDescription(answer)
|
|
|
|
.then(() => resolve())
|
|
|
|
.catch(error => reject(error));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This peer initiated negotiation. Step 4/4 of SDP offer-answer protocol
|
|
|
|
*/
|
|
|
|
processRemoteAnswer(sdpAnswer: string): Promise<void> {
|
2018-06-11 13:08:30 +02:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const answer: RTCSessionDescriptionInit = {
|
|
|
|
type: 'answer',
|
|
|
|
sdp: sdpAnswer
|
|
|
|
};
|
2020-05-04 20:01:56 +02:00
|
|
|
logger.debug('SDP answer received, setting remote description');
|
2018-06-11 13:08:30 +02:00
|
|
|
|
|
|
|
if (this.pc.signalingState === 'closed') {
|
2022-01-26 12:17:31 +01:00
|
|
|
return reject('RTCPeerConnection is closed when trying to set remote description');
|
2018-06-11 13:08:30 +02:00
|
|
|
}
|
2021-03-16 10:26:39 +01:00
|
|
|
this.setRemoteDescription(answer)
|
2022-01-28 14:03:26 +01:00
|
|
|
.then(() => {
|
|
|
|
// DEBUG: Uncomment for details.
|
|
|
|
// {
|
|
|
|
// const tc = this.pc.getTransceivers().find((tc) => tc.sender.track?.kind === "video");
|
|
|
|
// const sendParams = tc?.sender.getParameters();
|
|
|
|
// // prettier-ignore
|
|
|
|
// logger.error(`[processRemoteAnswer] Transceiver send parameters (effective):\n${JSON.stringify(sendParams, null, 2)}`);
|
|
|
|
// }
|
|
|
|
|
|
|
|
resolve();
|
|
|
|
})
|
|
|
|
.catch((error) => reject(error));
|
2020-06-30 10:48:55 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @hidden
|
|
|
|
*/
|
2021-03-16 10:26:39 +01:00
|
|
|
async setRemoteDescription(sdp: RTCSessionDescriptionInit): Promise<void> {
|
|
|
|
return this.pc.setRemoteDescription(sdp);
|
2018-06-11 13:08:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Callback function invoked when an ICE candidate is received
|
|
|
|
*/
|
|
|
|
addIceCandidate(iceCandidate: RTCIceCandidate): Promise<void> {
|
|
|
|
return new Promise((resolve, reject) => {
|
2020-05-04 20:01:56 +02:00
|
|
|
logger.debug('Remote ICE candidate received', iceCandidate);
|
2018-06-19 14:32:23 +02:00
|
|
|
this.remoteCandidatesQueue.push(iceCandidate);
|
2018-06-11 13:08:30 +02:00
|
|
|
switch (this.pc.signalingState) {
|
|
|
|
case 'closed':
|
|
|
|
reject(new Error('PeerConnection object is closed'));
|
|
|
|
break;
|
|
|
|
case 'stable':
|
|
|
|
if (!!this.pc.remoteDescription) {
|
2019-06-03 17:16:18 +02:00
|
|
|
this.pc.addIceCandidate(iceCandidate).then(() => resolve()).catch(error => reject(error));
|
2019-05-17 14:54:27 +02:00
|
|
|
} else {
|
|
|
|
this.iceCandidateList.push(iceCandidate);
|
|
|
|
resolve();
|
2018-06-11 13:08:30 +02:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
2018-06-19 14:32:23 +02:00
|
|
|
this.iceCandidateList.push(iceCandidate);
|
2018-06-11 13:08:30 +02:00
|
|
|
resolve();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-02-14 20:51:52 +01:00
|
|
|
addIceConnectionStateChangeListener(otherId: string) {
|
2021-03-22 11:46:52 +01:00
|
|
|
this.pc.addEventListener('iceconnectionstatechange', () => {
|
2020-02-14 20:51:52 +01:00
|
|
|
const iceConnectionState: RTCIceConnectionState = this.pc.iceConnectionState;
|
|
|
|
switch (iceConnectionState) {
|
|
|
|
case 'disconnected':
|
|
|
|
// Possible network disconnection
|
2021-06-11 17:33:35 +02:00
|
|
|
const msg1 = 'IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "disconnected". Possible network disconnection';
|
2021-05-17 13:30:08 +02:00
|
|
|
logger.warn(msg1);
|
2021-05-27 21:28:17 +02:00
|
|
|
this.configuration.onIceConnectionStateException(ExceptionEventName.ICE_CONNECTION_DISCONNECTED, msg1);
|
2020-02-14 20:51:52 +01:00
|
|
|
break;
|
|
|
|
case 'failed':
|
2021-06-11 17:33:35 +02:00
|
|
|
const msg2 = 'IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') to "failed"';
|
2021-05-17 13:30:08 +02:00
|
|
|
logger.error(msg2);
|
2021-05-27 21:28:17 +02:00
|
|
|
this.configuration.onIceConnectionStateException(ExceptionEventName.ICE_CONNECTION_FAILED, msg2);
|
2020-02-14 20:51:52 +01:00
|
|
|
break;
|
|
|
|
case 'closed':
|
2021-06-11 17:33:35 +02:00
|
|
|
logger.log('IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "closed"');
|
2020-02-14 20:51:52 +01:00
|
|
|
break;
|
|
|
|
case 'new':
|
2021-06-11 17:33:35 +02:00
|
|
|
logger.log('IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "new"');
|
2020-02-14 20:51:52 +01:00
|
|
|
break;
|
|
|
|
case 'checking':
|
2021-06-11 17:33:35 +02:00
|
|
|
logger.log('IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "checking"');
|
2020-02-14 20:51:52 +01:00
|
|
|
break;
|
|
|
|
case 'connected':
|
2021-06-11 17:33:35 +02:00
|
|
|
logger.log('IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "connected"');
|
2020-02-14 20:51:52 +01:00
|
|
|
break;
|
|
|
|
case 'completed':
|
2021-06-11 17:33:35 +02:00
|
|
|
logger.log('IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "completed"');
|
2020-02-14 20:51:52 +01:00
|
|
|
break;
|
|
|
|
}
|
2021-03-22 11:46:52 +01:00
|
|
|
});
|
2020-02-14 20:51:52 +01:00
|
|
|
}
|
|
|
|
|
2020-06-30 10:48:55 +02:00
|
|
|
/**
|
|
|
|
* @hidden
|
|
|
|
*/
|
|
|
|
generateUniqueId(): string {
|
2021-03-23 10:51:14 +01:00
|
|
|
return uuidv4();
|
2020-06-30 10:48:55 +02:00
|
|
|
}
|
|
|
|
|
2018-06-11 13:08:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export class WebRtcPeerRecvonly extends WebRtcPeer {
|
|
|
|
constructor(configuration: WebRtcPeerConfiguration) {
|
|
|
|
configuration.mode = 'recvonly';
|
|
|
|
super(configuration);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class WebRtcPeerSendonly extends WebRtcPeer {
|
|
|
|
constructor(configuration: WebRtcPeerConfiguration) {
|
|
|
|
configuration.mode = 'sendonly';
|
|
|
|
super(configuration);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class WebRtcPeerSendrecv extends WebRtcPeer {
|
|
|
|
constructor(configuration: WebRtcPeerConfiguration) {
|
|
|
|
configuration.mode = 'sendrecv';
|
|
|
|
super(configuration);
|
|
|
|
}
|
2021-06-30 19:24:23 +02:00
|
|
|
}
|