Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

writing.js 36KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057
  1. /**
  2. * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
  3. *
  4. * Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below).
  5. *
  6. * This program is free software; you can redistribute it and/or modify it under the
  7. * terms of the GNU Lesser General Public License as published by the Free Software
  8. * Foundation; either version 3.0 of the License, or (at your option) any later
  9. * version.
  10. *
  11. * BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
  12. * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
  13. * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU Lesser General Public License along
  16. * with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
  17. *
  18. */
  19. const logger = window.Logger || console;
  20. const params = getURLParameters();
  21. const meetingId = params.meetingId;
  22. const url = getFullURL();
  23. const metadataXML = url + '/metadata.xml';
  24. const shapesSVG = url + '/shapes.svg';
  25. const panzoomsXML = url + '/panzooms.xml';
  26. const cursorXML = url + '/cursor.xml';
  27. const deskshareXML = url + '/deskshare.xml';
  28. const textJSON = url + '/presentation_text.json';
  29. const captionsJSON = url + '/captions.json';
  30. const chatXML = url + '/slides_new.xml';
  31. const mediasURL = [
  32. '/video/webcams.webm',
  33. '/video/webcams.mp4',
  34. '/deskshare/deskshare.webm',
  35. '/deskshare/deskshare.mp4'
  36. ];
  37. const deskshareWidth = 1280.0;
  38. const deskshareHeight = 720.0;
  39. const mobileTimeout = 10 * 1000; // 10 seconds
  40. const mediaCheckInterval = 250;
  41. var lastFrameTime = 0.0;
  42. var firstLoad = true;
  43. var meetingDuration;
  44. var mediasToCheck = mediasURL.length;
  45. // Metadata
  46. var metadataXMLContent = null;
  47. // Media events
  48. var videoReady = false;
  49. var audioReady = false;
  50. var deskshareReady = false;
  51. var captionsReady = false;
  52. // Data events
  53. var svgReady = false;
  54. var textReady = false;
  55. var panzoomReady = false;
  56. var cursorReady = false;
  57. var deskshareXMLReady = false;
  58. // Shapes
  59. var timestampToId = {};
  60. var timestampToIdKeys = [];
  61. var mainShapeIds = {};
  62. var currentImage = null;
  63. var currentImageId = "image0";
  64. var shapesArray = [];
  65. var timestampToId = {};
  66. var shapesSVGContent = null;
  67. var slideAspectValues = {};
  68. var currentSlideAspect = 0;
  69. var imageAtTime = {};
  70. var slidePlainText = {}; //holds slide plain text for retrieval
  71. // Cursor
  72. var cursorShownAt = 0;
  73. var cursorValues = {};
  74. // Panzoom
  75. var vboxValues = {};
  76. // Deskshare
  77. var deskshareEvents = [];
  78. var deskshareImage = null;
  79. var widthScale = 1;
  80. var heightScale = 1;
  81. var widthTranslate = 0;
  82. var heightTranslate = 0;
  83. var isDeskshareActive = false;
  84. var canvasTransformed = false;
  85. function getURLParameters() {
  86. logger.info("==Getting URL params");
  87. let map = {};
  88. window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (m, key, value) {map[key] = value;});
  89. return map;
  90. };
  91. function getFullURL() {
  92. let url = '/presentation/' + meetingId;
  93. return url;
  94. };
  95. // http://stackoverflow.com/a/11381730
  96. function mobileAndTabletCheck() {
  97. let check = false;
  98. (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4)))check = true})(navigator.userAgent||navigator.vendor||window.opera);
  99. return check;
  100. }
  101. // Draw the cursor at a specific point
  102. function drawCursor(scaledX, scaledY) {
  103. let containerObj = $("#slide > div");
  104. // The offsets of the container that has the image inside it
  105. let imageOffsetX = containerObj.offset().left;
  106. let imageOffsetY = containerObj.offset().top;
  107. // Position of the cursor relative to the container
  108. let cursorXInImage = scaledX * containerObj.width();
  109. let cursorYInImage = scaledY * containerObj.height();
  110. // Absolute position of the cursor in the page
  111. let cursorLeft = parseInt(imageOffsetX + cursorXInImage, 10);
  112. let cursorTop = parseInt(imageOffsetY + cursorYInImage, 10);
  113. if (cursorLeft < 0) {
  114. cursorLeft = 0;
  115. }
  116. if (cursorTop < 0) {
  117. cursorTop = 0;
  118. }
  119. let cursorStyle = document.getElementById("cursor").style;
  120. cursorStyle.left = cursorLeft + "px";
  121. cursorStyle.top = cursorTop + "px";
  122. }
  123. function showCursor(show) {
  124. if (show) {
  125. document.getElementById("cursor").style.visibility = 'visible';
  126. } else {
  127. document.getElementById("cursor").style.visibility = 'hidden';
  128. }
  129. };
  130. function setViewBox(time) {
  131. let vboxVal = getViewboxAtTime(time);
  132. if (vboxVal !== undefined) {
  133. setTransform(time);
  134. let svgfile = getSVGFile();
  135. svgfile.setAttribute('viewBox', vboxVal);
  136. }
  137. };
  138. function getImageAtTime(time) {
  139. let currentTime = parseFloat(time);
  140. for (var key in imageAtTime) {
  141. if (imageAtTime.hasOwnProperty(key)) {
  142. let keyArray = key.split(",");
  143. if ((parseFloat(keyArray[0]) <= currentTime) && (parseFloat(keyArray[1]) > currentTime)) {
  144. return imageAtTime[key];
  145. }
  146. }
  147. }
  148. };
  149. function getShapesAtTime(time) {
  150. let shapesAtTime = timestampToId[time];
  151. let shapes = [];
  152. if (shapesAtTime != undefined) {
  153. for (var i = 0; i < shapesAtTime.length; i++) {
  154. let id = shapesAtTime[i];
  155. let shape = getSVGFile().getElementById(id);
  156. // If there is actually a new shape to be displayed
  157. if (shape !== null) {
  158. // Get actual shape tag for this specific time of playback
  159. shape = shape.getAttribute("shape");
  160. shapes.push(shape);
  161. }
  162. }
  163. }
  164. return shapes;
  165. };
  166. function getViewboxAtTime(time) {
  167. let currentTime = parseFloat(time);
  168. let showDeskshare = getDeskshareAtTime(time);
  169. for (var key in vboxValues) {
  170. if (vboxValues.hasOwnProperty(key)) {
  171. var keyArray = key.split(",");
  172. if (keyArray[1] == "end") {
  173. return showDeskshare ? adaptViewBoxToDeskshare(time) : vboxValues[key];
  174. } else if ((parseFloat(keyArray[0]) <= currentTime) && (parseFloat(keyArray[1]) >= currentTime)) {
  175. return showDeskshare ? adaptViewBoxToDeskshare(time) : vboxValues[key];
  176. }
  177. }
  178. }
  179. };
  180. function setSlideAspect(time, imageWidth, imageHeight) {
  181. let showDeskshare = getDeskshareAtTime(time);
  182. let aspectAtTime = getAspectAtTime(time);
  183. if (aspectAtTime != undefined && aspectAtTime != 0 && !showDeskshare) {
  184. currentSlideAspect = aspectAtTime;
  185. } else {
  186. currentSlideAspect = parseFloat((imageWidth / imageHeight));
  187. }
  188. };
  189. function getAspectAtTime(time) {
  190. let currentTime = parseFloat(time);
  191. for (var key in slideAspectValues) {
  192. if (slideAspectValues.hasOwnProperty(key)) {
  193. let keyArray = key.split(",");
  194. if (keyArray[1] == "end") {
  195. return slideAspectValues[key];
  196. } else if ((parseFloat(keyArray[0]) <= currentTime) && (parseFloat(keyArray[1]) >= currentTime)) {
  197. return slideAspectValues[key];
  198. }
  199. }
  200. }
  201. };
  202. function getCursorAtTime(time) {
  203. let coords = cursorValues[time];
  204. if (coords) return coords.split(' ');
  205. };
  206. function removeSlideChangeAttribute() {
  207. $('#video').removeAttr('slide-change');
  208. Popcorn('#video').unlisten(Popcorn.play, 'removeSlideChangeAttribute');
  209. };
  210. function getDeskshareAtTime(time) {
  211. let show = false;
  212. if (hasDeskshare) {
  213. for (var m = 0; m < deskshareEvents.length; m++) {
  214. let startTimestamp = deskshareEvents[m][0];
  215. let stopTimestamp = deskshareEvents[m][1];
  216. if (time >= startTimestamp && time <= stopTimestamp)
  217. show = true;
  218. }
  219. }
  220. return show;
  221. };
  222. function getDeskshareDimensionAtTime(time) {
  223. let startTimestamp = 0.0;
  224. let stopTimestamp = 0.0;
  225. let width = deskshareWidth;
  226. let height = deskshareHeight;
  227. if (hasDeskshare) {
  228. for (var m = 0; m < deskshareEvents.length; m++) {
  229. startTimestamp = deskshareEvents[m][0];
  230. stopTimestamp = deskshareEvents[m][1];
  231. if (time >= startTimestamp && time <= stopTimestamp) {
  232. width = deskshareEvents[m][2];
  233. height = deskshareEvents[m][3];
  234. break;
  235. }
  236. }
  237. }
  238. return {width: width, height: height};
  239. };
  240. function handlePresentationAreaContent(time) {
  241. if (time >= meetingDuration) return;
  242. let showDeskshare = getDeskshareAtTime(time);
  243. if (showDeskshare) {
  244. if (!isDeskshareActive) {
  245. logger.info("==Showing deskshare");
  246. isDeskshareActive = true;
  247. document.getElementById("deskshare-video").style.visibility = "visible";
  248. $('#slide').addClass('no-background');
  249. }
  250. resizeDeskshare();
  251. } else {
  252. if (isDeskshareActive) {
  253. logger.info("==Hiding deskshare");
  254. document.getElementById("deskshare-video").style.visibility = "hidden";
  255. $('#slide').removeClass('no-background');
  256. isDeskshareActive = false;
  257. }
  258. resizeSlide();
  259. }
  260. };
  261. function startLoadingBar() {
  262. logger.info("==Hide playback content");
  263. $("#playback-content").css('visibility', 'hidden');
  264. Pace.once('done', function() {
  265. // This is a hack to handle data from storage services
  266. function checkPlaybackLoaded() {
  267. if (firstLoad) {
  268. setTimeout(checkPlaybackLoaded, mediaCheckInterval);
  269. } else {
  270. logger.info("==Loading done");
  271. onLoadComplete(true);
  272. resizeComponents();
  273. }
  274. }
  275. checkPlaybackLoaded();
  276. });
  277. Pace.start();
  278. showLoadingMessage();
  279. };
  280. function onLoadComplete(success) {
  281. if (success) {
  282. document.title = "Recording Playback";
  283. hideLoadingMessage();
  284. logger.info("==Show playback content");
  285. $("#playback-content").css('visibility', 'visible');
  286. } else {
  287. document.title = "Error";
  288. Pace.off('done');
  289. Pace.stop();
  290. showLoadingErrorMessage();
  291. }
  292. };
  293. function showLoadingErrorMessage() {
  294. document.getElementById("load-msg").innerHTML = "Recording not found";
  295. $("#loading").css('visibility', 'visible');
  296. };
  297. function showLoadingMessage() {
  298. document.getElementById("load-img").classList.add('animate');
  299. $("#loading").css('visibility', 'visible');
  300. };
  301. function hideLoadingMessage() {
  302. $("#loading").css('visibility', 'hidden');
  303. $("#loading").css('height','0');
  304. };
  305. // Find the key in the timestampToId object of the last entry at or before the provided timestamp
  306. function timestampToIdLookup(t) {
  307. "use strict";
  308. t = (t * 10) | 0; // keys are in deciseconds as integers
  309. var minIndex = 0;
  310. var maxIndex = timestampToIdKeys.length - 1;
  311. var curIndex;
  312. var curElement;
  313. // I can't think of any better way to do this than just binary search.
  314. while (minIndex <= maxIndex) {
  315. curIndex = (minIndex + maxIndex) / 2 | 0;
  316. curElement = timestampToIdKeys[curIndex];
  317. if (curElement < t) {
  318. minIndex = curIndex + 1;
  319. } else if (curElement > t) {
  320. maxIndex = curIndex - 1;
  321. } else {
  322. return curElement;
  323. }
  324. }
  325. return timestampToIdKeys[maxIndex];
  326. }
  327. function get_shapes_in_time(t, shapes) {
  328. "use strict";
  329. var timestampToIdIndex = timestampToIdLookup(t);
  330. var shapes_in_time = timestampToId[timestampToIdIndex];
  331. if (shapes_in_time != undefined) {
  332. var shape = null;
  333. for (var i = 0; i < shapes_in_time.length; i++) {
  334. var s = shapes_in_time[i];
  335. shapes[s.shape] = s.id;
  336. }
  337. }
  338. return shapes;
  339. }
  340. function runPopcorn() {
  341. logger.info("==Running popcorn");
  342. var p = new Popcorn("#video");
  343. p.code({
  344. start: 0,
  345. end: p.duration(),
  346. onFrame: function(options) {
  347. "use strict";
  348. var currentTime = p.currentTime();
  349. if ((!p.paused() || p.seeking()) && (Math.abs(currentTime - lastFrameTime) >= 0.1)) {
  350. lastFrameTime = currentTime;
  351. // Get the time and round to 1 decimal place
  352. var t = currentTime.toFixed(1);
  353. // Create an object referencing the main versions of all the shapes
  354. var current_shapes = Object.create(mainShapeIds);
  355. // And update it with current state of currently being drawn shapes
  356. get_shapes_in_time(t, current_shapes);
  357. // Update shape visibility status
  358. for (var i = 0; i < shapesArray.length; i++) {
  359. var a_shape = shapesArray[i];
  360. var time = parseFloat(a_shape.getAttribute('timestamp'));
  361. var shapeId = a_shape.getAttribute('id');
  362. var shape_i = a_shape.getAttribute('shape');
  363. var undo = parseFloat(a_shape.getAttribute('undo'));
  364. let shape = getSVGFile().getElementById(shapesArray[i].getAttribute("id"));
  365. if (shape != null) {
  366. if (
  367. // It's not the current version of the shape
  368. (shapeId != current_shapes[shape_i]) ||
  369. // It's in the future
  370. (time > t) ||
  371. // It's in the past (undo or clear)
  372. ((undo != -1) && (undo < currentTime))) {
  373. shape.style.visibility = 'hidden';
  374. } else {
  375. shape.style.visibility = 'visible';
  376. }
  377. }
  378. }
  379. // Fetch the name of the image at this time
  380. let nextImageId = getImageAtTime(t);
  381. // Changing slide image
  382. if (currentImageId && (currentImageId !== nextImageId) && (nextImageId !== undefined)) {
  383. logger.debug("==Changing image", nextImageId);
  384. var img = getSVGFile().getElementById(currentImageId);
  385. var ni = getSVGFile().getElementById(nextImageId);
  386. if (img) {
  387. img.style.visibility = "hidden";
  388. }
  389. // Destroy old plain text
  390. document.getElementById("slideText").innerHTML = "";
  391. if (ni) {
  392. ni.style.visibility = "visible";
  393. }
  394. // Set new plain text
  395. document.getElementById("slideText").innerHTML = slidePlainText[nextImageId] + nextImageId;
  396. if ($("#accEnabled").is(':checked')) {
  397. // Pause the playback on slide change
  398. p.pause();
  399. $('#video').attr('slide-change', 'slide-change');
  400. p.listen(Popcorn.play, removeSlideChangeAttribute);
  401. }
  402. let currentCanvas = getCanvasFromImage(currentImageId);
  403. if (currentCanvas !== null) {
  404. currentCanvas.setAttribute("display", "none");
  405. }
  406. let nextCanvas = getCanvasFromImage(nextImageId);
  407. if ((nextCanvas !== undefined) && (nextCanvas != null)) {
  408. nextCanvas.setAttribute("display", "");
  409. }
  410. currentImageId = nextImageId;
  411. }
  412. let image = getSVGFile().getElementById(currentImageId);
  413. if (image) {
  414. var imageWidth = parseFloat(image.getAttribute("width"));
  415. var imageHeight = parseFloat(image.getAttribute("height"));
  416. setViewBox(t);
  417. setSlideAspect(t, imageWidth, imageHeight);
  418. let currentCursorVal = getCursorAtTime(t);
  419. if (currentCursorVal != null && currentCursorVal != undefined && !$('#slide').hasClass('no-background')) {
  420. var cursorX = parseFloat(currentCursorVal[0]);
  421. var cursorY = parseFloat(currentCursorVal[1]);
  422. if (cursorX >= 0 && cursorY >= 0) {
  423. showCursor(true);
  424. drawCursor(cursorX, cursorY);
  425. } else {
  426. showCursor(false);
  427. }
  428. }
  429. // Store the current slide
  430. currentImage = image;
  431. }
  432. handlePresentationAreaContent(t);
  433. }
  434. }
  435. });
  436. };
  437. function clearTransform() {
  438. logger.debug("==Cleaning canvas transformation");
  439. widthScale = 1;
  440. heightScale = 1;
  441. widthTranslate = 0;
  442. heightTranslate = 0;
  443. canvasTransformed = false;
  444. };
  445. function setDeskshareScale(originalVideoWidth, originalVideoHeight) {
  446. widthScale = originalVideoWidth / deskshareWidth;
  447. heightScale = originalVideoHeight / deskshareHeight;
  448. };
  449. function setDeskshareTranslate(originalVideoWidth, originalVideoHeight) {
  450. widthTranslate = (deskshareWidth - originalVideoWidth) / 2;
  451. heightTranslate = (deskshareHeight - originalVideoHeight) / 2;
  452. };
  453. // Deskshare viewBox has the information to transform the canvas to place it above the video
  454. function adaptViewBoxToDeskshare(time) {
  455. let dimension = getDeskshareDimensionAtTime(time);
  456. setDeskshareScale(dimension.width, dimension.height);
  457. setDeskshareTranslate(dimension.width, dimension.height);
  458. let viewBox = "0.0 0.0 " + deskshareWidth + " " + deskshareHeight;
  459. return viewBox;
  460. };
  461. function getCanvasFromImage(image) {
  462. let canvasId = "canvas" + image.substr(5);
  463. return getSVGFile().getElementById(canvasId);
  464. };
  465. function getDeskshareImage() {
  466. let images = getSVGFile().getElementsByTagName("image");
  467. for (var i = 0; i < images.length; i++) {
  468. let element = images[i];
  469. let id = element.getAttribute("id");
  470. let href = element.getAttribute("xlink:href");
  471. if (href != null && href.indexOf("deskshare") !=-1) {
  472. return id;
  473. }
  474. }
  475. return "image0";
  476. };
  477. // Transform canvas to fit the different deskshare video sizes
  478. function setTransform(time) {
  479. if (deskshareImage == null) {
  480. deskshareImage = getDeskshareImage();
  481. }
  482. if (getDeskshareAtTime(time)) {
  483. logger.debug("==Transforming annotation canvas");
  484. let canvas = getCanvasFromImage(deskshareImage);
  485. if (canvas !== null) {
  486. let scale = "scale(" + widthScale.toString() + ", " + heightScale.toString() + ")";
  487. let translate = "translate(" + widthTranslate.toString() + ", " + heightTranslate.toString() + ")";
  488. let transform = translate + " " + scale;
  489. canvas.setAttribute('transform', transform);
  490. canvasTransformed = true;
  491. }
  492. } else if (canvasTransformed) {
  493. clearTransform();
  494. }
  495. };
  496. function defineStartTime() {
  497. logger.info("==Defining start time");
  498. if (params.t === undefined)
  499. return 0;
  500. let extractNumber = /\d+/g;
  501. let extractUnit = /\D+/g;
  502. let startTime = 0;
  503. while (true) {
  504. let param1 = extractUnit.exec(params.t);
  505. let param2 = extractNumber.exec(params.t);
  506. if (param1 == null || param2 == null)
  507. break;
  508. let unit = String(param1).toLowerCase();
  509. let value = parseInt(String(param2));
  510. if (unit == "h")
  511. value *= 3600;
  512. else if (unit == "m")
  513. value *= 60;
  514. startTime += value;
  515. }
  516. logger.info("==Start time", startTime);
  517. return startTime;
  518. };
  519. function asyncRequest(method, url) {
  520. return new Promise(function (resolve, reject) {
  521. let xhr = new XMLHttpRequest();
  522. xhr.open(method, url);
  523. xhr.onload = function () {
  524. if (xhr.readyState !== xhr.DONE) {
  525. return;
  526. }
  527. if (xhr.status >= 200 && xhr.status < 300) {
  528. resolve(xhr);
  529. } else {
  530. reject({status: xhr.status, statusText: xhr.statusText});
  531. }
  532. };
  533. xhr.onerror = function () {
  534. reject({status: xhr.status, statusText: xhr.statusText});
  535. };
  536. xhr.send();
  537. });
  538. };
  539. function loadMetadata() {
  540. asyncRequest('GET', metadataXML).then(function (response) {
  541. processMetadataXML(response);
  542. }).catch(function(error) {
  543. logger.error("==Couldn't load metadata.xml", error);
  544. onLoadComplete(false);
  545. });
  546. };
  547. function processMetadataXML(response) {
  548. logger.info("==Processing metadata.xml");
  549. metadataXMLContent = response.responseXML;
  550. let metadata = metadataXMLContent.getElementsByTagName("meta");
  551. if (metadata.length > 0) {
  552. metadata = metadata[0];
  553. let meetingName = metadata.getElementsByTagName("meetingName");
  554. if (meetingName.length > 0) {
  555. $("#recording-title").text(meetingName[0].textContent);
  556. $("#recording-title").attr("title", meetingName[0].textContent);
  557. }
  558. }
  559. document.dispatchEvent(new CustomEvent('content-ready', {'detail': 'metadata'}));
  560. };
  561. function loadData() {
  562. asyncRequest('GET', shapesSVG).then(function (response) {
  563. processShapesSVG(response);
  564. }).catch(function(error) {
  565. logger.error("==Couldn't load shapes.svg", error);
  566. });
  567. asyncRequest('GET', panzoomsXML).then(function (response) {
  568. processPanzoomsXML(response);
  569. }).catch(function(error) {
  570. logger.error("==Couldn't load panzoom.xml", error);
  571. });
  572. asyncRequest('GET', cursorXML).then(function (response) {
  573. processCursorXML(response);
  574. }).catch(function(error) {
  575. logger.error("==Couldn't load cursor.xml", error);
  576. });
  577. asyncRequest('GET', deskshareXML).then(function (response) {
  578. processDeskshareXML(response);
  579. }).catch(function(error) {
  580. if (error.status == 404) {
  581. logger.warn("==Couldn't find deskshare.xml, assuming there's no deskshare");
  582. document.dispatchEvent(new CustomEvent('data-ready', {'detail': 'deskshare-xml'}));
  583. } else {
  584. logger.error("==Couldn't load deskshare.xml", error);
  585. }
  586. });
  587. };
  588. function processPanzoomsXML(response) {
  589. logger.info("==Processing panzooms.xml");
  590. let panelements = response.responseXML.getElementsByTagName("recording");
  591. let panZoomArray = panelements[0].getElementsByTagName("event");
  592. let viewBoxes = response.responseXML.getElementsByTagName("viewBox");
  593. let pzlen = panZoomArray.length;
  594. let secondVal;
  595. for (var k = 0;k < pzlen; k++) {
  596. if (panZoomArray[k+1] == undefined) {
  597. secondVal = "end";
  598. } else {
  599. secondVal = panZoomArray[k+1].getAttribute("timestamp");
  600. }
  601. vboxValues[[panZoomArray[k].getAttribute("timestamp"), secondVal]] = viewBoxes[k].childNodes[0].data;
  602. }
  603. document.dispatchEvent(new CustomEvent('data-ready', {'detail': 'panzoom'}));
  604. };
  605. function processCursorXML(response) {
  606. logger.info("==Processing cursor.xml");
  607. let curelements = response.responseXML.getElementsByTagName("recording");
  608. let cursorArray = curelements[0].getElementsByTagName("event");
  609. let coords = response.responseXML.getElementsByTagName("cursor");
  610. let clen = cursorArray.length;
  611. if (cursorArray.length != 0) cursorValues["0"] = "0 0";
  612. for (var m = 0; m < clen; m++) {
  613. cursorValues[cursorArray[m].getAttribute("timestamp")] = coords[m].childNodes[0].data;
  614. }
  615. document.dispatchEvent(new CustomEvent('data-ready', {'detail': 'cursor'}));
  616. };
  617. function processShapesSVG(response) {
  618. logger.info("==Processing shapes.svg");
  619. let svgobj = document.createElement('div');
  620. $(svgobj).css('height', '100%');
  621. $(svgobj).css('width', '100%');
  622. // for some reason, innerHTML was dropping part of the svg file, while it works
  623. // fine when using Ajax html method
  624. // svgobj.innerHTML = response.responseText;
  625. $(svgobj).html(response.responseText);
  626. // Update the links inside of the presentation to include the full URL
  627. $(svgobj).find('image').each(function() {
  628. let href = $(this).attr('xlink:href');
  629. href = url + '/' + href;
  630. $(this).attr('xlink:href', href);
  631. });
  632. // Clear the style, we're setting it via css
  633. $(svgobj).find('svg').attr('style', '');
  634. document.getElementById('slide').appendChild(svgobj);
  635. $("#svgfile").css('height', '100%');
  636. $("#svgfile").css('width', '100%');
  637. shapesSVGContent = response.responseXML;
  638. // Getting all the event tags
  639. let shapeelement = shapesSVGContent.getElementsByTagName("svg")[0];
  640. // Get an array of the elements for each "shape" in the drawing
  641. shapesArray = shapesSVGContent.querySelectorAll('g[class="shape"]');
  642. // To assist in finding the version of a shape shown at a particular time
  643. // (while being drawn, during updates), provide a lookup from time to id
  644. // Also save the id of the last version of each shape as its main id
  645. for (var j = 0; j < shapesArray.length; j++) {
  646. shape = shapesArray[j];
  647. var id = shape.getAttribute('id');
  648. var shape_i = shape.getAttribute('shape');
  649. var time = (parseFloat(shape.getAttribute('timestamp')) * 10) | 0;
  650. if (timestampToId[time] == undefined) {
  651. timestampToId[time] = [];
  652. timestampToIdKeys.push(time);
  653. }
  654. timestampToId[time].push({id: id, shape: shape_i})
  655. mainShapeIds[shape_i] = id;
  656. }
  657. asyncRequest('GET', textJSON).then(function (response) {
  658. processTextJSON(response);
  659. processSlideAspectTimes();
  660. document.dispatchEvent(new CustomEvent('data-ready', {'detail': 'text'}));
  661. }).catch(function(error) {
  662. logger.warn("==Couldn't load presentation_text.json", error);
  663. processTextFallback();
  664. processSlideAspectTimes();
  665. document.dispatchEvent(new CustomEvent('data-ready', {'detail': 'text'}));
  666. });
  667. document.dispatchEvent(new CustomEvent('data-ready', {'detail': 'svg'}));
  668. };
  669. function processDeskshareXML(response) {
  670. logger.info("==Processing deskshare.xml");
  671. let deskshareElements = response.responseXML.getElementsByTagName("recording");
  672. let deskshareArray = deskshareElements[0].getElementsByTagName("event");
  673. if (deskshareArray != null && deskshareArray.length != 0) {
  674. for (var m = 0; m < deskshareArray.length; m++) {
  675. let deskshareEvent = [
  676. parseFloat(deskshareArray[m].getAttribute("start_timestamp")),
  677. parseFloat(deskshareArray[m].getAttribute("stop_timestamp")),
  678. parseFloat(deskshareArray[m].getAttribute("video_width")),
  679. parseFloat(deskshareArray[m].getAttribute("video_height"))
  680. ];
  681. deskshareEvents[m] = deskshareEvent;
  682. }
  683. }
  684. document.dispatchEvent(new CustomEvent('data-ready', {'detail': 'deskshare-xml'}));
  685. };
  686. function checkMediaURL(url) {
  687. var xhr = new XMLHttpRequest();
  688. xhr.open('HEAD', url, true);
  689. xhr.onreadystatechange = function (e) {
  690. if (xhr.readyState === 4) {
  691. if (xhr.status == 200 || xhr.status == 206) {
  692. var pathname = new URL(xhr.responseURL).pathname;
  693. if (pathname.endsWith("webcams.webm") || pathname.endsWith("webcams.mp4")) {
  694. logger.info("==Found video", pathname);
  695. hasVideo = true;
  696. } else if (pathname.endsWith("deskshare.webm") || pathname.endsWith("deskshare.mp4")) {
  697. logger.info("==Found deskshare", pathname);
  698. hasDeskshare = true;
  699. }
  700. }
  701. mediasToCheck--;
  702. if (mediasToCheck == 0) {
  703. document.dispatchEvent(new CustomEvent('content-ready', {'detail': 'medias-checked'}));
  704. }
  705. }
  706. };
  707. xhr.send();
  708. };
  709. function checkMedias() {
  710. for (var i = 0 ; i < mediasURL.length ; i++) {
  711. checkMediaURL(url + mediasURL[i]);
  712. }
  713. };
  714. function initPopcorn() {
  715. firstLoad = false;
  716. generateThumbnails();
  717. var startTime = defineStartTime();
  718. Popcorn("#video").currentTime(startTime);
  719. if (hasDeskshare)
  720. Popcorn("#deskshare-video").currentTime(startTime);
  721. // Popcorn documentation suggests this way to get the duration,
  722. // since this information does not come with 'loadedmetadata' event.
  723. Popcorn("#video").cue(2, function() {
  724. meetingDuration = parseFloat(Popcorn("#video").duration().toFixed(1));
  725. logger.info("==Meeting duration (seconds)", meetingDuration);
  726. });
  727. };
  728. function processTextJSON(response) {
  729. logger.info("==Processing presentation_text.json");
  730. let slidesText = JSON.parse(response.responseText);
  731. let shapeElements = shapesSVGContent.getElementsByTagName("svg");
  732. let images = shapeElements[0].getElementsByClassName("slide");
  733. for (var m = 0; m < images.length; m++) {
  734. let len = images[m].getAttribute("in").split(" ").length;
  735. for (var n = 0; n < len; n++) {
  736. imageAtTime[[images[m].getAttribute("in").split(" ")[n], images[m].getAttribute("out").split(" ")[n]]] = images[m].getAttribute("id");
  737. }
  738. // The logo at the start has no text attribute
  739. if (images[m].getAttribute("text")) {
  740. // Have to save the value because images array might go out of scope
  741. var imgId = images[m].getAttribute("id");
  742. // Text format: presentation/PRESENTATION_ID/textfiles/SLIDE_ID.txt
  743. var imgTxt = images[m].getAttribute("text").split("/");
  744. var presentationId = imgTxt[1];
  745. var slideId = imgTxt[3].split(".")[0];
  746. if (slidesText[presentationId] && slidesText[presentationId][slideId]) {
  747. slidePlainText[imgId] = $('<div/>').text(slidesText[presentationId][slideId]).html();
  748. } else {
  749. slidePlainText[imgId] = $('<div/>')
  750. }
  751. }
  752. }
  753. };
  754. function processTextFallback() {
  755. logger.info("==Processing slides.txt");
  756. var textPathToImgId = {};
  757. let shapeElements = shapesSVGContent.getElementsByTagName("svg");
  758. let images = shapeElements[0].getElementsByClassName("slide");
  759. for (var m = 0; m < images.length; m++) {
  760. let len = images[m].getAttribute("in").split(" ").length;
  761. for (var n = 0; n < len; n++) {
  762. imageAtTime[[images[m].getAttribute("in").split(" ")[n], images[m].getAttribute("out").split(" ")[n]]] = images[m].getAttribute("id");
  763. }
  764. // The logo at the start has no text attribute
  765. if (images[m].getAttribute("text")) {
  766. var textPath = url + "/" + images[m].getAttribute("text");
  767. textPathToImgId[textPath] = images[m].getAttribute("id");
  768. getTextAsync(textPath,textPathToImgId);
  769. }
  770. }
  771. };
  772. function getTextAsync(textPath, textPathToImgId) {
  773. let xhr = new XMLHttpRequest();
  774. xhr.onreadystatechange = function() {
  775. if (xhr.readyState === 4) {
  776. if (xhr.status == 200 || xhr.status == 206) {
  777. var pathname = new URL(xhr.responseURL).pathname;
  778. var imgId = textPathToImgId[pathname];
  779. slidePlainText[imgId] = $('<div/>').text(xhr.responseText).html();
  780. }
  781. }
  782. };
  783. xhr.open("GET", textPath, true);
  784. xhr.send(null);
  785. };
  786. function processSlideAspectTimes() {
  787. var lastAspectValue = 0;
  788. for (var key in vboxValues) {
  789. if (vboxValues.hasOwnProperty(key)) {
  790. var startTimestamp = key.split(",")[0];
  791. var stopTimestamp = key.split(",")[1];
  792. var vboxWidth = parseFloat(vboxValues[key].split(" ")[2]);
  793. var vboxHeight = parseFloat(vboxValues[key].split(" ")[3]);
  794. var aspectValue = processAspectValue(vboxWidth, vboxHeight, startTimestamp, lastAspectValue);
  795. slideAspectValues[[startTimestamp, stopTimestamp]] = aspectValue;
  796. lastAspectValue = aspectValue;
  797. }
  798. }
  799. };
  800. function processAspectValue(vboxWidth, vboxHeight, time, lastAspectValue) {
  801. logger.debug("==Processing presentation aspect");
  802. let imageId;
  803. if (time == "0.0") {
  804. // A little hack 'cause function getImageAtTime with time = 0.0 returns the background image...
  805. // We need the first slide instead
  806. logger.debug("==First image");
  807. imageId = "image1";
  808. } else {
  809. imageId = getImageAtTime(time);
  810. logger.debug("==Image", imageId);
  811. }
  812. if (imageId !== undefined) {
  813. let image = getSVGFile().getElementById(imageId);
  814. if (image) {
  815. if (getDeskshareAtTime(parseFloat(time))) {
  816. return lastAspectValue;
  817. }
  818. let imageWidth = parseFloat(image.getAttribute("width"));
  819. let imageHeight = parseFloat(image.getAttribute("height"));
  820. // Fit-to-width: returning vbox aspect
  821. if (vboxWidth == imageWidth && vboxHeight < imageHeight) {
  822. logger.debug("==Fit-to-width aspect detected");
  823. return parseFloat(vboxWidth/vboxHeight);
  824. } else if (vboxWidth == imageWidth && vboxHeight == imageHeight) {
  825. // Fit-to-page: returning image aspect
  826. logger.debug("==Fit-to-page aspect detected");
  827. return parseFloat(imageWidth/imageHeight);
  828. } else {
  829. // If it's not fit-to-width neither fit-to-page we return the previous aspect
  830. return lastAspectValue;
  831. }
  832. } else {
  833. logger.error("==No image for id", imageId);
  834. return lastAspectValue;
  835. }
  836. } else {
  837. logger.error("==Image undefined");
  838. return lastAspectValue;
  839. }
  840. };
  841. // A small hack to hide the cursor when resizing the window, so it's not
  842. // misplaced while the window is being resized
  843. window.onresize = function(event) {
  844. showCursor(false);
  845. resizeComponents();
  846. resizeSlide();
  847. resizeDeskshare();
  848. };
  849. // Resize the container that has the slides (and whiteboard) to be the maximum
  850. // size possible but still maintaining the aspect ratio of the slides.
  851. //
  852. // This is done here only because of pan and zoom. Pan/zoom is done by modifiyng
  853. // the svg's viewBox, and that requires the container that has the svg to be the
  854. // exact size we want to display the slides so that parts of the svg that are outside
  855. // of its parent's area are hidden. If we let the svg occupy all presentation area
  856. // (letting the svg do the image resizing), the slides will move and zoom around the
  857. // entire area when pan/zoom is activated, usually displaying more of the slide
  858. // than we want to (i.e. more than was displayed in the conference).
  859. function resizeSlide() {
  860. logger.debug("==Resizing slide");
  861. if (currentImage) {
  862. let $slide = $("#slide");
  863. let maxWidth = currentSlideAspect * $slide.parent().outerHeight();
  864. $slide.css("max-width", maxWidth);
  865. let maxHeight = $slide.parent().width() / currentSlideAspect;
  866. $slide.css("max-height", maxHeight);
  867. logger.debug("==Size", {maxWidth, maxHeight});
  868. } else {
  869. logger.debug("==Slide not ready");
  870. }
  871. };
  872. function resizeDeskshare() {
  873. if (!hasDeskshare) return;
  874. logger.debug("==Resizing deskshare");
  875. let deskshareVideo = document.getElementById("deskshare-video");
  876. let $deskshareVideo = $("#deskshare-video");
  877. // Deskshare may exist and not be initialized yet
  878. if ($deskshareVideo && deskshareVideo) {
  879. let videoWidth = parseInt(deskshareVideo.videoWidth, 10);
  880. let videoHeight = parseInt(deskshareVideo.videoHeight, 10);
  881. let aspectRatio = videoWidth/videoHeight;
  882. let maxWidth = aspectRatio * $deskshareVideo.parent().outerHeight();
  883. $deskshareVideo.css("max-width", maxWidth);
  884. let maxHeight = $deskshareVideo.parent().width() / aspectRatio;
  885. $deskshareVideo.css("max-height", maxHeight);
  886. logger.debug("==Size", {maxWidth, maxHeight});
  887. } else {
  888. logger.debug("==Deskshare not ready");
  889. }
  890. };
  891. function getSVGFile() {
  892. return $('svg')[0];
  893. };
  894. function linkChatToMedia() {
  895. logger.info("==Linking chat to media");
  896. // Popcorn lib uses the 'mediaLoaded' event to link the chat timeline to the video
  897. var event = document.createEvent("HTMLEvents");
  898. event.initEvent("mediaLoaded", true, true);
  899. document.dispatchEvent(event);
  900. }
  901. document.addEventListener('media-ready', function(event) {
  902. logger.debug("==Media ready", event.detail);
  903. if (mediaReady) return;
  904. switch(event.detail) {
  905. case 'video':
  906. videoReady = true;
  907. break;
  908. case 'captions':
  909. captionsReady = true;
  910. break;
  911. case 'audio':
  912. audioReady = true;
  913. break;
  914. case 'deskshare':
  915. deskshareReady = true;
  916. break;
  917. default:
  918. logger.warn("==Unhandled media-ready event", event.detail);
  919. }
  920. if ((audioReady || (videoReady && captionsReady)) && deskshareReady) {
  921. logger.info("==All medias can be played");
  922. setMediaSync();
  923. linkChatToMedia();
  924. document.dispatchEvent(new CustomEvent('playback-ready', {'detail': 'media'}));
  925. }
  926. }, false);
  927. document.addEventListener('data-ready', function(event) {
  928. logger.debug("==Data ready", event.detail);
  929. if (dataReady) return;
  930. switch(event.detail) {
  931. case 'svg':
  932. svgReady = true;
  933. break;
  934. case 'text':
  935. textReady = true;
  936. break;
  937. case 'panzoom':
  938. panzoomReady = true;
  939. break;
  940. case 'cursor':
  941. cursorReady = true;
  942. break;
  943. case 'deskshare-xml':
  944. deskshareXMLReady = true;
  945. break;
  946. default:
  947. logger.warn("==Unhandled data-ready event", event.detail);
  948. }
  949. if (svgReady && textReady && panzoomReady && cursorReady && deskshareXMLReady) {
  950. document.dispatchEvent(new CustomEvent('playback-ready', {'detail': 'data'}));
  951. }
  952. }, false);