d8dae1ec90
Firefox doesnt fire the ended evt/onended callback for live video mediastreamtracks. That caused the stream storage to not run the cleanup procedure in some scenarios Manually emit the ended event which works with the onended callback when a track is stopped
4260 lines
150 KiB
JavaScript
Executable File
4260 lines
150 KiB
JavaScript
Executable File
(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.kurentoUtils = 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<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
|
|
var freeice = require('freeice');
|
|
var inherits = require('inherits');
|
|
var UAParser = require('ua-parser-js');
|
|
var uuid = require('uuid');
|
|
var hark = require('hark');
|
|
var EventEmitter = require('events').EventEmitter;
|
|
var recursive = require('merge').recursive.bind(undefined, true);
|
|
var sdpTranslator = require('sdp-translator');
|
|
var logger = window.Logger || console;
|
|
try {
|
|
require('kurento-browser-extensions');
|
|
} catch (error) {
|
|
if (typeof getScreenConstraints === 'undefined') {
|
|
logger.warn('screen sharing is not available');
|
|
getScreenConstraints = function getScreenConstraints(sendSource, callback) {
|
|
callback(new Error('This library is not enabled for screen sharing'));
|
|
};
|
|
}
|
|
}
|
|
var MEDIA_CONSTRAINTS = {
|
|
audio: true,
|
|
video: {
|
|
width: 640,
|
|
framerate: 15
|
|
}
|
|
};
|
|
var ua = window && window.navigator ? window.navigator.userAgent : '';
|
|
var parser = new UAParser(ua);
|
|
var browser = parser.getBrowser();
|
|
var usePlanB = false;
|
|
if (browser.name === 'Chrome' || browser.name === 'Chromium') {
|
|
logger.debug(browser.name + ': using SDP PlanB');
|
|
usePlanB = true;
|
|
}
|
|
function noop(error) {
|
|
if (error)
|
|
logger.error(error);
|
|
}
|
|
function trackStop(track) {
|
|
if (track && typeof track.stop === 'function' && track.readyState !== 'ended') {
|
|
track.stop();
|
|
// Manually emit the event as a safeguard; Firefox doesn't fire it when it
|
|
// should with live MediaStreamTracks...
|
|
const trackStoppedEvt = new MediaStreamTrackEvent('ended', { track });
|
|
track.dispatchEvent(trackStoppedEvt);
|
|
}
|
|
}
|
|
function streamStop(stream) {
|
|
let track = stream.track;
|
|
trackStop(track);
|
|
}
|
|
|
|
var dumpSDP = function (description) {
|
|
if (typeof description === 'undefined' || description === null) {
|
|
return '';
|
|
}
|
|
return 'type: ' + description.type + '\r\n' + description.sdp;
|
|
};
|
|
function bufferizeCandidates(pc, onerror) {
|
|
var candidatesQueue = [];
|
|
pc.addEventListener('signalingstatechange', function () {
|
|
if (this.signalingState === 'stable') {
|
|
while (candidatesQueue.length) {
|
|
var entry = candidatesQueue.shift();
|
|
this.addIceCandidate(entry.candidate, entry.callback, entry.callback);
|
|
}
|
|
}
|
|
});
|
|
return function (candidate, callback) {
|
|
callback = callback || onerror;
|
|
switch (pc.signalingState) {
|
|
case 'closed':
|
|
callback(new Error('PeerConnection object is closed'));
|
|
break;
|
|
case 'stable':
|
|
if (pc.remoteDescription) {
|
|
pc.addIceCandidate(candidate).then(callback).catch(err => {
|
|
callback(err);
|
|
});
|
|
break;
|
|
}
|
|
default:
|
|
candidatesQueue.push({
|
|
candidate: candidate,
|
|
callback: callback
|
|
});
|
|
}
|
|
};
|
|
}
|
|
function removeFIDFromOffer(sdp) {
|
|
var n = sdp.indexOf('a=ssrc-group:FID');
|
|
if (n > 0) {
|
|
return sdp.slice(0, n);
|
|
} else {
|
|
return sdp;
|
|
}
|
|
}
|
|
function getSimulcastInfo(videoStream) {
|
|
var videoTracks = videoStream.getVideoTracks();
|
|
if (!videoTracks.length) {
|
|
logger.warn('No video tracks available in the video stream');
|
|
return '';
|
|
}
|
|
var lines = [
|
|
'a=x-google-flag:conference',
|
|
'a=ssrc-group:SIM 1 2 3',
|
|
'a=ssrc:1 cname:localVideo',
|
|
'a=ssrc:1 msid:' + videoStream.id + ' ' + videoTracks[0].id,
|
|
'a=ssrc:1 mslabel:' + videoStream.id,
|
|
'a=ssrc:1 label:' + videoTracks[0].id,
|
|
'a=ssrc:2 cname:localVideo',
|
|
'a=ssrc:2 msid:' + videoStream.id + ' ' + videoTracks[0].id,
|
|
'a=ssrc:2 mslabel:' + videoStream.id,
|
|
'a=ssrc:2 label:' + videoTracks[0].id,
|
|
'a=ssrc:3 cname:localVideo',
|
|
'a=ssrc:3 msid:' + videoStream.id + ' ' + videoTracks[0].id,
|
|
'a=ssrc:3 mslabel:' + videoStream.id,
|
|
'a=ssrc:3 label:' + videoTracks[0].id
|
|
];
|
|
lines.push('');
|
|
return lines.join('\n');
|
|
}
|
|
function WebRtcPeer(mode, options, callback) {
|
|
if (!(this instanceof WebRtcPeer)) {
|
|
return new WebRtcPeer(mode, options, callback);
|
|
}
|
|
WebRtcPeer.super_.call(this);
|
|
if (options instanceof Function) {
|
|
callback = options;
|
|
options = undefined;
|
|
}
|
|
options = options || {};
|
|
callback = (callback || noop).bind(this);
|
|
var self = this;
|
|
const userAgent = window.navigator.userAgent.toLocaleLowerCase();
|
|
var localVideo = options.localVideo;
|
|
var remoteVideo = options.remoteVideo;
|
|
var videoStream = options.videoStream;
|
|
var audioStream = options.audioStream;
|
|
var mediaConstraints = options.mediaConstraints;
|
|
var connectionConstraints = options.connectionConstraints;
|
|
var pc = options.peerConnection;
|
|
var sendSource = options.sendSource || 'webcam';
|
|
var dataChannelConfig = options.dataChannelConfig;
|
|
var useDataChannels = options.dataChannels || false;
|
|
var dataChannel;
|
|
var guid = uuid.v4();
|
|
var configuration = recursive({ iceServers: freeice() }, options.configuration);
|
|
var onicecandidate = options.onicecandidate;
|
|
if (onicecandidate)
|
|
this.on('icecandidate', onicecandidate);
|
|
var oncandidategatheringdone = options.oncandidategatheringdone;
|
|
if (oncandidategatheringdone) {
|
|
this.on('candidategatheringdone', oncandidategatheringdone);
|
|
}
|
|
var simulcast = options.simulcast;
|
|
var multistream = options.multistream;
|
|
var interop = new sdpTranslator.Interop();
|
|
var candidatesQueueOut = [];
|
|
var candidategatheringdone = false;
|
|
Object.defineProperties(this, {
|
|
'peerConnection': {
|
|
get: function () {
|
|
return pc;
|
|
}
|
|
},
|
|
'id': {
|
|
value: options.id || guid,
|
|
writable: false
|
|
},
|
|
'remoteVideo': {
|
|
get: function () {
|
|
return remoteVideo;
|
|
}
|
|
},
|
|
'localVideo': {
|
|
get: function () {
|
|
return localVideo;
|
|
}
|
|
},
|
|
'dataChannel': {
|
|
get: function () {
|
|
return dataChannel;
|
|
}
|
|
},
|
|
'currentFrame': {
|
|
get: function () {
|
|
if (!remoteVideo)
|
|
return;
|
|
if (remoteVideo.readyState < remoteVideo.HAVE_CURRENT_DATA)
|
|
throw new Error('No video stream data available');
|
|
var canvas = document.createElement('canvas');
|
|
canvas.width = remoteVideo.videoWidth;
|
|
canvas.height = remoteVideo.videoHeight;
|
|
canvas.getContext('2d').drawImage(remoteVideo, 0, 0);
|
|
return canvas;
|
|
}
|
|
}
|
|
});
|
|
if (!pc) {
|
|
pc = new RTCPeerConnection(configuration);
|
|
|
|
//Add Transceiver for Webview on IOS
|
|
if ((userAgent.indexOf('iphone') > -1 || userAgent.indexOf('ipad') > -1) && userAgent.indexOf('safari') == -1) {
|
|
try {
|
|
pc.addTransceiver('video');
|
|
} catch(e) {}
|
|
}
|
|
|
|
if (useDataChannels && !dataChannel) {
|
|
var dcId = 'WebRtcPeer-' + self.id;
|
|
var dcOptions = undefined;
|
|
if (dataChannelConfig) {
|
|
dcId = dataChannelConfig.id || dcId;
|
|
dcOptions = dataChannelConfig.options;
|
|
}
|
|
dataChannel = pc.createDataChannel(dcId, dcOptions);
|
|
if (dataChannelConfig) {
|
|
dataChannel.onopen = dataChannelConfig.onopen;
|
|
dataChannel.onclose = dataChannelConfig.onclose;
|
|
dataChannel.onmessage = dataChannelConfig.onmessage;
|
|
dataChannel.onbufferedamountlow = dataChannelConfig.onbufferedamountlow;
|
|
dataChannel.onerror = dataChannelConfig.onerror || noop;
|
|
}
|
|
}
|
|
}
|
|
pc.addEventListener('icecandidate', function (event) {
|
|
var candidate = event.candidate;
|
|
if (EventEmitter.listenerCount(self, 'icecandidate') || EventEmitter.listenerCount(self, 'candidategatheringdone')) {
|
|
if (candidate) {
|
|
var cand;
|
|
if (multistream && usePlanB) {
|
|
cand = interop.candidateToUnifiedPlan(candidate);
|
|
} else {
|
|
cand = candidate;
|
|
}
|
|
self.emit('icecandidate', cand);
|
|
candidategatheringdone = false;
|
|
} else if (!candidategatheringdone) {
|
|
self.emit('candidategatheringdone');
|
|
candidategatheringdone = true;
|
|
}
|
|
} else if (!candidategatheringdone) {
|
|
candidatesQueueOut.push(candidate);
|
|
if (!candidate)
|
|
candidategatheringdone = true;
|
|
}
|
|
});
|
|
pc.onaddtrack = options.onaddstream;
|
|
pc.onnegotiationneeded = options.onnegotiationneeded;
|
|
this.on('newListener', function (event, listener) {
|
|
if (event === 'icecandidate' || event === 'candidategatheringdone') {
|
|
while (candidatesQueueOut.length) {
|
|
var candidate = candidatesQueueOut.shift();
|
|
if (!candidate === (event === 'candidategatheringdone')) {
|
|
listener(candidate);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
var addIceCandidate = bufferizeCandidates(pc);
|
|
this.addIceCandidate = function (iceCandidate, callback) {
|
|
var candidate;
|
|
if (multistream && usePlanB) {
|
|
candidate = interop.candidateToPlanB(iceCandidate);
|
|
} else {
|
|
candidate = new RTCIceCandidate(iceCandidate);
|
|
}
|
|
// logger.debug('Remote ICE candidate received', iceCandidate);
|
|
callback = (callback || noop).bind(this);
|
|
addIceCandidate(candidate, callback);
|
|
};
|
|
this.generateOffer = function (callback) {
|
|
callback = callback.bind(this);
|
|
const descriptionCallback = () => {
|
|
var localDescription = pc.localDescription;
|
|
// logger.debug('Local description set', localDescription.sdp);
|
|
if (multistream && usePlanB) {
|
|
localDescription = interop.toUnifiedPlan(localDescription);
|
|
logger.debug('offer::origPlanB->UnifiedPlan', dumpSDP(localDescription));
|
|
}
|
|
callback(null, localDescription.sdp, self.processAnswer.bind(self));
|
|
}
|
|
|
|
var offerAudio = true;
|
|
var offerVideo = true;
|
|
if (mediaConstraints) {
|
|
offerAudio = typeof mediaConstraints.audio === 'boolean' ? mediaConstraints.audio : true;
|
|
offerVideo = typeof mediaConstraints.video === 'boolean' ? mediaConstraints.video : true;
|
|
}
|
|
var browserDependantConstraints = {
|
|
offerToReceiveAudio: mode !== 'sendonly' && offerAudio,
|
|
offerToReceiveVideo: mode !== 'sendonly' && offerVideo
|
|
};
|
|
var constraints = browserDependantConstraints;
|
|
// logger.debug('constraints: ' + JSON.stringify(constraints));
|
|
pc.createOffer(constraints).then(function (offer) {
|
|
// logger.debug('Created SDP offer');
|
|
offer = mangleSdpToAddSimulcast(offer);
|
|
return pc.setLocalDescription(offer);
|
|
}).then(() => {
|
|
descriptionCallback();
|
|
}).catch(callback);
|
|
};
|
|
this.getLocalSessionDescriptor = function () {
|
|
return pc.localDescription;
|
|
};
|
|
this.getRemoteSessionDescriptor = function () {
|
|
return pc.remoteDescription;
|
|
};
|
|
function setRemoteVideo() {
|
|
if (remoteVideo) {
|
|
// TODO review the retry - prlanzarin 08/18
|
|
let played = false;
|
|
const MAX_RETRIES = 5;
|
|
let attempt = 0;
|
|
const playVideo = () => {
|
|
if (!played) {
|
|
if (attempt < MAX_RETRIES) {
|
|
remoteVideo.play()
|
|
.then(() => { remoteVideo.muted = false; played = true; attempt = 0;})
|
|
.catch(e => {
|
|
attempt++;
|
|
setTimeout(() => {
|
|
playVideo(remoteVideo);
|
|
}, 500);
|
|
});
|
|
} else {
|
|
const tagFailedEvent = new CustomEvent('mediaTagPlayFailed', { detail: { mediaTag: remoteVideo } });
|
|
window.dispatchEvent(tagFailedEvent);
|
|
}
|
|
}
|
|
}
|
|
|
|
let stream = self.getRemoteStream();
|
|
|
|
remoteVideo.oncanplaythrough = function() {
|
|
playVideo();
|
|
};
|
|
|
|
remoteVideo.pause();
|
|
remoteVideo.srcObject = stream;
|
|
remoteVideo.load();
|
|
logger.info('Remote URL:', remoteVideo.srcObject);
|
|
}
|
|
}
|
|
this.showLocalVideo = function () {
|
|
localVideo.srcObject = videoStream;
|
|
localVideo.muted = true;
|
|
};
|
|
this.send = function (data) {
|
|
if (dataChannel && dataChannel.readyState === 'open') {
|
|
dataChannel.send(data);
|
|
} else {
|
|
logger.warn('Trying to send data over a non-existing or closed data channel');
|
|
}
|
|
};
|
|
this.processAnswer = function (sdpAnswer, callback) {
|
|
callback = (callback || noop).bind(this);
|
|
var answer = new RTCSessionDescription({
|
|
type: 'answer',
|
|
sdp: sdpAnswer
|
|
});
|
|
if (multistream && usePlanB) {
|
|
var planBAnswer = interop.toPlanB(answer);
|
|
logger.debug('asnwer::planB', dumpSDP(planBAnswer));
|
|
answer = planBAnswer;
|
|
}
|
|
// logger.debug('SDP answer received, setting remote description');
|
|
if (pc.signalingState === 'closed') {
|
|
return callback('PeerConnection is closed');
|
|
}
|
|
pc.setRemoteDescription(answer).then(function () {
|
|
setRemoteVideo();
|
|
callback();
|
|
}).catch(callback);
|
|
};
|
|
this.processOffer = function (sdpOffer, callback) {
|
|
callback = callback.bind(this);
|
|
var offer = new RTCSessionDescription({
|
|
type: 'offer',
|
|
sdp: sdpOffer
|
|
});
|
|
if (multistream && usePlanB) {
|
|
var planBOffer = interop.toPlanB(offer);
|
|
logger.debug('offer::planB', dumpSDP(planBOffer));
|
|
offer = planBOffer;
|
|
}
|
|
// logger.debug('SDP offer received, setting remote description');
|
|
if (pc.signalingState === 'closed') {
|
|
return callback('PeerConnection is closed');
|
|
}
|
|
pc.setRemoteDescription(offer).then(function () {
|
|
return setRemoteVideo();
|
|
}).then(function () {
|
|
return pc.createAnswer();
|
|
}).then(function (answer) {
|
|
answer = mangleSdpToAddSimulcast(answer);
|
|
// logger.debug('Created SDP answer');
|
|
return pc.setLocalDescription(answer);
|
|
}).then(function () {
|
|
var localDescription = pc.localDescription;
|
|
if (multistream && usePlanB) {
|
|
localDescription = interop.toUnifiedPlan(localDescription);
|
|
logger.debug('answer::origPlanB->UnifiedPlan', dumpSDP(localDescription));
|
|
}
|
|
// logger.debug('Local description set', localDescription.sdp);
|
|
callback(null, localDescription.sdp);
|
|
}).catch(callback);
|
|
};
|
|
function mangleSdpToAddSimulcast(answer) {
|
|
if (simulcast) {
|
|
if (browser.name === 'Chrome' || browser.name === 'Chromium') {
|
|
logger.debug('Adding multicast info');
|
|
answer = new RTCSessionDescription({
|
|
'type': answer.type,
|
|
'sdp': removeFIDFromOffer(answer.sdp) + getSimulcastInfo(videoStream)
|
|
});
|
|
} else {
|
|
logger.warn('Simulcast is only available in Chrome browser.');
|
|
}
|
|
}
|
|
return answer;
|
|
}
|
|
function start() {
|
|
if (pc.signalingState === 'closed') {
|
|
callback('The peer connection object is in "closed" state. This is most likely due to an invocation of the dispose method before accepting in the dialogue');
|
|
}
|
|
if (videoStream && localVideo) {
|
|
self.showLocalVideo();
|
|
}
|
|
if (videoStream) {
|
|
if (typeof videoStream.getTracks === 'function'
|
|
&& typeof pc.addTrack === 'function') {
|
|
videoStream.getTracks().forEach(track => pc.addTrack(track, videoStream));
|
|
} else {
|
|
pc.addStream(videoStream);
|
|
}
|
|
}
|
|
if (audioStream) {
|
|
if (typeof audioStream.getTracks === 'function'
|
|
&& typeof pc.addTrack === 'function') {
|
|
audioStream.getTracks().forEach(track => pc.addTrack(track, audioStream));
|
|
} else {
|
|
pc.addStream(audioStream);
|
|
}
|
|
}
|
|
var browser = parser.getBrowser();
|
|
if (mode === 'sendonly' && (browser.name === 'Chrome' || browser.name === 'Chromium') && browser.major === 39) {
|
|
mode = 'sendrecv';
|
|
}
|
|
callback();
|
|
}
|
|
if (mode !== 'recvonly' && !videoStream && !audioStream) {
|
|
function getMedia(constraints) {
|
|
if (constraints === undefined) {
|
|
constraints = MEDIA_CONSTRAINTS;
|
|
}
|
|
navigator.mediaDevices.getUserMedia(constraints).then(function (stream) {
|
|
videoStream = stream;
|
|
start();
|
|
}).catch(callback);
|
|
}
|
|
if (sendSource === 'webcam') {
|
|
getMedia(mediaConstraints);
|
|
} else {
|
|
getScreenConstraints(sendSource, function (error, constraints_) {
|
|
if (error)
|
|
return callback(error);
|
|
constraints = [mediaConstraints];
|
|
constraints.unshift(constraints_);
|
|
let gDMCallback = function(stream) {
|
|
stream.getTracks()[0].applyConstraints(constraints[0].optional)
|
|
.then(() => {
|
|
videoStream = stream;
|
|
start();
|
|
}).catch(() => {
|
|
videoStream = stream;
|
|
start();
|
|
});
|
|
}
|
|
if (typeof navigator.getDisplayMedia === 'function') {
|
|
navigator.getDisplayMedia(recursive.apply(undefined, constraints))
|
|
.then(gDMCallback)
|
|
.catch(callback);
|
|
} else if (navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === 'function') {
|
|
navigator.mediaDevices.getDisplayMedia(recursive.apply(undefined, constraints))
|
|
.then(gDMCallback)
|
|
.catch(callback);
|
|
} else {
|
|
getMedia(recursive.apply(undefined, constraints));
|
|
}
|
|
}, guid);
|
|
}
|
|
} else {
|
|
setTimeout(start, 0);
|
|
}
|
|
this.on('_dispose', function () {
|
|
if (localVideo) {
|
|
localVideo.pause();
|
|
localVideo.src = '';
|
|
localVideo.load();
|
|
localVideo.muted = false;
|
|
}
|
|
if (remoteVideo) {
|
|
remoteVideo.pause();
|
|
remoteVideo.src = '';
|
|
remoteVideo.load();
|
|
}
|
|
self.removeAllListeners();
|
|
if (window.cancelChooseDesktopMedia !== undefined) {
|
|
window.cancelChooseDesktopMedia(guid);
|
|
}
|
|
});
|
|
}
|
|
inherits(WebRtcPeer, EventEmitter);
|
|
function createEnableDescriptor(type) {
|
|
var method = 'get' + type + 'Tracks';
|
|
return {
|
|
enumerable: true,
|
|
get: function () {
|
|
if (!this.peerConnection)
|
|
return;
|
|
|
|
var senders = this.peerConnection.getSenders();
|
|
if (!senders.length)
|
|
return;
|
|
|
|
senders.forEach(sender => {
|
|
let track = sender.track;
|
|
if (!track.enabled && track.kind === type) {
|
|
return false;
|
|
}
|
|
});
|
|
return true;
|
|
},
|
|
set: function (value) {
|
|
function trackSetEnable(track) {
|
|
track.enabled = value;
|
|
}
|
|
this.peerConnection.getSenders().forEach(function (stream) {
|
|
let track = stream.track;
|
|
if (track.kind === type) {
|
|
trackSetEnable(track);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
}
|
|
Object.defineProperties(WebRtcPeer.prototype, {
|
|
'enabled': {
|
|
enumerable: true,
|
|
get: function () {
|
|
return this.audioEnabled && this.videoEnabled;
|
|
},
|
|
set: function (value) {
|
|
this.audioEnabled = this.videoEnabled = value;
|
|
}
|
|
},
|
|
'audioEnabled': createEnableDescriptor('Audio'),
|
|
'videoEnabled': createEnableDescriptor('Video')
|
|
});
|
|
WebRtcPeer.prototype.getLocalStream = function () {
|
|
if (this.localStream) {
|
|
return this.localStream;
|
|
}
|
|
|
|
if (this.peerConnection) {
|
|
this.localStream = new MediaStream();
|
|
this.peerConnection.getSenders().forEach((ls) => {
|
|
let track = ls.track;
|
|
if (track && !track.muted) {
|
|
this.localStream.addTrack(track);
|
|
}
|
|
});
|
|
return this.localStream;
|
|
}
|
|
};
|
|
WebRtcPeer.prototype.getRemoteStream = function () {
|
|
if (this.remoteStream) {
|
|
return this.remoteStream;
|
|
}
|
|
|
|
if (this.peerConnection) {
|
|
this.remoteStream = new MediaStream();
|
|
this.peerConnection.getReceivers().forEach((rs) => {
|
|
let track = rs.track;
|
|
if (track) {
|
|
if (track.muted) {
|
|
track.onunmute = () => {
|
|
this.remoteStream.addTrack(track);
|
|
};
|
|
return;
|
|
}
|
|
this.remoteStream.addTrack(track);
|
|
}
|
|
});
|
|
return this.remoteStream;
|
|
}
|
|
};
|
|
WebRtcPeer.prototype.dispose = function () {
|
|
// logger.debug('Disposing WebRtcPeer');
|
|
var pc = this.peerConnection;
|
|
var dc = this.dataChannel;
|
|
try {
|
|
if (dc) {
|
|
if (dc.signalingState === 'closed')
|
|
return;
|
|
dc.close();
|
|
}
|
|
if (pc) {
|
|
if (pc.signalingState === 'closed')
|
|
return;
|
|
pc.getSenders().forEach(streamStop);
|
|
if (this.remoteStream) {
|
|
this.remoteStream = null;
|
|
}
|
|
if (this.localStream) {
|
|
this.localStream = null;
|
|
}
|
|
pc.close();
|
|
}
|
|
} catch (err) {
|
|
logger.warn('Exception disposing webrtc peer ' + err);
|
|
}
|
|
this.emit('_dispose');
|
|
};
|
|
function WebRtcPeerRecvonly(options, callback) {
|
|
if (!(this instanceof WebRtcPeerRecvonly)) {
|
|
return new WebRtcPeerRecvonly(options, callback);
|
|
}
|
|
WebRtcPeerRecvonly.super_.call(this, 'recvonly', options, callback);
|
|
}
|
|
inherits(WebRtcPeerRecvonly, WebRtcPeer);
|
|
function WebRtcPeerSendonly(options, callback) {
|
|
if (!(this instanceof WebRtcPeerSendonly)) {
|
|
return new WebRtcPeerSendonly(options, callback);
|
|
}
|
|
WebRtcPeerSendonly.super_.call(this, 'sendonly', options, callback);
|
|
}
|
|
inherits(WebRtcPeerSendonly, WebRtcPeer);
|
|
function WebRtcPeerSendrecv(options, callback) {
|
|
if (!(this instanceof WebRtcPeerSendrecv)) {
|
|
return new WebRtcPeerSendrecv(options, callback);
|
|
}
|
|
WebRtcPeerSendrecv.super_.call(this, 'sendrecv', options, callback);
|
|
}
|
|
inherits(WebRtcPeerSendrecv, WebRtcPeer);
|
|
function harkUtils(stream, options) {
|
|
return hark(stream, options);
|
|
}
|
|
exports.bufferizeCandidates = bufferizeCandidates;
|
|
exports.WebRtcPeerRecvonly = WebRtcPeerRecvonly;
|
|
exports.WebRtcPeerSendonly = WebRtcPeerSendonly;
|
|
exports.WebRtcPeerSendrecv = WebRtcPeerSendrecv;
|
|
exports.hark = harkUtils;
|
|
},{"events":4,"freeice":5,"hark":8,"inherits":9,"kurento-browser-extensions":10,"merge":11,"sdp-translator":18,"ua-parser-js":21,"uuid":23}],2:[function(require,module,exports){
|
|
if (window.addEventListener)
|
|
module.exports = require('./index');
|
|
},{"./index":3}],3:[function(require,module,exports){
|
|
var WebRtcPeer = require('./WebRtcPeer');
|
|
exports.WebRtcPeer = WebRtcPeer;
|
|
},{"./WebRtcPeer":1}],4:[function(require,module,exports){
|
|
// Copyright Joyent, Inc. and other Node contributors.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
|
// copy of this software and associated documentation files (the
|
|
// "Software"), to deal in the Software without restriction, including
|
|
// without limitation the rights to use, copy, modify, merge, publish,
|
|
// distribute, sublicense, and/or sell copies of the Software, and to permit
|
|
// persons to whom the Software is furnished to do so, subject to the
|
|
// following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included
|
|
// in all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
|
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
// USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
function EventEmitter() {
|
|
this._events = this._events || {};
|
|
this._maxListeners = this._maxListeners || undefined;
|
|
}
|
|
module.exports = EventEmitter;
|
|
|
|
// Backwards-compat with node 0.10.x
|
|
EventEmitter.EventEmitter = EventEmitter;
|
|
|
|
EventEmitter.prototype._events = undefined;
|
|
EventEmitter.prototype._maxListeners = undefined;
|
|
|
|
// By default EventEmitters will print a warning if more than 10 listeners are
|
|
// added to it. This is a useful default which helps finding memory leaks.
|
|
EventEmitter.defaultMaxListeners = 10;
|
|
|
|
// Obviously not all Emitters should be limited to 10. This function allows
|
|
// that to be increased. Set to zero for unlimited.
|
|
EventEmitter.prototype.setMaxListeners = function(n) {
|
|
if (!isNumber(n) || n < 0 || isNaN(n))
|
|
throw TypeError('n must be a positive number');
|
|
this._maxListeners = n;
|
|
return this;
|
|
};
|
|
|
|
EventEmitter.prototype.emit = function(type) {
|
|
var er, handler, len, args, i, listeners;
|
|
|
|
if (!this._events)
|
|
this._events = {};
|
|
|
|
// If there is no 'error' event listener then throw.
|
|
if (type === 'error') {
|
|
if (!this._events.error ||
|
|
(isObject(this._events.error) && !this._events.error.length)) {
|
|
er = arguments[1];
|
|
if (er instanceof Error) {
|
|
throw er; // Unhandled 'error' event
|
|
} else {
|
|
// At least give some kind of context to the user
|
|
var err = new Error('Uncaught, unspecified "error" event. (' + er + ')');
|
|
err.context = er;
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
|
|
handler = this._events[type];
|
|
|
|
if (isUndefined(handler))
|
|
return false;
|
|
|
|
if (isFunction(handler)) {
|
|
switch (arguments.length) {
|
|
// fast cases
|
|
case 1:
|
|
handler.call(this);
|
|
break;
|
|
case 2:
|
|
handler.call(this, arguments[1]);
|
|
break;
|
|
case 3:
|
|
handler.call(this, arguments[1], arguments[2]);
|
|
break;
|
|
// slower
|
|
default:
|
|
args = Array.prototype.slice.call(arguments, 1);
|
|
handler.apply(this, args);
|
|
}
|
|
} else if (isObject(handler)) {
|
|
args = Array.prototype.slice.call(arguments, 1);
|
|
listeners = handler.slice();
|
|
len = listeners.length;
|
|
for (i = 0; i < len; i++)
|
|
listeners[i].apply(this, args);
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
EventEmitter.prototype.addListener = function(type, listener) {
|
|
var m;
|
|
|
|
if (!isFunction(listener))
|
|
throw TypeError('listener must be a function');
|
|
|
|
if (!this._events)
|
|
this._events = {};
|
|
|
|
// To avoid recursion in the case that type === "newListener"! Before
|
|
// adding it to the listeners, first emit "newListener".
|
|
if (this._events.newListener)
|
|
this.emit('newListener', type,
|
|
isFunction(listener.listener) ?
|
|
listener.listener : listener);
|
|
|
|
if (!this._events[type])
|
|
// Optimize the case of one listener. Don't need the extra array object.
|
|
this._events[type] = listener;
|
|
else if (isObject(this._events[type]))
|
|
// If we've already got an array, just append.
|
|
this._events[type].push(listener);
|
|
else
|
|
// Adding the second element, need to change to array.
|
|
this._events[type] = [this._events[type], listener];
|
|
|
|
// Check for listener leak
|
|
if (isObject(this._events[type]) && !this._events[type].warned) {
|
|
if (!isUndefined(this._maxListeners)) {
|
|
m = this._maxListeners;
|
|
} else {
|
|
m = EventEmitter.defaultMaxListeners;
|
|
}
|
|
|
|
if (m && m > 0 && this._events[type].length > m) {
|
|
this._events[type].warned = true;
|
|
console.error('(node) warning: possible EventEmitter memory ' +
|
|
'leak detected. %d listeners added. ' +
|
|
'Use emitter.setMaxListeners() to increase limit.',
|
|
this._events[type].length);
|
|
if (typeof console.trace === 'function') {
|
|
// not supported in IE 10
|
|
console.trace();
|
|
}
|
|
}
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
|
|
|
|
EventEmitter.prototype.once = function(type, listener) {
|
|
if (!isFunction(listener))
|
|
throw TypeError('listener must be a function');
|
|
|
|
var fired = false;
|
|
|
|
function g() {
|
|
this.removeListener(type, g);
|
|
|
|
if (!fired) {
|
|
fired = true;
|
|
listener.apply(this, arguments);
|
|
}
|
|
}
|
|
|
|
g.listener = listener;
|
|
this.on(type, g);
|
|
|
|
return this;
|
|
};
|
|
|
|
// emits a 'removeListener' event iff the listener was removed
|
|
EventEmitter.prototype.removeListener = function(type, listener) {
|
|
var list, position, length, i;
|
|
|
|
if (!isFunction(listener))
|
|
throw TypeError('listener must be a function');
|
|
|
|
if (!this._events || !this._events[type])
|
|
return this;
|
|
|
|
list = this._events[type];
|
|
length = list.length;
|
|
position = -1;
|
|
|
|
if (list === listener ||
|
|
(isFunction(list.listener) && list.listener === listener)) {
|
|
delete this._events[type];
|
|
if (this._events.removeListener)
|
|
this.emit('removeListener', type, listener);
|
|
|
|
} else if (isObject(list)) {
|
|
for (i = length; i-- > 0;) {
|
|
if (list[i] === listener ||
|
|
(list[i].listener && list[i].listener === listener)) {
|
|
position = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (position < 0)
|
|
return this;
|
|
|
|
if (list.length === 1) {
|
|
list.length = 0;
|
|
delete this._events[type];
|
|
} else {
|
|
list.splice(position, 1);
|
|
}
|
|
|
|
if (this._events.removeListener)
|
|
this.emit('removeListener', type, listener);
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
EventEmitter.prototype.removeAllListeners = function(type) {
|
|
var key, listeners;
|
|
|
|
if (!this._events)
|
|
return this;
|
|
|
|
// not listening for removeListener, no need to emit
|
|
if (!this._events.removeListener) {
|
|
if (arguments.length === 0)
|
|
this._events = {};
|
|
else if (this._events[type])
|
|
delete this._events[type];
|
|
return this;
|
|
}
|
|
|
|
// emit removeListener for all listeners on all events
|
|
if (arguments.length === 0) {
|
|
for (key in this._events) {
|
|
if (key === 'removeListener') continue;
|
|
this.removeAllListeners(key);
|
|
}
|
|
this.removeAllListeners('removeListener');
|
|
this._events = {};
|
|
return this;
|
|
}
|
|
|
|
listeners = this._events[type];
|
|
|
|
if (isFunction(listeners)) {
|
|
this.removeListener(type, listeners);
|
|
} else if (listeners) {
|
|
// LIFO order
|
|
while (listeners.length)
|
|
this.removeListener(type, listeners[listeners.length - 1]);
|
|
}
|
|
delete this._events[type];
|
|
|
|
return this;
|
|
};
|
|
|
|
EventEmitter.prototype.listeners = function(type) {
|
|
var ret;
|
|
if (!this._events || !this._events[type])
|
|
ret = [];
|
|
else if (isFunction(this._events[type]))
|
|
ret = [this._events[type]];
|
|
else
|
|
ret = this._events[type].slice();
|
|
return ret;
|
|
};
|
|
|
|
EventEmitter.prototype.listenerCount = function(type) {
|
|
if (this._events) {
|
|
var evlistener = this._events[type];
|
|
|
|
if (isFunction(evlistener))
|
|
return 1;
|
|
else if (evlistener)
|
|
return evlistener.length;
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
EventEmitter.listenerCount = function(emitter, type) {
|
|
return emitter.listenerCount(type);
|
|
};
|
|
|
|
function isFunction(arg) {
|
|
return typeof arg === 'function';
|
|
}
|
|
|
|
function isNumber(arg) {
|
|
return typeof arg === 'number';
|
|
}
|
|
|
|
function isObject(arg) {
|
|
return typeof arg === 'object' && arg !== null;
|
|
}
|
|
|
|
function isUndefined(arg) {
|
|
return arg === void 0;
|
|
}
|
|
|
|
},{}],5:[function(require,module,exports){
|
|
/* jshint node: true */
|
|
'use strict';
|
|
|
|
var normalice = require('normalice');
|
|
|
|
/**
|
|
# freeice
|
|
|
|
The `freeice` module is a simple way of getting random STUN or TURN server
|
|
for your WebRTC application. The list of servers (just STUN at this stage)
|
|
were sourced from this [gist](https://gist.github.com/zziuni/3741933).
|
|
|
|
## Example Use
|
|
|
|
The following demonstrates how you can use `freeice` with
|
|
[rtc-quickconnect](https://github.com/rtc-io/rtc-quickconnect):
|
|
|
|
<<< examples/quickconnect.js
|
|
|
|
As the `freeice` module generates ice servers in a list compliant with the
|
|
WebRTC spec you will be able to use it with raw `RTCPeerConnection`
|
|
constructors and other WebRTC libraries.
|
|
|
|
## Hey, don't use my STUN/TURN server!
|
|
|
|
If for some reason your free STUN or TURN server ends up in the
|
|
list of servers ([stun](https://github.com/DamonOehlman/freeice/blob/master/stun.json) or
|
|
[turn](https://github.com/DamonOehlman/freeice/blob/master/turn.json))
|
|
that is used in this module, you can feel
|
|
free to open an issue on this repository and those servers will be removed
|
|
within 24 hours (or sooner). This is the quickest and probably the most
|
|
polite way to have something removed (and provides us some visibility
|
|
if someone opens a pull request requesting that a server is added).
|
|
|
|
## Please add my server!
|
|
|
|
If you have a server that you wish to add to the list, that's awesome! I'm
|
|
sure I speak on behalf of a whole pile of WebRTC developers who say thanks.
|
|
To get it into the list, feel free to either open a pull request or if you
|
|
find that process a bit daunting then just create an issue requesting
|
|
the addition of the server (make sure you provide all the details, and if
|
|
you have a Terms of Service then including that in the PR/issue would be
|
|
awesome).
|
|
|
|
## I know of a free server, can I add it?
|
|
|
|
Sure, if you do your homework and make sure it is ok to use (I'm currently
|
|
in the process of reviewing the terms of those STUN servers included from
|
|
the original list). If it's ok to go, then please see the previous entry
|
|
for how to add it.
|
|
|
|
## Current List of Servers
|
|
|
|
* current as at the time of last `README.md` file generation
|
|
|
|
### STUN
|
|
|
|
<<< stun.json
|
|
|
|
### TURN
|
|
|
|
<<< turn.json
|
|
|
|
**/
|
|
|
|
var freeice = module.exports = function(opts) {
|
|
// if a list of servers has been provided, then use it instead of defaults
|
|
var servers = {
|
|
stun: (opts || {}).stun || require('./stun.json'),
|
|
turn: (opts || {}).turn || require('./turn.json')
|
|
};
|
|
|
|
var stunCount = (opts || {}).stunCount || 2;
|
|
var turnCount = (opts || {}).turnCount || 0;
|
|
var selected;
|
|
|
|
function getServers(type, count) {
|
|
var out = [];
|
|
var input = [].concat(servers[type]);
|
|
var idx;
|
|
|
|
while (input.length && out.length < count) {
|
|
idx = (Math.random() * input.length) | 0;
|
|
out = out.concat(input.splice(idx, 1));
|
|
}
|
|
|
|
return out.map(function(url) {
|
|
//If it's a not a string, don't try to "normalice" it otherwise using type:url will screw it up
|
|
if ((typeof url !== 'string') && (! (url instanceof String))) {
|
|
return url;
|
|
} else {
|
|
return normalice(type + ':' + url);
|
|
}
|
|
});
|
|
}
|
|
|
|
// add stun servers
|
|
selected = [].concat(getServers('stun', stunCount));
|
|
|
|
if (turnCount) {
|
|
selected = selected.concat(getServers('turn', turnCount));
|
|
}
|
|
|
|
return selected;
|
|
};
|
|
|
|
},{"./stun.json":6,"./turn.json":7,"normalice":12}],6:[function(require,module,exports){
|
|
module.exports=[
|
|
]
|
|
|
|
},{}],7:[function(require,module,exports){
|
|
module.exports=[]
|
|
|
|
},{}],8:[function(require,module,exports){
|
|
var WildEmitter = require('wildemitter');
|
|
|
|
function getMaxVolume (analyser, fftBins) {
|
|
var maxVolume = -Infinity;
|
|
analyser.getFloatFrequencyData(fftBins);
|
|
|
|
for(var i=4, ii=fftBins.length; i < ii; i++) {
|
|
if (fftBins[i] > maxVolume && fftBins[i] < 0) {
|
|
maxVolume = fftBins[i];
|
|
}
|
|
};
|
|
|
|
return maxVolume;
|
|
}
|
|
|
|
|
|
var audioContextType = window.AudioContext || window.webkitAudioContext;
|
|
// use a single audio context due to hardware limits
|
|
var audioContext = null;
|
|
module.exports = function(stream, options) {
|
|
var harker = new WildEmitter();
|
|
|
|
|
|
// make it not break in non-supported browsers
|
|
if (!audioContextType) return harker;
|
|
|
|
//Config
|
|
var options = options || {},
|
|
smoothing = (options.smoothing || 0.1),
|
|
interval = (options.interval || 50),
|
|
threshold = options.threshold,
|
|
play = options.play,
|
|
history = options.history || 10,
|
|
running = true;
|
|
|
|
//Setup Audio Context
|
|
if (!audioContext) {
|
|
audioContext = new audioContextType();
|
|
}
|
|
var sourceNode, fftBins, analyser;
|
|
|
|
analyser = audioContext.createAnalyser();
|
|
analyser.fftSize = 512;
|
|
analyser.smoothingTimeConstant = smoothing;
|
|
fftBins = new Float32Array(analyser.fftSize);
|
|
|
|
if (stream.jquery) stream = stream[0];
|
|
if (stream instanceof HTMLAudioElement || stream instanceof HTMLVideoElement) {
|
|
//Audio Tag
|
|
sourceNode = audioContext.createMediaElementSource(stream);
|
|
if (typeof play === 'undefined') play = true;
|
|
threshold = threshold || -50;
|
|
} else {
|
|
//WebRTC Stream
|
|
sourceNode = audioContext.createMediaStreamSource(stream);
|
|
threshold = threshold || -50;
|
|
}
|
|
|
|
sourceNode.connect(analyser);
|
|
if (play) analyser.connect(audioContext.destination);
|
|
|
|
harker.speaking = false;
|
|
|
|
harker.setThreshold = function(t) {
|
|
threshold = t;
|
|
};
|
|
|
|
harker.setInterval = function(i) {
|
|
interval = i;
|
|
};
|
|
|
|
harker.stop = function() {
|
|
running = false;
|
|
harker.emit('volume_change', -100, threshold);
|
|
if (harker.speaking) {
|
|
harker.speaking = false;
|
|
harker.emit('stopped_speaking');
|
|
}
|
|
};
|
|
harker.speakingHistory = [];
|
|
for (var i = 0; i < history; i++) {
|
|
harker.speakingHistory.push(0);
|
|
}
|
|
|
|
// Poll the analyser node to determine if speaking
|
|
// and emit events if changed
|
|
var looper = function() {
|
|
setTimeout(function() {
|
|
|
|
//check if stop has been called
|
|
if(!running) {
|
|
return;
|
|
}
|
|
|
|
var currentVolume = getMaxVolume(analyser, fftBins);
|
|
|
|
harker.emit('volume_change', currentVolume, threshold);
|
|
|
|
var history = 0;
|
|
if (currentVolume > threshold && !harker.speaking) {
|
|
// trigger quickly, short history
|
|
for (var i = harker.speakingHistory.length - 3; i < harker.speakingHistory.length; i++) {
|
|
history += harker.speakingHistory[i];
|
|
}
|
|
if (history >= 2) {
|
|
harker.speaking = true;
|
|
harker.emit('speaking');
|
|
}
|
|
} else if (currentVolume < threshold && harker.speaking) {
|
|
for (var i = 0; i < harker.speakingHistory.length; i++) {
|
|
history += harker.speakingHistory[i];
|
|
}
|
|
if (history == 0) {
|
|
harker.speaking = false;
|
|
harker.emit('stopped_speaking');
|
|
}
|
|
}
|
|
harker.speakingHistory.shift();
|
|
harker.speakingHistory.push(0 + (currentVolume > threshold));
|
|
|
|
looper();
|
|
}, interval);
|
|
};
|
|
looper();
|
|
|
|
|
|
return harker;
|
|
}
|
|
|
|
},{"wildemitter":24}],9:[function(require,module,exports){
|
|
if (typeof Object.create === 'function') {
|
|
// implementation from standard node.js 'util' module
|
|
module.exports = function inherits(ctor, superCtor) {
|
|
ctor.super_ = superCtor
|
|
ctor.prototype = Object.create(superCtor.prototype, {
|
|
constructor: {
|
|
value: ctor,
|
|
enumerable: false,
|
|
writable: true,
|
|
configurable: true
|
|
}
|
|
});
|
|
};
|
|
} else {
|
|
// old school shim for old browsers
|
|
module.exports = function inherits(ctor, superCtor) {
|
|
ctor.super_ = superCtor
|
|
var TempCtor = function () {}
|
|
TempCtor.prototype = superCtor.prototype
|
|
ctor.prototype = new TempCtor()
|
|
ctor.prototype.constructor = ctor
|
|
}
|
|
}
|
|
|
|
},{}],10:[function(require,module,exports){
|
|
// Does nothing at all.
|
|
|
|
},{}],11:[function(require,module,exports){
|
|
/*!
|
|
* @name JavaScript/NodeJS Merge v2.1.1
|
|
* @author yeikos
|
|
* @repository https://github.com/yeikos/js.merge
|
|
|
|
* Copyright 2014 yeikos - MIT license
|
|
* https://raw.github.com/yeikos/js.merge/master/LICENSE
|
|
*/
|
|
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.isPlainObject = exports.clone = exports.recursive = exports.merge = exports.main = void 0;
|
|
module.exports = exports = main;
|
|
exports.default = main;
|
|
function main() {
|
|
var items = [];
|
|
for (var _i = 0; _i < arguments.length; _i++) {
|
|
items[_i] = arguments[_i];
|
|
}
|
|
return merge.apply(void 0, items);
|
|
}
|
|
exports.main = main;
|
|
main.clone = clone;
|
|
main.isPlainObject = isPlainObject;
|
|
main.recursive = recursive;
|
|
function merge() {
|
|
var items = [];
|
|
for (var _i = 0; _i < arguments.length; _i++) {
|
|
items[_i] = arguments[_i];
|
|
}
|
|
return _merge(items[0] === true, false, items);
|
|
}
|
|
exports.merge = merge;
|
|
function recursive() {
|
|
var items = [];
|
|
for (var _i = 0; _i < arguments.length; _i++) {
|
|
items[_i] = arguments[_i];
|
|
}
|
|
return _merge(items[0] === true, true, items);
|
|
}
|
|
exports.recursive = recursive;
|
|
function clone(input) {
|
|
if (Array.isArray(input)) {
|
|
var output = [];
|
|
for (var index = 0; index < input.length; ++index)
|
|
output.push(clone(input[index]));
|
|
return output;
|
|
}
|
|
else if (isPlainObject(input)) {
|
|
var output = {};
|
|
for (var index in input)
|
|
output[index] = clone(input[index]);
|
|
return output;
|
|
}
|
|
else {
|
|
return input;
|
|
}
|
|
}
|
|
exports.clone = clone;
|
|
function isPlainObject(input) {
|
|
return input && typeof input === 'object' && !Array.isArray(input);
|
|
}
|
|
exports.isPlainObject = isPlainObject;
|
|
function _recursiveMerge(base, extend) {
|
|
if (!isPlainObject(base))
|
|
return extend;
|
|
for (var key in extend) {
|
|
if (key === '__proto__' || key === 'constructor' || key === 'prototype')
|
|
continue;
|
|
base[key] = (isPlainObject(base[key]) && isPlainObject(extend[key])) ?
|
|
_recursiveMerge(base[key], extend[key]) :
|
|
extend[key];
|
|
}
|
|
return base;
|
|
}
|
|
function _merge(isClone, isRecursive, items) {
|
|
var result;
|
|
if (isClone || !isPlainObject(result = items.shift()))
|
|
result = {};
|
|
for (var index = 0; index < items.length; ++index) {
|
|
var item = items[index];
|
|
if (!isPlainObject(item))
|
|
continue;
|
|
for (var key in item) {
|
|
if (key === '__proto__' || key === 'constructor' || key === 'prototype')
|
|
continue;
|
|
var value = isClone ? clone(item[key]) : item[key];
|
|
result[key] = isRecursive ? _recursiveMerge(result[key], value) : value;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
},{}],12:[function(require,module,exports){
|
|
/**
|
|
# normalice
|
|
|
|
Normalize an ice server configuration object (or plain old string) into a format
|
|
that is usable in all browsers supporting WebRTC. Primarily this module is designed
|
|
to help with the transition of the `url` attribute of the configuration object to
|
|
the `urls` attribute.
|
|
|
|
## Example Usage
|
|
|
|
<<< examples/simple.js
|
|
|
|
**/
|
|
|
|
var protocols = [
|
|
'stun:',
|
|
'turn:'
|
|
];
|
|
|
|
module.exports = function(input) {
|
|
var url = (input || {}).url || input;
|
|
var protocol;
|
|
var parts;
|
|
var output = {};
|
|
|
|
// if we don't have a string url, then allow the input to passthrough
|
|
if (typeof url != 'string' && (! (url instanceof String))) {
|
|
return input;
|
|
}
|
|
|
|
// trim the url string, and convert to an array
|
|
url = url.trim();
|
|
|
|
// if the protocol is not known, then passthrough
|
|
protocol = protocols[protocols.indexOf(url.slice(0, 5))];
|
|
if (! protocol) {
|
|
return input;
|
|
}
|
|
|
|
// now let's attack the remaining url parts
|
|
url = url.slice(5);
|
|
parts = url.split('@');
|
|
|
|
output.username = input.username;
|
|
output.credential = input.credential;
|
|
// if we have an authentication part, then set the credentials
|
|
if (parts.length > 1) {
|
|
url = parts[1];
|
|
parts = parts[0].split(':');
|
|
|
|
// add the output credential and username
|
|
output.username = parts[0];
|
|
output.credential = (input || {}).credential || parts[1] || '';
|
|
}
|
|
|
|
output.url = protocol + url;
|
|
output.urls = [ output.url ];
|
|
|
|
return output;
|
|
};
|
|
|
|
},{}],13:[function(require,module,exports){
|
|
var grammar = module.exports = {
|
|
v: [{
|
|
name: 'version',
|
|
reg: /^(\d*)$/
|
|
}],
|
|
o: [{ //o=- 20518 0 IN IP4 203.0.113.1
|
|
// NB: sessionId will be a String in most cases because it is huge
|
|
name: 'origin',
|
|
reg: /^(\S*) (\d*) (\d*) (\S*) IP(\d) (\S*)/,
|
|
names: ['username', 'sessionId', 'sessionVersion', 'netType', 'ipVer', 'address'],
|
|
format: "%s %s %d %s IP%d %s"
|
|
}],
|
|
// default parsing of these only (though some of these feel outdated)
|
|
s: [{ name: 'name' }],
|
|
i: [{ name: 'description' }],
|
|
u: [{ name: 'uri' }],
|
|
e: [{ name: 'email' }],
|
|
p: [{ name: 'phone' }],
|
|
z: [{ name: 'timezones' }], // TODO: this one can actually be parsed properly..
|
|
r: [{ name: 'repeats' }], // TODO: this one can also be parsed properly
|
|
//k: [{}], // outdated thing ignored
|
|
t: [{ //t=0 0
|
|
name: 'timing',
|
|
reg: /^(\d*) (\d*)/,
|
|
names: ['start', 'stop'],
|
|
format: "%d %d"
|
|
}],
|
|
c: [{ //c=IN IP4 10.47.197.26
|
|
name: 'connection',
|
|
reg: /^IN IP(\d) (\S*)/,
|
|
names: ['version', 'ip'],
|
|
format: "IN IP%d %s"
|
|
}],
|
|
b: [{ //b=AS:4000
|
|
push: 'bandwidth',
|
|
reg: /^(TIAS|AS|CT|RR|RS):(\d*)/,
|
|
names: ['type', 'limit'],
|
|
format: "%s:%s"
|
|
}],
|
|
m: [{ //m=video 51744 RTP/AVP 126 97 98 34 31
|
|
// NB: special - pushes to session
|
|
// TODO: rtp/fmtp should be filtered by the payloads found here?
|
|
reg: /^(\w*) (\d*) ([\w\/]*)(?: (.*))?/,
|
|
names: ['type', 'port', 'protocol', 'payloads'],
|
|
format: "%s %d %s %s"
|
|
}],
|
|
a: [
|
|
{ //a=rtpmap:110 opus/48000/2
|
|
push: 'rtp',
|
|
reg: /^rtpmap:(\d*) ([\w\-]*)(?:\s*\/(\d*)(?:\s*\/(\S*))?)?/,
|
|
names: ['payload', 'codec', 'rate', 'encoding'],
|
|
format: function (o) {
|
|
return (o.encoding) ?
|
|
"rtpmap:%d %s/%s/%s":
|
|
o.rate ?
|
|
"rtpmap:%d %s/%s":
|
|
"rtpmap:%d %s";
|
|
}
|
|
},
|
|
{
|
|
//a=fmtp:108 profile-level-id=24;object=23;bitrate=64000
|
|
//a=fmtp:111 minptime=10; useinbandfec=1
|
|
push: 'fmtp',
|
|
reg: /^fmtp:(\d*) ([\S| ]*)/,
|
|
names: ['payload', 'config'],
|
|
format: "fmtp:%d %s"
|
|
},
|
|
{ //a=control:streamid=0
|
|
name: 'control',
|
|
reg: /^control:(.*)/,
|
|
format: "control:%s"
|
|
},
|
|
{ //a=rtcp:65179 IN IP4 193.84.77.194
|
|
name: 'rtcp',
|
|
reg: /^rtcp:(\d*)(?: (\S*) IP(\d) (\S*))?/,
|
|
names: ['port', 'netType', 'ipVer', 'address'],
|
|
format: function (o) {
|
|
return (o.address != null) ?
|
|
"rtcp:%d %s IP%d %s":
|
|
"rtcp:%d";
|
|
}
|
|
},
|
|
{ //a=rtcp-fb:98 trr-int 100
|
|
push: 'rtcpFbTrrInt',
|
|
reg: /^rtcp-fb:(\*|\d*) trr-int (\d*)/,
|
|
names: ['payload', 'value'],
|
|
format: "rtcp-fb:%d trr-int %d"
|
|
},
|
|
{ //a=rtcp-fb:98 nack rpsi
|
|
push: 'rtcpFb',
|
|
reg: /^rtcp-fb:(\*|\d*) ([\w-_]*)(?: ([\w-_]*))?/,
|
|
names: ['payload', 'type', 'subtype'],
|
|
format: function (o) {
|
|
return (o.subtype != null) ?
|
|
"rtcp-fb:%s %s %s":
|
|
"rtcp-fb:%s %s";
|
|
}
|
|
},
|
|
{ //a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
|
|
//a=extmap:1/recvonly URI-gps-string
|
|
push: 'ext',
|
|
reg: /^extmap:([\w_\/]*) (\S*)(?: (\S*))?/,
|
|
names: ['value', 'uri', 'config'], // value may include "/direction" suffix
|
|
format: function (o) {
|
|
return (o.config != null) ?
|
|
"extmap:%s %s %s":
|
|
"extmap:%s %s";
|
|
}
|
|
},
|
|
{
|
|
//a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:32
|
|
push: 'crypto',
|
|
reg: /^crypto:(\d*) ([\w_]*) (\S*)(?: (\S*))?/,
|
|
names: ['id', 'suite', 'config', 'sessionConfig'],
|
|
format: function (o) {
|
|
return (o.sessionConfig != null) ?
|
|
"crypto:%d %s %s %s":
|
|
"crypto:%d %s %s";
|
|
}
|
|
},
|
|
{ //a=setup:actpass
|
|
name: 'setup',
|
|
reg: /^setup:(\w*)/,
|
|
format: "setup:%s"
|
|
},
|
|
{ //a=mid:1
|
|
name: 'mid',
|
|
reg: /^mid:([^\s]*)/,
|
|
format: "mid:%s"
|
|
},
|
|
{ //a=msid:0c8b064d-d807-43b4-b434-f92a889d8587 98178685-d409-46e0-8e16-7ef0db0db64a
|
|
name: 'msid',
|
|
reg: /^msid:(.*)/,
|
|
format: "msid:%s"
|
|
},
|
|
{ //a=ptime:20
|
|
name: 'ptime',
|
|
reg: /^ptime:(\d*)/,
|
|
format: "ptime:%d"
|
|
},
|
|
{ //a=maxptime:60
|
|
name: 'maxptime',
|
|
reg: /^maxptime:(\d*)/,
|
|
format: "maxptime:%d"
|
|
},
|
|
{ //a=sendrecv
|
|
name: 'direction',
|
|
reg: /^(sendrecv|recvonly|sendonly|inactive)/
|
|
},
|
|
{ //a=ice-lite
|
|
name: 'icelite',
|
|
reg: /^(ice-lite)/
|
|
},
|
|
{ //a=ice-ufrag:F7gI
|
|
name: 'iceUfrag',
|
|
reg: /^ice-ufrag:(\S*)/,
|
|
format: "ice-ufrag:%s"
|
|
},
|
|
{ //a=ice-pwd:x9cml/YzichV2+XlhiMu8g
|
|
name: 'icePwd',
|
|
reg: /^ice-pwd:(\S*)/,
|
|
format: "ice-pwd:%s"
|
|
},
|
|
{ //a=fingerprint:SHA-1 00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33
|
|
name: 'fingerprint',
|
|
reg: /^fingerprint:(\S*) (\S*)/,
|
|
names: ['type', 'hash'],
|
|
format: "fingerprint:%s %s"
|
|
},
|
|
{
|
|
//a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host
|
|
//a=candidate:1162875081 1 udp 2113937151 192.168.34.75 60017 typ host generation 0
|
|
//a=candidate:3289912957 2 udp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 generation 0
|
|
//a=candidate:229815620 1 tcp 1518280447 192.168.150.19 60017 typ host tcptype active generation 0
|
|
//a=candidate:3289912957 2 tcp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 tcptype passive generation 0
|
|
push:'candidates',
|
|
reg: /^candidate:(\S*) (\d*) (\S*) (\d*) (\S*) (\d*) typ (\S*)(?: raddr (\S*) rport (\d*))?(?: tcptype (\S*))?(?: generation (\d*))?/,
|
|
names: ['foundation', 'component', 'transport', 'priority', 'ip', 'port', 'type', 'raddr', 'rport', 'tcptype', 'generation'],
|
|
format: function (o) {
|
|
var str = "candidate:%s %d %s %d %s %d typ %s";
|
|
|
|
str += (o.raddr != null) ? " raddr %s rport %d" : "%v%v";
|
|
|
|
// NB: candidate has three optional chunks, so %void middles one if it's missing
|
|
str += (o.tcptype != null) ? " tcptype %s" : "%v";
|
|
|
|
if (o.generation != null) {
|
|
str += " generation %d";
|
|
}
|
|
return str;
|
|
}
|
|
},
|
|
{ //a=end-of-candidates (keep after the candidates line for readability)
|
|
name: 'endOfCandidates',
|
|
reg: /^(end-of-candidates)/
|
|
},
|
|
{ //a=remote-candidates:1 203.0.113.1 54400 2 203.0.113.1 54401 ...
|
|
name: 'remoteCandidates',
|
|
reg: /^remote-candidates:(.*)/,
|
|
format: "remote-candidates:%s"
|
|
},
|
|
{ //a=ice-options:google-ice
|
|
name: 'iceOptions',
|
|
reg: /^ice-options:(\S*)/,
|
|
format: "ice-options:%s"
|
|
},
|
|
{ //a=ssrc:2566107569 cname:t9YU8M1UxTF8Y1A1
|
|
push: "ssrcs",
|
|
reg: /^ssrc:(\d*) ([\w_]*):(.*)/,
|
|
names: ['id', 'attribute', 'value'],
|
|
format: "ssrc:%d %s:%s"
|
|
},
|
|
{ //a=ssrc-group:FEC 1 2
|
|
push: "ssrcGroups",
|
|
reg: /^ssrc-group:(\w*) (.*)/,
|
|
names: ['semantics', 'ssrcs'],
|
|
format: "ssrc-group:%s %s"
|
|
},
|
|
{ //a=msid-semantic: WMS Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV
|
|
name: "msidSemantic",
|
|
reg: /^msid-semantic:\s?(\w*) (\S*)/,
|
|
names: ['semantic', 'token'],
|
|
format: "msid-semantic: %s %s" // space after ":" is not accidental
|
|
},
|
|
{ //a=group:BUNDLE audio video
|
|
push: 'groups',
|
|
reg: /^group:(\w*) (.*)/,
|
|
names: ['type', 'mids'],
|
|
format: "group:%s %s"
|
|
},
|
|
{ //a=rtcp-mux
|
|
name: 'rtcpMux',
|
|
reg: /^(rtcp-mux)/
|
|
},
|
|
{ //a=rtcp-rsize
|
|
name: 'rtcpRsize',
|
|
reg: /^(rtcp-rsize)/
|
|
},
|
|
{ // any a= that we don't understand is kepts verbatim on media.invalid
|
|
push: 'invalid',
|
|
names: ["value"]
|
|
}
|
|
]
|
|
};
|
|
|
|
// set sensible defaults to avoid polluting the grammar with boring details
|
|
Object.keys(grammar).forEach(function (key) {
|
|
var objs = grammar[key];
|
|
objs.forEach(function (obj) {
|
|
if (!obj.reg) {
|
|
obj.reg = /(.*)/;
|
|
}
|
|
if (!obj.format) {
|
|
obj.format = "%s";
|
|
}
|
|
});
|
|
});
|
|
|
|
},{}],14:[function(require,module,exports){
|
|
var parser = require('./parser');
|
|
var writer = require('./writer');
|
|
|
|
exports.write = writer;
|
|
exports.parse = parser.parse;
|
|
exports.parseFmtpConfig = parser.parseFmtpConfig;
|
|
exports.parsePayloads = parser.parsePayloads;
|
|
exports.parseRemoteCandidates = parser.parseRemoteCandidates;
|
|
|
|
},{"./parser":15,"./writer":16}],15:[function(require,module,exports){
|
|
var toIntIfInt = function (v) {
|
|
return String(Number(v)) === v ? Number(v) : v;
|
|
};
|
|
|
|
var attachProperties = function (match, location, names, rawName) {
|
|
if (rawName && !names) {
|
|
location[rawName] = toIntIfInt(match[1]);
|
|
}
|
|
else {
|
|
for (var i = 0; i < names.length; i += 1) {
|
|
if (match[i+1] != null) {
|
|
location[names[i]] = toIntIfInt(match[i+1]);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
var parseReg = function (obj, location, content) {
|
|
var needsBlank = obj.name && obj.names;
|
|
if (obj.push && !location[obj.push]) {
|
|
location[obj.push] = [];
|
|
}
|
|
else if (needsBlank && !location[obj.name]) {
|
|
location[obj.name] = {};
|
|
}
|
|
var keyLocation = obj.push ?
|
|
{} : // blank object that will be pushed
|
|
needsBlank ? location[obj.name] : location; // otherwise, named location or root
|
|
|
|
attachProperties(content.match(obj.reg), keyLocation, obj.names, obj.name);
|
|
|
|
if (obj.push) {
|
|
location[obj.push].push(keyLocation);
|
|
}
|
|
};
|
|
|
|
var grammar = require('./grammar');
|
|
var validLine = RegExp.prototype.test.bind(/^([a-z])=(.*)/);
|
|
|
|
exports.parse = function (sdp) {
|
|
var session = {}
|
|
, media = []
|
|
, location = session; // points at where properties go under (one of the above)
|
|
|
|
// parse lines we understand
|
|
sdp.split(/(\r\n|\r|\n)/).filter(validLine).forEach(function (l) {
|
|
var type = l[0];
|
|
var content = l.slice(2);
|
|
if (type === 'm') {
|
|
media.push({rtp: [], fmtp: []});
|
|
location = media[media.length-1]; // point at latest media line
|
|
}
|
|
|
|
for (var j = 0; j < (grammar[type] || []).length; j += 1) {
|
|
var obj = grammar[type][j];
|
|
if (obj.reg.test(content)) {
|
|
return parseReg(obj, location, content);
|
|
}
|
|
}
|
|
});
|
|
|
|
session.media = media; // link it up
|
|
return session;
|
|
};
|
|
|
|
var fmtpReducer = function (acc, expr) {
|
|
var s = expr.split('=');
|
|
if (s.length === 2) {
|
|
acc[s[0]] = toIntIfInt(s[1]);
|
|
}
|
|
return acc;
|
|
};
|
|
|
|
exports.parseFmtpConfig = function (str) {
|
|
return str.split(/\;\s?/).reduce(fmtpReducer, {});
|
|
};
|
|
|
|
exports.parsePayloads = function (str) {
|
|
return str.split(' ').map(Number);
|
|
};
|
|
|
|
exports.parseRemoteCandidates = function (str) {
|
|
var candidates = [];
|
|
var parts = str.split(' ').map(toIntIfInt);
|
|
for (var i = 0; i < parts.length; i += 3) {
|
|
candidates.push({
|
|
component: parts[i],
|
|
ip: parts[i + 1],
|
|
port: parts[i + 2]
|
|
});
|
|
}
|
|
return candidates;
|
|
};
|
|
|
|
},{"./grammar":13}],16:[function(require,module,exports){
|
|
var grammar = require('./grammar');
|
|
|
|
// customized util.format - discards excess arguments and can void middle ones
|
|
var formatRegExp = /%[sdv%]/g;
|
|
var format = function (formatStr) {
|
|
var i = 1;
|
|
var args = arguments;
|
|
var len = args.length;
|
|
return formatStr.replace(formatRegExp, function (x) {
|
|
if (i >= len) {
|
|
return x; // missing argument
|
|
}
|
|
var arg = args[i];
|
|
i += 1;
|
|
switch (x) {
|
|
case '%%':
|
|
return '%';
|
|
case '%s':
|
|
return String(arg);
|
|
case '%d':
|
|
return Number(arg);
|
|
case '%v':
|
|
return '';
|
|
}
|
|
});
|
|
// NB: we discard excess arguments - they are typically undefined from makeLine
|
|
};
|
|
|
|
var makeLine = function (type, obj, location) {
|
|
var str = obj.format instanceof Function ?
|
|
(obj.format(obj.push ? location : location[obj.name])) :
|
|
obj.format;
|
|
|
|
var args = [type + '=' + str];
|
|
if (obj.names) {
|
|
for (var i = 0; i < obj.names.length; i += 1) {
|
|
var n = obj.names[i];
|
|
if (obj.name) {
|
|
args.push(location[obj.name][n]);
|
|
}
|
|
else { // for mLine and push attributes
|
|
args.push(location[obj.names[i]]);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
args.push(location[obj.name]);
|
|
}
|
|
return format.apply(null, args);
|
|
};
|
|
|
|
// RFC specified order
|
|
// TODO: extend this with all the rest
|
|
var defaultOuterOrder = [
|
|
'v', 'o', 's', 'i',
|
|
'u', 'e', 'p', 'c',
|
|
'b', 't', 'r', 'z', 'a'
|
|
];
|
|
var defaultInnerOrder = ['i', 'c', 'b', 'a'];
|
|
|
|
|
|
module.exports = function (session, opts) {
|
|
opts = opts || {};
|
|
// ensure certain properties exist
|
|
if (session.version == null) {
|
|
session.version = 0; // "v=0" must be there (only defined version atm)
|
|
}
|
|
if (session.name == null) {
|
|
session.name = " "; // "s= " must be there if no meaningful name set
|
|
}
|
|
session.media.forEach(function (mLine) {
|
|
if (mLine.payloads == null) {
|
|
mLine.payloads = "";
|
|
}
|
|
});
|
|
|
|
var outerOrder = opts.outerOrder || defaultOuterOrder;
|
|
var innerOrder = opts.innerOrder || defaultInnerOrder;
|
|
var sdp = [];
|
|
|
|
// loop through outerOrder for matching properties on session
|
|
outerOrder.forEach(function (type) {
|
|
grammar[type].forEach(function (obj) {
|
|
if (obj.name in session && session[obj.name] != null) {
|
|
sdp.push(makeLine(type, obj, session));
|
|
}
|
|
else if (obj.push in session && session[obj.push] != null) {
|
|
session[obj.push].forEach(function (el) {
|
|
sdp.push(makeLine(type, obj, el));
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// then for each media line, follow the innerOrder
|
|
session.media.forEach(function (mLine) {
|
|
sdp.push(makeLine('m', grammar.m[0], mLine));
|
|
|
|
innerOrder.forEach(function (type) {
|
|
grammar[type].forEach(function (obj) {
|
|
if (obj.name in mLine && mLine[obj.name] != null) {
|
|
sdp.push(makeLine(type, obj, mLine));
|
|
}
|
|
else if (obj.push in mLine && mLine[obj.push] != null) {
|
|
mLine[obj.push].forEach(function (el) {
|
|
sdp.push(makeLine(type, obj, el));
|
|
});
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
return sdp.join('\r\n') + '\r\n';
|
|
};
|
|
|
|
},{"./grammar":13}],17:[function(require,module,exports){
|
|
/* Copyright @ 2015 Atlassian Pty Ltd
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
module.exports = function arrayEquals(array) {
|
|
// if the other array is a falsy value, return
|
|
if (!array)
|
|
return false;
|
|
|
|
// compare lengths - can save a lot of time
|
|
if (this.length != array.length)
|
|
return false;
|
|
|
|
for (var i = 0, l = this.length; i < l; i++) {
|
|
// Check if we have nested arrays
|
|
if (this[i] instanceof Array && array[i] instanceof Array) {
|
|
// recurse into the nested arrays
|
|
if (!arrayEquals.apply(this[i], [array[i]]))
|
|
return false;
|
|
} else if (this[i] != array[i]) {
|
|
// Warning - two different object instances will never be equal:
|
|
// {x:20} != {x:20}
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
|
|
},{}],18:[function(require,module,exports){
|
|
/* Copyright @ 2015 Atlassian Pty Ltd
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
exports.Interop = require('./interop');
|
|
|
|
},{"./interop":19}],19:[function(require,module,exports){
|
|
/* Copyright @ 2015 Atlassian Pty Ltd
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
/* global RTCSessionDescription */
|
|
/* global RTCIceCandidate */
|
|
/* jshint -W097 */
|
|
"use strict";
|
|
|
|
var transform = require('./transform');
|
|
var arrayEquals = require('./array-equals');
|
|
|
|
function Interop() {
|
|
|
|
/**
|
|
* This map holds the most recent Unified Plan offer/answer SDP that was
|
|
* converted to Plan B, with the SDP type ('offer' or 'answer') as keys and
|
|
* the SDP string as values.
|
|
*
|
|
* @type {{}}
|
|
*/
|
|
this.cache = {
|
|
mlB2UMap : {},
|
|
mlU2BMap : {}
|
|
};
|
|
}
|
|
|
|
module.exports = Interop;
|
|
|
|
/**
|
|
* Changes the candidate args to match with the related Unified Plan
|
|
*/
|
|
Interop.prototype.candidateToUnifiedPlan = function(candidate) {
|
|
var cand = new RTCIceCandidate(candidate);
|
|
|
|
cand.sdpMLineIndex = this.cache.mlB2UMap[cand.sdpMLineIndex];
|
|
/* TODO: change sdpMid to (audio|video)-SSRC */
|
|
|
|
return cand;
|
|
};
|
|
|
|
/**
|
|
* Changes the candidate args to match with the related Plan B
|
|
*/
|
|
Interop.prototype.candidateToPlanB = function(candidate) {
|
|
var cand = new RTCIceCandidate(candidate);
|
|
|
|
if (cand.sdpMid.indexOf('audio') === 0) {
|
|
cand.sdpMid = 'audio';
|
|
} else if (cand.sdpMid.indexOf('video') === 0) {
|
|
cand.sdpMid = 'video';
|
|
} else {
|
|
throw new Error('candidate with ' + cand.sdpMid + ' not allowed');
|
|
}
|
|
|
|
cand.sdpMLineIndex = this.cache.mlU2BMap[cand.sdpMLineIndex];
|
|
|
|
return cand;
|
|
};
|
|
|
|
/**
|
|
* Returns the index of the first m-line with the given media type and with a
|
|
* direction which allows sending, in the last Unified Plan description with
|
|
* type "answer" converted to Plan B. Returns {null} if there is no saved
|
|
* answer, or if none of its m-lines with the given type allow sending.
|
|
* @param type the media type ("audio" or "video").
|
|
* @returns {*}
|
|
*/
|
|
Interop.prototype.getFirstSendingIndexFromAnswer = function(type) {
|
|
if (!this.cache.answer) {
|
|
return null;
|
|
}
|
|
|
|
var session = transform.parse(this.cache.answer);
|
|
if (session && session.media && Array.isArray(session.media)){
|
|
for (var i = 0; i < session.media.length; i++) {
|
|
if (session.media[i].type == type &&
|
|
(!session.media[i].direction /* default to sendrecv */ ||
|
|
session.media[i].direction === 'sendrecv' ||
|
|
session.media[i].direction === 'sendonly')){
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* This method transforms a Unified Plan SDP to an equivalent Plan B SDP. A
|
|
* PeerConnection wrapper transforms the SDP to Plan B before passing it to the
|
|
* application.
|
|
*
|
|
* @param desc
|
|
* @returns {*}
|
|
*/
|
|
Interop.prototype.toPlanB = function(desc) {
|
|
var self = this;
|
|
//#region Preliminary input validation.
|
|
|
|
if (typeof desc !== 'object' || desc === null ||
|
|
typeof desc.sdp !== 'string') {
|
|
console.warn('An empty description was passed as an argument.');
|
|
return desc;
|
|
}
|
|
|
|
// Objectify the SDP for easier manipulation.
|
|
var session = transform.parse(desc.sdp);
|
|
|
|
// If the SDP contains no media, there's nothing to transform.
|
|
if (typeof session.media === 'undefined' ||
|
|
!Array.isArray(session.media) || session.media.length === 0) {
|
|
console.warn('The description has no media.');
|
|
return desc;
|
|
}
|
|
|
|
// Try some heuristics to "make sure" this is a Unified Plan SDP. Plan B
|
|
// SDP has a video, an audio and a data "channel" at most.
|
|
if (session.media.length <= 3 && session.media.every(function(m) {
|
|
return ['video', 'audio', 'data'].indexOf(m.mid) !== -1;
|
|
})) {
|
|
console.warn('This description does not look like Unified Plan.');
|
|
return desc;
|
|
}
|
|
|
|
//#endregion
|
|
|
|
// HACK https://bugzilla.mozilla.org/show_bug.cgi?id=1113443
|
|
var sdp = desc.sdp;
|
|
var rewrite = false;
|
|
for (var i = 0; i < session.media.length; i++) {
|
|
var uLine = session.media[i];
|
|
uLine.rtp.forEach(function(rtp) {
|
|
if (rtp.codec === 'NULL')
|
|
{
|
|
rewrite = true;
|
|
var offer = transform.parse(self.cache.offer);
|
|
rtp.codec = offer.media[i].rtp[0].codec;
|
|
}
|
|
});
|
|
}
|
|
if (rewrite) {
|
|
sdp = transform.write(session);
|
|
}
|
|
|
|
// Unified Plan SDP is our "precious". Cache it for later use in the Plan B
|
|
// -> Unified Plan transformation.
|
|
this.cache[desc.type] = sdp;
|
|
|
|
//#region Convert from Unified Plan to Plan B.
|
|
|
|
// We rebuild the session.media array.
|
|
var media = session.media;
|
|
session.media = [];
|
|
|
|
// Associative array that maps channel types to channel objects for fast
|
|
// access to channel objects by their type, e.g. type2bl['audio']->channel
|
|
// obj.
|
|
var type2bl = {};
|
|
|
|
// Used to build the group:BUNDLE value after the channels construction
|
|
// loop.
|
|
var types = [];
|
|
|
|
media.forEach(function(uLine) {
|
|
// rtcp-mux is required in the Plan B SDP.
|
|
if ((typeof uLine.rtcpMux !== 'string' ||
|
|
uLine.rtcpMux !== 'rtcp-mux') &&
|
|
uLine.direction !== 'inactive') {
|
|
throw new Error('Cannot convert to Plan B because m-lines ' +
|
|
'without the rtcp-mux attribute were found.');
|
|
}
|
|
|
|
// If we don't have a channel for this uLine.type OR the selected is
|
|
// inactive, then select this uLine as the channel basis.
|
|
if (typeof type2bl[uLine.type] === 'undefined' ||
|
|
type2bl[uLine.type].direction === 'inactive') {
|
|
type2bl[uLine.type] = uLine;
|
|
}
|
|
|
|
if (uLine.protocol != type2bl[uLine.type].protocol) {
|
|
throw new Error('Cannot convert to Plan B because m-lines ' +
|
|
'have different protocols and this library does not have ' +
|
|
'support for that');
|
|
}
|
|
|
|
if (uLine.payloads != type2bl[uLine.type].payloads) {
|
|
throw new Error('Cannot convert to Plan B because m-lines ' +
|
|
'have different payloads and this library does not have ' +
|
|
'support for that');
|
|
}
|
|
|
|
});
|
|
|
|
// Implode the Unified Plan m-lines/tracks into Plan B channels.
|
|
media.forEach(function(uLine) {
|
|
if (uLine.type === 'application') {
|
|
session.media.push(uLine);
|
|
types.push(uLine.mid);
|
|
return;
|
|
}
|
|
|
|
// Add sources to the channel and handle a=msid.
|
|
if (typeof uLine.sources === 'object') {
|
|
Object.keys(uLine.sources).forEach(function(ssrc) {
|
|
if (typeof type2bl[uLine.type].sources !== 'object')
|
|
type2bl[uLine.type].sources = {};
|
|
|
|
// Assign the sources to the channel.
|
|
type2bl[uLine.type].sources[ssrc] =
|
|
uLine.sources[ssrc];
|
|
|
|
if (typeof uLine.msid !== 'undefined') {
|
|
// In Plan B the msid is an SSRC attribute. Also, we don't
|
|
// care about the obsolete label and mslabel attributes.
|
|
//
|
|
// Note that it is not guaranteed that the uLine will
|
|
// have an msid. recvonly channels in particular don't have
|
|
// one.
|
|
type2bl[uLine.type].sources[ssrc].msid =
|
|
uLine.msid;
|
|
}
|
|
// NOTE ssrcs in ssrc groups will share msids, as
|
|
// draft-uberti-rtcweb-plan-00 mandates.
|
|
});
|
|
}
|
|
|
|
// Add ssrc groups to the channel.
|
|
if (typeof uLine.ssrcGroups !== 'undefined' &&
|
|
Array.isArray(uLine.ssrcGroups)) {
|
|
|
|
// Create the ssrcGroups array, if it's not defined.
|
|
if (typeof type2bl[uLine.type].ssrcGroups === 'undefined' ||
|
|
!Array.isArray(type2bl[uLine.type].ssrcGroups)) {
|
|
type2bl[uLine.type].ssrcGroups = [];
|
|
}
|
|
|
|
type2bl[uLine.type].ssrcGroups =
|
|
type2bl[uLine.type].ssrcGroups.concat(
|
|
uLine.ssrcGroups);
|
|
}
|
|
|
|
if (type2bl[uLine.type] === uLine) {
|
|
// Plan B mids are in ['audio', 'video', 'data']
|
|
uLine.mid = uLine.type;
|
|
|
|
// Plan B doesn't support/need the bundle-only attribute.
|
|
delete uLine.bundleOnly;
|
|
|
|
// In Plan B the msid is an SSRC attribute.
|
|
delete uLine.msid;
|
|
|
|
if (uLine.type == media[0].type) {
|
|
types.unshift(uLine.type);
|
|
// Add the channel to the new media array.
|
|
session.media.unshift(uLine);
|
|
} else {
|
|
types.push(uLine.type);
|
|
// Add the channel to the new media array.
|
|
session.media.push(uLine);
|
|
}
|
|
}
|
|
});
|
|
|
|
if (typeof session.groups !== 'undefined') {
|
|
// We regenerate the BUNDLE group with the new mids.
|
|
session.groups.some(function(group) {
|
|
if (group.type === 'BUNDLE') {
|
|
group.mids = types.join(' ');
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
|
|
// msid semantic
|
|
session.msidSemantic = {
|
|
semantic: 'WMS',
|
|
token: '*'
|
|
};
|
|
|
|
var resStr = transform.write(session);
|
|
|
|
return new RTCSessionDescription({
|
|
type: desc.type,
|
|
sdp: resStr
|
|
});
|
|
|
|
//#endregion
|
|
};
|
|
|
|
/* follow rules defined in RFC4145 */
|
|
function addSetupAttr(uLine) {
|
|
if (typeof uLine.setup === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
if (uLine.setup === "active") {
|
|
uLine.setup = "passive";
|
|
} else if (uLine.setup === "passive") {
|
|
uLine.setup = "active";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This method transforms a Plan B SDP to an equivalent Unified Plan SDP. A
|
|
* PeerConnection wrapper transforms the SDP to Unified Plan before passing it
|
|
* to FF.
|
|
*
|
|
* @param desc
|
|
* @returns {*}
|
|
*/
|
|
Interop.prototype.toUnifiedPlan = function(desc) {
|
|
var self = this;
|
|
//#region Preliminary input validation.
|
|
|
|
if (typeof desc !== 'object' || desc === null ||
|
|
typeof desc.sdp !== 'string') {
|
|
console.warn('An empty description was passed as an argument.');
|
|
return desc;
|
|
}
|
|
|
|
var session = transform.parse(desc.sdp);
|
|
|
|
// If the SDP contains no media, there's nothing to transform.
|
|
if (typeof session.media === 'undefined' ||
|
|
!Array.isArray(session.media) || session.media.length === 0) {
|
|
console.warn('The description has no media.');
|
|
return desc;
|
|
}
|
|
|
|
// Try some heuristics to "make sure" this is a Plan B SDP. Plan B SDP has
|
|
// a video, an audio and a data "channel" at most.
|
|
if (session.media.length > 3 || !session.media.every(function(m) {
|
|
return ['video', 'audio', 'data'].indexOf(m.mid) !== -1;
|
|
})) {
|
|
console.warn('This description does not look like Plan B.');
|
|
return desc;
|
|
}
|
|
|
|
// Make sure this Plan B SDP can be converted to a Unified Plan SDP.
|
|
var mids = [];
|
|
session.media.forEach(function(m) {
|
|
mids.push(m.mid);
|
|
});
|
|
|
|
var hasBundle = false;
|
|
if (typeof session.groups !== 'undefined' &&
|
|
Array.isArray(session.groups)) {
|
|
hasBundle = session.groups.every(function(g) {
|
|
return g.type !== 'BUNDLE' ||
|
|
arrayEquals.apply(g.mids.sort(), [mids.sort()]);
|
|
});
|
|
}
|
|
|
|
if (!hasBundle) {
|
|
var mustBeBundle = false;
|
|
|
|
session.media.forEach(function(m) {
|
|
if (m.direction !== 'inactive') {
|
|
mustBeBundle = true;
|
|
}
|
|
});
|
|
|
|
if (mustBeBundle) {
|
|
throw new Error("Cannot convert to Unified Plan because m-lines that" +
|
|
" are not bundled were found.");
|
|
}
|
|
}
|
|
|
|
//#endregion
|
|
|
|
|
|
//#region Convert from Plan B to Unified Plan.
|
|
|
|
// Unfortunately, a Plan B offer/answer doesn't have enough information to
|
|
// rebuild an equivalent Unified Plan offer/answer.
|
|
//
|
|
// For example, if this is a local answer (in Unified Plan style) that we
|
|
// convert to Plan B prior to handing it over to the application (the
|
|
// PeerConnection wrapper called us, for instance, after a successful
|
|
// createAnswer), we want to remember the m-line at which we've seen the
|
|
// (local) SSRC. That's because when the application wants to do call the
|
|
// SLD method, forcing us to do the inverse transformation (from Plan B to
|
|
// Unified Plan), we need to know to which m-line to assign the (local)
|
|
// SSRC. We also need to know all the other m-lines that the original
|
|
// answer had and include them in the transformed answer as well.
|
|
//
|
|
// Another example is if this is a remote offer that we convert to Plan B
|
|
// prior to giving it to the application, we want to remember the mid at
|
|
// which we've seen the (remote) SSRC.
|
|
//
|
|
// In the iteration that follows, we use the cached Unified Plan (if it
|
|
// exists) to assign mids to ssrcs.
|
|
|
|
var type;
|
|
if (desc.type === 'answer') {
|
|
type = 'offer';
|
|
} else if (desc.type === 'offer') {
|
|
type = 'answer';
|
|
} else {
|
|
throw new Error("Type '" + desc.type + "' not supported.");
|
|
}
|
|
|
|
var cached;
|
|
if (typeof this.cache[type] !== 'undefined') {
|
|
cached = transform.parse(this.cache[type]);
|
|
}
|
|
|
|
var recvonlySsrcs = {
|
|
audio: {},
|
|
video: {}
|
|
};
|
|
|
|
// A helper map that sends mids to m-line objects. We use it later to
|
|
// rebuild the Unified Plan style session.media array.
|
|
var mid2ul = {};
|
|
var bIdx = 0;
|
|
var uIdx = 0;
|
|
|
|
var sources2ul = {};
|
|
|
|
var candidates;
|
|
var iceUfrag;
|
|
var icePwd;
|
|
var fingerprint;
|
|
var payloads = {};
|
|
var rtcpFb = {};
|
|
var rtp = {};
|
|
|
|
session.media.forEach(function(bLine) {
|
|
if ((typeof bLine.rtcpMux !== 'string' ||
|
|
bLine.rtcpMux !== 'rtcp-mux') &&
|
|
bLine.direction !== 'inactive') {
|
|
throw new Error("Cannot convert to Unified Plan because m-lines " +
|
|
"without the rtcp-mux attribute were found.");
|
|
}
|
|
|
|
if (bLine.type === 'application') {
|
|
mid2ul[bLine.mid] = bLine;
|
|
return;
|
|
}
|
|
|
|
// With rtcp-mux and bundle all the channels should have the same ICE
|
|
// stuff.
|
|
var sources = bLine.sources;
|
|
var ssrcGroups = bLine.ssrcGroups;
|
|
var port = bLine.port;
|
|
|
|
/* Chrome adds different candidates even using bundle, so we concat the candidates list */
|
|
if (typeof bLine.candidates != 'undefined') {
|
|
if (typeof candidates != 'undefined') {
|
|
candidates = candidates.concat(bLine.candidates);
|
|
} else {
|
|
candidates = bLine.candidates;
|
|
}
|
|
}
|
|
|
|
if ((typeof iceUfrag != 'undefined') && (typeof bLine.iceUfrag != 'undefined') && (iceUfrag != bLine.iceUfrag)) {
|
|
throw new Error("Only BUNDLE supported, iceUfrag must be the same for all m-lines.\n" +
|
|
"\tLast iceUfrag: " + iceUfrag + "\n" +
|
|
"\tNew iceUfrag: " + bLine.iceUfrag
|
|
);
|
|
}
|
|
|
|
if (typeof bLine.iceUfrag != 'undefined') {
|
|
iceUfrag = bLine.iceUfrag;
|
|
}
|
|
|
|
if ((typeof icePwd != 'undefined') && (typeof bLine.icePwd != 'undefined') && (icePwd != bLine.icePwd)) {
|
|
throw new Error("Only BUNDLE supported, icePwd must be the same for all m-lines.\n" +
|
|
"\tLast icePwd: " + icePwd + "\n" +
|
|
"\tNew icePwd: " + bLine.icePwd
|
|
);
|
|
}
|
|
|
|
if (typeof bLine.icePwd != 'undefined') {
|
|
icePwd = bLine.icePwd;
|
|
}
|
|
|
|
if ((typeof fingerprint != 'undefined') && (typeof bLine.fingerprint != 'undefined') &&
|
|
(fingerprint.type != bLine.fingerprint.type || fingerprint.hash != bLine.fingerprint.hash)) {
|
|
throw new Error("Only BUNDLE supported, fingerprint must be the same for all m-lines.\n" +
|
|
"\tLast fingerprint: " + JSON.stringify(fingerprint) + "\n" +
|
|
"\tNew fingerprint: " + JSON.stringify(bLine.fingerprint)
|
|
);
|
|
}
|
|
|
|
if (typeof bLine.fingerprint != 'undefined') {
|
|
fingerprint = bLine.fingerprint;
|
|
}
|
|
|
|
payloads[bLine.type] = bLine.payloads;
|
|
rtcpFb[bLine.type] = bLine.rtcpFb;
|
|
rtp[bLine.type] = bLine.rtp;
|
|
|
|
// inverted ssrc group map
|
|
var ssrc2group = {};
|
|
if (typeof ssrcGroups !== 'undefined' && Array.isArray(ssrcGroups)) {
|
|
ssrcGroups.forEach(function (ssrcGroup) {
|
|
// XXX This might brake if an SSRC is in more than one group
|
|
// for some reason.
|
|
if (typeof ssrcGroup.ssrcs !== 'undefined' &&
|
|
Array.isArray(ssrcGroup.ssrcs)) {
|
|
ssrcGroup.ssrcs.forEach(function (ssrc) {
|
|
if (typeof ssrc2group[ssrc] === 'undefined') {
|
|
ssrc2group[ssrc] = [];
|
|
}
|
|
|
|
ssrc2group[ssrc].push(ssrcGroup);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// ssrc to m-line index.
|
|
var ssrc2ml = {};
|
|
|
|
if (typeof sources === 'object') {
|
|
|
|
// We'll use the "bLine" object as a prototype for each new "mLine"
|
|
// that we create, but first we need to clean it up a bit.
|
|
delete bLine.sources;
|
|
delete bLine.ssrcGroups;
|
|
delete bLine.candidates;
|
|
delete bLine.iceUfrag;
|
|
delete bLine.icePwd;
|
|
delete bLine.fingerprint;
|
|
delete bLine.port;
|
|
delete bLine.mid;
|
|
|
|
// Explode the Plan B channel sources with one m-line per source.
|
|
Object.keys(sources).forEach(function(ssrc) {
|
|
|
|
// The (unified) m-line for this SSRC. We either create it from
|
|
// scratch or, if it's a grouped SSRC, we re-use a related
|
|
// mline. In other words, if the source is grouped with another
|
|
// source, put the two together in the same m-line.
|
|
var uLine;
|
|
|
|
// We assume here that we are the answerer in the O/A, so any
|
|
// offers which we translate come from the remote side, while
|
|
// answers are local. So the check below is to make that we
|
|
// handle receive-only SSRCs in a special way only if they come
|
|
// from the remote side.
|
|
if (desc.type==='offer') {
|
|
// We want to detect SSRCs which are used by a remote peer
|
|
// in an m-line with direction=recvonly (i.e. they are
|
|
// being used for RTCP only).
|
|
// This information would have gotten lost if the remote
|
|
// peer used Unified Plan and their local description was
|
|
// translated to Plan B. So we use the lack of an MSID
|
|
// attribute to deduce a "receive only" SSRC.
|
|
if (!sources[ssrc].msid) {
|
|
recvonlySsrcs[bLine.type][ssrc] = sources[ssrc];
|
|
// Receive-only SSRCs must not create new m-lines. We
|
|
// will assign them to an existing m-line later.
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (typeof ssrc2group[ssrc] !== 'undefined' &&
|
|
Array.isArray(ssrc2group[ssrc])) {
|
|
ssrc2group[ssrc].some(function (ssrcGroup) {
|
|
// ssrcGroup.ssrcs *is* an Array, no need to check
|
|
// again here.
|
|
return ssrcGroup.ssrcs.some(function (related) {
|
|
if (typeof ssrc2ml[related] === 'object') {
|
|
uLine = ssrc2ml[related];
|
|
return true;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
if (typeof uLine === 'object') {
|
|
// the m-line already exists. Just add the source.
|
|
uLine.sources[ssrc] = sources[ssrc];
|
|
delete sources[ssrc].msid;
|
|
} else {
|
|
// Use the "bLine" as a prototype for the "uLine".
|
|
uLine = Object.create(bLine);
|
|
ssrc2ml[ssrc] = uLine;
|
|
|
|
if (typeof sources[ssrc].msid !== 'undefined') {
|
|
// Assign the msid of the source to the m-line. Note
|
|
// that it is not guaranteed that the source will have
|
|
// msid. In particular "recvonly" sources don't have an
|
|
// msid. Note that "recvonly" is a term only defined
|
|
// for m-lines.
|
|
uLine.msid = sources[ssrc].msid;
|
|
delete sources[ssrc].msid;
|
|
}
|
|
|
|
// We assign one SSRC per media line.
|
|
uLine.sources = {};
|
|
uLine.sources[ssrc] = sources[ssrc];
|
|
uLine.ssrcGroups = ssrc2group[ssrc];
|
|
|
|
// Use the cached Unified Plan SDP (if it exists) to assign
|
|
// SSRCs to mids.
|
|
if (typeof cached !== 'undefined' &&
|
|
typeof cached.media !== 'undefined' &&
|
|
Array.isArray(cached.media)) {
|
|
|
|
cached.media.forEach(function (m) {
|
|
if (typeof m.sources === 'object') {
|
|
Object.keys(m.sources).forEach(function (s) {
|
|
if (s === ssrc) {
|
|
uLine.mid = m.mid;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
if (typeof uLine.mid === 'undefined') {
|
|
|
|
// If this is an SSRC that we see for the first time
|
|
// assign it a new mid. This is typically the case when
|
|
// this method is called to transform a remote
|
|
// description for the first time or when there is a
|
|
// new SSRC in the remote description because a new
|
|
// peer has joined the conference. Local SSRCs should
|
|
// have already been added to the map in the toPlanB
|
|
// method.
|
|
//
|
|
// Because FF generates answers in Unified Plan style,
|
|
// we MUST already have a cached answer with all the
|
|
// local SSRCs mapped to some m-line/mid.
|
|
|
|
uLine.mid = [bLine.type, '-', ssrc].join('');
|
|
}
|
|
|
|
// Include the candidates in the 1st media line.
|
|
uLine.candidates = candidates;
|
|
uLine.iceUfrag = iceUfrag;
|
|
uLine.icePwd = icePwd;
|
|
uLine.fingerprint = fingerprint;
|
|
uLine.port = port;
|
|
|
|
mid2ul[uLine.mid] = uLine;
|
|
sources2ul[uIdx] = uLine.sources;
|
|
|
|
self.cache.mlU2BMap[uIdx] = bIdx;
|
|
if (typeof self.cache.mlB2UMap[bIdx] === 'undefined') {
|
|
self.cache.mlB2UMap[bIdx] = uIdx;
|
|
}
|
|
uIdx++;
|
|
}
|
|
});
|
|
} else {
|
|
var uLine = bLine;
|
|
|
|
uLine.candidates = candidates;
|
|
uLine.iceUfrag = iceUfrag;
|
|
uLine.icePwd = icePwd;
|
|
uLine.fingerprint = fingerprint;
|
|
uLine.port = port;
|
|
|
|
mid2ul[uLine.mid] = uLine;
|
|
|
|
self.cache.mlU2BMap[uIdx] = bIdx;
|
|
if (typeof self.cache.mlB2UMap[bIdx] === 'undefined') {
|
|
self.cache.mlB2UMap[bIdx] = uIdx;
|
|
}
|
|
}
|
|
|
|
bIdx++;
|
|
});
|
|
|
|
// Rebuild the media array in the right order and add the missing mLines
|
|
// (missing from the Plan B SDP).
|
|
session.media = [];
|
|
mids = []; // reuse
|
|
|
|
if (desc.type === 'answer') {
|
|
|
|
// The media lines in the answer must match the media lines in the
|
|
// offer. The order is important too. Here we assume that Firefox is
|
|
// the answerer, so we merely have to use the reconstructed (unified)
|
|
// answer to update the cached (unified) answer accordingly.
|
|
//
|
|
// In the general case, one would have to use the cached (unified)
|
|
// offer to find the m-lines that are missing from the reconstructed
|
|
// answer, potentially grabbing them from the cached (unified) answer.
|
|
// One has to be careful with this approach because inactive m-lines do
|
|
// not always have an mid, making it tricky (impossible?) to find where
|
|
// exactly and which m-lines are missing from the reconstructed answer.
|
|
|
|
for (var i = 0; i < cached.media.length; i++) {
|
|
var uLine = cached.media[i];
|
|
|
|
delete uLine.msid;
|
|
delete uLine.sources;
|
|
delete uLine.ssrcGroups;
|
|
|
|
if (typeof sources2ul[i] === 'undefined') {
|
|
if (!uLine.direction
|
|
|| uLine.direction === 'sendrecv')
|
|
uLine.direction = 'recvonly';
|
|
else if (uLine.direction === 'sendonly')
|
|
uLine.direction = 'inactive';
|
|
} else {
|
|
if (!uLine.direction
|
|
|| uLine.direction === 'sendrecv')
|
|
uLine.direction = 'sendrecv';
|
|
else if (uLine.direction === 'recvonly')
|
|
uLine.direction = 'sendonly';
|
|
}
|
|
|
|
uLine.sources = sources2ul[i];
|
|
uLine.candidates = candidates;
|
|
uLine.iceUfrag = iceUfrag;
|
|
uLine.icePwd = icePwd;
|
|
uLine.fingerprint = fingerprint;
|
|
|
|
uLine.rtp = rtp[uLine.type];
|
|
uLine.payloads = payloads[uLine.type];
|
|
uLine.rtcpFb = rtcpFb[uLine.type];
|
|
|
|
session.media.push(uLine);
|
|
|
|
if (typeof uLine.mid === 'string') {
|
|
// inactive lines don't/may not have an mid.
|
|
mids.push(uLine.mid);
|
|
}
|
|
}
|
|
} else {
|
|
|
|
// SDP offer/answer (and the JSEP spec) forbids removing an m-section
|
|
// under any circumstances. If we are no longer interested in sending a
|
|
// track, we just remove the msid and ssrc attributes and set it to
|
|
// either a=recvonly (as the reofferer, we must use recvonly if the
|
|
// other side was previously sending on the m-section, but we can also
|
|
// leave the possibility open if it wasn't previously in use), or
|
|
// a=inactive.
|
|
|
|
if (typeof cached !== 'undefined' &&
|
|
typeof cached.media !== 'undefined' &&
|
|
Array.isArray(cached.media)) {
|
|
cached.media.forEach(function(uLine) {
|
|
mids.push(uLine.mid);
|
|
if (typeof mid2ul[uLine.mid] !== 'undefined') {
|
|
session.media.push(mid2ul[uLine.mid]);
|
|
} else {
|
|
delete uLine.msid;
|
|
delete uLine.sources;
|
|
delete uLine.ssrcGroups;
|
|
|
|
if (!uLine.direction
|
|
|| uLine.direction === 'sendrecv') {
|
|
uLine.direction = 'sendonly';
|
|
}
|
|
if (!uLine.direction
|
|
|| uLine.direction === 'recvonly') {
|
|
uLine.direction = 'inactive';
|
|
}
|
|
|
|
addSetupAttr (uLine);
|
|
session.media.push(uLine);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add all the remaining (new) m-lines of the transformed SDP.
|
|
Object.keys(mid2ul).forEach(function(mid) {
|
|
if (mids.indexOf(mid) === -1) {
|
|
mids.push(mid);
|
|
if (mid2ul[mid].direction === 'recvonly') {
|
|
// This is a remote recvonly channel. Add its SSRC to the
|
|
// appropriate sendrecv or sendonly channel.
|
|
// TODO(gp) what if we don't have sendrecv/sendonly
|
|
// channel?
|
|
|
|
var done = false;
|
|
|
|
session.media.some(function (uLine) {
|
|
if ((uLine.direction === 'sendrecv' ||
|
|
uLine.direction === 'sendonly') &&
|
|
uLine.type === mid2ul[mid].type) {
|
|
// mid2ul[mid] shouldn't have any ssrc-groups
|
|
Object.keys(mid2ul[mid].sources).forEach(
|
|
function (ssrc) {
|
|
uLine.sources[ssrc] =
|
|
mid2ul[mid].sources[ssrc];
|
|
});
|
|
|
|
done = true;
|
|
return true;
|
|
}
|
|
});
|
|
|
|
if (!done) {
|
|
session.media.push(mid2ul[mid]);
|
|
}
|
|
} else {
|
|
session.media.push(mid2ul[mid]);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// After we have constructed the Plan Unified m-lines we can figure out
|
|
// where (in which m-line) to place the 'recvonly SSRCs'.
|
|
// Note: we assume here that we are the answerer in the O/A, so any offers
|
|
// which we translate come from the remote side, while answers are local
|
|
// (and so our last local description is cached as an 'answer').
|
|
["audio", "video"].forEach(function (type) {
|
|
if (!session || !session.media || !Array.isArray(session.media))
|
|
return;
|
|
|
|
var idx = null;
|
|
if (Object.keys(recvonlySsrcs[type]).length > 0) {
|
|
idx = self.getFirstSendingIndexFromAnswer(type);
|
|
if (idx === null){
|
|
// If this is the first offer we receive, we don't have a
|
|
// cached answer. Assume that we will be sending media using
|
|
// the first m-line for each media type.
|
|
|
|
for (var i = 0; i < session.media.length; i++) {
|
|
if (session.media[i].type === type) {
|
|
idx = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (idx && session.media.length > idx) {
|
|
var mLine = session.media[idx];
|
|
Object.keys(recvonlySsrcs[type]).forEach(function(ssrc) {
|
|
if (mLine.sources && mLine.sources[ssrc]) {
|
|
console.warn("Replacing an existing SSRC.");
|
|
}
|
|
if (!mLine.sources) {
|
|
mLine.sources = {};
|
|
}
|
|
|
|
mLine.sources[ssrc] = recvonlySsrcs[type][ssrc];
|
|
});
|
|
}
|
|
});
|
|
|
|
if (typeof session.groups !== 'undefined') {
|
|
// We regenerate the BUNDLE group (since we regenerated the mids)
|
|
session.groups.some(function(group) {
|
|
if (group.type === 'BUNDLE') {
|
|
group.mids = mids.join(' ');
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
|
|
// msid semantic
|
|
session.msidSemantic = {
|
|
semantic: 'WMS',
|
|
token: '*'
|
|
};
|
|
|
|
var resStr = transform.write(session);
|
|
|
|
// Cache the transformed SDP (Unified Plan) for later re-use in this
|
|
// function.
|
|
this.cache[desc.type] = resStr;
|
|
|
|
return new RTCSessionDescription({
|
|
type: desc.type,
|
|
sdp: resStr
|
|
});
|
|
|
|
//#endregion
|
|
};
|
|
|
|
},{"./array-equals":17,"./transform":20}],20:[function(require,module,exports){
|
|
/* Copyright @ 2015 Atlassian Pty Ltd
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
var transform = require('sdp-transform');
|
|
|
|
exports.write = function(session, opts) {
|
|
|
|
if (typeof session !== 'undefined' &&
|
|
typeof session.media !== 'undefined' &&
|
|
Array.isArray(session.media)) {
|
|
|
|
session.media.forEach(function (mLine) {
|
|
// expand sources to ssrcs
|
|
if (typeof mLine.sources !== 'undefined' &&
|
|
Object.keys(mLine.sources).length !== 0) {
|
|
mLine.ssrcs = [];
|
|
Object.keys(mLine.sources).forEach(function (ssrc) {
|
|
var source = mLine.sources[ssrc];
|
|
Object.keys(source).forEach(function (attribute) {
|
|
mLine.ssrcs.push({
|
|
id: ssrc,
|
|
attribute: attribute,
|
|
value: source[attribute]
|
|
});
|
|
});
|
|
});
|
|
delete mLine.sources;
|
|
}
|
|
|
|
// join ssrcs in ssrc groups
|
|
if (typeof mLine.ssrcGroups !== 'undefined' &&
|
|
Array.isArray(mLine.ssrcGroups)) {
|
|
mLine.ssrcGroups.forEach(function (ssrcGroup) {
|
|
if (typeof ssrcGroup.ssrcs !== 'undefined' &&
|
|
Array.isArray(ssrcGroup.ssrcs)) {
|
|
ssrcGroup.ssrcs = ssrcGroup.ssrcs.join(' ');
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// join group mids
|
|
if (typeof session !== 'undefined' &&
|
|
typeof session.groups !== 'undefined' && Array.isArray(session.groups)) {
|
|
|
|
session.groups.forEach(function (g) {
|
|
if (typeof g.mids !== 'undefined' && Array.isArray(g.mids)) {
|
|
g.mids = g.mids.join(' ');
|
|
}
|
|
});
|
|
}
|
|
|
|
return transform.write(session, opts);
|
|
};
|
|
|
|
exports.parse = function(sdp) {
|
|
var session = transform.parse(sdp);
|
|
|
|
if (typeof session !== 'undefined' && typeof session.media !== 'undefined' &&
|
|
Array.isArray(session.media)) {
|
|
|
|
session.media.forEach(function (mLine) {
|
|
// group sources attributes by ssrc
|
|
if (typeof mLine.ssrcs !== 'undefined' && Array.isArray(mLine.ssrcs)) {
|
|
mLine.sources = {};
|
|
mLine.ssrcs.forEach(function (ssrc) {
|
|
if (!mLine.sources[ssrc.id])
|
|
mLine.sources[ssrc.id] = {};
|
|
mLine.sources[ssrc.id][ssrc.attribute] = ssrc.value;
|
|
});
|
|
|
|
delete mLine.ssrcs;
|
|
}
|
|
|
|
// split ssrcs in ssrc groups
|
|
if (typeof mLine.ssrcGroups !== 'undefined' &&
|
|
Array.isArray(mLine.ssrcGroups)) {
|
|
mLine.ssrcGroups.forEach(function (ssrcGroup) {
|
|
if (typeof ssrcGroup.ssrcs === 'string') {
|
|
ssrcGroup.ssrcs = ssrcGroup.ssrcs.split(' ');
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
// split group mids
|
|
if (typeof session !== 'undefined' &&
|
|
typeof session.groups !== 'undefined' && Array.isArray(session.groups)) {
|
|
|
|
session.groups.forEach(function (g) {
|
|
if (typeof g.mids === 'string') {
|
|
g.mids = g.mids.split(' ');
|
|
}
|
|
});
|
|
}
|
|
|
|
return session;
|
|
};
|
|
|
|
|
|
},{"sdp-transform":14}],21:[function(require,module,exports){
|
|
|
|
/*!
|
|
* UAParser.js v0.7.24
|
|
* Lightweight JavaScript-based User-Agent string parser
|
|
* https://github.com/faisalman/ua-parser-js
|
|
*
|
|
* Copyright © 2012-2021 Faisal Salman <f@faisalman.com>
|
|
* Licensed under MIT License
|
|
*/
|
|
|
|
(function (window, undefined) {
|
|
|
|
'use strict';
|
|
|
|
//////////////
|
|
// Constants
|
|
/////////////
|
|
|
|
|
|
var LIBVERSION = '0.7.24',
|
|
EMPTY = '',
|
|
UNKNOWN = '?',
|
|
FUNC_TYPE = 'function',
|
|
UNDEF_TYPE = 'undefined',
|
|
OBJ_TYPE = 'object',
|
|
STR_TYPE = 'string',
|
|
MAJOR = 'major', // deprecated
|
|
MODEL = 'model',
|
|
NAME = 'name',
|
|
TYPE = 'type',
|
|
VENDOR = 'vendor',
|
|
VERSION = 'version',
|
|
ARCHITECTURE= 'architecture',
|
|
CONSOLE = 'console',
|
|
MOBILE = 'mobile',
|
|
TABLET = 'tablet',
|
|
SMARTTV = 'smarttv',
|
|
WEARABLE = 'wearable',
|
|
EMBEDDED = 'embedded';
|
|
|
|
|
|
///////////
|
|
// Helper
|
|
//////////
|
|
|
|
|
|
var util = {
|
|
extend : function (regexes, extensions) {
|
|
var mergedRegexes = {};
|
|
for (var i in regexes) {
|
|
if (extensions[i] && extensions[i].length % 2 === 0) {
|
|
mergedRegexes[i] = extensions[i].concat(regexes[i]);
|
|
} else {
|
|
mergedRegexes[i] = regexes[i];
|
|
}
|
|
}
|
|
return mergedRegexes;
|
|
},
|
|
has : function (str1, str2) {
|
|
if (typeof str1 === "string") {
|
|
return str2.toLowerCase().indexOf(str1.toLowerCase()) !== -1;
|
|
} else {
|
|
return false;
|
|
}
|
|
},
|
|
lowerize : function (str) {
|
|
return str.toLowerCase();
|
|
},
|
|
major : function (version) {
|
|
return typeof(version) === STR_TYPE ? version.replace(/[^\d\.]/g,'').split(".")[0] : undefined;
|
|
},
|
|
trim : function (str) {
|
|
return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
|
|
}
|
|
};
|
|
|
|
|
|
///////////////
|
|
// Map helper
|
|
//////////////
|
|
|
|
|
|
var mapper = {
|
|
|
|
rgx : function (ua, arrays) {
|
|
|
|
var i = 0, j, k, p, q, matches, match;
|
|
|
|
// loop through all regexes maps
|
|
while (i < arrays.length && !matches) {
|
|
|
|
var regex = arrays[i], // even sequence (0,2,4,..)
|
|
props = arrays[i + 1]; // odd sequence (1,3,5,..)
|
|
j = k = 0;
|
|
|
|
// try matching uastring with regexes
|
|
while (j < regex.length && !matches) {
|
|
|
|
matches = regex[j++].exec(ua);
|
|
|
|
if (!!matches) {
|
|
for (p = 0; p < props.length; p++) {
|
|
match = matches[++k];
|
|
q = props[p];
|
|
// check if given property is actually array
|
|
if (typeof q === OBJ_TYPE && q.length > 0) {
|
|
if (q.length == 2) {
|
|
if (typeof q[1] == FUNC_TYPE) {
|
|
// assign modified match
|
|
this[q[0]] = q[1].call(this, match);
|
|
} else {
|
|
// assign given value, ignore regex match
|
|
this[q[0]] = q[1];
|
|
}
|
|
} else if (q.length == 3) {
|
|
// check whether function or regex
|
|
if (typeof q[1] === FUNC_TYPE && !(q[1].exec && q[1].test)) {
|
|
// call function (usually string mapper)
|
|
this[q[0]] = match ? q[1].call(this, match, q[2]) : undefined;
|
|
} else {
|
|
// sanitize match using given regex
|
|
this[q[0]] = match ? match.replace(q[1], q[2]) : undefined;
|
|
}
|
|
} else if (q.length == 4) {
|
|
this[q[0]] = match ? q[3].call(this, match.replace(q[1], q[2])) : undefined;
|
|
}
|
|
} else {
|
|
this[q] = match ? match : undefined;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
i += 2;
|
|
}
|
|
},
|
|
|
|
str : function (str, map) {
|
|
|
|
for (var i in map) {
|
|
// check if array
|
|
if (typeof map[i] === OBJ_TYPE && map[i].length > 0) {
|
|
for (var j = 0; j < map[i].length; j++) {
|
|
if (util.has(map[i][j], str)) {
|
|
return (i === UNKNOWN) ? undefined : i;
|
|
}
|
|
}
|
|
} else if (util.has(map[i], str)) {
|
|
return (i === UNKNOWN) ? undefined : i;
|
|
}
|
|
}
|
|
return str;
|
|
}
|
|
};
|
|
|
|
|
|
///////////////
|
|
// String map
|
|
//////////////
|
|
|
|
|
|
var maps = {
|
|
|
|
browser : {
|
|
oldsafari : {
|
|
version : {
|
|
'1.0' : '/8',
|
|
'1.2' : '/1',
|
|
'1.3' : '/3',
|
|
'2.0' : '/412',
|
|
'2.0.2' : '/416',
|
|
'2.0.3' : '/417',
|
|
'2.0.4' : '/419',
|
|
'?' : '/'
|
|
}
|
|
}
|
|
},
|
|
|
|
device : {
|
|
amazon : {
|
|
model : {
|
|
'Fire Phone' : ['SD', 'KF']
|
|
}
|
|
},
|
|
sprint : {
|
|
model : {
|
|
'Evo Shift 4G' : '7373KT'
|
|
},
|
|
vendor : {
|
|
'HTC' : 'APA',
|
|
'Sprint' : 'Sprint'
|
|
}
|
|
}
|
|
},
|
|
|
|
os : {
|
|
windows : {
|
|
version : {
|
|
'ME' : '4.90',
|
|
'NT 3.11' : 'NT3.51',
|
|
'NT 4.0' : 'NT4.0',
|
|
'2000' : 'NT 5.0',
|
|
'XP' : ['NT 5.1', 'NT 5.2'],
|
|
'Vista' : 'NT 6.0',
|
|
'7' : 'NT 6.1',
|
|
'8' : 'NT 6.2',
|
|
'8.1' : 'NT 6.3',
|
|
'10' : ['NT 6.4', 'NT 10.0'],
|
|
'RT' : 'ARM'
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
//////////////
|
|
// Regex map
|
|
/////////////
|
|
|
|
|
|
var regexes = {
|
|
|
|
browser : [[
|
|
|
|
// Presto based
|
|
/(opera\smini)\/([\w\.-]+)/i, // Opera Mini
|
|
/(opera\s[mobiletab]{3,6}).+version\/([\w\.-]+)/i, // Opera Mobi/Tablet
|
|
/(opera).+version\/([\w\.]+)/i, // Opera > 9.80
|
|
/(opera)[\/\s]+([\w\.]+)/i // Opera < 9.80
|
|
], [NAME, VERSION], [
|
|
|
|
/(opios)[\/\s]+([\w\.]+)/i // Opera mini on iphone >= 8.0
|
|
], [[NAME, 'Opera Mini'], VERSION], [
|
|
|
|
/\s(opr)\/([\w\.]+)/i // Opera Webkit
|
|
], [[NAME, 'Opera'], VERSION], [
|
|
|
|
// Mixed
|
|
/(kindle)\/([\w\.]+)/i, // Kindle
|
|
/(lunascape|maxthon|netfront|jasmine|blazer|instagram)[\/\s]?([\w\.]*)/i,
|
|
// Lunascape/Maxthon/Netfront/Jasmine/Blazer/Instagram
|
|
// Trident based
|
|
/(avant\s|iemobile|slim)(?:browser)?[\/\s]?([\w\.]*)/i,
|
|
// Avant/IEMobile/SlimBrowser
|
|
/(bidubrowser|baidubrowser)[\/\s]?([\w\.]+)/i, // Baidu Browser
|
|
/(?:ms|\()(ie)\s([\w\.]+)/i, // Internet Explorer
|
|
|
|
// Webkit/KHTML based
|
|
/(rekonq)\/([\w\.]*)/i, // Rekonq
|
|
/(chromium|flock|rockmelt|midori|epiphany|silk|skyfire|ovibrowser|bolt|iron|vivaldi|iridium|phantomjs|bowser|quark|qupzilla|falkon)\/([\w\.-]+)/i
|
|
// Chromium/Flock/RockMelt/Midori/Epiphany/Silk/Skyfire/Bolt/Iron/Iridium/PhantomJS/Bowser/QupZilla/Falkon
|
|
], [NAME, VERSION], [
|
|
|
|
/(konqueror)\/([\w\.]+)/i // Konqueror
|
|
], [[NAME, 'Konqueror'], VERSION], [
|
|
|
|
/(trident).+rv[:\s]([\w\.]{1,9}).+like\sgecko/i // IE11
|
|
], [[NAME, 'IE'], VERSION], [
|
|
|
|
/(edge|edgios|edga|edg)\/((\d+)?[\w\.]+)/i // Microsoft Edge
|
|
], [[NAME, 'Edge'], VERSION], [
|
|
|
|
/(yabrowser)\/([\w\.]+)/i // Yandex
|
|
], [[NAME, 'Yandex'], VERSION], [
|
|
|
|
/(Avast)\/([\w\.]+)/i // Avast Secure Browser
|
|
], [[NAME, 'Avast Secure Browser'], VERSION], [
|
|
|
|
/(AVG)\/([\w\.]+)/i // AVG Secure Browser
|
|
], [[NAME, 'AVG Secure Browser'], VERSION], [
|
|
|
|
/(puffin)\/([\w\.]+)/i // Puffin
|
|
], [[NAME, 'Puffin'], VERSION], [
|
|
|
|
/(focus)\/([\w\.]+)/i // Firefox Focus
|
|
], [[NAME, 'Firefox Focus'], VERSION], [
|
|
|
|
/(opt)\/([\w\.]+)/i // Opera Touch
|
|
], [[NAME, 'Opera Touch'], VERSION], [
|
|
|
|
/((?:[\s\/])uc?\s?browser|(?:juc.+)ucweb)[\/\s]?([\w\.]+)/i // UCBrowser
|
|
], [[NAME, 'UCBrowser'], VERSION], [
|
|
|
|
/(comodo_dragon)\/([\w\.]+)/i // Comodo Dragon
|
|
], [[NAME, /_/g, ' '], VERSION], [
|
|
|
|
/((?:windowswechat)? qbcore)\/([\w\.]+).*(?:windowswechat)?/i // WeChat Desktop for Windows Built-in Browser
|
|
], [[NAME, 'WeChat(Win) Desktop'], VERSION], [
|
|
|
|
/(micromessenger)\/([\w\.]+)/i // WeChat
|
|
], [[NAME, 'WeChat'], VERSION], [
|
|
|
|
/(brave)\/([\w\.]+)/i // Brave browser
|
|
], [[NAME, 'Brave'], VERSION], [
|
|
|
|
/(whale)\/([\w\.]+)/i // Whale browser
|
|
], [[NAME, 'Whale'], VERSION], [
|
|
|
|
/(qqbrowserlite)\/([\w\.]+)/i // QQBrowserLite
|
|
], [NAME, VERSION], [
|
|
|
|
/(QQ)\/([\d\.]+)/i // QQ, aka ShouQ
|
|
], [NAME, VERSION], [
|
|
|
|
/m?(qqbrowser)[\/\s]?([\w\.]+)/i // QQBrowser
|
|
], [NAME, VERSION], [
|
|
|
|
/(baiduboxapp)[\/\s]?([\w\.]+)/i // Baidu App
|
|
], [NAME, VERSION], [
|
|
|
|
/(2345Explorer)[\/\s]?([\w\.]+)/i // 2345 Browser
|
|
], [NAME, VERSION], [
|
|
|
|
/(MetaSr)[\/\s]?([\w\.]+)/i // SouGouBrowser
|
|
], [NAME], [
|
|
|
|
/(LBBROWSER)/i // LieBao Browser
|
|
], [NAME], [
|
|
|
|
/(weibo)__([\d\.]+)/i // Weibo
|
|
], [NAME, VERSION], [
|
|
|
|
/xiaomi\/miuibrowser\/([\w\.]+)/i // MIUI Browser
|
|
], [VERSION, [NAME, 'MIUI Browser']], [
|
|
|
|
/;fbav\/([\w\.]+);/i // Facebook App for iOS & Android with version
|
|
], [VERSION, [NAME, 'Facebook']], [
|
|
|
|
/FBAN\/FBIOS|FB_IAB\/FB4A/i // Facebook App for iOS & Android without version
|
|
], [[NAME, 'Facebook']], [
|
|
|
|
/safari\s(line)\/([\w\.]+)/i, // Line App for iOS
|
|
/android.+(line)\/([\w\.]+)\/iab/i // Line App for Android
|
|
], [NAME, VERSION], [
|
|
|
|
/headlesschrome(?:\/([\w\.]+)|\s)/i // Chrome Headless
|
|
], [VERSION, [NAME, 'Chrome Headless']], [
|
|
|
|
/\swv\).+(chrome)\/([\w\.]+)/i // Chrome WebView
|
|
], [[NAME, /(.+)/, '$1 WebView'], VERSION], [
|
|
|
|
/((?:oculus|samsung)browser)\/([\w\.]+)/i
|
|
], [[NAME, /(.+(?:g|us))(.+)/, '$1 $2'], VERSION], [ // Oculus / Samsung Browser
|
|
|
|
/android.+version\/([\w\.]+)\s+(?:mobile\s?safari|safari)*/i // Android Browser
|
|
], [VERSION, [NAME, 'Android Browser']], [
|
|
|
|
/(coc_coc_browser)\/([\w\.]+)/i // Coc Coc Browser
|
|
], [[NAME, 'Coc Coc'], VERSION], [
|
|
|
|
/(sailfishbrowser)\/([\w\.]+)/i // Sailfish Browser
|
|
], [[NAME, 'Sailfish Browser'], VERSION], [
|
|
|
|
/(chrome|omniweb|arora|[tizenoka]{5}\s?browser)\/v?([\w\.]+)/i
|
|
// Chrome/OmniWeb/Arora/Tizen/Nokia
|
|
], [NAME, VERSION], [
|
|
|
|
/(dolfin)\/([\w\.]+)/i // Dolphin
|
|
], [[NAME, 'Dolphin'], VERSION], [
|
|
|
|
/(qihu|qhbrowser|qihoobrowser|360browser)/i // 360
|
|
], [[NAME, '360 Browser']], [
|
|
|
|
/((?:android.+)crmo|crios)\/([\w\.]+)/i // Chrome for Android/iOS
|
|
], [[NAME, 'Chrome'], VERSION], [
|
|
|
|
/(coast)\/([\w\.]+)/i // Opera Coast
|
|
], [[NAME, 'Opera Coast'], VERSION], [
|
|
|
|
/fxios\/([\w\.-]+)/i // Firefox for iOS
|
|
], [VERSION, [NAME, 'Firefox']], [
|
|
|
|
/version\/([\w\.]+)\s.*mobile\/\w+\s(safari)/i // Mobile Safari
|
|
], [VERSION, [NAME, 'Mobile Safari']], [
|
|
|
|
/version\/([\w\.]+)\s.*(mobile\s?safari|safari)/i // Safari & Safari Mobile
|
|
], [VERSION, NAME], [
|
|
|
|
/webkit.+?(gsa)\/([\w\.]+)\s.*(mobile\s?safari|safari)(\/[\w\.]+)/i // Google Search Appliance on iOS
|
|
], [[NAME, 'GSA'], VERSION], [
|
|
|
|
/webkit.+?(mobile\s?safari|safari)(\/[\w\.]+)/i // Safari < 3.0
|
|
], [NAME, [VERSION, mapper.str, maps.browser.oldsafari.version]], [
|
|
|
|
/(webkit|khtml)\/([\w\.]+)/i
|
|
], [NAME, VERSION], [
|
|
|
|
// Gecko based
|
|
/(navigator|netscape)\/([\w\.-]+)/i // Netscape
|
|
], [[NAME, 'Netscape'], VERSION], [
|
|
/(swiftfox)/i, // Swiftfox
|
|
/(icedragon|iceweasel|camino|chimera|fennec|maemo\sbrowser|minimo|conkeror)[\/\s]?([\w\.\+]+)/i,
|
|
// IceDragon/Iceweasel/Camino/Chimera/Fennec/Maemo/Minimo/Conkeror
|
|
/(firefox|seamonkey|k-meleon|icecat|iceape|firebird|phoenix|palemoon|basilisk|waterfox)\/([\w\.-]+)$/i,
|
|
|
|
// Firefox/SeaMonkey/K-Meleon/IceCat/IceApe/Firebird/Phoenix
|
|
/(firefox)\/([\w\.]+)\s[\w\s\-]+\/[\w\.]+$/i, // Other Firefox-based
|
|
/(mozilla)\/([\w\.]+)\s.+rv\:.+gecko\/\d+/i, // Mozilla
|
|
|
|
// Other
|
|
/(polaris|lynx|dillo|icab|doris|amaya|w3m|netsurf|sleipnir)[\/\s]?([\w\.]+)/i,
|
|
// Polaris/Lynx/Dillo/iCab/Doris/Amaya/w3m/NetSurf/Sleipnir
|
|
/(links)\s\(([\w\.]+)/i, // Links
|
|
/(gobrowser)\/?([\w\.]*)/i, // GoBrowser
|
|
/(ice\s?browser)\/v?([\w\._]+)/i, // ICE Browser
|
|
/(mosaic)[\/\s]([\w\.]+)/i // Mosaic
|
|
], [NAME, VERSION]
|
|
],
|
|
|
|
cpu : [[
|
|
|
|
/(?:(amd|x(?:(?:86|64)[_-])?|wow|win)64)[;\)]/i // AMD64
|
|
], [[ARCHITECTURE, 'amd64']], [
|
|
|
|
/(ia32(?=;))/i // IA32 (quicktime)
|
|
], [[ARCHITECTURE, util.lowerize]], [
|
|
|
|
/((?:i[346]|x)86)[;\)]/i // IA32
|
|
], [[ARCHITECTURE, 'ia32']], [
|
|
|
|
// PocketPC mistakenly identified as PowerPC
|
|
/windows\s(ce|mobile);\sppc;/i
|
|
], [[ARCHITECTURE, 'arm']], [
|
|
|
|
/((?:ppc|powerpc)(?:64)?)(?:\smac|;|\))/i // PowerPC
|
|
], [[ARCHITECTURE, /ower/, '', util.lowerize]], [
|
|
|
|
/(sun4\w)[;\)]/i // SPARC
|
|
], [[ARCHITECTURE, 'sparc']], [
|
|
|
|
/((?:avr32|ia64(?=;))|68k(?=\))|arm(?:64|(?=v\d+[;l]))|(?=atmel\s)avr|(?:irix|mips|sparc)(?:64)?(?=;)|pa-risc)/i
|
|
// IA64, 68K, ARM/64, AVR/32, IRIX/64, MIPS/64, SPARC/64, PA-RISC
|
|
], [[ARCHITECTURE, util.lowerize]]
|
|
],
|
|
|
|
device : [[
|
|
|
|
/\((ipad|playbook);[\w\s\),;-]+(rim|apple)/i // iPad/PlayBook
|
|
], [MODEL, VENDOR, [TYPE, TABLET]], [
|
|
|
|
/applecoremedia\/[\w\.]+ \((ipad)/ // iPad
|
|
], [MODEL, [VENDOR, 'Apple'], [TYPE, TABLET]], [
|
|
|
|
/(apple\s{0,1}tv)/i // Apple TV
|
|
], [[MODEL, 'Apple TV'], [VENDOR, 'Apple'], [TYPE, SMARTTV]], [
|
|
|
|
/(archos)\s(gamepad2?)/i, // Archos
|
|
/(hp).+(touchpad)/i, // HP TouchPad
|
|
/(hp).+(tablet)/i, // HP Tablet
|
|
/(kindle)\/([\w\.]+)/i, // Kindle
|
|
/\s(nook)[\w\s]+build\/(\w+)/i, // Nook
|
|
/(dell)\s(strea[kpr\s\d]*[\dko])/i // Dell Streak
|
|
], [VENDOR, MODEL, [TYPE, TABLET]], [
|
|
|
|
/(alexa)webm/i,
|
|
/(kf[A-z]+)(\sbuild\/|\)).+silk\//i // Kindle Fire HD
|
|
], [MODEL, [VENDOR, 'Amazon'], [TYPE, TABLET]], [
|
|
/(sd|kf)[0349hijorstuw]+(\sbuild\/|\)).+silk\//i // Fire Phone
|
|
], [[MODEL, mapper.str, maps.device.amazon.model], [VENDOR, 'Amazon'], [TYPE, MOBILE]], [
|
|
/android.+aft([\w])(\sbuild\/|\))/i // Fire TV
|
|
], [MODEL, [VENDOR, 'Amazon'], [TYPE, SMARTTV]], [
|
|
|
|
/\((ip[honed|\s\w*]+);.+(apple)/i // iPod/iPhone
|
|
], [MODEL, VENDOR, [TYPE, MOBILE]], [
|
|
/\((ip[honed|\s\w*]+);/i // iPod/iPhone
|
|
], [MODEL, [VENDOR, 'Apple'], [TYPE, MOBILE]], [
|
|
|
|
/(blackberry)[\s-]?(\w+)/i, // BlackBerry
|
|
/(blackberry|benq|palm(?=\-)|sonyericsson|acer|asus|dell|meizu|motorola|polytron)[\s_-]?([\w-]*)/i,
|
|
// BenQ/Palm/Sony-Ericsson/Acer/Asus/Dell/Meizu/Motorola/Polytron
|
|
/(hp)\s([\w\s]+\w)/i, // HP iPAQ
|
|
/(asus)-?(\w+)/i // Asus
|
|
], [VENDOR, MODEL, [TYPE, MOBILE]], [
|
|
/\(bb10;\s(\w+)/i // BlackBerry 10
|
|
], [MODEL, [VENDOR, 'BlackBerry'], [TYPE, MOBILE]], [
|
|
// Asus Tablets
|
|
/android.+(transfo[prime\s]{4,10}\s\w+|eeepc|slider\s\w+|nexus 7|padfone|p00c)/i
|
|
], [MODEL, [VENDOR, 'Asus'], [TYPE, TABLET]], [
|
|
|
|
/sony\stablet\s[ps]\sbuild\//i, // Sony
|
|
/(?:sony)?sgp\w+(?:\sbuild\/|\))/i
|
|
], [[MODEL, 'Xperia Tablet'], [VENDOR, 'Sony'], [TYPE, TABLET]], [
|
|
/android.+\s([c-g]\d{4}|so[-l]\w+|xq-a\w[4-7][12])(?=\sbuild\/|\).+chrome\/(?![1-6]{0,1}\d\.))/i
|
|
], [MODEL, [VENDOR, 'Sony'], [TYPE, MOBILE]], [
|
|
|
|
/\s(ouya)\s/i, // Ouya
|
|
/(nintendo)\s([wids3utch]+)/i // Nintendo
|
|
], [VENDOR, MODEL, [TYPE, CONSOLE]], [
|
|
|
|
/android.+;\s(shield)\sbuild/i // Nvidia
|
|
], [MODEL, [VENDOR, 'Nvidia'], [TYPE, CONSOLE]], [
|
|
|
|
/(playstation\s[345portablevi]+)/i // Playstation
|
|
], [MODEL, [VENDOR, 'Sony'], [TYPE, CONSOLE]], [
|
|
|
|
/(sprint\s(\w+))/i // Sprint Phones
|
|
], [[VENDOR, mapper.str, maps.device.sprint.vendor], [MODEL, mapper.str, maps.device.sprint.model], [TYPE, MOBILE]], [
|
|
|
|
/(htc)[;_\s-]{1,2}([\w\s]+(?=\)|\sbuild)|\w+)/i, // HTC
|
|
/(zte)-(\w*)/i, // ZTE
|
|
/(alcatel|geeksphone|nexian|panasonic|(?=;\s)sony)[_\s-]?([\w-]*)/i
|
|
// Alcatel/GeeksPhone/Nexian/Panasonic/Sony
|
|
], [VENDOR, [MODEL, /_/g, ' '], [TYPE, MOBILE]], [
|
|
|
|
/(nexus\s9)/i // HTC Nexus 9
|
|
], [MODEL, [VENDOR, 'HTC'], [TYPE, TABLET]], [
|
|
|
|
/d\/huawei([\w\s-]+)[;\)]/i, // Huawei
|
|
/android.+\s(nexus\s6p|vog-[at]?l\d\d|ane-[at]?l[x\d]\d|eml-a?l\d\da?|lya-[at]?l\d[\dc]|clt-a?l\d\di?)/i,
|
|
/android.+\s((?:A(?:GS2?|KA|LP|N[AE]|QM|RE|SK|TH)|B(?:A(?:C|H2)|G2|KL|LA|MH|Z[AKT])|C(?:AZ|DY|LT|OL|[MOR]R)|DUK|E(?:BG|DI|L[ES]|ML|V[AR])|FRD|G(?:LK|RA)|H(?:D[LN]|MA|LK|RY|WI)|INE|J(?:DN2|MM|NY|SN)|K(?:NT|OB|SA)|L(?:IO|LD|ON|[RY]A)|M(?:AR|ED|[HL]A|ON|RX|T7)|N(?:EO|TS|XT)|OXF|P(?:AR|CT|IC|LK|RA)|R(?:IO|VL)|S(?:C[ML]|EA|HT|PN|TF)|T(?:A[HS]|NY)|V(?:[CI]E|KY|OG|RD)|W(?:AS|LZ)|Y(?:635|AL))-[ATU][LN][01259][019])[;\)\s]/i
|
|
|
|
], [MODEL, [VENDOR, 'Huawei'], [TYPE, MOBILE]], [
|
|
|
|
/android.+(bah2?-a?[lw]\d{2})/i // Huawei MediaPad
|
|
], [MODEL, [VENDOR, 'Huawei'], [TYPE, TABLET]], [
|
|
|
|
/(microsoft);\s(lumia[\s\w]+)/i // Microsoft Lumia
|
|
], [VENDOR, MODEL, [TYPE, MOBILE]], [
|
|
|
|
/[\s\(;](xbox(?:\sone)?)[\s\);]/i // Microsoft Xbox
|
|
], [MODEL, [VENDOR, 'Microsoft'], [TYPE, CONSOLE]], [
|
|
/(kin\.[onetw]{3})/i // Microsoft Kin
|
|
], [[MODEL, /\./g, ' '], [VENDOR, 'Microsoft'], [TYPE, MOBILE]], [
|
|
|
|
// Motorola
|
|
/\s(milestone|droid(?:[2-4x]|\s(?:bionic|x2|pro|razr))?:?(\s4g)?)[\w\s]+build\//i,
|
|
/\smot[\s-](\w*)/i,
|
|
/(moto[\s\w\(\)]+(?=\sbuild|\)))/i,
|
|
/(XT\d{3,4}) build\//i,
|
|
/(nexus\s6)/i
|
|
], [MODEL, [VENDOR, 'Motorola'], [TYPE, MOBILE]], [
|
|
/android.+\s(mz60\d|xoom[\s2]{0,2})\sbuild\//i
|
|
], [MODEL, [VENDOR, 'Motorola'], [TYPE, TABLET]], [
|
|
|
|
/hbbtv\/\d+\.\d+\.\d+\s+\([\w\s]*;\s*(\w[^;]*);([^;]*)/i // HbbTV devices
|
|
], [[VENDOR, util.trim], [MODEL, util.trim], [TYPE, SMARTTV]], [
|
|
|
|
/hbbtv.+maple;(\d+)/i
|
|
], [[MODEL, /^/, 'SmartTV'], [VENDOR, 'Samsung'], [TYPE, SMARTTV]], [
|
|
|
|
/\(dtv[\);].+(aquos)/i // Sharp
|
|
], [MODEL, [VENDOR, 'Sharp'], [TYPE, SMARTTV]], [
|
|
|
|
/android.+((sch-i[89]0\d|shw-m380s|SM-P605|SM-P610|SM-P587|gt-p\d{4}|gt-n\d+|sgh-t8[56]9|nexus 10))/i,
|
|
/((SM-T\w+))/i
|
|
], [[VENDOR, 'Samsung'], MODEL, [TYPE, TABLET]], [ // Samsung
|
|
/smart-tv.+(samsung)/i
|
|
], [VENDOR, [TYPE, SMARTTV], MODEL], [
|
|
/((s[cgp]h-\w+|gt-\w+|galaxy\snexus|sm-\w[\w\d]+))/i,
|
|
/\s(sam)(?:sung)[\s-]([\w-]+)/i,
|
|
/sec-((sgh\w+))/i
|
|
], [[VENDOR, 'Samsung'], MODEL, [TYPE, MOBILE]], [
|
|
|
|
/sie-(\w*)/i // Siemens
|
|
], [MODEL, [VENDOR, 'Siemens'], [TYPE, MOBILE]], [
|
|
|
|
/(maemo|nokia).*(n900|lumia\s\d+)/i, // Nokia
|
|
/(nokia)[\s_-]?([\w\.-]*)/i
|
|
], [[VENDOR, 'Nokia'], MODEL, [TYPE, MOBILE]], [
|
|
|
|
/android[x\d\.\s;]+\s([ab][1-7]\-?[0178a]\d\d?)/i // Acer
|
|
], [MODEL, [VENDOR, 'Acer'], [TYPE, TABLET]], [
|
|
|
|
/android.+([vl]k\-?\d{3})\s+build/i // LG Tablet
|
|
], [MODEL, [VENDOR, 'LG'], [TYPE, TABLET]], [
|
|
/android\s3\.[\s\w;-]{10}(lg?)-([06cv9]{3,4})/i // LG Tablet
|
|
], [[VENDOR, 'LG'], MODEL, [TYPE, TABLET]], [
|
|
/linux;\snetcast.+smarttv/i, // LG SmartTV
|
|
/lg\snetcast\.tv-201\d/i
|
|
], [[VENDOR, 'LG'], MODEL, [TYPE, SMARTTV]], [
|
|
/(nexus\s[45])/i, // LG
|
|
/lg[e;\s\/-]+(\w*)/i,
|
|
/android.+lg(\-?[\d\w]+)\s+build/i
|
|
], [MODEL, [VENDOR, 'LG'], [TYPE, MOBILE]], [
|
|
|
|
/(lenovo)\s?(s(?:5000|6000)(?:[\w-]+)|tab(?:[\s\w]+)|[\w-]+)/i // Lenovo tablets
|
|
], [VENDOR, MODEL, [TYPE, TABLET]], [
|
|
/android.+(ideatab[a-z0-9\-\s]+)/i // Lenovo
|
|
], [MODEL, [VENDOR, 'Lenovo'], [TYPE, TABLET]], [
|
|
/(lenovo)[_\s-]?([\w-]+)/i
|
|
], [VENDOR, MODEL, [TYPE, MOBILE]], [
|
|
|
|
/linux;.+((jolla));/i // Jolla
|
|
], [VENDOR, MODEL, [TYPE, MOBILE]], [
|
|
|
|
/((pebble))app\/[\d\.]+\s/i // Pebble
|
|
], [VENDOR, MODEL, [TYPE, WEARABLE]], [
|
|
|
|
/android.+;\s(oppo)\s?([\w\s]+)\sbuild/i // OPPO
|
|
], [VENDOR, MODEL, [TYPE, MOBILE]], [
|
|
|
|
/crkey/i // Google Chromecast
|
|
], [[MODEL, 'Chromecast'], [VENDOR, 'Google'], [TYPE, SMARTTV]], [
|
|
|
|
/android.+;\s(glass)\s\d/i // Google Glass
|
|
], [MODEL, [VENDOR, 'Google'], [TYPE, WEARABLE]], [
|
|
|
|
/android.+;\s(pixel c)[\s)]/i // Google Pixel C
|
|
], [MODEL, [VENDOR, 'Google'], [TYPE, TABLET]], [
|
|
|
|
/android.+;\s(pixel( [2-9]a?)?( xl)?)[\s)]/i // Google Pixel
|
|
], [MODEL, [VENDOR, 'Google'], [TYPE, MOBILE]], [
|
|
|
|
/android.+;\s(\w+)\s+build\/hm\1/i, // Xiaomi Hongmi 'numeric' models
|
|
/android.+(hm[\s\-_]?note?[\s_]?(?:\d\w)?)\sbuild/i, // Xiaomi Hongmi
|
|
/android.+(redmi[\s\-_]?(?:note|k)?(?:[\s_]?[\w\s]+))(?:\sbuild|\))/i,
|
|
// Xiaomi Redmi
|
|
/android.+(mi[\s\-_]?(?:a\d|one|one[\s_]plus|note lte)?[\s_]?(?:\d?\w?)[\s_]?(?:plus)?)\sbuild/i
|
|
// Xiaomi Mi
|
|
], [[MODEL, /_/g, ' '], [VENDOR, 'Xiaomi'], [TYPE, MOBILE]], [
|
|
/android.+(mi[\s\-_]?(?:pad)(?:[\s_]?[\w\s]+))(?:\sbuild|\))/i // Mi Pad tablets
|
|
],[[MODEL, /_/g, ' '], [VENDOR, 'Xiaomi'], [TYPE, TABLET]], [
|
|
/android.+;\s(m[1-5]\snote)\sbuild/i // Meizu
|
|
], [MODEL, [VENDOR, 'Meizu'], [TYPE, MOBILE]], [
|
|
/(mz)-([\w-]{2,})/i
|
|
], [[VENDOR, 'Meizu'], MODEL, [TYPE, MOBILE]], [
|
|
|
|
/android.+a000(1)\s+build/i, // OnePlus
|
|
/android.+oneplus\s(a\d{4})[\s)]/i
|
|
], [MODEL, [VENDOR, 'OnePlus'], [TYPE, MOBILE]], [
|
|
|
|
/android.+[;\/]\s*(RCT[\d\w]+)\s+build/i // RCA Tablets
|
|
], [MODEL, [VENDOR, 'RCA'], [TYPE, TABLET]], [
|
|
|
|
/android.+[;\/\s](Venue[\d\s]{2,7})\s+build/i // Dell Venue Tablets
|
|
], [MODEL, [VENDOR, 'Dell'], [TYPE, TABLET]], [
|
|
|
|
/android.+[;\/]\s*(Q[T|M][\d\w]+)\s+build/i // Verizon Tablet
|
|
], [MODEL, [VENDOR, 'Verizon'], [TYPE, TABLET]], [
|
|
|
|
/android.+[;\/]\s+(Barnes[&\s]+Noble\s+|BN[RT])(\S(?:.*\S)?)\s+build/i // Barnes & Noble Tablet
|
|
], [[VENDOR, 'Barnes & Noble'], MODEL, [TYPE, TABLET]], [
|
|
|
|
/android.+[;\/]\s+(TM\d{3}.*\b)\s+build/i // Barnes & Noble Tablet
|
|
], [MODEL, [VENDOR, 'NuVision'], [TYPE, TABLET]], [
|
|
|
|
/android.+;\s(k88)\sbuild/i // ZTE K Series Tablet
|
|
], [MODEL, [VENDOR, 'ZTE'], [TYPE, TABLET]], [
|
|
|
|
/android.+;\s(nx\d{3}j)\sbuild/i // ZTE Nubia
|
|
], [MODEL, [VENDOR, 'ZTE'], [TYPE, MOBILE]], [
|
|
|
|
/android.+[;\/]\s*(gen\d{3})\s+build.*49h/i // Swiss GEN Mobile
|
|
], [MODEL, [VENDOR, 'Swiss'], [TYPE, MOBILE]], [
|
|
|
|
/android.+[;\/]\s*(zur\d{3})\s+build/i // Swiss ZUR Tablet
|
|
], [MODEL, [VENDOR, 'Swiss'], [TYPE, TABLET]], [
|
|
|
|
/android.+[;\/]\s*((Zeki)?TB.*\b)\s+build/i // Zeki Tablets
|
|
], [MODEL, [VENDOR, 'Zeki'], [TYPE, TABLET]], [
|
|
|
|
/(android).+[;\/]\s+([YR]\d{2})\s+build/i,
|
|
/android.+[;\/]\s+(Dragon[\-\s]+Touch\s+|DT)(\w{5})\sbuild/i // Dragon Touch Tablet
|
|
], [[VENDOR, 'Dragon Touch'], MODEL, [TYPE, TABLET]], [
|
|
|
|
/android.+[;\/]\s*(NS-?\w{0,9})\sbuild/i // Insignia Tablets
|
|
], [MODEL, [VENDOR, 'Insignia'], [TYPE, TABLET]], [
|
|
|
|
/android.+[;\/]\s*((NXA|Next)-?\w{0,9})\s+build/i // NextBook Tablets
|
|
], [MODEL, [VENDOR, 'NextBook'], [TYPE, TABLET]], [
|
|
|
|
/android.+[;\/]\s*(Xtreme\_)?(V(1[045]|2[015]|30|40|60|7[05]|90))\s+build/i
|
|
], [[VENDOR, 'Voice'], MODEL, [TYPE, MOBILE]], [ // Voice Xtreme Phones
|
|
|
|
/android.+[;\/]\s*(LVTEL\-)?(V1[12])\s+build/i // LvTel Phones
|
|
], [[VENDOR, 'LvTel'], MODEL, [TYPE, MOBILE]], [
|
|
|
|
/android.+;\s(PH-1)\s/i
|
|
], [MODEL, [VENDOR, 'Essential'], [TYPE, MOBILE]], [ // Essential PH-1
|
|
|
|
/android.+[;\/]\s*(V(100MD|700NA|7011|917G).*\b)\s+build/i // Envizen Tablets
|
|
], [MODEL, [VENDOR, 'Envizen'], [TYPE, TABLET]], [
|
|
|
|
/android.+[;\/]\s*(Le[\s\-]+Pan)[\s\-]+(\w{1,9})\s+build/i // Le Pan Tablets
|
|
], [VENDOR, MODEL, [TYPE, TABLET]], [
|
|
|
|
/android.+[;\/]\s*(Trio[\s\w\-\.]+)\s+build/i // MachSpeed Tablets
|
|
], [MODEL, [VENDOR, 'MachSpeed'], [TYPE, TABLET]], [
|
|
|
|
/android.+[;\/]\s*(Trinity)[\-\s]*(T\d{3})\s+build/i // Trinity Tablets
|
|
], [VENDOR, MODEL, [TYPE, TABLET]], [
|
|
|
|
/android.+[;\/]\s*TU_(1491)\s+build/i // Rotor Tablets
|
|
], [MODEL, [VENDOR, 'Rotor'], [TYPE, TABLET]], [
|
|
|
|
//android.+(KS(.+))\s+build/i // Amazon Kindle Tablets
|
|
//], [MODEL, [VENDOR, 'Amazon'], [TYPE, TABLET]], [
|
|
|
|
/android.+(Gigaset)[\s\-]+(Q\w{1,9})\s+build/i // Gigaset Tablets
|
|
], [VENDOR, MODEL, [TYPE, TABLET]], [
|
|
|
|
/[\s\/\(](android\stv|smart-?tv)[;\)\s]/i // SmartTV from Unidentified Vendors
|
|
], [[TYPE, SMARTTV]], [
|
|
// Android Phones from Unidentified Vendors
|
|
/android .+?; ([^;]+?)(?: build|\) applewebkit).+? mobile safari/i
|
|
], [MODEL, [TYPE, MOBILE]], [
|
|
// Android Tablets from Unidentified Vendors
|
|
/android .+?;\s([^;]+?)(?: build|\) applewebkit).+?(?! mobile) safari/i
|
|
], [MODEL, [TYPE, TABLET]], [
|
|
|
|
/\s(tablet|tab)[;\/]/i, // Unidentifiable Tablet
|
|
/\s(mobile)(?:[;\/]|\ssafari)/i // Unidentifiable Mobile
|
|
], [[TYPE, util.lowerize], VENDOR, MODEL], [
|
|
|
|
/(android[\w\.\s\-]{0,9});.+build/i // Generic Android Device
|
|
], [MODEL, [VENDOR, 'Generic']], [
|
|
|
|
/(phone)/i
|
|
], [[TYPE, MOBILE]]
|
|
],
|
|
|
|
engine : [[
|
|
|
|
/windows.+\sedge\/([\w\.]+)/i // EdgeHTML
|
|
], [VERSION, [NAME, 'EdgeHTML']], [
|
|
|
|
/webkit\/537\.36.+chrome\/(?!27)([\w\.]+)/i // Blink
|
|
], [VERSION, [NAME, 'Blink']], [
|
|
|
|
/(presto)\/([\w\.]+)/i, // Presto
|
|
/(webkit|trident|netfront|netsurf|amaya|lynx|w3m|goanna)\/([\w\.]+)/i,
|
|
// WebKit/Trident/NetFront/NetSurf/Amaya/Lynx/w3m/Goanna
|
|
/(khtml|tasman|links)[\/\s]\(?([\w\.]+)/i, // KHTML/Tasman/Links
|
|
/(icab)[\/\s]([23]\.[\d\.]+)/i // iCab
|
|
], [NAME, VERSION], [
|
|
|
|
/rv\:([\w\.]{1,9}).+(gecko)/i // Gecko
|
|
], [VERSION, NAME]
|
|
],
|
|
|
|
os : [[
|
|
|
|
// Xbox, consider this before other Windows-based devices
|
|
/(xbox);\s+xbox\s([^\);]+)/i, // Microsoft Xbox (360, One, X, S, Series X, Series S)
|
|
|
|
// Windows based
|
|
/microsoft\s(windows)\s(vista|xp)/i // Windows (iTunes)
|
|
], [NAME, VERSION], [
|
|
/(windows)\snt\s6\.2;\s(arm)/i, // Windows RT
|
|
/(windows\sphone(?:\sos)*)[\s\/]?([\d\.\s\w]*)/i, // Windows Phone
|
|
/(windows\smobile|windows)[\s\/]?([ntce\d\.\s]+\w)/i
|
|
], [NAME, [VERSION, mapper.str, maps.os.windows.version]], [
|
|
/(win(?=3|9|n)|win\s9x\s)([nt\d\.]+)/i
|
|
], [[NAME, 'Windows'], [VERSION, mapper.str, maps.os.windows.version]], [
|
|
|
|
// Mobile/Embedded OS
|
|
/\((bb)(10);/i // BlackBerry 10
|
|
], [[NAME, 'BlackBerry'], VERSION], [
|
|
/(blackberry)\w*\/?([\w\.]*)/i, // Blackberry
|
|
/(tizen|kaios)[\/\s]([\w\.]+)/i, // Tizen/KaiOS
|
|
/(android|webos|palm\sos|qnx|bada|rim\stablet\sos|meego|sailfish|contiki)[\/\s-]?([\w\.]*)/i
|
|
// Android/WebOS/Palm/QNX/Bada/RIM/MeeGo/Contiki/Sailfish OS
|
|
], [NAME, VERSION], [
|
|
/(symbian\s?os|symbos|s60(?=;))[\/\s-]?([\w\.]*)/i // Symbian
|
|
], [[NAME, 'Symbian'], VERSION], [
|
|
/\((series40);/i // Series 40
|
|
], [NAME], [
|
|
/mozilla.+\(mobile;.+gecko.+firefox/i // Firefox OS
|
|
], [[NAME, 'Firefox OS'], VERSION], [
|
|
|
|
// Google Chromecast
|
|
/crkey\/([\d\.]+)/i // Google Chromecast
|
|
], [VERSION, [NAME, 'Chromecast']], [
|
|
|
|
// Console
|
|
/(nintendo|playstation)\s([wids345portablevuch]+)/i, // Nintendo/Playstation
|
|
|
|
// GNU/Linux based
|
|
/(mint)[\/\s\(]?(\w*)/i, // Mint
|
|
/(mageia|vectorlinux)[;\s]/i, // Mageia/VectorLinux
|
|
/(joli|[kxln]?ubuntu|debian|suse|opensuse|gentoo|(?=\s)arch|slackware|fedora|mandriva|centos|pclinuxos|redhat|zenwalk|linpus)[\/\s-]?(?!chrom)([\w\.-]*)/i,
|
|
// Joli/Ubuntu/Debian/SUSE/Gentoo/Arch/Slackware
|
|
// Fedora/Mandriva/CentOS/PCLinuxOS/RedHat/Zenwalk/Linpus
|
|
/(hurd|linux)\s?([\w\.]*)/i, // Hurd/Linux
|
|
/(gnu)\s?([\w\.]*)/i // GNU
|
|
], [NAME, VERSION], [
|
|
|
|
/(cros)\s[\w]+\s([\w\.]+\w)/i // Chromium OS
|
|
], [[NAME, 'Chromium OS'], VERSION],[
|
|
|
|
// Solaris
|
|
/(sunos)\s?([\w\.\d]*)/i // Solaris
|
|
], [[NAME, 'Solaris'], VERSION], [
|
|
|
|
// BSD based
|
|
/\s([frentopc-]{0,4}bsd|dragonfly)\s?([\w\.]*)/i // FreeBSD/NetBSD/OpenBSD/PC-BSD/DragonFly
|
|
], [NAME, VERSION],[
|
|
|
|
/(haiku)\s(\w+)/i // Haiku
|
|
], [NAME, VERSION],[
|
|
|
|
/cfnetwork\/.+darwin/i,
|
|
/ip[honead]{2,4}(?:.*os\s([\w]+)\slike\smac|;\sopera)/i // iOS
|
|
], [[VERSION, /_/g, '.'], [NAME, 'iOS']], [
|
|
|
|
/(mac\sos\sx)\s?([\w\s\.]*)/i,
|
|
/(macintosh|mac(?=_powerpc)\s)/i // Mac OS
|
|
], [[NAME, 'Mac OS'], [VERSION, /_/g, '.']], [
|
|
|
|
// Other
|
|
/((?:open)?solaris)[\/\s-]?([\w\.]*)/i, // Solaris
|
|
/(aix)\s((\d)(?=\.|\)|\s)[\w\.])*/i, // AIX
|
|
/(plan\s9|minix|beos|os\/2|amigaos|morphos|risc\sos|openvms|fuchsia)/i,
|
|
// Plan9/Minix/BeOS/OS2/AmigaOS/MorphOS/RISCOS/OpenVMS/Fuchsia
|
|
/(unix)\s?([\w\.]*)/i // UNIX
|
|
], [NAME, VERSION]
|
|
]
|
|
};
|
|
|
|
|
|
/////////////////
|
|
// Constructor
|
|
////////////////
|
|
var UAParser = function (uastring, extensions) {
|
|
|
|
if (typeof uastring === 'object') {
|
|
extensions = uastring;
|
|
uastring = undefined;
|
|
}
|
|
|
|
if (!(this instanceof UAParser)) {
|
|
return new UAParser(uastring, extensions).getResult();
|
|
}
|
|
|
|
var ua = uastring || ((typeof window !== 'undefined' && window.navigator && window.navigator.userAgent) ? window.navigator.userAgent : EMPTY);
|
|
var rgxmap = extensions ? util.extend(regexes, extensions) : regexes;
|
|
|
|
this.getBrowser = function () {
|
|
var browser = { name: undefined, version: undefined };
|
|
mapper.rgx.call(browser, ua, rgxmap.browser);
|
|
browser.major = util.major(browser.version); // deprecated
|
|
return browser;
|
|
};
|
|
this.getCPU = function () {
|
|
var cpu = { architecture: undefined };
|
|
mapper.rgx.call(cpu, ua, rgxmap.cpu);
|
|
return cpu;
|
|
};
|
|
this.getDevice = function () {
|
|
var device = { vendor: undefined, model: undefined, type: undefined };
|
|
mapper.rgx.call(device, ua, rgxmap.device);
|
|
return device;
|
|
};
|
|
this.getEngine = function () {
|
|
var engine = { name: undefined, version: undefined };
|
|
mapper.rgx.call(engine, ua, rgxmap.engine);
|
|
return engine;
|
|
};
|
|
this.getOS = function () {
|
|
var os = { name: undefined, version: undefined };
|
|
mapper.rgx.call(os, ua, rgxmap.os);
|
|
return os;
|
|
};
|
|
this.getResult = function () {
|
|
return {
|
|
ua : this.getUA(),
|
|
browser : this.getBrowser(),
|
|
engine : this.getEngine(),
|
|
os : this.getOS(),
|
|
device : this.getDevice(),
|
|
cpu : this.getCPU()
|
|
};
|
|
};
|
|
this.getUA = function () {
|
|
return ua;
|
|
};
|
|
this.setUA = function (uastring) {
|
|
ua = uastring;
|
|
return this;
|
|
};
|
|
return this;
|
|
};
|
|
|
|
UAParser.VERSION = LIBVERSION;
|
|
UAParser.BROWSER = {
|
|
NAME : NAME,
|
|
MAJOR : MAJOR, // deprecated
|
|
VERSION : VERSION
|
|
};
|
|
UAParser.CPU = {
|
|
ARCHITECTURE : ARCHITECTURE
|
|
};
|
|
UAParser.DEVICE = {
|
|
MODEL : MODEL,
|
|
VENDOR : VENDOR,
|
|
TYPE : TYPE,
|
|
CONSOLE : CONSOLE,
|
|
MOBILE : MOBILE,
|
|
SMARTTV : SMARTTV,
|
|
TABLET : TABLET,
|
|
WEARABLE: WEARABLE,
|
|
EMBEDDED: EMBEDDED
|
|
};
|
|
UAParser.ENGINE = {
|
|
NAME : NAME,
|
|
VERSION : VERSION
|
|
};
|
|
UAParser.OS = {
|
|
NAME : NAME,
|
|
VERSION : VERSION
|
|
};
|
|
|
|
///////////
|
|
// Export
|
|
//////////
|
|
|
|
|
|
// check js environment
|
|
if (typeof(exports) !== UNDEF_TYPE) {
|
|
// nodejs env
|
|
if (typeof module !== UNDEF_TYPE && module.exports) {
|
|
exports = module.exports = UAParser;
|
|
}
|
|
exports.UAParser = UAParser;
|
|
} else {
|
|
// requirejs env (optional)
|
|
if (typeof(define) === 'function' && define.amd) {
|
|
define(function () {
|
|
return UAParser;
|
|
});
|
|
} else if (typeof window !== 'undefined') {
|
|
// browser env
|
|
window.UAParser = UAParser;
|
|
}
|
|
}
|
|
|
|
// jQuery/Zepto specific (optional)
|
|
// Note:
|
|
// In AMD env the global scope should be kept clean, but jQuery is an exception.
|
|
// jQuery always exports to global scope, unless jQuery.noConflict(true) is used,
|
|
// and we should catch that.
|
|
var $ = typeof window !== 'undefined' && (window.jQuery || window.Zepto);
|
|
if ($ && !$.ua) {
|
|
var parser = new UAParser();
|
|
$.ua = parser.getResult();
|
|
$.ua.get = function () {
|
|
return parser.getUA();
|
|
};
|
|
$.ua.set = function (uastring) {
|
|
parser.setUA(uastring);
|
|
var result = parser.getResult();
|
|
for (var prop in result) {
|
|
$.ua[prop] = result[prop];
|
|
}
|
|
};
|
|
}
|
|
|
|
})(typeof window === 'object' ? window : this);
|
|
|
|
},{}],22:[function(require,module,exports){
|
|
(function (global){
|
|
|
|
var rng;
|
|
|
|
var crypto = global.crypto || global.msCrypto; // for IE 11
|
|
if (crypto && crypto.getRandomValues) {
|
|
// WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto
|
|
// Moderately fast, high quality
|
|
var _rnds8 = new Uint8Array(16);
|
|
rng = function whatwgRNG() {
|
|
crypto.getRandomValues(_rnds8);
|
|
return _rnds8;
|
|
};
|
|
}
|
|
|
|
if (!rng) {
|
|
// Math.random()-based (RNG)
|
|
//
|
|
// If all else fails, use Math.random(). It's fast, but is of unspecified
|
|
// quality.
|
|
var _rnds = new Array(16);
|
|
rng = function() {
|
|
for (var i = 0, r; i < 16; i++) {
|
|
if ((i & 0x03) === 0) r = Math.random() * 0x100000000;
|
|
_rnds[i] = r >>> ((i & 0x03) << 3) & 0xff;
|
|
}
|
|
|
|
return _rnds;
|
|
};
|
|
}
|
|
|
|
module.exports = rng;
|
|
|
|
|
|
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
|
},{}],23:[function(require,module,exports){
|
|
// uuid.js
|
|
//
|
|
// Copyright (c) 2010-2012 Robert Kieffer
|
|
// MIT License - http://opensource.org/licenses/mit-license.php
|
|
|
|
// Unique ID creation requires a high quality random # generator. We feature
|
|
// detect to determine the best RNG source, normalizing to a function that
|
|
// returns 128-bits of randomness, since that's what's usually required
|
|
var _rng = require('./rng');
|
|
|
|
// Maps for number <-> hex string conversion
|
|
var _byteToHex = [];
|
|
var _hexToByte = {};
|
|
for (var i = 0; i < 256; i++) {
|
|
_byteToHex[i] = (i + 0x100).toString(16).substr(1);
|
|
_hexToByte[_byteToHex[i]] = i;
|
|
}
|
|
|
|
// **`parse()` - Parse a UUID into it's component bytes**
|
|
function parse(s, buf, offset) {
|
|
var i = (buf && offset) || 0, ii = 0;
|
|
|
|
buf = buf || [];
|
|
s.toLowerCase().replace(/[0-9a-f]{2}/g, function(oct) {
|
|
if (ii < 16) { // Don't overflow!
|
|
buf[i + ii++] = _hexToByte[oct];
|
|
}
|
|
});
|
|
|
|
// Zero out remaining bytes if string was short
|
|
while (ii < 16) {
|
|
buf[i + ii++] = 0;
|
|
}
|
|
|
|
return buf;
|
|
}
|
|
|
|
// **`unparse()` - Convert UUID byte array (ala parse()) into a string**
|
|
function unparse(buf, offset) {
|
|
var i = offset || 0, bth = _byteToHex;
|
|
return bth[buf[i++]] + bth[buf[i++]] +
|
|
bth[buf[i++]] + bth[buf[i++]] + '-' +
|
|
bth[buf[i++]] + bth[buf[i++]] + '-' +
|
|
bth[buf[i++]] + bth[buf[i++]] + '-' +
|
|
bth[buf[i++]] + bth[buf[i++]] + '-' +
|
|
bth[buf[i++]] + bth[buf[i++]] +
|
|
bth[buf[i++]] + bth[buf[i++]] +
|
|
bth[buf[i++]] + bth[buf[i++]];
|
|
}
|
|
|
|
// **`v1()` - Generate time-based UUID**
|
|
//
|
|
// Inspired by https://github.com/LiosK/UUID.js
|
|
// and http://docs.python.org/library/uuid.html
|
|
|
|
// random #'s we need to init node and clockseq
|
|
var _seedBytes = _rng();
|
|
|
|
// Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1)
|
|
var _nodeId = [
|
|
_seedBytes[0] | 0x01,
|
|
_seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5]
|
|
];
|
|
|
|
// Per 4.2.2, randomize (14 bit) clockseq
|
|
var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff;
|
|
|
|
// Previous uuid creation time
|
|
var _lastMSecs = 0, _lastNSecs = 0;
|
|
|
|
// See https://github.com/broofa/node-uuid for API details
|
|
function v1(options, buf, offset) {
|
|
var i = buf && offset || 0;
|
|
var b = buf || [];
|
|
|
|
options = options || {};
|
|
|
|
var clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq;
|
|
|
|
// UUID timestamps are 100 nano-second units since the Gregorian epoch,
|
|
// (1582-10-15 00:00). JSNumbers aren't precise enough for this, so
|
|
// time is handled internally as 'msecs' (integer milliseconds) and 'nsecs'
|
|
// (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00.
|
|
var msecs = options.msecs !== undefined ? options.msecs : new Date().getTime();
|
|
|
|
// Per 4.2.1.2, use count of uuid's generated during the current clock
|
|
// cycle to simulate higher resolution clock
|
|
var nsecs = options.nsecs !== undefined ? options.nsecs : _lastNSecs + 1;
|
|
|
|
// Time since last uuid creation (in msecs)
|
|
var dt = (msecs - _lastMSecs) + (nsecs - _lastNSecs)/10000;
|
|
|
|
// Per 4.2.1.2, Bump clockseq on clock regression
|
|
if (dt < 0 && options.clockseq === undefined) {
|
|
clockseq = clockseq + 1 & 0x3fff;
|
|
}
|
|
|
|
// Reset nsecs if clock regresses (new clockseq) or we've moved onto a new
|
|
// time interval
|
|
if ((dt < 0 || msecs > _lastMSecs) && options.nsecs === undefined) {
|
|
nsecs = 0;
|
|
}
|
|
|
|
// Per 4.2.1.2 Throw error if too many uuids are requested
|
|
if (nsecs >= 10000) {
|
|
throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec');
|
|
}
|
|
|
|
_lastMSecs = msecs;
|
|
_lastNSecs = nsecs;
|
|
_clockseq = clockseq;
|
|
|
|
// Per 4.1.4 - Convert from unix epoch to Gregorian epoch
|
|
msecs += 12219292800000;
|
|
|
|
// `time_low`
|
|
var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000;
|
|
b[i++] = tl >>> 24 & 0xff;
|
|
b[i++] = tl >>> 16 & 0xff;
|
|
b[i++] = tl >>> 8 & 0xff;
|
|
b[i++] = tl & 0xff;
|
|
|
|
// `time_mid`
|
|
var tmh = (msecs / 0x100000000 * 10000) & 0xfffffff;
|
|
b[i++] = tmh >>> 8 & 0xff;
|
|
b[i++] = tmh & 0xff;
|
|
|
|
// `time_high_and_version`
|
|
b[i++] = tmh >>> 24 & 0xf | 0x10; // include version
|
|
b[i++] = tmh >>> 16 & 0xff;
|
|
|
|
// `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant)
|
|
b[i++] = clockseq >>> 8 | 0x80;
|
|
|
|
// `clock_seq_low`
|
|
b[i++] = clockseq & 0xff;
|
|
|
|
// `node`
|
|
var node = options.node || _nodeId;
|
|
for (var n = 0; n < 6; n++) {
|
|
b[i + n] = node[n];
|
|
}
|
|
|
|
return buf ? buf : unparse(b);
|
|
}
|
|
|
|
// **`v4()` - Generate random UUID**
|
|
|
|
// See https://github.com/broofa/node-uuid for API details
|
|
function v4(options, buf, offset) {
|
|
// Deprecated - 'format' argument, as supported in v1.2
|
|
var i = buf && offset || 0;
|
|
|
|
if (typeof(options) == 'string') {
|
|
buf = options == 'binary' ? new Array(16) : null;
|
|
options = null;
|
|
}
|
|
options = options || {};
|
|
|
|
var rnds = options.random || (options.rng || _rng)();
|
|
|
|
// Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
|
|
rnds[6] = (rnds[6] & 0x0f) | 0x40;
|
|
rnds[8] = (rnds[8] & 0x3f) | 0x80;
|
|
|
|
// Copy bytes to buffer, if provided
|
|
if (buf) {
|
|
for (var ii = 0; ii < 16; ii++) {
|
|
buf[i + ii] = rnds[ii];
|
|
}
|
|
}
|
|
|
|
return buf || unparse(rnds);
|
|
}
|
|
|
|
// Export public API
|
|
var uuid = v4;
|
|
uuid.v1 = v1;
|
|
uuid.v4 = v4;
|
|
uuid.parse = parse;
|
|
uuid.unparse = unparse;
|
|
|
|
module.exports = uuid;
|
|
|
|
},{"./rng":22}],24:[function(require,module,exports){
|
|
/*
|
|
WildEmitter.js is a slim little event emitter by @henrikjoreteg largely based
|
|
on @visionmedia's Emitter from UI Kit.
|
|
|
|
Why? I wanted it standalone.
|
|
|
|
I also wanted support for wildcard emitters like this:
|
|
|
|
emitter.on('*', function (eventName, other, event, payloads) {
|
|
|
|
});
|
|
|
|
emitter.on('somenamespace*', function (eventName, payloads) {
|
|
|
|
});
|
|
|
|
Please note that callbacks triggered by wildcard registered events also get
|
|
the event name as the first argument.
|
|
*/
|
|
|
|
module.exports = WildEmitter;
|
|
|
|
function WildEmitter() { }
|
|
|
|
WildEmitter.mixin = function (constructor) {
|
|
var prototype = constructor.prototype || constructor;
|
|
|
|
prototype.isWildEmitter= true;
|
|
|
|
// Listen on the given `event` with `fn`. Store a group name if present.
|
|
prototype.on = function (event, groupName, fn) {
|
|
this.callbacks = this.callbacks || {};
|
|
var hasGroup = (arguments.length === 3),
|
|
group = hasGroup ? arguments[1] : undefined,
|
|
func = hasGroup ? arguments[2] : arguments[1];
|
|
func._groupName = group;
|
|
(this.callbacks[event] = this.callbacks[event] || []).push(func);
|
|
return this;
|
|
};
|
|
|
|
// Adds an `event` listener that will be invoked a single
|
|
// time then automatically removed.
|
|
prototype.once = function (event, groupName, fn) {
|
|
var self = this,
|
|
hasGroup = (arguments.length === 3),
|
|
group = hasGroup ? arguments[1] : undefined,
|
|
func = hasGroup ? arguments[2] : arguments[1];
|
|
function on() {
|
|
self.off(event, on);
|
|
func.apply(this, arguments);
|
|
}
|
|
this.on(event, group, on);
|
|
return this;
|
|
};
|
|
|
|
// Unbinds an entire group
|
|
prototype.releaseGroup = function (groupName) {
|
|
this.callbacks = this.callbacks || {};
|
|
var item, i, len, handlers;
|
|
for (item in this.callbacks) {
|
|
handlers = this.callbacks[item];
|
|
for (i = 0, len = handlers.length; i < len; i++) {
|
|
if (handlers[i]._groupName === groupName) {
|
|
//console.log('removing');
|
|
// remove it and shorten the array we're looping through
|
|
handlers.splice(i, 1);
|
|
i--;
|
|
len--;
|
|
}
|
|
}
|
|
}
|
|
return this;
|
|
};
|
|
|
|
// Remove the given callback for `event` or all
|
|
// registered callbacks.
|
|
prototype.off = function (event, fn) {
|
|
this.callbacks = this.callbacks || {};
|
|
var callbacks = this.callbacks[event],
|
|
i;
|
|
|
|
if (!callbacks) return this;
|
|
|
|
// remove all handlers
|
|
if (arguments.length === 1) {
|
|
delete this.callbacks[event];
|
|
return this;
|
|
}
|
|
|
|
// remove specific handler
|
|
i = callbacks.indexOf(fn);
|
|
callbacks.splice(i, 1);
|
|
if (callbacks.length === 0) {
|
|
delete this.callbacks[event];
|
|
}
|
|
return this;
|
|
};
|
|
|
|
/// Emit `event` with the given args.
|
|
// also calls any `*` handlers
|
|
prototype.emit = function (event) {
|
|
this.callbacks = this.callbacks || {};
|
|
var args = [].slice.call(arguments, 1),
|
|
callbacks = this.callbacks[event],
|
|
specialCallbacks = this.getWildcardCallbacks(event),
|
|
i,
|
|
len,
|
|
item,
|
|
listeners;
|
|
|
|
if (callbacks) {
|
|
listeners = callbacks.slice();
|
|
for (i = 0, len = listeners.length; i < len; ++i) {
|
|
if (!listeners[i]) {
|
|
break;
|
|
}
|
|
listeners[i].apply(this, args);
|
|
}
|
|
}
|
|
|
|
if (specialCallbacks) {
|
|
len = specialCallbacks.length;
|
|
listeners = specialCallbacks.slice();
|
|
for (i = 0, len = listeners.length; i < len; ++i) {
|
|
if (!listeners[i]) {
|
|
break;
|
|
}
|
|
listeners[i].apply(this, [event].concat(args));
|
|
}
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
// Helper for for finding special wildcard event handlers that match the event
|
|
prototype.getWildcardCallbacks = function (eventName) {
|
|
this.callbacks = this.callbacks || {};
|
|
var item,
|
|
split,
|
|
result = [];
|
|
|
|
for (item in this.callbacks) {
|
|
split = item.split('*');
|
|
if (item === '*' || (split.length === 2 && eventName.slice(0, split[0].length) === split[0])) {
|
|
result = result.concat(this.callbacks[item]);
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
|
|
};
|
|
|
|
WildEmitter.mixin(WildEmitter);
|
|
|
|
},{}]},{},[2])(2)
|
|
});
|