/* global __filename */ import { getLogger } from '@jitsi/logger'; import { parsePrimarySSRC, parseSecondarySSRC, SdpTransformWrap } from './SdpTransformUtil'; const logger = getLogger(__filename); /** * Handles the work of keeping video ssrcs consistent across multiple * o/a cycles, making it such that all stream operations can be * kept local and do not need to be signaled. * NOTE: This only keeps the 'primary' video ssrc consistent: meaning * the primary video stream */ export default class SdpConsistency { /** * Constructor * @param {string} logPrefix the log prefix appended to every logged * message, currently used to distinguish for which * TraceablePeerConnection the instance works. */ constructor(logPrefix) { this.clearVideoSsrcCache(); this.logPrefix = logPrefix; } /** * Clear the cached video primary and primary rtx ssrcs so that * they will not be used for the next call to * makeVideoPrimarySsrcsConsistent */ clearVideoSsrcCache() { this.cachedPrimarySsrc = null; this.injectRecvOnly = false; } /** * Explicitly set the primary ssrc to be used in * makeVideoPrimarySsrcsConsistent * @param {number} primarySsrc the primarySsrc to be used * in future calls to makeVideoPrimarySsrcsConsistent * @throws Error if primarySsrc is not a number */ setPrimarySsrc(primarySsrc) { if (typeof primarySsrc !== 'number') { throw new Error('Primary SSRC must be a number!'); } this.cachedPrimarySsrc = primarySsrc; } /** * Checks whether or not there is a primary video SSRC cached already. * @return {boolean} */ hasPrimarySsrcCached() { return Boolean(this.cachedPrimarySsrc); } /** * Given an sdp string, either: * 1) record the primary video and primary rtx ssrcs to be * used in future calls to makeVideoPrimarySsrcsConsistent or * 2) change the primary and primary rtx ssrcs in the given sdp * to match the ones previously cached * @param {string} sdpStr the sdp string to (potentially) * change to make the video ssrcs consistent * @returns {string} a (potentially) modified sdp string * with ssrcs consistent with this class' cache */ makeVideoPrimarySsrcsConsistent(sdpStr) { const sdpTransformer = new SdpTransformWrap(sdpStr); const videoMLine = sdpTransformer.selectMedia('video'); if (!videoMLine) { logger.debug(`${this.logPrefix} no 'video' media found in the sdp: ${sdpStr}`); return sdpStr; } if (videoMLine.direction === 'recvonly') { // If the mline is recvonly, we'll add the primary // ssrc as a recvonly ssrc if (this.cachedPrimarySsrc && this.injectRecvOnly) { videoMLine.addSSRCAttribute({ id: this.cachedPrimarySsrc, attribute: 'cname', value: `recvonly-${this.cachedPrimarySsrc}` }); } else { logger.info(`${this.logPrefix} no SSRC found for the recvonly video stream!`); } } else { const newPrimarySsrc = videoMLine.getPrimaryVideoSsrc(); if (!newPrimarySsrc) { logger.info(`${this.logPrefix} sdp-consistency couldn't parse new primary ssrc`); return sdpStr; } if (this.cachedPrimarySsrc) { videoMLine.replaceSSRC(newPrimarySsrc, this.cachedPrimarySsrc); for (const group of videoMLine.ssrcGroups) { if (group.semantics === 'FID') { const primarySsrc = parsePrimarySSRC(group); const rtxSsrc = parseSecondarySSRC(group); // eslint-disable-next-line max-depth if (primarySsrc === newPrimarySsrc) { group.ssrcs = `${this.cachedPrimarySsrc} ${rtxSsrc}`; } } } } else { this.cachedPrimarySsrc = newPrimarySsrc; } this.injectRecvOnly = true; } return sdpTransformer.toRawSDP(); } }