diff --git a/testenv/js/adapter.js b/testenv/js/adapter.js deleted file mode 100644 index 51d0f276..00000000 --- a/testenv/js/adapter.js +++ /dev/null @@ -1,3364 +0,0 @@ -(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.adapter = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i 0 && arguments[0] !== undefined ? arguments[0] : {}, - window = _ref.window; - var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { - shimChrome: true, - shimFirefox: true, - shimSafari: true - }; - // Utils. - var logging = utils.log; - var browserDetails = utils.detectBrowser(window); - var adapter = { - browserDetails: browserDetails, - commonShim: commonShim, - extractVersion: utils.extractVersion, - disableLog: utils.disableLog, - disableWarnings: utils.disableWarnings, - // Expose sdp as a convenience. For production apps include directly. - sdp: sdp - }; - - // Shim browser if found. - switch (browserDetails.browser) { - case 'chrome': - if (!chromeShim || !chromeShim.shimPeerConnection || !options.shimChrome) { - logging('Chrome shim is not included in this adapter release.'); - return adapter; - } - if (browserDetails.version === null) { - logging('Chrome shim can not determine version, not shimming.'); - return adapter; - } - logging('adapter.js shimming chrome.'); - // Export to the adapter global object visible in the browser. - adapter.browserShim = chromeShim; - - // Must be called before shimPeerConnection. - commonShim.shimAddIceCandidateNullOrEmpty(window, browserDetails); - commonShim.shimParameterlessSetLocalDescription(window, browserDetails); - chromeShim.shimGetUserMedia(window, browserDetails); - chromeShim.shimMediaStream(window, browserDetails); - chromeShim.shimPeerConnection(window, browserDetails); - chromeShim.shimOnTrack(window, browserDetails); - chromeShim.shimAddTrackRemoveTrack(window, browserDetails); - chromeShim.shimGetSendersWithDtmf(window, browserDetails); - chromeShim.shimGetStats(window, browserDetails); - chromeShim.shimSenderReceiverGetStats(window, browserDetails); - chromeShim.fixNegotiationNeeded(window, browserDetails); - commonShim.shimRTCIceCandidate(window, browserDetails); - commonShim.shimRTCIceCandidateRelayProtocol(window, browserDetails); - commonShim.shimConnectionState(window, browserDetails); - commonShim.shimMaxMessageSize(window, browserDetails); - commonShim.shimSendThrowTypeError(window, browserDetails); - commonShim.removeExtmapAllowMixed(window, browserDetails); - break; - case 'firefox': - if (!firefoxShim || !firefoxShim.shimPeerConnection || !options.shimFirefox) { - logging('Firefox shim is not included in this adapter release.'); - return adapter; - } - logging('adapter.js shimming firefox.'); - // Export to the adapter global object visible in the browser. - adapter.browserShim = firefoxShim; - - // Must be called before shimPeerConnection. - commonShim.shimAddIceCandidateNullOrEmpty(window, browserDetails); - commonShim.shimParameterlessSetLocalDescription(window, browserDetails); - firefoxShim.shimGetUserMedia(window, browserDetails); - firefoxShim.shimPeerConnection(window, browserDetails); - firefoxShim.shimOnTrack(window, browserDetails); - firefoxShim.shimRemoveStream(window, browserDetails); - firefoxShim.shimSenderGetStats(window, browserDetails); - firefoxShim.shimReceiverGetStats(window, browserDetails); - firefoxShim.shimRTCDataChannel(window, browserDetails); - firefoxShim.shimAddTransceiver(window, browserDetails); - firefoxShim.shimGetParameters(window, browserDetails); - firefoxShim.shimCreateOffer(window, browserDetails); - firefoxShim.shimCreateAnswer(window, browserDetails); - commonShim.shimRTCIceCandidate(window, browserDetails); - commonShim.shimConnectionState(window, browserDetails); - commonShim.shimMaxMessageSize(window, browserDetails); - commonShim.shimSendThrowTypeError(window, browserDetails); - break; - case 'safari': - if (!safariShim || !options.shimSafari) { - logging('Safari shim is not included in this adapter release.'); - return adapter; - } - logging('adapter.js shimming safari.'); - // Export to the adapter global object visible in the browser. - adapter.browserShim = safariShim; - - // Must be called before shimCallbackAPI. - commonShim.shimAddIceCandidateNullOrEmpty(window, browserDetails); - commonShim.shimParameterlessSetLocalDescription(window, browserDetails); - safariShim.shimRTCIceServerUrls(window, browserDetails); - safariShim.shimCreateOfferLegacy(window, browserDetails); - safariShim.shimCallbacksAPI(window, browserDetails); - safariShim.shimLocalStreamsAPI(window, browserDetails); - safariShim.shimRemoteStreamsAPI(window, browserDetails); - safariShim.shimTrackEventTransceiver(window, browserDetails); - safariShim.shimGetUserMedia(window, browserDetails); - safariShim.shimAudioContext(window, browserDetails); - commonShim.shimRTCIceCandidate(window, browserDetails); - commonShim.shimRTCIceCandidateRelayProtocol(window, browserDetails); - commonShim.shimMaxMessageSize(window, browserDetails); - commonShim.shimSendThrowTypeError(window, browserDetails); - commonShim.removeExtmapAllowMixed(window, browserDetails); - break; - default: - logging('Unsupported browser!'); - break; - } - return adapter; -} - -},{"./chrome/chrome_shim":3,"./common_shim":6,"./firefox/firefox_shim":7,"./safari/safari_shim":10,"./utils":11,"sdp":12}],3:[function(require,module,exports){ -/* - * Copyright (c) 2016 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. - */ -/* eslint-env node */ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.fixNegotiationNeeded = fixNegotiationNeeded; -exports.shimAddTrackRemoveTrack = shimAddTrackRemoveTrack; -exports.shimAddTrackRemoveTrackWithNative = shimAddTrackRemoveTrackWithNative; -Object.defineProperty(exports, "shimGetDisplayMedia", { - enumerable: true, - get: function get() { - return _getdisplaymedia.shimGetDisplayMedia; - } -}); -exports.shimGetSendersWithDtmf = shimGetSendersWithDtmf; -exports.shimGetStats = shimGetStats; -Object.defineProperty(exports, "shimGetUserMedia", { - enumerable: true, - get: function get() { - return _getusermedia.shimGetUserMedia; - } -}); -exports.shimMediaStream = shimMediaStream; -exports.shimOnTrack = shimOnTrack; -exports.shimPeerConnection = shimPeerConnection; -exports.shimSenderReceiverGetStats = shimSenderReceiverGetStats; -var utils = _interopRequireWildcard(require("../utils.js")); -var _getusermedia = require("./getusermedia"); -var _getdisplaymedia = require("./getdisplaymedia"); -function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } -function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } -function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } -function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); } -function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } -function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } -function shimMediaStream(window) { - window.MediaStream = window.MediaStream || window.webkitMediaStream; -} -function shimOnTrack(window) { - if (_typeof(window) === 'object' && window.RTCPeerConnection && !('ontrack' in window.RTCPeerConnection.prototype)) { - Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', { - get: function get() { - return this._ontrack; - }, - set: function set(f) { - if (this._ontrack) { - this.removeEventListener('track', this._ontrack); - } - this.addEventListener('track', this._ontrack = f); - }, - enumerable: true, - configurable: true - }); - var origSetRemoteDescription = window.RTCPeerConnection.prototype.setRemoteDescription; - window.RTCPeerConnection.prototype.setRemoteDescription = function setRemoteDescription() { - var _this = this; - if (!this._ontrackpoly) { - this._ontrackpoly = function (e) { - // onaddstream does not fire when a track is added to an existing - // stream. But stream.onaddtrack is implemented so we use that. - e.stream.addEventListener('addtrack', function (te) { - var receiver; - if (window.RTCPeerConnection.prototype.getReceivers) { - receiver = _this.getReceivers().find(function (r) { - return r.track && r.track.id === te.track.id; - }); - } else { - receiver = { - track: te.track - }; - } - var event = new Event('track'); - event.track = te.track; - event.receiver = receiver; - event.transceiver = { - receiver: receiver - }; - event.streams = [e.stream]; - _this.dispatchEvent(event); - }); - e.stream.getTracks().forEach(function (track) { - var receiver; - if (window.RTCPeerConnection.prototype.getReceivers) { - receiver = _this.getReceivers().find(function (r) { - return r.track && r.track.id === track.id; - }); - } else { - receiver = { - track: track - }; - } - var event = new Event('track'); - event.track = track; - event.receiver = receiver; - event.transceiver = { - receiver: receiver - }; - event.streams = [e.stream]; - _this.dispatchEvent(event); - }); - }; - this.addEventListener('addstream', this._ontrackpoly); - } - return origSetRemoteDescription.apply(this, arguments); - }; - } else { - // even if RTCRtpTransceiver is in window, it is only used and - // emitted in unified-plan. Unfortunately this means we need - // to unconditionally wrap the event. - utils.wrapPeerConnectionEvent(window, 'track', function (e) { - if (!e.transceiver) { - Object.defineProperty(e, 'transceiver', { - value: { - receiver: e.receiver - } - }); - } - return e; - }); - } -} -function shimGetSendersWithDtmf(window) { - // Overrides addTrack/removeTrack, depends on shimAddTrackRemoveTrack. - if (_typeof(window) === 'object' && window.RTCPeerConnection && !('getSenders' in window.RTCPeerConnection.prototype) && 'createDTMFSender' in window.RTCPeerConnection.prototype) { - var shimSenderWithDtmf = function shimSenderWithDtmf(pc, track) { - return { - track: track, - get dtmf() { - if (this._dtmf === undefined) { - if (track.kind === 'audio') { - this._dtmf = pc.createDTMFSender(track); - } else { - this._dtmf = null; - } - } - return this._dtmf; - }, - _pc: pc - }; - }; - - // augment addTrack when getSenders is not available. - if (!window.RTCPeerConnection.prototype.getSenders) { - window.RTCPeerConnection.prototype.getSenders = function getSenders() { - this._senders = this._senders || []; - return this._senders.slice(); // return a copy of the internal state. - }; - - var origAddTrack = window.RTCPeerConnection.prototype.addTrack; - window.RTCPeerConnection.prototype.addTrack = function addTrack(track, stream) { - var sender = origAddTrack.apply(this, arguments); - if (!sender) { - sender = shimSenderWithDtmf(this, track); - this._senders.push(sender); - } - return sender; - }; - var origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack; - window.RTCPeerConnection.prototype.removeTrack = function removeTrack(sender) { - origRemoveTrack.apply(this, arguments); - var idx = this._senders.indexOf(sender); - if (idx !== -1) { - this._senders.splice(idx, 1); - } - }; - } - var origAddStream = window.RTCPeerConnection.prototype.addStream; - window.RTCPeerConnection.prototype.addStream = function addStream(stream) { - var _this2 = this; - this._senders = this._senders || []; - origAddStream.apply(this, [stream]); - stream.getTracks().forEach(function (track) { - _this2._senders.push(shimSenderWithDtmf(_this2, track)); - }); - }; - var origRemoveStream = window.RTCPeerConnection.prototype.removeStream; - window.RTCPeerConnection.prototype.removeStream = function removeStream(stream) { - var _this3 = this; - this._senders = this._senders || []; - origRemoveStream.apply(this, [stream]); - stream.getTracks().forEach(function (track) { - var sender = _this3._senders.find(function (s) { - return s.track === track; - }); - if (sender) { - // remove sender - _this3._senders.splice(_this3._senders.indexOf(sender), 1); - } - }); - }; - } else if (_typeof(window) === 'object' && window.RTCPeerConnection && 'getSenders' in window.RTCPeerConnection.prototype && 'createDTMFSender' in window.RTCPeerConnection.prototype && window.RTCRtpSender && !('dtmf' in window.RTCRtpSender.prototype)) { - var origGetSenders = window.RTCPeerConnection.prototype.getSenders; - window.RTCPeerConnection.prototype.getSenders = function getSenders() { - var _this4 = this; - var senders = origGetSenders.apply(this, []); - senders.forEach(function (sender) { - return sender._pc = _this4; - }); - return senders; - }; - Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', { - get: function get() { - if (this._dtmf === undefined) { - if (this.track.kind === 'audio') { - this._dtmf = this._pc.createDTMFSender(this.track); - } else { - this._dtmf = null; - } - } - return this._dtmf; - } - }); - } -} -function shimGetStats(window) { - if (!window.RTCPeerConnection) { - return; - } - var origGetStats = window.RTCPeerConnection.prototype.getStats; - window.RTCPeerConnection.prototype.getStats = function getStats() { - var _this5 = this; - var _arguments = Array.prototype.slice.call(arguments), - selector = _arguments[0], - onSucc = _arguments[1], - onErr = _arguments[2]; - - // If selector is a function then we are in the old style stats so just - // pass back the original getStats format to avoid breaking old users. - if (arguments.length > 0 && typeof selector === 'function') { - return origGetStats.apply(this, arguments); - } - - // When spec-style getStats is supported, return those when called with - // either no arguments or the selector argument is null. - if (origGetStats.length === 0 && (arguments.length === 0 || typeof selector !== 'function')) { - return origGetStats.apply(this, []); - } - var fixChromeStats_ = function fixChromeStats_(response) { - var standardReport = {}; - var reports = response.result(); - reports.forEach(function (report) { - var standardStats = { - id: report.id, - timestamp: report.timestamp, - type: { - localcandidate: 'local-candidate', - remotecandidate: 'remote-candidate' - }[report.type] || report.type - }; - report.names().forEach(function (name) { - standardStats[name] = report.stat(name); - }); - standardReport[standardStats.id] = standardStats; - }); - return standardReport; - }; - - // shim getStats with maplike support - var makeMapStats = function makeMapStats(stats) { - return new Map(Object.keys(stats).map(function (key) { - return [key, stats[key]]; - })); - }; - if (arguments.length >= 2) { - var successCallbackWrapper_ = function successCallbackWrapper_(response) { - onSucc(makeMapStats(fixChromeStats_(response))); - }; - return origGetStats.apply(this, [successCallbackWrapper_, selector]); - } - - // promise-support - return new Promise(function (resolve, reject) { - origGetStats.apply(_this5, [function (response) { - resolve(makeMapStats(fixChromeStats_(response))); - }, reject]); - }).then(onSucc, onErr); - }; -} -function shimSenderReceiverGetStats(window) { - if (!(_typeof(window) === 'object' && window.RTCPeerConnection && window.RTCRtpSender && window.RTCRtpReceiver)) { - return; - } - - // shim sender stats. - if (!('getStats' in window.RTCRtpSender.prototype)) { - var origGetSenders = window.RTCPeerConnection.prototype.getSenders; - if (origGetSenders) { - window.RTCPeerConnection.prototype.getSenders = function getSenders() { - var _this6 = this; - var senders = origGetSenders.apply(this, []); - senders.forEach(function (sender) { - return sender._pc = _this6; - }); - return senders; - }; - } - var origAddTrack = window.RTCPeerConnection.prototype.addTrack; - if (origAddTrack) { - window.RTCPeerConnection.prototype.addTrack = function addTrack() { - var sender = origAddTrack.apply(this, arguments); - sender._pc = this; - return sender; - }; - } - window.RTCRtpSender.prototype.getStats = function getStats() { - var sender = this; - return this._pc.getStats().then(function (result) { - return ( - /* Note: this will include stats of all senders that - * send a track with the same id as sender.track as - * it is not possible to identify the RTCRtpSender. - */ - utils.filterStats(result, sender.track, true) - ); - }); - }; - } - - // shim receiver stats. - if (!('getStats' in window.RTCRtpReceiver.prototype)) { - var origGetReceivers = window.RTCPeerConnection.prototype.getReceivers; - if (origGetReceivers) { - window.RTCPeerConnection.prototype.getReceivers = function getReceivers() { - var _this7 = this; - var receivers = origGetReceivers.apply(this, []); - receivers.forEach(function (receiver) { - return receiver._pc = _this7; - }); - return receivers; - }; - } - utils.wrapPeerConnectionEvent(window, 'track', function (e) { - e.receiver._pc = e.srcElement; - return e; - }); - window.RTCRtpReceiver.prototype.getStats = function getStats() { - var receiver = this; - return this._pc.getStats().then(function (result) { - return utils.filterStats(result, receiver.track, false); - }); - }; - } - if (!('getStats' in window.RTCRtpSender.prototype && 'getStats' in window.RTCRtpReceiver.prototype)) { - return; - } - - // shim RTCPeerConnection.getStats(track). - var origGetStats = window.RTCPeerConnection.prototype.getStats; - window.RTCPeerConnection.prototype.getStats = function getStats() { - if (arguments.length > 0 && arguments[0] instanceof window.MediaStreamTrack) { - var track = arguments[0]; - var sender; - var receiver; - var err; - this.getSenders().forEach(function (s) { - if (s.track === track) { - if (sender) { - err = true; - } else { - sender = s; - } - } - }); - this.getReceivers().forEach(function (r) { - if (r.track === track) { - if (receiver) { - err = true; - } else { - receiver = r; - } - } - return r.track === track; - }); - if (err || sender && receiver) { - return Promise.reject(new DOMException('There are more than one sender or receiver for the track.', 'InvalidAccessError')); - } else if (sender) { - return sender.getStats(); - } else if (receiver) { - return receiver.getStats(); - } - return Promise.reject(new DOMException('There is no sender or receiver for the track.', 'InvalidAccessError')); - } - return origGetStats.apply(this, arguments); - }; -} -function shimAddTrackRemoveTrackWithNative(window) { - // shim addTrack/removeTrack with native variants in order to make - // the interactions with legacy getLocalStreams behave as in other browsers. - // Keeps a mapping stream.id => [stream, rtpsenders...] - window.RTCPeerConnection.prototype.getLocalStreams = function getLocalStreams() { - var _this8 = this; - this._shimmedLocalStreams = this._shimmedLocalStreams || {}; - return Object.keys(this._shimmedLocalStreams).map(function (streamId) { - return _this8._shimmedLocalStreams[streamId][0]; - }); - }; - var origAddTrack = window.RTCPeerConnection.prototype.addTrack; - window.RTCPeerConnection.prototype.addTrack = function addTrack(track, stream) { - if (!stream) { - return origAddTrack.apply(this, arguments); - } - this._shimmedLocalStreams = this._shimmedLocalStreams || {}; - var sender = origAddTrack.apply(this, arguments); - if (!this._shimmedLocalStreams[stream.id]) { - this._shimmedLocalStreams[stream.id] = [stream, sender]; - } else if (this._shimmedLocalStreams[stream.id].indexOf(sender) === -1) { - this._shimmedLocalStreams[stream.id].push(sender); - } - return sender; - }; - var origAddStream = window.RTCPeerConnection.prototype.addStream; - window.RTCPeerConnection.prototype.addStream = function addStream(stream) { - var _this9 = this; - this._shimmedLocalStreams = this._shimmedLocalStreams || {}; - stream.getTracks().forEach(function (track) { - var alreadyExists = _this9.getSenders().find(function (s) { - return s.track === track; - }); - if (alreadyExists) { - throw new DOMException('Track already exists.', 'InvalidAccessError'); - } - }); - var existingSenders = this.getSenders(); - origAddStream.apply(this, arguments); - var newSenders = this.getSenders().filter(function (newSender) { - return existingSenders.indexOf(newSender) === -1; - }); - this._shimmedLocalStreams[stream.id] = [stream].concat(newSenders); - }; - var origRemoveStream = window.RTCPeerConnection.prototype.removeStream; - window.RTCPeerConnection.prototype.removeStream = function removeStream(stream) { - this._shimmedLocalStreams = this._shimmedLocalStreams || {}; - delete this._shimmedLocalStreams[stream.id]; - return origRemoveStream.apply(this, arguments); - }; - var origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack; - window.RTCPeerConnection.prototype.removeTrack = function removeTrack(sender) { - var _this10 = this; - this._shimmedLocalStreams = this._shimmedLocalStreams || {}; - if (sender) { - Object.keys(this._shimmedLocalStreams).forEach(function (streamId) { - var idx = _this10._shimmedLocalStreams[streamId].indexOf(sender); - if (idx !== -1) { - _this10._shimmedLocalStreams[streamId].splice(idx, 1); - } - if (_this10._shimmedLocalStreams[streamId].length === 1) { - delete _this10._shimmedLocalStreams[streamId]; - } - }); - } - return origRemoveTrack.apply(this, arguments); - }; -} -function shimAddTrackRemoveTrack(window, browserDetails) { - if (!window.RTCPeerConnection) { - return; - } - // shim addTrack and removeTrack. - if (window.RTCPeerConnection.prototype.addTrack && browserDetails.version >= 65) { - return shimAddTrackRemoveTrackWithNative(window); - } - - // also shim pc.getLocalStreams when addTrack is shimmed - // to return the original streams. - var origGetLocalStreams = window.RTCPeerConnection.prototype.getLocalStreams; - window.RTCPeerConnection.prototype.getLocalStreams = function getLocalStreams() { - var _this11 = this; - var nativeStreams = origGetLocalStreams.apply(this); - this._reverseStreams = this._reverseStreams || {}; - return nativeStreams.map(function (stream) { - return _this11._reverseStreams[stream.id]; - }); - }; - var origAddStream = window.RTCPeerConnection.prototype.addStream; - window.RTCPeerConnection.prototype.addStream = function addStream(stream) { - var _this12 = this; - this._streams = this._streams || {}; - this._reverseStreams = this._reverseStreams || {}; - stream.getTracks().forEach(function (track) { - var alreadyExists = _this12.getSenders().find(function (s) { - return s.track === track; - }); - if (alreadyExists) { - throw new DOMException('Track already exists.', 'InvalidAccessError'); - } - }); - // Add identity mapping for consistency with addTrack. - // Unless this is being used with a stream from addTrack. - if (!this._reverseStreams[stream.id]) { - var newStream = new window.MediaStream(stream.getTracks()); - this._streams[stream.id] = newStream; - this._reverseStreams[newStream.id] = stream; - stream = newStream; - } - origAddStream.apply(this, [stream]); - }; - var origRemoveStream = window.RTCPeerConnection.prototype.removeStream; - window.RTCPeerConnection.prototype.removeStream = function removeStream(stream) { - this._streams = this._streams || {}; - this._reverseStreams = this._reverseStreams || {}; - origRemoveStream.apply(this, [this._streams[stream.id] || stream]); - delete this._reverseStreams[this._streams[stream.id] ? this._streams[stream.id].id : stream.id]; - delete this._streams[stream.id]; - }; - window.RTCPeerConnection.prototype.addTrack = function addTrack(track, stream) { - var _this13 = this; - if (this.signalingState === 'closed') { - throw new DOMException('The RTCPeerConnection\'s signalingState is \'closed\'.', 'InvalidStateError'); - } - var streams = [].slice.call(arguments, 1); - if (streams.length !== 1 || !streams[0].getTracks().find(function (t) { - return t === track; - })) { - // this is not fully correct but all we can manage without - // [[associated MediaStreams]] internal slot. - throw new DOMException('The adapter.js addTrack polyfill only supports a single ' + ' stream which is associated with the specified track.', 'NotSupportedError'); - } - var alreadyExists = this.getSenders().find(function (s) { - return s.track === track; - }); - if (alreadyExists) { - throw new DOMException('Track already exists.', 'InvalidAccessError'); - } - this._streams = this._streams || {}; - this._reverseStreams = this._reverseStreams || {}; - var oldStream = this._streams[stream.id]; - if (oldStream) { - // this is using odd Chrome behaviour, use with caution: - // https://bugs.chromium.org/p/webrtc/issues/detail?id=7815 - // Note: we rely on the high-level addTrack/dtmf shim to - // create the sender with a dtmf sender. - oldStream.addTrack(track); - - // Trigger ONN async. - Promise.resolve().then(function () { - _this13.dispatchEvent(new Event('negotiationneeded')); - }); - } else { - var newStream = new window.MediaStream([track]); - this._streams[stream.id] = newStream; - this._reverseStreams[newStream.id] = stream; - this.addStream(newStream); - } - return this.getSenders().find(function (s) { - return s.track === track; - }); - }; - - // replace the internal stream id with the external one and - // vice versa. - function replaceInternalStreamId(pc, description) { - var sdp = description.sdp; - Object.keys(pc._reverseStreams || []).forEach(function (internalId) { - var externalStream = pc._reverseStreams[internalId]; - var internalStream = pc._streams[externalStream.id]; - sdp = sdp.replace(new RegExp(internalStream.id, 'g'), externalStream.id); - }); - return new RTCSessionDescription({ - type: description.type, - sdp: sdp - }); - } - function replaceExternalStreamId(pc, description) { - var sdp = description.sdp; - Object.keys(pc._reverseStreams || []).forEach(function (internalId) { - var externalStream = pc._reverseStreams[internalId]; - var internalStream = pc._streams[externalStream.id]; - sdp = sdp.replace(new RegExp(externalStream.id, 'g'), internalStream.id); - }); - return new RTCSessionDescription({ - type: description.type, - sdp: sdp - }); - } - ['createOffer', 'createAnswer'].forEach(function (method) { - var nativeMethod = window.RTCPeerConnection.prototype[method]; - var methodObj = _defineProperty({}, method, function () { - var _this14 = this; - var args = arguments; - var isLegacyCall = arguments.length && typeof arguments[0] === 'function'; - if (isLegacyCall) { - return nativeMethod.apply(this, [function (description) { - var desc = replaceInternalStreamId(_this14, description); - args[0].apply(null, [desc]); - }, function (err) { - if (args[1]) { - args[1].apply(null, err); - } - }, arguments[2]]); - } - return nativeMethod.apply(this, arguments).then(function (description) { - return replaceInternalStreamId(_this14, description); - }); - }); - window.RTCPeerConnection.prototype[method] = methodObj[method]; - }); - var origSetLocalDescription = window.RTCPeerConnection.prototype.setLocalDescription; - window.RTCPeerConnection.prototype.setLocalDescription = function setLocalDescription() { - if (!arguments.length || !arguments[0].type) { - return origSetLocalDescription.apply(this, arguments); - } - arguments[0] = replaceExternalStreamId(this, arguments[0]); - return origSetLocalDescription.apply(this, arguments); - }; - - // TODO: mangle getStats: https://w3c.github.io/webrtc-stats/#dom-rtcmediastreamstats-streamidentifier - - var origLocalDescription = Object.getOwnPropertyDescriptor(window.RTCPeerConnection.prototype, 'localDescription'); - Object.defineProperty(window.RTCPeerConnection.prototype, 'localDescription', { - get: function get() { - var description = origLocalDescription.get.apply(this); - if (description.type === '') { - return description; - } - return replaceInternalStreamId(this, description); - } - }); - window.RTCPeerConnection.prototype.removeTrack = function removeTrack(sender) { - var _this15 = this; - if (this.signalingState === 'closed') { - throw new DOMException('The RTCPeerConnection\'s signalingState is \'closed\'.', 'InvalidStateError'); - } - // We can not yet check for sender instanceof RTCRtpSender - // since we shim RTPSender. So we check if sender._pc is set. - if (!sender._pc) { - throw new DOMException('Argument 1 of RTCPeerConnection.removeTrack ' + 'does not implement interface RTCRtpSender.', 'TypeError'); - } - var isLocal = sender._pc === this; - if (!isLocal) { - throw new DOMException('Sender was not created by this connection.', 'InvalidAccessError'); - } - - // Search for the native stream the senders track belongs to. - this._streams = this._streams || {}; - var stream; - Object.keys(this._streams).forEach(function (streamid) { - var hasTrack = _this15._streams[streamid].getTracks().find(function (track) { - return sender.track === track; - }); - if (hasTrack) { - stream = _this15._streams[streamid]; - } - }); - if (stream) { - if (stream.getTracks().length === 1) { - // if this is the last track of the stream, remove the stream. This - // takes care of any shimmed _senders. - this.removeStream(this._reverseStreams[stream.id]); - } else { - // relying on the same odd chrome behaviour as above. - stream.removeTrack(sender.track); - } - this.dispatchEvent(new Event('negotiationneeded')); - } - }; -} -function shimPeerConnection(window, browserDetails) { - if (!window.RTCPeerConnection && window.webkitRTCPeerConnection) { - // very basic support for old versions. - window.RTCPeerConnection = window.webkitRTCPeerConnection; - } - if (!window.RTCPeerConnection) { - return; - } - - // shim implicit creation of RTCSessionDescription/RTCIceCandidate - if (browserDetails.version < 53) { - ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'].forEach(function (method) { - var nativeMethod = window.RTCPeerConnection.prototype[method]; - var methodObj = _defineProperty({}, method, function () { - arguments[0] = new (method === 'addIceCandidate' ? window.RTCIceCandidate : window.RTCSessionDescription)(arguments[0]); - return nativeMethod.apply(this, arguments); - }); - window.RTCPeerConnection.prototype[method] = methodObj[method]; - }); - } -} - -// Attempt to fix ONN in plan-b mode. -function fixNegotiationNeeded(window, browserDetails) { - utils.wrapPeerConnectionEvent(window, 'negotiationneeded', function (e) { - var pc = e.target; - if (browserDetails.version < 72 || pc.getConfiguration && pc.getConfiguration().sdpSemantics === 'plan-b') { - if (pc.signalingState !== 'stable') { - return; - } - } - return e; - }); -} - -},{"../utils.js":11,"./getdisplaymedia":4,"./getusermedia":5}],4:[function(require,module,exports){ -/* - * Copyright (c) 2018 The adapter.js 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. - */ -/* eslint-env node */ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.shimGetDisplayMedia = shimGetDisplayMedia; -function shimGetDisplayMedia(window, getSourceId) { - if (window.navigator.mediaDevices && 'getDisplayMedia' in window.navigator.mediaDevices) { - return; - } - if (!window.navigator.mediaDevices) { - return; - } - // getSourceId is a function that returns a promise resolving with - // the sourceId of the screen/window/tab to be shared. - if (typeof getSourceId !== 'function') { - console.error('shimGetDisplayMedia: getSourceId argument is not ' + 'a function'); - return; - } - window.navigator.mediaDevices.getDisplayMedia = function getDisplayMedia(constraints) { - return getSourceId(constraints).then(function (sourceId) { - var widthSpecified = constraints.video && constraints.video.width; - var heightSpecified = constraints.video && constraints.video.height; - var frameRateSpecified = constraints.video && constraints.video.frameRate; - constraints.video = { - mandatory: { - chromeMediaSource: 'desktop', - chromeMediaSourceId: sourceId, - maxFrameRate: frameRateSpecified || 3 - } - }; - if (widthSpecified) { - constraints.video.mandatory.maxWidth = widthSpecified; - } - if (heightSpecified) { - constraints.video.mandatory.maxHeight = heightSpecified; - } - return window.navigator.mediaDevices.getUserMedia(constraints); - }); - }; -} - -},{}],5:[function(require,module,exports){ -/* - * Copyright (c) 2016 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. - */ -/* eslint-env node */ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.shimGetUserMedia = shimGetUserMedia; -var utils = _interopRequireWildcard(require("../utils.js")); -function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } -function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } -function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } -var logging = utils.log; -function shimGetUserMedia(window, browserDetails) { - var navigator = window && window.navigator; - if (!navigator.mediaDevices) { - return; - } - var constraintsToChrome_ = function constraintsToChrome_(c) { - if (_typeof(c) !== 'object' || c.mandatory || c.optional) { - return c; - } - var cc = {}; - Object.keys(c).forEach(function (key) { - if (key === 'require' || key === 'advanced' || key === 'mediaSource') { - return; - } - var r = _typeof(c[key]) === 'object' ? c[key] : { - ideal: c[key] - }; - if (r.exact !== undefined && typeof r.exact === 'number') { - r.min = r.max = r.exact; - } - var oldname_ = function oldname_(prefix, name) { - if (prefix) { - return prefix + name.charAt(0).toUpperCase() + name.slice(1); - } - return name === 'deviceId' ? 'sourceId' : name; - }; - if (r.ideal !== undefined) { - cc.optional = cc.optional || []; - var oc = {}; - if (typeof r.ideal === 'number') { - oc[oldname_('min', key)] = r.ideal; - cc.optional.push(oc); - oc = {}; - oc[oldname_('max', key)] = r.ideal; - cc.optional.push(oc); - } else { - oc[oldname_('', key)] = r.ideal; - cc.optional.push(oc); - } - } - if (r.exact !== undefined && typeof r.exact !== 'number') { - cc.mandatory = cc.mandatory || {}; - cc.mandatory[oldname_('', key)] = r.exact; - } else { - ['min', 'max'].forEach(function (mix) { - if (r[mix] !== undefined) { - cc.mandatory = cc.mandatory || {}; - cc.mandatory[oldname_(mix, key)] = r[mix]; - } - }); - } - }); - if (c.advanced) { - cc.optional = (cc.optional || []).concat(c.advanced); - } - return cc; - }; - var shimConstraints_ = function shimConstraints_(constraints, func) { - if (browserDetails.version >= 61) { - return func(constraints); - } - constraints = JSON.parse(JSON.stringify(constraints)); - if (constraints && _typeof(constraints.audio) === 'object') { - var remap = function remap(obj, a, b) { - if (a in obj && !(b in obj)) { - obj[b] = obj[a]; - delete obj[a]; - } - }; - constraints = JSON.parse(JSON.stringify(constraints)); - remap(constraints.audio, 'autoGainControl', 'googAutoGainControl'); - remap(constraints.audio, 'noiseSuppression', 'googNoiseSuppression'); - constraints.audio = constraintsToChrome_(constraints.audio); - } - if (constraints && _typeof(constraints.video) === 'object') { - // Shim facingMode for mobile & surface pro. - var face = constraints.video.facingMode; - face = face && (_typeof(face) === 'object' ? face : { - ideal: face - }); - var getSupportedFacingModeLies = browserDetails.version < 66; - if (face && (face.exact === 'user' || face.exact === 'environment' || face.ideal === 'user' || face.ideal === 'environment') && !(navigator.mediaDevices.getSupportedConstraints && navigator.mediaDevices.getSupportedConstraints().facingMode && !getSupportedFacingModeLies)) { - delete constraints.video.facingMode; - var matches; - if (face.exact === 'environment' || face.ideal === 'environment') { - matches = ['back', 'rear']; - } else if (face.exact === 'user' || face.ideal === 'user') { - matches = ['front']; - } - if (matches) { - // Look for matches in label, or use last cam for back (typical). - return navigator.mediaDevices.enumerateDevices().then(function (devices) { - devices = devices.filter(function (d) { - return d.kind === 'videoinput'; - }); - var dev = devices.find(function (d) { - return matches.some(function (match) { - return d.label.toLowerCase().includes(match); - }); - }); - if (!dev && devices.length && matches.includes('back')) { - dev = devices[devices.length - 1]; // more likely the back cam - } - - if (dev) { - constraints.video.deviceId = face.exact ? { - exact: dev.deviceId - } : { - ideal: dev.deviceId - }; - } - constraints.video = constraintsToChrome_(constraints.video); - logging('chrome: ' + JSON.stringify(constraints)); - return func(constraints); - }); - } - } - constraints.video = constraintsToChrome_(constraints.video); - } - logging('chrome: ' + JSON.stringify(constraints)); - return func(constraints); - }; - var shimError_ = function shimError_(e) { - if (browserDetails.version >= 64) { - return e; - } - return { - name: { - PermissionDeniedError: 'NotAllowedError', - PermissionDismissedError: 'NotAllowedError', - InvalidStateError: 'NotAllowedError', - DevicesNotFoundError: 'NotFoundError', - ConstraintNotSatisfiedError: 'OverconstrainedError', - TrackStartError: 'NotReadableError', - MediaDeviceFailedDueToShutdown: 'NotAllowedError', - MediaDeviceKillSwitchOn: 'NotAllowedError', - TabCaptureError: 'AbortError', - ScreenCaptureError: 'AbortError', - DeviceCaptureError: 'AbortError' - }[e.name] || e.name, - message: e.message, - constraint: e.constraint || e.constraintName, - toString: function toString() { - return this.name + (this.message && ': ') + this.message; - } - }; - }; - var getUserMedia_ = function getUserMedia_(constraints, onSuccess, onError) { - shimConstraints_(constraints, function (c) { - navigator.webkitGetUserMedia(c, onSuccess, function (e) { - if (onError) { - onError(shimError_(e)); - } - }); - }); - }; - navigator.getUserMedia = getUserMedia_.bind(navigator); - - // Even though Chrome 45 has navigator.mediaDevices and a getUserMedia - // function which returns a Promise, it does not accept spec-style - // constraints. - if (navigator.mediaDevices.getUserMedia) { - var origGetUserMedia = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices); - navigator.mediaDevices.getUserMedia = function (cs) { - return shimConstraints_(cs, function (c) { - return origGetUserMedia(c).then(function (stream) { - if (c.audio && !stream.getAudioTracks().length || c.video && !stream.getVideoTracks().length) { - stream.getTracks().forEach(function (track) { - track.stop(); - }); - throw new DOMException('', 'NotFoundError'); - } - return stream; - }, function (e) { - return Promise.reject(shimError_(e)); - }); - }); - }; - } -} - -},{"../utils.js":11}],6:[function(require,module,exports){ -/* - * Copyright (c) 2017 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. - */ -/* eslint-env node */ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.removeExtmapAllowMixed = removeExtmapAllowMixed; -exports.shimAddIceCandidateNullOrEmpty = shimAddIceCandidateNullOrEmpty; -exports.shimConnectionState = shimConnectionState; -exports.shimMaxMessageSize = shimMaxMessageSize; -exports.shimParameterlessSetLocalDescription = shimParameterlessSetLocalDescription; -exports.shimRTCIceCandidate = shimRTCIceCandidate; -exports.shimRTCIceCandidateRelayProtocol = shimRTCIceCandidateRelayProtocol; -exports.shimSendThrowTypeError = shimSendThrowTypeError; -var _sdp = _interopRequireDefault(require("sdp")); -var utils = _interopRequireWildcard(require("./utils")); -function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } -function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } -function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } -function shimRTCIceCandidate(window) { - // foundation is arbitrarily chosen as an indicator for full support for - // https://w3c.github.io/webrtc-pc/#rtcicecandidate-interface - if (!window.RTCIceCandidate || window.RTCIceCandidate && 'foundation' in window.RTCIceCandidate.prototype) { - return; - } - var NativeRTCIceCandidate = window.RTCIceCandidate; - window.RTCIceCandidate = function RTCIceCandidate(args) { - // Remove the a= which shouldn't be part of the candidate string. - if (_typeof(args) === 'object' && args.candidate && args.candidate.indexOf('a=') === 0) { - args = JSON.parse(JSON.stringify(args)); - args.candidate = args.candidate.substring(2); - } - if (args.candidate && args.candidate.length) { - // Augment the native candidate with the parsed fields. - var nativeCandidate = new NativeRTCIceCandidate(args); - var parsedCandidate = _sdp["default"].parseCandidate(args.candidate); - for (var key in parsedCandidate) { - if (!(key in nativeCandidate)) { - Object.defineProperty(nativeCandidate, key, { - value: parsedCandidate[key] - }); - } - } - - // Override serializer to not serialize the extra attributes. - nativeCandidate.toJSON = function toJSON() { - return { - candidate: nativeCandidate.candidate, - sdpMid: nativeCandidate.sdpMid, - sdpMLineIndex: nativeCandidate.sdpMLineIndex, - usernameFragment: nativeCandidate.usernameFragment - }; - }; - return nativeCandidate; - } - return new NativeRTCIceCandidate(args); - }; - window.RTCIceCandidate.prototype = NativeRTCIceCandidate.prototype; - - // Hook up the augmented candidate in onicecandidate and - // addEventListener('icecandidate', ...) - utils.wrapPeerConnectionEvent(window, 'icecandidate', function (e) { - if (e.candidate) { - Object.defineProperty(e, 'candidate', { - value: new window.RTCIceCandidate(e.candidate), - writable: 'false' - }); - } - return e; - }); -} -function shimRTCIceCandidateRelayProtocol(window) { - if (!window.RTCIceCandidate || window.RTCIceCandidate && 'relayProtocol' in window.RTCIceCandidate.prototype) { - return; - } - - // Hook up the augmented candidate in onicecandidate and - // addEventListener('icecandidate', ...) - utils.wrapPeerConnectionEvent(window, 'icecandidate', function (e) { - if (e.candidate) { - var parsedCandidate = _sdp["default"].parseCandidate(e.candidate.candidate); - if (parsedCandidate.type === 'relay') { - // This is a libwebrtc-specific mapping of local type preference - // to relayProtocol. - e.candidate.relayProtocol = { - 0: 'tls', - 1: 'tcp', - 2: 'udp' - }[parsedCandidate.priority >> 24]; - } - } - return e; - }); -} -function shimMaxMessageSize(window, browserDetails) { - if (!window.RTCPeerConnection) { - return; - } - if (!('sctp' in window.RTCPeerConnection.prototype)) { - Object.defineProperty(window.RTCPeerConnection.prototype, 'sctp', { - get: function get() { - return typeof this._sctp === 'undefined' ? null : this._sctp; - } - }); - } - var sctpInDescription = function sctpInDescription(description) { - if (!description || !description.sdp) { - return false; - } - var sections = _sdp["default"].splitSections(description.sdp); - sections.shift(); - return sections.some(function (mediaSection) { - var mLine = _sdp["default"].parseMLine(mediaSection); - return mLine && mLine.kind === 'application' && mLine.protocol.indexOf('SCTP') !== -1; - }); - }; - var getRemoteFirefoxVersion = function getRemoteFirefoxVersion(description) { - // TODO: Is there a better solution for detecting Firefox? - var match = description.sdp.match(/mozilla...THIS_IS_SDPARTA-(\d+)/); - if (match === null || match.length < 2) { - return -1; - } - var version = parseInt(match[1], 10); - // Test for NaN (yes, this is ugly) - return version !== version ? -1 : version; - }; - var getCanSendMaxMessageSize = function getCanSendMaxMessageSize(remoteIsFirefox) { - // Every implementation we know can send at least 64 KiB. - // Note: Although Chrome is technically able to send up to 256 KiB, the - // data does not reach the other peer reliably. - // See: https://bugs.chromium.org/p/webrtc/issues/detail?id=8419 - var canSendMaxMessageSize = 65536; - if (browserDetails.browser === 'firefox') { - if (browserDetails.version < 57) { - if (remoteIsFirefox === -1) { - // FF < 57 will send in 16 KiB chunks using the deprecated PPID - // fragmentation. - canSendMaxMessageSize = 16384; - } else { - // However, other FF (and RAWRTC) can reassemble PPID-fragmented - // messages. Thus, supporting ~2 GiB when sending. - canSendMaxMessageSize = 2147483637; - } - } else if (browserDetails.version < 60) { - // Currently, all FF >= 57 will reset the remote maximum message size - // to the default value when a data channel is created at a later - // stage. :( - // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1426831 - canSendMaxMessageSize = browserDetails.version === 57 ? 65535 : 65536; - } else { - // FF >= 60 supports sending ~2 GiB - canSendMaxMessageSize = 2147483637; - } - } - return canSendMaxMessageSize; - }; - var getMaxMessageSize = function getMaxMessageSize(description, remoteIsFirefox) { - // Note: 65536 bytes is the default value from the SDP spec. Also, - // every implementation we know supports receiving 65536 bytes. - var maxMessageSize = 65536; - - // FF 57 has a slightly incorrect default remote max message size, so - // we need to adjust it here to avoid a failure when sending. - // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1425697 - if (browserDetails.browser === 'firefox' && browserDetails.version === 57) { - maxMessageSize = 65535; - } - var match = _sdp["default"].matchPrefix(description.sdp, 'a=max-message-size:'); - if (match.length > 0) { - maxMessageSize = parseInt(match[0].substring(19), 10); - } else if (browserDetails.browser === 'firefox' && remoteIsFirefox !== -1) { - // If the maximum message size is not present in the remote SDP and - // both local and remote are Firefox, the remote peer can receive - // ~2 GiB. - maxMessageSize = 2147483637; - } - return maxMessageSize; - }; - var origSetRemoteDescription = window.RTCPeerConnection.prototype.setRemoteDescription; - window.RTCPeerConnection.prototype.setRemoteDescription = function setRemoteDescription() { - this._sctp = null; - // Chrome decided to not expose .sctp in plan-b mode. - // As usual, adapter.js has to do an 'ugly worakaround' - // to cover up the mess. - if (browserDetails.browser === 'chrome' && browserDetails.version >= 76) { - var _this$getConfiguratio = this.getConfiguration(), - sdpSemantics = _this$getConfiguratio.sdpSemantics; - if (sdpSemantics === 'plan-b') { - Object.defineProperty(this, 'sctp', { - get: function get() { - return typeof this._sctp === 'undefined' ? null : this._sctp; - }, - enumerable: true, - configurable: true - }); - } - } - if (sctpInDescription(arguments[0])) { - // Check if the remote is FF. - var isFirefox = getRemoteFirefoxVersion(arguments[0]); - - // Get the maximum message size the local peer is capable of sending - var canSendMMS = getCanSendMaxMessageSize(isFirefox); - - // Get the maximum message size of the remote peer. - var remoteMMS = getMaxMessageSize(arguments[0], isFirefox); - - // Determine final maximum message size - var maxMessageSize; - if (canSendMMS === 0 && remoteMMS === 0) { - maxMessageSize = Number.POSITIVE_INFINITY; - } else if (canSendMMS === 0 || remoteMMS === 0) { - maxMessageSize = Math.max(canSendMMS, remoteMMS); - } else { - maxMessageSize = Math.min(canSendMMS, remoteMMS); - } - - // Create a dummy RTCSctpTransport object and the 'maxMessageSize' - // attribute. - var sctp = {}; - Object.defineProperty(sctp, 'maxMessageSize', { - get: function get() { - return maxMessageSize; - } - }); - this._sctp = sctp; - } - return origSetRemoteDescription.apply(this, arguments); - }; -} -function shimSendThrowTypeError(window) { - if (!(window.RTCPeerConnection && 'createDataChannel' in window.RTCPeerConnection.prototype)) { - return; - } - - // Note: Although Firefox >= 57 has a native implementation, the maximum - // message size can be reset for all data channels at a later stage. - // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1426831 - - function wrapDcSend(dc, pc) { - var origDataChannelSend = dc.send; - dc.send = function send() { - var data = arguments[0]; - var length = data.length || data.size || data.byteLength; - if (dc.readyState === 'open' && pc.sctp && length > pc.sctp.maxMessageSize) { - throw new TypeError('Message too large (can send a maximum of ' + pc.sctp.maxMessageSize + ' bytes)'); - } - return origDataChannelSend.apply(dc, arguments); - }; - } - var origCreateDataChannel = window.RTCPeerConnection.prototype.createDataChannel; - window.RTCPeerConnection.prototype.createDataChannel = function createDataChannel() { - var dataChannel = origCreateDataChannel.apply(this, arguments); - wrapDcSend(dataChannel, this); - return dataChannel; - }; - utils.wrapPeerConnectionEvent(window, 'datachannel', function (e) { - wrapDcSend(e.channel, e.target); - return e; - }); -} - -/* shims RTCConnectionState by pretending it is the same as iceConnectionState. - * See https://bugs.chromium.org/p/webrtc/issues/detail?id=6145#c12 - * for why this is a valid hack in Chrome. In Firefox it is slightly incorrect - * since DTLS failures would be hidden. See - * https://bugzilla.mozilla.org/show_bug.cgi?id=1265827 - * for the Firefox tracking bug. - */ -function shimConnectionState(window) { - if (!window.RTCPeerConnection || 'connectionState' in window.RTCPeerConnection.prototype) { - return; - } - var proto = window.RTCPeerConnection.prototype; - Object.defineProperty(proto, 'connectionState', { - get: function get() { - return { - completed: 'connected', - checking: 'connecting' - }[this.iceConnectionState] || this.iceConnectionState; - }, - enumerable: true, - configurable: true - }); - Object.defineProperty(proto, 'onconnectionstatechange', { - get: function get() { - return this._onconnectionstatechange || null; - }, - set: function set(cb) { - if (this._onconnectionstatechange) { - this.removeEventListener('connectionstatechange', this._onconnectionstatechange); - delete this._onconnectionstatechange; - } - if (cb) { - this.addEventListener('connectionstatechange', this._onconnectionstatechange = cb); - } - }, - enumerable: true, - configurable: true - }); - ['setLocalDescription', 'setRemoteDescription'].forEach(function (method) { - var origMethod = proto[method]; - proto[method] = function () { - if (!this._connectionstatechangepoly) { - this._connectionstatechangepoly = function (e) { - var pc = e.target; - if (pc._lastConnectionState !== pc.connectionState) { - pc._lastConnectionState = pc.connectionState; - var newEvent = new Event('connectionstatechange', e); - pc.dispatchEvent(newEvent); - } - return e; - }; - this.addEventListener('iceconnectionstatechange', this._connectionstatechangepoly); - } - return origMethod.apply(this, arguments); - }; - }); -} -function removeExtmapAllowMixed(window, browserDetails) { - /* remove a=extmap-allow-mixed for webrtc.org < M71 */ - if (!window.RTCPeerConnection) { - return; - } - if (browserDetails.browser === 'chrome' && browserDetails.version >= 71) { - return; - } - if (browserDetails.browser === 'safari' && browserDetails.version >= 605) { - return; - } - var nativeSRD = window.RTCPeerConnection.prototype.setRemoteDescription; - window.RTCPeerConnection.prototype.setRemoteDescription = function setRemoteDescription(desc) { - if (desc && desc.sdp && desc.sdp.indexOf('\na=extmap-allow-mixed') !== -1) { - var sdp = desc.sdp.split('\n').filter(function (line) { - return line.trim() !== 'a=extmap-allow-mixed'; - }).join('\n'); - // Safari enforces read-only-ness of RTCSessionDescription fields. - if (window.RTCSessionDescription && desc instanceof window.RTCSessionDescription) { - arguments[0] = new window.RTCSessionDescription({ - type: desc.type, - sdp: sdp - }); - } else { - desc.sdp = sdp; - } - } - return nativeSRD.apply(this, arguments); - }; -} -function shimAddIceCandidateNullOrEmpty(window, browserDetails) { - // Support for addIceCandidate(null or undefined) - // as well as addIceCandidate({candidate: "", ...}) - // https://bugs.chromium.org/p/chromium/issues/detail?id=978582 - // Note: must be called before other polyfills which change the signature. - if (!(window.RTCPeerConnection && window.RTCPeerConnection.prototype)) { - return; - } - var nativeAddIceCandidate = window.RTCPeerConnection.prototype.addIceCandidate; - if (!nativeAddIceCandidate || nativeAddIceCandidate.length === 0) { - return; - } - window.RTCPeerConnection.prototype.addIceCandidate = function addIceCandidate() { - if (!arguments[0]) { - if (arguments[1]) { - arguments[1].apply(null); - } - return Promise.resolve(); - } - // Firefox 68+ emits and processes {candidate: "", ...}, ignore - // in older versions. - // Native support for ignoring exists for Chrome M77+. - // Safari ignores as well, exact version unknown but works in the same - // version that also ignores addIceCandidate(null). - if ((browserDetails.browser === 'chrome' && browserDetails.version < 78 || browserDetails.browser === 'firefox' && browserDetails.version < 68 || browserDetails.browser === 'safari') && arguments[0] && arguments[0].candidate === '') { - return Promise.resolve(); - } - return nativeAddIceCandidate.apply(this, arguments); - }; -} - -// Note: Make sure to call this ahead of APIs that modify -// setLocalDescription.length -function shimParameterlessSetLocalDescription(window, browserDetails) { - if (!(window.RTCPeerConnection && window.RTCPeerConnection.prototype)) { - return; - } - var nativeSetLocalDescription = window.RTCPeerConnection.prototype.setLocalDescription; - if (!nativeSetLocalDescription || nativeSetLocalDescription.length === 0) { - return; - } - window.RTCPeerConnection.prototype.setLocalDescription = function setLocalDescription() { - var _this = this; - var desc = arguments[0] || {}; - if (_typeof(desc) !== 'object' || desc.type && desc.sdp) { - return nativeSetLocalDescription.apply(this, arguments); - } - // The remaining steps should technically happen when SLD comes off the - // RTCPeerConnection's operations chain (not ahead of going on it), but - // this is too difficult to shim. Instead, this shim only covers the - // common case where the operations chain is empty. This is imperfect, but - // should cover many cases. Rationale: Even if we can't reduce the glare - // window to zero on imperfect implementations, there's value in tapping - // into the perfect negotiation pattern that several browsers support. - desc = { - type: desc.type, - sdp: desc.sdp - }; - if (!desc.type) { - switch (this.signalingState) { - case 'stable': - case 'have-local-offer': - case 'have-remote-pranswer': - desc.type = 'offer'; - break; - default: - desc.type = 'answer'; - break; - } - } - if (desc.sdp || desc.type !== 'offer' && desc.type !== 'answer') { - return nativeSetLocalDescription.apply(this, [desc]); - } - var func = desc.type === 'offer' ? this.createOffer : this.createAnswer; - return func.apply(this).then(function (d) { - return nativeSetLocalDescription.apply(_this, [d]); - }); - }; -} - -},{"./utils":11,"sdp":12}],7:[function(require,module,exports){ -/* - * Copyright (c) 2016 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. - */ -/* eslint-env node */ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.shimAddTransceiver = shimAddTransceiver; -exports.shimCreateAnswer = shimCreateAnswer; -exports.shimCreateOffer = shimCreateOffer; -Object.defineProperty(exports, "shimGetDisplayMedia", { - enumerable: true, - get: function get() { - return _getdisplaymedia.shimGetDisplayMedia; - } -}); -exports.shimGetParameters = shimGetParameters; -Object.defineProperty(exports, "shimGetUserMedia", { - enumerable: true, - get: function get() { - return _getusermedia.shimGetUserMedia; - } -}); -exports.shimOnTrack = shimOnTrack; -exports.shimPeerConnection = shimPeerConnection; -exports.shimRTCDataChannel = shimRTCDataChannel; -exports.shimReceiverGetStats = shimReceiverGetStats; -exports.shimRemoveStream = shimRemoveStream; -exports.shimSenderGetStats = shimSenderGetStats; -var utils = _interopRequireWildcard(require("../utils")); -var _getusermedia = require("./getusermedia"); -var _getdisplaymedia = require("./getdisplaymedia"); -function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } -function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } -function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); } -function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } -function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } -function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); } -function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); } -function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } -function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } -function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); } -function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } -function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } -function shimOnTrack(window) { - if (_typeof(window) === 'object' && window.RTCTrackEvent && 'receiver' in window.RTCTrackEvent.prototype && !('transceiver' in window.RTCTrackEvent.prototype)) { - Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', { - get: function get() { - return { - receiver: this.receiver - }; - } - }); - } -} -function shimPeerConnection(window, browserDetails) { - if (_typeof(window) !== 'object' || !(window.RTCPeerConnection || window.mozRTCPeerConnection)) { - return; // probably media.peerconnection.enabled=false in about:config - } - - if (!window.RTCPeerConnection && window.mozRTCPeerConnection) { - // very basic support for old versions. - window.RTCPeerConnection = window.mozRTCPeerConnection; - } - if (browserDetails.version < 53) { - // shim away need for obsolete RTCIceCandidate/RTCSessionDescription. - ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'].forEach(function (method) { - var nativeMethod = window.RTCPeerConnection.prototype[method]; - var methodObj = _defineProperty({}, method, function () { - arguments[0] = new (method === 'addIceCandidate' ? window.RTCIceCandidate : window.RTCSessionDescription)(arguments[0]); - return nativeMethod.apply(this, arguments); - }); - window.RTCPeerConnection.prototype[method] = methodObj[method]; - }); - } - var modernStatsTypes = { - inboundrtp: 'inbound-rtp', - outboundrtp: 'outbound-rtp', - candidatepair: 'candidate-pair', - localcandidate: 'local-candidate', - remotecandidate: 'remote-candidate' - }; - var nativeGetStats = window.RTCPeerConnection.prototype.getStats; - window.RTCPeerConnection.prototype.getStats = function getStats() { - var _arguments = Array.prototype.slice.call(arguments), - selector = _arguments[0], - onSucc = _arguments[1], - onErr = _arguments[2]; - return nativeGetStats.apply(this, [selector || null]).then(function (stats) { - if (browserDetails.version < 53 && !onSucc) { - // Shim only promise getStats with spec-hyphens in type names - // Leave callback version alone; misc old uses of forEach before Map - try { - stats.forEach(function (stat) { - stat.type = modernStatsTypes[stat.type] || stat.type; - }); - } catch (e) { - if (e.name !== 'TypeError') { - throw e; - } - // Avoid TypeError: "type" is read-only, in old versions. 34-43ish - stats.forEach(function (stat, i) { - stats.set(i, Object.assign({}, stat, { - type: modernStatsTypes[stat.type] || stat.type - })); - }); - } - } - return stats; - }).then(onSucc, onErr); - }; -} -function shimSenderGetStats(window) { - if (!(_typeof(window) === 'object' && window.RTCPeerConnection && window.RTCRtpSender)) { - return; - } - if (window.RTCRtpSender && 'getStats' in window.RTCRtpSender.prototype) { - return; - } - var origGetSenders = window.RTCPeerConnection.prototype.getSenders; - if (origGetSenders) { - window.RTCPeerConnection.prototype.getSenders = function getSenders() { - var _this = this; - var senders = origGetSenders.apply(this, []); - senders.forEach(function (sender) { - return sender._pc = _this; - }); - return senders; - }; - } - var origAddTrack = window.RTCPeerConnection.prototype.addTrack; - if (origAddTrack) { - window.RTCPeerConnection.prototype.addTrack = function addTrack() { - var sender = origAddTrack.apply(this, arguments); - sender._pc = this; - return sender; - }; - } - window.RTCRtpSender.prototype.getStats = function getStats() { - return this.track ? this._pc.getStats(this.track) : Promise.resolve(new Map()); - }; -} -function shimReceiverGetStats(window) { - if (!(_typeof(window) === 'object' && window.RTCPeerConnection && window.RTCRtpSender)) { - return; - } - if (window.RTCRtpSender && 'getStats' in window.RTCRtpReceiver.prototype) { - return; - } - var origGetReceivers = window.RTCPeerConnection.prototype.getReceivers; - if (origGetReceivers) { - window.RTCPeerConnection.prototype.getReceivers = function getReceivers() { - var _this2 = this; - var receivers = origGetReceivers.apply(this, []); - receivers.forEach(function (receiver) { - return receiver._pc = _this2; - }); - return receivers; - }; - } - utils.wrapPeerConnectionEvent(window, 'track', function (e) { - e.receiver._pc = e.srcElement; - return e; - }); - window.RTCRtpReceiver.prototype.getStats = function getStats() { - return this._pc.getStats(this.track); - }; -} -function shimRemoveStream(window) { - if (!window.RTCPeerConnection || 'removeStream' in window.RTCPeerConnection.prototype) { - return; - } - window.RTCPeerConnection.prototype.removeStream = function removeStream(stream) { - var _this3 = this; - utils.deprecated('removeStream', 'removeTrack'); - this.getSenders().forEach(function (sender) { - if (sender.track && stream.getTracks().includes(sender.track)) { - _this3.removeTrack(sender); - } - }); - }; -} -function shimRTCDataChannel(window) { - // rename DataChannel to RTCDataChannel (native fix in FF60): - // https://bugzilla.mozilla.org/show_bug.cgi?id=1173851 - if (window.DataChannel && !window.RTCDataChannel) { - window.RTCDataChannel = window.DataChannel; - } -} -function shimAddTransceiver(window) { - // https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647 - // Firefox ignores the init sendEncodings options passed to addTransceiver - // https://bugzilla.mozilla.org/show_bug.cgi?id=1396918 - if (!(_typeof(window) === 'object' && window.RTCPeerConnection)) { - return; - } - var origAddTransceiver = window.RTCPeerConnection.prototype.addTransceiver; - if (origAddTransceiver) { - window.RTCPeerConnection.prototype.addTransceiver = function addTransceiver() { - this.setParametersPromises = []; - // WebIDL input coercion and validation - var sendEncodings = arguments[1] && arguments[1].sendEncodings; - if (sendEncodings === undefined) { - sendEncodings = []; - } - sendEncodings = _toConsumableArray(sendEncodings); - var shouldPerformCheck = sendEncodings.length > 0; - if (shouldPerformCheck) { - // If sendEncodings params are provided, validate grammar - sendEncodings.forEach(function (encodingParam) { - if ('rid' in encodingParam) { - var ridRegex = /^[a-z0-9]{0,16}$/i; - if (!ridRegex.test(encodingParam.rid)) { - throw new TypeError('Invalid RID value provided.'); - } - } - if ('scaleResolutionDownBy' in encodingParam) { - if (!(parseFloat(encodingParam.scaleResolutionDownBy) >= 1.0)) { - throw new RangeError('scale_resolution_down_by must be >= 1.0'); - } - } - if ('maxFramerate' in encodingParam) { - if (!(parseFloat(encodingParam.maxFramerate) >= 0)) { - throw new RangeError('max_framerate must be >= 0.0'); - } - } - }); - } - var transceiver = origAddTransceiver.apply(this, arguments); - if (shouldPerformCheck) { - // Check if the init options were applied. If not we do this in an - // asynchronous way and save the promise reference in a global object. - // This is an ugly hack, but at the same time is way more robust than - // checking the sender parameters before and after the createOffer - // Also note that after the createoffer we are not 100% sure that - // the params were asynchronously applied so we might miss the - // opportunity to recreate offer. - var sender = transceiver.sender; - var params = sender.getParameters(); - if (!('encodings' in params) || - // Avoid being fooled by patched getParameters() below. - params.encodings.length === 1 && Object.keys(params.encodings[0]).length === 0) { - params.encodings = sendEncodings; - sender.sendEncodings = sendEncodings; - this.setParametersPromises.push(sender.setParameters(params).then(function () { - delete sender.sendEncodings; - })["catch"](function () { - delete sender.sendEncodings; - })); - } - } - return transceiver; - }; - } -} -function shimGetParameters(window) { - if (!(_typeof(window) === 'object' && window.RTCRtpSender)) { - return; - } - var origGetParameters = window.RTCRtpSender.prototype.getParameters; - if (origGetParameters) { - window.RTCRtpSender.prototype.getParameters = function getParameters() { - var params = origGetParameters.apply(this, arguments); - if (!('encodings' in params)) { - params.encodings = [].concat(this.sendEncodings || [{}]); - } - return params; - }; - } -} -function shimCreateOffer(window) { - // https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647 - // Firefox ignores the init sendEncodings options passed to addTransceiver - // https://bugzilla.mozilla.org/show_bug.cgi?id=1396918 - if (!(_typeof(window) === 'object' && window.RTCPeerConnection)) { - return; - } - var origCreateOffer = window.RTCPeerConnection.prototype.createOffer; - window.RTCPeerConnection.prototype.createOffer = function createOffer() { - var _arguments2 = arguments, - _this4 = this; - if (this.setParametersPromises && this.setParametersPromises.length) { - return Promise.all(this.setParametersPromises).then(function () { - return origCreateOffer.apply(_this4, _arguments2); - })["finally"](function () { - _this4.setParametersPromises = []; - }); - } - return origCreateOffer.apply(this, arguments); - }; -} -function shimCreateAnswer(window) { - // https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647 - // Firefox ignores the init sendEncodings options passed to addTransceiver - // https://bugzilla.mozilla.org/show_bug.cgi?id=1396918 - if (!(_typeof(window) === 'object' && window.RTCPeerConnection)) { - return; - } - var origCreateAnswer = window.RTCPeerConnection.prototype.createAnswer; - window.RTCPeerConnection.prototype.createAnswer = function createAnswer() { - var _arguments3 = arguments, - _this5 = this; - if (this.setParametersPromises && this.setParametersPromises.length) { - return Promise.all(this.setParametersPromises).then(function () { - return origCreateAnswer.apply(_this5, _arguments3); - })["finally"](function () { - _this5.setParametersPromises = []; - }); - } - return origCreateAnswer.apply(this, arguments); - }; -} - -},{"../utils":11,"./getdisplaymedia":8,"./getusermedia":9}],8:[function(require,module,exports){ -/* - * Copyright (c) 2018 The adapter.js 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. - */ -/* eslint-env node */ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.shimGetDisplayMedia = shimGetDisplayMedia; -function shimGetDisplayMedia(window, preferredMediaSource) { - if (window.navigator.mediaDevices && 'getDisplayMedia' in window.navigator.mediaDevices) { - return; - } - if (!window.navigator.mediaDevices) { - return; - } - window.navigator.mediaDevices.getDisplayMedia = function getDisplayMedia(constraints) { - if (!(constraints && constraints.video)) { - var err = new DOMException('getDisplayMedia without video ' + 'constraints is undefined'); - err.name = 'NotFoundError'; - // from https://heycam.github.io/webidl/#idl-DOMException-error-names - err.code = 8; - return Promise.reject(err); - } - if (constraints.video === true) { - constraints.video = { - mediaSource: preferredMediaSource - }; - } else { - constraints.video.mediaSource = preferredMediaSource; - } - return window.navigator.mediaDevices.getUserMedia(constraints); - }; -} - -},{}],9:[function(require,module,exports){ -/* - * Copyright (c) 2016 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. - */ -/* eslint-env node */ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.shimGetUserMedia = shimGetUserMedia; -var utils = _interopRequireWildcard(require("../utils")); -function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } -function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } -function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } -function shimGetUserMedia(window, browserDetails) { - var navigator = window && window.navigator; - var MediaStreamTrack = window && window.MediaStreamTrack; - navigator.getUserMedia = function (constraints, onSuccess, onError) { - // Replace Firefox 44+'s deprecation warning with unprefixed version. - utils.deprecated('navigator.getUserMedia', 'navigator.mediaDevices.getUserMedia'); - navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError); - }; - if (!(browserDetails.version > 55 && 'autoGainControl' in navigator.mediaDevices.getSupportedConstraints())) { - var remap = function remap(obj, a, b) { - if (a in obj && !(b in obj)) { - obj[b] = obj[a]; - delete obj[a]; - } - }; - var nativeGetUserMedia = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices); - navigator.mediaDevices.getUserMedia = function (c) { - if (_typeof(c) === 'object' && _typeof(c.audio) === 'object') { - c = JSON.parse(JSON.stringify(c)); - remap(c.audio, 'autoGainControl', 'mozAutoGainControl'); - remap(c.audio, 'noiseSuppression', 'mozNoiseSuppression'); - } - return nativeGetUserMedia(c); - }; - if (MediaStreamTrack && MediaStreamTrack.prototype.getSettings) { - var nativeGetSettings = MediaStreamTrack.prototype.getSettings; - MediaStreamTrack.prototype.getSettings = function () { - var obj = nativeGetSettings.apply(this, arguments); - remap(obj, 'mozAutoGainControl', 'autoGainControl'); - remap(obj, 'mozNoiseSuppression', 'noiseSuppression'); - return obj; - }; - } - if (MediaStreamTrack && MediaStreamTrack.prototype.applyConstraints) { - var nativeApplyConstraints = MediaStreamTrack.prototype.applyConstraints; - MediaStreamTrack.prototype.applyConstraints = function (c) { - if (this.kind === 'audio' && _typeof(c) === 'object') { - c = JSON.parse(JSON.stringify(c)); - remap(c, 'autoGainControl', 'mozAutoGainControl'); - remap(c, 'noiseSuppression', 'mozNoiseSuppression'); - } - return nativeApplyConstraints.apply(this, [c]); - }; - } - } -} - -},{"../utils":11}],10:[function(require,module,exports){ -/* - * Copyright (c) 2016 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. - */ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.shimAudioContext = shimAudioContext; -exports.shimCallbacksAPI = shimCallbacksAPI; -exports.shimConstraints = shimConstraints; -exports.shimCreateOfferLegacy = shimCreateOfferLegacy; -exports.shimGetUserMedia = shimGetUserMedia; -exports.shimLocalStreamsAPI = shimLocalStreamsAPI; -exports.shimRTCIceServerUrls = shimRTCIceServerUrls; -exports.shimRemoteStreamsAPI = shimRemoteStreamsAPI; -exports.shimTrackEventTransceiver = shimTrackEventTransceiver; -var utils = _interopRequireWildcard(require("../utils")); -function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } -function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } -function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } -function shimLocalStreamsAPI(window) { - if (_typeof(window) !== 'object' || !window.RTCPeerConnection) { - return; - } - if (!('getLocalStreams' in window.RTCPeerConnection.prototype)) { - window.RTCPeerConnection.prototype.getLocalStreams = function getLocalStreams() { - if (!this._localStreams) { - this._localStreams = []; - } - return this._localStreams; - }; - } - if (!('addStream' in window.RTCPeerConnection.prototype)) { - var _addTrack = window.RTCPeerConnection.prototype.addTrack; - window.RTCPeerConnection.prototype.addStream = function addStream(stream) { - var _this = this; - if (!this._localStreams) { - this._localStreams = []; - } - if (!this._localStreams.includes(stream)) { - this._localStreams.push(stream); - } - // Try to emulate Chrome's behaviour of adding in audio-video order. - // Safari orders by track id. - stream.getAudioTracks().forEach(function (track) { - return _addTrack.call(_this, track, stream); - }); - stream.getVideoTracks().forEach(function (track) { - return _addTrack.call(_this, track, stream); - }); - }; - window.RTCPeerConnection.prototype.addTrack = function addTrack(track) { - var _this2 = this; - for (var _len = arguments.length, streams = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { - streams[_key - 1] = arguments[_key]; - } - if (streams) { - streams.forEach(function (stream) { - if (!_this2._localStreams) { - _this2._localStreams = [stream]; - } else if (!_this2._localStreams.includes(stream)) { - _this2._localStreams.push(stream); - } - }); - } - return _addTrack.apply(this, arguments); - }; - } - if (!('removeStream' in window.RTCPeerConnection.prototype)) { - window.RTCPeerConnection.prototype.removeStream = function removeStream(stream) { - var _this3 = this; - if (!this._localStreams) { - this._localStreams = []; - } - var index = this._localStreams.indexOf(stream); - if (index === -1) { - return; - } - this._localStreams.splice(index, 1); - var tracks = stream.getTracks(); - this.getSenders().forEach(function (sender) { - if (tracks.includes(sender.track)) { - _this3.removeTrack(sender); - } - }); - }; - } -} -function shimRemoteStreamsAPI(window) { - if (_typeof(window) !== 'object' || !window.RTCPeerConnection) { - return; - } - if (!('getRemoteStreams' in window.RTCPeerConnection.prototype)) { - window.RTCPeerConnection.prototype.getRemoteStreams = function getRemoteStreams() { - return this._remoteStreams ? this._remoteStreams : []; - }; - } - if (!('onaddstream' in window.RTCPeerConnection.prototype)) { - Object.defineProperty(window.RTCPeerConnection.prototype, 'onaddstream', { - get: function get() { - return this._onaddstream; - }, - set: function set(f) { - var _this4 = this; - if (this._onaddstream) { - this.removeEventListener('addstream', this._onaddstream); - this.removeEventListener('track', this._onaddstreampoly); - } - this.addEventListener('addstream', this._onaddstream = f); - this.addEventListener('track', this._onaddstreampoly = function (e) { - e.streams.forEach(function (stream) { - if (!_this4._remoteStreams) { - _this4._remoteStreams = []; - } - if (_this4._remoteStreams.includes(stream)) { - return; - } - _this4._remoteStreams.push(stream); - var event = new Event('addstream'); - event.stream = stream; - _this4.dispatchEvent(event); - }); - }); - } - }); - var origSetRemoteDescription = window.RTCPeerConnection.prototype.setRemoteDescription; - window.RTCPeerConnection.prototype.setRemoteDescription = function setRemoteDescription() { - var pc = this; - if (!this._onaddstreampoly) { - this.addEventListener('track', this._onaddstreampoly = function (e) { - e.streams.forEach(function (stream) { - if (!pc._remoteStreams) { - pc._remoteStreams = []; - } - if (pc._remoteStreams.indexOf(stream) >= 0) { - return; - } - pc._remoteStreams.push(stream); - var event = new Event('addstream'); - event.stream = stream; - pc.dispatchEvent(event); - }); - }); - } - return origSetRemoteDescription.apply(pc, arguments); - }; - } -} -function shimCallbacksAPI(window) { - if (_typeof(window) !== 'object' || !window.RTCPeerConnection) { - return; - } - var prototype = window.RTCPeerConnection.prototype; - var origCreateOffer = prototype.createOffer; - var origCreateAnswer = prototype.createAnswer; - var setLocalDescription = prototype.setLocalDescription; - var setRemoteDescription = prototype.setRemoteDescription; - var addIceCandidate = prototype.addIceCandidate; - prototype.createOffer = function createOffer(successCallback, failureCallback) { - var options = arguments.length >= 2 ? arguments[2] : arguments[0]; - var promise = origCreateOffer.apply(this, [options]); - if (!failureCallback) { - return promise; - } - promise.then(successCallback, failureCallback); - return Promise.resolve(); - }; - prototype.createAnswer = function createAnswer(successCallback, failureCallback) { - var options = arguments.length >= 2 ? arguments[2] : arguments[0]; - var promise = origCreateAnswer.apply(this, [options]); - if (!failureCallback) { - return promise; - } - promise.then(successCallback, failureCallback); - return Promise.resolve(); - }; - var withCallback = function withCallback(description, successCallback, failureCallback) { - var promise = setLocalDescription.apply(this, [description]); - if (!failureCallback) { - return promise; - } - promise.then(successCallback, failureCallback); - return Promise.resolve(); - }; - prototype.setLocalDescription = withCallback; - withCallback = function withCallback(description, successCallback, failureCallback) { - var promise = setRemoteDescription.apply(this, [description]); - if (!failureCallback) { - return promise; - } - promise.then(successCallback, failureCallback); - return Promise.resolve(); - }; - prototype.setRemoteDescription = withCallback; - withCallback = function withCallback(candidate, successCallback, failureCallback) { - var promise = addIceCandidate.apply(this, [candidate]); - if (!failureCallback) { - return promise; - } - promise.then(successCallback, failureCallback); - return Promise.resolve(); - }; - prototype.addIceCandidate = withCallback; -} -function shimGetUserMedia(window) { - var navigator = window && window.navigator; - if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { - // shim not needed in Safari 12.1 - var mediaDevices = navigator.mediaDevices; - var _getUserMedia = mediaDevices.getUserMedia.bind(mediaDevices); - navigator.mediaDevices.getUserMedia = function (constraints) { - return _getUserMedia(shimConstraints(constraints)); - }; - } - if (!navigator.getUserMedia && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { - navigator.getUserMedia = function getUserMedia(constraints, cb, errcb) { - navigator.mediaDevices.getUserMedia(constraints).then(cb, errcb); - }.bind(navigator); - } -} -function shimConstraints(constraints) { - if (constraints && constraints.video !== undefined) { - return Object.assign({}, constraints, { - video: utils.compactObject(constraints.video) - }); - } - return constraints; -} -function shimRTCIceServerUrls(window) { - if (!window.RTCPeerConnection) { - return; - } - // migrate from non-spec RTCIceServer.url to RTCIceServer.urls - var OrigPeerConnection = window.RTCPeerConnection; - window.RTCPeerConnection = function RTCPeerConnection(pcConfig, pcConstraints) { - if (pcConfig && pcConfig.iceServers) { - var newIceServers = []; - for (var i = 0; i < pcConfig.iceServers.length; i++) { - var server = pcConfig.iceServers[i]; - if (server.urls === undefined && server.url) { - utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls'); - server = JSON.parse(JSON.stringify(server)); - server.urls = server.url; - delete server.url; - newIceServers.push(server); - } else { - newIceServers.push(pcConfig.iceServers[i]); - } - } - pcConfig.iceServers = newIceServers; - } - return new OrigPeerConnection(pcConfig, pcConstraints); - }; - window.RTCPeerConnection.prototype = OrigPeerConnection.prototype; - // wrap static methods. Currently just generateCertificate. - if ('generateCertificate' in OrigPeerConnection) { - Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { - get: function get() { - return OrigPeerConnection.generateCertificate; - } - }); - } -} -function shimTrackEventTransceiver(window) { - // Add event.transceiver member over deprecated event.receiver - if (_typeof(window) === 'object' && window.RTCTrackEvent && 'receiver' in window.RTCTrackEvent.prototype && !('transceiver' in window.RTCTrackEvent.prototype)) { - Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', { - get: function get() { - return { - receiver: this.receiver - }; - } - }); - } -} -function shimCreateOfferLegacy(window) { - var origCreateOffer = window.RTCPeerConnection.prototype.createOffer; - window.RTCPeerConnection.prototype.createOffer = function createOffer(offerOptions) { - if (offerOptions) { - if (typeof offerOptions.offerToReceiveAudio !== 'undefined') { - // support bit values - offerOptions.offerToReceiveAudio = !!offerOptions.offerToReceiveAudio; - } - var audioTransceiver = this.getTransceivers().find(function (transceiver) { - return transceiver.receiver.track.kind === 'audio'; - }); - if (offerOptions.offerToReceiveAudio === false && audioTransceiver) { - if (audioTransceiver.direction === 'sendrecv') { - if (audioTransceiver.setDirection) { - audioTransceiver.setDirection('sendonly'); - } else { - audioTransceiver.direction = 'sendonly'; - } - } else if (audioTransceiver.direction === 'recvonly') { - if (audioTransceiver.setDirection) { - audioTransceiver.setDirection('inactive'); - } else { - audioTransceiver.direction = 'inactive'; - } - } - } else if (offerOptions.offerToReceiveAudio === true && !audioTransceiver) { - this.addTransceiver('audio', { - direction: 'recvonly' - }); - } - if (typeof offerOptions.offerToReceiveVideo !== 'undefined') { - // support bit values - offerOptions.offerToReceiveVideo = !!offerOptions.offerToReceiveVideo; - } - var videoTransceiver = this.getTransceivers().find(function (transceiver) { - return transceiver.receiver.track.kind === 'video'; - }); - if (offerOptions.offerToReceiveVideo === false && videoTransceiver) { - if (videoTransceiver.direction === 'sendrecv') { - if (videoTransceiver.setDirection) { - videoTransceiver.setDirection('sendonly'); - } else { - videoTransceiver.direction = 'sendonly'; - } - } else if (videoTransceiver.direction === 'recvonly') { - if (videoTransceiver.setDirection) { - videoTransceiver.setDirection('inactive'); - } else { - videoTransceiver.direction = 'inactive'; - } - } - } else if (offerOptions.offerToReceiveVideo === true && !videoTransceiver) { - this.addTransceiver('video', { - direction: 'recvonly' - }); - } - } - return origCreateOffer.apply(this, arguments); - }; -} -function shimAudioContext(window) { - if (_typeof(window) !== 'object' || window.AudioContext) { - return; - } - window.AudioContext = window.webkitAudioContext; -} - -},{"../utils":11}],11:[function(require,module,exports){ -/* - * Copyright (c) 2016 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. - */ -/* eslint-env node */ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.compactObject = compactObject; -exports.deprecated = deprecated; -exports.detectBrowser = detectBrowser; -exports.disableLog = disableLog; -exports.disableWarnings = disableWarnings; -exports.extractVersion = extractVersion; -exports.filterStats = filterStats; -exports.log = log; -exports.walkStats = walkStats; -exports.wrapPeerConnectionEvent = wrapPeerConnectionEvent; -function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } -function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); } -function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } -function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } -var logDisabled_ = true; -var deprecationWarnings_ = true; - -/** - * Extract browser version out of the provided user agent string. - * - * @param {!string} uastring userAgent string. - * @param {!string} expr Regular expression used as match criteria. - * @param {!number} pos position in the version string to be returned. - * @return {!number} browser version. - */ -function extractVersion(uastring, expr, pos) { - var match = uastring.match(expr); - return match && match.length >= pos && parseInt(match[pos], 10); -} - -// Wraps the peerconnection event eventNameToWrap in a function -// which returns the modified event object (or false to prevent -// the event). -function wrapPeerConnectionEvent(window, eventNameToWrap, wrapper) { - if (!window.RTCPeerConnection) { - return; - } - var proto = window.RTCPeerConnection.prototype; - var nativeAddEventListener = proto.addEventListener; - proto.addEventListener = function (nativeEventName, cb) { - if (nativeEventName !== eventNameToWrap) { - return nativeAddEventListener.apply(this, arguments); - } - var wrappedCallback = function wrappedCallback(e) { - var modifiedEvent = wrapper(e); - if (modifiedEvent) { - if (cb.handleEvent) { - cb.handleEvent(modifiedEvent); - } else { - cb(modifiedEvent); - } - } - }; - this._eventMap = this._eventMap || {}; - if (!this._eventMap[eventNameToWrap]) { - this._eventMap[eventNameToWrap] = new Map(); - } - this._eventMap[eventNameToWrap].set(cb, wrappedCallback); - return nativeAddEventListener.apply(this, [nativeEventName, wrappedCallback]); - }; - var nativeRemoveEventListener = proto.removeEventListener; - proto.removeEventListener = function (nativeEventName, cb) { - if (nativeEventName !== eventNameToWrap || !this._eventMap || !this._eventMap[eventNameToWrap]) { - return nativeRemoveEventListener.apply(this, arguments); - } - if (!this._eventMap[eventNameToWrap].has(cb)) { - return nativeRemoveEventListener.apply(this, arguments); - } - var unwrappedCb = this._eventMap[eventNameToWrap].get(cb); - this._eventMap[eventNameToWrap]["delete"](cb); - if (this._eventMap[eventNameToWrap].size === 0) { - delete this._eventMap[eventNameToWrap]; - } - if (Object.keys(this._eventMap).length === 0) { - delete this._eventMap; - } - return nativeRemoveEventListener.apply(this, [nativeEventName, unwrappedCb]); - }; - Object.defineProperty(proto, 'on' + eventNameToWrap, { - get: function get() { - return this['_on' + eventNameToWrap]; - }, - set: function set(cb) { - if (this['_on' + eventNameToWrap]) { - this.removeEventListener(eventNameToWrap, this['_on' + eventNameToWrap]); - delete this['_on' + eventNameToWrap]; - } - if (cb) { - this.addEventListener(eventNameToWrap, this['_on' + eventNameToWrap] = cb); - } - }, - enumerable: true, - configurable: true - }); -} -function disableLog(bool) { - if (typeof bool !== 'boolean') { - return new Error('Argument type: ' + _typeof(bool) + '. Please use a boolean.'); - } - logDisabled_ = bool; - return bool ? 'adapter.js logging disabled' : 'adapter.js logging enabled'; -} - -/** - * Disable or enable deprecation warnings - * @param {!boolean} bool set to true to disable warnings. - */ -function disableWarnings(bool) { - if (typeof bool !== 'boolean') { - return new Error('Argument type: ' + _typeof(bool) + '. Please use a boolean.'); - } - deprecationWarnings_ = !bool; - return 'adapter.js deprecation warnings ' + (bool ? 'disabled' : 'enabled'); -} -function log() { - if ((typeof window === "undefined" ? "undefined" : _typeof(window)) === 'object') { - if (logDisabled_) { - return; - } - if (typeof console !== 'undefined' && typeof console.log === 'function') { - console.log.apply(console, arguments); - } - } -} - -/** - * Shows a deprecation warning suggesting the modern and spec-compatible API. - */ -function deprecated(oldMethod, newMethod) { - if (!deprecationWarnings_) { - return; - } - console.warn(oldMethod + ' is deprecated, please use ' + newMethod + ' instead.'); -} - -/** - * Browser detector. - * - * @return {object} result containing browser and version - * properties. - */ -function detectBrowser(window) { - // Returned result object. - var result = { - browser: null, - version: null - }; - - // Fail early if it's not a browser - if (typeof window === 'undefined' || !window.navigator) { - result.browser = 'Not a browser.'; - return result; - } - var navigator = window.navigator; - if (navigator.mozGetUserMedia) { - // Firefox. - result.browser = 'firefox'; - result.version = extractVersion(navigator.userAgent, /Firefox\/(\d+)\./, 1); - } else if (navigator.webkitGetUserMedia || window.isSecureContext === false && window.webkitRTCPeerConnection) { - // Chrome, Chromium, Webview, Opera. - // Version matches Chrome/WebRTC version. - // Chrome 74 removed webkitGetUserMedia on http as well so we need the - // more complicated fallback to webkitRTCPeerConnection. - result.browser = 'chrome'; - result.version = extractVersion(navigator.userAgent, /Chrom(e|ium)\/(\d+)\./, 2); - } else if (window.RTCPeerConnection && navigator.userAgent.match(/AppleWebKit\/(\d+)\./)) { - // Safari. - result.browser = 'safari'; - result.version = extractVersion(navigator.userAgent, /AppleWebKit\/(\d+)\./, 1); - result.supportsUnifiedPlan = window.RTCRtpTransceiver && 'currentDirection' in window.RTCRtpTransceiver.prototype; - } else { - // Default fallthrough: not supported. - result.browser = 'Not a supported browser.'; - return result; - } - return result; -} - -/** - * Checks if something is an object. - * - * @param {*} val The something you want to check. - * @return true if val is an object, false otherwise. - */ -function isObject(val) { - return Object.prototype.toString.call(val) === '[object Object]'; -} - -/** - * Remove all empty objects and undefined values - * from a nested object -- an enhanced and vanilla version - * of Lodash's `compact`. - */ -function compactObject(data) { - if (!isObject(data)) { - return data; - } - return Object.keys(data).reduce(function (accumulator, key) { - var isObj = isObject(data[key]); - var value = isObj ? compactObject(data[key]) : data[key]; - var isEmptyObject = isObj && !Object.keys(value).length; - if (value === undefined || isEmptyObject) { - return accumulator; - } - return Object.assign(accumulator, _defineProperty({}, key, value)); - }, {}); -} - -/* iterates the stats graph recursively. */ -function walkStats(stats, base, resultSet) { - if (!base || resultSet.has(base.id)) { - return; - } - resultSet.set(base.id, base); - Object.keys(base).forEach(function (name) { - if (name.endsWith('Id')) { - walkStats(stats, stats.get(base[name]), resultSet); - } else if (name.endsWith('Ids')) { - base[name].forEach(function (id) { - walkStats(stats, stats.get(id), resultSet); - }); - } - }); -} - -/* filter getStats for a sender/receiver track. */ -function filterStats(result, track, outbound) { - var streamStatsType = outbound ? 'outbound-rtp' : 'inbound-rtp'; - var filteredResult = new Map(); - if (track === null) { - return filteredResult; - } - var trackStats = []; - result.forEach(function (value) { - if (value.type === 'track' && value.trackIdentifier === track.id) { - trackStats.push(value); - } - }); - trackStats.forEach(function (trackStat) { - result.forEach(function (stats) { - if (stats.type === streamStatsType && stats.trackId === trackStat.id) { - walkStats(result, stats, filteredResult); - } - }); - }); - return filteredResult; -} - -},{}],12:[function(require,module,exports){ -/* eslint-env node */ -'use strict'; - -// SDP helpers. - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - -var SDPUtils = {}; - -// Generate an alphanumeric identifier for cname or mids. -// TODO: use UUIDs instead? https://gist.github.com/jed/982883 -SDPUtils.generateIdentifier = function () { - return Math.random().toString(36).substring(2, 12); -}; - -// The RTCP CNAME used by all peerconnections from the same JS. -SDPUtils.localCName = SDPUtils.generateIdentifier(); - -// Splits SDP into lines, dealing with both CRLF and LF. -SDPUtils.splitLines = function (blob) { - return blob.trim().split('\n').map(function (line) { - return line.trim(); - }); -}; -// Splits SDP into sessionpart and mediasections. Ensures CRLF. -SDPUtils.splitSections = function (blob) { - var parts = blob.split('\nm='); - return parts.map(function (part, index) { - return (index > 0 ? 'm=' + part : part).trim() + '\r\n'; - }); -}; - -// Returns the session description. -SDPUtils.getDescription = function (blob) { - var sections = SDPUtils.splitSections(blob); - return sections && sections[0]; -}; - -// Returns the individual media sections. -SDPUtils.getMediaSections = function (blob) { - var sections = SDPUtils.splitSections(blob); - sections.shift(); - return sections; -}; - -// Returns lines that start with a certain prefix. -SDPUtils.matchPrefix = function (blob, prefix) { - return SDPUtils.splitLines(blob).filter(function (line) { - return line.indexOf(prefix) === 0; - }); -}; - -// Parses an ICE candidate line. Sample input: -// candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8 -// rport 55996" -// Input can be prefixed with a=. -SDPUtils.parseCandidate = function (line) { - var parts = void 0; - // Parse both variants. - if (line.indexOf('a=candidate:') === 0) { - parts = line.substring(12).split(' '); - } else { - parts = line.substring(10).split(' '); - } - - var candidate = { - foundation: parts[0], - component: { 1: 'rtp', 2: 'rtcp' }[parts[1]] || parts[1], - protocol: parts[2].toLowerCase(), - priority: parseInt(parts[3], 10), - ip: parts[4], - address: parts[4], // address is an alias for ip. - port: parseInt(parts[5], 10), - // skip parts[6] == 'typ' - type: parts[7] - }; - - for (var i = 8; i < parts.length; i += 2) { - switch (parts[i]) { - case 'raddr': - candidate.relatedAddress = parts[i + 1]; - break; - case 'rport': - candidate.relatedPort = parseInt(parts[i + 1], 10); - break; - case 'tcptype': - candidate.tcpType = parts[i + 1]; - break; - case 'ufrag': - candidate.ufrag = parts[i + 1]; // for backward compatibility. - candidate.usernameFragment = parts[i + 1]; - break; - default: - // extension handling, in particular ufrag. Don't overwrite. - if (candidate[parts[i]] === undefined) { - candidate[parts[i]] = parts[i + 1]; - } - break; - } - } - return candidate; -}; - -// Translates a candidate object into SDP candidate attribute. -// This does not include the a= prefix! -SDPUtils.writeCandidate = function (candidate) { - var sdp = []; - sdp.push(candidate.foundation); - - var component = candidate.component; - if (component === 'rtp') { - sdp.push(1); - } else if (component === 'rtcp') { - sdp.push(2); - } else { - sdp.push(component); - } - sdp.push(candidate.protocol.toUpperCase()); - sdp.push(candidate.priority); - sdp.push(candidate.address || candidate.ip); - sdp.push(candidate.port); - - var type = candidate.type; - sdp.push('typ'); - sdp.push(type); - if (type !== 'host' && candidate.relatedAddress && candidate.relatedPort) { - sdp.push('raddr'); - sdp.push(candidate.relatedAddress); - sdp.push('rport'); - sdp.push(candidate.relatedPort); - } - if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') { - sdp.push('tcptype'); - sdp.push(candidate.tcpType); - } - if (candidate.usernameFragment || candidate.ufrag) { - sdp.push('ufrag'); - sdp.push(candidate.usernameFragment || candidate.ufrag); - } - return 'candidate:' + sdp.join(' '); -}; - -// Parses an ice-options line, returns an array of option tags. -// Sample input: -// a=ice-options:foo bar -SDPUtils.parseIceOptions = function (line) { - return line.substring(14).split(' '); -}; - -// Parses a rtpmap line, returns RTCRtpCoddecParameters. Sample input: -// a=rtpmap:111 opus/48000/2 -SDPUtils.parseRtpMap = function (line) { - var parts = line.substring(9).split(' '); - var parsed = { - payloadType: parseInt(parts.shift(), 10) // was: id - }; - - parts = parts[0].split('/'); - - parsed.name = parts[0]; - parsed.clockRate = parseInt(parts[1], 10); // was: clockrate - parsed.channels = parts.length === 3 ? parseInt(parts[2], 10) : 1; - // legacy alias, got renamed back to channels in ORTC. - parsed.numChannels = parsed.channels; - return parsed; -}; - -// Generates a rtpmap line from RTCRtpCodecCapability or -// RTCRtpCodecParameters. -SDPUtils.writeRtpMap = function (codec) { - var pt = codec.payloadType; - if (codec.preferredPayloadType !== undefined) { - pt = codec.preferredPayloadType; - } - var channels = codec.channels || codec.numChannels || 1; - return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate + (channels !== 1 ? '/' + channels : '') + '\r\n'; -}; - -// Parses a extmap line (headerextension from RFC 5285). Sample input: -// a=extmap:2 urn:ietf:params:rtp-hdrext:toffset -// a=extmap:2/sendonly urn:ietf:params:rtp-hdrext:toffset -SDPUtils.parseExtmap = function (line) { - var parts = line.substring(9).split(' '); - return { - id: parseInt(parts[0], 10), - direction: parts[0].indexOf('/') > 0 ? parts[0].split('/')[1] : 'sendrecv', - uri: parts[1], - attributes: parts.slice(2).join(' ') - }; -}; - -// Generates an extmap line from RTCRtpHeaderExtensionParameters or -// RTCRtpHeaderExtension. -SDPUtils.writeExtmap = function (headerExtension) { - return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) + (headerExtension.direction && headerExtension.direction !== 'sendrecv' ? '/' + headerExtension.direction : '') + ' ' + headerExtension.uri + (headerExtension.attributes ? ' ' + headerExtension.attributes : '') + '\r\n'; -}; - -// Parses a fmtp line, returns dictionary. Sample input: -// a=fmtp:96 vbr=on;cng=on -// Also deals with vbr=on; cng=on -SDPUtils.parseFmtp = function (line) { - var parsed = {}; - var kv = void 0; - var parts = line.substring(line.indexOf(' ') + 1).split(';'); - for (var j = 0; j < parts.length; j++) { - kv = parts[j].trim().split('='); - parsed[kv[0].trim()] = kv[1]; - } - return parsed; -}; - -// Generates a fmtp line from RTCRtpCodecCapability or RTCRtpCodecParameters. -SDPUtils.writeFmtp = function (codec) { - var line = ''; - var pt = codec.payloadType; - if (codec.preferredPayloadType !== undefined) { - pt = codec.preferredPayloadType; - } - if (codec.parameters && Object.keys(codec.parameters).length) { - var params = []; - Object.keys(codec.parameters).forEach(function (param) { - if (codec.parameters[param] !== undefined) { - params.push(param + '=' + codec.parameters[param]); - } else { - params.push(param); - } - }); - line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n'; - } - return line; -}; - -// Parses a rtcp-fb line, returns RTCPRtcpFeedback object. Sample input: -// a=rtcp-fb:98 nack rpsi -SDPUtils.parseRtcpFb = function (line) { - var parts = line.substring(line.indexOf(' ') + 1).split(' '); - return { - type: parts.shift(), - parameter: parts.join(' ') - }; -}; - -// Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters. -SDPUtils.writeRtcpFb = function (codec) { - var lines = ''; - var pt = codec.payloadType; - if (codec.preferredPayloadType !== undefined) { - pt = codec.preferredPayloadType; - } - if (codec.rtcpFeedback && codec.rtcpFeedback.length) { - // FIXME: special handling for trr-int? - codec.rtcpFeedback.forEach(function (fb) { - lines += 'a=rtcp-fb:' + pt + ' ' + fb.type + (fb.parameter && fb.parameter.length ? ' ' + fb.parameter : '') + '\r\n'; - }); - } - return lines; -}; - -// Parses a RFC 5576 ssrc media attribute. Sample input: -// a=ssrc:3735928559 cname:something -SDPUtils.parseSsrcMedia = function (line) { - var sp = line.indexOf(' '); - var parts = { - ssrc: parseInt(line.substring(7, sp), 10) - }; - var colon = line.indexOf(':', sp); - if (colon > -1) { - parts.attribute = line.substring(sp + 1, colon); - parts.value = line.substring(colon + 1); - } else { - parts.attribute = line.substring(sp + 1); - } - return parts; -}; - -// Parse a ssrc-group line (see RFC 5576). Sample input: -// a=ssrc-group:semantics 12 34 -SDPUtils.parseSsrcGroup = function (line) { - var parts = line.substring(13).split(' '); - return { - semantics: parts.shift(), - ssrcs: parts.map(function (ssrc) { - return parseInt(ssrc, 10); - }) - }; -}; - -// Extracts the MID (RFC 5888) from a media section. -// Returns the MID or undefined if no mid line was found. -SDPUtils.getMid = function (mediaSection) { - var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:')[0]; - if (mid) { - return mid.substring(6); - } -}; - -// Parses a fingerprint line for DTLS-SRTP. -SDPUtils.parseFingerprint = function (line) { - var parts = line.substring(14).split(' '); - return { - algorithm: parts[0].toLowerCase(), // algorithm is case-sensitive in Edge. - value: parts[1].toUpperCase() // the definition is upper-case in RFC 4572. - }; -}; - -// Extracts DTLS parameters from SDP media section or sessionpart. -// FIXME: for consistency with other functions this should only -// get the fingerprint line as input. See also getIceParameters. -SDPUtils.getDtlsParameters = function (mediaSection, sessionpart) { - var lines = SDPUtils.matchPrefix(mediaSection + sessionpart, 'a=fingerprint:'); - // Note: a=setup line is ignored since we use the 'auto' role in Edge. - return { - role: 'auto', - fingerprints: lines.map(SDPUtils.parseFingerprint) - }; -}; - -// Serializes DTLS parameters to SDP. -SDPUtils.writeDtlsParameters = function (params, setupType) { - var sdp = 'a=setup:' + setupType + '\r\n'; - params.fingerprints.forEach(function (fp) { - sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n'; - }); - return sdp; -}; - -// Parses a=crypto lines into -// https://rawgit.com/aboba/edgertc/master/msortc-rs4.html#dictionary-rtcsrtpsdesparameters-members -SDPUtils.parseCryptoLine = function (line) { - var parts = line.substring(9).split(' '); - return { - tag: parseInt(parts[0], 10), - cryptoSuite: parts[1], - keyParams: parts[2], - sessionParams: parts.slice(3) - }; -}; - -SDPUtils.writeCryptoLine = function (parameters) { - return 'a=crypto:' + parameters.tag + ' ' + parameters.cryptoSuite + ' ' + (_typeof(parameters.keyParams) === 'object' ? SDPUtils.writeCryptoKeyParams(parameters.keyParams) : parameters.keyParams) + (parameters.sessionParams ? ' ' + parameters.sessionParams.join(' ') : '') + '\r\n'; -}; - -// Parses the crypto key parameters into -// https://rawgit.com/aboba/edgertc/master/msortc-rs4.html#rtcsrtpkeyparam* -SDPUtils.parseCryptoKeyParams = function (keyParams) { - if (keyParams.indexOf('inline:') !== 0) { - return null; - } - var parts = keyParams.substring(7).split('|'); - return { - keyMethod: 'inline', - keySalt: parts[0], - lifeTime: parts[1], - mkiValue: parts[2] ? parts[2].split(':')[0] : undefined, - mkiLength: parts[2] ? parts[2].split(':')[1] : undefined - }; -}; - -SDPUtils.writeCryptoKeyParams = function (keyParams) { - return keyParams.keyMethod + ':' + keyParams.keySalt + (keyParams.lifeTime ? '|' + keyParams.lifeTime : '') + (keyParams.mkiValue && keyParams.mkiLength ? '|' + keyParams.mkiValue + ':' + keyParams.mkiLength : ''); -}; - -// Extracts all SDES parameters. -SDPUtils.getCryptoParameters = function (mediaSection, sessionpart) { - var lines = SDPUtils.matchPrefix(mediaSection + sessionpart, 'a=crypto:'); - return lines.map(SDPUtils.parseCryptoLine); -}; - -// Parses ICE information from SDP media section or sessionpart. -// FIXME: for consistency with other functions this should only -// get the ice-ufrag and ice-pwd lines as input. -SDPUtils.getIceParameters = function (mediaSection, sessionpart) { - var ufrag = SDPUtils.matchPrefix(mediaSection + sessionpart, 'a=ice-ufrag:')[0]; - var pwd = SDPUtils.matchPrefix(mediaSection + sessionpart, 'a=ice-pwd:')[0]; - if (!(ufrag && pwd)) { - return null; - } - return { - usernameFragment: ufrag.substring(12), - password: pwd.substring(10) - }; -}; - -// Serializes ICE parameters to SDP. -SDPUtils.writeIceParameters = function (params) { - var sdp = 'a=ice-ufrag:' + params.usernameFragment + '\r\n' + 'a=ice-pwd:' + params.password + '\r\n'; - if (params.iceLite) { - sdp += 'a=ice-lite\r\n'; - } - return sdp; -}; - -// Parses the SDP media section and returns RTCRtpParameters. -SDPUtils.parseRtpParameters = function (mediaSection) { - var description = { - codecs: [], - headerExtensions: [], - fecMechanisms: [], - rtcp: [] - }; - var lines = SDPUtils.splitLines(mediaSection); - var mline = lines[0].split(' '); - description.profile = mline[2]; - for (var i = 3; i < mline.length; i++) { - // find all codecs from mline[3..] - var pt = mline[i]; - var rtpmapline = SDPUtils.matchPrefix(mediaSection, 'a=rtpmap:' + pt + ' ')[0]; - if (rtpmapline) { - var codec = SDPUtils.parseRtpMap(rtpmapline); - var fmtps = SDPUtils.matchPrefix(mediaSection, 'a=fmtp:' + pt + ' '); - // Only the first a=fmtp: is considered. - codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {}; - codec.rtcpFeedback = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-fb:' + pt + ' ').map(SDPUtils.parseRtcpFb); - description.codecs.push(codec); - // parse FEC mechanisms from rtpmap lines. - switch (codec.name.toUpperCase()) { - case 'RED': - case 'ULPFEC': - description.fecMechanisms.push(codec.name.toUpperCase()); - break; - default: - // only RED and ULPFEC are recognized as FEC mechanisms. - break; - } - } - } - SDPUtils.matchPrefix(mediaSection, 'a=extmap:').forEach(function (line) { - description.headerExtensions.push(SDPUtils.parseExtmap(line)); - }); - var wildcardRtcpFb = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-fb:* ').map(SDPUtils.parseRtcpFb); - description.codecs.forEach(function (codec) { - wildcardRtcpFb.forEach(function (fb) { - var duplicate = codec.rtcpFeedback.find(function (existingFeedback) { - return existingFeedback.type === fb.type && existingFeedback.parameter === fb.parameter; - }); - if (!duplicate) { - codec.rtcpFeedback.push(fb); - } - }); - }); - // FIXME: parse rtcp. - return description; -}; - -// Generates parts of the SDP media section describing the capabilities / -// parameters. -SDPUtils.writeRtpDescription = function (kind, caps) { - var sdp = ''; - - // Build the mline. - sdp += 'm=' + kind + ' '; - sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs. - sdp += ' ' + (caps.profile || 'UDP/TLS/RTP/SAVPF') + ' '; - sdp += caps.codecs.map(function (codec) { - if (codec.preferredPayloadType !== undefined) { - return codec.preferredPayloadType; - } - return codec.payloadType; - }).join(' ') + '\r\n'; - - sdp += 'c=IN IP4 0.0.0.0\r\n'; - sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n'; - - // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb. - caps.codecs.forEach(function (codec) { - sdp += SDPUtils.writeRtpMap(codec); - sdp += SDPUtils.writeFmtp(codec); - sdp += SDPUtils.writeRtcpFb(codec); - }); - var maxptime = 0; - caps.codecs.forEach(function (codec) { - if (codec.maxptime > maxptime) { - maxptime = codec.maxptime; - } - }); - if (maxptime > 0) { - sdp += 'a=maxptime:' + maxptime + '\r\n'; - } - - if (caps.headerExtensions) { - caps.headerExtensions.forEach(function (extension) { - sdp += SDPUtils.writeExtmap(extension); - }); - } - // FIXME: write fecMechanisms. - return sdp; -}; - -// Parses the SDP media section and returns an array of -// RTCRtpEncodingParameters. -SDPUtils.parseRtpEncodingParameters = function (mediaSection) { - var encodingParameters = []; - var description = SDPUtils.parseRtpParameters(mediaSection); - var hasRed = description.fecMechanisms.indexOf('RED') !== -1; - var hasUlpfec = description.fecMechanisms.indexOf('ULPFEC') !== -1; - - // filter a=ssrc:... cname:, ignore PlanB-msid - var ssrcs = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:').map(function (line) { - return SDPUtils.parseSsrcMedia(line); - }).filter(function (parts) { - return parts.attribute === 'cname'; - }); - var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc; - var secondarySsrc = void 0; - - var flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID').map(function (line) { - var parts = line.substring(17).split(' '); - return parts.map(function (part) { - return parseInt(part, 10); - }); - }); - if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) { - secondarySsrc = flows[0][1]; - } - - description.codecs.forEach(function (codec) { - if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) { - var encParam = { - ssrc: primarySsrc, - codecPayloadType: parseInt(codec.parameters.apt, 10) - }; - if (primarySsrc && secondarySsrc) { - encParam.rtx = { ssrc: secondarySsrc }; - } - encodingParameters.push(encParam); - if (hasRed) { - encParam = JSON.parse(JSON.stringify(encParam)); - encParam.fec = { - ssrc: primarySsrc, - mechanism: hasUlpfec ? 'red+ulpfec' : 'red' - }; - encodingParameters.push(encParam); - } - } - }); - if (encodingParameters.length === 0 && primarySsrc) { - encodingParameters.push({ - ssrc: primarySsrc - }); - } - - // we support both b=AS and b=TIAS but interpret AS as TIAS. - var bandwidth = SDPUtils.matchPrefix(mediaSection, 'b='); - if (bandwidth.length) { - if (bandwidth[0].indexOf('b=TIAS:') === 0) { - bandwidth = parseInt(bandwidth[0].substring(7), 10); - } else if (bandwidth[0].indexOf('b=AS:') === 0) { - // use formula from JSEP to convert b=AS to TIAS value. - bandwidth = parseInt(bandwidth[0].substring(5), 10) * 1000 * 0.95 - 50 * 40 * 8; - } else { - bandwidth = undefined; - } - encodingParameters.forEach(function (params) { - params.maxBitrate = bandwidth; - }); - } - return encodingParameters; -}; - -// parses http://draft.ortc.org/#rtcrtcpparameters* -SDPUtils.parseRtcpParameters = function (mediaSection) { - var rtcpParameters = {}; - - // Gets the first SSRC. Note that with RTX there might be multiple - // SSRCs. - var remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:').map(function (line) { - return SDPUtils.parseSsrcMedia(line); - }).filter(function (obj) { - return obj.attribute === 'cname'; - })[0]; - if (remoteSsrc) { - rtcpParameters.cname = remoteSsrc.value; - rtcpParameters.ssrc = remoteSsrc.ssrc; - } - - // Edge uses the compound attribute instead of reducedSize - // compound is !reducedSize - var rsize = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-rsize'); - rtcpParameters.reducedSize = rsize.length > 0; - rtcpParameters.compound = rsize.length === 0; - - // parses the rtcp-mux attrÑ–bute. - // Note that Edge does not support unmuxed RTCP. - var mux = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-mux'); - rtcpParameters.mux = mux.length > 0; - - return rtcpParameters; -}; - -SDPUtils.writeRtcpParameters = function (rtcpParameters) { - var sdp = ''; - if (rtcpParameters.reducedSize) { - sdp += 'a=rtcp-rsize\r\n'; - } - if (rtcpParameters.mux) { - sdp += 'a=rtcp-mux\r\n'; - } - if (rtcpParameters.ssrc !== undefined && rtcpParameters.cname) { - sdp += 'a=ssrc:' + rtcpParameters.ssrc + ' cname:' + rtcpParameters.cname + '\r\n'; - } - return sdp; -}; - -// parses either a=msid: or a=ssrc:... msid lines and returns -// the id of the MediaStream and MediaStreamTrack. -SDPUtils.parseMsid = function (mediaSection) { - var parts = void 0; - var spec = SDPUtils.matchPrefix(mediaSection, 'a=msid:'); - if (spec.length === 1) { - parts = spec[0].substring(7).split(' '); - return { stream: parts[0], track: parts[1] }; - } - var planB = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:').map(function (line) { - return SDPUtils.parseSsrcMedia(line); - }).filter(function (msidParts) { - return msidParts.attribute === 'msid'; - }); - if (planB.length > 0) { - parts = planB[0].value.split(' '); - return { stream: parts[0], track: parts[1] }; - } -}; - -// SCTP -// parses draft-ietf-mmusic-sctp-sdp-26 first and falls back -// to draft-ietf-mmusic-sctp-sdp-05 -SDPUtils.parseSctpDescription = function (mediaSection) { - var mline = SDPUtils.parseMLine(mediaSection); - var maxSizeLine = SDPUtils.matchPrefix(mediaSection, 'a=max-message-size:'); - var maxMessageSize = void 0; - if (maxSizeLine.length > 0) { - maxMessageSize = parseInt(maxSizeLine[0].substring(19), 10); - } - if (isNaN(maxMessageSize)) { - maxMessageSize = 65536; - } - var sctpPort = SDPUtils.matchPrefix(mediaSection, 'a=sctp-port:'); - if (sctpPort.length > 0) { - return { - port: parseInt(sctpPort[0].substring(12), 10), - protocol: mline.fmt, - maxMessageSize: maxMessageSize - }; - } - var sctpMapLines = SDPUtils.matchPrefix(mediaSection, 'a=sctpmap:'); - if (sctpMapLines.length > 0) { - var parts = sctpMapLines[0].substring(10).split(' '); - return { - port: parseInt(parts[0], 10), - protocol: parts[1], - maxMessageSize: maxMessageSize - }; - } -}; - -// SCTP -// outputs the draft-ietf-mmusic-sctp-sdp-26 version that all browsers -// support by now receiving in this format, unless we originally parsed -// as the draft-ietf-mmusic-sctp-sdp-05 format (indicated by the m-line -// protocol of DTLS/SCTP -- without UDP/ or TCP/) -SDPUtils.writeSctpDescription = function (media, sctp) { - var output = []; - if (media.protocol !== 'DTLS/SCTP') { - output = ['m=' + media.kind + ' 9 ' + media.protocol + ' ' + sctp.protocol + '\r\n', 'c=IN IP4 0.0.0.0\r\n', 'a=sctp-port:' + sctp.port + '\r\n']; - } else { - output = ['m=' + media.kind + ' 9 ' + media.protocol + ' ' + sctp.port + '\r\n', 'c=IN IP4 0.0.0.0\r\n', 'a=sctpmap:' + sctp.port + ' ' + sctp.protocol + ' 65535\r\n']; - } - if (sctp.maxMessageSize !== undefined) { - output.push('a=max-message-size:' + sctp.maxMessageSize + '\r\n'); - } - return output.join(''); -}; - -// Generate a session ID for SDP. -// https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-20#section-5.2.1 -// recommends using a cryptographically random +ve 64-bit value -// but right now this should be acceptable and within the right range -SDPUtils.generateSessionId = function () { - return Math.random().toString().substr(2, 22); -}; - -// Write boiler plate for start of SDP -// sessId argument is optional - if not supplied it will -// be generated randomly -// sessVersion is optional and defaults to 2 -// sessUser is optional and defaults to 'thisisadapterortc' -SDPUtils.writeSessionBoilerplate = function (sessId, sessVer, sessUser) { - var sessionId = void 0; - var version = sessVer !== undefined ? sessVer : 2; - if (sessId) { - sessionId = sessId; - } else { - sessionId = SDPUtils.generateSessionId(); - } - var user = sessUser || 'thisisadapterortc'; - // FIXME: sess-id should be an NTP timestamp. - return 'v=0\r\n' + 'o=' + user + ' ' + sessionId + ' ' + version + ' IN IP4 127.0.0.1\r\n' + 's=-\r\n' + 't=0 0\r\n'; -}; - -// Gets the direction from the mediaSection or the sessionpart. -SDPUtils.getDirection = function (mediaSection, sessionpart) { - // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv. - var lines = SDPUtils.splitLines(mediaSection); - for (var i = 0; i < lines.length; i++) { - switch (lines[i]) { - case 'a=sendrecv': - case 'a=sendonly': - case 'a=recvonly': - case 'a=inactive': - return lines[i].substring(2); - default: - // FIXME: What should happen here? - } - } - if (sessionpart) { - return SDPUtils.getDirection(sessionpart); - } - return 'sendrecv'; -}; - -SDPUtils.getKind = function (mediaSection) { - var lines = SDPUtils.splitLines(mediaSection); - var mline = lines[0].split(' '); - return mline[0].substring(2); -}; - -SDPUtils.isRejected = function (mediaSection) { - return mediaSection.split(' ', 2)[1] === '0'; -}; - -SDPUtils.parseMLine = function (mediaSection) { - var lines = SDPUtils.splitLines(mediaSection); - var parts = lines[0].substring(2).split(' '); - return { - kind: parts[0], - port: parseInt(parts[1], 10), - protocol: parts[2], - fmt: parts.slice(3).join(' ') - }; -}; - -SDPUtils.parseOLine = function (mediaSection) { - var line = SDPUtils.matchPrefix(mediaSection, 'o=')[0]; - var parts = line.substring(2).split(' '); - return { - username: parts[0], - sessionId: parts[1], - sessionVersion: parseInt(parts[2], 10), - netType: parts[3], - addressType: parts[4], - address: parts[5] - }; -}; - -// a very naive interpretation of a valid SDP. -SDPUtils.isValidSDP = function (blob) { - if (typeof blob !== 'string' || blob.length === 0) { - return false; - } - var lines = SDPUtils.splitLines(blob); - for (var i = 0; i < lines.length; i++) { - if (lines[i].length < 2 || lines[i].charAt(1) !== '=') { - return false; - } - // TODO: check the modifier a bit more. - } - return true; -}; - -// Expose public methods. -if ((typeof module === 'undefined' ? 'undefined' : _typeof(module)) === 'object') { - module.exports = SDPUtils; -} -},{}]},{},[1])(1) -}); diff --git a/testenv/js/janus.js b/testenv/js/janus.js index 4544ccfc..a158db07 100644 --- a/testenv/js/janus.js +++ b/testenv/js/janus.js @@ -1,4 +1,3 @@ -import "./adapter.js" "use strict"; /* @@ -25,1185 +24,1313 @@ import "./adapter.js" OTHER DEALINGS IN THE SOFTWARE. */ -// List of sessions -Janus.sessions = {}; - -Janus.isExtensionEnabled = function() { - if(navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) { - // No need for the extension, getDisplayMedia is supported - return true; +// eslint-disable-next-line no-unused-vars +var Janus = (function (factory) { + if (typeof define === 'function' && define.amd) { + define(factory); + } else if (typeof module === 'object' && module.exports) { + module.exports = factory(); + } else if (typeof window === 'object') { + return factory(); } - if(window.navigator.userAgent.match('Chrome')) { - var chromever = parseInt(window.navigator.userAgent.match(/Chrome\/(.*) /)[1], 10); - var maxver = 33; - if(window.navigator.userAgent.match('Linux')) - maxver = 35; // "known" crash in chrome 34 and 35 on linux - if(chromever >= 26 && chromever <= maxver) { - // Older versions of Chrome don't support this extension-based approach, so lie +}(function () { + + // List of sessions + Janus.sessions = new Map(); + + Janus.isExtensionEnabled = function() { + if(navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) { + // No need for the extension, getDisplayMedia is supported return true; } - return Janus.extension.isInstalled(); - } else { - // Firefox and others, no need for the extension (but this doesn't mean it will work) - return true; - } -}; + if(window.navigator.userAgent.match('Chrome')) { + let chromever = parseInt(window.navigator.userAgent.match(/Chrome\/(.*) /)[1], 10); + let maxver = 33; + if(window.navigator.userAgent.match('Linux')) + maxver = 35; // "known" crash in chrome 34 and 35 on linux + if(chromever >= 26 && chromever <= maxver) { + // Older versions of Chrome don't support this extension-based approach, so lie + return true; + } + return Janus.extension.isInstalled(); + } else { + // Firefox and others, no need for the extension (but this doesn't mean it will work) + return true; + } + }; -var defaultExtension = { - // Screensharing Chrome Extension ID - extensionId: 'hapfgfdkleiggjjpfpenajgdnfckjpaj', - isInstalled: function() { return document.querySelector('#janus-extension-installed') !== null; }, - getScreen: function (callback) { - var pending = window.setTimeout(function () { - var error = new Error('NavigatorUserMediaError'); - error.name = 'The required Chrome extension is not installed: click here to install it. (NOTE: this will need you to refresh the page)'; - return callback(error); - }, 1000); - this.cache[pending] = callback; - window.postMessage({ type: 'janusGetScreen', id: pending }, '*'); - }, - init: function () { - var cache = {}; - this.cache = cache; - // Wait for events from the Chrome Extension - window.addEventListener('message', function (event) { - if(event.origin != window.location.origin) - return; - if(event.data.type == 'janusGotScreen' && cache[event.data.id]) { - var callback = cache[event.data.id]; - delete cache[event.data.id]; - - if (event.data.sourceId === '') { - // user canceled - var error = new Error('NavigatorUserMediaError'); - error.name = 'You cancelled the request for permission, giving up...'; - callback(error); - } else { - callback(null, event.data.sourceId); + var defaultExtension = { + // Screensharing Chrome Extension ID + extensionId: 'hapfgfdkleiggjjpfpenajgdnfckjpaj', + isInstalled: function() { return document.querySelector('#janus-extension-installed') !== null; }, + getScreen: function (callback) { + let pending = window.setTimeout(function () { + let error = new Error('NavigatorUserMediaError'); + error.name = 'The required Chrome extension is not installed: click here to install it. (NOTE: this will need you to refresh the page)'; + return callback(error); + }, 1000); + this.cache[pending] = callback; + window.postMessage({ type: 'janusGetScreen', id: pending }, '*'); + }, + init: function () { + let cache = {}; + this.cache = cache; + // Wait for events from the Chrome Extension + window.addEventListener('message', function (event) { + if(event.origin != window.location.origin) + return; + if(event.data.type == 'janusGotScreen' && cache[event.data.id]) { + let callback = cache[event.data.id]; + delete cache[event.data.id]; + if(event.data.sourceId === '') { + // user canceled + let error = new Error('NavigatorUserMediaError'); + error.name = 'You cancelled the request for permission, giving up...'; + callback(error); + } else { + callback(null, event.data.sourceId); + } + } else if(event.data.type == 'janusGetScreenPending') { + console.log('clearing ', event.data.id); + window.clearTimeout(event.data.id); } - } else if (event.data.type == 'janusGetScreenPending') { - console.log('clearing ', event.data.id); - window.clearTimeout(event.data.id); - } - }); - } -}; - -Janus.useDefaultDependencies = function (deps) { - var f = (deps && deps.fetch) || fetch; - var p = (deps && deps.Promise) || Promise; - var socketCls = (deps && deps.WebSocket) || WebSocket; - - return { - newWebSocket: function(server, proto) { return new socketCls(server, proto); }, - extension: (deps && deps.extension) || defaultExtension, - isArray: function(arr) { return Array.isArray(arr); }, - webRTCAdapter: (deps && deps.adapter) || adapter, - httpAPICall: function(url, options) { - var fetchOptions = { - method: options.verb, - headers: { - 'Accept': 'application/json, text/plain, */*' - }, - cache: 'no-cache' - }; - if(options.verb === "POST") { - fetchOptions.headers['Content-Type'] = 'application/json'; - } - if(options.withCredentials !== undefined) { - fetchOptions.credentials = options.withCredentials === true ? 'include' : (options.withCredentials ? options.withCredentials : 'omit'); - } - if(options.body) { - fetchOptions.body = JSON.stringify(options.body); - } - - var fetching = f(url, fetchOptions).catch(function(error) { - return p.reject({message: 'Probably a network error, is the server down?', error: error}); }); + } + }; - /* + Janus.useDefaultDependencies = function (deps) { + let f = (deps && deps.fetch) || fetch; + let p = (deps && deps.Promise) || Promise; + let socketCls = (deps && deps.WebSocket) || WebSocket; + + return { + newWebSocket: function(server, proto) { return new socketCls(server, proto); }, + extension: (deps && deps.extension) || defaultExtension, + isArray: function(arr) { return Array.isArray(arr); }, + webRTCAdapter: (deps && deps.adapter) || adapter, + httpAPICall: function(url, options) { + let fetchOptions = { + method: options.verb, + headers: { + 'Accept': 'application/json, text/plain, */*' + }, + cache: 'no-cache' + }; + if(options.verb === "POST") { + fetchOptions.headers['Content-Type'] = 'application/json'; + } + if(typeof options.withCredentials !== 'undefined') { + fetchOptions.credentials = options.withCredentials === true ? 'include' : (options.withCredentials ? options.withCredentials : 'omit'); + } + if(options.body) { + fetchOptions.body = JSON.stringify(options.body); + } + + let fetching = f(url, fetchOptions).catch(function(error) { + return p.reject({message: 'Probably a network error, is the server down?', error: error}); + }); + + /* * fetch() does not natively support timeouts. * Work around this by starting a timeout manually, and racing it agains the fetch() to see which thing resolves first. */ - if(options.timeout) { - var timeout = new p(function(resolve, reject) { - var timerId = setTimeout(function() { - clearTimeout(timerId); - return reject({message: 'Request timed out', timeout: options.timeout}); - }, options.timeout); - }); - fetching = p.race([fetching, timeout]); - } + if(options.timeout) { + // eslint-disable-next-line no-unused-vars + let timeout = new p(function(resolve, reject) { + let timerId = setTimeout(function() { + clearTimeout(timerId); + return reject({message: 'Request timed out', timeout: options.timeout}); + }, options.timeout); + }); + fetching = p.race([fetching, timeout]); + } - fetching.then(function(response) { - if(response.ok) { - if(typeof(options.success) === typeof(Janus.noop)) { - return response.json().then(function(parsed) { - try { - options.success(parsed); - } catch(error) { - Janus.error('Unhandled httpAPICall success callback error', error); - } - }, function(error) { - return p.reject({message: 'Failed to parse response body', error: error, response: response}); - }); + fetching.then(function(response) { + if(response.ok) { + if(typeof(options.success) === typeof(Janus.noop)) { + return response.json().then(function(parsed) { + try { + options.success(parsed); + } catch(error) { + Janus.error('Unhandled httpAPICall success callback error', error); + } + }, function(error) { + return p.reject({message: 'Failed to parse response body', error: error, response: response}); + }); + } } - } - else { - return p.reject({message: 'API call failed', response: response}); - } - }).catch(function(error) { - if(typeof(options.error) === typeof(Janus.noop)) { - options.error(error.message || '<< internal error >>', error); - } - }); - - return fetching; - } - } -}; - -Janus.useOldDependencies = function (deps) { - var jq = (deps && deps.jQuery) || jQuery; - var socketCls = (deps && deps.WebSocket) || WebSocket; - return { - newWebSocket: function(server, proto) { return new socketCls(server, proto); }, - isArray: function(arr) { return jq.isArray(arr); }, - extension: (deps && deps.extension) || defaultExtension, - webRTCAdapter: (deps && deps.adapter) || adapter, - httpAPICall: function(url, options) { - var payload = options.body !== undefined ? { - contentType: 'application/json', - data: JSON.stringify(options.body) - } : {}; - var credentials = options.withCredentials !== undefined ? {xhrFields: {withCredentials: options.withCredentials}} : {}; - - return jq.ajax(jq.extend(payload, credentials, { - url: url, - type: options.verb, - cache: false, - dataType: 'json', - async: options.async, - timeout: options.timeout, - success: function(result) { - if(typeof(options.success) === typeof(Janus.noop)) { - options.success(result); + else { + return p.reject({message: 'API call failed', response: response}); } - }, - error: function(xhr, status, err) { + }).catch(function(error) { if(typeof(options.error) === typeof(Janus.noop)) { - options.error(status, err); + options.error(error.message || '<< internal error >>', error); } - } - })); + }); + + return fetching; + } } }; -}; -Janus.noop = function() {}; + Janus.useOldDependencies = function (deps) { + let jq = (deps && deps.jQuery) || jQuery; + let socketCls = (deps && deps.WebSocket) || WebSocket; + return { + newWebSocket: function(server, proto) { return new socketCls(server, proto); }, + isArray: function(arr) { return jq.isArray(arr); }, + extension: (deps && deps.extension) || defaultExtension, + webRTCAdapter: (deps && deps.adapter) || adapter, + httpAPICall: function(url, options) { + let payload = (typeof options.body !== 'undefined') ? { + contentType: 'application/json', + data: JSON.stringify(options.body) + } : {}; + let credentials = (typeof options.withCredentials !== 'undefined') ? {xhrFields: {withCredentials: options.withCredentials}} : {}; -Janus.dataChanDefaultLabel = "JanusDataChannel"; - -// Note: in the future we may want to change this, e.g., as was -// attempted in https://github.com/meetecho/janus-gateway/issues/1670 -Janus.endOfCandidates = null; - -// Stop all tracks from a given stream -Janus.stopAllTracks = function(stream) { - try { - // Try a MediaStreamTrack.stop() for each track - var tracks = stream.getTracks(); - for(var mst of tracks) { - Janus.log(mst); - if(mst) { - mst.stop(); + return jq.ajax(jq.extend(payload, credentials, { + url: url, + type: options.verb, + cache: false, + dataType: 'json', + async: options.async, + timeout: options.timeout, + success: function(result) { + if(typeof(options.success) === typeof(Janus.noop)) { + options.success(result); + } + }, + // eslint-disable-next-line no-unused-vars + error: function(xhr, status, err) { + if(typeof(options.error) === typeof(Janus.noop)) { + options.error(status, err); + } + } + })); } - } - } catch(e) { - // Do nothing if this fails - } -} + }; + }; -// Initialization -Janus.init = function(options) { - options = options || {}; - options.callback = (typeof options.callback == "function") ? options.callback : Janus.noop; - if(Janus.initDone) { - // Already initialized - options.callback(); - } else { - if(typeof console.log == "undefined") { - console.log = function() {}; - } - // Console logging (all debugging disabled by default) - Janus.trace = Janus.noop; - Janus.debug = Janus.noop; - Janus.vdebug = Janus.noop; - Janus.log = Janus.noop; - Janus.warn = Janus.noop; - Janus.error = Janus.noop; - if(options.debug === true || options.debug === "all") { - // Enable all debugging levels - Janus.trace = console.trace.bind(console); - Janus.debug = console.debug.bind(console); - Janus.vdebug = console.debug.bind(console); - Janus.log = console.log.bind(console); - Janus.warn = console.warn.bind(console); - Janus.error = console.error.bind(console); - } else if(Array.isArray(options.debug)) { - for(var d of options.debug) { - switch(d) { - case "trace": - Janus.trace = console.trace.bind(console); - break; - case "debug": - Janus.debug = console.debug.bind(console); - break; - case "vdebug": - Janus.vdebug = console.debug.bind(console); - break; - case "log": - Janus.log = console.log.bind(console); - break; - case "warn": - Janus.warn = console.warn.bind(console); - break; - case "error": - Janus.error = console.error.bind(console); - break; - default: - console.error("Unknown debugging option '" + d + "' (supported: 'trace', 'debug', 'vdebug', 'log', warn', 'error')"); - break; + // Helper function to convert a deprecated media object to a tracks array + Janus.mediaToTracks = function(media) { + let tracks = []; + if(!media) { + // Default is bidirectional audio and video, using default devices + tracks.push({ type: 'audio', capture: true, recv: true }); + tracks.push({ type: 'video', capture: true, recv: true }); + } else { + if(!media.keepAudio && media.audio !== false && ((typeof media.audio === 'undefined') || media.audio || media.audioSend || media.audioRecv || + media.addAudio || media.replaceAudio || media.removeAudio)) { + // We may need an audio track + let track = { type: 'audio' }; + if(media.removeAudio) { + track.remove = true; + } else { + if(media.addAudio) + track.add = true; + else if(media.replaceAudio) + track.replace = true; + // Check if we need to capture an audio device + if(media.audioSend !== false) + track.capture = media.audio || true; + // Check if we need to receive audio + if(media.audioRecv !== false) + track.recv = true; } + // Add an audio track if needed + if(track.remove || track.capture || track.recv) + tracks.push(track); + } + if(!media.keepVideo && media.video !== false && ((typeof media.video === 'undefined') || media.video || media.videoSend || media.videoRecv || + media.addVideo || media.replaceVideo || media.removeVideo)) { + // We may need a video track + let track = { type: 'video' }; + if(media.removeVideo) { + track.remove = true; + } else { + if(media.addVideo) + track.add = true; + else if(media.replaceVideo) + track.replace = true; + // Check if we need to capture a video device + if(media.videoSend !== false) { + track.capture = media.video || true; + if(['screen', 'window', 'desktop'].includes(track.capture)) { + // Change the type to 'screen' + track.type = 'screen'; + track.capture = { video: {} }; + // Check if there's constraints + if(media.screenshareFrameRate) + track.capture.frameRate = media.screenshareFrameRate; + if(media.screenshareHeight) + track.capture.height = media.screenshareHeight; + if(media.screenshareWidth) + track.capture.width = media.screenshareWidth; + } + } + // Check if we need to receive video + if(media.videoRecv !== false) + track.recv = true; + } + // Add a video track if needed + if(track.remove || track.capture || track.recv) + tracks.push(track); + } + if(media.data) { + // We need a data channel + tracks.push({ type: 'data' }); } } - Janus.log("Initializing library"); + // Done + return tracks; + }; - var usedDependencies = options.dependencies || Janus.useDefaultDependencies(); - Janus.isArray = usedDependencies.isArray; - Janus.webRTCAdapter = usedDependencies.webRTCAdapter; - Janus.httpAPICall = usedDependencies.httpAPICall; - Janus.newWebSocket = usedDependencies.newWebSocket; - Janus.extension = usedDependencies.extension; - Janus.extension.init(); - - // Helper method to enumerate devices - Janus.listDevices = function(callback, config) { - callback = (typeof callback == "function") ? callback : Janus.noop; - if (config == null) config = { audio: true, video: true }; - if(Janus.isGetUserMediaAvailable()) { - navigator.mediaDevices.getUserMedia(config) - .then(function(stream) { - navigator.mediaDevices.enumerateDevices().then(function(devices) { - Janus.debug(devices); - callback(devices); - // Get rid of the now useless stream - Janus.stopAllTracks(stream) - }); - }) - .catch(function(err) { - Janus.error(err); - callback([]); - }); + // Helper function to convert a track object to a set of constraints + Janus.trackConstraints = function(track) { + let constraints = {}; + if(!track || !track.capture) + return constraints; + if(track.type === 'audio') { + // Just put the capture part in the constraints + constraints.audio = track.capture; + } else if(track.type === 'video') { + // Check if one of the keywords was passed + if((track.simulcast || track.svc) && track.capture === true) + track.capture = 'hires'; + if(track.capture === true || typeof track.capture === 'object') { + // Use the provided capture object as video constraint + constraints.video = track.capture; } else { - Janus.warn("navigator.mediaDevices unavailable"); - callback([]); + let width = 0; + let height = 0; + if(track.capture === 'lowres') { + // Small resolution, 4:3 + width = 320; + height = 240; + } else if(track.capture === 'lowres-16:9') { + // Small resolution, 16:9 + width = 320; + height = 180; + } else if(track.capture === 'hires' || track.capture === 'hires-16:9' || track.capture === 'hdres') { + // High(HD) resolution is only 16:9 + width = 1280; + height = 720; + } else if(track.capture === 'fhdres') { + // Full HD resolution is only 16:9 + width = 1920; + height = 1080; + } else if(track.capture === '4kres') { + // 4K resolution is only 16:9 + width = 3840; + height = 2160; + } else if(track.capture === 'stdres') { + // Normal resolution, 4:3 + width = 640; + height = 480; + } else if(track.capture === 'stdres-16:9') { + // Normal resolution, 16:9 + width = 640; + height = 360; + } else { + Janus.log('Default video setting is stdres 4:3'); + width = 640; + height = 480; + } + constraints.video = { + width: { ideal: width }, + height: { ideal: height } + }; } - }; - // Helper methods to attach/reattach a stream to a video element (previously part of adapter.js) - Janus.attachMediaStream = function(element, stream) { - try { - element.srcObject = stream; - } catch (e) { - try { - element.src = URL.createObjectURL(stream); - } catch (e) { - Janus.error("Error attaching stream to element"); + } else if(track.type === 'screen') { + // Use the provided capture object as video constraint + constraints.video = track.capture; + } + return constraints; + }; + + Janus.noop = function() {}; + + Janus.dataChanDefaultLabel = "JanusDataChannel"; + + // Note: in the future we may want to change this, e.g., as was + // attempted in https://github.com/meetecho/janus-gateway/issues/1670 + Janus.endOfCandidates = null; + + // Stop all tracks from a given stream + Janus.stopAllTracks = function(stream) { + try { + // Try a MediaStreamTrack.stop() for each track + let tracks = stream.getTracks(); + for(let mst of tracks) { + Janus.log(mst); + if(mst && mst.dontStop !== true) { + mst.stop(); } } - }; - Janus.reattachMediaStream = function(to, from) { - try { - to.srcObject = from.srcObject; - } catch (e) { - try { - to.src = from.src; - } catch (e) { - Janus.error("Error reattaching stream to element"); - } + // eslint-disable-next-line no-unused-vars + } catch(e) { + // Do nothing if this fails + } + } + + // Initialization + Janus.init = function(options) { + options = options || {}; + options.callback = (typeof options.callback == "function") ? options.callback : Janus.noop; + if(Janus.initDone) { + // Already initialized + options.callback(); + } else { + if(typeof console.log == "undefined") { + console.log = function() {}; } - }; - // Detect tab close: make sure we don't loose existing onbeforeunload handlers - // (note: for iOS we need to subscribe to a different event, 'pagehide', see - // https://gist.github.com/thehunmonkgroup/6bee8941a49b86be31a787fe8f4b8cfe) - var iOS = ['iPad', 'iPhone', 'iPod'].indexOf(navigator.platform) >= 0; - var eventName = iOS ? 'pagehide' : 'beforeunload'; - var oldOBF = window["on" + eventName]; - window.addEventListener(eventName, function() { - Janus.log("Closing window"); - for(var s in Janus.sessions) { - if(Janus.sessions[s] && Janus.sessions[s].destroyOnUnload) { - Janus.log("Destroying session " + s); - Janus.sessions[s].destroy({unload: true, notifyDestroyed: false}); - } - } - if(oldOBF && typeof oldOBF == "function") { - oldOBF(); - } - }); - // If this is a Safari, check if VP8 or VP9 are supported - Janus.safariVp8 = false; - Janus.safariVp9 = false; - if(Janus.webRTCAdapter.browserDetails.browser === 'safari' && - Janus.webRTCAdapter.browserDetails.version >= 605) { - // Let's see if RTCRtpSender.getCapabilities() is there - if(RTCRtpSender && RTCRtpSender.getCapabilities && RTCRtpSender.getCapabilities("video") && - RTCRtpSender.getCapabilities("video").codecs && RTCRtpSender.getCapabilities("video").codecs.length) { - for(var codec of RTCRtpSender.getCapabilities("video").codecs) { - if(codec && codec.mimeType && codec.mimeType.toLowerCase() === "video/vp8") { - Janus.safariVp8 = true; - } else if(codec && codec.mimeType && codec.mimeType.toLowerCase() === "video/vp9") { - Janus.safariVp9 = true; + // Console logging (all debugging disabled by default) + Janus.trace = Janus.noop; + Janus.debug = Janus.noop; + Janus.vdebug = Janus.noop; + Janus.log = Janus.noop; + Janus.warn = Janus.noop; + Janus.error = Janus.noop; + if(options.debug === true || options.debug === "all") { + // Enable all debugging levels + Janus.trace = console.trace.bind(console); + Janus.debug = console.debug.bind(console); + Janus.vdebug = console.debug.bind(console); + Janus.log = console.log.bind(console); + Janus.warn = console.warn.bind(console); + Janus.error = console.error.bind(console); + } else if(Array.isArray(options.debug)) { + for(let d of options.debug) { + switch(d) { + case "trace": + Janus.trace = console.trace.bind(console); + break; + case "debug": + Janus.debug = console.debug.bind(console); + break; + case "vdebug": + Janus.vdebug = console.debug.bind(console); + break; + case "log": + Janus.log = console.log.bind(console); + break; + case "warn": + Janus.warn = console.warn.bind(console); + break; + case "error": + Janus.error = console.error.bind(console); + break; + default: + console.error("Unknown debugging option '" + d + "' (supported: 'trace', 'debug', 'vdebug', 'log', warn', 'error')"); + break; } } - if(Janus.safariVp8) { - Janus.log("This version of Safari supports VP8"); + } + Janus.log("Initializing library"); + + let usedDependencies = options.dependencies || Janus.useDefaultDependencies(); + Janus.isArray = usedDependencies.isArray; + Janus.webRTCAdapter = usedDependencies.webRTCAdapter; + Janus.httpAPICall = usedDependencies.httpAPICall; + Janus.newWebSocket = usedDependencies.newWebSocket; + Janus.extension = usedDependencies.extension; + Janus.extension.init(); + + // Helper method to enumerate devices + Janus.listDevices = function(callback, config) { + callback = (typeof callback == "function") ? callback : Janus.noop; + if(!config) + config = { audio: true, video: true }; + if(Janus.isGetUserMediaAvailable()) { + navigator.mediaDevices.getUserMedia(config) + .then(function(stream) { + navigator.mediaDevices.enumerateDevices().then(function(devices) { + Janus.debug(devices); + callback(devices); + // Get rid of the now useless stream + Janus.stopAllTracks(stream) + }); + }) + .catch(function(err) { + Janus.error(err); + callback([]); + }); } else { - Janus.warn("This version of Safari does NOT support VP8: if you're using a Technology Preview, " + - "try enabling the 'WebRTC VP8 codec' setting in the 'Experimental Features' Develop menu"); + Janus.warn("navigator.mediaDevices unavailable"); + callback([]); } - } else { - // We do it in a very ugly way, as there's no alternative... - // We create a PeerConnection to see if VP8 is in an offer - var testpc = new RTCPeerConnection({}); - testpc.createOffer({offerToReceiveVideo: true}).then(function(offer) { - Janus.safariVp8 = offer.sdp.indexOf("VP8") !== -1; - Janus.safariVp9 = offer.sdp.indexOf("VP9") !== -1; + }; + // Helper methods to attach/reattach a stream to a video element (previously part of adapter.js) + Janus.attachMediaStream = function(element, stream) { + try { + element.srcObject = stream; + // eslint-disable-next-line no-unused-vars + } catch (e) { + try { + element.src = URL.createObjectURL(stream); + } catch (e) { + Janus.error("Error attaching stream to element", e); + } + } + }; + Janus.reattachMediaStream = function(to, from) { + try { + to.srcObject = from.srcObject; + // eslint-disable-next-line no-unused-vars + } catch (e) { + try { + to.src = from.src; + } catch (e) { + Janus.error("Error reattaching stream to element", e); + } + } + }; + // Detect tab close: make sure we don't loose existing onbeforeunload handlers + // (note: for iOS we need to subscribe to a different event, 'pagehide', see + // https://gist.github.com/thehunmonkgroup/6bee8941a49b86be31a787fe8f4b8cfe) + let iOS = ['iPad', 'iPhone', 'iPod'].indexOf(navigator.platform) >= 0; + let eventName = iOS ? 'pagehide' : 'beforeunload'; + let oldOBF = window["on" + eventName]; + window.addEventListener(eventName, function() { + Janus.log("Closing window"); + for(const [sessionId, session] of Janus.sessions) { + if(session && session.destroyOnUnload) { + Janus.log("Destroying session " + sessionId); + session.destroy({unload: true, notifyDestroyed: false}); + } + } + if(oldOBF && typeof oldOBF == "function") { + oldOBF(); + } + }); + // If this is a Safari, check if VP8 or VP9 are supported + Janus.safariVp8 = false; + Janus.safariVp9 = false; + if(Janus.webRTCAdapter.browserDetails.browser === 'safari' && + Janus.webRTCAdapter.browserDetails.version >= 605) { + // Let's see if RTCRtpSender.getCapabilities() is there + if(RTCRtpSender && RTCRtpSender.getCapabilities && RTCRtpSender.getCapabilities("video") && + RTCRtpSender.getCapabilities("video").codecs && RTCRtpSender.getCapabilities("video").codecs.length) { + for(let codec of RTCRtpSender.getCapabilities("video").codecs) { + if(codec && codec.mimeType && codec.mimeType.toLowerCase() === "video/vp8") { + Janus.safariVp8 = true; + } else if(codec && codec.mimeType && codec.mimeType.toLowerCase() === "video/vp9") { + Janus.safariVp9 = true; + } + } if(Janus.safariVp8) { Janus.log("This version of Safari supports VP8"); } else { Janus.warn("This version of Safari does NOT support VP8: if you're using a Technology Preview, " + - "try enabling the 'WebRTC VP8 codec' setting in the 'Experimental Features' Develop menu"); + "try enabling the 'WebRTC VP8 codec' setting in the 'Experimental Features' Develop menu"); } - testpc.close(); - testpc = null; - }); - } - } - // Check if this browser supports Unified Plan and transceivers - // Based on https://codepen.io/anon/pen/ZqLwWV?editors=0010 - Janus.unifiedPlan = false; - if(Janus.webRTCAdapter.browserDetails.browser === 'firefox' && - Janus.webRTCAdapter.browserDetails.version >= 59) { - // Firefox definitely does, starting from version 59 - Janus.unifiedPlan = true; - } else if(Janus.webRTCAdapter.browserDetails.browser === 'chrome' && - Janus.webRTCAdapter.browserDetails.version >= 72) { - // Chrome does, but it's only usable from version 72 on - Janus.unifiedPlan = true; - } else if(!window.RTCRtpTransceiver || !('currentDirection' in RTCRtpTransceiver.prototype)) { - // Safari supports addTransceiver() but not Unified Plan when - // currentDirection is not defined (see codepen above). - Janus.unifiedPlan = false; - } else { - // Check if addTransceiver() throws an exception - var tempPc = new RTCPeerConnection(); - try { - tempPc.addTransceiver('audio'); - Janus.unifiedPlan = true; - } catch (e) {} - tempPc.close(); - } - Janus.initDone = true; - options.callback(); - } -}; - -// Helper method to check whether WebRTC is supported by this browser -Janus.isWebrtcSupported = function() { - return !!window.RTCPeerConnection; -}; -// Helper method to check whether devices can be accessed by this browser (e.g., not possible via plain HTTP) -Janus.isGetUserMediaAvailable = function() { - return navigator.mediaDevices && navigator.mediaDevices.getUserMedia; -}; - -// Helper method to create random identifiers (e.g., transaction) -Janus.randomString = function(len) { - var charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - var randomString = ''; - for (var i = 0; i < len; i++) { - var randomPoz = Math.floor(Math.random() * charSet.length); - randomString += charSet.substring(randomPoz,randomPoz+1); - } - return randomString; -}; - -export function Janus(gatewayCallbacks) { - gatewayCallbacks = gatewayCallbacks || {}; - gatewayCallbacks.success = (typeof gatewayCallbacks.success == "function") ? gatewayCallbacks.success : Janus.noop; - gatewayCallbacks.error = (typeof gatewayCallbacks.error == "function") ? gatewayCallbacks.error : Janus.noop; - gatewayCallbacks.destroyed = (typeof gatewayCallbacks.destroyed == "function") ? gatewayCallbacks.destroyed : Janus.noop; - if(!Janus.initDone) { - gatewayCallbacks.error("Library not initialized"); - return {}; - } - if(!Janus.isWebrtcSupported()) { - gatewayCallbacks.error("WebRTC not supported by this browser"); - return {}; - } - Janus.log("Library initialized: " + Janus.initDone); - if(!gatewayCallbacks.server) { - gatewayCallbacks.error("Invalid server url"); - return {}; - } - var websockets = false; - var ws = null; - var wsHandlers = {}; - var wsKeepaliveTimeoutId = null; - var servers = null; - var serversIndex = 0; - var server = gatewayCallbacks.server; - if(Janus.isArray(server)) { - Janus.log("Multiple servers provided (" + server.length + "), will use the first that works"); - server = null; - servers = gatewayCallbacks.server; - Janus.debug(servers); - } else { - if(server.indexOf("ws") === 0) { - websockets = true; - Janus.log("Using WebSockets to contact Janus: " + server); - } else { - websockets = false; - Janus.log("Using REST API to contact Janus: " + server); - } - } - var iceServers = gatewayCallbacks.iceServers || [{urls: "stun:stun.l.google.com:19302"}]; - var iceTransportPolicy = gatewayCallbacks.iceTransportPolicy; - var bundlePolicy = gatewayCallbacks.bundlePolicy; - // Whether IPv6 candidates should be gathered - var ipv6Support = (gatewayCallbacks.ipv6 === true); - // Whether we should enable the withCredentials flag for XHR requests - var withCredentials = false; - if(gatewayCallbacks.withCredentials !== undefined && gatewayCallbacks.withCredentials !== null) - withCredentials = gatewayCallbacks.withCredentials === true; - // Optional max events - var maxev = 10; - if(gatewayCallbacks.max_poll_events !== undefined && gatewayCallbacks.max_poll_events !== null) - maxev = gatewayCallbacks.max_poll_events; - if(maxev < 1) - maxev = 1; - // Token to use (only if the token based authentication mechanism is enabled) - var token = null; - if(gatewayCallbacks.token !== undefined && gatewayCallbacks.token !== null) - token = gatewayCallbacks.token; - // API secret to use (only if the shared API secret is enabled) - var apisecret = null; - if(gatewayCallbacks.apisecret !== undefined && gatewayCallbacks.apisecret !== null) - apisecret = gatewayCallbacks.apisecret; - // Whether we should destroy this session when onbeforeunload is called - this.destroyOnUnload = true; - if(gatewayCallbacks.destroyOnUnload !== undefined && gatewayCallbacks.destroyOnUnload !== null) - this.destroyOnUnload = (gatewayCallbacks.destroyOnUnload === true); - // Some timeout-related values - var keepAlivePeriod = 25000; - if(gatewayCallbacks.keepAlivePeriod !== undefined && gatewayCallbacks.keepAlivePeriod !== null) - keepAlivePeriod = gatewayCallbacks.keepAlivePeriod; - if(isNaN(keepAlivePeriod)) - keepAlivePeriod = 25000; - var longPollTimeout = 60000; - if(gatewayCallbacks.longPollTimeout !== undefined && gatewayCallbacks.longPollTimeout !== null) - longPollTimeout = gatewayCallbacks.longPollTimeout; - if(isNaN(longPollTimeout)) - longPollTimeout = 60000; - - // overrides for default maxBitrate values for simulcasting - function getMaxBitrates(simulcastMaxBitrates) { - var maxBitrates = { - high: 900000, - medium: 300000, - low: 100000, - }; - - if (simulcastMaxBitrates !== undefined && simulcastMaxBitrates !== null) { - if (simulcastMaxBitrates.high) - maxBitrates.high = simulcastMaxBitrates.high; - if (simulcastMaxBitrates.medium) - maxBitrates.medium = simulcastMaxBitrates.medium; - if (simulcastMaxBitrates.low) - maxBitrates.low = simulcastMaxBitrates.low; - } - - return maxBitrates; - } - - var connected = false; - var sessionId = null; - var pluginHandles = {}; - var that = this; - var retries = 0; - var transactions = {}; - createSession(gatewayCallbacks); - - // Public methods - this.getServer = function() { return server; }; - this.isConnected = function() { return connected; }; - this.reconnect = function(callbacks) { - callbacks = callbacks || {}; - callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; - callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; - callbacks["reconnect"] = true; - createSession(callbacks); - }; - this.getSessionId = function() { return sessionId; }; - this.getInfo = function(callbacks) { getInfo(callbacks); }; - this.destroy = function(callbacks) { destroySession(callbacks); }; - this.attach = function(callbacks) { createHandle(callbacks); }; - - function eventHandler() { - if(sessionId == null) - return; - Janus.debug('Long poll...'); - if(!connected) { - Janus.warn("Is the server down? (connected=false)"); - return; - } - var longpoll = server + "/" + sessionId + "?rid=" + new Date().getTime(); - if(maxev) - longpoll = longpoll + "&maxev=" + maxev; - if(token) - longpoll = longpoll + "&token=" + encodeURIComponent(token); - if(apisecret) - longpoll = longpoll + "&apisecret=" + encodeURIComponent(apisecret); - Janus.httpAPICall(longpoll, { - verb: 'GET', - withCredentials: withCredentials, - success: handleEvent, - timeout: longPollTimeout, - error: function(textStatus, errorThrown) { - Janus.error(textStatus + ":", errorThrown); - retries++; - if(retries > 3) { - // Did we just lose the server? :-( - connected = false; - gatewayCallbacks.error("Lost connection to the server (is it down?)"); - return; - } - eventHandler(); - } - }); - } - - // Private event handler: this will trigger plugin callbacks, if set - function handleEvent(json, skipTimeout) { - retries = 0; - if(!websockets && sessionId !== undefined && sessionId !== null && skipTimeout !== true) - eventHandler(); - if(!websockets && Janus.isArray(json)) { - // We got an array: it means we passed a maxev > 1, iterate on all objects - for(var i=0; i 3) { + // Did we just lose the server? :-( + connected = false; + gatewayCallbacks.error("Lost connection to the server (is it down?)"); + return; + } + eventHandler(); + } + }); + } + + // Private event handler: this will trigger plugin callbacks, if set + function handleEvent(json, skipTimeout) { + retries = 0; + if(!websockets && typeof sessionId !== 'undefined' && sessionId !== null && skipTimeout !== true) + eventHandler(); + if(!websockets && Janus.isArray(json)) { + // We got an array: it means we passed a maxev > 1, iterate on all objects + for(let i=0; i data channel: ' + dcState); - if(dcState === 'open') { - // Any pending messages to send? - if(config.dataChannel[label].pending && config.dataChannel[label].pending.length > 0) { - Janus.log("Sending pending messages on <" + label + ">:", config.dataChannel[label].pending.length); - for(var data of config.dataChannel[label].pending) { - Janus.log("Sending data on data channel <" + label + ">"); - Janus.debug(data); - config.dataChannel[label].send(data); - } - config.dataChannel[label].pending = []; - } - // Notify the open data channel - pluginHandle.ondataopen(label, protocol); - } - }; - var onDataChannelError = function(error) { - Janus.error('Got error on data channel:', error); - // TODO - }; - if(!incoming) { - // FIXME Add options (ordered, maxRetransmits, etc.) - var dcoptions = config.dataChannelOptions; - if(dcprotocol) - dcoptions.protocol = dcprotocol; - config.dataChannel[dclabel] = config.pc.createDataChannel(dclabel, dcoptions); - } else { - // The channel was created by Janus - config.dataChannel[dclabel] = incoming; - } - config.dataChannel[dclabel].onmessage = onDataChannelMessage; - config.dataChannel[dclabel].onopen = onDataChannelStateChange; - config.dataChannel[dclabel].onclose = onDataChannelStateChange; - config.dataChannel[dclabel].onerror = onDataChannelError; - config.dataChannel[dclabel].pending = []; - if(pendingData) - config.dataChannel[dclabel].pending.push(pendingData); - } - - // Private method to send a data channel message - function sendData(handleId, callbacks) { - callbacks = callbacks || {}; - callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; - callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle || !pluginHandle.webrtcStuff) { - Janus.warn("Invalid handle"); - callbacks.error("Invalid handle"); - return; - } - var config = pluginHandle.webrtcStuff; - var data = callbacks.text || callbacks.data; - if(!data) { - Janus.warn("Invalid data"); - callbacks.error("Invalid data"); - return; - } - var label = callbacks.label ? callbacks.label : Janus.dataChanDefaultLabel; - if(!config.dataChannel[label]) { - // Create new data channel and wait for it to open - createDataChannel(handleId, label, callbacks.protocol, false, data, callbacks.protocol); - callbacks.success(); - return; - } - if(config.dataChannel[label].readyState !== "open") { - config.dataChannel[label].pending.push(data); - callbacks.success(); - return; - } - Janus.log("Sending data on data channel <" + label + ">"); - Janus.debug(data); - config.dataChannel[label].send(data); - callbacks.success(); - } - - // Private method to send a DTMF tone - function sendDtmf(handleId, callbacks) { - callbacks = callbacks || {}; - callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; - callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle || !pluginHandle.webrtcStuff) { - Janus.warn("Invalid handle"); - callbacks.error("Invalid handle"); - return; - } - var config = pluginHandle.webrtcStuff; - if(!config.dtmfSender) { - // Create the DTMF sender the proper way, if possible - if(config.pc) { - var senders = config.pc.getSenders(); - var audioSender = senders.find(function(sender) { - return sender.track && sender.track.kind === 'audio'; - }); - if(!audioSender) { - Janus.warn("Invalid DTMF configuration (no audio track)"); - callbacks.error("Invalid DTMF configuration (no audio track)"); - return; - } - config.dtmfSender = audioSender.dtmf; - if(config.dtmfSender) { - Janus.log("Created DTMF Sender"); - config.dtmfSender.ontonechange = function(tone) { Janus.debug("Sent DTMF tone: " + tone.tone); }; - } - } - if(!config.dtmfSender) { - Janus.warn("Invalid DTMF configuration"); - callbacks.error("Invalid DTMF configuration"); + // Private method to send a message + function sendMessage(handleId, callbacks) { + callbacks = callbacks || {}; + callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; + callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; + if(!connected) { + Janus.warn("Is the server down? (connected=false)"); + callbacks.error("Is the server down? (connected=false)"); return; } - } - var dtmf = callbacks.dtmf; - if(!dtmf) { - Janus.warn("Invalid DTMF parameters"); - callbacks.error("Invalid DTMF parameters"); - return; - } - var tones = dtmf.tones; - if(!tones) { - Janus.warn("Invalid DTMF string"); - callbacks.error("Invalid DTMF string"); - return; - } - var duration = (typeof dtmf.duration === 'number') ? dtmf.duration : 500; // We choose 500ms as the default duration for a tone - var gap = (typeof dtmf.gap === 'number') ? dtmf.gap : 50; // We choose 50ms as the default gap between tones - Janus.debug("Sending DTMF string " + tones + " (duration " + duration + "ms, gap " + gap + "ms)"); - config.dtmfSender.insertDTMF(tones, duration, gap); - callbacks.success(); - } - - // Private method to destroy a plugin handle - function destroyHandle(handleId, callbacks) { - callbacks = callbacks || {}; - callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; - callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; - var noRequest = (callbacks.noRequest === true); - Janus.log("Destroying handle " + handleId + " (only-locally=" + noRequest + ")"); - cleanupWebrtc(handleId); - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle || pluginHandle.detached) { - // Plugin was already detached by Janus, calling detach again will return a handle not found error, so just exit here - delete pluginHandles[handleId]; - callbacks.success(); - return; - } - pluginHandle.detached = true; - if(noRequest) { - // We're only removing the handle locally - delete pluginHandles[handleId]; - callbacks.success(); - return; - } - if(!connected) { - Janus.warn("Is the server down? (connected=false)"); - callbacks.error("Is the server down? (connected=false)"); - return; - } - var request = { "janus": "detach", "transaction": Janus.randomString(12) }; - if(pluginHandle.token) - request["token"] = pluginHandle.token; - if(apisecret) - request["apisecret"] = apisecret; - if(websockets) { - request["session_id"] = sessionId; - request["handle_id"] = handleId; - ws.send(JSON.stringify(request)); - delete pluginHandles[handleId]; - callbacks.success(); - return; - } - Janus.httpAPICall(server + "/" + sessionId + "/" + handleId, { - verb: 'POST', - withCredentials: withCredentials, - body: request, - success: function(json) { - Janus.log("Destroyed handle:"); - Janus.debug(json); - if(json["janus"] !== "success") { - Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME - } - delete pluginHandles[handleId]; - callbacks.success(); - }, - error: function(textStatus, errorThrown) { - Janus.error(textStatus + ":", errorThrown); // FIXME - // We cleanup anyway - delete pluginHandles[handleId]; - callbacks.success(); + let pluginHandle = pluginHandles.get(handleId); + if(!pluginHandle || !pluginHandle.webrtcStuff) { + Janus.warn("Invalid handle"); + callbacks.error("Invalid handle"); + return; } - }); - } - - // WebRTC stuff - function streamsDone(handleId, jsep, media, callbacks, stream) { - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle || !pluginHandle.webrtcStuff) { - Janus.warn("Invalid handle"); - // Close all tracks if the given stream has been created internally - if(!callbacks.stream) { - Janus.stopAllTracks(stream); - } - callbacks.error("Invalid handle"); - return; - } - var config = pluginHandle.webrtcStuff; - Janus.debug("streamsDone:", stream); - if(stream) { - Janus.debug(" -- Audio tracks:", stream.getAudioTracks()); - Janus.debug(" -- Video tracks:", stream.getVideoTracks()); - } - // We're now capturing the new stream: check if we're updating or if it's a new thing - var addTracks = false; - if(!config.myStream || !media.update || (config.streamExternal && !media.replaceAudio && !media.replaceVideo)) { - config.myStream = stream; - addTracks = true; - } else { - // We only need to update the existing stream - if(((!media.update && isAudioSendEnabled(media)) || (media.update && (media.addAudio || media.replaceAudio))) && - stream.getAudioTracks() && stream.getAudioTracks().length) { - config.myStream.addTrack(stream.getAudioTracks()[0]); - if(Janus.unifiedPlan) { - // Use Transceivers - Janus.log((media.replaceAudio ? "Replacing" : "Adding") + " audio track:", stream.getAudioTracks()[0]); - var audioTransceiver = null; - const transceivers = config.pc.getTransceivers(); + let message = callbacks.message; + let jsep = callbacks.jsep; + let transaction = Janus.randomString(12); + let request = { "janus": "message", "body": message, "transaction": transaction }; + if(pluginHandle.token) + request["token"] = pluginHandle.token; + if(apisecret) + request["apisecret"] = apisecret; + if(jsep) { + request.jsep = { + type: jsep.type, + sdp: jsep.sdp + }; + if(jsep.e2ee) + request.jsep.e2ee = true; + if(jsep.rid_order === "hml" || jsep.rid_order === "lmh") + request.jsep.rid_order = jsep.rid_order; + if(jsep.force_relay) + request.jsep.force_relay = true; + // Check if there's SVC video streams to tell Janus about + let svc = null; + let config = pluginHandle.webrtcStuff; + if(config.pc) { + let transceivers = config.pc.getTransceivers(); if(transceivers && transceivers.length > 0) { - for(const t of transceivers) { - if((t.sender && t.sender.track && t.sender.track.kind === "audio") || - (t.receiver && t.receiver.track && t.receiver.track.kind === "audio")) { - audioTransceiver = t; - break; + for(let mindex in transceivers) { + let tr = transceivers[mindex]; + if(tr && tr.sender && tr.sender.track && tr.sender.track.kind === 'video') { + let params = tr.sender.getParameters(); + if(params && params.encodings && params.encodings.length === 1 && + params.encodings[0] && params.encodings[0].scalabilityMode) { + // This video stream uses SVC + if(!svc) + svc = []; + svc.push({ + mindex: parseInt(mindex), + mid: tr.mid, + svc: params.encodings[0].scalabilityMode + }); + } } } } - if(audioTransceiver && audioTransceiver.sender) { - audioTransceiver.sender.replaceTrack(stream.getAudioTracks()[0]); - } else { - config.pc.addTrack(stream.getAudioTracks()[0], stream); - } - } else { - Janus.log((media.replaceAudio ? "Replacing" : "Adding") + " audio track:", stream.getAudioTracks()[0]); - config.pc.addTrack(stream.getAudioTracks()[0], stream); } + if(svc) + request.jsep.svc = svc; } - if(((!media.update && isVideoSendEnabled(media)) || (media.update && (media.addVideo || media.replaceVideo))) && - stream.getVideoTracks() && stream.getVideoTracks().length) { - config.myStream.addTrack(stream.getVideoTracks()[0]); - if(Janus.unifiedPlan) { - // Use Transceivers - Janus.log((media.replaceVideo ? "Replacing" : "Adding") + " video track:", stream.getVideoTracks()[0]); - var videoTransceiver = null; - const transceivers = config.pc.getTransceivers(); - if(transceivers && transceivers.length > 0) { - for(const t of transceivers) { - if((t.sender && t.sender.track && t.sender.track.kind === "video") || - (t.receiver && t.receiver.track && t.receiver.track.kind === "video")) { - videoTransceiver = t; - break; - } + Janus.debug("Sending message to plugin (handle=" + handleId + "):"); + Janus.debug(request); + if(websockets) { + request["session_id"] = sessionId; + request["handle_id"] = handleId; + transactions.set(transaction, function(json) { + Janus.debug("Message sent!"); + Janus.debug(json); + if(json["janus"] === "success") { + // We got a success, must have been a synchronous transaction + let plugindata = json["plugindata"]; + if(!plugindata) { + Janus.warn("Request succeeded, but missing plugindata..."); + callbacks.success(); + return; } + Janus.log("Synchronous transaction successful (" + plugindata["plugin"] + ")"); + let data = plugindata["data"]; + Janus.debug(data); + callbacks.success(data); + return; + } else if(json["janus"] !== "ack") { + // Not a success and not an ack, must be an error + if(json["error"]) { + Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME + callbacks.error(json["error"].code + " " + json["error"].reason); + } else { + Janus.error("Unknown error"); // FIXME + callbacks.error("Unknown error"); + } + return; } - if(videoTransceiver && videoTransceiver.sender) { - videoTransceiver.sender.replaceTrack(stream.getVideoTracks()[0]); - } else { - config.pc.addTrack(stream.getVideoTracks()[0], stream); + // If we got here, the plugin decided to handle the request asynchronously + callbacks.success(); + }); + ws.send(JSON.stringify(request)); + return; + } + Janus.httpAPICall(server + "/" + sessionId + "/" + handleId, { + verb: 'POST', + withCredentials: withCredentials, + body: request, + success: function(json) { + Janus.debug("Message sent!"); + Janus.debug(json); + if(json["janus"] === "success") { + // We got a success, must have been a synchronous transaction + let plugindata = json["plugindata"]; + if(!plugindata) { + Janus.warn("Request succeeded, but missing plugindata..."); + callbacks.success(); + return; + } + Janus.log("Synchronous transaction successful (" + plugindata["plugin"] + ")"); + let data = plugindata["data"]; + Janus.debug(data); + callbacks.success(data); + return; + } else if(json["janus"] !== "ack") { + // Not a success and not an ack, must be an error + if(json["error"]) { + Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME + callbacks.error(json["error"].code + " " + json["error"].reason); + } else { + Janus.error("Unknown error"); // FIXME + callbacks.error("Unknown error"); + } + return; } - } else { - Janus.log((media.replaceVideo ? "Replacing" : "Adding") + " video track:", stream.getVideoTracks()[0]); - config.pc.addTrack(stream.getVideoTracks()[0], stream); + // If we got here, the plugin decided to handle the request asynchronously + callbacks.success(); + }, + error: function(textStatus, errorThrown) { + Janus.error(textStatus + ":", errorThrown); // FIXME + callbacks.error(textStatus + ": " + errorThrown); + } + }); + } + + // Private method to send a trickle candidate + function sendTrickleCandidate(handleId, candidate) { + if(!connected) { + Janus.warn("Is the server down? (connected=false)"); + return; + } + let pluginHandle = pluginHandles.get(handleId); + if(!pluginHandle || !pluginHandle.webrtcStuff) { + Janus.warn("Invalid handle"); + return; + } + let request = { "janus": "trickle", "candidate": candidate, "transaction": Janus.randomString(12) }; + if(pluginHandle.token) + request["token"] = pluginHandle.token; + if(apisecret) + request["apisecret"] = apisecret; + Janus.vdebug("Sending trickle candidate (handle=" + handleId + "):"); + Janus.vdebug(request); + if(websockets) { + request["session_id"] = sessionId; + request["handle_id"] = handleId; + ws.send(JSON.stringify(request)); + return; + } + Janus.httpAPICall(server + "/" + sessionId + "/" + handleId, { + verb: 'POST', + withCredentials: withCredentials, + body: request, + success: function(json) { + Janus.vdebug("Candidate sent!"); + Janus.vdebug(json); + if(json["janus"] !== "ack") { + Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME + return; + } + }, + error: function(textStatus, errorThrown) { + Janus.error(textStatus + ":", errorThrown); // FIXME + } + }); + } + + // Private method to create a data channel + function createDataChannel(handleId, dclabel, dcprotocol, incoming, pendingData) { + let pluginHandle = pluginHandles.get(handleId); + if(!pluginHandle || !pluginHandle.webrtcStuff) { + Janus.warn("Invalid handle"); + return; + } + let config = pluginHandle.webrtcStuff; + if(!config.pc) { + Janus.warn("Invalid PeerConnection"); + return; + } + let onDataChannelMessage = function(event) { + Janus.log('Received message on data channel:', event); + let label = event.target.label; + pluginHandle.ondata(event.data, label); + }; + let onDataChannelStateChange = function(event) { + Janus.log('Received state change on data channel:', event); + let label = event.target.label; + let protocol = event.target.protocol; + let dcState = config.dataChannel[label] ? config.dataChannel[label].readyState : "null"; + Janus.log('State change on <' + label + '> data channel: ' + dcState); + if(dcState === 'open') { + // Any pending messages to send? + if(config.dataChannel[label].pending && config.dataChannel[label].pending.length > 0) { + Janus.log("Sending pending messages on <" + label + ">:", config.dataChannel[label].pending.length); + for(let data of config.dataChannel[label].pending) { + Janus.log("Sending data on data channel <" + label + ">"); + Janus.debug(data); + config.dataChannel[label].send(data); + } + config.dataChannel[label].pending = []; + } + // Notify the open data channel + pluginHandle.ondataopen(label, protocol); + } + }; + let onDataChannelError = function(error) { + Janus.error('Got error on data channel:', error); + // TODO + }; + if(!incoming) { + // FIXME Add options (ordered, maxRetransmits, etc.) + let dcoptions = config.dataChannelOptions; + if(dcprotocol) + dcoptions.protocol = dcprotocol; + config.dataChannel[dclabel] = config.pc.createDataChannel(dclabel, dcoptions); + } else { + // The channel was created by Janus + config.dataChannel[dclabel] = incoming; + } + config.dataChannel[dclabel].onmessage = onDataChannelMessage; + config.dataChannel[dclabel].onopen = onDataChannelStateChange; + config.dataChannel[dclabel].onclose = onDataChannelStateChange; + config.dataChannel[dclabel].onerror = onDataChannelError; + config.dataChannel[dclabel].pending = []; + if(pendingData) + config.dataChannel[dclabel].pending.push(pendingData); + } + + // Private method to send a data channel message + function sendData(handleId, callbacks) { + callbacks = callbacks || {}; + callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; + callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; + let pluginHandle = pluginHandles.get(handleId); + if(!pluginHandle || !pluginHandle.webrtcStuff) { + Janus.warn("Invalid handle"); + callbacks.error("Invalid handle"); + return; + } + let config = pluginHandle.webrtcStuff; + let data = callbacks.text || callbacks.data; + if(!data) { + Janus.warn("Invalid data"); + callbacks.error("Invalid data"); + return; + } + let label = callbacks.label ? callbacks.label : Janus.dataChanDefaultLabel; + if(!config.dataChannel[label]) { + // Create new data channel and wait for it to open + createDataChannel(handleId, label, callbacks.protocol, false, data, callbacks.protocol); + callbacks.success(); + return; + } + if(config.dataChannel[label].readyState !== "open") { + config.dataChannel[label].pending.push(data); + callbacks.success(); + return; + } + Janus.log("Sending data on data channel <" + label + ">"); + Janus.debug(data); + config.dataChannel[label].send(data); + callbacks.success(); + } + + // Private method to send a DTMF tone + function sendDtmf(handleId, callbacks) { + callbacks = callbacks || {}; + callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; + callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; + let pluginHandle = pluginHandles.get(handleId); + if(!pluginHandle || !pluginHandle.webrtcStuff) { + Janus.warn("Invalid handle"); + callbacks.error("Invalid handle"); + return; + } + let config = pluginHandle.webrtcStuff; + if(!config.dtmfSender) { + // Create the DTMF sender the proper way, if possible + if(config.pc) { + let senders = config.pc.getSenders(); + let audioSender = senders.find(function(sender) { + return sender.track && sender.track.kind === 'audio'; + }); + if(!audioSender) { + Janus.warn("Invalid DTMF configuration (no audio track)"); + callbacks.error("Invalid DTMF configuration (no audio track)"); + return; + } + config.dtmfSender = audioSender.dtmf; + if(config.dtmfSender) { + Janus.log("Created DTMF Sender"); + config.dtmfSender.ontonechange = function(tone) { Janus.debug("Sent DTMF tone: " + tone.tone); }; + } + } + if(!config.dtmfSender) { + Janus.warn("Invalid DTMF configuration"); + callbacks.error("Invalid DTMF configuration"); + return; } } + let dtmf = callbacks.dtmf; + if(!dtmf) { + Janus.warn("Invalid DTMF parameters"); + callbacks.error("Invalid DTMF parameters"); + return; + } + let tones = dtmf.tones; + if(!tones) { + Janus.warn("Invalid DTMF string"); + callbacks.error("Invalid DTMF string"); + return; + } + let duration = (typeof dtmf.duration === 'number') ? dtmf.duration : 500; // We choose 500ms as the default duration for a tone + let gap = (typeof dtmf.gap === 'number') ? dtmf.gap : 50; // We choose 50ms as the default gap between tones + Janus.debug("Sending DTMF string " + tones + " (duration " + duration + "ms, gap " + gap + "ms)"); + config.dtmfSender.insertDTMF(tones, duration, gap); + callbacks.success(); } - // If we still need to create a PeerConnection, let's do that - if(!config.pc) { - var pc_config = {"iceServers": iceServers, "iceTransportPolicy": iceTransportPolicy, "bundlePolicy": bundlePolicy}; - if(Janus.webRTCAdapter.browserDetails.browser === "chrome") { - // For Chrome versions before 72, we force a plan-b semantic, and unified-plan otherwise - pc_config["sdpSemantics"] = (Janus.webRTCAdapter.browserDetails.version < 72) ? "plan-b" : "unified-plan"; + + // Private method to destroy a plugin handle + function destroyHandle(handleId, callbacks) { + callbacks = callbacks || {}; + callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; + callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; + let noRequest = (callbacks.noRequest === true); + Janus.log("Destroying handle " + handleId + " (only-locally=" + noRequest + ")"); + cleanupWebrtc(handleId); + let pluginHandle = pluginHandles.get(handleId); + if(!pluginHandle || pluginHandle.detached) { + // Plugin was already detached by Janus, calling detach again will return a handle not found error, so just exit here + pluginHandles.delete(handleId); + callbacks.success(); + return; } - var pc_constraints = {}; - if(Janus.webRTCAdapter.browserDetails.browser === "edge") { - // This is Edge, enable BUNDLE explicitly - pc_config.bundlePolicy = "max-bundle"; + pluginHandle.detached = true; + if(noRequest) { + // We're only removing the handle locally + pluginHandles.delete(handleId); + callbacks.success(); + return; } + if(!connected) { + Janus.warn("Is the server down? (connected=false)"); + callbacks.error("Is the server down? (connected=false)"); + return; + } + let request = { "janus": "detach", "transaction": Janus.randomString(12) }; + if(pluginHandle.token) + request["token"] = pluginHandle.token; + if(apisecret) + request["apisecret"] = apisecret; + if(websockets) { + request["session_id"] = sessionId; + request["handle_id"] = handleId; + ws.send(JSON.stringify(request)); + pluginHandles.delete(handleId); + callbacks.success(); + return; + } + Janus.httpAPICall(server + "/" + sessionId + "/" + handleId, { + verb: 'POST', + withCredentials: withCredentials, + body: request, + success: function(json) { + Janus.log("Destroyed handle:"); + Janus.debug(json); + if(json["janus"] !== "success") { + Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME + } + pluginHandles.delete(handleId); + callbacks.success(); + }, + error: function(textStatus, errorThrown) { + Janus.error(textStatus + ":", errorThrown); // FIXME + // We cleanup anyway + pluginHandles.delete(handleId); + callbacks.success(); + } + }); + } + + // WebRTC stuff + // Helper function to create a new PeerConnection, if we need one + function createPeerconnectionIfNeeded(handleId, callbacks) { + let pluginHandle = pluginHandles.get(handleId); + if(!pluginHandle || !pluginHandle.webrtcStuff) { + Janus.warn("Invalid handle"); + throw "Invalid handle"; + } + let config = pluginHandle.webrtcStuff; + if(config.pc) { + // Nothing to do, we have a PeerConnection already + return; + } + let pc_config = { + iceServers: iceServers, + iceTransportPolicy: iceTransportPolicy, + bundlePolicy: bundlePolicy + }; + pc_config.sdpSemantics = 'unified-plan'; // Check if a sender or receiver transform has been provided - if(RTCRtpSender && (RTCRtpSender.prototype.createEncodedStreams || - (RTCRtpSender.prototype.createEncodedAudioStreams && - RTCRtpSender.prototype.createEncodedVideoStreams)) && - (callbacks.senderTransforms || callbacks.receiverTransforms)) { - config.senderTransforms = callbacks.senderTransforms; - config.receiverTransforms = callbacks.receiverTransforms; - pc_config["forceEncodedAudioInsertableStreams"] = true; - pc_config["forceEncodedVideoInsertableStreams"] = true; - pc_config["encodedInsertableStreams"] = true; + let insertableStreams = false; + if(callbacks.tracks) { + for(let track of callbacks.tracks) { + if(track.transforms && (track.transforms.sender || track.transforms.receiver)) { + insertableStreams = true; + break; + } + } } - Janus.log("Creating PeerConnection"); - Janus.debug(pc_constraints); - config.pc = new RTCPeerConnection(pc_config, pc_constraints); + if(callbacks.externalEncryption) { + insertableStreams = true; + config.externalEncryption = true; + } + if(RTCRtpSender && (RTCRtpSender.prototype.createEncodedStreams || + (RTCRtpSender.prototype.createEncodedAudioStreams && + RTCRtpSender.prototype.createEncodedVideoStreams)) && insertableStreams) { + config.insertableStreams = true; + pc_config.forceEncodedAudioInsertableStreams = true; + pc_config.forceEncodedVideoInsertableStreams = true; + pc_config.encodedInsertableStreams = true; + } + Janus.log('Creating PeerConnection'); + config.pc = new RTCPeerConnection(pc_config); Janus.debug(config.pc); if(config.pc.getStats) { // FIXME config.volume = {}; - config.bitrate.value = "0 kbits/sec"; + config.bitrate.value = '0 kbits/sec'; } - Janus.log("Preparing local SDP and gathering candidates (trickle=" + config.trickle + ")"); + Janus.log('Preparing local SDP and gathering candidates (trickle=' + config.trickle + ')'); + config.pc.onconnectionstatechange = function() { + if(config.pc) + pluginHandle.connectionState(config.pc.connectionState); + }; config.pc.oniceconnectionstatechange = function() { if(config.pc) pluginHandle.iceState(config.pc.iceConnectionState); }; config.pc.onicecandidate = function(event) { - if (!event.candidate || - (Janus.webRTCAdapter.browserDetails.browser === 'edge' && event.candidate.candidate.indexOf('endOfCandidates') > 0)) { - Janus.log("End of candidates."); + if(!event.candidate || (event.candidate.candidate && event.candidate.candidate.indexOf('endOfCandidates') > 0)) { + Janus.log('End of candidates.'); config.iceDone = true; if(config.trickle === true) { // Notify end of candidates - sendTrickleCandidate(handleId, {"completed": true}); + sendTrickleCandidate(handleId, { completed : true }); } else { // No trickle, time to send the complete SDP (including all candidates) sendSDP(handleId, callbacks); @@ -1870,10 +1950,10 @@ export function Janus(gatewayCallbacks) { } else { // JSON.stringify doesn't work on some WebRTC objects anymore // See https://code.google.com/p/chromium/issues/detail?id=467366 - var candidate = { - "candidate": event.candidate.candidate, - "sdpMid": event.candidate.sdpMid, - "sdpMLineIndex": event.candidate.sdpMLineIndex + let candidate = { + candidate: event.candidate.candidate, + sdpMid: event.candidate.sdpMid, + sdpMLineIndex: event.candidate.sdpMLineIndex }; if(config.trickle === true) { // Send candidate @@ -1882,1770 +1962,1245 @@ export function Janus(gatewayCallbacks) { } }; config.pc.ontrack = function(event) { - Janus.log("Handling Remote Track"); - Janus.debug(event); + Janus.log('Handling Remote Track', event); if(!event.streams) return; - config.remoteStream = event.streams[0]; - pluginHandle.onremotestream(config.remoteStream); + if(!event.track) + return; + // Notify about the new track event + let mid = event.transceiver ? event.transceiver.mid : event.track.id; + try { + if(event.transceiver && event.transceiver.mid && event.track.id) { + // Keep track of the mapping between track ID and mid, since + // when a track is removed the transceiver may be gone already + if(!pluginHandle.mids) + pluginHandle.mids = {}; + pluginHandle.mids[event.track.id] = event.transceiver.mid; + } + pluginHandle.onremotetrack(event.track, mid, true, { reason: 'created' }); + } catch(e) { + Janus.error("Error calling onremotetrack", e); + } if(event.track.onended) return; - if(config.receiverTransforms) { - var receiverStreams = null; - if(RTCRtpSender.prototype.createEncodedStreams) { - receiverStreams = event.receiver.createEncodedStreams(); - } else if(RTCRtpSender.prototype.createAudioEncodedStreams || RTCRtpSender.prototype.createEncodedVideoStreams) { - if(event.track.kind === "audio" && config.receiverTransforms["audio"]) { - receiverStreams = event.receiver.createEncodedAudioStreams(); - } else if(event.track.kind === "video" && config.receiverTransforms["video"]) { - receiverStreams = event.receiver.createEncodedVideoStreams(); - } - } - if(receiverStreams) { - console.log(receiverStreams); - if(receiverStreams.readableStream && receiverStreams.writableStream) { - receiverStreams.readableStream - .pipeThrough(config.receiverTransforms[event.track.kind]) - .pipeTo(receiverStreams.writableStream); - } else if(receiverStreams.readable && receiverStreams.writable) { - receiverStreams.readable - .pipeThrough(config.receiverTransforms[event.track.kind]) - .pipeTo(receiverStreams.writable); - } - } - } - var trackMutedTimeoutId = null; - Janus.log("Adding onended callback to track:", event.track); + let trackMutedTimeoutId = null; + Janus.log('Adding onended callback to track:', event.track); event.track.onended = function(ev) { - Janus.log("Remote track removed:", ev); - if(config.remoteStream) { - clearTimeout(trackMutedTimeoutId); - config.remoteStream.removeTrack(ev.target); - pluginHandle.onremotestream(config.remoteStream); + Janus.log('Remote track removed:', ev); + clearTimeout(trackMutedTimeoutId); + // Notify the application + let transceivers = config.pc ? config.pc.getTransceivers() : null; + let transceiver = transceivers ? transceivers.find( + t => t.receiver.track === ev.target) : null; + let mid = transceiver ? transceiver.mid : ev.target.id; + if(mid === ev.target.id && pluginHandle.mids && pluginHandle.mids[event.track.id]) + mid = pluginHandle.mids[event.track.id]; + try { + pluginHandle.onremotetrack(ev.target, mid, false, { reason: 'ended' }); + } catch(e) { + Janus.error("Error calling onremotetrack on removal", e); } + delete pluginHandle.mids[event.track.id]; }; event.track.onmute = function(ev) { - Janus.log("Remote track muted:", ev); - if(config.remoteStream && trackMutedTimeoutId == null) { + Janus.log('Remote track muted:', ev); + if(!trackMutedTimeoutId) { trackMutedTimeoutId = setTimeout(function() { - Janus.log("Removing remote track"); - if (config.remoteStream) { - config.remoteStream.removeTrack(ev.target); - pluginHandle.onremotestream(config.remoteStream); + Janus.log('Removing remote track'); + // Notify the application the track is gone + let transceivers = config.pc ? config.pc.getTransceivers() : null; + let transceiver = transceivers ? transceivers.find( + t => t.receiver.track === ev.target) : null; + let mid = transceiver ? transceiver.mid : ev.target.id; + if(mid === ev.target.id && pluginHandle.mids && pluginHandle.mids[event.track.id]) + mid = pluginHandle.mids[event.track.id]; + try { + pluginHandle.onremotetrack(ev.target, mid, false, { reason: 'mute' } ); + } catch(e) { + Janus.error("Error calling onremotetrack on mute", e); } trackMutedTimeoutId = null; - // Chrome seems to raise mute events only at multiples of 834ms; - // we set the timeout to three times this value (rounded to 840ms) + // Chrome seems to raise mute events only at multiples of 834ms; + // we set the timeout to three times this value (rounded to 840ms) }, 3 * 840); } }; event.track.onunmute = function(ev) { - Janus.log("Remote track flowing again:", ev); + Janus.log('Remote track flowing again:', ev); if(trackMutedTimeoutId != null) { clearTimeout(trackMutedTimeoutId); trackMutedTimeoutId = null; } else { try { - config.remoteStream.addTrack(ev.target); - pluginHandle.onremotestream(config.remoteStream); + // Notify the application the track is back + let transceivers = config.pc ? config.pc.getTransceivers() : null; + let transceiver = transceivers ? transceivers.find( + t => t.receiver.track === ev.target) : null; + let mid = transceiver ? transceiver.mid : ev.target.id; + pluginHandle.onremotetrack(ev.target, mid, true, { reason: 'unmute' }); } catch(e) { - Janus.error(e); + Janus.error("Error calling onremotetrack on unmute", e); } } }; }; } - if(addTracks && stream) { - Janus.log('Adding local stream'); - var simulcast = (callbacks.simulcast === true || callbacks.simulcast2 === true) && Janus.unifiedPlan; - var svc = callbacks.svc; - stream.getTracks().forEach(function(track) { - Janus.log('Adding local track:', track); - var sender = null; - if((!simulcast && !svc) || track.kind === 'audio') { - sender = config.pc.addTrack(track, stream); - } else if(simulcast) { - Janus.log('Enabling rid-based simulcasting:', track); - let maxBitrates = getMaxBitrates(callbacks.simulcastMaxBitrates); - let tr = config.pc.addTransceiver(track, { - direction: "sendrecv", - streams: [stream], - sendEncodings: callbacks.sendEncodings || [ - { rid: "h", active: true, maxBitrate: maxBitrates.high }, - { rid: "m", active: true, maxBitrate: maxBitrates.medium, scaleResolutionDownBy: 2 }, - { rid: "l", active: true, maxBitrate: maxBitrates.low, scaleResolutionDownBy: 4 } - ] - }); - if(tr) - sender = tr.sender; - } else { - Janus.log('Enabling SVC (' + svc + '):', track); - let tr = config.pc.addTransceiver(track, { - direction: "sendrecv", - streams: [stream], - sendEncodings: [ - { scalabilityMode: svc } - ] - }); - if(tr) - sender = tr.sender; - } - // Check if insertable streams are involved - if(sender && config.senderTransforms) { - var senderStreams = null; - if(RTCRtpSender.prototype.createEncodedStreams) { - senderStreams = sender.createEncodedStreams(); - } else if(RTCRtpSender.prototype.createAudioEncodedStreams || RTCRtpSender.prototype.createEncodedVideoStreams) { - if(sender.track.kind === "audio" && config.senderTransforms["audio"]) { - senderStreams = sender.createEncodedAudioStreams(); - } else if(sender.track.kind === "video" && config.senderTransforms["video"]) { - senderStreams = sender.createEncodedVideoStreams(); - } - } - if(senderStreams) { - console.log(senderStreams); - if(senderStreams.readableStream && senderStreams.writableStream) { - senderStreams.readableStream - .pipeThrough(config.senderTransforms[sender.track.kind]) - .pipeTo(senderStreams.writableStream); - } else if(senderStreams.readable && senderStreams.writable) { - senderStreams.readable - .pipeThrough(config.senderTransforms[sender.track.kind]) - .pipeTo(senderStreams.writable); - } - } - } - }); - } - // Any data channel to create? - if(isDataEnabled(media) && !config.dataChannel[Janus.dataChanDefaultLabel]) { - Janus.log("Creating default data channel"); - createDataChannel(handleId, Janus.dataChanDefaultLabel, null, false); - config.pc.ondatachannel = function(event) { - Janus.log("Data channel created by Janus:", event); - createDataChannel(handleId, event.channel.label, event.channel.protocol, event.channel); - }; - } - // If there's a new local stream, let's notify the application - if(config.myStream) { - pluginHandle.onlocalstream(config.myStream); - } - // Create offer/answer now - if(!jsep) { - createOffer(handleId, media, callbacks); - } else { - config.pc.setRemoteDescription(jsep) - .then(function() { - Janus.log("Remote description accepted!"); - config.remoteSdp = jsep.sdp; - // Any trickle candidate we cached? - if(config.candidates && config.candidates.length > 0) { - for(var i = 0; i< config.candidates.length; i++) { - var candidate = config.candidates[i]; - Janus.debug("Adding remote candidate:", candidate); - if(!candidate || candidate.completed === true) { - // end-of-candidates - config.pc.addIceCandidate(Janus.endOfCandidates); - } else { - // New candidate - config.pc.addIceCandidate(candidate); - } - } - config.candidates = []; - } - // Create the answer now - createAnswer(handleId, media, callbacks); - }, callbacks.error); - } - } - function prepareWebrtc(handleId, offer, callbacks) { - callbacks = callbacks || {}; - callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; - callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : webrtcError; - var jsep = callbacks.jsep; - if(offer && jsep) { - Janus.error("Provided a JSEP to a createOffer"); - callbacks.error("Provided a JSEP to a createOffer"); - return; - } else if(!offer && (!jsep || !jsep.type || !jsep.sdp)) { - Janus.error("A valid JSEP is required for createAnswer"); - callbacks.error("A valid JSEP is required for createAnswer"); - return; - } - /* Check that callbacks.media is a (not null) Object */ - callbacks.media = (typeof callbacks.media === 'object' && callbacks.media) ? callbacks.media : { audio: true, video: true }; - var media = callbacks.media; - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle || !pluginHandle.webrtcStuff) { - Janus.warn("Invalid handle"); - callbacks.error("Invalid handle"); - return; - } - var config = pluginHandle.webrtcStuff; - config.trickle = isTrickleEnabled(callbacks.trickle); - // Are we updating a session? - if(!config.pc) { - // Nope, new PeerConnection - media.update = false; - media.keepAudio = false; - media.keepVideo = false; - } else { - Janus.log("Updating existing media session"); - media.update = true; - // Check if there's anything to add/remove/replace, or if we - // can go directly to preparing the new SDP offer or answer - if(callbacks.stream) { - // External stream: is this the same as the one we were using before? - if(callbacks.stream !== config.myStream) { - Janus.log("Renegotiation involves a new external stream"); - } - } else { - // Check if there are changes on audio - if(media.addAudio) { - media.keepAudio = false; - media.replaceAudio = false; - media.removeAudio = false; - media.audioSend = true; - if(config.myStream && config.myStream.getAudioTracks() && config.myStream.getAudioTracks().length) { - Janus.error("Can't add audio stream, there already is one"); - callbacks.error("Can't add audio stream, there already is one"); - return; - } - } else if(media.removeAudio) { - media.keepAudio = false; - media.replaceAudio = false; - media.addAudio = false; - media.audioSend = false; - } else if(media.replaceAudio) { - media.keepAudio = false; - media.addAudio = false; - media.removeAudio = false; - media.audioSend = true; - } - if(!config.myStream) { - // No media stream: if we were asked to replace, it's actually an "add" - if(media.replaceAudio) { - media.keepAudio = false; - media.replaceAudio = false; - media.addAudio = true; - media.audioSend = true; - } - if(isAudioSendEnabled(media)) { - media.keepAudio = false; - media.addAudio = true; - } - } else { - if(!config.myStream.getAudioTracks() || config.myStream.getAudioTracks().length === 0) { - // No audio track: if we were asked to replace, it's actually an "add" - if(media.replaceAudio) { - media.keepAudio = false; - media.replaceAudio = false; - media.addAudio = true; - media.audioSend = true; - } - if(isAudioSendEnabled(media)) { - media.keepAudio = false; - media.addAudio = true; - } - } else { - // We have an audio track: should we keep it as it is? - if(isAudioSendEnabled(media) && - !media.removeAudio && !media.replaceAudio) { - media.keepAudio = true; - } - } - } - // Check if there are changes on video - if(media.addVideo) { - media.keepVideo = false; - media.replaceVideo = false; - media.removeVideo = false; - media.videoSend = true; - if(config.myStream && config.myStream.getVideoTracks() && config.myStream.getVideoTracks().length) { - Janus.error("Can't add video stream, there already is one"); - callbacks.error("Can't add video stream, there already is one"); - return; - } - } else if(media.removeVideo) { - media.keepVideo = false; - media.replaceVideo = false; - media.addVideo = false; - media.videoSend = false; - } else if(media.replaceVideo) { - media.keepVideo = false; - media.addVideo = false; - media.removeVideo = false; - media.videoSend = true; - } - if(!config.myStream) { - // No media stream: if we were asked to replace, it's actually an "add" - if(media.replaceVideo) { - media.keepVideo = false; - media.replaceVideo = false; - media.addVideo = true; - media.videoSend = true; - } - if(isVideoSendEnabled(media)) { - media.keepVideo = false; - media.addVideo = true; - } - } else { - if(!config.myStream.getVideoTracks() || config.myStream.getVideoTracks().length === 0) { - // No video track: if we were asked to replace, it's actually an "add" - if(media.replaceVideo) { - media.keepVideo = false; - media.replaceVideo = false; - media.addVideo = true; - media.videoSend = true; - } - if(isVideoSendEnabled(media)) { - media.keepVideo = false; - media.addVideo = true; - } - } else { - // We have a video track: should we keep it as it is? - if(isVideoSendEnabled(media) && !media.removeVideo && !media.replaceVideo) { - media.keepVideo = true; - } - } - } - // Data channels can only be added - if(media.addData) { - media.data = true; - } - } - // If we're updating and keeping all tracks, let's skip the getUserMedia part - if((isAudioSendEnabled(media) && media.keepAudio) && - (isVideoSendEnabled(media) && media.keepVideo)) { - pluginHandle.consentDialog(false); - streamsDone(handleId, jsep, media, callbacks, config.myStream); + // Helper function used when creating either an offer or answer: it + // prepares what needs to be prepared, including creating a new + // PeerConnection (if needed) and updating the tracks configuration, + // before invoking the function to actually generate the offer/answer + async function prepareWebrtc(handleId, offer, callbacks) { + callbacks = callbacks || {}; + callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; + callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : webrtcError; + let jsep = callbacks.jsep; + if(offer && jsep) { + Janus.error("Provided a JSEP to a createOffer"); + callbacks.error("Provided a JSEP to a createOffer"); + return; + } else if(!offer && (!jsep || !jsep.type || !jsep.sdp)) { + Janus.error("A valid JSEP is required for createAnswer"); + callbacks.error("A valid JSEP is required for createAnswer"); return; } - } - // If we're updating, check if we need to remove/replace one of the tracks - if(media.update && (!config.streamExternal || (config.streamExternal && (media.replaceAudio || media.replaceVideo)))) { - if(media.removeAudio || media.replaceAudio) { - if(config.myStream && config.myStream.getAudioTracks() && config.myStream.getAudioTracks().length) { - var at = config.myStream.getAudioTracks()[0]; - Janus.log("Removing audio track:", at); - config.myStream.removeTrack(at); - try { - at.stop(); - } catch(e) {} - } - if(config.pc.getSenders() && config.pc.getSenders().length) { - var ra = true; - if(media.replaceAudio && Janus.unifiedPlan) { - // We can use replaceTrack - ra = false; - } - if(ra) { - for(var asnd of config.pc.getSenders()) { - if(asnd && asnd.track && asnd.track.kind === "audio") { - Janus.log("Removing audio sender:", asnd); - config.pc.removeTrack(asnd); - } + // If the deprecated media was provided instead of tracks, translate it + if(callbacks.media && !callbacks.tracks) { + callbacks.tracks = Janus.mediaToTracks(callbacks.media); + if(callbacks.simulcast === true || callbacks.simulcast2 === true || callbacks.svc) { + // Find the video track and add simulcast/SVC info there + for(let track of callbacks.tracks) { + if(track.type === 'video') { + if(callbacks.simulcast === true || callbacks.simulcast2 === true) + track.simulcast = true; + else if(callbacks.svc) + track.svc = callbacks.svc; + break; } } } + Janus.warn('Deprecated media object passed, use tracks instead. Automatically translated to:', callbacks.tracks); } - if(media.removeVideo || media.replaceVideo) { - if(config.myStream && config.myStream.getVideoTracks() && config.myStream.getVideoTracks().length) { - var vt = config.myStream.getVideoTracks()[0]; - Janus.log("Removing video track:", vt); - config.myStream.removeTrack(vt); - try { - vt.stop(); - } catch(e) {} - } - if(config.pc.getSenders() && config.pc.getSenders().length) { - var rv = true; - if(media.replaceVideo && Janus.unifiedPlan) { - // We can use replaceTrack - rv = false; - } - if(rv) { - for(var vsnd of config.pc.getSenders()) { - if(vsnd && vsnd.track && vsnd.track.kind === "video") { - Janus.log("Removing video sender:", vsnd); - config.pc.removeTrack(vsnd); - } - } - } - } - } - } - // Was a MediaStream object passed, or do we need to take care of that? - if(callbacks.stream) { - var stream = callbacks.stream; - Janus.log("MediaStream provided by the application"); - Janus.debug(stream); - // If this is an update, let's check if we need to release the previous stream - if(media.update && config.myStream && config.myStream !== callbacks.stream && !config.streamExternal && !media.replaceAudio && !media.replaceVideo) { - // We're replacing a stream we captured ourselves with an external one - Janus.stopAllTracks(config.myStream); - config.myStream = null; - } - // Skip the getUserMedia part - config.streamExternal = true; - pluginHandle.consentDialog(false); - streamsDone(handleId, jsep, media, callbacks, stream); - return; - } - if(isAudioSendEnabled(media) || isVideoSendEnabled(media)) { - if(!Janus.isGetUserMediaAvailable()) { - callbacks.error("getUserMedia not available"); + // Check that callbacks.array is a valid array + if(callbacks.tracks && !Array.isArray(callbacks.tracks)) { + Janus.error("Tracks must be an array"); + callbacks.error("Tracks must be an array"); return; } - var constraints = { mandatory: {}, optional: []}; - pluginHandle.consentDialog(true); - var audioSupport = isAudioSendEnabled(media); - if(audioSupport && media && typeof media.audio === 'object') - audioSupport = media.audio; - var videoSupport = isVideoSendEnabled(media); - if(videoSupport && media) { - var simulcast = (callbacks.simulcast === true || callbacks.simulcast2 === true); - var svc = callbacks.svc; - if((simulcast || svc) && !jsep && !media.video) - media.video = "hires"; - if(media.video && media.video != 'screen' && media.video != 'window') { - if(typeof media.video === 'object') { - videoSupport = media.video; - } else { - var width = 0; - var height = 0; - if(media.video === 'lowres') { - // Small resolution, 4:3 - height = 240; - width = 320; - } else if(media.video === 'lowres-16:9') { - // Small resolution, 16:9 - height = 180; - width = 320; - } else if(media.video === 'hires' || media.video === 'hires-16:9' || media.video === 'hdres') { - // High(HD) resolution is only 16:9 - height = 720; - width = 1280; - } else if(media.video === 'fhdres') { - // Full HD resolution is only 16:9 - height = 1080; - width = 1920; - } else if(media.video === '4kres') { - // 4K resolution is only 16:9 - height = 2160; - width = 3840; - } else if(media.video === 'stdres') { - // Normal resolution, 4:3 - height = 480; - width = 640; - } else if(media.video === 'stdres-16:9') { - // Normal resolution, 16:9 - height = 360; - width = 640; - } else { - Janus.log("Default video setting is stdres 4:3"); - height = 480; - width = 640; - } - Janus.log("Adding media constraint:", media.video); - videoSupport = { - 'height': {'ideal': height}, - 'width': {'ideal': width} - }; - Janus.log("Adding video constraint:", videoSupport); - } - } else if(media.video === 'screen' || media.video === 'window') { - if(navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) { - // The new experimental getDisplayMedia API is available, let's use that - // https://groups.google.com/forum/#!topic/discuss-webrtc/Uf0SrR4uxzk - // https://webrtchacks.com/chrome-screensharing-getdisplaymedia/ - constraints.video = {}; - if(media.screenshareFrameRate) { - constraints.video.frameRate = media.screenshareFrameRate; - } - if(media.screenshareHeight) { - constraints.video.height = media.screenshareHeight; - } - if(media.screenshareWidth) { - constraints.video.width = media.screenshareWidth; - } - constraints.audio = media.captureDesktopAudio; - navigator.mediaDevices.getDisplayMedia(constraints) - .then(function(stream) { - pluginHandle.consentDialog(false); - if(isAudioSendEnabled(media) && !media.keepAudio) { - navigator.mediaDevices.getUserMedia({ audio: true, video: false }) - .then(function (audioStream) { - stream.addTrack(audioStream.getAudioTracks()[0]); - streamsDone(handleId, jsep, media, callbacks, stream); - }) - } else { - streamsDone(handleId, jsep, media, callbacks, stream); - } - }, function (error) { - pluginHandle.consentDialog(false); - callbacks.error(error); - }); - return; - } - // We're going to try and use the extension for Chrome 34+, the old approach - // for older versions of Chrome, or the experimental support in Firefox 33+ - const callbackUserMedia = function(error, stream) { - pluginHandle.consentDialog(false); - if(error) { - callbacks.error(error); - } else { - streamsDone(handleId, jsep, media, callbacks, stream); - } - } - const getScreenMedia = function(constraints, gsmCallback, useAudio) { - Janus.log("Adding media constraint (screen capture)"); - Janus.debug(constraints); - navigator.mediaDevices.getUserMedia(constraints) - .then(function(stream) { - if(useAudio) { - navigator.mediaDevices.getUserMedia({ audio: true, video: false }) - .then(function (audioStream) { - stream.addTrack(audioStream.getAudioTracks()[0]); - gsmCallback(null, stream); - }) - } else { - gsmCallback(null, stream); - } - }) - .catch(function(error) { pluginHandle.consentDialog(false); gsmCallback(error); }); - } - if(Janus.webRTCAdapter.browserDetails.browser === 'chrome') { - var chromever = Janus.webRTCAdapter.browserDetails.version; - var maxver = 33; - if(window.navigator.userAgent.match('Linux')) - maxver = 35; // "known" crash in chrome 34 and 35 on linux - if(chromever >= 26 && chromever <= maxver) { - // Chrome 26->33 requires some awkward chrome://flags manipulation - constraints = { - video: { - mandatory: { - googLeakyBucket: true, - maxWidth: window.screen.width, - maxHeight: window.screen.height, - minFrameRate: media.screenshareFrameRate, - maxFrameRate: media.screenshareFrameRate, - chromeMediaSource: 'screen' - } - }, - audio: isAudioSendEnabled(media) && !media.keepAudio - }; - getScreenMedia(constraints, callbackUserMedia); - } else { - // Chrome 34+ requires an extension - Janus.extension.getScreen(function (error, sourceId) { - if (error) { - pluginHandle.consentDialog(false); - return callbacks.error(error); - } - constraints = { - audio: false, - video: { - mandatory: { - chromeMediaSource: 'desktop', - maxWidth: window.screen.width, - maxHeight: window.screen.height, - minFrameRate: media.screenshareFrameRate, - maxFrameRate: media.screenshareFrameRate, - }, - optional: [ - {googLeakyBucket: true}, - {googTemporalLayeredScreencast: true} - ] - } - }; - constraints.video.mandatory.chromeMediaSourceId = sourceId; - getScreenMedia(constraints, callbackUserMedia, - isAudioSendEnabled(media) && !media.keepAudio); - }); - } - } else if(Janus.webRTCAdapter.browserDetails.browser === 'firefox') { - if(Janus.webRTCAdapter.browserDetails.version >= 33) { - // Firefox 33+ has experimental support for screen sharing - constraints = { - video: { - mozMediaSource: media.video, - mediaSource: media.video - }, - audio: isAudioSendEnabled(media) && !media.keepAudio - }; - getScreenMedia(constraints, function (err, stream) { - callbackUserMedia(err, stream); - // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1045810 - if (!err) { - var lastTime = stream.currentTime; - var polly = window.setInterval(function () { - if(!stream) - window.clearInterval(polly); - if(stream.currentTime == lastTime) { - window.clearInterval(polly); - if(stream.onended) { - stream.onended(); - } - } - lastTime = stream.currentTime; - }, 500); - } - }); - } else { - var error = new Error('NavigatorUserMediaError'); - error.name = 'Your version of Firefox does not support screen sharing, please install Firefox 33 (or more recent versions)'; - pluginHandle.consentDialog(false); - callbacks.error(error); - return; - } - } - return; - } - } - // If we got here, we're not screensharing - if(!media || media.video !== 'screen') { - // Check whether all media sources are actually available or not - navigator.mediaDevices.enumerateDevices().then(function(devices) { - var audioExist = devices.some(function(device) { - return device.kind === 'audioinput'; - }), - videoExist = isScreenSendEnabled(media) || devices.some(function(device) { - return device.kind === 'videoinput'; - }); - - // Check whether a missing device is really a problem - var audioSend = isAudioSendEnabled(media); - var videoSend = isVideoSendEnabled(media); - var needAudioDevice = isAudioSendRequired(media); - var needVideoDevice = isVideoSendRequired(media); - if(audioSend || videoSend || needAudioDevice || needVideoDevice) { - // We need to send either audio or video - var haveAudioDevice = audioSend ? audioExist : false; - var haveVideoDevice = videoSend ? videoExist : false; - if(!haveAudioDevice && !haveVideoDevice) { - // FIXME Should we really give up, or just assume recvonly for both? - pluginHandle.consentDialog(false); - callbacks.error('No capture device found'); - return false; - } else if(!haveAudioDevice && needAudioDevice) { - pluginHandle.consentDialog(false); - callbacks.error('Audio capture is required, but no capture device found'); - return false; - } else if(!haveVideoDevice && needVideoDevice) { - pluginHandle.consentDialog(false); - callbacks.error('Video capture is required, but no capture device found'); - return false; - } - } - - var gumConstraints = { - audio: (audioExist && !media.keepAudio) ? audioSupport : false, - video: (videoExist && !media.keepVideo) ? videoSupport : false - }; - Janus.debug("getUserMedia constraints", gumConstraints); - if (!gumConstraints.audio && !gumConstraints.video) { - pluginHandle.consentDialog(false); - streamsDone(handleId, jsep, media, callbacks, stream); - } else { - navigator.mediaDevices.getUserMedia(gumConstraints) - .then(function(stream) { - pluginHandle.consentDialog(false); - streamsDone(handleId, jsep, media, callbacks, stream); - }).catch(function(error) { - pluginHandle.consentDialog(false); - callbacks.error({code: error.code, name: error.name, message: error.message}); - }); - } - }) - .catch(function(error) { - pluginHandle.consentDialog(false); - callbacks.error(error); - }); - } - } else { - // No need to do a getUserMedia, create offer/answer right away - streamsDone(handleId, jsep, media, callbacks); - } - } - - function prepareWebrtcPeer(handleId, callbacks) { - callbacks = callbacks || {}; - callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; - callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : webrtcError; - callbacks.customizeSdp = (typeof callbacks.customizeSdp == "function") ? callbacks.customizeSdp : Janus.noop; - var jsep = callbacks.jsep; - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle || !pluginHandle.webrtcStuff) { - Janus.warn("Invalid handle"); - callbacks.error("Invalid handle"); - return; - } - var config = pluginHandle.webrtcStuff; - if(jsep) { - if(!config.pc) { - Janus.warn("Wait, no PeerConnection?? if this is an answer, use createAnswer and not handleRemoteJsep"); - callbacks.error("No PeerConnection: if this is an answer, use createAnswer and not handleRemoteJsep"); + // Get the plugin handle + let pluginHandle = pluginHandles.get(handleId); + if(!pluginHandle || !pluginHandle.webrtcStuff) { + Janus.warn("Invalid handle"); + callbacks.error("Invalid handle"); return; } - callbacks.customizeSdp(jsep); - config.pc.setRemoteDescription(jsep) - .then(function() { - Janus.log("Remote description accepted!"); - config.remoteSdp = jsep.sdp; - // Any trickle candidate we cached? - if(config.candidates && config.candidates.length > 0) { - for(var i = 0; i< config.candidates.length; i++) { - var candidate = config.candidates[i]; - Janus.debug("Adding remote candidate:", candidate); - if(!candidate || candidate.completed === true) { - // end-of-candidates - config.pc.addIceCandidate(Janus.endOfCandidates); - } else { - // New candidate - config.pc.addIceCandidate(candidate); - } - } - config.candidates = []; - } - // Done - callbacks.success(); - }, callbacks.error); - } else { - callbacks.error("Invalid JSEP"); - } - } - - function createOffer(handleId, media, callbacks) { - callbacks = callbacks || {}; - callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; - callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; - callbacks.customizeSdp = (typeof callbacks.customizeSdp == "function") ? callbacks.customizeSdp : Janus.noop; - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle || !pluginHandle.webrtcStuff) { - Janus.warn("Invalid handle"); - callbacks.error("Invalid handle"); - return; - } - var config = pluginHandle.webrtcStuff; - var simulcast = (callbacks.simulcast === true); - if(!simulcast) { - Janus.log("Creating offer (iceDone=" + config.iceDone + ")"); - } else { - Janus.log("Creating offer (iceDone=" + config.iceDone + ", simulcast=" + simulcast + ")"); - } - // https://code.google.com/p/webrtc/issues/detail?id=3508 - var mediaConstraints = {}; - if(Janus.unifiedPlan) { - // We can use Transceivers - var audioTransceiver = null, videoTransceiver = null; - var transceivers = config.pc.getTransceivers(); - if(transceivers && transceivers.length > 0) { - for(var t of transceivers) { - if((t.sender && t.sender.track && t.sender.track.kind === "audio") || - (t.receiver && t.receiver.track && t.receiver.track.kind === "audio")) { - if(!audioTransceiver) { - audioTransceiver = t; - } - continue; - } - if((t.sender && t.sender.track && t.sender.track.kind === "video") || - (t.receiver && t.receiver.track && t.receiver.track.kind === "video")) { - if(!videoTransceiver) { - videoTransceiver = t; - } - continue; - } - } - } - // Handle audio (and related changes, if any) - var audioSend = isAudioSendEnabled(media); - var audioRecv = isAudioRecvEnabled(media); - if(!audioSend && !audioRecv) { - // Audio disabled: have we removed it? - if(media.removeAudio && audioTransceiver) { - if (audioTransceiver.setDirection) { - audioTransceiver.setDirection("inactive"); - } else { - audioTransceiver.direction = "inactive"; - } - Janus.log("Setting audio transceiver to inactive:", audioTransceiver); - } - } else { - // Take care of audio m-line - if(audioSend && audioRecv) { - if(audioTransceiver) { - if (audioTransceiver.setDirection) { - audioTransceiver.setDirection("sendrecv"); - } else { - audioTransceiver.direction = "sendrecv"; - } - Janus.log("Setting audio transceiver to sendrecv:", audioTransceiver); - } - } else if(audioSend && !audioRecv) { - if(audioTransceiver) { - if (audioTransceiver.setDirection) { - audioTransceiver.setDirection("sendonly"); - } else { - audioTransceiver.direction = "sendonly"; - } - Janus.log("Setting audio transceiver to sendonly:", audioTransceiver); - } - } else if(!audioSend && audioRecv) { - if(audioTransceiver) { - if (audioTransceiver.setDirection) { - audioTransceiver.setDirection("recvonly"); - } else { - audioTransceiver.direction = "recvonly"; - } - Janus.log("Setting audio transceiver to recvonly:", audioTransceiver); - } else { - // In theory, this is the only case where we might not have a transceiver yet - audioTransceiver = config.pc.addTransceiver("audio", { direction: "recvonly" }); - Janus.log("Adding recvonly audio transceiver:", audioTransceiver); - } - } - } - // Handle video (and related changes, if any) - var videoSend = isVideoSendEnabled(media); - var videoRecv = isVideoRecvEnabled(media); - if(!videoSend && !videoRecv) { - // Video disabled: have we removed it? - if(media.removeVideo && videoTransceiver) { - if (videoTransceiver.setDirection) { - videoTransceiver.setDirection("inactive"); - } else { - videoTransceiver.direction = "inactive"; - } - Janus.log("Setting video transceiver to inactive:", videoTransceiver); - } - } else { - // Take care of video m-line - if(videoSend && videoRecv) { - if(videoTransceiver) { - if (videoTransceiver.setDirection) { - videoTransceiver.setDirection("sendrecv"); - } else { - videoTransceiver.direction = "sendrecv"; - } - Janus.log("Setting video transceiver to sendrecv:", videoTransceiver); - } - } else if(videoSend && !videoRecv) { - if(videoTransceiver) { - if (videoTransceiver.setDirection) { - videoTransceiver.setDirection("sendonly"); - } else { - videoTransceiver.direction = "sendonly"; - } - Janus.log("Setting video transceiver to sendonly:", videoTransceiver); - } - } else if(!videoSend && videoRecv) { - if(videoTransceiver) { - if (videoTransceiver.setDirection) { - videoTransceiver.setDirection("recvonly"); - } else { - videoTransceiver.direction = "recvonly"; - } - Janus.log("Setting video transceiver to recvonly:", videoTransceiver); - } else { - // In theory, this is the only case where we might not have a transceiver yet - videoTransceiver = config.pc.addTransceiver("video", { direction: "recvonly" }); - Janus.log("Adding recvonly video transceiver:", videoTransceiver); - } - } - } - } else { - mediaConstraints["offerToReceiveAudio"] = isAudioRecvEnabled(media); - mediaConstraints["offerToReceiveVideo"] = isVideoRecvEnabled(media); - } - var iceRestart = (callbacks.iceRestart === true); - if(iceRestart) { - mediaConstraints["iceRestart"] = true; - } - Janus.debug(mediaConstraints); - // Check if this is Firefox and we've been asked to do simulcasting - var sendVideo = isVideoSendEnabled(media); - if(sendVideo && simulcast && Janus.webRTCAdapter.browserDetails.browser === "firefox") { - // FIXME Based on https://gist.github.com/voluntas/088bc3cc62094730647b - Janus.log("Enabling Simulcasting for Firefox (RID)"); - var sender = config.pc.getSenders().find(function(s) {return s.track && s.track.kind === "video"}); - if(sender) { - var parameters = sender.getParameters(); - if(!parameters) { - parameters = {}; - } - var maxBitrates = getMaxBitrates(callbacks.simulcastMaxBitrates); - parameters.encodings = callbacks.sendEncodings || [ - { rid: "h", active: true, maxBitrate: maxBitrates.high }, - { rid: "m", active: true, maxBitrate: maxBitrates.medium, scaleResolutionDownBy: 2 }, - { rid: "l", active: true, maxBitrate: maxBitrates.low, scaleResolutionDownBy: 4 } - ]; - sender.setParameters(parameters); - } - } - config.pc.createOffer(mediaConstraints) - .then(function(offer) { - Janus.debug(offer); - // JSON.stringify doesn't work on some WebRTC objects anymore - // See https://code.google.com/p/chromium/issues/detail?id=467366 - var jsep = { - "type": offer.type, - "sdp": offer.sdp - }; - callbacks.customizeSdp(jsep); - offer.sdp = jsep.sdp; - Janus.log("Setting local description"); - if(sendVideo && simulcast && !Janus.unifiedPlan) { - // We only do simulcast via SDP munging on older versions of Chrome and Safari - if(Janus.webRTCAdapter.browserDetails.browser === "chrome" || - Janus.webRTCAdapter.browserDetails.browser === "safari") { - Janus.log("Enabling Simulcasting for Chrome (SDP munging)"); - offer.sdp = mungeSdpForSimulcasting(offer.sdp); - } - } - config.mySdp = { - type: "offer", - sdp: offer.sdp - }; - config.pc.setLocalDescription(offer) - .catch(callbacks.error); - config.mediaConstraints = mediaConstraints; - if(!config.iceDone && !config.trickle) { - // Don't do anything until we have all candidates - Janus.log("Waiting for all candidates..."); - return; - } - // If transforms are present, notify Janus that the media is end-to-end encrypted - if(config.senderTransforms || config.receiverTransforms) { - offer["e2ee"] = true; - } - callbacks.success(offer); - }, callbacks.error); - } - - function createAnswer(handleId, media, callbacks) { - callbacks = callbacks || {}; - callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; - callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; - callbacks.customizeSdp = (typeof callbacks.customizeSdp == "function") ? callbacks.customizeSdp : Janus.noop; - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle || !pluginHandle.webrtcStuff) { - Janus.warn("Invalid handle"); - callbacks.error("Invalid handle"); - return; - } - var config = pluginHandle.webrtcStuff; - var simulcast = (callbacks.simulcast === true || callbacks.simulcast2 === true); - if(!simulcast) { - Janus.log("Creating answer (iceDone=" + config.iceDone + ")"); - } else { - Janus.log("Creating answer (iceDone=" + config.iceDone + ", simulcast=" + simulcast + ")"); - } - var mediaConstraints = null; - if(Janus.unifiedPlan) { - // We can use Transceivers - mediaConstraints = {}; - var audioTransceiver = null, videoTransceiver = null; - var transceivers = config.pc.getTransceivers(); - if(transceivers && transceivers.length > 0) { - for(var t of transceivers) { - if((t.sender && t.sender.track && t.sender.track.kind === "audio") || - (t.receiver && t.receiver.track && t.receiver.track.kind === "audio")) { - if(!audioTransceiver) - audioTransceiver = t; - continue; - } - if((t.sender && t.sender.track && t.sender.track.kind === "video") || - (t.receiver && t.receiver.track && t.receiver.track.kind === "video")) { - if(!videoTransceiver) - videoTransceiver = t; - continue; - } - } - } - // Handle audio (and related changes, if any) - var audioSend = isAudioSendEnabled(media); - var audioRecv = isAudioRecvEnabled(media); - if(!audioSend && !audioRecv) { - // Audio disabled: have we removed it? - if(media.removeAudio && audioTransceiver) { - try { - if (audioTransceiver.setDirection) { - audioTransceiver.setDirection("inactive"); - } else { - audioTransceiver.direction = "inactive"; - } - Janus.log("Setting audio transceiver to inactive:", audioTransceiver); - } catch(e) { - Janus.error(e); - } - } - } else { - // Take care of audio m-line - if(audioSend && audioRecv) { - if(audioTransceiver) { - try { - if (audioTransceiver.setDirection) { - audioTransceiver.setDirection("sendrecv"); - } else { - audioTransceiver.direction = "sendrecv"; - } - Janus.log("Setting audio transceiver to sendrecv:", audioTransceiver); - } catch(e) { - Janus.error(e); - } - } - } else if(audioSend && !audioRecv) { - try { - if(audioTransceiver) { - if (audioTransceiver.setDirection) { - audioTransceiver.setDirection("sendonly"); - } else { - audioTransceiver.direction = "sendonly"; - } - Janus.log("Setting audio transceiver to sendonly:", audioTransceiver); - } - } catch(e) { - Janus.error(e); - } - } else if(!audioSend && audioRecv) { - if(audioTransceiver) { - try { - if (audioTransceiver.setDirection) { - audioTransceiver.setDirection("recvonly"); - } else { - audioTransceiver.direction = "recvonly"; - } - Janus.log("Setting audio transceiver to recvonly:", audioTransceiver); - } catch(e) { - Janus.error(e); - } - } else { - // In theory, this is the only case where we might not have a transceiver yet - audioTransceiver = config.pc.addTransceiver("audio", { direction: "recvonly" }); - Janus.log("Adding recvonly audio transceiver:", audioTransceiver); - } - } - } - // Handle video (and related changes, if any) - var videoSend = isVideoSendEnabled(media); - var videoRecv = isVideoRecvEnabled(media); - if(!videoSend && !videoRecv) { - // Video disabled: have we removed it? - if(media.removeVideo && videoTransceiver) { - try { - if (videoTransceiver.setDirection) { - videoTransceiver.setDirection("inactive"); - } else { - videoTransceiver.direction = "inactive"; - } - Janus.log("Setting video transceiver to inactive:", videoTransceiver); - } catch(e) { - Janus.error(e); - } - } - } else { - // Take care of video m-line - if(videoSend && videoRecv) { - if(videoTransceiver) { - try { - if (videoTransceiver.setDirection) { - videoTransceiver.setDirection("sendrecv"); - } else { - videoTransceiver.direction = "sendrecv"; - } - Janus.log("Setting video transceiver to sendrecv:", videoTransceiver); - } catch(e) { - Janus.error(e); - } - } - } else if(videoSend && !videoRecv) { - if(videoTransceiver) { - try { - if (videoTransceiver.setDirection) { - videoTransceiver.setDirection("sendonly"); - } else { - videoTransceiver.direction = "sendonly"; - } - Janus.log("Setting video transceiver to sendonly:", videoTransceiver); - } catch(e) { - Janus.error(e); - } - } - } else if(!videoSend && videoRecv) { - if(videoTransceiver) { - try { - if (videoTransceiver.setDirection) { - videoTransceiver.setDirection("recvonly"); - } else { - videoTransceiver.direction = "recvonly"; - } - Janus.log("Setting video transceiver to recvonly:", videoTransceiver); - } catch(e) { - Janus.error(e); - } - } else { - // In theory, this is the only case where we might not have a transceiver yet - videoTransceiver = config.pc.addTransceiver("video", { direction: "recvonly" }); - Janus.log("Adding recvonly video transceiver:", videoTransceiver); - } - } - } - } else { - if(Janus.webRTCAdapter.browserDetails.browser === "firefox" || Janus.webRTCAdapter.browserDetails.browser === "edge") { - mediaConstraints = { - offerToReceiveAudio: isAudioRecvEnabled(media), - offerToReceiveVideo: isVideoRecvEnabled(media) - }; - } else { - mediaConstraints = { - mandatory: { - OfferToReceiveAudio: isAudioRecvEnabled(media), - OfferToReceiveVideo: isVideoRecvEnabled(media) - } - }; - } - } - Janus.debug(mediaConstraints); - // Check if this is Firefox and we've been asked to do simulcasting - var sendVideo = isVideoSendEnabled(media); - if(sendVideo && simulcast && Janus.webRTCAdapter.browserDetails.browser === "firefox") { - // FIXME Based on https://gist.github.com/voluntas/088bc3cc62094730647b - Janus.log("Enabling Simulcasting for Firefox (RID)"); - var sender = config.pc.getSenders()[1]; - Janus.log(sender); - var parameters = sender.getParameters(); - Janus.log(parameters); - - var maxBitrates = getMaxBitrates(callbacks.simulcastMaxBitrates); - sender.setParameters({encodings: callbacks.sendEncodings || [ - { rid: "h", active: true, maxBitrate: maxBitrates.high }, - { rid: "m", active: true, maxBitrate: maxBitrates.medium, scaleResolutionDownBy: 2}, - { rid: "l", active: true, maxBitrate: maxBitrates.low, scaleResolutionDownBy: 4} - ]}); - } - config.pc.createAnswer(mediaConstraints) - .then(function(answer) { - Janus.debug(answer); - // JSON.stringify doesn't work on some WebRTC objects anymore - // See https://code.google.com/p/chromium/issues/detail?id=467366 - var jsep = { - "type": answer.type, - "sdp": answer.sdp - }; - callbacks.customizeSdp(jsep); - answer.sdp = jsep.sdp; - Janus.log("Setting local description"); - if(sendVideo && simulcast && !Janus.unifiedPlan) { - // We only do simulcast via SDP munging on older versions of Chrome and Safari - if(Janus.webRTCAdapter.browserDetails.browser === "chrome") { - // FIXME Apparently trying to simulcast when answering breaks video in Chrome... - //~ Janus.log("Enabling Simulcasting for Chrome (SDP munging)"); - //~ answer.sdp = mungeSdpForSimulcasting(answer.sdp); - Janus.warn("simulcast=true, but this is an answer, and video breaks in Chrome if we enable it"); - } - } - config.mySdp = { - type: "answer", - sdp: answer.sdp - }; - config.pc.setLocalDescription(answer) - .catch(callbacks.error); - config.mediaConstraints = mediaConstraints; - if(!config.iceDone && !config.trickle) { - // Don't do anything until we have all candidates - Janus.log("Waiting for all candidates..."); - return; - } - // If transforms are present, notify Janus that the media is end-to-end encrypted - if(config.senderTransforms || config.receiverTransforms) { - answer["e2ee"] = true; - } - callbacks.success(answer); - }, callbacks.error); - } - - function sendSDP(handleId, callbacks) { - callbacks = callbacks || {}; - callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; - callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle || !pluginHandle.webrtcStuff) { - Janus.warn("Invalid handle, not sending anything"); - return; - } - var config = pluginHandle.webrtcStuff; - Janus.log("Sending offer/answer SDP..."); - if(!config.mySdp) { - Janus.warn("Local SDP instance is invalid, not sending anything..."); - return; - } - config.mySdp = { - "type": config.pc.localDescription.type, - "sdp": config.pc.localDescription.sdp - }; - if(config.trickle === false) - config.mySdp["trickle"] = false; - Janus.debug(callbacks); - config.sdpSent = true; - callbacks.success(config.mySdp); - } - - function getVolume(handleId, remote) { - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle || !pluginHandle.webrtcStuff) { - Janus.warn("Invalid handle"); - return 0; - } - var stream = remote ? "remote" : "local"; - var config = pluginHandle.webrtcStuff; - if(!config.volume[stream]) - config.volume[stream] = { value: 0 }; - // Start getting the volume, if audioLevel in getStats is supported (apparently - // they're only available in Chrome/Safari right now: https://webrtc-stats.callstats.io/) - if(config.pc.getStats && (Janus.webRTCAdapter.browserDetails.browser === "chrome" || - Janus.webRTCAdapter.browserDetails.browser === "safari")) { - if(remote && !config.remoteStream) { - Janus.warn("Remote stream unavailable"); - return 0; - } else if(!remote && !config.myStream) { - Janus.warn("Local stream unavailable"); - return 0; - } - if(!config.volume[stream].timer) { - Janus.log("Starting " + stream + " volume monitor"); - config.volume[stream].timer = setInterval(function() { - config.pc.getStats() - .then(function(stats) { - stats.forEach(function (res) { - if(!res || res.kind !== "audio") - return; - if((remote && !res.remoteSource) || (!remote && res.type !== "media-source")) - return; - config.volume[stream].value = (res.audioLevel ? res.audioLevel : 0); - }); - }); - }, 200); - return 0; // We don't have a volume to return yet - } - return config.volume[stream].value; - } else { - // audioInputLevel and audioOutputLevel seem only available in Chrome? audioLevel - // seems to be available on Chrome and Firefox, but they don't seem to work - Janus.warn("Getting the " + stream + " volume unsupported by browser"); - return 0; - } - } - - function isMuted(handleId, video) { - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle || !pluginHandle.webrtcStuff) { - Janus.warn("Invalid handle"); - return true; - } - var config = pluginHandle.webrtcStuff; - if(!config.pc) { - Janus.warn("Invalid PeerConnection"); - return true; - } - if(!config.myStream) { - Janus.warn("Invalid local MediaStream"); - return true; - } - if(video) { - // Check video track - if(!config.myStream.getVideoTracks() || config.myStream.getVideoTracks().length === 0) { - Janus.warn("No video track"); - return true; - } - return !config.myStream.getVideoTracks()[0].enabled; - } else { - // Check audio track - if(!config.myStream.getAudioTracks() || config.myStream.getAudioTracks().length === 0) { - Janus.warn("No audio track"); - return true; - } - return !config.myStream.getAudioTracks()[0].enabled; - } - } - - function mute(handleId, video, mute) { - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle || !pluginHandle.webrtcStuff) { - Janus.warn("Invalid handle"); - return false; - } - var config = pluginHandle.webrtcStuff; - if(!config.pc) { - Janus.warn("Invalid PeerConnection"); - return false; - } - if(!config.myStream) { - Janus.warn("Invalid local MediaStream"); - return false; - } - if(video) { - // Mute/unmute video track - if(!config.myStream.getVideoTracks() || config.myStream.getVideoTracks().length === 0) { - Janus.warn("No video track"); - return false; - } - config.myStream.getVideoTracks()[0].enabled = !mute; - return true; - } else { - // Mute/unmute audio track - if(!config.myStream.getAudioTracks() || config.myStream.getAudioTracks().length === 0) { - Janus.warn("No audio track"); - return false; - } - config.myStream.getAudioTracks()[0].enabled = !mute; - return true; - } - } - - function getBitrate(handleId) { - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle || !pluginHandle.webrtcStuff) { - Janus.warn("Invalid handle"); - return "Invalid handle"; - } - var config = pluginHandle.webrtcStuff; - if(!config.pc) - return "Invalid PeerConnection"; - // Start getting the bitrate, if getStats is supported - if(config.pc.getStats) { - if(!config.bitrate.timer) { - Janus.log("Starting bitrate timer (via getStats)"); - config.bitrate.timer = setInterval(function() { - config.pc.getStats() - .then(function(stats) { - stats.forEach(function (res) { - if(!res) - return; - var inStats = false; - // Check if these are statistics on incoming media - if((res.mediaType === "video" || res.id.toLowerCase().indexOf("video") > -1) && - res.type === "inbound-rtp" && res.id.indexOf("rtcp") < 0) { - // New stats - inStats = true; - } else if(res.type == 'ssrc' && res.bytesReceived && - (res.googCodecName === "VP8" || res.googCodecName === "")) { - // Older Chromer versions - inStats = true; - } - // Parse stats now - if(inStats) { - config.bitrate.bsnow = res.bytesReceived; - config.bitrate.tsnow = res.timestamp; - if(config.bitrate.bsbefore === null || config.bitrate.tsbefore === null) { - // Skip this round - config.bitrate.bsbefore = config.bitrate.bsnow; - config.bitrate.tsbefore = config.bitrate.tsnow; - } else { - // Calculate bitrate - var timePassed = config.bitrate.tsnow - config.bitrate.tsbefore; - if(Janus.webRTCAdapter.browserDetails.browser === "safari") - timePassed = timePassed/1000; // Apparently the timestamp is in microseconds, in Safari - var bitRate = Math.round((config.bitrate.bsnow - config.bitrate.bsbefore) * 8 / timePassed); - if(Janus.webRTCAdapter.browserDetails.browser === "safari") - bitRate = parseInt(bitRate/1000); - config.bitrate.value = bitRate + ' kbits/sec'; - //~ Janus.log("Estimated bitrate is " + config.bitrate.value); - config.bitrate.bsbefore = config.bitrate.bsnow; - config.bitrate.tsbefore = config.bitrate.tsnow; - } - } - }); - }); - }, 1000); - return "0 kbits/sec"; // We don't have a bitrate value yet - } - return config.bitrate.value; - } else { - Janus.warn("Getting the video bitrate unsupported by browser"); - return "Feature unsupported by browser"; - } - } - - function webrtcError(error) { - Janus.error("WebRTC error:", error); - } - - function cleanupWebrtc(handleId, hangupRequest) { - Janus.log("Cleaning WebRTC stuff"); - var pluginHandle = pluginHandles[handleId]; - if(!pluginHandle) { - // Nothing to clean - return; - } - var config = pluginHandle.webrtcStuff; - if(config) { - if(hangupRequest === true) { - // Send a hangup request (we don't really care about the response) - var request = { "janus": "hangup", "transaction": Janus.randomString(12) }; - if(pluginHandle.token) - request["token"] = pluginHandle.token; - if(apisecret) - request["apisecret"] = apisecret; - Janus.debug("Sending hangup request (handle=" + handleId + "):"); - Janus.debug(request); - if(websockets) { - request["session_id"] = sessionId; - request["handle_id"] = handleId; - ws.send(JSON.stringify(request)); - } else { - Janus.httpAPICall(server + "/" + sessionId + "/" + handleId, { - verb: 'POST', - withCredentials: withCredentials, - body: request - }); - } - } - // Cleanup stack - config.remoteStream = null; - if(config.volume) { - if(config.volume["local"] && config.volume["local"].timer) - clearInterval(config.volume["local"].timer); - if(config.volume["remote"] && config.volume["remote"].timer) - clearInterval(config.volume["remote"].timer); - } - config.volume = {}; - if(config.bitrate.timer) - clearInterval(config.bitrate.timer); - config.bitrate.timer = null; - config.bitrate.bsnow = null; - config.bitrate.bsbefore = null; - config.bitrate.tsnow = null; - config.bitrate.tsbefore = null; - config.bitrate.value = null; - if(!config.streamExternal && config.myStream) { - Janus.log("Stopping local stream tracks"); - Janus.stopAllTracks(config.myStream); - } - config.streamExternal = false; - config.myStream = null; - // Close PeerConnection + let config = pluginHandle.webrtcStuff; + config.trickle = isTrickleEnabled(callbacks.trickle); try { - config.pc.close(); - } catch(e) { - // Do nothing - } - config.pc = null; - config.candidates = null; - config.mySdp = null; - config.remoteSdp = null; - config.iceDone = false; - config.dataChannel = {}; - config.dtmfSender = null; - config.senderTransforms = null; - config.receiverTransforms = null; - } - pluginHandle.oncleanup(); - } - - // Helper method to munge an SDP to enable simulcasting (Chrome only) - function mungeSdpForSimulcasting(sdp) { - // Let's munge the SDP to add the attributes for enabling simulcasting - // (based on https://gist.github.com/ggarber/a19b4c33510028b9c657) - var lines = sdp.split("\r\n"); - var video = false; - var ssrc = [ -1 ], ssrc_fid = [ -1 ]; - var cname = null, msid = null, mslabel = null, label = null; - var insertAt = -1; - for(let i=0; i -1) { - // We're done, let's add the new attributes here - insertAt = i; - break; + await config.pc.setRemoteDescription(jsep); + Janus.log("Remote description accepted!"); + config.remoteSdp = jsep.sdp; + // Any trickle candidate we cached? + if(config.candidates && config.candidates.length > 0) { + for(let i=0; i 0) { + for(let i=0; i (t.mid === track.mid && t.receiver.track.kind === kind)); + } else if(!track.add) { + // Find the first track of this type + transceiver = config.pc.getTransceivers() + .find(t => (t.receiver.track.kind === kind)); + } + if(track.replace || track.remove) { + if(!transceiver) { + Janus.warn("Couldn't find a transceiver for track:", track); + continue; + } + if(!transceiver.sender) { + Janus.warn('No sender in the transceiver for track:', track); + continue; + } + sender = transceiver.sender; + } + if(answer && !transceiver) { + transceiver = config.pc.getTransceivers() + .find(t => (t.receiver.track.kind === kind)); + if(!transceiver) { + Janus.warn("Couldn't find a transceiver for track:", track); + continue; + } + } + // Capture the new track, if we need to + let nt = null, trackId = null; + if(track.remove || track.replace) { + Janus.log('Removing track from PeerConnection', track); + trackId = sender.track ? sender.track.id : null; + await sender.replaceTrack(null); + // Get rid of the old track + if(trackId && config.myStream) { + let rt = null; + if(kind === 'audio' && config.myStream.getAudioTracks() && config.myStream.getAudioTracks().length) { + for(let t of config.myStream.getAudioTracks()) { + if(t.id === trackId) { + rt = t; + Janus.log('Removing audio track:', rt); + } + } + } else if(kind === 'video' && config.myStream.getVideoTracks() && config.myStream.getVideoTracks().length) { + for(let t of config.myStream.getVideoTracks()) { + if(t.id === trackId) { + rt = t; + Janus.log('Removing video track:', rt); + } + } + } + if(rt) { + // Remove the track and notify the application + try { + config.myStream.removeTrack(rt); + pluginHandle.onlocaltrack(rt, false); + } catch(e) { + Janus.error("Error calling onlocaltrack on removal for renegotiation", e); + } + // Close the old track (unless we've been asked not to) + if(rt.dontStop !== true) { + try { + rt.stop(); + // eslint-disable-next-line no-unused-vars + } catch(e) {} + } + } + } + } + if(track.capture) { + if(track.gumGroup && groups[track.gumGroup] && groups[track.gumGroup].stream) { + // We did a getUserMedia before already + let stream = groups[track.gumGroup].stream; + nt = (track.type === 'audio' ? stream.getAudioTracks()[0] : stream.getVideoTracks()[0]); + delete groups[track.gumGroup].stream; + delete groups[track.gumGroup]; + delete track.gumGroup; + } else if(track.capture instanceof MediaStreamTrack) { + // An external track was provided, use that + nt = track.capture; + } else { + if(!openedConsentDialog) { + openedConsentDialog = true; + pluginHandle.consentDialog(true); + } + let constraints = Janus.trackConstraints(track), stream = null; + if(track.type === 'audio' || track.type === 'video') { + // Use getUserMedia: check if we need to group audio and video together + if(track.gumGroup) { + let otherType = (track.type === 'audio' ? 'video' : 'audio'); + if(groups[track.gumGroup] && groups[track.gumGroup][otherType]) { + let otherTrack = groups[track.gumGroup][otherType]; + let otherConstraints = Janus.trackConstraints(otherTrack); + constraints[otherType] = otherConstraints[otherType]; + } + } + stream = await navigator.mediaDevices.getUserMedia(constraints); + if(track.gumGroup && constraints.audio && constraints.video) { + // We just performed a grouped getUserMedia, keep track of the + // stream so that we can immediately assign the track later + groups[track.gumGroup].stream = stream; + delete track.gumGroup; + } } else { - // We're done, let's add the new attributes here - insertAt = i; - break; + // Use getDisplayMedia + stream = await navigator.mediaDevices.getDisplayMedia(constraints); } + nt = (track.type === 'audio' ? stream.getAudioTracks()[0] : stream.getVideoTracks()[0]); + } + if(track.replace) { + // Replace the track + await sender.replaceTrack(nt); + // Update the transceiver direction + let newDirection = 'sendrecv'; + if(track.recv === false || transceiver.direction === 'inactive' || transceiver.direction === 'sendonly') + newDirection = 'sendonly'; + if(transceiver.setDirection) + transceiver.setDirection(newDirection); + else + transceiver.direction = newDirection; } else { - // New non-video m-line: do we have what we were looking for? - if(ssrc[0] > -1) { - // We're done, let's add the new attributes here - insertAt = i; - break; + // FIXME Add as a new track + if(!config.myStream) + config.myStream = new MediaStream(); + if(kind === 'audio' || (!track.simulcast && !track.svc)) { + sender = config.pc.addTrack(nt, config.myStream); + transceiver = config.pc.getTransceivers() + .find(t => (t.sender === sender)); + } else if(track.simulcast) { + // Standard RID + Janus.log('Enabling rid-based simulcasting:', nt); + let maxBitrates = getMaxBitrates(track.simulcastMaxBitrates); + transceiver = config.pc.addTransceiver(nt, { + direction: 'sendrecv', + streams: [config.myStream], + sendEncodings: track.sendEncodings || [ + { rid: 'h', active: true, scalabilityMode: 'L1T2', maxBitrate: maxBitrates.high }, + { rid: 'm', active: true, scalabilityMode: 'L1T2', maxBitrate: maxBitrates.medium, scaleResolutionDownBy: 2 }, + { rid: 'l', active: true, scalabilityMode: 'L1T2', maxBitrate: maxBitrates.low, scaleResolutionDownBy: 4 } + ] + }); + } else { + Janus.log('Enabling SVC (' + track.svc + '):', nt); + transceiver = config.pc.addTransceiver(nt, { + direction: 'sendrecv', + streams: [config.myStream], + sendEncodings: [ + { scalabilityMode: track.svc } + ] + }); + } + if(!sender) + sender = transceiver ? transceiver.sender : null; + // Check if we need to override some settings + if(track.codec) { + if(Janus.webRTCAdapter.browserDetails.browser === 'firefox') { + Janus.warn('setCodecPreferences not supported in Firefox, ignoring codec for track:', track); + } else if(typeof track.codec !== 'string') { + Janus.warn('Invalid codec value, ignoring for track:', track); + } else { + let mimeType = kind + '/' + track.codec.toLowerCase(); + let codecs = RTCRtpReceiver.getCapabilities(kind).codecs.filter(function(codec) { + return codec.mimeType.toLowerCase() === mimeType; + }); + if(!codecs || codecs.length === 0) { + Janus.warn('Codec not supported in this browser for this track, ignoring:', track); + } else if(transceiver) { + try { + transceiver.setCodecPreferences(codecs); + } catch(err) { + Janus.warn('Failed enforcing codec for this ' + kind + ' track:', err); + } + } + } + } + if(track.bitrate) { + // Override maximum bitrate + if(track.simulcast || track.svc) { + Janus.warn('Ignoring bitrate for simulcast/SVC track, use sendEncodings for that'); + } else if(isNaN(track.bitrate) || track.bitrate < 0) { + Janus.warn('Ignoring invalid bitrate for track:', track); + } else if(sender) { + let params = sender.getParameters(); + if(!params || !params.encodings || params.encodings.length === 0) { + Janus.warn('No encodings in the sender parameters, ignoring bitrate for track:', track); + } else { + params.encodings[0].maxBitrate = track.bitrate; + await sender.setParameters(params); + } + } + } + if(kind === 'video' && track.framerate) { + // Override maximum framerate + if(track.simulcast || track.svc) { + Janus.warn('Ignoring framerate for simulcast/SVC track, use sendEncodings for that'); + } else if(isNaN(track.framerate) || track.framerate < 0) { + Janus.warn('Ignoring invalid framerate for track:', track); + } else if(sender) { + let params = sender.getParameters(); + if(!params || !params.encodings || params.encodings.length === 0) { + Janus.warn('No encodings in the sender parameters, ignoring framerate for track:', track); + } else { + params.encodings[0].maxFramerate = track.framerate; + await sender.setParameters(params); + } + } + } + // Check if insertable streams are involved + if(track.transforms) { + if(sender && track.transforms.sender) { + // There's a sender transform, set it on the transceiver sender + let senderStreams = null; + if(RTCRtpSender.prototype.createEncodedStreams) { + senderStreams = sender.createEncodedStreams(); + } else if(RTCRtpSender.prototype.createAudioEncodedStreams || RTCRtpSender.prototype.createEncodedVideoStreams) { + if(kind === 'audio') { + senderStreams = sender.createEncodedAudioStreams(); + } else if(kind === 'video') { + senderStreams = sender.createEncodedVideoStreams(); + } + } + if(senderStreams) { + console.log('Insertable Streams sender transform:', senderStreams); + if(senderStreams.readableStream && senderStreams.writableStream) { + senderStreams.readableStream + .pipeThrough(track.transforms.sender) + .pipeTo(senderStreams.writableStream); + } else if(senderStreams.readable && senderStreams.writable) { + senderStreams.readable + .pipeThrough(track.transforms.sender) + .pipeTo(senderStreams.writable); + } + } + } + if(transceiver && transceiver.receiver && track.transforms.receiver) { + // There's a receiver transform, set it on the transceiver receiver + let receiverStreams = null; + if(RTCRtpReceiver.prototype.createEncodedStreams) { + receiverStreams = transceiver.receiver.createEncodedStreams(); + } else if(RTCRtpReceiver.prototype.createAudioEncodedStreams || RTCRtpReceiver.prototype.createEncodedVideoStreams) { + if(kind === 'audio') { + receiverStreams = transceiver.receiver.createEncodedAudioStreams(); + } else if(kind === 'video') { + receiverStreams = transceiver.receiver.createEncodedVideoStreams(); + } + } + if(receiverStreams) { + console.log('Insertable Streams receiver transform:', receiverStreams); + if(receiverStreams.readableStream && receiverStreams.writableStream) { + receiverStreams.readableStream + .pipeThrough(track.transforms.receiver) + .pipeTo(receiverStreams.writableStream); + } else if(receiverStreams.readable && receiverStreams.writable) { + receiverStreams.readable + .pipeThrough(track.transforms.receiver) + .pipeTo(receiverStreams.writable); + } + } + } } } - continue; - } - if(!video) - continue; - if(ssrc[0] < 0) { - var value = lines[i].match(/a=ssrc:(\d+)/); - if(value) { - ssrc[0] = value[1]; - lines.splice(i, 1); i--; - continue; + if(nt && track.dontStop === true) + nt.dontStop = true; + } else if(track.recv) { + // Maybe a new recvonly track + if(!transceiver) + transceiver = config.pc.addTransceiver(kind); + if(transceiver) { + // Check if we need to override some settings + if(track.codec) { + if(Janus.webRTCAdapter.browserDetails.browser === 'firefox') { + Janus.warn('setCodecPreferences not supported in Firefox, ignoring codec for track:', track); + } else if(typeof track.codec !== 'string') { + Janus.warn('Invalid codec value, ignoring for track:', track); + } else { + let mimeType = kind + '/' + track.codec.toLowerCase(); + let codecs = RTCRtpReceiver.getCapabilities(kind).codecs.filter(function(codec) { + return codec.mimeType.toLowerCase() === mimeType; + }); + if(!codecs || codecs.length === 0) { + Janus.warn('Codec not supported in this browser for this track, ignoring:', track); + } else { + try { + transceiver.setCodecPreferences(codecs); + } catch(err) { + Janus.warn('Failed enforcing codec for this ' + kind + ' track:', err); + } + } + } + } + // Check if insertable streams are involved + if(transceiver.receiver && track.transforms && track.transforms.receiver) { + // There's a receiver transform, set it on the transceiver receiver + let receiverStreams = null; + if(RTCRtpReceiver.prototype.createEncodedStreams) { + receiverStreams = transceiver.receiver.createEncodedStreams(); + } else if(RTCRtpReceiver.prototype.createAudioEncodedStreams || RTCRtpReceiver.prototype.createEncodedVideoStreams) { + if(kind === 'audio') { + receiverStreams = transceiver.receiver.createEncodedAudioStreams(); + } else if(kind === 'video') { + receiverStreams = transceiver.receiver.createEncodedVideoStreams(); + } + } + if(receiverStreams) { + console.log('Insertable Streams receiver transform:', receiverStreams); + if(receiverStreams.readableStream && receiverStreams.writableStream) { + receiverStreams.readableStream + .pipeThrough(track.transforms.receiver) + .pipeTo(receiverStreams.writableStream); + } else if(receiverStreams.readable && receiverStreams.writable) { + receiverStreams.readable + .pipeThrough(track.transforms.receiver) + .pipeTo(receiverStreams.writable); + } + } + } } + } + if(nt) { + // FIXME Add the new track locally + config.myStream.addTrack(nt); + // Notify the application about the new local track, if any + nt.onended = function(ev) { + Janus.log('Local track removed:', ev); + try { + pluginHandle.onlocaltrack(ev.target, false); + } catch(e) { + Janus.error("Error calling onlocaltrack following end", e); + } + } + try { + pluginHandle.onlocaltrack(nt, true); + } catch(e) { + Janus.error("Error calling onlocaltrack for track add", e); + } + } + // Update the direction of the transceiver + if(transceiver) { + let curdir = transceiver.direction, newdir = null; + let send = (nt && transceiver.sender.track), + recv = (track.recv !== false && transceiver.receiver.track); + if(send && recv) + newdir = 'sendrecv'; + else if(send && !recv) + newdir = 'sendonly'; + else if(!send && recv) + newdir = 'recvonly'; + else if(!send && !recv) + newdir = 'inactive'; + if(newdir && newdir !== curdir) { + Janus.warn('Changing direction of transceiver to ' + newdir + ' (was ' + curdir + ')', track); + if(transceiver.setDirection) + transceiver.setDirection(newdir); + else + transceiver.direction = newdir; + } + } + } + if(openedConsentDialog) + pluginHandle.consentDialog(false); + } + + function getLocalTracks(handleId) { + let pluginHandle = pluginHandles.get(handleId); + if(!pluginHandle || !pluginHandle.webrtcStuff) { + Janus.warn('Invalid handle'); + return null; + } + let config = pluginHandle.webrtcStuff; + if(!config.pc) { + Janus.warn('Invalid PeerConnection'); + return null; + } + let tracks = []; + let transceivers = config.pc.getTransceivers(); + for(let tr of transceivers) { + let track = null; + if(tr.sender && tr.sender.track) { + track = { mid: tr.mid }; + track.type = tr.sender.track.kind; + track.id = tr.sender.track.id; + track.label = tr.sender.track.label; + } + if(track) + tracks.push(track); + } + return tracks; + } + + function getRemoteTracks(handleId) { + let pluginHandle = pluginHandles.get(handleId); + if(!pluginHandle || !pluginHandle.webrtcStuff) { + Janus.warn('Invalid handle'); + return null; + } + let config = pluginHandle.webrtcStuff; + if(!config.pc) { + Janus.warn('Invalid PeerConnection'); + return null; + } + let tracks = []; + let transceivers = config.pc.getTransceivers(); + for(let tr of transceivers) { + let track = null; + if(tr.receiver && tr.receiver.track) { + track = { mid: tr.mid }; + track.type = tr.receiver.track.kind; + track.id = tr.receiver.track.id; + track.label = tr.receiver.track.label; + } + if(track) + tracks.push(track); + } + return tracks; + } + + function getVolume(handleId, mid, remote, result) { + result = (typeof result == "function") ? result : Janus.noop; + let pluginHandle = pluginHandles.get(handleId); + if(!pluginHandle || !pluginHandle.webrtcStuff) { + Janus.warn("Invalid handle"); + result(0); + return; + } + let stream = remote ? "remote" : "local"; + let config = pluginHandle.webrtcStuff; + if(!config.volume[stream]) + config.volume[stream] = { value: 0 }; + // Start getting the volume, if audioLevel in getStats is supported (apparently + // they're only available in Chrome/Safari right now: https://webrtc-stats.callstats.io/) + if(config.pc && config.pc.getStats && (Janus.webRTCAdapter.browserDetails.browser === "chrome" || + Janus.webRTCAdapter.browserDetails.browser === "safari")) { + // Are we interested in a mid in particular? + let query = config.pc; + if(mid) { + let transceiver = config.pc.getTransceivers() + .find(t => (t.mid === mid && t.receiver.track.kind === "audio")); + if(!transceiver) { + Janus.warn("No audio transceiver with mid " + mid); + result(0); + return; + } + if(remote && !transceiver.receiver) { + Janus.warn("Remote transceiver track unavailable"); + result(0); + return; + } else if(!remote && !transceiver.sender) { + Janus.warn("Local transceiver track unavailable"); + result(0); + return; + } + query = remote ? transceiver.receiver : transceiver.sender; + } + query.getStats() + .then(function(stats) { + stats.forEach(function (res) { + if(!res || res.kind !== "audio") + return; + if((remote && !res.remoteSource) || (!remote && res.type !== "media-source")) + return; + result(res.audioLevel ? res.audioLevel : 0); + }); + }); + return config.volume[stream].value; + } else { + // audioInputLevel and audioOutputLevel seem only available in Chrome? audioLevel + // seems to be available on Chrome and Firefox, but they don't seem to work + Janus.warn("Getting the " + stream + " volume unsupported by browser"); + result(0); + return; + } + } + + function isMuted(handleId, mid, video) { + let pluginHandle = pluginHandles.get(handleId); + if(!pluginHandle || !pluginHandle.webrtcStuff) { + Janus.warn("Invalid handle"); + return true; + } + let config = pluginHandle.webrtcStuff; + if(!config.pc) { + Janus.warn("Invalid PeerConnection"); + return true; + } + if(!config.myStream) { + Janus.warn("Invalid local MediaStream"); + return true; + } + if(video) { + // Check video track + if(!config.myStream.getVideoTracks() || config.myStream.getVideoTracks().length === 0) { + Janus.warn("No video track"); + return true; + } + if(mid) { + let transceiver = config.pc.getTransceivers() + .find(t => (t.mid === mid && t.receiver.track.kind === "video")); + if(!transceiver) { + Janus.warn("No video transceiver with mid " + mid); + return true; + } + if(!transceiver.sender || !transceiver.sender.track) { + Janus.warn("No video sender with mid " + mid); + return true; + } + return !transceiver.sender.track.enabled; } else { - let match = lines[i].match('a=ssrc:' + ssrc[0] + ' cname:(.+)') - if(match) { - cname = match[1]; - } - match = lines[i].match('a=ssrc:' + ssrc[0] + ' msid:(.+)') - if(match) { - msid = match[1]; - } - match = lines[i].match('a=ssrc:' + ssrc[0] + ' mslabel:(.+)') - if(match) { - mslabel = match[1]; - } - match = lines[i].match('a=ssrc:' + ssrc[0] + ' label:(.+)') - if(match) { - label = match[1]; - } - if(lines[i].indexOf('a=ssrc:' + ssrc_fid[0]) === 0) { - lines.splice(i, 1); i--; - continue; - } - if(lines[i].indexOf('a=ssrc:' + ssrc[0]) === 0) { - lines.splice(i, 1); i--; - continue; - } + return !config.myStream.getVideoTracks()[0].enabled; } - if(lines[i].length === 0) { - lines.splice(i, 1); i--; - continue; + } else { + // Check audio track + if(!config.myStream.getAudioTracks() || config.myStream.getAudioTracks().length === 0) { + Janus.warn("No audio track"); + return true; + } + if(mid) { + let transceiver = config.pc.getTransceivers() + .find(t => (t.mid === mid && t.receiver.track.kind === "audio")); + if(!transceiver) { + Janus.warn("No audio transceiver with mid " + mid); + return true; + } + if(!transceiver.sender || !transceiver.sender.track) { + Janus.warn("No audio sender with mid " + mid); + return true; + } + return !transceiver.sender.track.enabled; + } else { + return !config.myStream.getAudioTracks()[0].enabled; } } } - if(ssrc[0] < 0) { - // Still nothing, let's just return the SDP we were asked to munge - Janus.warn("Couldn't find the video SSRC, simulcasting NOT enabled"); - return sdp; + + function mute(handleId, mid, video, mute) { + let pluginHandle = pluginHandles.get(handleId); + if(!pluginHandle || !pluginHandle.webrtcStuff) { + Janus.warn("Invalid handle"); + return false; + } + let config = pluginHandle.webrtcStuff; + if(!config.pc) { + Janus.warn("Invalid PeerConnection"); + return false; + } + if(!config.myStream) { + Janus.warn("Invalid local MediaStream"); + return false; + } + if(video) { + // Mute/unmute video track + if(!config.myStream.getVideoTracks() || config.myStream.getVideoTracks().length === 0) { + Janus.warn("No video track"); + return false; + } + if(mid) { + let transceiver = config.pc.getTransceivers() + .find(t => (t.mid === mid && t.receiver.track.kind === "video")); + if(!transceiver) { + Janus.warn("No video transceiver with mid " + mid); + return false; + } + if(!transceiver.sender || !transceiver.sender.track) { + Janus.warn("No video sender with mid " + mid); + return false; + } + transceiver.sender.track.enabled = mute ? false : true; + } else { + for(const videostream of config.myStream.getVideoTracks()) { + videostream.enabled = !mute + } + } + } else { + // Mute/unmute audio track + if(!config.myStream.getAudioTracks() || config.myStream.getAudioTracks().length === 0) { + Janus.warn("No audio track"); + return false; + } + if(mid) { + let transceiver = config.pc.getTransceivers() + .find(t => (t.mid === mid && t.receiver.track.kind === "audio")); + if(!transceiver) { + Janus.warn("No audio transceiver with mid " + mid); + return false; + } + if(!transceiver.sender || !transceiver.sender.track) { + Janus.warn("No audio sender with mid " + mid); + return false; + } + transceiver.sender.track.enabled = mute ? false : true; + } else { + for(const audiostream of config.myStream.getAudioTracks()) { + audiostream.enabled = !mute + } + } + } + return true; } - if(insertAt < 0) { - // Append at the end - insertAt = lines.length; - } - // Generate a couple of SSRCs (for retransmissions too) - // Note: should we check if there are conflicts, here? - ssrc[1] = Math.floor(Math.random()*0xFFFFFFFF); - ssrc[2] = Math.floor(Math.random()*0xFFFFFFFF); - ssrc_fid[1] = Math.floor(Math.random()*0xFFFFFFFF); - ssrc_fid[2] = Math.floor(Math.random()*0xFFFFFFFF); - // Add attributes to the SDP - for(var i=0; i (t.mid === mid && t.receiver.track.kind === "video")); + if(!transceiver) { + Janus.warn("No video transceiver with mid " + mid); + return ("No video transceiver with mid " + mid); + } + if(!transceiver.receiver) { + Janus.warn("No video receiver with mid " + mid); + return ("No video receiver with mid " + mid); + } + query = transceiver.receiver; + } + if(!config.bitrate[target]) { + config.bitrate[target] = { + timer: null, + bsnow: null, + bsbefore: null, + tsnow: null, + tsbefore: null, + value: "0 kbits/sec" + }; + } + if(!config.bitrate[target].timer) { + Janus.log("Starting bitrate timer" + (mid ? (" for mid " + mid) : "") + " (via getStats)"); + config.bitrate[target].timer = setInterval(function() { + query.getStats() + .then(function(stats) { + stats.forEach(function (res) { + if(!res) + return; + let inStats = false; + // Check if these are statistics on incoming media + if((res.mediaType === "video" || res.kind === "video" || res.id.toLowerCase().indexOf("video") > -1) && + res.type === "inbound-rtp" && res.id.indexOf("rtcp") < 0) { + // New stats + inStats = true; + } else if(res.type == 'ssrc' && res.bytesReceived && + (res.googCodecName === "VP8" || res.googCodecName === "")) { + // Older Chromer versions + inStats = true; + } + // Parse stats now + if(inStats) { + config.bitrate[target].bsnow = res.bytesReceived; + config.bitrate[target].tsnow = res.timestamp; + if(config.bitrate[target].bsbefore === null || config.bitrate[target].tsbefore === null) { + // Skip this round + config.bitrate[target].bsbefore = config.bitrate[target].bsnow; + config.bitrate[target].tsbefore = config.bitrate[target].tsnow; + } else { + // Calculate bitrate + let timePassed = config.bitrate[target].tsnow - config.bitrate[target].tsbefore; + if(Janus.webRTCAdapter.browserDetails.browser === "safari") + timePassed = timePassed/1000; // Apparently the timestamp is in microseconds, in Safari + let bitRate = Math.round((config.bitrate[target].bsnow - config.bitrate[target].bsbefore) * 8 / timePassed); + if(Janus.webRTCAdapter.browserDetails.browser === "safari") + bitRate = parseInt(bitRate/1000); + config.bitrate[target].value = bitRate + ' kbits/sec'; + //~ Janus.log("Estimated bitrate is " + config.bitrate.value); + config.bitrate[target].bsbefore = config.bitrate[target].bsnow; + config.bitrate[target].tsbefore = config.bitrate[target].tsnow; + } + } + }); + }); + }, 1000); + return "0 kbits/sec"; // We don't have a bitrate value yet + } + return config.bitrate[target].value; + } else { + Janus.warn("Getting the video bitrate unsupported by browser"); + return "Feature unsupported by browser"; } } - lines.splice(insertAt, 0, 'a=ssrc-group:FID ' + ssrc[2] + ' ' + ssrc_fid[2]); - lines.splice(insertAt, 0, 'a=ssrc-group:FID ' + ssrc[1] + ' ' + ssrc_fid[1]); - lines.splice(insertAt, 0, 'a=ssrc-group:FID ' + ssrc[0] + ' ' + ssrc_fid[0]); - lines.splice(insertAt, 0, 'a=ssrc-group:SIM ' + ssrc[0] + ' ' + ssrc[1] + ' ' + ssrc[2]); - sdp = lines.join("\r\n"); - if(!sdp.endsWith("\r\n")) - sdp += "\r\n"; - return sdp; - } - // Helper methods to parse a media object - function isAudioSendEnabled(media) { - Janus.debug("isAudioSendEnabled:", media); - if(!media) - return true; // Default - if(media.audio === false) - return false; // Generic audio has precedence - if(media.audioSend === undefined || media.audioSend === null) - return true; // Default - return (media.audioSend === true); - } - - function isAudioSendRequired(media) { - Janus.debug("isAudioSendRequired:", media); - if(!media) - return false; // Default - if(media.audio === false || media.audioSend === false) - return false; // If we're not asking to capture audio, it's not required - if(media.failIfNoAudio === undefined || media.failIfNoAudio === null) - return false; // Default - return (media.failIfNoAudio === true); - } - - function isAudioRecvEnabled(media) { - Janus.debug("isAudioRecvEnabled:", media); - if(!media) - return true; // Default - if(media.audio === false) - return false; // Generic audio has precedence - if(media.audioRecv === undefined || media.audioRecv === null) - return true; // Default - return (media.audioRecv === true); - } - - function isVideoSendEnabled(media) { - Janus.debug("isVideoSendEnabled:", media); - if(!media) - return true; // Default - if(media.video === false) - return false; // Generic video has precedence - if(media.videoSend === undefined || media.videoSend === null) - return true; // Default - return (media.videoSend === true); - } - - function isVideoSendRequired(media) { - Janus.debug("isVideoSendRequired:", media); - if(!media) - return false; // Default - if(media.video === false || media.videoSend === false) - return false; // If we're not asking to capture video, it's not required - if(media.failIfNoVideo === undefined || media.failIfNoVideo === null) - return false; // Default - return (media.failIfNoVideo === true); - } - - function isVideoRecvEnabled(media) { - Janus.debug("isVideoRecvEnabled:", media); - if(!media) - return true; // Default - if(media.video === false) - return false; // Generic video has precedence - if(media.videoRecv === undefined || media.videoRecv === null) - return true; // Default - return (media.videoRecv === true); - } - - function isScreenSendEnabled(media) { - Janus.debug("isScreenSendEnabled:", media); - if (!media) - return false; - if (typeof media.video !== 'object' || typeof media.video.mandatory !== 'object') - return false; - var constraints = media.video.mandatory; - if (constraints.chromeMediaSource) - return constraints.chromeMediaSource === 'desktop' || constraints.chromeMediaSource === 'screen'; - else if (constraints.mozMediaSource) - return constraints.mozMediaSource === 'window' || constraints.mozMediaSource === 'screen'; - else if (constraints.mediaSource) - return constraints.mediaSource === 'window' || constraints.mediaSource === 'screen'; - return false; - } - - function isDataEnabled(media) { - Janus.debug("isDataEnabled:", media); - if(Janus.webRTCAdapter.browserDetails.browser === "edge") { - Janus.warn("Edge doesn't support data channels yet"); - return false; + function setBitrate(handleId, mid, bitrate) { + let pluginHandle = pluginHandles.get(handleId); + if(!pluginHandle || !pluginHandle.webrtcStuff) { + Janus.warn('Invalid handle'); + return; + } + let config = pluginHandle.webrtcStuff; + if(!config.pc) { + Janus.warn('Invalid PeerConnection'); + return; + } + let transceiver = config.pc.getTransceivers().find(t => (t.mid === mid)); + if(!transceiver) { + Janus.warn('No transceiver with mid', mid); + return; + } + if(!transceiver.sender) { + Janus.warn('No sender for transceiver with mid', mid); + return; + } + let params = transceiver.sender.getParameters(); + if(!params || !params.encodings || params.encodings.length === 0) { + Janus.warn('No parameters encodings'); + } else if(params.encodings.length > 1) { + Janus.warn('Ignoring bitrate for simulcast track, use sendEncodings for that'); + } else if(isNaN(bitrate) || bitrate < 0) { + Janus.warn('Invalid bitrate (must be a positive integer)'); + } else { + params.encodings[0].maxBitrate = bitrate; + transceiver.sender.setParameters(params); + } + } + + function webrtcError(error) { + Janus.error("WebRTC error:", error); + } + + function cleanupWebrtc(handleId, hangupRequest) { + Janus.log("Cleaning WebRTC stuff"); + let pluginHandle = pluginHandles.get(handleId); + if(!pluginHandle) { + // Nothing to clean + return; + } + let config = pluginHandle.webrtcStuff; + if(config) { + if(hangupRequest === true) { + // Send a hangup request (we don't really care about the response) + let request = { "janus": "hangup", "transaction": Janus.randomString(12) }; + if(pluginHandle.token) + request["token"] = pluginHandle.token; + if(apisecret) + request["apisecret"] = apisecret; + Janus.debug("Sending hangup request (handle=" + handleId + "):"); + Janus.debug(request); + if(websockets) { + request["session_id"] = sessionId; + request["handle_id"] = handleId; + ws.send(JSON.stringify(request)); + } else { + Janus.httpAPICall(server + "/" + sessionId + "/" + handleId, { + verb: 'POST', + withCredentials: withCredentials, + body: request + }); + } + } + // Cleanup stack + if(config.volume) { + if(config.volume["local"] && config.volume["local"].timer) + clearInterval(config.volume["local"].timer); + if(config.volume["remote"] && config.volume["remote"].timer) + clearInterval(config.volume["remote"].timer); + } + for(let i in config.bitrate) { + if(config.bitrate[i].timer) + clearInterval(config.bitrate[i].timer); + } + config.bitrate = {}; + if(!config.streamExternal && config.myStream) { + Janus.log("Stopping local stream tracks"); + Janus.stopAllTracks(config.myStream); + } + config.streamExternal = false; + config.myStream = null; + // Close PeerConnection + try { + config.pc.close(); + // eslint-disable-next-line no-unused-vars + } catch(e) { + // Do nothing + } + config.pc = null; + config.candidates = null; + config.mySdp = null; + config.remoteSdp = null; + config.iceDone = false; + config.dataChannel = {}; + config.dtmfSender = null; + config.insertableStreams = false; + config.externalEncryption = false; + } + pluginHandle.oncleanup(); + } + + function isTrickleEnabled(trickle) { + Janus.debug("isTrickleEnabled:", trickle); + return (trickle === false) ? false : true; } - if(media === undefined || media === null) - return false; // Default - return (media.data === true); } - function isTrickleEnabled(trickle) { - Janus.debug("isTrickleEnabled:", trickle); - return (trickle === false) ? false : true; - } -} + return Janus; + +}));