nativescript-connectycube
Version:
ConnectyCube chat and video chat SDK for NativeScript
1,371 lines (1,318 loc) • 127 kB
JavaScript
const {
adapter,
navigator,
MediaStream,
MediaStreamTrack,
RTCPeerConnection,
RTCRtpReceiver,
RTCRtpSender,
} = require('../cubeDependencies');
const Utils = require('../cubeInternalUtils');
('use strict');
/*
The MIT License (MIT)
Copyright (c) 2016 Meetecho
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.
*/
// List of sessions
Janus.sessions = {};
Janus.mobile = Utils.getEnv().reactnative;
Janus.isExtensionEnabled = function () {
if (Janus.mobile) {
return false;
}
if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) {
// No need for the extension, getDisplayMedia is supported
return true;
}
if (window.navigator.userAgent.match('Chrome')) {
let chromever = parseInt(window.navigator.userAgent.match(/Chrome\/(.*) /)[1], 10);
let maxver = 33;
if (window.navigator.userAgent.match('Linux')) maxver = 35; // "known" crash in chrome 34 and 35 on linux
if (chromever >= 26 && chromever <= maxver) {
// Older versions of Chrome don't support this extension-based approach, so lie
return true;
}
return Janus.extension.isInstalled();
} else {
// Firefox and others, no need for the extension (but this doesn't mean it will work)
return true;
}
};
const defaultExtension = {
// Screensharing Chrome Extension ID
extensionId: 'hapfgfdkleiggjjpfpenajgdnfckjpaj',
isInstalled: function () {
if (Janus.mobile) {
return false;
} else {
return document.querySelector('#janus-extension-installed') !== null;
}
},
getScreen: function (callback) {
if (Janus.mobile) {
callback();
} else {
let pending = window.setTimeout(function () {
let error = new Error('NavigatorUserMediaError');
error.name =
'The required Chrome extension is not installed: click <a href="#">here</a> to install it. (NOTE: this will need you to refresh the page)';
return callback(error);
}, 1000);
this.cache[pending] = callback;
window.postMessage({ type: 'janusGetScreen', id: pending }, '*');
}
},
init: function () {
let cache = {};
this.cache = cache;
// Wait for events from the Chrome Extension
if (!Janus.mobile) {
window.addEventListener('message', function (event) {
if (event.origin != window.location.origin) return;
if (event.data.type == 'janusGotScreen' && cache[event.data.id]) {
let callback = cache[event.data.id];
delete cache[event.data.id];
if (event.data.sourceId === '') {
// user canceled
let error = new Error('NavigatorUserMediaError');
error.name = 'You cancelled the request for permission, giving up...';
callback(error);
} else {
callback(null, event.data.sourceId);
}
} else if (event.data.type == 'janusGetScreenPending') {
window.clearTimeout(event.data.id);
}
});
}
},
};
Janus.useDefaultDependencies = function (deps) {
let f = (deps && deps.fetch) || fetch;
let p = (deps && deps.Promise) || Promise;
let socketCls = (deps && deps.WebSocket) || WebSocket;
return {
newWebSocket: function (server, proto) {
return new socketCls(server, proto);
},
extension: (deps && deps.extension) || defaultExtension,
isArray: function (arr) {
return Array.isArray(arr);
},
webRTCAdapter: (deps && deps.adapter) || adapter,
httpAPICall: function (url, options) {
let fetchOptions = {
method: options.verb,
headers: {
Accept: 'application/json, text/plain, */*',
},
cache: 'no-cache',
};
if (options.verb === 'POST') {
fetchOptions.headers['Content-Type'] = 'application/json';
}
if (typeof options.withCredentials !== 'undefined') {
fetchOptions.credentials =
options.withCredentials === true ? 'include' : options.withCredentials ? options.withCredentials : 'omit';
}
if (options.body) {
fetchOptions.body = JSON.stringify(options.body);
}
let fetching = f(url, fetchOptions).catch(function (error) {
return p.reject({
message: 'Probably a network error, is the server down?',
error: error,
});
});
/*
* fetch() does not natively support timeouts.
* Work around this by starting a timeout manually, and racing it agains the fetch() to see which thing resolves first.
*/
if (options.timeout) {
let timeout = new p(function (resolve, reject) {
let timerId = setTimeout(function () {
clearTimeout(timerId);
return reject({
message: 'Request timed out',
timeout: options.timeout,
});
}, options.timeout);
});
fetching = p.race([fetching, timeout]);
}
fetching
.then(function (response) {
if (response.ok) {
if (typeof options.success === typeof Janus.noop) {
return response.json().then(
function (parsed) {
try {
options.success(parsed);
} catch (error) {
Janus.error('Unhandled httpAPICall success callback error', error);
}
},
function (error) {
return p.reject({
message: 'Failed to parse response body',
error: error,
response: response,
});
}
);
}
} else {
return p.reject({ message: 'API call failed', response: response });
}
})
.catch(function (error) {
if (typeof options.error === typeof Janus.noop) {
options.error(error.message || '<< internal error >>', error);
}
});
return fetching;
},
};
};
Janus.useOldDependencies = function (deps) {
let jq = (deps && deps.jQuery) || jQuery;
let socketCls = (deps && deps.WebSocket) || WebSocket;
return {
newWebSocket: function (server, proto) {
return new socketCls(server, proto);
},
isArray: function (arr) {
return jq.isArray(arr);
},
extension: (deps && deps.extension) || defaultExtension,
webRTCAdapter: (deps && deps.adapter) || adapter,
httpAPICall: function (url, options) {
let payload =
typeof options.body !== 'undefined'
? {
contentType: 'application/json',
data: JSON.stringify(options.body),
}
: {};
let credentials =
typeof options.withCredentials !== 'undefined'
? { xhrFields: { withCredentials: options.withCredentials } }
: {};
return jq.ajax(
jq.extend(payload, credentials, {
url: url,
type: options.verb,
cache: false,
dataType: 'json',
async: options.async,
timeout: options.timeout,
success: function (result) {
if (typeof options.success === typeof Janus.noop) {
options.success(result);
}
},
error: function (xhr, status, err) {
if (typeof options.error === typeof Janus.noop) {
options.error(status, err);
}
},
})
);
},
};
};
// Helper function to convert a deprecated media object to a tracks array
Janus.mediaToTracks = function (media) {
let tracks = [];
if (!media) {
// Default is bidirectional audio and video, using default devices
tracks.push({ type: 'audio', capture: true, recv: true });
tracks.push({ type: 'video', capture: true, recv: true });
} else {
if (
!media.keepAudio &&
media.audio !== false &&
(typeof media.audio === 'undefined' ||
media.audio ||
media.audioSend ||
media.audioRecv ||
media.addAudio ||
media.replaceAudio ||
media.removeAudio)
) {
// We may need an audio track
let track = { type: 'audio' };
if (media.removeAudio) {
track.remove = true;
} else {
if (media.addAudio) track.add = true;
else if (media.replaceAudio) track.replace = true;
// Check if we need to capture an audio device
if (media.audioSend !== false) track.capture = media.audio || true;
// Check if we need to receive audio
if (media.audioRecv !== false) track.recv = true;
}
// Add an audio track if needed
if (track.remove || track.capture || track.recv) tracks.push(track);
}
if (
!media.keepVideo &&
media.video !== false &&
(typeof media.video === 'undefined' ||
media.video ||
media.videoSend ||
media.videoRecv ||
media.addVideo ||
media.replaceVideo ||
media.removeVideo)
) {
// We may need a video track
let track = { type: 'video' };
if (media.removeVideo) {
track.remove = true;
} else {
if (media.addVideo) track.add = true;
else if (media.replaceVideo) track.replace = true;
// Check if we need to capture a video device
if (media.videoSend !== false) {
track.capture = media.video || true;
if (['screen', 'window', 'desktop'].includes(track.capture)) {
// Change the type to 'screen'
track.type = 'screen';
track.capture = { video: {} };
// Check if there's constraints
if (media.screenshareFrameRate) track.capture.frameRate = media.screenshareFrameRate;
if (media.screenshareHeight) track.capture.height = media.screenshareHeight;
if (media.screenshareWidth) track.capture.width = media.screenshareWidth;
}
}
// Check if we need to receive video
if (media.videoRecv !== false) track.recv = true;
}
// Add a video track if needed
if (track.remove || track.capture || track.recv) tracks.push(track);
}
if (media.data) {
// We need a data channel
tracks.push({ type: 'data' });
}
}
// Done
return tracks;
};
// Helper function to convert a track object to a set of constraints
Janus.trackConstraints = function (track) {
let constraints = {};
if (!track || !track.capture) return constraints;
if (track.type === 'audio') {
// Just put the capture part in the constraints
constraints.audio = track.capture;
} else if (track.type === 'video') {
// Check if one of the keywords was passed
if ((track.simulcast || track.svc) && track.capture === true) track.capture = 'hires';
if (track.capture === true || typeof track.capture === 'object') {
// Use the provided capture object as video constraint
constraints.video = track.capture;
} else {
let width = 0;
let height = 0;
if (track.capture === 'lowres') {
// Small resolution, 4:3
width = 320;
height = 240;
} else if (track.capture === 'lowres-16:9') {
// Small resolution, 16:9
width = 320;
height = 180;
} else if (track.capture === 'hires' || track.capture === 'hires-16:9' || track.capture === 'hdres') {
// High(HD) resolution is only 16:9
width = 1280;
height = 720;
} else if (track.capture === 'fhdres') {
// Full HD resolution is only 16:9
width = 1920;
height = 1080;
} else if (track.capture === '4kres') {
// 4K resolution is only 16:9
width = 3840;
height = 2160;
} else if (track.capture === 'stdres') {
// Normal resolution, 4:3
width = 640;
height = 480;
} else if (track.capture === 'stdres-16:9') {
// Normal resolution, 16:9
width = 640;
height = 360;
} else {
Janus.log('Default video setting is stdres 4:3');
width = 640;
height = 480;
}
constraints.video = {
width: { ideal: width },
height: { ideal: height },
};
}
} else if (track.type === 'screen') {
// Use the provided capture object as video constraint
constraints.video = track.capture;
}
return constraints;
};
Janus.noop = function () { };
Janus.dataChanDefaultLabel = 'JanusDataChannel';
// Note: in the future we may want to change this, e.g., as was
// attempted in https://github.com/meetecho/janus-gateway/issues/1670
Janus.endOfCandidates = null;
// Stop all tracks from a given stream
Janus.stopAllTracks = function (stream) {
try {
// Try a MediaStreamTrack.stop() for each track
let tracks = stream.getTracks();
for (let mst of tracks) {
Janus.log(mst);
if (mst && mst.dontStop !== true) {
mst.stop();
}
}
} catch (e) {
// Do nothing if this fails
}
};
// Initialization
Janus.init = function (options) {
options = options || {};
options.callback = typeof options.callback == 'function' ? options.callback : Janus.noop;
if (Janus.initDone) {
// Already initialized
options.callback();
} else {
if (typeof console.log == 'undefined') {
console.log = function () { };
}
// Console logging (all debugging disabled by default)
Janus.trace = Janus.noop;
Janus.debug = Janus.noop;
Janus.vdebug = Janus.noop;
Janus.log = Janus.noop;
Janus.warn = Janus.noop;
Janus.error = Janus.noop;
if (options.debug === true || options.debug === 'all') {
// Enable all debugging levels
Janus.trace = console.trace.bind(console);
Janus.debug = console.debug.bind(console);
Janus.vdebug = console.debug.bind(console);
Janus.log = console.log.bind(console);
Janus.warn = console.warn.bind(console);
Janus.error = console.error.bind(console);
} else if (Array.isArray(options.debug)) {
for (let d of options.debug) {
switch (d) {
case 'trace':
Janus.trace = console.trace.bind(console);
break;
case 'debug':
Janus.debug = console.debug.bind(console);
break;
case 'vdebug':
Janus.vdebug = console.debug.bind(console);
break;
case 'log':
Janus.log = console.log.bind(console);
break;
case 'warn':
Janus.warn = console.warn.bind(console);
break;
case 'error':
Janus.error = console.error.bind(console);
break;
default:
console.error(
"Unknown debugging option '" + d + "' (supported: 'trace', 'debug', 'vdebug', 'log', warn', 'error')"
);
break;
}
}
}
Janus.log('Initializing library');
let usedDependencies = options.dependencies || Janus.useDefaultDependencies();
Janus.isArray = usedDependencies.isArray;
Janus.webRTCAdapter = usedDependencies.webRTCAdapter;
Janus.httpAPICall = usedDependencies.httpAPICall;
Janus.newWebSocket = usedDependencies.newWebSocket;
Janus.extension = usedDependencies.extension;
Janus.extension.init();
// Helper method to enumerate devices
Janus.listDevices = function (callback, config) {
callback = typeof callback == 'function' ? callback : Janus.noop;
if (!config) config = { audio: true, video: true };
if (Janus.isGetUserMediaAvailable()) {
navigator.mediaDevices
.getUserMedia(config)
.then(function (stream) {
navigator.mediaDevices.enumerateDevices().then(function (devices) {
Janus.debug(devices);
callback(devices);
// Get rid of the now useless stream
Janus.stopAllTracks(stream);
});
})
.catch(function (err) {
Janus.error(err);
callback([]);
});
} else {
Janus.warn('navigator.mediaDevices unavailable');
callback([]);
}
};
// Helper methods to attach/reattach a stream to a video element (previously part of adapter.js)
// Janus.attachMediaStream = function(element, stream) {
// try {
// element.srcObject = stream;
// } catch (e) {
// try {
// element.src = URL.createObjectURL(stream);
// } catch (e) {
// Janus.error("Error attaching stream to element", e);
// }
// }
// };
// Janus.reattachMediaStream = function(to, from) {
// try {
// to.srcObject = from.srcObject;
// } catch (e) {
// try {
// to.src = from.src;
// } catch (e) {
// Janus.error("Error reattaching stream to element", e);
// }
// }
// };
// Detect tab close: make sure we don't loose existing onbeforeunload handlers
// (note: for iOS we need to subscribe to a different event, 'pagehide', see
// https://gist.github.com/thehunmonkgroup/6bee8941a49b86be31a787fe8f4b8cfe)
// let iOS = ['iPad', 'iPhone', 'iPod'].indexOf(navigator.platform) >= 0;
// let eventName = iOS ? 'pagehide' : 'beforeunload';
// let oldOBF = window["on" + eventName];
// window.addEventListener(eventName, function() {
// Janus.log("Closing window");
// for(let s in Janus.sessions) {
// if(Janus.sessions[s] && Janus.sessions[s].destroyOnUnload) {
// Janus.log("Destroying session " + s);
// Janus.sessions[s].destroy({unload: true, notifyDestroyed: false});
// }
// }
// if(oldOBF && typeof oldOBF == "function") {
// oldOBF();
// }
// });
// If this is a Safari Technology Preview, check if VP8 is supported
Janus.safariVp8 = false;
if (Janus.webRTCAdapter.browserDetails.browser === 'safari' && Janus.webRTCAdapter.browserDetails.version >= 605) {
// Let's see if RTCRtpSender.getCapabilities() is there
if (
RTCRtpSender &&
RTCRtpSender.getCapabilities &&
RTCRtpSender.getCapabilities('video') &&
RTCRtpSender.getCapabilities('video').codecs &&
RTCRtpSender.getCapabilities('video').codecs.length
) {
for (let codec of RTCRtpSender.getCapabilities('video').codecs) {
if (codec && codec.mimeType && codec.mimeType.toLowerCase() === 'video/vp8') {
Janus.safariVp8 = true;
break;
}
}
if (Janus.safariVp8) {
Janus.log('This version of Safari supports VP8');
} else {
Janus.warn(
"This version of Safari does NOT support VP8: if you're using a Technology Preview, " +
"try enabling the 'WebRTC VP8 codec' setting in the 'Experimental Features' Develop menu"
);
}
} else {
// We do it in a very ugly way, as there's no alternative...
// We create a PeerConnection to see if VP8 is in an offer
let testpc = new RTCPeerConnection({});
testpc.createOffer({ offerToReceiveVideo: true }).then(function (offer) {
Janus.safariVp8 = offer.sdp.indexOf('VP8') !== -1;
if (Janus.safariVp8) {
Janus.log('This version of Safari supports VP8');
} else {
Janus.warn(
"This version of Safari does NOT support VP8: if you're using a Technology Preview, " +
"try enabling the 'WebRTC VP8 codec' setting in the 'Experimental Features' Develop menu"
);
}
testpc.close();
testpc = null;
});
}
}
Janus.initDone = true;
options.callback();
}
};
// Helper method to check whether WebRTC is supported by this browser
Janus.isWebrtcSupported = function () {
return !!RTCPeerConnection;
};
// Helper method to check whether devices can be accessed by this browser (e.g., not possible via plain HTTP)
Janus.isGetUserMediaAvailable = function () {
return navigator.mediaDevices && navigator.mediaDevices.getUserMedia;
};
// Helper method to create random identifiers (e.g., transaction)
Janus.randomString = function (len) {
let charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let randomString = '';
for (let i = 0; i < len; i++) {
let randomPoz = Math.floor(Math.random() * charSet.length);
randomString += charSet.charAt(randomPoz);
}
return randomString;
};
function Janus(gatewayCallbacks) {
gatewayCallbacks = gatewayCallbacks || {};
gatewayCallbacks.success = typeof gatewayCallbacks.success == 'function' ? gatewayCallbacks.success : Janus.noop;
gatewayCallbacks.error = typeof gatewayCallbacks.error == 'function' ? gatewayCallbacks.error : Janus.noop;
gatewayCallbacks.destroyed =
typeof gatewayCallbacks.destroyed == 'function' ? gatewayCallbacks.destroyed : Janus.noop;
if (!Janus.initDone) {
gatewayCallbacks.error('Library not initialized');
return {};
}
if (!Janus.isWebrtcSupported()) {
gatewayCallbacks.error('WebRTC not supported by this browser');
return {};
}
Janus.log('Library initialized: ' + Janus.initDone);
if (!gatewayCallbacks.server) {
gatewayCallbacks.error('Invalid server url');
return {};
}
let websockets = false;
let ws = null;
let wsHandlers = {};
let wsKeepaliveTimeoutId = null;
let servers = null;
let serversIndex = 0;
let server = gatewayCallbacks.server;
if (Janus.isArray(server)) {
Janus.log('Multiple servers provided (' + server.length + '), will use the first that works');
server = null;
servers = gatewayCallbacks.server;
Janus.debug(servers);
} else {
if (server.indexOf('ws') === 0) {
websockets = true;
Janus.log('Using WebSockets to contact Janus: ' + server);
} else {
websockets = false;
Janus.log('Using REST API to contact Janus: ' + server);
}
}
let iceServers = gatewayCallbacks.iceServers || [{ urls: 'stun:stun.l.google.com:19302' }];
let iceTransportPolicy = gatewayCallbacks.iceTransportPolicy;
let bundlePolicy = gatewayCallbacks.bundlePolicy;
// Whether we should enable the withCredentials flag for XHR requests
let withCredentials = false;
if (typeof gatewayCallbacks.withCredentials !== 'undefined' && gatewayCallbacks.withCredentials !== null)
withCredentials = gatewayCallbacks.withCredentials === true;
// Optional max events
let maxev = 10;
if (typeof gatewayCallbacks.max_poll_events !== 'undefined' && gatewayCallbacks.max_poll_events !== null)
maxev = gatewayCallbacks.max_poll_events;
if (maxev < 1) maxev = 1;
// Token to use (only if the token based authentication mechanism is enabled)
let token = null;
if (typeof gatewayCallbacks.token !== 'undefined' && gatewayCallbacks.token !== null) token = gatewayCallbacks.token;
// API secret to use (only if the shared API secret is enabled)
let apisecret = null;
if (typeof gatewayCallbacks.apisecret !== 'undefined' && gatewayCallbacks.apisecret !== null)
apisecret = gatewayCallbacks.apisecret;
// Whether we should destroy this session when onbeforeunload is called
this.destroyOnUnload = true;
if (typeof gatewayCallbacks.destroyOnUnload !== 'undefined' && gatewayCallbacks.destroyOnUnload !== null)
this.destroyOnUnload = gatewayCallbacks.destroyOnUnload === true;
// Some timeout-related values
let keepAlivePeriod = 25000;
if (typeof gatewayCallbacks.keepAlivePeriod !== 'undefined' && gatewayCallbacks.keepAlivePeriod !== null)
keepAlivePeriod = gatewayCallbacks.keepAlivePeriod;
if (isNaN(keepAlivePeriod)) keepAlivePeriod = 25000;
let longPollTimeout = 60000;
if (typeof gatewayCallbacks.longPollTimeout !== 'undefined' && gatewayCallbacks.longPollTimeout !== null)
longPollTimeout = gatewayCallbacks.longPollTimeout;
if (isNaN(longPollTimeout)) longPollTimeout = 60000;
// overrides for default maxBitrate values for simulcasting
function getMaxBitrates(simulcastMaxBitrates) {
let maxBitrates = {
high: 900000,
medium: 300000,
low: 100000,
};
if (typeof simulcastMaxBitrates !== 'undefined' && simulcastMaxBitrates !== null) {
if (simulcastMaxBitrates.high) maxBitrates.high = simulcastMaxBitrates.high;
if (simulcastMaxBitrates.medium) maxBitrates.medium = simulcastMaxBitrates.medium;
if (simulcastMaxBitrates.low) maxBitrates.low = simulcastMaxBitrates.low;
}
return maxBitrates;
}
let connected = false;
let sessionId = null;
let pluginHandles = {};
let that = this;
let retries = 0;
let transactions = {};
createSession(gatewayCallbacks);
// Public methods
this.getServer = function () {
return server;
};
this.isConnected = function () {
return connected;
};
this.reconnect = function (callbacks) {
callbacks = callbacks || {};
callbacks.success = typeof callbacks.success == 'function' ? callbacks.success : Janus.noop;
callbacks.error = typeof callbacks.error == 'function' ? callbacks.error : Janus.noop;
callbacks['reconnect'] = true;
createSession(callbacks);
};
this.getSessionId = function () {
return sessionId;
};
this.getInfo = function (callbacks) {
getInfo(callbacks);
};
this.destroy = function (callbacks) {
destroySession(callbacks);
};
this.attach = function (callbacks) {
createHandle(callbacks);
};
function eventHandler() {
if (sessionId == null) return;
Janus.debug('Long poll...');
if (!connected) {
Janus.warn('Is the server down? (connected=false)');
return;
}
let longpoll = server + '/' + sessionId + '?rid=' + new Date().getTime();
if (maxev) longpoll = longpoll + '&maxev=' + maxev;
if (token) longpoll = longpoll + '&token=' + encodeURIComponent(token);
if (apisecret) longpoll = longpoll + '&apisecret=' + encodeURIComponent(apisecret);
Janus.httpAPICall(longpoll, {
verb: 'GET',
withCredentials: withCredentials,
success: handleEvent,
timeout: longPollTimeout,
error: function (textStatus, errorThrown) {
Janus.error(textStatus + ':', errorThrown);
retries++;
if (retries > 3) {
// Did we just lose the server? :-(
connected = false;
gatewayCallbacks.error('Lost connection to the server (is it down?)');
return;
}
eventHandler();
},
});
}
// Private event handler: this will trigger plugin callbacks, if set
function handleEvent(json, skipTimeout) {
retries = 0;
if (!websockets && typeof sessionId !== 'undefined' && sessionId !== null && skipTimeout !== true) eventHandler();
if (!websockets && Janus.isArray(json)) {
// We got an array: it means we passed a maxev > 1, iterate on all objects
for (let i = 0; i < json.length; i++) {
handleEvent(json[i], true);
}
return;
}
if (json['janus'] === 'keepalive') {
// Nothing happened
Janus.vdebug('Got a keepalive on session ' + sessionId);
return;
} else if (json['janus'] === 'server_info') {
// Just info on the Janus instance
Janus.debug('Got info on the Janus instance');
Janus.debug(json);
const transaction = json['transaction'];
if (transaction) {
const reportSuccess = transactions[transaction];
if (reportSuccess) reportSuccess(json);
delete transactions[transaction];
}
return;
} else if (json['janus'] === 'ack') {
// Just an ack, we can probably ignore
Janus.debug('Got an ack on session ' + sessionId);
Janus.debug(json);
const transaction = json['transaction'];
if (transaction) {
const reportSuccess = transactions[transaction];
if (reportSuccess) reportSuccess(json);
delete transactions[transaction];
}
return;
} else if (json['janus'] === 'success') {
// Success!
Janus.debug('Got a success on session ' + sessionId);
Janus.debug(json);
const transaction = json['transaction'];
if (transaction) {
const reportSuccess = transactions[transaction];
if (reportSuccess) reportSuccess(json);
delete transactions[transaction];
}
return;
} else if (json['janus'] === 'trickle') {
// We got a trickle candidate from Janus
const sender = json['sender'];
if (!sender) {
Janus.warn('Missing sender...');
return;
}
const pluginHandle = pluginHandles[sender];
if (!pluginHandle) {
Janus.debug('This handle is not attached to this session');
return;
}
let candidate = json['candidate'];
Janus.debug('Got a trickled candidate on session ' + sessionId);
Janus.debug(candidate);
let config = pluginHandle.webrtcStuff;
if (config.pc && config.remoteSdp) {
// Add candidate right now
Janus.debug('Adding remote candidate:', candidate);
if (!candidate || candidate.completed === true) {
// end-of-candidates
config.pc.addIceCandidate(Janus.endOfCandidates);
} else {
// New candidate
config.pc.addIceCandidate(candidate);
}
} else {
// We didn't do setRemoteDescription (trickle got here before the offer?)
Janus.debug("We didn't do setRemoteDescription (trickle got here before the offer?), caching candidate");
if (!config.candidates) config.candidates = [];
config.candidates.push(candidate);
Janus.debug(config.candidates);
}
} else if (json['janus'] === 'webrtcup') {
// The PeerConnection with the server is up! Notify this
Janus.debug('Got a webrtcup event on session ' + sessionId);
Janus.debug(json);
const sender = json['sender'];
if (!sender) {
Janus.warn('Missing sender...');
return;
}
const pluginHandle = pluginHandles[sender];
if (!pluginHandle) {
Janus.debug('This handle is not attached to this session');
return;
}
pluginHandle.webrtcState(true);
return;
} else if (json['janus'] === 'hangup') {
// A plugin asked the core to hangup a PeerConnection on one of our handles
Janus.debug('Got a hangup event on session ' + sessionId);
Janus.debug(json);
const sender = json['sender'];
if (!sender) {
Janus.warn('Missing sender...');
return;
}
const pluginHandle = pluginHandles[sender];
if (!pluginHandle) {
Janus.debug('This handle is not attached to this session');
return;
}
pluginHandle.webrtcState(false, json['reason']);
pluginHandle.hangup();
} else if (json['janus'] === 'detached') {
// A plugin asked the core to detach one of our handles
Janus.debug('Got a detached event on session ' + sessionId);
Janus.debug(json);
const sender = json['sender'];
if (!sender) {
Janus.warn('Missing sender...');
return;
}
const pluginHandle = pluginHandles[sender];
if (!pluginHandle) {
// Don't warn here because destroyHandle causes this situation.
return;
}
pluginHandle.ondetached();
pluginHandle.detach();
} else if (json['janus'] === 'media') {
// Media started/stopped flowing
Janus.debug('Got a media event on session ' + sessionId);
Janus.debug(json);
const sender = json['sender'];
if (!sender) {
Janus.warn('Missing sender...');
return;
}
const pluginHandle = pluginHandles[sender];
if (!pluginHandle) {
Janus.debug('This handle is not attached to this session');
return;
}
pluginHandle.mediaState(json['type'], json['receiving'], json['mid']);
} else if (json['janus'] === 'slowlink') {
Janus.debug('Got a slowlink event on session ' + sessionId);
Janus.debug(json);
// Trouble uplink or downlink
const sender = json['sender'];
if (!sender) {
Janus.warn('Missing sender...');
return;
}
const pluginHandle = pluginHandles[sender];
if (!pluginHandle) {
Janus.debug('This handle is not attached to this session');
return;
}
pluginHandle.slowLink(json['uplink'], json['lost'], json['mid']);
} else if (json['janus'] === 'error') {
// Oops, something wrong happened
Janus.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason); // FIXME
Janus.debug(json);
let transaction = json['transaction'];
if (transaction) {
let reportSuccess = transactions[transaction];
if (reportSuccess) {
reportSuccess(json);
}
delete transactions[transaction];
}
return;
} else if (json['janus'] === 'event') {
Janus.debug('Got a plugin event on session ' + sessionId);
Janus.debug(json);
const sender = json['sender'];
if (!sender) {
Janus.warn('Missing sender...');
return;
}
let plugindata = json['plugindata'];
if (!plugindata) {
Janus.warn('Missing plugindata...');
return;
}
Janus.debug(' -- Event is coming from ' + sender + ' (' + plugindata['plugin'] + ')');
let data = plugindata['data'];
Janus.debug(data);
const pluginHandle = pluginHandles[sender];
if (!pluginHandle) {
Janus.warn('This handle is not attached to this session');
return;
}
let jsep = json['jsep'];
if (jsep) {
Janus.debug('Handling SDP as well...');
Janus.debug(jsep);
}
let callback = pluginHandle.onmessage;
if (callback) {
Janus.debug('Notifying application...');
// Send to callback specified when attaching plugin handle
callback(data, jsep);
} else {
// Send to generic callback (?)
Janus.debug('No provided notification callback');
}
} else if (json['janus'] === 'timeout') {
Janus.error('Timeout on session ' + sessionId);
Janus.debug(json);
if (websockets) {
ws.close(3504, 'Gateway timeout');
}
return;
} else {
Janus.warn("Unknown message/event '" + json['janus'] + "' on session " + sessionId);
Janus.debug(json);
}
}
// Private helper to send keep-alive messages on WebSockets
function keepAlive() {
if (!server || !websockets || !connected) return;
wsKeepaliveTimeoutId = setTimeout(keepAlive, keepAlivePeriod);
let request = {
janus: 'keepalive',
session_id: sessionId,
transaction: Janus.randomString(12),
};
if (token) request['token'] = token;
if (apisecret) request['apisecret'] = apisecret;
ws.send(JSON.stringify(request));
}
// Private method to create a session
function createSession(callbacks) {
let transaction = Janus.randomString(12);
let request = { janus: 'create', transaction: transaction };
if (callbacks['reconnect']) {
// We're reconnecting, claim the session
connected = false;
request['janus'] = 'claim';
request['session_id'] = sessionId;
// If we were using websockets, ignore the old connection
if (ws) {
ws.onopen = null;
ws.onerror = null;
ws.onclose = null;
if (wsKeepaliveTimeoutId) {
clearTimeout(wsKeepaliveTimeoutId);
wsKeepaliveTimeoutId = null;
}
}
}
if (token) request['token'] = token;
if (apisecret) request['apisecret'] = apisecret;
if (!server && Janus.isArray(servers)) {
// We still need to find a working server from the list we were given
server = servers[serversIndex];
if (server.indexOf('ws') === 0) {
websockets = true;
Janus.log('Server #' + (serversIndex + 1) + ': trying WebSockets to contact Janus (' + server + ')');
} else {
websockets = false;
Janus.log('Server #' + (serversIndex + 1) + ': trying REST API to contact Janus (' + server + ')');
}
}
if (websockets) {
ws = Janus.newWebSocket(server, 'janus-protocol');
wsHandlers = {
error: function () {
Janus.error('Error connecting to the Janus WebSockets server... ' + server);
if (Janus.isArray(servers) && !callbacks['reconnect']) {
serversIndex++;
if (serversIndex === servers.length) {
// We tried all the servers the user gave us and they all failed
callbacks.error('Error connecting to any of the provided Janus servers: Is the server down?');
return;
}
// Let's try the next server
server = null;
setTimeout(function () {
createSession(callbacks);
}, 200);
return;
}
callbacks.error('Error connecting to the Janus WebSockets server: Is the server down?');
},
open: function () {
// We need to be notified about the success
transactions[transaction] = function (json) {
Janus.debug(json);
if (json['janus'] !== 'success') {
Janus.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason); // FIXME
callbacks.error(json['error'].reason);
return;
}
wsKeepaliveTimeoutId = setTimeout(keepAlive, keepAlivePeriod);
connected = true;
sessionId = json['session_id'] ? json['session_id'] : json.data['id'];
if (callbacks['reconnect']) {
Janus.log('Claimed session: ' + sessionId);
} else {
Janus.log('Created session: ' + sessionId);
}
Janus.sessions[sessionId] = that;
callbacks.success();
};
ws.send(JSON.stringify(request));
},
message: function (event) {
handleEvent(JSON.parse(event.data));
},
close: function () {
if (!server || !connected) {
return;
}
connected = false;
// FIXME What if this is called when the page is closed?
gatewayCallbacks.error('Lost connection to the server (is it down?)');
},
};
for (let eventName in wsHandlers) {
ws.addEventListener(eventName, wsHandlers[eventName]);
}
return;
}
Janus.httpAPICall(server, {
verb: 'POST',
withCredentials: withCredentials,
body: request,
success: function (json) {
Janus.debug(json);
if (json['janus'] !== 'success') {
Janus.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason); // FIXME
callbacks.error(json['error'].reason);
return;
}
connected = true;
sessionId = json['session_id'] ? json['session_id'] : json.data['id'];
if (callbacks['reconnect']) {
Janus.log('Claimed session: ' + sessionId);
} else {
Janus.log('Created session: ' + sessionId);
}
Janus.sessions[sessionId] = that;
eventHandler();
callbacks.success();
},
error: function (textStatus, errorThrown) {
Janus.error(textStatus + ':', errorThrown); // FIXME
if (Janus.isArray(servers) && !callbacks['reconnect']) {
serversIndex++;
if (serversIndex === servers.length) {
// We tried all the servers the user gave us and they all failed
callbacks.error('Error connecting to any of the provided Janus servers: Is the server down?');
return;
}
// Let's try the next server
server = null;
setTimeout(function () {
createSession(callbacks);
}, 200);
return;
}
if (errorThrown === '') callbacks.error(textStatus + ': Is the server down?');
else if (errorThrown && errorThrown.error) callbacks.error(textStatus + ': ' + errorThrown.error.message);
else callbacks.error(textStatus + ': ' + errorThrown);
},
});
}
// Private method to get info on the server
function getInfo(callbacks) {
callbacks = callbacks || {};
// FIXME This method triggers a success even when we fail
callbacks.success = typeof callbacks.success == 'function' ? callbacks.success : Janus.noop;
callbacks.error = typeof callbacks.error == 'function' ? callbacks.error : Janus.noop;
Janus.log('Getting info on Janus instance');
if (!connected) {
Janus.warn('Is the server down? (connected=false)');
callbacks.error('Is the server down? (connected=false)');
return;
}
// We just need to send an "info" request
let transaction = Janus.randomString(12);
let request = { janus: 'info', transaction: transaction };
if (token) request['token'] = token;
if (apisecret) request['apisecret'] = apisecret;
if (websockets) {
transactions[transaction] = function (json) {
Janus.log('Server info:');
Janus.debug(json);
if (json['janus'] !== 'server_info') {
Janus.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason); // FIXME
}
callbacks.success(json);
};
ws.send(JSON.stringify(request));
return;
}
Janus.httpAPICall(server, {
verb: 'POST',
withCredentials: withCredentials,
body: request,
success: function (json) {
Janus.log('Server info:');
Janus.debug(json);
if (json['janus'] !== 'server_info') {
Janus.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason); // FIXME
}
callbacks.success(json);
},
error: function (textStatus, errorThrown) {
Janus.error(textStatus + ':', errorThrown); // FIXME
if (errorThrown === '') callbacks.error(textStatus + ': Is the server down?');
else callbacks.error(textStatus + ': ' + errorThrown);
},
});
}
// Private method to destroy a session
function destroySession(callbacks) {
callbacks = callbacks || {};
// FIXME This method triggers a success even when we fail
callbacks.success = typeof callbacks.success == 'function' ? callbacks.success : Janus.noop;
callbacks.error = typeof callbacks.error == 'function' ? callbacks.error : Janus.noop;
let unload = callbacks.unload === true;
let notifyDestroyed = true;
if (typeof callbacks.notifyDestroyed !== 'undefined' && callbacks.notifyDestroyed !== null)
notifyDestroyed = callbacks.notifyDestroyed === true;
let cleanupHandles = callbacks.cleanupHandles === true;
Janus.log('Destroying session ' + sessionId + ' (unload=' + unload + ')');
if (!sessionId) {
Janus.warn('No session to destroy');
callbacks.success();
if (notifyDestroyed) gatewayCallbacks.destroyed();
return;
}
if (cleanupHandles) {
for (let handleId in pluginHandles) destroyHandle(handleId, { noRequest: true });
}
if (!connected) {
Janus.warn('Is the server down? (connected=false)');
sessionId = null;
callbacks.success();
return;
}
// No need to destroy all handles first, Janus will do that itself
let request = { janus: 'destroy', transaction: Janus.randomString(12) };
if (token) request['token'] = token;
if (apisecret) request['apisecret'] = apisecret;
if (unload) {
// We're unloading the page: use sendBeacon for HTTP instead,
// or just close the WebSocket connection if we're using that
if (websockets) {
ws.onclose = null;
ws.close();
ws = null;
} else {
navigator.sendBeacon(server + '/' + sessionId, JSON.stringify(request));
}
Janus.log('Destroyed session:');
sessionId = null;
connected = false;
callbacks.success();
if (notifyDestroyed) gatewayCallbacks.destroyed();
return;
}
if (websockets) {
request['session_id'] = sessionId;
let unbindWebSocket = function () {
for (let eventName in wsHandlers) {
ws.removeEventListener(eventName, wsHandlers[eventName]);
}
ws.removeEventListener('message', onUnbindMessage);
ws.removeEventListener('error', onUnbindError);
if (wsKeepaliveTimeoutId) {
clearTimeout(wsKeepaliveTimeoutId);
}
ws.close();
};
let onUnbindMessage = function (event) {
let data = JSON.parse(event.data);
if (data.session_id == request.session_id && data.transaction == request.transaction) {
unbindWebSocket();
callbacks.success();
if (notifyDestroyed) gatewayCallbacks.destroyed();
}
};
let onUnbindError = function () {
unbindWebSocket();
callbacks.error('Failed to destroy the server: Is the server down?');
if (notifyDestroyed) gatewayCallbacks.destroyed();
};
ws.addEventListener('message', onUnbindMessage);
ws.addEventListener('error', onUnbindError);
if (ws.readyState === 1) {
ws.send(JSON.stringify(request));
} else {
onUnbindError();
}
return;
}
Janus.httpAPICall(server + '/' + sessionId, {
verb: 'POST',
withCredentials: withCredentials,
body: request,
success: function (json) {
Janus.log('Destroyed session:');
Janus.debug(json);
sessionId = null;
connected = false;
if (json['janus'] !== 'success') {
Janus.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason); // FIXME
}
callbacks.success();
if (notifyDestroyed) gatewayCallbacks.destroyed();
},
error: function (textStatus, errorThrown) {
Janus.error(textStatus + ':', errorThrown); // FIXME
// Reset everything anyway
sessionId = null;
connected = false;
callbacks.success();
if (notifyDestroyed) gatewayCallbacks.destroyed();
},
});
}
// Private method to create a plugin handle
function createHandle(callbacks) {
callbacks = callbacks || {};
callbacks.success = typeof callbacks.success == 'function' ? callbacks.success : Janus.noop;
callbacks.error = typeof callbacks.error == 'function' ? callbacks.error : Janus.noop;
callbacks.dataChannelOptions = callbacks.dataChannelOptions || {
ordered: true,
};
callbacks.consentDialog = typeof callbacks.consentDialog == 'function' ? callbacks.consentDialog : Janus.noop;
callbacks.iceState = typeof callbacks.iceState == 'function' ? callbacks.iceState : Janus.noop;
callbacks.mediaState = typeof callbacks.mediaState == 'function' ? callbacks.mediaState : Janus.noop;
callbacks.webrtcState = typeof callbacks.webrtcState == 'function' ? callbacks.webrtcState : Janus.noop;
callbacks.slowLink = typeof callbacks.slowLink == 'function' ? callbacks.slowLink : Janus.noop;
callbacks.onmessage = typeof callbacks.onmessage == 'function' ? callbacks.onmessage : Janus.noop;
callbacks.onlocaltrack = typeof callbacks.onlocaltrack == 'function' ? callbacks.onlocaltrack : Janus.noop;
callbacks.onremotetrack = typeof callbacks.onremotetrack == 'function' ? callbacks.onremotetrack : Janus.noop;
callbacks.ondata = typeof callbacks.ondata == 'function' ? callbacks.ondata : Janus.noop;
callbacks.ondataopen = typeof callbacks.ondataopen == 'function' ? callbacks.ondataopen : Janus.noop;
callbacks.oncleanup = typeof callbacks.oncleanup == 'function' ? callbacks.oncleanup : Janus.noop;
callbacks.ondetached = typeof callbacks.ondetached == 'function' ? callbacks.ondetached : Janus.noop;
if (!connected) {
Janus.warn('Is the server down? (connected=false)');
callbacks.error('Is the server down? (connected=false)');
return;
}
let plugin = callbacks.plugin;
if (!plugin) {
Janus.error('Invalid plugin');
callbacks.error('Invalid plugin');
return;
}
let opaqueId = callbacks.opaqueId;
let loopIndex = callbacks.loopIndex;
let handleToken = callbacks.token ? callbacks.token : token;
let transaction = Janus.randomString(12);
let request = {
janus: 'attach',
plugin: plugin,
opaque_id: opaqueId,
loop_index: loopIndex,
transaction: transaction,
};
if (handleToken) request['token'] = handleToken;
if (apisecret) request['apisecret'] = apisecret;
if (websockets) {
transactions[transaction] = function (json) {
Janus.debug(json);
if (json['janus'] !== 'success') {
Janus.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason); // FIXME
callbacks.error('Ooops: ' + json['error'].code + ' ' + json['error'].reason);
return;
}
let handleId = json.data['id'];
Janus.log('Created handle: ' + handleId);