summaryrefslogtreecommitdiffstats
path: root/webapp/client
diff options
context:
space:
mode:
authorenahum <nahumhbl@gmail.com>2017-01-03 12:08:21 -0300
committerCorey Hulen <corey@hulen.com>2017-01-03 10:08:21 -0500
commit144219964742c35eebf6d75ae27ad17732c89dae (patch)
tree499275f4079369e2e3da037926f5a841cd33961c /webapp/client
parent7cdfba62ef991f36363592a70f08db6a88b3c332 (diff)
downloadchat-144219964742c35eebf6d75ae27ad17732c89dae.tar.gz
chat-144219964742c35eebf6d75ae27ad17732c89dae.tar.bz2
chat-144219964742c35eebf6d75ae27ad17732c89dae.zip
PLT-4644 Update webrtc client (#4922)
* PLT-4644 Update webrtc client * update webrtc-adapter * Use default ice server if none is provided
Diffstat (limited to 'webapp/client')
-rw-r--r--webapp/client/webrtc_client.jsx145
-rw-r--r--webapp/client/webrtc_session.jsx1966
2 files changed, 0 insertions, 2111 deletions
diff --git a/webapp/client/webrtc_client.jsx b/webapp/client/webrtc_client.jsx
deleted file mode 100644
index 85204c25b..000000000
--- a/webapp/client/webrtc_client.jsx
+++ /dev/null
@@ -1,145 +0,0 @@
-// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-export default class WebrtcClient {
- constructor() {
- this.init = this.init.bind(this);
- this.listDevices = this.listDevices.bind(this);
- this.isExtensionEnabled = this.isExtensionEnabled.bind(this);
- this.isWebrtcSupported = this.isWebrtcSupported.bind(this);
-
- this.initDone = false;
- this.sessions = {};
- }
-
- noop() {} //eslint-disable-line no-empty-function
-
- // this function is going to be needed when we enable screen sharing
- isExtensionEnabled() {
- if (window.navigator.userAgent.match('Chrome')) {
- const 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 (document.getElementById('mattermost-extension-installed') !== null);
- }
-
- // Firefox of others, no need for the extension (but this doesn't mean it will work)
- return true;
- }
-
- init(opts) {
- const options = opts || {};
-
- if (this.initDone === true) {
- // Already initialized
- return;
- }
-
- this.trace = this.noop;
- this.debug = this.noop;
- this.log = this.noop;
- this.warn = this.noop;
- this.error = this.noop;
-
- /* eslint-disable */
- if (options.debug === true || options.debug === 'all') {
- // Enable all debugging levels
- this.trace = console.trace.bind(console);
- this.debug = console.debug.bind(console);
- this.log = console.log.bind(console);
- this.warn = console.warn.bind(console);
- this.error = console.error.bind(console);
- } else if (Array.isArray(options.debug)) {
- for (const i in options.debug) {
- if (options.debug.hasOwnProperty(i)) {
- const d = options.debug[i];
- switch (d) {
- case 'trace':
- this.trace = console.trace.bind(console);
- break;
- case 'debug':
- this.debug = console.debug.bind(console);
- break;
- case 'log':
- this.log = console.log.bind(console);
- break;
- case 'warn':
- this.warn = console.warn.bind(console);
- break;
- case 'error':
- this.error = console.error.bind(console);
- break;
- default:
- console.error("Unknown debugging option '" + d + "' (supported: 'trace', 'debug', 'log', warn', 'error')");
- break;
- }
- }
- }
- }
- /* eslint-enable */
-
- this.log('Initializing WebRTC Client library');
-
- // Detect tab close
- window.onbeforeunload = () => {
- this.log('Closing window');
- for (const s in this.sessions) {
- if (this.sessions.hasOwnProperty(s)) {
- if (this.sessions[s] && this.sessions[s].destroyOnUnload) {
- this.log('Destroying session ' + s);
- this.sessions[s].destroy();
- }
- }
- }
- };
-
- this.initDone = true;
- }
-
- // Helper method to enumerate devices
- listDevices(cb) {
- const callback = (typeof cb == 'function') ? cb : this.noop;
-
- if (navigator.mediaDevices) {
- navigator.getUserMedia({audio: true, video: true}, (stream) => {
- navigator.mediaDevices.enumerateDevices().then((devices) => {
- this.debug(devices);
- callback(devices);
-
- // Get rid of the now useless stream
- try {
- stream.stop();
- } catch (e) {
- this.error(e);
- }
-
- this.stopMedia(stream);
- });
- }, (err) => {
- this.error(err);
- callback([]);
- });
- } else {
- this.warn('navigator.mediaDevices unavailable');
- callback([]);
- }
- }
-
- // Helper method to check whether WebRTC is supported by this browser
- isWebrtcSupported() {
- return window.RTCPeerConnection && navigator.getUserMedia;
- }
-
- stopMedia(stream) {
- const tracks = stream.getTracks();
- tracks.forEach((track) => {
- track.stop();
- });
- }
-} \ No newline at end of file
diff --git a/webapp/client/webrtc_session.jsx b/webapp/client/webrtc_session.jsx
deleted file mode 100644
index df60a1053..000000000
--- a/webapp/client/webrtc_session.jsx
+++ /dev/null
@@ -1,1966 +0,0 @@
-// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import adapter from 'webrtc-adapter';
-import WebrtcClient from './webrtc_client.jsx';
-const transationLength = 12;
-
-export default class WebrtcSession {
- static randomString(len) {
- const charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
- let randomString = '';
- for (let i = 0; i < len; i++) {
- const randomPoz = Math.floor(Math.random() * charSet.length);
- randomString += charSet.substring(randomPoz, randomPoz + 1);
- }
- return randomString;
- }
-
- static getLocalMedia(constraints, element, callback) {
- const media = constraints || {audio: true, video: true};
- navigator.mediaDevices.getUserMedia(media).
- then((stream) => {
- if (element) {
- element.srcObject = stream;
- }
-
- if (callback && typeof callback === 'function') {
- callback(null, stream);
- }
- }).
- catch((error) => {
- callback(error);
- });
- }
-
- static stopMediaStream(stream) {
- const tracks = stream.getTracks();
- tracks.forEach((track) => {
- track.stop();
- });
- }
-
- constructor(opts) {
- // super();
- const options = opts || {};
- this.getServer = this.getServer.bind(this);
- this.isConnected = this.isConnected.bind(this);
- this.getSessionId = this.getSessionId.bind(this);
- this.handleEvent = this.handleEvent.bind(this);
- this.keepAlive = this.keepAlive.bind(this);
- this.createSession = this.createSession.bind(this);
- this.attach = this.attach.bind(this);
- this.sendMessage = this.sendMessage.bind(this);
- this.sendTrickleCandidate = this.sendTrickleCandidate.bind(this);
- this.sendData = this.sendData.bind(this);
- this.sendDtmf = this.sendDtmf.bind(this);
- this.destroy = this.destroy.bind(this);
- this.destroyHandle = this.destroyHandle.bind(this);
- this.streamsDone = this.streamsDone.bind(this);
- this.prepareWebrtc = this.prepareWebrtc.bind(this);
- this.prepareWebrtcPeer = this.prepareWebrtcPeer.bind(this);
- this.createOffer = this.createOffer.bind(this);
- this.createAnswer = this.createAnswer.bind(this);
- this.sendSDP = this.sendSDP.bind(this);
- this.getVolume = this.getVolume.bind(this);
- this.isMuted = this.isMuted.bind(this);
- this.mute = this.mute.bind(this);
- this.getBitrate = this.getBitrate.bind(this);
- this.webrtcError = this.webrtcError.bind(this);
- this.cleanupWebrtc = this.cleanupWebrtc.bind(this);
- this.isAudioSendEnabled = this.isAudioSendEnabled.bind(this);
- this.isAudioRecvEnabled = this.isAudioRecvEnabled.bind(this);
- this.isVideoSendEnabled = this.isVideoSendEnabled.bind(this);
- this.isVideoRecvEnabled = this.isVideoRecvEnabled.bind(this);
- this.isDataEnabled = this.isDataEnabled.bind(this);
- this.isTrickleEnabled = this.isTrickleEnabled.bind(this);
- this.unbindWebSocket = this.unbindWebSocket.bind(this);
-
- this.websockets = false;
- this.ws = null;
- this.wsHandlers = {};
- this.wsKeepaliveTimeoutId = null;
- this.servers = null;
- this.server = null;
- this.serversIndex = 0;
- this.connected = false;
- this.sessionId = null;
- this.pluginHandles = {};
- this.retries = 0;
- this.transactions = {};
-
- this.client = new WebrtcClient();
- this.client.init({debug: options.debug});
-
- this.gatewayCallbacks = options || {};
- this.gatewayCallbacks.success = (typeof options.success == 'function') ? options.success : this.client.noop;
- this.gatewayCallbacks.error = (typeof options.error == 'function') ? options.error : this.client.noop;
- this.gatewayCallbacks.destroyed = (typeof options.destroyed == 'function') ? options.destroyed : this.client.noop;
-
- if (!this.client.initDone) {
- this.gatewayCallbacks.error('webrtc_client.not_initialize', 'Library not initialized');
- return {};
- }
-
- if (!this.client.isWebrtcSupported()) {
- this.gatewayCallbacks.error('webrtc_client.browser.not_supported', 'WebRTC not supported by this browser');
- return {};
- }
-
- this.client.log('Library initialized: ' + this.client.initDone);
-
- if (!options.server) {
- this.gatewayCallbacks.error('webrtc_client.invalid_gateway', 'Invalid gateway url');
- return {};
- }
-
- if (Array.isArray(options.server)) {
- this.client.log('Multiple servers provided (' + options.server.length + '), will use the first that works');
- for (let i = 0; i < options.server; i++) {
- const server = options.server[i];
- if (server.indexOf('ws') !== 0) {
- this.gatewayCallbacks.error('webrtc_client.must_be_websocket', 'every server provided must be a websocket');
- return {};
- }
- }
- this.servers = options.server;
- this.client.debug(this.servers);
- } else if (options.server.indexOf('ws') === 0) {
- this.websockets = true;
- this.servers = [options.server];
- this.client.log('Using WebSockets to contact Janus: ' + options.server);
- } else {
- this.gatewayCallbacks.error('webrtc_client.invalid_websocket', 'This library must connect to a websocket');
- return {};
- }
-
- this.iceServers = options.iceServers;
- if (!this.iceServers || this.iceServers.length === 0) {
- this.iceServers = [{url: 'stun:stun.l.google.com:19302'}];
- }
-
- // Optional max events
- this.maxev = null;
- if (options.max_poll_events) {
- this.maxev = options.max_poll_events;
- }
- if (this.maxev < 1) {
- this.maxev = 1;
- }
-
- // Token to use (only if the token based authentication mechanism is enabled)
- this.token = null;
- if (options.token) {
- this.token = options.token;
- }
-
- // API secret to use (only if the shared API secret is enabled)
- this.apisecret = null;
- if (options.apisecret) {
- this.apisecret = options.apisecret;
- }
-
- // Whether we should destroy this session when onbeforeunload is called
- this.destroyOnUnload = options.destroyOnUnload !== false;
- this.createSession();
-
- return this;
- }
-
- getServer() {
- return this.server;
- }
-
- isConnected() {
- return this.connected;
- }
-
- getSessionId() {
- return this.sessionId;
- }
-
- handleEvent(json) {
- this.retries = 0;
- this.client.debug('Got event on session ' + this.sessionId);
- this.client.debug(json);
- const transaction = json.transaction;
- const sender = json.sender;
- const plugindata = json.plugindata;
- const jsep = json.jsep;
- switch (json.janus) {
- case 'keepalive':
- // Nothing happened
- break;
- case 'ack':
- case 'success':
- // Success or just an ack, we can probably ignore
- if (transaction) {
- const reportSuccess = this.transactions[transaction];
- if (reportSuccess) {
- reportSuccess(json);
- }
- Reflect.deleteProperty(this.transactions, transaction);
- }
- break;
- case 'webrtcup':
- // The PeerConnection with the gateway is up! Notify this
- if (sender) {
- const pluginHandle = this.pluginHandles[sender];
- if (pluginHandle) {
- pluginHandle.webrtcState(true);
- } else {
- this.client.warn('This handle is not attached to this session');
- }
- } else {
- this.client.warn('Missing sender...');
- }
- break;
- case 'hangup':
- // A plugin asked the core to hangup a PeerConnection on one of our handles
- if (sender) {
- const pluginHandle = this.pluginHandles[sender];
- if (pluginHandle) {
- pluginHandle.webrtcState(false);
- pluginHandle.hangup();
- } else {
- this.client.warn('This handle is not attached to this session');
- }
- } else {
- this.client.warn('Missing sender...');
- }
- break;
- case 'detached':
- // A plugin asked the core to detach one of our handles
- if (sender) {
- const pluginHandle = this.pluginHandles[sender];
- if (pluginHandle) {
- pluginHandle.ondetached();
- pluginHandle.detach();
- } else {
- this.client.warn('This handle is not attached to this session');
- }
- } else {
- this.client.warn('Missing sender...');
- }
- break;
- case 'media':
- // Media started/stopped flowing
- if (sender) {
- const pluginHandle = this.pluginHandles[sender];
- if (pluginHandle) {
- pluginHandle.mediaState(json.type, json.receiving);
- } else {
- this.client.warn('This handle is not attached to this session');
- }
- } else {
- this.client.warn('Missing sender...');
- }
- break;
- case 'error':
- // Oops, something wrong happened
- this.client.error('Ooops: ' + json.error.code + ' ' + json.error.reason);
- if (transaction) {
- const reportSuccess = this.transactions[transaction];
- if (reportSuccess) {
- reportSuccess(json);
- }
- Reflect.deleteProperty(this.transactions, transaction);
- }
- break;
- case 'event':
- if (sender) {
- if (plugindata) {
- this.client.debug(` -- Event is coming from ${sender} ( ${plugindata.plugin} )`);
- const data = plugindata.data;
- this.client.debug(data);
- const pluginHandle = this.pluginHandles[sender];
- if (pluginHandle) {
- pluginHandle.mediaState(json.type, json.receiving);
- if (jsep) {
- this.client.debug('Handling SDP as well...');
- this.client.debug(jsep);
- }
- const callback = pluginHandle.onmessage;
- if (callback) {
- this.client.debug('Notifying application...');
-
- // Send to callback specified when attaching plugin handle
- callback(data, jsep);
- } else {
- // Send to generic callback (?)
- this.client.debug('No provided notification callback');
- }
- } else {
- this.client.warn('This handle is not attached to this session');
- }
- } else {
- this.client.warn('Missing plugindata...');
- }
- } else {
- this.client.warn('Missing sender...');
- }
- break;
- default:
- this.client.warn(`Unknown message "${json.janus}"`);
- break;
- }
- }
-
- keepAlive() {
- if (this.server === null || !this.websockets || !this.connected) {
- return;
- }
- this.wsKeepaliveTimeoutId = setTimeout(this.keepAlive, 30000);
-
- const request = {
- janus: 'keepalive',
- session_id: this.sessionId,
- transaction: WebrtcSession.randomString(transationLength)
- };
-
- if (this.token) {
- request.token = this.token;
- }
-
- if (this.apisecret) {
- request.apisecret = this.apisecret;
- }
-
- this.ws.send(JSON.stringify(request));
- }
-
- createSession() {
- const transaction = WebrtcSession.randomString(transationLength);
- const request = {
- janus: 'create',
- transaction
- };
-
- if (this.token) {
- request.token = this.token;
- }
-
- if (this.apisecret) {
- request.apisecret = this.apisecret;
- }
-
- if (this.server === null && Array.isArray(this.servers)) {
- // We still need to find a working server from the list we were given
- this.server = this.servers[this.serversIndex];
- if (this.server.indexOf('ws') === 0) {
- this.websockets = true;
- this.client.log('Server #' + (this.serversIndex + 1) + ': trying WebSockets to contact Janus (' + this.server + ')');
- }
- }
-
- if (this.websockets) {
- this.ws = new WebSocket(this.server, 'janus-protocol');
- this.wsHandlers = {
- error: () => {
- this.client.error('Error connecting to the Janus WebSockets server... ' + this.server);
- if (Array.isArray(this.servers)) {
- this.serversIndex++;
- if (this.serversIndex === this.servers.length) {
- // We tried all the servers the user gave us and they all failed
- this.gatewayCallbacks.error('webrtc_client.cannot_connect_servers', 'Error connecting to any of the provided Janus servers: Is the gateway down?');
- return;
- }
-
- // Let's try the next server
- this.server = null;
- setTimeout(() => {
- this.createSession();
- }, 200);
- return;
- }
- this.gatewayCallbacks.error('webrtc_client.cannot_connect_server', 'Error connecting to the Janus WebSockets server: Is the gateway down?');
- },
-
- open: () => {
- // We need to be notified about the success
- this.transactions[transaction] = (json) => {
- this.client.debug(json);
- if (json.janus !== 'success') {
- this.client.error('Ooops: ' + json.error.code + ' ' + json.error.reason); // FIXME
- this.gatewayCallbacks.error(json.error.reason);
- return;
- }
- this.wsKeepaliveTimeoutId = setTimeout(this.keepAlive, 30000);
- this.connected = true;
- this.sessionId = json.data.id;
- this.client.log('Created session: ' + this.sessionId);
- this.client.sessions[this.sessionId] = this;
- this.gatewayCallbacks.success();
- };
- this.ws.send(JSON.stringify(request));
- },
-
- message: (event) => {
- this.handleEvent(JSON.parse(event.data));
- },
-
- close: () => {
- if (!this.connected) {
- return;
- }
- this.connected = false;
-
- // FIXME What if this is called when the page is closed?
- this.gatewayCallbacks.error('Lost connection to the gateway (is it down?)');
- }
- };
-
- for (var eventName in this.wsHandlers) {
- if (this.wsHandlers.hasOwnProperty(eventName)) {
- this.ws.addEventListener(eventName, this.wsHandlers[eventName]);
- }
- }
- }
- }
-
- attach(cbs) {
- const callbacks = cbs || {};
- callbacks.success = (typeof cbs.success == 'function') ? cbs.success : this.client.noop;
- callbacks.error = (typeof cbs.error == 'function') ? cbs.error : this.client.noop;
- callbacks.consentDialog = (typeof cbs.consentDialog == 'function') ? cbs.consentDialog : this.client.noop;
- callbacks.mediaState = (typeof cbs.mediaState == 'function') ? cbs.mediaState : this.client.noop;
- callbacks.webrtcState = (typeof cbs.webrtcState == 'function') ? cbs.webrtcState : this.client.noop;
- callbacks.onmessage = (typeof cbs.onmessage == 'function') ? cbs.onmessage : this.client.noop;
- callbacks.onlocalstream = (typeof cbs.onlocalstream == 'function') ? cbs.onlocalstream : this.client.noop;
- callbacks.onremotestream = (typeof cbs.onremotestream == 'function') ? cbs.onremotestream : this.client.noop;
- callbacks.ondata = (typeof cbs.ondata == 'function') ? cbs.ondata : this.client.noop;
- callbacks.ondataopen = (typeof cbs.ondataopen == 'function') ? cbs.ondataopen : this.client.noop;
- callbacks.oncleanup = (typeof cbs.oncleanup == 'function') ? cbs.oncleanup : this.client.noop;
- callbacks.ondetached = (typeof cbs.ondetached == 'function') ? cbs.ondetached : this.client.noop;
-
- if (!this.connected) {
- this.client.warn('Is the gateway down? (connected=false)');
- callbacks.error('Is the gateway down? (connected=false)');
- return;
- }
-
- const plugin = callbacks.plugin;
- if (!plugin) {
- this.client.error('Invalid plugin');
- callbacks.error('Invalid plugin');
- return;
- }
-
- const transaction = WebrtcSession.randomString(transationLength);
- const request = {
- janus: 'attach',
- plugin,
- transaction
- };
-
- if (this.token) {
- request.token = this.token;
- }
-
- if (this.apisecret) {
- request.apisecret = this.apisecret;
- }
-
- if (this.websockets) {
- this.transactions[transaction] = (json) => {
- this.client.debug(json);
-
- if (json.janus !== 'success') {
- const error = `Ooops: ${json.error.code} ${json.error.reason}`;
- this.client.error(error);
- callbacks.error(error);
- return;
- }
-
- const handleId = json.data.id;
-
- this.client.log('Created handle: ' + handleId);
- const pluginHandle = {
- session: this,
- plugin,
- id: handleId,
- webrtcStuff: {
- started: false,
- myStream: null,
- streamExternal: false,
- remoteStream: null,
- mySdp: null,
- pc: null,
- dataChannel: null,
- dtmfSender: null,
- trickle: true,
- iceDone: false,
- sdpSent: false,
- volume: {
- value: null,
- timer: null
- },
- bitrate: {
- value: null,
- bsnow: null,
- bsbefore: null,
- tsnow: null,
- tsbefore: null,
- timer: null
- }
- },
- getId: () => {
- return handleId;
- },
- getPlugin: () => {
- return plugin;
- },
- getVolume: () => {
- return this.getVolume(handleId);
- },
- isAudioMuted: () => {
- return this.isMuted(handleId, false);
- },
- muteAudio: () => {
- return this.mute(handleId, false, true);
- },
- unmuteAudio: () => {
- return this.mute(handleId, false, false);
- },
- isVideoMuted: () => {
- return this.isMuted(handleId, true);
- },
- muteVideo: () => {
- return this.mute(handleId, true, true);
- },
- unmuteVideo: () => {
- return this.mute(handleId, true, false);
- },
- getBitrate: () => {
- return this.getBitrate(handleId);
- },
- send: (cb) => {
- this.sendMessage(handleId, cb);
- },
- data: (cb) => {
- this.sendData(handleId, cb);
- },
- dtmf: (cb) => {
- this.sendDtmf(handleId, cb);
- },
- consentDialog: callbacks.consentDialog,
- mediaState: callbacks.mediaState,
- webrtcState: callbacks.webrtcState,
- onmessage: callbacks.onmessage,
- createOffer: (cb) => {
- this.prepareWebrtc(handleId, cb);
- },
- createAnswer: (cb) => {
- this.prepareWebrtc(handleId, cb);
- },
- handleRemoteJsep: (cb) => {
- this.prepareWebrtcPeer(handleId, cb);
- },
- onlocalstream: callbacks.onlocalstream,
- onremotestream: callbacks.onremotestream,
- ondata: callbacks.ondata,
- ondataopen: callbacks.ondataopen,
- oncleanup: callbacks.oncleanup,
- ondetached: callbacks.ondetached,
- hangup: (sendRequest) => {
- this.cleanupWebrtc(handleId, sendRequest === true);
- },
- detach: (cb) => {
- this.destroyHandle(handleId, cb);
- }
- };
- this.pluginHandles[handleId] = pluginHandle;
- callbacks.success(pluginHandle);
- };
- request.session_id = this.sessionId;
- this.ws.send(JSON.stringify(request));
- }
- }
-
- sendMessage(handleId, cbs) {
- const callbacks = cbs || {};
- callbacks.success = (typeof cbs.success == 'function') ? cbs.success : this.client.noop;
- callbacks.error = (typeof cbs.error == 'function') ? cbs.error : this.client.noop;
-
- if (!this.connected) {
- const error = 'Is the gateway down? (connected=false)';
- this.client.warn(error);
- callbacks.error(error);
- return;
- }
-
- const message = callbacks.message;
- const jsep = callbacks.jsep;
- const transaction = WebrtcSession.randomString(transationLength);
- const request = {
- janus: 'message',
- body: message,
- transaction
- };
-
- if (this.token) {
- request.token = this.token;
- }
-
- if (this.apisecret) {
- request.apisecret = this.apisecret;
- }
-
- if (jsep) {
- request.jsep = jsep;
- }
-
- this.client.debug('Sending message to plugin (handle=' + handleId + '):');
- this.client.debug(request);
-
- if (this.websockets) {
- request.session_id = this.sessionId;
- request.handle_id = handleId;
- this.transactions[transaction] = (json) => {
- this.client.debug('Message sent!');
- this.client.debug(json);
-
- if (json.janus === 'success') {
- // We got a success, must have been a synchronous transaction
- const plugindata = json.plugindata;
- if (!plugindata) {
- this.client.warn('Request succeeded, but missing plugindata...');
- callbacks.success();
- return;
- }
-
- this.client.log('Synchronous transaction successful (' + plugindata.plugin + ')');
- const data = plugindata.data;
- this.client.debug(data);
- callbacks.success(data);
- return;
- } else if (json.janus !== 'ack') {
- // Not a success and not an ack, must be an error
- if (json.error) {
- this.client.error('Ooops: ' + json.error.code + ' ' + json.error.reason);
- callbacks.error(json.error.code + ' ' + json.error.reason);
- } else {
- this.client.error('Unknown error');
- callbacks.error('Unknown error');
- }
- return;
- }
-
- // If we got here, the plugin decided to handle the request asynchronously
- callbacks.success();
- };
- this.ws.send(JSON.stringify(request));
- }
- }
-
- sendTrickleCandidate(handleId, candidate) {
- if (!this.connected) {
- this.client.warn('Is the gateway down? (connected=false)');
- return;
- }
- var request = {
- janus: 'trickle',
- candidate,
- transaction: WebrtcSession.randomString(transationLength)
- };
-
- if (this.token) {
- request.token = this.token;
- }
-
- if (this.apisecret) {
- request.apisecret = this.apisecret;
- }
- this.client.debug('Sending trickle candidate (handle=' + handleId + '):');
- this.client.debug(request);
-
- if (this.websockets) {
- request.session_id = this.sessionId;
- request.handle_id = handleId;
- this.ws.send(JSON.stringify(request));
- }
- }
-
- sendData(handleId, cbs) {
- const callbacks = cbs || {};
- callbacks.success = (typeof cbs.success == 'function') ? cbs.success : this.client.noop;
- callbacks.error = (typeof cbs.error == 'function') ? cbs.error : this.client.noop;
- const pluginHandle = this.pluginHandles[handleId];
- if (!pluginHandle || !pluginHandle.webrtcStuff) {
- this.client.warn('Invalid handle');
- callbacks.error('Invalid handle');
- return;
- }
-
- const config = pluginHandle.webrtcStuff;
- const text = callbacks.text;
- if (!text) {
- this.client.warn('Invalid text');
- callbacks.error('Invalid text');
- return;
- }
- this.client.log('Sending string on data channel: ' + text);
- config.dataChannel.send(text);
- callbacks.success();
- }
-
- sendDtmf(handleId, cbs) {
- const callbacks = cbs || {};
- callbacks.success = (typeof cbs.success == 'function') ? cbs.success : this.client.noop;
- callbacks.error = (typeof cbs.error == 'function') ? cbs.error : this.client.noop;
-
- const pluginHandle = this.pluginHandles[handleId];
- if (!pluginHandle || !pluginHandle.webrtcStuff) {
- this.client.warn('Invalid handle');
- callbacks.error('Invalid handle');
- return;
- }
-
- const config = pluginHandle.webrtcStuff;
- if (!config.dtmfSender) {
- // Create the DTMF sender, if possible
- if (config.myStream) {
- const tracks = config.myStream.getAudioTracks();
- if (tracks && tracks.length > 0) {
- const localAudioTrack = tracks[0];
- config.dtmfSender = config.pc.createDTMFSender(localAudioTrack);
- this.client.log('Created DTMF Sender');
- config.dtmfSender.ontonechange = (tone) => {
- this.client.debug('Sent DTMF tone: ' + tone.tone);
- };
- }
- }
- if (!config.dtmfSender) {
- this.client.warn('Invalid DTMF configuration');
- callbacks.error('Invalid DTMF configuration');
- return;
- }
- }
-
- const dtmf = callbacks.dtmf;
- if (!dtmf) {
- this.client.warn('Invalid DTMF parameters');
- callbacks.error('Invalid DTMF parameters');
- return;
- }
-
- const tones = dtmf.tones;
- if (!tones) {
- this.client.warn('Invalid DTMF string');
- callbacks.error('Invalid DTMF string');
- return;
- }
-
- let duration = dtmf.duration;
- if (!duration) {
- duration = 500; // We choose 500ms as the default duration for a tone
- }
-
- let gap = dtmf.gap;
- if (!gap) {
- gap = 50; // We choose 50ms as the default gap between tones
- }
-
- this.client.debug('Sending DTMF string ' + tones + ' (duration ' + duration + 'ms, gap ' + gap + 'ms');
- config.dtmfSender.insertDTMF(tones, duration, gap);
- }
-
- destroy(sync) {
- const syncRequest = (sync === true);
- this.client.log('Destroying session ' + this.sessionId);
-
- if (!this.connected) {
- this.client.warn('Is the gateway down? (connected=false)');
- return;
- }
-
- if (!this.sessionId) {
- this.client.warn('No session to destroy');
- this.gatewayCallbacks.destroyed();
- return;
- }
-
- Reflect.deleteProperty(this.client.sessions, this.sessionId);
-
- // Destroy all handles first
- for (const ph in this.pluginHandles) {
- if (this.pluginHandles.hasOwnProperty(ph)) {
- const phv = this.pluginHandles[ph];
- this.client.log('Destroying handle ' + phv.id + ' (' + phv.plugin + ')');
- this.destroyHandle(phv.id, null, syncRequest);
- }
- }
-
- // Ok, go on
- var request = {janus: 'destroy', transaction: WebrtcSession.randomString(transationLength)};
-
- if (this.token) {
- request.token = this.token;
- }
- if (this.apisecret) {
- request.apisecret = this.apisecret;
- }
- if (this.websockets) {
- request.session_id = this.sessionId;
-
- let onUnbindMessage = null;
- let onUnbindError = null;
- onUnbindMessage = (event) => {
- var data = JSON.parse(event.data);
- if (data.session_id === request.session_id && data.transaction === request.transaction) {
- this.unbindWebSocket(onUnbindMessage, onUnbindError);
- this.gatewayCallbacks.destroyed();
- }
- };
- onUnbindError = () => {
- this.unbindWebSocket(onUnbindMessage, onUnbindError);
- this.gatewayCallbacks.destroyed();
- };
-
- this.ws.addEventListener('message', onUnbindMessage);
- this.ws.addEventListener('error', onUnbindError);
-
- this.ws.send(JSON.stringify(request));
- }
- }
-
- disconnect() {
- this.connected = false;
- this.ws.close();
- }
-
- destroyHandle(handleId, cbs, sync) {
- const syncRequest = (sync === true);
- this.client.log('Destroying handle ' + handleId + ' (sync=' + syncRequest + ')');
- const callbacks = cbs || {};
- callbacks.success = (typeof callbacks.success == 'function') ? callbacks.success : this.client.noop;
- callbacks.error = (typeof callbacks.error == 'function') ? callbacks.error : this.client.noop;
- this.cleanupWebrtc(handleId);
- if (!this.connected) {
- this.client.warn('Is the gateway down? (connected=false)');
- return;
- }
- const request = {
- janus: 'detach',
- transaction: WebrtcSession.randomString(transationLength)
- };
-
- if (this.token) {
- request.token = this.token;
- }
-
- if (this.apisecret) {
- request.apisecret = this.apisecret;
- }
-
- if (this.websockets) {
- request.session_id = this.sessionId;
- request.handle_id = handleId;
- this.ws.send(JSON.stringify(request));
-
- Reflect.deleteProperty(this.pluginHandles, handleId);
-
- callbacks.success();
- }
- }
-
- streamsDone(handleId, jsep, media, callbacks, stream) {
- const pluginHandle = this.pluginHandles[handleId];
- if (!pluginHandle || !pluginHandle.webrtcStuff) {
- this.client.warn('Invalid handle');
- callbacks.error('Invalid handle');
- return;
- }
-
- const config = pluginHandle.webrtcStuff;
- this.client.debug('streamsDone:', stream);
- config.myStream = stream;
-
- const pcConfig = {iceServers: this.iceServers};
- const pcConstraints = {
- optional: [{DtlsSrtpKeyAgreement: true}]
- };
-
- this.client.log('Creating PeerConnection');
- this.client.debug(pcConstraints);
- config.pc = new window.RTCPeerConnection(pcConfig, pcConstraints);
- this.client.debug(config.pc);
- if (config.pc.getStats) { // FIXME
- config.volume.value = 0;
- config.bitrate.value = '0 kbits/sec';
- }
- this.client.log('Preparing local SDP and gathering candidates (trickle=' + config.trickle + ')');
-
- config.pc.onicecandidate = (event) => {
- if (!event.candidate || (adapter.browserDetails.browser === 'edge' && event.candidate.candidate.indexOf('endOfCandidates') > 0)) {
- this.client.log('End of candidates.');
- config.iceDone = true;
- if (config.trickle === true) {
- // Notify end of candidates
- this.sendTrickleCandidate(handleId, {completed: true});
- } else {
- // No trickle, time to send the complete SDP (including all candidates)
- this.sendSDP(handleId, callbacks);
- }
- } else {
- // JSON.stringify doesn't work on some WebRTC objects anymore
- // See https://code.google.com/p/chromium/issues/detail?id=467366
- const candidate = {
- candidate: event.candidate.candidate,
- sdpMid: event.candidate.sdpMid,
- sdpMLineIndex: event.candidate.sdpMLineIndex
- };
-
- if (config.trickle === true) {
- // Send candidate
- this.sendTrickleCandidate(handleId, candidate);
- }
- }
- };
-
- if (stream) {
- this.client.log('Adding local stream');
- config.pc.addStream(stream);
- pluginHandle.onlocalstream(stream);
- }
-
- config.pc.onaddstream = (remoteStream) => {
- this.client.log('Handling Remote Stream');
- this.client.debug(remoteStream);
- config.remoteStream = remoteStream;
- pluginHandle.onremotestream(remoteStream.stream);
- };
-
- // Any data channel to create?
- if (this.isDataEnabled(media)) {
- this.client.log('Creating data channel');
- const onDataChannelMessage = (event) => {
- this.client.log('Received message on data channel: ' + event.data);
- pluginHandle.ondata(event.data); // FIXME
- };
-
- const onDataChannelStateChange = () => {
- const dcState = config.dataChannel ? config.dataChannel.readyState : 'null';
- this.client.log('State change on data channel: ' + dcState);
- if (dcState === 'open') {
- pluginHandle.ondataopen(); // FIXME
- }
- };
-
- const onDataChannelError = (error) => {
- this.client.error('Got error on data channel:', error);
-
- // TODO
- };
-
- // Until we implement the proxying of open requests within the this.client core, we open a channel ourselves whatever the case
- config.dataChannel = config.pc.createDataChannel('this.clientDataChannel', {ordered: false}); // FIXME Add options (ordered, maxRetransmits, etc.)
- config.dataChannel.onmessage = onDataChannelMessage;
- config.dataChannel.onopen = onDataChannelStateChange;
- config.dataChannel.onclose = onDataChannelStateChange;
- config.dataChannel.onerror = onDataChannelError;
- }
-
- // Create offer/answer now DO I WANT THIS??
- if (jsep) {
- config.pc.setRemoteDescription(
- new RTCSessionDescription(jsep),
- () => {
- this.client.log('Remote description accepted!');
- this.createAnswer(handleId, media, callbacks);
- }, callbacks.error);
- } else {
- this.createOffer(handleId, media, callbacks);
- }
- }
-
- prepareWebrtc(handleId, cbs) {
- const callbacks = cbs || {};
- callbacks.success = (typeof cbs.success == 'function') ? cbs.success : this.client.noop;
- callbacks.error = (typeof cbs.error == 'function') ? cbs.error : this.webrtcError;
- const jsep = callbacks.jsep;
- const media = callbacks.media;
- const pluginHandle = this.pluginHandles[handleId];
-
- if (!pluginHandle || !pluginHandle.webrtcStuff) {
- this.client.warn('Invalid handle');
- callbacks.error('Invalid handle');
- return;
- }
-
- const config = pluginHandle.webrtcStuff;
-
- // Are we updating a session?
- if (config.pc) {
- this.client.log('Updating existing media session');
-
- // Create offer/answer now
- if (jsep) {
- config.pc.setRemoteDescription(
- new window.RTCSessionDescription(jsep),
- () => {
- this.client.log('Remote description accepted!');
- this.createAnswer(handleId, media, callbacks);
- }, callbacks.error);
- } else {
- this.createOffer(handleId, media, callbacks);
- }
- return;
- }
-
- // Was a MediaStream object passed, or do we need to take care of that?
- if (callbacks.stream) {
- const stream = callbacks.stream;
- this.client.log('MediaStream provided by the application');
- this.client.debug(stream);
-
- // Skip the getUserMedia part
- config.streamExternal = true;
- this.streamsDone(handleId, jsep, media, callbacks, stream);
- return;
- }
-
- config.trickle = this.isTrickleEnabled(callbacks.trickle);
- if (this.isAudioSendEnabled(media) || this.isVideoSendEnabled(media)) {
- let constraints = {mandatory: {}, optional: []};
- pluginHandle.consentDialog(true);
-
- let audioSupport = this.isAudioSendEnabled(media);
- if (audioSupport === true && media) {
- if (typeof media.audio === 'object') {
- audioSupport = media.audio;
- }
- }
-
- let videoSupport = this.isVideoSendEnabled(media);
- if (videoSupport === true && media) {
- if (media.video && media.video !== 'screen' && media.video !== 'window') {
- let width = 0;
- let height = 0;
- let maxHeight = 0;
-
- if (media.video === 'lowres') {
- // Small resolution, 4:3
- height = 240;
- maxHeight = 240;
- width = 320;
- } else if (media.video === 'lowres-16:9') {
- // Small resolution, 16:9
- height = 180;
- maxHeight = 180;
- width = 320;
- } else if (media.video === 'hires' || media.video === 'hires-16:9') {
- // High resolution is only 16:9
- height = 720;
- maxHeight = 720;
- width = 1280;
- if (navigator.mozGetUserMedia) {
- const firefoxVer = parseInt(window.navigator.userAgent.match(/Firefox\/(.*)/)[1], 10);
- if (firefoxVer < 38) {
- // Unless this is and old Firefox, which doesn't support it
- this.client.warn(media.video + ' unsupported, falling back to stdres (old Firefox)');
- height = 480;
- maxHeight = 480;
- width = 640;
- }
- }
- } else if (media.video === 'stdres') {
- // Normal resolution, 4:3
- height = 480;
- maxHeight = 480;
- width = 640;
- } else if (media.video === 'stdres-16:9') {
- // Normal resolution, 16:9
- height = 360;
- maxHeight = 360;
- width = 640;
- } else {
- this.client.log('Default video setting (' + media.video + ') is stdres 4:3');
- height = 480;
- maxHeight = 480;
- width = 640;
- }
-
- this.client.log('Adding media constraint ' + media.video);
-
- if (navigator.mozGetUserMedia) {
- const firefoxVer = parseInt(window.navigator.userAgent.match(/Firefox\/(.*)/)[1], 10);
- if (firefoxVer < 38) {
- videoSupport = {
- require: ['height', 'width'],
- height: {max: maxHeight, min: height},
- width: {max: width, min: width}
- };
- } else {
- // http://stackoverflow.com/questions/28282385/webrtc-firefox-constraints/28911694#28911694
- // https://github.com/meetecho/janus-gateway/pull/246
- videoSupport = {
- height: {ideal: height},
- width: {ideal: width}
- };
- }
- } else {
- videoSupport = {
- mandatory: {
- maxHeight,
- minHeight: height,
- maxWidth: width,
- minWidth: width
- },
- optional: []
- };
- }
-
- if (typeof media.video === 'object') {
- videoSupport = media.video;
- }
-
- this.client.debug(videoSupport);
- } else if (media.video === 'screen' || media.video === 'window') {
- // Not a webcam, but screen capture
- if (window.location.protocol !== 'https:') {
- // Screen sharing mandates HTTPS
- this.client.warn('Screen sharing only works on HTTPS, try the https:// version of this page');
- pluginHandle.consentDialog(false);
- callbacks.error('Screen sharing only works on HTTPS, try the https:// version of this page');
- return;
- }
-
- // We're going to try and use the extension for Chrome 34+, the old approach
- // for older versions of Chrome, or the experimental support in Firefox 33+
- const cache = {};
- const self = this;
-
- function callbackUserMedia(error, stream) {
- pluginHandle.consentDialog(false);
- if (error) {
- callbacks.error({code: error.code, name: error.name, message: error.message});
- } else {
- self.streamsDone(handleId, jsep, media, callbacks, stream);
- }
- }
-
- function getScreenMedia(constraint, gsmCallback) {
- this.client.log('Adding media constraint (screen capture)');
- this.client.debug(constraint);
- navigator.mediaDevices.getUserMedia(constraint).
- then((stream) => {
- gsmCallback(null, stream);
- }).
- catch((error) => {
- pluginHandle.consentDialog(false);
- gsmCallback(error);
- });
- }
-
- if (window.navigator.userAgent.match('Chrome')) {
- const 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) {
- // Chrome 26->33 requires some awkward chrome://flags manipulation
- constraints = {
- video: {
- mandatory: {
- googLeakyBucket: true,
- maxWidth: window.screen.width,
- maxHeight: window.screen.height,
- maxFrameRate: 3,
- chromeMediaSource: 'screen'
- }
- },
- audio: this.isAudioSendEnabled(media)
- };
- getScreenMedia(constraints, callbackUserMedia);
- } else {
- // Chrome 34+ requires an extension
- var pending = window.setTimeout(
- () => {
- const 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)';
- pluginHandle.consentDialog(false);
- return callbacks.error(error);
- }, 1000);
- cache[pending] = [callbackUserMedia, null];
- window.postMessage({
- type: 'janusGetScreen',
- id: pending
- }, '*');
- }
- } else if (window.navigator.userAgent.match('Firefox')) {
- const ffver = parseInt(window.navigator.userAgent.match(/Firefox\/(.*)/)[1], 10);
- if (ffver >= 33) {
- // Firefox 33+ has experimental support for screen sharing
- constraints = {
- video: {
- mozMediaSource: media.video,
- mediaSource: media.video
- },
- audio: this.isAudioSendEnabled(media)
- };
- getScreenMedia(constraints, (err, stream) => {
- callbackUserMedia(err, stream);
-
- // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1045810
- if (!err) {
- let lastTime = stream.currentTime;
- const polly = window.setInterval(() => {
- if (!stream) {
- window.clearInterval(polly);
- }
-
- if (stream.currentTime === lastTime) {
- window.clearInterval(polly);
- if (stream.onended) {
- stream.onended();
- }
- }
-
- lastTime = stream.currentTime;
- }, 500);
- }
- });
- } else {
- const error = new Error('NavigatorUserMediaError');
- error.name = 'Your version of Firefox does not support screen sharing, please install Firefox 33 (or more recent versions)';
- pluginHandle.consentDialog(false);
- callbacks.error(error);
- return;
- }
- }
-
- // Wait for events from the Chrome Extension
- window.addEventListener('message', (event) => {
- if (event.origin !== window.location.origin) {
- return;
- }
- if (event.data.type === 'mattermostGotScreen' && cache[event.data.id]) {
- const data = cache[event.data.id];
- const callback = data[0];
-
- Reflect.deleteProperty(cache, event.data.id);
-
- if (event.data.sourceId === '') {
- // user canceled
- const error = new Error('NavigatorUserMediaError');
- error.name = 'You cancelled the request for permission, giving up...';
- pluginHandle.consentDialog(false);
- callbacks.error(error);
- } else {
- constraints = {
- audio: this.isAudioSendEnabled(media),
- video: {
- mandatory: {
- chromeMediaSource: 'desktop',
- maxWidth: window.screen.width,
- maxHeight: window.screen.height,
- maxFrameRate: 3
- },
- optional: [
- {googLeakyBucket: true},
- {googTemporalLayeredScreencast: true}
- ]
- }
- };
- constraints.video.mandatory.chromeMediaSourceId = event.data.sourceId;
- getScreenMedia(constraints, callback);
- }
- } else if (event.data.type === 'mattermostGetScreenPending') {
- window.clearTimeout(event.data.id);
- }
- });
- return;
- }
- }
-
- // If we got here, we're not screensharing
- if (!media || media.video !== 'screen') {
- // Check whether all media sources are actually available or not
- navigator.mediaDevices.enumerateDevices().then((devices) => {
- const audioExist = devices.some((device) => {
- return device.kind === 'audioinput';
- });
-
- const videoExist = devices.some((device) => {
- return device.kind === 'videoinput';
- });
-
- // Check whether a missing device is really a problem
- const audioSend = this.isAudioSendEnabled(media);
- const videoSend = this.isVideoSendEnabled(media);
-
- if (audioSend || videoSend) {
- // We need to send either audio or video
- const haveAudioDevice = audioSend ? audioExist : false;
- const haveVideoDevice = videoSend ? videoExist : false;
-
- if (!haveAudioDevice && !haveVideoDevice) {
- // FIXME Should we really give up, or just assume recvonly for both?
- pluginHandle.consentDialog(false);
- callbacks.error('No capture device found');
- return false;
- }
- }
-
- navigator.mediaDevices.getUserMedia({
- audio: audioExist ? audioSupport : false,
- video: videoExist ? videoSupport : false
- }).
- then((stream) => {
- pluginHandle.consentDialog(false);
- this.streamsDone(handleId, jsep, media, callbacks, stream);
- }).
- catch((error) => {
- pluginHandle.consentDialog(false);
- callbacks.error({
- code: error.code,
- name: error.name,
- message: error.message
- });
- });
-
- return true;
- }).
- catch((error) => {
- pluginHandle.consentDialog(false);
- callbacks.error('enumerateDevices error', error);
- });
- }
- } else {
- // No need to do a getUserMedia, create offer/answer right away
- this.streamsDone(handleId, jsep, media, callbacks);
- }
- }
-
- prepareWebrtcPeer(handleId, cbs) {
- const callbacks = cbs || {};
- callbacks.success = (typeof cbs.success == 'function') ? cbs.success : this.client.noop;
- callbacks.error = (typeof cbs.error == 'function') ? cbs.error : this.webrtcError;
-
- const jsep = callbacks.jsep;
- const pluginHandle = this.pluginHandles[handleId];
-
- if (!pluginHandle || !pluginHandle.webrtcStuff) {
- this.client.warn('Invalid handle');
- callbacks.error('Invalid handle');
- return;
- }
-
- const config = pluginHandle.webrtcStuff;
-
- if (jsep) {
- if (config.pc === null) {
- this.client.warn('Wait, no PeerConnection?? if this is an answer, use createAnswer and not handleRemoteJsep');
- callbacks.error('No PeerConnection: if this is an answer, use createAnswer and not handleRemoteJsep');
- return;
- }
- config.pc.setRemoteDescription(
- new window.RTCSessionDescription(jsep),
- () => {
- this.client.log('Remote description accepted!');
- callbacks.success();
- }, callbacks.error);
- } else {
- callbacks.error('Invalid JSEP');
- }
- }
-
- createOffer(handleId, media, cbs) {
- const callbacks = cbs || {};
- callbacks.success = (typeof cbs.success == 'function') ? cbs.success : this.client.noop;
- callbacks.error = (typeof cbs.error == 'function') ? cbs.error : this.client.noop;
-
- const pluginHandle = this.pluginHandles[handleId];
- if (!pluginHandle || !pluginHandle.webrtcStuff) {
- this.client.warn('Invalid handle');
- callbacks.error('Invalid handle');
- return;
- }
-
- const config = pluginHandle.webrtcStuff;
- this.client.log('Creating offer (iceDone=' + config.iceDone + ')');
-
- // https://code.google.com/p/webrtc/issues/detail?id=3508
- let mediaConstraints = null;
- const browser = adapter.browserDetails.browser;
- if (browser === 'firefox' || browser === 'edge') {
- mediaConstraints = {
- offerToReceiveAudio: this.isAudioRecvEnabled(media),
- offerToReceiveVideo: this.isVideoRecvEnabled(media)
- };
- } else {
- mediaConstraints = {
- mandatory: {
- OfferToReceiveAudio: this.isAudioRecvEnabled(media),
- OfferToReceiveVideo: this.isVideoRecvEnabled(media)
- }
- };
- }
-
- this.client.debug(mediaConstraints);
- config.pc.createOffer(
- (offer) => {
- this.client.debug(offer);
-
- if (!config.mySdp) {
- this.client.log('Setting local description');
- config.mySdp = offer.sdp;
- config.pc.setLocalDescription(offer);
- }
-
- if (!config.iceDone && !config.trickle) {
- // Don't do anything until we have all candidates
- this.client.log('Waiting for all candidates...');
- return;
- }
-
- if (config.sdpSent) {
- this.client.log('Offer already sent, not sending it again');
- return;
- }
-
- this.client.log('Offer ready');
- this.client.debug(callbacks);
- config.sdpSent = true;
-
- // JSON.stringify doesn't work on some WebRTC objects anymore
- // See https://code.google.com/p/chromium/issues/detail?id=467366
- const jsep = {
- type: offer.type,
- sdp: offer.sdp
- };
- callbacks.success(jsep);
- }, callbacks.error, mediaConstraints);
- }
-
- createAnswer(handleId, media, cbs) {
- const callbacks = cbs || {};
- callbacks.success = (typeof cbs.success == 'function') ? cbs.success : this.client.noop;
- callbacks.error = (typeof cbs.error == 'function') ? cbs.error : this.client.noop;
-
- const pluginHandle = this.pluginHandles[handleId];
- if (!pluginHandle || !pluginHandle.webrtcStuff) {
- this.client.warn('Invalid handle');
- callbacks.error('Invalid handle');
- return;
- }
-
- const config = pluginHandle.webrtcStuff;
- this.client.log('Creating answer (iceDone=' + config.iceDone + ')');
-
- let mediaConstraints = null;
- const browser = adapter.browserDetails.browser;
- if (browser === 'firefox' || browser === 'edge') {
- mediaConstraints = {
- offerToReceiveAudio: this.isAudioRecvEnabled(media),
- offerToReceiveVideo: this.isVideoRecvEnabled(media)
- };
- } else {
- mediaConstraints = {
- mandatory: {
- OfferToReceiveAudio: this.isAudioRecvEnabled(media),
- OfferToReceiveVideo: this.isVideoRecvEnabled(media)
- }
- };
- }
- this.client.debug(mediaConstraints);
- config.pc.createAnswer(
- (answer) => {
- this.client.debug(answer);
- if (!config.mySdp) {
- this.client.log('Setting local description');
- config.mySdp = answer.sdp;
- config.pc.setLocalDescription(answer);
- }
- if (!config.iceDone && !config.trickle) {
- // Don't do anything until we have all candidates
- this.client.log('Waiting for all candidates...');
- return;
- }
- if (config.sdpSent) { // FIXME badly
- this.client.log('Answer already sent, not sending it again');
- return;
- }
- config.sdpSent = true;
-
- // JSON.stringify doesn't work on some WebRTC objects anymore
- // See https://code.google.com/p/chromium/issues/detail?id=467366
- const jsep = {
- type: answer.type,
- sdp: answer.sdp
- };
- callbacks.success(jsep);
- }, callbacks.error, mediaConstraints);
- }
-
- sendSDP(handleId, cbs) {
- const callbacks = cbs || {};
- callbacks.success = (typeof cbs.success == 'function') ? cbs.success : this.client.noop;
- callbacks.error = (typeof cbs.error == 'function') ? cbs.error : this.client.noop;
-
- const pluginHandle = this.pluginHandles[handleId];
- if (!pluginHandle || !pluginHandle.webrtcStuff) {
- this.client.warn('Invalid handle, not sending anything');
- return;
- }
-
- const config = pluginHandle.webrtcStuff;
- this.client.log('Sending offer/answer SDP...');
- if (!config.mySdp) {
- this.client.warn('Local SDP instance is invalid, not sending anything...');
- return;
- }
-
- config.mySdp = {
- type: config.pc.localDescription.type,
- sdp: config.pc.localDescription.sdp
- };
-
- if (config.sdpSent) {
- this.client.log('Offer/Answer SDP already sent, not sending it again');
- return;
- }
-
- if (config.trickle === false) {
- config.mySdp.trickle = false;
- }
- this.client.debug(callbacks);
- config.sdpSent = true;
- callbacks.success(config.mySdp);
- }
-
- getVolume(handleId) {
- const pluginHandle = this.pluginHandles[handleId];
- if (!pluginHandle || !pluginHandle.webrtcStuff) {
- this.client.warn('Invalid handle');
- return 0;
- }
-
- const config = pluginHandle.webrtcStuff;
- const browser = adapter.browserDetails.browser;
-
- // Start getting the volume, if getStats is supported
- if (config.pc.getStats && browser === 'chrome') { // FIXME
- if (!config.remoteStream) {
- this.client.warn('Remote stream unavailable');
- return 0;
- }
-
- // http://webrtc.googlecode.com/svn/trunk/samples/js/demos/html/constraints-and-stats.html
- if (!config.volume.timer) {
- this.client.log('Starting volume monitor');
- config.volume.timer = setInterval(() => {
- config.pc.getStats((stats) => {
- const results = stats.result();
- for (let i = 0; i < results.length; i++) {
- const res = results[i];
- if (res.type === 'ssrc' && res.stat('audioOutputLevel')) {
- config.volume.value = res.stat('audioOutputLevel');
- }
- }
- });
- }, 200);
- return 0; // We don't have a volume to return yet
- }
- return config.volume.value;
- }
-
- this.client.log('Getting the remote volume unsupported by browser');
- return 0;
- }
-
- isMuted(handleId, video) {
- const pluginHandle = this.pluginHandles[handleId];
- if (!pluginHandle || !pluginHandle.webrtcStuff) {
- this.client.warn('Invalid handle');
- return true;
- }
-
- const config = pluginHandle.webrtcStuff;
-
- if (!config.pc) {
- this.client.warn('Invalid PeerConnection');
- return true;
- }
-
- if (!config.myStream) {
- this.client.warn('Invalid local MediaStream');
- return true;
- }
-
- if (video) {
- // Check video track
- if (!config.myStream.getVideoTracks() || config.myStream.getVideoTracks().length === 0) {
- this.client.warn('No video track');
- return true;
- }
- return !config.myStream.getVideoTracks()[0].enabled;
- }
-
- // Check audio track
- if (!config.myStream.getAudioTracks() || config.myStream.getAudioTracks().length === 0) {
- this.client.warn('No audio track');
- return true;
- }
- return !config.myStream.getAudioTracks()[0].enabled;
- }
-
- mute(handleId, video, mute) {
- const pluginHandle = this.pluginHandles[handleId];
- if (!pluginHandle || !pluginHandle.webrtcStuff) {
- this.client.warn('Invalid handle');
- return false;
- }
-
- const config = pluginHandle.webrtcStuff;
- if (!config.pc) {
- this.client.warn('Invalid PeerConnection');
- return false;
- }
-
- if (!config.myStream) {
- this.client.warn('Invalid local MediaStream');
- return false;
- }
-
- if (video) {
- // Mute/unmute video track
- if (!config.myStream.getVideoTracks() || config.myStream.getVideoTracks().length === 0) {
- this.client.warn('No video track');
- return false;
- }
- config.myStream.getVideoTracks()[0].enabled = mute;
- return true;
- }
-
- // Mute/unmute audio track
- if (!config.myStream.getAudioTracks() || config.myStream.getAudioTracks().length === 0) {
- this.client.warn('No audio track');
- return false;
- }
- config.myStream.getAudioTracks()[0].enabled = mute;
- return true;
- }
-
- getBitrate(handleId) {
- const pluginHandle = this.pluginHandles[handleId];
- if (!pluginHandle || !pluginHandle.webrtcStuff) {
- this.client.warn('Invalid handle');
- return 'Invalid handle';
- }
-
- const config = pluginHandle.webrtcStuff;
- if (!config.pc) {
- return 'Invalid PeerConnection';
- }
-
- // Start getting the bitrate, if getStats is supported
- const browser = adapter.browserDetails.browser;
- if (config.pc.getStats && browser === 'chrome') {
- // Do it the Chrome way
- if (!config.remoteStream) {
- this.client.warn('Remote stream unavailable');
- return 'Remote stream unavailable';
- }
-
- // http://webrtc.googlecode.com/svn/trunk/samples/js/demos/html/constraints-and-stats.html
- if (!config.bitrate.timer) {
- this.client.log('Starting bitrate timer (Chrome)');
- config.bitrate.timer = setInterval(() => {
- config.pc.getStats((stats) => {
- const results = stats.result();
- for (let i = 0; i < results.length; i++) {
- const res = results[i];
- if (res.type === 'ssrc' && res.stat('googFrameHeightReceived')) {
- config.bitrate.bsnow = res.stat('bytesReceived');
- config.bitrate.tsnow = res.timestamp;
- if (config.bitrate.bsbefore === null || config.bitrate.tsbefore === null) {
- // Skip this round
- config.bitrate.bsbefore = config.bitrate.bsnow;
- config.bitrate.tsbefore = config.bitrate.tsnow;
- } else {
- // Calculate bitrate
- var bitRate = Math.round(((config.bitrate.bsnow - config.bitrate.bsbefore) * 8) / (config.bitrate.tsnow - config.bitrate.tsbefore));
- config.bitrate.value = bitRate + ' kbits/sec';
-
- //~ this.client.log('Estimated bitrate is ' + config.bitrate.value);
- config.bitrate.bsbefore = config.bitrate.bsnow;
- config.bitrate.tsbefore = config.bitrate.tsnow;
- }
- }
- }
- });
- }, 1000);
- return '0 kbits/sec'; // We don't have a bitrate value yet
- }
- return config.bitrate.value;
- } else if (config.pc.getStats && browser === 'firefox') {
- // Do it the Firefox way
- if (!config.remoteStream || !config.remoteStream.stream) {
- this.client.warn('Remote stream unavailable');
- return 'Remote stream unavailable';
- }
-
- const videoTracks = config.remoteStream.stream.getVideoTracks();
- if (!videoTracks || videoTracks.length < 1) {
- this.client.warn('No video track');
- return 'No video track';
- }
-
- // https://github.com/muaz-khan/getStats/blob/master/getStats.js
- if (!config.bitrate.timer) {
- this.client.log('Starting bitrate timer (Firefox)');
- config.bitrate.timer = setInterval(() => {
- // We need a helper callback
- function cb(res) {
- if (!res || res.inbound_rtp_video_1 == null || res.inbound_rtp_video_1 == null) {
- config.bitrate.value = 'Missing inbound_rtp_video_1';
- return;
- }
-
- config.bitrate.bsnow = res.inbound_rtp_video_1.bytesReceived;
- config.bitrate.tsnow = res.inbound_rtp_video_1.timestamp;
-
- if (config.bitrate.bsbefore === null || config.bitrate.tsbefore === null) {
- // Skip this round
- config.bitrate.bsbefore = config.bitrate.bsnow;
- config.bitrate.tsbefore = config.bitrate.tsnow;
- } else {
- // Calculate bitrate
- var bitRate = Math.round(((config.bitrate.bsnow - config.bitrate.bsbefore) * 8) / (config.bitrate.tsnow - config.bitrate.tsbefore));
- config.bitrate.value = bitRate + ' kbits/sec';
- config.bitrate.bsbefore = config.bitrate.bsnow;
- config.bitrate.tsbefore = config.bitrate.tsnow;
- }
- }
-
- // Actually get the stats
- config.pc.getStats(videoTracks[0], (stats) => {
- cb(stats);
- }, cb);
- }, 1000);
- return '0 kbits/sec'; // We don't have a bitrate value yet
- }
- return config.bitrate.value;
- }
-
- this.client.warn('Getting the video bitrate unsupported by browser');
- return 'Feature unsupported by browser';
- }
-
- webrtcError(error) {
- this.client.error('WebRTC error:', error);
- }
-
- cleanupWebrtc(handleId, hangupRequest) {
- this.client.log('Cleaning WebRTC stuff');
- const pluginHandle = this.pluginHandles[handleId];
- if (!pluginHandle) {
- // Nothing to clean
- return;
- }
-
- const config = pluginHandle.webrtcStuff;
- if (config) {
- if (hangupRequest === true) {
- // Send a hangup request (we don't really care about the response)
- const request = {
- janus: 'hangup',
- transaction: WebrtcSession.randomString(transationLength)
- };
-
- if (this.token) {
- request.token = this.token;
- }
-
- if (this.apisecret) {
- request.apisecret = this.apisecret;
- }
-
- this.client.debug('Sending hangup request (handle=' + handleId + '):');
- this.client.debug(request);
- if (this.websockets) {
- request.session_id = this.sessionId;
- request.handle_id = handleId;
- this.ws.send(JSON.stringify(request));
- }
- }
-
- // Cleanup stack
- config.remoteStream = null;
- if (config.volume.timer) {
- clearInterval(config.volume.timer);
- }
-
- config.volume.value = null;
- if (config.bitrate.timer) {
- clearInterval(config.bitrate.timer);
- }
-
- config.bitrate.timer = null;
- config.bitrate.bsnow = null;
- config.bitrate.bsbefore = null;
- config.bitrate.tsnow = null;
- config.bitrate.tsbefore = null;
- config.bitrate.value = null;
-
- try {
- // Try a MediaStream.stop() first
- if (!config.streamExternal && config.myStream) {
- this.client.log('Stopping local stream');
- config.myStream.stop();
- }
- } catch (e) {
- // Do nothing if this fails
- }
-
- try {
- // Try a MediaStreamTrack.stop() for each track as well
- if (!config.streamExternal && config.myStream) {
- this.client.log('Stopping local stream tracks');
- WebrtcSession.stopMediaStream(config.myStream);
- }
- } catch (e) {
- // Do nothing if this fails
- }
-
- config.streamExternal = false;
- config.myStream = null;
-
- // Close PeerConnection
- try {
- config.pc.close();
- } catch (e) {
- // Do nothing
- }
- config.pc = null;
- config.mySdp = null;
- config.iceDone = false;
- config.sdpSent = false;
- config.dataChannel = null;
- config.dtmfSender = null;
- }
- pluginHandle.oncleanup();
- }
-
- isAudioSendEnabled(media) {
- this.client.debug('isAudioSendEnabled:', media);
- if (!media) {
- return true; // Default
- }
-
- if (media.audio === false) {
- return false; // Generic audio has precedence
- }
-
- if (!media.audioSend) {
- return true; // Default
- }
-
- return (media.audioSend === true);
- }
-
- isAudioRecvEnabled(media) {
- this.client.debug('isAudioRecvEnabled:', media);
- if (!media) {
- return true; // Default
- }
-
- if (media.audio === false) {
- return false; // Generic audio has precedence
- }
-
- if (!media.audioRecv) {
- return true; // Default
- }
-
- return (media.audioRecv === true);
- }
-
- isVideoSendEnabled(media) {
- this.client.debug('isVideoSendEnabled:', media);
- const browser = adapter.browserDetails.browser;
- if (browser === 'edge') {
- this.client.warn("Edge doesn't support compatible video yet");
- return false;
- }
-
- if (!media) {
- return true; // Default
- }
-
- if (media.video === false) {
- return false; // Generic video has precedence
- }
-
- if (!media.videoSend) {
- return true; // Default
- }
-
- return (media.videoSend === true);
- }
-
- isVideoRecvEnabled(media) {
- this.client.debug('isVideoRecvEnabled:', media);
- const browser = adapter.browserDetails.browser;
- if (browser === 'edge') {
- this.client.warn("Edge doesn't support compatible video yet");
- return false;
- }
-
- if (!media) {
- return true; // Default
- }
-
- if (media.video === false) {
- return false; // Generic video has precedence
- }
-
- if (!media.videoRecv) {
- return true; // Default
- }
-
- return (media.videoRecv === true);
- }
-
- isDataEnabled(media) {
- this.client.debug('isDataEnabled:', media);
- const browser = adapter.browserDetails.browser;
- if (browser === 'edge') {
- this.client.warn("Edge doesn't support data channels yet");
- return false;
- }
-
- if (!media) {
- return false; // Default
- }
-
- return (media.data === true);
- }
-
- isTrickleEnabled(trickle) {
- this.client.debug('isTrickleEnabled:', trickle);
- if (!trickle) {
- return true; // Default is true
- }
-
- return (trickle === true);
- }
-
- unbindWebSocket(onUnbindMessage, onUnbindError) {
- for (var eventName in this.wsHandlers) {
- if (this.wsHandlers.hasOwnProperty(eventName)) {
- this.ws.removeEventListener(eventName, this.wsHandlers[eventName]);
- }
- }
- this.ws.removeEventListener('message', onUnbindMessage);
- this.ws.removeEventListener('error', onUnbindError);
- if (this.wsKeepaliveTimeoutId) {
- clearTimeout(this.wsKeepaliveTimeoutId);
- }
- }
-}