Skip to content

Instantly share code, notes, and snippets.

@roxlu
Last active November 3, 2021 20:19
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save roxlu/cacadd912462a0a810c247e9c628e57d to your computer and use it in GitHub Desktop.
Save roxlu/cacadd912462a0a810c247e9c628e57d to your computer and use it in GitHub Desktop.
Created this code while debuggging an issue where Janus only records a video of 8 bytes. Janus log: https://gist.github.com/roxlu/08f32169122dd23e132af7abfb37f928 You can clone this into a vhost; the test html contains a working version that allows you to create multiple video recordings w/o interruptions.
/*
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 Janus.checkJanusExtension();
} else {
// Firefox of others, no need for the extension (but this doesn't mean it will work)
return true;
}
};
Janus.useDefaultDependencies = function (deps) {
var f = (deps && deps.fetch) || fetch;
var p = (deps && deps.Promise) || Promise;
var socketCls = (deps && deps.WebSocket) || WebSocket;
return {
newWebSocket: function(server, proto) { return new socketCls(server, proto); },
isArray: function(arr) { return Array.isArray(arr); },
checkJanusExtension: function() { return document.querySelector('#janus-extension-installed') !== null; },
webRTCAdapter: (deps && deps.adapter) || adapter,
httpAPICall: function(url, options) {
var fetchOptions = {
method: options.verb,
headers: {
'Accept': 'application/json, text/plain, */*'
},
cache: 'no-cache'
};
if(options.verb === "POST") {
fetchOptions.headers['Content-Type'] = 'application/json';
}
if(options.withCredentials !== undefined) {
fetchOptions.credentials = options.withCredentials === true ? 'include' : (options.withCredentials ? options.withCredentials : 'omit');
}
if(options.body !== undefined) {
fetchOptions.body = JSON.stringify(options.body);
}
var fetching = f(url, fetchOptions).catch(function(error) {
return p.reject({message: 'Probably a network error, is the gateway down?', error: error});
});
/*
* fetch() does not natively support timeouts.
* Work around this by starting a timeout manually, and racing it agains the fetch() to see which thing resolves first.
*/
if(options.timeout !== undefined) {
var timeout = new p(function(resolve, reject) {
var timerId = setTimeout(function() {
clearTimeout(timerId);
return reject({message: 'Request timed out', timeout: options.timeout});
}, options.timeout);
});
fetching = p.race([fetching,timeout]);
}
fetching.then(function(response) {
if(response.ok) {
if(typeof(options.success) === typeof(Janus.noop)) {
return response.json().then(function(parsed) {
options.success(parsed);
}).catch(function(error) {
return p.reject({message: 'Failed to parse response body', error: error, response: response});
});
}
}
else {
return p.reject({message: 'API call failed', response: response});
}
}).catch(function(error) {
if(typeof(options.error) === typeof(Janus.noop)) {
options.error(error.message || '<< internal error >>', error);
}
});
return fetching;
}
}
};
Janus.useOldDependencies = function (deps) {
var jq = (deps && deps.jQuery) || jQuery;
var socketCls = (deps && deps.WebSocket) || WebSocket;
return {
newWebSocket: function(server, proto) { return new socketCls(server, proto); },
isArray: function(arr) { return jq.isArray(arr); },
checkJanusExtension: function() { return jq('#janus-extension-installed').length > 0; },
webRTCAdapter: (deps && deps.adapter) || adapter,
httpAPICall: function(url, options) {
var payload = options.body !== undefined ? {
contentType: 'application/json',
data: JSON.stringify(options.body)
} : {};
var credentials = options.withCredentials !== undefined ? {xhrFields: {withCredentials: options.withCredentials}} : {};
return jq.ajax(jq.extend(payload, credentials, {
url: url,
type: options.verb,
cache: false,
dataType: 'json',
async: options.async,
timeout: options.timeout,
success: function(result) {
if(typeof(options.success) === typeof(Janus.noop)) {
options.success(result);
}
},
error: function(xhr, status, err) {
if(typeof(options.error) === typeof(Janus.noop)) {
options.error(status, err);
}
}
}));
},
};
};
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");
var usedDependencies = options.dependencies || Janus.useDefaultDependencies();
Janus.isArray = usedDependencies.isArray;
Janus.webRTCAdapter = usedDependencies.webRTCAdapter;
Janus.httpAPICall = usedDependencies.httpAPICall;
Janus.checkJanusExtension = usedDependencies.checkJanusExtension;
Janus.newWebSocket = usedDependencies.newWebSocket;
// Helper method to enumerate devices
Janus.listDevices = function(callback, config) {
callback = (typeof callback == "function") ? callback : Janus.noop;
if (config == null) config = { audio: true, video: true };
if(navigator.mediaDevices) {
navigator.mediaDevices.getUserMedia(config)
.then(function(stream) {
navigator.mediaDevices.enumerateDevices().then(function(devices) {
Janus.debug(devices);
callback(devices);
// Get rid of the now useless stream
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(Janus.webRTCAdapter.browserDetails.browser === 'chrome') {
var chromever = Janus.webRTCAdapter.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(Janus.webRTCAdapter.browserDetails.browser === 'chrome') {
var chromever = Janus.webRTCAdapter.browserDetails.version;
if(chromever >= 43) {
to.srcObject = from.srcObject;
} else if(typeof to.src !== 'undefined') {
to.src = from.src;
} else {
Janus.error("Error reattaching stream to element");
}
} else {
to.srcObject = from.srcObject;
}
};
// 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, notifyDestroyed: false});
}
}
if(oldOBF && typeof oldOBF == "function")
oldOBF();
}
Janus.initDone = true;
options.callback();
}
};
// 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;
};
// Helper method to create random identifiers (e.g., transaction)
Janus.randomString = function(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 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(Janus.isArray(server)) {
Janus.log("Multiple servers provided (" + server.length + "), will use the first that works");
server = null;
servers = gatewayCallbacks.server;
Janus.debug(servers);
} else {
if(server.indexOf("ws") === 0) {
websockets = true;
Janus.log("Using WebSockets to contact Janus: " + server);
} else {
websockets = false;
Janus.log("Using REST API to contact Janus: " + server);
}
}
var iceServers = gatewayCallbacks.iceServers;
if(iceServers === undefined || iceServers === null)
iceServers = [{urls: "stun:stun.l.google.com:19302"}];
var iceTransportPolicy = gatewayCallbacks.iceTransportPolicy;
var bundlePolicy = gatewayCallbacks.bundlePolicy;
// Whether IPv6 candidates should be gathered
var ipv6Support = gatewayCallbacks.ipv6;
if(ipv6Support === undefined || ipv6Support === null)
ipv6Support = false;
// Whether we should enable the withCredentials flag for XHR requests
var withCredentials = false;
if(gatewayCallbacks.withCredentials !== undefined && gatewayCallbacks.withCredentials !== null)
withCredentials = gatewayCallbacks.withCredentials === true;
// 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); };
this.attach = function(callbacks) { createHandle(callbacks); };
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.httpAPICall(longpoll, {
verb: 'GET',
withCredentials: withCredentials,
success: handleEvent,
timeout: 60000, // FIXME
error: function(textStatus, errorThrown) {
Janus.error(textStatus + ":", errorThrown);
retries++;
if(retries > 3) {
// Did we just lose the gateway? :-(
connected = false;
gatewayCallbacks.error("Lost connection to the gateway (is it down?)");
return;
}
eventHandler();
}
});
}
// Private event handler: this will trigger plugin callbacks, if set
function handleEvent(json, skipTimeout) {
retries = 0;
if(!websockets && sessionId !== undefined && sessionId !== null && skipTimeout !== true)
setTimeout(eventHandler, 200);
if(!websockets && Janus.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], true);
}
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"] === "trickle") {
// We got a trickle candidate from Janus
var sender = json["sender"];
if(sender === undefined || sender === null) {
Janus.warn("Missing sender...");
return;
}
var pluginHandle = pluginHandles[sender];
if(pluginHandle === undefined || pluginHandle === null) {
Janus.debug("This handle is not attached to this session");
return;
}
var candidate = json["candidate"];
Janus.debug("Got a trickled candidate on session " + sessionId);
Janus.debug(candidate);
var config = pluginHandle.webrtcStuff;
if(config.pc && config.remoteSdp) {
// Add candidate right now
Janus.debug("Adding remote candidate:", candidate);
if(!candidate || candidate.completed === true) {
// end-of-candidates
config.pc.addIceCandidate();
} else {
// New candidate
config.pc.addIceCandidate(new RTCIceCandidate(candidate));
}
} else {
// We didn't do setRemoteDescription (trickle got here before the offer?)
Janus.debug("We didn't do setRemoteDescription (trickle got here before the offer?), caching candidate");
if(!config.candidates)
config.candidates = [];
config.candidates.push(candidate);
Janus.debug(config.candidates);
}
} else if(json["janus"] === "webrtcup") {
// The PeerConnection with the 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.debug("This handle is not attached to this session");
return;
}
pluginHandle.webrtcState(true);
return;
} else if(json["janus"] === "hangup") {
// A plugin asked the core to hangup a PeerConnection on one of our handles
Janus.debug("Got a hangup event on session " + sessionId);
Janus.debug(json);
var sender = json["sender"];
if(sender === undefined || sender === null) {
Janus.warn("Missing sender...");
return;
}
var pluginHandle = pluginHandles[sender];
if(pluginHandle === undefined || pluginHandle === null) {
Janus.debug("This handle is not attached to this session");
return;
}
pluginHandle.webrtcState(false, json["reason"]);
pluginHandle.hangup();
} else if(json["janus"] === "detached") {
// A plugin asked the core to detach one of our handles
Janus.debug("Got a detached event on session " + sessionId);
Janus.debug(json);
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.detached = true;
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.debug("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.debug("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("Unknown 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": Janus.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 = Janus.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 && Janus.isArray(servers)) {
// We still need to find a working server from the list we were given
server = servers[serversIndex];
if(server.indexOf("ws") === 0) {
websockets = true;
Janus.log("Server #" + (serversIndex+1) + ": trying WebSockets to contact Janus (" + server + ")");
} else {
websockets = false;
Janus.log("Server #" + (serversIndex+1) + ": trying REST API to contact Janus (" + server + ")");
}
}
if(websockets) {
ws = Janus.newWebSocket(server, 'janus-protocol');
wsHandlers = {
'error': function() {
Janus.error("Error connecting to the Janus WebSockets server... " + server);
if (Janus.isArray(servers)) {
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.httpAPICall(server, {
verb: 'POST',
withCredentials: withCredentials,
body: request,
success: function(json) {
Janus.debug(json);
if(json["janus"] !== "success") {
Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
callbacks.error(json["error"].reason);
return;
}
connected = true;
sessionId = json.data["id"];
Janus.log("Created session: " + sessionId);
Janus.sessions[sessionId] = that;
eventHandler();
callbacks.success();
},
error: function(textStatus, errorThrown) {
Janus.error(textStatus + ":", errorThrown); // FIXME
if(Janus.isArray(servers)) {
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);
}
});
}
// 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);
var notifyDestroyed = true;
if(callbacks.notifyDestroyed !== undefined && callbacks.notifyDestroyed !== null)
notifyDestroyed = (callbacks.notifyDestroyed === 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();
if(notifyDestroyed)
gatewayCallbacks.destroyed();
return;
}
delete Janus.sessions[sessionId];
// No need to destroy all handles first, Janus will do that itself
var request = { "janus": "destroy", "transaction": Janus.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);
}
ws.close();
};
var onUnbindMessage = function(event){
var data = JSON.parse(event.data);
if(data.session_id == request.session_id && data.transaction == request.transaction) {
unbindWebSocket();
callbacks.success();
if(notifyDestroyed)
gatewayCallbacks.destroyed();
}
};
var onUnbindError = function(event) {
unbindWebSocket();
callbacks.error("Failed to destroy the gateway: Is the gateway down?");
if(notifyDestroyed)
gatewayCallbacks.destroyed();
};
ws.addEventListener('message', onUnbindMessage);
ws.addEventListener('error', onUnbindError);
ws.send(JSON.stringify(request));
return;
}
Janus.httpAPICall(server + "/" + sessionId, {
verb: 'POST',
async: asyncRequest, // Sometimes we need false here, or destroying in onbeforeunload won't work
withCredentials: withCredentials,
body: request,
success: function(json) {
Janus.log("Destroyed session:");
Janus.debug(json);
sessionId = null;
connected = false;
if(json["janus"] !== "success") {
Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
}
callbacks.success();
if(notifyDestroyed)
gatewayCallbacks.destroyed();
},
error: function(textStatus, errorThrown) {
Janus.error(textStatus + ":", errorThrown); // FIXME
// Reset everything anyway
sessionId = null;
connected = false;
callbacks.success();
if(notifyDestroyed)
gatewayCallbacks.destroyed();
}
});
}
// Private method to create a plugin handle
function createHandle(callbacks) {
callbacks = callbacks || {};
callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop;
callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop;
callbacks.consentDialog = (typeof callbacks.consentDialog == "function") ? callbacks.consentDialog : Janus.noop;
callbacks.iceState = (typeof callbacks.iceState == "function") ? callbacks.iceState : Janus.noop;
callbacks.mediaState = (typeof callbacks.mediaState == "function") ? callbacks.mediaState : Janus.noop;
callbacks.webrtcState = (typeof callbacks.webrtcState == "function") ? callbacks.webrtcState : Janus.noop;
callbacks.slowLink = (typeof callbacks.slowLink == "function") ? callbacks.slowLink : Janus.noop;
callbacks.onmessage = (typeof callbacks.onmessage == "function") ? callbacks.onmessage : Janus.noop;
callbacks.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 opaqueId = callbacks.opaqueId;
var handleToken = callbacks.token ? callbacks.token : token;
var transaction = Janus.randomString(12);
var request = { "janus": "attach", "plugin": plugin, "opaque_id": opaqueId, "transaction": transaction };
if(handleToken !== null && handleToken !== undefined)
request["token"] = handleToken;
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,
token : handleToken,
detached : false,
webrtcStuff : {
started : false,
myStream : null,
streamExternal : false,
remoteStream : null,
mySdp : null,
mediaConstraints : null,
pc : null,
dataChannel : null,
dtmfSender : null,
trickle : true,
iceDone : 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,
iceState : callbacks.iceState,
mediaState : callbacks.mediaState,
webrtcState : callbacks.webrtcState,
slowLink : callbacks.slowLink,
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.httpAPICall(server + "/" + sessionId, {
verb: 'POST',
withCredentials: withCredentials,
body: request,
success: function(json) {
Janus.debug(json);
if(json["janus"] !== "success") {
Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
callbacks.error("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,
token : handleToken,
detached : false,
webrtcStuff : {
started : false,
myStream : null,
streamExternal : false,
remoteStream : null,
mySdp : null,
mediaConstraints : null,
pc : null,
dataChannel : null,
dtmfSender : null,
trickle : true,
iceDone : 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,
iceState : callbacks.iceState,
mediaState : callbacks.mediaState,
webrtcState : callbacks.webrtcState,
slowLink : callbacks.slowLink,
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(textStatus, errorThrown) {
Janus.error(textStatus + ":", errorThrown); // FIXME
}
});
}
// 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 pluginHandle = pluginHandles[handleId];
if(pluginHandle === null || pluginHandle === undefined ||
pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
Janus.warn("Invalid handle");
callbacks.error("Invalid handle");
return;
}
var message = callbacks.message;
var jsep = callbacks.jsep;
var transaction = Janus.randomString(12);
var request = { "janus": "message", "body": message, "transaction": transaction };
if(pluginHandle.token !== null && pluginHandle.token !== undefined)
request["token"] = pluginHandle.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.httpAPICall(server + "/" + sessionId + "/" + handleId, {
verb: 'POST',
withCredentials: withCredentials,
body: 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(textStatus, errorThrown) {
Janus.error(textStatus + ":", errorThrown); // FIXME
callbacks.error(textStatus + ": " + errorThrown);
}
});
}
// Private method to send a trickle candidate
function sendTrickleCandidate(handleId, candidate) {
if(!connected) {
Janus.warn("Is the gateway down? (connected=false)");
return;
}
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 request = { "janus": "trickle", "candidate": candidate, "transaction": Janus.randomString(12) };
if(pluginHandle.token !== null && pluginHandle.token !== undefined)
request["token"] = pluginHandle.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.httpAPICall(server + "/" + sessionId + "/" + handleId, {
verb: 'POST',
withCredentials: withCredentials,
body: 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(textStatus, errorThrown) {
Janus.error(textStatus + ":", errorThrown); // FIXME
}
});
}
// 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 + " (async=" + asyncRequest + ")");
cleanupWebrtc(handleId);
var pluginHandle = pluginHandles[handleId];
if(pluginHandle === null || pluginHandle === undefined || pluginHandle.detached) {
// Plugin was already detached by Janus, calling detach again will return a handle not found error, so just exit here
delete pluginHandles[handleId];
callbacks.success();
return;
}
if(!connected) {
Janus.warn("Is the gateway down? (connected=false)");
callbacks.error("Is the gateway down? (connected=false)");
return;
}
var request = { "janus": "detach", "transaction": Janus.randomString(12) };
if(pluginHandle.token !== null && pluginHandle.token !== undefined)
request["token"] = pluginHandle.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.httpAPICall(server + "/" + sessionId + "/" + handleId, {
verb: 'POST',
async: asyncRequest, // Sometimes we need false here, or destroying in onbeforeunload won't work
withCredentials: withCredentials,
body: 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(textStatus, errorThrown) {
Janus.error(textStatus + ":", errorThrown); // FIXME
// We cleanup anyway
delete pluginHandles[handleId];
callbacks.success();
}
});
}
// 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);
if(stream) {
Janus.debug(" -- Audio tracks:", stream.getAudioTracks());
Janus.debug(" -- Video tracks:", stream.getVideoTracks());
}
// We're now capturing the new stream: check if we're updating or if it's a new thing
var addTracks = false;
if(!config.myStream || !media.update || config.streamExternal) {
config.myStream = stream;
addTracks = true;
} else {
// We only need to update the existing stream
if(((!media.update && isAudioSendEnabled(media)) || (media.update && (media.addAudio || media.replaceAudio))) &&
stream.getAudioTracks() && stream.getAudioTracks().length) {
config.myStream.addTrack(stream.getAudioTracks()[0]);
if(media.replaceAudio && Janus.webRTCAdapter.browserDetails.browser === "firefox") {
Janus.log("Adding audio track:", stream.getAudioTracks()[0]);
for(var index in config.pc.getSenders()) {
var s = config.pc.getSenders()[index];
if(s && s.track && s.track.kind === "audio") {
s.replaceTrack(stream.getAudioTracks()[0]);
}
}
} else {
Janus.log((media.replaceAudio ? "Replacing" : "Adding") + " audio track:", stream.getAudioTracks()[0]);
config.pc.addTrack(stream.getAudioTracks()[0], stream);
}
}
if(((!media.update && isVideoSendEnabled(media)) || (media.update && (media.addVideo || media.replaceVideo))) &&
stream.getVideoTracks() && stream.getVideoTracks().length) {
config.myStream.addTrack(stream.getVideoTracks()[0]);
if(media.replaceVideo && Janus.webRTCAdapter.browserDetails.browser === "firefox") {
Janus.log("Replacing video track:", stream.getVideoTracks()[0]);
for(var index in config.pc.getSenders()) {
var s = config.pc.getSenders()[index];
if(s && s.track && s.track.kind === "video") {
s.replaceTrack(stream.getVideoTracks()[0]);
}
}
} else {
Janus.log((media.replaceVideo ? "Replacing" : "Adding") + " video track:", stream.getVideoTracks()[0]);
config.pc.addTrack(stream.getVideoTracks()[0], stream);
}
}
}
// If we still need to create a PeerConnection, let's do that
if(!config.pc) {
var pc_config = {"iceServers": iceServers, "iceTransportPolicy": iceTransportPolicy, "bundlePolicy": bundlePolicy};
//~ 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});
}
// Any custom constraint to add?
if(callbacks.rtcConstraints && typeof callbacks.rtcConstraints === 'object') {
Janus.debug("Adding custom PeerConnection constraints:", callbacks.rtcConstraints);
for(var i in callbacks.rtcConstraints) {
pc_constraints.optional.push(callbacks.rtcConstraints[i]);
}
}
if(Janus.webRTCAdapter.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.oniceconnectionstatechange = function(e) {
if(config.pc)
pluginHandle.iceState(config.pc.iceConnectionState);
};
config.pc.onicecandidate = function(event) {
if (event.candidate == null ||
(Janus.webRTCAdapter.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);
}
}
};
config.pc.ontrack = function(event) {
Janus.log("Handling Remote Track");
Janus.debug(event);
if(!event.streams)
return;
config.remoteStream = event.streams[0];
pluginHandle.onremotestream(config.remoteStream);
if(event.track && !event.track.onended) {
Janus.log("Adding onended callback to track:", event.track);
event.track.onended = function(ev) {
Janus.log("Remote track removed:", ev);
if(config.remoteStream) {
config.remoteStream.removeTrack(ev.target);
pluginHandle.onremotestream(config.remoteStream);
}
}
}
};
}
if(addTracks && stream !== null && stream !== undefined) {
Janus.log('Adding local stream');
stream.getTracks().forEach(function(track) { config.pc.addTrack(track, stream); });
}
// Any data channel to create?
if(isDataEnabled(media) && !config.dataChannel) {
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;
}
// If there's a new local stream, let's notify the application
if(config.myStream)
pluginHandle.onlocalstream(config.myStream);
// Create offer/answer now
if(jsep === null || jsep === undefined) {
createOffer(handleId, media, callbacks);
} else {
config.pc.setRemoteDescription(
new RTCSessionDescription(jsep),
function() {
Janus.log("Remote description accepted!");
config.remoteSdp = jsep.sdp;
// Any trickle candidate we cached?
if(config.candidates && config.candidates.length > 0) {
for(var i in config.candidates) {
var candidate = config.candidates[i];
Janus.debug("Adding remote candidate:", candidate);
if(!candidate || candidate.completed === true) {
// end-of-candidates
config.pc.addIceCandidate();
} else {
// New candidate
config.pc.addIceCandidate(new RTCIceCandidate(candidate));
}
}
config.candidates = [];
}
// Create the answer now
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;
callbacks.media = callbacks.media || { audio: true, video: true };
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;
config.trickle = isTrickleEnabled(callbacks.trickle);
// Are we updating a session?
if(config.pc === undefined || config.pc === null) {
// Nope, new PeerConnection
media.update = false;
} else if(config.pc !== undefined && config.pc !== null) {
Janus.log("Updating existing media session");
media.update = true;
// Check if there's anything do add/remove/replace, or if we
// can go directly to preparing the new SDP offer or answer
if(callbacks.stream !== null && callbacks.stream !== undefined) {
// External stream: is this the same as the one we were using before?
if(callbacks.stream !== config.myStream) {
Janus.log("Renegotiation involves a new external stream");
}
} else {
// Check if there are changes on audio
if(media.addAudio) {
media.replaceAudio = false;
media.removeAudio = false;
media.audioSend = true;
if(config.myStream && config.myStream.getAudioTracks() && config.myStream.getAudioTracks().length) {
Janus.error("Can't add audio stream, there already is one");
callbacks.error("Can't add audio stream, there already is one");
return;
}
} else if(media.removeAudio) {
media.replaceAudio = false;
media.addAudio = false;
media.audioSend = false;
} else if(media.replaceAudio) {
media.addAudio = false;
media.removeAudio = false;
media.audioSend = true;
}
if(config.myStream === null || config.myStream === undefined) {
// No media stream: if we were asked to replace, it's actually an "add"
if(media.replaceAudio) {
media.replaceAudio = false;
media.addAudio = true;
media.audioSend = true;
}
if(isAudioSendEnabled(media))
media.addAudio = true;
} else {
if(config.myStream.getAudioTracks() === null
|| config.myStream.getAudioTracks() === undefined
|| config.myStream.getAudioTracks().length === 0) {
// No audio track: if we were asked to replace, it's actually an "add"
if(media.replaceAudio) {
media.replaceAudio = false;
media.addAudio = true;
media.audioSend = true;
}
if(isAudioSendEnabled(media))
media.addAudio = true;
}
}
// Check if there are changes on video
if(media.addVideo) {
media.replaceVideo = false;
media.removeVideo = false;
media.videoSend = true;
if(config.myStream && config.myStream.getVideoTracks() && config.myStream.getVideoTracks().length) {
Janus.error("Can't add video stream, there already is one");
callbacks.error("Can't add video stream, there already is one");
return;
}
} else if(media.removeVideo) {
media.replaceVideo = false;
media.addVideo = false;
media.videoSend = false;
} else if(media.replaceVideo) {
media.addVideo = false;
media.removeVideo = false;
media.videoSend = true;
}
if(config.myStream === null || config.myStream === undefined) {
// No media stream: if we were asked to replace, it's actually an "add"
if(media.replaceVideo) {
media.replaceVideo = false;
media.addVideo = true;
media.videoSend = true;
}
if(isVideoSendEnabled(media))
media.addVideo = true;
} else {
if(config.myStream.getVideoTracks() === null
|| config.myStream.getVideoTracks() === undefined
|| config.myStream.getVideoTracks().length === 0) {
// No video track: if we were asked to replace, it's actually an "add"
if(media.replaceVideo) {
media.replaceVideo = false;
media.addVideo = true;
media.videoSend = true;
}
if(isVideoSendEnabled(media))
media.addVideo = true;
}
}
// Data channels can only be added
if(media.addData)
media.data = true;
}
}
// If we're updating, check if we need to remove/replace one of the tracks
if(media.update && !config.streamExternal) {
if(media.removeAudio || media.replaceAudio) {
if(config.myStream && config.myStream.getAudioTracks() && config.myStream.getAudioTracks().length) {
var s = config.myStream.getAudioTracks()[0];
Janus.log("Removing audio track:", s);
config.myStream.removeTrack(s);
try {
s.stop();
} catch(e) {};
}
if(config.pc.getSenders() && config.pc.getSenders().length) {
var ra = true;
if(media.replaceAudio && Janus.webRTCAdapter.browserDetails.browser === "firefox") {
// On Firefox we can use replaceTrack
ra = false;
}
if(ra) {
for(var index in config.pc.getSenders()) {
var s = config.pc.getSenders()[index];
if(s && s.track && s.track.kind === "audio") {
Janus.log("Removing audio sender:", s);
config.pc.removeTrack(s);
}
}
}
}
}
if(media.removeVideo || media.replaceVideo) {
if(config.myStream && config.myStream.getVideoTracks() && config.myStream.getVideoTracks().length) {
var s = config.myStream.getVideoTracks()[0];
Janus.log("Removing video track:", s);
config.myStream.removeTrack(s);
try {
s.stop();
} catch(e) {};
}
if(config.pc.getSenders() && config.pc.getSenders().length) {
var rv = true;
if(media.replaceVideo && Janus.webRTCAdapter.browserDetails.browser === "firefox") {
// On Firefox we can use replaceTrack
rv = false;
}
if(rv) {
for(var index in config.pc.getSenders()) {
var s = config.pc.getSenders()[index];
if(s && s.track && s.track.kind === "video") {
Janus.log("Removing video sender:", s);
config.pc.removeTrack(s);
}
}
}
}
}
}
// 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);
// If this is an update, let's check if we need to release the previous stream
if(media.update) {
if(config.myStream && config.myStream !== callbacks.stream && !config.streamExternal) {
// We're replacing a stream we captured ourselves with an external one
try {
// Try a MediaStreamTrack.stop() for each track
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.myStream = null;
}
}
// Skip the getUserMedia part
config.streamExternal = true;
streamsDone(handleId, jsep, media, callbacks, stream);
return;
}
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) {
var simulcast = callbacks.simulcast === true ? true : false;
if(simulcast && !jsep && (media.video === undefined || media.video === false))
media.video = "hires";
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 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') {
if(!media.screenshareFrameRate) {
media.screenshareFrameRate = 3;
}
// 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, useAudio) {
Janus.log("Adding media constraint (screen capture)");
Janus.debug(constraints);
navigator.mediaDevices.getUserMedia(constraints)
.then(function(stream) {
if(useAudio){
navigator.mediaDevices.getUserMedia({ audio: true, video: false })
.then(function (audioStream) {
stream.addTrack(audioStream.getAudioTracks()[0]);
gsmCallback(null, stream);
})
} else {
gsmCallback(null, stream);
}
})
.catch(function(error) { pluginHandle.consentDialog(false); gsmCallback(error); });
};
if(Janus.webRTCAdapter.browserDetails.browser === 'chrome') {
var chromever = Janus.webRTCAdapter.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,
minFrameRate: media.screenshareFrameRate,
maxFrameRate: media.screenshareFrameRate,
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: false,
video: {
mandatory: {
chromeMediaSource: 'desktop',
maxWidth: window.screen.width,
maxHeight: window.screen.height,
minFrameRate: media.screenshareFrameRate,
maxFrameRate: media.screenshareFrameRate,
},
optional: [
{googLeakyBucket: true},
{googTemporalLayeredScreencast: true}
]
}
};
constraints.video.mandatory.chromeMediaSourceId = event.data.sourceId;
getScreenMedia(constraints, callback, isAudioSendEnabled(media));
}
} 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;
}
config.pc.setRemoteDescription(
new RTCSessionDescription(jsep),
function() {
Janus.log("Remote description accepted!");
config.remoteSdp = jsep.sdp;
// Any trickle candidate we cached?
if(config.candidates && config.candidates.length > 0) {
for(var i in config.candidates) {
var candidate = config.candidates[i];
Janus.debug("Adding remote candidate:", candidate);
if(!candidate || candidate.completed === true) {
// end-of-candidates
config.pc.addIceCandidate();
} else {
// New candidate
config.pc.addIceCandidate(new RTCIceCandidate(candidate));
}
}
config.candidates = [];
}
// Done
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;
var simulcast = callbacks.simulcast === true ? true : false;
if(!simulcast) {
Janus.log("Creating offer (iceDone=" + config.iceDone + ")");
} else {
Janus.log("Creating offer (iceDone=" + config.iceDone + ", simulcast=" + simulcast + ")");
}
// https://code.google.com/p/webrtc/issues/detail?id=3508
var mediaConstraints = {
'offerToReceiveAudio':isAudioRecvEnabled(media),
'offerToReceiveVideo':isVideoRecvEnabled(media)
}
var iceRestart = callbacks.iceRestart === true ? true : false;
if(iceRestart) {
mediaConstraints["iceRestart"] = true;
}
Janus.debug(mediaConstraints);
// Check if this is Firefox and we've been asked to do simulcasting
var sendVideo = isVideoSendEnabled(media);
if(sendVideo && simulcast && Janus.webRTCAdapter.browserDetails.browser === "firefox") {
// FIXME Based on https://gist.github.com/voluntas/088bc3cc62094730647b
Janus.log("Enabling Simulcasting for Firefox (RID)");
var sender = config.pc.getSenders()[1];
Janus.log(sender);
var parameters = sender.getParameters();
Janus.log(parameters);
sender.setParameters({encodings: [
{ rid: "high", active: true, priority: "high", maxBitrate: 1000000 },
{ rid: "medium", active: true, priority: "medium", maxBitrate: 300000 },
{ rid: "low", active: true, priority: "low", maxBitrate: 100000 }
]});
}
config.pc.createOffer(
function(offer) {
Janus.debug(offer);
Janus.log("Setting local description");
if(sendVideo && simulcast) {
// This SDP munging only works with Chrome
if(Janus.webRTCAdapter.browserDetails.browser === "chrome") {
Janus.log("Enabling Simulcasting for Chrome (SDP munging)");
offer.sdp = mungeSdpForSimulcasting(offer.sdp);
} else if(Janus.webRTCAdapter.browserDetails.browser !== "firefox") {
Janus.warn("simulcast=true, but this is not Chrome nor Firefox, ignoring");
}
}
config.mySdp = offer.sdp;
config.pc.setLocalDescription(offer);
config.mediaConstraints = mediaConstraints;
if(!config.iceDone && !config.trickle) {
// Don't do anything until we have all candidates
Janus.log("Waiting for all candidates...");
return;
}
Janus.log("Offer ready");
Janus.debug(callbacks);
// 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;
var simulcast = callbacks.simulcast === true ? true : false;
if(!simulcast) {
Janus.log("Creating answer (iceDone=" + config.iceDone + ")");
} else {
Janus.log("Creating answer (iceDone=" + config.iceDone + ", simulcast=" + simulcast + ")");
}
var mediaConstraints = null;
if(Janus.webRTCAdapter.browserDetails.browser == "firefox" || Janus.webRTCAdapter.browserDetails.browser == "edge") {
mediaConstraints = {
'offerToReceiveAudio':isAudioRecvEnabled(media),
'offerToReceiveVideo':isVideoRecvEnabled(media)
};
} else {
mediaConstraints = {
'mandatory': {
'OfferToReceiveAudio':isAudioRecvEnabled(media),
'OfferToReceiveVideo':isVideoRecvEnabled(media)
}
};
}
Janus.debug(mediaConstraints);
// Check if this is Firefox and we've been asked to do simulcasting
var sendVideo = isVideoSendEnabled(media);
if(sendVideo && simulcast && Janus.webRTCAdapter.browserDetails.browser === "firefox") {
// FIXME Based on https://gist.github.com/voluntas/088bc3cc62094730647b
Janus.log("Enabling Simulcasting for Firefox (RID)");
var sender = config.pc.getSenders()[1];
Janus.log(sender);
var parameters = sender.getParameters();
Janus.log(parameters);
sender.setParameters({encodings: [
{ rid: "high", active: true, priority: "high", maxBitrate: 1000000 },
{ rid: "medium", active: true, priority: "medium", maxBitrate: 300000 },
{ rid: "low", active: true, priority: "low", maxBitrate: 100000 }
]});
}
config.pc.createAnswer(
function(answer) {
Janus.debug(answer);
Janus.log("Setting local description");
if(sendVideo && simulcast) {
// This SDP munging only works with Chrome
if(Janus.webRTCAdapter.browserDetails.browser === "chrome") {
// FIXME Apparently trying to simulcast when answering breaks video in Chrome...
//~ Janus.log("Enabling Simulcasting for Chrome (SDP munging)");
//~ answer.sdp = mungeSdpForSimulcasting(answer.sdp);
Janus.warn("simulcast=true, but this is an answer, and video breaks in Chrome if we enable it");
} else if(Janus.webRTCAdapter.browserDetails.browser !== "firefox") {
Janus.warn("simulcast=true, but this is not Chrome nor Firefox, ignoring");
}
}
config.mySdp = answer.sdp;
config.pc.setLocalDescription(answer);
config.mediaConstraints = mediaConstraints;
if(!config.iceDone && !config.trickle) {
// Don't do anything until we have all candidates
Janus.log("Waiting for all candidates...");
return;
}
// 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.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 && Janus.webRTCAdapter.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) {
if(config.bitrate.timer === null || config.bitrate.timer === undefined) {
Janus.log("Starting bitrate timer (via getStats)");
config.bitrate.timer = setInterval(function() {
config.pc.getStats()
.then(function(stats) {
stats.forEach(function (res) {
if(!res)
return;
var inStats = false;
// Check if these are statistics on incoming media
if((res.mediaType === "video" || res.id.toLowerCase().indexOf("video") > -1) &&
res.type === "inbound-rtp" && res.id.indexOf("rtcp") < 0) {
// New stats
inStats = true;
} else if(res.type == 'ssrc' && res.bytesReceived &&
(res.googCodecName === "VP8" || res.googCodecName === "")) {
// Older Chromer versions
inStats = true;
}
// Parse stats now
if(inStats) {
config.bitrate.bsnow = res.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 timePassed = config.bitrate.tsnow - config.bitrate.tsbefore;
if(Janus.webRTCAdapter.browserDetails.browser == "safari")
timePassed = timePassed/1000; // Apparently the timestamp is in microseconds, in Safari
var bitRate = Math.round((config.bitrate.bsnow - config.bitrate.bsbefore) * 8 / timePassed);
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 {
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": Janus.randomString(12) };
if(pluginHandle.token !== null && pluginHandle.token !== undefined)
request["token"] = pluginHandle.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 {
Janus.httpAPICall(server + "/" + sessionId + "/" + handleId, {
verb: 'POST',
withCredentials: withCredentials,
body: 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 MediaStreamTrack.stop() for each track
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.candidates = null;
config.mySdp = null;
config.remoteSdp = null;
config.iceDone = false;
config.dataChannel = null;
config.dtmfSender = null;
}
pluginHandle.oncleanup();
}
// Helper method to munge an SDP to enable simulcasting (Chrome only)
function mungeSdpForSimulcasting(sdp) {
// Let's munge the SDP to add the attributes for enabling simulcasting
// (based on https://gist.github.com/ggarber/a19b4c33510028b9c657)
var lines = sdp.split("\r\n");
var video = false;
var ssrc = [ -1 ], ssrc_fid = [ -1 ];
var cname = null, msid = null, mslabel = null, label = null;
var insertAt = -1;
for(var i=0; i<lines.length; i++) {
var mline = lines[i].match(/m=(\w+) */);
if(mline) {
var medium = mline[1];
if(medium === "video") {
// New video m-line: make sure it's the first one
if(ssrc[0] < 0) {
video = true;
} else {
// We're done, let's add the new attributes here
insertAt = i;
break;
}
} else {
// New non-video m-line: do we have what we were looking for?
if(ssrc[0] > -1) {
// We're done, let's add the new attributes here
insertAt = i;
break;
}
}
continue;
}
if(!video)
continue;
var fid = lines[i].match(/a=ssrc-group:FID (\d+) (\d+)/);
if(fid) {
ssrc[0] = fid[1];
ssrc_fid[0] = fid[2];
lines.splice(i, 1); i--;
continue;
}
if(ssrc[0]) {
var match = lines[i].match('a=ssrc:' + ssrc[0] + ' cname:(.+)')
if(match) {
cname = match[1];
}
match = lines[i].match('a=ssrc:' + ssrc[0] + ' msid:(.+)')
if(match) {
msid = match[1];
}
match = lines[i].match('a=ssrc:' + ssrc[0] + ' mslabel:(.+)')
if(match) {
mslabel = match[1];
}
match = lines[i].match('a=ssrc:' + ssrc + ' label:(.+)')
if(match) {
label = match[1];
}
if(lines[i].indexOf('a=ssrc:' + ssrc_fid) === 0) {
lines.splice(i, 1); i--;
continue;
}
if(lines[i].indexOf('a=ssrc:' + ssrc[0]) === 0) {
lines.splice(i, 1); i--;
continue;
}
}
if(lines[i].length == 0) {
lines.splice(i, 1); i--;
continue;
}
}
if(ssrc[0] < 0) {
// Couldn't find a FID attribute, let's just take the first video SSRC we find
insertAt = -1;
video = false;
for(var i=0; i<lines.length; i++) {
var mline = lines[i].match(/m=(\w+) */);
if(mline) {
var medium = mline[1];
if(medium === "video") {
// New video m-line: make sure it's the first one
if(ssrc[0] < 0) {
video = true;
} else {
// We're done, let's add the new attributes here
insertAt = i;
break;
}
} else {
// New non-video m-line: do we have what we were looking for?
if(ssrc[0] > -1) {
// We're done, let's add the new attributes here
insertAt = i;
break;
}
}
continue;
}
if(!video)
continue;
if(ssrc[0] < 0) {
var value = lines[i].match(/a=ssrc:(\d+)/);
if(value) {
ssrc[0] = value[1];
lines.splice(i, 1); i--;
continue;
}
} else {
var match = lines[i].match('a=ssrc:' + ssrc[0] + ' cname:(.+)')
if(match) {
cname = match[1];
}
match = lines[i].match('a=ssrc:' + ssrc[0] + ' msid:(.+)')
if(match) {
msid = match[1];
}
match = lines[i].match('a=ssrc:' + ssrc[0] + ' mslabel:(.+)')
if(match) {
mslabel = match[1];
}
match = lines[i].match('a=ssrc:' + ssrc[0] + ' label:(.+)')
if(match) {
label = match[1];
}
if(lines[i].indexOf('a=ssrc:' + ssrc_fid[0]) === 0) {
lines.splice(i, 1); i--;
continue;
}
if(lines[i].indexOf('a=ssrc:' + ssrc[0]) === 0) {
lines.splice(i, 1); i--;
continue;
}
}
if(lines[i].length == 0) {
lines.splice(i, 1); i--;
continue;
}
}
}
if(ssrc[0] < 0) {
// Still nothing, let's just return the SDP we were asked to munge
Janus.warn("Couldn't find the video SSRC, simulcasting NOT enabled");
return sdp;
}
if(insertAt < 0) {
// Append at the end
insertAt = lines.length;
}
// Generate a couple of SSRCs (for retransmissions too)
// Note: should we check if there are conflicts, here?
ssrc[1] = Math.floor(Math.random()*0xFFFFFFFF);
ssrc[2] = Math.floor(Math.random()*0xFFFFFFFF);
ssrc_fid[1] = Math.floor(Math.random()*0xFFFFFFFF);
ssrc_fid[2] = Math.floor(Math.random()*0xFFFFFFFF);
// Add attributes to the SDP
for(var i=0; i<ssrc.length; i++) {
if(cname) {
lines.splice(insertAt, 0, 'a=ssrc:' + ssrc[i] + ' cname:' + cname);
insertAt++;
}
if(msid) {
lines.splice(insertAt, 0, 'a=ssrc:' + ssrc[i] + ' msid:' + msid);
insertAt++;
}
if(mslabel) {
lines.splice(insertAt, 0, 'a=ssrc:' + ssrc[i] + ' mslabel:' + mslabel);
insertAt++;
}
if(label) {
lines.splice(insertAt, 0, 'a=ssrc:' + ssrc[i] + ' label:' + label);
insertAt++;
}
// Add the same info for the retransmission SSRC
if(cname) {
lines.splice(insertAt, 0, 'a=ssrc:' + ssrc_fid[i] + ' cname:' + cname);
insertAt++;
}
if(msid) {
lines.splice(insertAt, 0, 'a=ssrc:' + ssrc_fid[i] + ' msid:' + msid);
insertAt++;
}
if(mslabel) {
lines.splice(insertAt, 0, 'a=ssrc:' + ssrc_fid[i] + ' mslabel:' + mslabel);
insertAt++;
}
if(label) {
lines.splice(insertAt, 0, 'a=ssrc:' + ssrc_fid[i] + ' label:' + label);
insertAt++;
}
}
lines.splice(insertAt, 0, 'a=ssrc-group:FID ' + ssrc[2] + ' ' + ssrc_fid[2]);
lines.splice(insertAt, 0, 'a=ssrc-group:FID ' + ssrc[1] + ' ' + ssrc_fid[1]);
lines.splice(insertAt, 0, 'a=ssrc-group:FID ' + ssrc[0] + ' ' + ssrc_fid[0]);
lines.splice(insertAt, 0, 'a=ssrc-group:SIM ' + ssrc[0] + ' ' + ssrc[1] + ' ' + ssrc[2]);
sdp = lines.join("\r\n");
if(!sdp.endsWith("\r\n"))
sdp += "\r\n";
return sdp;
}
// 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(Janus.webRTCAdapter.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);
}
};
/*
---------------------------------------------------------------------------------
oooo
`888
oooo d8b .ooooo. oooo ooo 888 oooo oooo
`888""8P d88' `88b `88b..8P' 888 `888 `888
888 888 888 Y888' 888 888 888
888 888 888 .o8"'88b 888 888 888
d888b `Y8bod8P' o88' 888o o888o `V88V"V8P'
www.roxlu.com
www.apollomedia.nl
www.twitter.com/roxlu
---------------------------------------------------------------------------------
JANUS RECORD PLAY
=================
Simple class that demonstrates how you can use the `recordplay` plugin
to create multiple recordings w/o interrupting the video rendering.
*/
var RECPLAY_STATE_NONE = 0;
var RECPLAY_STATE_RECORDING_REQUESTED = 1;
var RECPLAY_STATE_RECORDING_STARTED = 2;
class JanusPluginRecordPlay {
/* ---------------------------------------------------------------------- */
/*
- el_video (html element): The <video> element into which we render the video stream.
- on_success: Called when the recordplay plugin was created.
- on_recording_started: Called when the recording started.
- on_recording_stopped: Called when the recording stopped.
*/
constructor(opt) {
this.opt = opt;
this.ctx = null;
this.handle = null;
this.state = RECPLAY_STATE_NONE;
this.stream = null;
this.offer = null;
}
async init(janusContext) {
this.ctx = janusContext;
if (!this.ctx) {
throw new Error("Given JanusContext is invalid.");
}
this.ctx.janus.attach({
plugin: "janus.plugin.recordplay",
success: (pluginHandle) => { this.onSuccess(pluginHandle); },
error: (err) => { this.onError(err); },
consentDialog: (on) => { this.onConsentDialog(on); },
webrtcState: (isOk, msg) => { this.onWebRtcState(isOk, msg); },
iceState: (state) => { this.onIceState(state); },
mediaState: () => { this.onMediaState(); },
slowLink: () => { this.onSlowLink(); },
onmessage: (msg, jsep) => { this.onMessage(msg, jsep); },
onlocalstream: (stream) => { this.onLocalStream(stream); },
ondataopen: () => { this.onDataOpen(); },
ondata: () => { this.onData(); },
oncleanup: () => { this.onCleanUp(); },
detached: () => { this.onDetached(); },
});
return true;
}
/* ---------------------------------------------------------------------- */
startVideoInput(useVideo, useAudio) {
var constraints = {
video: useVideo,
audio: useAudio
};
return navigator.mediaDevices.getUserMedia(constraints)
.then((userStream) => {
this.stream = userStream;
this.opt.el_video.srcObject = this.stream;
});
}
stopVideoInput() {
this.stream.getTracks().forEach(track => track.stop())
}
async startRecording(name) {
if (RECPLAY_STATE_NONE != this.state) {
throw new Error("Cannot start recording: already started.");
}
this.offer = await this.createOffer();
this.state = RECPLAY_STATE_RECORDING_REQUESTED;
this.handle.send({
"message": {
"request": "configure",
"video-bitrate-max": 1024 * 1024,
"video-keyframe-interval": 10000,
}
});
this.handle.send({
"message": {
"request": "record",
"name": name
},
"jsep": this.offer
});
}
async stopRecording() {
if (RECPLAY_STATE_RECORDING_STARTED != this.state) {
throw new Error("Cannot stop recording because we're not recording yet.");
}
this.handle.send({
message: {
request: "stop"
}
});
this.handle.hangup();
}
/* ---------------------------------------------------------------------- */
async createOffer() {
return new Promise((resolve, reject) => {
this.handle.createOffer({
stream: this.stream,
success: (jsep) => {
resolve(jsep);
},
error: (err) => {
reject(err);
}
});
});
}
/* ---------------------------------------------------------------------- */
onSuccess(pluginHandle) {
this.handle = pluginHandle;
this.opt.on_success();
}
onMessage(msg, jsep) {
try {
if (!msg) {
return false;
}
if (false == ("result" in msg)) {
return;
}
var status = null;
var result = msg['result'];
if ('string' == typeof(result)) {
status = result;
}
else if ("status" in result) {
status = result.status;
}
if ("recording" == status) {
this.state = RECPLAY_STATE_RECORDING_STARTED;
if (jsep) {
this.handle.handleRemoteJsep({jsep: jsep});
}
this.opt.on_recording_started();
}
else if("stopped" == status) {
this.state = RECPLAY_STATE_NONE;
this.opt.on_recording_stopped();
}
}
catch (e) {
console.error(e);
}
}
onError(err) {
console.error(err);
}
/* Might add some more feature later. */
onConsentDialog(on) {}
onWebRtcState(isOk, msg) {}
onIceState(state) { }
onMediaState() { }
onSlowLink() { }
onLocalStream(stream) { }
onDataOpen() { }
onData() { }
onCleanUp() { }
onDetached() { }
};
/* ---------------------------------------------------------------------- */
class JanusContext {
constructor() {
this.is_initialized = false
}
async init() {
await this.initJanus();
await this.initSession();
}
async initJanus() {
return new Promise((resolve, reject) => {
if (true == this.is_initialized) {
throw new Error("Already initialized.");
}
Janus.init({
debug: false,
dependencies: Janus.useDefaultDependencies(),
callback: () => {
this.is_initialized = true;
resolve();
}
});
});
}
async initSession() {
return new Promise((resolve, reject) => {
this.janus = new Janus({
server: "https://" +document.location.hostname +":8089/janus",
success: resolve,
error: reject,
destroyed: function() {
console.log("Janus session destroyed. @todo cleanup.");
}
});
});
}
async attachPlugin(plugin) {
if (!plugin) {
throw new Error("Plugin is invalid.");
}
return plugin.init(this);
}
};
/* ---------------------------------------------------------------------- */
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="author" content="www.roxlu.com">
<title>Janus: multiple recordings with same plugin Handle</title>
<style>
* {
font-family: 'Nunito Sans', sans-serif;
margin: 0;
padding: 0;
}
body {
background-color: whitesmoke;
}
h1 {
margin-bottom: 20px;
border-bottom: 1px solid blue;
}
a,
a:visited {
color: blue;
}
p {
margin-bottom: 20px;
}
section {
padding: 20px;
width: 640px;
height: 100vh;
margin: auto auto;
background-color: white;
font-size: 18px;
}
#buttons,
#video-capture {
display: block;
width: 640px;
margin: auto auto;
}
#video-capture {
height: 480px;
background-color: #222;
margin-bottom: 20px;
}
#buttons {
margin-bottom: 20px;
text-align: center;
}
button {
padding: 5px;
}
code {
font-family: monospace;
font-size: 13px;
}
</style>
</head>
<body>
<section>
<h1>Multiple Recordings with Janus</h1>
<p>
This example shows how you can use Janus with the <code>recordplay</code> plugin
to record multiple videos using the same video stream. When you use Janus
to get the user media (by specifying <code>media: {}</code> when you call <code>handle.createOffer()</code>),
the webcam stream that is rendered into the video element will be removed when you call <code>hangup()</code>.
By specifying our own stream we prevent this. See the <a href="https://janus.conf.meetecho.com/docs/JS.html" title="Janus documentation">notes about media here</a>.
</p>
<div id="buttons">
<button id="video-start-capture" disabled>Start video capture</button>
<button id="video-stop-capture" disabled>Stop video capture</button>
<button id="video-start-recording" disabled>Start Recording</button>
<button id="video-stop-recording" disabled>Stop Recording</button>
</div>
<video id="video-capture" width="640" height="480" autoplay muted></video>
</section>
<link href="https://fonts.googleapis.com/css?family=Nunito+Sans" rel="stylesheet">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/6.0.3/adapter.min.js"></script>
<script type="text/javascript" src="janus.js"></script>
<script type="text/javascript" src="JanusRecorder.js"></script>
<script>
(async() => {
var jr = null;
var rec = null;
/* ------------------------------------------------- */
var bt_start_capture = document.querySelector('#video-start-capture');
var bt_stop_capture = document.querySelector('#video-stop-capture');
var bt_start_recording = document.querySelector('#video-start-recording');
var bt_stop_recording = document.querySelector('#video-stop-recording');
bt_start_capture.addEventListener('click', async(ev) => {
try {
await rec.startVideoInput(true, false);
bt_start_capture.disabled = true;
bt_start_recording.disabled = false;
bt_stop_capture.disabled = false;
}
catch (e) {
/* Catched when we couldn't get access to the camera */
console.error(e);
}
});
bt_stop_capture.addEventListener('click', (ev) => {
rec.stopVideoInput();
bt_start_capture.disabled = false;
bt_start_recording.disabled = true;
bt_stop_capture.disabled = true;
});
bt_start_recording.addEventListener('click', (ev) => {
rec.startRecording("test");
bt_start_recording.disabled = true;
bt_stop_capture.disabled = true;
});
bt_stop_recording.addEventListener('click', (ev) => {
rec.stopRecording();
bt_stop_recording.disabled = true;
});
/* ------------------------------------------------- */
jr = new JanusContext();
rec = new JanusPluginRecordPlay({
el_video: document.querySelector("#video-capture"),
on_success: async () => {
bt_start_capture.disabled = false;
},
on_recording_started: () => {
bt_stop_recording.disabled = false;
},
on_recording_stopped: () => {
bt_start_recording.disabled = false;
bt_start_capture.disabled = true;
bt_stop_capture.disabled = false;
},
});
try {
await jr.init();
await jr.attachPlugin(rec);
}
catch (e) {
console.error(e);
}
/* ------------------------------------------------- */
})();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment