From ed0bd5fc7f22396d6447046e790dbcbdca2a155e Mon Sep 17 00:00:00 2001 From: Charlemagne Santos Date: Thu, 10 Feb 2022 11:35:15 -0800 Subject: [PATCH] 2.20.0 Release - preflight GA and plan-b removal (#1703) * Merge Preflight and PlanB removal feature branch (#1702) * VIDEO-7728 | Better Preflight Errors (#1689) * Adding better errors on preflight * lint * Update docs * Changelog * Update docs * Convert error to string * Adding timestamp to progress events * Properly raising signaling errors * lint * Update tests * Mak's feedback * Update test * Feature/remove plan b (#1697) * VIDEO-6587 | Remove Plan B in SDK (#1656) * Initial implementation. * Fixing integration tests * Updating unit tests * Adding back unit test job * Removing unneeded case * Fix build * Adding rename suggestion Co-authored-by: Manjesh Malavalli * Adding changelog * Update CHANGELOG.md Co-authored-by: Manjesh Malavalli * Adding ticket number Co-authored-by: Manjesh Malavalli Co-authored-by: Manjesh Malavalli * Move changelog to 2.20.0 section * 2.20.0-rc1 * 2.20.0-dev Co-authored-by: Manjesh Malavalli Co-authored-by: Manjesh Malavalli Co-authored-by: twilio-ci * Fix unit test from merge * 2.20.0-rc2 * 2.20.0-dev * Update doc * Update changelog Co-authored-by: Manjesh Malavalli Co-authored-by: Manjesh Malavalli Co-authored-by: twilio-ci * Prep for release Co-authored-by: Manjesh Malavalli Co-authored-by: Manjesh Malavalli Co-authored-by: twilio-ci --- .gitignore | 1 + CHANGELOG.md | 24 +- README.md | 2 +- lib/preflight/getturncredentials.ts | 27 +- lib/preflight/preflighttest.ts | 184 +++--- lib/signaling/v2/peerconnection.js | 164 ++---- lib/signaling/v2/twilioconnectiontransport.js | 13 +- lib/util/constants.js | 1 + lib/util/sdp/index.js | 123 +--- lib/util/sdp/issue8329.js | 5 +- lib/util/sdp/simulcast.js | 38 +- .../{trackmatcher/mid.js => trackmatcher.js} | 12 +- lib/util/sdp/trackmatcher/identity.js | 24 - lib/util/sdp/trackmatcher/ordered.js | 120 ---- package.json | 2 +- .../spec/localparticipant/regressions.js | 5 - test/integration/spec/preflight.js | 45 +- test/integration/spec/util/simulcast.js | 13 +- test/lib/mocksdp.js | 23 +- test/unit/index.js | 3 +- test/unit/spec/signaling/v2/peerconnection.js | 144 +++-- test/unit/spec/util/sdp/index.js | 543 ++---------------- .../{trackmatcher/mid.js => trackmatcher.js} | 10 +- test/unit/spec/util/trackmatcher/ordered.js | 111 ---- tsdef/PreflightTypes.d.ts | 8 +- tsdef/preflighttest.d.ts | 2 +- tsdef/types.d.ts | 1 + 27 files changed, 441 insertions(+), 1207 deletions(-) rename lib/util/sdp/{trackmatcher/mid.js => trackmatcher.js} (76%) delete mode 100644 lib/util/sdp/trackmatcher/identity.js delete mode 100644 lib/util/sdp/trackmatcher/ordered.js rename test/unit/spec/util/{trackmatcher/mid.js => trackmatcher.js} (90%) delete mode 100644 test/unit/spec/util/trackmatcher/ordered.js diff --git a/.gitignore b/.gitignore index 3b358910b..0079c707a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ test/lib/sdkdriver/test/integration/browser/index.js npm-debug.log package-lock.json test.json +nodemon.json yarn.lock .env .idea diff --git a/CHANGELOG.md b/CHANGELOG.md index 14476f06c..8b5ee48e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,15 +2,37 @@ The Twilio Programmable Video SDKs use [Semantic Versioning](http://www.semver.o **Version 1.x reached End of Life on September 8th, 2021.** See the changelog entry [here](https://www.twilio.com/changelog/end-of-life-complete-for-unsupported-versions-of-the-programmable-video-sdk). Support for the 1.x version ended on December 4th, 2020. +2.20.0 (February 10, 2022) +========================== + +Changes +------- + +The Preflight API ([runPreflight](https://sdk.twilio.com/js/video/releases/2.20.0/docs/module-twilio-video.html#.runPreflight__anchor)), originally released in [2.16.0](#2160-august-11-2021), has been promoted to GA. + +Thank you @morninng @eroidaaruqaj [#1622](https://github.com/twilio/twilio-video.js/issues/1622) for your feedback. Based on this feedback, we have made the following changes to `runPreflight`. (VIDEO-7728) + +- The [failed](https://sdk.twilio.com/js/video/releases/2.20.0/docs/PreflightTest.html#event:failed) event now provides a [PreflightTestReport](https://sdk.twilio.com/js/video/releases/2.20.0/docs/global.html#PreflightTestReport) which include partial results gathered during the test. Use this in addition to the error object to get more insights on the failure. + +- Signaling and Media Connection errors are now properly surfaced via the [failed](https://sdk.twilio.com/js/video/releases/2.20.0/docs/PreflightTest.html#event:failed) event. + +- [PreflightTestReport](https://sdk.twilio.com/js/video/releases/2.20.0/docs/global.html#PreflightTestReport) now includes a `progressEvents` property. This new property is an array of [PreflightProgress](https://sdk.twilio.com/js/video/releases/2.20.0/docs/global.html#PreflightProgress) events detected during the test. Use this information to determine which steps were completed and which ones were not. + +You can learn more about `runPreflight` usage in the documentation, [here](twilio.com/docs/video/troubleshooting/preflight-api). + +Other changes in this release includes: + +- In [October 2019](#200-beta15-october-24-2019), twilio-video.js started using Unified Plan where available, while also maintaining support for earlier browser versions with Plan B as the default SDP format. With this release, twilio-video.js will now stop supporting the Plan B SDP format and will only support the Unified Plan SDP format. Please refer to this [changelog](#200-beta15-october-24-2019) and this [public advisory](https://support.twilio.com/hc/en-us/articles/360039098974-Upcoming-Breaking-Changes-in-Twilio-Video-JavaScript-SDK-Google-Chrome) for more related information. (VIDEO-6587) + 2.19.1 (February 7, 2022) ========================= + Bug Fixes --------- - Fixed a bug where media connection was not getting reconnected after a network interruption if participant was not subscribed to any tracks. (VIDEO-8315) - Fixed a bug where network quality score stops updating after network glitches. (VIDEO-8413) - 2.19.0 (January 31, 2022) ========================= diff --git a/README.md b/README.md index 81dff8146..8d201a27d 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Releases of twilio-video.js are hosted on a CDN, and you can include these directly in your web app using a <script> tag. ```html - + ``` diff --git a/lib/preflight/getturncredentials.ts b/lib/preflight/getturncredentials.ts index d11471045..6fe84625e 100644 --- a/lib/preflight/getturncredentials.ts +++ b/lib/preflight/getturncredentials.ts @@ -1,5 +1,7 @@ +/* eslint-disable camelcase */ const TwilioConnection = require('../twilioconnection.js'); const { ICE_VERSION } = require('../util/constants'); +const { createTwilioError, SignalingConnectionError } = require('../util/twilio-video-errors'); import { RTCIceServer, RTCStats } from './rtctypes'; import { EventEmitter } from 'events'; @@ -23,21 +25,28 @@ export function getTurnCredentials(token: string, wsServer: string): Promise { + twilioConnection.once('close', () => { if (!done) { done = true; - reject(reason); + reject(new SignalingConnectionError()); } }); - // eslint-disable-next-line camelcase - twilioConnection.on('message', (message: { type: string; ice_servers: RTCIceServer[]; }) => { - if (message.type === 'iced') { - if (!done) { - done = true; - resolve(message.ice_servers); - twilioConnection.close(); + twilioConnection.on('message', (messageData: { + code: number; + message: string; + ice_servers: RTCIceServer[]; + type: string; + }) => { + const { code, message, ice_servers, type } = messageData; + if ((type === 'iced' || type === 'error') && !done) { + done = true; + if (type === 'iced') { + resolve(ice_servers); + } else { + reject(createTwilioError(code, message)); } + twilioConnection.close(); } }); }); diff --git a/lib/preflight/preflighttest.ts b/lib/preflight/preflighttest.ts index ee59f3e67..4c80e8596 100644 --- a/lib/preflight/preflighttest.ts +++ b/lib/preflight/preflighttest.ts @@ -1,7 +1,8 @@ import { DEFAULT_ENVIRONMENT, DEFAULT_LOGGER_NAME, DEFAULT_LOG_LEVEL, DEFAULT_REALM, SDK_NAME, SDK_VERSION } from '../util/constants'; -import { PreflightOptions, PreflightTestReport, RTCIceCandidateStats, SelectedIceCandidatePairStats, Stats } from '../../tsdef/PreflightTypes'; +import { PreflightOptions, PreflightTestReport, ProgressEvent, RTCIceCandidateStats, SelectedIceCandidatePairStats, Stats } from '../../tsdef/PreflightTypes'; import { StatsReport } from '../../tsdef/types'; import { Timer } from './timer'; +import { TwilioError } from '../../tsdef/TwilioError'; import { calculateMOS } from './mos'; import { getCombinedConnectionStats } from './getCombinedConnectionStats'; import { getTurnCredentials } from './getturncredentials'; @@ -17,6 +18,10 @@ const MovingAverageDelta = require('../util/movingaveragedelta'); const EventObserver = require('../util/eventobserver'); const InsightsPublisher = require('../util/insightspublisher'); const { createSID, sessionSID } = require('../util/sid'); +const { + SignalingConnectionTimeoutError, + MediaConnectionError +} = require('../util/twilio-video-errors'); const SECOND = 1000; const DEFAULT_TEST_DURATION = 10 * SECOND; @@ -37,29 +42,29 @@ const PreflightProgress = { connected: 'connected', /** - * subscriberParticipant successfully subscribed to media tracks. + * SubscriberParticipant successfully subscribed to media tracks. */ mediaSubscribed: 'mediaSubscribed', /** - * media flow was detected. + * Media flow was detected. */ mediaStarted: 'mediaStarted', /** - * established DTLS connection. This is measured from RTCDtlsTransport `connecting` to `connected` state. + * Established DTLS connection. This is measured from RTCDtlsTransport `connecting` to `connected` state. * On Safari, Support for measuring this is missing, this event will be not be emitted on Safari. */ dtlsConnected: 'dtlsConnected', /** - * established a PeerConnection, This is measured from PeerConnection `connecting` to `connected` state. + * Established a PeerConnection, This is measured from PeerConnection `connecting` to `connected` state. * On Firefox, Support for measuring this is missing, this event will be not be emitted on Firefox. */ peerConnectionConnected: 'peerConnectionConnected', /** - * established ICE connection. This is measured from ICE connection `checking` to `connected` state. + * Established ICE connection. This is measured from ICE connection `checking` to `connected` state. */ iceConnected: 'iceConnected' }; @@ -110,6 +115,7 @@ export class PreflightTest extends EventEmitter { private _connectTiming = new Timer(); private _sentBytesMovingAverage = new MovingAverageDelta(); private _packetLossMovingAverage = new MovingAverageDelta(); + private _progressEvents: ProgressEvent[] = []; private _receivedBytesMovingAverage = new MovingAverageDelta(); private _log: typeof Log; private _testDuration: number; @@ -145,7 +151,7 @@ export class PreflightTest extends EventEmitter { this._stopped = true; } - private _generatePreflightReport(collectedStats?: PreflightStats, error? : Error) : PreflightTestReportInternal { + private _generatePreflightReport(collectedStats?: PreflightStats) : PreflightTestReportInternal { this._testTiming.stop(); return { testTiming: this._testTiming.getTimeMeasurement(), @@ -163,13 +169,13 @@ export class PreflightTest extends EventEmitter { }, selectedIceCandidatePairStats: collectedStats ? collectedStats.selectedIceCandidatePairStats : null, iceCandidateStats: collectedStats ? collectedStats.iceCandidateStats : [], + progressEvents: this._progressEvents, // NOTE(mpatwardhan): internal properties. - error: error?.toString(), mos: makeStat(collectedStats?.mos), }; } - private async _executePreflightStep(stepName: string, step: () => T|Promise) : Promise { + private async _executePreflightStep(stepName: string, step: () => T|Promise, timeoutError?: TwilioError|Error) : Promise { this._log.debug('Executing step: ', stepName); const MAX_STEP_DURATION = this._testDuration + 10 * SECOND; if (this._stopped) { @@ -180,7 +186,7 @@ export class PreflightTest extends EventEmitter { let timer: number | null = null; const timeoutPromise = new Promise((_resolve, reject) => { timer = setTimeout(() => { - reject(new Error(`Timed out waiting for : ${stepName}`)); + reject(timeoutError || new Error(`${stepName} timeout.`)); }, MAX_STEP_DURATION) as unknown as number; }); try { @@ -193,43 +199,53 @@ export class PreflightTest extends EventEmitter { } } - private _trackNetworkTimings(pc: RTCPeerConnection) { - pc.addEventListener('iceconnectionstatechange', () => { - if (pc.iceConnectionState === 'checking') { - this._iceTiming.start(); - } - if (pc.iceConnectionState === 'connected') { - this._iceTiming.stop(); - this.emit('progress', PreflightProgress.iceConnected); - } - }); + private _collectNetworkTimings(pc: RTCPeerConnection): Promise { + return new Promise(resolve => { + let dtlsTransport: RTCDtlsTransport; - // firefox does not support connectionstatechange. - pc.addEventListener('connectionstatechange', () => { - if (pc.connectionState === 'connecting') { - this._peerConnectionTiming.start(); - } - if (pc.connectionState === 'connected') { - this._peerConnectionTiming.stop(); - this.emit('progress', PreflightProgress.peerConnectionConnected); - } - }); + pc.addEventListener('iceconnectionstatechange', () => { + if (pc.iceConnectionState === 'checking') { + this._iceTiming.start(); + } + if (pc.iceConnectionState === 'connected') { + this._iceTiming.stop(); + this._updateProgress(PreflightProgress.iceConnected); + if (!dtlsTransport || dtlsTransport && dtlsTransport.state === 'connected') { + resolve(); + } + } + }); - // Safari does not expose sender.transport. - let senders = pc.getSenders(); - let transport = senders.map(sender => sender.transport).find(notEmpty); - if (typeof transport !== 'undefined') { - const dtlsTransport = transport as RTCDtlsTransport; - dtlsTransport.addEventListener('statechange', () => { - if (dtlsTransport.state === 'connecting') { - this._dtlsTiming.start(); + // firefox does not support connectionstatechange. + pc.addEventListener('connectionstatechange', () => { + if (pc.connectionState === 'connecting') { + this._peerConnectionTiming.start(); } - if (dtlsTransport.state === 'connected') { - this._dtlsTiming.stop(); - this.emit('progress', PreflightProgress.dtlsConnected); + if (pc.connectionState === 'connected') { + this._peerConnectionTiming.stop(); + this._updateProgress(PreflightProgress.peerConnectionConnected); } }); - } + + // Safari does not expose sender.transport. + let senders = pc.getSenders(); + let transport = senders.map(sender => sender.transport).find(notEmpty); + if (typeof transport !== 'undefined') { + dtlsTransport = transport as RTCDtlsTransport; + dtlsTransport.addEventListener('statechange', () => { + if (dtlsTransport.state === 'connecting') { + this._dtlsTiming.start(); + } + if (dtlsTransport.state === 'connected') { + this._dtlsTiming.stop(); + this._updateProgress(PreflightProgress.dtlsConnected); + if (pc.iceConnectionState === 'connected') { + resolve(); + } + } + }); + } + }); } private _setupInsights({ token, environment = DEFAULT_ENVIRONMENT, realm = DEFAULT_REALM } : { @@ -279,6 +295,7 @@ export class PreflightTest extends EventEmitter { payload: { sessionSID, preflightSID: createSID('PF'), + progressEvents: JSON.stringify(report.progressEvents), testTiming: report.testTiming, dtlsTiming: report.networkTiming.dtls, iceTiming: report.networkTiming.ice, @@ -308,13 +325,15 @@ export class PreflightTest extends EventEmitter { try { let elements = []; localTracks = await this._executePreflightStep('Acquire media', () => [syntheticAudio(), syntheticVideo({ width: 640, height: 480 })]); - this.emit('progress', PreflightProgress.mediaAcquired); + + this._updateProgress(PreflightProgress.mediaAcquired); this.emit('debug', { localTracks }); this._connectTiming.start(); - let iceServers = await this._executePreflightStep('Get turn credentials', () => getTurnCredentials(token, wsServer)); + let iceServers = await this._executePreflightStep('Get turn credentials', () => getTurnCredentials(token, wsServer), new SignalingConnectionTimeoutError()); + this._connectTiming.stop(); - this.emit('progress', PreflightProgress.connected); + this._updateProgress(PreflightProgress.connected); const senderPC: RTCPeerConnection = new RTCPeerConnection({ iceServers, iceTransportPolicy: 'relay', bundlePolicy: 'max-bundle' }); const receiverPC: RTCPeerConnection = new RTCPeerConnection({ iceServers, bundlePolicy: 'max-bundle' }); @@ -344,22 +363,21 @@ export class PreflightTest extends EventEmitter { await receiverPC.setRemoteDescription(updatedOffer); const answer = await receiverPC.createAnswer(); - const updatedAnswer = answer; - await receiverPC.setLocalDescription(updatedAnswer); - await senderPC.setRemoteDescription(updatedAnswer); - this._trackNetworkTimings(senderPC); + await receiverPC.setLocalDescription(answer); + await senderPC.setRemoteDescription(answer); + await this._collectNetworkTimings(senderPC); return remoteTracksPromise; - }); + }, new MediaConnectionError()); this.emit('debug', { remoteTracks }); remoteTracks.forEach(track => { track.addEventListener('ended', () => this._log.warn(track.kind + ':ended')); track.addEventListener('mute', () => this._log.warn(track.kind + ':muted')); track.addEventListener('unmute', () => this._log.warn(track.kind + ':unmuted')); }); - this.emit('progress', PreflightProgress.mediaSubscribed); + this._updateProgress(PreflightProgress.mediaSubscribed); - await this._executePreflightStep('wait for tracks to start', () => { + await this._executePreflightStep('Wait for tracks to start', () => { return new Promise(resolve => { const element = document.createElement('video'); element.autoplay = true; @@ -370,21 +388,21 @@ export class PreflightTest extends EventEmitter { this.emit('debugElement', element); element.oncanplay = resolve; }); - }); + }, new MediaConnectionError()); this._mediaTiming.stop(); - this.emit('progress', PreflightProgress.mediaStarted); + this._updateProgress(PreflightProgress.mediaStarted); - const collectedStats = await this._executePreflightStep('collect stats for duration', + const collectedStats = await this._executePreflightStep('Collect stats for duration', () => this._collectRTCStatsForDuration(this._testDuration, initCollectedStats(), senderPC, receiverPC)); - const report = await this._executePreflightStep('generate report', () => this._generatePreflightReport(collectedStats)); + const report = await this._executePreflightStep('Generate report', () => this._generatePreflightReport(collectedStats)); reportToInsights({ report }); this.emit('completed', report); } catch (error) { - // eslint-disable-next-line no-undefined - reportToInsights({ report: this._generatePreflightReport(undefined, error) }); - this.emit('failed', error); + const preflightReport = this._generatePreflightReport(); + reportToInsights({ report: { ...preflightReport, error: error?.toString() } }); + this.emit('failed', error, preflightReport); } finally { pcs.forEach(pc => pc.close()); localTracks.forEach(track => track.stop()); @@ -438,6 +456,12 @@ export class PreflightTest extends EventEmitter { } return collectedStats; } + + private _updateProgress(name: string): void { + const duration = Date.now() - this._testTiming.getTimeMeasurement().start; + this._progressEvents.push({ duration, name }); + this.emit('progress', name); + } } @@ -478,16 +502,16 @@ function initCollectedStats() : PreflightStats { /** * Represents stats for a numerical metric. * @typedef {object} Stats - * @property {number} [average] - average value observed. - * @property {number} [max] - max value observed. - * @property {number} [min] - min value observed. + * @property {number} [average] - Average value observed. + * @property {number} [max] - Max value observed. + * @property {number} [min] - Min value observed. */ /** * Represents stats for a numerical metric. * @typedef {object} SelectedIceCandidatePairStats - * @property {RTCIceCandidateStats} [localCandidate] - selected local ice candidate - * @property {RTCIceCandidateStats} [remoteCandidate] - selected local ice candidate + * @property {RTCIceCandidateStats} [localCandidate] - Selected local ice candidate + * @property {RTCIceCandidateStats} [remoteCandidate] - Selected local ice candidate */ /** @@ -498,6 +522,13 @@ function initCollectedStats() : PreflightStats { * @property {Stats} [packetLoss] - Packet loss as a percent of total packets sent. */ +/** + * A {@link PreflightProgress} event with timing information. + * @typedef {object} ProgressEvent + * @property {number} [duration] - The duration of the event, measured from the start of the test. + * @property {string} [name] - The {@link PreflightProgress} event name. + */ + /** * Represents report generated by {@link PreflightTest}. * @typedef {object} PreflightTestReport @@ -505,7 +536,9 @@ function initCollectedStats() : PreflightStats { * @property {NetworkTiming} [networkTiming] - Network related time measurements. * @property {PreflightReportStats} [stats] - RTC related stats captured during the test. * @property {Array} [iceCandidateStats] - List of gathered ice candidates. - * @property {SelectedIceCandidatePairStats} selectedIceCandidatePairStats - stats for the ice candidates that were used for the connection. + * @property {SelectedIceCandidatePairStats} selectedIceCandidatePairStats - Stats for the ice candidates that were used for the connection. + * @property {Array} [progressEvents] - {@link ProgressEvent} events detected during the test. + * Use this information to determine which steps were completed and which ones were not. */ /** @@ -523,19 +556,22 @@ function initCollectedStats() : PreflightStats { /** * Preflight test has completed successfully. - * @param {PreflightTestReport} report - results of the test. + * @param {PreflightTestReport} report - Results of the test. * @event PreflightTest#completed */ /** - * Preflight test has encountered a failed and is now stopped. - * @param {TwilioError|Error} error - error object + * Preflight test has encountered a failure and is now stopped. + * @param {TwilioError|Error} error - A TwilioError or a DOMException. + * Possible TwilioErrors include Signaling and Media related errors which can be found + * here. + * @param {PreflightTestReport} report - Partial results gathered during the test. Use this information to help determine the cause of failure. * @event PreflightTest#failed */ /** * Emitted to indicate progress of the test - * @param {PreflightProgress} progress - indicates the status completed. + * @param {PreflightProgress} progress - Indicates the status completed. * @event PreflightTest#progress */ @@ -545,17 +581,17 @@ function initCollectedStats() : PreflightStats { * @description Run a preflight test. This method will start a test to check the quality of network connection. * @memberof module:twilio-video * @param {string} token - The Access Token string - * @param {PreflightOptions} options - options for the test - * @returns {PreflightTest} preflightTest - an instance to be used to monitor progress of the test. + * @param {PreflightOptions} options - Options for the test + * @returns {PreflightTest} preflightTest - An instance to be used to monitor progress of the test. * @example * var { runPreflight } = require('twilio-video'); - * var preflight = runPreflight(); + * var preflight = runPreflight(token, preflightOptions); * preflightTest.on('progress', progress => { * console.log('preflight progress:', progress); * }); * - * preflightTest.on('failed', error => { - * console.error('preflight error:', error); + * preflightTest.on('failed', (error, report) => { + * console.error('preflight error:', error, report); * }); * * preflightTest.on('completed', report => { diff --git a/lib/signaling/v2/peerconnection.js b/lib/signaling/v2/peerconnection.js index 909658e10..efd452799 100644 --- a/lib/signaling/v2/peerconnection.js +++ b/lib/signaling/v2/peerconnection.js @@ -3,7 +3,6 @@ const DefaultBackoff = require('backoff'); const { - MediaStream: DefaultMediaStream, RTCIceCandidate: DefaultRTCIceCandidate, RTCPeerConnection: DefaultRTCPeerConnection, RTCSessionDescription: DefaultRTCSessionDescription, @@ -11,8 +10,6 @@ const { } = require('@twilio/webrtc'); const util = require('@twilio/webrtc/lib/util'); -const { guessBrowser } = util; -const { getSdpFormat } = require('@twilio/webrtc/lib/util/sdp'); const { DEFAULT_ICE_GATHERING_TIMEOUT_MS, @@ -22,18 +19,17 @@ const { } = require('../../util/constants'); const { + addOrRewriteNewTrackIds, + addOrRewriteTrackIds, createCodecMapForMediaSection, disableRtx, enableDtxForOpus, + filterLocalCodecs, getMediaSections, removeSSRCAttributes, revertSimulcast, - setBitrateParameters, setCodecPreferences, - setSimulcast, - unifiedPlanAddOrRewriteNewTrackIds, - unifiedPlanAddOrRewriteTrackIds, - unifiedPlanFilterLocalCodecs + setSimulcast } = require('../../util/sdp'); const DefaultTimeout = require('../../util/timeout'); @@ -57,22 +53,16 @@ const DataTrackReceiver = require('../../data/receiver'); const MediaTrackReceiver = require('../../media/track/receiver'); const StateMachine = require('../../statemachine'); const Log = require('../../util/log'); -const IdentityTrackMatcher = require('../../util/sdp/trackmatcher/identity'); -const OrderedTrackMatcher = require('../../util/sdp/trackmatcher/ordered'); -const MIDTrackMatcher = require('../../util/sdp/trackmatcher/mid'); +const TrackMatcher = require('../../util/sdp/trackmatcher'); const workaroundIssue8329 = require('../../util/sdp/issue8329'); -const guess = guessBrowser(); +const guess = util.guessBrowser(); const platform = getPlatform(); const isAndroid = /android/.test(platform); const isChrome = guess === 'chrome'; const isFirefox = guess === 'firefox'; const isSafari = guess === 'safari'; -const isRTCRtpSenderParamsSupported = typeof RTCRtpSender !== 'undefined' - && typeof RTCRtpSender.prototype.getParameters === 'function' - && typeof RTCRtpSender.prototype.setParameters === 'function'; - let nInstances = 0; /* @@ -131,17 +121,14 @@ class PeerConnectionV2 extends StateMachine { dummyAudioMediaStreamTrack: null, isChromeScreenShareTrack, iceServers: [], - isRTCRtpSenderParamsSupported, logLevel: DEFAULT_LOG_LEVEL, offerOptions: {}, revertSimulcast, sessionTimeout: DEFAULT_SESSION_TIMEOUT_SEC * 1000, - setBitrateParameters, setCodecPreferences, setSimulcast, Backoff: DefaultBackoff, IceConnectionMonitor: DefaultIceConnectionMonitor, - MediaStream: DefaultMediaStream, RTCIceCandidate: DefaultRTCIceCandidate, RTCPeerConnection: DefaultRTCPeerConnection, RTCSessionDescription: DefaultRTCSessionDescription, @@ -149,10 +136,6 @@ class PeerConnectionV2 extends StateMachine { }, options); const configuration = getConfiguration(options); - const sdpFormat = getSdpFormat(configuration.sdpSemantics); - const isUnifiedPlan = sdpFormat === 'unified'; - - const localMediaStream = isUnifiedPlan ? null : new options.MediaStream(); const logLevels = buildLogLevels(options.logLevel); const RTCPeerConnection = options.RTCPeerConnection; @@ -166,7 +149,7 @@ class PeerConnectionV2 extends StateMachine { const peerConnection = new RTCPeerConnection(configuration, options.chromeSpecificConstraints); if (options.dummyAudioMediaStreamTrack) { - peerConnection.addTrack(options.dummyAudioMediaStreamTrack, localMediaStream || new options.MediaStream()); + peerConnection.addTrack(options.dummyAudioMediaStreamTrack); } Object.defineProperties(this, { @@ -229,12 +212,6 @@ class PeerConnectionV2 extends StateMachine { writable: true, value: false }, - _isUnifiedPlan: { - value: isUnifiedPlan - }, - _isRTCRtpSenderParamsSupported: { - value: options.isRTCRtpSenderParamsSupported - }, _lastIceConnectionState: { writable: true, value: null @@ -262,9 +239,6 @@ class PeerConnectionV2 extends StateMachine { writable: true, value: null }, - _localMediaStream: { - value: localMediaStream - }, _localUfrag: { writable: true, value: null @@ -304,13 +278,9 @@ class PeerConnectionV2 extends StateMachine { }, _onEncodingParametersChanged: { value: oncePerTick(() => { - if (this._isRTCRtpSenderParamsSupported) { - if (!this._needsAnswer) { - updateEncodingParameters(this); - } - return; + if (!this._needsAnswer) { + updateEncodingParameters(this); } - this.offer(); }) }, _peerConnection: { @@ -349,12 +319,6 @@ class PeerConnectionV2 extends StateMachine { writable: true, value: new IceBox() }, - _sdpFormat: { - value: sdpFormat - }, - _setBitrateParameters: { - value: options.setBitrateParameters - }, _setCodecPreferences: { // NOTE(mmalavalli): Re-ordering payload types in order to make sure a non-H264 // preferred codec is selected does not work on Android Firefox due to this behavior: @@ -692,7 +656,7 @@ class PeerConnectionV2 extends StateMachine { if (this._shouldApplySimulcast) { let sdpWithoutSimulcast = updatedSdp; - updatedSdp = this._setSimulcast(sdpWithoutSimulcast, this._sdpFormat, this._trackIdsToAttributes); + updatedSdp = this._setSimulcast(sdpWithoutSimulcast, this._trackIdsToAttributes); // NOTE(syerrapragada): VMS does not support H264 simulcast. So, // unset simulcast for sections in local offer where corresponding // sections in answer doesn't have vp8 as preferred codec and reapply offer. @@ -928,7 +892,7 @@ class PeerConnectionV2 extends StateMachine { /** * Handle a track event. * @private - * @param {Event} event + * @param {RTCTrackEvent} event * @returns {void} */ _handleTrackEvent(event) { @@ -936,26 +900,18 @@ class PeerConnectionV2 extends StateMachine { ? this._peerConnection.remoteDescription.sdp : null; - if (!this._trackMatcher) { - this._trackMatcher = event.transceiver && event.transceiver.mid - ? new MIDTrackMatcher() - // NOTE(mroberts): Until Chrome ships RTCRtpTransceivers with MID - // support, we have to use the same hacky solution as Safari. Revisit - // this when RTCRtpTransceivers and MIDs land. We should be able to use - // the same technique as Firefox. - : isSafari || this._isUnifiedPlan ? new OrderedTrackMatcher() : new IdentityTrackMatcher(); - } + this._trackMatcher = this._trackMatcher || new TrackMatcher(); this._trackMatcher.update(sdp); const mediaStreamTrack = event.track; const signaledTrackId = this._trackMatcher.match(event) || mediaStreamTrack.id; const mediaTrackReceiver = new MediaTrackReceiver(signaledTrackId, mediaStreamTrack); - // NOTE(mmalavalli): In unified plan mode, "ended" is not fired on the remote - // MediaStreamTrack when the remote peer removes a track. So, when this - // MediaStreamTrack is re-used for a different track due to the remote peer - // calling RTCRtpSender.replaceTrack(), we delete the previous MediaTrackReceiver - // that owned this MediaStreamTrack before adding the new MediaTrackReceiver. + // NOTE(mmalavalli): "ended" is not fired on the remote MediaStreamTrack when + // the remote peer removes a track. So, when this MediaStreamTrack is re-used + // for a different track due to the remote peer calling RTCRtpSender.replaceTrack(), + // we delete the previous MediaTrackReceiver that owned this MediaStreamTrack + // before adding the new MediaTrackReceiver. this._mediaTrackReceivers.forEach(trackReceiver => { if (trackReceiver.track.id === mediaTrackReceiver.track.id) { this._mediaTrackReceivers.delete(trackReceiver); @@ -1016,18 +972,16 @@ class PeerConnectionV2 extends StateMachine { let shouldReoffer = this._shouldOffer; if (localDescription && localDescription.sdp) { - // NOTE(mmalavalli): For "unified-plan" sdps, if the local RTCSessionDescription - // has fewer audio and/or video send* m= lines than the corresponding RTCRtpSenders - // with non-null MediaStreamTracks, it means that the newly added RTCRtpSenders - // require renegotiation. - if (this._isUnifiedPlan) { - const senders = this._peerConnection.getSenders().filter(sender => sender.track); - shouldReoffer = ['audio', 'video'].reduce((shouldOffer, kind) => { - const mediaSections = getMediaSections(localDescription.sdp, kind, '(sendrecv|sendonly)'); - const sendersOfKind = senders.filter(isSenderOfKind.bind(null, kind)); - return shouldOffer || (mediaSections.length < sendersOfKind.length); - }, shouldReoffer); - } + // NOTE(mmalavalli): If the local RTCSessionDescription has fewer audio and/or + // video send* m= lines than the corresponding RTCRtpSenders with non-null + // MediaStreamTracks, it means that the newly added RTCRtpSenders require + // renegotiation. + const senders = this._peerConnection.getSenders().filter(sender => sender.track); + shouldReoffer = ['audio', 'video'].reduce((shouldOffer, kind) => { + const mediaSections = getMediaSections(localDescription.sdp, kind, '(sendrecv|sendonly)'); + const sendersOfKind = senders.filter(isSenderOfKind.bind(null, kind)); + return shouldOffer || (mediaSections.length < sendersOfKind.length); + }, shouldReoffer); // NOTE(mroberts): We also need to re-offer if we have a DataTrack to share // but no m= application section. @@ -1084,8 +1038,8 @@ class PeerConnectionV2 extends StateMachine { // Looks like we are not referencing those attributes, but this changes goes ahead and removes them to see if it works. // this also helps reduce bytes on wires let sdp = removeSSRCAttributes(offer.sdp, ['mslabel', 'label']); - sdp = this._isUnifiedPlan && this._peerConnection.remoteDescription - ? unifiedPlanFilterLocalCodecs(sdp, this._peerConnection.remoteDescription.sdp) + sdp = this._peerConnection.remoteDescription + ? filterLocalCodecs(sdp, this._peerConnection.remoteDescription.sdp) : sdp; let updatedSdp = this._setCodecPreferences( @@ -1103,7 +1057,7 @@ class PeerConnectionV2 extends StateMachine { type: 'offer', sdp: updatedSdp }; - updatedSdp = this._setSimulcast(updatedSdp, this._sdpFormat, this._trackIdsToAttributes); + updatedSdp = this._setSimulcast(updatedSdp, this._trackIdsToAttributes); } return this._setLocalDescription({ type: 'offer', @@ -1126,7 +1080,7 @@ class PeerConnectionV2 extends StateMachine { } /** - * Add or rewrite local MediaStreamTrack IDs in the given Unified Plan RTCSessionDescription. + * Add or rewrite local MediaStreamTrack IDs in the given RTCSessionDescription. * @private * @param {RTCSessionDescription} description * @return {RTCSessionDescription} @@ -1141,7 +1095,7 @@ class PeerConnectionV2 extends StateMachine { // to the assigned m= sections here. const assignedTransceivers = activeTransceivers.filter(({ mid }) => mid); const midsToTrackIds = new Map(assignedTransceivers.map(({ mid, sender }) => [mid, this._getMediaTrackSenderId(sender.track.id)])); - const sdp1 = unifiedPlanAddOrRewriteTrackIds(description.sdp, midsToTrackIds); + const sdp1 = addOrRewriteTrackIds(description.sdp, midsToTrackIds); // NOTE(mmalavalli): Chrome and Safari do not apply the offer until they get an answer. // So, we add or re-write the actual MediaStreamTrack IDs to the unassigned m= sections here. @@ -1150,7 +1104,7 @@ class PeerConnectionV2 extends StateMachine { kind, unassignedTransceivers.filter(({ sender }) => sender.track.kind === kind).map(({ sender }) => this._getMediaTrackSenderId(sender.track.id)) ])); - const sdp2 = unifiedPlanAddOrRewriteNewTrackIds(sdp1, midsToTrackIds, newTrackIdsByKind); + const sdp2 = addOrRewriteNewTrackIds(sdp1, midsToTrackIds, newTrackIdsByKind); return new this._RTCSessionDescription({ sdp: sdp2, @@ -1199,7 +1153,7 @@ class PeerConnectionV2 extends StateMachine { throw errorToThrow; }).then(() => { if (description.type !== 'rollback') { - this._localDescription = this._isUnifiedPlan ? this._addOrRewriteLocalTrackIds(description) : description; + this._localDescription = this._addOrRewriteLocalTrackIds(description); // NOTE(mmalavalli): In order for this feature to be backward compatible with older // SDK versions which to not support opus DTX, we append "usedtx=1" to the local SDP @@ -1234,13 +1188,6 @@ class PeerConnectionV2 extends StateMachine { */ _setRemoteDescription(description) { if (description.sdp) { - if (!this._isRTCRtpSenderParamsSupported) { - description.sdp = this._setBitrateParameters( - description.sdp, - isFirefox ? 'TIAS' : 'AS', - this._encodingParameters.maxAudioBitrate, - this._encodingParameters.maxVideoBitrate); - } description.sdp = this._setCodecPreferences( description.sdp, this._preferredAudioCodecs, @@ -1505,14 +1452,8 @@ class PeerConnectionV2 extends StateMachine { if (this._peerConnection.signalingState === 'closed' || this._rtpSenders.has(mediaTrackSender)) { return; } - let sender; - if (this._localMediaStream) { - this._localMediaStream.addTrack(mediaTrackSender.track); - sender = this._peerConnection.addTrack(mediaTrackSender.track, this._localMediaStream); - } else { - const transceiver = this._addOrUpdateTransceiver(mediaTrackSender.track); - sender = transceiver.sender; - } + const transceiver = this._addOrUpdateTransceiver(mediaTrackSender.track); + const { sender } = transceiver; mediaTrackSender.addSender(sender, encodings => this._setPublisherHint(mediaTrackSender, encodings)); this._rtpNewSenders.add(sender); this._rtpSenders.set(mediaTrackSender, sender); @@ -1531,7 +1472,7 @@ class PeerConnectionV2 extends StateMachine { } /** - * Get the {@link DataTrackReceiver}s and the {@link MediaTrackReceivers} on the + * Get the {@link DataTrackReceiver}s and the {@link MediaTrackReceiver}s on the * {@link PeerConnectionV2}. * @returns {Array} trackReceivers */ @@ -1615,9 +1556,6 @@ class PeerConnectionV2 extends StateMachine { if (this._peerConnection.signalingState !== 'closed') { this._peerConnection.removeTrack(sender); } - if (this._localMediaStream) { - this._localMediaStream.removeTrack(mediaTrackSender.track); - } mediaTrackSender.removeSender(sender); // clean up any pending publisher hints associated with this mediaTrackSender. if (this._mediaTrackSenderToPublisherHints.has(mediaTrackSender)) { @@ -1825,7 +1763,7 @@ function takeRecycledTransceiver(pcv2, kind) { */ function updateLocalCodecs(pcv2) { const description = pcv2._peerConnection.localDescription; - if (!description) { + if (!description || !description.sdp) { return; } getMediaSections(description.sdp).forEach(section => { @@ -1841,11 +1779,15 @@ function updateLocalCodecs(pcv2) { */ function updateRemoteCodecMaps(pcv2) { const description = pcv2._peerConnection.remoteDescription; - if (!description) { + if (!description || !description.sdp) { return; } getMediaSections(description.sdp).forEach(section => { - const mid = section.match(/^a=mid:(.+)$/m)[1]; + const matched = section.match(/^a=mid:(.+)$/m); + if (!matched || !matched[1]) { + return; + } + const mid = matched[1]; const codecMap = createCodecMapForMediaSection(section); pcv2._remoteCodecMaps.set(mid, codecMap); }); @@ -1872,17 +1814,13 @@ function updateRecycledTransceivers(pcv2) { * @returns {void} */ function negotiationCompleted(pcv2) { - if (pcv2._isUnifiedPlan) { - updateRecycledTransceivers(pcv2); - updateLocalCodecs(pcv2); - updateRemoteCodecMaps(pcv2); - } - if (pcv2._isRTCRtpSenderParamsSupported) { - updateEncodingParameters(pcv2).then(() => { - // if there any any publisher hints queued, apply them now. - pcv2._handleQueuedPublisherHints(); - }); - } + updateRecycledTransceivers(pcv2); + updateLocalCodecs(pcv2); + updateRemoteCodecMaps(pcv2); + updateEncodingParameters(pcv2).then(() => { + // if there any any publisher hints queued, apply them now. + pcv2._handleQueuedPublisherHints(); + }); } /** diff --git a/lib/signaling/v2/twilioconnectiontransport.js b/lib/signaling/v2/twilioconnectiontransport.js index 0aa2eeee8..8091826fc 100644 --- a/lib/signaling/v2/twilioconnectiontransport.js +++ b/lib/signaling/v2/twilioconnectiontransport.js @@ -1,12 +1,11 @@ 'use strict'; -const { getSdpFormat } = require('@twilio/webrtc/lib/util/sdp'); const StateMachine = require('../../statemachine'); const TwilioConnection = require('../../twilioconnection'); const DefaultBackoff = require('backoff'); const { reconnectBackoffConfig } = require('../../util/constants'); const Timeout = require('../../util/timeout'); -const { SDK_NAME, SDK_VERSION } = require('../../util/constants'); +const { SDK_NAME, SDK_VERSION, SDP_FORMAT } = require('../../util/constants'); const { createBandwidthProfilePayload, @@ -88,7 +87,6 @@ class TwilioConnectionTransport extends StateMachine { Backoff: DefaultBackoff, TwilioConnection, iceServers: null, - sdpFormat: getSdpFormat(options.sdpSemantics), trackPriority: true, trackSwitchOff: true, renderHints: true, @@ -237,13 +235,8 @@ class TwilioConnectionTransport extends StateMachine { this._adaptiveSimulcast, this._renderHints); - message.subscribe = createSubscribePayload( - this._automaticSubscription); - - const sdpFormat = this._options.sdpFormat; - if (sdpFormat) { - message.format = sdpFormat; - } + message.subscribe = createSubscribePayload(this._automaticSubscription); + message.format = SDP_FORMAT; message.token = this._accessToken; } else if (message.type === 'sync') { message.session = this._session; diff --git a/lib/util/constants.js b/lib/util/constants.js index a8cc4faea..c4cd547de 100644 --- a/lib/util/constants.js +++ b/lib/util/constants.js @@ -2,6 +2,7 @@ const packageInfo = require('../../package.json'); module.exports.SDK_NAME = `${packageInfo.name}.js`; module.exports.SDK_VERSION = packageInfo.version; +module.exports.SDP_FORMAT = 'unified'; module.exports.DEFAULT_ENVIRONMENT = 'prod'; module.exports.DEFAULT_REALM = 'us1'; diff --git a/lib/util/sdp/index.js b/lib/util/sdp/index.js index c6bf157cc..09633b908 100644 --- a/lib/util/sdp/index.js +++ b/lib/util/sdp/index.js @@ -18,30 +18,6 @@ const ptToFixedBitrateAudioCodecName = { * @typedef {AudioCodec|VideoCodec} Codec */ -// NOTE(mmalavalli): This value is derived from the IETF spec -// for JSEP, and it is used to convert a 'b=TIAS' value in bps -// to a 'b=AS' value in kbps. -// Spec: https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-21#section-5.9 -const RTCP_BITRATE = 16000; - -/** - * Construct a b= line string for the given max bitrate in bps. If the modifier - * is 'AS', then the max bitrate will be converted to kbps using the formula - * specified in the IETF spec for JSEP mentioned above. - * @param {string} modifier - 'AS' | 'TIAS' - * @param {?number} maxBitrate - Max outgoing bitrate (bps) - * @returns {?string} - If "maxBitrate" is null, then returns null; - * otherwise return the constructed b= line string - */ -function createBLine(modifier, maxBitrate) { - if (!maxBitrate) { - return null; - } - return `\r\nb=${modifier}:${modifier === 'AS' - ? Math.round((maxBitrate + RTCP_BITRATE) / 950) - : maxBitrate}`; -} - /** * Create a Codec Map for the given m= section. * @param {string} section - The given m= section @@ -166,53 +142,6 @@ function getReorderedPayloadTypes(codecMap, preferredCodecs) { return preferredPayloadTypes.concat(remainingPayloadTypes); } -/** - * Set the specified max bitrate in the given m= section. - * @param {string} modifier - 'AS' | 'TIAS' - * @param {?number} maxBitrate - Max outgoing bitrate (bps) - * @param {string} section - m= section string - * @returns {string} The updated m= section - */ -function setBitrateInMediaSection(modifier, maxBitrate, section) { - let bLine = createBLine(modifier, maxBitrate) || ''; - const bLinePattern = /\r\nb=(AS|TIAS):([0-9]+)/; - const bLineMatched = section.match(bLinePattern); - - if (!bLineMatched) { - return section.replace(/(\r\n)?$/, `${bLine}$1`); - } - - const maxBitrateMatched = parseInt(bLineMatched[2], 10); - maxBitrate = maxBitrate || Infinity; - bLine = createBLine(modifier, Math.min(maxBitrateMatched, maxBitrate)); - return section.replace(bLinePattern, bLine); -} - -/** - * Set maximum bitrates to the media sections in a given sdp. - * @param {string} sdp - sdp string - * @param {string} modifier - 'AS' | 'TIAS" - * @param {?number} maxAudioBitrate - Max outgoing audio bitrate (bps), null - * if no limit is to be applied - * @param {?number} maxVideoBitrate - Max outgoing video bitrate (bps), null - * if no limit is to be applied - * @returns {string} - The updated sdp string - */ -function setBitrateParameters(sdp, modifier, maxAudioBitrate, maxVideoBitrate) { - const mediaSections = getMediaSections(sdp); - const session = sdp.split('\r\nm=')[0]; - return [session].concat(mediaSections.map(section => { - // Bitrate parameters should not be applied to m=application sections - // or to m=(audio|video) sections that do not receive media. - if (!/^m=(audio|video)/.test(section) || !/a=(recvonly|sendrecv)/.test(section)) { - return section; - } - const kind = section.match(/^m=(audio|video)/)[1]; - const maxBitrate = kind === 'audio' ? maxAudioBitrate : maxVideoBitrate; - return setBitrateInMediaSection(modifier, maxBitrate, section); - })).join('\r\n'); -} - /** * Set the given Codec Payload Types in the first line of the given m= section. * @param {Array} payloadTypes - Payload Types @@ -265,11 +194,10 @@ function setCodecPreferences(sdp, preferredAudioCodecs, preferredVideoCodecs) { /** * Return a new SDP string with simulcast settings. * @param {string} sdp - * @param {'planb' | 'unified'} sdpFormat * @param {Map} trackIdsToAttributes * @returns {string} Updated SDP string */ -function setSimulcast(sdp, sdpFormat, trackIdsToAttributes) { +function setSimulcast(sdp, trackIdsToAttributes) { const mediaSections = getMediaSections(sdp); const session = sdp.split('\r\nm=')[0]; return [session].concat(mediaSections.map(section => { @@ -283,13 +211,13 @@ function setSimulcast(sdp, sdpFormat, trackIdsToAttributes) { const hasVP8PayloadType = payloadTypes.some(payloadType => vp8PayloadTypes.has(payloadType)); return hasVP8PayloadType - ? setSimulcastInMediaSection(section, sdpFormat, trackIdsToAttributes) + ? setSimulcastInMediaSection(section, trackIdsToAttributes) : section; })).concat('').join('\r\n'); } /** - * Get the matching Payload Types in a unified plan m= section for a particular peer codec. + * Get the matching Payload Types in an m= section for a particular peer codec. * @param {Codec} peerCodec * @param {PT} peerPt * @param {Map} codecsToPts @@ -297,7 +225,7 @@ function setSimulcast(sdp, sdpFormat, trackIdsToAttributes) { * @param {string} peerSection * @returns {Array} */ -function unifiedPlanGetMatchingPayloadTypes(peerCodec, peerPt, codecsToPts, section, peerSection) { +function getMatchingPayloadTypes(peerCodec, peerPt, codecsToPts, section, peerSection) { // If there is at most one local Payload Type that matches the remote codec, retain it. const matchingPts = codecsToPts.get(peerCodec) || []; if (matchingPts.length <= 1) { @@ -328,14 +256,13 @@ function unifiedPlanGetMatchingPayloadTypes(peerCodec, peerPt, codecsToPts, sect } /** - * Filter codecs in a unified plan m= section based on its peer m= section. - * from the other peer. + * Filter codecs in an m= section based on its peer m= section from the other peer. * @param {string} section * @param {Map} peerMidsToMediaSections * @param {Array} codecsToRemove * @returns {string} */ -function unifiedPlanFilterCodecsInMediaSection(section, peerMidsToMediaSections, codecsToRemove) { +function filterCodecsInMediaSection(section, peerMidsToMediaSections, codecsToRemove) { // Do nothing if the m= section represents neither audio nor video. if (!/^m=(audio|video)/.test(section)) { return section; @@ -355,7 +282,7 @@ function unifiedPlanFilterCodecsInMediaSection(section, peerMidsToMediaSections, // Maintain a list of non-rtx Payload Types to retain. let pts = flatMap(Array.from(peerPtToCodecs), ([peerPt, peerCodec]) => peerCodec !== 'rtx' && !codecsToRemove.includes(peerCodec) - ? unifiedPlanGetMatchingPayloadTypes( + ? getMatchingPayloadTypes( peerCodec, peerPt, codecsToPts, @@ -389,17 +316,17 @@ function unifiedPlanFilterCodecsInMediaSection(section, peerMidsToMediaSections, } /** - * Filter local codecs based on the remote unified plan SDP. + * Filter local codecs based on the remote SDP. * @param {string} localSdp * @param {string} remoteSdp * @returns {string} - Updated local SDP */ -function unifiedPlanFilterLocalCodecs(localSdp, remoteSdp) { +function filterLocalCodecs(localSdp, remoteSdp) { const localMediaSections = getMediaSections(localSdp); const localSession = localSdp.split('\r\nm=')[0]; const remoteMidsToMediaSections = createMidToMediaSectionMap(remoteSdp); return [localSession].concat(localMediaSections.map(localSection => { - return unifiedPlanFilterCodecsInMediaSection(localSection, remoteMidsToMediaSections, []); + return filterCodecsInMediaSection(localSection, remoteMidsToMediaSections, []); })).join('\r\n'); } @@ -439,16 +366,16 @@ function revertSimulcast(localSdp, localSdpWithoutSimulcast, remoteSdp, revertFo } /** - * Add or rewrite MSIDs for new m= sections in the given Unified Plan SDP with their - * corresponding local MediaStreamTrack IDs. These can be different when previously - * removed MediaStreamTracks are added back (or Track IDs may not be present in the - * SDPs at all once browsers implement the latest WebRTC spec). + * Add or rewrite MSIDs for new m= sections in the given SDP with their corresponding + * local MediaStreamTrack IDs. These can be different when previously removed MediaStreamTracks + * are added back (or Track IDs may not be present in the SDPs at all once browsers implement + * the latest WebRTC spec). * @param {string} sdp * @param {Map} activeMidsToTrackIds * @param {Map>} trackIdsByKind * @returns {string} */ -function unifiedPlanAddOrRewriteNewTrackIds(sdp, activeMidsToTrackIds, trackIdsByKind) { +function addOrRewriteNewTrackIds(sdp, activeMidsToTrackIds, trackIdsByKind) { // NOTE(mmalavalli): The m= sections for the new MediaStreamTracks are usually // present after the m= sections for the existing MediaStreamTracks, in order // of addition. @@ -458,18 +385,18 @@ function unifiedPlanAddOrRewriteNewTrackIds(sdp, activeMidsToTrackIds, trackIdsB newMids.forEach((mid, i) => midsToTrackIds.set(mid, trackIds[i])); return midsToTrackIds; }, new Map()); - return unifiedPlanAddOrRewriteTrackIds(sdp, newMidsToTrackIds); + return addOrRewriteTrackIds(sdp, newMidsToTrackIds); } /** - * Add or rewrite MSIDs in the given Unified Plan SDP with their corresponding local - * MediaStreamTrack IDs. These IDs need not be the same (or Track IDs may not be - * present in the SDPs at all once browsers implement the latest WebRTC spec). + * Add or rewrite MSIDs in the given SDP with their corresponding local MediaStreamTrack IDs. + * These IDs need not be the same (or Track IDs may not be present in the SDPs at all once + * browsers implement the latest WebRTC spec). * @param {string} sdp * @param {Map} midsToTrackIds * @returns {string} */ -function unifiedPlanAddOrRewriteTrackIds(sdp, midsToTrackIds) { +function addOrRewriteTrackIds(sdp, midsToTrackIds) { const mediaSections = getMediaSections(sdp); const session = sdp.split('\r\nm=')[0]; return [session].concat(mediaSections.map(mediaSection => { @@ -501,7 +428,7 @@ function unifiedPlanAddOrRewriteTrackIds(sdp, midsToTrackIds) { } /** - * removes specified ssrc attributes from given sdp + * Removes specified ssrc attributes from given sdp. * @param {string} sdp * @param {Array} ssrcAttributesToRemove * @returns {string} @@ -632,17 +559,15 @@ function enableDtxForOpus(sdp, mids) { })).join('\r\n'); } +exports.addOrRewriteNewTrackIds = addOrRewriteNewTrackIds; +exports.addOrRewriteTrackIds = addOrRewriteTrackIds; exports.createCodecMapForMediaSection = createCodecMapForMediaSection; exports.createPtToCodecName = createPtToCodecName; exports.disableRtx = disableRtx; exports.enableDtxForOpus = enableDtxForOpus; +exports.filterLocalCodecs = filterLocalCodecs; exports.getMediaSections = getMediaSections; exports.removeSSRCAttributes = removeSSRCAttributes; exports.revertSimulcast = revertSimulcast; -exports.setBitrateParameters = setBitrateParameters; exports.setCodecPreferences = setCodecPreferences; exports.setSimulcast = setSimulcast; -exports.unifiedPlanFilterLocalCodecs = unifiedPlanFilterLocalCodecs; -exports.unifiedPlanAddOrRewriteNewTrackIds = unifiedPlanAddOrRewriteNewTrackIds; -exports.unifiedPlanAddOrRewriteTrackIds = unifiedPlanAddOrRewriteTrackIds; - diff --git a/lib/util/sdp/issue8329.js b/lib/util/sdp/issue8329.js index 00aa6534a..3bc572870 100644 --- a/lib/util/sdp/issue8329.js +++ b/lib/util/sdp/issue8329.js @@ -1,9 +1,8 @@ 'use strict'; -const RTCSessionDescription = require('@twilio/webrtc').RTCSessionDescription; +const { RTCSessionDescription } = require('@twilio/webrtc'); -const createPtToCodecName = require('./').createPtToCodecName; -const getMediaSections = require('./').getMediaSections; +const { createPtToCodecName, getMediaSections } = require('./'); /** * An RTX payload type diff --git a/lib/util/sdp/simulcast.js b/lib/util/sdp/simulcast.js index c37c02b05..bb5c42adc 100644 --- a/lib/util/sdp/simulcast.js +++ b/lib/util/sdp/simulcast.js @@ -1,7 +1,7 @@ 'use strict'; -const difference = require('../').difference; -const flatMap = require('../').flatMap; +const { difference, flatMap } = require('../'); + /** * Create a random {@link SSRC}. * @returns {SSRC} @@ -171,31 +171,9 @@ function getSSRCRtxPairs(section) { /** * Create SSRC attribute tuples. * @param {string} section - * @param {'planb' | 'unified'} sdpFormat - * @returns {Array<[SSRC, MediaStreamID, Track.ID]>} - */ -function createSSRCAttributeTuples(section, sdpFormat) { - return { - planb: createPlanBSSRCAttributeTuples, - unified: createUnifiedPlanSSRCAttributeTuples - }[sdpFormat](section); -} - -/** - * Create "plan-b" SSRC attribute tuples. - * @param {string} section - * @returns {Array<[SSRC, MediaStreamID, Track.ID]>} - */ -function createPlanBSSRCAttributeTuples(section) { - return getMatches(section, '^a=ssrc:([0-9]+) msid:([^\\s]+) ([^\\s]+)$'); -} - -/** - * Create "unified-plan" SSRC attribute tuples. - * @param {string} section * @returns {Array<[SSRC, MediaStreamID, Track.ID]>} */ -function createUnifiedPlanSSRCAttributeTuples(section) { +function createSSRCAttributeTuples(section) { const [streamId, trackId] = flatMap(getMatches(section, '^a=msid:(.+) (.+)$')); const ssrcs = flatMap(getMatches(section, '^a=ssrc:(.+) cname:.+$')); return ssrcs.map(ssrc => [ssrc, streamId, trackId]); @@ -204,13 +182,12 @@ function createUnifiedPlanSSRCAttributeTuples(section) { /** * Create a Map of MediaStreamTrack IDs and their {@link TrackAttributes}. * @param {string} section - SDP media section - * @param {'planb' | 'unified'} sdpFormat * @returns {Map} */ -function createTrackIdsToAttributes(section, sdpFormat) { +function createTrackIdsToAttributes(section) { const simSSRCs = getSimulcastSSRCs(section); const rtxPairs = getSSRCRtxPairs(section); - const ssrcAttrTuples = createSSRCAttributeTuples(section, sdpFormat); + const ssrcAttrTuples = createSSRCAttributeTuples(section); return ssrcAttrTuples.reduce((trackIdsToSSRCs, tuple) => { const ssrc = tuple[0]; @@ -231,13 +208,12 @@ function createTrackIdsToAttributes(section, sdpFormat) { /** * Apply simulcast settings to the given SDP media section. * @param {string} section - SDP media section - * @param {'planb' | 'unified'} sdpFormat * @param {Map} trackIdsToAttributes - Existing * map which will be updated for new MediaStreamTrack IDs * @returns {string} - The transformed SDP media section */ -function setSimulcastInMediaSection(section, sdpFormat, trackIdsToAttributes) { - const newTrackIdsToAttributes = createTrackIdsToAttributes(section, sdpFormat); +function setSimulcastInMediaSection(section, trackIdsToAttributes) { + const newTrackIdsToAttributes = createTrackIdsToAttributes(section); const newTrackIds = Array.from(newTrackIdsToAttributes.keys()); let trackIds = Array.from(trackIdsToAttributes.keys()); const trackIdsToAdd = difference(newTrackIds, trackIds); diff --git a/lib/util/sdp/trackmatcher/mid.js b/lib/util/sdp/trackmatcher.js similarity index 76% rename from lib/util/sdp/trackmatcher/mid.js rename to lib/util/sdp/trackmatcher.js index 96814489f..cc58d77f1 100644 --- a/lib/util/sdp/trackmatcher/mid.js +++ b/lib/util/sdp/trackmatcher.js @@ -1,14 +1,14 @@ 'use strict'; -const getMediaSections = require('../').getMediaSections; +const { getMediaSections } = require('./'); /** - * An {@link MIDTrackMatcher} matches an RTCTrackEvent with a MediaStreamTrack + * An {@link TrackMatcher} matches an RTCTrackEvent with a MediaStreamTrack * ID based on the MID of the underlying RTCRtpTransceiver. */ -class MIDTrackMatcher { +class TrackMatcher { /** - * Construct an {@link MIDTrackMatcher}. + * Construct an {@link TrackMatcher}. */ constructor() { Object.defineProperties(this, { @@ -29,7 +29,7 @@ class MIDTrackMatcher { } /** - * Update the {@link MIDTrackMatcher} with a new SDP. + * Update the {@link TrackMatcher} with a new SDP. * @param {string} sdp */ update(sdp) { @@ -44,4 +44,4 @@ class MIDTrackMatcher { } } -module.exports = MIDTrackMatcher; +module.exports = TrackMatcher; diff --git a/lib/util/sdp/trackmatcher/identity.js b/lib/util/sdp/trackmatcher/identity.js deleted file mode 100644 index 6be842cf4..000000000 --- a/lib/util/sdp/trackmatcher/identity.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -/** - * An {@link IdentityTrackMatcher} matches RTCTrackEvents with their respective - * MediaStreamTrack IDs. - */ -class IdentityTrackMatcher { - /** - * Match a given MediaStreamTrack with its ID. - * @param {RTCTrackEvent} event - * @returns {Track.ID} - */ - match(event) { - return event.track.id; - } - - /** - * Update the {@link IdentityTrackMatcher} with a new SDP. - * @param {string} sdp - */ - update() {} -} - -module.exports = IdentityTrackMatcher; diff --git a/lib/util/sdp/trackmatcher/ordered.js b/lib/util/sdp/trackmatcher/ordered.js deleted file mode 100644 index 3cc848e46..000000000 --- a/lib/util/sdp/trackmatcher/ordered.js +++ /dev/null @@ -1,120 +0,0 @@ -'use strict'; - -const util = require('../../'); -const getMediaSections = require('../').getMediaSections; - -// NOTE(mroberts): OrderedTrackMatcher is meant to solve the problem identified in -// -// https://bugs.webkit.org/show_bug.cgi?id=174519 -// -// Namely that, without MIDs, we cannot "correctly" identify MediaStreamTracks -// in Safari's current WebRTC implementation. So, this module tries to hack -// around this by making a possibly dangerous assumption: "track" events will -// be raised for MediaStreamTracks of a particular kind in the same order that -// those kinds' MSIDs appear in the SDP. By calling `update` with an -// RTCPeerConnection's `remoteDescription` and then invoking `match`, we ought -// to be able to dequeue MediaStreamTrack IDs in the correct order to be -// assigned to "track" events. - -/** - * @interface MatchedAndUnmatched - * @property {Set} matched - * @property {Set} unmatched - */ - -/** - * Create a new instance of {@link MatchedAndUnmatched}. - * @returns {MatchedAndUnmatched} - */ -function create() { - return { - matched: new Set(), - unmatched: new Set() - }; -} - -/** - * Attempt to match a MediaStreamTrack ID. - * @param {MatchedAndUnmatched} mAndM - * @returns {?Track.ID} id - */ -function match(mAndM) { - const unmatched = Array.from(mAndM.unmatched); - if (!unmatched.length) { - return null; - } - const id = unmatched[0]; - mAndM.matched.add(id); - mAndM.unmatched.delete(id); - return id; -} - -/** - * Update a {@link MatchedAndUnmatched}'s MediaStreamTrack IDs. - * @param {MatchedAndUnmatched} mAndM - * @param {Set} ids - * @returns {void} - */ -function update(mAndM, ids) { - ids = new Set(ids); - const removedMatchedIds = util.difference(mAndM.matched, ids); - removedMatchedIds.forEach(mAndM.matched.delete, mAndM.matched); - mAndM.unmatched = util.difference(ids, mAndM.matched); -} - -/** - * Parse MediaStreamTrack IDs of a particular kind from an SDP. - * @param {string} kind - * @param {string} sdp - * @returns {Set} ids - */ -function parse(kind, sdp) { - const mediaSections = getMediaSections(sdp, kind); - const pattern = 'msid: ?(.+) +(.+) ?$'; - return new Set(util.flatMap(mediaSections, mediaSection => mediaSection.match(new RegExp(pattern, 'mg')) || []).map(msid => msid.match(new RegExp(pattern))[2])); -} - -/** - * A {@link OrderedTrackMatcher} is used to match RTCTrackEvents. - * @property {MatchedAndUnmatched} audio - * @property {MatchedAndUnmatched} video - */ -class OrderedTrackMatcher { - constructor() { - if (!(this instanceof OrderedTrackMatcher)) { - return new OrderedTrackMatcher(); - } - Object.defineProperties(this, { - audio: { - enumerable: true, - value: create() - }, - video: { - enumerable: true, - value: create() - } - }); - } - - /** - * Attempt to match a new MediaStreamTrack ID. - * @param {RTCTrackEvent} event - * @returns {?Track.ID} id - */ - match(event) { - return match(this[event.track.kind]); - } - - /** - * Update the {@link OrderedTrackMatcher} with a new SDP. - * @param {string} sdp - * @returns {void} - */ - update(sdp) { - ['audio', 'video'].forEach(function(kind) { - update(this[kind], parse(kind, sdp)); - }, this); - } -} - -module.exports = OrderedTrackMatcher; diff --git a/package.json b/package.json index 4080d3962..b430459b5 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "twilio-video", "title": "Twilio Video", "description": "Twilio Video JavaScript Library", - "version": "2.19.2-dev", + "version": "2.20.0-dev", "homepage": "https://twilio.com", "author": "Mark Andrus Roberts ", "contributors": [ diff --git a/test/integration/spec/localparticipant/regressions.js b/test/integration/spec/localparticipant/regressions.js index ba0f5b7d4..ffea92b00 100644 --- a/test/integration/spec/localparticipant/regressions.js +++ b/test/integration/spec/localparticipant/regressions.js @@ -1,8 +1,6 @@ 'use strict'; const assert = require('assert'); -const { DEFAULT_CHROME_SDP_SEMANTICS } = require('../../../../es5/util/constants'); -const sdpFormat = require('@twilio/webrtc/lib/util/sdp').getSdpFormat(DEFAULT_CHROME_SDP_SEMANTICS); const { connect, @@ -139,9 +137,6 @@ describe('LocalParticipant: regresions', function() { assert.equal(thatTrack.sid, thisLocalTrackPublication1.trackSid); assert.equal(thatTrack.kind, thisLocalTrackPublication1.kind); assert.equal(thatTrack.enabled, thisLocalTrackPublication1.enabled); - if (isChrome && sdpFormat === 'planb') { - assert.equal(thatTrack.mediaStreamTrack.readyState, 'ended'); - } }); it('should eventually raise a "trackSubscribed" event for the published LocalVideoTrack', () => { diff --git a/test/integration/spec/preflight.js b/test/integration/spec/preflight.js index 7f1bb8467..6d659c17f 100644 --- a/test/integration/spec/preflight.js +++ b/test/integration/spec/preflight.js @@ -8,6 +8,27 @@ const defaults = require('../../lib/defaults'); const { randomName } = require('../../lib/util'); const { isFirefox, isSafari } = require('../../lib/guessbrowser'); +const expectedProgress = [ + 'mediaAcquired', + 'connected', + 'mediaSubscribed', + 'iceConnected', + 'mediaStarted' +]; +if (!isFirefox) { + expectedProgress.push('peerConnectionConnected'); +} +if (!isSafari) { + expectedProgress.push('dtlsConnected'); +} + +function assertProgressEvents(progressEvents) { + progressEvents.forEach(({ duration, name }) => { + assert.strictEqual(typeof duration, 'number'); + assert(expectedProgress.includes(name)); + }); +} + function assertTimeMeasurement(measurement) { assert.equal(typeof measurement.duration, 'number'); } @@ -36,6 +57,7 @@ function validateReport(report) { assertIceCandidate(report.selectedIceCandidatePairStats.localCandidate); assertIceCandidate(report.selectedIceCandidatePairStats.remoteCandidate); assert(report.iceCandidateStats.length > 0); + assertProgressEvents(report.progressEvents); report.iceCandidateStats.forEach(iceCandidate => assertIceCandidate(iceCandidate)); } @@ -60,19 +82,6 @@ describe('preflight', function() { preflight.on('completed', report => { validateReport(report); - const expectedProgress = [ - 'mediaAcquired', - 'connected', - 'mediaSubscribed', - 'iceConnected', - 'mediaStarted' - ]; - if (!isFirefox) { - expectedProgress.push('peerConnectionConnected'); - } - if (!isSafari) { - expectedProgress.push('dtlsConnected'); - } assert.deepStrictEqual(expectedProgress.sort(), progressReceived.sort()); deferred.resolve(); @@ -99,6 +108,9 @@ describe('preflight', function() { }); it('fails when bad token is supplied', async () => { + let errorResult; + let reportResult; + const preflight = runPreflight('badToken'); const deferred = {}; deferred.promise = new Promise((resolve, reject) => { @@ -110,13 +122,18 @@ describe('preflight', function() { deferred.reject('preflight completed unexpectedly'); }); - preflight.on('failed', error => { + preflight.on('failed', (error, report) => { // eslint-disable-next-line no-console console.log('preflight failed as expected:', error); + errorResult = error; + reportResult = report; deferred.resolve(); }); await deferred.promise; + + assert.strictEqual(errorResult.toString(), 'TwilioError 20101: Invalid Access Token'); + assert.strictEqual(typeof reportResult, 'object'); }); }); diff --git a/test/integration/spec/util/simulcast.js b/test/integration/spec/util/simulcast.js index f1f98b350..4dfa7ff43 100644 --- a/test/integration/spec/util/simulcast.js +++ b/test/integration/spec/util/simulcast.js @@ -2,13 +2,10 @@ const assert = require('assert'); const { guessBrowser } = require('@twilio/webrtc/lib/util'); -const { getSdpFormat } = require('@twilio/webrtc/lib/util/sdp'); const { getMediaSections, setSimulcast } = require('../../../../es5/util/sdp'); const { RTCPeerConnection, RTCSessionDescription } = require('@twilio/webrtc'); -const { DEFAULT_CHROME_SDP_SEMANTICS } = require('../../../../es5/util/constants'); const isChrome = guessBrowser() === 'chrome'; -const sdpFormat = getSdpFormat(DEFAULT_CHROME_SDP_SEMANTICS); describe('setSimulcast', () => { let answer1; @@ -41,7 +38,7 @@ describe('setSimulcast', () => { offer1 = createSdp === 'createOffer' ? new RTCSessionDescription({ type: offer.type, - sdp: setSimulcast(offer.sdp, sdpFormat, trackIdsToAttributes) + sdp: setSimulcast(offer.sdp, trackIdsToAttributes) }) : offer; await pc1.setLocalDescription(offer1); @@ -50,7 +47,7 @@ describe('setSimulcast', () => { answer1 = createSdp === 'createAnswer' ? new RTCSessionDescription({ type: answer.type, - sdp: setSimulcast(answer.sdp, sdpFormat, trackIdsToAttributes) + sdp: setSimulcast(answer.sdp, trackIdsToAttributes) }) : answer; await pc2.setLocalDescription(answer1); @@ -59,7 +56,7 @@ describe('setSimulcast', () => { offer2 = createSdp === 'createOffer' ? new RTCSessionDescription({ type: _offer.type, - sdp: setSimulcast(_offer.sdp, sdpFormat, trackIdsToAttributes) + sdp: setSimulcast(_offer.sdp, trackIdsToAttributes) }) : _offer; await pc2.setRemoteDescription(offer2); @@ -100,7 +97,7 @@ describe('setSimulcast', () => { offer1 = new RTCSessionDescription({ type: offer.type, - sdp: setSimulcast(offer.sdp, sdpFormat, trackIdsToAttributes) + sdp: setSimulcast(offer.sdp, trackIdsToAttributes) }); await pc1.setLocalDescription(offer1); @@ -109,7 +106,7 @@ describe('setSimulcast', () => { offer2 = new RTCSessionDescription({ type: _offer.type, - sdp: setSimulcast(_offer.sdp, sdpFormat, trackIdsToAttributes) + sdp: setSimulcast(_offer.sdp, trackIdsToAttributes) }); }); diff --git a/test/lib/mocksdp.js b/test/lib/mocksdp.js index 7a06f1a3a..894cc4648 100644 --- a/test/lib/mocksdp.js +++ b/test/lib/mocksdp.js @@ -18,14 +18,13 @@ /** * Create an SDP string. - * @param {'planb' | 'unified'} type * @param {TracksByKind} kinds * @param {number} [maxAudioBitrate] * @param {number} [maxVideoBitrate] * @param {boolean} [withAppData = true] * @returns {string} sdp */ -function makeSdpWithTracks(type, kinds, maxAudioBitrate, maxVideoBitrate, withAppData = true) { +function makeSdpWithTracks(kinds, maxAudioBitrate, maxVideoBitrate, withAppData = true) { const session = `\ v=0\r o=- 0 1 IN IP4 0.0.0.0\r @@ -42,9 +41,9 @@ a=fingerprint:sha-256 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:0 }[kind]; const bLine = kind === 'audio' && typeof maxAudioBitrate === 'number' - ? `b=${type === 'planb' ? 'AS' : 'TIAS'}:${maxAudioBitrate}\r\n` + ? `b=TIAS:${maxAudioBitrate}\r\n` : kind === 'video' && typeof maxVideoBitrate === 'number' - ? `b=${type === 'planb' ? 'AS' : 'TIAS'}:${maxVideoBitrate}\r\n` + ? `b=TIAS:${maxVideoBitrate}\r\n` : ''; const media = `\ @@ -69,31 +68,29 @@ a=rtcp-mux\r const { id, ssrc } = typeof trackAndSSRC === 'string' ? { id: trackAndSSRC, ssrc: 1 } : trackAndSSRC; - return sdp + (type === 'planb' ? '' : media + `\ -a=msid:-${type === 'planb' || withAppData ? ` ${id}` : ''}\r -`) + `\ + return sdp + media + `\ +a=msid:-${withAppData ? ` ${id}` : ''}\r a=ssrc:${ssrc} cname:0\r -a=ssrc:${ssrc} msid:${type === 'planb' ? 'stream' : '-'}${type === 'planb' || withAppData ? ` ${id}` : ''}\r +a=ssrc:${ssrc} msid:-${withAppData ? ` ${id}` : ''}\r a=mid:mid_${id}\r `; - }, sdp + (type === 'planb' ? media : '')); + }, sdp); }, session); } /** * Make an sdp to test simulcast. - * @param {'planb' | 'unified'} type * @param {Array} ssrcs * @returns {string} sdp */ -function makeSdpForSimulcast(type, ssrcs) { - const sdp = makeSdpWithTracks(type, { +function makeSdpForSimulcast(ssrcs) { + const sdp = makeSdpWithTracks({ audio: ['audio-1'], video: [{ id: 'video-1', ssrc: ssrcs[0] }] }); const ssrcSdpLines = ssrcs.length === 2 ? [ `a=ssrc:${ssrcs[1]} cname:0`, - `a=ssrc:${ssrcs[1]} msid:${type === 'planb' ? 'stream' : '-'} video-1` + `a=ssrc:${ssrcs[1]} msid:- video-1` ] : []; const fidSdpLines = ssrcs.length === 2 ? [`a=ssrc-group:FID ${ssrcs.join(' ')}`] diff --git a/test/unit/index.js b/test/unit/index.js index 5b2ecce8e..0de8be54d 100644 --- a/test/unit/index.js +++ b/test/unit/index.js @@ -76,8 +76,7 @@ require('./spec/util/networkmonitor'); require('./spec/util/sdp'); require('./spec/util/sdp/issue8329'); require('./spec/util/support'); -require('./spec/util/trackmatcher/mid'); -require('./spec/util/trackmatcher/ordered'); +require('./spec/util/trackmatcher.js'); require('./spec/util/twilioerror'); require('./spec/webaudio/audiocontext'); diff --git a/test/unit/spec/signaling/v2/peerconnection.js b/test/unit/spec/signaling/v2/peerconnection.js index d4325cb0f..838c65f2d 100644 --- a/test/unit/spec/signaling/v2/peerconnection.js +++ b/test/unit/spec/signaling/v2/peerconnection.js @@ -480,11 +480,12 @@ describe('PeerConnectionV2', () => { context(scenario, () => { beforeEach(() => { test = makeTest(); - const tracks = [{ id: 1 }]; + const track = { id: 1 }; + const tracks = [track]; stream = { getTracks() { return tracks; } }; - const trackSender = makeMediaTrackSender(tracks[0]); + const trackSender = makeMediaTrackSender(track); setup(test, trackSender); - test.pc.addTrack = sinon.spy(() => {}); + test.pc.addTransceiver = sinon.spy(() => ({ sender: { track } })); result = test.pcv2.addMediaTrackSender(trackSender); }); @@ -493,24 +494,19 @@ describe('PeerConnectionV2', () => { }); if (scenario === 'been added') { - it('does not call addTrack on the underlying RTCPeerConnection', () => { - sinon.assert.notCalled(test.pc.addTrack); + it('does not call addTransceiver on the underlying RTCPeerConnection', () => { + sinon.assert.notCalled(test.pc.addTransceiver); }); return; } - it('calls addTrack on the underlying RTCPeerConnection', () => { - sinon.assert.calledWith(test.pc.addTrack, stream.getTracks()[0]); + it('calls addTransceiver on the underlying RTCPeerConnection', () => { + sinon.assert.calledWith(test.pc.addTransceiver, stream.getTracks()[0]); }); }); }); }); - function makePublisherHints(layerIndex, enabled) { - // eslint-disable-next-line camelcase - return [{ enabled, layer_index: layerIndex }]; - } - describe('_setPublisherHint', () => { let test; combinationContext([ @@ -739,6 +735,7 @@ describe('PeerConnectionV2', () => { describe('#getTrackReceivers', () => { it('returns DataTrackReceivers and MediaTrackReceivers for any RTCDataChannels and MediaStreamTracks raised by the underlying RTCPeerConnection that have yet to be closed/ended', () => { const test = makeTest(); + const trackMatcher = { match: sinon.stub(), update: sinon.stub() }; const dataChannel1 = makeDataChannel(); const dataChannel2 = makeDataChannel(); const dataChannel3 = makeDataChannel(); @@ -749,6 +746,8 @@ describe('PeerConnectionV2', () => { return id || label; } + test.pcv2._trackMatcher = trackMatcher; + test.pc.dispatchEvent({ type: 'datachannel', channel: dataChannel1 }); test.pc.dispatchEvent({ type: 'datachannel', channel: dataChannel2 }); test.pc.dispatchEvent({ type: 'datachannel', channel: dataChannel3 }); @@ -763,6 +762,10 @@ describe('PeerConnectionV2', () => { assert.deepEqual(test.pcv2.getTrackReceivers().map(receiver => receiver.id), [dataChannel2, dataChannel3, mediaTrack2].map(getTrackIdOrChannelLabel)); + + sinon.assert.calledWith(trackMatcher.match, { type: 'track', track: mediaTrack1 }); + sinon.assert.calledWith(trackMatcher.match, { type: 'track', track: mediaTrack2 }); + sinon.assert.calledWith(trackMatcher.update, null); }); }); @@ -816,6 +819,16 @@ describe('PeerConnectionV2', () => { }); }); + it('returns the local description if reoffer is triggered', async () => { + const offer = makeOffer({ noMedia: true }); + const answer = makeAnswer({ noMedia: true }); + const test = makeTest({ offers: [offer, offer], answers: [answer] }); + await test.pcv2.offer(); + // Triggers Reoffer when the sdp has fewer audio and/or video send* m= lines + await test.pcv2.update(test.state().setDescription(answer, 1)); + assert.deepEqual(test.pcv2.getState(), test.state().setDescription(test.offers[1], 2)); + }); + it('returns last stable answer version when while new offer is being processed', async () => { // Apply remote offer for revision 1 const test = makeTest({ offers: 3, answers: 3 }); @@ -1203,21 +1216,16 @@ describe('PeerConnectionV2', () => { [true, false], x => `when the remote peer is ${x ? '' : 'not '}an ICE-lite agent` ], - [ - [true, false], - x => `When RTCRtpSenderParameters is ${x ? '' : 'not '}supported by WebRTC` - ], [ [true, false, undefined], x => `When enableDscp is ${typeof x === 'undefined' ? 'not specified' : `set to ${x}`}` ], [ [true, false], - // limit only to isRTCRtpSenderParamsSupported x => `When chromeScreenTrack is ${x ? 'present' : 'not present'}` ], // eslint-disable-next-line consistent-return - ], ([initial, vmsFailOver, signalingState, type, newerEqualOrOlder, matching, iceLite, isRTCRtpSenderParamsSupported, enableDscp, chromeScreenTrack]) => { + ], ([initial, vmsFailOver, signalingState, type, newerEqualOrOlder, matching, iceLite, enableDscp, chromeScreenTrack]) => { // these combinations grow exponentially // skip the ones that do not make much sense to test. if (vmsFailOver && (!initial || type !== 'offer' || signalingState !== 'have-local-offer')) { @@ -1225,12 +1233,7 @@ describe('PeerConnectionV2', () => { return; } - if (!isRTCRtpSenderParamsSupported && (chromeScreenTrack || enableDscp)) { - // screen share track and dscp options have special cases only when isRTCRtpSenderParamsSupported. - return; - } - - if (iceLite && (isRTCRtpSenderParamsSupported || enableDscp || chromeScreenTrack)) { + if (iceLite && (enableDscp || chromeScreenTrack)) { // iceLite does not need repeat for all combination of unrelated variables. return; } @@ -1275,9 +1278,9 @@ describe('PeerConnectionV2', () => { maxVideoBitrate: 50, enableDscp, isChromeScreenShareTrack: () => chromeScreenTrack, - isRTCRtpSenderParamsSupported, tracks, }); + descriptions = []; const ufrag = 'foo'; @@ -1433,26 +1436,19 @@ describe('PeerConnectionV2', () => { function itShouldApplyBandwidthConstraints() { it('should apply the specified bandwidth constraints for AudioTracks and non-screen VideoTracks (Chrome only)', () => { - if (isRTCRtpSenderParamsSupported) { - test.pc.getSenders().forEach(sender => { - const expectedMaxBitRate = sender.track.kind === 'audio' ? test.maxAudioBitrate : test.maxVideoBitrate; - if (sender.track.kind === 'video' && chromeScreenTrack) { - sinon.assert.neverCalledWith(sender.setParameters, sinon.match.hasNested('encodings[0].maxBitrate', expectedMaxBitRate)); - } else { - sinon.assert.calledWith(sender.setParameters, sinon.match.hasNested('encodings[0].maxBitrate', expectedMaxBitRate)); - } - }); - return; - } - const maxVideoBitrate = test.setBitrateParameters.args[0].pop(); - const maxAudioBitrate = test.setBitrateParameters.args[0].pop(); - assert.equal(maxAudioBitrate, test.maxAudioBitrate); - assert.equal(maxVideoBitrate, test.maxVideoBitrate); + test.pc.getSenders().forEach(sender => { + const expectedMaxBitRate = sender.track.kind === 'audio' ? test.maxAudioBitrate : test.maxVideoBitrate; + if (sender.track.kind === 'video' && chromeScreenTrack) { + sinon.assert.neverCalledWith(sender.setParameters, sinon.match.hasNested('encodings[0].maxBitrate', expectedMaxBitRate)); + } else { + sinon.assert.calledWith(sender.setParameters, sinon.match.hasNested('encodings[0].maxBitrate', expectedMaxBitRate)); + } + }); }); } function itShouldMaybeSetNetworkPriority() { - if (enableDscp && isRTCRtpSenderParamsSupported) { + if (enableDscp) { it('should set RTCRtpEncodingParameters.networkPriority to "high" all RTCRtpSenders', () => { test.pc.getSenders().forEach(sender => { sinon.assert.calledWith(sender.setParameters, sinon.match.hasNested('encodings[0].networkPriority', 'high')); @@ -1468,17 +1464,15 @@ describe('PeerConnectionV2', () => { } function itShouldNotSetResolutionScale() { - if (isRTCRtpSenderParamsSupported) { - it('should not set RTCRtpEncodingParameters.scaleResolutionDownBy for any video senders', () => { - test.pc.getSenders().forEach(sender => { - if (sender.track.kind === 'video') { - sinon.assert.calledWith(sender.setParameters, sinon.match(({ encodings }) => { - return !encodings.find(encoding => typeof encoding.scaleResolutionDownBy !== 'undefined'); - })); - } - }); + it('should not set RTCRtpEncodingParameters.scaleResolutionDownBy for any video senders', () => { + test.pc.getSenders().forEach(sender => { + if (sender.track.kind === 'video') { + sinon.assert.calledWith(sender.setParameters, sinon.match(({ encodings }) => { + return !encodings.find(encoding => typeof encoding.scaleResolutionDownBy !== 'undefined'); + })); + } }); - } + }); } // NOTE(mroberts): This test should really be extended. Instead of popping @@ -2095,6 +2089,8 @@ describe('PeerConnectionV2', () => { const mediaStream = { id: 'abc' }; const trackPromise = new Promise(resolve => test.pcv2.once('trackAdded', resolve)); + const trackMatcher = { match: sinon.stub(), update: sinon.stub() }; + test.pcv2._trackMatcher = trackMatcher; pc.emit('track', { type: 'track', @@ -2129,10 +2125,18 @@ describe('PeerConnectionV2', () => { test = makeTest({ offers: 3, answers: 3 }); }); - it('should emit a "description" event with a new offer', async () => { + it('should call setParameters with the correct values', async () => { test.encodingParameters.update({ maxAudioBitrate: 20, maxVideoBitrate: 30 }); - const { description } = await new Promise(resolve => test.pcv2.once('description', resolve)); - assert.deepEqual({ type: description.type, sdp: description.sdp }, test.pc.localDescription); + + // Wait for the internal promise to resolve + await new Promise(resolve => setTimeout(resolve, 1)); + + const senders = test.pc.getSenders(); + const audioSender = senders.filter(s => s.track.kind === 'audio')[0]; + const videoSender = senders.filter(s => s.track.kind === 'video')[0]; + + assert.deepStrictEqual(audioSender.setParameters.args[0][0], { encodings: [{ maxBitrate: 20 }] }); + assert.deepStrictEqual(videoSender.setParameters.args[0][0], { encodings: [{ maxBitrate: 30 }] }); }); }); @@ -2385,6 +2389,7 @@ describe('PeerConnectionV2', () => { }); }); }); + }); /** @@ -2425,8 +2430,9 @@ class MockPeerConnection extends EventEmitter { constructor(offers, answers, errorScenario) { super(); - this.senders = []; this.receivers = []; + this.senders = []; + this.transceivers = []; this.offerIndex = 0; this.answerIndex = 0; @@ -2541,6 +2547,15 @@ class MockPeerConnection extends EventEmitter { } } + addTransceiver(track) { + const sender = this.addTrack(track); + const transceiver = { + sender + }; + this.transceivers.push(transceiver); + return transceiver; + } + getSenders() { return this.senders; } @@ -2549,6 +2564,10 @@ class MockPeerConnection extends EventEmitter { return this.receivers; } + getTransceivers() { + return this.transceivers; + } + addIceCandidate() { return Promise.resolve(); } @@ -2619,7 +2638,6 @@ function makePeerConnectionV2(options) { options.RTCPeerConnection = options.RTCPeerConnection || RTCPeerConnection; options.isChromeScreenShareTrack = options.isChromeScreenShareTrack || sinon.spy(() => false); options.sessionTimeout = options.sessionTimeout || 100; - options.setBitrateParameters = options.setBitrateParameters || sinon.spy(sdp => sdp); options.setCodecPreferences = options.setCodecPreferences || sinon.spy(sdp => sdp); options.preferredCodecs = options.preferredCodecs || { audio: [], video: [] }; options.options = { @@ -2630,9 +2648,7 @@ function makePeerConnectionV2(options) { RTCSessionDescription: identity, isChromeScreenShareTrack: options.isChromeScreenShareTrack, eventObserver: options.eventObserver || { emit: sinon.spy() }, - isRTCRtpSenderParamsSupported: options.isRTCRtpSenderParamsSupported, sessionTimeout: options.sessionTimeout, - setBitrateParameters: options.setBitrateParameters, setCodecPreferences: options.setCodecPreferences }; @@ -2744,6 +2760,10 @@ function makeDescription(type, options) { type === 'pranswer') { const session = 'session' in options ? options.session : Number.parseInt(Math.random() * 1000); description.sdp = 'o=- ' + session + '\r\n'; + if (!options.noMedia) { + description.sdp += 'm=video 9 UDP/TLS/RTP/SAVPF 99 22\r\na=sendrecv\r\n'; + description.sdp += 'm=audio 9 UDP/TLS/RTP/SAVPF 111 9 0 8 126\r\na=sendrecv\r\n'; + } if (options.iceLite) { description.sdp += 'a=ice-lite\r\n'; } @@ -2830,8 +2850,8 @@ function makeDataTrackSender(id) { } function makeMediaTrackSender(track) { - const id = track.id || makeId(); - const kind = track.kind || makeMediaKind(); + const id = track.id = track.id || makeId(); + const kind = track.kind = track.kind || makeMediaKind(); return { id, kind, @@ -2850,6 +2870,10 @@ function makeDataChannel(id) { return dataChannel; } +function makePublisherHints(layerIndex, enabled) { + // eslint-disable-next-line camelcase + return [{ enabled, layer_index: layerIndex }]; +} /** * @interface MockPeerConnectionOptions diff --git a/test/unit/spec/util/sdp/index.js b/test/unit/spec/util/sdp/index.js index 513c3890c..a22933017 100644 --- a/test/unit/spec/util/sdp/index.js +++ b/test/unit/spec/util/sdp/index.js @@ -5,128 +5,23 @@ const assert = require('assert'); const { flatMap } = require('../../../../../lib/util'); const { + addOrRewriteNewTrackIds, + addOrRewriteTrackIds, disableRtx, enableDtxForOpus, + filterLocalCodecs, getMediaSections, - setBitrateParameters, setCodecPreferences, setSimulcast, - unifiedPlanFilterLocalCodecs, removeSSRCAttributes, - revertSimulcast, - unifiedPlanAddOrRewriteNewTrackIds, - unifiedPlanAddOrRewriteTrackIds + revertSimulcast } = require('../../../../../lib/util/sdp'); const { makeSdpForSimulcast, makeSdpWithTracks } = require('../../../../lib/mocksdp'); const { combinationContext } = require('../../../../lib/util'); -describe('setBitrateParameters', () => { - context('when there is no existing b= line in the SDP', () => { - combinationContext([ - [ - ['AS', 'TIAS'], - x => `when the modifier is ${x}` - ], - [ - [5000, null], - x => `when maxAudioBitrate is ${x ? 'not ' : ''}null` - ], - [ - [8000, null], - x => `when maxVideoBitrate is ${x ? 'not ' : ''}null` - ] - ], ([modifier, maxAudioBitrate, maxVideoBitrate]) => { - let sdp; - - beforeEach(() => { - sdp = makeSdpWithTracks(modifier === 'TIAS' ? 'unified' : 'planb', { - audio: ['audio-1', 'audio-2'], - video: ['video-1', 'video-2'] - }); - sdp = setBitrateParameters(sdp, modifier, maxAudioBitrate, maxVideoBitrate); - }); - - ['audio', 'video'].forEach(kind => { - const maxBitrate = kind === 'audio' - ? modifier === 'TIAS' - ? maxAudioBitrate - : maxAudioBitrate && Math.round((maxAudioBitrate + 16000) / 950) - : modifier === 'TIAS' - ? maxVideoBitrate - : maxVideoBitrate && Math.round((maxVideoBitrate + 16000) / 950); - - const bLine = new RegExp(`\r\nb=${modifier}:${maxBitrate || '([0-9])+'}`); - - it(`should ${maxBitrate ? '' : 'not '}add a b= line to the given m=${kind} line`, () => { - sdp.split('\r\nm=').slice(1).filter(section => section.split(' ')[0] === kind).forEach(section => { - assert(typeof maxBitrate === 'number' ? bLine.test(section) : !bLine.test(section)); - }); - }); - }); - }); - }); - - context('when there is an existing b= line in the SDP', () => { - combinationContext([ - [ - ['AS', 'TIAS'], - x => `when the modifier is ${x}` - ], - [ - [5000, 7000, null], - x => `when maxAudioBitrate is ${!x ? 'null' : (x === 5000 ? 'less' : 'greater') + ' than the current value'}` - ], - [ - [8000, 10000, null], - x => `when maxVideoBitrate is ${!x ? 'null' : (x === 8000 ? 'less' : 'greater') + ' than the current value'}` - ] - ], ([modifier, maxAudioBitrate, maxVideoBitrate]) => { - const currentMaxAudioBitrate = 6000; - const currentMaxVideoBitrate = 9000; - - let sdp; - - beforeEach(() => { - sdp = makeSdpWithTracks(modifier === 'TIAS' ? 'unified' : 'planb', { - audio: ['audio-1', 'audio-2'], - video: ['video-1', 'video-2'] - }, currentMaxAudioBitrate, currentMaxVideoBitrate); - sdp = setBitrateParameters(sdp, modifier, maxAudioBitrate, maxVideoBitrate); - }); - - ['audio', 'video'].forEach(kind => { - function getMaxBitrate(maxAudioBitrate, maxVideoBitrate) { - return kind === 'audio' - ? modifier === 'TIAS' - ? maxAudioBitrate - : maxAudioBitrate && Math.round((maxAudioBitrate + 16000) / 950) - : modifier === 'TIAS' - ? maxVideoBitrate - : maxVideoBitrate && Math.round((maxVideoBitrate + 16000) / 950); - } - - const currentMaxBitrate = getMaxBitrate(currentMaxAudioBitrate, currentMaxVideoBitrate); - const maxBitrate = getMaxBitrate(maxAudioBitrate, maxVideoBitrate); - const shouldUpdateBLine = maxBitrate && maxBitrate <= currentMaxBitrate; - const bLine = new RegExp(`\r\nb=${modifier}:${shouldUpdateBLine ? maxBitrate : currentMaxBitrate}`); - - it(`should ${shouldUpdateBLine ? '' : 'not '}update the b= line of the given m=${kind} line to the new value`, () => { - sdp.split('\r\nm=').slice(1).filter(section => section.split(' ')[0] === kind).forEach(section => { - assert(bLine.test(section)); - }); - }); - }); - }); - }); -}); - describe('setCodecPreferences', () => { combinationContext([ - [ - ['planb', 'unified'], - x => `when called with a ${x} sdp` - ], [ ['', 'PCMA,G722'], x => `when preferredAudioCodecs is ${x ? 'not ' : ''}empty` @@ -135,7 +30,7 @@ describe('setCodecPreferences', () => { ['', 'H264,VP9'], x => `when preferredVideoCodecs is ${x ? 'not ' : ''}empty` ] - ], ([sdpType, preferredAudioCodecs, preferredVideoCodecs]) => { + ], ([preferredAudioCodecs, preferredVideoCodecs]) => { preferredAudioCodecs = preferredAudioCodecs ? preferredAudioCodecs.split(',').map(codec => ({ codec })) : []; preferredVideoCodecs = preferredVideoCodecs ? preferredVideoCodecs.split(',').map(codec => ({ codec })) : []; context(`should ${preferredAudioCodecs.length ? 'update the' : 'preserve the existing'} audio codec order`, () => { @@ -146,7 +41,7 @@ describe('setCodecPreferences', () => { const expectedVideoCodecIds = preferredVideoCodecs.length ? ['126', '97', '121', '120', '99'] : ['120', '121', '126', '97', '99']; - itShouldHaveCodecOrder(sdpType, preferredAudioCodecs, preferredVideoCodecs, expectedAudioCodecIds, expectedVideoCodecIds); + itShouldHaveCodecOrder(preferredAudioCodecs, preferredVideoCodecs, expectedAudioCodecIds, expectedVideoCodecIds); }); }); }); @@ -154,10 +49,6 @@ describe('setCodecPreferences', () => { describe('setSimulcast', () => { combinationContext([ - [ - ['planb', 'unified'], - x => `when called with a ${x} sdp` - ], [ [true, false], x => `when the SDP ${x ? 'already has' : 'does not already have'} simulcast SSRCs` @@ -170,14 +61,14 @@ describe('setSimulcast', () => { [new Set(['01234']), new Set(['01234', '56789'])], x => `when retransmission is${x.size === 2 ? '' : ' not'} supported` ] - ], ([sdpType, areSimSSRCsAlreadyPresent, isVP8PayloadTypePresent, ssrcs]) => { + ], ([areSimSSRCsAlreadyPresent, isVP8PayloadTypePresent, ssrcs]) => { let sdp; let simSdp; let trackIdsToAttributes; before(() => { ssrcs = Array.from(ssrcs.values()); - sdp = makeSdpForSimulcast(sdpType, ssrcs); + sdp = makeSdpForSimulcast(ssrcs); trackIdsToAttributes = new Map(); if (!isVP8PayloadTypePresent) { @@ -185,9 +76,9 @@ describe('setSimulcast', () => { 'm=video 9 UDP/TLS/RTP/SAVPF 121 126 97'); } if (areSimSSRCsAlreadyPresent) { - sdp = setSimulcast(sdp, sdpType, trackIdsToAttributes); + sdp = setSimulcast(sdp, trackIdsToAttributes); } - simSdp = setSimulcast(sdp, sdpType, trackIdsToAttributes); + simSdp = setSimulcast(sdp, trackIdsToAttributes); }); if (!isVP8PayloadTypePresent || areSimSSRCsAlreadyPresent) { @@ -225,7 +116,7 @@ describe('setSimulcast', () => { context('when the SDP contains a previously added MediaStreamTrack ID', () => { before(() => { - simSdp = setSimulcast(sdp, sdpType, trackIdsToAttributes); + simSdp = setSimulcast(sdp, trackIdsToAttributes); }); it('should not generate new simulcast SSRCs and re-use the existing simulcast SSRCs', () => { @@ -250,253 +141,9 @@ describe('setSimulcast', () => { }); }); }); - - describe('when a subsequent offer disables RTX', () => { - it('does not add RTX SSRCs, FID groups, etc.', () => { - const sdp1 = `\ -v=0\r -o=- 6385359508499371184 3 IN IP4 127.0.0.1\r -s=-\r -t=0 0\r -a=group:BUNDLE audio video\r -a=msid-semantic: WMS 7a9d401b-3cf6-4216-b260-78f93ba4c32e\r -m=audio 22602 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126\r -c=IN IP4 34.203.250.135\r -a=rtcp:9 IN IP4 0.0.0.0\r -a=candidate:2235265311 1 udp 7935 34.203.250.135 22602 typ relay raddr 107.20.226.156 rport 51463 generation 0 network-cost 50\r -a=ice-ufrag:Cmuk\r -a=ice-pwd:qjHlb5sxe0bozbwpRSYqil3v\r -a=ice-options:trickle\r -a=fingerprint:sha-256 BE:29:0C:60:05:B6:6E:E6:EA:A8:28:D5:89:41:F9:5B:22:11:CD:26:01:98:E0:55:9D:FE:C2:F8:EA:4C:17:91\r -a=setup:actpass\r -a=mid:audio\r -a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r -a=recvonly\r -a=rtcp-mux\r -a=rtpmap:111 opus/48000/2\r -a=rtcp-fb:111 transport-cc\r -a=fmtp:111 minptime=10;useinbandfec=1\r -a=rtpmap:103 ISAC/16000\r -a=rtpmap:104 ISAC/32000\r -a=rtpmap:9 G722/8000\r -a=rtpmap:0 PCMU/8000\r -a=rtpmap:8 PCMA/8000\r -a=rtpmap:106 CN/32000\r -a=rtpmap:105 CN/16000\r -a=rtpmap:13 CN/8000\r -a=rtpmap:110 telephone-event/48000\r -a=rtpmap:112 telephone-event/32000\r -a=rtpmap:113 telephone-event/16000\r -a=rtpmap:126 telephone-event/8000\r -m=video 9 UDP/TLS/RTP/SAVPF 96 97 99 101 123 122 107 109 98 100 102 127 125 108 124\r -c=IN IP4 0.0.0.0\r -a=rtcp:9 IN IP4 0.0.0.0\r -a=ice-ufrag:Cmuk\r -a=ice-pwd:qjHlb5sxe0bozbwpRSYqil3v\r -a=ice-options:trickle\r -a=fingerprint:sha-256 BE:29:0C:60:05:B6:6E:E6:EA:A8:28:D5:89:41:F9:5B:22:11:CD:26:01:98:E0:55:9D:FE:C2:F8:EA:4C:17:91\r -a=setup:actpass\r -a=mid:video\r -a=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r -a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r -a=extmap:4 urn:3gpp:video-orientation\r -a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r -a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r -a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\r -a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/video-timing\r -a=sendrecv\r -a=rtcp-mux\r -a=rtcp-rsize\r -a=rtpmap:96 VP8/90000\r -a=rtcp-fb:96 goog-remb\r -a=rtcp-fb:96 transport-cc\r -a=rtcp-fb:96 ccm fir\r -a=rtcp-fb:96 nack\r -a=rtcp-fb:96 nack pli\r -a=rtpmap:97 rtx/90000\r -a=fmtp:97 apt=96\r -a=rtpmap:99 rtx/90000\r -a=fmtp:99 apt=98\r -a=rtpmap:101 rtx/90000\r -a=fmtp:101 apt=100\r -a=rtpmap:123 rtx/90000\r -a=fmtp:123 apt=102\r -a=rtpmap:122 rtx/90000\r -a=fmtp:122 apt=127\r -a=rtpmap:107 rtx/90000\r -a=fmtp:107 apt=125\r -a=rtpmap:109 rtx/90000\r -a=fmtp:109 apt=108\r -a=rtpmap:98 VP9/90000\r -a=rtcp-fb:98 goog-remb\r -a=rtcp-fb:98 transport-cc\r -a=rtcp-fb:98 ccm fir\r -a=rtcp-fb:98 nack\r -a=rtcp-fb:98 nack pli\r -a=rtpmap:100 H264/90000\r -a=rtcp-fb:100 goog-remb\r -a=rtcp-fb:100 transport-cc\r -a=rtcp-fb:100 ccm fir\r -a=rtcp-fb:100 nack\r -a=rtcp-fb:100 nack pli\r -a=fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\r -a=rtpmap:102 H264/90000\r -a=rtcp-fb:102 goog-remb\r -a=rtcp-fb:102 transport-cc\r -a=rtcp-fb:102 ccm fir\r -a=rtcp-fb:102 nack\r -a=rtcp-fb:102 nack pli\r -a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r -a=rtpmap:127 H264/90000\r -a=rtcp-fb:127 goog-remb\r -a=rtcp-fb:127 transport-cc\r -a=rtcp-fb:127 ccm fir\r -a=rtcp-fb:127 nack\r -a=rtcp-fb:127 nack pli\r -a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d0032\r -a=rtpmap:125 H264/90000\r -a=rtcp-fb:125 goog-remb\r -a=rtcp-fb:125 transport-cc\r -a=rtcp-fb:125 ccm fir\r -a=rtcp-fb:125 nack\r -a=rtcp-fb:125 nack pli\r -a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032\r -a=rtpmap:108 red/90000\r -a=rtpmap:124 ulpfec/90000\r -a=ssrc-group:FID 0000000000 1111111111\r -a=ssrc:0000000000 cname:s9hDwDQNjISOxWtK\r -a=ssrc:0000000000 msid:7a9d401b-3cf6-4216-b260-78f93ba4c32e d8b9a935-da54-4d21-a8de-522c87258244\r -a=ssrc:0000000000 mslabel:7a9d401b-3cf6-4216-b260-78f93ba4c32e\r -a=ssrc:0000000000 label:d8b9a935-da54-4d21-a8de-522c87258244\r -a=ssrc:1111111111 cname:s9hDwDQNjISOxWtK\r -a=ssrc:1111111111 msid:7a9d401b-3cf6-4216-b260-78f93ba4c32e d8b9a935-da54-4d21-a8de-522c87258244\r -a=ssrc:1111111111 mslabel:7a9d401b-3cf6-4216-b260-78f93ba4c32e\r -a=ssrc:1111111111 label:d8b9a935-da54-4d21-a8de-522c87258244\r -`; - - const sdp2 = `\ -v=0\r -o=- 6385359508499371184 3 IN IP4 127.0.0.1\r -s=-\r -t=0 0\r -a=group:BUNDLE audio video\r -a=msid-semantic: WMS 7a9d401b-3cf6-4216-b260-78f93ba4c32e\r -m=audio 22602 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126\r -c=IN IP4 34.203.250.135\r -a=rtcp:9 IN IP4 0.0.0.0\r -a=candidate:2235265311 1 udp 7935 34.203.250.135 22602 typ relay raddr 107.20.226.156 rport 51463 generation 0 network-cost 50\r -a=ice-ufrag:Cmuk\r -a=ice-pwd:qjHlb5sxe0bozbwpRSYqil3v\r -a=ice-options:trickle\r -a=fingerprint:sha-256 BE:29:0C:60:05:B6:6E:E6:EA:A8:28:D5:89:41:F9:5B:22:11:CD:26:01:98:E0:55:9D:FE:C2:F8:EA:4C:17:91\r -a=setup:actpass\r -a=mid:audio\r -a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r -a=recvonly\r -a=rtcp-mux\r -a=rtpmap:111 opus/48000/2\r -a=rtcp-fb:111 transport-cc\r -a=fmtp:111 minptime=10;useinbandfec=1\r -a=rtpmap:103 ISAC/16000\r -a=rtpmap:104 ISAC/32000\r -a=rtpmap:9 G722/8000\r -a=rtpmap:0 PCMU/8000\r -a=rtpmap:8 PCMA/8000\r -a=rtpmap:106 CN/32000\r -a=rtpmap:105 CN/16000\r -a=rtpmap:13 CN/8000\r -a=rtpmap:110 telephone-event/48000\r -a=rtpmap:112 telephone-event/32000\r -a=rtpmap:113 telephone-event/16000\r -a=rtpmap:126 telephone-event/8000\r -m=video 9 UDP/TLS/RTP/SAVPF 96 98 100 102 127 125 108 124\r -c=IN IP4 0.0.0.0\r -a=rtcp:9 IN IP4 0.0.0.0\r -a=ice-ufrag:Cmuk\r -a=ice-pwd:qjHlb5sxe0bozbwpRSYqil3v\r -a=ice-options:trickle\r -a=fingerprint:sha-256 BE:29:0C:60:05:B6:6E:E6:EA:A8:28:D5:89:41:F9:5B:22:11:CD:26:01:98:E0:55:9D:FE:C2:F8:EA:4C:17:91\r -a=setup:actpass\r -a=mid:video\r -a=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r -a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r -a=extmap:4 urn:3gpp:video-orientation\r -a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r -a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r -a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\r -a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/video-timing\r -a=sendrecv\r -a=rtcp-mux\r -a=rtcp-rsize\r -a=rtpmap:96 VP8/90000\r -a=rtcp-fb:96 goog-remb\r -a=rtcp-fb:96 transport-cc\r -a=rtcp-fb:96 ccm fir\r -a=rtcp-fb:96 nack\r -a=rtcp-fb:96 nack pli\r -a=rtpmap:98 VP9/90000\r -a=rtcp-fb:98 goog-remb\r -a=rtcp-fb:98 transport-cc\r -a=rtcp-fb:98 ccm fir\r -a=rtcp-fb:98 nack\r -a=rtcp-fb:98 nack pli\r -a=rtpmap:100 H264/90000\r -a=rtcp-fb:100 goog-remb\r -a=rtcp-fb:100 transport-cc\r -a=rtcp-fb:100 ccm fir\r -a=rtcp-fb:100 nack\r -a=rtcp-fb:100 nack pli\r -a=fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\r -a=rtpmap:102 H264/90000\r -a=rtcp-fb:102 goog-remb\r -a=rtcp-fb:102 transport-cc\r -a=rtcp-fb:102 ccm fir\r -a=rtcp-fb:102 nack\r -a=rtcp-fb:102 nack pli\r -a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r -a=rtpmap:127 H264/90000\r -a=rtcp-fb:127 goog-remb\r -a=rtcp-fb:127 transport-cc\r -a=rtcp-fb:127 ccm fir\r -a=rtcp-fb:127 nack\r -a=rtcp-fb:127 nack pli\r -a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d0032\r -a=rtpmap:125 H264/90000\r -a=rtcp-fb:125 goog-remb\r -a=rtcp-fb:125 transport-cc\r -a=rtcp-fb:125 ccm fir\r -a=rtcp-fb:125 nack\r -a=rtcp-fb:125 nack pli\r -a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032\r -a=rtpmap:108 red/90000\r -a=rtpmap:124 ulpfec/90000\r -a=ssrc:0000000000 cname:s9hDwDQNjISOxWtK\r -a=ssrc:0000000000 msid:7a9d401b-3cf6-4216-b260-78f93ba4c32e d8b9a935-da54-4d21-a8de-522c87258244\r -a=ssrc:0000000000 mslabel:7a9d401b-3cf6-4216-b260-78f93ba4c32e\r -a=ssrc:0000000000 label:d8b9a935-da54-4d21-a8de-522c87258244\r -`; - - const trackAttributes = new Map(); - - const simSdp1 = setSimulcast(sdp1, 'planb', trackAttributes); - - const fidGroups = simSdp1.match(/a=ssrc-group:FID/g); - assert.equal(fidGroups.length, 3, 'RTX is enabled; therefore, there should be 3 FID groups in the SDP'); - - const ssrcs1 = new Set(simSdp1.match(/a=ssrc:[0-9]+/g).map(line => line.match(/a=ssrc:([0-9]+)/)[1])); - assert.equal(ssrcs1.size, 6, 'RTX is enabled; therefore, there should be 6 SSRCs in the SDP'); - - const simSdp2 = setSimulcast(sdp2, 'planb', trackAttributes); - - assert(!simSdp2.match(/a=ssrc-group:FID/), 'RTX is disabled; therefore, there should be no FID groups in the SDP'); - - const ssrcs2 = new Set(simSdp2.match(/a=ssrc:[0-9]+/g).map(line => line.match(/a=ssrc:([0-9]+)/)[1])); - assert.equal(ssrcs2.size, 3, 'RTX is disabled; therefore, there should be just 3 SSRCs in the SDP'); - }); - }); }); -describe('unifiedPlanFilterLocalCodecs', () => { +describe('filterLocalCodecs', () => { it('should filter codecs in a local SDP based on those advertised in the remote SDP', () => { const localSdp = `\ v=0\r @@ -765,7 +412,7 @@ a=ssrc:1111111111 mslabel:7a9d401b-3cf6-4216-b260-78f93ba4c32e\r a=ssrc:1111111111 label:d8b9a935-da54-4d21-a8de-522c87258244\r `; - const filteredLocalSdp = unifiedPlanFilterLocalCodecs(localSdp, remoteSdp); + const filteredLocalSdp = filterLocalCodecs(localSdp, remoteSdp); const [audioSection, videoSection, newVideoSection] = getMediaSections(localSdp); const [filteredAudioSection, filteredVideoSection, filteredNewVideoSection] = getMediaSections(filteredLocalSdp); @@ -802,10 +449,6 @@ a=ssrc:1111111111 label:d8b9a935-da54-4d21-a8de-522c87258244\r describe('revertSimulcast', () => { combinationContext([ - [ - ['planb', 'unified'], - x => `when called with a ${x} sdp` - ], [ [true, false], x => `when the preferred payload type in answer is${x ? '' : ' not'} VP8` @@ -818,7 +461,7 @@ describe('revertSimulcast', () => { [new Set(['01234']), new Set(['01234', '56789'])], x => `when retransmission is${x.size === 2 ? '' : ' not'} supported` ] - ], ([sdpType, isVP8PreferredPayloadType, revertForAll, ssrcs]) => { + ], ([isVP8PreferredPayloadType, revertForAll, ssrcs]) => { let sdp; let simSdp; let remoteSdp; @@ -827,10 +470,10 @@ describe('revertSimulcast', () => { before(() => { ssrcs = Array.from(ssrcs.values()); - sdp = makeSdpForSimulcast(sdpType, ssrcs); + sdp = makeSdpForSimulcast(ssrcs); trackIdsToAttributes = new Map(); - simSdp = setSimulcast(sdp, sdpType, trackIdsToAttributes); - remoteSdp = makeSdpWithTracks(sdpType, { + simSdp = setSimulcast(sdp, trackIdsToAttributes); + remoteSdp = makeSdpWithTracks({ audio: ['audio-1'], video: [{ id: 'video-1', ssrc: ssrcs[0] }] }); @@ -854,17 +497,17 @@ describe('revertSimulcast', () => { }); }); -describe('unifiedPlanAddOrRewriteNewTrackIds', () => { +describe('addOrRewriteNewTrackIds', () => { [true, false].forEach(withAppData => { - context(`when the Unified Plan SDP ${withAppData ? 'has' : 'does not have'} MediaStreamTrack IDs in a=msid: lines`, () => { + context(`when the SDP ${withAppData ? 'has' : 'does not have'} MediaStreamTrack IDs in a=msid: lines`, () => { it('should rewrite Track IDs with the IDs of MediaStreamTracks associated with unassigned RTCRtpTransceivers', () => { - const sdp = makeSdpWithTracks('unified', { + const sdp = makeSdpWithTracks({ audio: ['foo', 'bar'], video: ['baz', 'zee'] }, null, null, withAppData); const activeMidsToTrackIds = new Map([['mid_baz', 'baz']]); const newTrackIdsByKind = new Map([['audio', ['xxx', 'yyy']], ['video', ['zzz']]]); - const newSdp = unifiedPlanAddOrRewriteNewTrackIds(sdp, activeMidsToTrackIds, newTrackIdsByKind); + const newSdp = addOrRewriteNewTrackIds(sdp, activeMidsToTrackIds, newTrackIdsByKind); const msAttrsAndKinds = getMediaSections(newSdp).map(section => [ section.match(/^a=msid:(.+)/m)[1], section.match(/^m=(audio|video)/)[1] @@ -880,13 +523,13 @@ describe('unifiedPlanAddOrRewriteNewTrackIds', () => { }); }); -describe('unifiedPlanAddOrRewriteTrackIds', () => { +describe('addOrRewriteTrackIds', () => { [true, false].forEach(withAppData => { - context(`when the Unified Plan SDP ${withAppData ? 'has' : 'does not have'} MediaStreamTrack IDs in a=msid: lines`, () => { + context(`when the SDP ${withAppData ? 'has' : 'does not have'} MediaStreamTrack IDs in a=msid: lines`, () => { it('should rewrite Track IDs with the IDs of MediaStreamTracks associated with RTCRtpTransceivers', () => { - const sdp = makeSdpWithTracks('unified', { audio: ['foo'], video: ['bar'] }, null, null, withAppData); + const sdp = makeSdpWithTracks({ audio: ['foo'], video: ['bar'] }, null, null, withAppData); const midsToTrackIds = new Map([['mid_foo', 'baz'], ['mid_bar', 'zee']]); - const newSdp = unifiedPlanAddOrRewriteTrackIds(sdp, midsToTrackIds); + const newSdp = addOrRewriteTrackIds(sdp, midsToTrackIds); const sections = getMediaSections(newSdp); midsToTrackIds.forEach((trackId, mid) => { const section = sections.find(section => new RegExp(`^a=mid:${mid}$`, 'm').test(section)); @@ -1173,80 +816,7 @@ a=ssrc-group:FID 2476366320 3143435575`; }); describe('enableDtxForOpus', () => { - const sdps = { - planb: `\ -v=0\r -o=- 6385359508499371184 3 IN IP4 127.0.0.1\r -s=-\r -t=0 0\r -a=group:BUNDLE audio video\r -a=msid-semantic: WMS 7a9d401b-3cf6-4216-b260-78f93ba4c32e\r -m=audio 22602 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126\r -c=IN IP4 34.203.250.135\r -a=rtcp:9 IN IP4 0.0.0.0\r -a=candidate:2235265311 1 udp 7935 34.203.250.135 22602 typ relay raddr 107.20.226.156 rport 51463 generation 0 network-cost 50\r -a=ice-ufrag:Cmuk\r -a=ice-pwd:qjHlb5sxe0bozbwpRSYqil3v\r -a=ice-options:trickle\r -a=fingerprint:sha-256 BE:29:0C:60:05:B6:6E:E6:EA:A8:28:D5:89:41:F9:5B:22:11:CD:26:01:98:E0:55:9D:FE:C2:F8:EA:4C:17:91\r -a=setup:actpass\r -a=mid:audio\r -a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r -a=sendrecv\r -a=rtcp-mux\r -a=rtpmap:111 opus/48000/2\r -a=rtcp-fb:111 transport-cc\r -a=fmtp:111 minptime=10;useinbandfec=1\r -a=rtpmap:103 ISAC/16000\r -a=rtpmap:104 ISAC/32000\r -a=rtpmap:9 G722/8000\r -a=rtpmap:0 PCMU/8000\r -a=rtpmap:8 PCMA/8000\r -a=rtpmap:106 CN/32000\r -a=rtpmap:105 CN/16000\r -a=rtpmap:13 CN/8000\r -a=rtpmap:110 telephone-event/48000\r -a=rtpmap:112 telephone-event/32000\r -a=rtpmap:113 telephone-event/16000\r -a=rtpmap:126 telephone-event/8000\r -m=video 9 UDP/TLS/RTP/SAVPF 96 97\r -c=IN IP4 0.0.0.0\r -a=rtcp:9 IN IP4 0.0.0.0\r -a=ice-ufrag:Cmuk\r -a=ice-pwd:qjHlb5sxe0bozbwpRSYqil3v\r -a=ice-options:trickle\r -a=fingerprint:sha-256 BE:29:0C:60:05:B6:6E:E6:EA:A8:28:D5:89:41:F9:5B:22:11:CD:26:01:98:E0:55:9D:FE:C2:F8:EA:4C:17:91\r -a=setup:actpass\r -a=mid:video\r -a=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r -a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r -a=extmap:4 urn:3gpp:video-orientation\r -a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r -a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r -a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\r -a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/video-timing\r -a=sendrecv\r -a=rtcp-mux\r -a=rtcp-rsize\r -a=rtpmap:96 VP8/90000\r -a=rtcp-fb:96 goog-remb\r -a=rtcp-fb:96 transport-cc\r -a=rtcp-fb:96 ccm fir\r -a=rtcp-fb:96 nack\r -a=rtcp-fb:96 nack pli\r -a=rtpmap:97 rtx/90000\r -a=fmtp:97 apt=96\r -a=ssrc-group:FID 0000000000 1111111111\r -a=ssrc:0000000000 cname:s9hDwDQNjISOxWtK\r -a=ssrc:0000000000 msid:7a9d401b-3cf6-4216-b260-78f93ba4c32e d8b9a935-da54-4d21-a8de-522c87258244\r -a=ssrc:0000000000 mslabel:7a9d401b-3cf6-4216-b260-78f93ba4c32e\r -a=ssrc:0000000000 label:d8b9a935-da54-4d21-a8de-522c87258244\r -a=ssrc:1111111111 cname:s9hDwDQNjISOxWtK\r -a=ssrc:1111111111 msid:7a9d401b-3cf6-4216-b260-78f93ba4c32e d8b9a935-da54-4d21-a8de-522c87258244\r -a=ssrc:1111111111 mslabel:7a9d401b-3cf6-4216-b260-78f93ba4c32e\r -a=ssrc:1111111111 label:d8b9a935-da54-4d21-a8de-522c87258244\r -`, - unified: `\ + const sdp = `\ v=0\r o=- 6385359508499371184 3 IN IP4 127.0.0.1\r s=-\r @@ -1345,51 +915,38 @@ a=rtpmap:110 telephone-event/48000\r a=rtpmap:112 telephone-event/32000\r a=rtpmap:113 telephone-event/16000\r a=rtpmap:126 telephone-event/8000\r -` - }; - - combinationContext([ - [ - ['planb', 'unified'], - x => `when the SDP is of the format "${x}"` - ], - [ - ['', '0'], - x => `when audio mids are ${x ? '' : 'not '}specified` - ] - ], ([sdpFormat, midsCsv]) => { - const mids = midsCsv ? midsCsv.split(',') : []; +`; - if (mids.length > 0 && sdpFormat === 'planb') { - return; - } - const dtxMids = mids.length > 0 ? mids : sdpFormat === 'planb' ? ['audio'] : ['0', '2']; + ['', '0'].forEach(midsCsv => { + context(`when audio mids are ${midsCsv ? '' : 'not '}specified`, () => { + const mids = midsCsv ? midsCsv.split(',') : []; + const dtxMids = mids.length > 0 ? mids : ['0', '2']; - it(`should enable opus DTX for ${mids.length > 0 ? 'the specified audio mids' : 'all the audio mids'}`, () => { - const sdp = sdps[sdpFormat]; - const sdp1 = mids.length > 0 ? enableDtxForOpus(sdp, mids) : enableDtxForOpus(sdp); - const mediaSections = getMediaSections(sdp); - const mediaSections1 = getMediaSections(sdp1); - mediaSections1.forEach((section, i) => { - if (!/^m=audio/.test(section)) { - assert.equal(section, mediaSections[i]); - } else { - const mid = section.match(/^a=mid:(.+)$/m)[1]; - if (dtxMids.includes(mid)) { - assert.notEqual(section, mediaSections[i]); - assert(/a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1/.test(section)); - assert(!/a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1/.test(mediaSections[i])); - } else { + it(`should enable opus DTX for ${mids.length > 0 ? 'the specified audio mids' : 'all the audio mids'}`, () => { + const sdp1 = mids.length > 0 ? enableDtxForOpus(sdp, mids) : enableDtxForOpus(sdp); + const mediaSections = getMediaSections(sdp); + const mediaSections1 = getMediaSections(sdp1); + mediaSections1.forEach((section, i) => { + if (!/^m=audio/.test(section)) { assert.equal(section, mediaSections[i]); + } else { + const mid = section.match(/^a=mid:(.+)$/m)[1]; + if (dtxMids.includes(mid)) { + assert.notEqual(section, mediaSections[i]); + assert(/a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1/.test(section)); + assert(!/a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1/.test(mediaSections[i])); + } else { + assert.equal(section, mediaSections[i]); + } } - } + }); }); }); }); }); -function itShouldHaveCodecOrder(sdpType, preferredAudioCodecs, preferredVideoCodecs, expectedAudioCodecIds, expectedVideoCodecIds) { - const sdp = makeSdpWithTracks(sdpType, { +function itShouldHaveCodecOrder(preferredAudioCodecs, preferredVideoCodecs, expectedAudioCodecIds, expectedVideoCodecIds) { + const sdp = makeSdpWithTracks({ audio: ['audio-1', 'audio-2'], video: ['video-1', 'video-2'] }); @@ -1401,5 +958,3 @@ function itShouldHaveCodecOrder(sdpType, preferredAudioCodecs, preferredVideoCod assert.equal(codecIds.join(' '), expectedCodecIds.join(' ')); }); } - - diff --git a/test/unit/spec/util/trackmatcher/mid.js b/test/unit/spec/util/trackmatcher.js similarity index 90% rename from test/unit/spec/util/trackmatcher/mid.js rename to test/unit/spec/util/trackmatcher.js index 36d9e2893..4f0702fa6 100644 --- a/test/unit/spec/util/trackmatcher/mid.js +++ b/test/unit/spec/util/trackmatcher.js @@ -1,10 +1,10 @@ 'use strict'; const assert = require('assert'); -const { makeSdpWithTracks } = require('../../../../lib/mocksdp'); -const MIDTrackMatcher = require('../../../../../lib/util/sdp/trackmatcher/mid'); +const { makeSdpWithTracks } = require('../../../lib/mocksdp'); +const TrackMatcher = require('../../../../lib/util/sdp/trackmatcher.js'); -describe('MIDTrackMatcher', () => { +describe('TrackMatcher', () => { const tests = [ // Add a first, then a second, and then two more audio MediaStreamTracks { @@ -88,10 +88,10 @@ describe('MIDTrackMatcher', () => { it('should match new MediaStreamTrack IDs to their MIDs in the SDP', () => { tests.forEach(test => { - const trackMatcher = new MIDTrackMatcher(); + const trackMatcher = new TrackMatcher(); test.sdps.forEach((tracksByKind, i) => { - const sdp = makeSdpWithTracks('unified', tracksByKind); + const sdp = makeSdpWithTracks(tracksByKind); const matchesByKind = test.matches[i]; trackMatcher.update(sdp); diff --git a/test/unit/spec/util/trackmatcher/ordered.js b/test/unit/spec/util/trackmatcher/ordered.js deleted file mode 100644 index 061f986f1..000000000 --- a/test/unit/spec/util/trackmatcher/ordered.js +++ /dev/null @@ -1,111 +0,0 @@ -'use strict'; - -const assert = require('assert'); -const { makeSdpWithTracks } = require('../../../../lib/mocksdp'); -const OrderedTrackMatcher = require('../../../../../lib/util/sdp/trackmatcher/ordered'); - -describe('OrderedTrackMatcher', () => { - const tests = [ - // Add a first, then a second, and then two more audio MediaStreamTracks - { - sdps: [ - { audio: ['track1'] }, - { audio: ['track1', 'track2'] }, - { audio: ['track1', 'track2', 'track3', 'track4'] } - ], - matches: [ - { audio: ['track1'] }, - { audio: ['track2'] }, - { audio: ['track3', 'track4'] } - ] - }, - // Add a first, then a second, and then two more video MediaStreamTracks - { - sdps: [ - { video: ['track1'] }, - { video: ['track1', 'track2'] }, - { video: ['track1', 'track2', 'track3', 'track4'] } - ], - matches: [ - { video: ['track1'] }, - { video: ['track2'] }, - { video: ['track3', 'track4'] } - ] - }, - // First add an audio MediaStreamTrack, then add a video MediaStreamTrack - { - sdps: [ - { audio: ['track1'] }, - { audio: ['track1'], video: ['track2'] } - ], - matches: [ - { audio: ['track1'] }, - { audio: [], video: ['track2'] } - ] - }, - // First add an video MediaStreamTrack, then add a audio MediaStreamTrack - { - sdps: [ - { video: ['track1'] }, - { video: ['track1'], audio: ['track2'] } - ], - matches: [ - { video: ['track1'] }, - { video: [], audio: ['track2'] } - ] - }, - // Add two audio MediaStreamTracks, remove the first one, then add it back - { - sdps: [ - { audio: ['track1'] }, - { audio: ['track1', 'track2'] }, - { audio: ['track2'] }, - { audio: ['track1'] } - ], - matches: [ - { audio: ['track1'] }, - { audio: ['track2'] }, - { audio: [] }, - { audio: ['track1'] } - ] - }, - // Add two video MediaStreamTracks, remove the first one, then add it back - { - sdps: [ - { video: ['track1'] }, - { video: ['track1', 'track2'] }, - { video: ['track2'] }, - { video: ['track1'] } - ], - matches: [ - { video: ['track1'] }, - { video: ['track2'] }, - { video: [] }, - { video: ['track1'] } - ] - } - ]; - - it('should match new MediaStreamTrack IDs in the order in which they appear in the SDP', () => { - tests.forEach(test => { - const trackMatcher = new OrderedTrackMatcher(); - - test.sdps.forEach((tracksByKind, i) => { - const sdp = makeSdpWithTracks('planb', tracksByKind); - const matchesByKind = test.matches[i]; - - trackMatcher.update(sdp); - - ['audio', 'video'].forEach(kind => { - const event = { track: { kind } }; - const matches = matchesByKind[kind] || []; - - matches.forEach(match => { - assert.equal(trackMatcher.match(event), match); - }); - assert.equal(trackMatcher.match(event), null); - }); - }); - }); - }); -}); diff --git a/tsdef/PreflightTypes.d.ts b/tsdef/PreflightTypes.d.ts index 32d4a61d0..2b6fa93db 100644 --- a/tsdef/PreflightTypes.d.ts +++ b/tsdef/PreflightTypes.d.ts @@ -39,6 +39,11 @@ export interface Stats { min: number; } +export interface ProgressEvent { + duration: number; + name: string; +} + export interface PreflightReportStats { jitter: Stats|null; rtt: Stats|null; @@ -51,6 +56,5 @@ export interface PreflightTestReport { stats: PreflightReportStats iceCandidateStats: RTCIceCandidateStats[]; selectedIceCandidatePairStats: SelectedIceCandidatePairStats | null; + progressEvents: ProgressEvent[]; } - - diff --git a/tsdef/preflighttest.d.ts b/tsdef/preflighttest.d.ts index 15759bf4f..996554bf1 100644 --- a/tsdef/preflighttest.d.ts +++ b/tsdef/preflighttest.d.ts @@ -9,7 +9,7 @@ import { PreflightTestReport } from '.'; export declare interface PreflightTest { on(event: 'progress', listener: (progress: string) => void): this; on(event: 'completed', listener: (report: PreflightTestReport)=> void): this; - on(event: 'failed', listener: (error: Error) => void): this; + on(event: 'failed', listener: (error: Error, report: PreflightTestReport) => void): this; } export declare class PreflightTest extends EventEmitter { diff --git a/tsdef/types.d.ts b/tsdef/types.d.ts index c6f9e0b9c..4595db98d 100644 --- a/tsdef/types.d.ts +++ b/tsdef/types.d.ts @@ -258,6 +258,7 @@ export class StatsReport { remoteAudioTrackStats: RemoteAudioTrackStats[]; remoteVideoTrackStats: RemoteVideoTrackStats[]; } + export interface CancelablePromise extends Promise { cancel: () => void; }