You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openmeetings.apache.org by so...@apache.org on 2022/12/09 09:39:06 UTC
[openmeetings] 01/03: [OPENMEETINGS-2253] RTC related JS code is simplified; deprecated kurento-utils-js is dropped
This is an automated email from the ASF dual-hosted git repository.
solomax pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/openmeetings.git
commit 92a8a51a6943c5fc673189e7284ca7ede8111f35
Author: Maxim Solodovnik <so...@gmail.com>
AuthorDate: Sun Nov 27 11:56:54 2022 +0700
[OPENMEETINGS-2253] RTC related JS code is simplified; deprecated kurento-utils-js is dropped
---
openmeetings-web/src/main/front/room/src/video.js | 214 +++-----
.../src/main/front/settings/package.json | 5 +-
.../src/main/front/settings/src/WebRtcPeer.js | 592 +++++++++++++++++++++
.../src/main/front/settings/src/index.js | 9 +-
.../src/main/front/settings/src/mic-level.js | 6 +-
.../src/main/front/settings/src/settings.js | 140 ++---
.../src/main/front/settings/src/video-util.js | 23 +-
7 files changed, 745 insertions(+), 244 deletions(-)
diff --git a/openmeetings-web/src/main/front/room/src/video.js b/openmeetings-web/src/main/front/room/src/video.js
index 676c9d551..3477c4bae 100644
--- a/openmeetings-web/src/main/front/room/src/video.js
+++ b/openmeetings-web/src/main/front/room/src/video.js
@@ -105,9 +105,7 @@ module.exports = class Video {
data.aDest = data.aCtx.createMediaStreamDestination();
data.analyser.connect(data.aDest);
_stream = data.aDest.stream;
- stream.getVideoTracks().forEach(function(track) {
- _stream.addTrack(track);
- });
+ stream.getVideoTracks().forEach(track => _stream.addTrack(track));
}
}
state.data = data;
@@ -131,86 +129,69 @@ module.exports = class Video {
});
});
}
- function __attachListener(state) {
- if (!state.disposed && state.data.rtcPeer) {
- const pc = state.data.rtcPeer.peerConnection;
- pc.onconnectionstatechange = function(event) {
- console.warn(`!!RTCPeerConnection state changed: ${pc.connectionState}, user: ${sd.user.displayName}, uid: ${sd.uid}`);
- switch(pc.connectionState) {
- case "connected":
- if (sd.self) {
- // The connection has become fully connected
- OmUtil.alert('info', `Connection to Media server has been established`, 3000);//notify user
- }
- break;
- case "disconnected":
- case "failed":
- //connection has been dropped
- OmUtil.alert('warning', `Media server connection for user ${sd.user.displayName} is ${pc.connectionState}, will try to re-connect`, 3000);//notify user
- _refresh();
- break;
- case "closed":
- // The connection has been closed
- break;
+ function __connectionStateChangeListener(state) {
+ const pc = state.data.rtcPeer.pc;
+ console.warn(`!!RTCPeerConnection state changed: ${pc.connectionState}, user: ${sd.user.displayName}, uid: ${sd.uid}`);
+ switch(pc.connectionState) {
+ case "connected":
+ if (sd.self) {
+ // The connection has become fully connected
+ OmUtil.alert('info', `Connection to Media server has been established`, 3000);//notify user
}
- }
+ break;
+ case "disconnected":
+ case "failed":
+ //connection has been dropped
+ OmUtil.alert('warning', `Media server connection for user ${sd.user.displayName} is ${pc.connectionState}, will try to re-connect`, 3000);//notify user
+ _refresh();
+ break;
+ case "closed":
+ // The connection has been closed
+ break;
}
}
function __createSendPeer(msg, state, cnts) {
state.options = {
- videoStream: state.stream
+ mediaStream: state.stream
, mediaConstraints: cnts
- , onicecandidate: self.onIceCandidate
+ , onIceCandidate: self.onIceCandidate
+ , onConnectionStateChange: () => __connectionStateChangeListener(state)
};
- if (!isSharing) {
- state.options.localVideo = __getVideo(state);
- }
+ const vid = __getVideo(state);
+ vid.srcObject = state.stream;
+
const data = state.data;
- data.rtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(
- VideoUtil.addIceServers(state.options, msg)
- , function (error) {
- if (state.disposed || true === data.rtcPeer.cleaned) {
- return;
+ data.rtcPeer = new WebRtcPeerSendonly(VideoUtil.addIceServers(state.options, msg));
+ if (data.analyser) {
+ level = new MicLevel();
+ level.meter(data.analyser, lm, _micActivity, OmUtil.error);
+ }
+ data.rtcPeer.createOffer()
+ .then(sdpOffer => {
+ data.rtcPeer.processLocalOffer(sdpOffer);
+ OmUtil.log('Invoking Sender SDP offer callback function');
+ const bmsg = {
+ id : 'broadcastStarted'
+ , uid: sd.uid
+ , sdpOffer: sdpOffer.sdp
+ }, vtracks = state.stream.getVideoTracks();
+ if (vtracks && vtracks.length > 0) {
+ const vts = vtracks[0].getSettings();
+ vidSize.width = vts.width;
+ vidSize.height = vts.height;
+ bmsg.width = vts.width;
+ bmsg.height = vts.height;
+ bmsg.fps = vts.frameRate;
}
- if (error) {
- return OmUtil.error(error);
+ VideoMgrUtil.sendMessage(bmsg);
+ if (isSharing) {
+ Sharer.setShareState(Sharer.SHARE_STARTED);
}
- if (data.analyser) {
- level = new MicLevel();
- level.meter(data.analyser, lm, _micActivity, OmUtil.error);
+ if (isRecording) {
+ Sharer.setRecState(Sharer.SHARE_STARTED);
}
- data.rtcPeer.generateOffer(function(genErr, offerSdp) {
- if (state.disposed || true === data.rtcPeer.cleaned) {
- return;
- }
- if (genErr) {
- return OmUtil.error('Sender sdp offer error ' + genErr);
- }
- OmUtil.log('Invoking Sender SDP offer callback function');
- const bmsg = {
- id : 'broadcastStarted'
- , uid: sd.uid
- , sdpOffer: offerSdp
- }, vtracks = state.stream.getVideoTracks();
- if (vtracks && vtracks.length > 0) {
- const vts = vtracks[0].getSettings();
- vidSize.width = vts.width;
- vidSize.height = vts.height;
- bmsg.width = vts.width;
- bmsg.height = vts.height;
- bmsg.fps = vts.frameRate;
- }
- VideoMgrUtil.sendMessage(bmsg);
- if (isSharing) {
- Sharer.setShareState(Sharer.SHARE_STARTED);
- }
- if (isRecording) {
- Sharer.setRecState(Sharer.SHARE_STARTED);
- }
- });
- });
- data.rtcPeer.cleaned = false;
- __attachListener(state);
+ })
+ .catch(error => OmUtil.error(error));
}
function _createSendPeer(msg, state) {
if (isSharing || isRecording) {
@@ -222,36 +203,23 @@ module.exports = class Video {
function _createResvPeer(msg, state) {
__createVideo(state);
const options = VideoUtil.addIceServers({
- remoteVideo : __getVideo(state)
- , onicecandidate : self.onIceCandidate
+ mediaConstraints: {audio: true, video: true}
+ , onIceCandidate : self.onIceCandidate
+ , onConnectionStateChange: () => __connectionStateChangeListener(state)
}, msg);
const data = state.data;
- data.rtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(
- options
- , function(error) {
- if (state.disposed || true === data.rtcPeer.cleaned) {
- return;
- }
- if (error) {
- return OmUtil.error(error);
- }
- data.rtcPeer.generateOffer(function(genErr, offerSdp) {
- if (state.disposed || true === data.rtcPeer.cleaned) {
- return;
- }
- if (genErr) {
- return OmUtil.error('Receiver sdp offer error ' + genErr);
- }
- OmUtil.log('Invoking Receiver SDP offer callback function');
- VideoMgrUtil.sendMessage({
- id : 'addListener'
- , sender: sd.uid
- , sdpOffer: offerSdp
- });
+ data.rtcPeer = new WebRtcPeerRecvonly(options);
+ data.rtcPeer.createOffer()
+ .then(sdpOffer => {
+ data.rtcPeer.processLocalOffer(sdpOffer);
+ OmUtil.log('Invoking Receiver SDP offer callback function');
+ VideoMgrUtil.sendMessage({
+ id : 'addListener'
+ , sender: sd.uid
+ , sdpOffer: sdpOffer.sdp
});
- });
- data.rtcPeer.cleaned = false;
- __attachListener(state);
+ })
+ .catch(genErr => OmUtil.error('Receiver sdp offer error ' + genErr));
}
function _handleMicStatus(state) {
if (!footer || !footer.is(':visible')) {
@@ -513,7 +481,6 @@ module.exports = class Video {
delete state.options.videoStream;
delete state.options.mediaConstraints;
delete state.options.onicecandidate;
- delete state.options.localVideo;
state.options = null;
}
_cleanData(state.data);
@@ -557,7 +524,7 @@ module.exports = class Video {
const data = state.data
, videoEl = state.video[0];
if (data.rtcPeer && (!videoEl.srcObject || !videoEl.srcObject.active)) {
- videoEl.srcObject = sd.self ? data.rtcPeer.getLocalStream() : data.rtcPeer.getRemoteStream();
+ videoEl.srcObject = data.rtcPeer.stream;
}
}
});
@@ -567,39 +534,30 @@ module.exports = class Video {
if (!state || state.disposed || !state.data.rtcPeer || state.data.rtcPeer.cleaned) {
return;
}
- state.data.rtcPeer.processAnswer(answer, function (error) {
- if (true === this.cleaned) {
- return;
- }
- const video = __getVideo(state);
- if (this.peerConnection.signalingState === 'stable' && video && video.paused) {
- video.play().catch(function (err) {
- if ('NotAllowedError' === err.name) {
- VideoUtil.askPermission(function () {
- video.play();
- });
- }
- });
- return;
- }
- if (error) {
- OmUtil.error(error, true);
- }
- });
+ state.data.rtcPeer.processRemoteAnswer(answer)
+ .then(() => {
+ const video = __getVideo(state);
+ const rStream = state.data.rtcPeer.pc.getRemoteStreams()[0];
+ if (rStream) {
+ video.srcObject = rStream;
+ }
+ if (state.data.rtcPeer.pc.signalingState === 'stable' && video && video.paused) {
+ video.play().catch(err => {
+ if ('NotAllowedError' === err.name) {
+ VideoUtil.askPermission(() => video.play());
+ }
+ });
+ }
+ })
+ .catch(error => OmUtil.error(error, true));
}
function _processIceCandidate(candidate) {
const state = states.length > 0 ? states[0] : null;
if (!state || state.disposed || !state.data.rtcPeer || state.data.rtcPeer.cleaned) {
return;
}
- state.data.rtcPeer.addIceCandidate(candidate, function (error) {
- if (true === this.cleaned) {
- return;
- }
- if (error) {
- OmUtil.error('Error adding candidate: ' + error, true);
- }
- });
+ state.data.rtcPeer.addIceCandidate(candidate)
+ .catch(error => OmUtil.error('Error adding candidate: ' + error, true));
}
function _init(_msg) {
sd = _msg.stream;
diff --git a/openmeetings-web/src/main/front/settings/package.json b/openmeetings-web/src/main/front/settings/package.json
index c45a30778..8437ecc37 100644
--- a/openmeetings-web/src/main/front/settings/package.json
+++ b/openmeetings-web/src/main/front/settings/package.json
@@ -16,7 +16,8 @@
"tinyify": "^3.1.0"
},
"dependencies": {
- "adapterjs": "^0.15.5",
- "kurento-utils": "^6.16.0"
+ "freeice": "2.2.2",
+ "uuid": "^9.0.0",
+ "webrtc-adapter": "^8.2.0"
}
}
diff --git a/openmeetings-web/src/main/front/settings/src/WebRtcPeer.js b/openmeetings-web/src/main/front/settings/src/WebRtcPeer.js
new file mode 100644
index 000000000..d40d4b015
--- /dev/null
+++ b/openmeetings-web/src/main/front/settings/src/WebRtcPeer.js
@@ -0,0 +1,592 @@
+/*
+ * (C) Copyright 2017-2022 OpenVidu (https://openvidu.io)
+ *
+ * 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.
+ *
+ */
+
+// taken from here:
+// https://github.com/OpenVidu/openvidu/blob/master/openvidu-browser/src/OpenViduInternal/WebRtcPeer/WebRtcPeer.ts
+// and monkey-patched
+
+const freeice = require('freeice');
+
+const ExceptionEventName = {
+ /**
+ * The [ICE connection state](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/iceConnectionState)
+ * of an [RTCPeerConnection](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection) reached `failed` status.
+ *
+ * This is a terminal error that won't have any kind of possible recovery. If the client is still connected to OpenVidu Server,
+ * then an automatic reconnection process of the media stream is immediately performed. If the ICE connection has broken due to
+ * a total network drop, then no automatic reconnection process will be possible.
+ *
+ * {@link ExceptionEvent} objects with this {@link ExceptionEvent.name} will have as {@link ExceptionEvent.origin} property a {@link Stream} object.
+ */
+ ICE_CONNECTION_FAILED: 'ICE_CONNECTION_FAILED',
+
+ /**
+ * The [ICE connection state](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/iceConnectionState)
+ * of an [RTCPeerConnection](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection) reached `disconnected` status.
+ *
+ * This is not a terminal error, and it is possible for the ICE connection to be reconnected. If the client is still connected to
+ * OpenVidu Server and after certain timeout the ICE connection has not reached a success or terminal status, then an automatic
+ * reconnection process of the media stream is performed. If the ICE connection has broken due to a total network drop, then no
+ * automatic reconnection process will be possible.
+ *
+ * You can customize the timeout for the reconnection attempt with property {@link OpenViduAdvancedConfiguration.iceConnectionDisconnectedExceptionTimeout},
+ * which by default is 4000 milliseconds.
+ *
+ * {@link ExceptionEvent} objects with this {@link ExceptionEvent.name} will have as {@link ExceptionEvent.origin} property a {@link Stream} object.
+ */
+ ICE_CONNECTION_DISCONNECTED: 'ICE_CONNECTION_DISCONNECTED',
+};
+
+class WebRtcPeer {
+ constructor(configuration) {
+ this.remoteCandidatesQueue = [];
+ this.localCandidatesQueue = [];
+ this.iceCandidateList = [];
+ this.candidategatheringdone = false;
+
+ // Same as WebRtcPeerConfiguration but without optional fields.
+ this.configuration = {
+ ...configuration,
+ iceServers: !!configuration.iceServers && configuration.iceServers.length > 0 ? configuration.iceServers : freeice(),
+ mediaStream: configuration.mediaStream !== undefined ? configuration.mediaStream : null,
+ mode: !!configuration.mode ? configuration.mode : 'sendrecv',
+ id: !!configuration.id ? configuration.id : this.generateUniqueId()
+ };
+ // prettier-ignore
+ OmUtil.log(`[WebRtcPeer] configuration:\n${JSON.stringify(this.configuration, null, 2)}`);
+
+ this.pc = new RTCPeerConnection({ iceServers: this.configuration.iceServers });
+
+ this._iceCandidateListener = (event) => {
+ if (event.candidate !== null) {
+ // `RTCPeerConnectionIceEvent.candidate` is supposed to be an RTCIceCandidate:
+ // https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnectioniceevent-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 = event.candidate;
+ const iceCandidate = new RTCIceCandidate(candidateInit);
+
+ this.configuration.onIceCandidate(iceCandidate);
+ if (iceCandidate.candidate !== '') {
+ this.localCandidatesQueue.push(iceCandidate);
+ }
+ }
+ };
+ this.pc.addEventListener('icecandidate', this._iceCandidateListener);
+
+ this._signalingStateChangeListener = () => {
+ if (this.pc.signalingState === 'stable') {
+ // SDP Offer/Answer finished. Add stored remote candidates.
+ while (this.iceCandidateList.length > 0) {
+ let candidate = this.iceCandidateList.shift();
+ this.pc.addIceCandidate(candidate);
+ }
+ }
+ };
+ this.pc.addEventListener('signalingstatechange', this._signalingStateChangeListener);
+ if (this.configuration.onConnectionStateChange) {
+ this.pc.addEventListener('connectionstatechange', this.configuration.onConnectionStateChange);
+ }
+ }
+
+ getId() {
+ return this.configuration.id;
+ }
+
+ /**
+ * This method frees the resources used by WebRtcPeer
+ */
+ dispose() {
+ OmUtil.log('Disposing WebRtcPeer');
+ if (this.pc) {
+ if (this.pc.signalingState === 'closed') {
+ return;
+ }
+ this.pc.removeEventListener('icecandidate', this._iceCandidateListener);
+ this._iceCandidateListener = undefined;
+ this.pc.removeEventListener('signalingstatechange', this._signalingStateChangeListener);
+ this._signalingStateChangeListener = undefined;
+ if (this._iceConnectionStateChangeListener) {
+ this.pc.removeEventListener('iceconnectionstatechange', this._iceConnectionStateChangeListener);
+ }
+ if (this.configuration.onConnectionStateChange) {
+ this.pc.removeEventListener('connectionstatechange', this.configuration.onConnectionStateChange);
+ }
+ this.configuration = {};
+ this.pc.close();
+ this.remoteCandidatesQueue = [];
+ this.localCandidatesQueue = [];
+ }
+ }
+
+ /**
+ * Creates an SDP offer from the local RTCPeerConnection to send to the other peer.
+ * Only if the negotiation was initiated by this peer.
+ */
+ async createOffer() {
+ // TODO: Delete this conditional when all supported browsers are
+ // modern enough to implement the Transceiver methods.
+ if (!('addTransceiver' in this.pc)) {
+ OmUtil.error(
+ '[createOffer] Method RTCPeerConnection.addTransceiver() is NOT available; using LEGACY offerToReceive{Audio,Video}'
+ );
+ return this.createOfferLegacy();
+ } else {
+ OmUtil.log('[createOffer] Method RTCPeerConnection.addTransceiver() is available; using it');
+ }
+
+ // Spec doc: https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-addtransceiver
+
+ 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.
+
+ if (!this.configuration.mediaStream) {
+ 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()) {
+ const tcInit = {
+ direction: this.configuration.mode,
+ streams: [this.configuration.mediaStream]
+ };
+
+ 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 = typeof(trackSettings.width) === 'object' ? trackConsts.width.ideal : trackConsts.width || 0;
+ const trackHeight = typeof(trackSettings.height) === 'object' ? trackConsts.height.ideal : trackConsts.height || 0;
+ OmUtil.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;
+ }
+
+ tcInit.sendEncodings = [];
+ for (let l = 0; l < maxLayers; l++) {
+ const layerDiv = 2 ** (maxLayers - l - 1);
+
+ const encoding = {
+ rid: 'rdiv' + layerDiv.toString(),
+
+ // @ts-ignore -- Property missing from DOM types.
+ scalabilityMode: 'L1T1'
+ };
+
+ if (['detail', 'text'].includes(track.contentHint)) {
+ // Prioritize best resolution, for maximum picture detail.
+ encoding.scaleResolutionDownBy = 1.0;
+
+ // @ts-ignore -- Property missing from DOM types.
+ encoding.maxFramerate = Math.floor(30 / layerDiv);
+ } else {
+ encoding.scaleResolutionDownBy = layerDiv;
+ }
+
+ tcInit.sendEncodings.push(encoding);
+ }
+ }
+
+ const tc = this.pc.addTransceiver(track, tcInit);
+
+ if (track.kind === 'video') {
+ let sendParams = tc.sender.getParameters();
+ let needSetParams = false;
+
+ if (sendParams.degradationPreference && !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)) {
+ sendParams.degradationPreference = 'maintain-resolution';
+ } else {
+ sendParams.degradationPreference = 'balanced';
+ }
+
+ OmUtil.info(`[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)
+
+ needSetParams = true;
+ }
+
+ // 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 (sendParams.encodings.length !== tcInit.sendEncodings.length) {
+ sendParams.encodings = tcInit.sendEncodings;
+
+ needSetParams = true;
+ }
+ }
+
+ if (needSetParams) {
+ OmUtil.log(`[createOffer] Setting new RTCRtpSendParameters to video sender`);
+ try {
+ 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);
+ }
+ }
+ }
+ }
+ } 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]
+ });
+ }
+ }
+
+ let sdpOffer;
+ try {
+ sdpOffer = await this.pc.createOffer();
+ } catch (error) {
+ let message = `[WebRtcPeer.createOffer] Browser failed creating an SDP Offer`;
+ if (error instanceof Error) {
+ message += `: ${error.message}`;
+ }
+ throw new Error(message);
+ }
+
+ return sdpOffer;
+ }
+
+ /**
+ * 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() {
+ return new Promise((resolve, reject) => {
+ // TODO: Delete this conditional when all supported browsers are
+ // modern enough to implement the Transceiver methods.
+ if ('getTransceivers' in this.pc) {
+ OmUtil.log('[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 {
+ return reject(new Error(`${kind} requested, but no transceiver was created from remote description`));
+ }
+ }
+
+ this.pc
+ .createAnswer()
+ .then((sdpAnswer) => resolve(sdpAnswer))
+ .catch((error) => reject(error));
+ } 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 = {
+ offerToReceiveAudio: offerAudio,
+ offerToReceiveVideo: offerVideo
+ };
+ (this.pc).createAnswer(constraints)
+ .then((sdpAnswer) => resolve(sdpAnswer))
+ .catch((error) => reject(error));
+ }
+ }
+
+ // else, there is nothing to do; the legacy createAnswer() options do
+ // not offer any control over which tracks are included in the answer.
+ });
+ }
+
+ /**
+ * This peer initiated negotiation. Step 1/4 of SDP offer-answer protocol
+ */
+ processLocalOffer(offer) {
+ return new Promise((resolve, reject) => {
+ this.pc
+ .setLocalDescription(offer)
+ .then(() => {
+ const localDescription = this.pc.localDescription;
+ if (!!localDescription) {
+ OmUtil.log('Local description set', localDescription.sdp);
+ return resolve();
+ } else {
+ return reject('Local description is not defined');
+ }
+ })
+ .catch((error) => reject(error));
+ });
+ }
+
+ /**
+ * Other peer initiated negotiation. Step 2/4 of SDP offer-answer protocol
+ */
+ processRemoteOffer(sdpOffer) {
+ return new Promise((resolve, reject) => {
+ const offer = {
+ type: 'offer',
+ sdp: sdpOffer
+ };
+ OmUtil.log('SDP offer received, setting remote description', offer);
+
+ if (this.pc.signalingState === 'closed') {
+ return reject('RTCPeerConnection is closed when trying to set remote description');
+ }
+ this.setRemoteDescription(offer)
+ .then(() => resolve())
+ .catch((error) => reject(error));
+ });
+ }
+
+ /**
+ * Other peer initiated negotiation. Step 3/4 of SDP offer-answer protocol
+ */
+ processLocalAnswer(answer) {
+ return new Promise((resolve, reject) => {
+ OmUtil.log('SDP answer created, setting local description');
+ if (this.pc.signalingState === 'closed') {
+ return reject('RTCPeerConnection is closed when trying to set local description');
+ }
+ this.pc
+ .setLocalDescription(answer)
+ .then(() => resolve())
+ .catch((error) => reject(error));
+ });
+ }
+
+ /**
+ * This peer initiated negotiation. Step 4/4 of SDP offer-answer protocol
+ */
+ processRemoteAnswer(sdpAnswer) {
+ return new Promise((resolve, reject) => {
+ const answer = {
+ type: 'answer',
+ sdp: sdpAnswer
+ };
+ OmUtil.log('SDP answer received, setting remote description');
+
+ if (this.pc.signalingState === 'closed') {
+ return reject('RTCPeerConnection is closed when trying to set remote description');
+ }
+ this.setRemoteDescription(answer)
+ .then(() => {
+ resolve();
+ })
+ .catch((error) => reject(error));
+ });
+ }
+
+ /**
+ * @hidden
+ */
+ async setRemoteDescription(sdp) {
+ return this.pc.setRemoteDescription(sdp);
+ }
+
+ /**
+ * Callback function invoked when an ICE candidate is received
+ */
+ addIceCandidate(iceCandidate) {
+ return new Promise((resolve, reject) => {
+ OmUtil.log('Remote ICE candidate received', iceCandidate);
+ this.remoteCandidatesQueue.push(iceCandidate);
+ switch (this.pc.signalingState) {
+ case 'closed':
+ reject(new Error('PeerConnection object is closed'));
+ break;
+ case 'stable':
+ if (!!this.pc.remoteDescription) {
+ this.pc
+ .addIceCandidate(iceCandidate)
+ .then(() => resolve())
+ .catch((error) => reject(error));
+ } else {
+ this.iceCandidateList.push(iceCandidate);
+ resolve();
+ }
+ break;
+ default:
+ this.iceCandidateList.push(iceCandidate);
+ resolve();
+ }
+ });
+ }
+
+ addIceConnectionStateChangeListener(otherId) {
+ if (!this._iceConnectionStateChangeListener) {
+ this._iceConnectionStateChangeListener = () => {
+ const iceConnectionState = this.pc.iceConnectionState;
+ switch (iceConnectionState) {
+ case 'disconnected':
+ // Possible network disconnection
+ const msg1 =
+ 'IceConnectionState of RTCPeerConnection ' +
+ this.configuration.id +
+ ' (' +
+ otherId +
+ ') change to "disconnected". Possible network disconnection';
+ logger.warn(msg1);
+ this.configuration.onIceConnectionStateException(ExceptionEventName.ICE_CONNECTION_DISCONNECTED, msg1);
+ break;
+ case 'failed':
+ const msg2 = 'IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') to "failed"';
+ logger.error(msg2);
+ this.configuration.onIceConnectionStateException(ExceptionEventName.ICE_CONNECTION_FAILED, msg2);
+ break;
+ case 'closed':
+ OmUtil.log(
+ 'IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "closed"'
+ );
+ break;
+ case 'new':
+ OmUtil.log('IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "new"');
+ break;
+ case 'checking':
+ logger.log(
+ 'IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "checking"'
+ );
+ break;
+ case 'connected':
+ logger.log(
+ 'IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "connected"'
+ );
+ break;
+ case 'completed':
+ logger.log(
+ 'IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "completed"'
+ );
+ break;
+ }
+ };
+ }
+ this.pc.addEventListener('iceconnectionstatechange', this._iceConnectionStateChangeListener);
+ }
+
+ /**
+ * @hidden
+ */
+ generateUniqueId() {
+ return uuidv4();
+ }
+
+ get stream() {
+ return this.pc.getLocalStreams()[0] || this.pc.getRemoteStreams()[0];
+ }
+
+ // LEGACY code
+ deprecatedPeerConnectionTrackApi() {
+ for (const track of this.configuration.mediaStream.getTracks()) {
+ this.pc.addTrack(track, this.configuration.mediaStream);
+ }
+ }
+
+ // DEPRECATED LEGACY METHOD: Old WebRTC versions don't implement
+ // Transceivers, and instead depend on the deprecated
+ // "offerToReceiveAudio" and "offerToReceiveVideo".
+ createOfferLegacy() {
+ if (!!this.configuration.mediaStream) {
+ this.deprecatedPeerConnectionTrackApi();
+ }
+
+ const hasAudio = this.configuration.mediaConstraints.audio;
+ const hasVideo = this.configuration.mediaConstraints.video;
+
+ const options = {
+ offerToReceiveAudio: this.configuration.mode !== 'sendonly' && hasAudio,
+ offerToReceiveVideo: this.configuration.mode !== 'sendonly' && hasVideo
+ };
+
+ OmUtil.log('[createOfferLegacy] RTCPeerConnection.createOffer() options:', JSON.stringify(options));
+
+ return this.pc.createOffer(options);
+ }
+}
+
+class WebRtcPeerRecvonly extends WebRtcPeer {
+ constructor(configuration) {
+ configuration.mode = 'recvonly';
+ super(configuration);
+ }
+};
+
+class WebRtcPeerSendonly extends WebRtcPeer {
+ constructor(configuration) {
+ configuration.mode = 'sendonly';
+ super(configuration);
+ }
+};
+
+class WebRtcPeerSendrecv extends WebRtcPeer {
+ constructor(configuration) {
+ configuration.mode = 'sendrecv';
+ super(configuration);
+ }
+};
+
+module.exports = {
+ WebRtcPeerRecvonly: WebRtcPeerRecvonly,
+ WebRtcPeerSendonly: WebRtcPeerSendonly
+};
diff --git a/openmeetings-web/src/main/front/settings/src/index.js b/openmeetings-web/src/main/front/settings/src/index.js
index 982236461..7edcb067f 100644
--- a/openmeetings-web/src/main/front/settings/src/index.js
+++ b/openmeetings-web/src/main/front/settings/src/index.js
@@ -1,5 +1,8 @@
/* Licensed under the Apache License, Version 2.0 (the "License") http://www.apache.org/licenses/LICENSE-2.0 */
const VideoUtil = require('./video-util');
+require('webrtc-adapter');
+const {v4: uuidv4} = require('uuid');
+const {WebRtcPeerRecvonly, WebRtcPeerSendonly} = require('./WebRtcPeer');
if (window.hasOwnProperty('isSecureContext') === false) {
window.isSecureContext = window.location.protocol == 'https:' || ["localhost", "127.0.0.1"].indexOf(window.location.hostname) !== -1;
@@ -10,9 +13,9 @@ Object.assign(window, {
, VIDWIN_SEL: VideoUtil.VIDWIN_SEL
, VID_SEL: VideoUtil.VID_SEL
, MicLevel: require('./mic-level')
+ , WebRtcPeerRecvonly: WebRtcPeerRecvonly
+ , WebRtcPeerSendonly: WebRtcPeerSendonly
, VideoSettings: require('./settings')
- // AdapterJS is not added for now
- , kurentoUtils: require('kurento-utils')
- , uuidv4: require('uuid/v4')
+ , uuidv4: uuidv4
});
diff --git a/openmeetings-web/src/main/front/settings/src/mic-level.js b/openmeetings-web/src/main/front/settings/src/mic-level.js
index 32668f69b..9fb3edc00 100644
--- a/openmeetings-web/src/main/front/settings/src/mic-level.js
+++ b/openmeetings-web/src/main/front/settings/src/mic-level.js
@@ -5,11 +5,7 @@ module.exports = class MicLevel {
constructor() {
let ctx, mic, analyser, vol = .0, vals = new RingBuffer(100);
- this.meterPeer = (rtcPeer, cnvs, _micActivity, _error, connectAudio) => {
- if (!rtcPeer || ('function' !== typeof(rtcPeer.getLocalStream) && 'function' !== typeof(rtcPeer.getRemoteStream))) {
- return;
- }
- const stream = rtcPeer.getLocalStream() || rtcPeer.getRemoteStream();
+ this.meterStream = (stream, cnvs, _micActivity, _error, connectAudio) => {
if (!stream || stream.getAudioTracks().length < 1) {
return;
}
diff --git a/openmeetings-web/src/main/front/settings/src/settings.js b/openmeetings-web/src/main/front/settings/src/settings.js
index 633eaf61e..6c16d4cc3 100644
--- a/openmeetings-web/src/main/front/settings/src/settings.js
+++ b/openmeetings-web/src/main/front/settings/src/settings.js
@@ -1,7 +1,6 @@
/* Licensed under the Apache License, Version 2.0 (the "License") http://www.apache.org/licenses/LICENSE-2.0 */
const MicLevel = require('./mic-level');
const VideoUtil = require('./video-util');
-const kurentoUtils = require('kurento-utils');
const DEV_AUDIO = 'audioinput'
, DEV_VIDEO = 'videoinput'
@@ -149,7 +148,7 @@ function _setCntsDimensions(cnts) {
//each bool OR https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints
// min/ideal/max/exact/mandatory can also be used
function _constraints(sd, callback) {
- _getDevConstraints(function(devCnts){
+ _getDevConstraints(function(devCnts) {
const cnts = {};
if (devCnts.video && false === o.audioOnly && VideoUtil.hasCam(sd) && s.video.cam > -1) {
cnts.video = {
@@ -202,39 +201,32 @@ function _readValues(msg, func) {
_constraints(null, function(cnts) {
if (cnts.video !== false || cnts.audio !== false) {
const options = VideoUtil.addIceServers({
- localVideo: vid[0]
- , mediaConstraints: cnts
+ mediaConstraints: cnts
+ , onIceCandidate: _onIceCandidate
}, msg);
- rtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(
- options
- , function(error) {
- if (error) {
- if (true === rtcPeer.cleaned) {
- return;
- }
- return OmUtil.error(error);
- }
+ navigator.mediaDevices.getUserMedia(cnts)
+ .then(stream => {
+ vid[0].srcObject = stream;
+ options.mediaStream = stream;
+
+ rtcPeer = new WebRtcPeerSendonly(options);
if (cnts.audio) {
lm.show();
level = new MicLevel();
- level.meterPeer(rtcPeer, lm, function(){}, OmUtil.error, false);
+ level.meterStream(stream, lm, function(){}, OmUtil.error, false);
} else {
lm.hide();
}
- rtcPeer.generateOffer(function(error, _offerSdp) {
- if (error) {
- if (true === rtcPeer.cleaned) {
- return;
- }
- return OmUtil.error('Error generating the offer');
- }
- if (typeof(func) === 'function') {
- func(_offerSdp, cnts);
- } else {
- _allowRec(true);
- }
- });
- });
+ return rtcPeer.createOffer();
+ })
+ .then(sdpOffer => {
+ rtcPeer.processLocalOffer(sdpOffer);
+ if (typeof(func) === 'function') {
+ func(sdpOffer.sdp, cnts);
+ } else {
+ _allowRec(true);
+ }
+ }).catch(_ => OmUtil.error('Error generating the offer'));
}
if (!msg) {
_updateRec();
@@ -384,75 +376,49 @@ function _onKMessage(m) {
, video: cnts.video !== false
, audio: cnts.audio !== false
}, MsgBase);
- rtcPeer.on('icecandidate', _onIceCandidate);
});
break;
- case 'canPlay':
- {
- const options = VideoUtil.addIceServers({
- remoteVideo: vid[0]
- , mediaConstraints: {audio: true, video: true}
- , onicecandidate: _onIceCandidate
- }, m);
- _clear();
- rtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(
- options
- , function(error) {
- if (error) {
- if (true === rtcPeer.cleaned) {
- return;
- }
- return OmUtil.error(error);
- }
- rtcPeer.generateOffer(function(error, offerSdp) {
- if (error) {
- if (true === rtcPeer.cleaned) {
- return;
- }
- return OmUtil.error('Error generating the offer');
- }
- OmUtil.sendMessage({
- id : 'play'
- , sdpOffer: offerSdp
- }, MsgBase);
- });
- });
- }
+ case 'canPlay': {
+ const options = VideoUtil.addIceServers({
+ mediaConstraints: {audio: true, video: true}
+ , onIceCandidate: _onIceCandidate
+ }, m);
+ _clear();
+ rtcPeer = new WebRtcPeerRecvonly(options);
+ rtcPeer.createOffer()
+ .then(sdpOffer => {
+ rtcPeer.processLocalOffer(sdpOffer);
+ OmUtil.sendMessage({
+ id : 'play'
+ , sdpOffer: sdpOffer.sdp
+ }, MsgBase);
+ })
+ .catch(_ => OmUtil.error('Error generating the offer'));
+ }
break;
case 'playResponse':
OmUtil.log('Play SDP answer received from server. Processing ...');
- rtcPeer.processAnswer(m.sdpAnswer, function(error) {
- if (error) {
- if (true === rtcPeer.cleaned) {
- return;
- }
- return OmUtil.error(error);
- }
- lm.show();
- level = new MicLevel();
- level.meterPeer(rtcPeer, lm, function(){}, OmUtil.error, true);
- });
+
+ rtcPeer.processRemoteAnswer(m.sdpAnswer)
+ .then(() => {
+ const stream = rtcPeer.stream;
+ if (stream) {
+ vid[0].srcObject = stream;
+ lm.show();
+ level = new MicLevel();
+ level.meterStream(stream, lm, function(){}, OmUtil.error, true);
+ };
+ })
+ .catch(error => OmUtil.error(error));
break;
case 'startResponse':
OmUtil.log('SDP answer received from server. Processing ...');
- rtcPeer.processAnswer(m.sdpAnswer, function(error) {
- if (error) {
- if (true === rtcPeer.cleaned) {
- return;
- }
- return OmUtil.error(error);
- }
- });
+ rtcPeer.processRemoteAnswer(m.sdpAnswer)
+ .catch(error => OmUtil.error(error));
break;
case 'iceCandidate':
- rtcPeer.addIceCandidate(m.candidate, function(error) {
- if (error) {
- if (true === rtcPeer.cleaned) {
- return;
- }
- return OmUtil.error('Error adding candidate: ' + error);
- }
- });
+ rtcPeer.addIceCandidate(m.candidate)
+ .catch(error => OmUtil.error('Error adding candidate: ' + error));
break;
case 'recording':
timer.show().find('.time').text(m.time);
diff --git a/openmeetings-web/src/main/front/settings/src/video-util.js b/openmeetings-web/src/main/front/settings/src/video-util.js
index 2d9c28c3f..d13f1e5c9 100644
--- a/openmeetings-web/src/main/front/settings/src/video-util.js
+++ b/openmeetings-web/src/main/front/settings/src/video-util.js
@@ -184,11 +184,10 @@ function _cleanStream(stream) {
stream.getTracks().forEach(track => track.stop());
}
}
-function _cleanPeer(peer) {
- if (!!peer) {
- peer.cleaned = true;
+function _cleanPeer(rtcPeer) {
+ if (!!rtcPeer) {
try {
- const pc = peer.peerConnection;
+ const pc = rtcPeer.pc;
if (!!pc) {
pc.getSenders().forEach(sender => {
try {
@@ -208,22 +207,8 @@ function _cleanPeer(peer) {
OmUtil.log('Failed to clean receiver' + e);
}
});
- pc.onconnectionstatechange = null;
- pc.ontrack = null;
- pc.onremovetrack = null;
- pc.onremovestream = null;
- pc.onicecandidate = null;
- pc.oniceconnectionstatechange = null;
- pc.onsignalingstatechange = null;
- pc.onicegatheringstatechange = null;
- pc.onnegotiationneeded = null;
}
- peer.dispose();
- peer.removeAllListeners('icecandidate');
- delete peer.generateOffer;
- delete peer.processAnswer;
- delete peer.processOffer;
- delete peer.addIceCandidate;
+ rtcPeer.dispose();
} catch(e) {
//no-op
}