if (console) console.log("WEBRTC Script version 17 STABLE") /**************************************************** *********** CROSS BROWSER CORS REQUEST ************* ****************************************************/ function createCORSRequest(config) { if (!config) throw "Parameter 'config' (obj) required"; if (!config || !config.method || !config.url || !config.async) { throw "Parameter config={'method, 'url', 'async'='true|false', timeout (optional), onload (optional), onerror (optional), ontimeout (optional)} required"; } var method = config.method; var url = config.url; var async = config.async; var timeout = config.timeout ? config.timeout : 15000; var cbOnLoad = (config.onload && typeof config.onload === "function") ? config.onload : function () {}; var cbOnError = (config.onerror && typeof config.onerror === "function") ? config.onerror : function () {}; var cbOnTimeout = (config.ontimeout && typeof config.ontimeout === "function") ? config.ontimeout : function () {}; var x, d = 0; if (window.XDomainRequest) { x = new XDomainRequest(); x.timeout = timeout; x.ontimeout = cbOnTimeout; x.onerror = cbOnError; x.onprogress = function () {}; x.onload = function () { if (async) { cbOnLoad(x); } d = 1; }; x.open(method, url); if (async) { return x; } else { while (d) {} cbOnLoad(x); return x; } } else { x = new XMLHttpRequest(); x.ontimeout = function () { cbOnTimeout(); }; x.onreadystatechange = function () { if (async && x.readyState == 4) { cbOnLoad(x); } }; x.open(method, url, async); x.timeout = timeout; x.onerror = function (err) { cbOnError(err); }; if (!async) { cbOnLoad(x); } return x; } } /**************************************************** ********************** POLYFILLS ******************* ****************************************************/ if (typeof console == "undefined") { console = { "log": function () {}, "debug": function () {}, "info": function () {}, "warn": function () {}, "error": function () {} }; } // global error handler /* window.onerror = function(err) { de.modima.communication.webrtcFeedback("n/a", "General error: " + err.msg + " ### " + err.stack); } */ /**************************************************** ********************** NAMESPACE ******************* ****************************************************/ var de; if (de == "[JavaPackage de]") throw new Error("Package 'de' is already in use by a java api!"); if (!de) de = {}; if (!de.modima) de.modima = {}; if (!de.modima.communication) de.modima.communication = {}; /**************************************************** ********************** LOGGER ********************** ****************************************************/ (function (module) { var LOGPREFIX = "WEBRTC"; /** * 0 - log / debug * 1 - info * 2 - warn * 3 - error */ window.webrtcloglevel = 0; function Logger() {} /** Alias for log() **/ Logger.prototype.debug = function () { try { this.log.apply(this, arguments); } catch (err) { if (console && console.error) console.error(err); } }; Logger.prototype.log = function () { if (window.webrtcloglevel > 0) return; try { if (console !== null && typeof console !== "undefined" && console.log !== null && typeof console.log == "function") { [].unshift.call(arguments, LOGPREFIX); console.log.apply(this, arguments); } } catch (err) { if (console && console.error) console.error(err); } }; Logger.prototype.info = function () { if (window.webrtcloglevel > 1) return; try { if (console !== null && typeof console !== "undefined" && console.info !== null && typeof console.info == "function") { [].unshift.call(arguments, LOGPREFIX); console.info.apply(this, arguments); } } catch (err) { if (console && console.error) console.error(err); } }; Logger.prototype.warn = function () { if (window.webrtcloglevel > 2) return; try { if (console !== null && typeof console !== "undefined" && console.warn !== null && typeof console.warn == "function") { [].unshift.call(arguments, LOGPREFIX); console.warn.apply(this, arguments); } } catch (err) { if (console && console.error) console.error(err); } }; Logger.prototype.error = function () { if (window.webrtcloglevel > 3) return; try { if (console !== null && typeof console !== "undefined" && console.error !== null && typeof console.error == "function") { [].unshift.call(arguments, LOGPREFIX); console.error.apply(this, arguments); } } catch (err) { if (console && console.error) console.error(err); } }; module.logger = new Logger(); }(de.modima.communication)); /**************************************************** ******************* ERROR LOG ********************** ****************************************************/ de.modima.communication.webrtcFeedback = function (msg, properties, force, iserror) { // prevent flooding if (!force && this.lastFeedback && this.lastFeedback > Date.now() - 1000) { console.log("OMIT FEEDBACK"); return; }; this.lastFeedback = Date.now(); try { var data = { host: window.location.hostname, }; for (var p in properties) { data[p] = properties[p]; } if (msg) data.msg = msg; if (typeof BrowserDetect != "undefined") { data.browser = BrowserDetect.browser; data.os = BrowserDetect.OS; } if (navigator && navigator.userAgent) { data.useragent = navigator.userAgent; } de.modima.communication.logger.log("SEND WEBRTC FEEDBACK: ", data); //var url = "//test-dot-appstackfive.appspot.com/!rtf3to6LDYQns6jMxx6n9sG9opFx90/api/jsonmonster/eat"; var url; if (iserror) { url = "//webrtc.24dial.com/!oND6ih_xVfkvF7qgrj9n5RaOB5Q79AcFR01l8OqVSXQKytyPp47bp_ai81AcFk2XX8K3w_NQDgi30_kGXhZjfQrWjBBTnBht31/api/logger/connectionError" } else { url = "//webrtc.24dial.com/!oND6ih_xVfkvF7qgrj9n5RaOB5Q79AcFR01l8OqVSXQKytyPp47bp_ai81AcFk2XX8K3w_NQDgi30_kGXhZjfQrWjBBTnBht31/api/logger/log" } if (typeof ajax != "undefined") { ajax.post(url, data); } else { var req = createCORSRequest({ "method": "POST", "url": url, "async": true }); req.onreadystatechange = function (aEvt) { if (req.readyState == 4) { if (req.status <= 300) { de.modima.communication.logger.log("SUCCESS reporting webrtc feedback"); } else { de.modima.communication.logger.log("ERROR reporting webrtc feedback: " + req.status + " - " + req.statusText); } } }; req.send(JSON.stringify(data)); return req.onreadystatechange(); } } catch (e) { de.modima.communication.logger.log("webrtcFeedback failed " + e); } }; (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 e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= 14393 && url.indexOf('?transport=udp') === -1; }); delete server.url; server.urls = isString ? urls[0] : urls; return !!urls.length; } }); } // Determines the intersection of local and remote capabilities. function getCommonCapabilities(localCapabilities, remoteCapabilities) { var commonCapabilities = { codecs: [], headerExtensions: [], fecMechanisms: [] }; var findCodecByPayloadType = function(pt, codecs) { pt = parseInt(pt, 10); for (var i = 0; i < codecs.length; i++) { if (codecs[i].payloadType === pt || codecs[i].preferredPayloadType === pt) { return codecs[i]; } } }; var rtxCapabilityMatches = function(lRtx, rRtx, lCodecs, rCodecs) { var lCodec = findCodecByPayloadType(lRtx.parameters.apt, lCodecs); var rCodec = findCodecByPayloadType(rRtx.parameters.apt, rCodecs); return lCodec && rCodec && lCodec.name.toLowerCase() === rCodec.name.toLowerCase(); }; localCapabilities.codecs.forEach(function(lCodec) { for (var i = 0; i < remoteCapabilities.codecs.length; i++) { var rCodec = remoteCapabilities.codecs[i]; if (lCodec.name.toLowerCase() === rCodec.name.toLowerCase() && lCodec.clockRate === rCodec.clockRate) { if (lCodec.name.toLowerCase() === 'rtx' && lCodec.parameters && rCodec.parameters.apt) { // for RTX we need to find the local rtx that has a apt // which points to the same local codec as the remote one. if (!rtxCapabilityMatches(lCodec, rCodec, localCapabilities.codecs, remoteCapabilities.codecs)) { continue; } } rCodec = JSON.parse(JSON.stringify(rCodec)); // deepcopy // number of channels is the highest common number of channels rCodec.numChannels = Math.min(lCodec.numChannels, rCodec.numChannels); // push rCodec so we reply with offerer payload type commonCapabilities.codecs.push(rCodec); // determine common feedback mechanisms rCodec.rtcpFeedback = rCodec.rtcpFeedback.filter(function(fb) { for (var j = 0; j < lCodec.rtcpFeedback.length; j++) { if (lCodec.rtcpFeedback[j].type === fb.type && lCodec.rtcpFeedback[j].parameter === fb.parameter) { return true; } } return false; }); // FIXME: also need to determine .parameters // see https://github.com/openpeer/ortc/issues/569 break; } } }); localCapabilities.headerExtensions.forEach(function(lHeaderExtension) { for (var i = 0; i < remoteCapabilities.headerExtensions.length; i++) { var rHeaderExtension = remoteCapabilities.headerExtensions[i]; if (lHeaderExtension.uri === rHeaderExtension.uri) { commonCapabilities.headerExtensions.push(rHeaderExtension); break; } } }); // FIXME: fecMechanisms return commonCapabilities; } // is action=setLocalDescription with type allowed in signalingState function isActionAllowedInSignalingState(action, type, signalingState) { return { offer: { setLocalDescription: ['stable', 'have-local-offer'], setRemoteDescription: ['stable', 'have-remote-offer'] }, answer: { setLocalDescription: ['have-remote-offer', 'have-local-pranswer'], setRemoteDescription: ['have-local-offer', 'have-remote-pranswer'] } }[type][action].indexOf(signalingState) !== -1; } function maybeAddCandidate(iceTransport, candidate) { // Edge's internal representation adds some fields therefore // not all fieldѕ are taken into account. var alreadyAdded = iceTransport.getRemoteCandidates() .find(function(remoteCandidate) { return candidate.foundation === remoteCandidate.foundation && candidate.ip === remoteCandidate.ip && candidate.port === remoteCandidate.port && candidate.priority === remoteCandidate.priority && candidate.protocol === remoteCandidate.protocol && candidate.type === remoteCandidate.type; }); if (!alreadyAdded) { iceTransport.addRemoteCandidate(candidate); } return !alreadyAdded; } // https://w3c.github.io/mediacapture-main/#mediastream // Helper function to add the track to the stream and // dispatch the event ourselves. function addTrackToStreamAndFireEvent(track, stream) { stream.addTrack(track); var e = new Event('addtrack'); // TODO: MediaStreamTrackEvent e.track = track; stream.dispatchEvent(e); } function removeTrackFromStreamAndFireEvent(track, stream) { stream.removeTrack(track); var e = new Event('removetrack'); // TODO: MediaStreamTrackEvent e.track = track; stream.dispatchEvent(e); } function fireAddTrack(pc, track, receiver, streams) { var trackEvent = new Event('track'); trackEvent.track = track; trackEvent.receiver = receiver; trackEvent.transceiver = {receiver: receiver}; trackEvent.streams = streams; window.setTimeout(function() { pc._dispatchEvent('track', trackEvent); }); } function makeError(name, description) { var e = new Error(description); e.name = name; return e; } module.exports = function(window, edgeVersion) { var RTCPeerConnection = function(config) { var pc = this; var _eventTarget = document.createDocumentFragment(); ['addEventListener', 'removeEventListener', 'dispatchEvent'] .forEach(function(method) { pc[method] = _eventTarget[method].bind(_eventTarget); }); this.canTrickleIceCandidates = null; this.needNegotiation = false; this.localStreams = []; this.remoteStreams = []; this.localDescription = null; this.remoteDescription = null; this.signalingState = 'stable'; this.iceConnectionState = 'new'; this.iceGatheringState = 'new'; config = JSON.parse(JSON.stringify(config || {})); this.usingBundle = config.bundlePolicy === 'max-bundle'; if (config.rtcpMuxPolicy === 'negotiate') { throw(makeError('NotSupportedError', 'rtcpMuxPolicy \'negotiate\' is not supported')); } else if (!config.rtcpMuxPolicy) { config.rtcpMuxPolicy = 'require'; } switch (config.iceTransportPolicy) { case 'all': case 'relay': break; default: config.iceTransportPolicy = 'all'; break; } switch (config.bundlePolicy) { case 'balanced': case 'max-compat': case 'max-bundle': break; default: config.bundlePolicy = 'balanced'; break; } config.iceServers = filterIceServers(config.iceServers || [], edgeVersion); this._iceGatherers = []; if (config.iceCandidatePoolSize) { for (var i = config.iceCandidatePoolSize; i > 0; i--) { this._iceGatherers = new window.RTCIceGatherer({ iceServers: config.iceServers, gatherPolicy: config.iceTransportPolicy }); } } else { config.iceCandidatePoolSize = 0; } this._config = config; // per-track iceGathers, iceTransports, dtlsTransports, rtpSenders, ... // everything that is needed to describe a SDP m-line. this.transceivers = []; this._sdpSessionId = SDPUtils.generateSessionId(); this._sdpSessionVersion = 0; this._dtlsRole = undefined; // role for a=setup to use in answers. this._isClosed = false; }; // set up event handlers on prototype RTCPeerConnection.prototype.onicecandidate = null; RTCPeerConnection.prototype.onaddstream = null; RTCPeerConnection.prototype.ontrack = null; RTCPeerConnection.prototype.onremovestream = null; RTCPeerConnection.prototype.onsignalingstatechange = null; RTCPeerConnection.prototype.oniceconnectionstatechange = null; RTCPeerConnection.prototype.onicegatheringstatechange = null; RTCPeerConnection.prototype.onnegotiationneeded = null; RTCPeerConnection.prototype.ondatachannel = null; RTCPeerConnection.prototype._dispatchEvent = function(name, event) { if (this._isClosed) { return; } this.dispatchEvent(event); if (typeof this['on' + name] === 'function') { this['on' + name](event); } }; RTCPeerConnection.prototype._emitGatheringStateChange = function() { var event = new Event('icegatheringstatechange'); this._dispatchEvent('icegatheringstatechange', event); }; RTCPeerConnection.prototype.getConfiguration = function() { return this._config; }; RTCPeerConnection.prototype.getLocalStreams = function() { return this.localStreams; }; RTCPeerConnection.prototype.getRemoteStreams = function() { return this.remoteStreams; }; // internal helper to create a transceiver object. // (whih is not yet the same as the WebRTC 1.0 transceiver) RTCPeerConnection.prototype._createTransceiver = function(kind) { var hasBundleTransport = this.transceivers.length > 0; var transceiver = { track: null, iceGatherer: null, iceTransport: null, dtlsTransport: null, localCapabilities: null, remoteCapabilities: null, rtpSender: null, rtpReceiver: null, kind: kind, mid: null, sendEncodingParameters: null, recvEncodingParameters: null, stream: null, associatedRemoteMediaStreams: [], wantReceive: true }; if (this.usingBundle && hasBundleTransport) { transceiver.iceTransport = this.transceivers[0].iceTransport; transceiver.dtlsTransport = this.transceivers[0].dtlsTransport; } else { var transports = this._createIceAndDtlsTransports(); transceiver.iceTransport = transports.iceTransport; transceiver.dtlsTransport = transports.dtlsTransport; } this.transceivers.push(transceiver); return transceiver; }; RTCPeerConnection.prototype.addTrack = function(track, stream) { var alreadyExists = this.transceivers.find(function(s) { return s.track === track; }); if (alreadyExists) { throw makeError('InvalidAccessError', 'Track already exists.'); } if (this.signalingState === 'closed') { throw makeError('InvalidStateError', 'Attempted to call addTrack on a closed peerconnection.'); } var transceiver; for (var i = 0; i < this.transceivers.length; i++) { if (!this.transceivers[i].track && this.transceivers[i].kind === track.kind) { transceiver = this.transceivers[i]; } } if (!transceiver) { transceiver = this._createTransceiver(track.kind); } this._maybeFireNegotiationNeeded(); if (this.localStreams.indexOf(stream) === -1) { this.localStreams.push(stream); } transceiver.track = track; transceiver.stream = stream; transceiver.rtpSender = new window.RTCRtpSender(track, transceiver.dtlsTransport); return transceiver.rtpSender; }; RTCPeerConnection.prototype.addStream = function(stream) { var pc = this; if (edgeVersion >= 15025) { stream.getTracks().forEach(function(track) { pc.addTrack(track, stream); }); } else { // Clone is necessary for local demos mostly, attaching directly // to two different senders does not work (build 10547). // Fixed in 15025 (or earlier) var clonedStream = stream.clone(); stream.getTracks().forEach(function(track, idx) { var clonedTrack = clonedStream.getTracks()[idx]; track.addEventListener('enabled', function(event) { clonedTrack.enabled = event.enabled; }); }); clonedStream.getTracks().forEach(function(track) { pc.addTrack(track, clonedStream); }); } }; RTCPeerConnection.prototype.removeTrack = function(sender) { if (!(sender instanceof window.RTCRtpSender)) { throw new TypeError('Argument 1 of RTCPeerConnection.removeTrack ' + 'does not implement interface RTCRtpSender.'); } var transceiver = this.transceivers.find(function(t) { return t.rtpSender === sender; }); if (!transceiver) { throw makeError('InvalidAccessError', 'Sender was not created by this connection.'); } var stream = transceiver.stream; transceiver.rtpSender.stop(); transceiver.rtpSender = null; transceiver.track = null; transceiver.stream = null; // remove the stream from the set of local streams var localStreams = this.transceivers.map(function(t) { return t.stream; }); if (localStreams.indexOf(stream) === -1 && this.localStreams.indexOf(stream) > -1) { this.localStreams.splice(this.localStreams.indexOf(stream), 1); } this._maybeFireNegotiationNeeded(); }; RTCPeerConnection.prototype.removeStream = function(stream) { var pc = this; stream.getTracks().forEach(function(track) { var sender = pc.getSenders().find(function(s) { return s.track === track; }); if (sender) { pc.removeTrack(sender); } }); }; RTCPeerConnection.prototype.getSenders = function() { return this.transceivers.filter(function(transceiver) { return !!transceiver.rtpSender; }) .map(function(transceiver) { return transceiver.rtpSender; }); }; RTCPeerConnection.prototype.getReceivers = function() { return this.transceivers.filter(function(transceiver) { return !!transceiver.rtpReceiver; }) .map(function(transceiver) { return transceiver.rtpReceiver; }); }; RTCPeerConnection.prototype._createIceGatherer = function(sdpMLineIndex, usingBundle) { var pc = this; if (usingBundle && sdpMLineIndex > 0) { return this.transceivers[0].iceGatherer; } else if (this._iceGatherers.length) { return this._iceGatherers.shift(); } var iceGatherer = new window.RTCIceGatherer({ iceServers: this._config.iceServers, gatherPolicy: this._config.iceTransportPolicy }); Object.defineProperty(iceGatherer, 'state', {value: 'new', writable: true} ); this.transceivers[sdpMLineIndex].candidates = []; this.transceivers[sdpMLineIndex].bufferCandidates = function(event) { var end = !event.candidate || Object.keys(event.candidate).length === 0; // polyfill since RTCIceGatherer.state is not implemented in // Edge 10547 yet. iceGatherer.state = end ? 'completed' : 'gathering'; if (pc.transceivers[sdpMLineIndex].candidates !== null) { pc.transceivers[sdpMLineIndex].candidates.push(event.candidate); } }; iceGatherer.addEventListener('localcandidate', this.transceivers[sdpMLineIndex].bufferCandidates); return iceGatherer; }; // start gathering from an RTCIceGatherer. RTCPeerConnection.prototype._gather = function(mid, sdpMLineIndex) { var pc = this; var iceGatherer = this.transceivers[sdpMLineIndex].iceGatherer; if (iceGatherer.onlocalcandidate) { return; } var candidates = this.transceivers[sdpMLineIndex].candidates; this.transceivers[sdpMLineIndex].candidates = null; iceGatherer.removeEventListener('localcandidate', this.transceivers[sdpMLineIndex].bufferCandidates); iceGatherer.onlocalcandidate = function(evt) { if (pc.usingBundle && sdpMLineIndex > 0) { // if we know that we use bundle we can drop candidates with // ѕdpMLineIndex > 0. If we don't do this then our state gets // confused since we dispose the extra ice gatherer. return; } var event = new Event('icecandidate'); event.candidate = {sdpMid: mid, sdpMLineIndex: sdpMLineIndex}; var cand = evt.candidate; // Edge emits an empty object for RTCIceCandidateComplete‥ var end = !cand || Object.keys(cand).length === 0; if (end) { // polyfill since RTCIceGatherer.state is not implemented in // Edge 10547 yet. if (iceGatherer.state === 'new' || iceGatherer.state === 'gathering') { iceGatherer.state = 'completed'; } } else { if (iceGatherer.state === 'new') { iceGatherer.state = 'gathering'; } // RTCIceCandidate doesn't have a component, needs to be added cand.component = 1; event.candidate.candidate = SDPUtils.writeCandidate(cand); } // update local description. var sections = SDPUtils.splitSections(pc.localDescription.sdp); if (!end) { sections[event.candidate.sdpMLineIndex + 1] += 'a=' + event.candidate.candidate + '\r\n'; } else { sections[event.candidate.sdpMLineIndex + 1] += 'a=end-of-candidates\r\n'; } pc.localDescription.sdp = sections.join(''); var complete = pc.transceivers.every(function(transceiver) { return transceiver.iceGatherer && transceiver.iceGatherer.state === 'completed'; }); if (pc.iceGatheringState !== 'gathering') { pc.iceGatheringState = 'gathering'; pc._emitGatheringStateChange(); } // Emit candidate. Also emit null candidate when all gatherers are // complete. if (!end) { pc._dispatchEvent('icecandidate', event); } if (complete) { pc._dispatchEvent('icecandidate', new Event('icecandidate')); pc.iceGatheringState = 'complete'; pc._emitGatheringStateChange(); } }; // emit already gathered candidates. window.setTimeout(function() { candidates.forEach(function(candidate) { var e = new Event('RTCIceGatherEvent'); e.candidate = candidate; iceGatherer.onlocalcandidate(e); }); }, 0); }; // Create ICE transport and DTLS transport. RTCPeerConnection.prototype._createIceAndDtlsTransports = function() { var pc = this; var iceTransport = new window.RTCIceTransport(null); iceTransport.onicestatechange = function() { pc._updateConnectionState(); }; var dtlsTransport = new window.RTCDtlsTransport(iceTransport); dtlsTransport.ondtlsstatechange = function() { pc._updateConnectionState(); }; dtlsTransport.onerror = function() { // onerror does not set state to failed by itpc. Object.defineProperty(dtlsTransport, 'state', {value: 'failed', writable: true}); pc._updateConnectionState(); }; return { iceTransport: iceTransport, dtlsTransport: dtlsTransport }; }; // Destroy ICE gatherer, ICE transport and DTLS transport. // Without triggering the callbacks. RTCPeerConnection.prototype._disposeIceAndDtlsTransports = function( sdpMLineIndex) { var iceGatherer = this.transceivers[sdpMLineIndex].iceGatherer; if (iceGatherer) { delete iceGatherer.onlocalcandidate; delete this.transceivers[sdpMLineIndex].iceGatherer; } var iceTransport = this.transceivers[sdpMLineIndex].iceTransport; if (iceTransport) { delete iceTransport.onicestatechange; delete this.transceivers[sdpMLineIndex].iceTransport; } var dtlsTransport = this.transceivers[sdpMLineIndex].dtlsTransport; if (dtlsTransport) { delete dtlsTransport.ondtlsstatechange; delete dtlsTransport.onerror; delete this.transceivers[sdpMLineIndex].dtlsTransport; } }; // Start the RTP Sender and Receiver for a transceiver. RTCPeerConnection.prototype._transceive = function(transceiver, send, recv) { var params = getCommonCapabilities(transceiver.localCapabilities, transceiver.remoteCapabilities); if (send && transceiver.rtpSender) { params.encodings = transceiver.sendEncodingParameters; params.rtcp = { cname: SDPUtils.localCName, compound: transceiver.rtcpParameters.compound }; if (transceiver.recvEncodingParameters.length) { params.rtcp.ssrc = transceiver.recvEncodingParameters[0].ssrc; } transceiver.rtpSender.send(params); } if (recv && transceiver.rtpReceiver && params.codecs.length > 0) { // remove RTX field in Edge 14942 if (transceiver.kind === 'video' && transceiver.recvEncodingParameters && edgeVersion < 15019) { transceiver.recvEncodingParameters.forEach(function(p) { delete p.rtx; }); } if (transceiver.recvEncodingParameters.length) { params.encodings = transceiver.recvEncodingParameters; } params.rtcp = { compound: transceiver.rtcpParameters.compound }; if (transceiver.rtcpParameters.cname) { params.rtcp.cname = transceiver.rtcpParameters.cname; } if (transceiver.sendEncodingParameters.length) { params.rtcp.ssrc = transceiver.sendEncodingParameters[0].ssrc; } transceiver.rtpReceiver.receive(params); } }; RTCPeerConnection.prototype.setLocalDescription = function(description) { var pc = this; if (!isActionAllowedInSignalingState('setLocalDescription', description.type, this.signalingState) || this._isClosed) { return Promise.reject(makeError('InvalidStateError', 'Can not set local ' + description.type + ' in state ' + pc.signalingState)); } var sections; var sessionpart; if (description.type === 'offer') { // VERY limited support for SDP munging. Limited to: // * changing the order of codecs sections = SDPUtils.splitSections(description.sdp); sessionpart = sections.shift(); sections.forEach(function(mediaSection, sdpMLineIndex) { var caps = SDPUtils.parseRtpParameters(mediaSection); pc.transceivers[sdpMLineIndex].localCapabilities = caps; }); this.transceivers.forEach(function(transceiver, sdpMLineIndex) { pc._gather(transceiver.mid, sdpMLineIndex); }); } else if (description.type === 'answer') { sections = SDPUtils.splitSections(pc.remoteDescription.sdp); sessionpart = sections.shift(); var isIceLite = SDPUtils.matchPrefix(sessionpart, 'a=ice-lite').length > 0; sections.forEach(function(mediaSection, sdpMLineIndex) { var transceiver = pc.transceivers[sdpMLineIndex]; var iceGatherer = transceiver.iceGatherer; var iceTransport = transceiver.iceTransport; var dtlsTransport = transceiver.dtlsTransport; var localCapabilities = transceiver.localCapabilities; var remoteCapabilities = transceiver.remoteCapabilities; // treat bundle-only as not-rejected. var rejected = SDPUtils.isRejected(mediaSection) && SDPUtils.matchPrefix(mediaSection, 'a=bundle-only').length === 0; if (!rejected && !transceiver.isDatachannel) { var remoteIceParameters = SDPUtils.getIceParameters( mediaSection, sessionpart); var remoteDtlsParameters = SDPUtils.getDtlsParameters( mediaSection, sessionpart); if (isIceLite) { remoteDtlsParameters.role = 'server'; } if (!pc.usingBundle || sdpMLineIndex === 0) { pc._gather(transceiver.mid, sdpMLineIndex); if (iceTransport.state === 'new') { iceTransport.start(iceGatherer, remoteIceParameters, isIceLite ? 'controlling' : 'controlled'); } if (dtlsTransport.state === 'new') { dtlsTransport.start(remoteDtlsParameters); } } // Calculate intersection of capabilities. var params = getCommonCapabilities(localCapabilities, remoteCapabilities); // Start the RTCRtpSender. The RTCRtpReceiver for this // transceiver has already been started in setRemoteDescription. pc._transceive(transceiver, params.codecs.length > 0, false); } }); } this.localDescription = { type: description.type, sdp: description.sdp }; switch (description.type) { case 'offer': this._updateSignalingState('have-local-offer'); break; case 'answer': this._updateSignalingState('stable'); break; default: throw new TypeError('unsupported type "' + description.type + '"'); } return Promise.resolve(); }; RTCPeerConnection.prototype.setRemoteDescription = function(description) { var pc = this; if (!isActionAllowedInSignalingState('setRemoteDescription', description.type, this.signalingState) || this._isClosed) { return Promise.reject(makeError('InvalidStateError', 'Can not set remote ' + description.type + ' in state ' + pc.signalingState)); } var streams = {}; this.remoteStreams.forEach(function(stream) { streams[stream.id] = stream; }); var receiverList = []; var sections = SDPUtils.splitSections(description.sdp); var sessionpart = sections.shift(); var isIceLite = SDPUtils.matchPrefix(sessionpart, 'a=ice-lite').length > 0; var usingBundle = SDPUtils.matchPrefix(sessionpart, 'a=group:BUNDLE ').length > 0; this.usingBundle = usingBundle; var iceOptions = SDPUtils.matchPrefix(sessionpart, 'a=ice-options:')[0]; if (iceOptions) { this.canTrickleIceCandidates = iceOptions.substr(14).split(' ') .indexOf('trickle') >= 0; } else { this.canTrickleIceCandidates = false; } sections.forEach(function(mediaSection, sdpMLineIndex) { var lines = SDPUtils.splitLines(mediaSection); var kind = SDPUtils.getKind(mediaSection); // treat bundle-only as not-rejected. var rejected = SDPUtils.isRejected(mediaSection) && SDPUtils.matchPrefix(mediaSection, 'a=bundle-only').length === 0; var protocol = lines[0].substr(2).split(' ')[2]; var direction = SDPUtils.getDirection(mediaSection, sessionpart); var remoteMsid = SDPUtils.parseMsid(mediaSection); var mid = SDPUtils.getMid(mediaSection) || SDPUtils.generateIdentifier(); // Reject datachannels which are not implemented yet. if (kind === 'application' && protocol === 'DTLS/SCTP') { pc.transceivers[sdpMLineIndex] = { mid: mid, isDatachannel: true }; return; } var transceiver; var iceGatherer; var iceTransport; var dtlsTransport; var rtpReceiver; var sendEncodingParameters; var recvEncodingParameters; var localCapabilities; var track; // FIXME: ensure the mediaSection has rtcp-mux set. var remoteCapabilities = SDPUtils.parseRtpParameters(mediaSection); var remoteIceParameters; var remoteDtlsParameters; if (!rejected) { remoteIceParameters = SDPUtils.getIceParameters(mediaSection, sessionpart); remoteDtlsParameters = SDPUtils.getDtlsParameters(mediaSection, sessionpart); remoteDtlsParameters.role = 'client'; } recvEncodingParameters = SDPUtils.parseRtpEncodingParameters(mediaSection); var rtcpParameters = SDPUtils.parseRtcpParameters(mediaSection); var isComplete = SDPUtils.matchPrefix(mediaSection, 'a=end-of-candidates', sessionpart).length > 0; var cands = SDPUtils.matchPrefix(mediaSection, 'a=candidate:') .map(function(cand) { return SDPUtils.parseCandidate(cand); }) .filter(function(cand) { return cand.component === 1; }); // Check if we can use BUNDLE and dispose transports. if ((description.type === 'offer' || description.type === 'answer') && !rejected && usingBundle && sdpMLineIndex > 0 && pc.transceivers[sdpMLineIndex]) { pc._disposeIceAndDtlsTransports(sdpMLineIndex); pc.transceivers[sdpMLineIndex].iceGatherer = pc.transceivers[0].iceGatherer; pc.transceivers[sdpMLineIndex].iceTransport = pc.transceivers[0].iceTransport; pc.transceivers[sdpMLineIndex].dtlsTransport = pc.transceivers[0].dtlsTransport; if (pc.transceivers[sdpMLineIndex].rtpSender) { pc.transceivers[sdpMLineIndex].rtpSender.setTransport( pc.transceivers[0].dtlsTransport); } if (pc.transceivers[sdpMLineIndex].rtpReceiver) { pc.transceivers[sdpMLineIndex].rtpReceiver.setTransport( pc.transceivers[0].dtlsTransport); } } if (description.type === 'offer' && !rejected) { transceiver = pc.transceivers[sdpMLineIndex] || pc._createTransceiver(kind); transceiver.mid = mid; if (!transceiver.iceGatherer) { transceiver.iceGatherer = pc._createIceGatherer(sdpMLineIndex, usingBundle); } if (cands.length && transceiver.iceTransport.state === 'new') { if (isComplete && (!usingBundle || sdpMLineIndex === 0)) { transceiver.iceTransport.setRemoteCandidates(cands); } else { cands.forEach(function(candidate) { maybeAddCandidate(transceiver.iceTransport, candidate); }); } } localCapabilities = window.RTCRtpReceiver.getCapabilities(kind); // filter RTX until additional stuff needed for RTX is implemented // in adapter.js if (edgeVersion < 15019) { localCapabilities.codecs = localCapabilities.codecs.filter( function(codec) { return codec.name !== 'rtx'; }); } sendEncodingParameters = transceiver.sendEncodingParameters || [{ ssrc: (2 * sdpMLineIndex + 2) * 1001 }]; // TODO: rewrite to use http://w3c.github.io/webrtc-pc/#set-associated-remote-streams var isNewTrack = false; if (direction === 'sendrecv' || direction === 'sendonly') { isNewTrack = !transceiver.rtpReceiver; rtpReceiver = transceiver.rtpReceiver || new window.RTCRtpReceiver(transceiver.dtlsTransport, kind); if (isNewTrack) { var stream; track = rtpReceiver.track; // FIXME: does not work with Plan B. if (remoteMsid && remoteMsid.stream === '-') { // no-op. a stream id of '-' means: no associated stream. } else if (remoteMsid) { if (!streams[remoteMsid.stream]) { streams[remoteMsid.stream] = new window.MediaStream(); Object.defineProperty(streams[remoteMsid.stream], 'id', { get: function() { return remoteMsid.stream; } }); } Object.defineProperty(track, 'id', { get: function() { return remoteMsid.track; } }); stream = streams[remoteMsid.stream]; } else { if (!streams.default) { streams.default = new window.MediaStream(); } stream = streams.default; } if (stream) { addTrackToStreamAndFireEvent(track, stream); transceiver.associatedRemoteMediaStreams.push(stream); } receiverList.push([track, rtpReceiver, stream]); } } else if (transceiver.rtpReceiver && transceiver.rtpReceiver.track) { transceiver.associatedRemoteMediaStreams.forEach(function(s) { var nativeTrack = s.getTracks().find(function(t) { return t.id === transceiver.rtpReceiver.track.id; }); if (nativeTrack) { removeTrackFromStreamAndFireEvent(nativeTrack, s); } }); transceiver.associatedRemoteMediaStreams = []; } transceiver.localCapabilities = localCapabilities; transceiver.remoteCapabilities = remoteCapabilities; transceiver.rtpReceiver = rtpReceiver; transceiver.rtcpParameters = rtcpParameters; transceiver.sendEncodingParameters = sendEncodingParameters; transceiver.recvEncodingParameters = recvEncodingParameters; // Start the RTCRtpReceiver now. The RTPSender is started in // setLocalDescription. pc._transceive(pc.transceivers[sdpMLineIndex], false, isNewTrack); } else if (description.type === 'answer' && !rejected) { transceiver = pc.transceivers[sdpMLineIndex]; iceGatherer = transceiver.iceGatherer; iceTransport = transceiver.iceTransport; dtlsTransport = transceiver.dtlsTransport; rtpReceiver = transceiver.rtpReceiver; sendEncodingParameters = transceiver.sendEncodingParameters; localCapabilities = transceiver.localCapabilities; pc.transceivers[sdpMLineIndex].recvEncodingParameters = recvEncodingParameters; pc.transceivers[sdpMLineIndex].remoteCapabilities = remoteCapabilities; pc.transceivers[sdpMLineIndex].rtcpParameters = rtcpParameters; if (cands.length && iceTransport.state === 'new') { if ((isIceLite || isComplete) && (!usingBundle || sdpMLineIndex === 0)) { iceTransport.setRemoteCandidates(cands); } else { cands.forEach(function(candidate) { maybeAddCandidate(transceiver.iceTransport, candidate); }); } } if (!usingBundle || sdpMLineIndex === 0) { if (iceTransport.state === 'new') { iceTransport.start(iceGatherer, remoteIceParameters, 'controlling'); } if (dtlsTransport.state === 'new') { dtlsTransport.start(remoteDtlsParameters); } } pc._transceive(transceiver, direction === 'sendrecv' || direction === 'recvonly', direction === 'sendrecv' || direction === 'sendonly'); // TODO: rewrite to use http://w3c.github.io/webrtc-pc/#set-associated-remote-streams if (rtpReceiver && (direction === 'sendrecv' || direction === 'sendonly')) { track = rtpReceiver.track; if (remoteMsid) { if (!streams[remoteMsid.stream]) { streams[remoteMsid.stream] = new window.MediaStream(); } addTrackToStreamAndFireEvent(track, streams[remoteMsid.stream]); receiverList.push([track, rtpReceiver, streams[remoteMsid.stream]]); } else { if (!streams.default) { streams.default = new window.MediaStream(); } addTrackToStreamAndFireEvent(track, streams.default); receiverList.push([track, rtpReceiver, streams.default]); } } else { // FIXME: actually the receiver should be created later. delete transceiver.rtpReceiver; } } }); if (this._dtlsRole === undefined) { this._dtlsRole = description.type === 'offer' ? 'active' : 'passive'; } this.remoteDescription = { type: description.type, sdp: description.sdp }; switch (description.type) { case 'offer': this._updateSignalingState('have-remote-offer'); break; case 'answer': this._updateSignalingState('stable'); break; default: throw new TypeError('unsupported type "' + description.type + '"'); } Object.keys(streams).forEach(function(sid) { var stream = streams[sid]; if (stream.getTracks().length) { if (pc.remoteStreams.indexOf(stream) === -1) { pc.remoteStreams.push(stream); var event = new Event('addstream'); event.stream = stream; window.setTimeout(function() { pc._dispatchEvent('addstream', event); }); } receiverList.forEach(function(item) { var track = item[0]; var receiver = item[1]; if (stream.id !== item[2].id) { return; } fireAddTrack(pc, track, receiver, [stream]); }); } }); receiverList.forEach(function(item) { if (item[2]) { return; } fireAddTrack(pc, item[0], item[1], []); }); // check whether addIceCandidate({}) was called within four seconds after // setRemoteDescription. window.setTimeout(function() { if (!(pc && pc.transceivers)) { return; } pc.transceivers.forEach(function(transceiver) { if (transceiver.iceTransport && transceiver.iceTransport.state === 'new' && transceiver.iceTransport.getRemoteCandidates().length > 0) { console.warn('Timeout for addRemoteCandidate. Consider sending ' + 'an end-of-candidates notification'); transceiver.iceTransport.addRemoteCandidate({}); } }); }, 4000); return Promise.resolve(); }; RTCPeerConnection.prototype.close = function() { this.transceivers.forEach(function(transceiver) { /* not yet if (transceiver.iceGatherer) { transceiver.iceGatherer.close(); } */ if (transceiver.iceTransport) { transceiver.iceTransport.stop(); } if (transceiver.dtlsTransport) { transceiver.dtlsTransport.stop(); } if (transceiver.rtpSender) { transceiver.rtpSender.stop(); } if (transceiver.rtpReceiver) { transceiver.rtpReceiver.stop(); } }); // FIXME: clean up tracks, local streams, remote streams, etc this._isClosed = true; this._updateSignalingState('closed'); }; // Update the signaling state. RTCPeerConnection.prototype._updateSignalingState = function(newState) { this.signalingState = newState; var event = new Event('signalingstatechange'); this._dispatchEvent('signalingstatechange', event); }; // Determine whether to fire the negotiationneeded event. RTCPeerConnection.prototype._maybeFireNegotiationNeeded = function() { var pc = this; if (this.signalingState !== 'stable' || this.needNegotiation === true) { return; } this.needNegotiation = true; window.setTimeout(function() { if (pc.needNegotiation === false) { return; } pc.needNegotiation = false; var event = new Event('negotiationneeded'); pc._dispatchEvent('negotiationneeded', event); }, 0); }; // Update the connection state. RTCPeerConnection.prototype._updateConnectionState = function() { var newState; var states = { 'new': 0, closed: 0, connecting: 0, checking: 0, connected: 0, completed: 0, disconnected: 0, failed: 0 }; this.transceivers.forEach(function(transceiver) { states[transceiver.iceTransport.state]++; states[transceiver.dtlsTransport.state]++; }); // ICETransport.completed and connected are the same for this purpose. states.connected += states.completed; newState = 'new'; if (states.failed > 0) { newState = 'failed'; } else if (states.connecting > 0 || states.checking > 0) { newState = 'connecting'; } else if (states.disconnected > 0) { newState = 'disconnected'; } else if (states.new > 0) { newState = 'new'; } else if (states.connected > 0 || states.completed > 0) { newState = 'connected'; } if (newState !== this.iceConnectionState) { this.iceConnectionState = newState; var event = new Event('iceconnectionstatechange'); this._dispatchEvent('iceconnectionstatechange', event); } }; RTCPeerConnection.prototype.createOffer = function() { var pc = this; if (this._isClosed) { return Promise.reject(makeError('InvalidStateError', 'Can not call createOffer after close')); } var numAudioTracks = this.transceivers.filter(function(t) { return t.kind === 'audio'; }).length; var numVideoTracks = this.transceivers.filter(function(t) { return t.kind === 'video'; }).length; // Determine number of audio and video tracks we need to send/recv. var offerOptions = arguments[0]; if (offerOptions) { // Reject Chrome legacy constraints. if (offerOptions.mandatory || offerOptions.optional) { throw new TypeError( 'Legacy mandatory/optional constraints not supported.'); } if (offerOptions.offerToReceiveAudio !== undefined) { if (offerOptions.offerToReceiveAudio === true) { numAudioTracks = 1; } else if (offerOptions.offerToReceiveAudio === false) { numAudioTracks = 0; } else { numAudioTracks = offerOptions.offerToReceiveAudio; } } if (offerOptions.offerToReceiveVideo !== undefined) { if (offerOptions.offerToReceiveVideo === true) { numVideoTracks = 1; } else if (offerOptions.offerToReceiveVideo === false) { numVideoTracks = 0; } else { numVideoTracks = offerOptions.offerToReceiveVideo; } } } this.transceivers.forEach(function(transceiver) { if (transceiver.kind === 'audio') { numAudioTracks--; if (numAudioTracks < 0) { transceiver.wantReceive = false; } } else if (transceiver.kind === 'video') { numVideoTracks--; if (numVideoTracks < 0) { transceiver.wantReceive = false; } } }); // Create M-lines for recvonly streams. while (numAudioTracks > 0 || numVideoTracks > 0) { if (numAudioTracks > 0) { this._createTransceiver('audio'); numAudioTracks--; } if (numVideoTracks > 0) { this._createTransceiver('video'); numVideoTracks--; } } var sdp = SDPUtils.writeSessionBoilerplate(this._sdpSessionId, this._sdpSessionVersion++); this.transceivers.forEach(function(transceiver, sdpMLineIndex) { // For each track, create an ice gatherer, ice transport, // dtls transport, potentially rtpsender and rtpreceiver. var track = transceiver.track; var kind = transceiver.kind; var mid = SDPUtils.generateIdentifier(); transceiver.mid = mid; if (!transceiver.iceGatherer) { transceiver.iceGatherer = pc._createIceGatherer(sdpMLineIndex, pc.usingBundle); } var localCapabilities = window.RTCRtpSender.getCapabilities(kind); // filter RTX until additional stuff needed for RTX is implemented // in adapter.js if (edgeVersion < 15019) { localCapabilities.codecs = localCapabilities.codecs.filter( function(codec) { return codec.name !== 'rtx'; }); } localCapabilities.codecs.forEach(function(codec) { // work around https://bugs.chromium.org/p/webrtc/issues/detail?id=6552 // by adding level-asymmetry-allowed=1 if (codec.name === 'H264' && codec.parameters['level-asymmetry-allowed'] === undefined) { codec.parameters['level-asymmetry-allowed'] = '1'; } }); // generate an ssrc now, to be used later in rtpSender.send var sendEncodingParameters = transceiver.sendEncodingParameters || [{ ssrc: (2 * sdpMLineIndex + 1) * 1001 }]; if (track) { // add RTX if (edgeVersion >= 15019 && kind === 'video' && !sendEncodingParameters[0].rtx) { sendEncodingParameters[0].rtx = { ssrc: sendEncodingParameters[0].ssrc + 1 }; } } if (transceiver.wantReceive) { transceiver.rtpReceiver = new window.RTCRtpReceiver( transceiver.dtlsTransport, kind); } transceiver.localCapabilities = localCapabilities; transceiver.sendEncodingParameters = sendEncodingParameters; }); // always offer BUNDLE and dispose on return if not supported. if (this._config.bundlePolicy !== 'max-compat') { sdp += 'a=group:BUNDLE ' + this.transceivers.map(function(t) { return t.mid; }).join(' ') + '\r\n'; } sdp += 'a=ice-options:trickle\r\n'; this.transceivers.forEach(function(transceiver, sdpMLineIndex) { sdp += writeMediaSection(transceiver, transceiver.localCapabilities, 'offer', transceiver.stream, pc._dtlsRole); sdp += 'a=rtcp-rsize\r\n'; if (transceiver.iceGatherer && pc.iceGatheringState !== 'new' && (sdpMLineIndex === 0 || !pc.usingBundle)) { transceiver.iceGatherer.getLocalCandidates().forEach(function(cand) { cand.component = 1; sdp += 'a=' + SDPUtils.writeCandidate(cand) + '\r\n'; }); if (transceiver.iceGatherer.state === 'completed') { sdp += 'a=end-of-candidates\r\n'; } } }); var desc = new window.RTCSessionDescription({ type: 'offer', sdp: sdp }); return Promise.resolve(desc); }; RTCPeerConnection.prototype.createAnswer = function() { var pc = this; if (this._isClosed) { return Promise.reject(makeError('InvalidStateError', 'Can not call createAnswer after close')); } var sdp = SDPUtils.writeSessionBoilerplate(this._sdpSessionId, this._sdpSessionVersion++); if (this.usingBundle) { sdp += 'a=group:BUNDLE ' + this.transceivers.map(function(t) { return t.mid; }).join(' ') + '\r\n'; } var mediaSectionsInOffer = SDPUtils.splitSections( this.remoteDescription.sdp).length - 1; this.transceivers.forEach(function(transceiver, sdpMLineIndex) { if (sdpMLineIndex + 1 > mediaSectionsInOffer) { return; } if (transceiver.isDatachannel) { sdp += 'm=application 0 DTLS/SCTP 5000\r\n' + 'c=IN IP4 0.0.0.0\r\n' + 'a=mid:' + transceiver.mid + '\r\n'; return; } // FIXME: look at direction. if (transceiver.stream) { var localTrack; if (transceiver.kind === 'audio') { localTrack = transceiver.stream.getAudioTracks()[0]; } else if (transceiver.kind === 'video') { localTrack = transceiver.stream.getVideoTracks()[0]; } if (localTrack) { // add RTX if (edgeVersion >= 15019 && transceiver.kind === 'video' && !transceiver.sendEncodingParameters[0].rtx) { transceiver.sendEncodingParameters[0].rtx = { ssrc: transceiver.sendEncodingParameters[0].ssrc + 1 }; } } } // Calculate intersection of capabilities. var commonCapabilities = getCommonCapabilities( transceiver.localCapabilities, transceiver.remoteCapabilities); var hasRtx = commonCapabilities.codecs.filter(function(c) { return c.name.toLowerCase() === 'rtx'; }).length; if (!hasRtx && transceiver.sendEncodingParameters[0].rtx) { delete transceiver.sendEncodingParameters[0].rtx; } sdp += writeMediaSection(transceiver, commonCapabilities, 'answer', transceiver.stream, pc._dtlsRole); if (transceiver.rtcpParameters && transceiver.rtcpParameters.reducedSize) { sdp += 'a=rtcp-rsize\r\n'; } }); var desc = new window.RTCSessionDescription({ type: 'answer', sdp: sdp }); return Promise.resolve(desc); }; RTCPeerConnection.prototype.addIceCandidate = function(candidate) { var sections; if (!candidate || candidate.candidate === '') { for (var j = 0; j < this.transceivers.length; j++) { if (this.transceivers[j].isDatachannel) { continue; } this.transceivers[j].iceTransport.addRemoteCandidate({}); sections = SDPUtils.splitSections(this.remoteDescription.sdp); sections[j + 1] += 'a=end-of-candidates\r\n'; this.remoteDescription.sdp = sections.join(''); if (this.usingBundle) { break; } } } else if (!(candidate.sdpMLineIndex !== undefined || candidate.sdpMid)) { throw new TypeError('sdpMLineIndex or sdpMid required'); } else if (!this.remoteDescription) { return Promise.reject(makeError('InvalidStateError', 'Can not add ICE candidate without a remote description')); } else { var sdpMLineIndex = candidate.sdpMLineIndex; if (candidate.sdpMid) { for (var i = 0; i < this.transceivers.length; i++) { if (this.transceivers[i].mid === candidate.sdpMid) { sdpMLineIndex = i; break; } } } var transceiver = this.transceivers[sdpMLineIndex]; if (transceiver) { if (transceiver.isDatachannel) { return Promise.resolve(); } var cand = Object.keys(candidate.candidate).length > 0 ? SDPUtils.parseCandidate(candidate.candidate) : {}; // Ignore Chrome's invalid candidates since Edge does not like them. if (cand.protocol === 'tcp' && (cand.port === 0 || cand.port === 9)) { return Promise.resolve(); } // Ignore RTCP candidates, we assume RTCP-MUX. if (cand.component && cand.component !== 1) { return Promise.resolve(); } // when using bundle, avoid adding candidates to the wrong // ice transport. And avoid adding candidates added in the SDP. if (sdpMLineIndex === 0 || (sdpMLineIndex > 0 && transceiver.iceTransport !== this.transceivers[0].iceTransport)) { if (!maybeAddCandidate(transceiver.iceTransport, cand)) { return Promise.reject(makeError('OperationError', 'Can not add ICE candidate')); } } // update the remoteDescription. var candidateString = candidate.candidate.trim(); if (candidateString.indexOf('a=') === 0) { candidateString = candidateString.substr(2); } sections = SDPUtils.splitSections(this.remoteDescription.sdp); sections[sdpMLineIndex + 1] += 'a=' + (cand.type ? candidateString : 'end-of-candidates') + '\r\n'; this.remoteDescription.sdp = sections.join(''); } else { return Promise.reject(makeError('OperationError', 'Can not add ICE candidate')); } } return Promise.resolve(); }; RTCPeerConnection.prototype.getStats = function() { var promises = []; this.transceivers.forEach(function(transceiver) { ['rtpSender', 'rtpReceiver', 'iceGatherer', 'iceTransport', 'dtlsTransport'].forEach(function(method) { if (transceiver[method]) { promises.push(transceiver[method].getStats()); } }); }); var fixStatsType = function(stat) { return { inboundrtp: 'inbound-rtp', outboundrtp: 'outbound-rtp', candidatepair: 'candidate-pair', localcandidate: 'local-candidate', remotecandidate: 'remote-candidate' }[stat.type] || stat.type; }; return new Promise(function(resolve) { // shim getStats with maplike support var results = new Map(); Promise.all(promises).then(function(res) { res.forEach(function(result) { Object.keys(result).forEach(function(id) { result[id].type = fixStatsType(result[id]); results.set(id, result[id]); }); }); resolve(results); }); }); }; // legacy callback shims. Should be moved to adapter.js some days. var methods = ['createOffer', 'createAnswer']; methods.forEach(function(method) { var nativeMethod = RTCPeerConnection.prototype[method]; RTCPeerConnection.prototype[method] = function() { var args = arguments; if (typeof args[0] === 'function' || typeof args[1] === 'function') { // legacy return nativeMethod.apply(this, [arguments[2]]) .then(function(description) { if (typeof args[0] === 'function') { args[0].apply(null, [description]); } }, function(error) { if (typeof args[1] === 'function') { args[1].apply(null, [error]); } }); } return nativeMethod.apply(this, arguments); }; }); methods = ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']; methods.forEach(function(method) { var nativeMethod = RTCPeerConnection.prototype[method]; RTCPeerConnection.prototype[method] = function() { var args = arguments; if (typeof args[1] === 'function' || typeof args[2] === 'function') { // legacy return nativeMethod.apply(this, arguments) .then(function() { if (typeof args[1] === 'function') { args[1].apply(null); } }, function(error) { if (typeof args[2] === 'function') { args[2].apply(null, [error]); } }); } return nativeMethod.apply(this, arguments); }; }); // getStats is special. It doesn't have a spec legacy method yet we support // getStats(something, cb) without error callbacks. ['getStats'].forEach(function(method) { var nativeMethod = RTCPeerConnection.prototype[method]; RTCPeerConnection.prototype[method] = function() { var args = arguments; if (typeof args[1] === 'function') { return nativeMethod.apply(this, arguments) .then(function() { if (typeof args[1] === 'function') { args[1].apply(null); } }); } return nativeMethod.apply(this, arguments); }; }); return RTCPeerConnection; }; },{"sdp":2}],2:[function(require,module,exports){ /* eslint-env node */ 'use strict'; // SDP helpers. 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).substr(2, 10); }; // 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 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" SDPUtils.parseCandidate = function(line) { var parts; // Parse both variants. if (line.indexOf('a=candidate:') === 0) { parts = line.substring(12).split(' '); } else { parts = line.substring(10).split(' '); } console.log("SDP Candidate Parts", parts); if (parts.length < 8) return; var candidate = { foundation: parts[0], component: parseInt(parts[1], 10), protocol: parts[2].toLowerCase(), priority: parseInt(parts[3], 10), ip: parts[4], 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 compability. candidate.usernameFragment = parts[i + 1]; break; default: // extension handling, in particular ufrag candidate[parts[i]] = parts[i + 1]; break; } } return candidate; }; // Translates a candidate object into SDP candidate attribute. SDPUtils.writeCandidate = function(candidate) { var sdp = []; sdp.push(candidate.foundation); sdp.push(candidate.component); sdp.push(candidate.protocol.toUpperCase()); sdp.push(candidate.priority); sdp.push(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); // was: relAddr sdp.push('rport'); sdp.push(candidate.relatedPort); // was: relPort } if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') { sdp.push('tcptype'); sdp.push(candidate.tcpType); } if (candidate.ufrag) { sdp.push('ufrag'); sdp.push(candidate.ufrag); } return 'candidate:' + sdp.join(' '); }; // Parses an ice-options line, returns an array of option tags. // a=ice-options:foo bar SDPUtils.parseIceOptions = function(line) { return line.substr(14).split(' '); } // Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input: // a=rtpmap:111 opus/48000/2 SDPUtils.parseRtpMap = function(line) { var parts = line.substr(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 // was: channels parsed.numChannels = parts.length === 3 ? parseInt(parts[2], 10) : 1; return parsed; }; // Generate an a=rtpmap line from RTCRtpCodecCapability or // RTCRtpCodecParameters. SDPUtils.writeRtpMap = function(codec) { var pt = codec.payloadType; if (codec.preferredPayloadType !== undefined) { pt = codec.preferredPayloadType; } return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate + (codec.numChannels !== 1 ? '/' + codec.numChannels : '') + '\r\n'; }; // Parses an 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.substr(9).split(' '); return { id: parseInt(parts[0], 10), direction: parts[0].indexOf('/') > 0 ? parts[0].split('/')[1] : 'sendrecv', uri: parts[1] }; }; // Generates a=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 + '\r\n'; }; // Parses an ftmp 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; var parts = line.substr(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 an a=ftmp 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) { params.push(param + '=' + codec.parameters[param]); }); line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n'; } return line; }; // Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input: // a=rtcp-fb:98 nack rpsi SDPUtils.parseRtcpFb = function(line) { var parts = line.substr(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 an 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.substr(7, sp - 7), 10) }; var colon = line.indexOf(':', sp); if (colon > -1) { parts.attribute = line.substr(sp + 1, colon - sp - 1); parts.value = line.substr(colon + 1); } else { parts.attribute = line.substr(sp + 1); } return parts; }; // 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.substr(6); } } SDPUtils.parseFingerprint = function(line) { var parts = line.substr(14).split(' '); return { algorithm: parts[0].toLowerCase(), // algorithm is case-sensitive in Edge. value: parts[1] }; }; // 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. // Note2: 'algorithm' is not case sensitive except 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 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 lines = SDPUtils.splitLines(mediaSection); // Search in session part, too. lines = lines.concat(SDPUtils.splitLines(sessionpart)); var iceParameters = { usernameFragment: lines.filter(function(line) { return line.indexOf('a=ice-ufrag:') === 0; })[0].substr(12), password: lines.filter(function(line) { return line.indexOf('a=ice-pwd:') === 0; })[0].substr(10) }; return iceParameters; }; // Serializes ICE parameters to SDP. SDPUtils.writeIceParameters = function(params) { return 'a=ice-ufrag:' + params.usernameFragment + '\r\n' + 'a=ice-pwd:' + params.password + '\r\n'; }; // 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(' '); 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)); }); // 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 += ' 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'; } sdp += 'a=rtcp-mux\r\n'; 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; var flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID') .map(function(line) { var parts = line.split(' '); parts.shift(); 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), rtx: { ssrc: secondarySsrc } }; encodingParameters.push(encParam); if (hasRed) { encParam = JSON.parse(JSON.stringify(encParam)); encParam.fec = { ssrc: secondarySsrc, 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].substr(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].substr(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 = {}; var cname; // 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; }; // parses either a=msid: or a=ssrc:... msid lines and returns // the id of the MediaStream and MediaStreamTrack. SDPUtils.parseMsid = function(mediaSection) { var parts; var spec = SDPUtils.matchPrefix(mediaSection, 'a=msid:'); if (spec.length === 1) { parts = spec[0].substr(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(parts) { return parts.attribute === 'msid'; }); if (planB.length > 0) { parts = planB[0].value.split(' '); return {stream: parts[0], track: parts[1]}; } }; // 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, 21); }; // Write boilder plate for start of SDP // sessId argument is optional - if not supplied it will // be generated randomly // sessVersion is optional and defaults to 2 SDPUtils.writeSessionBoilerplate = function(sessId, sessVer) { var sessionId; var version = sessVer !== undefined ? sessVer : 2; if (sessId) { sessionId = sessId; } else { sessionId = SDPUtils.generateSessionId(); } // FIXME: sess-id should be an NTP timestamp. return 'v=0\r\n' + 'o=thisisadapterortc ' + sessionId + ' ' + version + ' IN IP4 127.0.0.1\r\n' + 's=-\r\n' + 't=0 0\r\n'; }; SDPUtils.writeMediaSection = function(transceiver, caps, type, stream) { var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps); // Map ICE parameters (ufrag, pwd) to SDP. sdp += SDPUtils.writeIceParameters( transceiver.iceGatherer.getLocalParameters()); // Map DTLS parameters to SDP. sdp += SDPUtils.writeDtlsParameters( transceiver.dtlsTransport.getLocalParameters(), type === 'offer' ? 'actpass' : 'active'); sdp += 'a=mid:' + transceiver.mid + '\r\n'; if (transceiver.direction) { sdp += 'a=' + transceiver.direction + '\r\n'; } else if (transceiver.rtpSender && transceiver.rtpReceiver) { sdp += 'a=sendrecv\r\n'; } else if (transceiver.rtpSender) { sdp += 'a=sendonly\r\n'; } else if (transceiver.rtpReceiver) { sdp += 'a=recvonly\r\n'; } else { sdp += 'a=inactive\r\n'; } if (transceiver.rtpSender) { // spec. var msid = 'msid:' + stream.id + ' ' + transceiver.rtpSender.track.id + '\r\n'; sdp += 'a=' + msid; // for Chrome. sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc + ' ' + msid; if (transceiver.sendEncodingParameters[0].rtx) { sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc + ' ' + msid; sdp += 'a=ssrc-group:FID ' + transceiver.sendEncodingParameters[0].ssrc + ' ' + transceiver.sendEncodingParameters[0].rtx.ssrc + '\r\n'; } } // FIXME: this should be written by writeRtpDescription. sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc + ' cname:' + SDPUtils.localCName + '\r\n'; if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) { sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc + ' cname:' + SDPUtils.localCName + '\r\n'; } return sdp; }; // 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].substr(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].substr(2); }; SDPUtils.isRejected = function(mediaSection) { return mediaSection.split(' ', 2)[1] === '0'; }; SDPUtils.parseMLine = function(mediaSection) { var lines = SDPUtils.splitLines(mediaSection); var mline = lines[0].split(' '); return { kind: mline[0].substr(2), port: parseInt(mline[1], 10), protocol: mline[2], fmt: mline.slice(3).join(' ') }; }; // Expose public methods. if (typeof module === 'object') { module.exports = SDPUtils; } },{}],3:[function(require,module,exports){ (function (global){ /* * 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'; var adapterFactory = require('./adapter_factory.js'); module.exports = adapterFactory({window: global.window}); }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{"./adapter_factory.js":4}],4:[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'; var utils = require('./utils'); // Shimming starts here. module.exports = function(dependencies, opts) { var window = dependencies && dependencies.window; var options = { shimChrome: true, shimFirefox: true, shimEdge: true, shimSafari: true, }; for (var key in opts) { if (hasOwnProperty.call(opts, key)) { options[key] = opts[key]; } } // Utils. var logging = utils.log; var browserDetails = utils.detectBrowser(window); // Uncomment the line below if you want logging to occur, including logging // for the switch statement below. Can also be turned on in the browser via // adapter.disableLog(false), but then logging from the switch statement below // will not appear. // require('./utils').disableLog(false); // Browser shims. var chromeShim = require('./chrome/chrome_shim') || null; var edgeShim = require('./edge/edge_shim') || null; var firefoxShim = require('./firefox/firefox_shim') || null; var safariShim = require('./safari/safari_shim') || null; var commonShim = require('./common_shim') || null; // Export to the adapter global object visible in the browser. var adapter = { browserDetails: browserDetails, commonShim: commonShim, extractVersion: utils.extractVersion, disableLog: utils.disableLog, disableWarnings: utils.disableWarnings }; // 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; } logging('adapter.js shimming chrome.'); // Export to the adapter global object visible in the browser. adapter.browserShim = chromeShim; commonShim.shimCreateObjectURL(window); chromeShim.shimGetUserMedia(window); chromeShim.shimMediaStream(window); chromeShim.shimSourceObject(window); chromeShim.shimPeerConnection(window); chromeShim.shimOnTrack(window); chromeShim.shimAddTrackRemoveTrack(window); chromeShim.shimGetSendersWithDtmf(window); commonShim.shimRTCIceCandidate(window); commonShim.shimMaxMessageSize(window); commonShim.shimSendThrowTypeError(window); 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; commonShim.shimCreateObjectURL(window); firefoxShim.shimGetUserMedia(window); firefoxShim.shimSourceObject(window); firefoxShim.shimPeerConnection(window); firefoxShim.shimOnTrack(window); firefoxShim.shimRemoveStream(window); commonShim.shimRTCIceCandidate(window); commonShim.shimMaxMessageSize(window); commonShim.shimSendThrowTypeError(window); break; case 'edge': if (!edgeShim || !edgeShim.shimPeerConnection || !options.shimEdge) { logging('MS edge shim is not included in this adapter release.'); return adapter; } logging('adapter.js shimming edge.'); // Export to the adapter global object visible in the browser. adapter.browserShim = edgeShim; commonShim.shimCreateObjectURL(window); edgeShim.shimGetUserMedia(window); edgeShim.shimPeerConnection(window); edgeShim.shimReplaceTrack(window); // the edge shim implements the full RTCIceCandidate object. commonShim.shimMaxMessageSize(window); commonShim.shimSendThrowTypeError(window); 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; commonShim.shimCreateObjectURL(window); safariShim.shimRTCIceServerUrls(window); safariShim.shimCallbacksAPI(window); safariShim.shimLocalStreamsAPI(window); safariShim.shimRemoteStreamsAPI(window); safariShim.shimTrackEventTransceiver(window); safariShim.shimGetUserMedia(window); safariShim.shimCreateOfferLegacy(window); commonShim.shimRTCIceCandidate(window); commonShim.shimMaxMessageSize(window); commonShim.shimSendThrowTypeError(window); break; default: logging('Unsupported browser!'); break; } return adapter; }; },{"./chrome/chrome_shim":5,"./common_shim":7,"./edge/edge_shim":8,"./firefox/firefox_shim":10,"./safari/safari_shim":12,"./utils":13}],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'; var utils = require('../utils.js'); var logging = utils.log; module.exports = { shimGetUserMedia: require('./getusermedia'), shimMediaStream: function(window) { window.MediaStream = window.MediaStream || window.webkitMediaStream; }, shimOnTrack: function(window) { if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in window.RTCPeerConnection.prototype)) { Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', { get: function() { return this._ontrack; }, set: function(f) { if (this._ontrack) { this.removeEventListener('track', this._ontrack); } this.addEventListener('track', this._ontrack = f); } }); var origSetRemoteDescription = window.RTCPeerConnection.prototype.setRemoteDescription; window.RTCPeerConnection.prototype.setRemoteDescription = function() { var pc = this; if (!pc._ontrackpoly) { pc._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 = pc.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]; pc.dispatchEvent(event); }); e.stream.getTracks().forEach(function(track) { var receiver; if (window.RTCPeerConnection.prototype.getReceivers) { receiver = pc.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]; pc.dispatchEvent(event); }); }; pc.addEventListener('addstream', pc._ontrackpoly); } return origSetRemoteDescription.apply(pc, arguments); }; } else if (!('RTCRtpTransceiver' in window)) { utils.wrapPeerConnectionEvent(window, 'track', function(e) { if (!e.transceiver) { e.transceiver = {receiver: e.receiver}; } return e; }); } }, shimGetSendersWithDtmf: function(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(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() { 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(track, stream) { var pc = this; var sender = origAddTrack.apply(pc, arguments); if (!sender) { sender = shimSenderWithDtmf(pc, track); pc._senders.push(sender); } return sender; }; var origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack; window.RTCPeerConnection.prototype.removeTrack = function(sender) { var pc = this; origRemoveTrack.apply(pc, arguments); var idx = pc._senders.indexOf(sender); if (idx !== -1) { pc._senders.splice(idx, 1); } }; } var origAddStream = window.RTCPeerConnection.prototype.addStream; window.RTCPeerConnection.prototype.addStream = function(stream) { var pc = this; pc._senders = pc._senders || []; origAddStream.apply(pc, [stream]); stream.getTracks().forEach(function(track) { pc._senders.push(shimSenderWithDtmf(pc, track)); }); }; var origRemoveStream = window.RTCPeerConnection.prototype.removeStream; window.RTCPeerConnection.prototype.removeStream = function(stream) { var pc = this; pc._senders = pc._senders || []; origRemoveStream.apply(pc, [stream]); stream.getTracks().forEach(function(track) { var sender = pc._senders.find(function(s) { return s.track === track; }); if (sender) { pc._senders.splice(pc._senders.indexOf(sender), 1); // remove sender } }); }; } 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() { var pc = this; var senders = origGetSenders.apply(pc, []); senders.forEach(function(sender) { sender._pc = pc; }); return senders; }; Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', { get: function() { if (this._dtmf === undefined) { if (this.track.kind === 'audio') { this._dtmf = this._pc.createDTMFSender(this.track); } else { this._dtmf = null; } } return this._dtmf; } }); } }, shimSourceObject: function(window) { var URL = window && window.URL; if (typeof window === 'object') { if (window.HTMLMediaElement && !('srcObject' in window.HTMLMediaElement.prototype)) { // Shim the srcObject property, once, when HTMLMediaElement is found. Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', { get: function() { return this._srcObject; }, set: function(stream) { var self = this; // Use _srcObject as a private property for this shim this._srcObject = stream; if (this.src) { URL.revokeObjectURL(this.src); } if (!stream) { this.src = ''; return undefined; } this.src = URL.createObjectURL(stream); // We need to recreate the blob url when a track is added or // removed. Doing it manually since we want to avoid a recursion. stream.addEventListener('addtrack', function() { if (self.src) { URL.revokeObjectURL(self.src); } self.src = URL.createObjectURL(stream); }); stream.addEventListener('removetrack', function() { if (self.src) { URL.revokeObjectURL(self.src); } self.src = URL.createObjectURL(stream); }); } }); } } }, shimAddTrackRemoveTrackWithNative: function(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() { var pc = this; this._shimmedLocalStreams = this._shimmedLocalStreams || {}; return Object.keys(this._shimmedLocalStreams).map(function(streamId) { return pc._shimmedLocalStreams[streamId][0]; }); }; var origAddTrack = window.RTCPeerConnection.prototype.addTrack; window.RTCPeerConnection.prototype.addTrack = function(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(stream) { var pc = this; this._shimmedLocalStreams = this._shimmedLocalStreams || {}; stream.getTracks().forEach(function(track) { var alreadyExists = pc.getSenders().find(function(s) { return s.track === track; }); if (alreadyExists) { throw new DOMException('Track already exists.', 'InvalidAccessError'); } }); var existingSenders = pc.getSenders(); origAddStream.apply(this, arguments); var newSenders = pc.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(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(sender) { var pc = this; this._shimmedLocalStreams = this._shimmedLocalStreams || {}; if (sender) { Object.keys(this._shimmedLocalStreams).forEach(function(streamId) { var idx = pc._shimmedLocalStreams[streamId].indexOf(sender); if (idx !== -1) { pc._shimmedLocalStreams[streamId].splice(idx, 1); } if (pc._shimmedLocalStreams[streamId].length === 1) { delete pc._shimmedLocalStreams[streamId]; } }); } return origRemoveTrack.apply(this, arguments); }; }, shimAddTrackRemoveTrack: function(window) { var browserDetails = utils.detectBrowser(window); // shim addTrack and removeTrack. if (window.RTCPeerConnection.prototype.addTrack && browserDetails.version >= 65) { return this.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() { var pc = this; var nativeStreams = origGetLocalStreams.apply(this); pc._reverseStreams = pc._reverseStreams || {}; return nativeStreams.map(function(stream) { return pc._reverseStreams[stream.id]; }); }; var origAddStream = window.RTCPeerConnection.prototype.addStream; window.RTCPeerConnection.prototype.addStream = function(stream) { var pc = this; pc._streams = pc._streams || {}; pc._reverseStreams = pc._reverseStreams || {}; stream.getTracks().forEach(function(track) { var alreadyExists = pc.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 (!pc._reverseStreams[stream.id]) { var newStream = new window.MediaStream(stream.getTracks()); pc._streams[stream.id] = newStream; pc._reverseStreams[newStream.id] = stream; stream = newStream; } origAddStream.apply(pc, [stream]); }; var origRemoveStream = window.RTCPeerConnection.prototype.removeStream; window.RTCPeerConnection.prototype.removeStream = function(stream) { var pc = this; pc._streams = pc._streams || {}; pc._reverseStreams = pc._reverseStreams || {}; origRemoveStream.apply(pc, [(pc._streams[stream.id] || stream)]); delete pc._reverseStreams[(pc._streams[stream.id] ? pc._streams[stream.id].id : stream.id)]; delete pc._streams[stream.id]; }; window.RTCPeerConnection.prototype.addTrack = function(track, stream) { var pc = this; if (pc.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 = pc.getSenders().find(function(s) { return s.track === track; }); if (alreadyExists) { throw new DOMException('Track already exists.', 'InvalidAccessError'); } pc._streams = pc._streams || {}; pc._reverseStreams = pc._reverseStreams || {}; var oldStream = pc._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() { pc.dispatchEvent(new Event('negotiationneeded')); }); } else { var newStream = new window.MediaStream([track]); pc._streams[stream.id] = newStream; pc._reverseStreams[newStream.id] = stream; pc.addStream(newStream); } return pc.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]; window.RTCPeerConnection.prototype[method] = function() { var pc = this; var args = arguments; var isLegacyCall = arguments.length && typeof arguments[0] === 'function'; if (isLegacyCall) { return nativeMethod.apply(pc, [ function(description) { var desc = replaceInternalStreamId(pc, description); args[0].apply(null, [desc]); }, function(err) { if (args[1]) { args[1].apply(null, err); } }, arguments[2] ]); } return nativeMethod.apply(pc, arguments) .then(function(description) { return replaceInternalStreamId(pc, description); }); }; }); var origSetLocalDescription = window.RTCPeerConnection.prototype.setLocalDescription; window.RTCPeerConnection.prototype.setLocalDescription = function() { var pc = this; if (!arguments.length || !arguments[0].type) { return origSetLocalDescription.apply(pc, arguments); } arguments[0] = replaceExternalStreamId(pc, arguments[0]); return origSetLocalDescription.apply(pc, 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() { var pc = this; var description = origLocalDescription.get.apply(this); if (description.type === '') { return description; } return replaceInternalStreamId(pc, description); } }); window.RTCPeerConnection.prototype.removeTrack = function(sender) { var pc = this; if (pc.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 === pc; if (!isLocal) { throw new DOMException('Sender was not created by this connection.', 'InvalidAccessError'); } // Search for the native stream the senders track belongs to. pc._streams = pc._streams || {}; var stream; Object.keys(pc._streams).forEach(function(streamid) { var hasTrack = pc._streams[streamid].getTracks().find(function(track) { return sender.track === track; }); if (hasTrack) { stream = pc._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. pc.removeStream(pc._reverseStreams[stream.id]); } else { // relying on the same odd chrome behaviour as above. stream.removeTrack(sender.track); } pc.dispatchEvent(new Event('negotiationneeded')); } }; }, shimPeerConnection: function(window) { var browserDetails = utils.detectBrowser(window); // The RTCPeerConnection object. if (!window.RTCPeerConnection && window.webkitRTCPeerConnection) { window.RTCPeerConnection = function(pcConfig, pcConstraints) { // Translate iceTransportPolicy to iceTransports, // see https://code.google.com/p/webrtc/issues/detail?id=4869 // this was fixed in M56 along with unprefixing RTCPeerConnection. logging('PeerConnection'); if (pcConfig && pcConfig.iceTransportPolicy) { pcConfig.iceTransports = pcConfig.iceTransportPolicy; } return new window.webkitRTCPeerConnection(pcConfig, pcConstraints); }; window.RTCPeerConnection.prototype = window.webkitRTCPeerConnection.prototype; // wrap static methods. Currently just generateCertificate. if (window.webkitRTCPeerConnection.generateCertificate) { Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { get: function() { return window.webkitRTCPeerConnection.generateCertificate; } }); } } else { // migrate from non-spec RTCIceServer.url to RTCIceServer.urls var OrigPeerConnection = window.RTCPeerConnection; window.RTCPeerConnection = function(pcConfig, pcConstraints) { if (pcConfig && pcConfig.iceServers) { var newIceServers = []; for (var i = 0; i < pcConfig.iceServers.length; i++) { var server = pcConfig.iceServers[i]; if (!server.hasOwnProperty('urls') && server.hasOwnProperty('url')) { utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls'); server = JSON.parse(JSON.stringify(server)); server.urls = 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. Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { get: function() { return OrigPeerConnection.generateCertificate; } }); } var origGetStats = window.RTCPeerConnection.prototype.getStats; window.RTCPeerConnection.prototype.getStats = function(selector, successCallback, errorCallback) { var pc = this; var args = arguments; // 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 arguments[0] !== 'function')) { return origGetStats.apply(this, []); } var fixChromeStats_ = function(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(stats) { return new Map(Object.keys(stats).map(function(key) { return [key, stats[key]]; })); }; if (arguments.length >= 2) { var successCallbackWrapper_ = function(response) { args[1](makeMapStats(fixChromeStats_(response))); }; return origGetStats.apply(this, [successCallbackWrapper_, arguments[0]]); } // promise-support return new Promise(function(resolve, reject) { origGetStats.apply(pc, [ function(response) { resolve(makeMapStats(fixChromeStats_(response))); }, reject]); }).then(successCallback, errorCallback); }; // add promise support -- natively available in Chrome 51 if (browserDetails.version < 51) { ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'] .forEach(function(method) { var nativeMethod = window.RTCPeerConnection.prototype[method]; window.RTCPeerConnection.prototype[method] = function() { var args = arguments; var pc = this; var promise = new Promise(function(resolve, reject) { nativeMethod.apply(pc, [args[0], resolve, reject]); }); if (args.length < 2) { return promise; } return promise.then(function() { args[1].apply(null, []); }, function(err) { if (args.length >= 3) { args[2].apply(null, [err]); } }); }; }); } // promise support for createOffer and createAnswer. Available (without // bugs) since M52: crbug/619289 if (browserDetails.version < 52) { ['createOffer', 'createAnswer'].forEach(function(method) { var nativeMethod = window.RTCPeerConnection.prototype[method]; window.RTCPeerConnection.prototype[method] = function() { var pc = this; if (arguments.length < 1 || (arguments.length === 1 && typeof arguments[0] === 'object')) { var opts = arguments.length === 1 ? arguments[0] : undefined; return new Promise(function(resolve, reject) { nativeMethod.apply(pc, [resolve, reject, opts]); }); } return nativeMethod.apply(this, arguments); }; }); } // shim implicit creation of RTCSessionDescription/RTCIceCandidate ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'] .forEach(function(method) { var nativeMethod = window.RTCPeerConnection.prototype[method]; window.RTCPeerConnection.prototype[method] = function() { arguments[0] = new ((method === 'addIceCandidate') ? window.RTCIceCandidate : window.RTCSessionDescription)(arguments[0]); return nativeMethod.apply(this, arguments); }; }); // support for addIceCandidate(null or undefined) var nativeAddIceCandidate = window.RTCPeerConnection.prototype.addIceCandidate; window.RTCPeerConnection.prototype.addIceCandidate = function() { if (!arguments[0]) { if (arguments[1]) { arguments[1].apply(null); } return Promise.resolve(); } return nativeAddIceCandidate.apply(this, arguments); }; } }; },{"../utils.js":13,"./getusermedia":6}],6:[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'; var utils = require('../utils.js'); var logging = utils.log; // Expose public methods. module.exports = function(window) { var browserDetails = utils.detectBrowser(window); var navigator = window && window.navigator; var constraintsToChrome_ = function(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(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(constraints, func) { if (browserDetails.version >= 61) { return func(constraints); } constraints = JSON.parse(JSON.stringify(constraints)); if (constraints && typeof constraints.audio === 'object') { var remap = function(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().indexOf(match) !== -1; }); }); if (!dev && devices.length && matches.indexOf('back') !== -1) { 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(e) { return { name: { PermissionDeniedError: 'NotAllowedError', InvalidStateError: 'NotReadableError', DevicesNotFoundError: 'NotFoundError', ConstraintNotSatisfiedError: 'OverconstrainedError', TrackStartError: 'NotReadableError', MediaDeviceFailedDueToShutdown: 'NotReadableError', MediaDeviceKillSwitchOn: 'NotReadableError' }[e.name] || e.name, message: e.message, constraint: e.constraintName, toString: function() { return this.name + (this.message && ': ') + this.message; } }; }; var getUserMedia_ = function(constraints, onSuccess, onError) { shimConstraints_(constraints, function(c) { navigator.webkitGetUserMedia(c, onSuccess, function(e) { if (onError) { onError(shimError_(e)); } }); }); }; navigator.getUserMedia = getUserMedia_; // Returns the result of getUserMedia as a Promise. var getUserMediaPromise_ = function(constraints) { return new Promise(function(resolve, reject) { navigator.getUserMedia(constraints, resolve, reject); }); }; if (!navigator.mediaDevices) { navigator.mediaDevices = { getUserMedia: getUserMediaPromise_, enumerateDevices: function() { return new Promise(function(resolve) { var kinds = {audio: 'audioinput', video: 'videoinput'}; return window.MediaStreamTrack.getSources(function(devices) { resolve(devices.map(function(device) { return {label: device.label, kind: kinds[device.kind], deviceId: device.id, groupId: ''}; })); }); }); }, getSupportedConstraints: function() { return { deviceId: true, echoCancellation: true, facingMode: true, frameRate: true, height: true, width: true }; } }; } // A shim for getUserMedia method on the mediaDevices object. // TODO(KaptenJansson) remove once implemented in Chrome stable. if (!navigator.mediaDevices.getUserMedia) { navigator.mediaDevices.getUserMedia = function(constraints) { return getUserMediaPromise_(constraints); }; } else { // Even though Chrome 45 has navigator.mediaDevices and a getUserMedia // function which returns a Promise, it does not accept spec-style // constraints. 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)); }); }); }; } // Dummy devicechange event methods. // TODO(KaptenJansson) remove once implemented in Chrome stable. if (typeof navigator.mediaDevices.addEventListener === 'undefined') { navigator.mediaDevices.addEventListener = function() { logging('Dummy mediaDevices.addEventListener called.'); }; } if (typeof navigator.mediaDevices.removeEventListener === 'undefined') { navigator.mediaDevices.removeEventListener = function() { logging('Dummy mediaDevices.removeEventListener called.'); }; } }; },{"../utils.js":13}],7:[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'; var SDPUtils = require('sdp'); var utils = require('./utils'); module.exports = { shimRTCIceCandidate: function(window) { // foundation is arbitrarily chosen as an indicator for full support for // https://w3c.github.io/webrtc-pc/#rtcicecandidate-interface if (window.RTCIceCandidate && 'foundation' in window.RTCIceCandidate.prototype) { return; } var NativeRTCIceCandidate = window.RTCIceCandidate; window.RTCIceCandidate = function(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.substr(2); } // Augment the native candidate with the parsed fields. var nativeCandidate = new NativeRTCIceCandidate(args); var parsedCandidate = SDPUtils.parseCandidate(args.candidate); var augmentedCandidate = Object.assign(nativeCandidate, parsedCandidate); // Add a serializer that does not serialize the extra attributes. augmentedCandidate.toJSON = function() { return { candidate: augmentedCandidate.candidate, sdpMid: augmentedCandidate.sdpMid, sdpMLineIndex: augmentedCandidate.sdpMLineIndex, usernameFragment: augmentedCandidate.usernameFragment, }; }; return augmentedCandidate; }; // 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; }); }, // shimCreateObjectURL must be called before shimSourceObject to avoid loop. shimCreateObjectURL: function(window) { var URL = window && window.URL; if (!(typeof window === 'object' && window.HTMLMediaElement && 'srcObject' in window.HTMLMediaElement.prototype && URL.createObjectURL && URL.revokeObjectURL)) { // Only shim CreateObjectURL using srcObject if srcObject exists. return undefined; } var nativeCreateObjectURL = URL.createObjectURL.bind(URL); var nativeRevokeObjectURL = URL.revokeObjectURL.bind(URL); var streams = new Map(), newId = 0; URL.createObjectURL = function(stream) { if ('getTracks' in stream) { var url = 'polyblob:' + (++newId); streams.set(url, stream); utils.deprecated('URL.createObjectURL(stream)', 'elem.srcObject = stream'); return url; } return nativeCreateObjectURL(stream); }; URL.revokeObjectURL = function(url) { nativeRevokeObjectURL(url); streams.delete(url); }; var dsc = Object.getOwnPropertyDescriptor(window.HTMLMediaElement.prototype, 'src'); Object.defineProperty(window.HTMLMediaElement.prototype, 'src', { get: function() { return dsc.get.apply(this); }, set: function(url) { this.srcObject = streams.get(url) || null; return dsc.set.apply(this, [url]); } }); var nativeSetAttribute = window.HTMLMediaElement.prototype.setAttribute; window.HTMLMediaElement.prototype.setAttribute = function() { if (arguments.length === 2 && ('' + arguments[0]).toLowerCase() === 'src') { this.srcObject = streams.get(arguments[1]) || null; } return nativeSetAttribute.apply(this, arguments); }; }, shimMaxMessageSize: function(window) { if (window.RTCSctpTransport || !window.RTCPeerConnection) { return; } var browserDetails = utils.detectBrowser(window); if (!('sctp' in window.RTCPeerConnection.prototype)) { Object.defineProperty(window.RTCPeerConnection.prototype, 'sctp', { get: function() { return typeof this._sctp === 'undefined' ? null : this._sctp; } }); } var sctpInDescription = function(description) { var sections = SDPUtils.splitSections(description.sdp); sections.shift(); return sections.some(function(mediaSection) { var mLine = SDPUtils.parseMLine(mediaSection); return mLine && mLine.kind === 'application' && mLine.protocol.indexOf('SCTP') !== -1; }); }; var getRemoteFirefoxVersion = function(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(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 { // 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; } } return canSendMaxMessageSize; }; var getMaxMessageSize = function(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 = SDPUtils.matchPrefix(description.sdp, 'a=max-message-size:'); if (match.length > 0) { maxMessageSize = parseInt(match[0].substr(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() { var pc = this; pc._sctp = null; 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() { return maxMessageSize; } }); pc._sctp = sctp; } return origSetRemoteDescription.apply(pc, arguments); }; }, shimSendThrowTypeError: function(window) { // 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 var origCreateDataChannel = window.RTCPeerConnection.prototype.createDataChannel; window.RTCPeerConnection.prototype.createDataChannel = function() { var pc = this; var dataChannel = origCreateDataChannel.apply(pc, arguments); var origDataChannelSend = dataChannel.send; // Patch 'send' method dataChannel.send = function() { var dc = this; var data = arguments[0]; var length = data.length || data.size || data.byteLength; if (length > pc.sctp.maxMessageSize) { throw new DOMException('Message too large (can send a maximum of ' + pc.sctp.maxMessageSize + ' bytes)', 'TypeError'); } return origDataChannelSend.apply(dc, arguments); }; return dataChannel; }; } }; },{"./utils":13,"sdp":2}],8:[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'; var utils = require('../utils'); var shimRTCPeerConnection = require('rtcpeerconnection-shim'); module.exports = { shimGetUserMedia: require('./getusermedia'), shimPeerConnection: function(window) { var browserDetails = utils.detectBrowser(window); if (window.RTCIceGatherer) { // ORTC defines an RTCIceCandidate object but no constructor. // Not implemented in Edge. if (!window.RTCIceCandidate) { window.RTCIceCandidate = function(args) { return args; }; } // ORTC does not have a session description object but // other browsers (i.e. Chrome) that will support both PC and ORTC // in the future might have this defined already. if (!window.RTCSessionDescription) { window.RTCSessionDescription = function(args) { return args; }; } // this adds an additional event listener to MediaStrackTrack that signals // when a tracks enabled property was changed. Workaround for a bug in // addStream, see below. No longer required in 15025+ if (browserDetails.version < 15025) { var origMSTEnabled = Object.getOwnPropertyDescriptor( window.MediaStreamTrack.prototype, 'enabled'); Object.defineProperty(window.MediaStreamTrack.prototype, 'enabled', { set: function(value) { origMSTEnabled.set.call(this, value); var ev = new Event('enabled'); ev.enabled = value; this.dispatchEvent(ev); } }); } } // ORTC defines the DTMF sender a bit different. // https://github.com/w3c/ortc/issues/714 if (window.RTCRtpSender && !('dtmf' in window.RTCRtpSender.prototype)) { Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', { get: function() { if (this._dtmf === undefined) { if (this.track.kind === 'audio') { this._dtmf = new window.RTCDtmfSender(this); } else if (this.track.kind === 'video') { this._dtmf = null; } } return this._dtmf; } }); } window.RTCPeerConnection = shimRTCPeerConnection(window, browserDetails.version); }, shimReplaceTrack: function(window) { // ORTC has replaceTrack -- https://github.com/w3c/ortc/issues/614 if (window.RTCRtpSender && !('replaceTrack' in window.RTCRtpSender.prototype)) { window.RTCRtpSender.prototype.replaceTrack = window.RTCRtpSender.prototype.setTrack; } } }; },{"../utils":13,"./getusermedia":9,"rtcpeerconnection-shim":1}],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'; // Expose public methods. module.exports = function(window) { var navigator = window && window.navigator; var shimError_ = function(e) { return { name: {PermissionDeniedError: 'NotAllowedError'}[e.name] || e.name, message: e.message, constraint: e.constraint, toString: function() { return this.name; } }; }; // getUserMedia error shim. var origGetUserMedia = navigator.mediaDevices.getUserMedia. bind(navigator.mediaDevices); navigator.mediaDevices.getUserMedia = function(c) { return origGetUserMedia(c).catch(function(e) { return Promise.reject(shimError_(e)); }); }; }; },{}],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. */ /* eslint-env node */ 'use strict'; var utils = require('../utils'); module.exports = { shimGetUserMedia: require('./getusermedia'), shimOnTrack: function(window) { if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in window.RTCPeerConnection.prototype)) { Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', { get: function() { return this._ontrack; }, set: function(f) { if (this._ontrack) { this.removeEventListener('track', this._ontrack); this.removeEventListener('addstream', this._ontrackpoly); } this.addEventListener('track', this._ontrack = f); this.addEventListener('addstream', this._ontrackpoly = function(e) { e.stream.getTracks().forEach(function(track) { var event = new Event('track'); event.track = track; event.receiver = {track: track}; event.transceiver = {receiver: event.receiver}; event.streams = [e.stream]; this.dispatchEvent(event); }.bind(this)); }.bind(this)); } }); } if (typeof window === 'object' && window.RTCTrackEvent && ('receiver' in window.RTCTrackEvent.prototype) && !('transceiver' in window.RTCTrackEvent.prototype)) { Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', { get: function() { return {receiver: this.receiver}; } }); } }, shimSourceObject: function(window) { // Firefox has supported mozSrcObject since FF22, unprefixed in 42. if (typeof window === 'object') { if (window.HTMLMediaElement && !('srcObject' in window.HTMLMediaElement.prototype)) { // Shim the srcObject property, once, when HTMLMediaElement is found. Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', { get: function() { return this.mozSrcObject; }, set: function(stream) { this.mozSrcObject = stream; } }); } } }, shimPeerConnection: function(window) { var browserDetails = utils.detectBrowser(window); if (typeof window !== 'object' || !(window.RTCPeerConnection || window.mozRTCPeerConnection)) { return; // probably media.peerconnection.enabled=false in about:config } // The RTCPeerConnection object. if (!window.RTCPeerConnection) { window.RTCPeerConnection = function(pcConfig, pcConstraints) { if (browserDetails.version < 38) { // .urls is not supported in FF < 38. // create RTCIceServers with a single url. if (pcConfig && pcConfig.iceServers) { var newIceServers = []; for (var i = 0; i < pcConfig.iceServers.length; i++) { var server = pcConfig.iceServers[i]; if (server.hasOwnProperty('urls')) { for (var j = 0; j < server.urls.length; j++) { var newServer = { url: server.urls[j] }; if (server.urls[j].indexOf('turn') === 0) { newServer.username = server.username; newServer.credential = server.credential; } newIceServers.push(newServer); } } else { newIceServers.push(pcConfig.iceServers[i]); } } pcConfig.iceServers = newIceServers; } } return new window.mozRTCPeerConnection(pcConfig, pcConstraints); }; window.RTCPeerConnection.prototype = window.mozRTCPeerConnection.prototype; // wrap static methods. Currently just generateCertificate. if (window.mozRTCPeerConnection.generateCertificate) { Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { get: function() { return window.mozRTCPeerConnection.generateCertificate; } }); } window.RTCSessionDescription = window.mozRTCSessionDescription; window.RTCIceCandidate = window.mozRTCIceCandidate; } // shim away need for obsolete RTCIceCandidate/RTCSessionDescription. ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'] .forEach(function(method) { var nativeMethod = window.RTCPeerConnection.prototype[method]; window.RTCPeerConnection.prototype[method] = function() { arguments[0] = new ((method === 'addIceCandidate') ? window.RTCIceCandidate : window.RTCSessionDescription)(arguments[0]); return nativeMethod.apply(this, arguments); }; }); // support for addIceCandidate(null or undefined) var nativeAddIceCandidate = window.RTCPeerConnection.prototype.addIceCandidate; window.RTCPeerConnection.prototype.addIceCandidate = function() { if (!arguments[0]) { if (arguments[1]) { arguments[1].apply(null); } return Promise.resolve(); } return nativeAddIceCandidate.apply(this, arguments); }; // shim getStats with maplike support var makeMapStats = function(stats) { var map = new Map(); Object.keys(stats).forEach(function(key) { map.set(key, stats[key]); map[key] = stats[key]; }); return map; }; 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( selector, onSucc, onErr ) { return nativeGetStats.apply(this, [selector || null]) .then(function(stats) { if (browserDetails.version < 48) { stats = makeMapStats(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); }; }, shimRemoveStream: function(window) { if (!window.RTCPeerConnection || 'removeStream' in window.RTCPeerConnection.prototype) { return; } window.RTCPeerConnection.prototype.removeStream = function(stream) { var pc = this; utils.deprecated('removeStream', 'removeTrack'); this.getSenders().forEach(function(sender) { if (sender.track && stream.getTracks().indexOf(sender.track) !== -1) { pc.removeTrack(sender); } }); }; } }; },{"../utils":13,"./getusermedia":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'; var utils = require('../utils'); var logging = utils.log; // Expose public methods. module.exports = function(window) { var browserDetails = utils.detectBrowser(window); var navigator = window && window.navigator; var MediaStreamTrack = window && window.MediaStreamTrack; var shimError_ = function(e) { return { name: { InternalError: 'NotReadableError', NotSupportedError: 'TypeError', PermissionDeniedError: 'NotAllowedError', SecurityError: 'NotAllowedError' }[e.name] || e.name, message: { 'The operation is insecure.': 'The request is not allowed by the ' + 'user agent or the platform in the current context.' }[e.message] || e.message, constraint: e.constraint, toString: function() { return this.name + (this.message && ': ') + this.message; } }; }; // getUserMedia constraints shim. var getUserMedia_ = function(constraints, onSuccess, onError) { var constraintsToFF37_ = function(c) { if (typeof c !== 'object' || c.require) { return c; } var require = []; Object.keys(c).forEach(function(key) { if (key === 'require' || key === 'advanced' || key === 'mediaSource') { return; } var r = c[key] = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]}; if (r.min !== undefined || r.max !== undefined || r.exact !== undefined) { require.push(key); } if (r.exact !== undefined) { if (typeof r.exact === 'number') { r. min = r.max = r.exact; } else { c[key] = r.exact; } delete r.exact; } if (r.ideal !== undefined) { c.advanced = c.advanced || []; var oc = {}; if (typeof r.ideal === 'number') { oc[key] = {min: r.ideal, max: r.ideal}; } else { oc[key] = r.ideal; } c.advanced.push(oc); delete r.ideal; if (!Object.keys(r).length) { delete c[key]; } } }); if (require.length) { c.require = require; } return c; }; constraints = JSON.parse(JSON.stringify(constraints)); if (browserDetails.version < 38) { logging('spec: ' + JSON.stringify(constraints)); if (constraints.audio) { constraints.audio = constraintsToFF37_(constraints.audio); } if (constraints.video) { constraints.video = constraintsToFF37_(constraints.video); } logging('ff37: ' + JSON.stringify(constraints)); } return navigator.mozGetUserMedia(constraints, onSuccess, function(e) { onError(shimError_(e)); }); }; // Returns the result of getUserMedia as a Promise. var getUserMediaPromise_ = function(constraints) { return new Promise(function(resolve, reject) { getUserMedia_(constraints, resolve, reject); }); }; // Shim for mediaDevices on older versions. if (!navigator.mediaDevices) { navigator.mediaDevices = {getUserMedia: getUserMediaPromise_, addEventListener: function() { }, removeEventListener: function() { } }; } navigator.mediaDevices.enumerateDevices = navigator.mediaDevices.enumerateDevices || function() { return new Promise(function(resolve) { var infos = [ {kind: 'audioinput', deviceId: 'default', label: '', groupId: ''}, {kind: 'videoinput', deviceId: 'default', label: '', groupId: ''} ]; resolve(infos); }); }; if (browserDetails.version < 41) { // Work around http://bugzil.la/1169665 var orgEnumerateDevices = navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices); navigator.mediaDevices.enumerateDevices = function() { return orgEnumerateDevices().then(undefined, function(e) { if (e.name === 'NotFoundError') { return []; } throw e; }); }; } if (browserDetails.version < 49) { var origGetUserMedia = navigator.mediaDevices.getUserMedia. bind(navigator.mediaDevices); navigator.mediaDevices.getUserMedia = function(c) { return origGetUserMedia(c).then(function(stream) { // Work around https://bugzil.la/802326 if (c.audio && !stream.getAudioTracks().length || c.video && !stream.getVideoTracks().length) { stream.getTracks().forEach(function(track) { track.stop(); }); throw new DOMException('The object can not be found here.', 'NotFoundError'); } return stream; }, function(e) { return Promise.reject(shimError_(e)); }); }; } if (!(browserDetails.version > 55 && 'autoGainControl' in navigator.mediaDevices.getSupportedConstraints())) { var remap = function(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]); }; } } navigator.getUserMedia = function(constraints, onSuccess, onError) { if (browserDetails.version < 44) { return getUserMedia_(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); }; }; },{"../utils":13}],12:[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'; var utils = require('../utils'); module.exports = { shimLocalStreamsAPI: function(window) { if (typeof window !== 'object' || !window.RTCPeerConnection) { return; } if (!('getLocalStreams' in window.RTCPeerConnection.prototype)) { window.RTCPeerConnection.prototype.getLocalStreams = function() { if (!this._localStreams) { this._localStreams = []; } return this._localStreams; }; } if (!('getStreamById' in window.RTCPeerConnection.prototype)) { window.RTCPeerConnection.prototype.getStreamById = function(id) { var result = null; if (this._localStreams) { this._localStreams.forEach(function(stream) { if (stream.id === id) { result = stream; } }); } if (this._remoteStreams) { this._remoteStreams.forEach(function(stream) { if (stream.id === id) { result = stream; } }); } return result; }; } if (!('addStream' in window.RTCPeerConnection.prototype)) { var _addTrack = window.RTCPeerConnection.prototype.addTrack; window.RTCPeerConnection.prototype.addStream = function(stream) { if (!this._localStreams) { this._localStreams = []; } if (this._localStreams.indexOf(stream) === -1) { this._localStreams.push(stream); } var pc = this; stream.getTracks().forEach(function(track) { _addTrack.call(pc, track, stream); }); }; window.RTCPeerConnection.prototype.addTrack = function(track, stream) { if (stream) { if (!this._localStreams) { this._localStreams = [stream]; } else if (this._localStreams.indexOf(stream) === -1) { this._localStreams.push(stream); } } return _addTrack.call(this, track, stream); }; } if (!('removeStream' in window.RTCPeerConnection.prototype)) { window.RTCPeerConnection.prototype.removeStream = function(stream) { if (!this._localStreams) { this._localStreams = []; } var index = this._localStreams.indexOf(stream); if (index === -1) { return; } this._localStreams.splice(index, 1); var pc = this; var tracks = stream.getTracks(); this.getSenders().forEach(function(sender) { if (tracks.indexOf(sender.track) !== -1) { pc.removeTrack(sender); } }); }; } }, shimRemoteStreamsAPI: function(window) { if (typeof window !== 'object' || !window.RTCPeerConnection) { return; } if (!('getRemoteStreams' in window.RTCPeerConnection.prototype)) { window.RTCPeerConnection.prototype.getRemoteStreams = function() { return this._remoteStreams ? this._remoteStreams : []; }; } if (!('onaddstream' in window.RTCPeerConnection.prototype)) { Object.defineProperty(window.RTCPeerConnection.prototype, 'onaddstream', { get: function() { return this._onaddstream; }, set: function(f) { var pc = 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 (!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); }); }); } }); } }, shimCallbacksAPI: function(window) { if (typeof window !== 'object' || !window.RTCPeerConnection) { return; } var prototype = window.RTCPeerConnection.prototype; var createOffer = prototype.createOffer; var createAnswer = prototype.createAnswer; var setLocalDescription = prototype.setLocalDescription; var setRemoteDescription = prototype.setRemoteDescription; var addIceCandidate = prototype.addIceCandidate; prototype.createOffer = function(successCallback, failureCallback) { var options = (arguments.length >= 2) ? arguments[2] : arguments[0]; var promise = createOffer.apply(this, [options]); if (!failureCallback) { return promise; } promise.then(successCallback, failureCallback); return Promise.resolve(); }; prototype.createAnswer = function(successCallback, failureCallback) { var options = (arguments.length >= 2) ? arguments[2] : arguments[0]; var promise = createAnswer.apply(this, [options]); if (!failureCallback) { return promise; } promise.then(successCallback, failureCallback); return Promise.resolve(); }; var withCallback = function(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(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(candidate, successCallback, failureCallback) { var promise = addIceCandidate.apply(this, [candidate]); if (!failureCallback) { return promise; } promise.then(successCallback, failureCallback); return Promise.resolve(); }; prototype.addIceCandidate = withCallback; }, shimGetUserMedia: function(window) { var navigator = window && window.navigator; if (!navigator.getUserMedia) { if (navigator.webkitGetUserMedia) { navigator.getUserMedia = navigator.webkitGetUserMedia.bind(navigator); } else if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { navigator.getUserMedia = function(constraints, cb, errcb) { navigator.mediaDevices.getUserMedia(constraints) .then(cb, errcb); }.bind(navigator); } } }, shimRTCIceServerUrls: function(window) { // migrate from non-spec RTCIceServer.url to RTCIceServer.urls var OrigPeerConnection = window.RTCPeerConnection; window.RTCPeerConnection = function(pcConfig, pcConstraints) { if (pcConfig && pcConfig.iceServers) { var newIceServers = []; for (var i = 0; i < pcConfig.iceServers.length; i++) { var server = pcConfig.iceServers[i]; if (!server.hasOwnProperty('urls') && server.hasOwnProperty('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 window.RTCPeerConnection) { Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { get: function() { return OrigPeerConnection.generateCertificate; } }); } }, shimTrackEventTransceiver: function(window) { // Add event.transceiver member over deprecated event.receiver if (typeof window === 'object' && window.RTCPeerConnection && ('receiver' in window.RTCTrackEvent.prototype) && // can't check 'transceiver' in window.RTCTrackEvent.prototype, as it is // defined for some reason even when window.RTCTransceiver is not. !window.RTCTransceiver) { Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', { get: function() { return {receiver: this.receiver}; } }); } }, shimCreateOfferLegacy: function(window) { var origCreateOffer = window.RTCPeerConnection.prototype.createOffer; window.RTCPeerConnection.prototype.createOffer = function(offerOptions) { var pc = this; if (offerOptions) { var audioTransceiver = pc.getTransceivers().find(function(transceiver) { return transceiver.sender.track && transceiver.sender.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) { pc.addTransceiver('audio'); } var videoTransceiver = pc.getTransceivers().find(function(transceiver) { return transceiver.sender.track && transceiver.sender.track.kind === 'video'; }); if (offerOptions.offerToReceiveVideo === false && videoTransceiver) { if (videoTransceiver.direction === 'sendrecv') { videoTransceiver.setDirection('sendonly'); } else if (videoTransceiver.direction === 'recvonly') { videoTransceiver.setDirection('inactive'); } } else if (offerOptions.offerToReceiveVideo === true && !videoTransceiver) { pc.addTransceiver('video'); } } return origCreateOffer.apply(pc, arguments); }; } }; },{"../utils":13}],13:[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'; 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. 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(e) { cb(wrapper(e)); }; this._eventMap = this._eventMap || {}; this._eventMap[cb] = wrappedCallback; return nativeAddEventListener.apply(this, [nativeEventName, wrappedCallback]); }; var nativeRemoveEventListener = proto.removeEventListener; proto.removeEventListener = function(nativeEventName, cb) { if (nativeEventName !== eventNameToWrap || !this._eventMap || !this._eventMap[cb]) { return nativeRemoveEventListener.apply(this, arguments); } var unwrappedCb = this._eventMap[cb]; delete this._eventMap[cb]; return nativeRemoveEventListener.apply(this, [nativeEventName, unwrappedCb]); }; Object.defineProperty(proto, 'on' + eventNameToWrap, { get: function() { return this['_on' + eventNameToWrap]; }, set: function(cb) { if (this['_on' + eventNameToWrap]) { this.removeEventListener(eventNameToWrap, this['_on' + eventNameToWrap]); delete this['_on' + eventNameToWrap]; } if (cb) { this.addEventListener(eventNameToWrap, this['_on' + eventNameToWrap] = cb); } } }); } // Utility methods. module.exports = { extractVersion: extractVersion, wrapPeerConnectionEvent: wrapPeerConnectionEvent, disableLog: function(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. */ disableWarnings: function(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'); }, log: function() { if (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. */ deprecated: function(oldMethod, newMethod) { if (!deprecationWarnings_) { return; } console.warn(oldMethod + ' is deprecated, please use ' + newMethod + ' instead.'); }, /** * Browser detector. * * @return {object} result containing browser and version * properties. */ detectBrowser: function(window) { var navigator = window && window.navigator; // Returned result object. var result = {}; result.browser = null; result.version = null; // Fail early if it's not a browser if (typeof window === 'undefined' || !window.navigator) { result.browser = 'Not a browser.'; return result; } if (navigator.mozGetUserMedia) { // Firefox. result.browser = 'firefox'; result.version = extractVersion(navigator.userAgent, /Firefox\/(\d+)\./, 1); } else if (navigator.webkitGetUserMedia) { // Chrome, Chromium, Webview, Opera. // Version matches Chrome/WebRTC version. result.browser = 'chrome'; result.version = extractVersion(navigator.userAgent, /Chrom(e|ium)\/(\d+)\./, 2); } else if (navigator.mediaDevices && navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)) { // Edge. result.browser = 'edge'; result.version = extractVersion(navigator.userAgent, /Edge\/(\d+).(\d+)$/, 2); } else if (navigator.mediaDevices && navigator.userAgent.match(/AppleWebKit\/(\d+)\./)) { // Safari. result.browser = 'safari'; result.version = extractVersion(navigator.userAgent, /AppleWebKit\/(\d+)\./, 1); } else { // Default fallthrough: not supported. result.browser = 'Not a supported browser.'; return result; } return result; } }; },{}]},{},[3])(3) }); // GLOBAL VAR TO IDENTIRY WORMHOLE var de_modima_wormhole; (function (module) { /**************************************************** ************* SESSION STORAGE ********************** ****************************************************/ module.Storage = {}; module.Storage.getChannel = function (key) { if (!key) { return null; } var item = sessionStorage.getItem(key); if (item) { var channel = JSON.parse(item); if (channel && channel.privateId && channel.publicId) { return channel; } else { return null; } } }; module.Storage.storeChannel = function (channel, key) { if (!channel || !key) { return false; } // Add currently used wormhole channel.wormhole = de_modima_wormhole; sessionStorage.setItem(key, JSON.stringify(channel)); }; module.Storage.removeChannel = function (key) { sessionStorage.removeItem(key); }; /**************************************************** ************* ENDPOINT PUBLIC API ****************** ****************************************************/ module.Endpoint = function (options) { var self = this; // Channel offset is needed as indicator, when multiple endpoints are used in one clinet this.channelStoreKey = options && options.channelStoreKey ? options.channelStoreKey : "de_modima_communication_channel"; // Map of requests that are currently executed this.requests = {}; this.msgHandlers = {}; // External event handler this.connected = false; this.errors = []; this.longpollRequest = null; this.changeWHRequest = null; this.tsLastPolling = null; if (options && options.onWormholeError && typeof options.onWormholeError === "function") { this.onWormholeError = options.onWormholeError; } /************************ UNIQUE CLIENT ID **********************/ var clientId = localStorage.getItem("UNIQUE_CLIENT_ID"); if (!clientId) { clientId = createId(10); localStorage.setItem("UNIQUE_CLIENT_ID", clientId); } this.clientId = clientId; console.log("CLIENT_ID " + this.clientId); /************************ UNIQUE CLIENT ID END *******************/ setInterval(function () { self.checkPollingAlive(); }, 5000); //setInterval(function() {self.healthcheck()}, 30000); this.connect(); }; /******************************************************************** ************************ FEEDBACK HANDLING ************************* ********************************************************************/ module.Endpoint.prototype.webrtcFeedback = function (data, force, iserror) { module.logger.log("webrtc log", data); var clientaddress = "unknown"; var channel = module.Storage.getChannel(this.channelStoreKey); if (channel) { clientaddress = "SSIP/" + channel.publicId + "@" + channel.wormhole; } else { clientaddress = this.lastKnownClientAddress; } /* var seatId = "unknown"; if (this.seat) { seatId = this.seat.id; } */ var logdata = typeof data === "string" ? { msg: data } : data; logdata.clientaddress = clientaddress; if (this.clientId) logdata.clientId = this.clientId; //de.modima.communication.webrtcFeedback(msg, clientInfos, force); de.modima.communication.webrtcFeedback("", logdata, force, iserror); }; /******************************************************************** ********************* POLLING HEALTH CHECK ************************* ********************************************************************/ module.Endpoint.prototype.checkPollingAlive = function () { //console.log("Check polling", this) // Wait until longpolling was started for the first time if (!this.tsLastPolling) return; var interval = new Date().getTime() - this.tsLastPolling; if (interval > 20000) { module.logger.info("WH request timeout - waited " + (interval / 1000) + "s --> Restart longpolling"); // Restart longpolling this.longpoll(); } }; module.Endpoint.prototype.createChannel = function () { var self = this; if (!de_modima_wormhole) { this.changeWormhole(); return; } var onSuccess = function (resp) { try { var channel = JSON.parse(resp.responseText); // Store channel in session storage self.lastKnownClientAddress = "SSIP/" + channel.publicId + "@" + de_modima_wormhole; module.Storage.storeChannel(channel, self.channelStoreKey); self.longpoll(); self.connected = true; self.onConnect(); } catch (e) { // Problem with local storage on iOS --> Do not reconnect if (e.toString().indexOf("QuotaExceededError") >= 0) { self.onConnectionError(new Error("Endpoint.createChannel.onSuccess: Create new channel FAILED: " + e + " --> return"), true); throw e; } else { module.logger.error("createChannel - Failed"); var strErr = "Endpoint.createChannel.onSuccess: Create new channel FAILED: " + e; if (resp) { strErr += " ### " + stringifyObject(resp); } self.onConnectionError(new Error(strErr)); } } }; var onError = function (resp) { module.logger.error("createChannel - onError"); self.onConnectionError(new Error("Endpoint.createChannel.onError: Create new channel FAILED with status: " + resp.status)); // + " ### " + stringifyObject(resp))); }; var xhr = createCORSRequest({ method: "POST", url: "//" + de_modima_wormhole + "/channels/create", async: true, timeout: 5000, onload: onSuccess, onerror: onError, ontimeout: onError }); xhr.send(); }; function stringifyObject(obj) { var result = {}; if (!obj) return result; for (var p in obj) { result[p] = obj[p]; } return JSON.stringify(result); } module.Endpoint.prototype.connect = function () { if (de_modima_wormhole) { module.logger.info("connect to wormhole: " + de_modima_wormhole); } var self = this; // Try connecting to stored channel var channel = module.Storage.getChannel(this.channelStoreKey); if (!channel || (de_modima_wormhole && de_modima_wormhole != channel.wormhole)) { this.createChannel(); } else { de_modima_wormhole = channel.wormhole; // Try if locally stored channel is still valid on proxy var onSuccess = function (req) { if (req.status === undefined || req.status == 200) { module.logger.info("connect - Success - response status: " + req.status); self.connected = true; self.onConnect(); self.longpoll(); } else { module.logger.error("connect - Failed"); self.createChannel(); } }; var onError = function () { module.logger.error("connect - onError"); self.onChannelError(); }; var onTimeout = function () { module.logger.error("connect - onTimeout"); self.onConnectionError(new Error(new Date() + "Connect FAILED")); }; createCORSRequest({ method: "GET", url: "//" + de_modima_wormhole + "/private/" + channel.privateId + "/ping", async: true, timeout: 5000, onload: onSuccess, onerror: onError, ontimeout: onTimeout }).send(); } }; module.Endpoint.prototype.healthcheck = function () { module.logger.log("healthcheck"); var self = this; createCORSRequest({ method: "GET", url: "//webrtc.24dial.com/api/v1/wormholes/status/alive", async: true, timeout: 5000, onload: function (req) { if (req.status == 200) { let whList = JSON.parse(req.responseText); if (whList.indexOf(de_modima_wormhole) == -1) { self.changeWormhole(); } } } }).send(); } module.Endpoint.prototype.changeWormhole = function (attempt) { var self = this; if (this.changeWHRequest && this.changeWHRequest.readyState !== 4) { module.logger.info("Change WH Request Pending..."); return; } module.logger.info("change wormhole"); attempt = attempt !== undefined ? attempt : 0; var onSuccess = function (req) { if (req.status === undefined || req.status == 200) { module.logger.info("change wormhole - onSuccess"); var whList = JSON.parse(req.responseText); if (whList.length > 0) { var randIdx = Math.floor(Math.random() * whList.length); // Change wormhole de_modima_wormhole = whList[randIdx]; self.connect(); } } }; var onError = function () { module.logger.error("changeWormhole - onError"); setTimeout(function () { self.changeWormhole(++attempt); }, 1000); }; this.changeWHRequest = createCORSRequest({ method: "GET", url: "//webrtc.24dial.com/api/v1/wormholes/status/alive", async: true, timeout: 5000, onload: onSuccess, onerror: onError, ontimeout: onError }); this.changeWHRequest.send(); }; module.Endpoint.prototype.getURI = function (type) { var self = this; var channel = module.Storage.getChannel(this.channelStoreKey); if (!channel) { self.onConnectionError(new Error("No endpoint connection")); return; } if (type == "http") return location.protocol + "//" + channel.wormhole + "/client/" + channel.publicId; else if (type == "sip") return channel.publicId + "@" + channel.wormhole; }; /******************************************************** ******************** PRIVATE MEMBERS ******************* ********************************************************/ module.Endpoint.prototype.onConnect = function () { module.logger.log("onConnect --> Inform clients:", this.msgHandlers); for (var h in this.msgHandlers) { this.msgHandlers[h].onEndpointReady(); } }; module.Endpoint.prototype.onConnectionError = function (err, noWHChange) { var self = this; module.logger.error("onConnectionError: " + err); this.webrtcFeedback("Connection error: " + err.stack + " ### " + de_modima_wormhole); this.connected = false; this.errors.push(err); if (this.msgHandlers.length > 0) { for (var j = 0; j < this.errors.length; j++) { var error = this.errors[j].pop(); for (var i = 0; i < this.msgHandlers.length; i++) { this.msgHandlers[i].onError(error); } } } // Change Wormhole if (!noWHChange) { setTimeout(function () { self.changeWormhole(); }, 1000); } }; module.Endpoint.prototype.onChannelError = function () { module.logger.error("onChannelError --> Remove stored channel"); this.connected = false; module.Storage.removeChannel(this.channelStoreKey); if (this.onWormholeError && typeof this.onWormholeError === "function") { new Promise((resolve, reject) => { this.onWormholeError("connection to wormhole lost") }) } this.connect(); }; module.Endpoint.prototype.longpoll = function (requestData, attempt) { var self = this; attempt = attempt !== undefined ? attempt : 0; // Remember timestamp of last polling request self.tsLastPolling = new Date().getTime(); var channel = module.Storage.getChannel(this.channelStoreKey); if (!channel) { self.onChannelError(); return; } async function onSuccess(req) { // Mark onSuccess as async self.connected = true; // xhr on IE8 has no status property (undefined) if (req.status === undefined || req.status == 200) { var msg = {}; var result; if (req.responseText) { msg = JSON.parse(req.responseText); if (msg.type != "timeout") { result = await self.handleMessage(msg); // Await the async handleMessage call } } self.longpoll(result); } else if (!req.getAllResponseHeaders()) { module.logger.info("longpoll - Aborted"); } else { module.logger.error("longpoll - Failed"); self.onChannelError(); } } function onError(req) { module.logger.error("longpoll - onError"); self.onChannelError(); } function onTimeout(req) { module.logger.info("longpoll request timed out. --> Retry (" + attempt + ")"); if (self.onWormholeError && typeof self.onWormholeError === "function") { new Promise((resolve, reject) => { self.onWormholeError("connection to wormhole lost") }) } if (attempt < 3) { setTimeout(function () { self.longpoll(requestData, ++attempt); }, attempt * (1000 + Math.round(Math.random() * 1000))); } else { module.logger.info("Max retries (3) reached. --> Get new channel from proxy."); self.onChannelError(); } } // Quit exisiting polling request before starting a new one if (self.longpollRequest) { self.longpollRequest.abort(); } self.longpollRequest = createCORSRequest({ method: "POST", url: "//" + de_modima_wormhole + "/private/" + channel.privateId + "?" + Math.random(), // Hack for IE8 --> Use a random query parameter to avoid caching and instant return of xdr async: true, timeout: 15000, onload: onSuccess, onerror: onTimeout, // FIX FOR FIREFOX --> Reload(F5) causes abort of last longpolling attempt and provokes creation of new channel --> Use 3 Retries to workaround this problem ontimeout: onTimeout }); if (requestData) { self.longpollRequest.send(JSON.stringify(requestData)); } else { self.longpollRequest.send(); } }; /** * Message Type --> Handler: * "request" --> dispatcher.js * "sip" --> webRTCClient.js **/ module.Endpoint.prototype.registerMessageHandler = function (msgType, handler) { if (!handler) { throw "Parameter 'handler' is required."; } this.msgHandlers[msgType] = handler; }; module.Endpoint.prototype.handleMessage = async function (msg) { // Mark function as async var handler = this.msgHandlers[msg.type]; if (!handler) { var respMsg = { "id": msg.id, "type": "http", "params": { "status": 404, "statusText": "Handler not found for message type '" + msg.type + "'." } }; return respMsg; // This directly returns a value, which is fine in async functions } else { // Await the asynchronous handler to complete and return its promise return await handler.handleMessage(msg); } }; module.Endpoint.prototype.sendMessage = function (msg, attempts) { var self = this; var reqId = createId(5); this.requests[reqId] = { itr: 0, totalItr: attempts ? attempts : 1, exec: function () { doSend(msg, self.repeat(reqId)); } }; var doSend = function (msg, onFinished) { function onSuccess(req) { if (req.status === undefined || req.status == 200) { if (onFinished && typeof onFinished === "function") { onFinished(); } } else { module.logger.error("Error sending " + msg.type + " message: " + req.status + " - " + req.statusText + " (" + req.responseText + ")"); } } function onError(req) { module.logger.error("Sending " + msg.type + " message failed. Cause: " + req.status + " - " + req.statusText); } var channel = module.Storage.getChannel(self.channelStoreKey); module.logger.log("Send message", reqId, msg); createCORSRequest({ method: "POST", url: "//" + de_modima_wormhole + "/private/" + channel.privateId + "/" + msg.type, timeout: 5000, async: true, onload: onSuccess, onerror: onError }).send(JSON.stringify(msg)); }; this.repeat(reqId); return reqId; }; module.Endpoint.prototype.repeat = function (reqId) { var self = this; if (!this.requests[reqId]) { return; } let timeout = (self.requests[reqId].itr > 0 ? Math.pow(2, self.requests[reqId].itr - 1) : 0) * 1000; setTimeout(function () { if (!self.requests[reqId]) { return; } if (self.requests[reqId].itr == self.requests[reqId].totalItr) { //module.logger.log("Max retries reached: --> Delete request: ", reqId, self.requests[reqId]); delete self.requests[reqId]; return; } self.requests[reqId].itr++; self.requests[reqId].exec(); }, timeout); }; module.Endpoint.prototype.stopRetry = function (reqId) { module.logger.log("stop message retry", reqId); delete this.requests[reqId]; }; function createId(length) { var id = ""; var charlist = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"; var idLength = !length ? (idLength || 16) : length; for (var i = 0; i < idLength; i++) { id += charlist.charAt(Math.floor(Math.random() * 32)); } return id; } }(de.modima.communication)); (function (module) { /**************************************************** ************* HTTP REQUEST HANDLER ***************** ****************************************************/ module.HTTPRequestHandler = function (pattern, func) { // Split the pattern by the colon which separates the path from the methods var parts = pattern.split(':'); var path = parts[0]; // Check if specific methods are defined; if not, default to ["GET"] this.methods = parts.length > 1 ? parts[1].replace(/[\[\]]/g, '').split('|') : ["GET"]; // Remove a trailing slash from the path to simplify segment handling this.segments = path.replace(/\/$/, '').split('/'); // Assign the function passed to the handler this.func = func; }; module.HTTPRequestHandler.prototype.match = function(path, method) { // Check if the request method is supported if (!this.methods.includes(method)) { return false; } // Normalize path to handle trailing slashes consistently let normalizedPath = path.endsWith("/") ? path.slice(0, -1) + "/" : path; let matchsegs = normalizedPath.split("/"); if (matchsegs[matchsegs.length - 1] === "") { matchsegs.pop(); // Remove the empty element for the trailing slash } let params = {}; // Check each segment for a match for (let i = 0; i < this.segments.length; i++) { let templateSeg = this.segments[i]; let pathSeg = matchsegs[i]; if (pathSeg === undefined) { return false; // Not enough segments in the path to match the template } if (templateSeg.startsWith("{") && templateSeg.endsWith("}")) { let isWildcard = templateSeg.includes("_any_"); let paramName = templateSeg.slice(1, -1); if (isWildcard) { params[paramName] = matchsegs.slice(i).join("/"); return params; // Capture all remaining segments for wildcard } else { params[paramName] = pathSeg; } } else if (templateSeg !== pathSeg) { return false; // Segment mismatch } } // Handle case where there are more segments in the path than in the pattern if (matchsegs.length > this.segments.length) { return false; } return params; } /**************************************************** ********************** RESTCLIENT ****************** ****************************************************/ module.RestClient = function (config) { if (!config.endpoint) throw new Error("Endpoint required"); if (typeof config.onError !== 'function') throw new Error("Handler onError missing or not a function"); if (typeof config.onReady !== 'function') throw new Error("Handler onReady missing or not a function"); this.endpoint = config.endpoint; this.onError = this.createErrorHandler(config.onError); // Setup a dedicated error handler this.onReady = config.onReady; this.endpoint.registerMessageHandler("http", this); this.requestHandlers = {}; }; module.RestClient.prototype.createErrorHandler = function (onError) { var self = this; return function (err) { var errStr = typeof err === "string" ? err : (err.msg || "") + " --- " + (err.stack || ""); if (self.endpoint) { self.endpoint.webrtcFeedback("REST Error: " + errStr); } module.logger.log("REST Error: " + errStr); onError(errStr); if (typeof self.initialize === 'function') { self.initialize(); } }; }; module.RestClient.prototype.getURI = function () { return this.endpoint.getURI("http"); }; module.RestClient.prototype.onEndpointReady = function () { module.logger.log("REST: onEndpointReady received"); this.onReady(); }; module.RestClient.prototype.registerRequestHandler = function (pattern, handleFunc) { // Directly use the pattern as a key to store handlers. this.requestHandlers[pattern] = new module.HTTPRequestHandler(pattern, handleFunc); }; module.RestClient.prototype.getHandle = function(path, method) { // Iterate over registered request handlers to find a match for (let key in this.requestHandlers) { let handler = this.requestHandlers[key]; let params = handler.match(path, method); if (params) { // If a match is found, return the handler and the extracted parameters return { func: handler.func, params: params }; } } return null; // Return null if no handlers match } /**************************************************** ************* RESTCLIENT PUBLIC API **************** ****************************************************/ module.RestClient.prototype.handleMessage = async function (msg) { var self = this; var response = { "id": msg.id, "type": "http", "params": { "status": 200, "statusText": "OK" }, "sent": false // Initialize the response sent flag }; try { var handle = this.getHandle(msg.params["path"], msg.params["method"]); if (!handle) { response.params.status = 400; response.params.statusText = 'Bad Request (invalid path)'; return response; } var request = { "method": msg.params["method"], "pathParams": handle.params, "queryParams": msg.params["parameters"], "contentText": msg.content, "respond": async function (result) { if (!response.sent) { response.sent = true; // Mark response as sent self.endpoint.sendMessage({ "id": msg.id, "type": "http", "params": result || { "status": 200, "statusText": "OK" } }); } } }; var result = await handle.func(request); if (result !== undefined) { if (!response.sent) { response.sent = true; Object.assign(response.params, this.formatResponse(result)); self.endpoint.sendMessage(response); } } else { // Setup a fallback response if neither result nor respond is defined setTimeout(() => { if (!response.sent) { // Ensure response was not previously sent response.sent = true; self.endpoint.sendMessage(response); } }, 9000); // slightly before the timeout after 10 seconds } } catch (e) { console.error(e) console.error(e.stack) return { "id": msg.id, "type": "http", "params": { "status": e.status || 500, "statusText": e.message || "Internal Server Error" } }; } }; // Helper function to format the response based on the type of result module.RestClient.prototype.formatResponse = function (result) { var status = 200, statusText = "OK", contentText; switch (typeof result) { case "boolean": status = result ? 204 : 400; break; case "string": status = 400; statusText = result; break; case "number": status = result; break; case "object": if (result !== null) { contentText = JSON.stringify(result); } break; default: status = 204; statusText = "No Content"; break; } return { status, statusText, contentText }; }; }(de.modima.communication)); /**************************************************** ********************* WebRTC CLIENT **************** ****************************************************/ (function (module) { const VERSION = "17.0.0 STABLE"; /******************************************************************** ************************** Plugin Glue Code ************************ ********************************************************************/ module.plugins = module.plugins || {}; class WebRTCPlugin { config = null; wasm = null; audioContexts = []; // per channel audioSources = []; // per channel streamProcessors = []; // per channel messagebus = null; constructor(config) { if (new.target === WebRTCPlugin) throw new TypeError("Cannot construct WebRTCPlugin instances directly"); this.config = config; this.messagebus = this.config.messagebus; if (this.messagebus) this.messagebus.notify(this.config.pubsubTopic, JSON.stringify({ type: "webrtc_plugin_constructor" })) this.init(); } async loadWasm(wasmName) { try { if (this.messagebus) this.messagebus.notify(this.config.pubsubTopic, JSON.stringify({ type: "webrtc_wasm_load" })) if (!wasmName.endsWith(".wasm")) workletName += ".wasm"; let wasmURL = `${this.config.pluginResourcePath}${wasmName}`; const response = await fetch(wasmURL); const bytes = await response.arrayBuffer(); this.wasm = await WebAssembly.compile(bytes); } catch (error) { console.error("[WebRTC] Error loading wasm:", error); } } async createAudioContext(channel) { if (this.messagebus) this.messagebus.notify(this.config.pubsubTopic, JSON.stringify({ type: "webrtc_create_audio_context" })) let sampleRate = this.config.sampleRate || 8000; if (navigator.userAgent.includes("Firefox")) { sampleRate = 44100; } if (this.audioContexts[channel]) this.audioContexts[channel].close(); this.audioContexts[channel] = new AudioContext({ sampleRate: sampleRate }); if (this.config.workletName) { let workletName = this.config.workletName; if (!workletName.endsWith(".js")) workletName += ".js"; let workletURL = `${this.config.pluginResourcePath}${workletName}`; await this.audioContexts[channel].audioWorklet.addModule(workletURL); } } destroyAudioContexts(channel) { if (this.messagebus) this.messagebus.notify(this.config.pubsubTopic, JSON.stringify({ type: "webrtc_destroy_audio_context" })) if (typeof channel === "number") { if (this.streamProcessors[channel]) { this.streamProcessors[channel].port.onmessage = null; this.streamProcessors[channel].disconnect(); this.streamProcessors[channel] = null; } if (this.audioSources[channel]) { this.audioSources[channel].disconnect(); this.audioSources[channel] = null; } if (this.audioContexts[channel]) { this.audioContexts[channel].close(); this.audioContexts[channel] = null; } } else { for (let channel = 0; channel <= 1; channel++) { if (this.streamProcessors[channel]) { this.streamProcessors[channel].port.onmessage = null; this.streamProcessors[channel].disconnect(); this.streamProcessors[channel] = null; } if (this.audioSources[channel]) { this.audioSources[channel].disconnect(); this.audioSources[channel] = null; } if (this.audioContexts[channel]) { this.audioContexts[channel].close(); this.audioContexts[channel] = null; } } } } async init() { throw new Error("Method 'init()' must be implemented."); } postMessageToStreamProcessor(msg, channel) { if (this.messagebus) this.messagebus.notify(this.config.pubsubTopic, JSON.stringify({ type: "webrtc_send_message_to_stream_processor", state: { streamProcessors: !!this.streamProcessors[channel], channel: channel } })) if (typeof channel === "number" && this.streamProcessors[channel]) { this.streamProcessors[channel].port.postMessage(msg) } else { for (let channel = 0; channel <= 1; channel++) { if (this.streamProcessors[channel]) { this.streamProcessors[channel].port.postMessage(msg) } } } } async startStreamProcessing(stream, channel) { if (this.messagebus) this.messagebus.notify(this.config.pubsubTopic, JSON.stringify({ type: "webrtc_start_stream_processing" })) this.destroyAudioContexts(channel); if (this.config.features) { if (!this.config.features.agent && channel === 0) { console.log("[WebRTC] requested processing on customer channel only --> skip agent channel") return // process only customer channel } if (!this.config.features.customer && channel === 1) { console.log("[WebRTC] requested processing on agent channel only --> skip customer channel") return // process only agent channel } } await this.createAudioContext(channel); let workletName = this.config.workletName if (workletName.endsWith(".js")) workletName = workletName.slice(0, -3); console.log(`[WebRTC] init AudioWorkletProcessor: ${workletName}`) let processorOptions = { channel: channel } if (this.wasm) processorOptions.wasm = this.wasm if (this.config.features) { processorOptions.features = channel === 0 ? this.config.features.agent : this.config.features.customer } if (this.streamProcessors[channel]) { this.streamProcessors[channel].port.onmessage = null; this.streamProcessors[channel].disconnect(); } this.streamProcessors[channel] = new AudioWorkletNode(this.audioContexts[channel], workletName, { processorOptions: processorOptions }); this.streamProcessors[channel].port.onmessage = (e => { if (this.messagebus && this.config.pubsubTopic) { let msg = typeof e.data === "string" ? e.data : JSON.stringify(e.data) this.messagebus.notify(this.config.pubsubTopic, msg) } if (this.config.onmessage) this.config.onmessage(e.data, channel) }) this.streamProcessors[channel].onprocessorerror = (e) => { console.error("[WebRTC] streamProcessor error", e); }; console.log(`[WebRTC] start stream processing on channel ${channel}`) if (this.audioSources[channel]) this.audioSources[channel].disconnect(); this.audioSources[channel] = this.audioContexts[channel].createMediaStreamSource(stream); this.audioSources[channel].connect(this.streamProcessors[channel]); this.audioContexts[channel].resume(); } stopStreamProcessing() { if (this.messagebus) this.messagebus.notify(this.config.pubsubTopic, JSON.stringify({ type: "webrtc_stop_stream_processing" })) console.log("[WebRTC] stop stream processing"); this.destroyAudioContexts(); } pauseStreamProcessing() { if (this.messagebus) { this.messagebus.notify("GuidelineMatcher-events", JSON.stringify({ type: "webrtc_pause_stream_processing", })) } for (let channel = 0; channel <= 1; channel++) { this.postMessageToStreamProcessor({ disable: true }, channel) } } resumeStreamProcessing() { if (this.messagebus) { this.messagebus.notify("GuidelineMatcher-events", JSON.stringify({ type: "webrtc_resume_stream_processing", })) } for (let channel = 0; channel <= 1; channel++) { this.postMessageToStreamProcessor({ disable: false }, channel) } } } class GuidelineMatcher extends WebRTCPlugin { async init() { this.config.pluginResourcePath = `https://${VERSION.includes("DEV") ? "dev-xdot-guidematch" : "guidematch"}-dot-cloudstack5.appspot.com/plugins/GuidelineMatcher/`; this.config.workletName = "audio-transform-processor.js" if (this.messagebus) this.messagebus.notify(this.config.pubsubTopic, JSON.stringify({ type: "webrtc_guideline_matcher_init" })) this.loadWasm("audiotransform_bg.wasm") } } module.plugins.GuidelineMatcher = GuidelineMatcher; class VoiceActivityDetector extends WebRTCPlugin { async init() { this.config.pluginResourcePath = `https://${VERSION.includes("DEV") ? "dev-xdot-guidematch" : "guidematch"}-dot-cloudstack5.appspot.com/plugins/VoiceActivityDetector/`; this.config.workletName = "vad-processor.js" } } module.plugins.VoiceActivityDetector = VoiceActivityDetector; /******************************************************************** ************************** CONSTRUCTOR ***************************** ********************************************************************/ module.WebRTCClient = function (config) { this.version = VERSION; var self = this; window.webRTCClient = this; // MAKE GLOBAL this.webrtcsupport = true; if (!config.endpoint) throw "Endpoint required"; if (!config.onError || typeof config.onError != "function") throw "Handler onError missing or not a function"; if (!config.onReady || typeof config.onReady != "function") throw "Handler onReady missing or not a function"; /************************** UA DETECTION ************************/ this.userAgent = navigator.mozGetUserMedia ? "firefox" : navigator.webkitGetUserMedia ? "chrome" : "unknown"; /***** REGISTER CLIENT AS MESSAGE HANDLER FOR SIP MESSAGES ******/ this.endpoint = config.endpoint; this.endpoint.registerMessageHandler("sip", this); this.onReady = config.onReady; if (config.messagebus) this.messagebus = config.messagebus; if (config.onConnect) this.onConnect = config.onConnect; if (config.onDisconnect) this.onDisconnect = config.onDisconnect; this.dialerIPs = {}; // List of dialer IPs for firewall holepunching /**************************** PLUG-INS **************************/ if (config.plugins) { for (let name in config.plugins) { this.enablePlugin({ ...config.plugins[name], name, }) } } /************************** ERROR LOGGING ***********************/ this.onError = function (err, noReset) { var errStr = ""; if (typeof err === "string") { errStr = err; } else if ((err instanceof Error)) { errStr += err.msg ? err.msg + " --- " : ""; errStr += err.stack ? err.stack : ""; } if (this.endpoint) { // Parameter: data, force, iserror this.endpoint.webrtcFeedback(errStr, false, true); } module.logger.error("Error: " + errStr); //config.onError(errStr); if (!noReset) { self.initialize(); } }; /********************* MEDIA ACCESS EVENT HANDLER ******************/ this.onMediaDenied = config.onMediaDenied && typeof config.onMediaDenied == "function" ? config.onMediaDenied : function () { this.onError("User has denied media access.", true, false); }; this.onMediaGranted = config.onMediaGranted && typeof config.onMediaGranted == "function" ? config.onMediaGranted : function () {}; this.onMediaRequested = config.onMediaRequested && typeof config.onMediaRequested == "function" ? config.onMediaRequested : function () {}; /************** CREATE AUDIO ELEMENT FOR INBOUND STREAM *************/ this.remoteAudio = document.getElementById('audio_rtp_stream_inbound'); if (!this.remoteAudio) { this.remoteAudio = document.createElement('audio'); this.remoteAudio.setAttribute("id", "audio_rtp_stream_inbound"); this.remoteAudio.setAttribute("autoplay", true); } // Config without STUN this.connectionConfig_NO_STUN = { "iceServers": [] }; this.connectionConfig_STUN = { "iceServers": [{ "urls": ["stun:stun.l.google.com:19302"] } /*, { "urls": ["stun:stun.services.mozilla.com"] }, { "urls": ["stun:23.21.150.121"] }*/ ] }; this.connectionConstraints = { "optional": [{ "DtlsSrtpKeyAgreement": true }] }; /******************************* MEDIA CONFIG ************************/ this.mediaConstraints = { "audio": true }; /******************************* SDP CONFIG **************************/ this.sdpConstraints = {}; /**************************** STATE PROTOCOL *************************/ this.stateProtocol = []; /************************ SIP MESSAGE HISTORY ************************/ // SIP requests sent by this client; this.sipRequests = {}; // SIP responses sent by this client; this.sipResponses = {}; // Initialize this.initialize(); }; /************************ PLUGIN HANDLING ************************/ module.WebRTCClient.prototype.activatePlugin = function (pluginName, pluginConfig = {}) { let PluginClass = module.plugins[pluginName] if (this.messagebus) { console.log("[WebRTC] webrtc_activatePlugin_received", pluginName) this.messagebus.notify(pluginConfig.pubsubTopic, JSON.stringify({ type: "webrtc_activatePlugin_received", state: { pluginName: pluginName, pluginClassExists: !!PluginClass, pluginAlreadyActivated: !!this.audioStreamPlugin, } })) } if (!PluginClass) { console.error(`[WebRTC] unknown plugin: ${pluginName}`) return } if (this.audioStreamPlugin) { //console.log("[WebRTC] plugin", pluginName, "already activated --> enable") //this.enablePlugin(pluginName) this.audioStreamPlugin.stopStreamProcessing() return } console.log("[WebRTC] activate plugin", pluginName, pluginConfig) if (this.messagebus) pluginConfig.messagebus = this.messagebus this.audioStreamPlugin = new PluginClass(pluginConfig) } module.WebRTCClient.prototype.deactivatePlugin = function (pluginName) { console.log("[WebRTC] deactivate plugin", pluginName) if (this.audioStreamPlugin) { this.audioStreamPlugin.stopStreamProcessing() this.audioStreamPlugin = null } } module.WebRTCClient.prototype.enablePlugin = function (pluginName) { if (this.messagebus) { this.messagebus.notify("GuidelineMatcher-events", JSON.stringify({ type: "webrtc_enablePlugin", state: { pluginName: pluginName, pluginExists: !!this.audioStreamPlugin, } })) } console.log("[WebRTC] enable plugin", pluginName) if (this.audioStreamPlugin) { this.audioStreamPlugin.resumeStreamProcessing() } } module.WebRTCClient.prototype.disablePlugin = function (pluginName) { if (this.messagebus) { this.messagebus.notify("GuidelineMatcher-events", JSON.stringify({ type: "webrtc_disablePlugin", state: { pluginName: pluginName, pluginExists: !!this.audioStreamPlugin, } })) } if (this.audioStreamPlugin) { this.audioStreamPlugin.pauseStreamProcessing() } } /** PLUGINS END */ /***************************************************************** *************************** DEBUGGING *************************** *****************************************************************/ module.WebRTCClient.prototype.sendLogToServer = function (data) { console.log("sendLogToServer", data) if (this.endpoint) { this.endpoint.webrtcFeedback(data); } }; /******************************************************************** ******************************** INIT ****************************** ********************************************************************/ module.WebRTCClient.prototype.initialize = async function (cntRetry) { var self = this; module.logger.info("initialize"); if (!window.RTCPeerConnection) { this.webrtcsupport = false; module.logger.error("[WebRTC] User Agent does not support WebRTC. Please use an up to date Chrome or Firefox browser."); return; } this.userMediaRequestPending = false; /************************ RESET WEBRTC SESSION ************************/ this.inviteCounter = 0; this.session = null; // Destroy local audio stream this.destroyLocalAudio(); // Destroy remote audio stream this.destroyRemoteAudio(); // Reset peer connection this.resetPeerConnection(); }; /******************************************************************** ********************** PEER CONNECTION HANDLING ******************** ********************************************************************/ var statsIntervalId; module.WebRTCClient.prototype.createPeerConnection = function (config) { module.logger.info("create peer connection"); var self = this; /******************************************************************************************************************************************************************** * SIGNALLING STATES: * stable...................there is no SDP offer/answer exchange in progress. This is also the initial state of the connection. * have-local-offer.........the local end of the connection has locally applied a SDP offer. * have-remote-offer........the remote end of the connection has locally applied a SDP offer. * have-local-pranswer......a remote SDP offer has been applied, and a SDP pranswer applied locally. * have-remote-pranswer.....a local SDP offer has been applied, and a SDP pranswer applied remotely. * closed...................the connection is closed. ********************************************************************************************************************************************************************/ function onSignalingStateChanged(event) { if (event.target && event.target.signalingState) { module.logger.info("Signaling state changed", event.target.signalingState); } } /******************************************************************************************************************************************************************** * ICE CONNECTION STATES: * new..............the ICE agent is gathering addresses or waiting for remote candidates (or both) * checking.........the ICE agent has remote candidates, on at least one component, and is check them, though it has not found a connection yet. At the same time, it may still be gathering candidates. * connected........the ICE agent has found a usable connection for each component, but is still testing more remote candidates for a better connection. At the same time, it may still be gathering candidates. * completed........the ICE agent has found a usable connection for each component, and is no more testing remote candidates. * failed...........the ICE agent has checked all the remote candidates and didn't find a match for at least one component. Connections may have been found for some components. * disconnected.....liveness check has failed for at least one component. This may be a transient state, e. g. on a flaky network, that can recover by itself. * closed...........the ICE agent has shutdown and is not answering to requests. ********************************************************************************************************************************************************************/ function onIceConnectionStateChanged(event) { if (!self.peerConnection) return; console.info("[WebRTC] ice connection state: " + self.peerConnection.iceConnectionState); } function onIceCandidate(event) { // TODO: evtl. nicht auf 'null' Kandidat warten, sondern direkt bei SRVRFLX Kandidat absenden var candStr = "null"; if (event.candidate) { candStr = JSON.stringify(event.candidate); } module.logger.log("Ice candidate: ", event.candidate); if (!event || !event.candidate || event.candidate.type === "srflx") { self.sendAnswer(); } } function onRemoteTrackAdded(event) { console.log("remote track added", event) if (event.track.kind === "audio") { if (!event.streams.length) { console.log("no audio stream") return; } var remoteAudioStream = event.streams[0]; if (self.audioStreamPlugin) { console.log("[WEBRTC] audio stream plugin found --> process incoming stream") self.audioStreamPlugin.startStreamProcessing(remoteAudioStream, 1) } else { console.log("[WEBRTC] NO audio stream plugin found (skip)") } self.remoteAudio.srcObject = remoteAudioStream; self.remoteStream = remoteAudioStream; self.remoteStreamTrack = remoteAudioStream.getTracks()[0]; self.remoteStreamTrack.onended = onRemoteTrackEnded } } function onRemoteTrackEnded(event) { console.log("[WEBRTC] remote track ended received", event) } function onNegotiationNeeded(event) { console.log("[WEBRTC] negotiation needed received", event) } function onStreamRemoved(event) { console.log("[WEBRTC] stream removed received", event) } function onConnectionStateChanged(event) { module.logger.log("onConnectionStateChanged", self.peerConnection.connectionState); switch (self.peerConnection.connectionState) { case "connected": if (self.onConnect) self.onConnect() break; case "disconnected": case "failed": case "closed": if (self.onDisconnect) self.onDisconnect() break; } } /****************************************************** ********************** CREATE PC ********************* ******************************************************/ try { if (config) { this.peerConnection = new RTCPeerConnection(config, this.connectionConstraints); } else if (this.userAgent == "firefox") { //this.peerConnection = new RTCPeerConnection(this.connectionConfig_STUN, this.connectionConstraints); this.peerConnection = new RTCPeerConnection(this.connectionConfig_NO_STUN, this.connectionConstraints); } else { this.peerConnection = new RTCPeerConnection(this.connectionConfig_NO_STUN, this.connectionConstraints); //this.peerConnection = new RTCPeerConnection(this.connectionConfig_NO_STUN, this.connectionConstraints); } //this.peerConnection = new RTCPeerConnection(this.connectionConfig_NO_STUN, this.connectionConstraints); //statsIntervalId = window.setInterval(printConnectionStats.bind(this), 1000); this.signalReady(); } catch (e) { try { this.peerConnection = new RTCPeerConnection((config || this.connectionConfig_STUN), this.connectionConstraints); this.signalReady(); } catch (e) { var errMsg = e.msg || ""; e.msg = JSON.stringify(this.connectionConfig) + " ### "; e.msg += JSON.stringify(this.connectionConstraints) + " ### "; e.msg += errMsg; this.onError(e); return; } } /******************* EVENT HANDLERS *******************/ this.peerConnection.ontrack = onRemoteTrackAdded; this.peerConnection.onicecandidate = onIceCandidate; this.peerConnection.oniceconnectionstatechange = onIceConnectionStateChanged; this.peerConnection.onsignalingstatechange = onSignalingStateChanged; this.peerConnection.onnegotiationneeded = onNegotiationNeeded; this.peerConnection.onremovestream = onStreamRemoved; this.peerConnection.onconnectionstatechange = onConnectionStateChanged; }; module.WebRTCClient.prototype.getConnectionDetails = async function () { let stats = await this.peerConnection.getStats(null) return new Promise((resolve) => { let connectionDetails = []; stats.forEach(report => { connectionDetails.push(report); }); resolve(connectionDetails); }) } /** outbound-rtp: -------------------------------- - packetsSent inbound-rtp: -------------------------------- - jitter (sekunden) - packetsReceived - packetsLost remote-inbound-rtp: -------------------------------- - jitter (sekunden) - packetsLost - roundTripTime sammeln (avg bilden) mean opinion score: (1 (poor) .. 5 (excellent)) -------------------------------- ' Take the average latency, add jitter, but double the impact to latency ' then add 10 for protocol latencies EffectiveLatency = ( AverageLatency + Jitter * 2 + 10 ) ' Implement a basic curve - deduct 4 for the R value at 160ms of latency ' (round trip). Anything over that gets a much more agressive deduction if EffectiveLatency < 160 then R = 93.2 - (EffectiveLatency / 40) else R = 93.2 - (EffectiveLatency - 120) / 10 ' Now, let's deduct 2.5 R values per percentage of packet loss R = R - (PacketLoss * 2.5) ' Convert the R into an MOS value.(this is a known formula) MOS = 1 + (0.035) * R + (.000007) * R * (R-60) * (100-R) */ function calculateAudioScore(stats) { var audioScore = function (rtt, plr) { var LOCAL_DELAY = 20; //20 msecs: typical frame duration function H(x) { return (x < 0 ? 0 : 1) } var a = 0 // ILBC: a=10 var b = 19.8 var c = 29.7 //R = 94.2 − Id − Ie var R = function (rtt, packetLoss) { var d = rtt + LOCAL_DELAY; var Id = 0.024 * d + 0.11 * (d - 177.3) * H(d - 177.3); var P = packetLoss; var Ie = a + b * Math.log(1 + c * P); var R = 94.2 - Id - Ie; return R; } //For R < 0: MOS = 1 //For 0 R 100: MOS = 1 + 0.035 R + 7.10E-6 R(R-60)(100-R) //For R > 100: MOS = 4.5 var MOS = function (R) { if (R < 0) { return 1; } if (R > 100) { return 4.5; } return 1 + 0.035 * R + 7.10 / 1000000 * R * (R - 60) * (100 - R); } return MOS(R(rtt, plr)); } if (stats.length < 2) { return 0; } var currentStats = stats[stats.length - 1]; var lastStats = stats[stats.length - 2]; var totalAudioPackets = (currentStats.packetsLost - lastStats.packetsLost) + (currentStats.packetsReceived - lastStats.packetsReceived); console.log("currentStats.packetsLost", currentStats.packetsLost) console.log("currentStats.packetsReceived", currentStats.packetsReceived) console.log("lastStats.packetsLost", lastStats.packetsLost) console.log("lastStats.packetsReceived", lastStats.packetsReceived) console.log("totalAudioPackets", totalAudioPackets) if (0 == totalAudioPackets) { return 0; } var plr = (currentStats.packetsLost - lastStats.packetsLost) / totalAudioPackets; var rtt = currentStats.rtt var score = audioScore(rtt, plr); return score; } var audioStats = [] var rtt = 0 module.WebRTCClient.prototype.printConnectionStats = function () { if (!this.peerConnection) return; this.peerConnection.getStats(null).then(stats => { stats.forEach(report => { if (report.roundTripTime) { rtt = report.roundTripTime } if (report.type !== "inbound-rtp") return audioStats.rtt = rtt audioStats.push(report) audioStats = audioStats.splice(audioStats.length - 2) if (audioStats.length == 2) { var score = calculateAudioScore(audioStats) console.log("mos", score) } }) }) } /* var jitter = []; var rtts = []; var packetsSent; var packetsLost; function printConnectionStats() { console.log("connection stats") if (!this.peerConnection) return; this.peerConnection.getStats(null).then(stats => { stats.forEach(report => { //console.log(report.type) if (report.type === "outbound-rtp") { packetsSent = report.packetsSent } if (report.type !== "remote-inbound-rtp") return //console.log(JSON.stringify(report)) jitter.unshift(report.jitter) jitter = jitter.slice(0, 100) jitter.sort((a, b) => a - b) avgJit = jitter[Math.ceil(rtts.length / 2)] rtts.unshift(report.roundTripTime) rtts = rtts.slice(0, 100) rtts.sort((a, b) => a - b) avgRtt = rtts[Math.ceil(rtts.length / 2)] effectiveLat = (avgRtt + avgJit * 2 + 10) console.log("effectiveLat", effectiveLat) var R; if (effectiveLat < 160) { R = 93.2 - (effectiveLat / 40) } else { R = 93.2 - (effectiveLat - 120) / 10 } R = R - ((packetsSent - report.packetsLost) / packetsSent * 2.5) console.log("R-Value", R) mos = 1 + (0.035) * R + (.000007) * R * (R - 60) * (100 - R) console.log("MOS", mos) }); }); } */ module.WebRTCClient.prototype.resetPeerConnection = function (message) { // Stop stream processor / audio context if (this.audioStreamPlugin) { // TODO: eventuell nur disconnecten / nicht komplett zerstören this.audioStreamPlugin.stopStreamProcessing() } if (this.peerConnection) { module.logger.info("close peer connection"); try { this.peerConnection.close(); } catch (e) { module.logger.info("Failed to execute 'close' on 'RTCPeerConnection': The RTCPeerConnection's signalingState is 'closed'"); } } this.peerConnection = null; this.createPeerConnection(); }; /**************************************************** ************** PROXY MESSAGE HANDLER *************** ****************************************************/ module.WebRTCClient.prototype.handleMessage = function (message) { var self = this; try { //module.logger.log("Handle message: " + message.content); message.content = de.modima.communication.parseSIP(message.content); if (message.content.method) { /*** SIP REQUEST HANDLING ***/ this.handleSIPRequest(message); } else if (message.content.status) { /*** SIP RESPONSE HANDLING ***/ this.handleSIPResponse(message); } else { this.onError("Unexpected SIP message: " + JSON.stringify(message), true); } } catch (e) { this.onError(e); return; } }; /**************************************************** *********************** HANGUP ********************* ****************************************************/ module.WebRTCClient.prototype.hangup = function () { module.logger.info("client hangup"); this.createSIPRequest("BYE", true); this.destroyRemoteAudio(); this.destroyLocalAudio(); this.resetPeerConnection(); this.session = null; if (this.onDisconnect) this.onDisconnect() }; module.WebRTCClient.prototype.onRemoteHangup = function () { module.logger.info("remote peer hangup"); this.destroyRemoteAudio(); this.destroyLocalAudio(); this.resetPeerConnection(); this.session = null; if (this.onDisconnect) this.onDisconnect() }; /**************************************************** ******************** SIP HANDLING ****************** ****************************************************/ module.WebRTCClient.prototype.sendMessage = function (msg, retry) { module.logger.log("Send message: " + msg.content); var transportMsg = { "type": "sip", "params": { "srcIp": this.session.transport["dstIp"], "srcPort": this.session.transport["dstPort"], "dstIp": this.session.transport["srcIp"], "dstPort": this.session.transport["srcPort"] }, "content": msg.content }; var msgId = this.endpoint.sendMessage(transportMsg, retry ? 5 : 1); /*********************************************************** ********* REMEMBER SIP REQUESTS FOR RETRY HANDLING ******** ***********************************************************/ if (msg.reqId) { // REQUEST this.sipRequests[msg.reqId] = msgId; } else if (msg.respId) { // RESPONSE this.sipResponses[msg.respId] = msgId; } }; module.WebRTCClient.prototype.createSIPRequest = function (method, retry) { if (!this.session) { return; } try { var msg = this.session.createSIPRequest(method); this.sendMessage(msg, retry); } catch (e) { var errMsg = e.msg || ""; e.msg = JSON.stringify(this.session.protocol) + " ### "; if (this.session.server && this.session.server.contact) { e.msg += JSON.stringify(this.session.server.contact) + " ### "; } e.msg += errMsg; this.onError(e); return; } }; module.WebRTCClient.prototype.createSIPResponse = function (status, retry) { try { if (!this.session) { return; } var msg = this.session.createSIPResponse(status); if (msg) this.sendMessage(msg, retry); else module.logger.log("omit sip response"); } catch (e) { module.logger.error("WEBRTC Error", e); module.logger.error("WEBRTC Error - Stack", e.stack) this.onError(e); return; } }; /**************************************************** *************** SIP REQUEST HANDLING *************** ****************************************************/ //var testCounter = 0 module.WebRTCClient.prototype.handleSIPRequest = function (message) { module.logger.log("Handle SIP Request ", message) var self = this; var newSession = false; if (!this.session) { newSession = true; } else { /****************************** * * * UPDATE SIP SESSION * * * ******************************/ var sessionUpdateResult = this.session.update(message); module.logger.log("[WebRTC] Session update result: " + sessionUpdateResult, this.session); switch (sessionUpdateResult) { /** MESSAGE WAS IGNORED (irrelevant) **/ case module.protocols.sip.SIP_UPDATE_IGNORE: { module.logger.log("SIP_UPDATE_IGNORE") return; // Nothing todo } break; /** NEW SESSION **/ case module.protocols.sip.SIP_UPDATE_NEW: { module.logger.log("SIP_UPDATE_NEW") // Eventually hangup old call this.hangup(); newSession = true; } break; /** MESSAGE RETRANSMISSION **/ case module.protocols.sip.SIP_UPDATE_RETRANS: { module.logger.log("SIP_UPDATE_RETRANS"); // Check if current session was eventually terminated if (this.session.state === module.protocols.sip.SIP_STATE_TERMINATED) { { // Eventually hangup old call this.hangup(); newSession = true; break; } } if (this.session.state >= module.protocols.sip.SIP_STATE_ANSWERED) { return; // Nothing todo } } break; /** SIP TRANSACTION ERROR **/ case module.protocols.sip.SIP_UPDATE_ERROR: this.onError("SIP SESSION ERROR: " + JSON.stringify(message)); // Restart WebRTC Client //window.webRTCClient.initialize(); return; } } if (newSession) { this.session = new module.protocols.sip.SIPSession(message); } var sip = message.content; /** HANDLE MESSAGE **/ switch (sip.method) { case "INVITE": { // Create peer connection with or without STUN based on Browser and Freeswitch version /* try { let fsVersion = sip.headers["user-agent"].split("/")[1]; if (this.userAgent === "firefox" && fsVersion > "1.10") { console.log("Use stun server") this.createPeerConnection(this.connectionConfig_STUN) } else { console.log("Omit stun server") this.createPeerConnection(this.connectionConfig_NO_STUN) } } catch (e) { console.error("Detect remote SIP UA version failed") console.log("Use stun server") this.createPeerConnection(this.connectionConfig_STUN) } */ // CHECK ICE if (!this.peerConnection || (this.peerConnection && this.peerConnection.iceConnectionState && this.peerConnection.iceConnectionState == "failed")) { this.onError("ICE FAILED --> RESTART CLIENT"); return; } var cbOnUserMediaGranted = function () { self.onMediaGranted(); if (!self.peerConnection || !self.peerConnection.remoteDescription || !self.peerConnection.remoteDescription.sdp) { // https request to dialer let dialerIP = self.session.transport.srcIp; if (self.dialerIPs[dialerIP]) { if (!self.peerConnection.remoteDescription) { self.setRemoteOffer({ "type": "offer", "sdp": self.session.server.sdp }); } return; } console.log("new dialer", dialerIP, self.dialerIPs); self.dialerIPs[dialerIP] = dialerIP; timeoutFetch("https://" + ipToPseudoHex(dialerIP) + ".voip.24dial.com:35000", { method: "POST", timeout: 2000 }) .catch(err => {}) .finally(() => { if (!self.peerConnection.remoteDescription) { self.setRemoteOffer({ "type": "offer", "sdp": self.session.server.sdp }); } }); } }; var cbOnUserMediaDenied = function () { module.logger.warn("access to user media denied"); /** 603 DECLINED **/ self.createSIPResponse(603, true); self.initialize(); }; self.doGetUserMedia(cbOnUserMediaGranted, cbOnUserMediaDenied, self.onUserMediaRequested); } break; case "BYE": { this.createSIPResponse(200, true); this.onRemoteHangup(); } break; case "CANCEL": { this.createSIPResponse(200, true); this.onRemoteHangup(); } break; case "ACK": { this.stopResponseRetry(message); } break; case "INFO": { this.createSIPResponse(200, true); } break; default: this.onError("Received unexpected SIP request: " + JSON.stringify(sip.method), true); } }; const hexMap = { "0": "a", "1": "b", "2": "c", "3": "d", "4": "e", "5": "f", "6": "g", "7": "h", "8": "i", "9": "j", "a": "k", "b": "l", "c": "m", "d": "n", "e": "o", "f": "p" } function ipToPseudoHex(ip) { let pseudoHex = ""; let splits = ip.split("."); for (let s of splits) { let hex = Number(s).toString(16); if (hex.length === 1) hex = "0" + hex; pseudoHex += hexMap[hex[0]] + hexMap[hex[1]]; } return pseudoHex } function timeoutFetch(url, options = {}) { const timeout = options && Number(options.timeout) || 2000 return new Promise((resolve, reject) => { try { fetch(url, options).then(resolve, reject) } catch (e) { console.log("fetch failed", e) } setTimeout(() => reject(), timeout) }) } /**************************************************** *************** SIP RESPONSE HANDLING ************** ****************************************************/ module.WebRTCClient.prototype.handleSIPResponse = function (message) { var sip = message.content; switch (sip.status) { case "200": this.stopRequestRetry(message); break; case "481": this.stopRequestRetry(message); break; default: this.onError("Received unexpected SIP response: " + JSON.stringify(sip.method), true); } }; /**************************************************** ********************* RETRY HANDLING *************** ****************************************************/ module.WebRTCClient.prototype.stopResponseRetry = function (request) { var sip = request.content; if (sip.headers && sip.headers["call-id"] && sip.headers.cseq && sip.headers.cseq.seq) { var respId = sip.headers["call-id"] + "___" + sip.headers.cseq.seq; module.logger.log("Stop response: ", respId); this.endpoint.stopRetry(this.sipResponses[respId]); delete this.sipResponses[respId]; } }; module.WebRTCClient.prototype.stopRequestRetry = function (response) { var sip = response.content; if (sip.headers && sip.headers["call-id"] && sip.headers.cseq && sip.headers.cseq.seq) { var reqId = sip.headers["call-id"] + "___" + sip.headers.cseq.seq; this.endpoint.stopRetry(this.sipRequests[reqId]); module.logger.log("Stop request: " + reqId); delete this.sipRequests[reqId]; } }; /******************************************************************** ************************** MEDIA HANDLING ************************** ********************************************************************/ module.WebRTCClient.prototype.doGetUserMedia = function (cbOnUserMediaGranted, cbOnUserMediaDenied, cbOnUserMediaRequested) { var self = this; console.log("doGetUserMedia request"); if (!this.webrtcsupport) { this.onError("WebRTCClient.doGetUserMedia: Browser does not support WebRTC!", true); return; } if (!this.peerConnection) { this.onError("WebRTCClient.doGetUserMedia: Peerconnection not found!"); return; } // Send client address to client this.signalReady(); cbOnUserMediaGranted = cbOnUserMediaGranted || this.onMediaGranted; cbOnUserMediaDenied = cbOnUserMediaDenied || this.onMediaDenied; cbOnUserMediaRequested = cbOnUserMediaRequested || this.onMediaRequested; function startStreamProcessing(stream, channel) { if (self.audioStreamPlugin) { console.log("[WEBRTC] audio stream plugin found --> process outgoing stream") self.audioStreamPlugin.startStreamProcessing(stream, channel) } else { console.log("[WEBRTC] NO audio stream plugin found (skip)") } } function onUserMediaSuccess(stream) { if (self.messagebus) self.messagebus.notify("GuidelineMatcher-events", JSON.stringify({ type: "media_success" })) console.log("[WEBRTC] onUserMediaSuccess"); startStreamProcessing(stream, 0); self.userMediaRequestPending = false; self.peerConnection.addStream(stream); self.localStream = stream; self.localStreamTrack = stream.getAudioTracks()[0]; cbOnUserMediaGranted(); } function onUserMediaError(err) { console.log("onUserMediaError " + err); self.userMediaRequestPending = false; cbOnUserMediaDenied(err); } /********************************************************* ***************** ACQUIRE MEDIA ACCESS ****************** ********************************************************/ if (this.localStream && this.localStream.active) { console.log("[WebRTC] media stream existing"); startStreamProcessing(this.localStream, 0); cbOnUserMediaGranted(); } else if (this.userMediaRequestPending) { console.log("[WebRTC] Media request pending. Wait for user action!"); cbOnUserMediaRequested(); } else { /** REQUEST ACCESS TO USER MEDIA **/ try { // stop all tracks console.log("stop all audio tracks") if (this.localStream) { this.localStream.getTracks().forEach(track => track.stop()); } /* NEW API */ module.logger.info("request access to user media") navigator.mediaDevices.getUserMedia(this.mediaConstraints) .then(onUserMediaSuccess) .catch(onUserMediaError); } catch (e) { try { /* DEPRECTED API */ module.logger.log("doGetUserMedia error: ", e) module.logger.log("request access to user media - via DEPRECATED API") navigator.getUserMedia(this.mediaConstraints, onUserMediaSuccess, onUserMediaError); this.userMediaRequestPending = true; cbOnUserMediaRequested(); } catch (e) { this.onError(e); return; } } } }; module.WebRTCClient.prototype.getSpeakerVolume = function () { if (this.remoteAudio) { return this.remoteAudio.volume } }; module.WebRTCClient.prototype.setSpeakerVolume = function (value) { if (this.remoteAudio) { this.remoteAudio.volume = value; } }; module.WebRTCClient.prototype.muteMicrophone = function () { if (this.localStream && this.localStream.active) { var audioTracks = this.localStream.getAudioTracks(); for (var i = 0, l = audioTracks.length; i < l; i++) { audioTracks[i].enabled = false; } } }; module.WebRTCClient.prototype.unMuteMicrophone = function (value) { if (this.localStream && this.localStream.active) { var audioTracks = this.localStream.getAudioTracks(); for (var i = 0, l = audioTracks.length; i < l; i++) { audioTracks[i].enabled = true; } } }; module.WebRTCClient.prototype.destroyLocalAudio = function (value) { try { if (this.localStreamTrack) { if (this.localStreamTrack.stop && typeof this.localStreamTrack.stop === "function") { this.localStreamTrack.stop(); } if (this.peerConnection.removeTrack && typeof this.peerConnection.removeTrack === "function") { this.peerConnection.removeTrack(this.localStreamTrack); } this.localStreamTrack = null; } if (this.localStream) { this.localStream = null; } } catch (e) {} }; module.WebRTCClient.prototype.destroyRemoteAudio = function (value) { if (this.remoteStream) { /** * Medienstrom wird vom Telefonieserver beendet. **/ /* //, this.remoteStream, this.remoteStream.stop, this.remoteStream.ended); if (this.remoteStream.stop && typeof this.remoteStream === "function") { module.logger.log("Stop inbound stream", this.peerConnection); this.remoteStream.stop(); } try { if (this.peerConnection.removeStream && typeof this.peerConnection.removeStream === "function") { this.peerConnection.removeStream(this.remoteStream); } } catch (e) {} */ // !!! DO NOT REMOVE THIS LINE this.remoteStream = null; } }; /******************************************************************** ***************************** SDP HANDLING ************************* ********************************************************************/ module.WebRTCClient.prototype.createLocalDescription = function () { var self = this; function onCreateSDPError(error) { self.onError("Failed to create local SDP. Cause: " + error.toString()); } function onSetSDPSuccess() { module.logger.info("SET LOCAL SDP - SUCCESS"); //self.sendAnswer(); } function onSetSDPError(err) { //self.onError("Failed to set local SDP. Cause: " + error.toString()); console.error(err) var errStr = "Set local SDP ERROR: "; errStr += err.msg ? err.msg + " ---\n\n" : ""; errStr += err.stack ? err.stack + " ---\n\n" : ""; if (self.peerConnection && self.peerConnection.localDescription && self.peerConnection.localDescription.sdp) { errStr += "Local Description already existing\n\n"; } if (self.session) { errStr += "Session Protocol: " + JSON.stringify(self.session.protocol) + " ---\n\n"; } //self.onError(errStr, true); module.logger.error(errStr); } function setLocalSDP(sessionDescription) { if (!self.session) { self.onError("Failed to set local SDP. Cause: No session"); return; } self.session.client.sdp = sessionDescription.sdp; module.logger.info("SET LOCAL SDP"); self.peerConnection.setLocalDescription(sessionDescription, onSetSDPSuccess, onSetSDPError); } module.logger.info("CREATE LOCAL SDP"); this.peerConnection.createAnswer(setLocalSDP, onCreateSDPError, this.sdpConstraints); }; module.WebRTCClient.prototype.setRemoteOffer = function (sessionDescription) { var self = this; function onSetRemoteSDPSuccess() { module.logger.info("SET REMOTE SDP - SUCCESS"); self.createLocalDescription(); } function onSetRemoteSDPError(err) { console.error(err) var errStr = "Set remote SDP ERROR: "; errStr += err.msg ? err.msg + " ---\n\n" : ""; errStr += err.stack ? err.stack + " ---\n\n" : ""; if (self.peerConnection && self.peerConnection.remoteDescription && self.peerConnection.remoteDescription.sdp) { errStr += "Remote Description already existing\n\n"; } if (self.session) { errStr += "Session Protocol: " + JSON.stringify(self.session.protocol) + " ---\n\n"; } module.logger.error(errStr); } console.log("setRemoteOffer", sessionDescription) sessionDescription.sdp = de.modima.communication.stringifySDP(sessionDescription.sdp); this.peerConnection.setRemoteDescription(new RTCSessionDescription(sessionDescription), onSetRemoteSDPSuccess, onSetRemoteSDPError); }; module.WebRTCClient.prototype.sendAnswer = function () { module.logger.info("SEND ANSWER"); // Session available ? if (!this.session) { this.onError("Failed to send answer. Cause: No SIP session."); return; } // Answer already send ? if (this.session.state >= module.protocols.sip.SIP_STATE_ANSWERED) { module.logger.info("INVITE was already answered - Skip answer"); return; } function gcd(count) { if (count == 0) return; this.getConnectionDetails() setTimeout(gcd.bind(this, --count), 100) } setTimeout(gcd.bind(this, 5), 100) this.session.client.sdp = this.peerConnection.localDescription.sdp; this.createSIPResponse(200, true); }; /******************************************************************** ****************** ENDPOINT INTERFACE IMPLEMENTATION *************** ********************************************************************/ module.WebRTCClient.prototype.getURI = function () { return this.endpoint.getURI("sip"); }; module.WebRTCClient.prototype.onEndpointReady = function () { if (!this.webrtcsupport) { return; } // Check init state if (this.peerConnection) { this.signalReady(); } else { this.initialize(); } }; module.WebRTCClient.prototype.retryReady = 10; module.WebRTCClient.prototype.signalReady = function () { var self = this; if (this.endpoint && this.endpoint.connected && this.webrtcsupport) { this.onReady(); } else if (this.retryReady > 0) { this.retryReady--; setTimeout(function () { self.signalReady(); }, 100); } else { this.endpoint.connect(); } }; /***************************************************************** ***************************** HELPER **************************** *****************************************************************/ function stringifySIP(sipObj) { if (sipObj.headers["cseq"]) sipObj.headers["cseq"] = module.protocols.sip.stringifyCSeq(sipObj.headers["cseq"]); if (sipObj.headers["from"]) sipObj.headers["from"] = module.protocols.sip.stringifyContact(sipObj.headers["from"]); if (sipObj.headers["to"]) sipObj.headers["to"] = module.protocols.sip.stringifyTo(sipObj.headers["to"]); if (sipObj.headers["contact"]) sipObj.headers["contact"] = module.protocols.sip.stringifyTo(sipObj.headers["contact"]); if (sipObj.headers["via"]) { var viaArr = []; if (sipObj.headers["via"] instanceof Array) { for (var i = 0; i < sipObj.headers["via"].length; i++) { viaArr.push(module.protocols.sip.stringifyVia(sipObj.headers["via"][i])); } } else { viaArr.push(module.protocols.sip.stringifyVia(sipObj.headers["via"])); } sipObj.headers["via"] = viaArr; } var sipStr = module.protocols.sip.format(sipObj); return sipStr; } function parseSIP(sipStr) { var sipObj = module.protocols.sip.parse(sipStr); if (sipObj.headers["cseq"]) sipObj.headers["cseq"] = module.protocols.sip.parseCSeq(sipObj.headers["cseq"]); if (sipObj.headers["from"]) sipObj.headers["from"] = module.protocols.sip.parseContact(sipObj.headers["from"]); if (sipObj.headers["to"]) sipObj.headers["to"] = module.protocols.sip.parseContact(sipObj.headers["to"]); if (sipObj.headers["contact"]) sipObj.headers["contact"] = module.protocols.sip.parseContact(sipObj.headers["contact"]); if (sipObj.headers["via"]) { var viaArr = [] if (sipObj.headers["via"] instanceof Array) { for (var i = 0; i < sipObj.headers["via"].length; i++) { var viaObj = module.protocols.sip.parseVia(sipObj.headers["via"][i]); if (!viaObj.params) { viaObj.params = {}; } viaArr.push(viaObj); } } else { viaArr.push(module.protocols.sip.parseVia(sipObj.headers["via"])); } sipObj.headers["via"] = viaArr; } return sipObj; } function stringifySDP(sdpObj) { var sdpStr = de.modima.communication.protocols.sdp.stringify(sdpObj); return sdpStr; } function parseSDP(sdpStr) { var sdpObj = de.modima.communication.protocols.sdp.parse(sdpStr); return sdpObj; } // Export module.stringifySIP = stringifySIP; module.parseSIP = parseSIP; module.stringifySDP = stringifySDP; module.parseSDP = parseSDP; }(de.modima.communication)); /**************************************************** ********************* SIP ************************** ****************************************************/ if (!de.modima.communication.protocols) de.modima.communication.protocols = {}; if (!de.modima.communication.protocols.sip) de.modima.communication.protocols.sip = {}; (function (module) { /*********************************** * SIP TRANSACTION UPDATE RESPONSES ***********************************/ module.SIP_UPDATE_OK = "SIP_UPDATE_OK"; // Transaction state update ok module.SIP_UPDATE_IGNORE = "SIP_UPDATE_IGNORE"; // Message was ignored (e.g. INFO messages for previous SIP sessions) module.SIP_UPDATE_NEW = "SIP_UPDATE_NEW"; // New all module.SIP_UPDATE_RETRANS = "SIP_UPDATE_RETRANS"; // Message was a retransmission --> nothing todo module.SIP_UPDATE_ERROR = "SIP_UPDATE_ERROR"; // Transaction state update error /******************************** * SIP TRANSACTION STATES ********************************/ module.SIP_STATE_NEW = 0; module.SIP_STATE_INVITED = 1; //var SIP_STATE_TRYING = 2; module.SIP_STATE_ANSWERED = 3; module.SIP_STATE_CONFIRMED = 4; //var SIP_STATE_CONNECTED = 5; module.SIP_STATE_TERMINATED = 6; /******************************** * GLOBAL CSEQ COUNTER ********************************/ var cseqCounter = 0; var callHistory = []; var sipResponseHistory = []; /**************************************************** ******************* SIP SESSION ******************** ****************************************************/ module.SIPSession = function (message) { this.protocol = []; this.state = module.SIP_STATE_NEW; this.transport = {}; this["call-id"] = ""; this.server = { "cseq": "", "via": "", "record-route": "", "from": "", "contact": { "url": "", "params": { "tag": "" } }, "sdp": "" }; this.client = { "cseq": cseqCounter++, "via": "", "contact": { "url": "", "params": { "tag": "" } }, "sdp": "" }; if (message) { this.update(message); } }; module.SIPSession.prototype.getState = function () { return { state: this.state, transport: this.transport, call_id: this["call-id"], server_config: this.server, client_config: this.client, protocol: this.protocol } } /***************************************************************** ******************* SIP SESSION STATE UPDATE ******************** *****************************************************************/ module.SIPSession.prototype.update = function (message) { if (!message.content.method) { return module.SIP_UPDATE_ERROR; } switch (message.content.method) { case "INVITE": { // remember call id var callId = message.content.headers["call-id"]; if (callHistory.length > 0 && callHistory[0] !== callId && callHistory.indexOf(callId) !== -1) { console.log("delayed invite received --> ignore call: " + callId); return module.SIP_UPDATE_IGNORE } else if (callHistory[0] !== callId) { callHistory.unshift(callId); callHistory = callHistory.splice(0, 5); console.log("update call history", JSON.stringify(callHistory)); } this.inviteCtr = this.inviteCtr || 0; console.log("INCREASE INVITE CTR", this.inviteCtr) // NEW CALL / EXISTING SESSION --> NEW SESSION if (this["call-id"] && this["call-id"] !== message.content.headers["call-id"]) { this.inviteCtr = 1; de.modima.communication.logger.log("SIP INVITE: NEW CALL ID"); return module.SIP_UPDATE_NEW; // REGULAR INVITE (NO SESSION) } else if (this.state < module.SIP_STATE_INVITED) { this.inviteCtr = 1; this.state = module.SIP_STATE_INVITED; break; // INVITE RETRANSMISSION --> IGNORE } else { if (this.inviteCtr++ === 6) { if (window.webRTCClient) { return module.SIP_UPDATE_ERROR; } } de.modima.communication.logger.log("SIP INVITE: RETRANSMISSION / IGNORE"); return module.SIP_UPDATE_RETRANS; } } break; case "ACK": { if (this.state > module.SIP_STATE_NEW && this.state < module.SIP_STATE_CONFIRMED) { this.state = module.SIP_STATE_CONFIRMED; } // DO NOT UPDATE SESSION ON ACKs return module.SIP_UPDATE_OK; } break; case "CANCEL": { // Check call-id if (this["call-id"] && this["call-id"] === message.content.headers["call-id"]) { if (this.state > module.SIP_STATE_NEW) { this.state = module.SIP_STATE_TERMINATED; } } } break; case "BYE": { // Check call-id if (this["call-id"] && this["call-id"] === message.content.headers["call-id"]) { if (this.state > module.SIP_STATE_NEW) { this.state = module.SIP_STATE_TERMINATED; } } } break; case "INFO": { // DO NOT UPDATE SESSION ON CALL-ID MISMATCH if (this["call-id"] && this["call-id"] !== message.content.headers["call-id"]) { return module.SIP_UPDATE_IGNORE; } } } this.transport = message.params; var sip = message.content; if (sip.headers["call-id"]) this["call-id"] = sip.headers["call-id"]; // Server state if (sip.headers["cseq"]) this.server["cseq"] = sip.headers["cseq"]; if (sip.headers["via"]) this.server["via"] = sip.headers["via"]; if (sip.headers["record-route"]) this.server["record-route"] = sip.headers["record-route"]; if (sip.headers["from"]) this.server["from"] = sip.headers["from"]; if (sip.headers["contact"]) this.server["contact"] = sip.headers["contact"]; // Client state if (sip.headers["to"]) { this.client["contact"] = sip.headers["to"]; // Create random tag if (!this.client["contact"].params.tag) this.client["contact"].params.tag = this.createTag(); } if (sip.headers["content-length"] > 0 && sip.headers["content-type"] == "application/sdp") { this.server.sdp = de.modima.communication.parseSDP(sip.body); // Modify SDP for compatibility with browser this.server.sdp["m"]["0"]["proto"] = "RTP/SAVPF"; this.server.sdp["m"]["0"]["a"].push("connection:new"); this.server.sdp["m"]["0"]["a"].push("mid:0"); var hasRTCPMUX = false; // Remove crypto attribute for (var i = 0; i < this.server.sdp["m"]["0"]["a"].length; i++) { if (this.server.sdp["m"]["0"]["a"][i].substr(0, 6) == "crypto") { this.server.sdp["m"]["0"]["a"].splice(i, 1); } else if (this.server.sdp["m"]["0"]["a"][i] == "rtcp-mux") { hasRTCPMUX = true; } } // Add rtcp-mux attribute if (!hasRTCPMUX) { this.server.sdp["m"]["0"]["a"].push("rtcp-mux"); } } // Session update successfull return module.SIP_UPDATE_OK; }; module.SIPSession.prototype.createTag = function () { var tag = ""; var charlist = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"; var tagLength = 36; for (var i = 0; i < tagLength; i++) { tag += charlist.charAt(Math.floor(Math.random() * 32)); } return tag; }; module.SIPSession.prototype.createSIPRequest = function (method) { var uri = this.server.contact.uri; var from = this.client.contact; var to = this.server.from; var callid = this["call-id"]; var via = this.server.via; var seqNumber = this.server.cseq.seq; // increment cseq (https://tools.ietf.org/html/rfc3261#section-12.2.1.1) if (method !== "ACK" && method !== "CANCEL") { seqNumber++; } var message = module.createMessage({ "method": method, "uri": uri, "headers": { "via": via, "from": from, "to": to, //"contact": contact, "call-id": callid, "cseq": { "method": method, "seq": seqNumber } } }); // Add RR parameter if existing if (this.server["record-route"]) { message.headers["Record-Route"] = this.server["record-route"]; } if (method == "BYE") { message.headers["Reason"] = "Q.850;cause=16"; } var reqId = callid + "___" + seqNumber + this.server["cseq"].method; var msgStr = de.modima.communication.stringifySIP(message) return { "reqId": reqId, "content": msgStr } }; module.SIPSession.prototype.createSIPResponse = function (status) { var from = this.server.from; var to = this.client.contact; var contact = this.client.contact; var callid = this["call-id"]; var via = this.server.via; var cseq = this.server["cseq"]; var message = module.createMessage({ "status": status, "headers": { "via": via, "from": from, "to": to, "contact": contact, "call-id": callid, "cseq": cseq } }); // Add RR parameter if existing if (this.server["record-route"]) { message.headers["Record-Route"] = this.server["record-route"]; } // Send local session description if (status === 200 && this.server["cseq"].method === "INVITE" && this.client.sdp) { sdp = de.modima.communication.protocols.sdp.parse(this.client.sdp); // Set connection IP if not existing // This is a workaround for a bug in FF 35 / 36 // --> Asterisk needs the connection IP for DTLS Handshake (Server Hello) if (sdp["o"]["address"] == "0.0.0.0") { if (this.transport && this.transport.dstIp) { de.modima.communication.logger.info("Public IP of this client is 0.0.0.0 --> Replace with detected IP from WH: " + this.transport.dstIp); sdp["o"]["address"] = this.transport.dstIp; } } if (sdp["m"]["0"]["c"]["address"] == "0.0.0.0") { if (this.transport && this.transport.dstIp) { de.modima.communication.logger.info("Public IP of this client is 0.0.0.0 --> Replace with detected IP from WH: " + this.transport.dstIp); sdp["m"]["0"]["c"]["address"] = this.transport.dstIp; } } // Modify proto property that Asterisk accepts it sdp["m"]["0"]["proto"] = "UDP/TLS/RTP/SAVPF"; // Delete ssrc attributes to reduce message size var mediaAttributes = sdp["m"]["0"]["a"]; for (var a = 0; a < mediaAttributes.length; a++) { // Versuch eine Samplelänge von 20ms zu erzwingen if (mediaAttributes[a].indexOf("fmtp:") == 0) { mediaAttributes[a] = mediaAttributes[a].replace("minptime=10", "minptime=20;maxptime=20;ptime=20") } if (mediaAttributes[a].indexOf("ssrc:") == 0) { delete mediaAttributes[a]; } } // Add ice candidate // - Workaround to avoid ICE Gathering via STUN // - Works with FreeSWITCH-mod_sofia/1.6.13 or higher // - Freeswitch just needs ANY valid public IPv4 and ANY port to start the ICE Connection process with the client // see: https://tools.ietf.org/html/rfc5245#section-15.1 // If the candidate is a host candidate, and MUST be omitted // // Syntax: candidate: [rel-addr] [rel-port] // RTP Candidate (IP of WH1) mediaAttributes.push("candidate:0759429117 1 udp 659136 185.61.149.132 1 typ host"); // RTCP Candidate (IP of WH1) mediaAttributes.push("candidate:0759429117 2 udp 659136 185.61.149.132 1 typ host"); sdp["m"]["0"]["a"] = mediaAttributes; // MOVE global defined ICE attributes (ice-ufrag, ice-pwd, fingerprint) to media level since Asterisk needs them there for (var a in sdp["a"]) { sdp["m"]["0"]["a"].push(sdp["a"][a]); } delete sdp["a"]; sdp = de.modima.communication.protocols.sdp.stringify(sdp); message.headers["content-type"] = "application/sdp"; message.headers["content-length"] = sdp.length; message.body = sdp; // SET SESSION STATE TO ANSWERED this.state = module.SIP_STATE_ANSWERED; de.modima.communication.logger.log("SET SIP STATE: " + this.state); } var respId = this["call-id"] + "___" + this.server["cseq"].seq; var msgStr = de.modima.communication.stringifySIP(message); // response deduplication let cseqStr = this.server["cseq"].seq + this.server["cseq"].method; if (sipResponseHistory.length > 0 && sipResponseHistory.indexOf(cseqStr) !== -1) { return; } else { sipResponseHistory.unshift(cseqStr) sipResponseHistory = sipResponseHistory.splice(0, 5); } console.log("response history", JSON.stringify(sipResponseHistory), cseqStr, this.server["cseq"].seq + this.server["cseq"].method) return { "respId": respId, "content": msgStr } }; /**************************************************** ******************* SIP PARSER ********************* ****************************************************/ // ## Token Constants var CR = '\r'; var LF = '\n'; var CRLF = CR + LF; var HTAB = '\t'; var SPACE = ' '; var DOT = '.'; var COMMA = ','; var SEMI = ';'; var COLON = ':'; var EQUAL = '='; var DQUOT = '"'; var QUOT = '\''; var DASH = '-'; var AMPERSAND = '&'; var QMARK = '?'; var EMPTY = ''; // ## SIP Constants // Currently supported protocol version. var SIP_VERSION = '2.0'; // SIP messages types. var SIP_REQUEST = 1; var SIP_RESPONSE = 2; // SIP transaction states. var SIP_STATE_CALLING = 1; var SIP_STATE_TRYING = 2; var SIP_STATE_PROCEEDING = 3; var SIP_STATE_COMPLETED = 4; var SIP_STATE_CONFIRMED = 5; var SIP_STATE_TERMINATED = 6; // SIP Timers in miliseconds. var SIP_T1 = 500; var SIP_T2 = 4 * 1000; var SIP_T4 = 5 * 1000; var SIP_TIMER_A = SIP_T1; var SIP_TIMER_B = 64 * SIP_T1; var SIP_TIMER_C = 60 * 3 * 1000; var SIP_TIMER_D = 32 * 1000; var SIP_TIMER_E = SIP_T1; var SIP_TIMER_F = 64 * SIP_T1; var SIP_TIMER_G = SIP_T1; var SIP_TIMER_H = 64 * SIP_T1; var SIP_TIMER_I = SIP_T4; var SIP_TIMER_J = 64 * SIP_T1; var SIP_TIMER_K = SIP_T4; // SIP methods defined in *RFC 3261*, *RFC 3262*, *RFC 3265*, // *RFC 3428*, *RFC 3515* and *RFC 3856*. var SIP_ACK = 'ACK'; var SIP_BYE = 'BYE'; var SIP_CANCEL = 'CANCEL'; var SIP_INVITE = 'INVITE'; var SIP_MESSAGE = 'MESSAGE'; var SIP_NOTIFY = 'NOTIFY'; var SIP_OPTIONS = 'OPTIONS'; var SIP_PRACK = 'PRACK'; var SIP_PUBLISH = 'PUBLISH'; var SIP_REFER = 'REFER'; var SIP_REGISTER = 'REGISTER'; var SIP_SUBSCRIBE = 'SUBSCRIBE'; var SIP_INFO = 'INFO'; /** Added by Marko Seidenglanz **/ // ### Defined response status codes var SIP_STATUS = { // 1xx - Provisional status codes 100: 'Trying', 180: 'Ringing', 181: 'Call Is Being Forwarded', 182: 'Queued', 183: 'Session Progress', // 2xx - Success status codes 200: 'OK', // 3xx - Redirection status codes 300: 'Multiple Choises', 301: 'Moved Permanently', 302: 'Moved Temporarily', 305: 'Use Proxy', 380: 'Alternative Service', // 4xx - Client error status codes 400: 'Bad Request', 401: 'Unauthorized', 402: 'Payment Required', 403: 'Forbidden', 404: 'Not Found', 405: 'Method Not Allowed', 406: 'Not Acceptable', 407: 'Proxy Authentication Required', 408: 'Request Timeout', 410: 'Gone', 413: 'Request Entity Too Large', 414: 'Request-URI to Long', 415: 'Unsupported Media Type', 416: 'Unsupported URI Scheme', 420: 'Bad Extension', 421: 'Extension Required', 423: 'Interval Too Brief', 480: 'Temporarily Unavailable', 481: 'Call/Transaction Does Not Exist', 482: 'Loop Detected', 483: 'Too Many Hops', 484: 'Address Incomplete', 485: 'Ambiguous', 486: 'Busy Here', 487: 'Request Terminated', 488: 'Not Acceptable Here', 491: 'Request Pending', 493: 'Undecipherable', // 5xx - Server Error status codes 500: 'Server Internal Error', 501: 'Not Implemented', 502: 'Bad Gateway', 503: 'Service Unavailable', 504: 'Server Time-out', 505: 'Version Not Supported', 513: 'Message Too Large', // 6xx - Global failure status codes 600: 'Busy Everywhere', 603: 'Decline', 604: 'Does Not Exist Anywhere', 606: 'Not Acceptable' }; // ### Compact Form // Header names can be replace with shorter values. var SIP_COMPACT_VALUES = { 'content-type': 'c', 'content-encoding': 'e', 'from': 'f', 'call-id': 'i', 'supported': 'k', 'content-length': 'l', 'contact': 'm', 'subject': 's', 'to': 't', 'via': 'v' }; var SIP_COMPACT_HEADERS = { 'c': 'content-type', 'e': 'content-encoding', 'f': 'from', 'i': 'call-id', 'k': 'supported', 'l': 'content-length', 'm': 'contact', 's': 'subject', 't': 'to', 'v': 'via' }; // Predefined formated header names. This list only defines header // names that cannot be transformed to original form from lower case // string. Other header names are added during runtime. var sip_headers = { 'call-id': 'Call-ID', 'cseq': 'CSeq', 'mime-version': 'MIME-Version', 'rack': 'RAck', 'www-authenticate': 'WWW-Authenticate' }; // ## Custom methods // This method returns byte length for UTF8 strings. String.prototype.lengthUTF8 = function () { var m = encodeURIComponent(this).match(/%[89ABab]/g); return this.length + (m ? m.length : 0); }; // ## Helpers // Checks if object is instance of some class. // // Example: // // is(someObj, Message); /** * @param {*} obj Object to check. * @param {(function()|Object)} name Class object. * @return {boolean} */ function is(obj, name) { return (obj instanceof name); } // Checks if object is instance of JavaScript Object. // // Example: // // // same as is(someObj, Object); // isObject(someObj); /** * @param {*} obj Object to check. * @return {boolean} */ function isObject(obj) { return is(obj, Object); } // Checks if object is instance of JavaScript Array. // // Example: // // // same as is(someArr, Array); // isArray(someArr); /** * @param {*} obj Object to check. * @return {boolean} */ function isArray(obj) { return is(obj, Array); } // Deep clone of an object. // // Example: // // clone(msg, copy); /** * @param {Object} from Source object. * @param {Object} to Target object. */ function clone(from, to) { var attr, type, _attr; for (_attr in from) { if (from.hasOwnProperty(_attr)) { attr = from[_attr]; type = typeof attr; if (type === 'function') { continue; } if (attr !== null && type === 'object') { to[_attr] = (isArray(attr)) ? [] : {}; clone(attr, to[_attr]); } else { to[_attr] = attr; } } } } // Split header values from string to array. SIP parser uses this // function to get values from header. Values are seperated with // comma character - quoted string are ignored. // // For example *Record-Route* header value // // ", " // // is parsed to array // // [ "", "" ] /** * @param {string} value Header value. * @return {Array.} Array of header values. */ function splitHeaderValues(value) { if (value.indexOf(COMMA) === -1) { return [value]; } var values = []; var i = 0; var start = 0; var len = value.length; var inQuote = false; var c; while (i < len) { c = value.charAt(i); i += 1; if (c === DQUOT) { inQuote = !inQuote; } if (!inQuote && c === COMMA) { values.push(value.substr(start, i - start - 1)); start = i; } } values.push(value.substr(start, len)); return values; } // ## SIP Parser // // SIP parser is state machine that traverse message in main loop - byte // by byte. // Initialize new parser for parsing SIP messages. /** * @return {Function} Function for parsing messages. */ function initParser() { var i; var len; var data; // States of parser's state machine. var state_req_or_res = 0; var state_start_req = 1; var state_start_res = 2; var state_res_s = 3; var state_res_si = 4; var state_res_sip = 5; var state_req_method = 6; var state_req_uri = 7; var state_res_status = 8; var state_res_reason = 9; var state_msg_version = 10; var state_header_start = 11; var state_header_value = 12; var state_start_body = 13; var state_msg_end = 14; // List of headers that can hold more that one value. var multipleValueHeader = { 'contact': true, 'record-route': true, 'route': true, 'via': true }; // Character is pushed back to main loop. function push() { i -= 1; } // Character is pulled from main loop. function pull() { i += 1; return data.charAt(i); } // Main loop is reset. function reset() { i = -1; } // Throws parse error. function error(text) { i = len; throw new Error(text); } // Read characters until delimiter is found. function read_until(delimit) { if (!delimit) { return data.substr(i); } var start = i; var end = data.length; var c; while (i < len) { c = data.charAt(i); end = i; if (c === delimit) { break; } i += 1; } return data.substr(start, end - start); } // Main parser function. function _parse(raw) { var message = { headers: {}, body: EMPTY }; var state = state_req_or_res; var method, uri, version; var status, reason; var headers = {}; var header_name, header_value; var type; var header_multiline; var c; var _method, isReq, _, _values, next, _i, body; data = raw; len = data.length; i = -1; // Main loop read characters from message. Characters are pulled // and pushed back to loop. while (i < len) { i += 1; c = data.charAt(i); switch (state) { // This is initial state where figure out if parsing request or response. case state_req_or_res: if (c === CR || c === LF) { break; } state = (c === 'S') ? state_start_res : state_start_req; push(); break; // Message type and method is set. Method is set based on first // character. Method value is checked in *state_req_method* state. case state_start_req: type = SIP_REQUEST; switch (c) { case 'I': /** Changed by Marko Seidenglanz **/ if (data.charAt(i + 2) == "F") { method = SIP_INFO; } else if (data.charAt(i + 2) == "V") { method = SIP_INVITE; } break; case 'A': method = SIP_ACK; break; case 'C': method = SIP_CANCEL; break; case 'B': method = SIP_BYE; break; case 'R': method = SIP_REGISTER; break; case 'O': method = SIP_OPTIONS; break; case 'P': method = SIP_PRACK; break; case 'S': method = SIP_SUBSCRIBE; break; case 'N': method = SIP_NOTIFY; break; case 'M': method = SIP_MESSAGE; break; default: error('Invalid request method'); break; } state = state_req_method; break; case state_start_res: if (c === 'S') { state = state_res_s; } break; case state_res_s: if (c === 'I') { if (!type) { type = SIP_RESPONSE; } state = state_res_si; } else if (c === 'U') { state = state_start_req; reset(); } else { error('Invalid method or status'); } break; case state_res_si: if (c === 'P') { state = state_res_sip; } else { error('Invalid message'); } break; case state_res_sip: if (c === '/') { state = state_msg_version; } else { error('Missing protocol version'); } break; case state_res_status: status = parseInt(read_until(SPACE), 10); if (!SIP_STATUS[status]) { error('Invalid message status code'); } state = state_res_reason; message.status = status + EMPTY; break; case state_res_reason: reason = read_until(CR); state = state_header_start; message.reason = reason; break; case state_req_method: if (method === SIP_PRACK && c === 'U') { method = SIP_PUBLISH; } else if (method === SIP_REGISTER) { if (c === 'E') { if (pull() === 'F') { method = SIP_REFER; } push(); } } push(); _method = read_until(SPACE); if (!method || !_method || method !== _method) { error('Invalid message header'); } state = state_req_uri; message.method = method; break; case state_req_uri: if (c === SPACE) { break; } if (c === 's') { uri = read_until(SPACE); state = state_start_res; message.uri = uri; } else { error('Invalid request URI'); } break; case state_msg_version: isReq = (type === SIP_REQUEST); version = read_until(isReq ? CR : SPACE); state = isReq ? state_header_start : state_res_status; message.version = version; break; case state_header_start: if (c === LF) { break; } if (c === SPACE || c === HTAB) { header_multiline = true; } else if (c === CR) { next = pull(); if (next === LF) { state = state_start_body; } break; } if (!header_multiline) { header_name = read_until(COLON).toLowerCase(); if (SIP_COMPACT_HEADERS[header_name]) { header_name = SIP_COMPACT_HEADERS[header_name]; } } state = state_header_value; break; case state_header_value: header_value = read_until(CR).trim(); if (!header_value) { state = state_header_start; break; } if (headers[header_name]) { if (!isArray(headers[header_name])) { if (!header_multiline) { _ = headers[header_name]; headers[header_name] = [_]; } } } if (header_multiline) { if (isArray(headers[header_name])) { _ = headers[header_name].pop(); _ += header_value; headers[header_name].push(_); } else { headers[header_name] += header_value; } header_multiline = false; } else { if (headers[header_name]) { headers[header_name].push(header_value); } else { headers[header_name] = header_value; } } if (multipleValueHeader[header_name]) { if (isArray(headers[header_name])) { _ = headers[header_name].pop(); _values = splitHeaderValues(_); for (_i in _values) { if (_values.hasOwnProperty(_i)) { headers[header_name].push(_values[_i].trim()); } } } else { _ = headers[header_name]; _values = splitHeaderValues(_); if (_values.length > 1) { headers[header_name] = []; for (_i in _values) { if (_values.hasOwnProperty(_i)) { headers[header_name].push(_values[_i].trim()); } } } } } state = state_header_start; pull(); break; case state_start_body: body = read_until(null); message.headers = headers || {}; message.body = body || EMPTY; state = state_msg_end; break; default: break; } } if (state !== state_msg_end) { error('Invalid message: ' + state); } return message; } return function (raw) { return _parse(raw); }; } // ## Value Parsers var uriRe = /^(\w+):([\w\-\.\!\~\*\'\(\)\&\=\+\$\,\;\?\/]+):?([\w\-\.\!\~\*\'\(\)\&\=\+\$\,]+)?@?([\w\-\.]+)?:?(\d+)?;?([\w=@;\.\-_]+)?\??([\S\s]+)?/; var viaRe = /SIP\/(\d\.\d)\/(\w+)\s+([\w\-\.]+):?(\d*)?;?(.*)?/; // *parseParameters* function parses parameter values from string. // // parseParameters('branch=rgfh374ny;received=192.168.1.102'); // // // result // { // branch: 'rgfh374ny', // received: '192.168.1.102' // } /** * @param {string} value * @param {(string|null)=} sep Character that seperates parameters. * @param {boolean=} lower Covert parameter value to lower case. * @return {Object} */ function parseParameters(value, sep, lower) { var c; var i = 0; var start = 0; var end; var len = value.length; var params = {}; var paramName; sep = sep || SEMI; function getValue() { return value.substr(start, end); } while (i <= len) { c = value.charAt(i); i += 1; if ((i === len && paramName) || c === sep) { end = i - start - (i === len && c !== sep ? 0 : 1); if (!paramName) { paramName = getValue(); params[paramName] = ''; } else { params[paramName] = lower ? getValue().toLowerCase() : getValue(); } paramName = ''; start = i; } else if (c === EQUAL) { end = i - start - 1; paramName = getValue().toLowerCase(); start = i; } else if (i === len) { end = i - start; paramName = getValue().toLowerCase(); params[paramName] = ''; } } return params; } // *formatParameters* function does the opposite operation as // *parseParameters*. // // formatParameters({branch:'rgfh374ny',received:'192.168.1.102'}); // // // result // 'branch=rgfh374ny;received=192.168.1.102' /** * @param {Object} params * @param {string} sep * @param {string} delimit * @return {string} */ function formatParameters(params, sep, delimit) { var _ = []; var p; for (p in params) { if (params.hasOwnProperty(p)) { _.push(p + EQUAL + encodeURI(params[p])); } } return _.length ? delimit + _.join(sep) : ''; } // Parser for *Via* header value. // // parseVia('SIP/2.0/TCP pc33.example.com:5060;branch=bb654vt3f'); // // // result // { // 'version': '2.0', // 'protocol': 'TCP', // 'host': 'pc33.example.com', // 'port': '5060', // 'params': { // 'branch': 'bb654vt3f' // } // } /** * @param {string} value * @return {(Object.|null)} */ function parseVia(value) { var match = viaRe.exec(value); if (!match) { return null; } var params = parseParameters(match[5]); var result = match ? { 'version': match[1], 'protocol': match[2], 'host': match[3], 'port': match[4], 'params': params || {} } : null; return result; } // Parser for *Contact* header value. // // parseContact('Bob ;rinstance=65bv4'); // // // result // { // 'name': 'Bob', // 'uri': 'sip:bob@biloxi.example.com', // 'params': { // 'rinstance': '65bv4' // } // } /** * @param {string} value * @return {(Object.)} */ function parseContact(value) { var _contact = value.split('>;'); var name = '', uri = '', params = {}; var _addr = _contact[0]; var _params = [], _paramData; if (_contact[1]) { _params = _contact[1].split(SEMI); } if (_addr.indexOf('sip') < 2) { uri = _addr.substr(1, _addr.length).replace('>', ''); } else { _addr = _addr.split(SPACE); name = _addr[0].trim(); uri = _addr[1].substr(1, _addr[1].length).replace('>', ''); } var i; for (i = 0; i < _params.length; i += 1) { _paramData = _params[i].split(EQUAL); params[_paramData[0]] = _paramData[1]; } return { 'name': name, 'uri': uri, 'params': params }; } // Parser for *CSeq* header value. // // parseCSeq('242 INVITE'); // // // result // { // 'seq': '242', // 'method': 'INVITE' // } /** * @param {string} value * @return {(Object.|null)} */ function parseCSeq(value) { var _data = value.split(SPACE); return { 'seq': _data[0], 'method': _data[1] }; } var parsers = { 'contact': parseContact, 'cseq': parseCSeq, 'from': parseContact, 'via': parseVia, 'to': parseContact }; // ## Value Stringifiers /** * @param {Object} value */ function stringifyParameters(params) { var value = ''; var key; for (key in params) { if (params.hasOwnProperty(key)) { value += params[key] ? ';' + key + '=' + params[key] : ';' + key; } } return value; } /** * @param {Object} value */ function stringifyContact(value) { var result = value.name + ' <' + value.uri + '>' + stringifyParameters(value['params']); return result } /** * @param {Object} value */ function stringifyCSeq(value) { return value['seq'] + ' ' + value.method; } /** * @param {Object} value */ function stringifyVia(value) { var s = 'SIP/' + value.version + '/' + value.protocol + ' ' + value.host; if (value.port) { s += ':' + value.port; } s += stringifyParameters(value['params']); return s; } var stringifiers = { 'contact': stringifyContact, 'cseq': stringifyCSeq, 'from': stringifyContact, 'via': stringifyVia, 'to': stringifyContact }; // ## Message // // Message class represents SIP message which is similar to HTTP message. // Like in HTTP there are two types of messages - requests and responses. // Request is defined with *method* and *URI* value, and response is defined // with *status code* and *reason text*. Both types have *headers* and *body* // attributes. // // request = { // method: 'INVITE', // uri: 'sip:alice@example.org', // version: '2.0', // // headers: { ... }, // body: '' // } // // response = { // status: '200', // reason: 'OK', // version: '2.0', // // headers: { ... }, // body: '' // } // // Examples of creating new messages are described under section // [SIP.createMessage](#section-34). Supported methods and status codes // are defined under section [SIP Constants](#section-3). /** * @constructor * @param {(string|number|Object|Message|null)} arg1 SIP request method or response status or object. * @param {string=} arg2 Valid SIP URI or message status text. * @param {Object.=} headers SIP message headers. * @param {string=} body SIP message body content. */ function Message(arg1, arg2, headers, body) { if (is(arg1, Message)) { clone(arg1, this); return; } var exportArgs = isObject(arg1); var isResponse = (arg1 > 0); if (exportArgs) { var args = arg1; arg1 = args.method || args.status; isResponse = (arg1 > 0); if (isResponse) { arg2 = args.reason; } else { arg2 = args.uri; } headers = args.headers; body = args.body; } if (isResponse) { if (!SIP_STATUS[arg1]) { throw new TypeError('Invalid status code ' + arg1); } this.status = arg1; this.reason = arg2 || SIP_STATUS[arg1]; } else { if (!arg1) { throw new TypeError('Invalid message method'); } if (!arg2) { throw new TypeError('Invalid message URI'); } this.method = arg1; this.uri = arg2; } this.version = SIP_VERSION; this.headers = headers || {}; this.body = body || ''; } // ## Message.getHeader // // Fetch header value in string or object form. // // Notice - compact header names can be used, check // [compact headers](#section-15). // // Example: // // messsage.getHeader('to'); // // // get first value from via header // message.getHeader('via', false, 0); // // // get last value from via header // message.getHeader('via', false , -1); // // // get all values // message.getHeader('via'); // // Header value can be parsed to object. // // Example: // // message.getHeader('from', true); // // // result // { // 'name': 'Alice', // 'uri': 'sip:atlanta.example.com;transport=tcp', // 'params': { // 'tag': 'b7546u5e' // } // } /** * @param {string} name Header name. * @param {boolean} parse Return parsed header value. * @param {number=} pos Get header value from position. * @return {string} */ Message.prototype.getHeader = function (name, parse, pos) { name = name.toLowerCase(); if (SIP_COMPACT_HEADERS[name]) { name = SIP_COMPACT_HEADERS[name]; } var returnAll = (pos === undefined); var header = this.headers[name]; var multiHeader = isArray(header); if (pos < 0 && header) { pos += header.length; } else { pos = pos > 0 ? pos : 0; } var i; var value = (!returnAll && multiHeader) ? header[pos] : header; if (value && parse && parsers[name]) { if (multiHeader && returnAll) { var _values = []; for (i in value) { if (value.hasOwnProperty(i)) { _values.push(parsers[name](value[i])); } } value = _values; } else { value = parsers[name](value); } } return value || null; }; // ## Message.setHeader // // This method can be convenient for manipulating header values. Values // can be added, updated or removed. // // Notice - compact header names can be used, check // [compact headers](#section-15). // // Example: // // // add Contact header value // message.setHeader('contact', 'Bob '); // message.setHeader('t', 'Alice '); // // // add many Contact header values // message.setHeader('record-route', // ['', '=} headers SIP message headers. * @param {string=} body SIP message body content. * @return {Message} */ var createMessage = function (arg1, arg2, headers, body) { return new Message(arg1, arg2, headers, body); }; // ## Message.copy /** * @return {Message} */ Message.prototype.copy = function () { return createMessage(this); }; // ## SIP.format // // This function transforms *Message* object to raw message // which can be sent over the network. // // Example: // // SIP.format({ // method: 'MESSAGE', // uri: 'sip:alice@example.org', // body: 'Hello Alice!' // }); // // // result // 'MESSAGE sip:alice@example.org SIP/2.0 // ... (headers) // // Hello Alice!' /** * @param {(Object|Message)} msg Message object. * @param {boolean=} compact Format message with compact header names. * @return {string} */ function formatMessage(msg, compact) { var s = ''; if (msg.method) { s += msg.method + SPACE + msg.uri + ' SIP/' + msg.version + CRLF; } else { s += 'SIP/' + msg.version + SPACE + msg.status + SPACE + msg.reason + CRLF; } var h, i; var header, value, _; for (h in msg.headers) { if (msg.headers.hasOwnProperty(h)) { value = msg.headers[h]; if (compact && SIP_COMPACT_VALUES[h]) { header = SIP_COMPACT_VALUES[h]; } else { if (!sip_headers[h]) { header = []; _ = h.split(DASH); for (i in _) { if (_.hasOwnProperty(i)) { header.push(_[i].substr(0, 1).toUpperCase() + _[i].substr(1)); } } sip_headers[h] = header.join(DASH); } header = sip_headers[h]; } s += header + COLON + SPACE; // Stringify header objects /* if (isArray(value)) { for (var j = 0; j < value.length; j++) { if (stringifiers[h]) { value[j] = stringifiers[h](value[j]); } } } else { if (stringifiers[h]) { value = stringifiers[h](value); } } */ s += isArray(value) ? value.join(CRLF + SPACE + COMMA + SPACE) : value; s += CRLF; } } s += CRLF; if (msg.body) { s += msg.body; } return s; } // ## Message.format /** * @param {boolean} compact * @return {string} */ Message.prototype.format = function (compact) { return formatMessage(this, compact); }; // ## Message.toResponse /** * @param {string|number} status * @param {string=} reason * @return {Message} */ Message.prototype.toResponse = function (status, reason) { assert(this.method, 'Check message type'); assert(SIP_STATUS[status], 'Check status'); var msg = createMessage(this); delete msg.method; delete msg.uri; msg.status = status; msg.reason = reason || SIP_STATUS[status]; msg.body = ''; msg.setHeader('content-length', '0'); msg.setHeader('max-forwards', null); return msg; }; // ## Message.toRequest /** * @param {string} method * @param {string} uri * @return {Message} */ Message.prototype.toRequest = function (method, uri) { assert(this.status, 'Check message type'); assert(uriRe.exec(uri), 'Check URI'); var msg = createMessage(this); delete msg.status; delete msg.reason; msg.method = method; msg.uri = uri; msg.body = ''; msg.setHeader('content-length', '0'); msg.setHeader('max-forwards', 70); return msg; }; // ## SIP.isMessage // // Checks if object is instance of Message class and returns boolean value. /** * @param {*} obj Object to test against. * @return {boolean} */ function isMessage(obj) { return is(obj, Message); } // ## SIP.parseUri // // SIP URIs are not parsed during message parsing. Therefore, URIs // have to be parsed with function *parseUri**. // // Example: // // SIP.parseUri('sip:alice@atlanta.example.com;transport=udp'); // // // result // { // 'scheme': 'sip', // 'user': 'alice', // 'password': '', // 'hostname': 'atlanta.example.com', // 'port': '', // 'params': { // 'transport': 'udp' // }, // 'headers': {} // } /** * @param {string} value SIP uri value. * @param {boolean=} parse Parse parameters and headers into object. * @return {SIPCoreUri} */ function parseUri(value, parse) { var match = uriRe.exec(decodeURI(value)); if (!match) { return {}; } var uri = { 'scheme': (match[1] && match[1].toLowerCase()) || EMPTY, 'user': (match[4] && match[2]) || EMPTY, 'password': match[3] || EMPTY, 'hostname': (match[4] && match[4].toLowerCase()) || match[2] || EMPTY, 'port': match[5] || EMPTY, }; if (parse) { uri['params'] = (match[6] && parseParameters(match[6], null, true)) || {}; uri['headers'] = (match[7] && parseParameters(match[7], AMPERSAND)) || {}; } else { uri['params'] = match[6] || {}; uri['headers'] = match[7] || {}; } return uri; } // ## SIP.formatUri // // This function returns formatted URI object. // // Example: // // SIP.formatUri({ // 'scheme': 'sip', // 'user': 'alice', // 'hostname': 'atlanta.example.com', // 'params': { // 'transport': 'udp' // } // }) // // // result // 'sip:alice@atlanta.example.com;transport=udp' /** * @param {SIPCoreUri} uri URI object. * @return {string} */ function formatUri(uri) { var s = EMPTY; if (uri.scheme) { s += uri.scheme + COLON; } if (uri.user) { s += uri['user']; } if (uri.password) { s += COLON + uri['password']; } if (uri.hostname) { s += '@' + uri.hostname; } if (uri.port) { s += COLON + uri.port; } if (uri.params) { s += formatParameters(uri['params'], SEMI, SEMI); } if (uri.headers) { s += formatParameters(uri.headers, AMPERSAND, QMARK); } return s; } // ## SIP.parse var __parser; // This function parses SIP message into object. // // Example of parsing request: // // SIP.parse('INVITE sip:alice@atlanta.example.com SIP/2.0...'); // // // result // { // 'method': 'INVITE', // 'uri': 'sip:alice@atlanta.example.com', // 'version': '2.0', // 'headers': { ... }, // 'body': '' // } // // Example of parsing response: // // SIP.parse('SIP/2.0 200 OK...'); // // // result // { // 'status': '200', // 'reason': 'OK', // 'version': '2.0', // 'headers': { ... }, // 'body': '' // } /** * @param {string} raw * @return {Object} */ function parseMessage(raw) { if (!__parser) { __parser = initParser(); } return __parser(raw); } // ## Exports // // Exported functions - *parse*, *format*, *isMessage*, // *createMessage*, *parseUri* and *formatUri*. module.stringifyContact = stringifyContact; module.stringifyFrom = stringifyContact; module.stringifyTo = stringifyContact; module.stringifyCSeq = stringifyCSeq; module.stringifyVia = stringifyVia; module.formatParameters = formatParameters; module.parseContact = parseContact; module.parseFrom = parseContact; module.parseTo = parseContact; module.parseCSeq = parseCSeq; module.parseVia = parseVia; module.parseParameters = parseParameters; module.parse = parseMessage; module.isMessage = isMessage; module.createMessage = createMessage; module.format = formatMessage; module.parseUri = parseUri; module.formatUri = formatUri; }(de.modima.communication.protocols.sip)); /**************************************************** ************************ SDP *********************** ****************************************************/ if (!de.modima.communication.protocols.sdp) de.modima.communication.protocols.sdp = {}; (function (module) { var parsers = { o: function (o) { var t = o.split(/\s+/); return { username: t[0], id: t[1], version: t[2], nettype: t[3], addrtype: t[4], address: t[5] }; }, c: function (c) { var t = c.split(/\s+/); return { nettype: t[0], addrtype: t[1], address: t[2] }; }, m: function (m) { var t = /^(\w+) +(\d+)(?:\/(\d))? +(\S+) (\d+( +\d+)*)/.exec(m); return { media: t[1], port: +t[2], portnum: +(t[3] || 1), proto: t[4], fmt: t[5].split(/\s+/).map(function (x) { return +x; }) }; }, a: function (a) { return a; } }; module.parse = function (sdp) { var sdp = sdp.split(/\r\n/); var root = {}; var m; root.m = []; for (var i = 0; i < sdp.length; ++i) { var tmp = /^(\w)=(.*)/.exec(sdp[i]); if (tmp) { var c = (parsers[tmp[1]] || function (x) { return x; })(tmp[2]); switch (tmp[1]) { case 'm': if (m) root.m.push(m); m = c; break; case 'a': var o = (m || root); if (o.a === undefined) o.a = []; o.a.push(c); break; default: (m || root)[tmp[1]] = c; break; } } } if (m) root.m.push(m); return root; }; var stringifiers = { o: function (o) { return [o.username || '-', o.id, o.version, o.nettype || 'IN', o.addrtype || 'IP4', o.address].join(' '); }, c: function (c) { return [c.nettype || 'IN', c.addrtype || 'IP4', c.address].join(' '); }, m: function (m) { return [m.media || 'audio', m.port, m.proto || 'RTP/AVP', m.fmt.join(' ')].join(' '); } }; function stringifyParam(sdp, type, def) { if (sdp[type] !== undefined) { var stringifier = function (x) { return type + '=' + ((stringifiers[type] && stringifiers[type](x)) || x) + '\r\n'; }; if (Array.isArray(sdp[type])) return sdp[type].map(stringifier).join(''); return stringifier(sdp[type]); } if (def !== undefined) return type + '=' + def + '\r\n'; return ''; } module.stringify = function (sdp) { var s = ''; s += stringifyParam(sdp, 'v', 0); s += stringifyParam(sdp, 'o'); s += stringifyParam(sdp, 's', '-'); s += stringifyParam(sdp, 'i'); s += stringifyParam(sdp, 'u'); s += stringifyParam(sdp, 'e'); s += stringifyParam(sdp, 'p'); s += stringifyParam(sdp, 'c'); s += stringifyParam(sdp, 'b'); s += stringifyParam(sdp, 't', '0 0'); s += stringifyParam(sdp, 'r'); s += stringifyParam(sdp, 'z'); s += stringifyParam(sdp, 'k'); s += stringifyParam(sdp, 'a'); sdp.m.forEach(function (m) { s += stringifyParam({ m: m }, 'm'); s += stringifyParam(m, 'i'); s += stringifyParam(m, 'c'); s += stringifyParam(m, 'b'); s += stringifyParam(m, 'k'); s += stringifyParam(m, 'a'); }); return s; } }(de.modima.communication.protocols.sdp)); ;var de_modima_wormhole = 'wh6.24dial.com';