diff options
author | enahum <nahumhbl@gmail.com> | 2017-01-03 12:08:21 -0300 |
---|---|---|
committer | Corey Hulen <corey@hulen.com> | 2017-01-03 10:08:21 -0500 |
commit | 144219964742c35eebf6d75ae27ad17732c89dae (patch) | |
tree | 499275f4079369e2e3da037926f5a841cd33961c | |
parent | 7cdfba62ef991f36363592a70f08db6a88b3c332 (diff) | |
download | chat-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
-rw-r--r-- | webapp/client/webrtc_client.jsx | 145 | ||||
-rw-r--r-- | webapp/client/webrtc_session.jsx | 1966 | ||||
-rw-r--r-- | webapp/components/webrtc/webrtc_controller.jsx | 37 | ||||
-rw-r--r-- | webapp/non_npm_dependencies/janus/index.js | 2314 | ||||
-rw-r--r-- | webapp/package.json | 2 |
5 files changed, 2346 insertions, 2118 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); - } - } -} diff --git a/webapp/components/webrtc/webrtc_controller.jsx b/webapp/components/webrtc/webrtc_controller.jsx index 7e0cdb098..36278b3f4 100644 --- a/webapp/components/webrtc/webrtc_controller.jsx +++ b/webapp/components/webrtc/webrtc_controller.jsx @@ -7,7 +7,7 @@ import WebrtcStore from 'stores/webrtc_store.jsx'; import Client from 'client/web_client.jsx'; import WebSocketClient from 'client/web_websocket_client.jsx'; -import WebrtcSession from 'client/webrtc_session.jsx'; +import Janus from 'janus'; import SearchBox from '../search_bar.jsx'; import WebrtcHeader from './components/webrtc_header.jsx'; @@ -44,6 +44,8 @@ export default class WebrtcController extends React.Component { this.close = this.close.bind(this); this.clearError = this.clearError.bind(this); + this.getLocalMedia = this.getLocalMedia.bind(this); + this.stopMediaStream = this.stopMediaStream.bind(this); this.previewVideo = this.previewVideo.bind(this); this.stopRinging = this.stopRinging.bind(this); @@ -160,6 +162,30 @@ export default class WebrtcController extends React.Component { }, Constants.WEBRTC_CLEAR_ERROR_DELAY); } + 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); + }); + } + + stopMediaStream(stream) { + const tracks = stream.getTracks(); + tracks.forEach((track) => { + track.stop(); + }); + } + previewVideo() { if (this.mounted) { if (this.localMedia) { @@ -169,7 +195,7 @@ export default class WebrtcController extends React.Component { }); this.localMedia.enabled = true; } else { - WebrtcSession.getLocalMedia( + this.getLocalMedia( { audio: true, video: { @@ -399,12 +425,11 @@ export default class WebrtcController extends React.Component { if (this.session) { this.session.destroy(); - this.session.disconnect(); this.session = null; } if (this.localMedia) { - WebrtcSession.stopMediaStream(this.localMedia); + this.stopMediaStream(this.localMedia); this.localMedia = null; } @@ -648,8 +673,8 @@ export default class WebrtcController extends React.Component { }); } - this.session = new WebrtcSession({ - debug: global.mm_config.EnableDeveloper === 'true', + Janus.init({debug: global.mm_config.EnableDeveloper === 'true'}); + this.session = new Janus({ server: info.gateway_url, iceServers, token: info.token, diff --git a/webapp/non_npm_dependencies/janus/index.js b/webapp/non_npm_dependencies/janus/index.js new file mode 100644 index 000000000..2dc38ff9b --- /dev/null +++ b/webapp/non_npm_dependencies/janus/index.js @@ -0,0 +1,2314 @@ +/* + 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. + */ + +var adapter = require('webrtc-adapter'); + +// List of sessions +/* + 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 = {}; + +// Screensharing Chrome Extension ID +Janus.extensionId = "hapfgfdkleiggjjpfpenajgdnfckjpaj"; +Janus.isExtensionEnabled = function() { + if(window.navigator.userAgent.match('Chrome')) { + var chromever = parseInt(window.navigator.userAgent.match(/Chrome\/(.*) /)[1], 10); + var maxver = 33; + if(window.navigator.userAgent.match('Linux')) + maxver = 35; // "known" crash in chrome 34 and 35 on linux + if(chromever >= 26 && chromever <= maxver) { + // Older versions of Chrome don't support this extension-based approach, so lie + return true; + } + return (document.getElementById('janus-extension-installed') !== null); + } else { + // Firefox of others, no need for the extension (but this doesn't mean it will work) + return true; + } +}; + +Janus.noop = function() {}; + +// Initialization +Janus.init = function(options) { + options = options || {}; + options.callback = (typeof options.callback == "function") ? options.callback : Janus.noop; + if(Janus.initDone === true) { + // Already initialized + options.callback(); + } else { + if(typeof console == "undefined" || typeof console.log == "undefined") + console = { log: function() {} }; + // Console logging (all debugging disabled by default) + Janus.trace = Janus.noop; + Janus.debug = Janus.noop; + Janus.vdebug = Janus.noop; + Janus.log = Janus.noop; + Janus.warn = Janus.noop; + Janus.error = Janus.noop; + if(options.debug === true || options.debug === "all") { + // Enable all debugging levels + Janus.trace = console.trace.bind(console); + Janus.debug = console.debug.bind(console); + Janus.vdebug = console.debug.bind(console); + Janus.log = console.log.bind(console); + Janus.warn = console.warn.bind(console); + Janus.error = console.error.bind(console); + } else if(Array.isArray(options.debug)) { + for(var i in options.debug) { + var d = options.debug[i]; + 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"); + // Helper method to enumerate devices + Janus.listDevices = function(callback) { + callback = (typeof callback == "function") ? callback : Janus.noop; + if(navigator.mediaDevices) { + navigator.mediaDevices.getUserMedia({ audio: true, video: true }) + .then(function(stream) { + navigator.mediaDevices.enumerateDevices().then(function(devices) { + Janus.debug(devices); + callback(devices); + // Get rid of the now useless stream + try { + stream.stop(); + } catch(e) {} + try { + var tracks = stream.getTracks(); + for(var i in tracks) { + var mst = tracks[i]; + if(mst !== null && mst !== undefined) + mst.stop(); + } + } catch(e) {} + }); + }) + .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) { + if(adapter.browserDetails.browser === 'chrome') { + var chromever = adapter.browserDetails.version; + if(chromever >= 43) { + element.srcObject = stream; + } else if(typeof element.src !== 'undefined') { + element.src = URL.createObjectURL(stream); + } else { + Janus.error("Error attaching stream to element"); + } + } else { + element.srcObject = stream; + } + }; + Janus.reattachMediaStream = function(to, from) { + if(adapter.browserDetails.browser === 'chrome') { + var chromever = adapter.browserDetails.version; + if(chromever >= 43) { + to.srcObject = from.srcObject; + } else if(typeof to.src !== 'undefined') { + to.src = from.src; + } + } else { + to.srcObject = from.srcObject; + } + }; + // Prepare a helper method to send AJAX requests in a syntax similar to jQuery (at least for what we care) + Janus.ajax = function(params) { + // Check params + if(params === null || params === undefined) + return; + params.success = (typeof params.success == "function") ? params.success : Janus.noop; + params.error = (typeof params.error == "function") ? params.error : Janus.noop; + // Make sure there's an URL + if(params.url === null || params.url === undefined) { + Janus.error('Missing url', params.url); + params.error(null, -1, 'Missing url'); + return; + } + // Validate async + params.async = (params.async === null || params.async === undefined) ? true : (params.async === true); + Janus.log(params); + // IE doesn't even know what WebRTC is, so no polyfill needed + var XHR = new XMLHttpRequest(); + XHR.open(params.type, params.url, params.async); + if(params.contentType !== null && params.contentType !== undefined) + XHR.setRequestHeader('Content-type', params.contentType); + if(params.async) { + XHR.onreadystatechange = function () { + if(XHR.readyState != 4) + return; + if(XHR.status !== 200) { + // Got an error? + if(XHR.status === 0) + XHR.status = "error"; + params.error(XHR, XHR.status, ""); + return; + } + // Got payload + params.success(JSON.parse(XHR.responseText)); + }; + } + try { + XHR.send(params.data); + if(!params.async) { + if(XHR.status !== 200) { + // Got an error? + if(XHR.status === 0) + XHR.status = "error"; + params.error(XHR, XHR.status, ""); + return; + } + // Got payload + params.success(JSON.parse(XHR.responseText)); + } + } catch(e) { + // Something broke up + params.error(XHR, 'error', ''); + }; + }; + // Detect tab close: make sure we don't loose existing onbeforeunload handlers + var oldOBF = window.onbeforeunload; + window.onbeforeunload = function() { + Janus.log("Closing window"); + for(var s in Janus.sessions) { + if(Janus.sessions[s] !== null && Janus.sessions[s] !== undefined && + Janus.sessions[s].destroyOnUnload) { + Janus.log("Destroying session " + s); + Janus.sessions[s].destroy({asyncRequest: false}); + } + } + if(oldOBF && typeof oldOBF == "function") + oldOBF(); + } + function addJsList(srcArray) { + if (!srcArray || !Array.isArray(srcArray) || srcArray.length == 0) { + options.callback(); + } + var count = 0; + addJs(srcArray[count],next); + + function next() { + count++; + if (count<srcArray.length) { + addJs(srcArray[count],next); + } + else { + options.callback(); + } + } + } + function addJs(src,done) { + try { + if(adapter) { + // Already loaded + Janus.debug(src + " already loaded, skipping"); + done(); + return; + } + } catch(e) {}; + var oHead = document.getElementsByTagName('head').item(0); + var oScript = document.createElement("script"); + oScript.type = "text/javascript"; + oScript.src = src; + oScript.onload = function() { + Janus.log("Library " + src + " loaded"); + done(); + } + oHead.appendChild(oScript); + } + Janus.initDone = true; + addJsList(["adapter.js"]); // We may need others in the future + } +}; + +// Helper method to check whether WebRTC is supported by this browser +Janus.isWebrtcSupported = function() { + return window.RTCPeerConnection !== undefined && window.RTCPeerConnection !== null && + navigator.getUserMedia !== undefined && navigator.getUserMedia !== null; +}; + + +// Janus session object +function Janus(gatewayCallbacks) { + if(Janus.initDone === undefined) { + gatewayCallbacks.error("Library not initialized"); + return {}; + } + if(!Janus.isWebrtcSupported()) { + gatewayCallbacks.error("WebRTC not supported by this browser"); + return {}; + } + Janus.log("Library initialized: " + Janus.initDone); + 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(gatewayCallbacks.server === null || gatewayCallbacks.server === undefined) { + gatewayCallbacks.error("Invalid gateway url"); + return {}; + } + var websockets = false; + var ws = null; + var wsHandlers = {}; + var wsKeepaliveTimeoutId = null; + + var servers = null, serversIndex = 0; + var server = gatewayCallbacks.server; + if(Array.isArray(server)) { + Janus.log("Multiple servers provided (" + server.length + "), will use the first that works"); + server = null; + servers = gatewayCallbacks.server; + Janus.debug(servers); + } else { + if(server.indexOf("ws") === 0) { + websockets = true; + Janus.log("Using WebSockets to contact Janus: " + server); + } else { + websockets = false; + Janus.log("Using REST API to contact Janus: " + server); + } + } + var iceServers = gatewayCallbacks.iceServers; + if(iceServers === undefined || iceServers === null || iceServers.length === 0) + iceServers = [{urls: "stun:stun.l.google.com:19302"}]; + console.log('using ice', iceServers); + var iceTransportPolicy = gatewayCallbacks.iceTransportPolicy; + // Whether IPv6 candidates should be gathered + var ipv6Support = gatewayCallbacks.ipv6; + if(ipv6Support === undefined || ipv6Support === null) + ipv6Support = false; + // Optional max events + var maxev = null; + if(gatewayCallbacks.max_poll_events !== undefined && gatewayCallbacks.max_poll_events !== null) + maxev = gatewayCallbacks.max_poll_events; + if(maxev < 1) + maxev = 1; + // Token to use (only if the token based authentication mechanism is enabled) + var token = null; + if(gatewayCallbacks.token !== undefined && gatewayCallbacks.token !== null) + token = gatewayCallbacks.token; + // API secret to use (only if the shared API secret is enabled) + var apisecret = null; + if(gatewayCallbacks.apisecret !== undefined && gatewayCallbacks.apisecret !== null) + apisecret = gatewayCallbacks.apisecret; + // Whether we should destroy this session when onbeforeunload is called + this.destroyOnUnload = true; + if(gatewayCallbacks.destroyOnUnload !== undefined && gatewayCallbacks.destroyOnUnload !== null) + this.destroyOnUnload = (gatewayCallbacks.destroyOnUnload === true); + + var connected = false; + var sessionId = null; + var pluginHandles = {}; + var that = this; + var retries = 0; + var transactions = {}; + createSession(gatewayCallbacks); + + // Public methods + this.getServer = function() { return server; }; + this.isConnected = function() { return connected; }; + this.getSessionId = function() { return sessionId; }; + this.destroy = function(callbacks) { destroySession(callbacks, true); }; + this.attach = function(callbacks) { createHandle(callbacks); }; + + // Private method to create random identifiers (e.g., transaction) + function randomString(len) { + var charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + var randomString = ''; + for (var i = 0; i < len; i++) { + var randomPoz = Math.floor(Math.random() * charSet.length); + randomString += charSet.substring(randomPoz,randomPoz+1); + } + return randomString; + } + + function eventHandler() { + if(sessionId == null) + return; + Janus.debug('Long poll...'); + if(!connected) { + Janus.warn("Is the gateway down? (connected=false)"); + return; + } + var longpoll = server + "/" + sessionId + "?rid=" + new Date().getTime(); + if(maxev !== undefined && maxev !== null) + longpoll = longpoll + "&maxev=" + maxev; + if(token !== null && token !== undefined) + longpoll = longpoll + "&token=" + token; + if(apisecret !== null && apisecret !== undefined) + longpoll = longpoll + "&apisecret=" + apisecret; + Janus.ajax({ + type: 'GET', + url: longpoll, + cache: false, + timeout: 60000, // FIXME + success: handleEvent, + error: function(XMLHttpRequest, textStatus, errorThrown) { + Janus.error(textStatus + ": " + errorThrown); + //~ clearTimeout(timeoutTimer); + retries++; + if(retries > 3) { + // Did we just lose the gateway? :-( + connected = false; + gatewayCallbacks.error("Lost connection to the gateway (is it down?)"); + return; + } + eventHandler(); + }, + dataType: "json" + }); + } + + // Private event handler: this will trigger plugin callbacks, if set + function handleEvent(json) { + retries = 0; + if(!websockets && sessionId !== undefined && sessionId !== null) + setTimeout(eventHandler, 200); + if(!websockets && Array.isArray(json)) { + // We got an array: it means we passed a maxev > 1, iterate on all objects + for(var i=0; i<json.length; i++) { + handleEvent(json[i]); + } + return; + } + if(json["janus"] === "keepalive") { + // Nothing happened + Janus.vdebug("Got a keepalive on session " + sessionId); + return; + } else if(json["janus"] === "ack") { + // Just an ack, we can probably ignore + Janus.debug("Got an ack on session " + sessionId); + Janus.debug(json); + var transaction = json["transaction"]; + if(transaction !== null && transaction !== undefined) { + var reportSuccess = transactions[transaction]; + if(reportSuccess !== null && reportSuccess !== undefined) { + reportSuccess(json); + } + delete transactions[transaction]; + } + return; + } else if(json["janus"] === "success") { + // Success! + Janus.debug("Got a success on session " + sessionId); + Janus.debug(json); + var transaction = json["transaction"]; + if(transaction !== null && transaction !== undefined) { + var reportSuccess = transactions[transaction]; + if(reportSuccess !== null && reportSuccess !== undefined) { + reportSuccess(json); + } + delete transactions[transaction]; + } + return; + } else if(json["janus"] === "webrtcup") { + // The PeerConnection with the gateway is up! Notify this + Janus.debug("Got a webrtcup event on session " + sessionId); + Janus.debug(json); + var sender = json["sender"]; + if(sender === undefined || sender === null) { + Janus.warn("Missing sender..."); + return; + } + var pluginHandle = pluginHandles[sender]; + if(pluginHandle === undefined || pluginHandle === null) { + Janus.warn("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); + var sender = json["sender"]; + if(sender === undefined || sender === null) { + Janus.warn("Missing sender..."); + return; + } + var pluginHandle = pluginHandles[sender]; + if(pluginHandle === undefined || pluginHandle === null) { + Janus.warn("This handle is not attached to this session"); + return; + } + pluginHandle.webrtcState(false); + 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); + var sender = json["sender"]; + if(sender === undefined || sender === null) { + Janus.warn("Missing sender..."); + return; + } + var pluginHandle = pluginHandles[sender]; + if(pluginHandle === undefined || pluginHandle === null) { + // 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); + var sender = json["sender"]; + if(sender === undefined || sender === null) { + Janus.warn("Missing sender..."); + return; + } + var pluginHandle = pluginHandles[sender]; + if(pluginHandle === undefined || pluginHandle === null) { + Janus.warn("This handle is not attached to this session"); + return; + } + pluginHandle.mediaState(json["type"], json["receiving"]); + } else if(json["janus"] === "slowlink") { + Janus.debug("Got a slowlink event on session " + sessionId); + Janus.debug(json); + // Trouble uplink or downlink + var sender = json["sender"]; + if(sender === undefined || sender === null) { + Janus.warn("Missing sender..."); + return; + } + var pluginHandle = pluginHandles[sender]; + if(pluginHandle === undefined || pluginHandle === null) { + Janus.warn("This handle is not attached to this session"); + return; + } + pluginHandle.slowLink(json["uplink"], json["nacks"]); + } else if(json["janus"] === "error") { + // Oops, something wrong happened + Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME + Janus.debug(json); + var transaction = json["transaction"]; + if(transaction !== null && transaction !== undefined) { + var reportSuccess = transactions[transaction]; + if(reportSuccess !== null && reportSuccess !== undefined) { + reportSuccess(json); + } + delete transactions[transaction]; + } + return; + } else if(json["janus"] === "event") { + Janus.debug("Got a plugin event on session " + sessionId); + Janus.debug(json); + var sender = json["sender"]; + if(sender === undefined || sender === null) { + Janus.warn("Missing sender..."); + return; + } + var plugindata = json["plugindata"]; + if(plugindata === undefined || plugindata === null) { + Janus.warn("Missing plugindata..."); + return; + } + Janus.debug(" -- Event is coming from " + sender + " (" + plugindata["plugin"] + ")"); + var data = plugindata["data"]; + Janus.debug(data); + var pluginHandle = pluginHandles[sender]; + if(pluginHandle === undefined || pluginHandle === null) { + Janus.warn("This handle is not attached to this session"); + return; + } + var jsep = json["jsep"]; + if(jsep !== undefined && jsep !== null) { + Janus.debug("Handling SDP as well..."); + Janus.debug(jsep); + } + var callback = pluginHandle.onmessage; + if(callback !== null && callback !== undefined) { + 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 { + Janus.warn("Unkown message/event '" + json["janus"] + "' on session " + sessionId); + Janus.debug(json); + } + } + + // Private helper to send keep-alive messages on WebSockets + function keepAlive() { + if(server === null || !websockets || !connected) + return; + wsKeepaliveTimeoutId = setTimeout(keepAlive, 30000); + var request = { "janus": "keepalive", "session_id": sessionId, "transaction": randomString(12) }; + if(token !== null && token !== undefined) + request["token"] = token; + if(apisecret !== null && apisecret !== undefined) + request["apisecret"] = apisecret; + ws.send(JSON.stringify(request)); + } + + // Private method to create a session + function createSession(callbacks) { + var transaction = randomString(12); + var request = { "janus": "create", "transaction": transaction }; + if(token !== null && token !== undefined) + request["token"] = token; + if(apisecret !== null && apisecret !== undefined) + request["apisecret"] = apisecret; + if(server === null && Array.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 = new WebSocket(server, 'janus-protocol'); + wsHandlers = { + 'error': function() { + Janus.error("Error connecting to the Janus WebSockets server... " + server); + if (Array.isArray(servers)) { + 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 gateway 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 gateway 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, 30000); + connected = true; + sessionId = json.data["id"]; + 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 === null || !connected) { + return; + } + connected = false; + // FIXME What if this is called when the page is closed? + gatewayCallbacks.error("Lost connection to the gateway (is it down?)"); + } + }; + + for(var eventName in wsHandlers) { + ws.addEventListener(eventName, wsHandlers[eventName]); + } + + return; + } + Janus.ajax({ + type: 'POST', + url: server, + cache: false, + contentType: "application/json", + data: JSON.stringify(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.data["id"]; + Janus.log("Created session: " + sessionId); + Janus.sessions[sessionId] = that; + eventHandler(); + callbacks.success(); + }, + error: function(XMLHttpRequest, textStatus, errorThrown) { + Janus.error(textStatus + ": " + errorThrown); // FIXME + if(Array.isArray(servers)) { + 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 gateway down?"); + return; + } + // Let's try the next server + server = null; + setTimeout(function() { createSession(callbacks); }, 200); + return; + } + if(errorThrown === "") + callbacks.error(textStatus + ": Is the gateway down?"); + else + callbacks.error(textStatus + ": " + errorThrown); + }, + dataType: "json" + }); + } + + // 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; + var asyncRequest = true; + if(callbacks.asyncRequest !== undefined && callbacks.asyncRequest !== null) + asyncRequest = (callbacks.asyncRequest === true); + Janus.log("Destroying session " + sessionId + " (async=" + asyncRequest + ")"); + if(!connected) { + Janus.warn("Is the gateway down? (connected=false)"); + callbacks.success(); + return; + } + if(sessionId === undefined || sessionId === null) { + Janus.warn("No session to destroy"); + callbacks.success(); + gatewayCallbacks.destroyed(); + return; + } + delete Janus.sessions[sessionId]; + // Destroy all handles first + for(var ph in pluginHandles) { + var phv = pluginHandles[ph]; + Janus.log("Destroying handle " + phv.id + " (" + phv.plugin + ")"); + destroyHandle(phv.id, {asyncRequest: asyncRequest}); + } + // Ok, go on + var request = { "janus": "destroy", "transaction": randomString(12) }; + if(token !== null && token !== undefined) + request["token"] = token; + if(apisecret !== null && apisecret !== undefined) + request["apisecret"] = apisecret; + if(websockets) { + request["session_id"] = sessionId; + + var unbindWebSocket = function() { + for(var eventName in wsHandlers) { + ws.removeEventListener(eventName, wsHandlers[eventName]); + } + ws.removeEventListener('message', onUnbindMessage); + ws.removeEventListener('error', onUnbindError); + if(wsKeepaliveTimeoutId) { + clearTimeout(wsKeepaliveTimeoutId); + } + }; + + var onUnbindMessage = function(event){ + var data = JSON.parse(event.data); + if(data.session_id == request.session_id && data.transaction == request.transaction) { + unbindWebSocket(); + callbacks.success(); + gatewayCallbacks.destroyed(); + } + }; + var onUnbindError = function(event) { + unbindWebSocket(); + callbacks.error("Failed to destroy the gateway: Is the gateway down?"); + gatewayCallbacks.destroyed(); + }; + + ws.addEventListener('message', onUnbindMessage); + ws.addEventListener('error', onUnbindError); + + ws.send(JSON.stringify(request)); + return; + } + Janus.ajax({ + type: 'POST', + url: server + "/" + sessionId, + async: asyncRequest, // Sometimes we need false here, or destroying in onbeforeunload won't work + cache: false, + contentType: "application/json", + data: JSON.stringify(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(); + gatewayCallbacks.destroyed(); + }, + error: function(XMLHttpRequest, textStatus, errorThrown) { + Janus.error(textStatus + ": " + errorThrown); // FIXME + // Reset everything anyway + sessionId = null; + connected = false; + callbacks.success(); + gatewayCallbacks.destroyed(); + }, + dataType: "json" + }); + } + + // 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.consentDialog = (typeof callbacks.consentDialog == "function") ? callbacks.consentDialog : Janus.noop; + callbacks.mediaState = (typeof callbacks.mediaState == "function") ? callbacks.mediaState : Janus.noop; + callbacks.webrtcState = (typeof callbacks.webrtcState == "function") ? callbacks.webrtcState : Janus.noop; + callbacks.onmessage = (typeof callbacks.onmessage == "function") ? callbacks.onmessage : Janus.noop; + callbacks.onlocalstream = (typeof callbacks.onlocalstream == "function") ? callbacks.onlocalstream : Janus.noop; + callbacks.onremotestream = (typeof callbacks.onremotestream == "function") ? callbacks.onremotestream : 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 gateway down? (connected=false)"); + callbacks.error("Is the gateway down? (connected=false)"); + return; + } + var plugin = callbacks.plugin; + if(plugin === undefined || plugin === null) { + Janus.error("Invalid plugin"); + callbacks.error("Invalid plugin"); + return; + } + var transaction = randomString(12); + var request = { "janus": "attach", "plugin": plugin, "transaction": transaction }; + if(token !== null && token !== undefined) + request["token"] = token; + if(apisecret !== null && apisecret !== undefined) + 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; + } + var handleId = json.data["id"]; + Janus.log("Created handle: " + handleId); + var pluginHandle = + { + session : that, + plugin : 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 : function() { return handleId; }, + getPlugin : function() { return plugin; }, + getVolume : function() { return getVolume(handleId); }, + isAudioMuted : function() { return isMuted(handleId, false); }, + muteAudio : function() { return mute(handleId, false, true); }, + unmuteAudio : function() { return mute(handleId, false, false); }, + isVideoMuted : function() { return isMuted(handleId, true); }, + muteVideo : function() { return mute(handleId, true, true); }, + unmuteVideo : function() { return mute(handleId, true, false); }, + getBitrate : function() { return getBitrate(handleId); }, + send : function(callbacks) { sendMessage(handleId, callbacks); }, + data : function(callbacks) { sendData(handleId, callbacks); }, + dtmf : function(callbacks) { sendDtmf(handleId, callbacks); }, + consentDialog : callbacks.consentDialog, + mediaState : callbacks.mediaState, + webrtcState : callbacks.webrtcState, + onmessage : callbacks.onmessage, + createOffer : function(callbacks) { prepareWebrtc(handleId, callbacks); }, + createAnswer : function(callbacks) { prepareWebrtc(handleId, callbacks); }, + handleRemoteJsep : function(callbacks) { prepareWebrtcPeer(handleId, callbacks); }, + onlocalstream : callbacks.onlocalstream, + onremotestream : callbacks.onremotestream, + ondata : callbacks.ondata, + ondataopen : callbacks.ondataopen, + oncleanup : callbacks.oncleanup, + ondetached : callbacks.ondetached, + hangup : function(sendRequest) { cleanupWebrtc(handleId, sendRequest === true); }, + detach : function(callbacks) { destroyHandle(handleId, callbacks); } + } + pluginHandles[handleId] = pluginHandle; + callbacks.success(pluginHandle); + }; + request["session_id"] = sessionId; + ws.send(JSON.stringify(request)); + return; + } + Janus.ajax({ + type: 'POST', + url: server + "/" + sessionId, + cache: false, + contentType: "application/json", + data: JSON.stringify(request), + success: 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; + } + var handleId = json.data["id"]; + Janus.log("Created handle: " + handleId); + var pluginHandle = + { + session : that, + plugin : 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 : function() { return handleId; }, + getPlugin : function() { return plugin; }, + getVolume : function() { return getVolume(handleId); }, + isAudioMuted : function() { return isMuted(handleId, false); }, + muteAudio : function() { return mute(handleId, false, true); }, + unmuteAudio : function() { return mute(handleId, false, false); }, + isVideoMuted : function() { return isMuted(handleId, true); }, + muteVideo : function() { return mute(handleId, true, true); }, + unmuteVideo : function() { return mute(handleId, true, false); }, + getBitrate : function() { return getBitrate(handleId); }, + send : function(callbacks) { sendMessage(handleId, callbacks); }, + data : function(callbacks) { sendData(handleId, callbacks); }, + dtmf : function(callbacks) { sendDtmf(handleId, callbacks); }, + consentDialog : callbacks.consentDialog, + mediaState : callbacks.mediaState, + webrtcState : callbacks.webrtcState, + onmessage : callbacks.onmessage, + createOffer : function(callbacks) { prepareWebrtc(handleId, callbacks); }, + createAnswer : function(callbacks) { prepareWebrtc(handleId, callbacks); }, + handleRemoteJsep : function(callbacks) { prepareWebrtcPeer(handleId, callbacks); }, + onlocalstream : callbacks.onlocalstream, + onremotestream : callbacks.onremotestream, + ondata : callbacks.ondata, + ondataopen : callbacks.ondataopen, + oncleanup : callbacks.oncleanup, + ondetached : callbacks.ondetached, + hangup : function(sendRequest) { cleanupWebrtc(handleId, sendRequest === true); }, + detach : function(callbacks) { destroyHandle(handleId, callbacks); } + } + pluginHandles[handleId] = pluginHandle; + callbacks.success(pluginHandle); + }, + error: function(XMLHttpRequest, textStatus, errorThrown) { + Janus.error(textStatus + ": " + errorThrown); // FIXME + }, + dataType: "json" + }); + } + + // Private method to send a message + function sendMessage(handleId, callbacks) { + callbacks = callbacks || {}; + callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; + callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; + if(!connected) { + Janus.warn("Is the gateway down? (connected=false)"); + callbacks.error("Is the gateway down? (connected=false)"); + return; + } + var message = callbacks.message; + var jsep = callbacks.jsep; + var transaction = randomString(12); + var request = { "janus": "message", "body": message, "transaction": transaction }; + if(token !== null && token !== undefined) + request["token"] = token; + if(apisecret !== null && apisecret !== undefined) + request["apisecret"] = apisecret; + if(jsep !== null && jsep !== undefined) + request.jsep = jsep; + Janus.debug("Sending message to plugin (handle=" + handleId + "):"); + Janus.debug(request); + if(websockets) { + request["session_id"] = sessionId; + request["handle_id"] = handleId; + transactions[transaction] = function(json) { + Janus.debug("Message sent!"); + Janus.debug(json); + if(json["janus"] === "success") { + // We got a success, must have been a synchronous transaction + var plugindata = json["plugindata"]; + if(plugindata === undefined || plugindata === null) { + Janus.warn("Request succeeded, but missing plugindata..."); + callbacks.success(); + return; + } + Janus.log("Synchronous transaction successful (" + plugindata["plugin"] + ")"); + var data = plugindata["data"]; + Janus.debug(data); + callbacks.success(data); + return; + } else if(json["janus"] !== "ack") { + // Not a success and not an ack, must be an error + if(json["error"] !== undefined && json["error"] !== null) { + Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME + callbacks.error(json["error"].code + " " + json["error"].reason); + } else { + Janus.error("Unknown error"); // FIXME + callbacks.error("Unknown error"); + } + return; + } + // If we got here, the plugin decided to handle the request asynchronously + callbacks.success(); + }; + ws.send(JSON.stringify(request)); + return; + } + Janus.ajax({ + type: 'POST', + url: server + "/" + sessionId + "/" + handleId, + cache: false, + contentType: "application/json", + data: JSON.stringify(request), + success: function(json) { + Janus.debug("Message sent!"); + Janus.debug(json); + if(json["janus"] === "success") { + // We got a success, must have been a synchronous transaction + var plugindata = json["plugindata"]; + if(plugindata === undefined || plugindata === null) { + Janus.warn("Request succeeded, but missing plugindata..."); + callbacks.success(); + return; + } + Janus.log("Synchronous transaction successful (" + plugindata["plugin"] + ")"); + var data = plugindata["data"]; + Janus.debug(data); + callbacks.success(data); + return; + } else if(json["janus"] !== "ack") { + // Not a success and not an ack, must be an error + if(json["error"] !== undefined && json["error"] !== null) { + Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME + callbacks.error(json["error"].code + " " + json["error"].reason); + } else { + Janus.error("Unknown error"); // FIXME + callbacks.error("Unknown error"); + } + return; + } + // If we got here, the plugin decided to handle the request asynchronously + callbacks.success(); + }, + error: function(XMLHttpRequest, textStatus, errorThrown) { + Janus.error(textStatus + ": " + errorThrown); // FIXME + callbacks.error(textStatus + ": " + errorThrown); + }, + dataType: "json" + }); + } + + // Private method to send a trickle candidate + function sendTrickleCandidate(handleId, candidate) { + if(!connected) { + Janus.warn("Is the gateway down? (connected=false)"); + return; + } + var request = { "janus": "trickle", "candidate": candidate, "transaction": randomString(12) }; + if(token !== null && token !== undefined) + request["token"] = token; + if(apisecret !== null && apisecret !== undefined) + request["apisecret"] = apisecret; + Janus.vdebug("Sending trickle candidate (handle=" + handleId + "):"); + Janus.vdebug(request); + if(websockets) { + request["session_id"] = sessionId; + request["handle_id"] = handleId; + ws.send(JSON.stringify(request)); + return; + } + Janus.ajax({ + type: 'POST', + url: server + "/" + sessionId + "/" + handleId, + cache: false, + contentType: "application/json", + data: JSON.stringify(request), + success: function(json) { + Janus.vdebug("Candidate sent!"); + Janus.vdebug(json); + if(json["janus"] !== "ack") { + Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME + return; + } + }, + error: function(XMLHttpRequest, textStatus, errorThrown) { + Janus.error(textStatus + ": " + errorThrown); // FIXME + }, + dataType: "json" + }); + } + + // Private method to send a data channel message + function sendData(handleId, callbacks) { + callbacks = callbacks || {}; + callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; + callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; + var pluginHandle = pluginHandles[handleId]; + if(pluginHandle === null || pluginHandle === undefined || + pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) { + Janus.warn("Invalid handle"); + callbacks.error("Invalid handle"); + return; + } + var config = pluginHandle.webrtcStuff; + var text = callbacks.text; + if(text === null || text === undefined) { + Janus.warn("Invalid text"); + callbacks.error("Invalid text"); + return; + } + Janus.log("Sending string on data channel: " + text); + config.dataChannel.send(text); + callbacks.success(); + } + + // Private method to send a DTMF tone + function sendDtmf(handleId, callbacks) { + callbacks = callbacks || {}; + callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; + callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; + var pluginHandle = pluginHandles[handleId]; + if(pluginHandle === null || pluginHandle === undefined || + pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) { + Janus.warn("Invalid handle"); + callbacks.error("Invalid handle"); + return; + } + var config = pluginHandle.webrtcStuff; + if(config.dtmfSender === null || config.dtmfSender === undefined) { + // Create the DTMF sender, if possible + if(config.myStream !== undefined && config.myStream !== null) { + var tracks = config.myStream.getAudioTracks(); + if(tracks !== null && tracks !== undefined && tracks.length > 0) { + var local_audio_track = tracks[0]; + config.dtmfSender = config.pc.createDTMFSender(local_audio_track); + Janus.log("Created DTMF Sender"); + config.dtmfSender.ontonechange = function(tone) { Janus.debug("Sent DTMF tone: " + tone.tone); }; + } + } + if(config.dtmfSender === null || config.dtmfSender === undefined) { + Janus.warn("Invalid DTMF configuration"); + callbacks.error("Invalid DTMF configuration"); + return; + } + } + var dtmf = callbacks.dtmf; + if(dtmf === null || dtmf === undefined) { + Janus.warn("Invalid DTMF parameters"); + callbacks.error("Invalid DTMF parameters"); + return; + } + var tones = dtmf.tones; + if(tones === null || tones === undefined) { + Janus.warn("Invalid DTMF string"); + callbacks.error("Invalid DTMF string"); + return; + } + var duration = dtmf.duration; + if(duration === null || duration === undefined) + duration = 500; // We choose 500ms as the default duration for a tone + var gap = dtmf.gap; + if(gap === null || gap === undefined) + gap = 50; // We choose 50ms as the default gap between tones + Janus.debug("Sending DTMF string " + tones + " (duration " + duration + "ms, gap " + gap + "ms)"); + config.dtmfSender.insertDTMF(tones, duration, gap); + } + + // Private method to destroy a plugin handle + function destroyHandle(handleId, callbacks) { + callbacks = callbacks || {}; + callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; + callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; + var asyncRequest = true; + if(callbacks.asyncRequest !== undefined && callbacks.asyncRequest !== null) + asyncRequest = (callbacks.asyncRequest === true); + Janus.log("Destroying handle " + handleId + " (sync=" + asyncRequest + ")"); + cleanupWebrtc(handleId); + if(!connected) { + Janus.warn("Is the gateway down? (connected=false)"); + callbacks.error("Is the gateway down? (connected=false)"); + return; + } + var request = { "janus": "detach", "transaction": randomString(12) }; + if(token !== null && token !== undefined) + request["token"] = token; + if(apisecret !== null && apisecret !== undefined) + request["apisecret"] = apisecret; + if(websockets) { + request["session_id"] = sessionId; + request["handle_id"] = handleId; + ws.send(JSON.stringify(request)); + delete pluginHandles[handleId]; + callbacks.success(); + return; + } + Janus.ajax({ + type: 'POST', + url: server + "/" + sessionId + "/" + handleId, + async: asyncRequest, // Sometimes we need false here, or destroying in onbeforeunload won't work + cache: false, + contentType: "application/json", + data: JSON.stringify(request), + success: function(json) { + Janus.log("Destroyed handle:"); + Janus.debug(json); + if(json["janus"] !== "success") { + Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME + } + delete pluginHandles[handleId]; + callbacks.success(); + }, + error: function(XMLHttpRequest, textStatus, errorThrown) { + Janus.error(textStatus + ": " + errorThrown); // FIXME + // We cleanup anyway + delete pluginHandles[handleId]; + callbacks.success(); + }, + dataType: "json" + }); + } + + // WebRTC stuff + function streamsDone(handleId, jsep, media, callbacks, stream) { + var pluginHandle = pluginHandles[handleId]; + if(pluginHandle === null || pluginHandle === undefined || + pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) { + Janus.warn("Invalid handle"); + callbacks.error("Invalid handle"); + return; + } + var config = pluginHandle.webrtcStuff; + Janus.debug("streamsDone:", stream); + config.myStream = stream; + var pc_config = {"iceServers": iceServers, "iceTransportPolicy": iceTransportPolicy}; + //~ var pc_constraints = {'mandatory': {'MozDontOfferDataChannel':true}}; + var pc_constraints = { + "optional": [{"DtlsSrtpKeyAgreement": true}] + }; + if(ipv6Support === true) { + // FIXME This is only supported in Chrome right now + // For support in Firefox track this: https://bugzilla.mozilla.org/show_bug.cgi?id=797262 + pc_constraints.optional.push({"googIPv6":true}); + } + if(adapter.browserDetails.browser === "edge") { + // This is Edge, enable BUNDLE explicitly + pc_config.bundlePolicy = "max-bundle"; + } + Janus.log("Creating PeerConnection"); + Janus.debug(pc_constraints); + config.pc = new RTCPeerConnection(pc_config, pc_constraints); + Janus.debug(config.pc); + if(config.pc.getStats) { // FIXME + config.volume.value = 0; + config.bitrate.value = "0 kbits/sec"; + } + Janus.log("Preparing local SDP and gathering candidates (trickle=" + config.trickle + ")"); + config.pc.onicecandidate = function(event) { + if (event.candidate == null || + (adapter.browserDetails.browser === 'edge' && event.candidate.candidate.indexOf('endOfCandidates') > 0)) { + Janus.log("End of candidates."); + config.iceDone = true; + if(config.trickle === true) { + // Notify end of candidates + sendTrickleCandidate(handleId, {"completed": true}); + } else { + // No trickle, time to send the complete SDP (including all candidates) + 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 + var candidate = { + "candidate": event.candidate.candidate, + "sdpMid": event.candidate.sdpMid, + "sdpMLineIndex": event.candidate.sdpMLineIndex + }; + if(config.trickle === true) { + // Send candidate + sendTrickleCandidate(handleId, candidate); + } + } + }; + if(stream !== null && stream !== undefined) { + Janus.log('Adding local stream'); + config.pc.addStream(stream); + pluginHandle.onlocalstream(stream); + } + config.pc.onaddstream = function(remoteStream) { + Janus.log("Handling Remote Stream"); + Janus.debug(remoteStream); + config.remoteStream = remoteStream; + pluginHandle.onremotestream(remoteStream.stream); + }; + // Any data channel to create? + if(isDataEnabled(media)) { + Janus.log("Creating data channel"); + var onDataChannelMessage = function(event) { + Janus.log('Received message on data channel: ' + event.data); + pluginHandle.ondata(event.data); // FIXME + } + var onDataChannelStateChange = function() { + var dcState = config.dataChannel !== null ? config.dataChannel.readyState : "null"; + Janus.log('State change on data channel: ' + dcState); + if(dcState === 'open') { + pluginHandle.ondataopen(); // FIXME + } + } + var onDataChannelError = function(error) { + Janus.error('Got error on data channel:', error); + // TODO + } + // Until we implement the proxying of open requests within the Janus core, we open a channel ourselves whatever the case + config.dataChannel = config.pc.createDataChannel("JanusDataChannel", {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 + if(jsep === null || jsep === undefined) { + createOffer(handleId, media, callbacks); + } else { + if(adapter.browserDetails.browser === "edge") { + // This is Edge, add an a=end-of-candidates at the end + jsep.sdp += "a=end-of-candidates\r\n"; + } + config.pc.setRemoteDescription( + new RTCSessionDescription(jsep), + function() { + Janus.log("Remote description accepted!"); + createAnswer(handleId, media, callbacks); + }, callbacks.error); + } + } + + function prepareWebrtc(handleId, callbacks) { + callbacks = callbacks || {}; + callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; + callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : webrtcError; + var jsep = callbacks.jsep; + var media = callbacks.media; + var pluginHandle = pluginHandles[handleId]; + if(pluginHandle === null || pluginHandle === undefined || + pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) { + Janus.warn("Invalid handle"); + callbacks.error("Invalid handle"); + return; + } + var config = pluginHandle.webrtcStuff; + // Are we updating a session? + if(config.pc !== undefined && config.pc !== null) { + Janus.log("Updating existing media session"); + // Create offer/answer now + if(jsep === null || jsep === undefined) { + createOffer(handleId, media, callbacks); + } else { + if(adapter.browserDetails.browser === "edge") { + // This is Edge, add an a=end-of-candidates at the end + jsep.sdp += "a=end-of-candidates\r\n"; + } + config.pc.setRemoteDescription( + new RTCSessionDescription(jsep), + function() { + Janus.log("Remote description accepted!"); + createAnswer(handleId, media, callbacks); + }, callbacks.error); + } + return; + } + // Was a MediaStream object passed, or do we need to take care of that? + if(callbacks.stream !== null && callbacks.stream !== undefined) { + var stream = callbacks.stream; + Janus.log("MediaStream provided by the application"); + Janus.debug(stream); + // Skip the getUserMedia part + config.streamExternal = true; + streamsDone(handleId, jsep, media, callbacks, stream); + return; + } + config.trickle = isTrickleEnabled(callbacks.trickle); + if(isAudioSendEnabled(media) || isVideoSendEnabled(media)) { + var constraints = { mandatory: {}, optional: []}; + pluginHandle.consentDialog(true); + var audioSupport = isAudioSendEnabled(media); + if(audioSupport === true && media != undefined && media != null) { + if(typeof media.audio === 'object') { + audioSupport = media.audio; + } + } + var videoSupport = isVideoSendEnabled(media); + if(videoSupport === true && media != undefined && media != null) { + if(media.video && media.video != 'screen' && media.video != 'window') { + var width = 0; + var height = 0, 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) { + var firefoxVer = parseInt(window.navigator.userAgent.match(/Firefox\/(.*)/)[1], 10); + if(firefoxVer < 38) { + // Unless this is and old Firefox, which doesn't support it + Janus.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 { + Janus.log("Default video setting (" + media.video + ") is stdres 4:3"); + height = 480; + maxHeight = 480; + width = 640; + } + Janus.log("Adding media constraint " + media.video); + if(navigator.mozGetUserMedia) { + var 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': maxHeight, + 'minHeight': height, + 'maxWidth': width, + 'minWidth': width + }, + 'optional': [] + }; + } + if(typeof media.video === 'object') { + videoSupport = media.video; + } + Janus.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 + Janus.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+ + var cache = {}; + function callbackUserMedia (error, stream) { + pluginHandle.consentDialog(false); + if(error) { + callbacks.error({code: error.code, name: error.name, message: error.message}); + } else { + streamsDone(handleId, jsep, media, callbacks, stream); + } + }; + function getScreenMedia(constraints, gsmCallback) { + Janus.log("Adding media constraint (screen capture)"); + Janus.debug(constraints); + navigator.mediaDevices.getUserMedia(constraints) + .then(function(stream) { gsmCallback(null, stream); }) + .catch(function(error) { pluginHandle.consentDialog(false); gsmCallback(error); }); + }; + if(adapter.browserDetails.browser === 'chrome') { + var chromever = adapter.browserDetails.version; + var maxver = 33; + if(window.navigator.userAgent.match('Linux')) + maxver = 35; // "known" crash in chrome 34 and 35 on linux + if(chromever >= 26 && chromever <= maxver) { + // Chrome 26->33 requires some awkward chrome://flags manipulation + constraints = { + video: { + mandatory: { + googLeakyBucket: true, + maxWidth: window.screen.width, + maxHeight: window.screen.height, + maxFrameRate: 3, + chromeMediaSource: 'screen' + } + }, + audio: isAudioSendEnabled(media) + }; + getScreenMedia(constraints, callbackUserMedia); + } else { + // Chrome 34+ requires an extension + var pending = window.setTimeout( + function () { + 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')) { + var 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: isAudioSendEnabled(media) + }; + getScreenMedia(constraints, function (err, stream) { + callbackUserMedia(err, stream); + // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1045810 + if (!err) { + var lastTime = stream.currentTime; + var polly = window.setInterval(function () { + if(!stream) + window.clearInterval(polly); + if(stream.currentTime == lastTime) { + window.clearInterval(polly); + if(stream.onended) { + stream.onended(); + } + } + lastTime = stream.currentTime; + }, 500); + } + }); + } else { + var error = new Error('NavigatorUserMediaError'); + error.name = 'Your version of Firefox does not support screen sharing, please install Firefox 33 (or more recent versions)'; + pluginHandle.consentDialog(false); + callbacks.error(error); + return; + } + } + + // Wait for events from the Chrome Extension + window.addEventListener('message', function (event) { + if(event.origin != window.location.origin) + return; + if(event.data.type == 'janusGotScreen' && cache[event.data.id]) { + var data = cache[event.data.id]; + var callback = data[0]; + delete cache[event.data.id]; + + if (event.data.sourceId === '') { + // user canceled + var error = new Error('NavigatorUserMediaError'); + error.name = 'You cancelled the request for permission, giving up...'; + pluginHandle.consentDialog(false); + callbacks.error(error); + } else { + constraints = { + audio: 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 == 'janusGetScreenPending') { + window.clearTimeout(event.data.id); + } + }); + return; + } + } + // If we got here, we're not screensharing + if(media === null || media === undefined || media.video !== 'screen') { + // Check whether all media sources are actually available or not + navigator.mediaDevices.enumerateDevices().then(function(devices) { + var audioExist = devices.some(function(device) { + return device.kind === 'audioinput'; + }), + videoExist = devices.some(function(device) { + return device.kind === 'videoinput'; + }); + + // Check whether a missing device is really a problem + var audioSend = isAudioSendEnabled(media); + var videoSend = isVideoSendEnabled(media); + var needAudioDevice = isAudioSendRequired(media); + var needVideoDevice = isVideoSendRequired(media); + if(audioSend || videoSend || needAudioDevice || needVideoDevice) { + // We need to send either audio or video + var haveAudioDevice = audioSend ? audioExist : false; + var haveVideoDevice = videoSend ? videoExist : false; + if(!haveAudioDevice && !haveVideoDevice) { + // FIXME Should we really give up, or just assume recvonly for both? + pluginHandle.consentDialog(false); + callbacks.error('No capture device found'); + return false; + } else if(!haveAudioDevice && needAudioDevice) { + pluginHandle.consentDialog(false); + callbacks.error('Audio capture is required, but no capture device found'); + return false; + } else if(!haveVideoDevice && needVideoDevice) { + pluginHandle.consentDialog(false); + callbacks.error('Video capture is required, but no capture device found'); + return false; + } + } + + navigator.mediaDevices.getUserMedia({ + audio: audioExist ? audioSupport : false, + video: videoExist ? videoSupport : false + }) + .then(function(stream) { pluginHandle.consentDialog(false); streamsDone(handleId, jsep, media, callbacks, stream); }) + .catch(function(error) { pluginHandle.consentDialog(false); callbacks.error({code: error.code, name: error.name, message: error.message}); }); + }) + .catch(function(error) { + pluginHandle.consentDialog(false); + callbacks.error('enumerateDevices error', error); + }); + } + } else { + // No need to do a getUserMedia, create offer/answer right away + streamsDone(handleId, jsep, media, callbacks); + } + } + + function prepareWebrtcPeer(handleId, callbacks) { + callbacks = callbacks || {}; + callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; + callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : webrtcError; + var jsep = callbacks.jsep; + var pluginHandle = pluginHandles[handleId]; + if(pluginHandle === null || pluginHandle === undefined || + pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) { + Janus.warn("Invalid handle"); + callbacks.error("Invalid handle"); + return; + } + var config = pluginHandle.webrtcStuff; + if(jsep !== undefined && jsep !== null) { + if(config.pc === null) { + Janus.warn("Wait, no PeerConnection?? if this is an answer, use createAnswer and not handleRemoteJsep"); + callbacks.error("No PeerConnection: if this is an answer, use createAnswer and not handleRemoteJsep"); + return; + } + if(adapter.browserDetails.browser === "edge") { + // This is Edge, add an a=end-of-candidates at the end + jsep.sdp += "a=end-of-candidates\r\n"; + } + config.pc.setRemoteDescription( + new RTCSessionDescription(jsep), + function() { + Janus.log("Remote description accepted!"); + callbacks.success(); + }, callbacks.error); + } else { + callbacks.error("Invalid JSEP"); + } + } + + function createOffer(handleId, media, callbacks) { + callbacks = callbacks || {}; + callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; + callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; + var pluginHandle = pluginHandles[handleId]; + if(pluginHandle === null || pluginHandle === undefined || + pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) { + Janus.warn("Invalid handle"); + callbacks.error("Invalid handle"); + return; + } + var config = pluginHandle.webrtcStuff; + Janus.log("Creating offer (iceDone=" + config.iceDone + ")"); + // https://code.google.com/p/webrtc/issues/detail?id=3508 + var mediaConstraints = null; + if(adapter.browserDetails.browser == "firefox" || adapter.browserDetails.browser == "edge") { + mediaConstraints = { + 'offerToReceiveAudio':isAudioRecvEnabled(media), + 'offerToReceiveVideo':isVideoRecvEnabled(media) + }; + } else { + mediaConstraints = { + 'mandatory': { + 'OfferToReceiveAudio':isAudioRecvEnabled(media), + 'OfferToReceiveVideo':isVideoRecvEnabled(media) + } + }; + } + Janus.debug(mediaConstraints); + config.pc.createOffer( + function(offer) { + Janus.debug(offer); + if(config.mySdp === null || config.mySdp === undefined) { + Janus.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 + Janus.log("Waiting for all candidates..."); + return; + } + if(config.sdpSent) { + Janus.log("Offer already sent, not sending it again"); + return; + } + Janus.log("Offer ready"); + Janus.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 + var jsep = { + "type": offer.type, + "sdp": offer.sdp + }; + callbacks.success(jsep); + }, callbacks.error, mediaConstraints); + } + + function createAnswer(handleId, media, callbacks) { + callbacks = callbacks || {}; + callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; + callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; + var pluginHandle = pluginHandles[handleId]; + if(pluginHandle === null || pluginHandle === undefined || + pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) { + Janus.warn("Invalid handle"); + callbacks.error("Invalid handle"); + return; + } + var config = pluginHandle.webrtcStuff; + Janus.log("Creating answer (iceDone=" + config.iceDone + ")"); + var mediaConstraints = null; + if(adapter.browserDetails.browser == "firefox" || adapter.browserDetails.browser == "edge") { + mediaConstraints = { + 'offerToReceiveAudio':isAudioRecvEnabled(media), + 'offerToReceiveVideo':isVideoRecvEnabled(media) + }; + } else { + mediaConstraints = { + 'mandatory': { + 'OfferToReceiveAudio':isAudioRecvEnabled(media), + 'OfferToReceiveVideo':isVideoRecvEnabled(media) + } + }; + } + Janus.debug(mediaConstraints); + config.pc.createAnswer( + function(answer) { + Janus.debug(answer); + if(config.mySdp === null || config.mySdp === undefined) { + Janus.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 + Janus.log("Waiting for all candidates..."); + return; + } + if(config.sdpSent) { // FIXME badly + Janus.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 + var jsep = { + "type": answer.type, + "sdp": answer.sdp + }; + callbacks.success(jsep); + }, callbacks.error, mediaConstraints); + } + + function sendSDP(handleId, callbacks) { + callbacks = callbacks || {}; + callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop; + callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop; + var pluginHandle = pluginHandles[handleId]; + if(pluginHandle === null || pluginHandle === undefined || + pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) { + Janus.warn("Invalid handle, not sending anything"); + return; + } + var config = pluginHandle.webrtcStuff; + Janus.log("Sending offer/answer SDP..."); + if(config.mySdp === null || config.mySdp === undefined) { + Janus.warn("Local SDP instance is invalid, not sending anything..."); + return; + } + config.mySdp = { + "type": config.pc.localDescription.type, + "sdp": config.pc.localDescription.sdp + }; + if(config.sdpSent) { + Janus.log("Offer/Answer SDP already sent, not sending it again"); + return; + } + if(config.trickle === false) + config.mySdp["trickle"] = false; + Janus.debug(callbacks); + config.sdpSent = true; + callbacks.success(config.mySdp); + } + + function getVolume(handleId) { + var pluginHandle = pluginHandles[handleId]; + if(pluginHandle === null || pluginHandle === undefined || + pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) { + Janus.warn("Invalid handle"); + return 0; + } + var config = pluginHandle.webrtcStuff; + // Start getting the volume, if getStats is supported + if(config.pc.getStats && adapter.browserDetails.browser == "chrome") { // FIXME + if(config.remoteStream === null || config.remoteStream === undefined) { + Janus.warn("Remote stream unavailable"); + return 0; + } + // http://webrtc.googlecode.com/svn/trunk/samples/js/demos/html/constraints-and-stats.html + if(config.volume.timer === null || config.volume.timer === undefined) { + Janus.log("Starting volume monitor"); + config.volume.timer = setInterval(function() { + config.pc.getStats(function(stats) { + var results = stats.result(); + for(var i=0; i<results.length; i++) { + var 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; + } else { + Janus.log("Getting the remote volume unsupported by browser"); + return 0; + } + } + + function isMuted(handleId, video) { + var pluginHandle = pluginHandles[handleId]; + if(pluginHandle === null || pluginHandle === undefined || + pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) { + Janus.warn("Invalid handle"); + return true; + } + var config = pluginHandle.webrtcStuff; + if(config.pc === null || config.pc === undefined) { + Janus.warn("Invalid PeerConnection"); + return true; + } + if(config.myStream === undefined || config.myStream === null) { + Janus.warn("Invalid local MediaStream"); + return true; + } + if(video) { + // Check video track + if(config.myStream.getVideoTracks() === null + || config.myStream.getVideoTracks() === undefined + || config.myStream.getVideoTracks().length === 0) { + Janus.warn("No video track"); + return true; + } + return !config.myStream.getVideoTracks()[0].enabled; + } else { + // Check audio track + if(config.myStream.getAudioTracks() === null + || config.myStream.getAudioTracks() === undefined + || config.myStream.getAudioTracks().length === 0) { + Janus.warn("No audio track"); + return true; + } + return !config.myStream.getAudioTracks()[0].enabled; + } + } + + function mute(handleId, video, mute) { + var pluginHandle = pluginHandles[handleId]; + if(pluginHandle === null || pluginHandle === undefined || + pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) { + Janus.warn("Invalid handle"); + return false; + } + var config = pluginHandle.webrtcStuff; + if(config.pc === null || config.pc === undefined) { + Janus.warn("Invalid PeerConnection"); + return false; + } + if(config.myStream === undefined || config.myStream === null) { + Janus.warn("Invalid local MediaStream"); + return false; + } + if(video) { + // Mute/unmute video track + if(config.myStream.getVideoTracks() === null + || config.myStream.getVideoTracks() === undefined + || config.myStream.getVideoTracks().length === 0) { + Janus.warn("No video track"); + return false; + } + config.myStream.getVideoTracks()[0].enabled = mute ? false : true; + return true; + } else { + // Mute/unmute audio track + if(config.myStream.getAudioTracks() === null + || config.myStream.getAudioTracks() === undefined + || config.myStream.getAudioTracks().length === 0) { + Janus.warn("No audio track"); + return false; + } + config.myStream.getAudioTracks()[0].enabled = mute ? false : true; + return true; + } + } + + function getBitrate(handleId) { + var pluginHandle = pluginHandles[handleId]; + if(pluginHandle === null || pluginHandle === undefined || + pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) { + Janus.warn("Invalid handle"); + return "Invalid handle"; + } + var config = pluginHandle.webrtcStuff; + if(config.pc === null || config.pc === undefined) + return "Invalid PeerConnection"; + // Start getting the bitrate, if getStats is supported + if(config.pc.getStats && adapter.browserDetails.browser == "chrome") { + // Do it the Chrome way + if(config.remoteStream === null || config.remoteStream === undefined) { + Janus.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 === null || config.bitrate.timer === undefined) { + Janus.log("Starting bitrate timer (Chrome)"); + config.bitrate.timer = setInterval(function() { + config.pc.getStats(function(stats) { + var results = stats.result(); + for(var i=0; i<results.length; i++) { + var 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'; + //~ Janus.log("Estimated bitrate is " + config.bitrate.value); + config.bitrate.bsbefore = config.bitrate.bsnow; + config.bitrate.tsbefore = config.bitrate.tsnow; + } + } + } + }); + }, 1000); + return "0 kbits/sec"; // We don't have a bitrate value yet + } + return config.bitrate.value; + } else if(config.pc.getStats && adapter.browserDetails.browser == "firefox") { + // Do it the Firefox way + if(config.remoteStream === null || config.remoteStream === undefined + || config.remoteStream.stream === null || config.remoteStream.stream === undefined) { + Janus.warn("Remote stream unavailable"); + return "Remote stream unavailable"; + } + var videoTracks = config.remoteStream.stream.getVideoTracks(); + if(videoTracks === null || videoTracks === undefined || videoTracks.length < 1) { + Janus.warn("No video track"); + return "No video track"; + } + // https://github.com/muaz-khan/getStats/blob/master/getStats.js + if(config.bitrate.timer === null || config.bitrate.timer === undefined) { + Janus.log("Starting bitrate timer (Firefox)"); + config.bitrate.timer = setInterval(function() { + // We need a helper callback + var cb = function(res) { + if(res === null || res === undefined || + 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], function(stats) { + cb(stats); + }, cb); + }, 1000); + return "0 kbits/sec"; // We don't have a bitrate value yet + } + return config.bitrate.value; + } else { + Janus.warn("Getting the video bitrate unsupported by browser"); + return "Feature unsupported by browser"; + } + } + + function webrtcError(error) { + Janus.error("WebRTC error:", error); + } + + function cleanupWebrtc(handleId, hangupRequest) { + Janus.log("Cleaning WebRTC stuff"); + var pluginHandle = pluginHandles[handleId]; + if(pluginHandle === null || pluginHandle === undefined) { + // Nothing to clean + return; + } + var config = pluginHandle.webrtcStuff; + if(config !== null && config !== undefined) { + if(hangupRequest === true) { + // Send a hangup request (we don't really care about the response) + var request = { "janus": "hangup", "transaction": randomString(12) }; + if(token !== null && token !== undefined) + request["token"] = token; + if(apisecret !== null && apisecret !== undefined) + request["apisecret"] = apisecret; + Janus.debug("Sending hangup request (handle=" + handleId + "):"); + Janus.debug(request); + if(websockets) { + request["session_id"] = sessionId; + request["handle_id"] = handleId; + ws.send(JSON.stringify(request)); + } else { + $.ajax({ + type: 'POST', + url: server + "/" + sessionId + "/" + handleId, + cache: false, + contentType: "application/json", + data: JSON.stringify(request), + dataType: "json" + }); + } + } + // 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 !== null && config.myStream !== undefined) { + Janus.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 !== null && config.myStream !== undefined) { + Janus.log("Stopping local stream tracks"); + var tracks = config.myStream.getTracks(); + for(var i in tracks) { + var mst = tracks[i]; + Janus.log(mst); + if(mst !== null && mst !== undefined) + mst.stop(); + } + } + } 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(); + } + + // Helper methods to parse a media object + function isAudioSendEnabled(media) { + Janus.debug("isAudioSendEnabled:", media); + if(media === undefined || media === null) + return true; // Default + if(media.audio === false) + return false; // Generic audio has precedence + if(media.audioSend === undefined || media.audioSend === null) + return true; // Default + return (media.audioSend === true); + } + + function isAudioSendRequired(media) { + Janus.debug("isAudioSendRequired:", media); + if(media === undefined || media === null) + return false; // Default + if(media.audio === false || media.audioSend === false) + return false; // If we're not asking to capture audio, it's not required + if(media.failIfNoAudio === undefined || media.failIfNoAudio === null) + return false; // Default + return (media.failIfNoAudio === true); + } + + function isAudioRecvEnabled(media) { + Janus.debug("isAudioRecvEnabled:", media); + if(media === undefined || media === null) + return true; // Default + if(media.audio === false) + return false; // Generic audio has precedence + if(media.audioRecv === undefined || media.audioRecv === null) + return true; // Default + return (media.audioRecv === true); + } + + function isVideoSendEnabled(media) { + Janus.debug("isVideoSendEnabled:", media); + if(media === undefined || media === null) + return true; // Default + if(media.video === false) + return false; // Generic video has precedence + if(media.videoSend === undefined || media.videoSend === null) + return true; // Default + return (media.videoSend === true); + } + + function isVideoSendRequired(media) { + Janus.debug("isVideoSendRequired:", media); + if(media === undefined || media === null) + return false; // Default + if(media.video === false || media.videoSend === false) + return false; // If we're not asking to capture video, it's not required + if(media.failIfNoVideo === undefined || media.failIfNoVideo === null) + return false; // Default + return (media.failIfNoVideo === true); + } + + function isVideoRecvEnabled(media) { + Janus.debug("isVideoRecvEnabled:", media); + if(media === undefined || media === null) + return true; // Default + if(media.video === false) + return false; // Generic video has precedence + if(media.videoRecv === undefined || media.videoRecv === null) + return true; // Default + return (media.videoRecv === true); + } + + function isDataEnabled(media) { + Janus.debug("isDataEnabled:", media); + if(adapter.browserDetails.browser == "edge") { + Janus.warn("Edge doesn't support data channels yet"); + return false; + } + if(media === undefined || media === null) + return false; // Default + return (media.data === true); + } + + function isTrickleEnabled(trickle) { + Janus.debug("isTrickleEnabled:", trickle); + if(trickle === undefined || trickle === null) + return true; // Default is true + return (trickle === true); + } +}; + +module.exports = Janus; diff --git a/webapp/package.json b/webapp/package.json index 0796bfb70..1086fea30 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -32,7 +32,7 @@ "superagent": "2.3.0", "twemoji": "2.2.0", "velocity-animate": "1.3.1", - "webrtc-adapter": "2.0.8", + "webrtc-adapter": "2.1.0", "xregexp": "3.1.1" }, "devDependencies": { |