Merge branch 'master' of https://github.com/OpenVidu/openvidu into feature/custom-ice-servers

pull/698/head
cruizba 2022-02-16 18:12:03 +01:00
commit 7cc5e0c7d0
3 changed files with 256 additions and 242 deletions

View File

@ -27,6 +27,7 @@ import { OpenViduError, OpenViduErrorName } from '../OpenViduInternal/Enums/Open
import { VideoInsertMode } from '../OpenViduInternal/Enums/VideoInsertMode'; import { VideoInsertMode } from '../OpenViduInternal/Enums/VideoInsertMode';
import { OpenViduLogger } from '../OpenViduInternal/Logger/OpenViduLogger'; import { OpenViduLogger } from '../OpenViduInternal/Logger/OpenViduLogger';
import { PlatformUtils } from '../OpenViduInternal/Utils/Platform'; import { PlatformUtils } from '../OpenViduInternal/Utils/Platform';
import { TypeOfVideo } from '../OpenViduInternal/Enums/TypeOfVideo';
/** /**
* @hidden * @hidden
@ -398,6 +399,37 @@ export class Publisher extends StreamManager {
mediaStream.getVideoTracks()[0].enabled = enabled; mediaStream.getVideoTracks()[0].enabled = enabled;
} }
// Set Content Hint on all MediaStreamTracks
for (const track of mediaStream.getAudioTracks()) {
if (!track.contentHint?.length) {
// contentHint for audio: "", "speech", "speech-recognition", "music".
// https://w3c.github.io/mst-content-hint/#audio-content-hints
track.contentHint = "";
logger.info(`Audio track Content Hint set: '${track.contentHint}'`);
}
}
for (const track of mediaStream.getVideoTracks()) {
if (!track.contentHint?.length) {
// contentHint for video: "", "motion", "detail", "text".
// https://w3c.github.io/mst-content-hint/#video-content-hints
switch (this.stream.typeOfVideo) {
case TypeOfVideo.SCREEN:
track.contentHint = "detail";
break;
case TypeOfVideo.CUSTOM:
logger.warn("CUSTOM type video track was provided without Content Hint!");
track.contentHint = "motion";
break;
case TypeOfVideo.CAMERA:
case TypeOfVideo.IPCAM:
default:
track.contentHint = "motion";
break;
}
logger.info(`Video track Content Hint set: '${track.contentHint}'`);
}
}
this.initializeVideoReference(mediaStream); this.initializeVideoReference(mediaStream);
if (!this.stream.displayMyRemote()) { if (!this.stream.displayMyRemote()) {
@ -443,6 +475,7 @@ export class Publisher extends StreamManager {
this.stream.isLocalStreamReadyToPublish = true; this.stream.isLocalStreamReadyToPublish = true;
this.stream.ee.emitEvent('stream-ready-to-publish', []); this.stream.ee.emitEvent('stream-ready-to-publish', []);
} }
return resolve(); return resolve();
}; };

View File

@ -995,29 +995,20 @@ export class Session extends EventDispatcher {
* @hidden * @hidden
*/ */
recvIceCandidate(event: { senderConnectionId: string, endpointName: string, sdpMLineIndex: number, sdpMid: string, candidate: string }): void { recvIceCandidate(event: { senderConnectionId: string, endpointName: string, sdpMLineIndex: number, sdpMid: string, candidate: string }): void {
const candidate: RTCIceCandidate = { // The event contains fields that can be used to obtain a proper candidate,
address: null, // using the RTCIceCandidate constructor:
// https://w3c.github.io/webrtc-pc/#dom-rtcicecandidate-constructor
const candidateInit: RTCIceCandidateInit = {
candidate: event.candidate, candidate: event.candidate,
sdpMid: event.sdpMid,
sdpMLineIndex: event.sdpMLineIndex, sdpMLineIndex: event.sdpMLineIndex,
component: null, sdpMid: event.sdpMid,
foundation: null,
port: null,
priority: null,
protocol: null,
relatedAddress: null,
relatedPort: null,
tcpType: null,
usernameFragment: null,
type: null,
toJSON: () => {
return { candidate: event.candidate };
}
}; };
this.getConnection(event.senderConnectionId, 'Connection not found for connectionId ' + event.senderConnectionId + ' owning endpoint ' + event.endpointName + '. Ice candidate will be ignored: ' + candidate) const iceCandidate = new RTCIceCandidate(candidateInit);
this.getConnection(event.senderConnectionId, 'Connection not found for connectionId ' + event.senderConnectionId + ' owning endpoint ' + event.endpointName + '. Ice candidate will be ignored: ' + iceCandidate)
.then(connection => { .then(connection => {
const stream: Stream = connection.stream!; const stream: Stream = connection.stream!;
stream.getWebRtcPeer().addIceCandidate(candidate).catch(error => { stream.getWebRtcPeer().addIceCandidate(iceCandidate).catch(error => {
logger.error('Error adding candidate for ' + stream!.streamId logger.error('Error adding candidate for ' + stream!.streamId
+ ' stream of endpoint ' + event.endpointName + ': ' + error); + ' stream of endpoint ' + event.endpointName + ': ' + error);
}); });

View File

@ -80,12 +80,20 @@ export class WebRtcPeer {
this.pc = new RTCPeerConnection({ iceServers: this.configuration.iceServers }); this.pc = new RTCPeerConnection({ iceServers: this.configuration.iceServers });
this.pc.addEventListener('icecandidate', (event: RTCPeerConnectionIceEvent) => { this.pc.addEventListener("icecandidate", (event: RTCPeerConnectionIceEvent) => {
if (event.candidate != null) { if (event.candidate !== null) {
const candidate: RTCIceCandidate = event.candidate; // `RTCPeerConnectionIceEvent.candidate` is supposed to be an RTCIceCandidate:
this.configuration.onIceCandidate(candidate); // https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnectioniceevent-candidate
if (candidate.candidate !== '') { //
this.localCandidatesQueue.push(<RTCIceCandidate>{ candidate: candidate.candidate }); // But in practice, it is actually an RTCIceCandidateInit that can be used to
// obtain a proper candidate, using the RTCIceCandidate constructor:
// https://w3c.github.io/webrtc-pc/#dom-rtcicecandidate-constructor
const candidateInit: RTCIceCandidateInit = event.candidate as RTCIceCandidateInit;
const iceCandidate = new RTCIceCandidate(candidateInit);
this.configuration.onIceCandidate(iceCandidate);
if (iceCandidate.candidate !== '') {
this.localCandidatesQueue.push(iceCandidate);
} }
} }
}); });
@ -120,26 +128,53 @@ export class WebRtcPeer {
} }
} }
// 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);
}
/** /**
* Creates an SDP offer from the local RTCPeerConnection to send to the other peer * Creates an SDP offer from the local RTCPeerConnection to send to the other peer.
* Only if the negotiation was initiated by this peer * Only if the negotiation was initiated by this peer.
*/ */
createOffer(): Promise<RTCSessionDescriptionInit> { async createOffer(): Promise<RTCSessionDescriptionInit> {
return new Promise(async (resolve, reject) => {
// TODO: Delete this conditional when all supported browsers are // TODO: Delete this conditional when all supported browsers are
// modern enough to implement the Transceiver methods. // modern enough to implement the Transceiver methods.
if ("addTransceiver" in this.pc) { 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"); logger.debug("[createOffer] Method RTCPeerConnection.addTransceiver() is available; using it");
}
// Spec doc: https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-addtransceiver // Spec doc: https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-addtransceiver
if (this.configuration.mode !== "recvonly") { if (this.configuration.mode !== "recvonly") {
// To send media, assume that all desired media tracks // To send media, assume that all desired media tracks have been
// have been already added by higher level code to our // already added by higher level code to our MediaStream.
// MediaStream.
if (!this.configuration.mediaStream) { if (!this.configuration.mediaStream) {
return reject(new Error(`${this.configuration.mode} direction requested, but no stream was configured to be sent`)); throw new Error(
`[WebRtcPeer.createOffer] Direction is '${this.configuration.mode}', but no stream was configured to be sent`
);
} }
for (const track of this.configuration.mediaStream.getTracks()) { for (const track of this.configuration.mediaStream.getTracks()) {
@ -148,27 +183,7 @@ export class WebRtcPeer {
streams: [this.configuration.mediaStream], streams: [this.configuration.mediaStream],
}; };
if (track.kind === "audio") { if (track.kind === "video" && this.configuration.simulcast) {
if ("contentHint" in track) {
// For audio: "", "speech", "speech-recognition", "music".
// https://w3c.github.io/mst-content-hint/#audio-content-hints
track.contentHint = "";
logger.info(`[createOffer] Audio track Content Hint set: '${track.contentHint}'`);
}
} else if (track.kind === "video") {
if ("contentHint" in track) {
// For video: "", "motion", "detail", "text".
// https://w3c.github.io/mst-content-hint/#video-content-hints
if (this.configuration.typeOfVideo === TypeOfVideo.SCREEN) {
track.contentHint = "detail";
} else {
track.contentHint = "motion";
}
logger.info(`[createOffer] Video track Content Hint set: '${track.contentHint}'`);
}
logger.info(`[createOffer] this.configuration.simulcast: ${this.configuration.simulcast}`);
if (this.configuration.simulcast) {
// Check if the requested size is enough to ask for 3 layers. // Check if the requested size is enough to ask for 3 layers.
const trackSettings = track.getSettings(); const trackSettings = track.getSettings();
const trackConsts = track.getConstraints(); const trackConsts = track.getConstraints();
@ -200,19 +215,18 @@ export class WebRtcPeer {
const layerDiv = 2 ** (maxLayers - l - 1); const layerDiv = 2 ** (maxLayers - l - 1);
const encoding: RTCRtpEncodingParameters = { const encoding: RTCRtpEncodingParameters = {
rid: "rDiv" + layerDiv.toString(), rid: "rdiv" + layerDiv.toString(),
// @ts-ignore: Property missing from DOM types. // @ts-ignore -- Property missing from DOM types.
scalabilityMode: "L1T1", scalabilityMode: "L1T1",
}; };
if (this.configuration.typeOfVideo === TypeOfVideo.SCREEN) { if (["detail", "text"].includes(track.contentHint)) {
// Prioritize best resolution, for maximum picture detail. // Prioritize best resolution, for maximum picture detail.
encoding.scaleResolutionDownBy = 1.0; encoding.scaleResolutionDownBy = 1.0;
// @ts-ignore: Property missing from DOM types. // @ts-ignore -- Property missing from DOM types.
encoding.maxFramerate = Math.floor(30 / layerDiv); encoding.maxFramerate = Math.floor(30 / layerDiv);
// encoding.maxFramerate = (l === 2) ? 30 : Math.floor(30 / (2 * layerDiv)); // TESTING
} else { } else {
encoding.scaleResolutionDownBy = layerDiv; encoding.scaleResolutionDownBy = layerDiv;
} }
@ -220,7 +234,6 @@ export class WebRtcPeer {
tcInit.sendEncodings.push(encoding); tcInit.sendEncodings.push(encoding);
} }
} }
}
const tc = this.pc.addTransceiver(track, tcInit); const tc = this.pc.addTransceiver(track, tcInit);
@ -228,21 +241,16 @@ export class WebRtcPeer {
let sendParams = tc.sender.getParameters(); let sendParams = tc.sender.getParameters();
let needSetParams = false; let needSetParams = false;
if (!("degradationPreference" in sendParams)) { if (!sendParams.degradationPreference?.length) {
logger.debug( // degradationPreference for video: "balanced", "maintain-framerate", "maintain-resolution".
`[createOffer] RTCRtpSendParameters.degradationPreference attribute not present` // https://www.w3.org/TR/2018/CR-webrtc-20180927/#dom-rtcdegradationpreference
); if (["detail", "text"].includes(track.contentHint)) {
// Asked about why this might happen. Check it:
// https://groups.google.com/g/discuss-webrtc/c/R8Xug-irfRY
// For video: "balanced", "maintain-framerate", "maintain-resolution".
if (this.configuration.typeOfVideo === TypeOfVideo.SCREEN) {
sendParams.degradationPreference = "maintain-resolution"; sendParams.degradationPreference = "maintain-resolution";
} else { } else {
sendParams.degradationPreference = "balanced"; sendParams.degradationPreference = "balanced";
} }
logger.debug( logger.info(
`[createOffer] Video sender Degradation Preference set: ${sendParams.degradationPreference}` `[createOffer] Video sender Degradation Preference set: ${sendParams.degradationPreference}`
); );
@ -264,10 +272,7 @@ export class WebRtcPeer {
// * https://github.com/webrtcHacks/adapter/issues/998 // * https://github.com/webrtcHacks/adapter/issues/998
// * https://github.com/webrtcHacks/adapter/blob/v7.7.0/src/js/firefox/firefox_shim.js#L231-L255 // * https://github.com/webrtcHacks/adapter/blob/v7.7.0/src/js/firefox/firefox_shim.js#L231-L255
if (this.configuration.simulcast) { if (this.configuration.simulcast) {
if ( if (sendParams.encodings?.length !== tcInit.sendEncodings!.length) {
!("encodings" in sendParams) ||
sendParams.encodings.length !== tcInit.sendEncodings!.length
) {
sendParams.encodings = tcInit.sendEncodings!; sendParams.encodings = tcInit.sendEncodings!;
needSetParams = true; needSetParams = true;
@ -275,8 +280,16 @@ export class WebRtcPeer {
} }
if (needSetParams) { if (needSetParams) {
logger.debug(`[createOffer] Setting new RTCRtpSendParameters`); logger.debug(`[createOffer] Setting new RTCRtpSendParameters to video sender`);
try {
await tc.sender.setParameters(sendParams); await tc.sender.setParameters(sendParams);
} catch (error) {
let message = `[WebRtcPeer.createOffer] Cannot set RTCRtpSendParameters to video sender`;
if (error instanceof Error) {
message += `: ${error.message}`;
}
throw new Error(message);
}
} }
} }
@ -284,15 +297,15 @@ export class WebRtcPeer {
// if (track.kind === "video" && this.configuration.simulcast) { // if (track.kind === "video" && this.configuration.simulcast) {
// // Print browser capabilities. // // Print browser capabilities.
// // prettier-ignore // // prettier-ignore
// logger.error(`[createOffer] Transceiver send capabilities (static):\n${JSON.stringify(RTCRtpSender.getCapabilities?.("video"), null, 2)}`); // logger.debug(`[createOffer] Transceiver send capabilities (static):\n${JSON.stringify(RTCRtpSender.getCapabilities?.("video"), null, 2)}`);
// // prettier-ignore // // prettier-ignore
// logger.error(`[createOffer] Transceiver recv capabilities (static):\n${JSON.stringify(RTCRtpReceiver.getCapabilities?.("video"), null, 2)}`); // logger.debug(`[createOffer] Transceiver recv capabilities (static):\n${JSON.stringify(RTCRtpReceiver.getCapabilities?.("video"), null, 2)}`);
// // Print requested Transceiver encodings and parameters. // // Print requested Transceiver encodings and parameters.
// // prettier-ignore // // prettier-ignore
// logger.error(`[createOffer] Transceiver send encodings (requested):\n${JSON.stringify(tcInit.sendEncodings, null, 2)}`); // logger.debug(`[createOffer] Transceiver send encodings (requested):\n${JSON.stringify(tcInit.sendEncodings, null, 2)}`);
// // prettier-ignore // // prettier-ignore
// logger.error(`[createOffer] Transceiver send parameters (requested):\n${JSON.stringify(tc.sender.getParameters(), null, 2)}`); // logger.debug(`[createOffer] Transceiver send parameters (accepted):\n${JSON.stringify(tc.sender.getParameters(), null, 2)}`);
// } // }
} }
} else { } else {
@ -311,40 +324,18 @@ export class WebRtcPeer {
} }
} }
this.pc let sdpOffer: RTCSessionDescriptionInit;
.createOffer() try {
.then((sdpOffer) => resolve(sdpOffer)) sdpOffer = await this.pc.createOffer();
.catch((error) => reject(error)); } catch (error) {
} else { let message = `[WebRtcPeer.createOffer] Browser failed creating an SDP Offer`;
logger.warn("[createOffer] Method RTCPeerConnection.addTransceiver() is NOT available; using LEGACY offerToReceive{Audio,Video}"); if (error instanceof Error) {
message += `: ${error.message}`;
// DEPRECATED LEGACY METHOD: Old WebRTC versions don't implement }
// Transceivers, and instead depend on the deprecated throw new Error(message);
// "offerToReceiveAudio" and "offerToReceiveVideo".
if (!!this.configuration.mediaStream) {
this.deprecatedPeerConnectionTrackApi();
} }
const hasAudio = this.configuration.mediaConstraints.audio; return sdpOffer;
const hasVideo = this.configuration.mediaConstraints.video;
const options: RTCOfferOptions = {
offerToReceiveAudio:
this.configuration.mode !== "sendonly" && hasAudio,
offerToReceiveVideo:
this.configuration.mode !== "sendonly" && hasVideo,
};
logger.debug("RTCPeerConnection.createOffer() options:", JSON.stringify(options));
this.pc
// @ts-ignore - Compiler is too clever and thinks this branch will never execute.
.createOffer(options)
.then((sdpOffer) => resolve(sdpOffer))
.catch((error) => reject(error));
}
});
} }
deprecatedPeerConnectionTrackApi() { deprecatedPeerConnectionTrackApi() {
@ -492,9 +483,8 @@ export class WebRtcPeer {
// DEBUG: Uncomment for details. // DEBUG: Uncomment for details.
// { // {
// const tc = this.pc.getTransceivers().find((tc) => tc.sender.track?.kind === "video"); // const tc = this.pc.getTransceivers().find((tc) => tc.sender.track?.kind === "video");
// const sendParams = tc?.sender.getParameters();
// // prettier-ignore // // prettier-ignore
// logger.error(`[processRemoteAnswer] Transceiver send parameters (effective):\n${JSON.stringify(sendParams, null, 2)}`); // logger.debug(`[processRemoteAnswer] Transceiver send parameters (effective):\n${JSON.stringify(tc?.sender.getParameters(), null, 2)}`);
// } // }
resolve(); resolve();