diff --git a/.gitignore b/.gitignore index 26fece0..89b03c0 100644 --- a/.gitignore +++ b/.gitignore @@ -75,6 +75,9 @@ typings/ .env .env.test +# mac +.DS_Store + # parcel-bundler cache (https://parceljs.org/) .cache diff --git a/.prettierignore b/.prettierignore index a80b18f..4231c41 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,4 @@ README.md -*.min.js \ No newline at end of file +*.min.js +*.min.css \ No newline at end of file diff --git a/README.md b/README.md index b7144da..a290b1a 100644 --- a/README.md +++ b/README.md @@ -2,22 +2,26 @@ Example website for multi-party video/audio/screen conferencing using mediasoup. This project is intended to better understand how mediasoup works with a simple example. -This project is featured on the [mediasoup examples page](https://mediasoup.org/documentation/examples/) with many other examples. Checkout mediasoup at [mediasoup.org](https://mediasoup.org) +This project is featured on the [mediasoup examples page](https://mediasoup.org/documentation/examples/) with many other examples. Checkout mediasoup at [mediasoup.org](https://mediasoup.org). -# Running the code +## Running the code - run `npm install` then `npm start` to run the application. Then open your browser at `https://localhost:3016` or your own defined port/url in the config file. + - (optional) edit the `src/config.js` file according to your needs and replace the `ssl/key.pem ssl/cert.pem` certificates with your own. -# Deployment +## Deployment - in `config.js` replace the `announcedIP` with your public ip address of the server and modify the port you want to serve it in. + - add firewall rules of the port of the webpage (default 3016) and the rtc connections (default udp 10000-10100) for the machine. -# Pull Requests +## Pull Requests + +- Please run `npm run lint` before to make a PR. -- Please run `npx prettier --write .` before to make a PR. +## Notes -notes : Best to run the project on a linux system as the mediasoup installation could have issues by installing on windows. If you have a windows system consider installing WSL to be able to run it. +Best to run the project on a linux system as the mediasoup installation could have issues by installing on windows. -[installing wsl on windows 10](https://docs.microsoft.com/en-us/windows/wsl/install-win10) +More details regarding mediasoup requirements can be found [here](https://mediasoup.org/documentation/v3/mediasoup/installation/#requirements). diff --git a/package.json b/package.json index 0c15b88..e557325 100644 --- a/package.json +++ b/package.json @@ -15,16 +15,16 @@ "docker-stop": "docker stop dirvann-mediasoup-rooms", "compile-mediasoup-client": "npx browserify mediasoup-client-compile.js -o public/modules/mediasoupclient.min.js" }, - "author": "", + "author": "Dirk Vanbeveren & Miroslav Pejic", "license": "ISC", "dependencies": { - "express": "^4.17.1", + "express": "^4.18.2", "httpolyglot": "^0.1.2", - "mediasoup": "^3.8.2", - "mediasoup-client": "^3.6.37", - "socket.io": "^4.1.3" + "mediasoup": "^3.11.11", + "mediasoup-client": "^3.6.82", + "socket.io": "^4.6.1" }, "devDependencies": { - "prettier": "2.3.2" + "prettier": "2.8.4" } } diff --git a/public/RoomClient.js b/public/RoomClient.js index 29d11cd..9a69a9e 100644 --- a/public/RoomClient.js +++ b/public/RoomClient.js @@ -86,6 +86,8 @@ class RoomClient { this.device = device await this.initTransports(device) this.socket.emit('getProducers') + startVideoButton.click() + startAudioButton.click() }.bind(this) ) .catch((err) => { @@ -290,9 +292,6 @@ class RoomClient { ideal: 1080 }, deviceId: deviceId - /*aspectRatio: { - ideal: 1.7777777778 - }*/ } } break @@ -328,18 +327,17 @@ class RoomClient { { rid: 'r0', maxBitrate: 100000, - //scaleResolutionDownBy: 10.0, - scalabilityMode: 'S1T3' + scalabilityMode: 'L3T3' }, { rid: 'r1', maxBitrate: 300000, - scalabilityMode: 'S1T3' + scalabilityMode: 'L3T3' }, { rid: 'r2', maxBitrate: 900000, - scalabilityMode: 'S1T3' + scalabilityMode: 'L3T3' } ] params.codecOptions = { @@ -521,6 +519,11 @@ class RoomClient { } } + closeThenProduce(type, deviceId) { + this.closeProducer(type) + this.produce(type, deviceId) + } + pauseProducer(type) { if (!this.producerLabel.has(type)) { console.log('There is no producer for this type ' + type) @@ -616,9 +619,10 @@ class RoomClient { document.body.appendChild(tmpInput) tmpInput.value = window.location.href tmpInput.select() - document.execCommand('copy') + tmpInput.setSelectionRange(0, 99999) // For mobile devices + navigator.clipboard.writeText(tmpInput.value) document.body.removeChild(tmpInput) - console.log('URL copied to clipboard 👍') + alert('ROOM URL copied to clipboard 👍') } showDevices() { diff --git a/public/index.html b/public/index.html index be3e730..b515929 100644 --- a/public/index.html +++ b/public/index.html @@ -68,10 +68,20 @@


diff --git a/public/index.js b/public/index.js index 28ffcfb..59c9313 100644 --- a/public/index.js +++ b/public/index.js @@ -4,6 +4,8 @@ const socket = io() let producer = null +let isEnumerateDevices = false + nameInput.value = 'user_' + Math.round(Math.random() * 1000) socket.request = function request(type, data = {}) { @@ -93,8 +95,6 @@ function addListeners() { }) } -let isEnumerateDevices = false - function initEnumerateDevices() { // Many browsers, without the consent of getUserMedia, cannot enumerate the devices. if (isEnumerateDevices) return diff --git a/public/modules/mediasoupclient.min.js b/public/modules/mediasoupclient.min.js index 7b8efd0..02ad4e7 100644 --- a/public/modules/mediasoupclient.min.js +++ b/public/modules/mediasoupclient.min.js @@ -2,1179 +2,790 @@ const client = require('mediasoup-client') window.mediasoupClient = client -},{"mediasoup-client":35}],2:[function(require,module,exports){ + +},{"mediasoup-client":42}],2:[function(require,module,exports){ "use strict"; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); +exports.Logger = void 0; +const debug_1 = __importDefault(require("debug")); +const LIB_NAME = 'awaitqueue'; +class Logger { + constructor(prefix) { + if (prefix) { + this._debug = (0, debug_1.default)(`${LIB_NAME}:${prefix}`); + this._warn = (0, debug_1.default)(`${LIB_NAME}:WARN:${prefix}`); + this._error = (0, debug_1.default)(`${LIB_NAME}:ERROR:${prefix}`); + } + else { + this._debug = (0, debug_1.default)(LIB_NAME); + this._warn = (0, debug_1.default)(`${LIB_NAME}:WARN`); + this._error = (0, debug_1.default)(`${LIB_NAME}:ERROR`); + } + /* eslint-disable no-console */ + this._debug.log = console.info.bind(console); + this._warn.log = console.warn.bind(console); + this._error.log = console.error.bind(console); + /* eslint-enable no-console */ + } + get debug() { + return this._debug; + } + get warn() { + return this._warn; + } + get error() { + return this._error; + } +} +exports.Logger = Logger; + +},{"debug":4}],3:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.AwaitQueue = exports.AwaitQueueRemovedTaskError = exports.AwaitQueueStoppedError = void 0; +const Logger_1 = require("./Logger"); +const logger = new Logger_1.Logger(); +/** + * Custom Error derived class used to reject pending tasks once stop() method + * has been called. + */ +class AwaitQueueStoppedError extends Error { + constructor(message) { + super(message !== null && message !== void 0 ? message : 'AwaitQueue stopped'); + this.name = 'AwaitQueueStoppedError'; + // @ts-ignore + if (typeof Error.captureStackTrace === 'function') { + // @ts-ignore + Error.captureStackTrace(this, AwaitQueueStoppedError); + } + } +} +exports.AwaitQueueStoppedError = AwaitQueueStoppedError; +/** + * Custom Error derived class used to reject pending tasks once removeTask() + * method has been called. + */ +class AwaitQueueRemovedTaskError extends Error { + constructor(message) { + super(message !== null && message !== void 0 ? message : 'AwaitQueue task removed'); + this.name = 'AwaitQueueRemovedTaskError'; + // @ts-ignore + if (typeof Error.captureStackTrace === 'function') { + // @ts-ignore + Error.captureStackTrace(this, AwaitQueueRemovedTaskError); + } + } +} +exports.AwaitQueueRemovedTaskError = AwaitQueueRemovedTaskError; class AwaitQueue { - constructor({ ClosedErrorClass = Error, StoppedErrorClass = Error } = { - ClosedErrorClass: Error, - StoppedErrorClass: Error - }) { - // Closed flag. - this.closed = false; - // Queue of pending tasks. - this.pendingTasks = []; - // Error class used when rejecting a task due to AwaitQueue being closed. - this.ClosedErrorClass = Error; - // Error class used when rejecting a task due to AwaitQueue being stopped. - this.StoppedErrorClass = Error; - this.ClosedErrorClass = ClosedErrorClass; - this.StoppedErrorClass = StoppedErrorClass; + constructor() { + // Queue of pending tasks (map of PendingTasks indexed by id). + this.pendingTasks = new Map(); + // Incrementing PendingTask id. + this.nextTaskId = 0; + // Whether stop() method is stopping all pending tasks. + this.stopping = false; } - /** - * The number of ongoing enqueued tasks. - */ get size() { - return this.pendingTasks.length; + return this.pendingTasks.size; } - /** - * Closes the AwaitQueue. Pending tasks will be rejected with ClosedErrorClass - * error. - */ - close() { - if (this.closed) - return; - this.closed = true; - for (const pendingTask of this.pendingTasks) { - pendingTask.stopped = true; - pendingTask.reject(new this.ClosedErrorClass('AwaitQueue closed')); + async push(task, name) { + name = name !== null && name !== void 0 ? name : task.name; + logger.debug(`push() [name:${name}]`); + if (typeof task !== 'function') { + throw new TypeError('given task is not a function'); } - // Enpty the pending tasks array. - this.pendingTasks.length = 0; - } - /** - * Accepts a task as argument (and an optional task name) and enqueues it after - * pending tasks. Once processed, the push() method resolves (or rejects) with - * the result returned by the given task. - * - * The given task must return a Promise or directly a value. - */ - push(task, name) { - return __awaiter(this, void 0, void 0, function* () { - if (this.closed) - throw new this.ClosedErrorClass('AwaitQueue closed'); - if (typeof task !== 'function') - throw new TypeError('given task is not a function'); - if (!task.name && name) { - try { - Object.defineProperty(task, 'name', { value: name }); + if (name) { + try { + Object.defineProperty(task, 'name', { value: name }); + } + catch (error) { } + } + return new Promise((resolve, reject) => { + const pendingTask = { + id: this.nextTaskId++, + task: task, + name: name, + enqueuedAt: Date.now(), + executedAt: undefined, + completed: false, + resolve: (result) => { + // pendingTask.resolve() can only be called in execute() method. Since + // resolve() was called it means that the task successfully completed. + // However the task may have been stopped before it completed (via + // stop() or remove()) so its completed flag was already set. If this + // is the case, abort here since next task (if any) is already being + // executed. + if (pendingTask.completed) { + return; + } + pendingTask.completed = true; + // Remove the task from the queue. + this.pendingTasks.delete(pendingTask.id); + logger.debug(`resolving task [name:${pendingTask.name}]`); + // Resolve the task with the obtained result. + resolve(result); + // Execute the next pending task (if any). + const [nextPendingTask] = this.pendingTasks.values(); + // NOTE: During the resolve() callback the user app may have interacted + // with the queue. For instance, the app may have pushed a task while + // the queue was empty so such a task is already being executed. If so, + // don't execute it twice. + if (nextPendingTask && !nextPendingTask.executedAt) { + void this.execute(nextPendingTask); + } + }, + reject: (error) => { + // pendingTask.reject() can be called within execute() method if the + // task completed with error. However it may have also been called in + // stop() or remove() methods (before or while being executed) so its + // completed flag was already set. If so, abort here since next task + // (if any) is already being executed. + if (pendingTask.completed) { + return; + } + pendingTask.completed = true; + // Remove the task from the queue. + this.pendingTasks.delete(pendingTask.id); + logger.debug(`rejecting task [name:${pendingTask.name}]: %s`, String(error)); + // Reject the task with the obtained error. + reject(error); + // Execute the next pending task (if any) unless stop() is running. + if (!this.stopping) { + const [nextPendingTask] = this.pendingTasks.values(); + // NOTE: During the reject() callback the user app may have interacted + // with the queue. For instance, the app may have pushed a task while + // the queue was empty so such a task is already being executed. If so, + // don't execute it twice. + if (nextPendingTask && !nextPendingTask.executedAt) { + void this.execute(nextPendingTask); + } + } } - catch (error) { } + }; + // Append task to the queue. + this.pendingTasks.set(pendingTask.id, pendingTask); + // And execute it if this is the only task in the queue. + if (this.pendingTasks.size === 1) { + void this.execute(pendingTask); } - return new Promise((resolve, reject) => { - const pendingTask = { - task, - name, - resolve, - reject, - stopped: false, - enqueuedAt: new Date(), - executedAt: undefined - }; - // Append task to the queue. - this.pendingTasks.push(pendingTask); - // And run it if this is the only task in the queue. - if (this.pendingTasks.length === 1) - this.next(); - }); }); } - /** - * Make ongoing pending tasks reject with the given StoppedErrorClass error. - * The AwaitQueue instance is still usable for future tasks added via push() - * method. - */ stop() { - if (this.closed) + logger.debug('stop()'); + this.stopping = true; + for (const pendingTask of this.pendingTasks.values()) { + logger.debug(`stop() | stopping task [name:${pendingTask.name}]`); + pendingTask.reject(new AwaitQueueStoppedError()); + } + this.stopping = false; + } + remove(taskIdx) { + logger.debug(`remove() [taskIdx:${taskIdx}]`); + const pendingTask = Array.from(this.pendingTasks.values())[taskIdx]; + if (!pendingTask) { + logger.debug(`stop() | no task with given idx [taskIdx:${taskIdx}]`); return; - for (const pendingTask of this.pendingTasks) { - pendingTask.stopped = true; - pendingTask.reject(new this.StoppedErrorClass('AwaitQueue stopped')); } - // Enpty the pending tasks array. - this.pendingTasks.length = 0; + pendingTask.reject(new AwaitQueueRemovedTaskError()); } dump() { - const now = new Date(); - return this.pendingTasks.map((pendingTask) => { - return { - task: pendingTask.task, - name: pendingTask.name, - enqueuedTime: pendingTask.executedAt - ? pendingTask.executedAt.getTime() - pendingTask.enqueuedAt.getTime() - : now.getTime() - pendingTask.enqueuedAt.getTime(), - executingTime: pendingTask.executedAt - ? now.getTime() - pendingTask.executedAt.getTime() - : 0 - }; - }); - } - next() { - return __awaiter(this, void 0, void 0, function* () { - // Take the first pending task. - const pendingTask = this.pendingTasks[0]; - if (!pendingTask) - return; - // Execute it. - yield this.executeTask(pendingTask); - // Remove the first pending task (the completed one) from the queue. - this.pendingTasks.shift(); - // And continue. - this.next(); - }); - } - executeTask(pendingTask) { - return __awaiter(this, void 0, void 0, function* () { - // If the task is stopped, ignore it. - if (pendingTask.stopped) - return; - pendingTask.executedAt = new Date(); - try { - const result = yield pendingTask.task(); - // If the task is stopped, ignore it. - if (pendingTask.stopped) - return; - // Resolve the task with the returned result (if any). - pendingTask.resolve(result); - } - catch (error) { - // If the task is stopped, ignore it. - if (pendingTask.stopped) - return; - // Reject the task with its own error. - pendingTask.reject(error); - } - }); + const now = Date.now(); + let idx = 0; + return Array.from(this.pendingTasks.values()).map((pendingTask) => ({ + idx: idx++, + task: pendingTask.task, + name: pendingTask.name, + enqueuedTime: pendingTask.executedAt + ? pendingTask.executedAt - pendingTask.enqueuedAt + : now - pendingTask.enqueuedAt, + executionTime: pendingTask.executedAt + ? now - pendingTask.executedAt + : 0 + })); + } + async execute(pendingTask) { + logger.debug(`execute() [name:${pendingTask.name}]`); + if (pendingTask.executedAt) { + throw new Error('task already being executed'); + } + pendingTask.executedAt = Date.now(); + try { + const result = await pendingTask.task(); + // Resolve the task with its resolved result (if any). + pendingTask.resolve(result); + } + catch (error) { + // Reject the task with its rejected error. + pendingTask.reject(error); + } } } exports.AwaitQueue = AwaitQueue; -},{}],3:[function(require,module,exports){ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.bowser=t():e.bowser=t()}(this,(function(){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var i=t[n]={i:n,l:!1,exports:{}};return e[n].call(i.exports,i,i.exports,r),i.l=!0,i.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)r.d(n,i,function(t){return e[t]}.bind(null,i));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=90)}({17:function(e,t,r){"use strict";t.__esModule=!0,t.default=void 0;var n=r(18),i=function(){function e(){}return e.getFirstMatch=function(e,t){var r=t.match(e);return r&&r.length>0&&r[1]||""},e.getSecondMatch=function(e,t){var r=t.match(e);return r&&r.length>1&&r[2]||""},e.matchAndReturnConst=function(e,t,r){if(e.test(t))return r},e.getWindowsVersionName=function(e){switch(e){case"NT":return"NT";case"XP":return"XP";case"NT 5.0":return"2000";case"NT 5.1":return"XP";case"NT 5.2":return"2003";case"NT 6.0":return"Vista";case"NT 6.1":return"7";case"NT 6.2":return"8";case"NT 6.3":return"8.1";case"NT 10.0":return"10";default:return}},e.getMacOSVersionName=function(e){var t=e.split(".").splice(0,2).map((function(e){return parseInt(e,10)||0}));if(t.push(0),10===t[0])switch(t[1]){case 5:return"Leopard";case 6:return"Snow Leopard";case 7:return"Lion";case 8:return"Mountain Lion";case 9:return"Mavericks";case 10:return"Yosemite";case 11:return"El Capitan";case 12:return"Sierra";case 13:return"High Sierra";case 14:return"Mojave";case 15:return"Catalina";default:return}},e.getAndroidVersionName=function(e){var t=e.split(".").splice(0,2).map((function(e){return parseInt(e,10)||0}));if(t.push(0),!(1===t[0]&&t[1]<5))return 1===t[0]&&t[1]<6?"Cupcake":1===t[0]&&t[1]>=6?"Donut":2===t[0]&&t[1]<2?"Eclair":2===t[0]&&2===t[1]?"Froyo":2===t[0]&&t[1]>2?"Gingerbread":3===t[0]?"Honeycomb":4===t[0]&&t[1]<1?"Ice Cream Sandwich":4===t[0]&&t[1]<4?"Jelly Bean":4===t[0]&&t[1]>=4?"KitKat":5===t[0]?"Lollipop":6===t[0]?"Marshmallow":7===t[0]?"Nougat":8===t[0]?"Oreo":9===t[0]?"Pie":void 0},e.getVersionPrecision=function(e){return e.split(".").length},e.compareVersions=function(t,r,n){void 0===n&&(n=!1);var i=e.getVersionPrecision(t),s=e.getVersionPrecision(r),a=Math.max(i,s),o=0,u=e.map([t,r],(function(t){var r=a-e.getVersionPrecision(t),n=t+new Array(r+1).join(".0");return e.map(n.split("."),(function(e){return new Array(20-e.length).join("0")+e})).reverse()}));for(n&&(o=a-Math.min(i,s)),a-=1;a>=o;){if(u[0][a]>u[1][a])return 1;if(u[0][a]===u[1][a]){if(a===o)return 0;a-=1}else if(u[0][a]1?i-1:0),a=1;a0){var a=Object.keys(r),u=o.default.find(a,(function(e){return t.isOS(e)}));if(u){var d=this.satisfies(r[u]);if(void 0!==d)return d}var c=o.default.find(a,(function(e){return t.isPlatform(e)}));if(c){var f=this.satisfies(r[c]);if(void 0!==f)return f}}if(s>0){var l=Object.keys(i),h=o.default.find(l,(function(e){return t.isBrowser(e,!0)}));if(void 0!==h)return this.compareVersion(i[h])}},t.isBrowser=function(e,t){void 0===t&&(t=!1);var r=this.getBrowserName().toLowerCase(),n=e.toLowerCase(),i=o.default.getBrowserTypeByAlias(n);return t&&i&&(n=i.toLowerCase()),n===r},t.compareVersion=function(e){var t=[0],r=e,n=!1,i=this.getBrowserVersion();if("string"==typeof i)return">"===e[0]||"<"===e[0]?(r=e.substr(1),"="===e[1]?(n=!0,r=e.substr(2)):t=[],">"===e[0]?t.push(1):t.push(-1)):"="===e[0]?r=e.substr(1):"~"===e[0]&&(n=!0,r=e.substr(1)),t.indexOf(o.default.compareVersions(i,r,n))>-1},t.isOS=function(e){return this.getOSName(!0)===String(e).toLowerCase()},t.isPlatform=function(e){return this.getPlatformType(!0)===String(e).toLowerCase()},t.isEngine=function(e){return this.getEngineName(!0)===String(e).toLowerCase()},t.is=function(e,t){return void 0===t&&(t=!1),this.isBrowser(e,t)||this.isOS(e)||this.isPlatform(e)},t.some=function(e){var t=this;return void 0===e&&(e=[]),e.some((function(e){return t.is(e)}))},e}();t.default=d,e.exports=t.default},92:function(e,t,r){"use strict";t.__esModule=!0,t.default=void 0;var n,i=(n=r(17))&&n.__esModule?n:{default:n};var s=/version\/(\d+(\.?_?\d+)+)/i,a=[{test:[/googlebot/i],describe:function(e){var t={name:"Googlebot"},r=i.default.getFirstMatch(/googlebot\/(\d+(\.\d+))/i,e)||i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/opera/i],describe:function(e){var t={name:"Opera"},r=i.default.getFirstMatch(s,e)||i.default.getFirstMatch(/(?:opera)[\s/](\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/opr\/|opios/i],describe:function(e){var t={name:"Opera"},r=i.default.getFirstMatch(/(?:opr|opios)[\s/](\S+)/i,e)||i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/SamsungBrowser/i],describe:function(e){var t={name:"Samsung Internet for Android"},r=i.default.getFirstMatch(s,e)||i.default.getFirstMatch(/(?:SamsungBrowser)[\s/](\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/Whale/i],describe:function(e){var t={name:"NAVER Whale Browser"},r=i.default.getFirstMatch(s,e)||i.default.getFirstMatch(/(?:whale)[\s/](\d+(?:\.\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/MZBrowser/i],describe:function(e){var t={name:"MZ Browser"},r=i.default.getFirstMatch(/(?:MZBrowser)[\s/](\d+(?:\.\d+)+)/i,e)||i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/focus/i],describe:function(e){var t={name:"Focus"},r=i.default.getFirstMatch(/(?:focus)[\s/](\d+(?:\.\d+)+)/i,e)||i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/swing/i],describe:function(e){var t={name:"Swing"},r=i.default.getFirstMatch(/(?:swing)[\s/](\d+(?:\.\d+)+)/i,e)||i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/coast/i],describe:function(e){var t={name:"Opera Coast"},r=i.default.getFirstMatch(s,e)||i.default.getFirstMatch(/(?:coast)[\s/](\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/opt\/\d+(?:.?_?\d+)+/i],describe:function(e){var t={name:"Opera Touch"},r=i.default.getFirstMatch(/(?:opt)[\s/](\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/yabrowser/i],describe:function(e){var t={name:"Yandex Browser"},r=i.default.getFirstMatch(/(?:yabrowser)[\s/](\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/ucbrowser/i],describe:function(e){var t={name:"UC Browser"},r=i.default.getFirstMatch(s,e)||i.default.getFirstMatch(/(?:ucbrowser)[\s/](\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/Maxthon|mxios/i],describe:function(e){var t={name:"Maxthon"},r=i.default.getFirstMatch(s,e)||i.default.getFirstMatch(/(?:Maxthon|mxios)[\s/](\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/epiphany/i],describe:function(e){var t={name:"Epiphany"},r=i.default.getFirstMatch(s,e)||i.default.getFirstMatch(/(?:epiphany)[\s/](\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/puffin/i],describe:function(e){var t={name:"Puffin"},r=i.default.getFirstMatch(s,e)||i.default.getFirstMatch(/(?:puffin)[\s/](\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/sleipnir/i],describe:function(e){var t={name:"Sleipnir"},r=i.default.getFirstMatch(s,e)||i.default.getFirstMatch(/(?:sleipnir)[\s/](\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/k-meleon/i],describe:function(e){var t={name:"K-Meleon"},r=i.default.getFirstMatch(s,e)||i.default.getFirstMatch(/(?:k-meleon)[\s/](\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/micromessenger/i],describe:function(e){var t={name:"WeChat"},r=i.default.getFirstMatch(/(?:micromessenger)[\s/](\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/qqbrowser/i],describe:function(e){var t={name:/qqbrowserlite/i.test(e)?"QQ Browser Lite":"QQ Browser"},r=i.default.getFirstMatch(/(?:qqbrowserlite|qqbrowser)[/](\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/msie|trident/i],describe:function(e){var t={name:"Internet Explorer"},r=i.default.getFirstMatch(/(?:msie |rv:)(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/\sedg\//i],describe:function(e){var t={name:"Microsoft Edge"},r=i.default.getFirstMatch(/\sedg\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/edg([ea]|ios)/i],describe:function(e){var t={name:"Microsoft Edge"},r=i.default.getSecondMatch(/edg([ea]|ios)\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/vivaldi/i],describe:function(e){var t={name:"Vivaldi"},r=i.default.getFirstMatch(/vivaldi\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/seamonkey/i],describe:function(e){var t={name:"SeaMonkey"},r=i.default.getFirstMatch(/seamonkey\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/sailfish/i],describe:function(e){var t={name:"Sailfish"},r=i.default.getFirstMatch(/sailfish\s?browser\/(\d+(\.\d+)?)/i,e);return r&&(t.version=r),t}},{test:[/silk/i],describe:function(e){var t={name:"Amazon Silk"},r=i.default.getFirstMatch(/silk\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/phantom/i],describe:function(e){var t={name:"PhantomJS"},r=i.default.getFirstMatch(/phantomjs\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/slimerjs/i],describe:function(e){var t={name:"SlimerJS"},r=i.default.getFirstMatch(/slimerjs\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/blackberry|\bbb\d+/i,/rim\stablet/i],describe:function(e){var t={name:"BlackBerry"},r=i.default.getFirstMatch(s,e)||i.default.getFirstMatch(/blackberry[\d]+\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/(web|hpw)[o0]s/i],describe:function(e){var t={name:"WebOS Browser"},r=i.default.getFirstMatch(s,e)||i.default.getFirstMatch(/w(?:eb)?[o0]sbrowser\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/bada/i],describe:function(e){var t={name:"Bada"},r=i.default.getFirstMatch(/dolfin\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/tizen/i],describe:function(e){var t={name:"Tizen"},r=i.default.getFirstMatch(/(?:tizen\s?)?browser\/(\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/qupzilla/i],describe:function(e){var t={name:"QupZilla"},r=i.default.getFirstMatch(/(?:qupzilla)[\s/](\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/firefox|iceweasel|fxios/i],describe:function(e){var t={name:"Firefox"},r=i.default.getFirstMatch(/(?:firefox|iceweasel|fxios)[\s/](\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/electron/i],describe:function(e){var t={name:"Electron"},r=i.default.getFirstMatch(/(?:electron)\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/MiuiBrowser/i],describe:function(e){var t={name:"Miui"},r=i.default.getFirstMatch(/(?:MiuiBrowser)[\s/](\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/chromium/i],describe:function(e){var t={name:"Chromium"},r=i.default.getFirstMatch(/(?:chromium)[\s/](\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/chrome|crios|crmo/i],describe:function(e){var t={name:"Chrome"},r=i.default.getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/GSA/i],describe:function(e){var t={name:"Google Search"},r=i.default.getFirstMatch(/(?:GSA)\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:function(e){var t=!e.test(/like android/i),r=e.test(/android/i);return t&&r},describe:function(e){var t={name:"Android Browser"},r=i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/playstation 4/i],describe:function(e){var t={name:"PlayStation 4"},r=i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/safari|applewebkit/i],describe:function(e){var t={name:"Safari"},r=i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/.*/i],describe:function(e){var t=-1!==e.search("\\(")?/^(.*)\/(.*)[ \t]\((.*)/:/^(.*)\/(.*) /;return{name:i.default.getFirstMatch(t,e),version:i.default.getSecondMatch(t,e)}}}];t.default=a,e.exports=t.default},93:function(e,t,r){"use strict";t.__esModule=!0,t.default=void 0;var n,i=(n=r(17))&&n.__esModule?n:{default:n},s=r(18);var a=[{test:[/Roku\/DVP/],describe:function(e){var t=i.default.getFirstMatch(/Roku\/DVP-(\d+\.\d+)/i,e);return{name:s.OS_MAP.Roku,version:t}}},{test:[/windows phone/i],describe:function(e){var t=i.default.getFirstMatch(/windows phone (?:os)?\s?(\d+(\.\d+)*)/i,e);return{name:s.OS_MAP.WindowsPhone,version:t}}},{test:[/windows /i],describe:function(e){var t=i.default.getFirstMatch(/Windows ((NT|XP)( \d\d?.\d)?)/i,e),r=i.default.getWindowsVersionName(t);return{name:s.OS_MAP.Windows,version:t,versionName:r}}},{test:[/Macintosh(.*?) FxiOS(.*?)\//],describe:function(e){var t={name:s.OS_MAP.iOS},r=i.default.getSecondMatch(/(Version\/)(\d[\d.]+)/,e);return r&&(t.version=r),t}},{test:[/macintosh/i],describe:function(e){var t=i.default.getFirstMatch(/mac os x (\d+(\.?_?\d+)+)/i,e).replace(/[_\s]/g,"."),r=i.default.getMacOSVersionName(t),n={name:s.OS_MAP.MacOS,version:t};return r&&(n.versionName=r),n}},{test:[/(ipod|iphone|ipad)/i],describe:function(e){var t=i.default.getFirstMatch(/os (\d+([_\s]\d+)*) like mac os x/i,e).replace(/[_\s]/g,".");return{name:s.OS_MAP.iOS,version:t}}},{test:function(e){var t=!e.test(/like android/i),r=e.test(/android/i);return t&&r},describe:function(e){var t=i.default.getFirstMatch(/android[\s/-](\d+(\.\d+)*)/i,e),r=i.default.getAndroidVersionName(t),n={name:s.OS_MAP.Android,version:t};return r&&(n.versionName=r),n}},{test:[/(web|hpw)[o0]s/i],describe:function(e){var t=i.default.getFirstMatch(/(?:web|hpw)[o0]s\/(\d+(\.\d+)*)/i,e),r={name:s.OS_MAP.WebOS};return t&&t.length&&(r.version=t),r}},{test:[/blackberry|\bbb\d+/i,/rim\stablet/i],describe:function(e){var t=i.default.getFirstMatch(/rim\stablet\sos\s(\d+(\.\d+)*)/i,e)||i.default.getFirstMatch(/blackberry\d+\/(\d+([_\s]\d+)*)/i,e)||i.default.getFirstMatch(/\bbb(\d+)/i,e);return{name:s.OS_MAP.BlackBerry,version:t}}},{test:[/bada/i],describe:function(e){var t=i.default.getFirstMatch(/bada\/(\d+(\.\d+)*)/i,e);return{name:s.OS_MAP.Bada,version:t}}},{test:[/tizen/i],describe:function(e){var t=i.default.getFirstMatch(/tizen[/\s](\d+(\.\d+)*)/i,e);return{name:s.OS_MAP.Tizen,version:t}}},{test:[/linux/i],describe:function(){return{name:s.OS_MAP.Linux}}},{test:[/CrOS/],describe:function(){return{name:s.OS_MAP.ChromeOS}}},{test:[/PlayStation 4/],describe:function(e){var t=i.default.getFirstMatch(/PlayStation 4[/\s](\d+(\.\d+)*)/i,e);return{name:s.OS_MAP.PlayStation4,version:t}}}];t.default=a,e.exports=t.default},94:function(e,t,r){"use strict";t.__esModule=!0,t.default=void 0;var n,i=(n=r(17))&&n.__esModule?n:{default:n},s=r(18);var a=[{test:[/googlebot/i],describe:function(){return{type:"bot",vendor:"Google"}}},{test:[/huawei/i],describe:function(e){var t=i.default.getFirstMatch(/(can-l01)/i,e)&&"Nova",r={type:s.PLATFORMS_MAP.mobile,vendor:"Huawei"};return t&&(r.model=t),r}},{test:[/nexus\s*(?:7|8|9|10).*/i],describe:function(){return{type:s.PLATFORMS_MAP.tablet,vendor:"Nexus"}}},{test:[/ipad/i],describe:function(){return{type:s.PLATFORMS_MAP.tablet,vendor:"Apple",model:"iPad"}}},{test:[/Macintosh(.*?) FxiOS(.*?)\//],describe:function(){return{type:s.PLATFORMS_MAP.tablet,vendor:"Apple",model:"iPad"}}},{test:[/kftt build/i],describe:function(){return{type:s.PLATFORMS_MAP.tablet,vendor:"Amazon",model:"Kindle Fire HD 7"}}},{test:[/silk/i],describe:function(){return{type:s.PLATFORMS_MAP.tablet,vendor:"Amazon"}}},{test:[/tablet(?! pc)/i],describe:function(){return{type:s.PLATFORMS_MAP.tablet}}},{test:function(e){var t=e.test(/ipod|iphone/i),r=e.test(/like (ipod|iphone)/i);return t&&!r},describe:function(e){var t=i.default.getFirstMatch(/(ipod|iphone)/i,e);return{type:s.PLATFORMS_MAP.mobile,vendor:"Apple",model:t}}},{test:[/nexus\s*[0-6].*/i,/galaxy nexus/i],describe:function(){return{type:s.PLATFORMS_MAP.mobile,vendor:"Nexus"}}},{test:[/[^-]mobi/i],describe:function(){return{type:s.PLATFORMS_MAP.mobile}}},{test:function(e){return"blackberry"===e.getBrowserName(!0)},describe:function(){return{type:s.PLATFORMS_MAP.mobile,vendor:"BlackBerry"}}},{test:function(e){return"bada"===e.getBrowserName(!0)},describe:function(){return{type:s.PLATFORMS_MAP.mobile}}},{test:function(e){return"windows phone"===e.getBrowserName()},describe:function(){return{type:s.PLATFORMS_MAP.mobile,vendor:"Microsoft"}}},{test:function(e){var t=Number(String(e.getOSVersion()).split(".")[0]);return"android"===e.getOSName(!0)&&t>=3},describe:function(){return{type:s.PLATFORMS_MAP.tablet}}},{test:function(e){return"android"===e.getOSName(!0)},describe:function(){return{type:s.PLATFORMS_MAP.mobile}}},{test:function(e){return"macos"===e.getOSName(!0)},describe:function(){return{type:s.PLATFORMS_MAP.desktop,vendor:"Apple"}}},{test:function(e){return"windows"===e.getOSName(!0)},describe:function(){return{type:s.PLATFORMS_MAP.desktop}}},{test:function(e){return"linux"===e.getOSName(!0)},describe:function(){return{type:s.PLATFORMS_MAP.desktop}}},{test:function(e){return"playstation 4"===e.getOSName(!0)},describe:function(){return{type:s.PLATFORMS_MAP.tv}}},{test:function(e){return"roku"===e.getOSName(!0)},describe:function(){return{type:s.PLATFORMS_MAP.tv}}}];t.default=a,e.exports=t.default},95:function(e,t,r){"use strict";t.__esModule=!0,t.default=void 0;var n,i=(n=r(17))&&n.__esModule?n:{default:n},s=r(18);var a=[{test:function(e){return"microsoft edge"===e.getBrowserName(!0)},describe:function(e){if(/\sedg\//i.test(e))return{name:s.ENGINE_MAP.Blink};var t=i.default.getFirstMatch(/edge\/(\d+(\.?_?\d+)+)/i,e);return{name:s.ENGINE_MAP.EdgeHTML,version:t}}},{test:[/trident/i],describe:function(e){var t={name:s.ENGINE_MAP.Trident},r=i.default.getFirstMatch(/trident\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:function(e){return e.test(/presto/i)},describe:function(e){var t={name:s.ENGINE_MAP.Presto},r=i.default.getFirstMatch(/presto\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:function(e){var t=e.test(/gecko/i),r=e.test(/like gecko/i);return t&&!r},describe:function(e){var t={name:s.ENGINE_MAP.Gecko},r=i.default.getFirstMatch(/gecko\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/(apple)?webkit\/537\.36/i],describe:function(){return{name:s.ENGINE_MAP.Blink}}},{test:[/(apple)?webkit/i],describe:function(e){var t={name:s.ENGINE_MAP.WebKit},r=i.default.getFirstMatch(/webkit\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}}];t.default=a,e.exports=t.default}})})); -},{}],4:[function(require,module,exports){ -const debug = require('debug')('h264-profile-level-id'); - -/* eslint-disable no-console */ -debug.log = console.info.bind(console); -/* eslint-enable no-console */ - -const ProfileConstrainedBaseline = 1; -const ProfileBaseline = 2; -const ProfileMain = 3; -const ProfileConstrainedHigh = 4; -const ProfileHigh = 5; - -exports.ProfileConstrainedBaseline = ProfileConstrainedBaseline; -exports.ProfileBaseline = ProfileBaseline; -exports.ProfileMain = ProfileMain; -exports.ProfileConstrainedHigh = ProfileConstrainedHigh; -exports.ProfileHigh = ProfileHigh; - -// All values are equal to ten times the level number, except level 1b which is -// special. -const Level1_b = 0; -const Level1 = 10; -const Level1_1 = 11; -const Level1_2 = 12; -const Level1_3 = 13; -const Level2 = 20; -const Level2_1 = 21; -const Level2_2 = 22; -const Level3 = 30; -const Level3_1 = 31; -const Level3_2 = 32; -const Level4 = 40; -const Level4_1 = 41; -const Level4_2 = 42; -const Level5 = 50; -const Level5_1 = 51; -const Level5_2 = 52; - -exports.Level1_b = Level1_b; -exports.Level1 = Level1; -exports.Level1_1 = Level1_1; -exports.Level1_2 = Level1_2; -exports.Level1_3 = Level1_3; -exports.Level2 = Level2; -exports.Level2_1 = Level2_1; -exports.Level2_2 = Level2_2; -exports.Level3 = Level3; -exports.Level3_1 = Level3_1; -exports.Level3_2 = Level3_2; -exports.Level4 = Level4; -exports.Level4_1 = Level4_1; -exports.Level4_2 = Level4_2; -exports.Level5 = Level5; -exports.Level5_1 = Level5_1; -exports.Level5_2 = Level5_2; - -class ProfileLevelId -{ - constructor(profile, level) - { - this.profile = profile; - this.level = level; - } -} - -exports.ProfileLevelId = ProfileLevelId; - -// Default ProfileLevelId. -// -// TODO: The default should really be profile Baseline and level 1 according to -// the spec: https://tools.ietf.org/html/rfc6184#section-8.1. In order to not -// break backwards compatibility with older versions of WebRTC where external -// codecs don't have any parameters, use profile ConstrainedBaseline level 3_1 -// instead. This workaround will only be done in an interim period to allow -// external clients to update their code. -// -// http://crbug/webrtc/6337. -const DefaultProfileLevelId = - new ProfileLevelId(ProfileConstrainedBaseline, Level3_1); - -// For level_idc=11 and profile_idc=0x42, 0x4D, or 0x58, the constraint set3 -// flag specifies if level 1b or level 1.1 is used. -const ConstraintSet3Flag = 0x10; - -// Class for matching bit patterns such as "x1xx0000" where 'x' is allowed to be -// either 0 or 1. -class BitPattern -{ - constructor(str) - { - this._mask = ~byteMaskString('x', str); - this._maskedValue = byteMaskString('1', str); - } +},{"./Logger":2}],4:[function(require,module,exports){ +(function (process){(function (){ +/* eslint-env browser */ - isMatch(value) - { - return this._maskedValue === (value & this._mask); - } -} +/** + * This is the web browser implementation of `debug()`. + */ -// Class for converting between profile_idc/profile_iop to Profile. -class ProfilePattern -{ - constructor(profile_idc, profile_iop, profile) - { - this.profile_idc = profile_idc; - this.profile_iop = profile_iop; - this.profile = profile; - } -} +exports.formatArgs = formatArgs; +exports.save = save; +exports.load = load; +exports.useColors = useColors; +exports.storage = localstorage(); +exports.destroy = (() => { + let warned = false; -// This is from https://tools.ietf.org/html/rfc6184#section-8.1. -const ProfilePatterns = -[ - new ProfilePattern(0x42, new BitPattern('x1xx0000'), ProfileConstrainedBaseline), - new ProfilePattern(0x4D, new BitPattern('1xxx0000'), ProfileConstrainedBaseline), - new ProfilePattern(0x58, new BitPattern('11xx0000'), ProfileConstrainedBaseline), - new ProfilePattern(0x42, new BitPattern('x0xx0000'), ProfileBaseline), - new ProfilePattern(0x58, new BitPattern('10xx0000'), ProfileBaseline), - new ProfilePattern(0x4D, new BitPattern('0x0x0000'), ProfileMain), - new ProfilePattern(0x64, new BitPattern('00000000'), ProfileHigh), - new ProfilePattern(0x64, new BitPattern('00001100'), ProfileConstrainedHigh) -]; + return () => { + if (!warned) { + warned = true; + console.warn('Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.'); + } + }; +})(); /** - * Parse profile level id that is represented as a string of 3 hex bytes. - * Nothing will be returned if the string is not a recognized H264 profile - * level id. - * - * @param {String} str - profile-level-id value as a string of 3 hex bytes. - * - * @returns {ProfileLevelId} + * Colors. */ -exports.parseProfileLevelId = function(str) -{ - // The string should consist of 3 bytes in hexadecimal format. - if (typeof str !== 'string' || str.length !== 6) - return null; - const profile_level_id_numeric = parseInt(str, 16); - - if (profile_level_id_numeric === 0) - return null; - - // Separate into three bytes. - const level_idc = profile_level_id_numeric & 0xFF; - const profile_iop = (profile_level_id_numeric >> 8) & 0xFF; - const profile_idc = (profile_level_id_numeric >> 16) & 0xFF; - - // Parse level based on level_idc and constraint set 3 flag. - let level; +exports.colors = [ + '#0000CC', + '#0000FF', + '#0033CC', + '#0033FF', + '#0066CC', + '#0066FF', + '#0099CC', + '#0099FF', + '#00CC00', + '#00CC33', + '#00CC66', + '#00CC99', + '#00CCCC', + '#00CCFF', + '#3300CC', + '#3300FF', + '#3333CC', + '#3333FF', + '#3366CC', + '#3366FF', + '#3399CC', + '#3399FF', + '#33CC00', + '#33CC33', + '#33CC66', + '#33CC99', + '#33CCCC', + '#33CCFF', + '#6600CC', + '#6600FF', + '#6633CC', + '#6633FF', + '#66CC00', + '#66CC33', + '#9900CC', + '#9900FF', + '#9933CC', + '#9933FF', + '#99CC00', + '#99CC33', + '#CC0000', + '#CC0033', + '#CC0066', + '#CC0099', + '#CC00CC', + '#CC00FF', + '#CC3300', + '#CC3333', + '#CC3366', + '#CC3399', + '#CC33CC', + '#CC33FF', + '#CC6600', + '#CC6633', + '#CC9900', + '#CC9933', + '#CCCC00', + '#CCCC33', + '#FF0000', + '#FF0033', + '#FF0066', + '#FF0099', + '#FF00CC', + '#FF00FF', + '#FF3300', + '#FF3333', + '#FF3366', + '#FF3399', + '#FF33CC', + '#FF33FF', + '#FF6600', + '#FF6633', + '#FF9900', + '#FF9933', + '#FFCC00', + '#FFCC33' +]; - switch (level_idc) - { - case Level1_1: - { - level = (profile_iop & ConstraintSet3Flag) !== 0 ? Level1_b : Level1_1; - break; - } - case Level1: - case Level1_2: - case Level1_3: - case Level2: - case Level2_1: - case Level2_2: - case Level3: - case Level3_1: - case Level3_2: - case Level4: - case Level4_1: - case Level4_2: - case Level5: - case Level5_1: - case Level5_2: - { - level = level_idc; - break; - } - // Unrecognized level_idc. - default: - { - debug('parseProfileLevelId() | unrecognized level_idc:%s', level_idc); +/** + * Currently only WebKit-based Web Inspectors, Firefox >= v31, + * and the Firebug extension (any Firefox version) are known + * to support "%c" CSS customizations. + * + * TODO: add a `localStorage` variable to explicitly enable/disable colors + */ - return null; - } +// eslint-disable-next-line complexity +function useColors() { + // NB: In an Electron preload script, document will be defined but not fully + // initialized. Since we know we're in Chrome, we'll just detect this case + // explicitly + if (typeof window !== 'undefined' && window.process && (window.process.type === 'renderer' || window.process.__nwjs)) { + return true; } - // Parse profile_idc/profile_iop into a Profile enum. - for (const pattern of ProfilePatterns) - { - if ( - profile_idc === pattern.profile_idc && - pattern.profile_iop.isMatch(profile_iop) - ) - { - return new ProfileLevelId(pattern.profile, level); - } + // Internet Explorer and Edge do not support colors. + if (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/)) { + return false; } - debug('parseProfileLevelId() | unrecognized profile_idc/profile_iop combination'); - - return null; -}; + // Is webkit? http://stackoverflow.com/a/16459606/376773 + // document is undefined in react-native: https://github.com/facebook/react-native/pull/1632 + return (typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance) || + // Is firebug? http://stackoverflow.com/a/398120/376773 + (typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) || + // Is firefox >= v31? + // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages + (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31) || + // Double check webkit in userAgent just in case we are in a worker + (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)); +} /** - * Returns canonical string representation as three hex bytes of the profile - * level id, or returns nothing for invalid profile level ids. - * - * @param {ProfileLevelId} profile_level_id + * Colorize log arguments if enabled. * - * @returns {String} + * @api public */ -exports.profileLevelIdToString = function(profile_level_id) -{ - // Handle special case level == 1b. - if (profile_level_id.level == Level1_b) - { - switch (profile_level_id.profile) - { - case ProfileConstrainedBaseline: - { - return '42f00b'; - } - case ProfileBaseline: - { - return '42100b'; - } - case ProfileMain: - { - return '4d100b'; - } - // Level 1_b is not allowed for other profiles. - default: - { - debug( - 'profileLevelIdToString() | Level 1_b not is allowed for profile:%s', - profile_level_id.profile); - return null; - } - } +function formatArgs(args) { + args[0] = (this.useColors ? '%c' : '') + + this.namespace + + (this.useColors ? ' %c' : ' ') + + args[0] + + (this.useColors ? '%c ' : ' ') + + '+' + module.exports.humanize(this.diff); + + if (!this.useColors) { + return; } - let profile_idc_iop_string; + const c = 'color: ' + this.color; + args.splice(1, 0, c, 'color: inherit'); - switch (profile_level_id.profile) - { - case ProfileConstrainedBaseline: - { - profile_idc_iop_string = '42e0'; - break; - } - case ProfileBaseline: - { - profile_idc_iop_string = '4200'; - break; - } - case ProfileMain: - { - profile_idc_iop_string = '4d00'; - break; - } - case ProfileConstrainedHigh: - { - profile_idc_iop_string = '640c'; - break; - } - case ProfileHigh: - { - profile_idc_iop_string = '6400'; - break; + // The final "%c" is somewhat tricky, because there could be other + // arguments passed either before or after the %c, so we need to + // figure out the correct index to insert the CSS into + let index = 0; + let lastC = 0; + args[0].replace(/%[a-zA-Z%]/g, match => { + if (match === '%%') { + return; } - default: - { - debug( - 'profileLevelIdToString() | unrecognized profile:%s', - profile_level_id.profile); - - return null; + index++; + if (match === '%c') { + // We only are interested in the *last* %c + // (the user may have provided their own) + lastC = index; } - } + }); - let levelStr = (profile_level_id.level).toString(16); - - if (levelStr.length === 1) - levelStr = `0${levelStr}`; - - return `${profile_idc_iop_string}${levelStr}`; -}; + args.splice(lastC, 0, c); +} /** - * Parse profile level id that is represented as a string of 3 hex bytes - * contained in an SDP key-value map. A default profile level id will be - * returned if the profile-level-id key is missing. Nothing will be returned if - * the key is present but the string is invalid. - * - * @param {Object} [params={}] - Codec parameters object. + * Invokes `console.debug()` when available. + * No-op when `console.debug` is not a "function". + * If `console.debug` is not available, falls back + * to `console.log`. * - * @returns {ProfileLevelId} + * @api public */ -exports.parseSdpProfileLevelId = function(params = {}) -{ - const profile_level_id = params['profile-level-id']; - - return !profile_level_id - ? DefaultProfileLevelId - : exports.parseProfileLevelId(profile_level_id); -}; +exports.log = console.debug || console.log || (() => {}); /** - * Returns true if the parameters have the same H264 profile, i.e. the same - * H264 profile (Baseline, High, etc). + * Save `namespaces`. * - * @param {Object} [params1={}] - Codec parameters object. - * @param {Object} [params2={}] - Codec parameters object. + * @param {String} namespaces + * @api private + */ +function save(namespaces) { + try { + if (namespaces) { + exports.storage.setItem('debug', namespaces); + } else { + exports.storage.removeItem('debug'); + } + } catch (error) { + // Swallow + // XXX (@Qix-) should we be logging these? + } +} + +/** + * Load `namespaces`. * - * @returns {Boolean} + * @return {String} returns the previously persisted debug modes + * @api private */ -exports.isSameProfile = function(params1 = {}, params2 = {}) -{ - const profile_level_id_1 = exports.parseSdpProfileLevelId(params1); - const profile_level_id_2 = exports.parseSdpProfileLevelId(params2); +function load() { + let r; + try { + r = exports.storage.getItem('debug'); + } catch (error) { + // Swallow + // XXX (@Qix-) should we be logging these? + } - // Compare H264 profiles, but not levels. - return Boolean( - profile_level_id_1 && - profile_level_id_2 && - profile_level_id_1.profile === profile_level_id_2.profile - ); -}; + // If debug isn't set in LS, and we're in Electron, try to load $DEBUG + if (!r && typeof process !== 'undefined' && 'env' in process) { + r = process.env.DEBUG; + } + + return r; +} /** - * Generate codec parameters that will be used as answer in an SDP negotiation - * based on local supported parameters and remote offered parameters. Both - * local_supported_params and remote_offered_params represent sendrecv media - * descriptions, i.e they are a mix of both encode and decode capabilities. In - * theory, when the profile in local_supported_params represent a strict superset - * of the profile in remote_offered_params, we could limit the profile in the - * answer to the profile in remote_offered_params. - * - * However, to simplify the code, each supported H264 profile should be listed - * explicitly in the list of local supported codecs, even if they are redundant. - * Then each local codec in the list should be tested one at a time against the - * remote codec, and only when the profiles are equal should this function be - * called. Therefore, this function does not need to handle profile intersection, - * and the profile of local_supported_params and remote_offered_params must be - * equal before calling this function. The parameters that are used when - * negotiating are the level part of profile-level-id and level-asymmetry-allowed. - * - * @param {Object} [local_supported_params={}] - * @param {Object} [remote_offered_params={}] + * Localstorage attempts to return the localstorage. * - * @returns {String} Canonical string representation as three hex bytes of the - * profile level id, or null if no one of the params have profile-level-id. + * This is necessary because safari throws + * when a user disables cookies/localstorage + * and you attempt to access it. * - * @throws {TypeError} If Profile mismatch or invalid params. + * @return {LocalStorage} + * @api private */ -exports.generateProfileLevelIdForAnswer = function( - local_supported_params = {}, - remote_offered_params = {} -) -{ - // If both local and remote params do not contain profile-level-id, they are - // both using the default profile. In this case, don't return anything. - if ( - !local_supported_params['profile-level-id'] && - !remote_offered_params['profile-level-id'] - ) - { - debug( - 'generateProfileLevelIdForAnswer() | no profile-level-id in local and remote params'); - return null; +function localstorage() { + try { + // TVMLKit (Apple TV JS Runtime) does not have a window object, just localStorage in the global context + // The Browser also has localStorage in the global context. + return localStorage; + } catch (error) { + // Swallow + // XXX (@Qix-) should we be logging these? } +} - // Parse profile-level-ids. - const local_profile_level_id = - exports.parseSdpProfileLevelId(local_supported_params); - const remote_profile_level_id = - exports.parseSdpProfileLevelId(remote_offered_params); +module.exports = require('./common')(exports); - // The local and remote codec must have valid and equal H264 Profiles. - if (!local_profile_level_id) - throw new TypeError('invalid local_profile_level_id'); +const {formatters} = module.exports; - if (!remote_profile_level_id) - throw new TypeError('invalid remote_profile_level_id'); +/** + * Map %j to `JSON.stringify()`, since no Web Inspectors do that by default. + */ - if (local_profile_level_id.profile !== remote_profile_level_id.profile) - throw new TypeError('H264 Profile mismatch'); +formatters.j = function (v) { + try { + return JSON.stringify(v); + } catch (error) { + return '[UnexpectedJSONParseError]: ' + error.message; + } +}; - // Parse level information. - const level_asymmetry_allowed = ( - isLevelAsymmetryAllowed(local_supported_params) && - isLevelAsymmetryAllowed(remote_offered_params) - ); +}).call(this)}).call(this,require('_process')) +},{"./common":5,"_process":56}],5:[function(require,module,exports){ - const local_level = local_profile_level_id.level; - const remote_level = remote_profile_level_id.level; - const min_level = minLevel(local_level, remote_level); +/** + * This is the common logic for both the Node.js and web browser + * implementations of `debug()`. + */ - // Determine answer level. When level asymmetry is not allowed, level upgrade - // is not allowed, i.e., the level in the answer must be equal to or lower - // than the level in the offer. - const answer_level = level_asymmetry_allowed ? local_level : min_level; +function setup(env) { + createDebug.debug = createDebug; + createDebug.default = createDebug; + createDebug.coerce = coerce; + createDebug.disable = disable; + createDebug.enable = enable; + createDebug.enabled = enabled; + createDebug.humanize = require('ms'); + createDebug.destroy = destroy; - debug( - 'generateProfileLevelIdForAnswer() | result: [profile:%s, level:%s]', - local_profile_level_id.profile, answer_level); + Object.keys(env).forEach(key => { + createDebug[key] = env[key]; + }); - // Return the resulting profile-level-id for the answer parameters. - return exports.profileLevelIdToString( - new ProfileLevelId(local_profile_level_id.profile, answer_level)); -}; + /** + * The currently active debug mode names, and names to skip. + */ -// Convert a string of 8 characters into a byte where the positions containing -// character c will have their bit set. For example, c = 'x', str = "x1xx0000" -// will return 0b10110000. -function byteMaskString(c, str) -{ - return ( - ((str[0] === c) << 7) | ((str[1] === c) << 6) | ((str[2] === c) << 5) | - ((str[3] === c) << 4) | ((str[4] === c) << 3) | ((str[5] === c) << 2) | - ((str[6] === c) << 1) | ((str[7] === c) << 0) - ); -} + createDebug.names = []; + createDebug.skips = []; -// Compare H264 levels and handle the level 1b case. -function isLessLevel(a, b) -{ - if (a === Level1_b) - return b !== Level1 && b !== Level1_b; + /** + * Map of special "%n" handling functions, for the debug "format" argument. + * + * Valid key names are a single, lower or upper-case letter, i.e. "n" and "N". + */ + createDebug.formatters = {}; - if (b === Level1_b) - return a !== Level1; + /** + * Selects a color for a debug namespace + * @param {String} namespace The namespace string for the debug instance to be colored + * @return {Number|String} An ANSI color code for the given namespace + * @api private + */ + function selectColor(namespace) { + let hash = 0; - return a < b; -} + for (let i = 0; i < namespace.length; i++) { + hash = ((hash << 5) - hash) + namespace.charCodeAt(i); + hash |= 0; // Convert to 32bit integer + } -function minLevel(a, b) -{ - return isLessLevel(a, b) ? a : b; -} + return createDebug.colors[Math.abs(hash) % createDebug.colors.length]; + } + createDebug.selectColor = selectColor; -function isLevelAsymmetryAllowed(params = {}) -{ - const level_asymmetry_allowed = params['level-asymmetry-allowed']; + /** + * Create a debugger with the given `namespace`. + * + * @param {String} namespace + * @return {Function} + * @api public + */ + function createDebug(namespace) { + let prevTime; + let enableOverride = null; + let namespacesCache; + let enabledCache; - return ( - level_asymmetry_allowed === 1 || - level_asymmetry_allowed === '1' - ); -} + function debug(...args) { + // Disabled? + if (!debug.enabled) { + return; + } -},{"debug":5}],5:[function(require,module,exports){ -(function (process){(function (){ -/* eslint-env browser */ + const self = debug; -/** - * This is the web browser implementation of `debug()`. - */ + // Set `diff` timestamp + const curr = Number(new Date()); + const ms = curr - (prevTime || curr); + self.diff = ms; + self.prev = prevTime; + self.curr = curr; + prevTime = curr; -exports.formatArgs = formatArgs; -exports.save = save; -exports.load = load; -exports.useColors = useColors; -exports.storage = localstorage(); -exports.destroy = (() => { - let warned = false; + args[0] = createDebug.coerce(args[0]); - return () => { - if (!warned) { - warned = true; - console.warn('Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.'); + if (typeof args[0] !== 'string') { + // Anything else let's inspect with %O + args.unshift('%O'); + } + + // Apply any `formatters` transformations + let index = 0; + args[0] = args[0].replace(/%([a-zA-Z%])/g, (match, format) => { + // If we encounter an escaped % then don't increase the array index + if (match === '%%') { + return '%'; + } + index++; + const formatter = createDebug.formatters[format]; + if (typeof formatter === 'function') { + const val = args[index]; + match = formatter.call(self, val); + + // Now we need to remove `args[index]` since it's inlined in the `format` + args.splice(index, 1); + index--; + } + return match; + }); + + // Apply env-specific formatting (colors, etc.) + createDebug.formatArgs.call(self, args); + + const logFn = self.log || createDebug.log; + logFn.apply(self, args); } - }; -})(); -/** - * Colors. - */ + debug.namespace = namespace; + debug.useColors = createDebug.useColors(); + debug.color = createDebug.selectColor(namespace); + debug.extend = extend; + debug.destroy = createDebug.destroy; // XXX Temporary. Will be removed in the next major release. -exports.colors = [ - '#0000CC', - '#0000FF', - '#0033CC', - '#0033FF', - '#0066CC', - '#0066FF', - '#0099CC', - '#0099FF', - '#00CC00', - '#00CC33', - '#00CC66', - '#00CC99', - '#00CCCC', - '#00CCFF', - '#3300CC', - '#3300FF', - '#3333CC', - '#3333FF', - '#3366CC', - '#3366FF', - '#3399CC', - '#3399FF', - '#33CC00', - '#33CC33', - '#33CC66', - '#33CC99', - '#33CCCC', - '#33CCFF', - '#6600CC', - '#6600FF', - '#6633CC', - '#6633FF', - '#66CC00', - '#66CC33', - '#9900CC', - '#9900FF', - '#9933CC', - '#9933FF', - '#99CC00', - '#99CC33', - '#CC0000', - '#CC0033', - '#CC0066', - '#CC0099', - '#CC00CC', - '#CC00FF', - '#CC3300', - '#CC3333', - '#CC3366', - '#CC3399', - '#CC33CC', - '#CC33FF', - '#CC6600', - '#CC6633', - '#CC9900', - '#CC9933', - '#CCCC00', - '#CCCC33', - '#FF0000', - '#FF0033', - '#FF0066', - '#FF0099', - '#FF00CC', - '#FF00FF', - '#FF3300', - '#FF3333', - '#FF3366', - '#FF3399', - '#FF33CC', - '#FF33FF', - '#FF6600', - '#FF6633', - '#FF9900', - '#FF9933', - '#FFCC00', - '#FFCC33' -]; + Object.defineProperty(debug, 'enabled', { + enumerable: true, + configurable: false, + get: () => { + if (enableOverride !== null) { + return enableOverride; + } + if (namespacesCache !== createDebug.namespaces) { + namespacesCache = createDebug.namespaces; + enabledCache = createDebug.enabled(namespace); + } -/** - * Currently only WebKit-based Web Inspectors, Firefox >= v31, - * and the Firebug extension (any Firefox version) are known - * to support "%c" CSS customizations. - * - * TODO: add a `localStorage` variable to explicitly enable/disable colors - */ + return enabledCache; + }, + set: v => { + enableOverride = v; + } + }); -// eslint-disable-next-line complexity -function useColors() { - // NB: In an Electron preload script, document will be defined but not fully - // initialized. Since we know we're in Chrome, we'll just detect this case - // explicitly - if (typeof window !== 'undefined' && window.process && (window.process.type === 'renderer' || window.process.__nwjs)) { - return true; + // Env-specific initialization logic for debug instances + if (typeof createDebug.init === 'function') { + createDebug.init(debug); + } + + return debug; } - // Internet Explorer and Edge do not support colors. - if (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/)) { - return false; + function extend(namespace, delimiter) { + const newDebug = createDebug(this.namespace + (typeof delimiter === 'undefined' ? ':' : delimiter) + namespace); + newDebug.log = this.log; + return newDebug; } - // Is webkit? http://stackoverflow.com/a/16459606/376773 - // document is undefined in react-native: https://github.com/facebook/react-native/pull/1632 - return (typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance) || - // Is firebug? http://stackoverflow.com/a/398120/376773 - (typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) || - // Is firefox >= v31? - // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages - (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31) || - // Double check webkit in userAgent just in case we are in a worker - (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)); -} + /** + * Enables a debug mode by namespaces. This can include modes + * separated by a colon and wildcards. + * + * @param {String} namespaces + * @api public + */ + function enable(namespaces) { + createDebug.save(namespaces); + createDebug.namespaces = namespaces; -/** - * Colorize log arguments if enabled. - * - * @api public - */ + createDebug.names = []; + createDebug.skips = []; -function formatArgs(args) { - args[0] = (this.useColors ? '%c' : '') + - this.namespace + - (this.useColors ? ' %c' : ' ') + - args[0] + - (this.useColors ? '%c ' : ' ') + - '+' + module.exports.humanize(this.diff); + let i; + const split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/); + const len = split.length; - if (!this.useColors) { - return; - } + for (i = 0; i < len; i++) { + if (!split[i]) { + // ignore empty strings + continue; + } - const c = 'color: ' + this.color; - args.splice(1, 0, c, 'color: inherit'); + namespaces = split[i].replace(/\*/g, '.*?'); - // The final "%c" is somewhat tricky, because there could be other - // arguments passed either before or after the %c, so we need to - // figure out the correct index to insert the CSS into - let index = 0; - let lastC = 0; - args[0].replace(/%[a-zA-Z%]/g, match => { - if (match === '%%') { - return; + if (namespaces[0] === '-') { + createDebug.skips.push(new RegExp('^' + namespaces.slice(1) + '$')); + } else { + createDebug.names.push(new RegExp('^' + namespaces + '$')); + } } - index++; - if (match === '%c') { - // We only are interested in the *last* %c - // (the user may have provided their own) - lastC = index; - } - }); - - args.splice(lastC, 0, c); -} - -/** - * Invokes `console.debug()` when available. - * No-op when `console.debug` is not a "function". - * If `console.debug` is not available, falls back - * to `console.log`. - * - * @api public - */ -exports.log = console.debug || console.log || (() => {}); - -/** - * Save `namespaces`. - * - * @param {String} namespaces - * @api private - */ -function save(namespaces) { - try { - if (namespaces) { - exports.storage.setItem('debug', namespaces); - } else { - exports.storage.removeItem('debug'); - } - } catch (error) { - // Swallow - // XXX (@Qix-) should we be logging these? - } -} - -/** - * Load `namespaces`. - * - * @return {String} returns the previously persisted debug modes - * @api private - */ -function load() { - let r; - try { - r = exports.storage.getItem('debug'); - } catch (error) { - // Swallow - // XXX (@Qix-) should we be logging these? } - // If debug isn't set in LS, and we're in Electron, try to load $DEBUG - if (!r && typeof process !== 'undefined' && 'env' in process) { - r = process.env.DEBUG; + /** + * Disable debug output. + * + * @return {String} namespaces + * @api public + */ + function disable() { + const namespaces = [ + ...createDebug.names.map(toNamespace), + ...createDebug.skips.map(toNamespace).map(namespace => '-' + namespace) + ].join(','); + createDebug.enable(''); + return namespaces; } - return r; -} - -/** - * Localstorage attempts to return the localstorage. - * - * This is necessary because safari throws - * when a user disables cookies/localstorage - * and you attempt to access it. - * - * @return {LocalStorage} - * @api private - */ - -function localstorage() { - try { - // TVMLKit (Apple TV JS Runtime) does not have a window object, just localStorage in the global context - // The Browser also has localStorage in the global context. - return localStorage; - } catch (error) { - // Swallow - // XXX (@Qix-) should we be logging these? - } -} + /** + * Returns true if the given mode name is enabled, false otherwise. + * + * @param {String} name + * @return {Boolean} + * @api public + */ + function enabled(name) { + if (name[name.length - 1] === '*') { + return true; + } -module.exports = require('./common')(exports); + let i; + let len; -const {formatters} = module.exports; + for (i = 0, len = createDebug.skips.length; i < len; i++) { + if (createDebug.skips[i].test(name)) { + return false; + } + } -/** - * Map %j to `JSON.stringify()`, since no Web Inspectors do that by default. - */ + for (i = 0, len = createDebug.names.length; i < len; i++) { + if (createDebug.names[i].test(name)) { + return true; + } + } -formatters.j = function (v) { - try { - return JSON.stringify(v); - } catch (error) { - return '[UnexpectedJSONParseError]: ' + error.message; + return false; } -}; - -}).call(this)}).call(this,require('_process')) -},{"./common":6,"_process":48}],6:[function(require,module,exports){ - -/** - * This is the common logic for both the Node.js and web browser - * implementations of `debug()`. - */ - -function setup(env) { - createDebug.debug = createDebug; - createDebug.default = createDebug; - createDebug.coerce = coerce; - createDebug.disable = disable; - createDebug.enable = enable; - createDebug.enabled = enabled; - createDebug.humanize = require('ms'); - createDebug.destroy = destroy; - - Object.keys(env).forEach(key => { - createDebug[key] = env[key]; - }); - - /** - * The currently active debug mode names, and names to skip. - */ - - createDebug.names = []; - createDebug.skips = []; /** - * Map of special "%n" handling functions, for the debug "format" argument. + * Convert regexp to namespace * - * Valid key names are a single, lower or upper-case letter, i.e. "n" and "N". + * @param {RegExp} regxep + * @return {String} namespace + * @api private */ - createDebug.formatters = {}; + function toNamespace(regexp) { + return regexp.toString() + .substring(2, regexp.toString().length - 2) + .replace(/\.\*\?$/, '*'); + } /** - * Selects a color for a debug namespace - * @param {String} namespace The namespace string for the for the debug instance to be colored - * @return {Number|String} An ANSI color code for the given namespace + * Coerce `val`. + * + * @param {Mixed} val + * @return {Mixed} * @api private */ - function selectColor(namespace) { - let hash = 0; - - for (let i = 0; i < namespace.length; i++) { - hash = ((hash << 5) - hash) + namespace.charCodeAt(i); - hash |= 0; // Convert to 32bit integer + function coerce(val) { + if (val instanceof Error) { + return val.stack || val.message; } - - return createDebug.colors[Math.abs(hash) % createDebug.colors.length]; + return val; } - createDebug.selectColor = selectColor; /** - * Create a debugger with the given `namespace`. - * - * @param {String} namespace - * @return {Function} - * @api public + * XXX DO NOT USE. This is a temporary stub function. + * XXX It WILL be removed in the next major release. */ - function createDebug(namespace) { - let prevTime; - let enableOverride = null; - let namespacesCache; - let enabledCache; - - function debug(...args) { - // Disabled? - if (!debug.enabled) { - return; - } - - const self = debug; - - // Set `diff` timestamp - const curr = Number(new Date()); - const ms = curr - (prevTime || curr); - self.diff = ms; - self.prev = prevTime; - self.curr = curr; - prevTime = curr; + function destroy() { + console.warn('Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.'); + } - args[0] = createDebug.coerce(args[0]); + createDebug.enable(createDebug.load()); - if (typeof args[0] !== 'string') { - // Anything else let's inspect with %O - args.unshift('%O'); - } + return createDebug; +} - // Apply any `formatters` transformations - let index = 0; - args[0] = args[0].replace(/%([a-zA-Z%])/g, (match, format) => { - // If we encounter an escaped % then don't increase the array index - if (match === '%%') { - return '%'; - } - index++; - const formatter = createDebug.formatters[format]; - if (typeof formatter === 'function') { - const val = args[index]; - match = formatter.call(self, val); +module.exports = setup; - // Now we need to remove `args[index]` since it's inlined in the `format` - args.splice(index, 1); - index--; - } - return match; - }); +},{"ms":6}],6:[function(require,module,exports){ +/** + * Helpers. + */ - // Apply env-specific formatting (colors, etc.) - createDebug.formatArgs.call(self, args); - - const logFn = self.log || createDebug.log; - logFn.apply(self, args); - } - - debug.namespace = namespace; - debug.useColors = createDebug.useColors(); - debug.color = createDebug.selectColor(namespace); - debug.extend = extend; - debug.destroy = createDebug.destroy; // XXX Temporary. Will be removed in the next major release. - - Object.defineProperty(debug, 'enabled', { - enumerable: true, - configurable: false, - get: () => { - if (enableOverride !== null) { - return enableOverride; - } - if (namespacesCache !== createDebug.namespaces) { - namespacesCache = createDebug.namespaces; - enabledCache = createDebug.enabled(namespace); - } - - return enabledCache; - }, - set: v => { - enableOverride = v; - } - }); - - // Env-specific initialization logic for debug instances - if (typeof createDebug.init === 'function') { - createDebug.init(debug); - } - - return debug; - } - - function extend(namespace, delimiter) { - const newDebug = createDebug(this.namespace + (typeof delimiter === 'undefined' ? ':' : delimiter) + namespace); - newDebug.log = this.log; - return newDebug; - } - - /** - * Enables a debug mode by namespaces. This can include modes - * separated by a colon and wildcards. - * - * @param {String} namespaces - * @api public - */ - function enable(namespaces) { - createDebug.save(namespaces); - createDebug.namespaces = namespaces; - - createDebug.names = []; - createDebug.skips = []; - - let i; - const split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/); - const len = split.length; - - for (i = 0; i < len; i++) { - if (!split[i]) { - // ignore empty strings - continue; - } - - namespaces = split[i].replace(/\*/g, '.*?'); - - if (namespaces[0] === '-') { - createDebug.skips.push(new RegExp('^' + namespaces.substr(1) + '$')); - } else { - createDebug.names.push(new RegExp('^' + namespaces + '$')); - } - } - } - - /** - * Disable debug output. - * - * @return {String} namespaces - * @api public - */ - function disable() { - const namespaces = [ - ...createDebug.names.map(toNamespace), - ...createDebug.skips.map(toNamespace).map(namespace => '-' + namespace) - ].join(','); - createDebug.enable(''); - return namespaces; - } - - /** - * Returns true if the given mode name is enabled, false otherwise. - * - * @param {String} name - * @return {Boolean} - * @api public - */ - function enabled(name) { - if (name[name.length - 1] === '*') { - return true; - } - - let i; - let len; - - for (i = 0, len = createDebug.skips.length; i < len; i++) { - if (createDebug.skips[i].test(name)) { - return false; - } - } - - for (i = 0, len = createDebug.names.length; i < len; i++) { - if (createDebug.names[i].test(name)) { - return true; - } - } - - return false; - } - - /** - * Convert regexp to namespace - * - * @param {RegExp} regxep - * @return {String} namespace - * @api private - */ - function toNamespace(regexp) { - return regexp.toString() - .substring(2, regexp.toString().length - 2) - .replace(/\.\*\?$/, '*'); - } - - /** - * Coerce `val`. - * - * @param {Mixed} val - * @return {Mixed} - * @api private - */ - function coerce(val) { - if (val instanceof Error) { - return val.stack || val.message; - } - return val; - } - - /** - * XXX DO NOT USE. This is a temporary stub function. - * XXX It WILL be removed in the next major release. - */ - function destroy() { - console.warn('Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.'); - } - - createDebug.enable(createDebug.load()); - - return createDebug; -} - -module.exports = setup; - -},{"ms":7}],7:[function(require,module,exports){ -/** - * Helpers. - */ - -var s = 1000; -var m = s * 60; -var h = m * 60; -var d = h * 24; -var w = d * 7; -var y = d * 365.25; +var s = 1000; +var m = s * 60; +var h = m * 60; +var d = h * 24; +var w = d * 7; +var y = d * 365.25; /** * Parse or format the given `val`. @@ -1328,95 +939,1644 @@ function plural(ms, msAbs, n, name) { return Math.round(ms / n) + ' ' + name + (isPlural ? 's' : ''); } +},{}],7:[function(require,module,exports){ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.bowser=t():e.bowser=t()}(this,(function(){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var i=t[n]={i:n,l:!1,exports:{}};return e[n].call(i.exports,i,i.exports,r),i.l=!0,i.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)r.d(n,i,function(t){return e[t]}.bind(null,i));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=90)}({17:function(e,t,r){"use strict";t.__esModule=!0,t.default=void 0;var n=r(18),i=function(){function e(){}return e.getFirstMatch=function(e,t){var r=t.match(e);return r&&r.length>0&&r[1]||""},e.getSecondMatch=function(e,t){var r=t.match(e);return r&&r.length>1&&r[2]||""},e.matchAndReturnConst=function(e,t,r){if(e.test(t))return r},e.getWindowsVersionName=function(e){switch(e){case"NT":return"NT";case"XP":return"XP";case"NT 5.0":return"2000";case"NT 5.1":return"XP";case"NT 5.2":return"2003";case"NT 6.0":return"Vista";case"NT 6.1":return"7";case"NT 6.2":return"8";case"NT 6.3":return"8.1";case"NT 10.0":return"10";default:return}},e.getMacOSVersionName=function(e){var t=e.split(".").splice(0,2).map((function(e){return parseInt(e,10)||0}));if(t.push(0),10===t[0])switch(t[1]){case 5:return"Leopard";case 6:return"Snow Leopard";case 7:return"Lion";case 8:return"Mountain Lion";case 9:return"Mavericks";case 10:return"Yosemite";case 11:return"El Capitan";case 12:return"Sierra";case 13:return"High Sierra";case 14:return"Mojave";case 15:return"Catalina";default:return}},e.getAndroidVersionName=function(e){var t=e.split(".").splice(0,2).map((function(e){return parseInt(e,10)||0}));if(t.push(0),!(1===t[0]&&t[1]<5))return 1===t[0]&&t[1]<6?"Cupcake":1===t[0]&&t[1]>=6?"Donut":2===t[0]&&t[1]<2?"Eclair":2===t[0]&&2===t[1]?"Froyo":2===t[0]&&t[1]>2?"Gingerbread":3===t[0]?"Honeycomb":4===t[0]&&t[1]<1?"Ice Cream Sandwich":4===t[0]&&t[1]<4?"Jelly Bean":4===t[0]&&t[1]>=4?"KitKat":5===t[0]?"Lollipop":6===t[0]?"Marshmallow":7===t[0]?"Nougat":8===t[0]?"Oreo":9===t[0]?"Pie":void 0},e.getVersionPrecision=function(e){return e.split(".").length},e.compareVersions=function(t,r,n){void 0===n&&(n=!1);var i=e.getVersionPrecision(t),s=e.getVersionPrecision(r),a=Math.max(i,s),o=0,u=e.map([t,r],(function(t){var r=a-e.getVersionPrecision(t),n=t+new Array(r+1).join(".0");return e.map(n.split("."),(function(e){return new Array(20-e.length).join("0")+e})).reverse()}));for(n&&(o=a-Math.min(i,s)),a-=1;a>=o;){if(u[0][a]>u[1][a])return 1;if(u[0][a]===u[1][a]){if(a===o)return 0;a-=1}else if(u[0][a]1?i-1:0),a=1;a0){var a=Object.keys(r),u=o.default.find(a,(function(e){return t.isOS(e)}));if(u){var d=this.satisfies(r[u]);if(void 0!==d)return d}var c=o.default.find(a,(function(e){return t.isPlatform(e)}));if(c){var f=this.satisfies(r[c]);if(void 0!==f)return f}}if(s>0){var l=Object.keys(i),h=o.default.find(l,(function(e){return t.isBrowser(e,!0)}));if(void 0!==h)return this.compareVersion(i[h])}},t.isBrowser=function(e,t){void 0===t&&(t=!1);var r=this.getBrowserName().toLowerCase(),n=e.toLowerCase(),i=o.default.getBrowserTypeByAlias(n);return t&&i&&(n=i.toLowerCase()),n===r},t.compareVersion=function(e){var t=[0],r=e,n=!1,i=this.getBrowserVersion();if("string"==typeof i)return">"===e[0]||"<"===e[0]?(r=e.substr(1),"="===e[1]?(n=!0,r=e.substr(2)):t=[],">"===e[0]?t.push(1):t.push(-1)):"="===e[0]?r=e.substr(1):"~"===e[0]&&(n=!0,r=e.substr(1)),t.indexOf(o.default.compareVersions(i,r,n))>-1},t.isOS=function(e){return this.getOSName(!0)===String(e).toLowerCase()},t.isPlatform=function(e){return this.getPlatformType(!0)===String(e).toLowerCase()},t.isEngine=function(e){return this.getEngineName(!0)===String(e).toLowerCase()},t.is=function(e,t){return void 0===t&&(t=!1),this.isBrowser(e,t)||this.isOS(e)||this.isPlatform(e)},t.some=function(e){var t=this;return void 0===e&&(e=[]),e.some((function(e){return t.is(e)}))},e}();t.default=d,e.exports=t.default},92:function(e,t,r){"use strict";t.__esModule=!0,t.default=void 0;var n,i=(n=r(17))&&n.__esModule?n:{default:n};var s=/version\/(\d+(\.?_?\d+)+)/i,a=[{test:[/googlebot/i],describe:function(e){var t={name:"Googlebot"},r=i.default.getFirstMatch(/googlebot\/(\d+(\.\d+))/i,e)||i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/opera/i],describe:function(e){var t={name:"Opera"},r=i.default.getFirstMatch(s,e)||i.default.getFirstMatch(/(?:opera)[\s/](\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/opr\/|opios/i],describe:function(e){var t={name:"Opera"},r=i.default.getFirstMatch(/(?:opr|opios)[\s/](\S+)/i,e)||i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/SamsungBrowser/i],describe:function(e){var t={name:"Samsung Internet for Android"},r=i.default.getFirstMatch(s,e)||i.default.getFirstMatch(/(?:SamsungBrowser)[\s/](\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/Whale/i],describe:function(e){var t={name:"NAVER Whale Browser"},r=i.default.getFirstMatch(s,e)||i.default.getFirstMatch(/(?:whale)[\s/](\d+(?:\.\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/MZBrowser/i],describe:function(e){var t={name:"MZ Browser"},r=i.default.getFirstMatch(/(?:MZBrowser)[\s/](\d+(?:\.\d+)+)/i,e)||i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/focus/i],describe:function(e){var t={name:"Focus"},r=i.default.getFirstMatch(/(?:focus)[\s/](\d+(?:\.\d+)+)/i,e)||i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/swing/i],describe:function(e){var t={name:"Swing"},r=i.default.getFirstMatch(/(?:swing)[\s/](\d+(?:\.\d+)+)/i,e)||i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/coast/i],describe:function(e){var t={name:"Opera Coast"},r=i.default.getFirstMatch(s,e)||i.default.getFirstMatch(/(?:coast)[\s/](\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/opt\/\d+(?:.?_?\d+)+/i],describe:function(e){var t={name:"Opera Touch"},r=i.default.getFirstMatch(/(?:opt)[\s/](\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/yabrowser/i],describe:function(e){var t={name:"Yandex Browser"},r=i.default.getFirstMatch(/(?:yabrowser)[\s/](\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/ucbrowser/i],describe:function(e){var t={name:"UC Browser"},r=i.default.getFirstMatch(s,e)||i.default.getFirstMatch(/(?:ucbrowser)[\s/](\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/Maxthon|mxios/i],describe:function(e){var t={name:"Maxthon"},r=i.default.getFirstMatch(s,e)||i.default.getFirstMatch(/(?:Maxthon|mxios)[\s/](\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/epiphany/i],describe:function(e){var t={name:"Epiphany"},r=i.default.getFirstMatch(s,e)||i.default.getFirstMatch(/(?:epiphany)[\s/](\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/puffin/i],describe:function(e){var t={name:"Puffin"},r=i.default.getFirstMatch(s,e)||i.default.getFirstMatch(/(?:puffin)[\s/](\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/sleipnir/i],describe:function(e){var t={name:"Sleipnir"},r=i.default.getFirstMatch(s,e)||i.default.getFirstMatch(/(?:sleipnir)[\s/](\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/k-meleon/i],describe:function(e){var t={name:"K-Meleon"},r=i.default.getFirstMatch(s,e)||i.default.getFirstMatch(/(?:k-meleon)[\s/](\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/micromessenger/i],describe:function(e){var t={name:"WeChat"},r=i.default.getFirstMatch(/(?:micromessenger)[\s/](\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/qqbrowser/i],describe:function(e){var t={name:/qqbrowserlite/i.test(e)?"QQ Browser Lite":"QQ Browser"},r=i.default.getFirstMatch(/(?:qqbrowserlite|qqbrowser)[/](\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/msie|trident/i],describe:function(e){var t={name:"Internet Explorer"},r=i.default.getFirstMatch(/(?:msie |rv:)(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/\sedg\//i],describe:function(e){var t={name:"Microsoft Edge"},r=i.default.getFirstMatch(/\sedg\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/edg([ea]|ios)/i],describe:function(e){var t={name:"Microsoft Edge"},r=i.default.getSecondMatch(/edg([ea]|ios)\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/vivaldi/i],describe:function(e){var t={name:"Vivaldi"},r=i.default.getFirstMatch(/vivaldi\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/seamonkey/i],describe:function(e){var t={name:"SeaMonkey"},r=i.default.getFirstMatch(/seamonkey\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/sailfish/i],describe:function(e){var t={name:"Sailfish"},r=i.default.getFirstMatch(/sailfish\s?browser\/(\d+(\.\d+)?)/i,e);return r&&(t.version=r),t}},{test:[/silk/i],describe:function(e){var t={name:"Amazon Silk"},r=i.default.getFirstMatch(/silk\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/phantom/i],describe:function(e){var t={name:"PhantomJS"},r=i.default.getFirstMatch(/phantomjs\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/slimerjs/i],describe:function(e){var t={name:"SlimerJS"},r=i.default.getFirstMatch(/slimerjs\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/blackberry|\bbb\d+/i,/rim\stablet/i],describe:function(e){var t={name:"BlackBerry"},r=i.default.getFirstMatch(s,e)||i.default.getFirstMatch(/blackberry[\d]+\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/(web|hpw)[o0]s/i],describe:function(e){var t={name:"WebOS Browser"},r=i.default.getFirstMatch(s,e)||i.default.getFirstMatch(/w(?:eb)?[o0]sbrowser\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/bada/i],describe:function(e){var t={name:"Bada"},r=i.default.getFirstMatch(/dolfin\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/tizen/i],describe:function(e){var t={name:"Tizen"},r=i.default.getFirstMatch(/(?:tizen\s?)?browser\/(\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/qupzilla/i],describe:function(e){var t={name:"QupZilla"},r=i.default.getFirstMatch(/(?:qupzilla)[\s/](\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/firefox|iceweasel|fxios/i],describe:function(e){var t={name:"Firefox"},r=i.default.getFirstMatch(/(?:firefox|iceweasel|fxios)[\s/](\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/electron/i],describe:function(e){var t={name:"Electron"},r=i.default.getFirstMatch(/(?:electron)\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/MiuiBrowser/i],describe:function(e){var t={name:"Miui"},r=i.default.getFirstMatch(/(?:MiuiBrowser)[\s/](\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/chromium/i],describe:function(e){var t={name:"Chromium"},r=i.default.getFirstMatch(/(?:chromium)[\s/](\d+(\.?_?\d+)+)/i,e)||i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/chrome|crios|crmo/i],describe:function(e){var t={name:"Chrome"},r=i.default.getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/GSA/i],describe:function(e){var t={name:"Google Search"},r=i.default.getFirstMatch(/(?:GSA)\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:function(e){var t=!e.test(/like android/i),r=e.test(/android/i);return t&&r},describe:function(e){var t={name:"Android Browser"},r=i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/playstation 4/i],describe:function(e){var t={name:"PlayStation 4"},r=i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/safari|applewebkit/i],describe:function(e){var t={name:"Safari"},r=i.default.getFirstMatch(s,e);return r&&(t.version=r),t}},{test:[/.*/i],describe:function(e){var t=-1!==e.search("\\(")?/^(.*)\/(.*)[ \t]\((.*)/:/^(.*)\/(.*) /;return{name:i.default.getFirstMatch(t,e),version:i.default.getSecondMatch(t,e)}}}];t.default=a,e.exports=t.default},93:function(e,t,r){"use strict";t.__esModule=!0,t.default=void 0;var n,i=(n=r(17))&&n.__esModule?n:{default:n},s=r(18);var a=[{test:[/Roku\/DVP/],describe:function(e){var t=i.default.getFirstMatch(/Roku\/DVP-(\d+\.\d+)/i,e);return{name:s.OS_MAP.Roku,version:t}}},{test:[/windows phone/i],describe:function(e){var t=i.default.getFirstMatch(/windows phone (?:os)?\s?(\d+(\.\d+)*)/i,e);return{name:s.OS_MAP.WindowsPhone,version:t}}},{test:[/windows /i],describe:function(e){var t=i.default.getFirstMatch(/Windows ((NT|XP)( \d\d?.\d)?)/i,e),r=i.default.getWindowsVersionName(t);return{name:s.OS_MAP.Windows,version:t,versionName:r}}},{test:[/Macintosh(.*?) FxiOS(.*?)\//],describe:function(e){var t={name:s.OS_MAP.iOS},r=i.default.getSecondMatch(/(Version\/)(\d[\d.]+)/,e);return r&&(t.version=r),t}},{test:[/macintosh/i],describe:function(e){var t=i.default.getFirstMatch(/mac os x (\d+(\.?_?\d+)+)/i,e).replace(/[_\s]/g,"."),r=i.default.getMacOSVersionName(t),n={name:s.OS_MAP.MacOS,version:t};return r&&(n.versionName=r),n}},{test:[/(ipod|iphone|ipad)/i],describe:function(e){var t=i.default.getFirstMatch(/os (\d+([_\s]\d+)*) like mac os x/i,e).replace(/[_\s]/g,".");return{name:s.OS_MAP.iOS,version:t}}},{test:function(e){var t=!e.test(/like android/i),r=e.test(/android/i);return t&&r},describe:function(e){var t=i.default.getFirstMatch(/android[\s/-](\d+(\.\d+)*)/i,e),r=i.default.getAndroidVersionName(t),n={name:s.OS_MAP.Android,version:t};return r&&(n.versionName=r),n}},{test:[/(web|hpw)[o0]s/i],describe:function(e){var t=i.default.getFirstMatch(/(?:web|hpw)[o0]s\/(\d+(\.\d+)*)/i,e),r={name:s.OS_MAP.WebOS};return t&&t.length&&(r.version=t),r}},{test:[/blackberry|\bbb\d+/i,/rim\stablet/i],describe:function(e){var t=i.default.getFirstMatch(/rim\stablet\sos\s(\d+(\.\d+)*)/i,e)||i.default.getFirstMatch(/blackberry\d+\/(\d+([_\s]\d+)*)/i,e)||i.default.getFirstMatch(/\bbb(\d+)/i,e);return{name:s.OS_MAP.BlackBerry,version:t}}},{test:[/bada/i],describe:function(e){var t=i.default.getFirstMatch(/bada\/(\d+(\.\d+)*)/i,e);return{name:s.OS_MAP.Bada,version:t}}},{test:[/tizen/i],describe:function(e){var t=i.default.getFirstMatch(/tizen[/\s](\d+(\.\d+)*)/i,e);return{name:s.OS_MAP.Tizen,version:t}}},{test:[/linux/i],describe:function(){return{name:s.OS_MAP.Linux}}},{test:[/CrOS/],describe:function(){return{name:s.OS_MAP.ChromeOS}}},{test:[/PlayStation 4/],describe:function(e){var t=i.default.getFirstMatch(/PlayStation 4[/\s](\d+(\.\d+)*)/i,e);return{name:s.OS_MAP.PlayStation4,version:t}}}];t.default=a,e.exports=t.default},94:function(e,t,r){"use strict";t.__esModule=!0,t.default=void 0;var n,i=(n=r(17))&&n.__esModule?n:{default:n},s=r(18);var a=[{test:[/googlebot/i],describe:function(){return{type:"bot",vendor:"Google"}}},{test:[/huawei/i],describe:function(e){var t=i.default.getFirstMatch(/(can-l01)/i,e)&&"Nova",r={type:s.PLATFORMS_MAP.mobile,vendor:"Huawei"};return t&&(r.model=t),r}},{test:[/nexus\s*(?:7|8|9|10).*/i],describe:function(){return{type:s.PLATFORMS_MAP.tablet,vendor:"Nexus"}}},{test:[/ipad/i],describe:function(){return{type:s.PLATFORMS_MAP.tablet,vendor:"Apple",model:"iPad"}}},{test:[/Macintosh(.*?) FxiOS(.*?)\//],describe:function(){return{type:s.PLATFORMS_MAP.tablet,vendor:"Apple",model:"iPad"}}},{test:[/kftt build/i],describe:function(){return{type:s.PLATFORMS_MAP.tablet,vendor:"Amazon",model:"Kindle Fire HD 7"}}},{test:[/silk/i],describe:function(){return{type:s.PLATFORMS_MAP.tablet,vendor:"Amazon"}}},{test:[/tablet(?! pc)/i],describe:function(){return{type:s.PLATFORMS_MAP.tablet}}},{test:function(e){var t=e.test(/ipod|iphone/i),r=e.test(/like (ipod|iphone)/i);return t&&!r},describe:function(e){var t=i.default.getFirstMatch(/(ipod|iphone)/i,e);return{type:s.PLATFORMS_MAP.mobile,vendor:"Apple",model:t}}},{test:[/nexus\s*[0-6].*/i,/galaxy nexus/i],describe:function(){return{type:s.PLATFORMS_MAP.mobile,vendor:"Nexus"}}},{test:[/[^-]mobi/i],describe:function(){return{type:s.PLATFORMS_MAP.mobile}}},{test:function(e){return"blackberry"===e.getBrowserName(!0)},describe:function(){return{type:s.PLATFORMS_MAP.mobile,vendor:"BlackBerry"}}},{test:function(e){return"bada"===e.getBrowserName(!0)},describe:function(){return{type:s.PLATFORMS_MAP.mobile}}},{test:function(e){return"windows phone"===e.getBrowserName()},describe:function(){return{type:s.PLATFORMS_MAP.mobile,vendor:"Microsoft"}}},{test:function(e){var t=Number(String(e.getOSVersion()).split(".")[0]);return"android"===e.getOSName(!0)&&t>=3},describe:function(){return{type:s.PLATFORMS_MAP.tablet}}},{test:function(e){return"android"===e.getOSName(!0)},describe:function(){return{type:s.PLATFORMS_MAP.mobile}}},{test:function(e){return"macos"===e.getOSName(!0)},describe:function(){return{type:s.PLATFORMS_MAP.desktop,vendor:"Apple"}}},{test:function(e){return"windows"===e.getOSName(!0)},describe:function(){return{type:s.PLATFORMS_MAP.desktop}}},{test:function(e){return"linux"===e.getOSName(!0)},describe:function(){return{type:s.PLATFORMS_MAP.desktop}}},{test:function(e){return"playstation 4"===e.getOSName(!0)},describe:function(){return{type:s.PLATFORMS_MAP.tv}}},{test:function(e){return"roku"===e.getOSName(!0)},describe:function(){return{type:s.PLATFORMS_MAP.tv}}}];t.default=a,e.exports=t.default},95:function(e,t,r){"use strict";t.__esModule=!0,t.default=void 0;var n,i=(n=r(17))&&n.__esModule?n:{default:n},s=r(18);var a=[{test:function(e){return"microsoft edge"===e.getBrowserName(!0)},describe:function(e){if(/\sedg\//i.test(e))return{name:s.ENGINE_MAP.Blink};var t=i.default.getFirstMatch(/edge\/(\d+(\.?_?\d+)+)/i,e);return{name:s.ENGINE_MAP.EdgeHTML,version:t}}},{test:[/trident/i],describe:function(e){var t={name:s.ENGINE_MAP.Trident},r=i.default.getFirstMatch(/trident\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:function(e){return e.test(/presto/i)},describe:function(e){var t={name:s.ENGINE_MAP.Presto},r=i.default.getFirstMatch(/presto\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:function(e){var t=e.test(/gecko/i),r=e.test(/like gecko/i);return t&&!r},describe:function(e){var t={name:s.ENGINE_MAP.Gecko},r=i.default.getFirstMatch(/gecko\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}},{test:[/(apple)?webkit\/537\.36/i],describe:function(){return{name:s.ENGINE_MAP.Blink}}},{test:[/(apple)?webkit/i],describe:function(e){var t={name:s.ENGINE_MAP.WebKit},r=i.default.getFirstMatch(/webkit\/(\d+(\.?_?\d+)+)/i,e);return r&&(t.version=r),t}}];t.default=a,e.exports=t.default}})})); },{}],8:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Consumer = void 0; -const Logger_1 = require("./Logger"); -const EnhancedEventEmitter_1 = require("./EnhancedEventEmitter"); -const errors_1 = require("./errors"); -const logger = new Logger_1.Logger('Consumer'); -class Consumer extends EnhancedEventEmitter_1.EnhancedEventEmitter { - /** - * @emits transportclose - * @emits trackended - * @emits @getstats - * @emits @close - */ - constructor({ id, localId, producerId, rtpReceiver, track, rtpParameters, appData }) { - super(); - // Closed flag. - this._closed = false; - // Observer instance. - this._observer = new EnhancedEventEmitter_1.EnhancedEventEmitter(); - logger.debug('constructor()'); - this._id = id; - this._localId = localId; - this._producerId = producerId; - this._rtpReceiver = rtpReceiver; - this._track = track; - this._rtpParameters = rtpParameters; - this._paused = !track.enabled; - this._appData = appData; - this._onTrackEnded = this._onTrackEnded.bind(this); - this._handleTrack(); - } - /** - * Consumer id. - */ - get id() { - return this._id; - } - /** - * Local id. - */ - get localId() { - return this._localId; - } - /** - * Associated Producer id. - */ - get producerId() { - return this._producerId; - } - /** - * Whether the Consumer is closed. - */ - get closed() { - return this._closed; - } - /** - * Media kind. - */ - get kind() { - return this._track.kind; - } - /** - * Associated RTCRtpReceiver. - */ - get rtpReceiver() { - return this._rtpReceiver; - } - /** - * The associated track. - */ - get track() { - return this._track; - } - /** - * RTP parameters. - */ - get rtpParameters() { - return this._rtpParameters; - } - /** - * Whether the Consumer is paused. - */ - get paused() { - return this._paused; - } - /** - * App custom data. +const debug = require('debug')('h264-profile-level-id'); + +/* eslint-disable no-console */ +debug.log = console.info.bind(console); +/* eslint-enable no-console */ + +const ProfileConstrainedBaseline = 1; +const ProfileBaseline = 2; +const ProfileMain = 3; +const ProfileConstrainedHigh = 4; +const ProfileHigh = 5; + +exports.ProfileConstrainedBaseline = ProfileConstrainedBaseline; +exports.ProfileBaseline = ProfileBaseline; +exports.ProfileMain = ProfileMain; +exports.ProfileConstrainedHigh = ProfileConstrainedHigh; +exports.ProfileHigh = ProfileHigh; + +// All values are equal to ten times the level number, except level 1b which is +// special. +const Level1_b = 0; +const Level1 = 10; +const Level1_1 = 11; +const Level1_2 = 12; +const Level1_3 = 13; +const Level2 = 20; +const Level2_1 = 21; +const Level2_2 = 22; +const Level3 = 30; +const Level3_1 = 31; +const Level3_2 = 32; +const Level4 = 40; +const Level4_1 = 41; +const Level4_2 = 42; +const Level5 = 50; +const Level5_1 = 51; +const Level5_2 = 52; + +exports.Level1_b = Level1_b; +exports.Level1 = Level1; +exports.Level1_1 = Level1_1; +exports.Level1_2 = Level1_2; +exports.Level1_3 = Level1_3; +exports.Level2 = Level2; +exports.Level2_1 = Level2_1; +exports.Level2_2 = Level2_2; +exports.Level3 = Level3; +exports.Level3_1 = Level3_1; +exports.Level3_2 = Level3_2; +exports.Level4 = Level4; +exports.Level4_1 = Level4_1; +exports.Level4_2 = Level4_2; +exports.Level5 = Level5; +exports.Level5_1 = Level5_1; +exports.Level5_2 = Level5_2; + +class ProfileLevelId +{ + constructor(profile, level) + { + this.profile = profile; + this.level = level; + } +} + +exports.ProfileLevelId = ProfileLevelId; + +// Default ProfileLevelId. +// +// TODO: The default should really be profile Baseline and level 1 according to +// the spec: https://tools.ietf.org/html/rfc6184#section-8.1. In order to not +// break backwards compatibility with older versions of WebRTC where external +// codecs don't have any parameters, use profile ConstrainedBaseline level 3_1 +// instead. This workaround will only be done in an interim period to allow +// external clients to update their code. +// +// http://crbug/webrtc/6337. +const DefaultProfileLevelId = + new ProfileLevelId(ProfileConstrainedBaseline, Level3_1); + +// For level_idc=11 and profile_idc=0x42, 0x4D, or 0x58, the constraint set3 +// flag specifies if level 1b or level 1.1 is used. +const ConstraintSet3Flag = 0x10; + +// Class for matching bit patterns such as "x1xx0000" where 'x' is allowed to be +// either 0 or 1. +class BitPattern +{ + constructor(str) + { + this._mask = ~byteMaskString('x', str); + this._maskedValue = byteMaskString('1', str); + } + + isMatch(value) + { + return this._maskedValue === (value & this._mask); + } +} + +// Class for converting between profile_idc/profile_iop to Profile. +class ProfilePattern +{ + constructor(profile_idc, profile_iop, profile) + { + this.profile_idc = profile_idc; + this.profile_iop = profile_iop; + this.profile = profile; + } +} + +// This is from https://tools.ietf.org/html/rfc6184#section-8.1. +const ProfilePatterns = +[ + new ProfilePattern(0x42, new BitPattern('x1xx0000'), ProfileConstrainedBaseline), + new ProfilePattern(0x4D, new BitPattern('1xxx0000'), ProfileConstrainedBaseline), + new ProfilePattern(0x58, new BitPattern('11xx0000'), ProfileConstrainedBaseline), + new ProfilePattern(0x42, new BitPattern('x0xx0000'), ProfileBaseline), + new ProfilePattern(0x58, new BitPattern('10xx0000'), ProfileBaseline), + new ProfilePattern(0x4D, new BitPattern('0x0x0000'), ProfileMain), + new ProfilePattern(0x64, new BitPattern('00000000'), ProfileHigh), + new ProfilePattern(0x64, new BitPattern('00001100'), ProfileConstrainedHigh) +]; + +/** + * Parse profile level id that is represented as a string of 3 hex bytes. + * Nothing will be returned if the string is not a recognized H264 profile + * level id. + * + * @param {String} str - profile-level-id value as a string of 3 hex bytes. + * + * @returns {ProfileLevelId} + */ +exports.parseProfileLevelId = function(str) +{ + // The string should consist of 3 bytes in hexadecimal format. + if (typeof str !== 'string' || str.length !== 6) + return null; + + const profile_level_id_numeric = parseInt(str, 16); + + if (profile_level_id_numeric === 0) + return null; + + // Separate into three bytes. + const level_idc = profile_level_id_numeric & 0xFF; + const profile_iop = (profile_level_id_numeric >> 8) & 0xFF; + const profile_idc = (profile_level_id_numeric >> 16) & 0xFF; + + // Parse level based on level_idc and constraint set 3 flag. + let level; + + switch (level_idc) + { + case Level1_1: + { + level = (profile_iop & ConstraintSet3Flag) !== 0 ? Level1_b : Level1_1; + break; + } + case Level1: + case Level1_2: + case Level1_3: + case Level2: + case Level2_1: + case Level2_2: + case Level3: + case Level3_1: + case Level3_2: + case Level4: + case Level4_1: + case Level4_2: + case Level5: + case Level5_1: + case Level5_2: + { + level = level_idc; + break; + } + // Unrecognized level_idc. + default: + { + debug('parseProfileLevelId() | unrecognized level_idc:%s', level_idc); + + return null; + } + } + + // Parse profile_idc/profile_iop into a Profile enum. + for (const pattern of ProfilePatterns) + { + if ( + profile_idc === pattern.profile_idc && + pattern.profile_iop.isMatch(profile_iop) + ) + { + return new ProfileLevelId(pattern.profile, level); + } + } + + debug('parseProfileLevelId() | unrecognized profile_idc/profile_iop combination'); + + return null; +}; + +/** + * Returns canonical string representation as three hex bytes of the profile + * level id, or returns nothing for invalid profile level ids. + * + * @param {ProfileLevelId} profile_level_id + * + * @returns {String} + */ +exports.profileLevelIdToString = function(profile_level_id) +{ + // Handle special case level == 1b. + if (profile_level_id.level == Level1_b) + { + switch (profile_level_id.profile) + { + case ProfileConstrainedBaseline: + { + return '42f00b'; + } + case ProfileBaseline: + { + return '42100b'; + } + case ProfileMain: + { + return '4d100b'; + } + // Level 1_b is not allowed for other profiles. + default: + { + debug( + 'profileLevelIdToString() | Level 1_b not is allowed for profile:%s', + profile_level_id.profile); + + return null; + } + } + } + + let profile_idc_iop_string; + + switch (profile_level_id.profile) + { + case ProfileConstrainedBaseline: + { + profile_idc_iop_string = '42e0'; + break; + } + case ProfileBaseline: + { + profile_idc_iop_string = '4200'; + break; + } + case ProfileMain: + { + profile_idc_iop_string = '4d00'; + break; + } + case ProfileConstrainedHigh: + { + profile_idc_iop_string = '640c'; + break; + } + case ProfileHigh: + { + profile_idc_iop_string = '6400'; + break; + } + default: + { + debug( + 'profileLevelIdToString() | unrecognized profile:%s', + profile_level_id.profile); + + return null; + } + } + + let levelStr = (profile_level_id.level).toString(16); + + if (levelStr.length === 1) + levelStr = `0${levelStr}`; + + return `${profile_idc_iop_string}${levelStr}`; +}; + +/** + * Parse profile level id that is represented as a string of 3 hex bytes + * contained in an SDP key-value map. A default profile level id will be + * returned if the profile-level-id key is missing. Nothing will be returned if + * the key is present but the string is invalid. + * + * @param {Object} [params={}] - Codec parameters object. + * + * @returns {ProfileLevelId} + */ +exports.parseSdpProfileLevelId = function(params = {}) +{ + const profile_level_id = params['profile-level-id']; + + return !profile_level_id + ? DefaultProfileLevelId + : exports.parseProfileLevelId(profile_level_id); +}; + +/** + * Returns true if the parameters have the same H264 profile, i.e. the same + * H264 profile (Baseline, High, etc). + * + * @param {Object} [params1={}] - Codec parameters object. + * @param {Object} [params2={}] - Codec parameters object. + * + * @returns {Boolean} + */ +exports.isSameProfile = function(params1 = {}, params2 = {}) +{ + const profile_level_id_1 = exports.parseSdpProfileLevelId(params1); + const profile_level_id_2 = exports.parseSdpProfileLevelId(params2); + + // Compare H264 profiles, but not levels. + return Boolean( + profile_level_id_1 && + profile_level_id_2 && + profile_level_id_1.profile === profile_level_id_2.profile + ); +}; + +/** + * Generate codec parameters that will be used as answer in an SDP negotiation + * based on local supported parameters and remote offered parameters. Both + * local_supported_params and remote_offered_params represent sendrecv media + * descriptions, i.e they are a mix of both encode and decode capabilities. In + * theory, when the profile in local_supported_params represent a strict superset + * of the profile in remote_offered_params, we could limit the profile in the + * answer to the profile in remote_offered_params. + * + * However, to simplify the code, each supported H264 profile should be listed + * explicitly in the list of local supported codecs, even if they are redundant. + * Then each local codec in the list should be tested one at a time against the + * remote codec, and only when the profiles are equal should this function be + * called. Therefore, this function does not need to handle profile intersection, + * and the profile of local_supported_params and remote_offered_params must be + * equal before calling this function. The parameters that are used when + * negotiating are the level part of profile-level-id and level-asymmetry-allowed. + * + * @param {Object} [local_supported_params={}] + * @param {Object} [remote_offered_params={}] + * + * @returns {String} Canonical string representation as three hex bytes of the + * profile level id, or null if no one of the params have profile-level-id. + * + * @throws {TypeError} If Profile mismatch or invalid params. + */ +exports.generateProfileLevelIdForAnswer = function( + local_supported_params = {}, + remote_offered_params = {} +) +{ + // If both local and remote params do not contain profile-level-id, they are + // both using the default profile. In this case, don't return anything. + if ( + !local_supported_params['profile-level-id'] && + !remote_offered_params['profile-level-id'] + ) + { + debug( + 'generateProfileLevelIdForAnswer() | no profile-level-id in local and remote params'); + + return null; + } + + // Parse profile-level-ids. + const local_profile_level_id = + exports.parseSdpProfileLevelId(local_supported_params); + const remote_profile_level_id = + exports.parseSdpProfileLevelId(remote_offered_params); + + // The local and remote codec must have valid and equal H264 Profiles. + if (!local_profile_level_id) + throw new TypeError('invalid local_profile_level_id'); + + if (!remote_profile_level_id) + throw new TypeError('invalid remote_profile_level_id'); + + if (local_profile_level_id.profile !== remote_profile_level_id.profile) + throw new TypeError('H264 Profile mismatch'); + + // Parse level information. + const level_asymmetry_allowed = ( + isLevelAsymmetryAllowed(local_supported_params) && + isLevelAsymmetryAllowed(remote_offered_params) + ); + + const local_level = local_profile_level_id.level; + const remote_level = remote_profile_level_id.level; + const min_level = minLevel(local_level, remote_level); + + // Determine answer level. When level asymmetry is not allowed, level upgrade + // is not allowed, i.e., the level in the answer must be equal to or lower + // than the level in the offer. + const answer_level = level_asymmetry_allowed ? local_level : min_level; + + debug( + 'generateProfileLevelIdForAnswer() | result: [profile:%s, level:%s]', + local_profile_level_id.profile, answer_level); + + // Return the resulting profile-level-id for the answer parameters. + return exports.profileLevelIdToString( + new ProfileLevelId(local_profile_level_id.profile, answer_level)); +}; + +// Convert a string of 8 characters into a byte where the positions containing +// character c will have their bit set. For example, c = 'x', str = "x1xx0000" +// will return 0b10110000. +function byteMaskString(c, str) +{ + return ( + ((str[0] === c) << 7) | ((str[1] === c) << 6) | ((str[2] === c) << 5) | + ((str[3] === c) << 4) | ((str[4] === c) << 3) | ((str[5] === c) << 2) | + ((str[6] === c) << 1) | ((str[7] === c) << 0) + ); +} + +// Compare H264 levels and handle the level 1b case. +function isLessLevel(a, b) +{ + if (a === Level1_b) + return b !== Level1 && b !== Level1_b; + + if (b === Level1_b) + return a !== Level1; + + return a < b; +} + +function minLevel(a, b) +{ + return isLessLevel(a, b) ? a : b; +} + +function isLevelAsymmetryAllowed(params = {}) +{ + const level_asymmetry_allowed = params['level-asymmetry-allowed']; + + return ( + level_asymmetry_allowed === 1 || + level_asymmetry_allowed === '1' + ); +} + +},{"debug":9}],9:[function(require,module,exports){ +arguments[4][4][0].apply(exports,arguments) +},{"./common":10,"_process":56,"dup":4}],10:[function(require,module,exports){ +arguments[4][5][0].apply(exports,arguments) +},{"dup":5,"ms":11}],11:[function(require,module,exports){ +arguments[4][6][0].apply(exports,arguments) +},{"dup":6}],12:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Consumer = void 0; +const Logger_1 = require("./Logger"); +const EnhancedEventEmitter_1 = require("./EnhancedEventEmitter"); +const errors_1 = require("./errors"); +const logger = new Logger_1.Logger('Consumer'); +class Consumer extends EnhancedEventEmitter_1.EnhancedEventEmitter { + constructor({ id, localId, producerId, rtpReceiver, track, rtpParameters, appData }) { + super(); + // Closed flag. + this._closed = false; + // Observer instance. + this._observer = new EnhancedEventEmitter_1.EnhancedEventEmitter(); + logger.debug('constructor()'); + this._id = id; + this._localId = localId; + this._producerId = producerId; + this._rtpReceiver = rtpReceiver; + this._track = track; + this._rtpParameters = rtpParameters; + this._paused = !track.enabled; + this._appData = appData || {}; + this.onTrackEnded = this.onTrackEnded.bind(this); + this.handleTrack(); + } + /** + * Consumer id. + */ + get id() { + return this._id; + } + /** + * Local id. + */ + get localId() { + return this._localId; + } + /** + * Associated Producer id. + */ + get producerId() { + return this._producerId; + } + /** + * Whether the Consumer is closed. + */ + get closed() { + return this._closed; + } + /** + * Media kind. + */ + get kind() { + return this._track.kind; + } + /** + * Associated RTCRtpReceiver. + */ + get rtpReceiver() { + return this._rtpReceiver; + } + /** + * The associated track. + */ + get track() { + return this._track; + } + /** + * RTP parameters. + */ + get rtpParameters() { + return this._rtpParameters; + } + /** + * Whether the Consumer is paused. + */ + get paused() { + return this._paused; + } + /** + * App custom data. + */ + get appData() { + return this._appData; + } + /** + * Invalid setter. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + set appData(appData) { + throw new Error('cannot override appData object'); + } + get observer() { + return this._observer; + } + /** + * Closes the Consumer. + */ + close() { + if (this._closed) { + return; + } + logger.debug('close()'); + this._closed = true; + this.destroyTrack(); + this.emit('@close'); + // Emit observer event. + this._observer.safeEmit('close'); + } + /** + * Transport was closed. + */ + transportClosed() { + if (this._closed) { + return; + } + logger.debug('transportClosed()'); + this._closed = true; + this.destroyTrack(); + this.safeEmit('transportclose'); + // Emit observer event. + this._observer.safeEmit('close'); + } + /** + * Get associated RTCRtpReceiver stats. + */ + async getStats() { + if (this._closed) { + throw new errors_1.InvalidStateError('closed'); + } + return new Promise((resolve, reject) => { + this.safeEmit('@getstats', resolve, reject); + }); + } + /** + * Pauses receiving media. + */ + pause() { + logger.debug('pause()'); + if (this._closed) { + logger.error('pause() | Consumer closed'); + return; + } + if (this._paused) { + logger.debug('pause() | Consumer is already paused'); + return; + } + this._paused = true; + this._track.enabled = false; + this.emit('@pause'); + // Emit observer event. + this._observer.safeEmit('pause'); + } + /** + * Resumes receiving media. + */ + resume() { + logger.debug('resume()'); + if (this._closed) { + logger.error('resume() | Consumer closed'); + return; + } + if (!this._paused) { + logger.debug('resume() | Consumer is already resumed'); + return; + } + this._paused = false; + this._track.enabled = true; + this.emit('@resume'); + // Emit observer event. + this._observer.safeEmit('resume'); + } + onTrackEnded() { + logger.debug('track "ended" event'); + this.safeEmit('trackended'); + // Emit observer event. + this._observer.safeEmit('trackended'); + } + handleTrack() { + this._track.addEventListener('ended', this.onTrackEnded); + } + destroyTrack() { + try { + this._track.removeEventListener('ended', this.onTrackEnded); + this._track.stop(); + } + catch (error) { } + } +} +exports.Consumer = Consumer; + +},{"./EnhancedEventEmitter":16,"./Logger":17,"./errors":22}],13:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.DataConsumer = void 0; +const Logger_1 = require("./Logger"); +const EnhancedEventEmitter_1 = require("./EnhancedEventEmitter"); +const logger = new Logger_1.Logger('DataConsumer'); +class DataConsumer extends EnhancedEventEmitter_1.EnhancedEventEmitter { + constructor({ id, dataProducerId, dataChannel, sctpStreamParameters, appData }) { + super(); + // Closed flag. + this._closed = false; + // Observer instance. + this._observer = new EnhancedEventEmitter_1.EnhancedEventEmitter(); + logger.debug('constructor()'); + this._id = id; + this._dataProducerId = dataProducerId; + this._dataChannel = dataChannel; + this._sctpStreamParameters = sctpStreamParameters; + this._appData = appData || {}; + this.handleDataChannel(); + } + /** + * DataConsumer id. + */ + get id() { + return this._id; + } + /** + * Associated DataProducer id. + */ + get dataProducerId() { + return this._dataProducerId; + } + /** + * Whether the DataConsumer is closed. + */ + get closed() { + return this._closed; + } + /** + * SCTP stream parameters. + */ + get sctpStreamParameters() { + return this._sctpStreamParameters; + } + /** + * DataChannel readyState. + */ + get readyState() { + return this._dataChannel.readyState; + } + /** + * DataChannel label. + */ + get label() { + return this._dataChannel.label; + } + /** + * DataChannel protocol. + */ + get protocol() { + return this._dataChannel.protocol; + } + /** + * DataChannel binaryType. + */ + get binaryType() { + return this._dataChannel.binaryType; + } + /** + * Set DataChannel binaryType. + */ + set binaryType(binaryType) { + this._dataChannel.binaryType = binaryType; + } + /** + * App custom data. + */ + get appData() { + return this._appData; + } + /** + * Invalid setter. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + set appData(appData) { + throw new Error('cannot override appData object'); + } + get observer() { + return this._observer; + } + /** + * Closes the DataConsumer. + */ + close() { + if (this._closed) { + return; + } + logger.debug('close()'); + this._closed = true; + this._dataChannel.close(); + this.emit('@close'); + // Emit observer event. + this._observer.safeEmit('close'); + } + /** + * Transport was closed. + */ + transportClosed() { + if (this._closed) { + return; + } + logger.debug('transportClosed()'); + this._closed = true; + this._dataChannel.close(); + this.safeEmit('transportclose'); + // Emit observer event. + this._observer.safeEmit('close'); + } + handleDataChannel() { + this._dataChannel.addEventListener('open', () => { + if (this._closed) { + return; + } + logger.debug('DataChannel "open" event'); + this.safeEmit('open'); + }); + this._dataChannel.addEventListener('error', (event) => { + if (this._closed) { + return; + } + let { error } = event; + if (!error) { + error = new Error('unknown DataChannel error'); + } + if (error.errorDetail === 'sctp-failure') { + logger.error('DataChannel SCTP error [sctpCauseCode:%s]: %s', error.sctpCauseCode, error.message); + } + else { + logger.error('DataChannel "error" event: %o', error); + } + this.safeEmit('error', error); + }); + this._dataChannel.addEventListener('close', () => { + if (this._closed) { + return; + } + logger.warn('DataChannel "close" event'); + this._closed = true; + this.emit('@close'); + this.safeEmit('close'); + // Emit observer event. + this._observer.safeEmit('close'); + }); + this._dataChannel.addEventListener('message', (event) => { + if (this._closed) { + return; + } + this.safeEmit('message', event.data); + }); + } +} +exports.DataConsumer = DataConsumer; + +},{"./EnhancedEventEmitter":16,"./Logger":17}],14:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.DataProducer = void 0; +const Logger_1 = require("./Logger"); +const EnhancedEventEmitter_1 = require("./EnhancedEventEmitter"); +const errors_1 = require("./errors"); +const logger = new Logger_1.Logger('DataProducer'); +class DataProducer extends EnhancedEventEmitter_1.EnhancedEventEmitter { + constructor({ id, dataChannel, sctpStreamParameters, appData }) { + super(); + // Closed flag. + this._closed = false; + // Observer instance. + this._observer = new EnhancedEventEmitter_1.EnhancedEventEmitter(); + logger.debug('constructor()'); + this._id = id; + this._dataChannel = dataChannel; + this._sctpStreamParameters = sctpStreamParameters; + this._appData = appData || {}; + this.handleDataChannel(); + } + /** + * DataProducer id. + */ + get id() { + return this._id; + } + /** + * Whether the DataProducer is closed. + */ + get closed() { + return this._closed; + } + /** + * SCTP stream parameters. + */ + get sctpStreamParameters() { + return this._sctpStreamParameters; + } + /** + * DataChannel readyState. + */ + get readyState() { + return this._dataChannel.readyState; + } + /** + * DataChannel label. + */ + get label() { + return this._dataChannel.label; + } + /** + * DataChannel protocol. + */ + get protocol() { + return this._dataChannel.protocol; + } + /** + * DataChannel bufferedAmount. + */ + get bufferedAmount() { + return this._dataChannel.bufferedAmount; + } + /** + * DataChannel bufferedAmountLowThreshold. + */ + get bufferedAmountLowThreshold() { + return this._dataChannel.bufferedAmountLowThreshold; + } + /** + * Set DataChannel bufferedAmountLowThreshold. + */ + set bufferedAmountLowThreshold(bufferedAmountLowThreshold) { + this._dataChannel.bufferedAmountLowThreshold = bufferedAmountLowThreshold; + } + /** + * App custom data. + */ + get appData() { + return this._appData; + } + /** + * Invalid setter. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + set appData(appData) { + throw new Error('cannot override appData object'); + } + get observer() { + return this._observer; + } + /** + * Closes the DataProducer. + */ + close() { + if (this._closed) { + return; + } + logger.debug('close()'); + this._closed = true; + this._dataChannel.close(); + this.emit('@close'); + // Emit observer event. + this._observer.safeEmit('close'); + } + /** + * Transport was closed. + */ + transportClosed() { + if (this._closed) { + return; + } + logger.debug('transportClosed()'); + this._closed = true; + this._dataChannel.close(); + this.safeEmit('transportclose'); + // Emit observer event. + this._observer.safeEmit('close'); + } + /** + * Send a message. + * + * @param {String|Blob|ArrayBuffer|ArrayBufferView} data. + */ + send(data) { + logger.debug('send()'); + if (this._closed) { + throw new errors_1.InvalidStateError('closed'); + } + this._dataChannel.send(data); + } + handleDataChannel() { + this._dataChannel.addEventListener('open', () => { + if (this._closed) { + return; + } + logger.debug('DataChannel "open" event'); + this.safeEmit('open'); + }); + this._dataChannel.addEventListener('error', (event) => { + if (this._closed) { + return; + } + let { error } = event; + if (!error) { + error = new Error('unknown DataChannel error'); + } + if (error.errorDetail === 'sctp-failure') { + logger.error('DataChannel SCTP error [sctpCauseCode:%s]: %s', error.sctpCauseCode, error.message); + } + else { + logger.error('DataChannel "error" event: %o', error); + } + this.safeEmit('error', error); + }); + this._dataChannel.addEventListener('close', () => { + if (this._closed) { + return; + } + logger.warn('DataChannel "close" event'); + this._closed = true; + this.emit('@close'); + this.safeEmit('close'); + // Emit observer event. + this._observer.safeEmit('close'); + }); + this._dataChannel.addEventListener('message', () => { + if (this._closed) { + return; + } + logger.warn('DataChannel "message" event in a DataProducer, message discarded'); + }); + this._dataChannel.addEventListener('bufferedamountlow', () => { + if (this._closed) { + return; + } + this.safeEmit('bufferedamountlow'); + }); + } +} +exports.DataProducer = DataProducer; + +},{"./EnhancedEventEmitter":16,"./Logger":17,"./errors":22}],15:[function(require,module,exports){ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Device = exports.detectDevice = void 0; +const bowser_1 = __importDefault(require("bowser")); +const Logger_1 = require("./Logger"); +const EnhancedEventEmitter_1 = require("./EnhancedEventEmitter"); +const errors_1 = require("./errors"); +const utils = __importStar(require("./utils")); +const ortc = __importStar(require("./ortc")); +const Transport_1 = require("./Transport"); +const Chrome111_1 = require("./handlers/Chrome111"); +const Chrome74_1 = require("./handlers/Chrome74"); +const Chrome70_1 = require("./handlers/Chrome70"); +const Chrome67_1 = require("./handlers/Chrome67"); +const Chrome55_1 = require("./handlers/Chrome55"); +const Firefox60_1 = require("./handlers/Firefox60"); +const Safari12_1 = require("./handlers/Safari12"); +const Safari11_1 = require("./handlers/Safari11"); +const Edge11_1 = require("./handlers/Edge11"); +const ReactNativeUnifiedPlan_1 = require("./handlers/ReactNativeUnifiedPlan"); +const ReactNative_1 = require("./handlers/ReactNative"); +const logger = new Logger_1.Logger('Device'); +function detectDevice() { + // React-Native. + // NOTE: react-native-webrtc >= 1.75.0 is required. + // NOTE: react-native-webrtc with Unified Plan requires version >= 106.0.0. + if (typeof navigator === 'object' && navigator.product === 'ReactNative') { + if (typeof RTCPeerConnection === 'undefined') { + logger.warn('this._detectDevice() | unsupported react-native-webrtc without RTCPeerConnection, forgot to call registerGlobals()?'); + return undefined; + } + if (typeof RTCRtpTransceiver !== 'undefined') { + logger.debug('this._detectDevice() | ReactNative UnifiedPlan handler chosen'); + return 'ReactNativeUnifiedPlan'; + } + else { + logger.debug('this._detectDevice() | ReactNative PlanB handler chosen'); + return 'ReactNative'; + } + } + // Browser. + else if (typeof navigator === 'object' && typeof navigator.userAgent === 'string') { + const ua = navigator.userAgent; + const browser = bowser_1.default.getParser(ua); + const engine = browser.getEngine(); + // Chrome, Chromium, and Edge. + if (browser.satisfies({ chrome: '>=111', chromium: '>=111', 'microsoft edge': '>=111' })) { + return 'Chrome111'; + } + else if (browser.satisfies({ chrome: '>=74', chromium: '>=74', 'microsoft edge': '>=88' })) { + return 'Chrome74'; + } + else if (browser.satisfies({ chrome: '>=70', chromium: '>=70' })) { + return 'Chrome70'; + } + else if (browser.satisfies({ chrome: '>=67', chromium: '>=67' })) { + return 'Chrome67'; + } + else if (browser.satisfies({ chrome: '>=55', chromium: '>=55' })) { + return 'Chrome55'; + } + // Firefox. + else if (browser.satisfies({ firefox: '>=60' })) { + return 'Firefox60'; + } + // Firefox on iOS. + else if (browser.satisfies({ ios: { OS: '>=14.3', firefox: '>=30.0' } })) { + return 'Safari12'; + } + // Safari with Unified-Plan support enabled. + else if (browser.satisfies({ safari: '>=12.0' }) && + typeof RTCRtpTransceiver !== 'undefined' && + RTCRtpTransceiver.prototype.hasOwnProperty('currentDirection')) { + return 'Safari12'; + } + // Safari with Plab-B support. + else if (browser.satisfies({ safari: '>=11' })) { + return 'Safari11'; + } + // Old Edge with ORTC support. + else if (browser.satisfies({ 'microsoft edge': '>=11' }) && + browser.satisfies({ 'microsoft edge': '<=18' })) { + return 'Edge11'; + } + // Best effort for Chromium based browsers. + else if (engine.name && engine.name.toLowerCase() === 'blink') { + const match = ua.match(/(?:(?:Chrome|Chromium))[ /](\w+)/i); + if (match) { + const version = Number(match[1]); + if (version >= 111) { + return 'Chrome111'; + } + else if (version >= 74) { + return 'Chrome74'; + } + else if (version >= 70) { + return 'Chrome70'; + } + else if (version >= 67) { + return 'Chrome67'; + } + else { + return 'Chrome55'; + } + } + else { + return 'Chrome111'; + } + } + // Unsupported browser. + else { + logger.warn('this._detectDevice() | browser not supported [name:%s, version:%s]', browser.getBrowserName(), browser.getBrowserVersion()); + return undefined; + } + } + // Unknown device. + else { + logger.warn('this._detectDevice() | unknown device'); + return undefined; + } +} +exports.detectDevice = detectDevice; +class Device { + /** + * Create a new Device to connect to mediasoup server. + * + * @throws {UnsupportedError} if device is not supported. + */ + constructor({ handlerName, handlerFactory, Handler } = {}) { + // Loaded flag. + this._loaded = false; + // Observer instance. + this._observer = new EnhancedEventEmitter_1.EnhancedEventEmitter(); + logger.debug('constructor()'); + // Handle deprecated option. + if (Handler) { + logger.warn('constructor() | Handler option is DEPRECATED, use handlerName or handlerFactory instead'); + if (typeof Handler === 'string') { + handlerName = Handler; + } + else { + throw new TypeError('non string Handler option no longer supported, use handlerFactory instead'); + } + } + if (handlerName && handlerFactory) { + throw new TypeError('just one of handlerName or handlerInterface can be given'); + } + if (handlerFactory) { + this._handlerFactory = handlerFactory; + } + else { + if (handlerName) { + logger.debug('constructor() | handler given: %s', handlerName); + } + else { + handlerName = detectDevice(); + if (handlerName) { + logger.debug('constructor() | detected handler: %s', handlerName); + } + else { + throw new errors_1.UnsupportedError('device not supported'); + } + } + switch (handlerName) { + case 'Chrome111': + this._handlerFactory = Chrome111_1.Chrome111.createFactory(); + break; + case 'Chrome74': + this._handlerFactory = Chrome74_1.Chrome74.createFactory(); + break; + case 'Chrome70': + this._handlerFactory = Chrome70_1.Chrome70.createFactory(); + break; + case 'Chrome67': + this._handlerFactory = Chrome67_1.Chrome67.createFactory(); + break; + case 'Chrome55': + this._handlerFactory = Chrome55_1.Chrome55.createFactory(); + break; + case 'Firefox60': + this._handlerFactory = Firefox60_1.Firefox60.createFactory(); + break; + case 'Safari12': + this._handlerFactory = Safari12_1.Safari12.createFactory(); + break; + case 'Safari11': + this._handlerFactory = Safari11_1.Safari11.createFactory(); + break; + case 'Edge11': + this._handlerFactory = Edge11_1.Edge11.createFactory(); + break; + case 'ReactNativeUnifiedPlan': + this._handlerFactory = ReactNativeUnifiedPlan_1.ReactNativeUnifiedPlan.createFactory(); + break; + case 'ReactNative': + this._handlerFactory = ReactNative_1.ReactNative.createFactory(); + break; + default: + throw new TypeError(`unknown handlerName "${handlerName}"`); + } + } + // Create a temporal handler to get its name. + const handler = this._handlerFactory(); + this._handlerName = handler.name; + handler.close(); + this._extendedRtpCapabilities = undefined; + this._recvRtpCapabilities = undefined; + this._canProduceByKind = + { + audio: false, + video: false + }; + this._sctpCapabilities = undefined; + } + /** + * The RTC handler name. + */ + get handlerName() { + return this._handlerName; + } + /** + * Whether the Device is loaded. + */ + get loaded() { + return this._loaded; + } + /** + * RTP capabilities of the Device for receiving media. + * + * @throws {InvalidStateError} if not loaded. + */ + get rtpCapabilities() { + if (!this._loaded) { + throw new errors_1.InvalidStateError('not loaded'); + } + return this._recvRtpCapabilities; + } + /** + * SCTP capabilities of the Device. + * + * @throws {InvalidStateError} if not loaded. + */ + get sctpCapabilities() { + if (!this._loaded) { + throw new errors_1.InvalidStateError('not loaded'); + } + return this._sctpCapabilities; + } + get observer() { + return this._observer; + } + /** + * Initialize the Device. + */ + async load({ routerRtpCapabilities }) { + logger.debug('load() [routerRtpCapabilities:%o]', routerRtpCapabilities); + routerRtpCapabilities = utils.clone(routerRtpCapabilities, undefined); + // Temporal handler to get its capabilities. + let handler; + try { + if (this._loaded) { + throw new errors_1.InvalidStateError('already loaded'); + } + // This may throw. + ortc.validateRtpCapabilities(routerRtpCapabilities); + handler = this._handlerFactory(); + const nativeRtpCapabilities = await handler.getNativeRtpCapabilities(); + logger.debug('load() | got native RTP capabilities:%o', nativeRtpCapabilities); + // This may throw. + ortc.validateRtpCapabilities(nativeRtpCapabilities); + // Get extended RTP capabilities. + this._extendedRtpCapabilities = ortc.getExtendedRtpCapabilities(nativeRtpCapabilities, routerRtpCapabilities); + logger.debug('load() | got extended RTP capabilities:%o', this._extendedRtpCapabilities); + // Check whether we can produce audio/video. + this._canProduceByKind.audio = + ortc.canSend('audio', this._extendedRtpCapabilities); + this._canProduceByKind.video = + ortc.canSend('video', this._extendedRtpCapabilities); + // Generate our receiving RTP capabilities for receiving media. + this._recvRtpCapabilities = + ortc.getRecvRtpCapabilities(this._extendedRtpCapabilities); + // This may throw. + ortc.validateRtpCapabilities(this._recvRtpCapabilities); + logger.debug('load() | got receiving RTP capabilities:%o', this._recvRtpCapabilities); + // Generate our SCTP capabilities. + this._sctpCapabilities = await handler.getNativeSctpCapabilities(); + logger.debug('load() | got native SCTP capabilities:%o', this._sctpCapabilities); + // This may throw. + ortc.validateSctpCapabilities(this._sctpCapabilities); + logger.debug('load() succeeded'); + this._loaded = true; + handler.close(); + } + catch (error) { + if (handler) { + handler.close(); + } + throw error; + } + } + /** + * Whether we can produce audio/video. + * + * @throws {InvalidStateError} if not loaded. + * @throws {TypeError} if wrong arguments. + */ + canProduce(kind) { + if (!this._loaded) { + throw new errors_1.InvalidStateError('not loaded'); + } + else if (kind !== 'audio' && kind !== 'video') { + throw new TypeError(`invalid kind "${kind}"`); + } + return this._canProduceByKind[kind]; + } + /** + * Creates a Transport for sending media. + * + * @throws {InvalidStateError} if not loaded. + * @throws {TypeError} if wrong arguments. + */ + createSendTransport({ id, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, appData }) { + logger.debug('createSendTransport()'); + return this.createTransport({ + direction: 'send', + id: id, + iceParameters: iceParameters, + iceCandidates: iceCandidates, + dtlsParameters: dtlsParameters, + sctpParameters: sctpParameters, + iceServers: iceServers, + iceTransportPolicy: iceTransportPolicy, + additionalSettings: additionalSettings, + proprietaryConstraints: proprietaryConstraints, + appData: appData + }); + } + /** + * Creates a Transport for receiving media. + * + * @throws {InvalidStateError} if not loaded. + * @throws {TypeError} if wrong arguments. + */ + createRecvTransport({ id, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, appData }) { + logger.debug('createRecvTransport()'); + return this.createTransport({ + direction: 'recv', + id: id, + iceParameters: iceParameters, + iceCandidates: iceCandidates, + dtlsParameters: dtlsParameters, + sctpParameters: sctpParameters, + iceServers: iceServers, + iceTransportPolicy: iceTransportPolicy, + additionalSettings: additionalSettings, + proprietaryConstraints: proprietaryConstraints, + appData: appData + }); + } + createTransport({ direction, id, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, appData }) { + if (!this._loaded) { + throw new errors_1.InvalidStateError('not loaded'); + } + else if (typeof id !== 'string') { + throw new TypeError('missing id'); + } + else if (typeof iceParameters !== 'object') { + throw new TypeError('missing iceParameters'); + } + else if (!Array.isArray(iceCandidates)) { + throw new TypeError('missing iceCandidates'); + } + else if (typeof dtlsParameters !== 'object') { + throw new TypeError('missing dtlsParameters'); + } + else if (sctpParameters && typeof sctpParameters !== 'object') { + throw new TypeError('wrong sctpParameters'); + } + else if (appData && typeof appData !== 'object') { + throw new TypeError('if given, appData must be an object'); + } + // Create a new Transport. + const transport = new Transport_1.Transport({ + direction, + id, + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + iceServers, + iceTransportPolicy, + additionalSettings, + proprietaryConstraints, + appData, + handlerFactory: this._handlerFactory, + extendedRtpCapabilities: this._extendedRtpCapabilities, + canProduceByKind: this._canProduceByKind + }); + // Emit observer event. + this._observer.safeEmit('newtransport', transport); + return transport; + } +} +exports.Device = Device; + +},{"./EnhancedEventEmitter":16,"./Logger":17,"./Transport":21,"./errors":22,"./handlers/Chrome111":23,"./handlers/Chrome55":24,"./handlers/Chrome67":25,"./handlers/Chrome70":26,"./handlers/Chrome74":27,"./handlers/Edge11":28,"./handlers/Firefox60":29,"./handlers/ReactNative":31,"./handlers/ReactNativeUnifiedPlan":32,"./handlers/Safari11":33,"./handlers/Safari12":34,"./ortc":43,"./utils":46,"bowser":7}],16:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.EnhancedEventEmitter = void 0; +const events_1 = require("events"); +const Logger_1 = require("./Logger"); +const logger = new Logger_1.Logger('EnhancedEventEmitter'); +class EnhancedEventEmitter extends events_1.EventEmitter { + constructor() { + super(); + this.setMaxListeners(Infinity); + } + emit(eventName, ...args) { + return super.emit(eventName, ...args); + } + /** + * Special addition to the EventEmitter API. + */ + safeEmit(eventName, ...args) { + const numListeners = super.listenerCount(eventName); + try { + return super.emit(eventName, ...args); + } + catch (error) { + logger.error('safeEmit() | event listener threw an error [eventName:%s]:%o', eventName, error); + return Boolean(numListeners); + } + } + on(eventName, listener) { + super.on(eventName, listener); + return this; + } + off(eventName, listener) { + super.off(eventName, listener); + return this; + } + addListener(eventName, listener) { + super.on(eventName, listener); + return this; + } + prependListener(eventName, listener) { + super.prependListener(eventName, listener); + return this; + } + once(eventName, listener) { + super.once(eventName, listener); + return this; + } + prependOnceListener(eventName, listener) { + super.prependOnceListener(eventName, listener); + return this; + } + removeListener(eventName, listener) { + super.off(eventName, listener); + return this; + } + removeAllListeners(eventName) { + super.removeAllListeners(eventName); + return this; + } + listenerCount(eventName) { + return super.listenerCount(eventName); + } + listeners(eventName) { + return super.listeners(eventName); + } + rawListeners(eventName) { + return super.rawListeners(eventName); + } +} +exports.EnhancedEventEmitter = EnhancedEventEmitter; + +},{"./Logger":17,"events":55}],17:[function(require,module,exports){ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Logger = void 0; +const debug_1 = __importDefault(require("debug")); +const APP_NAME = 'mediasoup-client'; +class Logger { + constructor(prefix) { + if (prefix) { + this._debug = (0, debug_1.default)(`${APP_NAME}:${prefix}`); + this._warn = (0, debug_1.default)(`${APP_NAME}:WARN:${prefix}`); + this._error = (0, debug_1.default)(`${APP_NAME}:ERROR:${prefix}`); + } + else { + this._debug = (0, debug_1.default)(APP_NAME); + this._warn = (0, debug_1.default)(`${APP_NAME}:WARN`); + this._error = (0, debug_1.default)(`${APP_NAME}:ERROR`); + } + /* eslint-disable no-console */ + this._debug.log = console.info.bind(console); + this._warn.log = console.warn.bind(console); + this._error.log = console.error.bind(console); + /* eslint-enable no-console */ + } + get debug() { + return this._debug; + } + get warn() { + return this._warn; + } + get error() { + return this._error; + } +} +exports.Logger = Logger; + +},{"debug":47}],18:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Producer = void 0; +const Logger_1 = require("./Logger"); +const EnhancedEventEmitter_1 = require("./EnhancedEventEmitter"); +const errors_1 = require("./errors"); +const logger = new Logger_1.Logger('Producer'); +class Producer extends EnhancedEventEmitter_1.EnhancedEventEmitter { + constructor({ id, localId, rtpSender, track, rtpParameters, stopTracks, disableTrackOnPause, zeroRtpOnPause, appData }) { + super(); + // Closed flag. + this._closed = false; + // Observer instance. + this._observer = new EnhancedEventEmitter_1.EnhancedEventEmitter(); + logger.debug('constructor()'); + this._id = id; + this._localId = localId; + this._rtpSender = rtpSender; + this._track = track; + this._kind = track.kind; + this._rtpParameters = rtpParameters; + this._paused = disableTrackOnPause ? !track.enabled : false; + this._maxSpatialLayer = undefined; + this._stopTracks = stopTracks; + this._disableTrackOnPause = disableTrackOnPause; + this._zeroRtpOnPause = zeroRtpOnPause; + this._appData = appData || {}; + this.onTrackEnded = this.onTrackEnded.bind(this); + // NOTE: Minor issue. If zeroRtpOnPause is true, we cannot emit the + // '@replacetrack' event here, so RTCRtpSender.track won't be null. + this.handleTrack(); + } + /** + * Producer id. + */ + get id() { + return this._id; + } + /** + * Local id. + */ + get localId() { + return this._localId; + } + /** + * Whether the Producer is closed. + */ + get closed() { + return this._closed; + } + /** + * Media kind. + */ + get kind() { + return this._kind; + } + /** + * Associated RTCRtpSender. + */ + get rtpSender() { + return this._rtpSender; + } + /** + * The associated track. + */ + get track() { + return this._track; + } + /** + * RTP parameters. + */ + get rtpParameters() { + return this._rtpParameters; + } + /** + * Whether the Producer is paused. + */ + get paused() { + return this._paused; + } + /** + * Max spatial layer. + * + * @type {Number | undefined} + */ + get maxSpatialLayer() { + return this._maxSpatialLayer; + } + /** + * App custom data. */ get appData() { return this._appData; @@ -1424,29 +2584,23 @@ class Consumer extends EnhancedEventEmitter_1.EnhancedEventEmitter { /** * Invalid setter. */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars set appData(appData) { throw new Error('cannot override appData object'); } - /** - * Observer. - * - * @emits close - * @emits pause - * @emits resume - * @emits trackended - */ get observer() { return this._observer; } /** - * Closes the Consumer. + * Closes the Producer. */ close() { - if (this._closed) + if (this._closed) { return; + } logger.debug('close()'); this._closed = true; - this._destroyTrack(); + this.destroyTrack(); this.emit('@close'); // Emit observer event. this._observer.safeEmit('close'); @@ -1455,153 +2609,332 @@ class Consumer extends EnhancedEventEmitter_1.EnhancedEventEmitter { * Transport was closed. */ transportClosed() { - if (this._closed) + if (this._closed) { return; + } logger.debug('transportClosed()'); this._closed = true; - this._destroyTrack(); + this.destroyTrack(); this.safeEmit('transportclose'); // Emit observer event. this._observer.safeEmit('close'); } /** - * Get associated RTCRtpReceiver stats. + * Get associated RTCRtpSender stats. */ async getStats() { - if (this._closed) + if (this._closed) { throw new errors_1.InvalidStateError('closed'); - return this.safeEmitAsPromise('@getstats'); + } + return new Promise((resolve, reject) => { + this.safeEmit('@getstats', resolve, reject); + }); } /** - * Pauses receiving media. + * Pauses sending media. */ pause() { logger.debug('pause()'); if (this._closed) { - logger.error('pause() | Consumer closed'); + logger.error('pause() | Producer closed'); return; } this._paused = true; - this._track.enabled = false; + if (this._track && this._disableTrackOnPause) { + this._track.enabled = false; + } + if (this._zeroRtpOnPause) { + new Promise((resolve, reject) => { + this.safeEmit('@pause', resolve, reject); + }).catch(() => { }); + } // Emit observer event. this._observer.safeEmit('pause'); } /** - * Resumes receiving media. + * Resumes sending media. */ resume() { logger.debug('resume()'); if (this._closed) { - logger.error('resume() | Consumer closed'); + logger.error('resume() | Producer closed'); return; } this._paused = false; - this._track.enabled = true; + if (this._track && this._disableTrackOnPause) { + this._track.enabled = true; + } + if (this._zeroRtpOnPause) { + new Promise((resolve, reject) => { + this.safeEmit('@resume', resolve, reject); + }).catch(() => { }); + } // Emit observer event. this._observer.safeEmit('resume'); } - _onTrackEnded() { + /** + * Replaces the current track with a new one or null. + */ + async replaceTrack({ track }) { + logger.debug('replaceTrack() [track:%o]', track); + if (this._closed) { + // This must be done here. Otherwise there is no chance to stop the given + // track. + if (track && this._stopTracks) { + try { + track.stop(); + } + catch (error) { } + } + throw new errors_1.InvalidStateError('closed'); + } + else if (track && track.readyState === 'ended') { + throw new errors_1.InvalidStateError('track ended'); + } + // Do nothing if this is the same track as the current handled one. + if (track === this._track) { + logger.debug('replaceTrack() | same track, ignored'); + return; + } + await new Promise((resolve, reject) => { + this.safeEmit('@replacetrack', track, resolve, reject); + }); + // Destroy the previous track. + this.destroyTrack(); + // Set the new track. + this._track = track; + // If this Producer was paused/resumed and the state of the new + // track does not match, fix it. + if (this._track && this._disableTrackOnPause) { + if (!this._paused) { + this._track.enabled = true; + } + else if (this._paused) { + this._track.enabled = false; + } + } + // Handle the effective track. + this.handleTrack(); + } + /** + * Sets the video max spatial layer to be sent. + */ + async setMaxSpatialLayer(spatialLayer) { + if (this._closed) { + throw new errors_1.InvalidStateError('closed'); + } + else if (this._kind !== 'video') { + throw new errors_1.UnsupportedError('not a video Producer'); + } + else if (typeof spatialLayer !== 'number') { + throw new TypeError('invalid spatialLayer'); + } + if (spatialLayer === this._maxSpatialLayer) { + return; + } + await new Promise((resolve, reject) => { + this.safeEmit('@setmaxspatiallayer', spatialLayer, resolve, reject); + }).catch(() => { }); + this._maxSpatialLayer = spatialLayer; + } + async setRtpEncodingParameters(params) { + if (this._closed) { + throw new errors_1.InvalidStateError('closed'); + } + else if (typeof params !== 'object') { + throw new TypeError('invalid params'); + } + await new Promise((resolve, reject) => { + this.safeEmit('@setrtpencodingparameters', params, resolve, reject); + }); + } + onTrackEnded() { logger.debug('track "ended" event'); this.safeEmit('trackended'); // Emit observer event. this._observer.safeEmit('trackended'); } - _handleTrack() { - this._track.addEventListener('ended', this._onTrackEnded); + handleTrack() { + if (!this._track) { + return; + } + this._track.addEventListener('ended', this.onTrackEnded); } - _destroyTrack() { + destroyTrack() { + if (!this._track) { + return; + } try { - this._track.removeEventListener('ended', this._onTrackEnded); - this._track.stop(); + this._track.removeEventListener('ended', this.onTrackEnded); + // Just stop the track unless the app set stopTracks: false. + if (this._stopTracks) { + this._track.stop(); + } } catch (error) { } } } -exports.Consumer = Consumer; +exports.Producer = Producer; + +},{"./EnhancedEventEmitter":16,"./Logger":17,"./errors":22}],19:[function(require,module,exports){ +"use strict"; +/** + * The RTP capabilities define what mediasoup or an endpoint can receive at + * media level. + */ +Object.defineProperty(exports, "__esModule", { value: true }); -},{"./EnhancedEventEmitter":12,"./Logger":13,"./errors":18}],9:[function(require,module,exports){ +},{}],20:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.DataConsumer = void 0; + +},{}],21:[function(require,module,exports){ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Transport = void 0; +const awaitqueue_1 = require("awaitqueue"); +const queue_microtask_1 = __importDefault(require("queue-microtask")); const Logger_1 = require("./Logger"); const EnhancedEventEmitter_1 = require("./EnhancedEventEmitter"); -const logger = new Logger_1.Logger('DataConsumer'); -class DataConsumer extends EnhancedEventEmitter_1.EnhancedEventEmitter { - /** - * @emits transportclose - * @emits open - * @emits error - (error: Error) - * @emits close - * @emits message - (message: any) - * @emits @close - */ - constructor({ id, dataProducerId, dataChannel, sctpStreamParameters, appData }) { +const errors_1 = require("./errors"); +const utils = __importStar(require("./utils")); +const ortc = __importStar(require("./ortc")); +const Producer_1 = require("./Producer"); +const Consumer_1 = require("./Consumer"); +const DataProducer_1 = require("./DataProducer"); +const DataConsumer_1 = require("./DataConsumer"); +const logger = new Logger_1.Logger('Transport'); +class ConsumerCreationTask { + constructor(consumerOptions) { + this.consumerOptions = consumerOptions; + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + } +} +class Transport extends EnhancedEventEmitter_1.EnhancedEventEmitter { + constructor({ direction, id, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, appData, handlerFactory, extendedRtpCapabilities, canProduceByKind }) { super(); // Closed flag. this._closed = false; + // Transport connection state. + this._connectionState = 'new'; + // Map of Producers indexed by id. + this._producers = new Map(); + // Map of Consumers indexed by id. + this._consumers = new Map(); + // Map of DataProducers indexed by id. + this._dataProducers = new Map(); + // Map of DataConsumers indexed by id. + this._dataConsumers = new Map(); + // Whether the Consumer for RTP probation has been created. + this._probatorConsumerCreated = false; + // AwaitQueue instance to make async tasks happen sequentially. + this._awaitQueue = new awaitqueue_1.AwaitQueue(); + // Consumer creation tasks awaiting to be processed. + this._pendingConsumerTasks = []; + // Consumer creation in progress flag. + this._consumerCreationInProgress = false; + // Consumers pending to be paused. + this._pendingPauseConsumers = new Map(); + // Consumer pause in progress flag. + this._consumerPauseInProgress = false; + // Consumers pending to be resumed. + this._pendingResumeConsumers = new Map(); + // Consumer resume in progress flag. + this._consumerResumeInProgress = false; + // Consumers pending to be closed. + this._pendingCloseConsumers = new Map(); + // Consumer close in progress flag. + this._consumerCloseInProgress = false; // Observer instance. this._observer = new EnhancedEventEmitter_1.EnhancedEventEmitter(); - logger.debug('constructor()'); + logger.debug('constructor() [id:%s, direction:%s]', id, direction); this._id = id; - this._dataProducerId = dataProducerId; - this._dataChannel = dataChannel; - this._sctpStreamParameters = sctpStreamParameters; - this._appData = appData; - this._handleDataChannel(); + this._direction = direction; + this._extendedRtpCapabilities = extendedRtpCapabilities; + this._canProduceByKind = canProduceByKind; + this._maxSctpMessageSize = + sctpParameters ? sctpParameters.maxMessageSize : null; + // Clone and sanitize additionalSettings. + additionalSettings = utils.clone(additionalSettings, {}); + delete additionalSettings.iceServers; + delete additionalSettings.iceTransportPolicy; + delete additionalSettings.bundlePolicy; + delete additionalSettings.rtcpMuxPolicy; + delete additionalSettings.sdpSemantics; + this._handler = handlerFactory(); + this._handler.run({ + direction, + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + iceServers, + iceTransportPolicy, + additionalSettings, + proprietaryConstraints, + extendedRtpCapabilities + }); + this._appData = appData || {}; + this.handleHandler(); } /** - * DataConsumer id. + * Transport id. */ get id() { return this._id; } /** - * Associated DataProducer id. - */ - get dataProducerId() { - return this._dataProducerId; - } - /** - * Whether the DataConsumer is closed. + * Whether the Transport is closed. */ get closed() { return this._closed; } /** - * SCTP stream parameters. - */ - get sctpStreamParameters() { - return this._sctpStreamParameters; - } - /** - * DataChannel readyState. - */ - get readyState() { - return this._dataChannel.readyState; - } - /** - * DataChannel label. - */ - get label() { - return this._dataChannel.label; - } - /** - * DataChannel protocol. + * Transport direction. */ - get protocol() { - return this._dataChannel.protocol; + get direction() { + return this._direction; } /** - * DataChannel binaryType. + * RTC handler instance. */ - get binaryType() { - return this._dataChannel.binaryType; + get handler() { + return this._handler; } /** - * Set DataChannel binaryType. + * Connection state. */ - set binaryType(binaryType) { - this._dataChannel.binaryType = binaryType; + get connectionState() { + return this._connectionState; } /** * App custom data. @@ -1612,1616 +2945,1925 @@ class DataConsumer extends EnhancedEventEmitter_1.EnhancedEventEmitter { /** * Invalid setter. */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars set appData(appData) { throw new Error('cannot override appData object'); } - /** - * Observer. - * - * @emits close - */ get observer() { return this._observer; } /** - * Closes the DataConsumer. + * Close the Transport. */ close() { - if (this._closed) + if (this._closed) { return; + } logger.debug('close()'); this._closed = true; - this._dataChannel.close(); - this.emit('@close'); - // Emit observer event. - this._observer.safeEmit('close'); - } - /** - * Transport was closed. - */ - transportClosed() { - if (this._closed) - return; - logger.debug('transportClosed()'); - this._closed = true; - this._dataChannel.close(); - this.safeEmit('transportclose'); - // Emit observer event. - this._observer.safeEmit('close'); - } - _handleDataChannel() { - this._dataChannel.addEventListener('open', () => { - if (this._closed) - return; - logger.debug('DataChannel "open" event'); - this.safeEmit('open'); - }); - this._dataChannel.addEventListener('error', (event) => { - if (this._closed) - return; - let { error } = event; - if (!error) - error = new Error('unknown DataChannel error'); - if (error.errorDetail === 'sctp-failure') { - logger.error('DataChannel SCTP error [sctpCauseCode:%s]: %s', error.sctpCauseCode, error.message); - } - else { - logger.error('DataChannel "error" event: %o', error); - } - this.safeEmit('error', error); - }); - this._dataChannel.addEventListener('close', () => { - if (this._closed) - return; - logger.warn('DataChannel "close" event'); - this._closed = true; - this.emit('@close'); - this.safeEmit('close'); - }); - this._dataChannel.addEventListener('message', (event) => { - if (this._closed) - return; - this.safeEmit('message', event.data); - }); - } -} -exports.DataConsumer = DataConsumer; - -},{"./EnhancedEventEmitter":12,"./Logger":13}],10:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.DataProducer = void 0; -const Logger_1 = require("./Logger"); -const EnhancedEventEmitter_1 = require("./EnhancedEventEmitter"); -const errors_1 = require("./errors"); -const logger = new Logger_1.Logger('DataProducer'); -class DataProducer extends EnhancedEventEmitter_1.EnhancedEventEmitter { - /** - * @emits transportclose - * @emits open - * @emits error - (error: Error) - * @emits close - * @emits bufferedamountlow - * @emits @close - */ - constructor({ id, dataChannel, sctpStreamParameters, appData }) { - super(); - // Closed flag. - this._closed = false; - // Observer instance. - this._observer = new EnhancedEventEmitter_1.EnhancedEventEmitter(); - logger.debug('constructor()'); - this._id = id; - this._dataChannel = dataChannel; - this._sctpStreamParameters = sctpStreamParameters; - this._appData = appData; - this._handleDataChannel(); - } - /** - * DataProducer id. - */ - get id() { - return this._id; - } - /** - * Whether the DataProducer is closed. - */ - get closed() { - return this._closed; - } - /** - * SCTP stream parameters. - */ - get sctpStreamParameters() { - return this._sctpStreamParameters; - } - /** - * DataChannel readyState. - */ - get readyState() { - return this._dataChannel.readyState; - } - /** - * DataChannel label. - */ - get label() { - return this._dataChannel.label; - } - /** - * DataChannel protocol. - */ - get protocol() { - return this._dataChannel.protocol; - } - /** - * DataChannel bufferedAmount. - */ - get bufferedAmount() { - return this._dataChannel.bufferedAmount; + // Stop the AwaitQueue. + this._awaitQueue.stop(); + // Close the handler. + this._handler.close(); + // Close all Producers. + for (const producer of this._producers.values()) { + producer.transportClosed(); + } + this._producers.clear(); + // Close all Consumers. + for (const consumer of this._consumers.values()) { + consumer.transportClosed(); + } + this._consumers.clear(); + // Close all DataProducers. + for (const dataProducer of this._dataProducers.values()) { + dataProducer.transportClosed(); + } + this._dataProducers.clear(); + // Close all DataConsumers. + for (const dataConsumer of this._dataConsumers.values()) { + dataConsumer.transportClosed(); + } + this._dataConsumers.clear(); + // Emit observer event. + this._observer.safeEmit('close'); } /** - * DataChannel bufferedAmountLowThreshold. + * Get associated Transport (RTCPeerConnection) stats. + * + * @returns {RTCStatsReport} */ - get bufferedAmountLowThreshold() { - return this._dataChannel.bufferedAmountLowThreshold; + async getStats() { + if (this._closed) { + throw new errors_1.InvalidStateError('closed'); + } + return this._handler.getTransportStats(); } /** - * Set DataChannel bufferedAmountLowThreshold. + * Restart ICE connection. */ - set bufferedAmountLowThreshold(bufferedAmountLowThreshold) { - this._dataChannel.bufferedAmountLowThreshold = bufferedAmountLowThreshold; + async restartIce({ iceParameters }) { + logger.debug('restartIce()'); + if (this._closed) { + throw new errors_1.InvalidStateError('closed'); + } + else if (!iceParameters) { + throw new TypeError('missing iceParameters'); + } + // Enqueue command. + return this._awaitQueue.push(async () => this._handler.restartIce(iceParameters), 'transport.restartIce()'); } /** - * App custom data. + * Update ICE servers. */ - get appData() { - return this._appData; + async updateIceServers({ iceServers } = {}) { + logger.debug('updateIceServers()'); + if (this._closed) { + throw new errors_1.InvalidStateError('closed'); + } + else if (!Array.isArray(iceServers)) { + throw new TypeError('missing iceServers'); + } + // Enqueue command. + return this._awaitQueue.push(async () => this._handler.updateIceServers(iceServers), 'transport.updateIceServers()'); } /** - * Invalid setter. + * Create a Producer. */ - set appData(appData) { - throw new Error('cannot override appData object'); + async produce({ track, encodings, codecOptions, codec, stopTracks = true, disableTrackOnPause = true, zeroRtpOnPause = false, appData = {} } = {}) { + logger.debug('produce() [track:%o]', track); + if (this._closed) { + throw new errors_1.InvalidStateError('closed'); + } + else if (!track) { + throw new TypeError('missing track'); + } + else if (this._direction !== 'send') { + throw new errors_1.UnsupportedError('not a sending Transport'); + } + else if (!this._canProduceByKind[track.kind]) { + throw new errors_1.UnsupportedError(`cannot produce ${track.kind}`); + } + else if (track.readyState === 'ended') { + throw new errors_1.InvalidStateError('track ended'); + } + else if (this.listenerCount('connect') === 0 && this._connectionState === 'new') { + throw new TypeError('no "connect" listener set into this transport'); + } + else if (this.listenerCount('produce') === 0) { + throw new TypeError('no "produce" listener set into this transport'); + } + else if (appData && typeof appData !== 'object') { + throw new TypeError('if given, appData must be an object'); + } + // Enqueue command. + return this._awaitQueue.push(async () => { + let normalizedEncodings; + if (encodings && !Array.isArray(encodings)) { + throw TypeError('encodings must be an array'); + } + else if (encodings && encodings.length === 0) { + normalizedEncodings = undefined; + } + else if (encodings) { + normalizedEncodings = encodings + .map((encoding) => { + const normalizedEncoding = { active: true }; + if (encoding.active === false) { + normalizedEncoding.active = false; + } + if (typeof encoding.dtx === 'boolean') { + normalizedEncoding.dtx = encoding.dtx; + } + if (typeof encoding.scalabilityMode === 'string') { + normalizedEncoding.scalabilityMode = encoding.scalabilityMode; + } + if (typeof encoding.scaleResolutionDownBy === 'number') { + normalizedEncoding.scaleResolutionDownBy = encoding.scaleResolutionDownBy; + } + if (typeof encoding.maxBitrate === 'number') { + normalizedEncoding.maxBitrate = encoding.maxBitrate; + } + if (typeof encoding.maxFramerate === 'number') { + normalizedEncoding.maxFramerate = encoding.maxFramerate; + } + if (typeof encoding.adaptivePtime === 'boolean') { + normalizedEncoding.adaptivePtime = encoding.adaptivePtime; + } + if (typeof encoding.priority === 'string') { + normalizedEncoding.priority = encoding.priority; + } + if (typeof encoding.networkPriority === 'string') { + normalizedEncoding.networkPriority = encoding.networkPriority; + } + return normalizedEncoding; + }); + } + const { localId, rtpParameters, rtpSender } = await this._handler.send({ + track, + encodings: normalizedEncodings, + codecOptions, + codec + }); + try { + // This will fill rtpParameters's missing fields with default values. + ortc.validateRtpParameters(rtpParameters); + const { id } = await new Promise((resolve, reject) => { + this.safeEmit('produce', { + kind: track.kind, + rtpParameters, + appData + }, resolve, reject); + }); + const producer = new Producer_1.Producer({ + id, + localId, + rtpSender, + track, + rtpParameters, + stopTracks, + disableTrackOnPause, + zeroRtpOnPause, + appData + }); + this._producers.set(producer.id, producer); + this.handleProducer(producer); + // Emit observer event. + this._observer.safeEmit('newproducer', producer); + return producer; + } + catch (error) { + this._handler.stopSending(localId) + .catch(() => { }); + throw error; + } + }, 'transport.produce()') + // This catch is needed to stop the given track if the command above + // failed due to closed Transport. + .catch((error) => { + if (stopTracks) { + try { + track.stop(); + } + catch (error2) { } + } + throw error; + }); } /** - * Observer. - * - * @emits close + * Create a Consumer to consume a remote Producer. */ - get observer() { - return this._observer; + async consume({ id, producerId, kind, rtpParameters, streamId, appData = {} }) { + logger.debug('consume()'); + rtpParameters = utils.clone(rtpParameters, undefined); + if (this._closed) { + throw new errors_1.InvalidStateError('closed'); + } + else if (this._direction !== 'recv') { + throw new errors_1.UnsupportedError('not a receiving Transport'); + } + else if (typeof id !== 'string') { + throw new TypeError('missing id'); + } + else if (typeof producerId !== 'string') { + throw new TypeError('missing producerId'); + } + else if (kind !== 'audio' && kind !== 'video') { + throw new TypeError(`invalid kind '${kind}'`); + } + else if (this.listenerCount('connect') === 0 && this._connectionState === 'new') { + throw new TypeError('no "connect" listener set into this transport'); + } + else if (appData && typeof appData !== 'object') { + throw new TypeError('if given, appData must be an object'); + } + // Ensure the device can consume it. + const canConsume = ortc.canReceive(rtpParameters, this._extendedRtpCapabilities); + if (!canConsume) { + throw new errors_1.UnsupportedError('cannot consume this Producer'); + } + const consumerCreationTask = new ConsumerCreationTask({ + id, + producerId, + kind, + rtpParameters, + streamId, + appData + }); + // Store the Consumer creation task. + this._pendingConsumerTasks.push(consumerCreationTask); + // There is no Consumer creation in progress, create it now. + (0, queue_microtask_1.default)(() => { + if (this._closed) { + throw new errors_1.InvalidStateError('closed'); + } + if (this._consumerCreationInProgress === false) { + this.createPendingConsumers(); + } + }); + return consumerCreationTask.promise; } /** - * Closes the DataProducer. + * Create a DataProducer */ - close() { - if (this._closed) - return; - logger.debug('close()'); - this._closed = true; - this._dataChannel.close(); - this.emit('@close'); - // Emit observer event. - this._observer.safeEmit('close'); + async produceData({ ordered = true, maxPacketLifeTime, maxRetransmits, label = '', protocol = '', appData = {} } = {}) { + logger.debug('produceData()'); + if (this._closed) { + throw new errors_1.InvalidStateError('closed'); + } + else if (this._direction !== 'send') { + throw new errors_1.UnsupportedError('not a sending Transport'); + } + else if (!this._maxSctpMessageSize) { + throw new errors_1.UnsupportedError('SCTP not enabled by remote Transport'); + } + else if (this.listenerCount('connect') === 0 && this._connectionState === 'new') { + throw new TypeError('no "connect" listener set into this transport'); + } + else if (this.listenerCount('producedata') === 0) { + throw new TypeError('no "producedata" listener set into this transport'); + } + else if (appData && typeof appData !== 'object') { + throw new TypeError('if given, appData must be an object'); + } + if (maxPacketLifeTime || maxRetransmits) { + ordered = false; + } + // Enqueue command. + return this._awaitQueue.push(async () => { + const { dataChannel, sctpStreamParameters } = await this._handler.sendDataChannel({ + ordered, + maxPacketLifeTime, + maxRetransmits, + label, + protocol + }); + // This will fill sctpStreamParameters's missing fields with default values. + ortc.validateSctpStreamParameters(sctpStreamParameters); + const { id } = await new Promise((resolve, reject) => { + this.safeEmit('producedata', { + sctpStreamParameters, + label, + protocol, + appData + }, resolve, reject); + }); + const dataProducer = new DataProducer_1.DataProducer({ id, dataChannel, sctpStreamParameters, appData }); + this._dataProducers.set(dataProducer.id, dataProducer); + this.handleDataProducer(dataProducer); + // Emit observer event. + this._observer.safeEmit('newdataproducer', dataProducer); + return dataProducer; + }, 'transport.produceData()'); } /** - * Transport was closed. + * Create a DataConsumer */ - transportClosed() { - if (this._closed) - return; - logger.debug('transportClosed()'); - this._closed = true; - this._dataChannel.close(); - this.safeEmit('transportclose'); - // Emit observer event. - this._observer.safeEmit('close'); + async consumeData({ id, dataProducerId, sctpStreamParameters, label = '', protocol = '', appData = {} }) { + logger.debug('consumeData()'); + sctpStreamParameters = utils.clone(sctpStreamParameters, undefined); + if (this._closed) { + throw new errors_1.InvalidStateError('closed'); + } + else if (this._direction !== 'recv') { + throw new errors_1.UnsupportedError('not a receiving Transport'); + } + else if (!this._maxSctpMessageSize) { + throw new errors_1.UnsupportedError('SCTP not enabled by remote Transport'); + } + else if (typeof id !== 'string') { + throw new TypeError('missing id'); + } + else if (typeof dataProducerId !== 'string') { + throw new TypeError('missing dataProducerId'); + } + else if (this.listenerCount('connect') === 0 && this._connectionState === 'new') { + throw new TypeError('no "connect" listener set into this transport'); + } + else if (appData && typeof appData !== 'object') { + throw new TypeError('if given, appData must be an object'); + } + // This may throw. + ortc.validateSctpStreamParameters(sctpStreamParameters); + // Enqueue command. + return this._awaitQueue.push(async () => { + const { dataChannel } = await this._handler.receiveDataChannel({ + sctpStreamParameters, + label, + protocol + }); + const dataConsumer = new DataConsumer_1.DataConsumer({ + id, + dataProducerId, + dataChannel, + sctpStreamParameters, + appData + }); + this._dataConsumers.set(dataConsumer.id, dataConsumer); + this.handleDataConsumer(dataConsumer); + // Emit observer event. + this._observer.safeEmit('newdataconsumer', dataConsumer); + return dataConsumer; + }, 'transport.consumeData()'); } - /** - * Send a message. - * - * @param {String|Blob|ArrayBuffer|ArrayBufferView} data. - */ - send(data) { - logger.debug('send()'); - if (this._closed) - throw new errors_1.InvalidStateError('closed'); - this._dataChannel.send(data); + // This method is guaranteed to never throw. + async createPendingConsumers() { + this._consumerCreationInProgress = true; + this._awaitQueue.push(async () => { + if (this._pendingConsumerTasks.length === 0) { + logger.debug('createPendingConsumers() | there is no Consumer to be created'); + return; + } + const pendingConsumerTasks = [...this._pendingConsumerTasks]; + // Clear pending Consumer tasks. + this._pendingConsumerTasks = []; + // Video Consumer in order to create the probator. + let videoConsumerForProbator = undefined; + // Fill options list. + const optionsList = []; + for (const task of pendingConsumerTasks) { + const { id, kind, rtpParameters, streamId } = task.consumerOptions; + optionsList.push({ + trackId: id, + kind: kind, + rtpParameters, + streamId + }); + } + try { + const results = await this._handler.receive(optionsList); + for (let idx = 0; idx < results.length; idx++) { + const task = pendingConsumerTasks[idx]; + const result = results[idx]; + const { id, producerId, kind, rtpParameters, appData } = task.consumerOptions; + const { localId, rtpReceiver, track } = result; + const consumer = new Consumer_1.Consumer({ + id: id, + localId, + producerId: producerId, + rtpReceiver, + track, + rtpParameters, + appData + }); + this._consumers.set(consumer.id, consumer); + this.handleConsumer(consumer); + // If this is the first video Consumer and the Consumer for RTP probation + // has not yet been created, it's time to create it. + if (!this._probatorConsumerCreated && + !videoConsumerForProbator && kind === 'video') { + videoConsumerForProbator = consumer; + } + // Emit observer event. + this._observer.safeEmit('newconsumer', consumer); + task.resolve(consumer); + } + } + catch (error) { + for (const task of pendingConsumerTasks) { + task.reject(error); + } + } + // If RTP probation must be handled, do it now. + if (videoConsumerForProbator) { + try { + const probatorRtpParameters = ortc.generateProbatorRtpParameters(videoConsumerForProbator.rtpParameters); + await this._handler.receive([{ + trackId: 'probator', + kind: 'video', + rtpParameters: probatorRtpParameters + }]); + logger.debug('createPendingConsumers() | Consumer for RTP probation created'); + this._probatorConsumerCreated = true; + } + catch (error) { + logger.error('createPendingConsumers() | failed to create Consumer for RTP probation:%o', error); + } + } + }, 'transport.createPendingConsumers()') + .then(() => { + this._consumerCreationInProgress = false; + // There are pending Consumer tasks, enqueue their creation. + if (this._pendingConsumerTasks.length > 0) { + this.createPendingConsumers(); + } + }) + // NOTE: We only get here when the await queue is closed. + .catch(() => { }); + } + pausePendingConsumers() { + this._consumerPauseInProgress = true; + this._awaitQueue.push(async () => { + if (this._pendingPauseConsumers.size === 0) { + logger.debug('pausePendingConsumers() | there is no Consumer to be paused'); + return; + } + const pendingPauseConsumers = Array.from(this._pendingPauseConsumers.values()); + // Clear pending pause Consumer map. + this._pendingPauseConsumers.clear(); + try { + const localIds = pendingPauseConsumers + .map((consumer) => consumer.localId); + await this._handler.pauseReceiving(localIds); + } + catch (error) { + logger.error('pausePendingConsumers() | failed to pause Consumers:', error); + } + }, 'transport.pausePendingConsumers') + .then(() => { + this._consumerPauseInProgress = false; + // There are pending Consumers to be paused, do it. + if (this._pendingPauseConsumers.size > 0) { + this.pausePendingConsumers(); + } + }) + // NOTE: We only get here when the await queue is closed. + .catch(() => { }); + } + resumePendingConsumers() { + this._consumerResumeInProgress = true; + this._awaitQueue.push(async () => { + if (this._pendingResumeConsumers.size === 0) { + logger.debug('resumePendingConsumers() | there is no Consumer to be resumed'); + return; + } + const pendingResumeConsumers = Array.from(this._pendingResumeConsumers.values()); + // Clear pending resume Consumer map. + this._pendingResumeConsumers.clear(); + try { + const localIds = pendingResumeConsumers + .map((consumer) => consumer.localId); + await this._handler.resumeReceiving(localIds); + } + catch (error) { + logger.error('resumePendingConsumers() | failed to resume Consumers:', error); + } + }, 'transport.resumePendingConsumers') + .then(() => { + this._consumerResumeInProgress = false; + // There are pending Consumer to be resumed, do it. + if (this._pendingResumeConsumers.size > 0) { + this.resumePendingConsumers(); + } + }) + // NOTE: We only get here when the await queue is closed. + .catch(() => { }); + } + closePendingConsumers() { + this._consumerCloseInProgress = true; + this._awaitQueue.push(async () => { + if (this._pendingCloseConsumers.size === 0) { + logger.debug('closePendingConsumers() | there is no Consumer to be closed'); + return; + } + const pendingCloseConsumers = Array.from(this._pendingCloseConsumers.values()); + // Clear pending close Consumer map. + this._pendingCloseConsumers.clear(); + try { + await this._handler.stopReceiving(pendingCloseConsumers.map((consumer) => consumer.localId)); + } + catch (error) { + logger.error('closePendingConsumers() | failed to close Consumers:', error); + } + }, 'transport.closePendingConsumers') + .then(() => { + this._consumerCloseInProgress = false; + // There are pending Consumer to be resumed, do it. + if (this._pendingCloseConsumers.size > 0) { + this.closePendingConsumers(); + } + }) + // NOTE: We only get here when the await queue is closed. + .catch(() => { }); } - _handleDataChannel() { - this._dataChannel.addEventListener('open', () => { - if (this._closed) + handleHandler() { + const handler = this._handler; + handler.on('@connect', ({ dtlsParameters }, callback, errback) => { + if (this._closed) { + errback(new errors_1.InvalidStateError('closed')); return; - logger.debug('DataChannel "open" event'); - this.safeEmit('open'); + } + this.safeEmit('connect', { dtlsParameters }, callback, errback); }); - this._dataChannel.addEventListener('error', (event) => { - if (this._closed) + handler.on('@connectionstatechange', (connectionState) => { + if (connectionState === this._connectionState) { return; - let { error } = event; - if (!error) - error = new Error('unknown DataChannel error'); - if (error.errorDetail === 'sctp-failure') { - logger.error('DataChannel SCTP error [sctpCauseCode:%s]: %s', error.sctpCauseCode, error.message); } - else { - logger.error('DataChannel "error" event: %o', error); + logger.debug('connection state changed to %s', connectionState); + this._connectionState = connectionState; + if (!this._closed) { + this.safeEmit('connectionstatechange', connectionState); } - this.safeEmit('error', error); }); - this._dataChannel.addEventListener('close', () => { - if (this._closed) + } + handleProducer(producer) { + producer.on('@close', () => { + this._producers.delete(producer.id); + if (this._closed) { return; - logger.warn('DataChannel "close" event'); - this._closed = true; - this.emit('@close'); - this.safeEmit('close'); + } + this._awaitQueue.push(async () => this._handler.stopSending(producer.localId), 'producer @close event') + .catch((error) => logger.warn('producer.close() failed:%o', error)); }); - this._dataChannel.addEventListener('message', () => { - if (this._closed) - return; - logger.warn('DataChannel "message" event in a DataProducer, message discarded'); + producer.on('@pause', (callback, errback) => { + this._awaitQueue.push(async () => this._handler.pauseSending(producer.localId), 'producer @pause event') + .then(callback) + .catch(errback); }); - this._dataChannel.addEventListener('bufferedamountlow', () => { - if (this._closed) - return; - this.safeEmit('bufferedamountlow'); + producer.on('@resume', (callback, errback) => { + this._awaitQueue.push(async () => this._handler.resumeSending(producer.localId), 'producer @resume event') + .then(callback) + .catch(errback); + }); + producer.on('@replacetrack', (track, callback, errback) => { + this._awaitQueue.push(async () => this._handler.replaceTrack(producer.localId, track), 'producer @replacetrack event') + .then(callback) + .catch(errback); + }); + producer.on('@setmaxspatiallayer', (spatialLayer, callback, errback) => { + this._awaitQueue.push(async () => (this._handler.setMaxSpatialLayer(producer.localId, spatialLayer)), 'producer @setmaxspatiallayer event') + .then(callback) + .catch(errback); + }); + producer.on('@setrtpencodingparameters', (params, callback, errback) => { + this._awaitQueue.push(async () => (this._handler.setRtpEncodingParameters(producer.localId, params)), 'producer @setrtpencodingparameters event') + .then(callback) + .catch(errback); + }); + producer.on('@getstats', (callback, errback) => { + if (this._closed) { + return errback(new errors_1.InvalidStateError('closed')); + } + this._handler.getSenderStats(producer.localId) + .then(callback) + .catch(errback); }); } -} -exports.DataProducer = DataProducer; - -},{"./EnhancedEventEmitter":12,"./Logger":13,"./errors":18}],11:[function(require,module,exports){ -"use strict"; -/* global RTCRtpTransceiver */ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Device = exports.detectDevice = void 0; -const bowser_1 = __importDefault(require("bowser")); -const Logger_1 = require("./Logger"); -const EnhancedEventEmitter_1 = require("./EnhancedEventEmitter"); -const errors_1 = require("./errors"); -const utils = __importStar(require("./utils")); -const ortc = __importStar(require("./ortc")); -const Transport_1 = require("./Transport"); -const Chrome74_1 = require("./handlers/Chrome74"); -const Chrome70_1 = require("./handlers/Chrome70"); -const Chrome67_1 = require("./handlers/Chrome67"); -const Chrome55_1 = require("./handlers/Chrome55"); -const Firefox60_1 = require("./handlers/Firefox60"); -const Safari12_1 = require("./handlers/Safari12"); -const Safari11_1 = require("./handlers/Safari11"); -const Edge11_1 = require("./handlers/Edge11"); -const ReactNative_1 = require("./handlers/ReactNative"); -const logger = new Logger_1.Logger('Device'); -function detectDevice() { - // React-Native. - // NOTE: react-native-webrtc >= 1.75.0 is required. - if (typeof navigator === 'object' && navigator.product === 'ReactNative') { - if (typeof RTCPeerConnection === 'undefined') { - logger.warn('this._detectDevice() | unsupported ReactNative without RTCPeerConnection'); - return undefined; - } - logger.debug('this._detectDevice() | ReactNative handler chosen'); - return 'ReactNative'; - } - // Browser. - else if (typeof navigator === 'object' && typeof navigator.userAgent === 'string') { - const ua = navigator.userAgent; - const browser = bowser_1.default.getParser(ua); - const engine = browser.getEngine(); - // Chrome and Chromium. - if (browser.satisfies({ chrome: '>=74', chromium: '>=74' })) { - return 'Chrome74'; - } - else if (browser.satisfies({ chrome: '>=70', chromium: '>=70' })) { - return 'Chrome70'; - } - else if (browser.satisfies({ chrome: '>=67', chromium: '>=67' })) { - return 'Chrome67'; - } - else if (browser.satisfies({ chrome: '>=55', chromium: '>=55' })) { - return 'Chrome55'; - } - // Firefox. - else if (browser.satisfies({ firefox: '>=60' })) { - return 'Firefox60'; - } - // Safari with Unified-Plan support enabled. - else if (browser.satisfies({ safari: '>=12.0' }) && - typeof RTCRtpTransceiver !== 'undefined' && - RTCRtpTransceiver.prototype.hasOwnProperty('currentDirection')) { - return 'Safari12'; - } - // Safari with Plab-B support. - else if (browser.satisfies({ safari: '>=11' })) { - return 'Safari11'; - } - // Old Edge with ORTC support. - else if (browser.satisfies({ 'microsoft edge': '>=11' }) && - browser.satisfies({ 'microsoft edge': '<=18' })) { - return 'Edge11'; - } - // Best effort for Chromium based browsers. - else if (engine.name && engine.name.toLowerCase() === 'blink') { - const match = ua.match(/(?:(?:Chrome|Chromium))[ /](\w+)/i); - if (match) { - const version = Number(match[1]); - if (version >= 74) { - return 'Chrome74'; + handleConsumer(consumer) { + consumer.on('@close', () => { + this._consumers.delete(consumer.id); + this._pendingPauseConsumers.delete(consumer.id); + this._pendingResumeConsumers.delete(consumer.id); + if (this._closed) { + return; + } + // Store the Consumer into the close list. + this._pendingCloseConsumers.set(consumer.id, consumer); + // There is no Consumer close in progress, do it now. + if (this._consumerCloseInProgress === false) { + this.closePendingConsumers(); + } + }); + consumer.on('@pause', () => { + // If Consumer is pending to be resumed, remove from pending resume list. + if (this._pendingResumeConsumers.has(consumer.id)) { + this._pendingResumeConsumers.delete(consumer.id); + } + // Store the Consumer into the pending list. + this._pendingPauseConsumers.set(consumer.id, consumer); + // There is no Consumer pause in progress, do it now. + (0, queue_microtask_1.default)(() => { + if (this._closed) { + return; } - else if (version >= 70) { - return 'Chrome70'; + if (this._consumerPauseInProgress === false) { + this.pausePendingConsumers(); } - else if (version >= 67) { - return 'Chrome67'; + }); + }); + consumer.on('@resume', () => { + // If Consumer is pending to be paused, remove from pending pause list. + if (this._pendingPauseConsumers.has(consumer.id)) { + this._pendingPauseConsumers.delete(consumer.id); + } + // Store the Consumer into the pending list. + this._pendingResumeConsumers.set(consumer.id, consumer); + // There is no Consumer resume in progress, do it now. + (0, queue_microtask_1.default)(() => { + if (this._closed) { + return; } - else { - return 'Chrome55'; + if (this._consumerResumeInProgress === false) { + this.resumePendingConsumers(); } - } - else { - return 'Chrome74'; - } - } - // Unsupported browser. - else { - logger.warn('this._detectDevice() | browser not supported [name:%s, version:%s]', browser.getBrowserName(), browser.getBrowserVersion()); - return undefined; - } + }); + }); + consumer.on('@getstats', (callback, errback) => { + if (this._closed) { + return errback(new errors_1.InvalidStateError('closed')); + } + this._handler.getReceiverStats(consumer.localId) + .then(callback) + .catch(errback); + }); } - // Unknown device. - else { - logger.warn('this._detectDevice() | unknown device'); - return undefined; + handleDataProducer(dataProducer) { + dataProducer.on('@close', () => { + this._dataProducers.delete(dataProducer.id); + }); + } + handleDataConsumer(dataConsumer) { + dataConsumer.on('@close', () => { + this._dataConsumers.delete(dataConsumer.id); + }); } } -exports.detectDevice = detectDevice; -class Device { - /** - * Create a new Device to connect to mediasoup server. - * - * @throws {UnsupportedError} if device is not supported. - */ - constructor({ handlerName, handlerFactory, Handler } = {}) { - // Loaded flag. - this._loaded = false; - // Observer instance. - this._observer = new EnhancedEventEmitter_1.EnhancedEventEmitter(); - logger.debug('constructor()'); - // Handle deprecated option. - if (Handler) { - logger.warn('constructor() | Handler option is DEPRECATED, use handlerName or handlerFactory instead'); - if (typeof Handler === 'string') - handlerName = Handler; - else - throw new TypeError('non string Handler option no longer supported, use handlerFactory instead'); +exports.Transport = Transport; + +},{"./Consumer":12,"./DataConsumer":13,"./DataProducer":14,"./EnhancedEventEmitter":16,"./Logger":17,"./Producer":18,"./errors":22,"./ortc":43,"./utils":46,"awaitqueue":3,"queue-microtask":50}],22:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.InvalidStateError = exports.UnsupportedError = void 0; +/** + * Error indicating not support for something. + */ +class UnsupportedError extends Error { + constructor(message) { + super(message); + this.name = 'UnsupportedError'; + if (Error.hasOwnProperty('captureStackTrace')) // Just in V8. + { + // @ts-ignore + Error.captureStackTrace(this, UnsupportedError); } - if (handlerName && handlerFactory) { - throw new TypeError('just one of handlerName or handlerInterface can be given'); + else { + this.stack = (new Error(message)).stack; } - if (handlerFactory) { - this._handlerFactory = handlerFactory; + } +} +exports.UnsupportedError = UnsupportedError; +/** + * Error produced when calling a method in an invalid state. + */ +class InvalidStateError extends Error { + constructor(message) { + super(message); + this.name = 'InvalidStateError'; + if (Error.hasOwnProperty('captureStackTrace')) // Just in V8. + { + // @ts-ignore + Error.captureStackTrace(this, InvalidStateError); } else { - if (handlerName) { - logger.debug('constructor() | handler given: %s', handlerName); - } - else { - handlerName = detectDevice(); - if (handlerName) - logger.debug('constructor() | detected handler: %s', handlerName); - else - throw new errors_1.UnsupportedError('device not supported'); - } - switch (handlerName) { - case 'Chrome74': - this._handlerFactory = Chrome74_1.Chrome74.createFactory(); - break; - case 'Chrome70': - this._handlerFactory = Chrome70_1.Chrome70.createFactory(); - break; - case 'Chrome67': - this._handlerFactory = Chrome67_1.Chrome67.createFactory(); - break; - case 'Chrome55': - this._handlerFactory = Chrome55_1.Chrome55.createFactory(); - break; - case 'Firefox60': - this._handlerFactory = Firefox60_1.Firefox60.createFactory(); - break; - case 'Safari12': - this._handlerFactory = Safari12_1.Safari12.createFactory(); - break; - case 'Safari11': - this._handlerFactory = Safari11_1.Safari11.createFactory(); - break; - case 'Edge11': - this._handlerFactory = Edge11_1.Edge11.createFactory(); - break; - case 'ReactNative': - this._handlerFactory = ReactNative_1.ReactNative.createFactory(); - break; - default: - throw new TypeError(`unknown handlerName "${handlerName}"`); - } + this.stack = (new Error(message)).stack; } - // Create a temporal handler to get its name. - const handler = this._handlerFactory(); - this._handlerName = handler.name; - handler.close(); - this._extendedRtpCapabilities = undefined; - this._recvRtpCapabilities = undefined; - this._canProduceByKind = - { - audio: false, - video: false - }; - this._sctpCapabilities = undefined; } - /** - * The RTC handler name. - */ - get handlerName() { - return this._handlerName; +} +exports.InvalidStateError = InvalidStateError; + +},{}],23:[function(require,module,exports){ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Chrome111 = void 0; +const sdpTransform = __importStar(require("sdp-transform")); +const Logger_1 = require("../Logger"); +const utils = __importStar(require("../utils")); +const ortc = __importStar(require("../ortc")); +const sdpCommonUtils = __importStar(require("./sdp/commonUtils")); +const sdpUnifiedPlanUtils = __importStar(require("./sdp/unifiedPlanUtils")); +const ortcUtils = __importStar(require("./ortc/utils")); +const HandlerInterface_1 = require("./HandlerInterface"); +const RemoteSdp_1 = require("./sdp/RemoteSdp"); +const scalabilityModes_1 = require("../scalabilityModes"); +const logger = new Logger_1.Logger('Chrome111'); +const SCTP_NUM_STREAMS = { OS: 1024, MIS: 1024 }; +class Chrome111 extends HandlerInterface_1.HandlerInterface { /** - * Whether the Device is loaded. + * Creates a factory function. */ - get loaded() { - return this._loaded; + static createFactory() { + return () => new Chrome111(); } - /** - * RTP capabilities of the Device for receiving media. - * - * @throws {InvalidStateError} if not loaded. - */ - get rtpCapabilities() { - if (!this._loaded) - throw new errors_1.InvalidStateError('not loaded'); - return this._recvRtpCapabilities; + constructor() { + super(); + // Map of RTCTransceivers indexed by MID. + this._mapMidTransceiver = new Map(); + // Local stream for sending. + this._sendStream = new MediaStream(); + // Whether a DataChannel m=application section has been created. + this._hasDataChannelMediaSection = false; + // Sending DataChannel id value counter. Incremented for each new DataChannel. + this._nextSendSctpStreamId = 0; + // Got transport local and remote parameters. + this._transportReady = false; } - /** - * SCTP capabilities of the Device. - * - * @throws {InvalidStateError} if not loaded. - */ - get sctpCapabilities() { - if (!this._loaded) - throw new errors_1.InvalidStateError('not loaded'); - return this._sctpCapabilities; + get name() { + return 'Chrome111'; } - /** - * Observer. - * - * @emits newtransport - (transport: Transport) - */ - get observer() { - return this._observer; + close() { + logger.debug('close()'); + // Close RTCPeerConnection. + if (this._pc) { + try { + this._pc.close(); + } + catch (error) { } + } + this.emit('@close'); } - /** - * Initialize the Device. - */ - async load({ routerRtpCapabilities }) { - logger.debug('load() [routerRtpCapabilities:%o]', routerRtpCapabilities); - routerRtpCapabilities = utils.clone(routerRtpCapabilities, undefined); - // Temporal handler to get its capabilities. - let handler; + async getNativeRtpCapabilities() { + logger.debug('getNativeRtpCapabilities()'); + const pc = new RTCPeerConnection({ + iceServers: [], + iceTransportPolicy: 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'unified-plan' + }); try { - if (this._loaded) - throw new errors_1.InvalidStateError('already loaded'); - // This may throw. - ortc.validateRtpCapabilities(routerRtpCapabilities); - handler = this._handlerFactory(); - const nativeRtpCapabilities = await handler.getNativeRtpCapabilities(); - logger.debug('load() | got native RTP capabilities:%o', nativeRtpCapabilities); - // This may throw. - ortc.validateRtpCapabilities(nativeRtpCapabilities); - // Get extended RTP capabilities. - this._extendedRtpCapabilities = ortc.getExtendedRtpCapabilities(nativeRtpCapabilities, routerRtpCapabilities); - logger.debug('load() | got extended RTP capabilities:%o', this._extendedRtpCapabilities); - // Check whether we can produce audio/video. - this._canProduceByKind.audio = - ortc.canSend('audio', this._extendedRtpCapabilities); - this._canProduceByKind.video = - ortc.canSend('video', this._extendedRtpCapabilities); - // Generate our receiving RTP capabilities for receiving media. - this._recvRtpCapabilities = - ortc.getRecvRtpCapabilities(this._extendedRtpCapabilities); - // This may throw. - ortc.validateRtpCapabilities(this._recvRtpCapabilities); - logger.debug('load() | got receiving RTP capabilities:%o', this._recvRtpCapabilities); - // Generate our SCTP capabilities. - this._sctpCapabilities = await handler.getNativeSctpCapabilities(); - logger.debug('load() | got native SCTP capabilities:%o', this._sctpCapabilities); - // This may throw. - ortc.validateSctpCapabilities(this._sctpCapabilities); - logger.debug('load() succeeded'); - this._loaded = true; - handler.close(); + pc.addTransceiver('audio'); + pc.addTransceiver('video'); + const offer = await pc.createOffer(); + try { + pc.close(); + } + catch (error) { } + const sdpObject = sdpTransform.parse(offer.sdp); + const nativeRtpCapabilities = sdpCommonUtils.extractRtpCapabilities({ sdpObject }); + // libwebrtc supports NACK for OPUS but doesn't announce it. + ortcUtils.addNackSuppportForOpus(nativeRtpCapabilities); + return nativeRtpCapabilities; } catch (error) { - if (handler) - handler.close(); + try { + pc.close(); + } + catch (error2) { } throw error; } } - /** - * Whether we can produce audio/video. - * - * @throws {InvalidStateError} if not loaded. - * @throws {TypeError} if wrong arguments. - */ - canProduce(kind) { - if (!this._loaded) - throw new errors_1.InvalidStateError('not loaded'); - else if (kind !== 'audio' && kind !== 'video') - throw new TypeError(`invalid kind "${kind}"`); - return this._canProduceByKind[kind]; - } - /** - * Creates a Transport for sending media. - * - * @throws {InvalidStateError} if not loaded. - * @throws {TypeError} if wrong arguments. - */ - createSendTransport({ id, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, appData = {} }) { - logger.debug('createSendTransport()'); - return this._createTransport({ - direction: 'send', - id: id, - iceParameters: iceParameters, - iceCandidates: iceCandidates, - dtlsParameters: dtlsParameters, - sctpParameters: sctpParameters, - iceServers: iceServers, - iceTransportPolicy: iceTransportPolicy, - additionalSettings: additionalSettings, - proprietaryConstraints: proprietaryConstraints, - appData: appData - }); - } - /** - * Creates a Transport for receiving media. - * - * @throws {InvalidStateError} if not loaded. - * @throws {TypeError} if wrong arguments. - */ - createRecvTransport({ id, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, appData = {} }) { - logger.debug('createRecvTransport()'); - return this._createTransport({ - direction: 'recv', - id: id, - iceParameters: iceParameters, - iceCandidates: iceCandidates, - dtlsParameters: dtlsParameters, - sctpParameters: sctpParameters, - iceServers: iceServers, - iceTransportPolicy: iceTransportPolicy, - additionalSettings: additionalSettings, - proprietaryConstraints: proprietaryConstraints, - appData: appData - }); + async getNativeSctpCapabilities() { + logger.debug('getNativeSctpCapabilities()'); + return { + numStreams: SCTP_NUM_STREAMS + }; } - _createTransport({ direction, id, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, appData = {} }) { - if (!this._loaded) - throw new errors_1.InvalidStateError('not loaded'); - else if (typeof id !== 'string') - throw new TypeError('missing id'); - else if (typeof iceParameters !== 'object') - throw new TypeError('missing iceParameters'); - else if (!Array.isArray(iceCandidates)) - throw new TypeError('missing iceCandidates'); - else if (typeof dtlsParameters !== 'object') - throw new TypeError('missing dtlsParameters'); - else if (sctpParameters && typeof sctpParameters !== 'object') - throw new TypeError('wrong sctpParameters'); - else if (appData && typeof appData !== 'object') - throw new TypeError('if given, appData must be an object'); - // Create a new Transport. - const transport = new Transport_1.Transport({ - direction, - id, + run({ direction, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, extendedRtpCapabilities }) { + logger.debug('run()'); + this._direction = direction; + this._remoteSdp = new RemoteSdp_1.RemoteSdp({ iceParameters, iceCandidates, dtlsParameters, - sctpParameters, - iceServers, - iceTransportPolicy, - additionalSettings, - proprietaryConstraints, - appData, - handlerFactory: this._handlerFactory, - extendedRtpCapabilities: this._extendedRtpCapabilities, - canProduceByKind: this._canProduceByKind + sctpParameters }); - // Emit observer event. - this._observer.safeEmit('newtransport', transport); - return transport; + this._sendingRtpParametersByKind = + { + audio: ortc.getSendingRtpParameters('audio', extendedRtpCapabilities), + video: ortc.getSendingRtpParameters('video', extendedRtpCapabilities) + }; + this._sendingRemoteRtpParametersByKind = + { + audio: ortc.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), + video: ortc.getSendingRemoteRtpParameters('video', extendedRtpCapabilities) + }; + if (dtlsParameters.role && dtlsParameters.role !== 'auto') { + this._forcedLocalDtlsRole = dtlsParameters.role === 'server' + ? 'client' + : 'server'; + } + this._pc = new RTCPeerConnection({ + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'unified-plan', + ...additionalSettings + }, proprietaryConstraints); + if (this._pc.connectionState) { + this._pc.addEventListener('connectionstatechange', () => { + this.emit('@connectionstatechange', this._pc.connectionState); + }); + } + else { + logger.warn('run() | pc.connectionState not supported, using pc.iceConnectionState'); + this._pc.addEventListener('iceconnectionstatechange', () => { + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } } -} -exports.Device = Device; - -},{"./EnhancedEventEmitter":12,"./Logger":13,"./Transport":17,"./errors":18,"./handlers/Chrome55":19,"./handlers/Chrome67":20,"./handlers/Chrome70":21,"./handlers/Chrome74":22,"./handlers/Edge11":23,"./handlers/Firefox60":24,"./handlers/ReactNative":26,"./handlers/Safari11":27,"./handlers/Safari12":28,"./ortc":36,"./utils":39,"bowser":3}],12:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.EnhancedEventEmitter = void 0; -const events_1 = require("events"); -const Logger_1 = require("./Logger"); -const logger = new Logger_1.Logger('EnhancedEventEmitter'); -class EnhancedEventEmitter extends events_1.EventEmitter { - constructor() { - super(); - this.setMaxListeners(Infinity); + async updateIceServers(iceServers) { + logger.debug('updateIceServers()'); + const configuration = this._pc.getConfiguration(); + configuration.iceServers = iceServers; + this._pc.setConfiguration(configuration); + } + async restartIce(iceParameters) { + logger.debug('restartIce()'); + // Provide the remote SDP handler with new remote ICE parameters. + this._remoteSdp.updateIceParameters(iceParameters); + if (!this._transportReady) { + return; + } + if (this._direction === 'send') { + const offer = await this._pc.createOffer({ iceRestart: true }); + logger.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('restartIce() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + else { + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('restartIce() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('restartIce() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + } + async getTransportStats() { + return this._pc.getStats(); + } + async send({ track, encodings, codecOptions, codec }) { + var _a; + this.assertSendDirection(); + logger.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); + if (encodings && encodings.length > 1) { + encodings.forEach((encoding, idx) => { + encoding.rid = `r${idx}`; + }); + // Set rid and verify scalabilityMode in each encoding. + // NOTE: Even if WebRTC allows different scalabilityMode (different number + // of temporal layers) per simulcast stream, we need that those are the + // same in all them, so let's pick up the highest value. + // NOTE: If scalabilityMode is not given, Chrome will use L1T3. + let nextRid = 1; + let maxTemporalLayers = 1; + for (const encoding of encodings) { + const temporalLayers = encoding.scalabilityMode + ? (0, scalabilityModes_1.parse)(encoding.scalabilityMode).temporalLayers + : 3; + if (temporalLayers > maxTemporalLayers) { + maxTemporalLayers = temporalLayers; + } + } + for (const encoding of encodings) { + encoding.rid = `r${nextRid++}`; + encoding.scalabilityMode = `L1T${maxTemporalLayers}`; + } + } + const sendingRtpParameters = utils.clone(this._sendingRtpParametersByKind[track.kind], {}); + // This may throw. + sendingRtpParameters.codecs = + ortc.reduceCodecs(sendingRtpParameters.codecs, codec); + const sendingRemoteRtpParameters = utils.clone(this._sendingRemoteRtpParametersByKind[track.kind], {}); + // This may throw. + sendingRemoteRtpParameters.codecs = + ortc.reduceCodecs(sendingRemoteRtpParameters.codecs, codec); + const mediaSectionIdx = this._remoteSdp.getNextMediaSectionIdx(); + const transceiver = this._pc.addTransceiver(track, { + direction: 'sendonly', + streams: [this._sendStream], + sendEncodings: encodings + }); + const offer = await this._pc.createOffer(); + let localSdpObject = sdpTransform.parse(offer.sdp); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); + } + logger.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + // We can now get the transceiver.mid. + const localId = transceiver.mid; + // Set MID. + sendingRtpParameters.mid = localId; + localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + const offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; + // Set RTCP CNAME. + sendingRtpParameters.rtcp.cname = + sdpCommonUtils.getCname({ offerMediaObject }); + // Set RTP encodings by parsing the SDP offer if no encodings are given. + if (!encodings) { + sendingRtpParameters.encodings = + sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject }); + } + // Set RTP encodings by parsing the SDP offer and complete them with given + // one if just a single encoding has been given. + else if (encodings.length === 1) { + const newEncodings = sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject }); + Object.assign(newEncodings[0], encodings[0]); + sendingRtpParameters.encodings = newEncodings; + } + // Otherwise if more than 1 encoding are given use them verbatim. + else { + sendingRtpParameters.encodings = encodings; + } + this._remoteSdp.send({ + offerMediaObject, + reuseMid: mediaSectionIdx.reuseMid, + offerRtpParameters: sendingRtpParameters, + answerRtpParameters: sendingRemoteRtpParameters, + codecOptions, + extmapAllowMixed: true + }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + return { + localId, + rtpParameters: sendingRtpParameters, + rtpSender: transceiver.sender + }; + } + async stopSending(localId) { + this.assertSendDirection(); + logger.debug('stopSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.sender.replaceTrack(null); + this._pc.removeTrack(transceiver.sender); + const mediaSectionClosed = this._remoteSdp.closeMediaSection(transceiver.mid); + if (mediaSectionClosed) { + try { + transceiver.stop(); + } + catch (error) { } + } + const offer = await this._pc.createOffer(); + logger.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._mapMidTransceiver.delete(localId); + } + async pauseSending(localId) { + this.assertSendDirection(); + logger.debug('pauseSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'inactive'; + this._remoteSdp.pauseMediaSection(localId); + const offer = await this._pc.createOffer(); + logger.debug('pauseSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('pauseSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); } - safeEmit(event, ...args) { - const numListeners = this.listenerCount(event); - try { - return this.emit(event, ...args); + async resumeSending(localId) { + this.assertSendDirection(); + logger.debug('resumeSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + this._remoteSdp.resumeSendingMediaSection(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); } - catch (error) { - logger.error('safeEmit() | event listener threw an error [event:%s]:%o', event, error); - return Boolean(numListeners); + transceiver.direction = 'sendonly'; + const offer = await this._pc.createOffer(); + logger.debug('resumeSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('resumeSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async replaceTrack(localId, track) { + this.assertSendDirection(); + if (track) { + logger.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id); + } + else { + logger.debug('replaceTrack() [localId:%s, no track]', localId); + } + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); } + await transceiver.sender.replaceTrack(track); } - async safeEmitAsPromise(event, ...args) { - return new Promise((resolve, reject) => { - try { - this.emit(event, ...args, resolve, reject); + async setMaxSpatialLayer(localId, spatialLayer) { + this.assertSendDirection(); + logger.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + const parameters = transceiver.sender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + if (idx <= spatialLayer) { + encoding.active = true; } - catch (error) { - logger.error('safeEmitAsPromise() | event listener threw an error [event:%s]:%o', event, error); - reject(error); + else { + encoding.active = false; } }); + await transceiver.sender.setParameters(parameters); + this._remoteSdp.muxMediaSectionSimulcast(localId, parameters.encodings); + const offer = await this._pc.createOffer(); + logger.debug('setMaxSpatialLayer() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('setMaxSpatialLayer() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); } -} -exports.EnhancedEventEmitter = EnhancedEventEmitter; - -},{"./Logger":13,"events":47}],13:[function(require,module,exports){ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Logger = void 0; -const debug_1 = __importDefault(require("debug")); -const APP_NAME = 'mediasoup-client'; -class Logger { - constructor(prefix) { - if (prefix) { - this._debug = debug_1.default(`${APP_NAME}:${prefix}`); - this._warn = debug_1.default(`${APP_NAME}:WARN:${prefix}`); - this._error = debug_1.default(`${APP_NAME}:ERROR:${prefix}`); - } - else { - this._debug = debug_1.default(APP_NAME); - this._warn = debug_1.default(`${APP_NAME}:WARN`); - this._error = debug_1.default(`${APP_NAME}:ERROR`); + async setRtpEncodingParameters(localId, params) { + this.assertSendDirection(); + logger.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); } - /* eslint-disable no-console */ - this._debug.log = console.info.bind(console); - this._warn.log = console.warn.bind(console); - this._error.log = console.error.bind(console); - /* eslint-enable no-console */ - } - get debug() { - return this._debug; - } - get warn() { - return this._warn; - } - get error() { - return this._error; - } -} -exports.Logger = Logger; - -},{"debug":40}],14:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Producer = void 0; -const Logger_1 = require("./Logger"); -const EnhancedEventEmitter_1 = require("./EnhancedEventEmitter"); -const errors_1 = require("./errors"); -const logger = new Logger_1.Logger('Producer'); -class Producer extends EnhancedEventEmitter_1.EnhancedEventEmitter { - /** - * @emits transportclose - * @emits trackended - * @emits @replacetrack - (track: MediaStreamTrack | null) - * @emits @setmaxspatiallayer - (spatialLayer: string) - * @emits @setrtpencodingparameters - (params: any) - * @emits @getstats - * @emits @close - */ - constructor({ id, localId, rtpSender, track, rtpParameters, stopTracks, disableTrackOnPause, zeroRtpOnPause, appData }) { - super(); - // Closed flag. - this._closed = false; - // Observer instance. - this._observer = new EnhancedEventEmitter_1.EnhancedEventEmitter(); - logger.debug('constructor()'); - this._id = id; - this._localId = localId; - this._rtpSender = rtpSender; - this._track = track; - this._kind = track.kind; - this._rtpParameters = rtpParameters; - this._paused = disableTrackOnPause ? !track.enabled : false; - this._maxSpatialLayer = undefined; - this._stopTracks = stopTracks; - this._disableTrackOnPause = disableTrackOnPause; - this._zeroRtpOnPause = zeroRtpOnPause; - this._appData = appData; - this._onTrackEnded = this._onTrackEnded.bind(this); - // NOTE: Minor issue. If zeroRtpOnPause is true, we cannot emit the - // '@replacetrack' event here, so RTCRtpSender.track won't be null. - this._handleTrack(); - } - /** - * Producer id. - */ - get id() { - return this._id; - } - /** - * Local id. - */ - get localId() { - return this._localId; - } - /** - * Whether the Producer is closed. - */ - get closed() { - return this._closed; - } - /** - * Media kind. - */ - get kind() { - return this._kind; - } - /** - * Associated RTCRtpSender. - */ - get rtpSender() { - return this._rtpSender; - } - /** - * The associated track. - */ - get track() { - return this._track; - } - /** - * RTP parameters. - */ - get rtpParameters() { - return this._rtpParameters; - } - /** - * Whether the Producer is paused. - */ - get paused() { - return this._paused; - } - /** - * Max spatial layer. - * - * @type {Number | undefined} - */ - get maxSpatialLayer() { - return this._maxSpatialLayer; - } - /** - * App custom data. - */ - get appData() { - return this._appData; - } - /** - * Invalid setter. - */ - set appData(appData) { - throw new Error('cannot override appData object'); - } - /** - * Observer. - * - * @emits close - * @emits pause - * @emits resume - * @emits trackended - */ - get observer() { - return this._observer; - } - /** - * Closes the Producer. - */ - close() { - if (this._closed) - return; - logger.debug('close()'); - this._closed = true; - this._destroyTrack(); - this.emit('@close'); - // Emit observer event. - this._observer.safeEmit('close'); + const parameters = transceiver.sender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + parameters.encodings[idx] = { ...encoding, ...params }; + }); + await transceiver.sender.setParameters(parameters); + this._remoteSdp.muxMediaSectionSimulcast(localId, parameters.encodings); + const offer = await this._pc.createOffer(); + logger.debug('setRtpEncodingParameters() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('setRtpEncodingParameters() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); } - /** - * Transport was closed. - */ - transportClosed() { - if (this._closed) - return; - logger.debug('transportClosed()'); - this._closed = true; - this._destroyTrack(); - this.safeEmit('transportclose'); - // Emit observer event. - this._observer.safeEmit('close'); + async getSenderStats(localId) { + this.assertSendDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + return transceiver.sender.getStats(); } - /** - * Get associated RTCRtpSender stats. - */ - async getStats() { - if (this._closed) - throw new errors_1.InvalidStateError('closed'); - return this.safeEmitAsPromise('@getstats'); + async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { + var _a; + this.assertSendDirection(); + const options = { + negotiated: true, + id: this._nextSendSctpStreamId, + ordered, + maxPacketLifeTime, + maxRetransmits, + protocol + }; + logger.debug('sendDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // Increase next id. + this._nextSendSctpStreamId = + ++this._nextSendSctpStreamId % SCTP_NUM_STREAMS.MIS; + // If this is the first DataChannel we need to create the SDP answer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + const offer = await this._pc.createOffer(); + const localSdpObject = sdpTransform.parse(offer.sdp); + const offerMediaObject = localSdpObject.media + .find((m) => m.type === 'application'); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); + } + logger.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + this._remoteSdp.sendSctpAssociation({ offerMediaObject }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._hasDataChannelMediaSection = true; + } + const sctpStreamParameters = { + streamId: options.id, + ordered: options.ordered, + maxPacketLifeTime: options.maxPacketLifeTime, + maxRetransmits: options.maxRetransmits + }; + return { dataChannel, sctpStreamParameters }; } - /** - * Pauses sending media. - */ - pause() { - logger.debug('pause()'); - if (this._closed) { - logger.error('pause() | Producer closed'); - return; + async receive(optionsList) { + var _a; + this.assertRecvDirection(); + const results = []; + const mapLocalId = new Map(); + for (const options of optionsList) { + const { trackId, kind, rtpParameters, streamId } = options; + logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const localId = rtpParameters.mid || String(this._mapMidTransceiver.size); + mapLocalId.set(trackId, localId); + this._remoteSdp.receive({ + mid: localId, + kind, + offerRtpParameters: rtpParameters, + streamId: streamId || rtpParameters.rtcp.cname, + trackId + }); } - this._paused = true; - if (this._track && this._disableTrackOnPause) { - this._track.enabled = false; + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + let answer = await this._pc.createAnswer(); + const localSdpObject = sdpTransform.parse(answer.sdp); + for (const options of optionsList) { + const { trackId, rtpParameters } = options; + const localId = mapLocalId.get(trackId); + const answerMediaObject = localSdpObject.media + .find((m) => String(m.mid) === localId); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject + }); } - if (this._zeroRtpOnPause) { - this.safeEmitAsPromise('@replacetrack', null) - .catch(() => { }); + answer = { type: 'answer', sdp: sdpTransform.write(localSdpObject) }; + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); } - // Emit observer event. - this._observer.safeEmit('pause'); + logger.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + for (const options of optionsList) { + const { trackId } = options; + const localId = mapLocalId.get(trackId); + const transceiver = this._pc.getTransceivers() + .find((t) => t.mid === localId); + if (!transceiver) { + throw new Error('new RTCRtpTransceiver not found'); + } + else { + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + results.push({ + localId, + track: transceiver.receiver.track, + rtpReceiver: transceiver.receiver + }); + } + } + return results; } - /** - * Resumes sending media. - */ - resume() { - logger.debug('resume()'); - if (this._closed) { - logger.error('resume() | Producer closed'); - return; + async stopReceiving(localIds) { + this.assertRecvDirection(); + for (const localId of localIds) { + logger.debug('stopReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + this._remoteSdp.closeMediaSection(transceiver.mid); } - this._paused = false; - if (this._track && this._disableTrackOnPause) { - this._track.enabled = true; + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + for (const localId of localIds) { + this._mapMidTransceiver.delete(localId); } - if (this._zeroRtpOnPause) { - this.safeEmitAsPromise('@replacetrack', this._track) - .catch(() => { }); + } + async pauseReceiving(localIds) { + this.assertRecvDirection(); + for (const localId of localIds) { + logger.debug('pauseReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'inactive'; + this._remoteSdp.pauseMediaSection(localId); } - // Emit observer event. - this._observer.safeEmit('resume'); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('pauseReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('pauseReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); } - /** - * Replaces the current track with a new one or null. - */ - async replaceTrack({ track }) { - logger.debug('replaceTrack() [track:%o]', track); - if (this._closed) { - // This must be done here. Otherwise there is no chance to stop the given - // track. - if (track && this._stopTracks) { - try { - track.stop(); - } - catch (error) { } + async resumeReceiving(localIds) { + this.assertRecvDirection(); + for (const localId of localIds) { + logger.debug('resumeReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); } - throw new errors_1.InvalidStateError('closed'); + transceiver.direction = 'recvonly'; + this._remoteSdp.resumeReceivingMediaSection(localId); } - else if (track && track.readyState === 'ended') { - throw new errors_1.InvalidStateError('track ended'); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('resumeReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('resumeReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + async getReceiverStats(localId) { + this.assertRecvDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); } - // Do nothing if this is the same track as the current handled one. - if (track === this._track) { - logger.debug('replaceTrack() | same track, ignored'); - return; + return transceiver.receiver.getStats(); + } + async receiveDataChannel({ sctpStreamParameters, label, protocol }) { + var _a; + this.assertRecvDirection(); + const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; + const options = { + negotiated: true, + id: streamId, + ordered, + maxPacketLifeTime, + maxRetransmits, + protocol + }; + logger.debug('receiveDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // If this is the first DataChannel we need to create the SDP offer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + this._remoteSdp.receiveSctpAssociation(); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + if (!this._transportReady) { + const localSdpObject = sdpTransform.parse(answer.sdp); + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); + } + logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + this._hasDataChannelMediaSection = true; } - if (!this._zeroRtpOnPause || !this._paused) { - await this.safeEmitAsPromise('@replacetrack', track); + return { dataChannel }; + } + async setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) { + localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); } - // Destroy the previous track. - this._destroyTrack(); - // Set the new track. - this._track = track; - // If this Producer was paused/resumed and the state of the new - // track does not match, fix it. - if (this._track && this._disableTrackOnPause) { - if (!this._paused) - this._track.enabled = true; - else if (this._paused) - this._track.enabled = false; + // Get our local DTLS parameters. + const dtlsParameters = sdpCommonUtils.extractDtlsParameters({ sdpObject: localSdpObject }); + // Set our DTLS role. + dtlsParameters.role = localDtlsRole; + // Update the remote DTLS role in the SDP. + this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); + // Need to tell the remote transport about our parameters. + await new Promise((resolve, reject) => { + this.safeEmit('@connect', { dtlsParameters }, resolve, reject); + }); + this._transportReady = true; + } + assertSendDirection() { + if (this._direction !== 'send') { + throw new Error('method can just be called for handlers with "send" direction'); } - // Handle the effective track. - this._handleTrack(); } - /** - * Sets the video max spatial layer to be sent. - */ - async setMaxSpatialLayer(spatialLayer) { - if (this._closed) - throw new errors_1.InvalidStateError('closed'); - else if (this._kind !== 'video') - throw new errors_1.UnsupportedError('not a video Producer'); - else if (typeof spatialLayer !== 'number') - throw new TypeError('invalid spatialLayer'); - if (spatialLayer === this._maxSpatialLayer) - return; - await this.safeEmitAsPromise('@setmaxspatiallayer', spatialLayer); - this._maxSpatialLayer = spatialLayer; + assertRecvDirection() { + if (this._direction !== 'recv') { + throw new Error('method can just be called for handlers with "recv" direction'); + } } +} +exports.Chrome111 = Chrome111; + +},{"../Logger":17,"../ortc":43,"../scalabilityModes":44,"../utils":46,"./HandlerInterface":30,"./ortc/utils":36,"./sdp/RemoteSdp":38,"./sdp/commonUtils":39,"./sdp/unifiedPlanUtils":41,"sdp-transform":52}],24:[function(require,module,exports){ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Chrome55 = void 0; +const sdpTransform = __importStar(require("sdp-transform")); +const Logger_1 = require("../Logger"); +const errors_1 = require("../errors"); +const utils = __importStar(require("../utils")); +const ortc = __importStar(require("../ortc")); +const sdpCommonUtils = __importStar(require("./sdp/commonUtils")); +const sdpPlanBUtils = __importStar(require("./sdp/planBUtils")); +const HandlerInterface_1 = require("./HandlerInterface"); +const RemoteSdp_1 = require("./sdp/RemoteSdp"); +const logger = new Logger_1.Logger('Chrome55'); +const SCTP_NUM_STREAMS = { OS: 1024, MIS: 1024 }; +class Chrome55 extends HandlerInterface_1.HandlerInterface { /** - * Sets the DSCP value. + * Creates a factory function. */ - async setRtpEncodingParameters(params) { - if (this._closed) - throw new errors_1.InvalidStateError('closed'); - else if (typeof params !== 'object') - throw new TypeError('invalid params'); - await this.safeEmitAsPromise('@setrtpencodingparameters', params); + static createFactory() { + return () => new Chrome55(); } - _onTrackEnded() { - logger.debug('track "ended" event'); - this.safeEmit('trackended'); - // Emit observer event. - this._observer.safeEmit('trackended'); + constructor() { + super(); + // Local stream for sending. + this._sendStream = new MediaStream(); + // Map of sending MediaStreamTracks indexed by localId. + this._mapSendLocalIdTrack = new Map(); + // Next sending localId. + this._nextSendLocalId = 0; + // Map of MID, RTP parameters and RTCRtpReceiver indexed by local id. + // Value is an Object with mid, rtpParameters and rtpReceiver. + this._mapRecvLocalIdInfo = new Map(); + // Whether a DataChannel m=application section has been created. + this._hasDataChannelMediaSection = false; + // Sending DataChannel id value counter. Incremented for each new DataChannel. + this._nextSendSctpStreamId = 0; + // Got transport local and remote parameters. + this._transportReady = false; } - _handleTrack() { - if (!this._track) - return; - this._track.addEventListener('ended', this._onTrackEnded); + get name() { + return 'Chrome55'; } - _destroyTrack() { - if (!this._track) - return; + close() { + logger.debug('close()'); + // Close RTCPeerConnection. + if (this._pc) { + try { + this._pc.close(); + } + catch (error) { } + } + this.emit('@close'); + } + async getNativeRtpCapabilities() { + logger.debug('getNativeRtpCapabilities()'); + const pc = new RTCPeerConnection({ + iceServers: [], + iceTransportPolicy: 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'plan-b' + }); try { - this._track.removeEventListener('ended', this._onTrackEnded); - // Just stop the track unless the app set stopTracks: false. - if (this._stopTracks) - this._track.stop(); + const offer = await pc.createOffer({ + offerToReceiveAudio: true, + offerToReceiveVideo: true + }); + try { + pc.close(); + } + catch (error) { } + const sdpObject = sdpTransform.parse(offer.sdp); + const nativeRtpCapabilities = sdpCommonUtils.extractRtpCapabilities({ sdpObject }); + return nativeRtpCapabilities; + } + catch (error) { + try { + pc.close(); + } + catch (error2) { } + throw error; } - catch (error) { } } -} -exports.Producer = Producer; - -},{"./EnhancedEventEmitter":12,"./Logger":13,"./errors":18}],15:[function(require,module,exports){ -"use strict"; -/** - * The RTP capabilities define what mediasoup or an endpoint can receive at - * media level. - */ -Object.defineProperty(exports, "__esModule", { value: true }); - -},{}],16:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); - -},{}],17:[function(require,module,exports){ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Transport = void 0; -const awaitqueue_1 = require("awaitqueue"); -const Logger_1 = require("./Logger"); -const EnhancedEventEmitter_1 = require("./EnhancedEventEmitter"); -const errors_1 = require("./errors"); -const utils = __importStar(require("./utils")); -const ortc = __importStar(require("./ortc")); -const Producer_1 = require("./Producer"); -const Consumer_1 = require("./Consumer"); -const DataProducer_1 = require("./DataProducer"); -const DataConsumer_1 = require("./DataConsumer"); -const logger = new Logger_1.Logger('Transport'); -class Transport extends EnhancedEventEmitter_1.EnhancedEventEmitter { - /** - * @emits connect - (transportLocalParameters: any, callback: Function, errback: Function) - * @emits connectionstatechange - (connectionState: ConnectionState) - * @emits produce - (producerLocalParameters: any, callback: Function, errback: Function) - * @emits producedata - (dataProducerLocalParameters: any, callback: Function, errback: Function) - */ - constructor({ direction, id, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, appData, handlerFactory, extendedRtpCapabilities, canProduceByKind }) { - super(); - // Closed flag. - this._closed = false; - // Transport connection state. - this._connectionState = 'new'; - // Map of Producers indexed by id. - this._producers = new Map(); - // Map of Consumers indexed by id. - this._consumers = new Map(); - // Map of DataProducers indexed by id. - this._dataProducers = new Map(); - // Map of DataConsumers indexed by id. - this._dataConsumers = new Map(); - // Whether the Consumer for RTP probation has been created. - this._probatorConsumerCreated = false; - // AwaitQueue instance to make async tasks happen sequentially. - this._awaitQueue = new awaitqueue_1.AwaitQueue({ ClosedErrorClass: errors_1.InvalidStateError }); - // Observer instance. - this._observer = new EnhancedEventEmitter_1.EnhancedEventEmitter(); - logger.debug('constructor() [id:%s, direction:%s]', id, direction); - this._id = id; + async getNativeSctpCapabilities() { + logger.debug('getNativeSctpCapabilities()'); + return { + numStreams: SCTP_NUM_STREAMS + }; + } + run({ direction, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, extendedRtpCapabilities }) { + logger.debug('run()'); this._direction = direction; - this._extendedRtpCapabilities = extendedRtpCapabilities; - this._canProduceByKind = canProduceByKind; - this._maxSctpMessageSize = - sctpParameters ? sctpParameters.maxMessageSize : null; - // Clone and sanitize additionalSettings. - additionalSettings = utils.clone(additionalSettings, {}); - delete additionalSettings.iceServers; - delete additionalSettings.iceTransportPolicy; - delete additionalSettings.bundlePolicy; - delete additionalSettings.rtcpMuxPolicy; - delete additionalSettings.sdpSemantics; - this._handler = handlerFactory(); - this._handler.run({ - direction, + this._remoteSdp = new RemoteSdp_1.RemoteSdp({ iceParameters, iceCandidates, dtlsParameters, sctpParameters, - iceServers, - iceTransportPolicy, - additionalSettings, - proprietaryConstraints, - extendedRtpCapabilities + planB: true }); - this._appData = appData; - this._handleHandler(); - } - /** - * Transport id. - */ - get id() { - return this._id; - } - /** - * Whether the Transport is closed. - */ - get closed() { - return this._closed; - } - /** - * Transport direction. - */ - get direction() { - return this._direction; - } - /** - * RTC handler instance. - */ - get handler() { - return this._handler; + this._sendingRtpParametersByKind = + { + audio: ortc.getSendingRtpParameters('audio', extendedRtpCapabilities), + video: ortc.getSendingRtpParameters('video', extendedRtpCapabilities) + }; + this._sendingRemoteRtpParametersByKind = + { + audio: ortc.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), + video: ortc.getSendingRemoteRtpParameters('video', extendedRtpCapabilities) + }; + if (dtlsParameters.role && dtlsParameters.role !== 'auto') { + this._forcedLocalDtlsRole = dtlsParameters.role === 'server' + ? 'client' + : 'server'; + } + this._pc = new RTCPeerConnection({ + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'plan-b', + ...additionalSettings + }, proprietaryConstraints); + if (this._pc.connectionState) { + this._pc.addEventListener('connectionstatechange', () => { + this.emit('@connectionstatechange', this._pc.connectionState); + }); + } + else { + this._pc.addEventListener('iceconnectionstatechange', () => { + logger.warn('run() | pc.connectionState not supported, using pc.iceConnectionState'); + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } } - /** - * Connection state. - */ - get connectionState() { - return this._connectionState; + async updateIceServers(iceServers) { + logger.debug('updateIceServers()'); + const configuration = this._pc.getConfiguration(); + configuration.iceServers = iceServers; + this._pc.setConfiguration(configuration); } - /** - * App custom data. - */ - get appData() { - return this._appData; + async restartIce(iceParameters) { + logger.debug('restartIce()'); + // Provide the remote SDP handler with new remote ICE parameters. + this._remoteSdp.updateIceParameters(iceParameters); + if (!this._transportReady) { + return; + } + if (this._direction === 'send') { + const offer = await this._pc.createOffer({ iceRestart: true }); + logger.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('restartIce() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + else { + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('restartIce() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('restartIce() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } } - /** - * Invalid setter. - */ - set appData(appData) { - throw new Error('cannot override appData object'); + async getTransportStats() { + return this._pc.getStats(); } - /** - * Observer. - * - * @emits close - * @emits newproducer - (producer: Producer) - * @emits newconsumer - (producer: Producer) - * @emits newdataproducer - (dataProducer: DataProducer) - * @emits newdataconsumer - (dataProducer: DataProducer) - */ - get observer() { - return this._observer; + async send({ track, encodings, codecOptions, codec }) { + var _a; + this.assertSendDirection(); + logger.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); + if (codec) { + logger.warn('send() | codec selection is not available in %s handler', this.name); + } + this._sendStream.addTrack(track); + this._pc.addStream(this._sendStream); + let offer = await this._pc.createOffer(); + let localSdpObject = sdpTransform.parse(offer.sdp); + let offerMediaObject; + const sendingRtpParameters = utils.clone(this._sendingRtpParametersByKind[track.kind], {}); + sendingRtpParameters.codecs = + ortc.reduceCodecs(sendingRtpParameters.codecs); + const sendingRemoteRtpParameters = utils.clone(this._sendingRemoteRtpParametersByKind[track.kind], {}); + sendingRemoteRtpParameters.codecs = + ortc.reduceCodecs(sendingRemoteRtpParameters.codecs); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); + } + if (track.kind === 'video' && encodings && encodings.length > 1) { + logger.debug('send() | enabling simulcast'); + localSdpObject = sdpTransform.parse(offer.sdp); + offerMediaObject = localSdpObject.media.find((m) => m.type === 'video'); + sdpPlanBUtils.addLegacySimulcast({ + offerMediaObject, + track, + numStreams: encodings.length + }); + offer = { type: 'offer', sdp: sdpTransform.write(localSdpObject) }; + } + logger.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + offerMediaObject = localSdpObject.media + .find((m) => m.type === track.kind); + // Set RTCP CNAME. + sendingRtpParameters.rtcp.cname = + sdpCommonUtils.getCname({ offerMediaObject }); + // Set RTP encodings. + sendingRtpParameters.encodings = + sdpPlanBUtils.getRtpEncodings({ offerMediaObject, track }); + // Complete encodings with given values. + if (encodings) { + for (let idx = 0; idx < sendingRtpParameters.encodings.length; ++idx) { + if (encodings[idx]) { + Object.assign(sendingRtpParameters.encodings[idx], encodings[idx]); + } + } + } + // If VP8 and there is effective simulcast, add scalabilityMode to each + // encoding. + if (sendingRtpParameters.encodings.length > 1 && + sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8') { + for (const encoding of sendingRtpParameters.encodings) { + encoding.scalabilityMode = 'L1T3'; + } + } + this._remoteSdp.send({ + offerMediaObject, + offerRtpParameters: sendingRtpParameters, + answerRtpParameters: sendingRemoteRtpParameters, + codecOptions + }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + const localId = String(this._nextSendLocalId); + this._nextSendLocalId++; + // Insert into the map. + this._mapSendLocalIdTrack.set(localId, track); + return { + localId: localId, + rtpParameters: sendingRtpParameters + }; } - /** - * Close the Transport. - */ - close() { - if (this._closed) - return; - logger.debug('close()'); - this._closed = true; - // Close the AwaitQueue. - this._awaitQueue.close(); - // Close the handler. - this._handler.close(); - // Close all Producers. - for (const producer of this._producers.values()) { - producer.transportClosed(); + async stopSending(localId) { + this.assertSendDirection(); + logger.debug('stopSending() [localId:%s]', localId); + const track = this._mapSendLocalIdTrack.get(localId); + if (!track) { + throw new Error('track not found'); } - this._producers.clear(); - // Close all Consumers. - for (const consumer of this._consumers.values()) { - consumer.transportClosed(); + this._mapSendLocalIdTrack.delete(localId); + this._sendStream.removeTrack(track); + this._pc.addStream(this._sendStream); + const offer = await this._pc.createOffer(); + logger.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); + try { + await this._pc.setLocalDescription(offer); } - this._consumers.clear(); - // Close all DataProducers. - for (const dataProducer of this._dataProducers.values()) { - dataProducer.transportClosed(); + catch (error) { + // NOTE: If there are no sending tracks, setLocalDescription() will fail with + // "Failed to create channels". If so, ignore it. + if (this._sendStream.getTracks().length === 0) { + logger.warn('stopSending() | ignoring expected error due no sending tracks: %s', error.toString()); + return; + } + throw error; } - this._dataProducers.clear(); - // Close all DataConsumers. - for (const dataConsumer of this._dataConsumers.values()) { - dataConsumer.transportClosed(); + if (this._pc.signalingState === 'stable') { + return; } - this._dataConsumers.clear(); - // Emit observer event. - this._observer.safeEmit('close'); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); } - /** - * Get associated Transport (RTCPeerConnection) stats. - * - * @returns {RTCStatsReport} - */ - async getStats() { - if (this._closed) - throw new errors_1.InvalidStateError('closed'); - return this._handler.getTransportStats(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async pauseSending(localId) { + // Unimplemented. } - /** - * Restart ICE connection. - */ - async restartIce({ iceParameters }) { - logger.debug('restartIce()'); - if (this._closed) - throw new errors_1.InvalidStateError('closed'); - else if (!iceParameters) - throw new TypeError('missing iceParameters'); - // Enqueue command. - return this._awaitQueue.push(async () => this._handler.restartIce(iceParameters), 'transport.restartIce()'); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async resumeSending(localId) { + // Unimplemented. } - /** - * Update ICE servers. - */ - async updateIceServers({ iceServers } = {}) { - logger.debug('updateIceServers()'); - if (this._closed) - throw new errors_1.InvalidStateError('closed'); - else if (!Array.isArray(iceServers)) - throw new TypeError('missing iceServers'); - // Enqueue command. - return this._awaitQueue.push(async () => this._handler.updateIceServers(iceServers), 'transport.updateIceServers()'); + async replaceTrack( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localId, track) { + throw new errors_1.UnsupportedError('not implemented'); } - /** - * Create a Producer. - */ - async produce({ track, encodings, codecOptions, codec, stopTracks = true, disableTrackOnPause = true, zeroRtpOnPause = false, appData = {} } = {}) { - logger.debug('produce() [track:%o]', track); - if (!track) - throw new TypeError('missing track'); - else if (this._direction !== 'send') - throw new errors_1.UnsupportedError('not a sending Transport'); - else if (!this._canProduceByKind[track.kind]) - throw new errors_1.UnsupportedError(`cannot produce ${track.kind}`); - else if (track.readyState === 'ended') - throw new errors_1.InvalidStateError('track ended'); - else if (this.listenerCount('connect') === 0 && this._connectionState === 'new') - throw new TypeError('no "connect" listener set into this transport'); - else if (this.listenerCount('produce') === 0) - throw new TypeError('no "produce" listener set into this transport'); - else if (appData && typeof appData !== 'object') - throw new TypeError('if given, appData must be an object'); - // Enqueue command. - return this._awaitQueue.push(async () => { - let normalizedEncodings; - if (encodings && !Array.isArray(encodings)) { - throw TypeError('encodings must be an array'); - } - else if (encodings && encodings.length === 0) { - normalizedEncodings = undefined; - } - else if (encodings) { - normalizedEncodings = encodings - .map((encoding) => { - const normalizedEncoding = { active: true }; - if (encoding.active === false) - normalizedEncoding.active = false; - if (typeof encoding.dtx === 'boolean') - normalizedEncoding.dtx = encoding.dtx; - if (typeof encoding.scalabilityMode === 'string') - normalizedEncoding.scalabilityMode = encoding.scalabilityMode; - if (typeof encoding.scaleResolutionDownBy === 'number') - normalizedEncoding.scaleResolutionDownBy = encoding.scaleResolutionDownBy; - if (typeof encoding.maxBitrate === 'number') - normalizedEncoding.maxBitrate = encoding.maxBitrate; - if (typeof encoding.maxFramerate === 'number') - normalizedEncoding.maxFramerate = encoding.maxFramerate; - if (typeof encoding.adaptivePtime === 'boolean') - normalizedEncoding.adaptivePtime = encoding.adaptivePtime; - if (typeof encoding.priority === 'string') - normalizedEncoding.priority = encoding.priority; - if (typeof encoding.networkPriority === 'string') - normalizedEncoding.networkPriority = encoding.networkPriority; - return normalizedEncoding; - }); - } - const { localId, rtpParameters, rtpSender } = await this._handler.send({ - track, - encodings: normalizedEncodings, - codecOptions, - codec - }); - try { - // This will fill rtpParameters's missing fields with default values. - ortc.validateRtpParameters(rtpParameters); - const { id } = await this.safeEmitAsPromise('produce', { - kind: track.kind, - rtpParameters, - appData - }); - const producer = new Producer_1.Producer({ - id, - localId, - rtpSender, - track, - rtpParameters, - stopTracks, - disableTrackOnPause, - zeroRtpOnPause, - appData - }); - this._producers.set(producer.id, producer); - this._handleProducer(producer); - // Emit observer event. - this._observer.safeEmit('newproducer', producer); - return producer; - } - catch (error) { - this._handler.stopSending(localId) - .catch(() => { }); - throw error; - } - }, 'transport.produce()') - // This catch is needed to stop the given track if the command above - // failed due to closed Transport. - .catch((error) => { - if (stopTracks) { - try { - track.stop(); - } - catch (error2) { } - } - throw error; - }); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async setMaxSpatialLayer(localId, spatialLayer) { + throw new errors_1.UnsupportedError(' not implemented'); } - /** - * Create a Consumer to consume a remote Producer. - */ - async consume({ id, producerId, kind, rtpParameters, appData = {} }) { - logger.debug('consume()'); - rtpParameters = utils.clone(rtpParameters, undefined); - if (this._closed) - throw new errors_1.InvalidStateError('closed'); - else if (this._direction !== 'recv') - throw new errors_1.UnsupportedError('not a receiving Transport'); - else if (typeof id !== 'string') - throw new TypeError('missing id'); - else if (typeof producerId !== 'string') - throw new TypeError('missing producerId'); - else if (kind !== 'audio' && kind !== 'video') - throw new TypeError(`invalid kind '${kind}'`); - else if (this.listenerCount('connect') === 0 && this._connectionState === 'new') - throw new TypeError('no "connect" listener set into this transport'); - else if (appData && typeof appData !== 'object') - throw new TypeError('if given, appData must be an object'); - // Enqueue command. - return this._awaitQueue.push(async () => { - // Ensure the device can consume it. - const canConsume = ortc.canReceive(rtpParameters, this._extendedRtpCapabilities); - if (!canConsume) - throw new errors_1.UnsupportedError('cannot consume this Producer'); - const { localId, rtpReceiver, track } = await this._handler.receive({ trackId: id, kind, rtpParameters }); - const consumer = new Consumer_1.Consumer({ - id, - localId, - producerId, - rtpReceiver, - track, - rtpParameters, - appData - }); - this._consumers.set(consumer.id, consumer); - this._handleConsumer(consumer); - // If this is the first video Consumer and the Consumer for RTP probation - // has not yet been created, create it now. - if (!this._probatorConsumerCreated && kind === 'video') { - try { - const probatorRtpParameters = ortc.generateProbatorRtpParameters(consumer.rtpParameters); - await this._handler.receive({ - trackId: 'probator', - kind: 'video', - rtpParameters: probatorRtpParameters - }); - logger.debug('consume() | Consumer for RTP probation created'); - this._probatorConsumerCreated = true; - } - catch (error) { - logger.error('consume() | failed to create Consumer for RTP probation:%o', error); - } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async setRtpEncodingParameters(localId, params) { + throw new errors_1.UnsupportedError('not supported'); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getSenderStats(localId) { + throw new errors_1.UnsupportedError('not implemented'); + } + async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { + var _a; + this.assertSendDirection(); + const options = { + negotiated: true, + id: this._nextSendSctpStreamId, + ordered, + maxPacketLifeTime, + maxRetransmitTime: maxPacketLifeTime, + maxRetransmits, + protocol + }; + logger.debug('sendDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // Increase next id. + this._nextSendSctpStreamId = + ++this._nextSendSctpStreamId % SCTP_NUM_STREAMS.MIS; + // If this is the first DataChannel we need to create the SDP answer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + const offer = await this._pc.createOffer(); + const localSdpObject = sdpTransform.parse(offer.sdp); + const offerMediaObject = localSdpObject.media + .find((m) => m.type === 'application'); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); } - // Emit observer event. - this._observer.safeEmit('newconsumer', consumer); - return consumer; - }, 'transport.consume()'); + logger.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + this._remoteSdp.sendSctpAssociation({ offerMediaObject }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._hasDataChannelMediaSection = true; + } + const sctpStreamParameters = { + streamId: options.id, + ordered: options.ordered, + maxPacketLifeTime: options.maxPacketLifeTime, + maxRetransmits: options.maxRetransmits + }; + return { dataChannel, sctpStreamParameters }; } - /** - * Create a DataProducer - */ - async produceData({ ordered = true, maxPacketLifeTime, maxRetransmits, label = '', protocol = '', appData = {} } = {}) { - logger.debug('produceData()'); - if (this._direction !== 'send') - throw new errors_1.UnsupportedError('not a sending Transport'); - else if (!this._maxSctpMessageSize) - throw new errors_1.UnsupportedError('SCTP not enabled by remote Transport'); - else if (this.listenerCount('connect') === 0 && this._connectionState === 'new') - throw new TypeError('no "connect" listener set into this transport'); - else if (this.listenerCount('producedata') === 0) - throw new TypeError('no "producedata" listener set into this transport'); - else if (appData && typeof appData !== 'object') - throw new TypeError('if given, appData must be an object'); - if (maxPacketLifeTime || maxRetransmits) - ordered = false; - // Enqueue command. - return this._awaitQueue.push(async () => { - const { dataChannel, sctpStreamParameters } = await this._handler.sendDataChannel({ - ordered, - maxPacketLifeTime, - maxRetransmits, - label, - protocol - }); - // This will fill sctpStreamParameters's missing fields with default values. - ortc.validateSctpStreamParameters(sctpStreamParameters); - const { id } = await this.safeEmitAsPromise('producedata', { - sctpStreamParameters, - label, - protocol, - appData + async receive(optionsList) { + var _a; + this.assertRecvDirection(); + const results = []; + for (const options of optionsList) { + const { trackId, kind, rtpParameters, streamId } = options; + logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const mid = kind; + this._remoteSdp.receive({ + mid, + kind, + offerRtpParameters: rtpParameters, + streamId: streamId || rtpParameters.rtcp.cname, + trackId }); - const dataProducer = new DataProducer_1.DataProducer({ id, dataChannel, sctpStreamParameters, appData }); - this._dataProducers.set(dataProducer.id, dataProducer); - this._handleDataProducer(dataProducer); - // Emit observer event. - this._observer.safeEmit('newdataproducer', dataProducer); - return dataProducer; - }, 'transport.produceData()'); - } - /** - * Create a DataConsumer - */ - async consumeData({ id, dataProducerId, sctpStreamParameters, label = '', protocol = '', appData = {} }) { - logger.debug('consumeData()'); - sctpStreamParameters = utils.clone(sctpStreamParameters, undefined); - if (this._closed) - throw new errors_1.InvalidStateError('closed'); - else if (this._direction !== 'recv') - throw new errors_1.UnsupportedError('not a receiving Transport'); - else if (!this._maxSctpMessageSize) - throw new errors_1.UnsupportedError('SCTP not enabled by remote Transport'); - else if (typeof id !== 'string') - throw new TypeError('missing id'); - else if (typeof dataProducerId !== 'string') - throw new TypeError('missing dataProducerId'); - else if (this.listenerCount('connect') === 0 && this._connectionState === 'new') - throw new TypeError('no "connect" listener set into this transport'); - else if (appData && typeof appData !== 'object') - throw new TypeError('if given, appData must be an object'); - // This may throw. - ortc.validateSctpStreamParameters(sctpStreamParameters); - // Enqueue command. - return this._awaitQueue.push(async () => { - const { dataChannel } = await this._handler.receiveDataChannel({ - sctpStreamParameters, - label, - protocol + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + let answer = await this._pc.createAnswer(); + const localSdpObject = sdpTransform.parse(answer.sdp); + for (const options of optionsList) { + const { kind, rtpParameters } = options; + const mid = kind; + const answerMediaObject = localSdpObject.media + .find((m) => String(m.mid) === mid); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject }); - const dataConsumer = new DataConsumer_1.DataConsumer({ - id, - dataProducerId, - dataChannel, - sctpStreamParameters, - appData + } + answer = { type: 'answer', sdp: sdpTransform.write(localSdpObject) }; + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject }); - this._dataConsumers.set(dataConsumer.id, dataConsumer); - this._handleDataConsumer(dataConsumer); - // Emit observer event. - this._observer.safeEmit('newdataconsumer', dataConsumer); - return dataConsumer; - }, 'transport.consumeData()'); - } - _handleHandler() { - const handler = this._handler; - handler.on('@connect', ({ dtlsParameters }, callback, errback) => { - if (this._closed) { - errback(new errors_1.InvalidStateError('closed')); - return; + } + logger.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + for (const options of optionsList) { + const { kind, trackId, rtpParameters } = options; + const mid = kind; + const localId = trackId; + const streamId = options.streamId || rtpParameters.rtcp.cname; + const stream = this._pc.getRemoteStreams() + .find((s) => s.id === streamId); + const track = stream.getTrackById(localId); + if (!track) { + throw new Error('remote track not found'); } - this.safeEmit('connect', { dtlsParameters }, callback, errback); - }); - handler.on('@connectionstatechange', (connectionState) => { - if (connectionState === this._connectionState) - return; - logger.debug('connection state changed to %s', connectionState); - this._connectionState = connectionState; - if (!this._closed) - this.safeEmit('connectionstatechange', connectionState); - }); + // Insert into the map. + this._mapRecvLocalIdInfo.set(localId, { mid, rtpParameters }); + results.push({ localId, track }); + } + return results; } - _handleProducer(producer) { - producer.on('@close', () => { - this._producers.delete(producer.id); - if (this._closed) - return; - this._awaitQueue.push(async () => this._handler.stopSending(producer.localId), 'producer @close event') - .catch((error) => logger.warn('producer.close() failed:%o', error)); - }); - producer.on('@replacetrack', (track, callback, errback) => { - this._awaitQueue.push(async () => this._handler.replaceTrack(producer.localId, track), 'producer @replacetrack event') - .then(callback) - .catch(errback); - }); - producer.on('@setmaxspatiallayer', (spatialLayer, callback, errback) => { - this._awaitQueue.push(async () => (this._handler.setMaxSpatialLayer(producer.localId, spatialLayer)), 'producer @setmaxspatiallayer event') - .then(callback) - .catch(errback); - }); - producer.on('@setrtpencodingparameters', (params, callback, errback) => { - this._awaitQueue.push(async () => (this._handler.setRtpEncodingParameters(producer.localId, params)), 'producer @setrtpencodingparameters event') - .then(callback) - .catch(errback); - }); - producer.on('@getstats', (callback, errback) => { - if (this._closed) - return errback(new errors_1.InvalidStateError('closed')); - this._handler.getSenderStats(producer.localId) - .then(callback) - .catch(errback); - }); + async stopReceiving(localIds) { + this.assertRecvDirection(); + for (const localId of localIds) { + logger.debug('stopReceiving() [localId:%s]', localId); + const { mid, rtpParameters } = this._mapRecvLocalIdInfo.get(localId) || {}; + // Remove from the map. + this._mapRecvLocalIdInfo.delete(localId); + this._remoteSdp.planBStopReceiving({ mid: mid, offerRtpParameters: rtpParameters }); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); } - _handleConsumer(consumer) { - consumer.on('@close', () => { - this._consumers.delete(consumer.id); - if (this._closed) - return; - this._awaitQueue.push(async () => this._handler.stopReceiving(consumer.localId), 'consumer @close event') - .catch(() => { }); - }); - consumer.on('@getstats', (callback, errback) => { - if (this._closed) - return errback(new errors_1.InvalidStateError('closed')); - this._handler.getReceiverStats(consumer.localId) - .then(callback) - .catch(errback); - }); + async pauseReceiving( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localIds) { + // Unimplemented. } - _handleDataProducer(dataProducer) { - dataProducer.on('@close', () => { - this._dataProducers.delete(dataProducer.id); - }); + async resumeReceiving( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localIds) { + // Unimplemented. } - _handleDataConsumer(dataConsumer) { - dataConsumer.on('@close', () => { - this._dataConsumers.delete(dataConsumer.id); - }); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getReceiverStats(localId) { + throw new errors_1.UnsupportedError('not implemented'); } -} -exports.Transport = Transport; - -},{"./Consumer":8,"./DataConsumer":9,"./DataProducer":10,"./EnhancedEventEmitter":12,"./Logger":13,"./Producer":14,"./errors":18,"./ortc":36,"./utils":39,"awaitqueue":2}],18:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.InvalidStateError = exports.UnsupportedError = void 0; -/** - * Error indicating not support for something. - */ -class UnsupportedError extends Error { - constructor(message) { - super(message); - this.name = 'UnsupportedError'; - if (Error.hasOwnProperty('captureStackTrace')) // Just in V8. - { - // @ts-ignore - Error.captureStackTrace(this, UnsupportedError); + async receiveDataChannel({ sctpStreamParameters, label, protocol }) { + var _a; + this.assertRecvDirection(); + const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; + const options = { + negotiated: true, + id: streamId, + ordered, + maxPacketLifeTime, + maxRetransmitTime: maxPacketLifeTime, + maxRetransmits, + protocol + }; + logger.debug('receiveDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // If this is the first DataChannel we need to create the SDP offer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + this._remoteSdp.receiveSctpAssociation({ oldDataChannelSpec: true }); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + if (!this._transportReady) { + const localSdpObject = sdpTransform.parse(answer.sdp); + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); + } + logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + this._hasDataChannelMediaSection = true; } - else { - this.stack = (new Error(message)).stack; + return { dataChannel }; + } + async setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) { + localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); } + // Get our local DTLS parameters. + const dtlsParameters = sdpCommonUtils.extractDtlsParameters({ sdpObject: localSdpObject }); + // Set our DTLS role. + dtlsParameters.role = localDtlsRole; + // Update the remote DTLS role in the SDP. + this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); + // Need to tell the remote transport about our parameters. + await new Promise((resolve, reject) => { + this.safeEmit('@connect', { dtlsParameters }, resolve, reject); + }); + this._transportReady = true; } -} -exports.UnsupportedError = UnsupportedError; -/** - * Error produced when calling a method in an invalid state. - */ -class InvalidStateError extends Error { - constructor(message) { - super(message); - this.name = 'InvalidStateError'; - if (Error.hasOwnProperty('captureStackTrace')) // Just in V8. - { - // @ts-ignore - Error.captureStackTrace(this, InvalidStateError); + assertSendDirection() { + if (this._direction !== 'send') { + throw new Error('method can just be called for handlers with "send" direction'); } - else { - this.stack = (new Error(message)).stack; + } + assertRecvDirection() { + if (this._direction !== 'recv') { + throw new Error('method can just be called for handlers with "recv" direction'); } } } -exports.InvalidStateError = InvalidStateError; +exports.Chrome55 = Chrome55; -},{}],19:[function(require,module,exports){ +},{"../Logger":17,"../errors":22,"../ortc":43,"../utils":46,"./HandlerInterface":30,"./sdp/RemoteSdp":38,"./sdp/commonUtils":39,"./sdp/planBUtils":40,"sdp-transform":52}],25:[function(require,module,exports){ "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; @@ -3234,30 +4876,35 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.Chrome55 = void 0; +exports.Chrome67 = void 0; const sdpTransform = __importStar(require("sdp-transform")); const Logger_1 = require("../Logger"); -const errors_1 = require("../errors"); const utils = __importStar(require("../utils")); const ortc = __importStar(require("../ortc")); const sdpCommonUtils = __importStar(require("./sdp/commonUtils")); const sdpPlanBUtils = __importStar(require("./sdp/planBUtils")); const HandlerInterface_1 = require("./HandlerInterface"); const RemoteSdp_1 = require("./sdp/RemoteSdp"); -const logger = new Logger_1.Logger('Chrome55'); +const logger = new Logger_1.Logger('Chrome67'); const SCTP_NUM_STREAMS = { OS: 1024, MIS: 1024 }; -class Chrome55 extends HandlerInterface_1.HandlerInterface { +class Chrome67 extends HandlerInterface_1.HandlerInterface { + /** + * Creates a factory function. + */ + static createFactory() { + return () => new Chrome67(); + } constructor() { super(); // Local stream for sending. this._sendStream = new MediaStream(); - // Map of sending MediaStreamTracks indexed by localId. - this._mapSendLocalIdTrack = new Map(); + // Map of RTCRtpSender indexed by localId. + this._mapSendLocalIdRtpSender = new Map(); // Next sending localId. this._nextSendLocalId = 0; // Map of MID, RTP parameters and RTCRtpReceiver indexed by local id. @@ -3270,14 +4917,8 @@ class Chrome55 extends HandlerInterface_1.HandlerInterface { // Got transport local and remote parameters. this._transportReady = false; } - /** - * Creates a factory function. - */ - static createFactory() { - return () => new Chrome55(); - } get name() { - return 'Chrome55'; + return 'Chrome67'; } close() { logger.debug('close()'); @@ -3288,6 +4929,7 @@ class Chrome55 extends HandlerInterface_1.HandlerInterface { } catch (error) { } } + this.emit('@close'); } async getNativeRtpCapabilities() { logger.debug('getNativeRtpCapabilities()'); @@ -3345,28 +4987,47 @@ class Chrome55 extends HandlerInterface_1.HandlerInterface { audio: ortc.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), video: ortc.getSendingRemoteRtpParameters('video', extendedRtpCapabilities) }; - this._pc = new RTCPeerConnection(Object.assign({ iceServers: iceServers || [], iceTransportPolicy: iceTransportPolicy || 'all', bundlePolicy: 'max-bundle', rtcpMuxPolicy: 'require', sdpSemantics: 'plan-b' }, additionalSettings), proprietaryConstraints); - // Handle RTCPeerConnection connection status. - this._pc.addEventListener('iceconnectionstatechange', () => { - switch (this._pc.iceConnectionState) { - case 'checking': - this.emit('@connectionstatechange', 'connecting'); - break; - case 'connected': - case 'completed': - this.emit('@connectionstatechange', 'connected'); - break; - case 'failed': - this.emit('@connectionstatechange', 'failed'); - break; - case 'disconnected': - this.emit('@connectionstatechange', 'disconnected'); - break; - case 'closed': - this.emit('@connectionstatechange', 'closed'); - break; - } - }); + if (dtlsParameters.role && dtlsParameters.role !== 'auto') { + this._forcedLocalDtlsRole = dtlsParameters.role === 'server' + ? 'client' + : 'server'; + } + this._pc = new RTCPeerConnection({ + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'plan-b', + ...additionalSettings + }, proprietaryConstraints); + if (this._pc.connectionState) { + this._pc.addEventListener('connectionstatechange', () => { + this.emit('@connectionstatechange', this._pc.connectionState); + }); + } + else { + this._pc.addEventListener('iceconnectionstatechange', () => { + logger.warn('run() | pc.connectionState not supported, using pc.iceConnectionState'); + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } } async updateIceServers(iceServers) { logger.debug('updateIceServers()'); @@ -3378,8 +5039,9 @@ class Chrome55 extends HandlerInterface_1.HandlerInterface { logger.debug('restartIce()'); // Provide the remote SDP handler with new remote ICE parameters. this._remoteSdp.updateIceParameters(iceParameters); - if (!this._transportReady) + if (!this._transportReady) { return; + } if (this._direction === 'send') { const offer = await this._pc.createOffer({ iceRestart: true }); logger.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); @@ -3401,13 +5063,14 @@ class Chrome55 extends HandlerInterface_1.HandlerInterface { return this._pc.getStats(); } async send({ track, encodings, codecOptions, codec }) { - this._assertSendDirection(); + var _a; + this.assertSendDirection(); logger.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); if (codec) { logger.warn('send() | codec selection is not available in %s handler', this.name); } this._sendStream.addTrack(track); - this._pc.addStream(this._sendStream); + this._pc.addTrack(track, this._sendStream); let offer = await this._pc.createOffer(); let localSdpObject = sdpTransform.parse(offer.sdp); let offerMediaObject; @@ -3417,12 +5080,17 @@ class Chrome55 extends HandlerInterface_1.HandlerInterface { const sendingRemoteRtpParameters = utils.clone(this._sendingRemoteRtpParametersByKind[track.kind], {}); sendingRemoteRtpParameters.codecs = ortc.reduceCodecs(sendingRemoteRtpParameters.codecs); - if (!this._transportReady) - await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); + } if (track.kind === 'video' && encodings && encodings.length > 1) { logger.debug('send() | enabling simulcast'); localSdpObject = sdpTransform.parse(offer.sdp); - offerMediaObject = localSdpObject.media.find((m) => m.type === 'video'); + offerMediaObject = localSdpObject.media + .find((m) => m.type === 'video'); sdpPlanBUtils.addLegacySimulcast({ offerMediaObject, track, @@ -3444,8 +5112,9 @@ class Chrome55 extends HandlerInterface_1.HandlerInterface { // Complete encodings with given values. if (encodings) { for (let idx = 0; idx < sendingRtpParameters.encodings.length; ++idx) { - if (encodings[idx]) + if (encodings[idx]) { Object.assign(sendingRtpParameters.encodings[idx], encodings[idx]); + } } } // If VP8 and there is effective simulcast, add scalabilityMode to each @@ -3453,7 +5122,7 @@ class Chrome55 extends HandlerInterface_1.HandlerInterface { if (sendingRtpParameters.encodings.length > 1 && sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8') { for (const encoding of sendingRtpParameters.encodings) { - encoding.scalabilityMode = 'S1T3'; + encoding.scalabilityMode = 'L1T3'; } } this._remoteSdp.send({ @@ -3467,22 +5136,28 @@ class Chrome55 extends HandlerInterface_1.HandlerInterface { await this._pc.setRemoteDescription(answer); const localId = String(this._nextSendLocalId); this._nextSendLocalId++; + const rtpSender = this._pc.getSenders() + .find((s) => s.track === track); // Insert into the map. - this._mapSendLocalIdTrack.set(localId, track); + this._mapSendLocalIdRtpSender.set(localId, rtpSender); return { localId: localId, - rtpParameters: sendingRtpParameters + rtpParameters: sendingRtpParameters, + rtpSender }; } async stopSending(localId) { - this._assertSendDirection(); + this.assertSendDirection(); logger.debug('stopSending() [localId:%s]', localId); - const track = this._mapSendLocalIdTrack.get(localId); - if (!track) - throw new Error('track not found'); - this._mapSendLocalIdTrack.delete(localId); - this._sendStream.removeTrack(track); - this._pc.addStream(this._sendStream); + const rtpSender = this._mapSendLocalIdRtpSender.get(localId); + if (!rtpSender) { + throw new Error('associated RTCRtpSender not found'); + } + this._pc.removeTrack(rtpSender); + if (rtpSender.track) { + this._sendStream.removeTrack(rtpSender.track); + } + this._mapSendLocalIdRtpSender.delete(localId); const offer = await this._pc.createOffer(); logger.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); try { @@ -3497,31 +5172,86 @@ class Chrome55 extends HandlerInterface_1.HandlerInterface { } throw error; } - if (this._pc.signalingState === 'stable') + if (this._pc.signalingState === 'stable') { return; + } const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; logger.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); await this._pc.setRemoteDescription(answer); } - async replaceTrack( // eslint-disable-next-line @typescript-eslint/no-unused-vars - localId, track) { - throw new errors_1.UnsupportedError('not implemented'); + async pauseSending(localId) { + // Unimplemented. } // eslint-disable-next-line @typescript-eslint/no-unused-vars + async resumeSending(localId) { + // Unimplemented. + } + async replaceTrack(localId, track) { + this.assertSendDirection(); + if (track) { + logger.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id); + } + else { + logger.debug('replaceTrack() [localId:%s, no track]', localId); + } + const rtpSender = this._mapSendLocalIdRtpSender.get(localId); + if (!rtpSender) { + throw new Error('associated RTCRtpSender not found'); + } + const oldTrack = rtpSender.track; + await rtpSender.replaceTrack(track); + // Remove the old track from the local stream. + if (oldTrack) { + this._sendStream.removeTrack(oldTrack); + } + // Add the new track to the local stream. + if (track) { + this._sendStream.addTrack(track); + } + } async setMaxSpatialLayer(localId, spatialLayer) { - throw new errors_1.UnsupportedError(' not implemented'); + this.assertSendDirection(); + logger.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer); + const rtpSender = this._mapSendLocalIdRtpSender.get(localId); + if (!rtpSender) { + throw new Error('associated RTCRtpSender not found'); + } + const parameters = rtpSender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + if (idx <= spatialLayer) { + encoding.active = true; + } + else { + encoding.active = false; + } + }); + await rtpSender.setParameters(parameters); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars async setRtpEncodingParameters(localId, params) { - throw new errors_1.UnsupportedError('not supported'); + this.assertSendDirection(); + logger.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params); + const rtpSender = this._mapSendLocalIdRtpSender.get(localId); + if (!rtpSender) { + throw new Error('associated RTCRtpSender not found'); + } + const parameters = rtpSender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + parameters.encodings[idx] = { ...encoding, ...params }; + }); + await rtpSender.setParameters(parameters); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars async getSenderStats(localId) { - throw new errors_1.UnsupportedError('not implemented'); + this.assertSendDirection(); + const rtpSender = this._mapSendLocalIdRtpSender.get(localId); + if (!rtpSender) { + throw new Error('associated RTCRtpSender not found'); + } + return rtpSender.getStats(); } async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { - this._assertSendDirection(); + var _a; + this.assertSendDirection(); const options = { negotiated: true, id: this._nextSendSctpStreamId, @@ -3543,8 +5273,12 @@ class Chrome55 extends HandlerInterface_1.HandlerInterface { const localSdpObject = sdpTransform.parse(offer.sdp); const offerMediaObject = localSdpObject.media .find((m) => m.type === 'application'); - if (!this._transportReady) - await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); + } logger.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); await this._pc.setLocalDescription(offer); this._remoteSdp.sendSctpAssociation({ offerMediaObject }); @@ -3561,53 +5295,76 @@ class Chrome55 extends HandlerInterface_1.HandlerInterface { }; return { dataChannel, sctpStreamParameters }; } - async receive({ trackId, kind, rtpParameters }) { - this._assertRecvDirection(); - logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); - const localId = trackId; - const mid = kind; - const streamId = rtpParameters.rtcp.cname; - this._remoteSdp.receive({ - mid, - kind, - offerRtpParameters: rtpParameters, - streamId, - trackId - }); + async receive(optionsList) { + var _a; + this.assertRecvDirection(); + const results = []; + for (const options of optionsList) { + const { trackId, kind, rtpParameters, streamId } = options; + logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const mid = kind; + this._remoteSdp.receive({ + mid, + kind, + offerRtpParameters: rtpParameters, + streamId: streamId || rtpParameters.rtcp.cname, + trackId + }); + } const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; logger.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); await this._pc.setRemoteDescription(offer); let answer = await this._pc.createAnswer(); const localSdpObject = sdpTransform.parse(answer.sdp); - const answerMediaObject = localSdpObject.media - .find((m) => String(m.mid) === mid); - // May need to modify codec parameters in the answer based on codec - // parameters in the offer. - sdpCommonUtils.applyCodecParameters({ - offerRtpParameters: rtpParameters, - answerMediaObject - }); + for (const options of optionsList) { + const { kind, rtpParameters } = options; + const mid = kind; + const answerMediaObject = localSdpObject.media + .find((m) => String(m.mid) === mid); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject + }); + } answer = { type: 'answer', sdp: sdpTransform.write(localSdpObject) }; - if (!this._transportReady) - await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); + } logger.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); await this._pc.setLocalDescription(answer); - const stream = this._pc.getRemoteStreams() - .find((s) => s.id === streamId); - const track = stream.getTrackById(localId); - if (!track) - throw new Error('remote track not found'); - // Insert into the map. - this._mapRecvLocalIdInfo.set(localId, { mid, rtpParameters }); - return { localId, track }; - } - async stopReceiving(localId) { - this._assertRecvDirection(); - logger.debug('stopReceiving() [localId:%s]', localId); - const { mid, rtpParameters } = this._mapRecvLocalIdInfo.get(localId) || {}; - // Remove from the map. - this._mapRecvLocalIdInfo.delete(localId); - this._remoteSdp.planBStopReceiving({ mid: mid, offerRtpParameters: rtpParameters }); + for (const options of optionsList) { + const { kind, trackId, rtpParameters } = options; + const localId = trackId; + const mid = kind; + const rtpReceiver = this._pc.getReceivers() + .find((r) => r.track && r.track.id === localId); + if (!rtpReceiver) { + throw new Error('new RTCRtpReceiver not'); + } + // Insert into the map. + this._mapRecvLocalIdInfo.set(localId, { mid, rtpParameters, rtpReceiver }); + results.push({ + localId, + track: rtpReceiver.track, + rtpReceiver + }); + } + return results; + } + async stopReceiving(localIds) { + this.assertRecvDirection(); + for (const localId of localIds) { + logger.debug('stopReceiving() [localId:%s]', localId); + const { mid, rtpParameters } = this._mapRecvLocalIdInfo.get(localId) || {}; + // Remove from the map. + this._mapRecvLocalIdInfo.delete(localId); + this._remoteSdp.planBStopReceiving({ mid: mid, offerRtpParameters: rtpParameters }); + } const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; logger.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); await this._pc.setRemoteDescription(offer); @@ -3615,12 +5372,27 @@ class Chrome55 extends HandlerInterface_1.HandlerInterface { logger.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); await this._pc.setLocalDescription(answer); } + async pauseReceiving( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localIds) { + // Unimplemented. + } + async resumeReceiving( // eslint-disable-next-line @typescript-eslint/no-unused-vars + localIds) { + // Unimplemented. + } async getReceiverStats(localId) { - throw new errors_1.UnsupportedError('not implemented'); + this.assertRecvDirection(); + const { rtpReceiver } = this._mapRecvLocalIdInfo.get(localId) || {}; + if (!rtpReceiver) { + throw new Error('associated RTCRtpReceiver not found'); + } + return rtpReceiver.getStats(); } async receiveDataChannel({ sctpStreamParameters, label, protocol }) { - this._assertRecvDirection(); + var _a; + this.assertRecvDirection(); const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; const options = { negotiated: true, @@ -3643,7 +5415,10 @@ class Chrome55 extends HandlerInterface_1.HandlerInterface { const answer = await this._pc.createAnswer(); if (!this._transportReady) { const localSdpObject = sdpTransform.parse(answer.sdp); - await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); } logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); await this._pc.setLocalDescription(answer); @@ -3651,9 +5426,10 @@ class Chrome55 extends HandlerInterface_1.HandlerInterface { } return { dataChannel }; } - async _setupTransport({ localDtlsRole, localSdpObject }) { - if (!localSdpObject) + async setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) { localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + } // Get our local DTLS parameters. const dtlsParameters = sdpCommonUtils.extractDtlsParameters({ sdpObject: localSdpObject }); // Set our DTLS role. @@ -3661,27 +5437,33 @@ class Chrome55 extends HandlerInterface_1.HandlerInterface { // Update the remote DTLS role in the SDP. this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); // Need to tell the remote transport about our parameters. - await this.safeEmitAsPromise('@connect', { dtlsParameters }); + await new Promise((resolve, reject) => { + this.safeEmit('@connect', { dtlsParameters }, resolve, reject); + }); this._transportReady = true; } - _assertSendDirection() { + assertSendDirection() { if (this._direction !== 'send') { throw new Error('method can just be called for handlers with "send" direction'); } } - _assertRecvDirection() { + assertRecvDirection() { if (this._direction !== 'recv') { throw new Error('method can just be called for handlers with "recv" direction'); } } } -exports.Chrome55 = Chrome55; +exports.Chrome67 = Chrome67; -},{"../Logger":13,"../errors":18,"../ortc":36,"../utils":39,"./HandlerInterface":25,"./sdp/RemoteSdp":31,"./sdp/commonUtils":32,"./sdp/planBUtils":33,"sdp-transform":44}],20:[function(require,module,exports){ +},{"../Logger":17,"../ortc":43,"../utils":46,"./HandlerInterface":30,"./sdp/RemoteSdp":38,"./sdp/commonUtils":39,"./sdp/planBUtils":40,"sdp-transform":52}],26:[function(require,module,exports){ "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; @@ -3694,34 +5476,36 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.Chrome67 = void 0; +exports.Chrome70 = void 0; const sdpTransform = __importStar(require("sdp-transform")); const Logger_1 = require("../Logger"); const utils = __importStar(require("../utils")); const ortc = __importStar(require("../ortc")); const sdpCommonUtils = __importStar(require("./sdp/commonUtils")); -const sdpPlanBUtils = __importStar(require("./sdp/planBUtils")); +const sdpUnifiedPlanUtils = __importStar(require("./sdp/unifiedPlanUtils")); const HandlerInterface_1 = require("./HandlerInterface"); const RemoteSdp_1 = require("./sdp/RemoteSdp"); -const logger = new Logger_1.Logger('Chrome67'); +const scalabilityModes_1 = require("../scalabilityModes"); +const logger = new Logger_1.Logger('Chrome70'); const SCTP_NUM_STREAMS = { OS: 1024, MIS: 1024 }; -class Chrome67 extends HandlerInterface_1.HandlerInterface { +class Chrome70 extends HandlerInterface_1.HandlerInterface { + /** + * Creates a factory function. + */ + static createFactory() { + return () => new Chrome70(); + } constructor() { super(); + // Map of RTCTransceivers indexed by MID. + this._mapMidTransceiver = new Map(); // Local stream for sending. this._sendStream = new MediaStream(); - // Map of RTCRtpSender indexed by localId. - this._mapSendLocalIdRtpSender = new Map(); - // Next sending localId. - this._nextSendLocalId = 0; - // Map of MID, RTP parameters and RTCRtpReceiver indexed by local id. - // Value is an Object with mid, rtpParameters and rtpReceiver. - this._mapRecvLocalIdInfo = new Map(); // Whether a DataChannel m=application section has been created. this._hasDataChannelMediaSection = false; // Sending DataChannel id value counter. Incremented for each new DataChannel. @@ -3729,14 +5513,8 @@ class Chrome67 extends HandlerInterface_1.HandlerInterface { // Got transport local and remote parameters. this._transportReady = false; } - /** - * Creates a factory function. - */ - static createFactory() { - return () => new Chrome67(); - } get name() { - return 'Chrome67'; + return 'Chrome70'; } close() { logger.debug('close()'); @@ -3747,6 +5525,7 @@ class Chrome67 extends HandlerInterface_1.HandlerInterface { } catch (error) { } } + this.emit('@close'); } async getNativeRtpCapabilities() { logger.debug('getNativeRtpCapabilities()'); @@ -3755,13 +5534,12 @@ class Chrome67 extends HandlerInterface_1.HandlerInterface { iceTransportPolicy: 'all', bundlePolicy: 'max-bundle', rtcpMuxPolicy: 'require', - sdpSemantics: 'plan-b' + sdpSemantics: 'unified-plan' }); try { - const offer = await pc.createOffer({ - offerToReceiveAudio: true, - offerToReceiveVideo: true - }); + pc.addTransceiver('audio'); + pc.addTransceiver('video'); + const offer = await pc.createOffer(); try { pc.close(); } @@ -3791,8 +5569,7 @@ class Chrome67 extends HandlerInterface_1.HandlerInterface { iceParameters, iceCandidates, dtlsParameters, - sctpParameters, - planB: true + sctpParameters }); this._sendingRtpParametersByKind = { @@ -3804,28 +5581,47 @@ class Chrome67 extends HandlerInterface_1.HandlerInterface { audio: ortc.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), video: ortc.getSendingRemoteRtpParameters('video', extendedRtpCapabilities) }; - this._pc = new RTCPeerConnection(Object.assign({ iceServers: iceServers || [], iceTransportPolicy: iceTransportPolicy || 'all', bundlePolicy: 'max-bundle', rtcpMuxPolicy: 'require', sdpSemantics: 'plan-b' }, additionalSettings), proprietaryConstraints); - // Handle RTCPeerConnection connection status. - this._pc.addEventListener('iceconnectionstatechange', () => { - switch (this._pc.iceConnectionState) { - case 'checking': - this.emit('@connectionstatechange', 'connecting'); - break; - case 'connected': - case 'completed': - this.emit('@connectionstatechange', 'connected'); - break; - case 'failed': - this.emit('@connectionstatechange', 'failed'); - break; - case 'disconnected': - this.emit('@connectionstatechange', 'disconnected'); - break; - case 'closed': - this.emit('@connectionstatechange', 'closed'); - break; - } - }); + if (dtlsParameters.role && dtlsParameters.role !== 'auto') { + this._forcedLocalDtlsRole = dtlsParameters.role === 'server' + ? 'client' + : 'server'; + } + this._pc = new RTCPeerConnection({ + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'unified-plan', + ...additionalSettings + }, proprietaryConstraints); + if (this._pc.connectionState) { + this._pc.addEventListener('connectionstatechange', () => { + this.emit('@connectionstatechange', this._pc.connectionState); + }); + } + else { + this._pc.addEventListener('iceconnectionstatechange', () => { + logger.warn('run() | pc.connectionState not supported, using pc.iceConnectionState'); + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } } async updateIceServers(iceServers) { logger.debug('updateIceServers()'); @@ -3837,8 +5633,9 @@ class Chrome67 extends HandlerInterface_1.HandlerInterface { logger.debug('restartIce()'); // Provide the remote SDP handler with new remote ICE parameters. this._remoteSdp.updateIceParameters(iceParameters); - if (!this._transportReady) + if (!this._transportReady) { return; + } if (this._direction === 'send') { const offer = await this._pc.createOffer({ iceRestart: true }); logger.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); @@ -3860,64 +5657,108 @@ class Chrome67 extends HandlerInterface_1.HandlerInterface { return this._pc.getStats(); } async send({ track, encodings, codecOptions, codec }) { - this._assertSendDirection(); + var _a; + this.assertSendDirection(); logger.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); - if (codec) { - logger.warn('send() | codec selection is not available in %s handler', this.name); - } - this._sendStream.addTrack(track); - this._pc.addTrack(track, this._sendStream); - let offer = await this._pc.createOffer(); - let localSdpObject = sdpTransform.parse(offer.sdp); - let offerMediaObject; const sendingRtpParameters = utils.clone(this._sendingRtpParametersByKind[track.kind], {}); + // This may throw. sendingRtpParameters.codecs = - ortc.reduceCodecs(sendingRtpParameters.codecs); + ortc.reduceCodecs(sendingRtpParameters.codecs, codec); const sendingRemoteRtpParameters = utils.clone(this._sendingRemoteRtpParametersByKind[track.kind], {}); + // This may throw. sendingRemoteRtpParameters.codecs = - ortc.reduceCodecs(sendingRemoteRtpParameters.codecs); - if (!this._transportReady) - await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); - if (track.kind === 'video' && encodings && encodings.length > 1) { - logger.debug('send() | enabling simulcast'); + ortc.reduceCodecs(sendingRemoteRtpParameters.codecs, codec); + const mediaSectionIdx = this._remoteSdp.getNextMediaSectionIdx(); + const transceiver = this._pc.addTransceiver(track, { direction: 'sendonly', streams: [this._sendStream] }); + let offer = await this._pc.createOffer(); + let localSdpObject = sdpTransform.parse(offer.sdp); + let offerMediaObject; + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); + } + if (encodings && encodings.length > 1) { + logger.debug('send() | enabling legacy simulcast'); + localSdpObject = sdpTransform.parse(offer.sdp); + offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; + sdpUnifiedPlanUtils.addLegacySimulcast({ + offerMediaObject, + numStreams: encodings.length + }); + offer = { type: 'offer', sdp: sdpTransform.write(localSdpObject) }; + } + // Special case for VP9 with SVC. + let hackVp9Svc = false; + const layers = (0, scalabilityModes_1.parse)((encodings || [{}])[0].scalabilityMode); + if (encodings && + encodings.length === 1 && + layers.spatialLayers > 1 && + sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp9') { + logger.debug('send() | enabling legacy simulcast for VP9 SVC'); + hackVp9Svc = true; localSdpObject = sdpTransform.parse(offer.sdp); - offerMediaObject = localSdpObject.media - .find((m) => m.type === 'video'); - sdpPlanBUtils.addLegacySimulcast({ + offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; + sdpUnifiedPlanUtils.addLegacySimulcast({ offerMediaObject, - track, - numStreams: encodings.length + numStreams: layers.spatialLayers }); offer = { type: 'offer', sdp: sdpTransform.write(localSdpObject) }; } logger.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); await this._pc.setLocalDescription(offer); + // If encodings are given, apply them now. + if (encodings) { + logger.debug('send() | applying given encodings'); + const parameters = transceiver.sender.getParameters(); + for (let idx = 0; idx < (parameters.encodings || []).length; ++idx) { + const encoding = parameters.encodings[idx]; + const desiredEncoding = encodings[idx]; + // Should not happen but just in case. + if (!desiredEncoding) { + break; + } + parameters.encodings[idx] = Object.assign(encoding, desiredEncoding); + } + await transceiver.sender.setParameters(parameters); + } + // We can now get the transceiver.mid. + const localId = transceiver.mid; + // Set MID. + sendingRtpParameters.mid = localId; localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); - offerMediaObject = localSdpObject.media - .find((m) => m.type === track.kind); + offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; // Set RTCP CNAME. sendingRtpParameters.rtcp.cname = sdpCommonUtils.getCname({ offerMediaObject }); // Set RTP encodings. sendingRtpParameters.encodings = - sdpPlanBUtils.getRtpEncodings({ offerMediaObject, track }); + sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject }); // Complete encodings with given values. if (encodings) { for (let idx = 0; idx < sendingRtpParameters.encodings.length; ++idx) { - if (encodings[idx]) + if (encodings[idx]) { Object.assign(sendingRtpParameters.encodings[idx], encodings[idx]); + } } } - // If VP8 and there is effective simulcast, add scalabilityMode to each - // encoding. + // Hack for VP9 SVC. + if (hackVp9Svc) { + sendingRtpParameters.encodings = [sendingRtpParameters.encodings[0]]; + } + // If VP8 or H264 and there is effective simulcast, add scalabilityMode to + // each encoding. if (sendingRtpParameters.encodings.length > 1 && - sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8') { + (sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8' || + sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/h264')) { for (const encoding of sendingRtpParameters.encodings) { - encoding.scalabilityMode = 'S1T3'; + encoding.scalabilityMode = 'L1T3'; } } this._remoteSdp.send({ offerMediaObject, + reuseMid: mediaSectionIdx.reuseMid, offerRtpParameters: sendingRtpParameters, answerRtpParameters: sendingRemoteRtpParameters, codecOptions @@ -3925,104 +5766,116 @@ class Chrome67 extends HandlerInterface_1.HandlerInterface { const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; logger.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer); await this._pc.setRemoteDescription(answer); - const localId = String(this._nextSendLocalId); - this._nextSendLocalId++; - const rtpSender = this._pc.getSenders() - .find((s) => s.track === track); - // Insert into the map. - this._mapSendLocalIdRtpSender.set(localId, rtpSender); + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); return { - localId: localId, + localId, rtpParameters: sendingRtpParameters, - rtpSender + rtpSender: transceiver.sender }; } async stopSending(localId) { - this._assertSendDirection(); + this.assertSendDirection(); logger.debug('stopSending() [localId:%s]', localId); - const rtpSender = this._mapSendLocalIdRtpSender.get(localId); - if (!rtpSender) - throw new Error('associated RTCRtpSender not found'); - this._pc.removeTrack(rtpSender); - if (rtpSender.track) - this._sendStream.removeTrack(rtpSender.track); - this._mapSendLocalIdRtpSender.delete(localId); - const offer = await this._pc.createOffer(); - logger.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); - try { - await this._pc.setLocalDescription(offer); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); } - catch (error) { - // NOTE: If there are no sending tracks, setLocalDescription() will fail with - // "Failed to create channels". If so, ignore it. - if (this._sendStream.getTracks().length === 0) { - logger.warn('stopSending() | ignoring expected error due no sending tracks: %s', error.toString()); - return; + transceiver.sender.replaceTrack(null); + this._pc.removeTrack(transceiver.sender); + const mediaSectionClosed = this._remoteSdp.closeMediaSection(transceiver.mid); + if (mediaSectionClosed) { + try { + transceiver.stop(); } - throw error; + catch (error) { } } - if (this._pc.signalingState === 'stable') - return; + const offer = await this._pc.createOffer(); + logger.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; logger.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); await this._pc.setRemoteDescription(answer); + this._mapMidTransceiver.delete(localId); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async pauseSending(localId) { + // Unimplemented. + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async resumeSending(localId) { + // Unimplemented. } async replaceTrack(localId, track) { - this._assertSendDirection(); + this.assertSendDirection(); if (track) { logger.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id); } else { logger.debug('replaceTrack() [localId:%s, no track]', localId); } - const rtpSender = this._mapSendLocalIdRtpSender.get(localId); - if (!rtpSender) - throw new Error('associated RTCRtpSender not found'); - const oldTrack = rtpSender.track; - await rtpSender.replaceTrack(track); - // Remove the old track from the local stream. - if (oldTrack) - this._sendStream.removeTrack(oldTrack); - // Add the new track to the local stream. - if (track) - this._sendStream.addTrack(track); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + await transceiver.sender.replaceTrack(track); } async setMaxSpatialLayer(localId, spatialLayer) { - this._assertSendDirection(); + this.assertSendDirection(); logger.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer); - const rtpSender = this._mapSendLocalIdRtpSender.get(localId); - if (!rtpSender) - throw new Error('associated RTCRtpSender not found'); - const parameters = rtpSender.getParameters(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + const parameters = transceiver.sender.getParameters(); parameters.encodings.forEach((encoding, idx) => { - if (idx <= spatialLayer) + if (idx <= spatialLayer) { encoding.active = true; - else + } + else { encoding.active = false; + } }); - await rtpSender.setParameters(parameters); + await transceiver.sender.setParameters(parameters); + this._remoteSdp.muxMediaSectionSimulcast(localId, parameters.encodings); + const offer = await this._pc.createOffer(); + logger.debug('setMaxSpatialLayer() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('setMaxSpatialLayer() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); } async setRtpEncodingParameters(localId, params) { - this._assertSendDirection(); + this.assertSendDirection(); logger.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params); - const rtpSender = this._mapSendLocalIdRtpSender.get(localId); - if (!rtpSender) - throw new Error('associated RTCRtpSender not found'); - const parameters = rtpSender.getParameters(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + const parameters = transceiver.sender.getParameters(); parameters.encodings.forEach((encoding, idx) => { - parameters.encodings[idx] = Object.assign(Object.assign({}, encoding), params); + parameters.encodings[idx] = { ...encoding, ...params }; }); - await rtpSender.setParameters(parameters); + await transceiver.sender.setParameters(parameters); + this._remoteSdp.muxMediaSectionSimulcast(localId, parameters.encodings); + const offer = await this._pc.createOffer(); + logger.debug('setRtpEncodingParameters() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('setRtpEncodingParameters() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); } async getSenderStats(localId) { - this._assertSendDirection(); - const rtpSender = this._mapSendLocalIdRtpSender.get(localId); - if (!rtpSender) - throw new Error('associated RTCRtpSender not found'); - return rtpSender.getStats(); + this.assertSendDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + return transceiver.sender.getStats(); } async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { - this._assertSendDirection(); + var _a; + this.assertSendDirection(); const options = { negotiated: true, id: this._nextSendSctpStreamId, @@ -4044,8 +5897,12 @@ class Chrome67 extends HandlerInterface_1.HandlerInterface { const localSdpObject = sdpTransform.parse(offer.sdp); const offerMediaObject = localSdpObject.media .find((m) => m.type === 'application'); - if (!this._transportReady) - await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); + } logger.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); await this._pc.setLocalDescription(offer); this._remoteSdp.sendSctpAssociation({ offerMediaObject }); @@ -4062,71 +5919,109 @@ class Chrome67 extends HandlerInterface_1.HandlerInterface { }; return { dataChannel, sctpStreamParameters }; } - async receive({ trackId, kind, rtpParameters }) { - this._assertRecvDirection(); - logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); - const localId = trackId; - const mid = kind; - this._remoteSdp.receive({ - mid, - kind, - offerRtpParameters: rtpParameters, - streamId: rtpParameters.rtcp.cname, - trackId - }); + async receive(optionsList) { + var _a; + this.assertRecvDirection(); + const results = []; + const mapLocalId = new Map(); + for (const options of optionsList) { + const { trackId, kind, rtpParameters, streamId } = options; + logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const localId = rtpParameters.mid || String(this._mapMidTransceiver.size); + mapLocalId.set(trackId, localId); + this._remoteSdp.receive({ + mid: localId, + kind, + offerRtpParameters: rtpParameters, + streamId: streamId || rtpParameters.rtcp.cname, + trackId + }); + } const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; logger.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); await this._pc.setRemoteDescription(offer); let answer = await this._pc.createAnswer(); const localSdpObject = sdpTransform.parse(answer.sdp); - const answerMediaObject = localSdpObject.media - .find((m) => String(m.mid) === mid); - // May need to modify codec parameters in the answer based on codec - // parameters in the offer. - sdpCommonUtils.applyCodecParameters({ - offerRtpParameters: rtpParameters, - answerMediaObject - }); + for (const options of optionsList) { + const { trackId, rtpParameters } = options; + const localId = mapLocalId.get(trackId); + const answerMediaObject = localSdpObject.media + .find((m) => String(m.mid) === localId); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject + }); + } answer = { type: 'answer', sdp: sdpTransform.write(localSdpObject) }; - if (!this._transportReady) - await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); + } logger.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); await this._pc.setLocalDescription(answer); - const rtpReceiver = this._pc.getReceivers() - .find((r) => r.track && r.track.id === localId); - if (!rtpReceiver) - throw new Error('new RTCRtpReceiver not'); - // Insert into the map. - this._mapRecvLocalIdInfo.set(localId, { mid, rtpParameters, rtpReceiver }); - return { - localId, - track: rtpReceiver.track, - rtpReceiver - }; + for (const options of optionsList) { + const { trackId } = options; + const localId = mapLocalId.get(trackId); + const transceiver = this._pc.getTransceivers() + .find((t) => t.mid === localId); + if (!transceiver) { + throw new Error('new RTCRtpTransceiver not found'); + } + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + results.push({ + localId, + track: transceiver.receiver.track, + rtpReceiver: transceiver.receiver + }); + } + return results; } - async stopReceiving(localId) { - this._assertRecvDirection(); - logger.debug('stopReceiving() [localId:%s]', localId); - const { mid, rtpParameters } = this._mapRecvLocalIdInfo.get(localId) || {}; - // Remove from the map. - this._mapRecvLocalIdInfo.delete(localId); - this._remoteSdp.planBStopReceiving({ mid: mid, offerRtpParameters: rtpParameters }); + async stopReceiving(localIds) { + this.assertRecvDirection(); + for (const localId of localIds) { + logger.debug('stopReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + this._remoteSdp.closeMediaSection(transceiver.mid); + } const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; logger.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); await this._pc.setRemoteDescription(offer); const answer = await this._pc.createAnswer(); logger.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); await this._pc.setLocalDescription(answer); + for (const localId of localIds) { + this._mapMidTransceiver.delete(localId); + } + } + async pauseReceiving( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localIds) { + // Unimplemented. + } + async resumeReceiving( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localIds) { + // Unimplemented. } async getReceiverStats(localId) { - this._assertRecvDirection(); - const { rtpReceiver } = this._mapRecvLocalIdInfo.get(localId) || {}; - if (!rtpReceiver) - throw new Error('associated RTCRtpReceiver not found'); - return rtpReceiver.getStats(); + this.assertRecvDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + return transceiver.receiver.getStats(); } async receiveDataChannel({ sctpStreamParameters, label, protocol }) { - this._assertRecvDirection(); + var _a; + this.assertRecvDirection(); const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; const options = { negotiated: true, @@ -4142,14 +6037,17 @@ class Chrome67 extends HandlerInterface_1.HandlerInterface { // If this is the first DataChannel we need to create the SDP offer with // m=application section. if (!this._hasDataChannelMediaSection) { - this._remoteSdp.receiveSctpAssociation({ oldDataChannelSpec: true }); + this._remoteSdp.receiveSctpAssociation(); const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [offer:%o]', offer); await this._pc.setRemoteDescription(offer); const answer = await this._pc.createAnswer(); if (!this._transportReady) { const localSdpObject = sdpTransform.parse(answer.sdp); - await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); } logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); await this._pc.setLocalDescription(answer); @@ -4157,9 +6055,10 @@ class Chrome67 extends HandlerInterface_1.HandlerInterface { } return { dataChannel }; } - async _setupTransport({ localDtlsRole, localSdpObject }) { - if (!localSdpObject) + async setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) { localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + } // Get our local DTLS parameters. const dtlsParameters = sdpCommonUtils.extractDtlsParameters({ sdpObject: localSdpObject }); // Set our DTLS role. @@ -4167,27 +6066,33 @@ class Chrome67 extends HandlerInterface_1.HandlerInterface { // Update the remote DTLS role in the SDP. this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); // Need to tell the remote transport about our parameters. - await this.safeEmitAsPromise('@connect', { dtlsParameters }); + await new Promise((resolve, reject) => { + this.safeEmit('@connect', { dtlsParameters }, resolve, reject); + }); this._transportReady = true; } - _assertSendDirection() { + assertSendDirection() { if (this._direction !== 'send') { throw new Error('method can just be called for handlers with "send" direction'); } } - _assertRecvDirection() { + assertRecvDirection() { if (this._direction !== 'recv') { throw new Error('method can just be called for handlers with "recv" direction'); } } } -exports.Chrome67 = Chrome67; +exports.Chrome70 = Chrome70; -},{"../Logger":13,"../ortc":36,"../utils":39,"./HandlerInterface":25,"./sdp/RemoteSdp":31,"./sdp/commonUtils":32,"./sdp/planBUtils":33,"sdp-transform":44}],21:[function(require,module,exports){ +},{"../Logger":17,"../ortc":43,"../scalabilityModes":44,"../utils":46,"./HandlerInterface":30,"./sdp/RemoteSdp":38,"./sdp/commonUtils":39,"./sdp/unifiedPlanUtils":41,"sdp-transform":52}],27:[function(require,module,exports){ "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; @@ -4200,24 +6105,31 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.Chrome70 = void 0; +exports.Chrome74 = void 0; const sdpTransform = __importStar(require("sdp-transform")); const Logger_1 = require("../Logger"); const utils = __importStar(require("../utils")); const ortc = __importStar(require("../ortc")); const sdpCommonUtils = __importStar(require("./sdp/commonUtils")); const sdpUnifiedPlanUtils = __importStar(require("./sdp/unifiedPlanUtils")); +const ortcUtils = __importStar(require("./ortc/utils")); const HandlerInterface_1 = require("./HandlerInterface"); const RemoteSdp_1 = require("./sdp/RemoteSdp"); const scalabilityModes_1 = require("../scalabilityModes"); -const logger = new Logger_1.Logger('Chrome70'); +const logger = new Logger_1.Logger('Chrome74'); const SCTP_NUM_STREAMS = { OS: 1024, MIS: 1024 }; -class Chrome70 extends HandlerInterface_1.HandlerInterface { +class Chrome74 extends HandlerInterface_1.HandlerInterface { + /** + * Creates a factory function. + */ + static createFactory() { + return () => new Chrome74(); + } constructor() { super(); // Map of RTCTransceivers indexed by MID. @@ -4231,14 +6143,8 @@ class Chrome70 extends HandlerInterface_1.HandlerInterface { // Got transport local and remote parameters. this._transportReady = false; } - /** - * Creates a factory function. - */ - static createFactory() { - return () => new Chrome70(); - } get name() { - return 'Chrome70'; + return 'Chrome74'; } close() { logger.debug('close()'); @@ -4249,6 +6155,7 @@ class Chrome70 extends HandlerInterface_1.HandlerInterface { } catch (error) { } } + this.emit('@close'); } async getNativeRtpCapabilities() { logger.debug('getNativeRtpCapabilities()'); @@ -4269,6 +6176,8 @@ class Chrome70 extends HandlerInterface_1.HandlerInterface { catch (error) { } const sdpObject = sdpTransform.parse(offer.sdp); const nativeRtpCapabilities = sdpCommonUtils.extractRtpCapabilities({ sdpObject }); + // libwebrtc supports NACK for OPUS but doesn't announce it. + ortcUtils.addNackSuppportForOpus(nativeRtpCapabilities); return nativeRtpCapabilities; } catch (error) { @@ -4304,28 +6213,47 @@ class Chrome70 extends HandlerInterface_1.HandlerInterface { audio: ortc.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), video: ortc.getSendingRemoteRtpParameters('video', extendedRtpCapabilities) }; - this._pc = new RTCPeerConnection(Object.assign({ iceServers: iceServers || [], iceTransportPolicy: iceTransportPolicy || 'all', bundlePolicy: 'max-bundle', rtcpMuxPolicy: 'require', sdpSemantics: 'unified-plan' }, additionalSettings), proprietaryConstraints); - // Handle RTCPeerConnection connection status. - this._pc.addEventListener('iceconnectionstatechange', () => { - switch (this._pc.iceConnectionState) { - case 'checking': - this.emit('@connectionstatechange', 'connecting'); - break; - case 'connected': - case 'completed': - this.emit('@connectionstatechange', 'connected'); - break; - case 'failed': - this.emit('@connectionstatechange', 'failed'); - break; - case 'disconnected': - this.emit('@connectionstatechange', 'disconnected'); - break; - case 'closed': - this.emit('@connectionstatechange', 'closed'); - break; - } - }); + if (dtlsParameters.role && dtlsParameters.role !== 'auto') { + this._forcedLocalDtlsRole = dtlsParameters.role === 'server' + ? 'client' + : 'server'; + } + this._pc = new RTCPeerConnection({ + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'unified-plan', + ...additionalSettings + }, proprietaryConstraints); + if (this._pc.connectionState) { + this._pc.addEventListener('connectionstatechange', () => { + this.emit('@connectionstatechange', this._pc.connectionState); + }); + } + else { + logger.warn('run() | pc.connectionState not supported, using pc.iceConnectionState'); + this._pc.addEventListener('iceconnectionstatechange', () => { + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } } async updateIceServers(iceServers) { logger.debug('updateIceServers()'); @@ -4337,8 +6265,9 @@ class Chrome70 extends HandlerInterface_1.HandlerInterface { logger.debug('restartIce()'); // Provide the remote SDP handler with new remote ICE parameters. this._remoteSdp.updateIceParameters(iceParameters); - if (!this._transportReady) + if (!this._transportReady) { return; + } if (this._direction === 'send') { const offer = await this._pc.createOffer({ iceRestart: true }); logger.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); @@ -4360,8 +6289,14 @@ class Chrome70 extends HandlerInterface_1.HandlerInterface { return this._pc.getStats(); } async send({ track, encodings, codecOptions, codec }) { - this._assertSendDirection(); + var _a; + this.assertSendDirection(); logger.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); + if (encodings && encodings.length > 1) { + encodings.forEach((encoding, idx) => { + encoding.rid = `r${idx}`; + }); + } const sendingRtpParameters = utils.clone(this._sendingRtpParametersByKind[track.kind], {}); // This may throw. sendingRtpParameters.codecs = @@ -4371,25 +6306,23 @@ class Chrome70 extends HandlerInterface_1.HandlerInterface { sendingRemoteRtpParameters.codecs = ortc.reduceCodecs(sendingRemoteRtpParameters.codecs, codec); const mediaSectionIdx = this._remoteSdp.getNextMediaSectionIdx(); - const transceiver = this._pc.addTransceiver(track, { direction: 'sendonly', streams: [this._sendStream] }); - let offer = await this._pc.createOffer(); - let localSdpObject = sdpTransform.parse(offer.sdp); - let offerMediaObject; - if (!this._transportReady) - await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); - if (encodings && encodings.length > 1) { - logger.debug('send() | enabling legacy simulcast'); - localSdpObject = sdpTransform.parse(offer.sdp); - offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; - sdpUnifiedPlanUtils.addLegacySimulcast({ - offerMediaObject, - numStreams: encodings.length + const transceiver = this._pc.addTransceiver(track, { + direction: 'sendonly', + streams: [this._sendStream], + sendEncodings: encodings + }); + let offer = await this._pc.createOffer(); + let localSdpObject = sdpTransform.parse(offer.sdp); + let offerMediaObject; + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject }); - offer = { type: 'offer', sdp: sdpTransform.write(localSdpObject) }; } // Special case for VP9 with SVC. let hackVp9Svc = false; - const layers = scalabilityModes_1.parse((encodings || [{}])[0].scalabilityMode); + const layers = (0, scalabilityModes_1.parse)((encodings || [{}])[0].scalabilityMode); if (encodings && encodings.length === 1 && layers.spatialLayers > 1 && @@ -4406,20 +6339,6 @@ class Chrome70 extends HandlerInterface_1.HandlerInterface { } logger.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); await this._pc.setLocalDescription(offer); - // If encodings are given, apply them now. - if (encodings) { - logger.debug('send() | applying given encodings'); - const parameters = transceiver.sender.getParameters(); - for (let idx = 0; idx < (parameters.encodings || []).length; ++idx) { - const encoding = parameters.encodings[idx]; - const desiredEncoding = encodings[idx]; - // Should not happen but just in case. - if (!desiredEncoding) - break; - parameters.encodings[idx] = Object.assign(encoding, desiredEncoding); - } - await transceiver.sender.setParameters(parameters); - } // We can now get the transceiver.mid. const localId = transceiver.mid; // Set MID. @@ -4429,19 +6348,25 @@ class Chrome70 extends HandlerInterface_1.HandlerInterface { // Set RTCP CNAME. sendingRtpParameters.rtcp.cname = sdpCommonUtils.getCname({ offerMediaObject }); - // Set RTP encodings. - sendingRtpParameters.encodings = - sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject }); - // Complete encodings with given values. - if (encodings) { - for (let idx = 0; idx < sendingRtpParameters.encodings.length; ++idx) { - if (encodings[idx]) - Object.assign(sendingRtpParameters.encodings[idx], encodings[idx]); + // Set RTP encodings by parsing the SDP offer if no encodings are given. + if (!encodings) { + sendingRtpParameters.encodings = + sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject }); + } + // Set RTP encodings by parsing the SDP offer and complete them with given + // one if just a single encoding has been given. + else if (encodings.length === 1) { + let newEncodings = sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject }); + Object.assign(newEncodings[0], encodings[0]); + // Hack for VP9 SVC. + if (hackVp9Svc) { + newEncodings = [newEncodings[0]]; } + sendingRtpParameters.encodings = newEncodings; } - // Hack for VP9 SVC. - if (hackVp9Svc) { - sendingRtpParameters.encodings = [sendingRtpParameters.encodings[0]]; + // Otherwise if more than 1 encoding are given use them verbatim. + else { + sendingRtpParameters.encodings = encodings; } // If VP8 or H264 and there is effective simulcast, add scalabilityMode to // each encoding. @@ -4449,7 +6374,12 @@ class Chrome70 extends HandlerInterface_1.HandlerInterface { (sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8' || sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/h264')) { for (const encoding of sendingRtpParameters.encodings) { - encoding.scalabilityMode = 'S1T3'; + if (encoding.scalabilityMode) { + encoding.scalabilityMode = `L1T${layers.temporalLayers}`; + } + else { + encoding.scalabilityMode = 'L1T3'; + } } } this._remoteSdp.send({ @@ -4457,7 +6387,8 @@ class Chrome70 extends HandlerInterface_1.HandlerInterface { reuseMid: mediaSectionIdx.reuseMid, offerRtpParameters: sendingRtpParameters, answerRtpParameters: sendingRemoteRtpParameters, - codecOptions + codecOptions, + extmapAllowMixed: true }); const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; logger.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer); @@ -4471,23 +6402,63 @@ class Chrome70 extends HandlerInterface_1.HandlerInterface { }; } async stopSending(localId) { - this._assertSendDirection(); + this.assertSendDirection(); logger.debug('stopSending() [localId:%s]', localId); const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) + if (!transceiver) { throw new Error('associated RTCRtpTransceiver not found'); + } transceiver.sender.replaceTrack(null); this._pc.removeTrack(transceiver.sender); - this._remoteSdp.closeMediaSection(transceiver.mid); + const mediaSectionClosed = this._remoteSdp.closeMediaSection(transceiver.mid); + if (mediaSectionClosed) { + try { + transceiver.stop(); + } + catch (error) { } + } const offer = await this._pc.createOffer(); logger.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); await this._pc.setLocalDescription(offer); const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; logger.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); await this._pc.setRemoteDescription(answer); + this._mapMidTransceiver.delete(localId); + } + async pauseSending(localId) { + this.assertSendDirection(); + logger.debug('pauseSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'inactive'; + this._remoteSdp.pauseMediaSection(localId); + const offer = await this._pc.createOffer(); + logger.debug('pauseSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('pauseSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async resumeSending(localId) { + this.assertSendDirection(); + logger.debug('resumeSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + this._remoteSdp.resumeSendingMediaSection(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'sendonly'; + const offer = await this._pc.createOffer(); + logger.debug('resumeSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('resumeSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); } async replaceTrack(localId, track) { - this._assertSendDirection(); + this.assertSendDirection(); if (track) { logger.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id); } @@ -4495,52 +6466,72 @@ class Chrome70 extends HandlerInterface_1.HandlerInterface { logger.debug('replaceTrack() [localId:%s, no track]', localId); } const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) + if (!transceiver) { throw new Error('associated RTCRtpTransceiver not found'); + } await transceiver.sender.replaceTrack(track); } async setMaxSpatialLayer(localId, spatialLayer) { - this._assertSendDirection(); + this.assertSendDirection(); logger.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer); const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) + if (!transceiver) { throw new Error('associated RTCRtpTransceiver not found'); + } const parameters = transceiver.sender.getParameters(); parameters.encodings.forEach((encoding, idx) => { - if (idx <= spatialLayer) + if (idx <= spatialLayer) { encoding.active = true; - else + } + else { encoding.active = false; + } }); await transceiver.sender.setParameters(parameters); + this._remoteSdp.muxMediaSectionSimulcast(localId, parameters.encodings); + const offer = await this._pc.createOffer(); + logger.debug('setMaxSpatialLayer() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('setMaxSpatialLayer() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); } async setRtpEncodingParameters(localId, params) { - this._assertSendDirection(); + this.assertSendDirection(); logger.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params); const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) + if (!transceiver) { throw new Error('associated RTCRtpTransceiver not found'); + } const parameters = transceiver.sender.getParameters(); parameters.encodings.forEach((encoding, idx) => { - parameters.encodings[idx] = Object.assign(Object.assign({}, encoding), params); + parameters.encodings[idx] = { ...encoding, ...params }; }); await transceiver.sender.setParameters(parameters); + this._remoteSdp.muxMediaSectionSimulcast(localId, parameters.encodings); + const offer = await this._pc.createOffer(); + logger.debug('setRtpEncodingParameters() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('setRtpEncodingParameters() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); } async getSenderStats(localId) { - this._assertSendDirection(); + this.assertSendDirection(); const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) + if (!transceiver) { throw new Error('associated RTCRtpTransceiver not found'); + } return transceiver.sender.getStats(); } async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { - this._assertSendDirection(); + var _a; + this.assertSendDirection(); const options = { negotiated: true, id: this._nextSendSctpStreamId, ordered, maxPacketLifeTime, - maxRetransmitTime: maxPacketLifeTime, maxRetransmits, protocol }; @@ -4556,8 +6547,12 @@ class Chrome70 extends HandlerInterface_1.HandlerInterface { const localSdpObject = sdpTransform.parse(offer.sdp); const offerMediaObject = localSdpObject.media .find((m) => m.type === 'application'); - if (!this._transportReady) - await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); + } logger.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); await this._pc.setLocalDescription(offer); this._remoteSdp.sendSctpAssociation({ offerMediaObject }); @@ -4574,77 +6569,143 @@ class Chrome70 extends HandlerInterface_1.HandlerInterface { }; return { dataChannel, sctpStreamParameters }; } - async receive({ trackId, kind, rtpParameters }) { - this._assertRecvDirection(); - logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); - const localId = rtpParameters.mid || String(this._mapMidTransceiver.size); - this._remoteSdp.receive({ - mid: localId, - kind, - offerRtpParameters: rtpParameters, - streamId: rtpParameters.rtcp.cname, - trackId - }); + async receive(optionsList) { + var _a; + this.assertRecvDirection(); + const results = []; + const mapLocalId = new Map(); + for (const options of optionsList) { + const { trackId, kind, rtpParameters, streamId } = options; + logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const localId = rtpParameters.mid || String(this._mapMidTransceiver.size); + mapLocalId.set(trackId, localId); + this._remoteSdp.receive({ + mid: localId, + kind, + offerRtpParameters: rtpParameters, + streamId: streamId || rtpParameters.rtcp.cname, + trackId + }); + } const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; logger.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); await this._pc.setRemoteDescription(offer); let answer = await this._pc.createAnswer(); const localSdpObject = sdpTransform.parse(answer.sdp); - const answerMediaObject = localSdpObject.media - .find((m) => String(m.mid) === localId); - // May need to modify codec parameters in the answer based on codec - // parameters in the offer. - sdpCommonUtils.applyCodecParameters({ - offerRtpParameters: rtpParameters, - answerMediaObject - }); + for (const options of optionsList) { + const { trackId, rtpParameters } = options; + const localId = mapLocalId.get(trackId); + const answerMediaObject = localSdpObject.media + .find((m) => String(m.mid) === localId); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject + }); + } answer = { type: 'answer', sdp: sdpTransform.write(localSdpObject) }; - if (!this._transportReady) - await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); + } logger.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); await this._pc.setLocalDescription(answer); - const transceiver = this._pc.getTransceivers() - .find((t) => t.mid === localId); - if (!transceiver) - throw new Error('new RTCRtpTransceiver not found'); - // Store in the map. - this._mapMidTransceiver.set(localId, transceiver); - return { - localId, - track: transceiver.receiver.track, - rtpReceiver: transceiver.receiver - }; + for (const options of optionsList) { + const { trackId } = options; + const localId = mapLocalId.get(trackId); + const transceiver = this._pc.getTransceivers() + .find((t) => t.mid === localId); + if (!transceiver) { + throw new Error('new RTCRtpTransceiver not found'); + } + else { + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + results.push({ + localId, + track: transceiver.receiver.track, + rtpReceiver: transceiver.receiver + }); + } + } + return results; } - async stopReceiving(localId) { - this._assertRecvDirection(); - logger.debug('stopReceiving() [localId:%s]', localId); - const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) - throw new Error('associated RTCRtpTransceiver not found'); - this._remoteSdp.closeMediaSection(transceiver.mid); + async stopReceiving(localIds) { + this.assertRecvDirection(); + for (const localId of localIds) { + logger.debug('stopReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + this._remoteSdp.closeMediaSection(transceiver.mid); + } const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; logger.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); await this._pc.setRemoteDescription(offer); const answer = await this._pc.createAnswer(); logger.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); await this._pc.setLocalDescription(answer); + for (const localId of localIds) { + this._mapMidTransceiver.delete(localId); + } + } + async pauseReceiving(localIds) { + this.assertRecvDirection(); + for (const localId of localIds) { + logger.debug('pauseReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'inactive'; + this._remoteSdp.pauseMediaSection(localId); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('pauseReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('pauseReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + async resumeReceiving(localIds) { + this.assertRecvDirection(); + for (const localId of localIds) { + logger.debug('resumeReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'recvonly'; + this._remoteSdp.resumeReceivingMediaSection(localId); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('resumeReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('resumeReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); } async getReceiverStats(localId) { - this._assertRecvDirection(); + this.assertRecvDirection(); const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) + if (!transceiver) { throw new Error('associated RTCRtpTransceiver not found'); + } return transceiver.receiver.getStats(); } async receiveDataChannel({ sctpStreamParameters, label, protocol }) { - this._assertRecvDirection(); + var _a; + this.assertRecvDirection(); const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; const options = { negotiated: true, id: streamId, ordered, maxPacketLifeTime, - maxRetransmitTime: maxPacketLifeTime, maxRetransmits, protocol }; @@ -4660,7 +6721,10 @@ class Chrome70 extends HandlerInterface_1.HandlerInterface { const answer = await this._pc.createAnswer(); if (!this._transportReady) { const localSdpObject = sdpTransform.parse(answer.sdp); - await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); } logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); await this._pc.setLocalDescription(answer); @@ -4668,9 +6732,10 @@ class Chrome70 extends HandlerInterface_1.HandlerInterface { } return { dataChannel }; } - async _setupTransport({ localDtlsRole, localSdpObject }) { - if (!localSdpObject) + async setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) { localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + } // Get our local DTLS parameters. const dtlsParameters = sdpCommonUtils.extractDtlsParameters({ sdpObject: localSdpObject }); // Set our DTLS role. @@ -4678,27 +6743,33 @@ class Chrome70 extends HandlerInterface_1.HandlerInterface { // Update the remote DTLS role in the SDP. this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); // Need to tell the remote transport about our parameters. - await this.safeEmitAsPromise('@connect', { dtlsParameters }); + await new Promise((resolve, reject) => { + this.safeEmit('@connect', { dtlsParameters }, resolve, reject); + }); this._transportReady = true; } - _assertSendDirection() { + assertSendDirection() { if (this._direction !== 'send') { throw new Error('method can just be called for handlers with "send" direction'); } } - _assertRecvDirection() { + assertRecvDirection() { if (this._direction !== 'recv') { throw new Error('method can just be called for handlers with "recv" direction'); } } } -exports.Chrome70 = Chrome70; +exports.Chrome74 = Chrome74; -},{"../Logger":13,"../ortc":36,"../scalabilityModes":37,"../utils":39,"./HandlerInterface":25,"./sdp/RemoteSdp":31,"./sdp/commonUtils":32,"./sdp/unifiedPlanUtils":34,"sdp-transform":44}],22:[function(require,module,exports){ +},{"../Logger":17,"../ortc":43,"../scalabilityModes":44,"../utils":46,"./HandlerInterface":30,"./ortc/utils":36,"./sdp/RemoteSdp":38,"./sdp/commonUtils":39,"./sdp/unifiedPlanUtils":41,"sdp-transform":52}],28:[function(require,module,exports){ "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; @@ -4711,494 +6782,443 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.Chrome74 = void 0; -const sdpTransform = __importStar(require("sdp-transform")); +exports.Edge11 = void 0; const Logger_1 = require("../Logger"); +const errors_1 = require("../errors"); const utils = __importStar(require("../utils")); const ortc = __importStar(require("../ortc")); -const sdpCommonUtils = __importStar(require("./sdp/commonUtils")); -const sdpUnifiedPlanUtils = __importStar(require("./sdp/unifiedPlanUtils")); +const edgeUtils = __importStar(require("./ortc/edgeUtils")); const HandlerInterface_1 = require("./HandlerInterface"); -const RemoteSdp_1 = require("./sdp/RemoteSdp"); -const scalabilityModes_1 = require("../scalabilityModes"); -const logger = new Logger_1.Logger('Chrome74'); -const SCTP_NUM_STREAMS = { OS: 1024, MIS: 1024 }; -class Chrome74 extends HandlerInterface_1.HandlerInterface { - constructor() { - super(); - // Map of RTCTransceivers indexed by MID. - this._mapMidTransceiver = new Map(); - // Local stream for sending. - this._sendStream = new MediaStream(); - // Whether a DataChannel m=application section has been created. - this._hasDataChannelMediaSection = false; - // Sending DataChannel id value counter. Incremented for each new DataChannel. - this._nextSendSctpStreamId = 0; - // Got transport local and remote parameters. - this._transportReady = false; - } +const logger = new Logger_1.Logger('Edge11'); +class Edge11 extends HandlerInterface_1.HandlerInterface { /** * Creates a factory function. */ static createFactory() { - return () => new Chrome74(); + return () => new Edge11(); + } + constructor() { + super(); + // Map of RTCRtpSenders indexed by id. + this._rtpSenders = new Map(); + // Map of RTCRtpReceivers indexed by id. + this._rtpReceivers = new Map(); + // Next localId for sending tracks. + this._nextSendLocalId = 0; + // Got transport local and remote parameters. + this._transportReady = false; } get name() { - return 'Chrome74'; + return 'Edge11'; } close() { logger.debug('close()'); - // Close RTCPeerConnection. - if (this._pc) { - try { - this._pc.close(); - } - catch (error) { } + // Close the ICE gatherer. + // NOTE: Not yet implemented by Edge. + try { + this._iceGatherer.close(); } - } - async getNativeRtpCapabilities() { - logger.debug('getNativeRtpCapabilities()'); - const pc = new RTCPeerConnection({ - iceServers: [], - iceTransportPolicy: 'all', - bundlePolicy: 'max-bundle', - rtcpMuxPolicy: 'require', - sdpSemantics: 'unified-plan' - }); + catch (error) { } + // Close the ICE transport. try { - pc.addTransceiver('audio'); - pc.addTransceiver('video'); - const offer = await pc.createOffer(); + this._iceTransport.stop(); + } + catch (error) { } + // Close the DTLS transport. + try { + this._dtlsTransport.stop(); + } + catch (error) { } + // Close RTCRtpSenders. + for (const rtpSender of this._rtpSenders.values()) { try { - pc.close(); + rtpSender.stop(); } catch (error) { } - const sdpObject = sdpTransform.parse(offer.sdp); - const nativeRtpCapabilities = sdpCommonUtils.extractRtpCapabilities({ sdpObject }); - return nativeRtpCapabilities; } - catch (error) { + // Close RTCRtpReceivers. + for (const rtpReceiver of this._rtpReceivers.values()) { try { - pc.close(); + rtpReceiver.stop(); } - catch (error2) { } - throw error; + catch (error) { } } + this.emit('@close'); + } + async getNativeRtpCapabilities() { + logger.debug('getNativeRtpCapabilities()'); + return edgeUtils.getCapabilities(); } async getNativeSctpCapabilities() { logger.debug('getNativeSctpCapabilities()'); return { - numStreams: SCTP_NUM_STREAMS + numStreams: { OS: 0, MIS: 0 } }; } - run({ direction, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, extendedRtpCapabilities }) { + run({ direction, // eslint-disable-line @typescript-eslint/no-unused-vars + iceParameters, iceCandidates, dtlsParameters, sctpParameters, // eslint-disable-line @typescript-eslint/no-unused-vars + iceServers, iceTransportPolicy, additionalSettings, // eslint-disable-line @typescript-eslint/no-unused-vars + proprietaryConstraints, // eslint-disable-line @typescript-eslint/no-unused-vars + extendedRtpCapabilities }) { logger.debug('run()'); - this._direction = direction; - this._remoteSdp = new RemoteSdp_1.RemoteSdp({ - iceParameters, - iceCandidates, - dtlsParameters, - sctpParameters - }); this._sendingRtpParametersByKind = - { - audio: ortc.getSendingRtpParameters('audio', extendedRtpCapabilities), - video: ortc.getSendingRtpParameters('video', extendedRtpCapabilities) - }; - this._sendingRemoteRtpParametersByKind = - { - audio: ortc.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), - video: ortc.getSendingRemoteRtpParameters('video', extendedRtpCapabilities) + { + audio: ortc.getSendingRtpParameters('audio', extendedRtpCapabilities), + video: ortc.getSendingRtpParameters('video', extendedRtpCapabilities) }; - this._pc = new RTCPeerConnection(Object.assign({ iceServers: iceServers || [], iceTransportPolicy: iceTransportPolicy || 'all', bundlePolicy: 'max-bundle', rtcpMuxPolicy: 'require', sdpSemantics: 'unified-plan' }, additionalSettings), proprietaryConstraints); - // Handle RTCPeerConnection connection status. - this._pc.addEventListener('iceconnectionstatechange', () => { - switch (this._pc.iceConnectionState) { - case 'checking': - this.emit('@connectionstatechange', 'connecting'); - break; - case 'connected': - case 'completed': - this.emit('@connectionstatechange', 'connected'); - break; - case 'failed': - this.emit('@connectionstatechange', 'failed'); - break; - case 'disconnected': - this.emit('@connectionstatechange', 'disconnected'); - break; - case 'closed': - this.emit('@connectionstatechange', 'closed'); - break; - } - }); + this._remoteIceParameters = iceParameters; + this._remoteIceCandidates = iceCandidates; + this._remoteDtlsParameters = dtlsParameters; + this._cname = `CNAME-${utils.generateRandomNumber()}`; + this.setIceGatherer({ iceServers, iceTransportPolicy }); + this.setIceTransport(); + this.setDtlsTransport(); } + // eslint-disable-next-line @typescript-eslint/no-unused-vars async updateIceServers(iceServers) { - logger.debug('updateIceServers()'); - const configuration = this._pc.getConfiguration(); - configuration.iceServers = iceServers; - this._pc.setConfiguration(configuration); + // NOTE: Edge 11 does not implement iceGatherer.gater(). + throw new errors_1.UnsupportedError('not supported'); } async restartIce(iceParameters) { logger.debug('restartIce()'); - // Provide the remote SDP handler with new remote ICE parameters. - this._remoteSdp.updateIceParameters(iceParameters); - if (!this._transportReady) + this._remoteIceParameters = iceParameters; + if (!this._transportReady) { return; - if (this._direction === 'send') { - const offer = await this._pc.createOffer({ iceRestart: true }); - logger.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); - await this._pc.setLocalDescription(offer); - const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; - logger.debug('restartIce() | calling pc.setRemoteDescription() [answer:%o]', answer); - await this._pc.setRemoteDescription(answer); } - else { - const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; - logger.debug('restartIce() | calling pc.setRemoteDescription() [offer:%o]', offer); - await this._pc.setRemoteDescription(offer); - const answer = await this._pc.createAnswer(); - logger.debug('restartIce() | calling pc.setLocalDescription() [answer:%o]', answer); - await this._pc.setLocalDescription(answer); + logger.debug('restartIce() | calling iceTransport.start()'); + this._iceTransport.start(this._iceGatherer, iceParameters, 'controlling'); + for (const candidate of this._remoteIceCandidates) { + this._iceTransport.addRemoteCandidate(candidate); } + this._iceTransport.addRemoteCandidate({}); } async getTransportStats() { - return this._pc.getStats(); + return this._iceTransport.getStats(); } - async send({ track, encodings, codecOptions, codec }) { - this._assertSendDirection(); + async send( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + { track, encodings, codecOptions, codec }) { logger.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); - if (encodings && encodings.length > 1) { - encodings.forEach((encoding, idx) => { - encoding.rid = `r${idx}`; - }); + if (!this._transportReady) { + await this.setupTransport({ localDtlsRole: 'server' }); } - const sendingRtpParameters = utils.clone(this._sendingRtpParametersByKind[track.kind], {}); - // This may throw. - sendingRtpParameters.codecs = - ortc.reduceCodecs(sendingRtpParameters.codecs, codec); - const sendingRemoteRtpParameters = utils.clone(this._sendingRemoteRtpParametersByKind[track.kind], {}); - // This may throw. - sendingRemoteRtpParameters.codecs = - ortc.reduceCodecs(sendingRemoteRtpParameters.codecs, codec); - const mediaSectionIdx = this._remoteSdp.getNextMediaSectionIdx(); - const transceiver = this._pc.addTransceiver(track, { - direction: 'sendonly', - streams: [this._sendStream], - sendEncodings: encodings - }); - let offer = await this._pc.createOffer(); - let localSdpObject = sdpTransform.parse(offer.sdp); - let offerMediaObject; - if (!this._transportReady) - await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); - // Special case for VP9 with SVC. - let hackVp9Svc = false; - const layers = scalabilityModes_1.parse((encodings || [{}])[0].scalabilityMode); - if (encodings && - encodings.length === 1 && - layers.spatialLayers > 1 && - sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp9') { - logger.debug('send() | enabling legacy simulcast for VP9 SVC'); - hackVp9Svc = true; - localSdpObject = sdpTransform.parse(offer.sdp); - offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; - sdpUnifiedPlanUtils.addLegacySimulcast({ - offerMediaObject, - numStreams: layers.spatialLayers - }); - offer = { type: 'offer', sdp: sdpTransform.write(localSdpObject) }; - } - logger.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); - await this._pc.setLocalDescription(offer); - // We can now get the transceiver.mid. - const localId = transceiver.mid; - // Set MID. - sendingRtpParameters.mid = localId; - localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); - offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; - // Set RTCP CNAME. - sendingRtpParameters.rtcp.cname = - sdpCommonUtils.getCname({ offerMediaObject }); - // Set RTP encodings by parsing the SDP offer if no encodings are given. + logger.debug('send() | calling new RTCRtpSender()'); + const rtpSender = new RTCRtpSender(track, this._dtlsTransport); + const rtpParameters = utils.clone(this._sendingRtpParametersByKind[track.kind], {}); + rtpParameters.codecs = ortc.reduceCodecs(rtpParameters.codecs, codec); + const useRtx = rtpParameters.codecs + .some((_codec) => /.+\/rtx$/i.test(_codec.mimeType)); if (!encodings) { - sendingRtpParameters.encodings = - sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject }); - } - // Set RTP encodings by parsing the SDP offer and complete them with given - // one if just a single encoding has been given. - else if (encodings.length === 1) { - let newEncodings = sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject }); - Object.assign(newEncodings[0], encodings[0]); - // Hack for VP9 SVC. - if (hackVp9Svc) - newEncodings = [newEncodings[0]]; - sendingRtpParameters.encodings = newEncodings; - } - // Otherwise if more than 1 encoding are given use them verbatim. - else { - sendingRtpParameters.encodings = encodings; + encodings = [{}]; } - // If VP8 or H264 and there is effective simulcast, add scalabilityMode to - // each encoding. - if (sendingRtpParameters.encodings.length > 1 && - (sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8' || - sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/h264')) { - for (const encoding of sendingRtpParameters.encodings) { - encoding.scalabilityMode = 'S1T3'; + for (const encoding of encodings) { + encoding.ssrc = utils.generateRandomNumber(); + if (useRtx) { + encoding.rtx = { ssrc: utils.generateRandomNumber() }; } } - this._remoteSdp.send({ - offerMediaObject, - reuseMid: mediaSectionIdx.reuseMid, - offerRtpParameters: sendingRtpParameters, - answerRtpParameters: sendingRemoteRtpParameters, - codecOptions, - extmapAllowMixed: true - }); - const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; - logger.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer); - await this._pc.setRemoteDescription(answer); - // Store in the map. - this._mapMidTransceiver.set(localId, transceiver); - return { - localId, - rtpParameters: sendingRtpParameters, - rtpSender: transceiver.sender - }; + rtpParameters.encodings = encodings; + // Fill RTCRtpParameters.rtcp. + rtpParameters.rtcp = + { + cname: this._cname, + reducedSize: true, + mux: true + }; + // NOTE: Convert our standard RTCRtpParameters into those that Edge + // expects. + const edgeRtpParameters = edgeUtils.mangleRtpParameters(rtpParameters); + logger.debug('send() | calling rtpSender.send() [params:%o]', edgeRtpParameters); + await rtpSender.send(edgeRtpParameters); + const localId = String(this._nextSendLocalId); + this._nextSendLocalId++; + // Store it. + this._rtpSenders.set(localId, rtpSender); + return { localId, rtpParameters, rtpSender }; } async stopSending(localId) { - this._assertSendDirection(); logger.debug('stopSending() [localId:%s]', localId); - const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) - throw new Error('associated RTCRtpTransceiver not found'); - transceiver.sender.replaceTrack(null); - this._pc.removeTrack(transceiver.sender); - this._remoteSdp.closeMediaSection(transceiver.mid); - const offer = await this._pc.createOffer(); - logger.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); - await this._pc.setLocalDescription(offer); - const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; - logger.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); - await this._pc.setRemoteDescription(answer); + const rtpSender = this._rtpSenders.get(localId); + if (!rtpSender) { + throw new Error('RTCRtpSender not found'); + } + this._rtpSenders.delete(localId); + try { + logger.debug('stopSending() | calling rtpSender.stop()'); + rtpSender.stop(); + } + catch (error) { + logger.warn('stopSending() | rtpSender.stop() failed:%o', error); + throw error; + } + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async pauseSending(localId) { + // Unimplemented. + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async resumeSending(localId) { + // Unimplemented. } async replaceTrack(localId, track) { - this._assertSendDirection(); if (track) { logger.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id); } else { logger.debug('replaceTrack() [localId:%s, no track]', localId); } - const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) - throw new Error('associated RTCRtpTransceiver not found'); - await transceiver.sender.replaceTrack(track); + const rtpSender = this._rtpSenders.get(localId); + if (!rtpSender) { + throw new Error('RTCRtpSender not found'); + } + rtpSender.setTrack(track); } async setMaxSpatialLayer(localId, spatialLayer) { - this._assertSendDirection(); logger.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer); - const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) - throw new Error('associated RTCRtpTransceiver not found'); - const parameters = transceiver.sender.getParameters(); - parameters.encodings.forEach((encoding, idx) => { - if (idx <= spatialLayer) + const rtpSender = this._rtpSenders.get(localId); + if (!rtpSender) { + throw new Error('RTCRtpSender not found'); + } + const parameters = rtpSender.getParameters(); + parameters.encodings + .forEach((encoding, idx) => { + if (idx <= spatialLayer) { encoding.active = true; - else + } + else { encoding.active = false; + } }); - await transceiver.sender.setParameters(parameters); + await rtpSender.setParameters(parameters); } async setRtpEncodingParameters(localId, params) { - this._assertSendDirection(); logger.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params); - const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) - throw new Error('associated RTCRtpTransceiver not found'); - const parameters = transceiver.sender.getParameters(); + const rtpSender = this._rtpSenders.get(localId); + if (!rtpSender) { + throw new Error('RTCRtpSender not found'); + } + const parameters = rtpSender.getParameters(); parameters.encodings.forEach((encoding, idx) => { - parameters.encodings[idx] = Object.assign(Object.assign({}, encoding), params); + parameters.encodings[idx] = { ...encoding, ...params }; }); - await transceiver.sender.setParameters(parameters); + await rtpSender.setParameters(parameters); + } + async getSenderStats(localId) { + const rtpSender = this._rtpSenders.get(localId); + if (!rtpSender) { + throw new Error('RTCRtpSender not found'); + } + return rtpSender.getStats(); + } + async sendDataChannel( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + options) { + throw new errors_1.UnsupportedError('not implemented'); + } + async receive(optionsList) { + const results = []; + for (const options of optionsList) { + const { trackId, kind } = options; + logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + } + if (!this._transportReady) { + await this.setupTransport({ localDtlsRole: 'server' }); + } + for (const options of optionsList) { + const { trackId, kind, rtpParameters } = options; + logger.debug('receive() | calling new RTCRtpReceiver()'); + const rtpReceiver = new RTCRtpReceiver(this._dtlsTransport, kind); + rtpReceiver.addEventListener('error', (event) => { + logger.error('rtpReceiver "error" event [event:%o]', event); + }); + // NOTE: Convert our standard RTCRtpParameters into those that Edge + // expects. + const edgeRtpParameters = edgeUtils.mangleRtpParameters(rtpParameters); + logger.debug('receive() | calling rtpReceiver.receive() [params:%o]', edgeRtpParameters); + await rtpReceiver.receive(edgeRtpParameters); + const localId = trackId; + // Store it. + this._rtpReceivers.set(localId, rtpReceiver); + results.push({ + localId, + track: rtpReceiver.track, + rtpReceiver + }); + } + return results; + } + async stopReceiving(localIds) { + for (const localId of localIds) { + logger.debug('stopReceiving() [localId:%s]', localId); + const rtpReceiver = this._rtpReceivers.get(localId); + if (!rtpReceiver) { + throw new Error('RTCRtpReceiver not found'); + } + this._rtpReceivers.delete(localId); + try { + logger.debug('stopReceiving() | calling rtpReceiver.stop()'); + rtpReceiver.stop(); + } + catch (error) { + logger.warn('stopReceiving() | rtpReceiver.stop() failed:%o', error); + } + } } - async getSenderStats(localId) { - this._assertSendDirection(); - const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) - throw new Error('associated RTCRtpTransceiver not found'); - return transceiver.sender.getStats(); + async pauseReceiving( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localIds) { + // Unimplemented. } - async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { - this._assertSendDirection(); - const options = { - negotiated: true, - id: this._nextSendSctpStreamId, - ordered, - maxPacketLifeTime, - maxRetransmits, - protocol - }; - logger.debug('sendDataChannel() [options:%o]', options); - const dataChannel = this._pc.createDataChannel(label, options); - // Increase next id. - this._nextSendSctpStreamId = - ++this._nextSendSctpStreamId % SCTP_NUM_STREAMS.MIS; - // If this is the first DataChannel we need to create the SDP answer with - // m=application section. - if (!this._hasDataChannelMediaSection) { - const offer = await this._pc.createOffer(); - const localSdpObject = sdpTransform.parse(offer.sdp); - const offerMediaObject = localSdpObject.media - .find((m) => m.type === 'application'); - if (!this._transportReady) - await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); - logger.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); - await this._pc.setLocalDescription(offer); - this._remoteSdp.sendSctpAssociation({ offerMediaObject }); - const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; - logger.debug('sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); - await this._pc.setRemoteDescription(answer); - this._hasDataChannelMediaSection = true; + async resumeReceiving( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localIds) { + // Unimplemented. + } + async getReceiverStats(localId) { + const rtpReceiver = this._rtpReceivers.get(localId); + if (!rtpReceiver) { + throw new Error('RTCRtpReceiver not found'); } - const sctpStreamParameters = { - streamId: options.id, - ordered: options.ordered, - maxPacketLifeTime: options.maxPacketLifeTime, - maxRetransmits: options.maxRetransmits - }; - return { dataChannel, sctpStreamParameters }; + return rtpReceiver.getStats(); } - async receive({ trackId, kind, rtpParameters }) { - this._assertRecvDirection(); - logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); - const localId = rtpParameters.mid || String(this._mapMidTransceiver.size); - this._remoteSdp.receive({ - mid: localId, - kind, - offerRtpParameters: rtpParameters, - streamId: rtpParameters.rtcp.cname, - trackId + async receiveDataChannel( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + options) { + throw new errors_1.UnsupportedError('not implemented'); + } + setIceGatherer({ iceServers, iceTransportPolicy }) { + // @ts-ignore + const iceGatherer = new RTCIceGatherer({ + iceServers: iceServers || [], + gatherPolicy: iceTransportPolicy || 'all' }); - const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; - logger.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); - await this._pc.setRemoteDescription(offer); - let answer = await this._pc.createAnswer(); - const localSdpObject = sdpTransform.parse(answer.sdp); - const answerMediaObject = localSdpObject.media - .find((m) => String(m.mid) === localId); - // May need to modify codec parameters in the answer based on codec - // parameters in the offer. - sdpCommonUtils.applyCodecParameters({ - offerRtpParameters: rtpParameters, - answerMediaObject + iceGatherer.addEventListener('error', (event) => { + logger.error('iceGatherer "error" event [event:%o]', event); }); - answer = { type: 'answer', sdp: sdpTransform.write(localSdpObject) }; - if (!this._transportReady) - await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); - logger.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); - await this._pc.setLocalDescription(answer); - const transceiver = this._pc.getTransceivers() - .find((t) => t.mid === localId); - if (!transceiver) - throw new Error('new RTCRtpTransceiver not found'); - // Store in the map. - this._mapMidTransceiver.set(localId, transceiver); - return { - localId, - track: transceiver.receiver.track, - rtpReceiver: transceiver.receiver - }; - } - async stopReceiving(localId) { - this._assertRecvDirection(); - logger.debug('stopReceiving() [localId:%s]', localId); - const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) - throw new Error('associated RTCRtpTransceiver not found'); - this._remoteSdp.closeMediaSection(transceiver.mid); - const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; - logger.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); - await this._pc.setRemoteDescription(offer); - const answer = await this._pc.createAnswer(); - logger.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); - await this._pc.setLocalDescription(answer); + // NOTE: Not yet implemented by Edge, which starts gathering automatically. + try { + iceGatherer.gather(); + } + catch (error) { + logger.debug('setIceGatherer() | iceGatherer.gather() failed: %s', error.toString()); + } + this._iceGatherer = iceGatherer; } - async getReceiverStats(localId) { - this._assertRecvDirection(); - const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) - throw new Error('associated RTCRtpTransceiver not found'); - return transceiver.receiver.getStats(); + setIceTransport() { + const iceTransport = new RTCIceTransport(this._iceGatherer); + // NOTE: Not yet implemented by Edge. + iceTransport.addEventListener('statechange', () => { + switch (iceTransport.state) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + // NOTE: Not standard, but implemented by Edge. + iceTransport.addEventListener('icestatechange', () => { + switch (iceTransport.state) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + iceTransport.addEventListener('candidatepairchange', (event) => { + logger.debug('iceTransport "candidatepairchange" event [pair:%o]', event.pair); + }); + this._iceTransport = iceTransport; } - async receiveDataChannel({ sctpStreamParameters, label, protocol }) { - this._assertRecvDirection(); - const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; - const options = { - negotiated: true, - id: streamId, - ordered, - maxPacketLifeTime, - maxRetransmits, - protocol - }; - logger.debug('receiveDataChannel() [options:%o]', options); - const dataChannel = this._pc.createDataChannel(label, options); - // If this is the first DataChannel we need to create the SDP offer with - // m=application section. - if (!this._hasDataChannelMediaSection) { - this._remoteSdp.receiveSctpAssociation(); - const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; - logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [offer:%o]', offer); - await this._pc.setRemoteDescription(offer); - const answer = await this._pc.createAnswer(); - if (!this._transportReady) { - const localSdpObject = sdpTransform.parse(answer.sdp); - await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + setDtlsTransport() { + const dtlsTransport = new RTCDtlsTransport(this._iceTransport); + // NOTE: Not yet implemented by Edge. + dtlsTransport.addEventListener('statechange', () => { + logger.debug('dtlsTransport "statechange" event [state:%s]', dtlsTransport.state); + }); + // NOTE: Not standard, but implemented by Edge. + dtlsTransport.addEventListener('dtlsstatechange', () => { + logger.debug('dtlsTransport "dtlsstatechange" event [state:%s]', dtlsTransport.state); + if (dtlsTransport.state === 'closed') { + this.emit('@connectionstatechange', 'closed'); } - logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); - await this._pc.setLocalDescription(answer); - this._hasDataChannelMediaSection = true; - } - return { dataChannel }; + }); + dtlsTransport.addEventListener('error', (event) => { + logger.error('dtlsTransport "error" event [event:%o]', event); + }); + this._dtlsTransport = dtlsTransport; } - async _setupTransport({ localDtlsRole, localSdpObject }) { - if (!localSdpObject) - localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + async setupTransport({ localDtlsRole }) { + logger.debug('setupTransport()'); // Get our local DTLS parameters. - const dtlsParameters = sdpCommonUtils.extractDtlsParameters({ sdpObject: localSdpObject }); - // Set our DTLS role. + const dtlsParameters = this._dtlsTransport.getLocalParameters(); dtlsParameters.role = localDtlsRole; - // Update the remote DTLS role in the SDP. - this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); // Need to tell the remote transport about our parameters. - await this.safeEmitAsPromise('@connect', { dtlsParameters }); - this._transportReady = true; - } - _assertSendDirection() { - if (this._direction !== 'send') { - throw new Error('method can just be called for handlers with "send" direction'); - } - } - _assertRecvDirection() { - if (this._direction !== 'recv') { - throw new Error('method can just be called for handlers with "recv" direction'); + await new Promise((resolve, reject) => { + this.safeEmit('@connect', { dtlsParameters }, resolve, reject); + }); + // Start the RTCIceTransport. + this._iceTransport.start(this._iceGatherer, this._remoteIceParameters, 'controlling'); + // Add remote ICE candidates. + for (const candidate of this._remoteIceCandidates) { + this._iceTransport.addRemoteCandidate(candidate); } + // Also signal a 'complete' candidate as per spec. + // NOTE: It should be {complete: true} but Edge prefers {}. + // NOTE: If we don't signal end of candidates, the Edge RTCIceTransport + // won't enter the 'completed' state. + this._iceTransport.addRemoteCandidate({}); + // NOTE: Edge does not like SHA less than 256. + this._remoteDtlsParameters.fingerprints = this._remoteDtlsParameters.fingerprints + .filter((fingerprint) => { + return (fingerprint.algorithm === 'sha-256' || + fingerprint.algorithm === 'sha-384' || + fingerprint.algorithm === 'sha-512'); + }); + // Start the RTCDtlsTransport. + this._dtlsTransport.start(this._remoteDtlsParameters); + this._transportReady = true; } } -exports.Chrome74 = Chrome74; +exports.Edge11 = Edge11; -},{"../Logger":13,"../ortc":36,"../scalabilityModes":37,"../utils":39,"./HandlerInterface":25,"./sdp/RemoteSdp":31,"./sdp/commonUtils":32,"./sdp/unifiedPlanUtils":34,"sdp-transform":44}],23:[function(require,module,exports){ +},{"../Logger":17,"../errors":22,"../ortc":43,"../utils":46,"./HandlerInterface":30,"./ortc/edgeUtils":35}],29:[function(require,module,exports){ "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; @@ -5211,392 +7231,689 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.Edge11 = void 0; +exports.Firefox60 = void 0; +const sdpTransform = __importStar(require("sdp-transform")); const Logger_1 = require("../Logger"); const errors_1 = require("../errors"); const utils = __importStar(require("../utils")); const ortc = __importStar(require("../ortc")); -const edgeUtils = __importStar(require("./ortc/edgeUtils")); +const sdpCommonUtils = __importStar(require("./sdp/commonUtils")); +const sdpUnifiedPlanUtils = __importStar(require("./sdp/unifiedPlanUtils")); const HandlerInterface_1 = require("./HandlerInterface"); -const logger = new Logger_1.Logger('Edge11'); -class Edge11 extends HandlerInterface_1.HandlerInterface { - constructor() { - super(); - // Map of RTCRtpSenders indexed by id. - this._rtpSenders = new Map(); - // Map of RTCRtpReceivers indexed by id. - this._rtpReceivers = new Map(); - // Next localId for sending tracks. - this._nextSendLocalId = 0; - // Got transport local and remote parameters. - this._transportReady = false; - } +const RemoteSdp_1 = require("./sdp/RemoteSdp"); +const scalabilityModes_1 = require("../scalabilityModes"); +const logger = new Logger_1.Logger('Firefox60'); +const SCTP_NUM_STREAMS = { OS: 16, MIS: 2048 }; +class Firefox60 extends HandlerInterface_1.HandlerInterface { /** * Creates a factory function. */ static createFactory() { - return () => new Edge11(); + return () => new Firefox60(); + } + constructor() { + super(); + // Map of RTCTransceivers indexed by MID. + this._mapMidTransceiver = new Map(); + // Local stream for sending. + this._sendStream = new MediaStream(); + // Whether a DataChannel m=application section has been created. + this._hasDataChannelMediaSection = false; + // Sending DataChannel id value counter. Incremented for each new DataChannel. + this._nextSendSctpStreamId = 0; + // Got transport local and remote parameters. + this._transportReady = false; } get name() { - return 'Edge11'; + return 'Firefox60'; } close() { logger.debug('close()'); - // Close the ICE gatherer. - // NOTE: Not yet implemented by Edge. - try { - this._iceGatherer.close(); - } - catch (error) { } - // Close the ICE transport. - try { - this._iceTransport.stop(); + // Close RTCPeerConnection. + if (this._pc) { + try { + this._pc.close(); + } + catch (error) { } } - catch (error) { } - // Close the DTLS transport. + this.emit('@close'); + } + async getNativeRtpCapabilities() { + logger.debug('getNativeRtpCapabilities()'); + const pc = new RTCPeerConnection({ + iceServers: [], + iceTransportPolicy: 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require' + }); + // NOTE: We need to add a real video track to get the RID extension mapping. + const canvas = document.createElement('canvas'); + // NOTE: Otherwise Firefox fails in next line. + canvas.getContext('2d'); + const fakeStream = canvas.captureStream(); + const fakeVideoTrack = fakeStream.getVideoTracks()[0]; try { - this._dtlsTransport.stop(); - } - catch (error) { } - // Close RTCRtpSenders. - for (const rtpSender of this._rtpSenders.values()) { + pc.addTransceiver('audio', { direction: 'sendrecv' }); + const videoTransceiver = pc.addTransceiver(fakeVideoTrack, { direction: 'sendrecv' }); + const parameters = videoTransceiver.sender.getParameters(); + const encodings = [ + { rid: 'r0', maxBitrate: 100000 }, + { rid: 'r1', maxBitrate: 500000 } + ]; + parameters.encodings = encodings; + await videoTransceiver.sender.setParameters(parameters); + const offer = await pc.createOffer(); try { - rtpSender.stop(); + canvas.remove(); } catch (error) { } - } - // Close RTCRtpReceivers. - for (const rtpReceiver of this._rtpReceivers.values()) { try { - rtpReceiver.stop(); + fakeVideoTrack.stop(); + } + catch (error) { } + try { + pc.close(); } catch (error) { } + const sdpObject = sdpTransform.parse(offer.sdp); + const nativeRtpCapabilities = sdpCommonUtils.extractRtpCapabilities({ sdpObject }); + return nativeRtpCapabilities; + } + catch (error) { + try { + canvas.remove(); + } + catch (error2) { } + try { + fakeVideoTrack.stop(); + } + catch (error2) { } + try { + pc.close(); + } + catch (error2) { } + throw error; } - } - async getNativeRtpCapabilities() { - logger.debug('getNativeRtpCapabilities()'); - return edgeUtils.getCapabilities(); } async getNativeSctpCapabilities() { logger.debug('getNativeSctpCapabilities()'); return { - numStreams: { OS: 0, MIS: 0 } + numStreams: SCTP_NUM_STREAMS }; } - run({ direction, // eslint-disable-line @typescript-eslint/no-unused-vars - iceParameters, iceCandidates, dtlsParameters, sctpParameters, // eslint-disable-line @typescript-eslint/no-unused-vars - iceServers, iceTransportPolicy, additionalSettings, // eslint-disable-line @typescript-eslint/no-unused-vars - proprietaryConstraints, // eslint-disable-line @typescript-eslint/no-unused-vars - extendedRtpCapabilities }) { + run({ direction, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, extendedRtpCapabilities }) { logger.debug('run()'); + this._direction = direction; + this._remoteSdp = new RemoteSdp_1.RemoteSdp({ + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters + }); this._sendingRtpParametersByKind = { audio: ortc.getSendingRtpParameters('audio', extendedRtpCapabilities), video: ortc.getSendingRtpParameters('video', extendedRtpCapabilities) }; - this._remoteIceParameters = iceParameters; - this._remoteIceCandidates = iceCandidates; - this._remoteDtlsParameters = dtlsParameters; - this._cname = `CNAME-${utils.generateRandomNumber()}`; - this._setIceGatherer({ iceServers, iceTransportPolicy }); - this._setIceTransport(); - this._setDtlsTransport(); + this._sendingRemoteRtpParametersByKind = + { + audio: ortc.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), + video: ortc.getSendingRemoteRtpParameters('video', extendedRtpCapabilities) + }; + this._pc = new RTCPeerConnection({ + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + ...additionalSettings + }, proprietaryConstraints); + if (this._pc.connectionState) { + this._pc.addEventListener('connectionstatechange', () => { + this.emit('@connectionstatechange', this._pc.connectionState); + }); + } + else { + this._pc.addEventListener('iceconnectionstatechange', () => { + logger.warn('run() | pc.connectionState not supported, using pc.iceConnectionState'); + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } } // eslint-disable-next-line @typescript-eslint/no-unused-vars async updateIceServers(iceServers) { - // NOTE: Edge 11 does not implement iceGatherer.gater(). + // NOTE: Firefox does not implement pc.setConfiguration(). throw new errors_1.UnsupportedError('not supported'); } async restartIce(iceParameters) { logger.debug('restartIce()'); - this._remoteIceParameters = iceParameters; - if (!this._transportReady) + // Provide the remote SDP handler with new remote ICE parameters. + this._remoteSdp.updateIceParameters(iceParameters); + if (!this._transportReady) { return; - logger.debug('restartIce() | calling iceTransport.start()'); - this._iceTransport.start(this._iceGatherer, iceParameters, 'controlling'); - for (const candidate of this._remoteIceCandidates) { - this._iceTransport.addRemoteCandidate(candidate); } - this._iceTransport.addRemoteCandidate({}); + if (this._direction === 'send') { + const offer = await this._pc.createOffer({ iceRestart: true }); + logger.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('restartIce() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + else { + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('restartIce() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('restartIce() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } } async getTransportStats() { - return this._iceTransport.getStats(); + return this._pc.getStats(); } - async send( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - { track, encodings, codecOptions, codec }) { + async send({ track, encodings, codecOptions, codec }) { + this.assertSendDirection(); logger.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); - if (!this._transportReady) - await this._setupTransport({ localDtlsRole: 'server' }); - logger.debug('send() | calling new RTCRtpSender()'); - const rtpSender = new RTCRtpSender(track, this._dtlsTransport); - const rtpParameters = utils.clone(this._sendingRtpParametersByKind[track.kind], {}); - rtpParameters.codecs = ortc.reduceCodecs(rtpParameters.codecs, codec); - const useRtx = rtpParameters.codecs - .some((_codec) => /.+\/rtx$/i.test(_codec.mimeType)); - if (!encodings) - encodings = [{}]; - for (const encoding of encodings) { - encoding.ssrc = utils.generateRandomNumber(); - if (useRtx) - encoding.rtx = { ssrc: utils.generateRandomNumber() }; + if (encodings) { + encodings = utils.clone(encodings, []); + if (encodings.length > 1) { + encodings.forEach((encoding, idx) => { + encoding.rid = `r${idx}`; + }); + // Clone the encodings and reverse them because Firefox likes them + // from high to low. + encodings.reverse(); + } } - rtpParameters.encodings = encodings; - // Fill RTCRtpParameters.rtcp. - rtpParameters.rtcp = - { - cname: this._cname, - reducedSize: true, - mux: true - }; - // NOTE: Convert our standard RTCRtpParameters into those that Edge - // expects. - const edgeRtpParameters = edgeUtils.mangleRtpParameters(rtpParameters); - logger.debug('send() | calling rtpSender.send() [params:%o]', edgeRtpParameters); - await rtpSender.send(edgeRtpParameters); - const localId = String(this._nextSendLocalId); - this._nextSendLocalId++; - // Store it. - this._rtpSenders.set(localId, rtpSender); - return { localId, rtpParameters, rtpSender }; + const sendingRtpParameters = utils.clone(this._sendingRtpParametersByKind[track.kind], {}); + // This may throw. + sendingRtpParameters.codecs = + ortc.reduceCodecs(sendingRtpParameters.codecs, codec); + const sendingRemoteRtpParameters = utils.clone(this._sendingRemoteRtpParametersByKind[track.kind], {}); + // This may throw. + sendingRemoteRtpParameters.codecs = + ortc.reduceCodecs(sendingRemoteRtpParameters.codecs, codec); + // NOTE: Firefox fails sometimes to properly anticipate the closed media + // section that it should use, so don't reuse closed media sections. + // https://github.com/versatica/mediasoup-client/issues/104 + // + // const mediaSectionIdx = this._remoteSdp!.getNextMediaSectionIdx(); + const transceiver = this._pc.addTransceiver(track, { direction: 'sendonly', streams: [this._sendStream] }); + // NOTE: This is not spec compliants. Encodings should be given in addTransceiver + // second argument, but Firefox does not support it. + if (encodings) { + const parameters = transceiver.sender.getParameters(); + parameters.encodings = encodings; + await transceiver.sender.setParameters(parameters); + } + const offer = await this._pc.createOffer(); + let localSdpObject = sdpTransform.parse(offer.sdp); + // In Firefox use DTLS role client even if we are the "offerer" since + // Firefox does not respect ICE-Lite. + if (!this._transportReady) { + await this.setupTransport({ localDtlsRole: 'client', localSdpObject }); + } + const layers = (0, scalabilityModes_1.parse)((encodings || [{}])[0].scalabilityMode); + logger.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + // We can now get the transceiver.mid. + const localId = transceiver.mid; + // Set MID. + sendingRtpParameters.mid = localId; + localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + const offerMediaObject = localSdpObject.media[localSdpObject.media.length - 1]; + // Set RTCP CNAME. + sendingRtpParameters.rtcp.cname = + sdpCommonUtils.getCname({ offerMediaObject }); + // Set RTP encodings by parsing the SDP offer if no encodings are given. + if (!encodings) { + sendingRtpParameters.encodings = + sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject }); + } + // Set RTP encodings by parsing the SDP offer and complete them with given + // one if just a single encoding has been given. + else if (encodings.length === 1) { + const newEncodings = sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject }); + Object.assign(newEncodings[0], encodings[0]); + sendingRtpParameters.encodings = newEncodings; + } + // Otherwise if more than 1 encoding are given use them verbatim (but + // reverse them back since we reversed them above to satisfy Firefox). + else { + sendingRtpParameters.encodings = encodings.reverse(); + } + // If VP8 or H264 and there is effective simulcast, add scalabilityMode to + // each encoding. + if (sendingRtpParameters.encodings.length > 1 && + (sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8' || + sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/h264')) { + for (const encoding of sendingRtpParameters.encodings) { + if (encoding.scalabilityMode) { + encoding.scalabilityMode = `L1T${layers.temporalLayers}`; + } + else { + encoding.scalabilityMode = 'L1T3'; + } + } + } + this._remoteSdp.send({ + offerMediaObject, + offerRtpParameters: sendingRtpParameters, + answerRtpParameters: sendingRemoteRtpParameters, + codecOptions, + extmapAllowMixed: true + }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + return { + localId, + rtpParameters: sendingRtpParameters, + rtpSender: transceiver.sender + }; } async stopSending(localId) { logger.debug('stopSending() [localId:%s]', localId); - const rtpSender = this._rtpSenders.get(localId); - if (!rtpSender) - throw new Error('RTCRtpSender not found'); - this._rtpSenders.delete(localId); - try { - logger.debug('stopSending() | calling rtpSender.stop()'); - rtpSender.stop(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated transceiver not found'); } - catch (error) { - logger.warn('stopSending() | rtpSender.stop() failed:%o', error); - throw error; + transceiver.sender.replaceTrack(null); + // NOTE: Cannot use stop() the transceiver due to the the note above in + // send() method. + // try + // { + // transceiver.stop(); + // } + // catch (error) + // {} + this._pc.removeTrack(transceiver.sender); + // NOTE: Cannot use closeMediaSection() due to the the note above in send() + // method. + // this._remoteSdp!.closeMediaSection(transceiver.mid); + this._remoteSdp.disableMediaSection(transceiver.mid); + const offer = await this._pc.createOffer(); + logger.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._mapMidTransceiver.delete(localId); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async pauseSending(localId) { + this.assertSendDirection(); + logger.debug('pauseSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'inactive'; + this._remoteSdp.pauseMediaSection(localId); + const offer = await this._pc.createOffer(); + logger.debug('pauseSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('pauseSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async resumeSending(localId) { + this.assertSendDirection(); + logger.debug('resumeSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); } + transceiver.direction = 'sendonly'; + this._remoteSdp.resumeSendingMediaSection(localId); + const offer = await this._pc.createOffer(); + logger.debug('resumeSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('resumeSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); } async replaceTrack(localId, track) { + this.assertSendDirection(); if (track) { logger.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id); } else { logger.debug('replaceTrack() [localId:%s, no track]', localId); } - const rtpSender = this._rtpSenders.get(localId); - if (!rtpSender) - throw new Error('RTCRtpSender not found'); - rtpSender.setTrack(track); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + await transceiver.sender.replaceTrack(track); } async setMaxSpatialLayer(localId, spatialLayer) { + this.assertSendDirection(); logger.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer); - const rtpSender = this._rtpSenders.get(localId); - if (!rtpSender) - throw new Error('RTCRtpSender not found'); - const parameters = rtpSender.getParameters(); - parameters.encodings - .forEach((encoding, idx) => { - if (idx <= spatialLayer) + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated transceiver not found'); + } + const parameters = transceiver.sender.getParameters(); + // NOTE: We require encodings given from low to high, however Firefox + // requires them in reverse order, so do magic here. + spatialLayer = parameters.encodings.length - 1 - spatialLayer; + parameters.encodings.forEach((encoding, idx) => { + if (idx >= spatialLayer) { encoding.active = true; - else + } + else { encoding.active = false; + } }); - await rtpSender.setParameters(parameters); + await transceiver.sender.setParameters(parameters); + this._remoteSdp.muxMediaSectionSimulcast(localId, parameters.encodings); + const offer = await this._pc.createOffer(); + logger.debug('setMaxSpatialLayer() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('setMaxSpatialLayer() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); } async setRtpEncodingParameters(localId, params) { + this.assertSendDirection(); logger.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params); - const rtpSender = this._rtpSenders.get(localId); - if (!rtpSender) - throw new Error('RTCRtpSender not found'); - const parameters = rtpSender.getParameters(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + const parameters = transceiver.sender.getParameters(); parameters.encodings.forEach((encoding, idx) => { - parameters.encodings[idx] = Object.assign(Object.assign({}, encoding), params); + parameters.encodings[idx] = { ...encoding, ...params }; }); - await rtpSender.setParameters(parameters); + await transceiver.sender.setParameters(parameters); + this._remoteSdp.muxMediaSectionSimulcast(localId, parameters.encodings); + const offer = await this._pc.createOffer(); + logger.debug('setRtpEncodingParameters() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('setRtpEncodingParameters() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); } async getSenderStats(localId) { - const rtpSender = this._rtpSenders.get(localId); - if (!rtpSender) - throw new Error('RTCRtpSender not found'); - return rtpSender.getStats(); - } - async sendDataChannel( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - options) { - throw new errors_1.UnsupportedError('not implemented'); + this.assertSendDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + return transceiver.sender.getStats(); } - async receive({ trackId, kind, rtpParameters }) { - logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); - if (!this._transportReady) - await this._setupTransport({ localDtlsRole: 'server' }); - logger.debug('receive() | calling new RTCRtpReceiver()'); - const rtpReceiver = new RTCRtpReceiver(this._dtlsTransport, kind); - rtpReceiver.addEventListener('error', (event) => { - logger.error('rtpReceiver "error" event [event:%o]', event); - }); - // NOTE: Convert our standard RTCRtpParameters into those that Edge - // expects. - const edgeRtpParameters = edgeUtils.mangleRtpParameters(rtpParameters); - logger.debug('receive() | calling rtpReceiver.receive() [params:%o]', edgeRtpParameters); - await rtpReceiver.receive(edgeRtpParameters); - const localId = trackId; - // Store it. - this._rtpReceivers.set(localId, rtpReceiver); - return { - localId, - track: rtpReceiver.track, - rtpReceiver + async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { + this.assertSendDirection(); + const options = { + negotiated: true, + id: this._nextSendSctpStreamId, + ordered, + maxPacketLifeTime, + maxRetransmits, + protocol }; - } - async stopReceiving(localId) { - logger.debug('stopReceiving() [localId:%s]', localId); - const rtpReceiver = this._rtpReceivers.get(localId); - if (!rtpReceiver) - throw new Error('RTCRtpReceiver not found'); - this._rtpReceivers.delete(localId); - try { - logger.debug('stopReceiving() | calling rtpReceiver.stop()'); - rtpReceiver.stop(); - } - catch (error) { - logger.warn('stopReceiving() | rtpReceiver.stop() failed:%o', error); + logger.debug('sendDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // Increase next id. + this._nextSendSctpStreamId = + ++this._nextSendSctpStreamId % SCTP_NUM_STREAMS.MIS; + // If this is the first DataChannel we need to create the SDP answer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + const offer = await this._pc.createOffer(); + const localSdpObject = sdpTransform.parse(offer.sdp); + const offerMediaObject = localSdpObject.media + .find((m) => m.type === 'application'); + if (!this._transportReady) { + await this.setupTransport({ localDtlsRole: 'client', localSdpObject }); + } + logger.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + this._remoteSdp.sendSctpAssociation({ offerMediaObject }); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + this._hasDataChannelMediaSection = true; } + const sctpStreamParameters = { + streamId: options.id, + ordered: options.ordered, + maxPacketLifeTime: options.maxPacketLifeTime, + maxRetransmits: options.maxRetransmits + }; + return { dataChannel, sctpStreamParameters }; } - async getReceiverStats(localId) { - const rtpReceiver = this._rtpReceivers.get(localId); - if (!rtpReceiver) - throw new Error('RTCRtpReceiver not found'); - return rtpReceiver.getStats(); - } - async receiveDataChannel( + async receive( // eslint-disable-next-line @typescript-eslint/no-unused-vars - options) { - throw new errors_1.UnsupportedError('not implemented'); + optionsList) { + this.assertRecvDirection(); + const results = []; + const mapLocalId = new Map(); + for (const options of optionsList) { + const { trackId, kind, rtpParameters, streamId } = options; + logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const localId = rtpParameters.mid || String(this._mapMidTransceiver.size); + mapLocalId.set(trackId, localId); + this._remoteSdp.receive({ + mid: localId, + kind, + offerRtpParameters: rtpParameters, + streamId: streamId || rtpParameters.rtcp.cname, + trackId + }); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + let answer = await this._pc.createAnswer(); + const localSdpObject = sdpTransform.parse(answer.sdp); + for (const options of optionsList) { + const { trackId, rtpParameters } = options; + const localId = mapLocalId.get(trackId); + const answerMediaObject = localSdpObject.media + .find((m) => String(m.mid) === localId); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject + }); + answer = { type: 'answer', sdp: sdpTransform.write(localSdpObject) }; + } + if (!this._transportReady) { + await this.setupTransport({ localDtlsRole: 'client', localSdpObject }); + } + logger.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + for (const options of optionsList) { + const { trackId } = options; + const localId = mapLocalId.get(trackId); + const transceiver = this._pc.getTransceivers() + .find((t) => t.mid === localId); + if (!transceiver) { + throw new Error('new RTCRtpTransceiver not found'); + } + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + results.push({ + localId, + track: transceiver.receiver.track, + rtpReceiver: transceiver.receiver + }); + } + return results; } - _setIceGatherer({ iceServers, iceTransportPolicy }) { - const iceGatherer = new RTCIceGatherer({ - iceServers: iceServers || [], - gatherPolicy: iceTransportPolicy || 'all' - }); - iceGatherer.addEventListener('error', (event) => { - logger.error('iceGatherer "error" event [event:%o]', event); - }); - // NOTE: Not yet implemented by Edge, which starts gathering automatically. - try { - iceGatherer.gather(); + async stopReceiving(localIds) { + this.assertRecvDirection(); + for (const localId of localIds) { + logger.debug('stopReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + this._remoteSdp.closeMediaSection(transceiver.mid); } - catch (error) { - logger.debug('_setIceGatherer() | iceGatherer.gather() failed: %s', error.toString()); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + for (const localId of localIds) { + this._mapMidTransceiver.delete(localId); } - this._iceGatherer = iceGatherer; } - _setIceTransport() { - const iceTransport = new RTCIceTransport(this._iceGatherer); - // NOTE: Not yet implemented by Edge. - iceTransport.addEventListener('statechange', () => { - switch (iceTransport.state) { - case 'checking': - this.emit('@connectionstatechange', 'connecting'); - break; - case 'connected': - case 'completed': - this.emit('@connectionstatechange', 'connected'); - break; - case 'failed': - this.emit('@connectionstatechange', 'failed'); - break; - case 'disconnected': - this.emit('@connectionstatechange', 'disconnected'); - break; - case 'closed': - this.emit('@connectionstatechange', 'closed'); - break; + async pauseReceiving(localIds) { + this.assertRecvDirection(); + for (const localId of localIds) { + logger.debug('pauseReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); } - }); - // NOTE: Not standard, but implemented by Edge. - iceTransport.addEventListener('icestatechange', () => { - switch (iceTransport.state) { - case 'checking': - this.emit('@connectionstatechange', 'connecting'); - break; - case 'connected': - case 'completed': - this.emit('@connectionstatechange', 'connected'); - break; - case 'failed': - this.emit('@connectionstatechange', 'failed'); - break; - case 'disconnected': - this.emit('@connectionstatechange', 'disconnected'); - break; - case 'closed': - this.emit('@connectionstatechange', 'closed'); - break; + transceiver.direction = 'inactive'; + this._remoteSdp.pauseMediaSection(localId); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('pauseReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('pauseReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + async resumeReceiving(localIds) { + this.assertRecvDirection(); + for (const localId of localIds) { + logger.debug('resumeReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); } - }); - iceTransport.addEventListener('candidatepairchange', (event) => { - logger.debug('iceTransport "candidatepairchange" event [pair:%o]', event.pair); - }); - this._iceTransport = iceTransport; + transceiver.direction = 'recvonly'; + this._remoteSdp.resumeReceivingMediaSection(localId); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('resumeReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('resumeReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); } - _setDtlsTransport() { - const dtlsTransport = new RTCDtlsTransport(this._iceTransport); - // NOTE: Not yet implemented by Edge. - dtlsTransport.addEventListener('statechange', () => { - logger.debug('dtlsTransport "statechange" event [state:%s]', dtlsTransport.state); - }); - // NOTE: Not standard, but implemented by Edge. - dtlsTransport.addEventListener('dtlsstatechange', () => { - logger.debug('dtlsTransport "dtlsstatechange" event [state:%s]', dtlsTransport.state); - if (dtlsTransport.state === 'closed') - this.emit('@connectionstatechange', 'closed'); - }); - dtlsTransport.addEventListener('error', (event) => { - logger.error('dtlsTransport "error" event [event:%o]', event); - }); - this._dtlsTransport = dtlsTransport; + async getReceiverStats(localId) { + this.assertRecvDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + return transceiver.receiver.getStats(); + } + async receiveDataChannel({ sctpStreamParameters, label, protocol }) { + this.assertRecvDirection(); + const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; + const options = { + negotiated: true, + id: streamId, + ordered, + maxPacketLifeTime, + maxRetransmits, + protocol + }; + logger.debug('receiveDataChannel() [options:%o]', options); + const dataChannel = this._pc.createDataChannel(label, options); + // If this is the first DataChannel we need to create the SDP offer with + // m=application section. + if (!this._hasDataChannelMediaSection) { + this._remoteSdp.receiveSctpAssociation(); + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + if (!this._transportReady) { + const localSdpObject = sdpTransform.parse(answer.sdp); + await this.setupTransport({ localDtlsRole: 'client', localSdpObject }); + } + logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + this._hasDataChannelMediaSection = true; + } + return { dataChannel }; } - async _setupTransport({ localDtlsRole }) { - logger.debug('_setupTransport()'); + async setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) { + localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + } // Get our local DTLS parameters. - const dtlsParameters = this._dtlsTransport.getLocalParameters(); + const dtlsParameters = sdpCommonUtils.extractDtlsParameters({ sdpObject: localSdpObject }); + // Set our DTLS role. dtlsParameters.role = localDtlsRole; + // Update the remote DTLS role in the SDP. + this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); // Need to tell the remote transport about our parameters. - await this.safeEmitAsPromise('@connect', { dtlsParameters }); - // Start the RTCIceTransport. - this._iceTransport.start(this._iceGatherer, this._remoteIceParameters, 'controlling'); - // Add remote ICE candidates. - for (const candidate of this._remoteIceCandidates) { - this._iceTransport.addRemoteCandidate(candidate); - } - // Also signal a 'complete' candidate as per spec. - // NOTE: It should be {complete: true} but Edge prefers {}. - // NOTE: If we don't signal end of candidates, the Edge RTCIceTransport - // won't enter the 'completed' state. - this._iceTransport.addRemoteCandidate({}); - // NOTE: Edge does not like SHA less than 256. - this._remoteDtlsParameters.fingerprints = this._remoteDtlsParameters.fingerprints - .filter((fingerprint) => { - return (fingerprint.algorithm === 'sha-256' || - fingerprint.algorithm === 'sha-384' || - fingerprint.algorithm === 'sha-512'); + await new Promise((resolve, reject) => { + this.safeEmit('@connect', { dtlsParameters }, resolve, reject); }); - // Start the RTCDtlsTransport. - this._dtlsTransport.start(this._remoteDtlsParameters); this._transportReady = true; } + assertSendDirection() { + if (this._direction !== 'send') { + throw new Error('method can just be called for handlers with "send" direction'); + } + } + assertRecvDirection() { + if (this._direction !== 'recv') { + throw new Error('method can just be called for handlers with "recv" direction'); + } + } } -exports.Edge11 = Edge11; +exports.Firefox60 = Firefox60; + +},{"../Logger":17,"../errors":22,"../ortc":43,"../scalabilityModes":44,"../utils":46,"./HandlerInterface":30,"./sdp/RemoteSdp":38,"./sdp/commonUtils":39,"./sdp/unifiedPlanUtils":41,"sdp-transform":52}],30:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.HandlerInterface = void 0; +const EnhancedEventEmitter_1 = require("../EnhancedEventEmitter"); +class HandlerInterface extends EnhancedEventEmitter_1.EnhancedEventEmitter { + constructor() { + super(); + } +} +exports.HandlerInterface = HandlerInterface; -},{"../Logger":13,"../errors":18,"../ortc":36,"../utils":39,"./HandlerInterface":25,"./ortc/edgeUtils":29}],24:[function(require,module,exports){ +},{"../EnhancedEventEmitter":16}],31:[function(require,module,exports){ "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; @@ -5609,30 +7926,41 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.Firefox60 = void 0; +exports.ReactNative = void 0; const sdpTransform = __importStar(require("sdp-transform")); const Logger_1 = require("../Logger"); const errors_1 = require("../errors"); const utils = __importStar(require("../utils")); const ortc = __importStar(require("../ortc")); const sdpCommonUtils = __importStar(require("./sdp/commonUtils")); -const sdpUnifiedPlanUtils = __importStar(require("./sdp/unifiedPlanUtils")); +const sdpPlanBUtils = __importStar(require("./sdp/planBUtils")); const HandlerInterface_1 = require("./HandlerInterface"); const RemoteSdp_1 = require("./sdp/RemoteSdp"); -const logger = new Logger_1.Logger('Firefox60'); -const SCTP_NUM_STREAMS = { OS: 16, MIS: 2048 }; -class Firefox60 extends HandlerInterface_1.HandlerInterface { +const logger = new Logger_1.Logger('ReactNative'); +const SCTP_NUM_STREAMS = { OS: 1024, MIS: 1024 }; +class ReactNative extends HandlerInterface_1.HandlerInterface { + /** + * Creates a factory function. + */ + static createFactory() { + return () => new ReactNative(); + } constructor() { super(); - // Map of RTCTransceivers indexed by MID. - this._mapMidTransceiver = new Map(); // Local stream for sending. this._sendStream = new MediaStream(); + // Map of sending MediaStreamTracks indexed by localId. + this._mapSendLocalIdTrack = new Map(); + // Next sending localId. + this._nextSendLocalId = 0; + // Map of MID, RTP parameters and RTCRtpReceiver indexed by local id. + // Value is an Object with mid, rtpParameters and rtpReceiver. + this._mapRecvLocalIdInfo = new Map(); // Whether a DataChannel m=application section has been created. this._hasDataChannelMediaSection = false; // Sending DataChannel id value counter. Incremented for each new DataChannel. @@ -5640,17 +7968,15 @@ class Firefox60 extends HandlerInterface_1.HandlerInterface { // Got transport local and remote parameters. this._transportReady = false; } - /** - * Creates a factory function. - */ - static createFactory() { - return () => new Firefox60(); - } get name() { - return 'Firefox60'; + return 'ReactNative'; } close() { logger.debug('close()'); + // Free/dispose native MediaStream but DO NOT free/dispose native + // MediaStreamTracks (that is parent's business). + // @ts-ignore (proprietary API in react-native-webrtc). + this._sendStream.release(/* releaseTracks */ false); // Close RTCPeerConnection. if (this._pc) { try { @@ -5658,6 +7984,7 @@ class Firefox60 extends HandlerInterface_1.HandlerInterface { } catch (error) { } } + this.emit('@close'); } async getNativeRtpCapabilities() { logger.debug('getNativeRtpCapabilities()'); @@ -5665,33 +7992,14 @@ class Firefox60 extends HandlerInterface_1.HandlerInterface { iceServers: [], iceTransportPolicy: 'all', bundlePolicy: 'max-bundle', - rtcpMuxPolicy: 'require' + rtcpMuxPolicy: 'require', + sdpSemantics: 'plan-b' }); - // NOTE: We need to add a real video track to get the RID extension mapping. - const canvas = document.createElement('canvas'); - // NOTE: Otherwise Firefox fails in next line. - canvas.getContext('2d'); - const fakeStream = canvas.captureStream(); - const fakeVideoTrack = fakeStream.getVideoTracks()[0]; try { - pc.addTransceiver('audio', { direction: 'sendrecv' }); - const videoTransceiver = pc.addTransceiver(fakeVideoTrack, { direction: 'sendrecv' }); - const parameters = videoTransceiver.sender.getParameters(); - const encodings = [ - { rid: 'r0', maxBitrate: 100000 }, - { rid: 'r1', maxBitrate: 500000 } - ]; - parameters.encodings = encodings; - await videoTransceiver.sender.setParameters(parameters); - const offer = await pc.createOffer(); - try { - canvas.remove(); - } - catch (error) { } - try { - fakeVideoTrack.stop(); - } - catch (error) { } + const offer = await pc.createOffer({ + offerToReceiveAudio: true, + offerToReceiveVideo: true + }); try { pc.close(); } @@ -5701,14 +8009,6 @@ class Firefox60 extends HandlerInterface_1.HandlerInterface { return nativeRtpCapabilities; } catch (error) { - try { - canvas.remove(); - } - catch (error2) { } - try { - fakeVideoTrack.stop(); - } - catch (error2) { } try { pc.close(); } @@ -5729,7 +8029,8 @@ class Firefox60 extends HandlerInterface_1.HandlerInterface { iceParameters, iceCandidates, dtlsParameters, - sctpParameters + sctpParameters, + planB: true }); this._sendingRtpParametersByKind = { @@ -5741,40 +8042,61 @@ class Firefox60 extends HandlerInterface_1.HandlerInterface { audio: ortc.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), video: ortc.getSendingRemoteRtpParameters('video', extendedRtpCapabilities) }; - this._pc = new RTCPeerConnection(Object.assign({ iceServers: iceServers || [], iceTransportPolicy: iceTransportPolicy || 'all', bundlePolicy: 'max-bundle', rtcpMuxPolicy: 'require' }, additionalSettings), proprietaryConstraints); - // Handle RTCPeerConnection connection status. - this._pc.addEventListener('iceconnectionstatechange', () => { - switch (this._pc.iceConnectionState) { - case 'checking': - this.emit('@connectionstatechange', 'connecting'); - break; - case 'connected': - case 'completed': - this.emit('@connectionstatechange', 'connected'); - break; - case 'failed': - this.emit('@connectionstatechange', 'failed'); - break; - case 'disconnected': - this.emit('@connectionstatechange', 'disconnected'); - break; - case 'closed': - this.emit('@connectionstatechange', 'closed'); - break; - } - }); + if (dtlsParameters.role && dtlsParameters.role !== 'auto') { + this._forcedLocalDtlsRole = dtlsParameters.role === 'server' + ? 'client' + : 'server'; + } + this._pc = new RTCPeerConnection({ + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'plan-b', + ...additionalSettings + }, proprietaryConstraints); + if (this._pc.connectionState) { + this._pc.addEventListener('connectionstatechange', () => { + this.emit('@connectionstatechange', this._pc.connectionState); + }); + } + else { + this._pc.addEventListener('iceconnectionstatechange', () => { + logger.warn('run() | pc.connectionState not supported, using pc.iceConnectionState'); + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } } - // eslint-disable-next-line @typescript-eslint/no-unused-vars async updateIceServers(iceServers) { - // NOTE: Firefox does not implement pc.setConfiguration(). - throw new errors_1.UnsupportedError('not supported'); + logger.debug('updateIceServers()'); + const configuration = this._pc.getConfiguration(); + configuration.iceServers = iceServers; + this._pc.setConfiguration(configuration); } async restartIce(iceParameters) { logger.debug('restartIce()'); // Provide the remote SDP handler with new remote ICE parameters. this._remoteSdp.updateIceParameters(iceParameters); - if (!this._transportReady) + if (!this._transportReady) { return; + } if (this._direction === 'send') { const offer = await this._pc.createOffer({ iceRestart: true }); logger.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); @@ -5796,73 +8118,59 @@ class Firefox60 extends HandlerInterface_1.HandlerInterface { return this._pc.getStats(); } async send({ track, encodings, codecOptions, codec }) { - this._assertSendDirection(); + var _a; + this.assertSendDirection(); logger.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); - if (encodings) { - encodings = utils.clone(encodings, []); - if (encodings.length > 1) { - encodings.forEach((encoding, idx) => { - encoding.rid = `r${idx}`; - }); - // Clone the encodings and reverse them because Firefox likes them - // from high to low. - encodings.reverse(); - } + if (codec) { + logger.warn('send() | codec selection is not available in %s handler', this.name); } + this._sendStream.addTrack(track); + this._pc.addStream(this._sendStream); + let offer = await this._pc.createOffer(); + let localSdpObject = sdpTransform.parse(offer.sdp); + let offerMediaObject; const sendingRtpParameters = utils.clone(this._sendingRtpParametersByKind[track.kind], {}); - // This may throw. sendingRtpParameters.codecs = - ortc.reduceCodecs(sendingRtpParameters.codecs, codec); + ortc.reduceCodecs(sendingRtpParameters.codecs); const sendingRemoteRtpParameters = utils.clone(this._sendingRemoteRtpParametersByKind[track.kind], {}); - // This may throw. sendingRemoteRtpParameters.codecs = - ortc.reduceCodecs(sendingRemoteRtpParameters.codecs, codec); - // NOTE: Firefox fails sometimes to properly anticipate the closed media - // section that it should use, so don't reuse closed media sections. - // https://github.com/versatica/mediasoup-client/issues/104 - // - // const mediaSectionIdx = this._remoteSdp!.getNextMediaSectionIdx(); - const transceiver = this._pc.addTransceiver(track, { direction: 'sendonly', streams: [this._sendStream] }); - // NOTE: This is not spec compliants. Encodings should be given in addTransceiver - // second argument, but Firefox does not support it. - if (encodings) { - const parameters = transceiver.sender.getParameters(); - parameters.encodings = encodings; - await transceiver.sender.setParameters(parameters); + ortc.reduceCodecs(sendingRemoteRtpParameters.codecs); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); + } + if (track.kind === 'video' && encodings && encodings.length > 1) { + logger.debug('send() | enabling simulcast'); + localSdpObject = sdpTransform.parse(offer.sdp); + offerMediaObject = localSdpObject.media + .find((m) => m.type === 'video'); + sdpPlanBUtils.addLegacySimulcast({ + offerMediaObject, + track, + numStreams: encodings.length + }); + offer = { type: 'offer', sdp: sdpTransform.write(localSdpObject) }; } - const offer = await this._pc.createOffer(); - let localSdpObject = sdpTransform.parse(offer.sdp); - // In Firefox use DTLS role client even if we are the "offerer" since - // Firefox does not respect ICE-Lite. - if (!this._transportReady) - await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); logger.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); await this._pc.setLocalDescription(offer); - // We can now get the transceiver.mid. - const localId = transceiver.mid; - // Set MID. - sendingRtpParameters.mid = localId; localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); - const offerMediaObject = localSdpObject.media[localSdpObject.media.length - 1]; + offerMediaObject = localSdpObject.media + .find((m) => m.type === track.kind); // Set RTCP CNAME. sendingRtpParameters.rtcp.cname = sdpCommonUtils.getCname({ offerMediaObject }); - // Set RTP encodings by parsing the SDP offer if no encodings are given. - if (!encodings) { - sendingRtpParameters.encodings = - sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject }); - } - // Set RTP encodings by parsing the SDP offer and complete them with given - // one if just a single encoding has been given. - else if (encodings.length === 1) { - const newEncodings = sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject }); - Object.assign(newEncodings[0], encodings[0]); - sendingRtpParameters.encodings = newEncodings; - } - // Otherwise if more than 1 encoding are given use them verbatim (but - // reverse them back since we reversed them above to satisfy Firefox). - else { - sendingRtpParameters.encodings = encodings.reverse(); + // Set RTP encodings. + sendingRtpParameters.encodings = + sdpPlanBUtils.getRtpEncodings({ offerMediaObject, track }); + // Complete encodings with given values. + if (encodings) { + for (let idx = 0; idx < sendingRtpParameters.encodings.length; ++idx) { + if (encodings[idx]) { + Object.assign(sendingRtpParameters.encodings[idx], encodings[idx]); + } + } } // If VP8 or H264 and there is effective simulcast, add scalabilityMode to // each encoding. @@ -5870,102 +8178,92 @@ class Firefox60 extends HandlerInterface_1.HandlerInterface { (sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8' || sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/h264')) { for (const encoding of sendingRtpParameters.encodings) { - encoding.scalabilityMode = 'S1T3'; + encoding.scalabilityMode = 'L1T3'; } } this._remoteSdp.send({ offerMediaObject, offerRtpParameters: sendingRtpParameters, answerRtpParameters: sendingRemoteRtpParameters, - codecOptions, - extmapAllowMixed: true + codecOptions }); const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; logger.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer); await this._pc.setRemoteDescription(answer); - // Store in the map. - this._mapMidTransceiver.set(localId, transceiver); + const localId = String(this._nextSendLocalId); + this._nextSendLocalId++; + // Insert into the map. + this._mapSendLocalIdTrack.set(localId, track); return { - localId, - rtpParameters: sendingRtpParameters, - rtpSender: transceiver.sender + localId: localId, + rtpParameters: sendingRtpParameters }; } async stopSending(localId) { + this.assertSendDirection(); logger.debug('stopSending() [localId:%s]', localId); - const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) - throw new Error('associated transceiver not found'); - transceiver.sender.replaceTrack(null); - this._pc.removeTrack(transceiver.sender); - // NOTE: Cannot use closeMediaSection() due to the the note above in send() - // method. - // this._remoteSdp!.closeMediaSection(transceiver.mid); - this._remoteSdp.disableMediaSection(transceiver.mid); + const track = this._mapSendLocalIdTrack.get(localId); + if (!track) { + throw new Error('track not found'); + } + this._mapSendLocalIdTrack.delete(localId); + this._sendStream.removeTrack(track); + this._pc.addStream(this._sendStream); const offer = await this._pc.createOffer(); logger.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); - await this._pc.setLocalDescription(offer); + try { + await this._pc.setLocalDescription(offer); + } + catch (error) { + // NOTE: If there are no sending tracks, setLocalDescription() will fail with + // "Failed to create channels". If so, ignore it. + if (this._sendStream.getTracks().length === 0) { + logger.warn('stopSending() | ignoring expected error due no sending tracks: %s', error.toString()); + return; + } + throw error; + } + if (this._pc.signalingState === 'stable') { + return; + } const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; logger.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); await this._pc.setRemoteDescription(answer); } - async replaceTrack(localId, track) { - this._assertSendDirection(); - if (track) { - logger.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id); - } - else { - logger.debug('replaceTrack() [localId:%s, no track]', localId); - } - const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) - throw new Error('associated RTCRtpTransceiver not found'); - await transceiver.sender.replaceTrack(track); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async pauseSending(localId) { + // Unimplemented. + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async resumeSending(localId) { + // Unimplemented. + } + async replaceTrack( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localId, track) { + throw new errors_1.UnsupportedError('not implemented'); } + // eslint-disable-next-line @typescript-eslint/no-unused-vars async setMaxSpatialLayer(localId, spatialLayer) { - this._assertSendDirection(); - logger.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer); - const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) - throw new Error('associated transceiver not found'); - const parameters = transceiver.sender.getParameters(); - // NOTE: We require encodings given from low to high, however Firefox - // requires them in reverse order, so do magic here. - spatialLayer = parameters.encodings.length - 1 - spatialLayer; - parameters.encodings.forEach((encoding, idx) => { - if (idx >= spatialLayer) - encoding.active = true; - else - encoding.active = false; - }); - await transceiver.sender.setParameters(parameters); + throw new errors_1.UnsupportedError('not implemented'); } + // eslint-disable-next-line @typescript-eslint/no-unused-vars async setRtpEncodingParameters(localId, params) { - this._assertSendDirection(); - logger.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params); - const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) - throw new Error('associated RTCRtpTransceiver not found'); - const parameters = transceiver.sender.getParameters(); - parameters.encodings.forEach((encoding, idx) => { - parameters.encodings[idx] = Object.assign(Object.assign({}, encoding), params); - }); - await transceiver.sender.setParameters(parameters); + throw new errors_1.UnsupportedError('not implemented'); } + // eslint-disable-next-line @typescript-eslint/no-unused-vars async getSenderStats(localId) { - this._assertSendDirection(); - const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) - throw new Error('associated RTCRtpTransceiver not found'); - return transceiver.sender.getStats(); + throw new errors_1.UnsupportedError('not implemented'); } async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { - this._assertSendDirection(); + var _a; + this.assertSendDirection(); const options = { negotiated: true, id: this._nextSendSctpStreamId, ordered, maxPacketLifeTime, + maxRetransmitTime: maxPacketLifeTime, maxRetransmits, protocol }; @@ -5981,8 +8279,12 @@ class Firefox60 extends HandlerInterface_1.HandlerInterface { const localSdpObject = sdpTransform.parse(offer.sdp); const offerMediaObject = localSdpObject.media .find((m) => m.type === 'application'); - if (!this._transportReady) - await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); + } logger.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); await this._pc.setLocalDescription(offer); this._remoteSdp.sendSctpAssociation({ offerMediaObject }); @@ -5999,54 +8301,84 @@ class Firefox60 extends HandlerInterface_1.HandlerInterface { }; return { dataChannel, sctpStreamParameters }; } - async receive({ trackId, kind, rtpParameters }) { - this._assertRecvDirection(); - logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); - const localId = rtpParameters.mid || String(this._mapMidTransceiver.size); - this._remoteSdp.receive({ - mid: localId, - kind, - offerRtpParameters: rtpParameters, - streamId: rtpParameters.rtcp.cname, - trackId - }); + async receive(optionsList) { + var _a; + this.assertRecvDirection(); + const results = []; + const mapStreamId = new Map(); + for (const options of optionsList) { + const { trackId, kind, rtpParameters } = options; + logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const mid = kind; + let streamId = options.streamId || rtpParameters.rtcp.cname; + // NOTE: In React-Native we cannot reuse the same remote MediaStream for new + // remote tracks. This is because react-native-webrtc does not react on new + // tracks generated within already existing streams, so force the streamId + // to be different. See: + // https://github.com/react-native-webrtc/react-native-webrtc/issues/401 + logger.debug('receive() | forcing a random remote streamId to avoid well known bug in react-native-webrtc'); + streamId += `-hack-${utils.generateRandomNumber()}`; + mapStreamId.set(trackId, streamId); + this._remoteSdp.receive({ + mid, + kind, + offerRtpParameters: rtpParameters, + streamId, + trackId + }); + } const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; logger.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); await this._pc.setRemoteDescription(offer); let answer = await this._pc.createAnswer(); const localSdpObject = sdpTransform.parse(answer.sdp); - const answerMediaObject = localSdpObject.media - .find((m) => String(m.mid) === localId); - // May need to modify codec parameters in the answer based on codec - // parameters in the offer. - sdpCommonUtils.applyCodecParameters({ - offerRtpParameters: rtpParameters, - answerMediaObject - }); + for (const options of optionsList) { + const { kind, rtpParameters } = options; + const mid = kind; + const answerMediaObject = localSdpObject.media + .find((m) => String(m.mid) === mid); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject + }); + } answer = { type: 'answer', sdp: sdpTransform.write(localSdpObject) }; - if (!this._transportReady) - await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); + } logger.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); await this._pc.setLocalDescription(answer); - const transceiver = this._pc.getTransceivers() - .find((t) => t.mid === localId); - if (!transceiver) - throw new Error('new RTCRtpTransceiver not found'); - // Store in the map. - this._mapMidTransceiver.set(localId, transceiver); - return { - localId, - track: transceiver.receiver.track, - rtpReceiver: transceiver.receiver - }; + for (const options of optionsList) { + const { kind, trackId, rtpParameters } = options; + const localId = trackId; + const mid = kind; + const streamId = mapStreamId.get(trackId); + const stream = this._pc.getRemoteStreams() + .find((s) => s.id === streamId); + const track = stream.getTrackById(localId); + if (!track) { + throw new Error('remote track not found'); + } + // Insert into the map. + this._mapRecvLocalIdInfo.set(localId, { mid, rtpParameters }); + results.push({ localId, track }); + } + return results; } - async stopReceiving(localId) { - this._assertRecvDirection(); - logger.debug('stopReceiving() [localId:%s]', localId); - const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) - throw new Error('associated RTCRtpTransceiver not found'); - this._remoteSdp.closeMediaSection(transceiver.mid); + async stopReceiving(localIds) { + this.assertRecvDirection(); + for (const localId of localIds) { + logger.debug('stopReceiving() [localId:%s]', localId); + const { mid, rtpParameters } = this._mapRecvLocalIdInfo.get(localId) || {}; + // Remove from the map. + this._mapRecvLocalIdInfo.delete(localId); + this._remoteSdp.planBStopReceiving({ mid: mid, offerRtpParameters: rtpParameters }); + } const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; logger.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); await this._pc.setRemoteDescription(offer); @@ -6054,21 +8386,30 @@ class Firefox60 extends HandlerInterface_1.HandlerInterface { logger.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); await this._pc.setLocalDescription(answer); } + async pauseReceiving( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localIds) { + // Unimplemented. + } + async resumeReceiving( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localIds) { + // Unimplemented. + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars async getReceiverStats(localId) { - this._assertRecvDirection(); - const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) - throw new Error('associated RTCRtpTransceiver not found'); - return transceiver.receiver.getStats(); + throw new errors_1.UnsupportedError('not implemented'); } async receiveDataChannel({ sctpStreamParameters, label, protocol }) { - this._assertRecvDirection(); + var _a; + this.assertRecvDirection(); const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; const options = { negotiated: true, id: streamId, ordered, maxPacketLifeTime, + maxRetransmitTime: maxPacketLifeTime, maxRetransmits, protocol }; @@ -6077,14 +8418,17 @@ class Firefox60 extends HandlerInterface_1.HandlerInterface { // If this is the first DataChannel we need to create the SDP offer with // m=application section. if (!this._hasDataChannelMediaSection) { - this._remoteSdp.receiveSctpAssociation(); + this._remoteSdp.receiveSctpAssociation({ oldDataChannelSpec: true }); const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [offer:%o]', offer); await this._pc.setRemoteDescription(offer); const answer = await this._pc.createAnswer(); if (!this._transportReady) { const localSdpObject = sdpTransform.parse(answer.sdp); - await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); } logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); await this._pc.setLocalDescription(answer); @@ -6092,9 +8436,10 @@ class Firefox60 extends HandlerInterface_1.HandlerInterface { } return { dataChannel }; } - async _setupTransport({ localDtlsRole, localSdpObject }) { - if (!localSdpObject) + async setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) { localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + } // Get our local DTLS parameters. const dtlsParameters = sdpCommonUtils.extractDtlsParameters({ sdpObject: localSdpObject }); // Set our DTLS role. @@ -6102,47 +8447,33 @@ class Firefox60 extends HandlerInterface_1.HandlerInterface { // Update the remote DTLS role in the SDP. this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); // Need to tell the remote transport about our parameters. - await this.safeEmitAsPromise('@connect', { dtlsParameters }); + await new Promise((resolve, reject) => { + this.safeEmit('@connect', { dtlsParameters }, resolve, reject); + }); this._transportReady = true; } - _assertSendDirection() { + assertSendDirection() { if (this._direction !== 'send') { throw new Error('method can just be called for handlers with "send" direction'); } } - _assertRecvDirection() { + assertRecvDirection() { if (this._direction !== 'recv') { throw new Error('method can just be called for handlers with "recv" direction'); } } } -exports.Firefox60 = Firefox60; - -},{"../Logger":13,"../errors":18,"../ortc":36,"../utils":39,"./HandlerInterface":25,"./sdp/RemoteSdp":31,"./sdp/commonUtils":32,"./sdp/unifiedPlanUtils":34,"sdp-transform":44}],25:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.HandlerInterface = void 0; -const EnhancedEventEmitter_1 = require("../EnhancedEventEmitter"); -class HandlerInterface extends EnhancedEventEmitter_1.EnhancedEventEmitter { - /** - * @emits @connect - ( - * { dtlsParameters: DtlsParameters }, - * callback: Function, - * errback: Function - * ) - * @emits @connectionstatechange - (connectionState: ConnectionState) - */ - constructor() { - super(); - } -} -exports.HandlerInterface = HandlerInterface; +exports.ReactNative = ReactNative; -},{"../EnhancedEventEmitter":12}],26:[function(require,module,exports){ +},{"../Logger":17,"../errors":22,"../ortc":43,"../utils":46,"./HandlerInterface":30,"./sdp/RemoteSdp":38,"./sdp/commonUtils":39,"./sdp/planBUtils":40,"sdp-transform":52}],32:[function(require,module,exports){ "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; @@ -6155,35 +8486,37 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.ReactNative = void 0; +exports.ReactNativeUnifiedPlan = void 0; const sdpTransform = __importStar(require("sdp-transform")); const Logger_1 = require("../Logger"); -const errors_1 = require("../errors"); const utils = __importStar(require("../utils")); const ortc = __importStar(require("../ortc")); const sdpCommonUtils = __importStar(require("./sdp/commonUtils")); -const sdpPlanBUtils = __importStar(require("./sdp/planBUtils")); +const sdpUnifiedPlanUtils = __importStar(require("./sdp/unifiedPlanUtils")); +const ortcUtils = __importStar(require("./ortc/utils")); const HandlerInterface_1 = require("./HandlerInterface"); const RemoteSdp_1 = require("./sdp/RemoteSdp"); -const logger = new Logger_1.Logger('ReactNative'); +const scalabilityModes_1 = require("../scalabilityModes"); +const logger = new Logger_1.Logger('ReactNativeUnifiedPlan'); const SCTP_NUM_STREAMS = { OS: 1024, MIS: 1024 }; -class ReactNative extends HandlerInterface_1.HandlerInterface { +class ReactNativeUnifiedPlan extends HandlerInterface_1.HandlerInterface { + /** + * Creates a factory function. + */ + static createFactory() { + return () => new ReactNativeUnifiedPlan(); + } constructor() { super(); + // Map of RTCTransceivers indexed by MID. + this._mapMidTransceiver = new Map(); // Local stream for sending. this._sendStream = new MediaStream(); - // Map of sending MediaStreamTracks indexed by localId. - this._mapSendLocalIdTrack = new Map(); - // Next sending localId. - this._nextSendLocalId = 0; - // Map of MID, RTP parameters and RTCRtpReceiver indexed by local id. - // Value is an Object with mid, rtpParameters and rtpReceiver. - this._mapRecvLocalIdInfo = new Map(); // Whether a DataChannel m=application section has been created. this._hasDataChannelMediaSection = false; // Sending DataChannel id value counter. Incremented for each new DataChannel. @@ -6191,20 +8524,15 @@ class ReactNative extends HandlerInterface_1.HandlerInterface { // Got transport local and remote parameters. this._transportReady = false; } - /** - * Creates a factory function. - */ - static createFactory() { - return () => new ReactNative(); - } get name() { - return 'ReactNative'; + return 'ReactNativeUnifiedPlan'; } close() { logger.debug('close()'); - // Free/dispose native stream and tracks. + // Free/dispose native MediaStream but DO NOT free/dispose native + // MediaStreamTracks (that is parent's business). // @ts-ignore (proprietary API in react-native-webrtc). - this._sendStream.release(); + this._sendStream.release(/* releaseTracks */ false); // Close RTCPeerConnection. if (this._pc) { try { @@ -6212,6 +8540,7 @@ class ReactNative extends HandlerInterface_1.HandlerInterface { } catch (error) { } } + this.emit('@close'); } async getNativeRtpCapabilities() { logger.debug('getNativeRtpCapabilities()'); @@ -6220,19 +8549,20 @@ class ReactNative extends HandlerInterface_1.HandlerInterface { iceTransportPolicy: 'all', bundlePolicy: 'max-bundle', rtcpMuxPolicy: 'require', - sdpSemantics: 'plan-b' + sdpSemantics: 'unified-plan' }); try { - const offer = await pc.createOffer({ - offerToReceiveAudio: true, - offerToReceiveVideo: true - }); + pc.addTransceiver('audio'); + pc.addTransceiver('video'); + const offer = await pc.createOffer(); try { pc.close(); } catch (error) { } const sdpObject = sdpTransform.parse(offer.sdp); const nativeRtpCapabilities = sdpCommonUtils.extractRtpCapabilities({ sdpObject }); + // libwebrtc supports NACK for OPUS but doesn't announce it. + ortcUtils.addNackSuppportForOpus(nativeRtpCapabilities); return nativeRtpCapabilities; } catch (error) { @@ -6252,45 +8582,63 @@ class ReactNative extends HandlerInterface_1.HandlerInterface { run({ direction, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, extendedRtpCapabilities }) { logger.debug('run()'); this._direction = direction; - this._remoteSdp = new RemoteSdp_1.RemoteSdp({ - iceParameters, - iceCandidates, - dtlsParameters, - sctpParameters, - planB: true - }); - this._sendingRtpParametersByKind = - { - audio: ortc.getSendingRtpParameters('audio', extendedRtpCapabilities), - video: ortc.getSendingRtpParameters('video', extendedRtpCapabilities) - }; - this._sendingRemoteRtpParametersByKind = - { - audio: ortc.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), - video: ortc.getSendingRemoteRtpParameters('video', extendedRtpCapabilities) - }; - this._pc = new RTCPeerConnection(Object.assign({ iceServers: iceServers || [], iceTransportPolicy: iceTransportPolicy || 'all', bundlePolicy: 'max-bundle', rtcpMuxPolicy: 'require', sdpSemantics: 'plan-b' }, additionalSettings), proprietaryConstraints); - // Handle RTCPeerConnection connection status. - this._pc.addEventListener('iceconnectionstatechange', () => { - switch (this._pc.iceConnectionState) { - case 'checking': - this.emit('@connectionstatechange', 'connecting'); - break; - case 'connected': - case 'completed': - this.emit('@connectionstatechange', 'connected'); - break; - case 'failed': - this.emit('@connectionstatechange', 'failed'); - break; - case 'disconnected': - this.emit('@connectionstatechange', 'disconnected'); - break; - case 'closed': - this.emit('@connectionstatechange', 'closed'); - break; - } + this._remoteSdp = new RemoteSdp_1.RemoteSdp({ + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters }); + this._sendingRtpParametersByKind = + { + audio: ortc.getSendingRtpParameters('audio', extendedRtpCapabilities), + video: ortc.getSendingRtpParameters('video', extendedRtpCapabilities) + }; + this._sendingRemoteRtpParametersByKind = + { + audio: ortc.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), + video: ortc.getSendingRemoteRtpParameters('video', extendedRtpCapabilities) + }; + if (dtlsParameters.role && dtlsParameters.role !== 'auto') { + this._forcedLocalDtlsRole = dtlsParameters.role === 'server' + ? 'client' + : 'server'; + } + this._pc = new RTCPeerConnection({ + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + sdpSemantics: 'unified-plan', + ...additionalSettings + }, proprietaryConstraints); + if (this._pc.connectionState) { + this._pc.addEventListener('connectionstatechange', () => { + this.emit('@connectionstatechange', this._pc.connectionState); + }); + } + else { + this._pc.addEventListener('iceconnectionstatechange', () => { + logger.warn('run() | pc.connectionState not supported, using pc.iceConnectionState'); + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } } async updateIceServers(iceServers) { logger.debug('updateIceServers()'); @@ -6302,8 +8650,9 @@ class ReactNative extends HandlerInterface_1.HandlerInterface { logger.debug('restartIce()'); // Provide the remote SDP handler with new remote ICE parameters. this._remoteSdp.updateIceParameters(iceParameters); - if (!this._transportReady) + if (!this._transportReady) { return; + } if (this._direction === 'send') { const offer = await this._pc.createOffer({ iceRestart: true }); logger.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); @@ -6325,53 +8674,84 @@ class ReactNative extends HandlerInterface_1.HandlerInterface { return this._pc.getStats(); } async send({ track, encodings, codecOptions, codec }) { - this._assertSendDirection(); + var _a; + this.assertSendDirection(); logger.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); - if (codec) { - logger.warn('send() | codec selection is not available in %s handler', this.name); + if (encodings && encodings.length > 1) { + encodings.forEach((encoding, idx) => { + encoding.rid = `r${idx}`; + }); } - this._sendStream.addTrack(track); - this._pc.addStream(this._sendStream); - let offer = await this._pc.createOffer(); - let localSdpObject = sdpTransform.parse(offer.sdp); - let offerMediaObject; const sendingRtpParameters = utils.clone(this._sendingRtpParametersByKind[track.kind], {}); + // This may throw. sendingRtpParameters.codecs = - ortc.reduceCodecs(sendingRtpParameters.codecs); + ortc.reduceCodecs(sendingRtpParameters.codecs, codec); const sendingRemoteRtpParameters = utils.clone(this._sendingRemoteRtpParametersByKind[track.kind], {}); + // This may throw. sendingRemoteRtpParameters.codecs = - ortc.reduceCodecs(sendingRemoteRtpParameters.codecs); - if (!this._transportReady) - await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); - if (track.kind === 'video' && encodings && encodings.length > 1) { - logger.debug('send() | enabling simulcast'); + ortc.reduceCodecs(sendingRemoteRtpParameters.codecs, codec); + const mediaSectionIdx = this._remoteSdp.getNextMediaSectionIdx(); + const transceiver = this._pc.addTransceiver(track, { + direction: 'sendonly', + streams: [this._sendStream], + sendEncodings: encodings + }); + let offer = await this._pc.createOffer(); + let localSdpObject = sdpTransform.parse(offer.sdp); + let offerMediaObject; + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); + } + // Special case for VP9 with SVC. + let hackVp9Svc = false; + const layers = (0, scalabilityModes_1.parse)((encodings || [{}])[0].scalabilityMode); + if (encodings && + encodings.length === 1 && + layers.spatialLayers > 1 && + sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp9') { + logger.debug('send() | enabling legacy simulcast for VP9 SVC'); + hackVp9Svc = true; localSdpObject = sdpTransform.parse(offer.sdp); - offerMediaObject = localSdpObject.media - .find((m) => m.type === 'video'); - sdpPlanBUtils.addLegacySimulcast({ + offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; + sdpUnifiedPlanUtils.addLegacySimulcast({ offerMediaObject, - track, - numStreams: encodings.length + numStreams: layers.spatialLayers }); offer = { type: 'offer', sdp: sdpTransform.write(localSdpObject) }; } logger.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); await this._pc.setLocalDescription(offer); + // We can now get the transceiver.mid. + const localId = transceiver.mid; + // Set MID. + sendingRtpParameters.mid = localId; localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); - offerMediaObject = localSdpObject.media - .find((m) => m.type === track.kind); + offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; // Set RTCP CNAME. sendingRtpParameters.rtcp.cname = sdpCommonUtils.getCname({ offerMediaObject }); - // Set RTP encodings. - sendingRtpParameters.encodings = - sdpPlanBUtils.getRtpEncodings({ offerMediaObject, track }); - // Complete encodings with given values. - if (encodings) { - for (let idx = 0; idx < sendingRtpParameters.encodings.length; ++idx) { - if (encodings[idx]) - Object.assign(sendingRtpParameters.encodings[idx], encodings[idx]); + // Set RTP encodings by parsing the SDP offer if no encodings are given. + if (!encodings) { + sendingRtpParameters.encodings = + sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject }); + } + // Set RTP encodings by parsing the SDP offer and complete them with given + // one if just a single encoding has been given. + else if (encodings.length === 1) { + let newEncodings = sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject }); + Object.assign(newEncodings[0], encodings[0]); + // Hack for VP9 SVC. + if (hackVp9Svc) { + newEncodings = [newEncodings[0]]; } + sendingRtpParameters.encodings = newEncodings; + } + // Otherwise if more than 1 encoding are given use them verbatim. + else { + sendingRtpParameters.encodings = encodings; } // If VP8 or H264 and there is effective simulcast, add scalabilityMode to // each encoding. @@ -6379,81 +8759,164 @@ class ReactNative extends HandlerInterface_1.HandlerInterface { (sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8' || sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/h264')) { for (const encoding of sendingRtpParameters.encodings) { - encoding.scalabilityMode = 'S1T3'; + if (encoding.scalabilityMode) { + encoding.scalabilityMode = `L1T${layers.temporalLayers}`; + } + else { + encoding.scalabilityMode = 'L1T3'; + } } } this._remoteSdp.send({ offerMediaObject, + reuseMid: mediaSectionIdx.reuseMid, offerRtpParameters: sendingRtpParameters, answerRtpParameters: sendingRemoteRtpParameters, - codecOptions + codecOptions, + extmapAllowMixed: true }); const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; logger.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer); await this._pc.setRemoteDescription(answer); - const localId = String(this._nextSendLocalId); - this._nextSendLocalId++; - // Insert into the map. - this._mapSendLocalIdTrack.set(localId, track); + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); return { - localId: localId, - rtpParameters: sendingRtpParameters + localId, + rtpParameters: sendingRtpParameters, + rtpSender: transceiver.sender }; } async stopSending(localId) { - this._assertSendDirection(); + this.assertSendDirection(); logger.debug('stopSending() [localId:%s]', localId); - const track = this._mapSendLocalIdTrack.get(localId); - if (!track) - throw new Error('track not found'); - this._mapSendLocalIdTrack.delete(localId); - this._sendStream.removeTrack(track); - this._pc.addStream(this._sendStream); - const offer = await this._pc.createOffer(); - logger.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); - try { - await this._pc.setLocalDescription(offer); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); } - catch (error) { - // NOTE: If there are no sending tracks, setLocalDescription() will fail with - // "Failed to create channels". If so, ignore it. - if (this._sendStream.getTracks().length === 0) { - logger.warn('stopSending() | ignoring expected error due no sending tracks: %s', error.toString()); - return; + transceiver.sender.replaceTrack(null); + this._pc.removeTrack(transceiver.sender); + const mediaSectionClosed = this._remoteSdp.closeMediaSection(transceiver.mid); + if (mediaSectionClosed) { + try { + transceiver.stop(); } - throw error; + catch (error) { } } - if (this._pc.signalingState === 'stable') - return; + const offer = await this._pc.createOffer(); + logger.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; logger.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); await this._pc.setRemoteDescription(answer); + this._mapMidTransceiver.delete(localId); } - async replaceTrack( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - localId, track) { - throw new errors_1.UnsupportedError('not implemented'); + async pauseSending(localId) { + this.assertSendDirection(); + logger.debug('pauseSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'inactive'; + this._remoteSdp.pauseMediaSection(localId); + const offer = await this._pc.createOffer(); + logger.debug('pauseSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('pauseSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async resumeSending(localId) { + this.assertSendDirection(); + logger.debug('resumeSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + this._remoteSdp.resumeSendingMediaSection(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'sendonly'; + const offer = await this._pc.createOffer(); + logger.debug('resumeSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('resumeSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + async replaceTrack(localId, track) { + this.assertSendDirection(); + if (track) { + logger.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id); + } + else { + logger.debug('replaceTrack() [localId:%s, no track]', localId); + } + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + await transceiver.sender.replaceTrack(track); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars async setMaxSpatialLayer(localId, spatialLayer) { - throw new errors_1.UnsupportedError('not implemented'); + this.assertSendDirection(); + logger.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + const parameters = transceiver.sender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + if (idx <= spatialLayer) { + encoding.active = true; + } + else { + encoding.active = false; + } + }); + await transceiver.sender.setParameters(parameters); + this._remoteSdp.muxMediaSectionSimulcast(localId, parameters.encodings); + const offer = await this._pc.createOffer(); + logger.debug('setMaxSpatialLayer() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('setMaxSpatialLayer() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars async setRtpEncodingParameters(localId, params) { - throw new errors_1.UnsupportedError('not implemented'); + this.assertSendDirection(); + logger.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + const parameters = transceiver.sender.getParameters(); + parameters.encodings.forEach((encoding, idx) => { + parameters.encodings[idx] = { ...encoding, ...params }; + }); + await transceiver.sender.setParameters(parameters); + this._remoteSdp.muxMediaSectionSimulcast(localId, parameters.encodings); + const offer = await this._pc.createOffer(); + logger.debug('setRtpEncodingParameters() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('setRtpEncodingParameters() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars async getSenderStats(localId) { - throw new errors_1.UnsupportedError('not implemented'); + this.assertSendDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + return transceiver.sender.getStats(); } async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { - this._assertSendDirection(); + var _a; + this.assertSendDirection(); const options = { negotiated: true, id: this._nextSendSctpStreamId, ordered, maxPacketLifeTime, - maxRetransmitTime: maxPacketLifeTime, maxRetransmits, protocol }; @@ -6469,8 +8932,12 @@ class ReactNative extends HandlerInterface_1.HandlerInterface { const localSdpObject = sdpTransform.parse(offer.sdp); const offerMediaObject = localSdpObject.media .find((m) => m.type === 'application'); - if (!this._transportReady) - await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); + } logger.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); await this._pc.setLocalDescription(offer); this._remoteSdp.sendSctpAssociation({ offerMediaObject }); @@ -6487,79 +8954,143 @@ class ReactNative extends HandlerInterface_1.HandlerInterface { }; return { dataChannel, sctpStreamParameters }; } - async receive({ trackId, kind, rtpParameters }) { - this._assertRecvDirection(); - logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); - const localId = trackId; - const mid = kind; - let streamId = rtpParameters.rtcp.cname; - // NOTE: In React-Native we cannot reuse the same remote MediaStream for new - // remote tracks. This is because react-native-webrtc does not react on new - // tracks generated within already existing streams, so force the streamId - // to be different. - logger.debug('receive() | forcing a random remote streamId to avoid well known bug in react-native-webrtc'); - streamId += `-hack-${utils.generateRandomNumber()}`; - this._remoteSdp.receive({ - mid, - kind, - offerRtpParameters: rtpParameters, - streamId, - trackId - }); + async receive(optionsList) { + var _a; + this.assertRecvDirection(); + const results = []; + const mapLocalId = new Map(); + for (const options of optionsList) { + const { trackId, kind, rtpParameters, streamId } = options; + logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const localId = rtpParameters.mid || String(this._mapMidTransceiver.size); + mapLocalId.set(trackId, localId); + this._remoteSdp.receive({ + mid: localId, + kind, + offerRtpParameters: rtpParameters, + streamId: streamId || rtpParameters.rtcp.cname, + trackId + }); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + let answer = await this._pc.createAnswer(); + const localSdpObject = sdpTransform.parse(answer.sdp); + for (const options of optionsList) { + const { trackId, rtpParameters } = options; + const localId = mapLocalId.get(trackId); + const answerMediaObject = localSdpObject.media + .find((m) => String(m.mid) === localId); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject + }); + } + answer = { type: 'answer', sdp: sdpTransform.write(localSdpObject) }; + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); + } + logger.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + for (const options of optionsList) { + const { trackId } = options; + const localId = mapLocalId.get(trackId); + const transceiver = this._pc.getTransceivers() + .find((t) => t.mid === localId); + if (!transceiver) { + throw new Error('new RTCRtpTransceiver not found'); + } + else { + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + results.push({ + localId, + track: transceiver.receiver.track, + rtpReceiver: transceiver.receiver + }); + } + } + return results; + } + async stopReceiving(localIds) { + this.assertRecvDirection(); + for (const localId of localIds) { + logger.debug('stopReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + this._remoteSdp.closeMediaSection(transceiver.mid); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + for (const localId of localIds) { + this._mapMidTransceiver.delete(localId); + } + } + async pauseReceiving(localIds) { + this.assertRecvDirection(); + for (const localId of localIds) { + logger.debug('pauseReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'inactive'; + this._remoteSdp.pauseMediaSection(localId); + } const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; - logger.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); + logger.debug('pauseReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); await this._pc.setRemoteDescription(offer); - let answer = await this._pc.createAnswer(); - const localSdpObject = sdpTransform.parse(answer.sdp); - const answerMediaObject = localSdpObject.media - .find((m) => String(m.mid) === mid); - // May need to modify codec parameters in the answer based on codec - // parameters in the offer. - sdpCommonUtils.applyCodecParameters({ - offerRtpParameters: rtpParameters, - answerMediaObject - }); - answer = { type: 'answer', sdp: sdpTransform.write(localSdpObject) }; - if (!this._transportReady) - await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); - logger.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); + const answer = await this._pc.createAnswer(); + logger.debug('pauseReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); await this._pc.setLocalDescription(answer); - const stream = this._pc.getRemoteStreams() - .find((s) => s.id === streamId); - const track = stream.getTrackById(localId); - if (!track) - throw new Error('remote track not found'); - // Insert into the map. - this._mapRecvLocalIdInfo.set(localId, { mid, rtpParameters }); - return { localId, track }; - } - async stopReceiving(localId) { - this._assertRecvDirection(); - logger.debug('stopReceiving() [localId:%s]', localId); - const { mid, rtpParameters } = this._mapRecvLocalIdInfo.get(localId) || {}; - // Remove from the map. - this._mapRecvLocalIdInfo.delete(localId); - this._remoteSdp.planBStopReceiving({ mid: mid, offerRtpParameters: rtpParameters }); + } + async resumeReceiving(localIds) { + this.assertRecvDirection(); + for (const localId of localIds) { + logger.debug('resumeReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'recvonly'; + this._remoteSdp.resumeReceivingMediaSection(localId); + } const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; - logger.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + logger.debug('resumeReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); await this._pc.setRemoteDescription(offer); const answer = await this._pc.createAnswer(); - logger.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + logger.debug('resumeReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); await this._pc.setLocalDescription(answer); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars async getReceiverStats(localId) { - throw new errors_1.UnsupportedError('not implemented'); + this.assertRecvDirection(); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + return transceiver.receiver.getStats(); } async receiveDataChannel({ sctpStreamParameters, label, protocol }) { - this._assertRecvDirection(); + var _a; + this.assertRecvDirection(); const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; const options = { negotiated: true, id: streamId, ordered, maxPacketLifeTime, - maxRetransmitTime: maxPacketLifeTime, maxRetransmits, protocol }; @@ -6568,14 +9099,17 @@ class ReactNative extends HandlerInterface_1.HandlerInterface { // If this is the first DataChannel we need to create the SDP offer with // m=application section. if (!this._hasDataChannelMediaSection) { - this._remoteSdp.receiveSctpAssociation({ oldDataChannelSpec: true }); + this._remoteSdp.receiveSctpAssociation(); const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [offer:%o]', offer); await this._pc.setRemoteDescription(offer); const answer = await this._pc.createAnswer(); if (!this._transportReady) { const localSdpObject = sdpTransform.parse(answer.sdp); - await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); } logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); await this._pc.setLocalDescription(answer); @@ -6583,9 +9117,10 @@ class ReactNative extends HandlerInterface_1.HandlerInterface { } return { dataChannel }; } - async _setupTransport({ localDtlsRole, localSdpObject }) { - if (!localSdpObject) + async setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) { localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + } // Get our local DTLS parameters. const dtlsParameters = sdpCommonUtils.extractDtlsParameters({ sdpObject: localSdpObject }); // Set our DTLS role. @@ -6593,27 +9128,33 @@ class ReactNative extends HandlerInterface_1.HandlerInterface { // Update the remote DTLS role in the SDP. this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); // Need to tell the remote transport about our parameters. - await this.safeEmitAsPromise('@connect', { dtlsParameters }); + await new Promise((resolve, reject) => { + this.safeEmit('@connect', { dtlsParameters }, resolve, reject); + }); this._transportReady = true; } - _assertSendDirection() { + assertSendDirection() { if (this._direction !== 'send') { throw new Error('method can just be called for handlers with "send" direction'); } } - _assertRecvDirection() { + assertRecvDirection() { if (this._direction !== 'recv') { throw new Error('method can just be called for handlers with "recv" direction'); } } } -exports.ReactNative = ReactNative; +exports.ReactNativeUnifiedPlan = ReactNativeUnifiedPlan; -},{"../Logger":13,"../errors":18,"../ortc":36,"../utils":39,"./HandlerInterface":25,"./sdp/RemoteSdp":31,"./sdp/commonUtils":32,"./sdp/planBUtils":33,"sdp-transform":44}],27:[function(require,module,exports){ +},{"../Logger":17,"../ortc":43,"../scalabilityModes":44,"../utils":46,"./HandlerInterface":30,"./ortc/utils":36,"./sdp/RemoteSdp":38,"./sdp/commonUtils":39,"./sdp/unifiedPlanUtils":41,"sdp-transform":52}],33:[function(require,module,exports){ "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; @@ -6626,7 +9167,7 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; @@ -6643,6 +9184,12 @@ const RemoteSdp_1 = require("./sdp/RemoteSdp"); const logger = new Logger_1.Logger('Safari11'); const SCTP_NUM_STREAMS = { OS: 1024, MIS: 1024 }; class Safari11 extends HandlerInterface_1.HandlerInterface { + /** + * Creates a factory function. + */ + static createFactory() { + return () => new Safari11(); + } constructor() { super(); // Local stream for sending. @@ -6661,12 +9208,6 @@ class Safari11 extends HandlerInterface_1.HandlerInterface { // Got transport local and remote parameters. this._transportReady = false; } - /** - * Creates a factory function. - */ - static createFactory() { - return () => new Safari11(); - } get name() { return 'Safari11'; } @@ -6679,6 +9220,7 @@ class Safari11 extends HandlerInterface_1.HandlerInterface { } catch (error) { } } + this.emit('@close'); } async getNativeRtpCapabilities() { logger.debug('getNativeRtpCapabilities()'); @@ -6736,28 +9278,46 @@ class Safari11 extends HandlerInterface_1.HandlerInterface { audio: ortc.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), video: ortc.getSendingRemoteRtpParameters('video', extendedRtpCapabilities) }; - this._pc = new RTCPeerConnection(Object.assign({ iceServers: iceServers || [], iceTransportPolicy: iceTransportPolicy || 'all', bundlePolicy: 'max-bundle', rtcpMuxPolicy: 'require' }, additionalSettings), proprietaryConstraints); - // Handle RTCPeerConnection connection status. - this._pc.addEventListener('iceconnectionstatechange', () => { - switch (this._pc.iceConnectionState) { - case 'checking': - this.emit('@connectionstatechange', 'connecting'); - break; - case 'connected': - case 'completed': - this.emit('@connectionstatechange', 'connected'); - break; - case 'failed': - this.emit('@connectionstatechange', 'failed'); - break; - case 'disconnected': - this.emit('@connectionstatechange', 'disconnected'); - break; - case 'closed': - this.emit('@connectionstatechange', 'closed'); - break; - } - }); + if (dtlsParameters.role && dtlsParameters.role !== 'auto') { + this._forcedLocalDtlsRole = dtlsParameters.role === 'server' + ? 'client' + : 'server'; + } + this._pc = new RTCPeerConnection({ + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + ...additionalSettings + }, proprietaryConstraints); + if (this._pc.connectionState) { + this._pc.addEventListener('connectionstatechange', () => { + this.emit('@connectionstatechange', this._pc.connectionState); + }); + } + else { + this._pc.addEventListener('iceconnectionstatechange', () => { + logger.warn('run() | pc.connectionState not supported, using pc.iceConnectionState'); + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } } async updateIceServers(iceServers) { logger.debug('updateIceServers()'); @@ -6769,8 +9329,9 @@ class Safari11 extends HandlerInterface_1.HandlerInterface { logger.debug('restartIce()'); // Provide the remote SDP handler with new remote ICE parameters. this._remoteSdp.updateIceParameters(iceParameters); - if (!this._transportReady) + if (!this._transportReady) { return; + } if (this._direction === 'send') { const offer = await this._pc.createOffer({ iceRestart: true }); logger.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); @@ -6792,7 +9353,8 @@ class Safari11 extends HandlerInterface_1.HandlerInterface { return this._pc.getStats(); } async send({ track, encodings, codecOptions, codec }) { - this._assertSendDirection(); + var _a; + this.assertSendDirection(); logger.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); if (codec) { logger.warn('send() | codec selection is not available in %s handler', this.name); @@ -6808,8 +9370,12 @@ class Safari11 extends HandlerInterface_1.HandlerInterface { const sendingRemoteRtpParameters = utils.clone(this._sendingRemoteRtpParametersByKind[track.kind], {}); sendingRemoteRtpParameters.codecs = ortc.reduceCodecs(sendingRemoteRtpParameters.codecs); - if (!this._transportReady) - await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); + } if (track.kind === 'video' && encodings && encodings.length > 1) { logger.debug('send() | enabling simulcast'); localSdpObject = sdpTransform.parse(offer.sdp); @@ -6835,8 +9401,9 @@ class Safari11 extends HandlerInterface_1.HandlerInterface { // Complete encodings with given values. if (encodings) { for (let idx = 0; idx < sendingRtpParameters.encodings.length; ++idx) { - if (encodings[idx]) + if (encodings[idx]) { Object.assign(sendingRtpParameters.encodings[idx], encodings[idx]); + } } } // If VP8 and there is effective simulcast, add scalabilityMode to each @@ -6844,7 +9411,7 @@ class Safari11 extends HandlerInterface_1.HandlerInterface { if (sendingRtpParameters.encodings.length > 1 && sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8') { for (const encoding of sendingRtpParameters.encodings) { - encoding.scalabilityMode = 'S1T3'; + encoding.scalabilityMode = 'L1T3'; } } this._remoteSdp.send({ @@ -6869,12 +9436,14 @@ class Safari11 extends HandlerInterface_1.HandlerInterface { }; } async stopSending(localId) { - this._assertSendDirection(); + this.assertSendDirection(); const rtpSender = this._mapSendLocalIdRtpSender.get(localId); - if (!rtpSender) + if (!rtpSender) { throw new Error('associated RTCRtpSender not found'); - if (rtpSender.track) + } + if (rtpSender.track) { this._sendStream.removeTrack(rtpSender.track); + } this._mapSendLocalIdRtpSender.delete(localId); const offer = await this._pc.createOffer(); logger.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); @@ -6890,14 +9459,23 @@ class Safari11 extends HandlerInterface_1.HandlerInterface { } throw error; } - if (this._pc.signalingState === 'stable') + if (this._pc.signalingState === 'stable') { return; + } const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; logger.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); await this._pc.setRemoteDescription(answer); } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async pauseSending(localId) { + // Unimplemented. + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async resumeSending(localId) { + // Unimplemented. + } async replaceTrack(localId, track) { - this._assertSendDirection(); + this.assertSendDirection(); if (track) { logger.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id); } @@ -6905,53 +9483,62 @@ class Safari11 extends HandlerInterface_1.HandlerInterface { logger.debug('replaceTrack() [localId:%s, no track]', localId); } const rtpSender = this._mapSendLocalIdRtpSender.get(localId); - if (!rtpSender) + if (!rtpSender) { throw new Error('associated RTCRtpSender not found'); + } const oldTrack = rtpSender.track; await rtpSender.replaceTrack(track); // Remove the old track from the local stream. - if (oldTrack) + if (oldTrack) { this._sendStream.removeTrack(oldTrack); + } // Add the new track to the local stream. - if (track) + if (track) { this._sendStream.addTrack(track); + } } async setMaxSpatialLayer(localId, spatialLayer) { - this._assertSendDirection(); + this.assertSendDirection(); logger.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer); const rtpSender = this._mapSendLocalIdRtpSender.get(localId); - if (!rtpSender) + if (!rtpSender) { throw new Error('associated RTCRtpSender not found'); + } const parameters = rtpSender.getParameters(); parameters.encodings.forEach((encoding, idx) => { - if (idx <= spatialLayer) + if (idx <= spatialLayer) { encoding.active = true; - else + } + else { encoding.active = false; + } }); await rtpSender.setParameters(parameters); } async setRtpEncodingParameters(localId, params) { - this._assertSendDirection(); + this.assertSendDirection(); logger.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params); const rtpSender = this._mapSendLocalIdRtpSender.get(localId); - if (!rtpSender) + if (!rtpSender) { throw new Error('associated RTCRtpSender not found'); + } const parameters = rtpSender.getParameters(); parameters.encodings.forEach((encoding, idx) => { - parameters.encodings[idx] = Object.assign(Object.assign({}, encoding), params); + parameters.encodings[idx] = { ...encoding, ...params }; }); await rtpSender.setParameters(parameters); } async getSenderStats(localId) { - this._assertSendDirection(); + this.assertSendDirection(); const rtpSender = this._mapSendLocalIdRtpSender.get(localId); - if (!rtpSender) + if (!rtpSender) { throw new Error('associated RTCRtpSender not found'); + } return rtpSender.getStats(); } async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { - this._assertSendDirection(); + var _a; + this.assertSendDirection(); const options = { negotiated: true, id: this._nextSendSctpStreamId, @@ -6972,8 +9559,12 @@ class Safari11 extends HandlerInterface_1.HandlerInterface { const localSdpObject = sdpTransform.parse(offer.sdp); const offerMediaObject = localSdpObject.media .find((m) => m.type === 'application'); - if (!this._transportReady) - await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); + } logger.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); await this._pc.setLocalDescription(offer); this._remoteSdp.sendSctpAssociation({ offerMediaObject }); @@ -6990,55 +9581,76 @@ class Safari11 extends HandlerInterface_1.HandlerInterface { }; return { dataChannel, sctpStreamParameters }; } - async receive({ trackId, kind, rtpParameters }) { - this._assertRecvDirection(); - logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); - const localId = trackId; - const mid = kind; - this._remoteSdp.receive({ - mid, - kind, - offerRtpParameters: rtpParameters, - streamId: rtpParameters.rtcp.cname, - trackId - }); + async receive(optionsList) { + var _a; + this.assertRecvDirection(); + const results = []; + for (const options of optionsList) { + const { trackId, kind, rtpParameters, streamId } = options; + logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const mid = kind; + this._remoteSdp.receive({ + mid, + kind, + offerRtpParameters: rtpParameters, + streamId: streamId || rtpParameters.rtcp.cname, + trackId + }); + } const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; logger.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); await this._pc.setRemoteDescription(offer); let answer = await this._pc.createAnswer(); const localSdpObject = sdpTransform.parse(answer.sdp); - const answerMediaObject = localSdpObject.media - .find((m) => String(m.mid) === mid); - // May need to modify codec parameters in the answer based on codec - // parameters in the offer. - sdpCommonUtils.applyCodecParameters({ - offerRtpParameters: rtpParameters, - answerMediaObject - }); + for (const options of optionsList) { + const { kind, rtpParameters } = options; + const mid = kind; + const answerMediaObject = localSdpObject.media + .find((m) => String(m.mid) === mid); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject + }); + } answer = { type: 'answer', sdp: sdpTransform.write(localSdpObject) }; - if (!this._transportReady) - await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); + } logger.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); await this._pc.setLocalDescription(answer); - const rtpReceiver = this._pc.getReceivers() - .find((r) => r.track && r.track.id === localId); - if (!rtpReceiver) - throw new Error('new RTCRtpReceiver not'); - // Insert into the map. - this._mapRecvLocalIdInfo.set(localId, { mid, rtpParameters, rtpReceiver }); - return { - localId, - track: rtpReceiver.track, - rtpReceiver - }; + for (const options of optionsList) { + const { kind, trackId, rtpParameters } = options; + const mid = kind; + const localId = trackId; + const rtpReceiver = this._pc.getReceivers() + .find((r) => r.track && r.track.id === localId); + if (!rtpReceiver) { + throw new Error('new RTCRtpReceiver not'); + } + // Insert into the map. + this._mapRecvLocalIdInfo.set(localId, { mid, rtpParameters, rtpReceiver }); + results.push({ + localId, + track: rtpReceiver.track, + rtpReceiver + }); + } + return results; } - async stopReceiving(localId) { - this._assertRecvDirection(); - logger.debug('stopReceiving() [localId:%s]', localId); - const { mid, rtpParameters } = this._mapRecvLocalIdInfo.get(localId) || {}; - // Remove from the map. - this._mapRecvLocalIdInfo.delete(localId); - this._remoteSdp.planBStopReceiving({ mid: mid, offerRtpParameters: rtpParameters }); + async stopReceiving(localIds) { + this.assertRecvDirection(); + for (const localId of localIds) { + logger.debug('stopReceiving() [localId:%s]', localId); + const { mid, rtpParameters } = this._mapRecvLocalIdInfo.get(localId) || {}; + // Remove from the map. + this._mapRecvLocalIdInfo.delete(localId); + this._remoteSdp.planBStopReceiving({ mid: mid, offerRtpParameters: rtpParameters }); + } const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; logger.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); await this._pc.setRemoteDescription(offer); @@ -7047,14 +9659,26 @@ class Safari11 extends HandlerInterface_1.HandlerInterface { await this._pc.setLocalDescription(answer); } async getReceiverStats(localId) { - this._assertRecvDirection(); + this.assertRecvDirection(); const { rtpReceiver } = this._mapRecvLocalIdInfo.get(localId) || {}; - if (!rtpReceiver) + if (!rtpReceiver) { throw new Error('associated RTCRtpReceiver not found'); + } return rtpReceiver.getStats(); } + async pauseReceiving( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localIds) { + // Unimplemented. + } + async resumeReceiving( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + localIds) { + // Unimplemented. + } async receiveDataChannel({ sctpStreamParameters, label, protocol }) { - this._assertRecvDirection(); + var _a; + this.assertRecvDirection(); const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; const options = { negotiated: true, @@ -7076,7 +9700,10 @@ class Safari11 extends HandlerInterface_1.HandlerInterface { const answer = await this._pc.createAnswer(); if (!this._transportReady) { const localSdpObject = sdpTransform.parse(answer.sdp); - await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); } logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); await this._pc.setLocalDescription(answer); @@ -7084,9 +9711,10 @@ class Safari11 extends HandlerInterface_1.HandlerInterface { } return { dataChannel }; } - async _setupTransport({ localDtlsRole, localSdpObject }) { - if (!localSdpObject) + async setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) { localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + } // Get our local DTLS parameters. const dtlsParameters = sdpCommonUtils.extractDtlsParameters({ sdpObject: localSdpObject }); // Set our DTLS role. @@ -7094,15 +9722,17 @@ class Safari11 extends HandlerInterface_1.HandlerInterface { // Update the remote DTLS role in the SDP. this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); // Need to tell the remote transport about our parameters. - await this.safeEmitAsPromise('@connect', { dtlsParameters }); + await new Promise((resolve, reject) => { + this.safeEmit('@connect', { dtlsParameters }, resolve, reject); + }); this._transportReady = true; } - _assertSendDirection() { + assertSendDirection() { if (this._direction !== 'send') { throw new Error('method can just be called for handlers with "send" direction'); } } - _assertRecvDirection() { + assertRecvDirection() { if (this._direction !== 'recv') { throw new Error('method can just be called for handlers with "recv" direction'); } @@ -7110,11 +9740,15 @@ class Safari11 extends HandlerInterface_1.HandlerInterface { } exports.Safari11 = Safari11; -},{"../Logger":13,"../ortc":36,"../utils":39,"./HandlerInterface":25,"./sdp/RemoteSdp":31,"./sdp/commonUtils":32,"./sdp/planBUtils":33,"sdp-transform":44}],28:[function(require,module,exports){ +},{"../Logger":17,"../ortc":43,"../utils":46,"./HandlerInterface":30,"./sdp/RemoteSdp":38,"./sdp/commonUtils":39,"./sdp/planBUtils":40,"sdp-transform":52}],34:[function(require,module,exports){ "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; @@ -7127,7 +9761,7 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; @@ -7139,11 +9773,19 @@ const utils = __importStar(require("../utils")); const ortc = __importStar(require("../ortc")); const sdpCommonUtils = __importStar(require("./sdp/commonUtils")); const sdpUnifiedPlanUtils = __importStar(require("./sdp/unifiedPlanUtils")); +const ortcUtils = __importStar(require("./ortc/utils")); const HandlerInterface_1 = require("./HandlerInterface"); const RemoteSdp_1 = require("./sdp/RemoteSdp"); +const scalabilityModes_1 = require("../scalabilityModes"); const logger = new Logger_1.Logger('Safari12'); const SCTP_NUM_STREAMS = { OS: 1024, MIS: 1024 }; class Safari12 extends HandlerInterface_1.HandlerInterface { + /** + * Creates a factory function. + */ + static createFactory() { + return () => new Safari12(); + } constructor() { super(); // Map of RTCTransceivers indexed by MID. @@ -7157,12 +9799,6 @@ class Safari12 extends HandlerInterface_1.HandlerInterface { // Got transport local and remote parameters. this._transportReady = false; } - /** - * Creates a factory function. - */ - static createFactory() { - return () => new Safari12(); - } get name() { return 'Safari12'; } @@ -7175,6 +9811,7 @@ class Safari12 extends HandlerInterface_1.HandlerInterface { } catch (error) { } } + this.emit('@close'); } async getNativeRtpCapabilities() { logger.debug('getNativeRtpCapabilities()'); @@ -7194,6 +9831,8 @@ class Safari12 extends HandlerInterface_1.HandlerInterface { catch (error) { } const sdpObject = sdpTransform.parse(offer.sdp); const nativeRtpCapabilities = sdpCommonUtils.extractRtpCapabilities({ sdpObject }); + // libwebrtc supports NACK for OPUS but doesn't announce it. + ortcUtils.addNackSuppportForOpus(nativeRtpCapabilities); return nativeRtpCapabilities; } catch (error) { @@ -7229,28 +9868,46 @@ class Safari12 extends HandlerInterface_1.HandlerInterface { audio: ortc.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), video: ortc.getSendingRemoteRtpParameters('video', extendedRtpCapabilities) }; - this._pc = new RTCPeerConnection(Object.assign({ iceServers: iceServers || [], iceTransportPolicy: iceTransportPolicy || 'all', bundlePolicy: 'max-bundle', rtcpMuxPolicy: 'require' }, additionalSettings), proprietaryConstraints); - // Handle RTCPeerConnection connection status. - this._pc.addEventListener('iceconnectionstatechange', () => { - switch (this._pc.iceConnectionState) { - case 'checking': - this.emit('@connectionstatechange', 'connecting'); - break; - case 'connected': - case 'completed': - this.emit('@connectionstatechange', 'connected'); - break; - case 'failed': - this.emit('@connectionstatechange', 'failed'); - break; - case 'disconnected': - this.emit('@connectionstatechange', 'disconnected'); - break; - case 'closed': - this.emit('@connectionstatechange', 'closed'); - break; - } - }); + if (dtlsParameters.role && dtlsParameters.role !== 'auto') { + this._forcedLocalDtlsRole = dtlsParameters.role === 'server' + ? 'client' + : 'server'; + } + this._pc = new RTCPeerConnection({ + iceServers: iceServers || [], + iceTransportPolicy: iceTransportPolicy || 'all', + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + ...additionalSettings + }, proprietaryConstraints); + if (this._pc.connectionState) { + this._pc.addEventListener('connectionstatechange', () => { + this.emit('@connectionstatechange', this._pc.connectionState); + }); + } + else { + this._pc.addEventListener('iceconnectionstatechange', () => { + logger.warn('run() | pc.connectionState not supported, using pc.iceConnectionState'); + switch (this._pc.iceConnectionState) { + case 'checking': + this.emit('@connectionstatechange', 'connecting'); + break; + case 'connected': + case 'completed': + this.emit('@connectionstatechange', 'connected'); + break; + case 'failed': + this.emit('@connectionstatechange', 'failed'); + break; + case 'disconnected': + this.emit('@connectionstatechange', 'disconnected'); + break; + case 'closed': + this.emit('@connectionstatechange', 'closed'); + break; + } + }); + } } async updateIceServers(iceServers) { logger.debug('updateIceServers()'); @@ -7262,8 +9919,9 @@ class Safari12 extends HandlerInterface_1.HandlerInterface { logger.debug('restartIce()'); // Provide the remote SDP handler with new remote ICE parameters. this._remoteSdp.updateIceParameters(iceParameters); - if (!this._transportReady) + if (!this._transportReady) { return; + } if (this._direction === 'send') { const offer = await this._pc.createOffer({ iceRestart: true }); logger.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); @@ -7285,7 +9943,8 @@ class Safari12 extends HandlerInterface_1.HandlerInterface { return this._pc.getStats(); } async send({ track, encodings, codecOptions, codec }) { - this._assertSendDirection(); + var _a; + this.assertSendDirection(); logger.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); const sendingRtpParameters = utils.clone(this._sendingRtpParametersByKind[track.kind], {}); // This may throw. @@ -7300,8 +9959,13 @@ class Safari12 extends HandlerInterface_1.HandlerInterface { let offer = await this._pc.createOffer(); let localSdpObject = sdpTransform.parse(offer.sdp); let offerMediaObject; - if (!this._transportReady) - await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); + } + const layers = (0, scalabilityModes_1.parse)((encodings || [{}])[0].scalabilityMode); if (encodings && encodings.length > 1) { logger.debug('send() | enabling legacy simulcast'); localSdpObject = sdpTransform.parse(offer.sdp); @@ -7329,8 +9993,9 @@ class Safari12 extends HandlerInterface_1.HandlerInterface { // Complete encodings with given values. if (encodings) { for (let idx = 0; idx < sendingRtpParameters.encodings.length; ++idx) { - if (encodings[idx]) + if (encodings[idx]) { Object.assign(sendingRtpParameters.encodings[idx], encodings[idx]); + } } } // If VP8 or H264 and there is effective simulcast, add scalabilityMode to @@ -7339,7 +10004,12 @@ class Safari12 extends HandlerInterface_1.HandlerInterface { (sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8' || sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/h264')) { for (const encoding of sendingRtpParameters.encodings) { - encoding.scalabilityMode = 'S1T3'; + if (encoding.scalabilityMode) { + encoding.scalabilityMode = `L1T${layers.temporalLayers}`; + } + else { + encoding.scalabilityMode = 'L1T3'; + } } } this._remoteSdp.send({ @@ -7361,23 +10031,65 @@ class Safari12 extends HandlerInterface_1.HandlerInterface { }; } async stopSending(localId) { - this._assertSendDirection(); + this.assertSendDirection(); logger.debug('stopSending() [localId:%s]', localId); const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) + if (!transceiver) { throw new Error('associated RTCRtpTransceiver not found'); + } transceiver.sender.replaceTrack(null); this._pc.removeTrack(transceiver.sender); - this._remoteSdp.closeMediaSection(transceiver.mid); + const mediaSectionClosed = this._remoteSdp.closeMediaSection(transceiver.mid); + if (mediaSectionClosed) { + try { + transceiver.stop(); + } + catch (error) { } + } const offer = await this._pc.createOffer(); logger.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); await this._pc.setLocalDescription(offer); const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; logger.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); await this._pc.setRemoteDescription(answer); + this._mapMidTransceiver.delete(localId); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async pauseSending(localId) { + this.assertSendDirection(); + logger.debug('pauseSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'inactive'; + this._remoteSdp.pauseMediaSection(localId); + const offer = await this._pc.createOffer(); + logger.debug('pauseSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('pauseSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async resumeSending(localId) { + this.assertSendDirection(); + logger.debug('resumeSending() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'sendonly'; + this._remoteSdp.resumeSendingMediaSection(localId); + const offer = await this._pc.createOffer(); + logger.debug('resumeSending() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('resumeSending() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); } async replaceTrack(localId, track) { - this._assertSendDirection(); + this.assertSendDirection(); if (track) { logger.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id); } @@ -7385,46 +10097,67 @@ class Safari12 extends HandlerInterface_1.HandlerInterface { logger.debug('replaceTrack() [localId:%s, no track]', localId); } const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) + if (!transceiver) { throw new Error('associated RTCRtpTransceiver not found'); + } await transceiver.sender.replaceTrack(track); } async setMaxSpatialLayer(localId, spatialLayer) { - this._assertSendDirection(); + this.assertSendDirection(); logger.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer); const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) + if (!transceiver) { throw new Error('associated RTCRtpTransceiver not found'); + } const parameters = transceiver.sender.getParameters(); parameters.encodings.forEach((encoding, idx) => { - if (idx <= spatialLayer) + if (idx <= spatialLayer) { encoding.active = true; - else + } + else { encoding.active = false; + } }); await transceiver.sender.setParameters(parameters); + this._remoteSdp.muxMediaSectionSimulcast(localId, parameters.encodings); + const offer = await this._pc.createOffer(); + logger.debug('setMaxSpatialLayer() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('setMaxSpatialLayer() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); } async setRtpEncodingParameters(localId, params) { - this._assertSendDirection(); + this.assertSendDirection(); logger.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params); const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) + if (!transceiver) { throw new Error('associated RTCRtpTransceiver not found'); + } const parameters = transceiver.sender.getParameters(); parameters.encodings.forEach((encoding, idx) => { - parameters.encodings[idx] = Object.assign(Object.assign({}, encoding), params); + parameters.encodings[idx] = { ...encoding, ...params }; }); await transceiver.sender.setParameters(parameters); + this._remoteSdp.muxMediaSectionSimulcast(localId, parameters.encodings); + const offer = await this._pc.createOffer(); + logger.debug('setRtpEncodingParameters() | calling pc.setLocalDescription() [offer:%o]', offer); + await this._pc.setLocalDescription(offer); + const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() }; + logger.debug('setRtpEncodingParameters() | calling pc.setRemoteDescription() [answer:%o]', answer); + await this._pc.setRemoteDescription(answer); } async getSenderStats(localId) { - this._assertSendDirection(); + this.assertSendDirection(); const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) + if (!transceiver) { throw new Error('associated RTCRtpTransceiver not found'); + } return transceiver.sender.getStats(); } async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol }) { - this._assertSendDirection(); + var _a; + this.assertSendDirection(); const options = { negotiated: true, id: this._nextSendSctpStreamId, @@ -7445,8 +10178,12 @@ class Safari12 extends HandlerInterface_1.HandlerInterface { const localSdpObject = sdpTransform.parse(offer.sdp); const offerMediaObject = localSdpObject.media .find((m) => m.type === 'application'); - if (!this._transportReady) - await this._setupTransport({ localDtlsRole: 'server', localSdpObject }); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); + } logger.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); await this._pc.setLocalDescription(offer); this._remoteSdp.sendSctpAssociation({ offerMediaObject }); @@ -7463,70 +10200,135 @@ class Safari12 extends HandlerInterface_1.HandlerInterface { }; return { dataChannel, sctpStreamParameters }; } - async receive({ trackId, kind, rtpParameters }) { - this._assertRecvDirection(); - logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); - const localId = rtpParameters.mid || String(this._mapMidTransceiver.size); - this._remoteSdp.receive({ - mid: localId, - kind, - offerRtpParameters: rtpParameters, - streamId: rtpParameters.rtcp.cname, - trackId - }); + async receive(optionsList) { + var _a; + this.assertRecvDirection(); + const results = []; + const mapLocalId = new Map(); + for (const options of optionsList) { + const { trackId, kind, rtpParameters, streamId } = options; + logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); + const localId = rtpParameters.mid || String(this._mapMidTransceiver.size); + mapLocalId.set(trackId, localId); + this._remoteSdp.receive({ + mid: localId, + kind, + offerRtpParameters: rtpParameters, + streamId: streamId || rtpParameters.rtcp.cname, + trackId + }); + } const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; logger.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); await this._pc.setRemoteDescription(offer); let answer = await this._pc.createAnswer(); const localSdpObject = sdpTransform.parse(answer.sdp); - const answerMediaObject = localSdpObject.media - .find((m) => String(m.mid) === localId); - // May need to modify codec parameters in the answer based on codec - // parameters in the offer. - sdpCommonUtils.applyCodecParameters({ - offerRtpParameters: rtpParameters, - answerMediaObject - }); + for (const options of optionsList) { + const { trackId, rtpParameters } = options; + const localId = mapLocalId.get(trackId); + const answerMediaObject = localSdpObject.media + .find((m) => String(m.mid) === localId); + // May need to modify codec parameters in the answer based on codec + // parameters in the offer. + sdpCommonUtils.applyCodecParameters({ + offerRtpParameters: rtpParameters, + answerMediaObject + }); + } answer = { type: 'answer', sdp: sdpTransform.write(localSdpObject) }; - if (!this._transportReady) - await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + if (!this._transportReady) { + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); + } logger.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); await this._pc.setLocalDescription(answer); - const transceiver = this._pc.getTransceivers() - .find((t) => t.mid === localId); - if (!transceiver) - throw new Error('new RTCRtpTransceiver not found'); - // Store in the map. - this._mapMidTransceiver.set(localId, transceiver); - return { - localId, - track: transceiver.receiver.track, - rtpReceiver: transceiver.receiver - }; + for (const options of optionsList) { + const { trackId } = options; + const localId = mapLocalId.get(trackId); + const transceiver = this._pc.getTransceivers() + .find((t) => t.mid === localId); + if (!transceiver) { + throw new Error('new RTCRtpTransceiver not found'); + } + // Store in the map. + this._mapMidTransceiver.set(localId, transceiver); + results.push({ + localId, + track: transceiver.receiver.track, + rtpReceiver: transceiver.receiver + }); + } + return results; } - async stopReceiving(localId) { - this._assertRecvDirection(); - logger.debug('stopReceiving() [localId:%s]', localId); - const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) - throw new Error('associated RTCRtpTransceiver not found'); - this._remoteSdp.closeMediaSection(transceiver.mid); + async stopReceiving(localIds) { + this.assertRecvDirection(); + for (const localId of localIds) { + logger.debug('stopReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + this._remoteSdp.closeMediaSection(transceiver.mid); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + for (const localId of localIds) { + this._mapMidTransceiver.delete(localId); + } + } + async pauseReceiving(localIds) { + this.assertRecvDirection(); + for (const localId of localIds) { + logger.debug('pauseReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'inactive'; + this._remoteSdp.pauseMediaSection(localId); + } + const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; + logger.debug('pauseReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + await this._pc.setRemoteDescription(offer); + const answer = await this._pc.createAnswer(); + logger.debug('pauseReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + await this._pc.setLocalDescription(answer); + } + async resumeReceiving(localIds) { + this.assertRecvDirection(); + for (const localId of localIds) { + logger.debug('resumeReceiving() [localId:%s]', localId); + const transceiver = this._mapMidTransceiver.get(localId); + if (!transceiver) { + throw new Error('associated RTCRtpTransceiver not found'); + } + transceiver.direction = 'recvonly'; + this._remoteSdp.resumeReceivingMediaSection(localId); + } const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() }; - logger.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); + logger.debug('resumeReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); await this._pc.setRemoteDescription(offer); const answer = await this._pc.createAnswer(); - logger.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); + logger.debug('resumeReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); await this._pc.setLocalDescription(answer); } async getReceiverStats(localId) { - this._assertRecvDirection(); + this.assertRecvDirection(); const transceiver = this._mapMidTransceiver.get(localId); - if (!transceiver) + if (!transceiver) { throw new Error('associated RTCRtpTransceiver not found'); + } return transceiver.receiver.getStats(); } async receiveDataChannel({ sctpStreamParameters, label, protocol }) { - this._assertRecvDirection(); + var _a; + this.assertRecvDirection(); const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters; const options = { negotiated: true, @@ -7548,7 +10350,10 @@ class Safari12 extends HandlerInterface_1.HandlerInterface { const answer = await this._pc.createAnswer(); if (!this._transportReady) { const localSdpObject = sdpTransform.parse(answer.sdp); - await this._setupTransport({ localDtlsRole: 'client', localSdpObject }); + await this.setupTransport({ + localDtlsRole: (_a = this._forcedLocalDtlsRole) !== null && _a !== void 0 ? _a : 'client', + localSdpObject + }); } logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); await this._pc.setLocalDescription(answer); @@ -7556,9 +10361,10 @@ class Safari12 extends HandlerInterface_1.HandlerInterface { } return { dataChannel }; } - async _setupTransport({ localDtlsRole, localSdpObject }) { - if (!localSdpObject) + async setupTransport({ localDtlsRole, localSdpObject }) { + if (!localSdpObject) { localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); + } // Get our local DTLS parameters. const dtlsParameters = sdpCommonUtils.extractDtlsParameters({ sdpObject: localSdpObject }); // Set our DTLS role. @@ -7566,15 +10372,17 @@ class Safari12 extends HandlerInterface_1.HandlerInterface { // Update the remote DTLS role in the SDP. this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); // Need to tell the remote transport about our parameters. - await this.safeEmitAsPromise('@connect', { dtlsParameters }); + await new Promise((resolve, reject) => { + this.safeEmit('@connect', { dtlsParameters }, resolve, reject); + }); this._transportReady = true; } - _assertSendDirection() { + assertSendDirection() { if (this._direction !== 'send') { throw new Error('method can just be called for handlers with "send" direction'); } } - _assertRecvDirection() { + assertRecvDirection() { if (this._direction !== 'recv') { throw new Error('method can just be called for handlers with "recv" direction'); } @@ -7582,11 +10390,15 @@ class Safari12 extends HandlerInterface_1.HandlerInterface { } exports.Safari12 = Safari12; -},{"../Logger":13,"../ortc":36,"../utils":39,"./HandlerInterface":25,"./sdp/RemoteSdp":31,"./sdp/commonUtils":32,"./sdp/unifiedPlanUtils":34,"sdp-transform":44}],29:[function(require,module,exports){ +},{"../Logger":17,"../ortc":43,"../scalabilityModes":44,"../utils":46,"./HandlerInterface":30,"./ortc/utils":36,"./sdp/RemoteSdp":38,"./sdp/commonUtils":39,"./sdp/unifiedPlanUtils":41,"sdp-transform":52}],35:[function(require,module,exports){ "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; @@ -7599,7 +10411,7 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; @@ -7622,15 +10434,18 @@ function getCapabilities() { // NOTE: Edge sets some numeric parameters as string rather than number. Fix them. if (codec.parameters) { const parameters = codec.parameters; - if (parameters.apt) + if (parameters.apt) { parameters.apt = Number(parameters.apt); - if (parameters['packetization-mode']) + } + if (parameters['packetization-mode']) { parameters['packetization-mode'] = Number(parameters['packetization-mode']); + } } // Delete emty parameter String in rtcpFeedback. for (const feedback of codec.rtcpFeedback || []) { - if (!feedback.parameter) + if (!feedback.parameter) { feedback.parameter = ''; + } } } return caps; @@ -7653,8 +10468,9 @@ function mangleRtpParameters(rtpParameters) { delete codec.channels; } // Add codec.name (requried by Edge). - if (codec.mimeType && !codec.name) + if (codec.mimeType && !codec.name) { codec.name = codec.mimeType.split('/')[1]; + } // Remove mimeType. delete codec.mimeType; } @@ -7662,11 +10478,37 @@ function mangleRtpParameters(rtpParameters) { } exports.mangleRtpParameters = mangleRtpParameters; -},{"../../utils":39}],30:[function(require,module,exports){ +},{"../../utils":46}],36:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.addNackSuppportForOpus = void 0; +/** + * This function adds RTCP NACK support for OPUS codec in given capabilities. + */ +function addNackSuppportForOpus(rtpCapabilities) { + var _a; + for (const codec of (rtpCapabilities.codecs || [])) { + if ((codec.mimeType.toLowerCase() === 'audio/opus' || + codec.mimeType.toLowerCase() === 'audio/multiopus') && + !((_a = codec.rtcpFeedback) === null || _a === void 0 ? void 0 : _a.some((fb) => fb.type === 'nack' && !fb.parameter))) { + if (!codec.rtcpFeedback) { + codec.rtcpFeedback = []; + } + codec.rtcpFeedback.push({ type: 'nack' }); + } + } +} +exports.addNackSuppportForOpus = addNackSuppportForOpus; + +},{}],37:[function(require,module,exports){ "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; @@ -7679,12 +10521,13 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.OfferMediaSection = exports.AnswerMediaSection = exports.MediaSection = void 0; +const sdpTransform = __importStar(require("sdp-transform")); const utils = __importStar(require("../../utils")); class MediaSection { constructor({ iceParameters, iceCandidates, dtlsParameters, planB = false }) { @@ -7706,8 +10549,9 @@ class MediaSection { candidateObject.priority = candidate.priority; candidateObject.transport = candidate.protocol; candidateObject.type = candidate.type; - if (candidate.tcpType) + if (candidate.tcpType) { candidateObject.tcptype = candidate.tcpType; + } this._mediaObject.candidates.push(candidateObject); } this._mediaObject.endOfCandidates = 'end-of-candidates'; @@ -7730,25 +10574,22 @@ class MediaSection { this._mediaObject.iceUfrag = iceParameters.usernameFragment; this._mediaObject.icePwd = iceParameters.password; } - disable() { + pause() { this._mediaObject.direction = 'inactive'; + } + disable() { + this.pause(); delete this._mediaObject.ext; delete this._mediaObject.ssrcs; delete this._mediaObject.ssrcGroups; delete this._mediaObject.simulcast; delete this._mediaObject.simulcast_03; delete this._mediaObject.rids; + delete this._mediaObject.extmapAllowMixed; } close() { - this._mediaObject.direction = 'inactive'; + this.disable(); this._mediaObject.port = 0; - delete this._mediaObject.ext; - delete this._mediaObject.ssrcs; - delete this._mediaObject.ssrcGroups; - delete this._mediaObject.simulcast; - delete this._mediaObject.simulcast_03; - delete this._mediaObject.rids; - delete this._mediaObject.extmapAllowMixed; } } exports.MediaSection = MediaSection; @@ -7784,16 +10625,19 @@ class AnswerMediaSection extends MediaSection { codec: getCodecName(codec), rate: codec.clockRate }; - if (codec.channels > 1) + if (codec.channels > 1) { rtp.encoding = codec.channels; + } this._mediaObject.rtp.push(rtp); const codecParameters = utils.clone(codec.parameters, {}); + let codecRtcpFeedback = utils.clone(codec.rtcpFeedback, []); if (codecOptions) { - const { opusStereo, opusFec, opusDtx, opusMaxPlaybackRate, opusMaxAverageBitrate, opusPtime, videoGoogleStartBitrate, videoGoogleMaxBitrate, videoGoogleMinBitrate } = codecOptions; + const { opusStereo, opusFec, opusDtx, opusMaxPlaybackRate, opusMaxAverageBitrate, opusPtime, opusNack, videoGoogleStartBitrate, videoGoogleMaxBitrate, videoGoogleMinBitrate } = codecOptions; const offerCodec = offerRtpParameters.codecs .find((c) => (c.payloadType === codec.payloadType)); switch (codec.mimeType.toLowerCase()) { case 'audio/opus': + case 'audio/multiopus': { if (opusStereo !== undefined) { offerCodec.parameters['sprop-stereo'] = opusStereo ? 1 : 0; @@ -7817,6 +10661,16 @@ class AnswerMediaSection extends MediaSection { offerCodec.parameters.ptime = opusPtime; codecParameters.ptime = opusPtime; } + // If opusNack is not set, we must remove NACK support for OPUS. + // Otherwise it would be enabled for those handlers that artificially + // announce it in their RTP capabilities. + if (!opusNack) { + offerCodec.rtcpFeedback = offerCodec + .rtcpFeedback + .filter((fb) => fb.type !== 'nack' || fb.parameter); + codecRtcpFeedback = codecRtcpFeedback + .filter((fb) => fb.type !== 'nack' || fb.parameter); + } break; } case 'video/vp8': @@ -7824,12 +10678,15 @@ class AnswerMediaSection extends MediaSection { case 'video/h264': case 'video/h265': { - if (videoGoogleStartBitrate !== undefined) + if (videoGoogleStartBitrate !== undefined) { codecParameters['x-google-start-bitrate'] = videoGoogleStartBitrate; - if (videoGoogleMaxBitrate !== undefined) + } + if (videoGoogleMaxBitrate !== undefined) { codecParameters['x-google-max-bitrate'] = videoGoogleMaxBitrate; - if (videoGoogleMinBitrate !== undefined) + } + if (videoGoogleMinBitrate !== undefined) { codecParameters['x-google-min-bitrate'] = videoGoogleMinBitrate; + } break; } } @@ -7839,13 +10696,15 @@ class AnswerMediaSection extends MediaSection { config: '' }; for (const key of Object.keys(codecParameters)) { - if (fmtp.config) + if (fmtp.config) { fmtp.config += ';'; + } fmtp.config += `${key}=${codecParameters[key]}`; } - if (fmtp.config) + if (fmtp.config) { this._mediaObject.fmtp.push(fmtp); - for (const fb of codec.rtcpFeedback) { + } + for (const fb of codecRtcpFeedback) { this._mediaObject.rtcpFb.push({ payload: codec.payloadType, type: fb.type, @@ -7861,8 +10720,9 @@ class AnswerMediaSection extends MediaSection { // Don't add a header extension if not present in the offer. const found = (offerMediaObject.ext || []) .some((localExt) => localExt.uri === ext.uri); - if (!found) + if (!found) { continue; + } this._mediaObject.ext.push({ uri: ext.uri, value: ext.id @@ -7882,8 +10742,9 @@ class AnswerMediaSection extends MediaSection { }; this._mediaObject.rids = []; for (const rid of offerMediaObject.rids || []) { - if (rid.direction !== 'send') + if (rid.direction !== 'send') { continue; + } this._mediaObject.rids.push({ id: rid.id, direction: 'recv' @@ -7899,8 +10760,9 @@ class AnswerMediaSection extends MediaSection { }; this._mediaObject.rids = []; for (const rid of offerMediaObject.rids || []) { - if (rid.direction !== 'send') + if (rid.direction !== 'send') { continue; + } this._mediaObject.rids.push({ id: rid.id, direction: 'recv' @@ -7909,8 +10771,9 @@ class AnswerMediaSection extends MediaSection { } this._mediaObject.rtcpMux = 'rtcp-mux'; this._mediaObject.rtcpRsize = 'rtcp-rsize'; - if (this._planB && this._mediaObject.type === 'video') + if (this._planB && this._mediaObject.type === 'video') { this._mediaObject.xGoogleFlag = 'conference'; + } break; } case 'application': @@ -7948,6 +10811,32 @@ class AnswerMediaSection extends MediaSection { break; } } + resume() { + this._mediaObject.direction = 'recvonly'; + } + muxSimulcastStreams(encodings) { + var _a; + if (!this._mediaObject.simulcast || !this._mediaObject.simulcast.list1) { + return; + } + const layers = {}; + for (const encoding of encodings) { + if (encoding.rid) { + layers[encoding.rid] = encoding; + } + } + const raw = this._mediaObject.simulcast.list1; + // NOTE: Ignore bug in @types/sdp-transform. + // Ongoing PR: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/64119 + // @ts-ignore + const simulcastStreams = sdpTransform.parseSimulcastStreamList(raw); + for (const simulcastStream of simulcastStreams) { + for (const simulcastFormat of simulcastStream) { + simulcastFormat.paused = !((_a = layers[simulcastFormat.scid]) === null || _a === void 0 ? void 0 : _a.active); + } + } + this._mediaObject.simulcast.list1 = simulcastStreams.map((simulcastFormats) => simulcastFormats.map((f) => `${f.paused ? '~' : ''}${f.scid}`).join(',')).join(';'); + } } exports.AnswerMediaSection = AnswerMediaSection; class OfferMediaSection extends MediaSection { @@ -7957,10 +10846,12 @@ class OfferMediaSection extends MediaSection { this._mediaObject.type = kind; if (!plainRtpParameters) { this._mediaObject.connection = { ip: '127.0.0.1', version: 4 }; - if (!sctpParameters) + if (!sctpParameters) { this._mediaObject.protocol = 'UDP/TLS/RTP/SAVPF'; - else + } + else { this._mediaObject.protocol = 'UDP/DTLS/SCTP'; + } this._mediaObject.port = 7; } else { @@ -7980,28 +10871,32 @@ class OfferMediaSection extends MediaSection { this._mediaObject.rtp = []; this._mediaObject.rtcpFb = []; this._mediaObject.fmtp = []; - if (!this._planB) + if (!this._planB) { this._mediaObject.msid = `${streamId || '-'} ${trackId}`; + } for (const codec of offerRtpParameters.codecs) { const rtp = { payload: codec.payloadType, codec: getCodecName(codec), rate: codec.clockRate }; - if (codec.channels > 1) + if (codec.channels > 1) { rtp.encoding = codec.channels; + } this._mediaObject.rtp.push(rtp); const fmtp = { payload: codec.payloadType, config: '' }; for (const key of Object.keys(codec.parameters)) { - if (fmtp.config) + if (fmtp.config) { fmtp.config += ';'; + } fmtp.config += `${key}=${codec.parameters[key]}`; } - if (fmtp.config) + if (fmtp.config) { this._mediaObject.fmtp.push(fmtp); + } for (const fb of codec.rtcpFeedback) { this._mediaObject.rtcpFb.push({ payload: codec.payloadType, @@ -8093,6 +10988,9 @@ class OfferMediaSection extends MediaSection { // Always 'actpass'. this._mediaObject.setup = 'actpass'; } + resume() { + this._mediaObject.direction = 'sendonly'; + } planBReceive({ offerRtpParameters, streamId, trackId }) { const encoding = offerRtpParameters.encodings[0]; const ssrc = encoding.ssrc; @@ -8109,20 +11007,23 @@ class OfferMediaSection extends MediaSection { codec: getCodecName(codec), rate: codec.clockRate }; - if (codec.channels > 1) + if (codec.channels > 1) { rtp.encoding = codec.channels; + } this._mediaObject.rtp.push(rtp); const fmtp = { payload: codec.payloadType, config: '' }; for (const key of Object.keys(codec.parameters)) { - if (fmtp.config) + if (fmtp.config) { fmtp.config += ';'; + } fmtp.config += `${key}=${codec.parameters[key]}`; } - if (fmtp.config) + if (fmtp.config) { this._mediaObject.fmtp.push(fmtp); + } for (const fb of codec.rtcpFeedback) { this._mediaObject.rtcpFb.push({ payload: codec.payloadType, @@ -8187,16 +11088,21 @@ exports.OfferMediaSection = OfferMediaSection; function getCodecName(codec) { const MimeTypeRegex = new RegExp('^(audio|video)/(.+)', 'i'); const mimeTypeMatch = MimeTypeRegex.exec(codec.mimeType); - if (!mimeTypeMatch) + if (!mimeTypeMatch) { throw new TypeError('invalid codec.mimeType'); + } return mimeTypeMatch[2]; } -},{"../../utils":39}],31:[function(require,module,exports){ +},{"../../utils":46,"sdp-transform":52}],38:[function(require,module,exports){ "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; @@ -8209,7 +11115,7 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; @@ -8287,8 +11193,9 @@ class RemoteSdp { // If a closed media section is found, return its index. for (let idx = 0; idx < this._mediaSections.length; ++idx) { const mediaSection = this._mediaSections[idx]; - if (mediaSection.closed) + if (mediaSection.closed) { return { idx, reuseMid: mediaSection.mid }; + } } // If no closed media section is found, return next one. return { idx: this._mediaSections.length }; @@ -8322,8 +11229,9 @@ class RemoteSdp { receive({ mid, kind, offerRtpParameters, streamId, trackId }) { const idx = this._midToIndex.get(mid); let mediaSection; - if (idx !== undefined) + if (idx !== undefined) { mediaSection = this._mediaSections[idx]; + } // Unified-Plan or different media kind. if (!mediaSection) { mediaSection = new MediaSection_1.OfferMediaSection({ @@ -8354,37 +11262,50 @@ class RemoteSdp { this._replaceMediaSection(mediaSection); } } + pauseMediaSection(mid) { + const mediaSection = this._findMediaSection(mid); + mediaSection.pause(); + } + resumeSendingMediaSection(mid) { + const mediaSection = this._findMediaSection(mid); + mediaSection.resume(); + } + resumeReceivingMediaSection(mid) { + const mediaSection = this._findMediaSection(mid); + mediaSection.resume(); + } disableMediaSection(mid) { - const idx = this._midToIndex.get(mid); - if (idx === undefined) { - throw new Error(`no media section found with mid '${mid}'`); - } - const mediaSection = this._mediaSections[idx]; + const mediaSection = this._findMediaSection(mid); mediaSection.disable(); } + /** + * Closes media section. Returns true if the given MID corresponds to a m + * section that has been indeed closed. False otherwise. + * + * NOTE: Closing the first m section is a pain since it invalidates the bundled + * transport, so instead closing it we just disable it. + */ closeMediaSection(mid) { - const idx = this._midToIndex.get(mid); - if (idx === undefined) { - throw new Error(`no media section found with mid '${mid}'`); - } - const mediaSection = this._mediaSections[idx]; + const mediaSection = this._findMediaSection(mid); // NOTE: Closing the first m section is a pain since it invalidates the // bundled transport, so let's avoid it. if (mid === this._firstMid) { logger.debug('closeMediaSection() | cannot close first media section, disabling it instead [mid:%s]', mid); this.disableMediaSection(mid); - return; + return false; } mediaSection.close(); // Regenerate BUNDLE mids. this._regenerateBundleMids(); + return true; + } + muxMediaSectionSimulcast(mid, encodings) { + const mediaSection = this._findMediaSection(mid); + mediaSection.muxSimulcastStreams(encodings); + this._replaceMediaSection(mediaSection); } planBStopReceiving({ mid, offerRtpParameters }) { - const idx = this._midToIndex.get(mid); - if (idx === undefined) { - throw new Error(`no media section found with mid '${mid}'`); - } - const mediaSection = this._mediaSections[idx]; + const mediaSection = this._findMediaSection(mid); mediaSection.planBStopReceiving({ offerRtpParameters }); this._replaceMediaSection(mediaSection); } @@ -8418,8 +11339,9 @@ class RemoteSdp { return sdpTransform.write(this._sdpObject); } _addMediaSection(newMediaSection) { - if (!this._firstMid) + if (!this._firstMid) { this._firstMid = newMediaSection.mid; + } // Add to the vector. this._mediaSections.push(newMediaSection); // Add to the map. @@ -8458,9 +11380,17 @@ class RemoteSdp { this._sdpObject.media[idx] = newMediaSection.getObject(); } } + _findMediaSection(mid) { + const idx = this._midToIndex.get(mid); + if (idx === undefined) { + throw new Error(`no media section found with mid '${mid}'`); + } + return this._mediaSections[idx]; + } _regenerateBundleMids() { - if (!this._dtlsParameters) + if (!this._dtlsParameters) { return; + } this._sdpObject.groups[0].mids = this._mediaSections .filter((mediaSection) => !mediaSection.closed) .map((mediaSection) => mediaSection.mid) @@ -8469,11 +11399,15 @@ class RemoteSdp { } exports.RemoteSdp = RemoteSdp; -},{"../../Logger":13,"./MediaSection":30,"sdp-transform":44}],32:[function(require,module,exports){ +},{"../../Logger":17,"./MediaSection":37,"sdp-transform":52}],39:[function(require,module,exports){ "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; @@ -8486,13 +11420,17 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.applyCodecParameters = exports.getCname = exports.extractDtlsParameters = exports.extractRtpCapabilities = void 0; const sdpTransform = __importStar(require("sdp-transform")); +/** + * This function must be called with an SDP with 1 m=audio and 1 m=video + * sections. + */ function extractRtpCapabilities({ sdpObject }) { // Map of RtpCodecParameters indexed by payload type. const codecsMap = new Map(); @@ -8506,15 +11444,17 @@ function extractRtpCapabilities({ sdpObject }) { switch (kind) { case 'audio': { - if (gotAudio) + if (gotAudio) { continue; + } gotAudio = true; break; } case 'video': { - if (gotVideo) + if (gotVideo) { continue; + } gotVideo = true; break; } @@ -8540,31 +11480,49 @@ function extractRtpCapabilities({ sdpObject }) { for (const fmtp of m.fmtp || []) { const parameters = sdpTransform.parseParams(fmtp.config); const codec = codecsMap.get(fmtp.payload); - if (!codec) + if (!codec) { continue; + } // Specials case to convert parameter value to string. - if (parameters && parameters.hasOwnProperty('profile-level-id')) + if (parameters && parameters.hasOwnProperty('profile-level-id')) { parameters['profile-level-id'] = String(parameters['profile-level-id']); + } codec.parameters = parameters; } // Get RTCP feedback for each codec. for (const fb of m.rtcpFb || []) { - const codec = codecsMap.get(fb.payload); - if (!codec) - continue; const feedback = { type: fb.type, parameter: fb.subtype }; - if (!feedback.parameter) + if (!feedback.parameter) { delete feedback.parameter; - codec.rtcpFeedback.push(feedback); + } + // rtcp-fb payload is not '*', so just apply it to its corresponding + // codec. + if (fb.payload !== '*') { + const codec = codecsMap.get(fb.payload); + if (!codec) { + continue; + } + codec.rtcpFeedback.push(feedback); + } + // If rtcp-fb payload is '*' it must be applied to all codecs with same + // kind (with some exceptions such as RTX codec). + else { + for (const codec of codecsMap.values()) { + if (codec.kind === kind && !/.+\/rtx$/i.test(codec.mimeType)) { + codec.rtcpFeedback.push(feedback); + } + } + } } // Get RTP header extensions. for (const ext of m.ext || []) { // Ignore encrypted extensions (not yet supported in mediasoup). - if (ext['encrypt-uri']) + if (ext['encrypt-uri']) { continue; + } const headerExtension = { kind: kind, uri: ext.uri, @@ -8583,8 +11541,9 @@ exports.extractRtpCapabilities = extractRtpCapabilities; function extractDtlsParameters({ sdpObject }) { const mediaObject = (sdpObject.media || []) .find((m) => (m.iceUfrag && m.port !== 0)); - if (!mediaObject) + if (!mediaObject) { throw new Error('no active media section found'); + } const fingerprint = mediaObject.fingerprint || sdpObject.fingerprint; let role; switch (mediaObject.setup) { @@ -8613,8 +11572,9 @@ exports.extractDtlsParameters = extractDtlsParameters; function getCname({ offerMediaObject }) { const ssrcCnameLine = (offerMediaObject.ssrcs || []) .find((line) => line.attribute === 'cname'); - if (!ssrcCnameLine) + if (!ssrcCnameLine) { return ''; + } return ssrcCnameLine.value; } exports.getCname = getCname; @@ -8626,12 +11586,14 @@ function applyCodecParameters({ offerRtpParameters, answerMediaObject }) { for (const codec of offerRtpParameters.codecs) { const mimeType = codec.mimeType.toLowerCase(); // Avoid parsing codec parameters for unhandled codecs. - if (mimeType !== 'audio/opus') + if (mimeType !== 'audio/opus') { continue; + } const rtp = (answerMediaObject.rtp || []) .find((r) => r.payload === codec.payloadType); - if (!rtp) + if (!rtp) { continue; + } // Just in case. answerMediaObject.fmtp = answerMediaObject.fmtp || []; let fmtp = answerMediaObject.fmtp @@ -8645,23 +11607,25 @@ function applyCodecParameters({ offerRtpParameters, answerMediaObject }) { case 'audio/opus': { const spropStereo = codec.parameters['sprop-stereo']; - if (spropStereo !== undefined) + if (spropStereo !== undefined) { parameters.stereo = spropStereo ? 1 : 0; + } break; } } // Write the codec fmtp.config back. fmtp.config = ''; for (const key of Object.keys(parameters)) { - if (fmtp.config) + if (fmtp.config) { fmtp.config += ';'; + } fmtp.config += `${key}=${parameters[key]}`; } } } exports.applyCodecParameters = applyCodecParameters; -},{"sdp-transform":44}],33:[function(require,module,exports){ +},{"sdp-transform":52}],40:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.addLegacySimulcast = exports.getRtpEncodings = void 0; @@ -8670,23 +11634,27 @@ function getRtpEncodings({ offerMediaObject, track }) { let firstSsrc; const ssrcs = new Set(); for (const line of offerMediaObject.ssrcs || []) { - if (line.attribute !== 'msid') + if (line.attribute !== 'msid') { continue; + } const trackId = line.value.split(' ')[1]; if (trackId === track.id) { const ssrc = line.id; ssrcs.add(ssrc); - if (!firstSsrc) + if (!firstSsrc) { firstSsrc = ssrc; + } } } - if (ssrcs.size === 0) + if (ssrcs.size === 0) { throw new Error(`a=ssrc line with msid information not found [track.id:${track.id}]`); + } const ssrcToRtxSsrc = new Map(); // First assume RTX is used. for (const line of offerMediaObject.ssrcGroups || []) { - if (line.semantics !== 'FID') + if (line.semantics !== 'FID') { continue; + } let [ssrc, rtxSsrc] = line.ssrcs.split(/\s+/); ssrc = Number(ssrc); rtxSsrc = Number(rtxSsrc); @@ -8708,8 +11676,9 @@ function getRtpEncodings({ offerMediaObject, track }) { const encodings = []; for (const [ssrc, rtxSsrc] of ssrcToRtxSsrc) { const encoding = { ssrc }; - if (rtxSsrc) + if (rtxSsrc) { encoding.rtx = { ssrc: rtxSsrc }; + } encodings.push(encoding); } return encodings; @@ -8719,16 +11688,18 @@ exports.getRtpEncodings = getRtpEncodings; * Adds multi-ssrc based simulcast into the given SDP media section offer. */ function addLegacySimulcast({ offerMediaObject, track, numStreams }) { - if (numStreams <= 1) + if (numStreams <= 1) { throw new TypeError('numStreams must be greater than 1'); + } let firstSsrc; let firstRtxSsrc; let streamId; // Get the SSRC. const ssrcMsidLine = (offerMediaObject.ssrcs || []) .find((line) => { - if (line.attribute !== 'msid') + if (line.attribute !== 'msid') { return false; + } const trackId = line.value.split(' ')[1]; if (trackId === track.id) { firstSsrc = line.id; @@ -8739,13 +11710,15 @@ function addLegacySimulcast({ offerMediaObject, track, numStreams }) { return false; } }); - if (!ssrcMsidLine) + if (!ssrcMsidLine) { throw new Error(`a=ssrc line with msid information not found [track.id:${track.id}]`); + } // Get the SSRC for RTX. (offerMediaObject.ssrcGroups || []) .some((line) => { - if (line.semantics !== 'FID') + if (line.semantics !== 'FID') { return false; + } const ssrcs = line.ssrcs.split(/\s+/); if (Number(ssrcs[0]) === firstSsrc) { firstRtxSsrc = Number(ssrcs[1]); @@ -8757,15 +11730,17 @@ function addLegacySimulcast({ offerMediaObject, track, numStreams }) { }); const ssrcCnameLine = offerMediaObject.ssrcs .find((line) => (line.attribute === 'cname' && line.id === firstSsrc)); - if (!ssrcCnameLine) + if (!ssrcCnameLine) { throw new Error(`a=ssrc line with cname information not found [track.id:${track.id}]`); + } const cname = ssrcCnameLine.value; const ssrcs = []; const rtxSsrcs = []; for (let i = 0; i < numStreams; ++i) { ssrcs.push(firstSsrc + i); - if (firstRtxSsrc) + if (firstRtxSsrc) { rtxSsrcs.push(firstRtxSsrc + i); + } } offerMediaObject.ssrcGroups = offerMediaObject.ssrcGroups || []; offerMediaObject.ssrcs = offerMediaObject.ssrcs || []; @@ -8807,7 +11782,7 @@ function addLegacySimulcast({ offerMediaObject, track, numStreams }) { } exports.addLegacySimulcast = addLegacySimulcast; -},{}],34:[function(require,module,exports){ +},{}],41:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.addLegacySimulcast = exports.getRtpEncodings = void 0; @@ -8817,27 +11792,29 @@ function getRtpEncodings({ offerMediaObject }) { const ssrc = line.id; ssrcs.add(ssrc); } - if (ssrcs.size === 0) + 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') + 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. + // 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. + // 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); @@ -8845,8 +11822,9 @@ function getRtpEncodings({ offerMediaObject }) { const encodings = []; for (const [ssrc, rtxSsrc] of ssrcToRtxSsrc) { const encoding = { ssrc }; - if (rtxSsrc) + if (rtxSsrc) { encoding.rtx = { ssrc: rtxSsrc }; + } encodings.push(encoding); } return encodings; @@ -8856,21 +11834,24 @@ exports.getRtpEncodings = getRtpEncodings; * Adds multi-ssrc based simulcast into the given SDP media section offer. */ function addLegacySimulcast({ offerMediaObject, numStreams }) { - if (numStreams <= 1) + if (numStreams <= 1) { throw new TypeError('numStreams must be greater than 1'); + } // Get the SSRC. const ssrcMsidLine = (offerMediaObject.ssrcs || []) .find((line) => line.attribute === 'msid'); - if (!ssrcMsidLine) + if (!ssrcMsidLine) { throw new Error('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') + if (line.semantics !== 'FID') { return false; + } const ssrcs = line.ssrcs.split(/\s+/); if (Number(ssrcs[0]) === firstSsrc) { firstRtxSsrc = Number(ssrcs[1]); @@ -8882,15 +11863,17 @@ function addLegacySimulcast({ offerMediaObject, numStreams }) { }); const ssrcCnameLine = offerMediaObject.ssrcs .find((line) => line.attribute === 'cname'); - if (!ssrcCnameLine) + if (!ssrcCnameLine) { throw new Error('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) + if (firstRtxSsrc) { rtxSsrcs.push(firstRtxSsrc + i); + } } offerMediaObject.ssrcGroups = []; offerMediaObject.ssrcs = []; @@ -8932,11 +11915,15 @@ function addLegacySimulcast({ offerMediaObject, numStreams }) { } exports.addLegacySimulcast = addLegacySimulcast; -},{}],35:[function(require,module,exports){ +},{}],42:[function(require,module,exports){ "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; @@ -8949,7 +11936,7 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; @@ -8957,7 +11944,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.debug = exports.detectDevice = exports.Device = exports.version = exports.types = void 0; +exports.debug = exports.parseScalabilityMode = exports.detectDevice = exports.Device = exports.version = exports.types = void 0; const debug_1 = __importDefault(require("debug")); exports.debug = debug_1.default; const Device_1 = require("./Device"); @@ -8968,18 +11955,22 @@ exports.types = types; /** * Expose mediasoup-client version. */ -exports.version = '3.6.37'; +exports.version = '3.6.82'; /** * Expose parseScalabilityMode() function. */ var scalabilityModes_1 = require("./scalabilityModes"); Object.defineProperty(exports, "parseScalabilityMode", { enumerable: true, get: function () { return scalabilityModes_1.parse; } }); -},{"./Device":11,"./scalabilityModes":37,"./types":38,"debug":40}],36:[function(require,module,exports){ +},{"./Device":15,"./scalabilityModes":44,"./types":45,"debug":47}],43:[function(require,module,exports){ "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; @@ -8992,7 +11983,7 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; @@ -9009,21 +12000,26 @@ const RTP_PROBATOR_CODEC_PAYLOAD_TYPE = 127; * It throws if invalid. */ function validateRtpCapabilities(caps) { - if (typeof caps !== 'object') + if (typeof caps !== 'object') { throw new TypeError('caps is not an object'); + } // codecs is optional. If unset, fill with an empty array. - if (caps.codecs && !Array.isArray(caps.codecs)) + if (caps.codecs && !Array.isArray(caps.codecs)) { throw new TypeError('caps.codecs is not an array'); - else if (!caps.codecs) + } + else if (!caps.codecs) { caps.codecs = []; + } for (const codec of caps.codecs) { validateRtpCodecCapability(codec); } // headerExtensions is optional. If unset, fill with an empty array. - if (caps.headerExtensions && !Array.isArray(caps.headerExtensions)) + if (caps.headerExtensions && !Array.isArray(caps.headerExtensions)) { throw new TypeError('caps.headerExtensions is not an array'); - else if (!caps.headerExtensions) + } + else if (!caps.headerExtensions) { caps.headerExtensions = []; + } for (const ext of caps.headerExtensions) { validateRtpHeaderExtension(ext); } @@ -9036,33 +12032,40 @@ exports.validateRtpCapabilities = validateRtpCapabilities; */ function validateRtpCodecCapability(codec) { const MimeTypeRegex = new RegExp('^(audio|video)/(.+)', 'i'); - if (typeof codec !== 'object') + if (typeof codec !== 'object') { throw new TypeError('codec is not an object'); + } // mimeType is mandatory. - if (!codec.mimeType || typeof codec.mimeType !== 'string') + if (!codec.mimeType || typeof codec.mimeType !== 'string') { throw new TypeError('missing codec.mimeType'); + } const mimeTypeMatch = MimeTypeRegex.exec(codec.mimeType); - if (!mimeTypeMatch) + if (!mimeTypeMatch) { throw new TypeError('invalid codec.mimeType'); + } // Just override kind with media component of mimeType. codec.kind = mimeTypeMatch[1].toLowerCase(); // preferredPayloadType is optional. - if (codec.preferredPayloadType && typeof codec.preferredPayloadType !== 'number') + if (codec.preferredPayloadType && typeof codec.preferredPayloadType !== 'number') { throw new TypeError('invalid codec.preferredPayloadType'); + } // clockRate is mandatory. - if (typeof codec.clockRate !== 'number') + if (typeof codec.clockRate !== 'number') { throw new TypeError('missing codec.clockRate'); + } // channels is optional. If unset, set it to 1 (just if audio). if (codec.kind === 'audio') { - if (typeof codec.channels !== 'number') + if (typeof codec.channels !== 'number') { codec.channels = 1; + } } else { delete codec.channels; } // parameters is optional. If unset, set it to an empty object. - if (!codec.parameters || typeof codec.parameters !== 'object') + if (!codec.parameters || typeof codec.parameters !== 'object') { codec.parameters = {}; + } for (const key of Object.keys(codec.parameters)) { let value = codec.parameters[key]; if (value === undefined) { @@ -9074,13 +12077,15 @@ function validateRtpCodecCapability(codec) { } // Specific parameters validation. if (key === 'apt') { - if (typeof value !== 'number') + if (typeof value !== 'number') { throw new TypeError('invalid codec apt parameter'); + } } } // rtcpFeedback is optional. If unset, set it to an empty array. - if (!codec.rtcpFeedback || !Array.isArray(codec.rtcpFeedback)) + if (!codec.rtcpFeedback || !Array.isArray(codec.rtcpFeedback)) { codec.rtcpFeedback = []; + } for (const fb of codec.rtcpFeedback) { validateRtcpFeedback(fb); } @@ -9092,14 +12097,17 @@ exports.validateRtpCodecCapability = validateRtpCodecCapability; * It throws if invalid. */ function validateRtcpFeedback(fb) { - if (typeof fb !== 'object') + if (typeof fb !== 'object') { throw new TypeError('fb is not an object'); + } // type is mandatory. - if (!fb.type || typeof fb.type !== 'string') + if (!fb.type || typeof fb.type !== 'string') { throw new TypeError('missing fb.type'); + } // parameter is optional. If unset set it to an empty string. - if (!fb.parameter || typeof fb.parameter !== 'string') + if (!fb.parameter || typeof fb.parameter !== 'string') { fb.parameter = ''; + } } exports.validateRtcpFeedback = validateRtcpFeedback; /** @@ -9108,27 +12116,35 @@ exports.validateRtcpFeedback = validateRtcpFeedback; * It throws if invalid. */ function validateRtpHeaderExtension(ext) { - if (typeof ext !== 'object') + if (typeof ext !== 'object') { throw new TypeError('ext is not an object'); + } // kind is mandatory. - if (ext.kind !== 'audio' && ext.kind !== 'video') + if (ext.kind !== 'audio' && ext.kind !== 'video') { throw new TypeError('invalid ext.kind'); + } // uri is mandatory. - if (!ext.uri || typeof ext.uri !== 'string') + if (!ext.uri || typeof ext.uri !== 'string') { throw new TypeError('missing ext.uri'); + } // preferredId is mandatory. - if (typeof ext.preferredId !== 'number') + if (typeof ext.preferredId !== 'number') { throw new TypeError('missing ext.preferredId'); + } // preferredEncrypt is optional. If unset set it to false. - if (ext.preferredEncrypt && typeof ext.preferredEncrypt !== 'boolean') + if (ext.preferredEncrypt && typeof ext.preferredEncrypt !== 'boolean') { throw new TypeError('invalid ext.preferredEncrypt'); - else if (!ext.preferredEncrypt) + } + else if (!ext.preferredEncrypt) { ext.preferredEncrypt = false; + } // direction is optional. If unset set it to sendrecv. - if (ext.direction && typeof ext.direction !== 'string') + if (ext.direction && typeof ext.direction !== 'string') { throw new TypeError('invalid ext.direction'); - else if (!ext.direction) + } + else if (!ext.direction) { ext.direction = 'sendrecv'; + } } exports.validateRtpHeaderExtension = validateRtpHeaderExtension; /** @@ -9137,38 +12153,47 @@ exports.validateRtpHeaderExtension = validateRtpHeaderExtension; * It throws if invalid. */ function validateRtpParameters(params) { - if (typeof params !== 'object') + if (typeof params !== 'object') { throw new TypeError('params is not an object'); + } // mid is optional. - if (params.mid && typeof params.mid !== 'string') + if (params.mid && typeof params.mid !== 'string') { throw new TypeError('params.mid is not a string'); + } // codecs is mandatory. - if (!Array.isArray(params.codecs)) + if (!Array.isArray(params.codecs)) { throw new TypeError('missing params.codecs'); + } for (const codec of params.codecs) { validateRtpCodecParameters(codec); } // headerExtensions is optional. If unset, fill with an empty array. - if (params.headerExtensions && !Array.isArray(params.headerExtensions)) + if (params.headerExtensions && !Array.isArray(params.headerExtensions)) { throw new TypeError('params.headerExtensions is not an array'); - else if (!params.headerExtensions) + } + else if (!params.headerExtensions) { params.headerExtensions = []; + } for (const ext of params.headerExtensions) { validateRtpHeaderExtensionParameters(ext); } // encodings is optional. If unset, fill with an empty array. - if (params.encodings && !Array.isArray(params.encodings)) + if (params.encodings && !Array.isArray(params.encodings)) { throw new TypeError('params.encodings is not an array'); - else if (!params.encodings) + } + else if (!params.encodings) { params.encodings = []; + } for (const encoding of params.encodings) { validateRtpEncodingParameters(encoding); } // rtcp is optional. If unset, fill with an empty object. - if (params.rtcp && typeof params.rtcp !== 'object') + if (params.rtcp && typeof params.rtcp !== 'object') { throw new TypeError('params.rtcp is not an object'); - else if (!params.rtcp) + } + else if (!params.rtcp) { params.rtcp = {}; + } validateRtcpParameters(params.rtcp); } exports.validateRtpParameters = validateRtpParameters; @@ -9179,32 +12204,39 @@ exports.validateRtpParameters = validateRtpParameters; */ function validateRtpCodecParameters(codec) { const MimeTypeRegex = new RegExp('^(audio|video)/(.+)', 'i'); - if (typeof codec !== 'object') + if (typeof codec !== 'object') { throw new TypeError('codec is not an object'); + } // mimeType is mandatory. - if (!codec.mimeType || typeof codec.mimeType !== 'string') + if (!codec.mimeType || typeof codec.mimeType !== 'string') { throw new TypeError('missing codec.mimeType'); + } const mimeTypeMatch = MimeTypeRegex.exec(codec.mimeType); - if (!mimeTypeMatch) + if (!mimeTypeMatch) { throw new TypeError('invalid codec.mimeType'); + } // payloadType is mandatory. - if (typeof codec.payloadType !== 'number') + if (typeof codec.payloadType !== 'number') { throw new TypeError('missing codec.payloadType'); + } // clockRate is mandatory. - if (typeof codec.clockRate !== 'number') + if (typeof codec.clockRate !== 'number') { throw new TypeError('missing codec.clockRate'); + } const kind = mimeTypeMatch[1].toLowerCase(); // channels is optional. If unset, set it to 1 (just if audio). if (kind === 'audio') { - if (typeof codec.channels !== 'number') + if (typeof codec.channels !== 'number') { codec.channels = 1; + } } else { delete codec.channels; } // parameters is optional. If unset, set it to an empty object. - if (!codec.parameters || typeof codec.parameters !== 'object') + if (!codec.parameters || typeof codec.parameters !== 'object') { codec.parameters = {}; + } for (const key of Object.keys(codec.parameters)) { let value = codec.parameters[key]; if (value === undefined) { @@ -9216,13 +12248,15 @@ function validateRtpCodecParameters(codec) { } // Specific parameters validation. if (key === 'apt') { - if (typeof value !== 'number') + if (typeof value !== 'number') { throw new TypeError('invalid codec apt parameter'); + } } } // rtcpFeedback is optional. If unset, set it to an empty array. - if (!codec.rtcpFeedback || !Array.isArray(codec.rtcpFeedback)) + if (!codec.rtcpFeedback || !Array.isArray(codec.rtcpFeedback)) { codec.rtcpFeedback = []; + } for (const fb of codec.rtcpFeedback) { validateRtcpFeedback(fb); } @@ -9234,30 +12268,37 @@ exports.validateRtpCodecParameters = validateRtpCodecParameters; * It throws if invalid. */ function validateRtpHeaderExtensionParameters(ext) { - if (typeof ext !== 'object') + if (typeof ext !== 'object') { throw new TypeError('ext is not an object'); + } // uri is mandatory. - if (!ext.uri || typeof ext.uri !== 'string') + if (!ext.uri || typeof ext.uri !== 'string') { throw new TypeError('missing ext.uri'); + } // id is mandatory. - if (typeof ext.id !== 'number') + if (typeof ext.id !== 'number') { throw new TypeError('missing ext.id'); + } // encrypt is optional. If unset set it to false. - if (ext.encrypt && typeof ext.encrypt !== 'boolean') + if (ext.encrypt && typeof ext.encrypt !== 'boolean') { throw new TypeError('invalid ext.encrypt'); - else if (!ext.encrypt) + } + else if (!ext.encrypt) { ext.encrypt = false; + } // parameters is optional. If unset, set it to an empty object. - if (!ext.parameters || typeof ext.parameters !== 'object') + if (!ext.parameters || typeof ext.parameters !== 'object') { ext.parameters = {}; + } for (const key of Object.keys(ext.parameters)) { let value = ext.parameters[key]; if (value === undefined) { ext.parameters[key] = ''; value = ''; } - if (typeof value !== 'string' && typeof value !== 'number') + if (typeof value !== 'string' && typeof value !== 'number') { throw new TypeError('invalid header extension parameter'); + } } } exports.validateRtpHeaderExtensionParameters = validateRtpHeaderExtensionParameters; @@ -9267,29 +12308,35 @@ exports.validateRtpHeaderExtensionParameters = validateRtpHeaderExtensionParamet * It throws if invalid. */ function validateRtpEncodingParameters(encoding) { - if (typeof encoding !== 'object') + if (typeof encoding !== 'object') { throw new TypeError('encoding is not an object'); + } // ssrc is optional. - if (encoding.ssrc && typeof encoding.ssrc !== 'number') + if (encoding.ssrc && typeof encoding.ssrc !== 'number') { throw new TypeError('invalid encoding.ssrc'); + } // rid is optional. - if (encoding.rid && typeof encoding.rid !== 'string') + if (encoding.rid && typeof encoding.rid !== 'string') { throw new TypeError('invalid encoding.rid'); + } // rtx is optional. if (encoding.rtx && typeof encoding.rtx !== 'object') { throw new TypeError('invalid encoding.rtx'); } else if (encoding.rtx) { // RTX ssrc is mandatory if rtx is present. - if (typeof encoding.rtx.ssrc !== 'number') + if (typeof encoding.rtx.ssrc !== 'number') { throw new TypeError('missing encoding.rtx.ssrc'); + } } // dtx is optional. If unset set it to false. - if (!encoding.dtx || typeof encoding.dtx !== 'boolean') + if (!encoding.dtx || typeof encoding.dtx !== 'boolean') { encoding.dtx = false; + } // scalabilityMode is optional. - if (encoding.scalabilityMode && typeof encoding.scalabilityMode !== 'string') + if (encoding.scalabilityMode && typeof encoding.scalabilityMode !== 'string') { throw new TypeError('invalid encoding.scalabilityMode'); + } } exports.validateRtpEncodingParameters = validateRtpEncodingParameters; /** @@ -9298,14 +12345,17 @@ exports.validateRtpEncodingParameters = validateRtpEncodingParameters; * It throws if invalid. */ function validateRtcpParameters(rtcp) { - if (typeof rtcp !== 'object') + if (typeof rtcp !== 'object') { throw new TypeError('rtcp is not an object'); + } // cname is optional. - if (rtcp.cname && typeof rtcp.cname !== 'string') + if (rtcp.cname && typeof rtcp.cname !== 'string') { throw new TypeError('invalid rtcp.cname'); + } // reducedSize is optional. If unset set it to true. - if (!rtcp.reducedSize || typeof rtcp.reducedSize !== 'boolean') + if (!rtcp.reducedSize || typeof rtcp.reducedSize !== 'boolean') { rtcp.reducedSize = true; + } } exports.validateRtcpParameters = validateRtcpParameters; /** @@ -9314,11 +12364,13 @@ exports.validateRtcpParameters = validateRtcpParameters; * It throws if invalid. */ function validateSctpCapabilities(caps) { - if (typeof caps !== 'object') + if (typeof caps !== 'object') { throw new TypeError('caps is not an object'); + } // numStreams is mandatory. - if (!caps.numStreams || typeof caps.numStreams !== 'object') + if (!caps.numStreams || typeof caps.numStreams !== 'object') { throw new TypeError('missing caps.numStreams'); + } validateNumSctpStreams(caps.numStreams); } exports.validateSctpCapabilities = validateSctpCapabilities; @@ -9328,14 +12380,17 @@ exports.validateSctpCapabilities = validateSctpCapabilities; * It throws if invalid. */ function validateNumSctpStreams(numStreams) { - if (typeof numStreams !== 'object') + if (typeof numStreams !== 'object') { throw new TypeError('numStreams is not an object'); + } // OS is mandatory. - if (typeof numStreams.OS !== 'number') + if (typeof numStreams.OS !== 'number') { throw new TypeError('missing numStreams.OS'); + } // MIS is mandatory. - if (typeof numStreams.MIS !== 'number') + if (typeof numStreams.MIS !== 'number') { throw new TypeError('missing numStreams.MIS'); + } } exports.validateNumSctpStreams = validateNumSctpStreams; /** @@ -9344,20 +12399,25 @@ exports.validateNumSctpStreams = validateNumSctpStreams; * It throws if invalid. */ function validateSctpParameters(params) { - if (typeof params !== 'object') + if (typeof params !== 'object') { throw new TypeError('params is not an object'); + } // port is mandatory. - if (typeof params.port !== 'number') + if (typeof params.port !== 'number') { throw new TypeError('missing params.port'); + } // OS is mandatory. - if (typeof params.OS !== 'number') + if (typeof params.OS !== 'number') { throw new TypeError('missing params.OS'); + } // MIS is mandatory. - if (typeof params.MIS !== 'number') + if (typeof params.MIS !== 'number') { throw new TypeError('missing params.MIS'); + } // maxMessageSize is mandatory. - if (typeof params.maxMessageSize !== 'number') + if (typeof params.maxMessageSize !== 'number') { throw new TypeError('missing params.maxMessageSize'); + } } exports.validateSctpParameters = validateSctpParameters; /** @@ -9366,25 +12426,32 @@ exports.validateSctpParameters = validateSctpParameters; * It throws if invalid. */ function validateSctpStreamParameters(params) { - if (typeof params !== 'object') + if (typeof params !== 'object') { throw new TypeError('params is not an object'); + } // streamId is mandatory. - if (typeof params.streamId !== 'number') + if (typeof params.streamId !== 'number') { throw new TypeError('missing params.streamId'); + } // ordered is optional. let orderedGiven = false; - if (typeof params.ordered === 'boolean') + if (typeof params.ordered === 'boolean') { orderedGiven = true; - else + } + else { params.ordered = true; + } // maxPacketLifeTime is optional. - if (params.maxPacketLifeTime && typeof params.maxPacketLifeTime !== 'number') + if (params.maxPacketLifeTime && typeof params.maxPacketLifeTime !== 'number') { throw new TypeError('invalid params.maxPacketLifeTime'); + } // maxRetransmits is optional. - if (params.maxRetransmits && typeof params.maxRetransmits !== 'number') + if (params.maxRetransmits && typeof params.maxRetransmits !== 'number') { throw new TypeError('invalid params.maxRetransmits'); - if (params.maxPacketLifeTime && params.maxRetransmits) + } + if (params.maxPacketLifeTime && params.maxRetransmits) { throw new TypeError('cannot provide both maxPacketLifeTime and maxRetransmits'); + } if (orderedGiven && params.ordered && (params.maxPacketLifeTime || params.maxRetransmits)) { @@ -9394,11 +12461,13 @@ function validateSctpStreamParameters(params) { params.ordered = false; } // label is optional. - if (params.label && typeof params.label !== 'string') + if (params.label && typeof params.label !== 'string') { throw new TypeError('invalid params.label'); + } // protocol is optional. - if (params.protocol && typeof params.protocol !== 'string') + if (params.protocol && typeof params.protocol !== 'string') { throw new TypeError('invalid params.protocol'); + } } exports.validateSctpStreamParameters = validateSctpStreamParameters; /** @@ -9411,12 +12480,14 @@ function getExtendedRtpCapabilities(localCaps, remoteCaps) { }; // Match media codecs and keep the order preferred by remoteCaps. for (const remoteCodec of remoteCaps.codecs || []) { - if (isRtxCodec(remoteCodec)) + if (isRtxCodec(remoteCodec)) { continue; + } const matchingLocalCodec = (localCaps.codecs || []) .find((localCodec) => (matchCodecs(localCodec, remoteCodec, { strict: true, modify: true }))); - if (!matchingLocalCodec) + if (!matchingLocalCodec) { continue; + } const extendedCodec = { mimeType: matchingLocalCodec.mimeType, kind: matchingLocalCodec.kind, @@ -9449,8 +12520,9 @@ function getExtendedRtpCapabilities(localCaps, remoteCaps) { for (const remoteExt of remoteCaps.headerExtensions) { const matchingLocalExt = localCaps.headerExtensions .find((localExt) => (matchHeaderExtensions(localExt, remoteExt))); - if (!matchingLocalExt) + if (!matchingLocalExt) { continue; + } const extendedExt = { kind: remoteExt.kind, uri: remoteExt.uri, @@ -9499,8 +12571,9 @@ function getRecvRtpCapabilities(extendedRtpCapabilities) { }; rtpCapabilities.codecs.push(codec); // Add RTX codec. - if (!extendedCodec.remoteRtxPayloadType) + if (!extendedCodec.remoteRtxPayloadType) { continue; + } const rtxCodec = { mimeType: `${extendedCodec.kind}/rtx`, kind: extendedCodec.kind, @@ -9545,8 +12618,9 @@ function getSendingRtpParameters(kind, extendedRtpCapabilities) { rtcp: {} }; for (const extendedCodec of extendedRtpCapabilities.codecs) { - if (extendedCodec.kind !== kind) + if (extendedCodec.kind !== kind) { continue; + } const codec = { mimeType: extendedCodec.mimeType, payloadType: extendedCodec.localPayloadType, @@ -9600,8 +12674,9 @@ function getSendingRemoteRtpParameters(kind, extendedRtpCapabilities) { rtcp: {} }; for (const extendedCodec of extendedRtpCapabilities.codecs) { - if (extendedCodec.kind !== kind) + if (extendedCodec.kind !== kind) { continue; + } const codec = { mimeType: extendedCodec.mimeType, payloadType: extendedCodec.localPayloadType, @@ -9678,21 +12753,24 @@ function reduceCodecs(codecs, capCodec) { // If no capability codec is given, take the first one (and RTX). if (!capCodec) { filteredCodecs.push(codecs[0]); - if (isRtxCodec(codecs[1])) + if (isRtxCodec(codecs[1])) { filteredCodecs.push(codecs[1]); + } } // Otherwise look for a compatible set of codecs. else { for (let idx = 0; idx < codecs.length; ++idx) { if (matchCodecs(codecs[idx], capCodec)) { filteredCodecs.push(codecs[idx]); - if (isRtxCodec(codecs[idx + 1])) + if (isRtxCodec(codecs[idx + 1])) { filteredCodecs.push(codecs[idx + 1]); + } break; } } - if (filteredCodecs.length === 0) + if (filteredCodecs.length === 0) { throw new TypeError('no matching codec found'); + } } return filteredCodecs; } @@ -9733,39 +12811,45 @@ exports.canSend = canSend; function canReceive(rtpParameters, extendedRtpCapabilities) { // This may throw. validateRtpParameters(rtpParameters); - if (rtpParameters.codecs.length === 0) + if (rtpParameters.codecs.length === 0) { return false; + } const firstMediaCodec = rtpParameters.codecs[0]; return extendedRtpCapabilities.codecs .some((codec) => codec.remotePayloadType === firstMediaCodec.payloadType); } exports.canReceive = canReceive; function isRtxCodec(codec) { - if (!codec) + if (!codec) { return false; + } return /.+\/rtx$/i.test(codec.mimeType); } function matchCodecs(aCodec, bCodec, { strict = false, modify = false } = {}) { const aMimeType = aCodec.mimeType.toLowerCase(); const bMimeType = bCodec.mimeType.toLowerCase(); - if (aMimeType !== bMimeType) + if (aMimeType !== bMimeType) { return false; - if (aCodec.clockRate !== bCodec.clockRate) + } + if (aCodec.clockRate !== bCodec.clockRate) { return false; - if (aCodec.channels !== bCodec.channels) + } + if (aCodec.channels !== bCodec.channels) { return false; + } // Per codec special checks. switch (aMimeType) { case 'video/h264': { - const aPacketizationMode = aCodec.parameters['packetization-mode'] || 0; - const bPacketizationMode = bCodec.parameters['packetization-mode'] || 0; - if (aPacketizationMode !== bPacketizationMode) - return false; - // If strict matching check profile-level-id. if (strict) { - if (!h264.isSameProfile(aCodec.parameters, bCodec.parameters)) + const aPacketizationMode = aCodec.parameters['packetization-mode'] || 0; + const bPacketizationMode = bCodec.parameters['packetization-mode'] || 0; + if (aPacketizationMode !== bPacketizationMode) { + return false; + } + if (!h264.isSameProfile(aCodec.parameters, bCodec.parameters)) { return false; + } let selectedProfileLevelId; try { selectedProfileLevelId = @@ -9789,12 +12873,12 @@ function matchCodecs(aCodec, bCodec, { strict = false, modify = false } = {}) { } case 'video/vp9': { - // If strict matching check profile-id. if (strict) { const aProfileId = aCodec.parameters['profile-id'] || 0; const bProfileId = bCodec.parameters['profile-id'] || 0; - if (aProfileId !== bProfileId) + if (aProfileId !== bProfileId) { return false; + } } break; } @@ -9802,10 +12886,12 @@ function matchCodecs(aCodec, bCodec, { strict = false, modify = false } = {}) { return true; } function matchHeaderExtensions(aExt, bExt) { - if (aExt.kind && bExt.kind && aExt.kind !== bExt.kind) + if (aExt.kind && bExt.kind && aExt.kind !== bExt.kind) { return false; - if (aExt.uri !== bExt.uri) + } + if (aExt.uri !== bExt.uri) { return false; + } return true; } function reduceRtcpFeedback(codecA, codecB) { @@ -9814,13 +12900,14 @@ function reduceRtcpFeedback(codecA, codecB) { const matchingBFb = (codecB.rtcpFeedback || []) .find((bFb) => (bFb.type === aFb.type && (bFb.parameter === aFb.parameter || (!bFb.parameter && !aFb.parameter)))); - if (matchingBFb) + if (matchingBFb) { reducedRtcpFeedback.push(matchingBFb); + } } return reducedRtcpFeedback; } -},{"./utils":39,"h264-profile-level-id":4}],37:[function(require,module,exports){ +},{"./utils":46,"h264-profile-level-id":8}],44:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.parse = void 0; @@ -9842,17 +12929,21 @@ function parse(scalabilityMode) { } exports.parse = parse; -},{}],38:[function(require,module,exports){ +},{}],45:[function(require,module,exports){ "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __exportStar = (this && this.__exportStar) || function(m, exports) { - for (var p in m) if (p !== "default" && !exports.hasOwnProperty(p)) __createBinding(exports, m, p); + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; Object.defineProperty(exports, "__esModule", { value: true }); __exportStar(require("./Device"), exports); @@ -9866,7 +12957,7 @@ __exportStar(require("./SctpParameters"), exports); __exportStar(require("./handlers/HandlerInterface"), exports); __exportStar(require("./errors"), exports); -},{"./Consumer":8,"./DataConsumer":9,"./DataProducer":10,"./Device":11,"./Producer":14,"./RtpParameters":15,"./SctpParameters":16,"./Transport":17,"./errors":18,"./handlers/HandlerInterface":25}],39:[function(require,module,exports){ +},{"./Consumer":12,"./DataConsumer":13,"./DataProducer":14,"./Device":15,"./Producer":18,"./RtpParameters":19,"./SctpParameters":20,"./Transport":21,"./errors":22,"./handlers/HandlerInterface":30}],46:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateRandomNumber = exports.clone = void 0; @@ -9874,8 +12965,9 @@ exports.generateRandomNumber = exports.clone = void 0; * Clones the given data. */ function clone(data, defaultValue) { - if (typeof data === 'undefined') + if (typeof data === 'undefined') { return defaultValue; + } return JSON.parse(JSON.stringify(data)); } exports.clone = clone; @@ -9887,13 +12979,26 @@ function generateRandomNumber() { } exports.generateRandomNumber = generateRandomNumber; -},{}],40:[function(require,module,exports){ +},{}],47:[function(require,module,exports){ +arguments[4][4][0].apply(exports,arguments) +},{"./common":48,"_process":56,"dup":4}],48:[function(require,module,exports){ arguments[4][5][0].apply(exports,arguments) -},{"./common":41,"_process":48,"dup":5}],41:[function(require,module,exports){ +},{"dup":5,"ms":49}],49:[function(require,module,exports){ arguments[4][6][0].apply(exports,arguments) -},{"dup":6,"ms":42}],42:[function(require,module,exports){ -arguments[4][7][0].apply(exports,arguments) -},{"dup":7}],43:[function(require,module,exports){ +},{"dup":6}],50:[function(require,module,exports){ +(function (global){(function (){ +/*! queue-microtask. MIT License. Feross Aboukhadijeh */ +let promise + +module.exports = typeof queueMicrotask === 'function' + ? queueMicrotask.bind(typeof window !== 'undefined' ? window : global) + // reuse resolved promise, and allocate it lazily + : cb => (promise || (promise = Promise.resolve())) + .then(cb) + .catch(err => setTimeout(() => { throw err }, 0)) + +}).call(this)}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],51:[function(require,module,exports){ var grammar = module.exports = { v: [{ name: 'version', @@ -10389,7 +13494,7 @@ Object.keys(grammar).forEach(function (key) { }); }); -},{}],44:[function(require,module,exports){ +},{}],52:[function(require,module,exports){ var parser = require('./parser'); var writer = require('./writer'); @@ -10402,7 +13507,7 @@ exports.parseRemoteCandidates = parser.parseRemoteCandidates; exports.parseImageAttributes = parser.parseImageAttributes; exports.parseSimulcastStreamList = parser.parseSimulcastStreamList; -},{"./parser":45,"./writer":46}],45:[function(require,module,exports){ +},{"./parser":53,"./writer":54}],53:[function(require,module,exports){ var toIntIfInt = function (v) { return String(Number(v)) === v ? Number(v) : v; }; @@ -10528,7 +13633,7 @@ exports.parseSimulcastStreamList = function (str) { }); }; -},{"./grammar":43}],46:[function(require,module,exports){ +},{"./grammar":51}],54:[function(require,module,exports){ var grammar = require('./grammar'); // customized util.format - discards excess arguments and can void middle ones @@ -10644,7 +13749,7 @@ module.exports = function (session, opts) { return sdp.join('\r\n') + '\r\n'; }; -},{"./grammar":43}],47:[function(require,module,exports){ +},{"./grammar":51}],55:[function(require,module,exports){ // Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a @@ -11143,7 +14248,7 @@ function eventTargetAgnosticAddListener(emitter, name, listener, flags) { } } -},{}],48:[function(require,module,exports){ +},{}],56:[function(require,module,exports){ // shim for using process in browser var process = module.exports = {}; diff --git a/src/Peer.js b/src/Peer.js index 10c0199..50d4e12 100644 --- a/src/Peer.js +++ b/src/Peer.js @@ -57,10 +57,10 @@ module.exports = class Peer { if (consumer.type === 'simulcast') { await consumer.setPreferredLayers({ - spatialLayer: 2, - temporalLayer: 2 + spatialLayer: 3, + temporalLayer: 3 }) - } + } //https://www.w3.org/TR/webrtc-svc/ this.consumers.set(consumer.id, consumer) diff --git a/src/app.js b/src/app.js index 9efd546..a5ec340 100644 --- a/src/app.js +++ b/src/app.js @@ -4,6 +4,7 @@ const app = express() const https = require('httpolyglot') const fs = require('fs') const mediasoup = require('mediasoup') +const mediasoupClient = require('mediasoup-client') const config = require('./config') const path = require('path') const Room = require('./Room') @@ -20,7 +21,12 @@ const io = require('socket.io')(httpsServer) app.use(express.static(path.join(__dirname, '..', 'public'))) httpsServer.listen(config.listenPort, () => { - console.log('Listening on https://' + config.listenIp + ':' + config.listenPort) + console.log('Server', { + listening: 'https://' + 'localhost' + ':' + config.listenPort, + mediasoup_server: mediasoup.version, + mediasoup_client: mediasoupClient.version, + node_version: process.versions.node, + }) }) // all mediasoup workers @@ -167,7 +173,7 @@ io.on('connection', (socket) => { console.log('Produce', { type: `${kind}`, name: `${roomList.get(socket.room_id).getPeers().get(socket.id).name}`, - id: `${producer_id}` + producer_id: `${producer_id}` }) callback({ @@ -237,21 +243,6 @@ io.on('connection', (socket) => { }) }) -// TODO remove - never used? -function room() { - return Object.values(roomList).map((r) => { - return { - router: r.router.id, - peers: Object.values(r.peers).map((p) => { - return { - name: p.name - } - }), - id: r.id - } - }) -} - /** * Get next mediasoup Worker. */