UNPKG

js-3dstk

Version:

Universal JavaScript Client for the 3D Streaming Toolkit - Supports Node.js, Browser and React-Native.

565 lines (493 loc) 19.4 kB
/* * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* More information about these options at jshint.com/docs/options */ /* globals adapter, trace */ /* exported setCodecParam, iceCandidateType, formatTypePreference, maybeSetOpusOptions, maybePreferAudioReceiveCodec, maybePreferAudioSendCodec, maybeSetAudioReceiveBitRate, maybeSetAudioSendBitRate, maybePreferVideoReceiveCodec, maybePreferVideoSendCodec, maybeSetVideoReceiveBitRate, maybeSetVideoSendBitRate, maybeSetVideoSendInitialBitRate, maybeRemoveVideoFec, mergeConstraints, removeCodecParam*/ (function(module) { 'use strict'; module.exports.setCodecParam = setCodecParam; module.exports.iceCandidateType = iceCandidateType; module.exports.formatTypePreference = formatTypePreference; module.exports.maybeSetOpusOptions = maybeSetOpusOptions; module.exports.maybePreferAudioReceiveCodec = maybePreferAudioReceiveCodec; module.exports.maybeSetAudioReceiveBitRate = maybeSetAudioReceiveBitRate; module.exports.maybeSetAudioSendBitRate = maybeSetAudioSendBitRate; module.exports.maybePreferVideoReceiveCodec = maybePreferVideoReceiveCodec; module.exports.maybePreferVideoSendCodec = maybePreferVideoSendCodec; module.exports.maybeSetVideoReceiveBitRate = maybeSetVideoReceiveBitRate; module.exports.maybeSetVideoSendBitRate = maybeSetVideoSendBitRate; module.exports.maybeSetVideoSendInitialBitRate = maybeSetVideoSendInitialBitRate; module.exports.maybePreferCodec = maybePreferCodec; module.exports.mergeConstraints = mergeConstraints; module.exports.removeCodecParam = removeCodecParam; function mergeConstraints(cons1, cons2) { if (!cons1 || !cons2) { return cons1 || cons2; } var merged = cons1; for (var key in cons2) { merged[key] = cons2[key]; } return merged; } function iceCandidateType(candidateStr) { return candidateStr.split(' ')[7]; } // Turns the local type preference into a human-readable string. // Note that this mapping is browser-specific. function formatTypePreference(pref) { if (adapter.browserDetails.browser === 'chrome') { switch (pref) { case 0: return 'TURN/TLS'; case 1: return 'TURN/TCP'; case 2: return 'TURN/UDP'; default: break; } } else if (adapter.browserDetails.browser === 'firefox') { switch (pref) { case 0: return 'TURN/TCP'; case 5: return 'TURN/UDP'; default: break; } } return ''; } function maybeSetOpusOptions(sdp, params) { // Set Opus in Stereo, if stereo is true, unset it, if stereo is false, and // do nothing if otherwise. if (params.opusStereo === 'true') { sdp = setCodecParam(sdp, 'opus/48000', 'stereo', '1'); } else if (params.opusStereo === 'false') { sdp = removeCodecParam(sdp, 'opus/48000', 'stereo'); } // Set Opus FEC, if opusfec is true, unset it, if opusfec is false, and // do nothing if otherwise. if (params.opusFec === 'true') { sdp = setCodecParam(sdp, 'opus/48000', 'useinbandfec', '1'); } else if (params.opusFec === 'false') { sdp = removeCodecParam(sdp, 'opus/48000', 'useinbandfec'); } // Set Opus DTX, if opusdtx is true, unset it, if opusdtx is false, and // do nothing if otherwise. if (params.opusDtx === 'true') { sdp = setCodecParam(sdp, 'opus/48000', 'usedtx', '1'); } else if (params.opusDtx === 'false') { sdp = removeCodecParam(sdp, 'opus/48000', 'usedtx'); } // Set Opus maxplaybackrate, if requested. if (params.opusMaxPbr) { sdp = setCodecParam( sdp, 'opus/48000', 'maxplaybackrate', params.opusMaxPbr); } return sdp; } function maybeSetAudioSendBitRate(sdp, params) { if (!params.audioSendBitrate) { return sdp; } trace('Prefer audio send bitrate: ' + params.audioSendBitrate); return preferBitRate(sdp, params.audioSendBitrate, 'audio'); } function maybeSetAudioReceiveBitRate(sdp, params) { if (!params.audioRecvBitrate) { return sdp; } trace('Prefer audio receive bitrate: ' + params.audioRecvBitrate); return preferBitRate(sdp, params.audioRecvBitrate, 'audio'); } function maybeSetVideoSendBitRate(sdp, params) { if (!params.videoSendBitrate) { return sdp; } trace('Prefer video send bitrate: ' + params.videoSendBitrate); return preferBitRate(sdp, params.videoSendBitrate, 'video'); } function maybeSetVideoReceiveBitRate(sdp, params) { if (!params.videoRecvBitrate) { return sdp; } trace('Prefer video receive bitrate: ' + params.videoRecvBitrate); return preferBitRate(sdp, params.videoRecvBitrate, 'video'); } // Add a b=AS:bitrate line to the m=mediaType section. function preferBitRate(sdp, bitrate, mediaType) { var sdpLines = sdp.split('\r\n'); // Find m line for the given mediaType. var mLineIndex = findLine(sdpLines, 'm=', mediaType); if (mLineIndex === null) { trace('Failed to add bandwidth line to sdp, as no m-line found'); return sdp; } // Find next m-line if any. var nextMLineIndex = findLineInRange(sdpLines, mLineIndex + 1, -1, 'm='); if (nextMLineIndex === null) { nextMLineIndex = sdpLines.length; } // Find c-line corresponding to the m-line. var cLineIndex = findLineInRange(sdpLines, mLineIndex + 1, nextMLineIndex, 'c='); if (cLineIndex === null) { trace('Failed to add bandwidth line to sdp, as no c-line found'); return sdp; } // Check if bandwidth line already exists between c-line and next m-line. var bLineIndex = findLineInRange(sdpLines, cLineIndex + 1, nextMLineIndex, 'b=AS'); if (bLineIndex) { sdpLines.splice(bLineIndex, 1); } // Create the b (bandwidth) sdp line. var bwLine = 'b=AS:' + bitrate; // As per RFC 4566, the b line should follow after c-line. sdpLines.splice(cLineIndex + 1, 0, bwLine); sdp = sdpLines.join('\r\n'); return sdp; } // Add an a=fmtp: x-google-min-bitrate=kbps line, if videoSendInitialBitrate // is specified. We'll also add a x-google-min-bitrate value, since the max // must be >= the min. function maybeSetVideoSendInitialBitRate(sdp, params) { var initialBitrate = parseInt(params.videoSendInitialBitrate); if (!initialBitrate) { return sdp; } // Validate the initial bitrate value. var maxBitrate = parseInt(initialBitrate); var bitrate = parseInt(params.videoSendBitrate); if (bitrate) { if (initialBitrate > bitrate) { trace('Clamping initial bitrate to max bitrate of ' + bitrate + ' kbps.'); initialBitrate = bitrate; params.videoSendInitialBitrate = initialBitrate; } maxBitrate = bitrate; } var sdpLines = sdp.split('\r\n'); // Search for m line. var mLineIndex = findLine(sdpLines, 'm=', 'video'); if (mLineIndex === null) { trace('Failed to find video m-line'); return sdp; } // Figure out the first codec payload type on the m=video SDP line. var videoMLine = sdpLines[mLineIndex]; var pattern = new RegExp('m=video\\s\\d+\\s[A-Z/]+\\s'); var sendPayloadType = videoMLine.split(pattern)[1].split(' ')[0]; var fmtpLine = sdpLines[findLine(sdpLines, 'a=rtpmap', sendPayloadType)]; var codecName = fmtpLine.split('a=rtpmap:' + sendPayloadType)[1].split('/')[0]; // Use codec from params if specified via URL param, otherwise use from SDP. var codec = params.videoSendCodec || codecName; sdp = setCodecParam(sdp, codec, 'x-google-min-bitrate', params.videoSendInitialBitrate.toString()); sdp = setCodecParam(sdp, codec, 'x-google-max-bitrate', maxBitrate.toString()); return sdp; } function removePayloadTypeFromMline(mLine, payloadType) { mLine = mLine.split(' '); for (var i = 0; i < mLine.length; ++i) { if (mLine[i] === payloadType.toString()) { mLine.splice(i, 1); } } return mLine.join(' '); } function removeCodecByName(sdpLines, codec) { var index = findLine(sdpLines, 'a=rtpmap', codec); if (index === null) { return sdpLines; } var payloadType = getCodecPayloadTypeFromLine(sdpLines[index]); sdpLines.splice(index, 1); // Search for the video m= line and remove the codec. var mLineIndex = findLine(sdpLines, 'm=', 'video'); if (mLineIndex === null) { return sdpLines; } sdpLines[mLineIndex] = removePayloadTypeFromMline(sdpLines[mLineIndex], payloadType); return sdpLines; } function removeCodecByPayloadType(sdpLines, payloadType) { var index = findLine(sdpLines, 'a=rtpmap', payloadType.toString()); if (index === null) { return sdpLines; } sdpLines.splice(index, 1); // Search for the video m= line and remove the codec. var mLineIndex = findLine(sdpLines, 'm=', 'video'); if (mLineIndex === null) { return sdpLines; } sdpLines[mLineIndex] = removePayloadTypeFromMline(sdpLines[mLineIndex], payloadType); return sdpLines; } function maybeRemoveVideoFec(sdp, params) { if (params.videoFec !== 'false') { return sdp; } var sdpLines = sdp.split('\r\n'); var index = findLine(sdpLines, 'a=rtpmap', 'red'); if (index === null) { return sdp; } var redPayloadType = getCodecPayloadTypeFromLine(sdpLines[index]); sdpLines = removeCodecByPayloadType(sdpLines, redPayloadType); sdpLines = removeCodecByName(sdpLines, 'ulpfec'); // Remove fmtp lines associated with red codec. index = findLine(sdpLines, 'a=fmtp', redPayloadType.toString()); if (index === null) { return sdp; } var fmtpLine = parseFmtpLine(sdpLines[index]); var rtxPayloadType = fmtpLine.pt; if (rtxPayloadType === null) { return sdp; } sdpLines.splice(index, 1); sdpLines = removeCodecByPayloadType(sdpLines, rtxPayloadType); return sdpLines.join('\r\n'); } // Promotes |audioSendCodec| to be the first in the m=audio line, if set. function maybePreferAudioSendCodec(sdp, params) { return maybePreferCodec(sdp, 'audio', 'send', params.audioSendCodec); } // Promotes |audioRecvCodec| to be the first in the m=audio line, if set. function maybePreferAudioReceiveCodec(sdp, params) { return maybePreferCodec(sdp, 'audio', 'receive', params.audioRecvCodec); } // Promotes |videoSendCodec| to be the first in the m=audio line, if set. function maybePreferVideoSendCodec(sdp, params) { return maybePreferCodec(sdp, 'video', 'send', params.videoSendCodec); } // Promotes |videoRecvCodec| to be the first in the m=audio line, if set. function maybePreferVideoReceiveCodec(sdp, params) { return maybePreferCodec(sdp, 'video', 'receive', params.videoRecvCodec); } // Sets |codec| as the default |type| codec if it's present. // The format of |codec| is 'NAME/RATE', e.g. 'opus/48000'. function maybePreferCodec(sdp, type, dir, codec) { var str = type + ' ' + dir + ' codec'; if (!codec) { trace('No preference on ' + str + '.'); return sdp; } trace('Prefer ' + str + ': ' + codec); var sdpLines = sdp.split('\r\n'); // Search for m line. var mLineIndex = findLine(sdpLines, 'm=', type); if (mLineIndex === null) { return sdp; } // If the codec is available, set it as the default in m line. var payload = null; // Iterate through rtpmap enumerations to find all matching codec entries for (var i = sdpLines.length-1; i >= 0 ; --i) { // Finds first match in rtpmap var index = findLineInRange(sdpLines, i, 0, 'a=rtpmap', codec, "desc"); if (index !== null) { // Skip all of the entries between i and index match i = index; payload = getCodecPayloadTypeFromLine(sdpLines[index]); if (payload) { // Move codec to top sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], payload); } } else { // No match means we can break the loop break; } } sdp = sdpLines.join('\r\n'); return sdp; } // Set fmtp param to specific codec in SDP. If param does not exists, add it. function setCodecParam(sdp, codec, param, value) { var sdpLines = sdp.split('\r\n'); var fmtpLineIndex = findFmtpLine(sdpLines, codec); var fmtpObj = {}; if (fmtpLineIndex === null) { var index = findLine(sdpLines, 'a=rtpmap', codec); if (index === null) { return sdp; } var payload = getCodecPayloadTypeFromLine(sdpLines[index]); fmtpObj.pt = payload.toString(); fmtpObj.params = {}; fmtpObj.params[param] = value; sdpLines.splice(index + 1, 0, writeFmtpLine(fmtpObj)); } else { fmtpObj = parseFmtpLine(sdpLines[fmtpLineIndex]); fmtpObj.params[param] = value; sdpLines[fmtpLineIndex] = writeFmtpLine(fmtpObj); } sdp = sdpLines.join('\r\n'); return sdp; } // Remove fmtp param if it exists. function removeCodecParam(sdp, codec, param) { var sdpLines = sdp.split('\r\n'); var fmtpLineIndex = findFmtpLine(sdpLines, codec); if (fmtpLineIndex === null) { return sdp; } var map = parseFmtpLine(sdpLines[fmtpLineIndex]); delete map.params[param]; var newLine = writeFmtpLine(map); if (newLine === null) { sdpLines.splice(fmtpLineIndex, 1); } else { sdpLines[fmtpLineIndex] = newLine; } sdp = sdpLines.join('\r\n'); return sdp; } // Split an fmtp line into an object including 'pt' and 'params'. function parseFmtpLine(fmtpLine) { var fmtpObj = {}; var spacePos = fmtpLine.indexOf(' '); var keyValues = fmtpLine.substring(spacePos + 1).split(';'); var pattern = new RegExp('a=fmtp:(\\d+)'); var result = fmtpLine.match(pattern); if (result && result.length === 2) { fmtpObj.pt = result[1]; } else { return null; } var params = {}; for (var i = 0; i < keyValues.length; ++i) { var pair = keyValues[i].split('='); if (pair.length === 2) { params[pair[0]] = pair[1]; } } fmtpObj.params = params; return fmtpObj; } // Generate an fmtp line from an object including 'pt' and 'params'. function writeFmtpLine(fmtpObj) { if (!fmtpObj.hasOwnProperty('pt') || !fmtpObj.hasOwnProperty('params')) { return null; } var pt = fmtpObj.pt; var params = fmtpObj.params; var keyValues = []; var i = 0; for (var key in params) { keyValues[i] = key + '=' + params[key]; ++i; } if (i === 0) { return null; } return 'a=fmtp:' + pt.toString() + ' ' + keyValues.join(';'); } // Find fmtp attribute for |codec| in |sdpLines|. function findFmtpLine(sdpLines, codec) { // Find payload of codec. var payload = getCodecPayloadType(sdpLines, codec); // Find the payload in fmtp line. return payload ? findLine(sdpLines, 'a=fmtp:' + payload.toString()) : null; } // Find the line in sdpLines that starts with |prefix|, and, if specified, // contains |substr| (case-insensitive search). function findLine(sdpLines, prefix, substr) { return findLineInRange(sdpLines, 0, -1, prefix, substr); } // Find the line in sdpLines[startLine...endLine - 1] that starts with |prefix| // and, if specified, contains |substr| (case-insensitive search). function findLineInRange( sdpLines, startLine, endLine, prefix, substr, direction ) { if (direction === undefined) { direction = 'asc'; } direction = direction || 'asc'; if (direction === 'asc') { // Search beginning to end var realEndLine = endLine !== -1 ? endLine : sdpLines.length; for (var i = startLine; i < realEndLine; ++i) { if (sdpLines[i].indexOf(prefix) === 0) { if (!substr || sdpLines[i].toLowerCase().indexOf(substr.toLowerCase()) !== -1) { return i; } } } } else { // Search end to beginning var realStartLine = startLine !== -1 ? startLine : sdpLines.length-1; for (var j = realStartLine; j >= 0; --j) { if (sdpLines[j].indexOf(prefix) === 0) { if (!substr || sdpLines[j].toLowerCase().indexOf(substr.toLowerCase()) !== -1) { return j; } } } } return null; } // Gets the codec payload type from sdp lines. function getCodecPayloadType(sdpLines, codec) { var index = findLine(sdpLines, 'a=rtpmap', codec); return index ? getCodecPayloadTypeFromLine(sdpLines[index]) : null; } // Gets the codec payload type from an a=rtpmap:X line. function getCodecPayloadTypeFromLine(sdpLine) { var pattern = new RegExp('a=rtpmap:(\\d+) [a-zA-Z0-9-]+\\/\\d+'); var result = sdpLine.match(pattern); return (result && result.length === 2) ? result[1] : null; } // Returns a new m= line with the specified codec as the first one. function setDefaultCodec(mLine, payload) { var elements = mLine.split(' '); // Just copy the first three parameters; codec order starts on fourth. var newLine = elements.slice(0, 3); // Put target payload first and copy in the rest. newLine.push(payload); for (var i = 3; i < elements.length; i++) { if (elements[i] !== payload) { newLine.push(elements[i]); } } return newLine.join(' '); } function trace(text) { // This function is used for logging. if (text[text.length - 1] === '\n') { text = text.substring(0, text.length - 1); } if (window.performance) { var now = (window.performance.now() / 1000).toFixed(3); console.log(now + ': ' + text); } else { console.log(text); } } })(module);