UNPKG

twilio-video

Version:

Twilio Video JavaScript Library

1,121 lines 58.2 kB
'use strict'; var __values = (this && this.__values) || function(o) { var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; if (m) return m.call(o); if (o && typeof o.length === "number") return { next: function () { if (o && i >= o.length) o = void 0; return { value: o && o[i++], done: !o }; } }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); }; var __read = (this && this.__read) || function (o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; }; var _a = require('./util'), flatMap = _a.flatMap, guessBrowser = _a.guessBrowser, guessBrowserVersion = _a.guessBrowserVersion; var getSdpFormat = require('./util/sdp').getSdpFormat; var guess = guessBrowser(); var guessVersion = guessBrowserVersion(); var isChrome = guess === 'chrome'; var isFirefox = guess === 'firefox'; var isSafari = guess === 'safari'; var chromeMajorVersion = isChrome ? guessVersion.major : null; var CHROME_LEGACY_MAX_AUDIO_LEVEL = 32767; /** * Helper function to find a specific stat from a report. * Browsers provide the stats report as a Map, * but Citrix provides stats report as an array. * @private */ function getStatById(report, id) { if (typeof report.get === 'function') { return report.get(id); } return report.find(function (s) { return s.id === id; }); } /** * Filter the RTCStatsReport to only include stats related to a specific track. * This function is designed for use with Citrix, where getStats(track) is not supported. * It includes specific logic to filter the statistics report returned by Citrix and should * only be used when getStats(track) fails. * * @param {RTCStatsReport|Array<RTCStats>} arrayOrMap - Full stats report or array of stats * @param {MediaStreamTrack} track - The track to filter by * @param {boolean} [isRemote=false] - Whether this is a remote track * @returns {RTCStatsReport} Filtered stats * @private */ function filterStatsByTrack(arrayOrMap, track, isRemote) { var e_1, _a, e_2, _b, e_3, _c, e_4, _d, e_5, _e, e_6, _f, e_7, _g, e_8, _h, e_9, _j, e_10, _k, e_11, _l; if (isRemote === void 0) { isRemote = false; } // Handle different input types var allStats; if (Array.isArray(arrayOrMap)) { allStats = new Map(arrayOrMap.map(function (stat) { return [stat.id || String(Math.random()), stat]; })); } else if (arrayOrMap instanceof Map) { allStats = arrayOrMap; } else if (typeof arrayOrMap === 'object' && arrayOrMap !== null) { // Handle object-style stats (non-standard) var statsMap_1 = new Map(); Object.keys(arrayOrMap).forEach(function (key) { statsMap_1.set(key, arrayOrMap[key]); }); allStats = statsMap_1; } else { return new Map(); } if (!allStats || !track) { return new Map(); } var filteredReport = new Map(); var trackId = track.id; var trackKind = track.kind; // Step 1: Find the primary track-specific stats var primaryStats = null; var primaryStatsId = null; var ssrc = null; try { // Find the primary stat for this track (inbound-rtp for remote, media-source for local) for (var allStats_1 = __values(allStats), allStats_1_1 = allStats_1.next(); !allStats_1_1.done; allStats_1_1 = allStats_1.next()) { var _m = __read(allStats_1_1.value, 2), id = _m[0], stat = _m[1]; // For remote tracks, find matching inbound-rtp with matching trackIdentifier if (isRemote && stat.type === 'inbound-rtp' && stat.trackIdentifier === trackId) { primaryStats = stat; primaryStatsId = id; ssrc = stat.ssrc; break; } else if (!isRemote && stat.type === 'media-source' && stat.trackIdentifier === trackId) { // For local tracks, find matching media-source with matching trackIdentifier primaryStats = stat; primaryStatsId = id; break; } else if (stat.type === 'track' && stat.trackIdentifier === trackId) { // Also check for track stats with matching trackIdentifier if (!primaryStats) { primaryStats = stat; primaryStatsId = id; } } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (allStats_1_1 && !allStats_1_1.done && (_a = allStats_1.return)) _a.call(allStats_1); } finally { if (e_1) throw e_1.error; } } // If no primary stat was found using the trackId, try a more lenient approach if (!primaryStats) { // For remote tracks, try to find an inbound-rtp of the correct kind if (isRemote) { // Get all inbound-rtp stats of the right kind var candidateInbounds = []; try { for (var allStats_2 = __values(allStats), allStats_2_1 = allStats_2.next(); !allStats_2_1.done; allStats_2_1 = allStats_2.next()) { var _o = __read(allStats_2_1.value, 2), id = _o[0], stat = _o[1]; if (stat.type === 'inbound-rtp' && (stat.kind === trackKind || stat.mediaType === trackKind)) { candidateInbounds.push({ id: id, stat: stat }); } } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (allStats_2_1 && !allStats_2_1.done && (_b = allStats_2.return)) _b.call(allStats_2); } finally { if (e_2) throw e_2.error; } } // If there are multiple candidates, we need to be careful if (candidateInbounds.length === 1) { // Only one candidate, use it primaryStats = candidateInbounds[0].stat; primaryStatsId = candidateInbounds[0].id; ssrc = primaryStats.ssrc; } else if (candidateInbounds.length > 1) { // Multiple candidates - if we have the trackId, try to match by mid // otherwise just take the first one primaryStats = candidateInbounds[0].stat; primaryStatsId = candidateInbounds[0].id; ssrc = primaryStats.ssrc; } } else { try { // For local tracks, try to find a media-source of the correct kind for (var allStats_3 = __values(allStats), allStats_3_1 = allStats_3.next(); !allStats_3_1.done; allStats_3_1 = allStats_3.next()) { var _p = __read(allStats_3_1.value, 2), id = _p[0], stat = _p[1]; if (stat.type === 'media-source' && stat.kind === trackKind) { primaryStats = stat; primaryStatsId = id; break; } } } catch (e_3_1) { e_3 = { error: e_3_1 }; } finally { try { if (allStats_3_1 && !allStats_3_1.done && (_c = allStats_3.return)) _c.call(allStats_3); } finally { if (e_3) throw e_3.error; } } } } // If we still didn't find a primary stat, return an empty report if (!primaryStats) { return filteredReport; } // Step 2: Add the primary stat filteredReport.set(primaryStatsId, primaryStats); // Step 3: Add related stats using direct references var directlyRelatedIds = new Set(); // Track different types of related IDs if (isRemote) { // For remote tracks (inbound-rtp is primary) if (primaryStats.codecId) { directlyRelatedIds.add(primaryStats.codecId); } if (primaryStats.transportId) { directlyRelatedIds.add(primaryStats.transportId); } if (primaryStats.remoteId) { directlyRelatedIds.add(primaryStats.remoteId); } // Find remote-outbound-rtp based on ssrc if (ssrc) { try { for (var allStats_4 = __values(allStats), allStats_4_1 = allStats_4.next(); !allStats_4_1.done; allStats_4_1 = allStats_4.next()) { var _q = __read(allStats_4_1.value, 2), id = _q[0], stat = _q[1]; if (stat.type === 'remote-outbound-rtp' && stat.ssrc === ssrc) { directlyRelatedIds.add(id); } } } catch (e_4_1) { e_4 = { error: e_4_1 }; } finally { try { if (allStats_4_1 && !allStats_4_1.done && (_d = allStats_4.return)) _d.call(allStats_4); } finally { if (e_4) throw e_4.error; } } } try { // Add codec, transport, and remote stats for (var directlyRelatedIds_1 = __values(directlyRelatedIds), directlyRelatedIds_1_1 = directlyRelatedIds_1.next(); !directlyRelatedIds_1_1.done; directlyRelatedIds_1_1 = directlyRelatedIds_1.next()) { var relatedId = directlyRelatedIds_1_1.value; if (allStats.has(relatedId)) { filteredReport.set(relatedId, allStats.get(relatedId)); } } } catch (e_5_1) { e_5 = { error: e_5_1 }; } finally { try { if (directlyRelatedIds_1_1 && !directlyRelatedIds_1_1.done && (_e = directlyRelatedIds_1.return)) _e.call(directlyRelatedIds_1); } finally { if (e_5) throw e_5.error; } } try { // Add the track stats if it exists for (var allStats_5 = __values(allStats), allStats_5_1 = allStats_5.next(); !allStats_5_1.done; allStats_5_1 = allStats_5.next()) { var _r = __read(allStats_5_1.value, 2), id = _r[0], stat = _r[1]; if (stat.type === 'track' && stat.trackIdentifier === trackId) { filteredReport.set(id, stat); } } } catch (e_6_1) { e_6 = { error: e_6_1 }; } finally { try { if (allStats_5_1 && !allStats_5_1.done && (_f = allStats_5.return)) _f.call(allStats_5); } finally { if (e_6) throw e_6.error; } } } else { try { // For local tracks (media-source is primary) // Find outbound-rtp that references this media source for (var allStats_6 = __values(allStats), allStats_6_1 = allStats_6.next(); !allStats_6_1.done; allStats_6_1 = allStats_6.next()) { var _s = __read(allStats_6_1.value, 2), id = _s[0], stat = _s[1]; if (stat.type === 'outbound-rtp' && stat.mediaSourceId === primaryStatsId) { filteredReport.set(id, stat); // Add codec and transport if (stat.codecId) { directlyRelatedIds.add(stat.codecId); } if (stat.transportId) { directlyRelatedIds.add(stat.transportId); } // Find remote-inbound-rtp that references this outbound-rtp var outboundId = id; try { for (var allStats_7 = (e_8 = void 0, __values(allStats)), allStats_7_1 = allStats_7.next(); !allStats_7_1.done; allStats_7_1 = allStats_7.next()) { var _t = __read(allStats_7_1.value, 2), remoteId = _t[0], remoteStat = _t[1]; if (remoteStat.type === 'remote-inbound-rtp' && remoteStat.localId === outboundId) { filteredReport.set(remoteId, remoteStat); } } } catch (e_8_1) { e_8 = { error: e_8_1 }; } finally { try { if (allStats_7_1 && !allStats_7_1.done && (_h = allStats_7.return)) _h.call(allStats_7); } finally { if (e_8) throw e_8.error; } } } } } catch (e_7_1) { e_7 = { error: e_7_1 }; } finally { try { if (allStats_6_1 && !allStats_6_1.done && (_g = allStats_6.return)) _g.call(allStats_6); } finally { if (e_7) throw e_7.error; } } try { // Add codec and transport stats for (var directlyRelatedIds_2 = __values(directlyRelatedIds), directlyRelatedIds_2_1 = directlyRelatedIds_2.next(); !directlyRelatedIds_2_1.done; directlyRelatedIds_2_1 = directlyRelatedIds_2.next()) { var relatedId = directlyRelatedIds_2_1.value; if (allStats.has(relatedId)) { filteredReport.set(relatedId, allStats.get(relatedId)); } } } catch (e_9_1) { e_9 = { error: e_9_1 }; } finally { try { if (directlyRelatedIds_2_1 && !directlyRelatedIds_2_1.done && (_j = directlyRelatedIds_2.return)) _j.call(directlyRelatedIds_2); } finally { if (e_9) throw e_9.error; } } } // Step 4: Add candidate pair and certificate info for context // This is useful information that applies to all tracks // but doesn't risk mixing data between tracks var selectedPairId = null; var transportIds = new Set(); try { // Find all transport IDs referenced in our filtered stats for (var _u = __values(filteredReport.values()), _v = _u.next(); !_v.done; _v = _u.next()) { var stat = _v.value; if (stat.transportId) { transportIds.add(stat.transportId); } } } catch (e_10_1) { e_10 = { error: e_10_1 }; } finally { try { if (_v && !_v.done && (_k = _u.return)) _k.call(_u); } finally { if (e_10) throw e_10.error; } } try { // Add the transports for (var transportIds_1 = __values(transportIds), transportIds_1_1 = transportIds_1.next(); !transportIds_1_1.done; transportIds_1_1 = transportIds_1.next()) { var transportId = transportIds_1_1.value; if (allStats.has(transportId)) { var transport = allStats.get(transportId); filteredReport.set(transportId, transport); // Track the selected candidate pair if (transport.selectedCandidatePairId) { selectedPairId = transport.selectedCandidatePairId; } // Add certificate info if (transport.localCertificateId && allStats.has(transport.localCertificateId)) { filteredReport.set(transport.localCertificateId, allStats.get(transport.localCertificateId)); } if (transport.remoteCertificateId && allStats.has(transport.remoteCertificateId)) { filteredReport.set(transport.remoteCertificateId, allStats.get(transport.remoteCertificateId)); } } } } catch (e_11_1) { e_11 = { error: e_11_1 }; } finally { try { if (transportIds_1_1 && !transportIds_1_1.done && (_l = transportIds_1.return)) _l.call(transportIds_1); } finally { if (e_11) throw e_11.error; } } // Add only the selected candidate pair, not all candidate pairs if (selectedPairId && allStats.has(selectedPairId)) { var selectedPair = allStats.get(selectedPairId); filteredReport.set(selectedPairId, selectedPair); // Add the local and remote candidates for the selected pair if (selectedPair.localCandidateId && allStats.has(selectedPair.localCandidateId)) { filteredReport.set(selectedPair.localCandidateId, allStats.get(selectedPair.localCandidateId)); } if (selectedPair.remoteCandidateId && allStats.has(selectedPair.remoteCandidateId)) { filteredReport.set(selectedPair.remoteCandidateId, allStats.get(selectedPair.remoteCandidateId)); } } return filteredReport; } /** * Get the standardized {@link RTCPeerConnection} statistics. * @param {RTCPeerConnection} peerConnection * @param {object} [options] - Used for testing * @returns {Promise.<StandardizedStatsResponse>} */ function getStats(peerConnection, options) { if (!(peerConnection && typeof peerConnection.getStats === 'function')) { return Promise.reject(new Error('Given PeerConnection does not support getStats')); } return _getStats(peerConnection, options); } /** * getStats() implementation. * @param {RTCPeerConnection} peerConnection * @param {object} [options] - Used for testing * @returns {Promise.<StandardizedStatsResponse>} */ function _getStats(peerConnection, options) { var localAudioTracks = getTracks(peerConnection, 'audio', 'local'); var localVideoTracks = getTracks(peerConnection, 'video', 'local'); var remoteAudioTracks = getTracks(peerConnection, 'audio'); var remoteVideoTracks = getTracks(peerConnection, 'video'); var statsResponse = { activeIceCandidatePair: null, localAudioTrackStats: [], localVideoTrackStats: [], remoteAudioTrackStats: [], remoteVideoTrackStats: [] }; var trackStatsPromises = flatMap([ [localAudioTracks, 'localAudioTrackStats', false], [localVideoTracks, 'localVideoTrackStats', false], [remoteAudioTracks, 'remoteAudioTrackStats', true], [remoteVideoTracks, 'remoteVideoTrackStats', true] ], function (_a) { var _b = __read(_a, 3), tracks = _b[0], statsArrayName = _b[1], isRemote = _b[2]; return tracks.map(function (track) { return getTrackStats(peerConnection, track, Object.assign({ isRemote: isRemote }, options)).then(function (trackStatsArray) { trackStatsArray.forEach(function (trackStats) { trackStats.trackId = track.id; statsResponse[statsArrayName].push(trackStats); }); }); }); }); return Promise.all(trackStatsPromises).then(function () { return getActiveIceCandidatePairStats(peerConnection, options); }).then(function (activeIceCandidatePairStatsReport) { statsResponse.activeIceCandidatePair = activeIceCandidatePairStatsReport; return statsResponse; }); } /** * Generate the {@link StandardizedActiveIceCandidatePairStatsReport} for the * {@link RTCPeerConnection}. * @param {RTCPeerConnection} peerConnection * @param {object} [options] * @returns {Promise<StandardizedActiveIceCandidatePairStatsReport>} */ function getActiveIceCandidatePairStats(peerConnection, options) { if (options === void 0) { options = {}; } if (typeof options.testForChrome !== 'undefined' || isChrome || typeof options.testForSafari !== 'undefined' || isSafari) { return peerConnection.getStats().then(standardizeChromeOrSafariActiveIceCandidatePairStats); } if (typeof options.testForFirefox !== 'undefined' || isFirefox) { return peerConnection.getStats().then(standardizeFirefoxActiveIceCandidatePairStats); } return Promise.reject(new Error('RTCPeerConnection#getStats() not supported')); } /** * Standardize the active RTCIceCandidate pair's statistics in Chrome or Safari. * @param {RTCStatsReport} stats * @returns {?StandardizedActiveIceCandidatePairStatsReport} */ function standardizeChromeOrSafariActiveIceCandidatePairStats(stats) { var activeCandidatePairStats = Array.from(stats.values()).find(function (_a) { var nominated = _a.nominated, type = _a.type; return type === 'candidate-pair' && nominated; }); if (!activeCandidatePairStats) { return null; } var activeLocalCandidateStats = getStatById(stats, activeCandidatePairStats.localCandidateId); var activeRemoteCandidateStats = getStatById(stats, activeCandidatePairStats.remoteCandidateId); var standardizedCandidateStatsKeys = [ { key: 'candidateType', type: 'string' }, { key: 'ip', type: 'string' }, { key: 'port', type: 'number' }, { key: 'priority', type: 'number' }, { key: 'protocol', type: 'string' }, { key: 'url', type: 'string' } ]; var standardizedLocalCandidateStatsKeys = standardizedCandidateStatsKeys.concat([ { key: 'deleted', type: 'boolean' }, { key: 'relayProtocol', type: 'string' }, { key: 'networkType', type: 'string' } ]); var standatdizedLocalCandidateStatsReport = activeLocalCandidateStats ? standardizedLocalCandidateStatsKeys.reduce(function (report, _a) { var key = _a.key, type = _a.type; report[key] = typeof activeLocalCandidateStats[key] === type ? activeLocalCandidateStats[key] : key === 'deleted' ? false : null; return report; }, {}) : null; var standardizedRemoteCandidateStatsReport = activeRemoteCandidateStats ? standardizedCandidateStatsKeys.reduce(function (report, _a) { var key = _a.key, type = _a.type; report[key] = typeof activeRemoteCandidateStats[key] === type ? activeRemoteCandidateStats[key] : null; return report; }, {}) : null; return [ { key: 'availableIncomingBitrate', type: 'number' }, { key: 'availableOutgoingBitrate', type: 'number' }, { key: 'bytesReceived', type: 'number' }, { key: 'bytesSent', type: 'number' }, { key: 'consentRequestsSent', type: 'number' }, { key: 'currentRoundTripTime', type: 'number' }, { key: 'lastPacketReceivedTimestamp', type: 'number' }, { key: 'lastPacketSentTimestamp', type: 'number' }, { key: 'nominated', type: 'boolean' }, { key: 'priority', type: 'number' }, { key: 'readable', type: 'boolean' }, { key: 'requestsReceived', type: 'number' }, { key: 'requestsSent', type: 'number' }, { key: 'responsesReceived', type: 'number' }, { key: 'responsesSent', type: 'number' }, { key: 'retransmissionsReceived', type: 'number' }, { key: 'retransmissionsSent', type: 'number' }, { key: 'state', type: 'string', fixup: function (state) { return state === 'inprogress' ? 'in-progress' : state; } }, { key: 'totalRoundTripTime', type: 'number' }, { key: 'transportId', type: 'string' }, { key: 'writable', type: 'boolean' } ].reduce(function (report, _a) { var key = _a.key, type = _a.type, fixup = _a.fixup; report[key] = typeof activeCandidatePairStats[key] === type ? (fixup ? fixup(activeCandidatePairStats[key]) : activeCandidatePairStats[key]) : null; return report; }, { localCandidate: standatdizedLocalCandidateStatsReport, remoteCandidate: standardizedRemoteCandidateStatsReport }); } /** * Standardize the active RTCIceCandidate pair's statistics in Firefox. * @param {RTCStatsReport} stats * @returns {?StandardizedActiveIceCandidatePairStatsReport} */ function standardizeFirefoxActiveIceCandidatePairStats(stats) { var activeCandidatePairStats = Array.from(stats.values()).find(function (_a) { var nominated = _a.nominated, type = _a.type; return type === 'candidate-pair' && nominated; }); if (!activeCandidatePairStats) { return null; } var activeLocalCandidateStats = getStatById(stats, activeCandidatePairStats.localCandidateId); var activeRemoteCandidateStats = getStatById(stats, activeCandidatePairStats.remoteCandidateId); var standardizedCandidateStatsKeys = [ { key: 'candidateType', type: 'string' }, { key: 'ip', ffKeys: ['address', 'ipAddress'], type: 'string' }, { key: 'port', ffKeys: ['portNumber'], type: 'number' }, { key: 'priority', type: 'number' }, { key: 'protocol', ffKeys: ['transport'], type: 'string' }, { key: 'url', type: 'string' } ]; var standardizedLocalCandidateStatsKeys = standardizedCandidateStatsKeys.concat([ { key: 'deleted', type: 'boolean' }, { key: 'relayProtocol', type: 'string' } ]); var candidateTypes = { host: 'host', peerreflexive: 'prflx', relayed: 'relay', serverreflexive: 'srflx' }; var standatdizedLocalCandidateStatsReport = activeLocalCandidateStats ? standardizedLocalCandidateStatsKeys.reduce(function (report, _a) { var ffKeys = _a.ffKeys, key = _a.key, type = _a.type; var localStatKey = ffKeys && ffKeys.find(function (key) { return key in activeLocalCandidateStats; }) || key; report[key] = typeof activeLocalCandidateStats[localStatKey] === type ? localStatKey === 'candidateType' ? candidateTypes[activeLocalCandidateStats[localStatKey]] || activeLocalCandidateStats[localStatKey] : activeLocalCandidateStats[localStatKey] : localStatKey === 'deleted' ? false : null; return report; }, {}) : null; var standardizedRemoteCandidateStatsReport = activeRemoteCandidateStats ? standardizedCandidateStatsKeys.reduce(function (report, _a) { var ffKeys = _a.ffKeys, key = _a.key, type = _a.type; var remoteStatKey = ffKeys && ffKeys.find(function (key) { return key in activeRemoteCandidateStats; }) || key; report[key] = typeof activeRemoteCandidateStats[remoteStatKey] === type ? remoteStatKey === 'candidateType' ? candidateTypes[activeRemoteCandidateStats[remoteStatKey]] || activeRemoteCandidateStats[remoteStatKey] : activeRemoteCandidateStats[remoteStatKey] : null; return report; }, {}) : null; return [ { key: 'availableIncomingBitrate', type: 'number' }, { key: 'availableOutgoingBitrate', type: 'number' }, { key: 'bytesReceived', type: 'number' }, { key: 'bytesSent', type: 'number' }, { key: 'consentRequestsSent', type: 'number' }, { key: 'currentRoundTripTime', type: 'number' }, { key: 'lastPacketReceivedTimestamp', type: 'number' }, { key: 'lastPacketSentTimestamp', type: 'number' }, { key: 'nominated', type: 'boolean' }, { key: 'priority', type: 'number' }, { key: 'readable', type: 'boolean' }, { key: 'requestsReceived', type: 'number' }, { key: 'requestsSent', type: 'number' }, { key: 'responsesReceived', type: 'number' }, { key: 'responsesSent', type: 'number' }, { key: 'retransmissionsReceived', type: 'number' }, { key: 'retransmissionsSent', type: 'number' }, { key: 'state', type: 'string' }, { key: 'totalRoundTripTime', type: 'number' }, { key: 'transportId', type: 'string' }, { key: 'writable', type: 'boolean' } ].reduce(function (report, _a) { var key = _a.key, type = _a.type; report[key] = typeof activeCandidatePairStats[key] === type ? activeCandidatePairStats[key] : null; return report; }, { localCandidate: standatdizedLocalCandidateStatsReport, remoteCandidate: standardizedRemoteCandidateStatsReport }); } /** * Get local/remote audio/video MediaStreamTracks. * @param {RTCPeerConnection} peerConnection - The RTCPeerConnection * @param {string} kind - 'audio' or 'video' * @param {string} [localOrRemote] - 'local' or 'remote' * @returns {Array<MediaStreamTrack>} */ function getTracks(peerConnection, kind, localOrRemote) { var getSendersOrReceivers = localOrRemote === 'local' ? 'getSenders' : 'getReceivers'; if (peerConnection[getSendersOrReceivers]) { return peerConnection[getSendersOrReceivers]() .map(function (_a) { var track = _a.track; return track; }) .filter(function (track) { return track && track.kind === kind; }); } var getStreams = localOrRemote === 'local' ? 'getLocalStreams' : 'getRemoteStreams'; var getTracks = kind === 'audio' ? 'getAudioTracks' : 'getVideoTracks'; return flatMap(peerConnection[getStreams](), function (stream) { return stream[getTracks](); }); } /** * Determine if a track is remote by examining the PeerConnection's receivers. * This function is designed for use with Citrix, where getStats(track) is not supported. * @param {RTCPeerConnection} peerConnection * @param {MediaStreamTrack} track * @returns {boolean} True if the track is a remote track * @private */ function isRemoteTrack(peerConnection, track) { var e_12, _a, e_13, _b, e_14, _c; if (!peerConnection || !track) { return false; } // Check if the track belongs to any receiver (remote) if (peerConnection.getReceivers) { var receivers = peerConnection.getReceivers(); try { for (var receivers_1 = __values(receivers), receivers_1_1 = receivers_1.next(); !receivers_1_1.done; receivers_1_1 = receivers_1.next()) { var receiver = receivers_1_1.value; if (receiver.track && receiver.track.id === track.id) { return true; } } } catch (e_12_1) { e_12 = { error: e_12_1 }; } finally { try { if (receivers_1_1 && !receivers_1_1.done && (_a = receivers_1.return)) _a.call(receivers_1); } finally { if (e_12) throw e_12.error; } } } // Check remote streams if getReceivers is not available if (peerConnection.getRemoteStreams) { var remoteStreams = peerConnection.getRemoteStreams(); try { for (var remoteStreams_1 = __values(remoteStreams), remoteStreams_1_1 = remoteStreams_1.next(); !remoteStreams_1_1.done; remoteStreams_1_1 = remoteStreams_1.next()) { var stream = remoteStreams_1_1.value; var tracks = stream.getTracks(); try { for (var tracks_1 = (e_14 = void 0, __values(tracks)), tracks_1_1 = tracks_1.next(); !tracks_1_1.done; tracks_1_1 = tracks_1.next()) { var remoteTrack = tracks_1_1.value; if (remoteTrack.id === track.id) { return true; } } } catch (e_14_1) { e_14 = { error: e_14_1 }; } finally { try { if (tracks_1_1 && !tracks_1_1.done && (_c = tracks_1.return)) _c.call(tracks_1); } finally { if (e_14) throw e_14.error; } } } } catch (e_13_1) { e_13 = { error: e_13_1 }; } finally { try { if (remoteStreams_1_1 && !remoteStreams_1_1.done && (_b = remoteStreams_1.return)) _b.call(remoteStreams_1); } finally { if (e_13) throw e_13.error; } } } // The track is not in any remote source, so it's likely local return false; } /** * Get the standardized statistics for a particular MediaStreamTrack. * @param {RTCPeerConnection} peerConnection * @param {MediaStreamTrack} track * @param {object} [options] - Used for testing * @returns {Promise.<Array<StandardizedTrackStatsReport>>} */ function getTrackStats(peerConnection, track, options) { if (options === void 0) { options = {}; } if (typeof options.testForChrome !== 'undefined' || isChrome) { return chromeOrSafariGetTrackStats(peerConnection, track, options); } if (typeof options.testForFirefox !== 'undefined' || isFirefox) { return firefoxGetTrackStats(peerConnection, track, options); } if (typeof options.testForSafari !== 'undefined' || isSafari) { if (typeof options.testForSafari !== 'undefined' || getSdpFormat() === 'unified') { return chromeOrSafariGetTrackStats(peerConnection, track, options); } // NOTE(syerrapragada): getStats() is not supported on // Safari versions where plan-b is the SDP format // due to this bug: https://bugs.webkit.org/show_bug.cgi?id=192601 return Promise.reject(new Error([ 'getStats() is not supported on this version of Safari', 'due to this bug: https://bugs.webkit.org/show_bug.cgi?id=192601' ].join(' '))); } return Promise.reject(new Error('RTCPeerConnection#getStats() not supported')); } /** * Get the standardized statistics for a particular MediaStreamTrack in Chrome or Safari. * @param {RTCPeerConnection} peerConnection * @param {MediaStreamTrack} track * @param {object} options - Used for testing * @returns {Promise.<Array<StandardizedTrackStatsReport>>} */ function chromeOrSafariGetTrackStats(peerConnection, track, options) { var log = options.log; if (chromeMajorVersion && chromeMajorVersion < 67) { return new Promise(function (resolve, reject) { peerConnection.getStats(function (response) { resolve([standardizeChromeLegacyStats(response, track)]); }, null, reject); }); } return peerConnection.getStats(track) .then(function (response) { log.info('getStats by track successful'); return standardizeChromeOrSafariStats(response, options); }) .catch(function () { // NOTE(lrivas): Citrix doesn't support track-specific getStats, // so this workaround tries getting all stats and filtering by track. log.warn('getStats by track failed. Getting default stats'); return peerConnection.getStats() .then(function (stats) { log.info('getStats by default successful'); var isRemote = isRemoteTrack(peerConnection, track); log.info("Starting filtering stats for ".concat(isRemote ? 'remote' : 'local', " track")); var filteredStats = filterStatsByTrack(stats, track, isRemote); log.info("Completed filtering stats for ".concat(isRemote ? 'remote' : 'local', " track")); return standardizeChromeOrSafariStats(filteredStats, options); }); }); } /** * Get the standardized statistics for a particular MediaStreamTrack in Firefox. * @param {RTCPeerConnection} peerConnection * @param {MediaStreamTrack} track * @param {object} options * @returns {Promise.<Array<StandardizedTrackStatsReport>>} */ function firefoxGetTrackStats(peerConnection, track, options) { return peerConnection.getStats(track).then(function (response) { return [standardizeFirefoxStats(response, options)]; }); } /** * Standardize the MediaStreamTrack's legacy statistics in Chrome. * @param {RTCStatsResponse} response * @param {MediaStreamTrack} track * @returns {StandardizedTrackStatsReport} */ function standardizeChromeLegacyStats(response, track) { var ssrcReport = response.result().find(function (report) { return report.type === 'ssrc' && report.stat('googTrackId') === track.id; }); var standardizedStats = {}; if (ssrcReport) { standardizedStats.timestamp = Math.round(Number(ssrcReport.timestamp)); standardizedStats = ssrcReport.names().reduce(function (stats, name) { switch (name) { case 'googCodecName': stats.codecName = ssrcReport.stat(name); break; case 'googRtt': stats.roundTripTime = Number(ssrcReport.stat(name)); break; case 'googJitterReceived': stats.jitter = Number(ssrcReport.stat(name)); break; case 'googFrameWidthInput': stats.frameWidthInput = Number(ssrcReport.stat(name)); break; case 'googFrameHeightInput': stats.frameHeightInput = Number(ssrcReport.stat(name)); break; case 'googFrameWidthSent': stats.frameWidthSent = Number(ssrcReport.stat(name)); break; case 'googFrameHeightSent': stats.frameHeightSent = Number(ssrcReport.stat(name)); break; case 'googFrameWidthReceived': stats.frameWidthReceived = Number(ssrcReport.stat(name)); break; case 'googFrameHeightReceived': stats.frameHeightReceived = Number(ssrcReport.stat(name)); break; case 'googFrameRateInput': stats.frameRateInput = Number(ssrcReport.stat(name)); break; case 'googFrameRateSent': stats.frameRateSent = Number(ssrcReport.stat(name)); break; case 'googFrameRateReceived': stats.frameRateReceived = Number(ssrcReport.stat(name)); break; case 'ssrc': stats[name] = ssrcReport.stat(name); break; case 'bytesReceived': case 'bytesSent': case 'packetsLost': case 'packetsReceived': case 'packetsSent': case 'audioInputLevel': case 'audioOutputLevel': stats[name] = Number(ssrcReport.stat(name)); break; } return stats; }, standardizedStats); } return standardizedStats; } /** * Standardize the MediaStreamTrack's statistics in Chrome or Safari. * @param {RTCStatsReport} response * @param {object} options - Used for testing * @returns {Array<StandardizedTrackStatsReport>} */ function standardizeChromeOrSafariStats(response, _a) { var _b = _a.simulateExceptionWhileStandardizingStats, simulateExceptionWhileStandardizingStats = _b === void 0 ? false : _b; if (simulateExceptionWhileStandardizingStats) { throw new Error('Error while gathering stats'); } var inbound = null; // NOTE(mpatwardhan): We should expect more than one "outbound-rtp" stats for a // VP8 simulcast MediaStreamTrack. var outbound = []; var remoteInbound = null; var remoteOutbound = null; var track = null; var codec = null; var localMedia = null; response.forEach(function (stat) { var type = stat.type; switch (type) { case 'inbound-rtp': inbound = stat; break; case 'outbound-rtp': outbound.push(stat); break; case 'media-source': localMedia = stat; break; case 'track': track = stat; break; case 'codec': codec = stat; break; case 'remote-inbound-rtp': remoteInbound = stat; break; case 'remote-outbound-rtp': remoteOutbound = stat; break; } }); var isRemote = track ? track.remoteSource : !localMedia; var mainSources = isRemote ? [inbound] : outbound; var stats = []; var remoteSource = isRemote ? remoteOutbound : remoteInbound; // remote rtp stats mainSources.forEach(function (source) { var standardizedStats = {}; var statSources = [ source, // local rtp stats localMedia, track, codec, remoteSource && remoteSource.ssrc === source.ssrc ? remoteSource : null, // remote rtp stats ]; function getStatValue(name) { var sourceFound = statSources.find(function (statSource) { return statSource && typeof statSource[name] !== 'undefined'; }) || null; return sourceFound ? sourceFound[name] : null; } var ssrc = getStatValue('ssrc'); if (typeof ssrc === 'number') { standardizedStats.ssrc = String(ssrc); } var timestamp = getStatValue('timestamp'); standardizedStats.timestamp = Math.round(timestamp); var mimeType = getStatValue('mimeType'); if (typeof mimeType === 'string') { mimeType = mimeType.split('/'); standardizedStats.codecName = mimeType[mimeType.length - 1]; } var roundTripTime = getStatValue('roundTripTime'); if (typeof roundTripTime === 'number') { standardizedStats.roundTripTime = Math.round(roundTripTime * 1000); } var jitter = getStatValue('jitter'); if (typeof jitter === 'number') { standardizedStats.jitter = Math.round(jitter * 1000); } var frameWidth = getStatValue('frameWidth'); if (typeof frameWidth === 'number') { if (isRemote) { standardizedStats.frameWidthReceived = frameWidth; } else { standardizedStats.frameWidthSent = frameWidth; standardizedStats.frameWidthInput = track ? track.frameWidth : localMedia.width; } } var frameHeight = getStatValue('frameHeight'); if (typeof frameHeight === 'number') { if (isRemote) { standardizedStats.frameHeightReceived = frameHeight; } else { standardizedStats.frameHeightSent = frameHeight; standardizedStats.frameHeightInput = track ? track.frameHeight : localMedia.height; } } var framesPerSecond = getStatValue('framesPerSecond'); if (typeof framesPerSecond === 'number' || framesPerSecond === null) { standardizedStats[isRemote ? 'frameRateReceived' : 'frameRateSent'] = framesPerSecond; } var qualityLimitationReason = getStatValue('qualityLimitationReason'); if (typeof qualityLimitationReason === 'string') { standardizedStats.qualityLimitationReason = qualityLimitationReason; } var bytesReceived = getStatValue('bytesReceived'); if (typeof bytesReceived === 'number') { standardizedStats.bytesReceived = bytesReceived; } var bytesSent = getStatValue('bytesSent'); if (typeof bytesSent === 'number') { standardizedStats.bytesSent = bytesSent; } var packetsLost = getStatValue('packetsLost'); if (typeof packetsLost === 'number') { standardizedStats.packetsLost = packetsLost; } var packetsReceived = getStatValue('packetsReceived'); if (typeof packetsReceived === 'number') { standardizedStats.packetsReceived = packetsReceived; } var packetsSent = getStatValue('packetsSent'); if (typeof packetsSent === 'number') { standardizedStats.packetsSent = packetsSent; } var audioLevel = getStatValue('audioLevel'); if (typeof audioLevel === 'number') { audioLevel = Math.round(audioLevel * CHROME_LEGACY_MAX_AUDIO_LEVEL); if (isRemote) { standardizedStats.audioOutputLevel = audioLevel; } else { standardizedStats.audioInputLevel = audioLevel; } } var totalPacketSendDalay = getStatValue('totalPacketSendDelay'); if (typeof totalPacketSendDalay === 'number') { standardizedStats.totalPacketSendDelay = totalPacketSendDalay; } var totalEncodeTime = getStatValue('totalEncodeTime'); if (typeof totalEncodeTime === 'number') { standardizedStats.totalEncodeTime = totalEncodeTime; } var framesEncoded = getStatValue('framesEncoded'); if (typeof framesEncoded === 'number') { standardizedStats.framesEncoded = framesEncoded; } var estimatedPlayoutTimestamp = getStatValue('estimatedPlayoutTimestamp'); if (typeof estimatedPlayoutTimestamp === 'number') { standardizedStats.estimatedPlayoutTimestamp = estimatedPlayoutTimestamp; } var totalDecodeTime = getStatValue('totalDecodeTime'); if (typeof totalDecodeTime === 'number') { standardizedStats.totalDecodeTime = totalDecodeTime; } var framesDecoded = getStatValue('framesDecoded'); if (typeof framesDecoded === 'number') { standardizedStats.framesDecoded = framesDecoded; } var jitterBufferDelay = getStatValue('jitterBufferDelay'); if (typeof jitterBufferDelay === 'number') { standardizedStats.jitterBufferDelay = jitterBufferDelay; } var jitterBufferEmittedCount = getStatValue('jitterBufferEmittedCount'); if (typeof jitterBufferEmittedCount === 'number') { standardizedStats.jitterBufferEmittedCount = jitterBufferEmittedCount; } var freezeCount = getStatValue('freezeCount'); if (typeof freezeCount === 'number') { standardizedStats.freezeCount = freezeCount; } stats.push(standardizedStats); }); return stats; } /** * Standardize the MediaStreamTrack's statistics in Firefox. * @param {RTCStatsReport} response * @param {object} options - Used for testing * @returns {StandardizedTrackStatsReport} */ function standardizeFirefoxStats(response, _a) { if (response === void 0) { response = new Map(); } var isRemote = _a.isRemote, _b = _a.simulateExceptionWhileStandardizingStats, simulateExceptionWhileStandardizingStats = _b === void 0 ? false : _b; if (simulateExceptionWhileStandardizingStats) { throw new Error('Error while gathering stats'); } // NOTE(mroberts): If getStats is called on a closed RTCPeerConnection, // Firefox returns undefined instead of an RTCStatsReport. We workaround this // here. See the following bug for more details: // // https://bugzilla.mozilla.org/show_bug.cgi?id=1377225 // var inbound = null; var outbound = null; // NOTE(mmalavalli): Starting from Firefox 63, RTC{Inbound, Outbound}RTPStreamStats.isRemote // will be deprecated, followed by its removal in Firefox 66. Also, trying to // access members of the remote RTC{Inbound, Outbound}RTPStreamStats without // using RTCStatsReport.get(remoteId) will trigger console warnings. So, we // no longer depend on "isRemote", and we call RTCStatsReport.get(remoteId) // to access the remote RTC{Inbound, Outbound}RTPStreamStats. // // Source: https://blog.mozilla.org/webrtc/getstats-isremote-65/ // response.forEach(function (stat) { var isRemote = stat.isRemote, remoteId = stat.remoteId, type = stat.type; if (isRemote) { return; } switch (type) { case 'inbound-rtp': inbound = stat; outbound = getStatById(response, remoteId); break; case 'outbound-rtp': outbound = stat; inbound = getStatById(response, remoteId); break; } }); var first = isRemote ? inbound : outbound; var second = isRemote ? outbound : inbound; function getStatValue(name) { if (first && typeof first[name] !== 'undefined') { return first[name]; } if (second && typeof second[name] !== 'undefined') { return second[name]; } return null; } var standardizedStats = {}; var timestamp = getStatValue('timestamp'); standardizedStats.timestamp = Math.round(timestamp); var ssrc = getStatValue('ssrc'); if (typeof ssrc === 'number') { standardizedStats.ssrc = String(ssrc); } var bytesSent = getStatValue('bytesSent'); if (typeof bytesSent === 'number') { standardizedStats.bytesSent = bytesSent; } var packetsLost = getStatValue('packetsLost'); if (typeof packetsLost === 'number') { standardizedStats.packetsLost = packetsLost; } var packetsSent = getStatValue('packetsSent'); if (typeof packetsSent === 'number') { standardizedStats.packetsSent = packetsSent; } var roundTripTime = getStatValue('roundTripTime'); if (typeof roundTripTime === 'number') { // roundTripTime is double - measured in seconds. // https://www.w3.org/TR/webrtc-stats/#dom-rtcremoteinboundrtpstreamstats-roundtriptime // cover it to milliseconds (and make it integer) standardizedStats.roundTripTime = Math.round(roundTripTime * 1000); } var jitter = getStatValue('jitter'); if (typeof jitter === 'number') { standardizedStats.jitter = Math.round(jitter * 1000); } var frameRateSent = getStatValue('framesPerSecond'); if (typeof frameRateSent === 'number' || frameRateSent === null) { standardizedStats.frameRateSent