diff --git a/erizo_controller/erizoClient/lib/unifiedPlanUtils.js b/erizo_controller/erizoClient/lib/unifiedPlanUtils.js new file mode 100644 index 000000000..eb305244e --- /dev/null +++ b/erizo_controller/erizoClient/lib/unifiedPlanUtils.js @@ -0,0 +1,123 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.addLegacySimulcast = exports.getRtpEncodings = void 0; +function getRtpEncodings({ offerMediaObject }) { + const ssrcs = new Set(); + for (const line of offerMediaObject.ssrcs || []) { + const ssrc = line.id; + ssrcs.add(ssrc); + } + if (ssrcs.size === 0) + throw new Error('no a=ssrc lines found'); + const ssrcToRtxSsrc = new Map(); + // First assume RTX is used. + for (const line of offerMediaObject.ssrcGroups || []) { + if (line.semantics !== 'FID') + continue; + let [ssrc, rtxSsrc] = line.ssrcs.split(/\s+/); + ssrc = Number(ssrc); + rtxSsrc = Number(rtxSsrc); + if (ssrcs.has(ssrc)) { + // Remove both the SSRC and RTX SSRC from the set so later we know that they + // are already handled. + ssrcs.delete(ssrc); + ssrcs.delete(rtxSsrc); + // Add to the map. + ssrcToRtxSsrc.set(ssrc, rtxSsrc); + } + } + // If the set of SSRCs is not empty it means that RTX is not being used, so take + // media SSRCs from there. + for (const ssrc of ssrcs) { + // Add to the map. + ssrcToRtxSsrc.set(ssrc, null); + } + const encodings = []; + for (const [ssrc, rtxSsrc] of ssrcToRtxSsrc) { + const encoding = { ssrc }; + if (rtxSsrc) + encoding.rtx = { ssrc: rtxSsrc }; + encodings.push(encoding); + } + return encodings; +} +exports.getRtpEncodings = getRtpEncodings; +/** + * Adds multi-ssrc based simulcast into the given SDP media section offer. + */ +function addLegacySimulcast({ offerMediaObject, numStreams }) { + if (numStreams <= 1) + console.log('numStreams must be greater than 1'); + // Get the SSRC. + const ssrcMsidLine = (offerMediaObject.ssrcs || []) + .find((line) => line.attribute === 'msid'); + if (!ssrcMsidLine) + console.log('a=ssrc line with msid information not found'); + const [streamId, trackId] = ssrcMsidLine.value.split(' '); + const firstSsrc = ssrcMsidLine.id; + let firstRtxSsrc; + // Get the SSRC for RTX. + (offerMediaObject.ssrcGroups || []) + .some((line) => { + if (line.semantics !== 'FID') + return false; + const ssrcs = line.ssrcs.split(/\s+/); + if (Number(ssrcs[0]) === firstSsrc) { + firstRtxSsrc = Number(ssrcs[1]); + return true; + } + else { + return false; + } + }); + const ssrcCnameLine = offerMediaObject.ssrcs + .find((line) => line.attribute === 'cname'); + if (!ssrcCnameLine) + console.log('a=ssrc line with cname information not found'); + const cname = ssrcCnameLine.value; + const ssrcs = []; + const rtxSsrcs = []; + for (let i = 0; i < numStreams; ++i) { + ssrcs.push(firstSsrc + i); + if (firstRtxSsrc) + rtxSsrcs.push(firstRtxSsrc + i); + } + offerMediaObject.ssrcGroups = []; + offerMediaObject.ssrcs = []; + offerMediaObject.ssrcGroups.push({ + semantics: 'SIM', + ssrcs: ssrcs.join(' ') + }); + for (let i = 0; i < ssrcs.length; ++i) { + const ssrc = ssrcs[i]; + offerMediaObject.ssrcs.push({ + id: ssrc, + attribute: 'cname', + value: cname + }); + offerMediaObject.ssrcs.push({ + id: ssrc, + attribute: 'msid', + value: `${streamId} ${trackId}` + }); + } + for (let i = 0; i < rtxSsrcs.length; ++i) { + const ssrc = ssrcs[i]; + const rtxSsrc = rtxSsrcs[i]; + offerMediaObject.ssrcs.push({ + id: rtxSsrc, + attribute: 'cname', + value: cname + }); + offerMediaObject.ssrcs.push({ + id: rtxSsrc, + attribute: 'msid', + value: `${streamId} ${trackId}` + }); + offerMediaObject.ssrcGroups.push({ + semantics: 'FID', + ssrcs: `${ssrc} ${rtxSsrc}` + }); + } +} +exports.addLegacySimulcast = addLegacySimulcast; diff --git a/erizo_controller/erizoClient/src/Room.js b/erizo_controller/erizoClient/src/Room.js index 19a0d09fc..08d931a67 100644 --- a/erizo_controller/erizoClient/src/Room.js +++ b/erizo_controller/erizoClient/src/Room.js @@ -250,6 +250,7 @@ const Room = (altIo, altConnectionHelpers, altConnectionManager, specInput) => { disableIceRestart: that.disableIceRestart, forceTurn: stream.forceTurn, p2p: false, + svc: options.svc, streamRemovedListener: onRemoteStreamRemovedListener, isRemote, }; @@ -832,6 +833,8 @@ const Room = (altIo, altConnectionHelpers, altConnectionManager, specInput) => { video: stream.videoMuted, }; + options.svc = options.svc || false; + // 1- If the stream is not local or it is a failed stream we do nothing. if (stream && stream.local && !stream.failed && !localStreams.has(stream.getID())) { // 2- Publish Media Stream to Erizo-Controller diff --git a/erizo_controller/erizoClient/src/Stream.js b/erizo_controller/erizoClient/src/Stream.js index 5b1204897..ec070d37e 100644 --- a/erizo_controller/erizoClient/src/Stream.js +++ b/erizo_controller/erizoClient/src/Stream.js @@ -224,7 +224,7 @@ const Stream = (altConnectionHelpers, specInput) => { that.hasSimulcast = () => Object.keys(videoSenderLicodeParameters).length > 1; that.generateEncoderParameters = () => { - const nativeSenderParameters = []; + let nativeSenderParameters = []; const requestedLayers = Object.keys(videoSenderLicodeParameters).length || defaultSimulcastSpatialLayers; const isScreenshare = that.hasScreen(); @@ -236,6 +236,7 @@ const Stream = (altConnectionHelpers, specInput) => { layerConfig.scaleResolutionDownBy = base ** (requestedLayers - layer); nativeSenderParameters.push(layerConfig); } + nativeSenderParameters = [nativeSenderParameters]; return nativeSenderParameters; }; diff --git a/erizo_controller/erizoClient/src/webrtc-stacks/ChromeStableStack.js b/erizo_controller/erizoClient/src/webrtc-stacks/ChromeStableStack.js index 1d10c2a66..8d83902bf 100644 --- a/erizo_controller/erizoClient/src/webrtc-stacks/ChromeStableStack.js +++ b/erizo_controller/erizoClient/src/webrtc-stacks/ChromeStableStack.js @@ -1,6 +1,9 @@ import BaseStack from './BaseStack'; import SdpHelpers from './../utils/SdpHelpers'; import Logger from '../utils/Logger'; +import { addLegacySimulcast } from '../../lib/unifiedPlanUtils'; + +const sdpTransform = require('sdp-transform'); const log = Logger.module('ChromeStableStack'); @@ -29,6 +32,29 @@ const ChromeStableStack = (specInput) => { } }; + if (specInput.svc) { + that.peerConnection.onnegotiationneeded = async () => { + // This is for testing the negotiation step by step + if (specInput.managed) { + return; + } + try { + let offer = await that.peerConnection.createOffer(); + const localOffer = sdpTransform.parse(offer.sdp); + const offerMediaObject = localOffer.media[localOffer.media.length - 1]; + addLegacySimulcast({ offerMediaObject, numStreams: 3 }); + offer = { type: 'offer', sdp: sdpTransform.write(localOffer) }; + await that.peerConnection.setLocalDescription(offer); + spec.callback({ + type: that.peerConnection.localDescription.type, + sdp: that.peerConnection.localDescription.sdp, + }); + } catch (e) { + log.error('onnegotiationneeded - error', e.message); + } + }; + } + return that; }; diff --git a/extras/basic_example/public/index.html b/extras/basic_example/public/index.html index 42d290acc..79d77d804 100644 --- a/extras/basic_example/public/index.html +++ b/extras/basic_example/public/index.html @@ -10,6 +10,7 @@ +
diff --git a/extras/basic_example/public/script.js b/extras/basic_example/public/script.js index 6356d89c3..752b5f93b 100644 --- a/extras/basic_example/public/script.js +++ b/extras/basic_example/public/script.js @@ -9,6 +9,7 @@ let localStream; let room; let localStreamIndex = 0; const localStreams = new Map(); +let layerControlsActive = false; const configFlags = { noStart: false, // disable start button when only subscribe forceStart: false, // force start button in all cases @@ -30,20 +31,48 @@ const createSubscriberContainer = (stream) => { container.setAttribute('style', 'width: 320px; height: 280px;float:left;'); container.setAttribute('id', `container_${stream.getID()}`); + const layerControlDiv = document.createElement('div'); + layerControlDiv.setAttribute('class', 'controlDiv'); + layerControlDiv.hidden = !layerControlsActive; + const videoContainer = document.createElement('div'); videoContainer.setAttribute('style', 'width: 320px; height: 240px;'); videoContainer.setAttribute('id', `test${stream.getID()}`); container.appendChild(videoContainer); + const unsubscribeButton = document.createElement('button'); unsubscribeButton.textContent = 'Unsubscribe'; unsubscribeButton.setAttribute('style', 'float:left;'); + const slideshowButton = document.createElement('button'); slideshowButton.textContent = 'Toggle Slideshow'; slideshowButton.setAttribute('style', 'float:left;'); stream.slideshowMode = false; - container.appendChild(unsubscribeButton); container.appendChild(slideshowButton); + + const layerChangeButton = document.createElement('button'); + layerChangeButton.textContent = 'Change Layer'; + layerChangeButton.onclick = () => { + // eslint-disable-next-line no-use-before-define + changeLayer(stream, sLayer.value, tLayer.value); + }; + + const tLayer = document.createElement('input'); + tLayer.setAttribute('type', 'number'); + tLayer.setAttribute('id', `tLayer${stream.getID()}`); + tLayer.setAttribute('placeholder', 'Temporal Layer'); + + const sLayer = document.createElement('input'); + sLayer.setAttribute('type', 'number'); + sLayer.setAttribute('id', `sLayer${stream.getID()}`); + sLayer.setAttribute('placeholder', 'Spatial Layer'); + + layerControlDiv.appendChild(sLayer); + layerControlDiv.appendChild(tLayer); + layerControlDiv.appendChild(layerChangeButton); + container.appendChild(layerControlDiv); + unsubscribeButton.onclick = () => { room.unsubscribe(stream); document.getElementById('videoContainer').removeChild(container); @@ -100,7 +129,6 @@ const createPublisherContainer = (stream, index) => { stopRecordButton.hidden = true; }; - const div = document.createElement('div'); div.setAttribute('style', 'width: 320px; height: 240px; float:left'); div.setAttribute('id', `myVideo${index}`); @@ -335,6 +363,22 @@ const startBasicExample = () => { }); }; +// eslint-disable-next-line no-unused-vars +const changeLayer = (stream, s, t) => { + stream._setStaticQualityLayer(s, t); +}; + +// eslint-disable-next-line no-unused-vars +const toggleLayerControls = () => { + layerControlsActive = !layerControlsActive; + // eslint-disable-next-line no-return-assign + const list = document.getElementsByClassName('controlDiv'); + // eslint-disable-next-line no-restricted-syntax + for (const item of list) { + item.hidden = !layerControlsActive; + } +}; + window.onload = () => { fillInConfigFlagsFromParameters(configFlags); window.configFlags = configFlags;