1967 строки
76 KiB
JavaScript
1967 строки
76 KiB
JavaScript
// 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);
|
|
}
|
|
}
|
|
}
|