summaryrefslogtreecommitdiff
path: root/js/lib/janus.js
blob: b8bc4f38f7d9254563aa2e8d2d99e3e8aec0c9e5 (plain)
  1. /*
  2.     The MIT License (MIT)
  3. Copyright (c) 2016 Meetecho
  4. Permission is hereby granted, free of charge, to any person obtaining
  5. a copy of this software and associated documentation files (the "Software"),
  6. to deal in the Software without restriction, including without limitation
  7. the rights to use, copy, modify, merge, publish, distribute, sublicense,
  8. and/or sell copies of the Software, and to permit persons to whom the
  9. Software is furnished to do so, subject to the following conditions:
  10. The above copyright notice and this permission notice shall be included
  11. in all copies or substantial portions of the Software.
  12. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
  13. OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  14. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
  15. THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
  16. OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
  17. ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
  18. OTHER DEALINGS IN THE SOFTWARE.
  19. */
  20. // List of sessions
  21. Janus.sessions = {};
  22. // Screensharing Chrome Extension ID
  23. Janus.extensionId = "hapfgfdkleiggjjpfpenajgdnfckjpaj";
  24. Janus.isExtensionEnabled = function() {
  25. if(window.navigator.userAgent.match('Chrome')) {
  26. var chromever = parseInt(window.navigator.userAgent.match(/Chrome\/(.*) /)[1], 10);
  27. var maxver = 33;
  28. if(window.navigator.userAgent.match('Linux'))
  29. maxver = 35; // "known" crash in chrome 34 and 35 on linux
  30. if(chromever >= 26 && chromever <= maxver) {
  31. // Older versions of Chrome don't support this extension-based approach, so lie
  32. return true;
  33. }
  34. return ($('#janus-extension-installed').length > 0);
  35. } else {
  36. // Firefox of others, no need for the extension (but this doesn't mean it will work)
  37. return true;
  38. }
  39. };
  40. Janus.noop = function() {};
  41. // Initialization
  42. Janus.init = function(options) {
  43. options = options || {};
  44. options.callback = (typeof options.callback == "function") ? options.callback : Janus.noop;
  45. if(Janus.initDone === true) {
  46. // Already initialized
  47. options.callback();
  48. } else {
  49. if(typeof console == "undefined" || typeof console.log == "undefined")
  50. console = { log: function() {} };
  51. // Console logging (all debugging disabled by default)
  52. Janus.trace = Janus.noop;
  53. Janus.debug = Janus.noop;
  54. Janus.vdebug = Janus.noop;
  55. Janus.log = Janus.noop;
  56. Janus.warn = Janus.noop;
  57. Janus.error = Janus.noop;
  58. if(options.debug === true || options.debug === "all") {
  59. // Enable all debugging levels
  60. Janus.trace = console.trace.bind(console);
  61. Janus.debug = console.debug.bind(console);
  62. Janus.vdebug = console.debug.bind(console);
  63. Janus.log = console.log.bind(console);
  64. Janus.warn = console.warn.bind(console);
  65. Janus.error = console.error.bind(console);
  66. } else if(Array.isArray(options.debug)) {
  67. for(var i in options.debug) {
  68. var d = options.debug[i];
  69. switch(d) {
  70. case "trace":
  71. Janus.trace = console.trace.bind(console);
  72. break;
  73. case "debug":
  74. Janus.debug = console.debug.bind(console);
  75. break;
  76. case "vdebug":
  77. Janus.vdebug = console.debug.bind(console);
  78. break;
  79. case "log":
  80. Janus.log = console.log.bind(console);
  81. break;
  82. case "warn":
  83. Janus.warn = console.warn.bind(console);
  84. break;
  85. case "error":
  86. Janus.error = console.error.bind(console);
  87. break;
  88. default:
  89. console.error("Unknown debugging option '" + d + "' (supported: 'trace', 'debug', 'vdebug', 'log', warn', 'error')");
  90. break;
  91. }
  92. }
  93. }
  94. Janus.log("Initializing library");
  95. // Helper method to enumerate devices
  96. Janus.listDevices = function(callback) {
  97. callback = (typeof callback == "function") ? callback : Janus.noop;
  98. if(navigator.mediaDevices) {
  99. navigator.mediaDevices.getUserMedia({ audio: true, video: true })
  100. .then(function(stream) {
  101. navigator.mediaDevices.enumerateDevices().then(function(devices) {
  102. Janus.debug(devices);
  103. callback(devices);
  104. // Get rid of the now useless stream
  105. try {
  106. stream.stop();
  107. } catch(e) {}
  108. try {
  109. var tracks = stream.getTracks();
  110. for(var i in tracks) {
  111. var mst = tracks[i];
  112. if(mst !== null && mst !== undefined)
  113. mst.stop();
  114. }
  115. } catch(e) {}
  116. });
  117. })
  118. .catch(function(err) {
  119. Janus.error(err);
  120. callback([]);
  121. });
  122. } else {
  123. Janus.warn("navigator.mediaDevices unavailable");
  124. callback([]);
  125. }
  126. }
  127. // Helper methods to attach/reattach a stream to a video element (previously part of adapter.js)
  128. Janus.attachMediaStream = function(element, stream) {
  129. if(adapter.browserDetails.browser === 'chrome') {
  130. var chromever = adapter.browserDetails.version;
  131. if(chromever >= 43) {
  132. element.srcObject = stream;
  133. } else if(typeof element.src !== 'undefined') {
  134. element.src = URL.createObjectURL(stream);
  135. } else {
  136. Janus.error("Error attaching stream to element");
  137. }
  138. } else if(adapter.browserDetails.browser === 'safari' || window.navigator.userAgent.match(/iPad/i) || window.navigator.userAgent.match(/iPhone/i)) {
  139. element.src = URL.createObjectURL(stream);
  140. } else {
  141. element.srcObject = stream;
  142. }
  143. };
  144. Janus.reattachMediaStream = function(to, from) {
  145. if(adapter.browserDetails.browser === 'chrome') {
  146. var chromever = adapter.browserDetails.version;
  147. if(chromever >= 43) {
  148. to.srcObject = from.srcObject;
  149. } else if(typeof to.src !== 'undefined') {
  150. to.src = from.src;
  151. }
  152. } else if(adapter.browserDetails.browser === 'safari' || window.navigator.userAgent.match(/iPad/i) || window.navigator.userAgent.match(/iPhone/i)) {
  153. to.src = from.src;
  154. } else {
  155. to.srcObject = from.srcObject;
  156. }
  157. };
  158. // Detect tab close: make sure we don't loose existing onbeforeunload handlers
  159. var oldOBF = window.onbeforeunload;
  160. window.onbeforeunload = function() {
  161. Janus.log("Closing window");
  162. for(var s in Janus.sessions) {
  163. if(Janus.sessions[s] !== null && Janus.sessions[s] !== undefined &&
  164. Janus.sessions[s].destroyOnUnload) {
  165. Janus.log("Destroying session " + s);
  166. Janus.sessions[s].destroy({asyncRequest: false});
  167. }
  168. }
  169. if(oldOBF && typeof oldOBF == "function")
  170. oldOBF();
  171. }
  172. Janus.initDone = true;
  173. options.callback();
  174. }
  175. };
  176. // Helper method to check whether WebRTC is supported by this browser
  177. Janus.isWebrtcSupported = function() {
  178. return window.RTCPeerConnection !== undefined && window.RTCPeerConnection !== null &&
  179. navigator.getUserMedia !== undefined && navigator.getUserMedia !== null;
  180. };
  181. // Helper method to create random identifiers (e.g., transaction)
  182. Janus.randomString = function(len) {
  183. var charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  184. var randomString = '';
  185. for (var i = 0; i < len; i++) {
  186. var randomPoz = Math.floor(Math.random() * charSet.length);
  187. randomString += charSet.substring(randomPoz,randomPoz+1);
  188. }
  189. return randomString;
  190. }
  191. function Janus(gatewayCallbacks) {
  192. if(Janus.initDone === undefined) {
  193. gatewayCallbacks.error("Library not initialized");
  194. return {};
  195. }
  196. if(!Janus.isWebrtcSupported()) {
  197. gatewayCallbacks.error("WebRTC not supported by this browser");
  198. return {};
  199. }
  200. Janus.log("Library initialized: " + Janus.initDone);
  201. gatewayCallbacks = gatewayCallbacks || {};
  202. gatewayCallbacks.success = (typeof gatewayCallbacks.success == "function") ? gatewayCallbacks.success : jQuery.noop;
  203. gatewayCallbacks.error = (typeof gatewayCallbacks.error == "function") ? gatewayCallbacks.error : jQuery.noop;
  204. gatewayCallbacks.destroyed = (typeof gatewayCallbacks.destroyed == "function") ? gatewayCallbacks.destroyed : jQuery.noop;
  205. if(gatewayCallbacks.server === null || gatewayCallbacks.server === undefined) {
  206. gatewayCallbacks.error("Invalid gateway url");
  207. return {};
  208. }
  209. var websockets = false;
  210. var ws = null;
  211. var wsHandlers = {};
  212. var wsKeepaliveTimeoutId = null;
  213. var servers = null, serversIndex = 0;
  214. var server = gatewayCallbacks.server;
  215. if($.isArray(server)) {
  216. Janus.log("Multiple servers provided (" + server.length + "), will use the first that works");
  217. server = null;
  218. servers = gatewayCallbacks.server;
  219. Janus.debug(servers);
  220. } else {
  221. if(server.indexOf("ws") === 0) {
  222. websockets = true;
  223. Janus.log("Using WebSockets to contact Janus: " + server);
  224. } else {
  225. websockets = false;
  226. Janus.log("Using REST API to contact Janus: " + server);
  227. }
  228. }
  229. var iceServers = gatewayCallbacks.iceServers;
  230. if(iceServers === undefined || iceServers === null)
  231. iceServers = [{urls: "stun:stun.l.google.com:19302"}];
  232. var iceTransportPolicy = gatewayCallbacks.iceTransportPolicy;
  233. // Whether IPv6 candidates should be gathered
  234. var ipv6Support = gatewayCallbacks.ipv6;
  235. if(ipv6Support === undefined || ipv6Support === null)
  236. ipv6Support = false;
  237. // Whether we should enable the withCredentials flag for XHR requests
  238. var withCredentials = false;
  239. if(gatewayCallbacks.withCredentials !== undefined && gatewayCallbacks.withCredentials !== null)
  240. withCredentials = gatewayCallbacks.withCredentials === true;
  241. // Optional max events
  242. var maxev = null;
  243. if(gatewayCallbacks.max_poll_events !== undefined && gatewayCallbacks.max_poll_events !== null)
  244. maxev = gatewayCallbacks.max_poll_events;
  245. if(maxev < 1)
  246. maxev = 1;
  247. // Token to use (only if the token based authentication mechanism is enabled)
  248. var token = null;
  249. if(gatewayCallbacks.token !== undefined && gatewayCallbacks.token !== null)
  250. token = gatewayCallbacks.token;
  251. // API secret to use (only if the shared API secret is enabled)
  252. var apisecret = null;
  253. if(gatewayCallbacks.apisecret !== undefined && gatewayCallbacks.apisecret !== null)
  254. apisecret = gatewayCallbacks.apisecret;
  255. // Whether we should destroy this session when onbeforeunload is called
  256. this.destroyOnUnload = true;
  257. if(gatewayCallbacks.destroyOnUnload !== undefined && gatewayCallbacks.destroyOnUnload !== null)
  258. this.destroyOnUnload = (gatewayCallbacks.destroyOnUnload === true);
  259. var connected = false;
  260. var sessionId = null;
  261. var pluginHandles = {};
  262. var that = this;
  263. var retries = 0;
  264. var transactions = {};
  265. createSession(gatewayCallbacks);
  266. // Public methods
  267. this.getServer = function() { return server; };
  268. this.isConnected = function() { return connected; };
  269. this.getSessionId = function() { return sessionId; };
  270. this.destroy = function(callbacks) { destroySession(callbacks); };
  271. this.attach = function(callbacks) { createHandle(callbacks); };
  272. function eventHandler() {
  273. if(sessionId == null)
  274. return;
  275. Janus.debug('Long poll...');
  276. if(!connected) {
  277. Janus.warn("Is the gateway down? (connected=false)");
  278. return;
  279. }
  280. var longpoll = server + "/" + sessionId + "?rid=" + new Date().getTime();
  281. if(maxev !== undefined && maxev !== null)
  282. longpoll = longpoll + "&maxev=" + maxev;
  283. if(token !== null && token !== undefined)
  284. longpoll = longpoll + "&token=" + token;
  285. if(apisecret !== null && apisecret !== undefined)
  286. longpoll = longpoll + "&apisecret=" + apisecret;
  287. $.ajax({
  288. type: 'GET',
  289. url: longpoll,
  290. xhrFields: {
  291. withCredentials: withCredentials
  292. },
  293. cache: false,
  294. timeout: 60000, // FIXME
  295. success: handleEvent,
  296. error: function(XMLHttpRequest, textStatus, errorThrown) {
  297. Janus.error(textStatus + ": " + errorThrown);
  298. retries++;
  299. if(retries > 3) {
  300. // Did we just lose the gateway? :-(
  301. connected = false;
  302. gatewayCallbacks.error("Lost connection to the gateway (is it down?)");
  303. return;
  304. }
  305. eventHandler();
  306. },
  307. dataType: "json"
  308. });
  309. }
  310. // Private event handler: this will trigger plugin callbacks, if set
  311. function handleEvent(json) {
  312. retries = 0;
  313. if(!websockets && sessionId !== undefined && sessionId !== null)
  314. setTimeout(eventHandler, 200);
  315. if(!websockets && $.isArray(json)) {
  316. // We got an array: it means we passed a maxev > 1, iterate on all objects
  317. for(var i=0; i<json.length; i++) {
  318. handleEvent(json[i]);
  319. }
  320. return;
  321. }
  322. if(json["janus"] === "keepalive") {
  323. // Nothing happened
  324. Janus.vdebug("Got a keepalive on session " + sessionId);
  325. return;
  326. } else if(json["janus"] === "ack") {
  327. // Just an ack, we can probably ignore
  328. Janus.debug("Got an ack on session " + sessionId);
  329. Janus.debug(json);
  330. var transaction = json["transaction"];
  331. if(transaction !== null && transaction !== undefined) {
  332. var reportSuccess = transactions[transaction];
  333. if(reportSuccess !== null && reportSuccess !== undefined) {
  334. reportSuccess(json);
  335. }
  336. delete transactions[transaction];
  337. }
  338. return;
  339. } else if(json["janus"] === "success") {
  340. // Success!
  341. Janus.debug("Got a success on session " + sessionId);
  342. Janus.debug(json);
  343. var transaction = json["transaction"];
  344. if(transaction !== null && transaction !== undefined) {
  345. var reportSuccess = transactions[transaction];
  346. if(reportSuccess !== null && reportSuccess !== undefined) {
  347. reportSuccess(json);
  348. }
  349. delete transactions[transaction];
  350. }
  351. return;
  352. } else if(json["janus"] === "webrtcup") {
  353. // The PeerConnection with the gateway is up! Notify this
  354. Janus.debug("Got a webrtcup event on session " + sessionId);
  355. Janus.debug(json);
  356. var sender = json["sender"];
  357. if(sender === undefined || sender === null) {
  358. Janus.warn("Missing sender...");
  359. return;
  360. }
  361. var pluginHandle = pluginHandles[sender];
  362. if(pluginHandle === undefined || pluginHandle === null) {
  363. Janus.debug("This handle is not attached to this session");
  364. return;
  365. }
  366. pluginHandle.webrtcState(true);
  367. return;
  368. } else if(json["janus"] === "hangup") {
  369. // A plugin asked the core to hangup a PeerConnection on one of our handles
  370. Janus.debug("Got a hangup event on session " + sessionId);
  371. Janus.debug(json);
  372. var sender = json["sender"];
  373. if(sender === undefined || sender === null) {
  374. Janus.warn("Missing sender...");
  375. return;
  376. }
  377. var pluginHandle = pluginHandles[sender];
  378. if(pluginHandle === undefined || pluginHandle === null) {
  379. Janus.debug("This handle is not attached to this session");
  380. return;
  381. }
  382. pluginHandle.webrtcState(false, json["reason"]);
  383. pluginHandle.hangup();
  384. } else if(json["janus"] === "detached") {
  385. // A plugin asked the core to detach one of our handles
  386. Janus.debug("Got a detached event on session " + sessionId);
  387. Janus.debug(json);
  388. var sender = json["sender"];
  389. if(sender === undefined || sender === null) {
  390. Janus.warn("Missing sender...");
  391. return;
  392. }
  393. var pluginHandle = pluginHandles[sender];
  394. if(pluginHandle === undefined || pluginHandle === null) {
  395. // Don't warn here because destroyHandle causes this situation.
  396. return;
  397. }
  398. pluginHandle.detached = true;
  399. pluginHandle.ondetached();
  400. pluginHandle.detach();
  401. } else if(json["janus"] === "media") {
  402. // Media started/stopped flowing
  403. Janus.debug("Got a media event on session " + sessionId);
  404. Janus.debug(json);
  405. var sender = json["sender"];
  406. if(sender === undefined || sender === null) {
  407. Janus.warn("Missing sender...");
  408. return;
  409. }
  410. var pluginHandle = pluginHandles[sender];
  411. if(pluginHandle === undefined || pluginHandle === null) {
  412. Janus.debug("This handle is not attached to this session");
  413. return;
  414. }
  415. pluginHandle.mediaState(json["type"], json["receiving"]);
  416. } else if(json["janus"] === "slowlink") {
  417. Janus.debug("Got a slowlink event on session " + sessionId);
  418. Janus.debug(json);
  419. // Trouble uplink or downlink
  420. var sender = json["sender"];
  421. if(sender === undefined || sender === null) {
  422. Janus.warn("Missing sender...");
  423. return;
  424. }
  425. var pluginHandle = pluginHandles[sender];
  426. if(pluginHandle === undefined || pluginHandle === null) {
  427. Janus.debug("This handle is not attached to this session");
  428. return;
  429. }
  430. pluginHandle.slowLink(json["uplink"], json["nacks"]);
  431. } else if(json["janus"] === "error") {
  432. // Oops, something wrong happened
  433. Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
  434. Janus.debug(json);
  435. var transaction = json["transaction"];
  436. if(transaction !== null && transaction !== undefined) {
  437. var reportSuccess = transactions[transaction];
  438. if(reportSuccess !== null && reportSuccess !== undefined) {
  439. reportSuccess(json);
  440. }
  441. delete transactions[transaction];
  442. }
  443. return;
  444. } else if(json["janus"] === "event") {
  445. Janus.debug("Got a plugin event on session " + sessionId);
  446. Janus.debug(json);
  447. var sender = json["sender"];
  448. if(sender === undefined || sender === null) {
  449. Janus.warn("Missing sender...");
  450. return;
  451. }
  452. var plugindata = json["plugindata"];
  453. if(plugindata === undefined || plugindata === null) {
  454. Janus.warn("Missing plugindata...");
  455. return;
  456. }
  457. Janus.debug(" -- Event is coming from " + sender + " (" + plugindata["plugin"] + ")");
  458. var data = plugindata["data"];
  459. Janus.debug(data);
  460. var pluginHandle = pluginHandles[sender];
  461. if(pluginHandle === undefined || pluginHandle === null) {
  462. Janus.warn("This handle is not attached to this session");
  463. return;
  464. }
  465. var jsep = json["jsep"];
  466. if(jsep !== undefined && jsep !== null) {
  467. Janus.debug("Handling SDP as well...");
  468. Janus.debug(jsep);
  469. }
  470. var callback = pluginHandle.onmessage;
  471. if(callback !== null && callback !== undefined) {
  472. Janus.debug("Notifying application...");
  473. // Send to callback specified when attaching plugin handle
  474. callback(data, jsep);
  475. } else {
  476. // Send to generic callback (?)
  477. Janus.debug("No provided notification callback");
  478. }
  479. } else {
  480. Janus.warn("Unkown message/event '" + json["janus"] + "' on session " + sessionId);
  481. Janus.debug(json);
  482. }
  483. }
  484. // Private helper to send keep-alive messages on WebSockets
  485. function keepAlive() {
  486. if(server === null || !websockets || !connected)
  487. return;
  488. wsKeepaliveTimeoutId = setTimeout(keepAlive, 30000);
  489. var request = { "janus": "keepalive", "session_id": sessionId, "transaction": Janus.randomString(12) };
  490. if(token !== null && token !== undefined)
  491. request["token"] = token;
  492. if(apisecret !== null && apisecret !== undefined)
  493. request["apisecret"] = apisecret;
  494. ws.send(JSON.stringify(request));
  495. }
  496. // Private method to create a session
  497. function createSession(callbacks) {
  498. var transaction = Janus.randomString(12);
  499. var request = { "janus": "create", "transaction": transaction };
  500. if(token !== null && token !== undefined)
  501. request["token"] = token;
  502. if(apisecret !== null && apisecret !== undefined)
  503. request["apisecret"] = apisecret;
  504. if(server === null && $.isArray(servers)) {
  505. // We still need to find a working server from the list we were given
  506. server = servers[serversIndex];
  507. if(server.indexOf("ws") === 0) {
  508. websockets = true;
  509. Janus.log("Server #" + (serversIndex+1) + ": trying WebSockets to contact Janus (" + server + ")");
  510. } else {
  511. websockets = false;
  512. Janus.log("Server #" + (serversIndex+1) + ": trying REST API to contact Janus (" + server + ")");
  513. }
  514. }
  515. if(websockets) {
  516. ws = new WebSocket(server, 'janus-protocol');
  517. wsHandlers = {
  518. 'error': function() {
  519. Janus.error("Error connecting to the Janus WebSockets server... " + server);
  520. if ($.isArray(servers)) {
  521. serversIndex++;
  522. if (serversIndex == servers.length) {
  523. // We tried all the servers the user gave us and they all failed
  524. callbacks.error("Error connecting to any of the provided Janus servers: Is the gateway down?");
  525. return;
  526. }
  527. // Let's try the next server
  528. server = null;
  529. setTimeout(function() {
  530. createSession(callbacks);
  531. }, 200);
  532. return;
  533. }
  534. callbacks.error("Error connecting to the Janus WebSockets server: Is the gateway down?");
  535. },
  536. 'open': function() {
  537. // We need to be notified about the success
  538. transactions[transaction] = function(json) {
  539. Janus.debug(json);
  540. if (json["janus"] !== "success") {
  541. Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
  542. callbacks.error(json["error"].reason);
  543. return;
  544. }
  545. wsKeepaliveTimeoutId = setTimeout(keepAlive, 30000);
  546. connected = true;
  547. sessionId = json.data["id"];
  548. Janus.log("Created session: " + sessionId);
  549. Janus.sessions[sessionId] = that;
  550. callbacks.success();
  551. };
  552. ws.send(JSON.stringify(request));
  553. },
  554. 'message': function(event) {
  555. handleEvent(JSON.parse(event.data));
  556. },
  557. 'close': function() {
  558. if (server === null || !connected) {
  559. return;
  560. }
  561. connected = false;
  562. // FIXME What if this is called when the page is closed?
  563. gatewayCallbacks.error("Lost connection to the gateway (is it down?)");
  564. }
  565. };
  566. for(var eventName in wsHandlers) {
  567. ws.addEventListener(eventName, wsHandlers[eventName]);
  568. }
  569. return;
  570. }
  571. $.ajax({
  572. type: 'POST',
  573. url: server,
  574. xhrFields: {
  575. withCredentials: withCredentials
  576. },
  577. cache: false,
  578. contentType: "application/json",
  579. data: JSON.stringify(request),
  580. success: function(json) {
  581. Janus.debug(json);
  582. if(json["janus"] !== "success") {
  583. Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
  584. callbacks.error(json["error"].reason);
  585. return;
  586. }
  587. connected = true;
  588. sessionId = json.data["id"];
  589. Janus.log("Created session: " + sessionId);
  590. Janus.sessions[sessionId] = that;
  591. eventHandler();
  592. callbacks.success();
  593. },
  594. error: function(XMLHttpRequest, textStatus, errorThrown) {
  595. Janus.error(textStatus + ": " + errorThrown); // FIXME
  596. if($.isArray(servers)) {
  597. serversIndex++;
  598. if(serversIndex == servers.length) {
  599. // We tried all the servers the user gave us and they all failed
  600. callbacks.error("Error connecting to any of the provided Janus servers: Is the gateway down?");
  601. return;
  602. }
  603. // Let's try the next server
  604. server = null;
  605. setTimeout(function() { createSession(callbacks); }, 200);
  606. return;
  607. }
  608. if(errorThrown === "")
  609. callbacks.error(textStatus + ": Is the gateway down?");
  610. else
  611. callbacks.error(textStatus + ": " + errorThrown);
  612. },
  613. dataType: "json"
  614. });
  615. }
  616. // Private method to destroy a session
  617. function destroySession(callbacks) {
  618. callbacks = callbacks || {};
  619. // FIXME This method triggers a success even when we fail
  620. callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : jQuery.noop;
  621. var asyncRequest = true;
  622. if(callbacks.asyncRequest !== undefined && callbacks.asyncRequest !== null)
  623. asyncRequest = (callbacks.asyncRequest === true);
  624. Janus.log("Destroying session " + sessionId + " (async=" + asyncRequest + ")");
  625. if(!connected) {
  626. Janus.warn("Is the gateway down? (connected=false)");
  627. callbacks.success();
  628. return;
  629. }
  630. if(sessionId === undefined || sessionId === null) {
  631. Janus.warn("No session to destroy");
  632. callbacks.success();
  633. gatewayCallbacks.destroyed();
  634. return;
  635. }
  636. delete Janus.sessions[sessionId];
  637. // No need to destroy all handles first, Janus will do that itself
  638. var request = { "janus": "destroy", "transaction": Janus.randomString(12) };
  639. if(token !== null && token !== undefined)
  640. request["token"] = token;
  641. if(apisecret !== null && apisecret !== undefined)
  642. request["apisecret"] = apisecret;
  643. if(websockets) {
  644. request["session_id"] = sessionId;
  645. var unbindWebSocket = function() {
  646. for(var eventName in wsHandlers) {
  647. ws.removeEventListener(eventName, wsHandlers[eventName]);
  648. }
  649. ws.removeEventListener('message', onUnbindMessage);
  650. ws.removeEventListener('error', onUnbindError);
  651. if(wsKeepaliveTimeoutId) {
  652. clearTimeout(wsKeepaliveTimeoutId);
  653. }
  654. };
  655. var onUnbindMessage = function(event){
  656. var data = JSON.parse(event.data);
  657. if(data.session_id == request.session_id && data.transaction == request.transaction) {
  658. unbindWebSocket();
  659. callbacks.success();
  660. gatewayCallbacks.destroyed();
  661. }
  662. };
  663. var onUnbindError = function(event) {
  664. unbindWebSocket();
  665. callbacks.error("Failed to destroy the gateway: Is the gateway down?");
  666. gatewayCallbacks.destroyed();
  667. };
  668. ws.addEventListener('message', onUnbindMessage);
  669. ws.addEventListener('error', onUnbindError);
  670. ws.send(JSON.stringify(request));
  671. return;
  672. }
  673. $.ajax({
  674. type: 'POST',
  675. url: server + "/" + sessionId,
  676. async: asyncRequest, // Sometimes we need false here, or destroying in onbeforeunload won't work
  677. xhrFields: {
  678. withCredentials: withCredentials
  679. },
  680. cache: false,
  681. contentType: "application/json",
  682. data: JSON.stringify(request),
  683. success: function(json) {
  684. Janus.log("Destroyed session:");
  685. Janus.debug(json);
  686. sessionId = null;
  687. connected = false;
  688. if(json["janus"] !== "success") {
  689. Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
  690. }
  691. callbacks.success();
  692. gatewayCallbacks.destroyed();
  693. },
  694. error: function(XMLHttpRequest, textStatus, errorThrown) {
  695. Janus.error(textStatus + ": " + errorThrown); // FIXME
  696. // Reset everything anyway
  697. sessionId = null;
  698. connected = false;
  699. callbacks.success();
  700. gatewayCallbacks.destroyed();
  701. },
  702. dataType: "json"
  703. });
  704. }
  705. // Private method to create a plugin handle
  706. function createHandle(callbacks) {
  707. callbacks = callbacks || {};
  708. callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : jQuery.noop;
  709. callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : jQuery.noop;
  710. callbacks.consentDialog = (typeof callbacks.consentDialog == "function") ? callbacks.consentDialog : jQuery.noop;
  711. callbacks.iceState = (typeof callbacks.iceState == "function") ? callbacks.iceState : jQuery.noop;
  712. callbacks.mediaState = (typeof callbacks.mediaState == "function") ? callbacks.mediaState : jQuery.noop;
  713. callbacks.webrtcState = (typeof callbacks.webrtcState == "function") ? callbacks.webrtcState : jQuery.noop;
  714. callbacks.slowLink = (typeof callbacks.slowLink == "function") ? callbacks.slowLink : jQuery.noop;
  715. callbacks.onmessage = (typeof callbacks.onmessage == "function") ? callbacks.onmessage : jQuery.noop;
  716. callbacks.onlocalstream = (typeof callbacks.onlocalstream == "function") ? callbacks.onlocalstream : jQuery.noop;
  717. callbacks.onremotestream = (typeof callbacks.onremotestream == "function") ? callbacks.onremotestream : jQuery.noop;
  718. callbacks.ondata = (typeof callbacks.ondata == "function") ? callbacks.ondata : jQuery.noop;
  719. callbacks.ondataopen = (typeof callbacks.ondataopen == "function") ? callbacks.ondataopen : jQuery.noop;
  720. callbacks.oncleanup = (typeof callbacks.oncleanup == "function") ? callbacks.oncleanup : jQuery.noop;
  721. callbacks.ondetached = (typeof callbacks.ondetached == "function") ? callbacks.ondetached : jQuery.noop;
  722. if(!connected) {
  723. Janus.warn("Is the gateway down? (connected=false)");
  724. callbacks.error("Is the gateway down? (connected=false)");
  725. return;
  726. }
  727. var plugin = callbacks.plugin;
  728. if(plugin === undefined || plugin === null) {
  729. Janus.error("Invalid plugin");
  730. callbacks.error("Invalid plugin");
  731. return;
  732. }
  733. var opaqueId = callbacks.opaqueId;
  734. var transaction = Janus.randomString(12);
  735. var request = { "janus": "attach", "plugin": plugin, "opaque_id": opaqueId, "transaction": transaction };
  736. if(token !== null && token !== undefined)
  737. request["token"] = token;
  738. if(apisecret !== null && apisecret !== undefined)
  739. request["apisecret"] = apisecret;
  740. if(websockets) {
  741. transactions[transaction] = function(json) {
  742. Janus.debug(json);
  743. if(json["janus"] !== "success") {
  744. Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
  745. callbacks.error("Ooops: " + json["error"].code + " " + json["error"].reason);
  746. return;
  747. }
  748. var handleId = json.data["id"];
  749. Janus.log("Created handle: " + handleId);
  750. var pluginHandle =
  751. {
  752. session : that,
  753. plugin : plugin,
  754. id : handleId,
  755. detached : false,
  756. webrtcStuff : {
  757. started : false,
  758. myStream : null,
  759. streamExternal : false,
  760. remoteStream : null,
  761. mySdp : null,
  762. pc : null,
  763. dataChannel : null,
  764. dtmfSender : null,
  765. trickle : true,
  766. iceDone : false,
  767. sdpSent : false,
  768. volume : {
  769. value : null,
  770. timer : null
  771. },
  772. bitrate : {
  773. value : null,
  774. bsnow : null,
  775. bsbefore : null,
  776. tsnow : null,
  777. tsbefore : null,
  778. timer : null
  779. }
  780. },
  781. getId : function() { return handleId; },
  782. getPlugin : function() { return plugin; },
  783. getVolume : function() { return getVolume(handleId); },
  784. isAudioMuted : function() { return isMuted(handleId, false); },
  785. muteAudio : function() { return mute(handleId, false, true); },
  786. unmuteAudio : function() { return mute(handleId, false, false); },
  787. isVideoMuted : function() { return isMuted(handleId, true); },
  788. muteVideo : function() { return mute(handleId, true, true); },
  789. unmuteVideo : function() { return mute(handleId, true, false); },
  790. getBitrate : function() { return getBitrate(handleId); },
  791. send : function(callbacks) { sendMessage(handleId, callbacks); },
  792. data : function(callbacks) { sendData(handleId, callbacks); },
  793. dtmf : function(callbacks) { sendDtmf(handleId, callbacks); },
  794. consentDialog : callbacks.consentDialog,
  795. iceState : callbacks.iceState,
  796. mediaState : callbacks.mediaState,
  797. webrtcState : callbacks.webrtcState,
  798. slowLink : callbacks.slowLink,
  799. onmessage : callbacks.onmessage,
  800. createOffer : function(callbacks) { prepareWebrtc(handleId, callbacks); },
  801. createAnswer : function(callbacks) { prepareWebrtc(handleId, callbacks); },
  802. handleRemoteJsep : function(callbacks) { prepareWebrtcPeer(handleId, callbacks); },
  803. onlocalstream : callbacks.onlocalstream,
  804. onremotestream : callbacks.onremotestream,
  805. ondata : callbacks.ondata,
  806. ondataopen : callbacks.ondataopen,
  807. oncleanup : callbacks.oncleanup,
  808. ondetached : callbacks.ondetached,
  809. hangup : function(sendRequest) { cleanupWebrtc(handleId, sendRequest === true); },
  810. detach : function(callbacks) { destroyHandle(handleId, callbacks); }
  811. }
  812. pluginHandles[handleId] = pluginHandle;
  813. callbacks.success(pluginHandle);
  814. };
  815. request["session_id"] = sessionId;
  816. ws.send(JSON.stringify(request));
  817. return;
  818. }
  819. $.ajax({
  820. type: 'POST',
  821. url: server + "/" + sessionId,
  822. xhrFields: {
  823. withCredentials: withCredentials
  824. },
  825. cache: false,
  826. contentType: "application/json",
  827. data: JSON.stringify(request),
  828. success: function(json) {
  829. Janus.debug(json);
  830. if(json["janus"] !== "success") {
  831. Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
  832. callbacks.error("Ooops: " + json["error"].code + " " + json["error"].reason);
  833. return;
  834. }
  835. var handleId = json.data["id"];
  836. Janus.log("Created handle: " + handleId);
  837. var pluginHandle =
  838. {
  839. session : that,
  840. plugin : plugin,
  841. id : handleId,
  842. detached : false,
  843. webrtcStuff : {
  844. started : false,
  845. myStream : null,
  846. streamExternal : false,
  847. remoteStream : null,
  848. mySdp : null,
  849. pc : null,
  850. dataChannel : null,
  851. dtmfSender : null,
  852. trickle : true,
  853. iceDone : false,
  854. sdpSent : false,
  855. volume : {
  856. value : null,
  857. timer : null
  858. },
  859. bitrate : {
  860. value : null,
  861. bsnow : null,
  862. bsbefore : null,
  863. tsnow : null,
  864. tsbefore : null,
  865. timer : null
  866. }
  867. },
  868. getId : function() { return handleId; },
  869. getPlugin : function() { return plugin; },
  870. getVolume : function() { return getVolume(handleId); },
  871. isAudioMuted : function() { return isMuted(handleId, false); },
  872. muteAudio : function() { return mute(handleId, false, true); },
  873. unmuteAudio : function() { return mute(handleId, false, false); },
  874. isVideoMuted : function() { return isMuted(handleId, true); },
  875. muteVideo : function() { return mute(handleId, true, true); },
  876. unmuteVideo : function() { return mute(handleId, true, false); },
  877. getBitrate : function() { return getBitrate(handleId); },
  878. send : function(callbacks) { sendMessage(handleId, callbacks); },
  879. data : function(callbacks) { sendData(handleId, callbacks); },
  880. dtmf : function(callbacks) { sendDtmf(handleId, callbacks); },
  881. consentDialog : callbacks.consentDialog,
  882. iceState : callbacks.iceState,
  883. mediaState : callbacks.mediaState,
  884. webrtcState : callbacks.webrtcState,
  885. slowLink : callbacks.slowLink,
  886. onmessage : callbacks.onmessage,
  887. createOffer : function(callbacks) { prepareWebrtc(handleId, callbacks); },
  888. createAnswer : function(callbacks) { prepareWebrtc(handleId, callbacks); },
  889. handleRemoteJsep : function(callbacks) { prepareWebrtcPeer(handleId, callbacks); },
  890. onlocalstream : callbacks.onlocalstream,
  891. onremotestream : callbacks.onremotestream,
  892. ondata : callbacks.ondata,
  893. ondataopen : callbacks.ondataopen,
  894. oncleanup : callbacks.oncleanup,
  895. ondetached : callbacks.ondetached,
  896. hangup : function(sendRequest) { cleanupWebrtc(handleId, sendRequest === true); },
  897. detach : function(callbacks) { destroyHandle(handleId, callbacks); }
  898. }
  899. pluginHandles[handleId] = pluginHandle;
  900. callbacks.success(pluginHandle);
  901. },
  902. error: function(XMLHttpRequest, textStatus, errorThrown) {
  903. Janus.error(textStatus + ": " + errorThrown); // FIXME
  904. },
  905. dataType: "json"
  906. });
  907. }
  908. // Private method to send a message
  909. function sendMessage(handleId, callbacks) {
  910. callbacks = callbacks || {};
  911. callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : jQuery.noop;
  912. callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : jQuery.noop;
  913. if(!connected) {
  914. Janus.warn("Is the gateway down? (connected=false)");
  915. callbacks.error("Is the gateway down? (connected=false)");
  916. return;
  917. }
  918. var message = callbacks.message;
  919. var jsep = callbacks.jsep;
  920. var transaction = Janus.randomString(12);
  921. var request = { "janus": "message", "body": message, "transaction": transaction };
  922. if(token !== null && token !== undefined)
  923. request["token"] = token;
  924. if(apisecret !== null && apisecret !== undefined)
  925. request["apisecret"] = apisecret;
  926. if(jsep !== null && jsep !== undefined)
  927. request.jsep = jsep;
  928. Janus.debug("Sending message to plugin (handle=" + handleId + "):");
  929. Janus.debug(request);
  930. if(websockets) {
  931. request["session_id"] = sessionId;
  932. request["handle_id"] = handleId;
  933. transactions[transaction] = function(json) {
  934. Janus.debug("Message sent!");
  935. Janus.debug(json);
  936. if(json["janus"] === "success") {
  937. // We got a success, must have been a synchronous transaction
  938. var plugindata = json["plugindata"];
  939. if(plugindata === undefined || plugindata === null) {
  940. Janus.warn("Request succeeded, but missing plugindata...");
  941. callbacks.success();
  942. return;
  943. }
  944. Janus.log("Synchronous transaction successful (" + plugindata["plugin"] + ")");
  945. var data = plugindata["data"];
  946. Janus.debug(data);
  947. callbacks.success(data);
  948. return;
  949. } else if(json["janus"] !== "ack") {
  950. // Not a success and not an ack, must be an error
  951. if(json["error"] !== undefined && json["error"] !== null) {
  952. Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
  953. callbacks.error(json["error"].code + " " + json["error"].reason);
  954. } else {
  955. Janus.error("Unknown error"); // FIXME
  956. callbacks.error("Unknown error");
  957. }
  958. return;
  959. }
  960. // If we got here, the plugin decided to handle the request asynchronously
  961. callbacks.success();
  962. };
  963. ws.send(JSON.stringify(request));
  964. return;
  965. }
  966. $.ajax({
  967. type: 'POST',
  968. url: server + "/" + sessionId + "/" + handleId,
  969. xhrFields: {
  970. withCredentials: withCredentials
  971. },
  972. cache: false,
  973. contentType: "application/json",
  974. data: JSON.stringify(request),
  975. success: function(json) {
  976. Janus.debug("Message sent!");
  977. Janus.debug(json);
  978. if(json["janus"] === "success") {
  979. // We got a success, must have been a synchronous transaction
  980. var plugindata = json["plugindata"];
  981. if(plugindata === undefined || plugindata === null) {
  982. Janus.warn("Request succeeded, but missing plugindata...");
  983. callbacks.success();
  984. return;
  985. }
  986. Janus.log("Synchronous transaction successful (" + plugindata["plugin"] + ")");
  987. var data = plugindata["data"];
  988. Janus.debug(data);
  989. callbacks.success(data);
  990. return;
  991. } else if(json["janus"] !== "ack") {
  992. // Not a success and not an ack, must be an error
  993. if(json["error"] !== undefined && json["error"] !== null) {
  994. Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
  995. callbacks.error(json["error"].code + " " + json["error"].reason);
  996. } else {
  997. Janus.error("Unknown error"); // FIXME
  998. callbacks.error("Unknown error");
  999. }
  1000. return;
  1001. }
  1002. // If we got here, the plugin decided to handle the request asynchronously
  1003. callbacks.success();
  1004. },
  1005. error: function(XMLHttpRequest, textStatus, errorThrown) {
  1006. Janus.error(textStatus + ": " + errorThrown); // FIXME
  1007. callbacks.error(textStatus + ": " + errorThrown);
  1008. },
  1009. dataType: "json"
  1010. });
  1011. }
  1012. // Private method to send a trickle candidate
  1013. function sendTrickleCandidate(handleId, candidate) {
  1014. if(!connected) {
  1015. Janus.warn("Is the gateway down? (connected=false)");
  1016. return;
  1017. }
  1018. var request = { "janus": "trickle", "candidate": candidate, "transaction": Janus.randomString(12) };
  1019. if(token !== null && token !== undefined)
  1020. request["token"] = token;
  1021. if(apisecret !== null && apisecret !== undefined)
  1022. request["apisecret"] = apisecret;
  1023. Janus.vdebug("Sending trickle candidate (handle=" + handleId + "):");
  1024. Janus.vdebug(request);
  1025. if(websockets) {
  1026. request["session_id"] = sessionId;
  1027. request["handle_id"] = handleId;
  1028. ws.send(JSON.stringify(request));
  1029. return;
  1030. }
  1031. $.ajax({
  1032. type: 'POST',
  1033. url: server + "/" + sessionId + "/" + handleId,
  1034. xhrFields: {
  1035. withCredentials: withCredentials
  1036. },
  1037. cache: false,
  1038. contentType: "application/json",
  1039. data: JSON.stringify(request),
  1040. success: function(json) {
  1041. Janus.vdebug("Candidate sent!");
  1042. Janus.vdebug(json);
  1043. if(json["janus"] !== "ack") {
  1044. Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
  1045. return;
  1046. }
  1047. },
  1048. error: function(XMLHttpRequest, textStatus, errorThrown) {
  1049. Janus.error(textStatus + ": " + errorThrown); // FIXME
  1050. },
  1051. dataType: "json"
  1052. });
  1053. }
  1054. // Private method to send a data channel message
  1055. function sendData(handleId, callbacks) {
  1056. callbacks = callbacks || {};
  1057. callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : jQuery.noop;
  1058. callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : jQuery.noop;
  1059. var pluginHandle = pluginHandles[handleId];
  1060. if(pluginHandle === null || pluginHandle === undefined ||
  1061. pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
  1062. Janus.warn("Invalid handle");
  1063. callbacks.error("Invalid handle");
  1064. return;
  1065. }
  1066. var config = pluginHandle.webrtcStuff;
  1067. var text = callbacks.text;
  1068. if(text === null || text === undefined) {
  1069. Janus.warn("Invalid text");
  1070. callbacks.error("Invalid text");
  1071. return;
  1072. }
  1073. Janus.log("Sending string on data channel: " + text);
  1074. config.dataChannel.send(text);
  1075. callbacks.success();
  1076. }
  1077. // Private method to send a DTMF tone
  1078. function sendDtmf(handleId, callbacks) {
  1079. callbacks = callbacks || {};
  1080. callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : jQuery.noop;
  1081. callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : jQuery.noop;
  1082. var pluginHandle = pluginHandles[handleId];
  1083. if(pluginHandle === null || pluginHandle === undefined ||
  1084. pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
  1085. Janus.warn("Invalid handle");
  1086. callbacks.error("Invalid handle");
  1087. return;
  1088. }
  1089. var config = pluginHandle.webrtcStuff;
  1090. if(config.dtmfSender === null || config.dtmfSender === undefined) {
  1091. // Create the DTMF sender, if possible
  1092. if(config.myStream !== undefined && config.myStream !== null) {
  1093. var tracks = config.myStream.getAudioTracks();
  1094. if(tracks !== null && tracks !== undefined && tracks.length > 0) {
  1095. var local_audio_track = tracks[0];
  1096. config.dtmfSender = config.pc.createDTMFSender(local_audio_track);
  1097. Janus.log("Created DTMF Sender");
  1098. config.dtmfSender.ontonechange = function(tone) { Janus.debug("Sent DTMF tone: " + tone.tone); };
  1099. }
  1100. }
  1101. if(config.dtmfSender === null || config.dtmfSender === undefined) {
  1102. Janus.warn("Invalid DTMF configuration");
  1103. callbacks.error("Invalid DTMF configuration");
  1104. return;
  1105. }
  1106. }
  1107. var dtmf = callbacks.dtmf;
  1108. if(dtmf === null || dtmf === undefined) {
  1109. Janus.warn("Invalid DTMF parameters");
  1110. callbacks.error("Invalid DTMF parameters");
  1111. return;
  1112. }
  1113. var tones = dtmf.tones;
  1114. if(tones === null || tones === undefined) {
  1115. Janus.warn("Invalid DTMF string");
  1116. callbacks.error("Invalid DTMF string");
  1117. return;
  1118. }
  1119. var duration = dtmf.duration;
  1120. if(duration === null || duration === undefined)
  1121. duration = 500; // We choose 500ms as the default duration for a tone
  1122. var gap = dtmf.gap;
  1123. if(gap === null || gap === undefined)
  1124. gap = 50; // We choose 50ms as the default gap between tones
  1125. Janus.debug("Sending DTMF string " + tones + " (duration " + duration + "ms, gap " + gap + "ms)");
  1126. config.dtmfSender.insertDTMF(tones, duration, gap);
  1127. }
  1128. // Private method to destroy a plugin handle
  1129. function destroyHandle(handleId, callbacks) {
  1130. callbacks = callbacks || {};
  1131. callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : jQuery.noop;
  1132. callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : jQuery.noop;
  1133. Janus.warn(callbacks);
  1134. var asyncRequest = true;
  1135. if(callbacks.asyncRequest !== undefined && callbacks.asyncRequest !== null)
  1136. asyncRequest = (callbacks.asyncRequest === true);
  1137. Janus.log("Destroying handle " + handleId + " (async=" + asyncRequest + ")");
  1138. cleanupWebrtc(handleId);
  1139. if (pluginHandles[handleId].detached) {
  1140. // Plugin was already detached by Janus, calling detach again will return a handle not found error, so just exit here
  1141. delete pluginHandles[handleId];
  1142. callbacks.success();
  1143. return;
  1144. }
  1145. if(!connected) {
  1146. Janus.warn("Is the gateway down? (connected=false)");
  1147. callbacks.error("Is the gateway down? (connected=false)");
  1148. return;
  1149. }
  1150. var request = { "janus": "detach", "transaction": Janus.randomString(12) };
  1151. if(token !== null && token !== undefined)
  1152. request["token"] = token;
  1153. if(apisecret !== null && apisecret !== undefined)
  1154. request["apisecret"] = apisecret;
  1155. if(websockets) {
  1156. request["session_id"] = sessionId;
  1157. request["handle_id"] = handleId;
  1158. ws.send(JSON.stringify(request));
  1159. delete pluginHandles[handleId];
  1160. callbacks.success();
  1161. return;
  1162. }
  1163. $.ajax({
  1164. type: 'POST',
  1165. url: server + "/" + sessionId + "/" + handleId,
  1166. async: asyncRequest, // Sometimes we need false here, or destroying in onbeforeunload won't work
  1167. xhrFields: {
  1168. withCredentials: withCredentials
  1169. },
  1170. cache: false,
  1171. contentType: "application/json",
  1172. data: JSON.stringify(request),
  1173. success: function(json) {
  1174. Janus.log("Destroyed handle:");
  1175. Janus.debug(json);
  1176. if(json["janus"] !== "success") {
  1177. Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
  1178. }
  1179. delete pluginHandles[handleId];
  1180. callbacks.success();
  1181. },
  1182. error: function(XMLHttpRequest, textStatus, errorThrown) {
  1183. Janus.error(textStatus + ": " + errorThrown); // FIXME
  1184. // We cleanup anyway
  1185. delete pluginHandles[handleId];
  1186. callbacks.success();
  1187. },
  1188. dataType: "json"
  1189. });
  1190. }
  1191. // WebRTC stuff
  1192. function streamsDone(handleId, jsep, media, callbacks, stream) {
  1193. var pluginHandle = pluginHandles[handleId];
  1194. if(pluginHandle === null || pluginHandle === undefined ||
  1195. pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
  1196. Janus.warn("Invalid handle");
  1197. callbacks.error("Invalid handle");
  1198. return;
  1199. }
  1200. var config = pluginHandle.webrtcStuff;
  1201. Janus.debug("streamsDone:", stream);
  1202. config.myStream = stream;
  1203. var pc_config = {"iceServers": iceServers, "iceTransportPolicy": iceTransportPolicy};
  1204. //~ var pc_constraints = {'mandatory': {'MozDontOfferDataChannel':true}};
  1205. var pc_constraints = {
  1206. "optional": [{"DtlsSrtpKeyAgreement": true}]
  1207. };
  1208. if(ipv6Support === true) {
  1209. // FIXME This is only supported in Chrome right now
  1210. // For support in Firefox track this: https://bugzilla.mozilla.org/show_bug.cgi?id=797262
  1211. pc_constraints.optional.push({"googIPv6":true});
  1212. }
  1213. if(adapter.browserDetails.browser === "edge") {
  1214. // This is Edge, enable BUNDLE explicitly
  1215. pc_config.bundlePolicy = "max-bundle";
  1216. }
  1217. Janus.log("Creating PeerConnection");
  1218. Janus.debug(pc_constraints);
  1219. config.pc = new RTCPeerConnection(pc_config, pc_constraints);
  1220. Janus.debug(config.pc);
  1221. if(config.pc.getStats) { // FIXME
  1222. config.volume.value = 0;
  1223. config.bitrate.value = "0 kbits/sec";
  1224. }
  1225. Janus.log("Preparing local SDP and gathering candidates (trickle=" + config.trickle + ")");
  1226. config.pc.oniceconnectionstatechange = function(e) {
  1227. if(config.pc)
  1228. pluginHandle.iceState(config.pc.iceConnectionState);
  1229. };
  1230. config.pc.onicecandidate = function(event) {
  1231. if (event.candidate == null ||
  1232. (adapter.browserDetails.browser === 'edge' && event.candidate.candidate.indexOf('endOfCandidates') > 0)) {
  1233. Janus.log("End of candidates.");
  1234. config.iceDone = true;
  1235. if(config.trickle === true) {
  1236. // Notify end of candidates
  1237. sendTrickleCandidate(handleId, {"completed": true});
  1238. } else {
  1239. // No trickle, time to send the complete SDP (including all candidates)
  1240. sendSDP(handleId, callbacks);
  1241. }
  1242. } else {
  1243. // JSON.stringify doesn't work on some WebRTC objects anymore
  1244. // See https://code.google.com/p/chromium/issues/detail?id=467366
  1245. var candidate = {
  1246. "candidate": event.candidate.candidate,
  1247. "sdpMid": event.candidate.sdpMid,
  1248. "sdpMLineIndex": event.candidate.sdpMLineIndex
  1249. };
  1250. if(config.trickle === true) {
  1251. // Send candidate
  1252. sendTrickleCandidate(handleId, candidate);
  1253. }
  1254. }
  1255. };
  1256. if(stream !== null && stream !== undefined) {
  1257. Janus.log('Adding local stream');
  1258. config.pc.addStream(stream);
  1259. pluginHandle.onlocalstream(stream);
  1260. }
  1261. config.pc.ontrack = function(remoteStream) {
  1262. Janus.log("Handling Remote Stream");
  1263. Janus.debug(remoteStream);
  1264. config.remoteStream = remoteStream;
  1265. pluginHandle.onremotestream(remoteStream.streams[0]);
  1266. };
  1267. // Any data channel to create?
  1268. if(isDataEnabled(media)) {
  1269. Janus.log("Creating data channel");
  1270. var onDataChannelMessage = function(event) {
  1271. Janus.log('Received message on data channel: ' + event.data);
  1272. pluginHandle.ondata(event.data); // FIXME
  1273. }
  1274. var onDataChannelStateChange = function() {
  1275. var dcState = config.dataChannel !== null ? config.dataChannel.readyState : "null";
  1276. Janus.log('State change on data channel: ' + dcState);
  1277. if(dcState === 'open') {
  1278. pluginHandle.ondataopen(); // FIXME
  1279. }
  1280. }
  1281. var onDataChannelError = function(error) {
  1282. Janus.error('Got error on data channel:', error);
  1283. // TODO
  1284. }
  1285. // Until we implement the proxying of open requests within the Janus core, we open a channel ourselves whatever the case
  1286. config.dataChannel = config.pc.createDataChannel("JanusDataChannel", {ordered:false}); // FIXME Add options (ordered, maxRetransmits, etc.)
  1287. config.dataChannel.onmessage = onDataChannelMessage;
  1288. config.dataChannel.onopen = onDataChannelStateChange;
  1289. config.dataChannel.onclose = onDataChannelStateChange;
  1290. config.dataChannel.onerror = onDataChannelError;
  1291. }
  1292. // Create offer/answer now
  1293. if(jsep === null || jsep === undefined) {
  1294. createOffer(handleId, media, callbacks);
  1295. } else {
  1296. if(adapter.browserDetails.browser === "edge") {
  1297. // This is Edge, add an a=end-of-candidates at the end
  1298. jsep.sdp += "a=end-of-candidates\r\n";
  1299. }
  1300. config.pc.setRemoteDescription(
  1301. new RTCSessionDescription(jsep),
  1302. function() {
  1303. Janus.log("Remote description accepted!");
  1304. createAnswer(handleId, media, callbacks);
  1305. }, callbacks.error);
  1306. }
  1307. }
  1308. function prepareWebrtc(handleId, callbacks) {
  1309. callbacks = callbacks || {};
  1310. callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : jQuery.noop;
  1311. callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : webrtcError;
  1312. var jsep = callbacks.jsep;
  1313. var media = callbacks.media;
  1314. var pluginHandle = pluginHandles[handleId];
  1315. if(pluginHandle === null || pluginHandle === undefined ||
  1316. pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
  1317. Janus.warn("Invalid handle");
  1318. callbacks.error("Invalid handle");
  1319. return;
  1320. }
  1321. var config = pluginHandle.webrtcStuff;
  1322. // Are we updating a session?
  1323. if(config.pc !== undefined && config.pc !== null) {
  1324. Janus.log("Updating existing media session");
  1325. // Create offer/answer now
  1326. if(jsep === null || jsep === undefined) {
  1327. createOffer(handleId, media, callbacks);
  1328. } else {
  1329. if(adapter.browserDetails.browser === "edge") {
  1330. // This is Edge, add an a=end-of-candidates at the end
  1331. jsep.sdp += "a=end-of-candidates\r\n";
  1332. }
  1333. config.pc.setRemoteDescription(
  1334. new RTCSessionDescription(jsep),
  1335. function() {
  1336. Janus.log("Remote description accepted!");
  1337. createAnswer(handleId, media, callbacks);
  1338. }, callbacks.error);
  1339. }
  1340. return;
  1341. }
  1342. // Was a MediaStream object passed, or do we need to take care of that?
  1343. if(callbacks.stream !== null && callbacks.stream !== undefined) {
  1344. var stream = callbacks.stream;
  1345. Janus.log("MediaStream provided by the application");
  1346. Janus.debug(stream);
  1347. // Skip the getUserMedia part
  1348. config.streamExternal = true;
  1349. streamsDone(handleId, jsep, media, callbacks, stream);
  1350. return;
  1351. }
  1352. config.trickle = isTrickleEnabled(callbacks.trickle);
  1353. if(isAudioSendEnabled(media) || isVideoSendEnabled(media)) {
  1354. var constraints = { mandatory: {}, optional: []};
  1355. pluginHandle.consentDialog(true);
  1356. var audioSupport = isAudioSendEnabled(media);
  1357. if(audioSupport === true && media != undefined && media != null) {
  1358. if(typeof media.audio === 'object') {
  1359. audioSupport = media.audio;
  1360. }
  1361. }
  1362. var videoSupport = isVideoSendEnabled(media);
  1363. if(videoSupport === true && media != undefined && media != null) {
  1364. if(media.video && media.video != 'screen' && media.video != 'window') {
  1365. var width = 0;
  1366. var height = 0, maxHeight = 0;
  1367. if(media.video === 'lowres') {
  1368. // Small resolution, 4:3
  1369. height = 240;
  1370. maxHeight = 240;
  1371. width = 320;
  1372. } else if(media.video === 'lowres-16:9') {
  1373. // Small resolution, 16:9
  1374. height = 180;
  1375. maxHeight = 180;
  1376. width = 320;
  1377. } else if(media.video === 'hires' || media.video === 'hires-16:9' ) {
  1378. // High resolution is only 16:9
  1379. height = 720;
  1380. maxHeight = 720;
  1381. width = 1280;
  1382. if(navigator.mozGetUserMedia) {
  1383. var firefoxVer = parseInt(window.navigator.userAgent.match(/Firefox\/(.*)/)[1], 10);
  1384. if(firefoxVer < 38) {
  1385. // Unless this is and old Firefox, which doesn't support it
  1386. Janus.warn(media.video + " unsupported, falling back to stdres (old Firefox)");
  1387. height = 480;
  1388. maxHeight = 480;
  1389. width = 640;
  1390. }
  1391. }
  1392. } else if(media.video === 'stdres') {
  1393. // Normal resolution, 4:3
  1394. height = 480;
  1395. maxHeight = 480;
  1396. width = 640;
  1397. } else if(media.video === 'stdres-16:9') {
  1398. // Normal resolution, 16:9
  1399. height = 360;
  1400. maxHeight = 360;
  1401. width = 640;
  1402. } else {
  1403. Janus.log("Default video setting (" + media.video + ") is stdres 4:3");
  1404. height = 480;
  1405. maxHeight = 480;
  1406. width = 640;
  1407. }
  1408. Janus.log("Adding media constraint " + media.video);
  1409. if(navigator.mozGetUserMedia) {
  1410. var firefoxVer = parseInt(window.navigator.userAgent.match(/Firefox\/(.*)/)[1], 10);
  1411. if(firefoxVer < 38) {
  1412. videoSupport = {
  1413. 'require': ['height', 'width'],
  1414. 'height': {'max': maxHeight, 'min': height},
  1415. 'width': {'max': width, 'min': width}
  1416. };
  1417. } else {
  1418. // http://stackoverflow.com/questions/28282385/webrtc-firefox-constraints/28911694#28911694
  1419. // https://github.com/meetecho/janus-gateway/pull/246
  1420. videoSupport = {
  1421. 'height': {'ideal': height},
  1422. 'width': {'ideal': width}
  1423. };
  1424. }
  1425. } else {
  1426. videoSupport = {
  1427. 'mandatory': {
  1428. 'maxHeight': maxHeight,
  1429. 'minHeight': height,
  1430. 'maxWidth': width,
  1431. 'minWidth': width
  1432. },
  1433. 'optional': []
  1434. };
  1435. }
  1436. if(typeof media.video === 'object') {
  1437. videoSupport = media.video;
  1438. }
  1439. Janus.debug(videoSupport);
  1440. } else if(media.video === 'screen' || media.video === 'window') {
  1441. if (!media.screenshareFrameRate) {
  1442. media.screenshareFrameRate = 3;
  1443. }
  1444. // Not a webcam, but screen capture
  1445. if(window.location.protocol !== 'https:') {
  1446. // Screen sharing mandates HTTPS
  1447. Janus.warn("Screen sharing only works on HTTPS, try the https:// version of this page");
  1448. pluginHandle.consentDialog(false);
  1449. callbacks.error("Screen sharing only works on HTTPS, try the https:// version of this page");
  1450. return;
  1451. }
  1452. // We're going to try and use the extension for Chrome 34+, the old approach
  1453. // for older versions of Chrome, or the experimental support in Firefox 33+
  1454. var cache = {};
  1455. function callbackUserMedia (error, stream) {
  1456. pluginHandle.consentDialog(false);
  1457. if(error) {
  1458. callbacks.error({code: error.code, name: error.name, message: error.message});
  1459. } else {
  1460. streamsDone(handleId, jsep, media, callbacks, stream);
  1461. }
  1462. };
  1463. function getScreenMedia(constraints, gsmCallback) {
  1464. Janus.log("Adding media constraint (screen capture)");
  1465. Janus.debug(constraints);
  1466. navigator.mediaDevices.getUserMedia(constraints)
  1467. .then(function(stream) { gsmCallback(null, stream); })
  1468. .catch(function(error) { pluginHandle.consentDialog(false); gsmCallback(error); });
  1469. };
  1470. if(adapter.browserDetails.browser === 'chrome') {
  1471. var chromever = adapter.browserDetails.version;
  1472. var maxver = 33;
  1473. if(window.navigator.userAgent.match('Linux'))
  1474. maxver = 35; // "known" crash in chrome 34 and 35 on linux
  1475. if(chromever >= 26 && chromever <= maxver) {
  1476. // Chrome 26->33 requires some awkward chrome://flags manipulation
  1477. constraints = {
  1478. video: {
  1479. mandatory: {
  1480. googLeakyBucket: true,
  1481. maxWidth: window.screen.width,
  1482. maxHeight: window.screen.height,
  1483. minFrameRate: media.screenshareFrameRate,
  1484. maxFrameRate: media.screenshareFrameRate,
  1485. chromeMediaSource: 'screen'
  1486. }
  1487. },
  1488. audio: isAudioSendEnabled(media)
  1489. };
  1490. getScreenMedia(constraints, callbackUserMedia);
  1491. } else {
  1492. // Chrome 34+ requires an extension
  1493. var pending = window.setTimeout(
  1494. function () {
  1495. error = new Error('NavigatorUserMediaError');
  1496. 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)';
  1497. pluginHandle.consentDialog(false);
  1498. return callbacks.error(error);
  1499. }, 1000);
  1500. cache[pending] = [callbackUserMedia, null];
  1501. window.postMessage({ type: 'janusGetScreen', id: pending }, '*');
  1502. }
  1503. } else if (window.navigator.userAgent.match('Firefox')) {
  1504. var ffver = parseInt(window.navigator.userAgent.match(/Firefox\/(.*)/)[1], 10);
  1505. if(ffver >= 33) {
  1506. // Firefox 33+ has experimental support for screen sharing
  1507. constraints = {
  1508. video: {
  1509. mozMediaSource: media.video,
  1510. mediaSource: media.video
  1511. },
  1512. audio: isAudioSendEnabled(media)
  1513. };
  1514. getScreenMedia(constraints, function (err, stream) {
  1515. callbackUserMedia(err, stream);
  1516. // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1045810
  1517. if (!err) {
  1518. var lastTime = stream.currentTime;
  1519. var polly = window.setInterval(function () {
  1520. if(!stream)
  1521. window.clearInterval(polly);
  1522. if(stream.currentTime == lastTime) {
  1523. window.clearInterval(polly);
  1524. if(stream.onended) {
  1525. stream.onended();
  1526. }
  1527. }
  1528. lastTime = stream.currentTime;
  1529. }, 500);
  1530. }
  1531. });
  1532. } else {
  1533. var error = new Error('NavigatorUserMediaError');
  1534. error.name = 'Your version of Firefox does not support screen sharing, please install Firefox 33 (or more recent versions)';
  1535. pluginHandle.consentDialog(false);
  1536. callbacks.error(error);
  1537. return;
  1538. }
  1539. }
  1540. // Wait for events from the Chrome Extension
  1541. window.addEventListener('message', function (event) {
  1542. if(event.origin != window.location.origin)
  1543. return;
  1544. if(event.data.type == 'janusGotScreen' && cache[event.data.id]) {
  1545. var data = cache[event.data.id];
  1546. var callback = data[0];
  1547. delete cache[event.data.id];
  1548. if (event.data.sourceId === '') {
  1549. // user canceled
  1550. var error = new Error('NavigatorUserMediaError');
  1551. error.name = 'You cancelled the request for permission, giving up...';
  1552. pluginHandle.consentDialog(false);
  1553. callbacks.error(error);
  1554. } else {
  1555. constraints = {
  1556. audio: isAudioSendEnabled(media),
  1557. video: {
  1558. mandatory: {
  1559. chromeMediaSource: 'desktop',
  1560. maxWidth: window.screen.width,
  1561. maxHeight: window.screen.height,
  1562. minFrameRate: media.screenshareFrameRate,
  1563. maxFrameRate: media.screenshareFrameRate,
  1564. },
  1565. optional: [
  1566. {googLeakyBucket: true},
  1567. {googTemporalLayeredScreencast: true}
  1568. ]
  1569. }
  1570. };
  1571. constraints.video.mandatory.chromeMediaSourceId = event.data.sourceId;
  1572. getScreenMedia(constraints, callback);
  1573. }
  1574. } else if (event.data.type == 'janusGetScreenPending') {
  1575. window.clearTimeout(event.data.id);
  1576. }
  1577. });
  1578. return;
  1579. }
  1580. }
  1581. // If we got here, we're not screensharing
  1582. if(media === null || media === undefined || media.video !== 'screen') {
  1583. // Check whether all media sources are actually available or not
  1584. navigator.mediaDevices.enumerateDevices().then(function(devices) {
  1585. var audioExist = devices.some(function(device) {
  1586. return device.kind === 'audioinput';
  1587. }),
  1588. videoExist = devices.some(function(device) {
  1589. return device.kind === 'videoinput';
  1590. });
  1591. // Check whether a missing device is really a problem
  1592. var audioSend = isAudioSendEnabled(media);
  1593. var videoSend = isVideoSendEnabled(media);
  1594. var needAudioDevice = isAudioSendRequired(media);
  1595. var needVideoDevice = isVideoSendRequired(media);
  1596. if(audioSend || videoSend || needAudioDevice || needVideoDevice) {
  1597. // We need to send either audio or video
  1598. var haveAudioDevice = audioSend ? audioExist : false;
  1599. var haveVideoDevice = videoSend ? videoExist : false;
  1600. if(!haveAudioDevice && !haveVideoDevice) {
  1601. // FIXME Should we really give up, or just assume recvonly for both?
  1602. pluginHandle.consentDialog(false);
  1603. callbacks.error('No capture device found');
  1604. return false;
  1605. } else if(!haveAudioDevice && needAudioDevice) {
  1606. pluginHandle.consentDialog(false);
  1607. callbacks.error('Audio capture is required, but no capture device found');
  1608. return false;
  1609. } else if(!haveVideoDevice && needVideoDevice) {
  1610. pluginHandle.consentDialog(false);
  1611. callbacks.error('Video capture is required, but no capture device found');
  1612. return false;
  1613. }
  1614. }
  1615. navigator.mediaDevices.getUserMedia({
  1616. audio: audioExist ? audioSupport : false,
  1617. video: videoExist ? videoSupport : false
  1618. })
  1619. .then(function(stream) { pluginHandle.consentDialog(false); streamsDone(handleId, jsep, media, callbacks, stream); })
  1620. .catch(function(error) { pluginHandle.consentDialog(false); callbacks.error({code: error.code, name: error.name, message: error.message}); });
  1621. })
  1622. .catch(function(error) {
  1623. pluginHandle.consentDialog(false);
  1624. callbacks.error('enumerateDevices error', error);
  1625. });
  1626. }
  1627. } else {
  1628. // No need to do a getUserMedia, create offer/answer right away
  1629. streamsDone(handleId, jsep, media, callbacks);
  1630. }
  1631. }
  1632. function prepareWebrtcPeer(handleId, callbacks) {
  1633. callbacks = callbacks || {};
  1634. callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : jQuery.noop;
  1635. callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : webrtcError;
  1636. var jsep = callbacks.jsep;
  1637. var pluginHandle = pluginHandles[handleId];
  1638. if(pluginHandle === null || pluginHandle === undefined ||
  1639. pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
  1640. Janus.warn("Invalid handle");
  1641. callbacks.error("Invalid handle");
  1642. return;
  1643. }
  1644. var config = pluginHandle.webrtcStuff;
  1645. if(jsep !== undefined && jsep !== null) {
  1646. if(config.pc === null) {
  1647. Janus.warn("Wait, no PeerConnection?? if this is an answer, use createAnswer and not handleRemoteJsep");
  1648. callbacks.error("No PeerConnection: if this is an answer, use createAnswer and not handleRemoteJsep");
  1649. return;
  1650. }
  1651. if(adapter.browserDetails.browser === "edge") {
  1652. // This is Edge, add an a=end-of-candidates at the end
  1653. jsep.sdp += "a=end-of-candidates\r\n";
  1654. }
  1655. config.pc.setRemoteDescription(
  1656. new RTCSessionDescription(jsep),
  1657. function() {
  1658. Janus.log("Remote description accepted!");
  1659. callbacks.success();
  1660. }, callbacks.error);
  1661. } else {
  1662. callbacks.error("Invalid JSEP");
  1663. }
  1664. }
  1665. function createOffer(handleId, media, callbacks) {
  1666. callbacks = callbacks || {};
  1667. callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : jQuery.noop;
  1668. callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : jQuery.noop;
  1669. var pluginHandle = pluginHandles[handleId];
  1670. if(pluginHandle === null || pluginHandle === undefined ||
  1671. pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
  1672. Janus.warn("Invalid handle");
  1673. callbacks.error("Invalid handle");
  1674. return;
  1675. }
  1676. var config = pluginHandle.webrtcStuff;
  1677. Janus.log("Creating offer (iceDone=" + config.iceDone + ")");
  1678. // https://code.google.com/p/webrtc/issues/detail?id=3508
  1679. var mediaConstraints = null;
  1680. if(adapter.browserDetails.browser == "firefox" || adapter.browserDetails.browser == "edge") {
  1681. mediaConstraints = {
  1682. 'offerToReceiveAudio':isAudioRecvEnabled(media),
  1683. 'offerToReceiveVideo':isVideoRecvEnabled(media)
  1684. };
  1685. } else {
  1686. mediaConstraints = {
  1687. 'mandatory': {
  1688. 'OfferToReceiveAudio':isAudioRecvEnabled(media),
  1689. 'OfferToReceiveVideo':isVideoRecvEnabled(media)
  1690. }
  1691. };
  1692. }
  1693. Janus.debug(mediaConstraints);
  1694. config.pc.createOffer(
  1695. function(offer) {
  1696. Janus.debug(offer);
  1697. if(config.mySdp === null || config.mySdp === undefined) {
  1698. Janus.log("Setting local description");
  1699. config.mySdp = offer.sdp;
  1700. config.pc.setLocalDescription(offer);
  1701. }
  1702. if(!config.iceDone && !config.trickle) {
  1703. // Don't do anything until we have all candidates
  1704. Janus.log("Waiting for all candidates...");
  1705. return;
  1706. }
  1707. if(config.sdpSent) {
  1708. Janus.log("Offer already sent, not sending it again");
  1709. return;
  1710. }
  1711. Janus.log("Offer ready");
  1712. Janus.debug(callbacks);
  1713. config.sdpSent = true;
  1714. // JSON.stringify doesn't work on some WebRTC objects anymore
  1715. // See https://code.google.com/p/chromium/issues/detail?id=467366
  1716. var jsep = {
  1717. "type": offer.type,
  1718. "sdp": offer.sdp
  1719. };
  1720. callbacks.success(jsep);
  1721. }, callbacks.error, mediaConstraints);
  1722. }
  1723. function createAnswer(handleId, media, callbacks) {
  1724. callbacks = callbacks || {};
  1725. callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : jQuery.noop;
  1726. callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : jQuery.noop;
  1727. var pluginHandle = pluginHandles[handleId];
  1728. if(pluginHandle === null || pluginHandle === undefined ||
  1729. pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
  1730. Janus.warn("Invalid handle");
  1731. callbacks.error("Invalid handle");
  1732. return;
  1733. }
  1734. var config = pluginHandle.webrtcStuff;
  1735. Janus.log("Creating answer (iceDone=" + config.iceDone + ")");
  1736. var mediaConstraints = null;
  1737. if(adapter.browserDetails.browser == "firefox" || adapter.browserDetails.browser == "edge") {
  1738. mediaConstraints = {
  1739. 'offerToReceiveAudio':isAudioRecvEnabled(media),
  1740. 'offerToReceiveVideo':isVideoRecvEnabled(media)
  1741. };
  1742. } else {
  1743. mediaConstraints = {
  1744. 'mandatory': {
  1745. 'OfferToReceiveAudio':isAudioRecvEnabled(media),
  1746. 'OfferToReceiveVideo':isVideoRecvEnabled(media)
  1747. }
  1748. };
  1749. }
  1750. Janus.debug(mediaConstraints);
  1751. config.pc.createAnswer(
  1752. function(answer) {
  1753. Janus.debug(answer);
  1754. if(config.mySdp === null || config.mySdp === undefined) {
  1755. Janus.log("Setting local description");
  1756. config.mySdp = answer.sdp;
  1757. config.pc.setLocalDescription(answer);
  1758. }
  1759. if(!config.iceDone && !config.trickle) {
  1760. // Don't do anything until we have all candidates
  1761. Janus.log("Waiting for all candidates...");
  1762. return;
  1763. }
  1764. if(config.sdpSent) { // FIXME badly
  1765. Janus.log("Answer already sent, not sending it again");
  1766. return;
  1767. }
  1768. config.sdpSent = true;
  1769. // JSON.stringify doesn't work on some WebRTC objects anymore
  1770. // See https://code.google.com/p/chromium/issues/detail?id=467366
  1771. var jsep = {
  1772. "type": answer.type,
  1773. "sdp": answer.sdp
  1774. };
  1775. callbacks.success(jsep);
  1776. }, callbacks.error, mediaConstraints);
  1777. }
  1778. function sendSDP(handleId, callbacks) {
  1779. callbacks = callbacks || {};
  1780. callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : jQuery.noop;
  1781. callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : jQuery.noop;
  1782. var pluginHandle = pluginHandles[handleId];
  1783. if(pluginHandle === null || pluginHandle === undefined ||
  1784. pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
  1785. Janus.warn("Invalid handle, not sending anything");
  1786. return;
  1787. }
  1788. var config = pluginHandle.webrtcStuff;
  1789. Janus.log("Sending offer/answer SDP...");
  1790. if(config.mySdp === null || config.mySdp === undefined) {
  1791. Janus.warn("Local SDP instance is invalid, not sending anything...");
  1792. return;
  1793. }
  1794. config.mySdp = {
  1795. "type": config.pc.localDescription.type,
  1796. "sdp": config.pc.localDescription.sdp
  1797. };
  1798. if(config.sdpSent) {
  1799. Janus.log("Offer/Answer SDP already sent, not sending it again");
  1800. return;
  1801. }
  1802. if(config.trickle === false)
  1803. config.mySdp["trickle"] = false;
  1804. Janus.debug(callbacks);
  1805. config.sdpSent = true;
  1806. callbacks.success(config.mySdp);
  1807. }
  1808. function getVolume(handleId) {
  1809. var pluginHandle = pluginHandles[handleId];
  1810. if(pluginHandle === null || pluginHandle === undefined ||
  1811. pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
  1812. Janus.warn("Invalid handle");
  1813. return 0;
  1814. }
  1815. var config = pluginHandle.webrtcStuff;
  1816. // Start getting the volume, if getStats is supported
  1817. if(config.pc.getStats && adapter.browserDetails.browser == "chrome") { // FIXME
  1818. if(config.remoteStream === null || config.remoteStream === undefined) {
  1819. Janus.warn("Remote stream unavailable");
  1820. return 0;
  1821. }
  1822. // http://webrtc.googlecode.com/svn/trunk/samples/js/demos/html/constraints-and-stats.html
  1823. if(config.volume.timer === null || config.volume.timer === undefined) {
  1824. Janus.log("Starting volume monitor");
  1825. config.volume.timer = setInterval(function() {
  1826. config.pc.getStats(function(stats) {
  1827. var results = stats.result();
  1828. for(var i=0; i<results.length; i++) {
  1829. var res = results[i];
  1830. if(res.type == 'ssrc' && res.stat('audioOutputLevel')) {
  1831. config.volume.value = res.stat('audioOutputLevel');
  1832. }
  1833. }
  1834. });
  1835. }, 200);
  1836. return 0; // We don't have a volume to return yet
  1837. }
  1838. return config.volume.value;
  1839. } else {
  1840. Janus.log("Getting the remote volume unsupported by browser");
  1841. return 0;
  1842. }
  1843. }
  1844. function isMuted(handleId, video) {
  1845. var pluginHandle = pluginHandles[handleId];
  1846. if(pluginHandle === null || pluginHandle === undefined ||
  1847. pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
  1848. Janus.warn("Invalid handle");
  1849. return true;
  1850. }
  1851. var config = pluginHandle.webrtcStuff;
  1852. if(config.pc === null || config.pc === undefined) {
  1853. Janus.warn("Invalid PeerConnection");
  1854. return true;
  1855. }
  1856. if(config.myStream === undefined || config.myStream === null) {
  1857. Janus.warn("Invalid local MediaStream");
  1858. return true;
  1859. }
  1860. if(video) {
  1861. // Check video track
  1862. if(config.myStream.getVideoTracks() === null
  1863. || config.myStream.getVideoTracks() === undefined
  1864. || config.myStream.getVideoTracks().length === 0) {
  1865. Janus.warn("No video track");
  1866. return true;
  1867. }
  1868. return !config.myStream.getVideoTracks()[0].enabled;
  1869. } else {
  1870. // Check audio track
  1871. if(config.myStream.getAudioTracks() === null
  1872. || config.myStream.getAudioTracks() === undefined
  1873. || config.myStream.getAudioTracks().length === 0) {
  1874. Janus.warn("No audio track");
  1875. return true;
  1876. }
  1877. return !config.myStream.getAudioTracks()[0].enabled;
  1878. }
  1879. }
  1880. function mute(handleId, video, mute) {
  1881. var pluginHandle = pluginHandles[handleId];
  1882. if(pluginHandle === null || pluginHandle === undefined ||
  1883. pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
  1884. Janus.warn("Invalid handle");
  1885. return false;
  1886. }
  1887. var config = pluginHandle.webrtcStuff;
  1888. if(config.pc === null || config.pc === undefined) {
  1889. Janus.warn("Invalid PeerConnection");
  1890. return false;
  1891. }
  1892. if(config.myStream === undefined || config.myStream === null) {
  1893. Janus.warn("Invalid local MediaStream");
  1894. return false;
  1895. }
  1896. if(video) {
  1897. // Mute/unmute video track
  1898. if(config.myStream.getVideoTracks() === null
  1899. || config.myStream.getVideoTracks() === undefined
  1900. || config.myStream.getVideoTracks().length === 0) {
  1901. Janus.warn("No video track");
  1902. return false;
  1903. }
  1904. config.myStream.getVideoTracks()[0].enabled = mute ? false : true;
  1905. return true;
  1906. } else {
  1907. // Mute/unmute audio track
  1908. if(config.myStream.getAudioTracks() === null
  1909. || config.myStream.getAudioTracks() === undefined
  1910. || config.myStream.getAudioTracks().length === 0) {
  1911. Janus.warn("No audio track");
  1912. return false;
  1913. }
  1914. config.myStream.getAudioTracks()[0].enabled = mute ? false : true;
  1915. return true;
  1916. }
  1917. }
  1918. function getBitrate(handleId) {
  1919. var pluginHandle = pluginHandles[handleId];
  1920. if(pluginHandle === null || pluginHandle === undefined ||
  1921. pluginHandle.webrtcStuff === null || pluginHandle.webrtcStuff === undefined) {
  1922. Janus.warn("Invalid handle");
  1923. return "Invalid handle";
  1924. }
  1925. var config = pluginHandle.webrtcStuff;
  1926. if(config.pc === null || config.pc === undefined)
  1927. return "Invalid PeerConnection";
  1928. // Start getting the bitrate, if getStats is supported
  1929. if(config.pc.getStats && adapter.browserDetails.browser == "chrome") {
  1930. // Do it the Chrome way
  1931. if(config.remoteStream === null || config.remoteStream === undefined) {
  1932. Janus.warn("Remote stream unavailable");
  1933. return "Remote stream unavailable";
  1934. }
  1935. // http://webrtc.googlecode.com/svn/trunk/samples/js/demos/html/constraints-and-stats.html
  1936. if(config.bitrate.timer === null || config.bitrate.timer === undefined) {
  1937. Janus.log("Starting bitrate timer (Chrome)");
  1938. config.bitrate.timer = setInterval(function() {
  1939. config.pc.getStats(function(stats) {
  1940. var results = stats.result();
  1941. for(var i=0; i<results.length; i++) {
  1942. var res = results[i];
  1943. if(res.type == 'ssrc' && res.stat('googFrameHeightReceived')) {
  1944. config.bitrate.bsnow = res.stat('bytesReceived');
  1945. config.bitrate.tsnow = res.timestamp;
  1946. if(config.bitrate.bsbefore === null || config.bitrate.tsbefore === null) {
  1947. // Skip this round
  1948. config.bitrate.bsbefore = config.bitrate.bsnow;
  1949. config.bitrate.tsbefore = config.bitrate.tsnow;
  1950. } else {
  1951. // Calculate bitrate
  1952. var bitRate = Math.round((config.bitrate.bsnow - config.bitrate.bsbefore) * 8 / (config.bitrate.tsnow - config.bitrate.tsbefore));
  1953. config.bitrate.value = bitRate + ' kbits/sec';
  1954. //~ Janus.log("Estimated bitrate is " + config.bitrate.value);
  1955. config.bitrate.bsbefore = config.bitrate.bsnow;
  1956. config.bitrate.tsbefore = config.bitrate.tsnow;
  1957. }
  1958. }
  1959. }
  1960. });
  1961. }, 1000);
  1962. return "0 kbits/sec"; // We don't have a bitrate value yet
  1963. }
  1964. return config.bitrate.value;
  1965. } else if(config.pc.getStats && adapter.browserDetails.browser == "firefox") {
  1966. // Do it the Firefox way
  1967. if(config.remoteStream === null || config.remoteStream === undefined
  1968. || config.remoteStream.streams[0] === null || config.remoteStream.streams[0] === undefined) {
  1969. Janus.warn("Remote stream unavailable");
  1970. return "Remote stream unavailable";
  1971. }
  1972. var videoTracks = config.remoteStream.streams[0].getVideoTracks();
  1973. if(videoTracks === null || videoTracks === undefined || videoTracks.length < 1) {
  1974. Janus.warn("No video track");
  1975. return "No video track";
  1976. }
  1977. // https://github.com/muaz-khan/getStats/blob/master/getStats.js
  1978. if(config.bitrate.timer === null || config.bitrate.timer === undefined) {
  1979. Janus.log("Starting bitrate timer (Firefox)");
  1980. config.bitrate.timer = setInterval(function() {
  1981. // We need a helper callback
  1982. var cb = function(res) {
  1983. if(res === null || res === undefined ||
  1984. res.inbound_rtp_video_1 == null || res.inbound_rtp_video_1 == null) {
  1985. config.bitrate.value = "Missing inbound_rtp_video_1";
  1986. return;
  1987. }
  1988. config.bitrate.bsnow = res.inbound_rtp_video_1.bytesReceived;
  1989. config.bitrate.tsnow = res.inbound_rtp_video_1.timestamp;
  1990. if(config.bitrate.bsbefore === null || config.bitrate.tsbefore === null) {
  1991. // Skip this round
  1992. config.bitrate.bsbefore = config.bitrate.bsnow;
  1993. config.bitrate.tsbefore = config.bitrate.tsnow;
  1994. } else {
  1995. // Calculate bitrate
  1996. var bitRate = Math.round((config.bitrate.bsnow - config.bitrate.bsbefore) * 8 / (config.bitrate.tsnow - config.bitrate.tsbefore));
  1997. config.bitrate.value = bitRate + ' kbits/sec';
  1998. config.bitrate.bsbefore = config.bitrate.bsnow;
  1999. config.bitrate.tsbefore = config.bitrate.tsnow;
  2000. }
  2001. };
  2002. // Actually get the stats
  2003. config.pc.getStats(videoTracks[0], function(stats) {
  2004. cb(stats);
  2005. }, cb);
  2006. }, 1000);
  2007. return "0 kbits/sec"; // We don't have a bitrate value yet
  2008. }
  2009. return config.bitrate.value;
  2010. } else {
  2011. Janus.warn("Getting the video bitrate unsupported by browser");
  2012. return "Feature unsupported by browser";
  2013. }
  2014. }
  2015. function webrtcError(error) {
  2016. Janus.error("WebRTC error:", error);
  2017. }
  2018. function cleanupWebrtc(handleId, hangupRequest) {
  2019. Janus.log("Cleaning WebRTC stuff");
  2020. var pluginHandle = pluginHandles[handleId];
  2021. if(pluginHandle === null || pluginHandle === undefined) {
  2022. // Nothing to clean
  2023. return;
  2024. }
  2025. var config = pluginHandle.webrtcStuff;
  2026. if(config !== null && config !== undefined) {
  2027. if(hangupRequest === true) {
  2028. // Send a hangup request (we don't really care about the response)
  2029. var request = { "janus": "hangup", "transaction": Janus.randomString(12) };
  2030. if(token !== null && token !== undefined)
  2031. request["token"] = token;
  2032. if(apisecret !== null && apisecret !== undefined)
  2033. request["apisecret"] = apisecret;
  2034. Janus.debug("Sending hangup request (handle=" + handleId + "):");
  2035. Janus.debug(request);
  2036. if(websockets) {
  2037. request["session_id"] = sessionId;
  2038. request["handle_id"] = handleId;
  2039. ws.send(JSON.stringify(request));
  2040. } else {
  2041. $.ajax({
  2042. type: 'POST',
  2043. url: server + "/" + sessionId + "/" + handleId,
  2044. xhrFields: {
  2045. withCredentials: withCredentials
  2046. },
  2047. cache: false,
  2048. contentType: "application/json",
  2049. data: JSON.stringify(request),
  2050. dataType: "json"
  2051. });
  2052. }
  2053. }
  2054. // Cleanup stack
  2055. config.remoteStream = null;
  2056. if(config.volume.timer)
  2057. clearInterval(config.volume.timer);
  2058. config.volume.value = null;
  2059. if(config.bitrate.timer)
  2060. clearInterval(config.bitrate.timer);
  2061. config.bitrate.timer = null;
  2062. config.bitrate.bsnow = null;
  2063. config.bitrate.bsbefore = null;
  2064. config.bitrate.tsnow = null;
  2065. config.bitrate.tsbefore = null;
  2066. config.bitrate.value = null;
  2067. try {
  2068. // Try a MediaStream.stop() first
  2069. if(!config.streamExternal && config.myStream !== null && config.myStream !== undefined) {
  2070. Janus.log("Stopping local stream");
  2071. config.myStream.stop();
  2072. }
  2073. } catch(e) {
  2074. // Do nothing if this fails
  2075. }
  2076. try {
  2077. // Try a MediaStreamTrack.stop() for each track as well
  2078. if(!config.streamExternal && config.myStream !== null && config.myStream !== undefined) {
  2079. Janus.log("Stopping local stream tracks");
  2080. var tracks = config.myStream.getTracks();
  2081. for(var i in tracks) {
  2082. var mst = tracks[i];
  2083. Janus.log(mst);
  2084. if(mst !== null && mst !== undefined)
  2085. mst.stop();
  2086. }
  2087. }
  2088. } catch(e) {
  2089. // Do nothing if this fails
  2090. }
  2091. config.streamExternal = false;
  2092. config.myStream = null;
  2093. // Close PeerConnection
  2094. try {
  2095. config.pc.close();
  2096. } catch(e) {
  2097. // Do nothing
  2098. }
  2099. config.pc = null;
  2100. config.mySdp = null;
  2101. config.iceDone = false;
  2102. config.sdpSent = false;
  2103. config.dataChannel = null;
  2104. config.dtmfSender = null;
  2105. }
  2106. pluginHandle.oncleanup();
  2107. }
  2108. // Helper methods to parse a media object
  2109. function isAudioSendEnabled(media) {
  2110. Janus.debug("isAudioSendEnabled:", media);
  2111. if(media === undefined || media === null)
  2112. return true; // Default
  2113. if(media.audio === false)
  2114. return false; // Generic audio has precedence
  2115. if(media.audioSend === undefined || media.audioSend === null)
  2116. return true; // Default
  2117. return (media.audioSend === true);
  2118. }
  2119. function isAudioSendRequired(media) {
  2120. Janus.debug("isAudioSendRequired:", media);
  2121. if(media === undefined || media === null)
  2122. return false; // Default
  2123. if(media.audio === false || media.audioSend === false)
  2124. return false; // If we're not asking to capture audio, it's not required
  2125. if(media.failIfNoAudio === undefined || media.failIfNoAudio === null)
  2126. return false; // Default
  2127. return (media.failIfNoAudio === true);
  2128. }
  2129. function isAudioRecvEnabled(media) {
  2130. Janus.debug("isAudioRecvEnabled:", media);
  2131. if(media === undefined || media === null)
  2132. return true; // Default
  2133. if(media.audio === false)
  2134. return false; // Generic audio has precedence
  2135. if(media.audioRecv === undefined || media.audioRecv === null)
  2136. return true; // Default
  2137. return (media.audioRecv === true);
  2138. }
  2139. function isVideoSendEnabled(media) {
  2140. Janus.debug("isVideoSendEnabled:", media);
  2141. if(media === undefined || media === null)
  2142. return true; // Default
  2143. if(media.video === false)
  2144. return false; // Generic video has precedence
  2145. if(media.videoSend === undefined || media.videoSend === null)
  2146. return true; // Default
  2147. return (media.videoSend === true);
  2148. }
  2149. function isVideoSendRequired(media) {
  2150. Janus.debug("isVideoSendRequired:", media);
  2151. if(media === undefined || media === null)
  2152. return false; // Default
  2153. if(media.video === false || media.videoSend === false)
  2154. return false; // If we're not asking to capture video, it's not required
  2155. if(media.failIfNoVideo === undefined || media.failIfNoVideo === null)
  2156. return false; // Default
  2157. return (media.failIfNoVideo === true);
  2158. }
  2159. function isVideoRecvEnabled(media) {
  2160. Janus.debug("isVideoRecvEnabled:", media);
  2161. if(media === undefined || media === null)
  2162. return true; // Default
  2163. if(media.video === false)
  2164. return false; // Generic video has precedence
  2165. if(media.videoRecv === undefined || media.videoRecv === null)
  2166. return true; // Default
  2167. return (media.videoRecv === true);
  2168. }
  2169. function isDataEnabled(media) {
  2170. Janus.debug("isDataEnabled:", media);
  2171. if(adapter.browserDetails.browser == "edge") {
  2172. Janus.warn("Edge doesn't support data channels yet");
  2173. return false;
  2174. }
  2175. if(media === undefined || media === null)
  2176. return false; // Default
  2177. return (media.data === true);
  2178. }
  2179. function isTrickleEnabled(trickle) {
  2180. Janus.debug("isTrickleEnabled:", trickle);
  2181. if(trickle === undefined || trickle === null)
  2182. return true; // Default is true
  2183. return (trickle === true);
  2184. }
  2185. };