/** * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ * * Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below). * * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free Software * Foundation; either version 3.0 of the License, or (at your option) any later * version. * * BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License along * with BigBlueButton; if not, see . * */ const logger = window.Logger || console; const params = getURLParameters(); const meetingId = params.meetingId; const url = getFullURL(); const metadataXML = url + '/metadata.xml'; const shapesSVG = url + '/shapes.svg'; const panzoomsXML = url + '/panzooms.xml'; const cursorXML = url + '/cursor.xml'; const deskshareXML = url + '/deskshare.xml'; const textJSON = url + '/presentation_text.json'; const captionsJSON = url + '/captions.json'; const chatXML = url + '/slides_new.xml'; const mediasURL = [ '/video/webcams.webm', '/video/webcams.mp4', '/deskshare/deskshare.webm', '/deskshare/deskshare.mp4' ]; const deskshareWidth = 1280.0; const deskshareHeight = 720.0; const mobileTimeout = 10 * 1000; // 10 seconds const mediaCheckInterval = 250; var lastFrameTime = 0.0; var firstLoad = true; var meetingDuration; var mediasToCheck = mediasURL.length; // Metadata var metadataXMLContent = null; // Media events var videoReady = false; var audioReady = false; var deskshareReady = false; var captionsReady = false; // Data events var svgReady = false; var textReady = false; var panzoomReady = false; var cursorReady = false; var deskshareXMLReady = false; // Shapes var timestampToId = {}; var timestampToIdKeys = []; var mainShapeIds = {}; var currentImage = null; var currentImageId = "image0"; var shapesArray = []; var timestampToId = {}; var shapesSVGContent = null; var slideAspectValues = {}; var currentSlideAspect = 0; var imageAtTime = {}; var slidePlainText = {}; //holds slide plain text for retrieval // Cursor var cursorShownAt = 0; var cursorValues = {}; // Panzoom var vboxValues = {}; // Deskshare var deskshareEvents = []; var deskshareImage = null; var widthScale = 1; var heightScale = 1; var widthTranslate = 0; var heightTranslate = 0; var isDeskshareActive = false; var canvasTransformed = false; function getURLParameters() { logger.info("==Getting URL params"); let map = {}; window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (m, key, value) {map[key] = value;}); return map; }; function getFullURL() { let url = '/presentation/' + meetingId; return url; }; // https://stackoverflow.com/a/60553965 function detectLyingiOS13iPad() { var userAgent = navigator.userAgent || navigator.vendor || window.opera; // Lying iOS13 iPad if (userAgent.match(/Macintosh/i) !== null) { // need to distinguish between Macbook and iPad var canvas = document.createElement("canvas"); if (canvas !== null) { var context = canvas.getContext("webgl") || canvas.getContext("experimental-webgl"); if (context) { var info = context.getExtension("WEBGL_debug_renderer_info"); if (info) { var renderer = context.getParameter(info.UNMASKED_RENDERER_WEBGL); if (renderer.indexOf("Apple") !== -1) return true; } } } } return false; } // http://stackoverflow.com/a/11381730 function mobileAndTabletCheck() { let check = false; (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); return check || detectLyingiOS13iPad();; } // Draw the cursor at a specific point function drawCursor(scaledX, scaledY) { let containerObj = $("#slide > div"); // The offsets of the container that has the image inside it let imageOffsetX = containerObj.offset().left; let imageOffsetY = containerObj.offset().top; // Position of the cursor relative to the container let cursorXInImage = scaledX * containerObj.width(); let cursorYInImage = scaledY * containerObj.height(); // Absolute position of the cursor in the page let cursorLeft = parseInt(imageOffsetX + cursorXInImage, 10); let cursorTop = parseInt(imageOffsetY + cursorYInImage, 10); if (cursorLeft < 0) { cursorLeft = 0; } if (cursorTop < 0) { cursorTop = 0; } let cursorStyle = document.getElementById("cursor").style; cursorStyle.left = cursorLeft + "px"; cursorStyle.top = cursorTop + "px"; } function showCursor(show) { if (show) { document.getElementById("cursor").style.visibility = 'visible'; } else { document.getElementById("cursor").style.visibility = 'hidden'; } }; function setViewBox(time) { let vboxVal = getViewboxAtTime(time); if (vboxVal !== undefined) { setTransform(time); let svgfile = getSVGFile(); svgfile.setAttribute('viewBox', vboxVal); } }; function getImageAtTime(time) { let currentTime = parseFloat(time); for (var key in imageAtTime) { if (imageAtTime.hasOwnProperty(key)) { let keyArray = key.split(","); if ((parseFloat(keyArray[0]) <= currentTime) && (parseFloat(keyArray[1]) > currentTime)) { return imageAtTime[key]; } } } }; function getShapesAtTime(time) { let shapesAtTime = timestampToId[time]; let shapes = []; if (shapesAtTime != undefined) { for (var i = 0; i < shapesAtTime.length; i++) { let id = shapesAtTime[i]; let shape = getSVGFile().getElementById(id); // If there is actually a new shape to be displayed if (shape !== null) { // Get actual shape tag for this specific time of playback shape = shape.getAttribute("shape"); shapes.push(shape); } } } return shapes; }; function getViewboxAtTime(time) { let currentTime = parseFloat(time); let showDeskshare = getDeskshareAtTime(time); for (var key in vboxValues) { if (vboxValues.hasOwnProperty(key)) { var keyArray = key.split(","); if (keyArray[1] == "end") { return showDeskshare ? adaptViewBoxToDeskshare(time) : vboxValues[key]; } else if ((parseFloat(keyArray[0]) <= currentTime) && (parseFloat(keyArray[1]) >= currentTime)) { return showDeskshare ? adaptViewBoxToDeskshare(time) : vboxValues[key]; } } } }; function setSlideAspect(time, imageWidth, imageHeight) { let showDeskshare = getDeskshareAtTime(time); let aspectAtTime = getAspectAtTime(time); if (aspectAtTime != undefined && aspectAtTime != 0 && !showDeskshare) { currentSlideAspect = aspectAtTime; } else { currentSlideAspect = parseFloat((imageWidth / imageHeight)); } }; function getAspectAtTime(time) { let currentTime = parseFloat(time); for (var key in slideAspectValues) { if (slideAspectValues.hasOwnProperty(key)) { let keyArray = key.split(","); if (keyArray[1] == "end") { return slideAspectValues[key]; } else if ((parseFloat(keyArray[0]) <= currentTime) && (parseFloat(keyArray[1]) >= currentTime)) { return slideAspectValues[key]; } } } }; function getCursorAtTime(time) { let coords = cursorValues[time]; if (coords) return coords.split(' '); }; function removeSlideChangeAttribute() { $('#video').removeAttr('slide-change'); Popcorn('#video').unlisten(Popcorn.play, 'removeSlideChangeAttribute'); }; function getDeskshareAtTime(time) { let show = false; if (hasDeskshare) { for (var m = 0; m < deskshareEvents.length; m++) { let startTimestamp = deskshareEvents[m][0]; let stopTimestamp = deskshareEvents[m][1]; if (time >= startTimestamp && time <= stopTimestamp) show = true; } } return show; }; function getDeskshareDimensionAtTime(time) { let startTimestamp = 0.0; let stopTimestamp = 0.0; let width = deskshareWidth; let height = deskshareHeight; if (hasDeskshare) { for (var m = 0; m < deskshareEvents.length; m++) { startTimestamp = deskshareEvents[m][0]; stopTimestamp = deskshareEvents[m][1]; if (time >= startTimestamp && time <= stopTimestamp) { width = deskshareEvents[m][2]; height = deskshareEvents[m][3]; break; } } } return {width: width, height: height}; }; function handlePresentationAreaContent(time) { if (time >= meetingDuration) return; let showDeskshare = getDeskshareAtTime(time); if (showDeskshare) { if (!isDeskshareActive) { logger.info("==Showing deskshare"); isDeskshareActive = true; document.getElementById("deskshare-video").style.visibility = "visible"; $('#slide').addClass('no-background'); } resizeDeskshare(); } else { if (isDeskshareActive) { logger.info("==Hiding deskshare"); document.getElementById("deskshare-video").style.visibility = "hidden"; $('#slide').removeClass('no-background'); isDeskshareActive = false; } resizeSlide(); } }; function startLoadingBar() { logger.info("==Hide playback content"); $("#playback-content").css('visibility', 'hidden'); Pace.once('done', function() { // This is a hack to handle data from storage services function checkPlaybackLoaded() { if (firstLoad) { setTimeout(checkPlaybackLoaded, mediaCheckInterval); } else { logger.info("==Loading done"); onLoadComplete(true); resizeComponents(); } } checkPlaybackLoaded(); }); Pace.start(); showLoadingMessage(); }; function onLoadComplete(success) { if (success) { document.title = "Recording Playback"; hideLoadingMessage(); logger.info("==Show playback content"); $("#playback-content").css('visibility', 'visible'); } else { document.title = "Error"; Pace.off('done'); Pace.stop(); showLoadingErrorMessage(); } }; function showLoadingErrorMessage() { document.getElementById("load-msg").innerHTML = "Recording not found"; $("#loading").css('visibility', 'visible'); }; function showLoadingMessage() { document.getElementById("load-img").classList.add('animate'); $("#loading").css('visibility', 'visible'); }; function hideLoadingMessage() { $("#loading").css('visibility', 'hidden'); $("#loading").css('height','0'); }; // Find the key in the timestampToId object of the last entry at or before the provided timestamp function timestampToIdLookup(t) { "use strict"; t = (t * 10) | 0; // keys are in deciseconds as integers var minIndex = 0; var maxIndex = timestampToIdKeys.length - 1; var curIndex; var curElement; // I can't think of any better way to do this than just binary search. while (minIndex <= maxIndex) { curIndex = (minIndex + maxIndex) / 2 | 0; curElement = timestampToIdKeys[curIndex]; if (curElement < t) { minIndex = curIndex + 1; } else if (curElement > t) { maxIndex = curIndex - 1; } else { return curElement; } } return timestampToIdKeys[maxIndex]; } function get_shapes_in_time(t, shapes) { "use strict"; var timestampToIdIndex = timestampToIdLookup(t); var shapes_in_time = timestampToId[timestampToIdIndex]; if (shapes_in_time != undefined) { var shape = null; for (var i = 0; i < shapes_in_time.length; i++) { var s = shapes_in_time[i]; shapes[s.shape] = s.id; } } return shapes; } function runPopcorn() { logger.info("==Running popcorn"); var p = new Popcorn("#video"); p.code({ start: 0, end: p.duration(), onFrame: function(options) { "use strict"; var currentTime = p.currentTime(); if ((!p.paused() || p.seeking()) && (Math.abs(currentTime - lastFrameTime) >= 0.1)) { lastFrameTime = currentTime; // Get the time and round to 1 decimal place var t = currentTime.toFixed(1); // Create an object referencing the main versions of all the shapes var current_shapes = Object.create(mainShapeIds); // And update it with current state of currently being drawn shapes get_shapes_in_time(t, current_shapes); // Update shape visibility status for (var i = 0; i < shapesArray.length; i++) { var a_shape = shapesArray[i]; var time = parseFloat(a_shape.getAttribute('timestamp')); var shapeId = a_shape.getAttribute('id'); var shape_i = a_shape.getAttribute('shape'); var undo = parseFloat(a_shape.getAttribute('undo')); let shape = getSVGFile().getElementById(shapesArray[i].getAttribute("id")); if (shape != null) { if ( // It's not the current version of the shape (shapeId != current_shapes[shape_i]) || // It's in the future (time > t) || // It's in the past (undo or clear) ((undo != -1) && (undo < currentTime))) { shape.style.visibility = 'hidden'; } else { shape.style.visibility = 'visible'; } } } // Fetch the name of the image at this time let nextImageId = getImageAtTime(t); // Changing slide image if (currentImageId && (currentImageId !== nextImageId) && (nextImageId !== undefined)) { logger.debug("==Changing image", nextImageId); var img = getSVGFile().getElementById(currentImageId); var ni = getSVGFile().getElementById(nextImageId); if (img) { img.style.visibility = "hidden"; } // Destroy old plain text document.getElementById("slideText").innerHTML = ""; if (ni) { ni.style.visibility = "visible"; } // Set new plain text document.getElementById("slideText").innerHTML = slidePlainText[nextImageId] + nextImageId; if ($("#accEnabled").is(':checked')) { // Pause the playback on slide change p.pause(); $('#video').attr('slide-change', 'slide-change'); p.listen(Popcorn.play, removeSlideChangeAttribute); } let currentCanvas = getCanvasFromImage(currentImageId); if (currentCanvas !== null) { currentCanvas.setAttribute("display", "none"); } let nextCanvas = getCanvasFromImage(nextImageId); if ((nextCanvas !== undefined) && (nextCanvas != null)) { nextCanvas.setAttribute("display", ""); } currentImageId = nextImageId; } let image = getSVGFile().getElementById(currentImageId); if (image) { var imageWidth = parseFloat(image.getAttribute("width")); var imageHeight = parseFloat(image.getAttribute("height")); setViewBox(t); setSlideAspect(t, imageWidth, imageHeight); let currentCursorVal = getCursorAtTime(t); if (currentCursorVal != null && currentCursorVal != undefined && !$('#slide').hasClass('no-background')) { var cursorX = parseFloat(currentCursorVal[0]); var cursorY = parseFloat(currentCursorVal[1]); if (cursorX >= 0 && cursorY >= 0) { showCursor(true); drawCursor(cursorX, cursorY); } else { showCursor(false); } } // Store the current slide currentImage = image; } handlePresentationAreaContent(t); } } }); }; function clearTransform() { logger.debug("==Cleaning canvas transformation"); widthScale = 1; heightScale = 1; widthTranslate = 0; heightTranslate = 0; canvasTransformed = false; }; function setDeskshareScale(originalVideoWidth, originalVideoHeight) { widthScale = originalVideoWidth / deskshareWidth; heightScale = originalVideoHeight / deskshareHeight; }; function setDeskshareTranslate(originalVideoWidth, originalVideoHeight) { widthTranslate = (deskshareWidth - originalVideoWidth) / 2; heightTranslate = (deskshareHeight - originalVideoHeight) / 2; }; // Deskshare viewBox has the information to transform the canvas to place it above the video function adaptViewBoxToDeskshare(time) { let dimension = getDeskshareDimensionAtTime(time); setDeskshareScale(dimension.width, dimension.height); setDeskshareTranslate(dimension.width, dimension.height); let viewBox = "0.0 0.0 " + deskshareWidth + " " + deskshareHeight; return viewBox; }; function getCanvasFromImage(image) { let canvasId = "canvas" + image.substr(5); return getSVGFile().getElementById(canvasId); }; function getDeskshareImage() { let images = getSVGFile().getElementsByTagName("image"); for (var i = 0; i < images.length; i++) { let element = images[i]; let id = element.getAttribute("id"); let href = element.getAttribute("xlink:href"); if (href != null && href.indexOf("deskshare") !=-1) { return id; } } return "image0"; }; // Transform canvas to fit the different deskshare video sizes function setTransform(time) { if (deskshareImage == null) { deskshareImage = getDeskshareImage(); } if (getDeskshareAtTime(time)) { logger.debug("==Transforming annotation canvas"); let canvas = getCanvasFromImage(deskshareImage); if (canvas !== null) { let scale = "scale(" + widthScale.toString() + ", " + heightScale.toString() + ")"; let translate = "translate(" + widthTranslate.toString() + ", " + heightTranslate.toString() + ")"; let transform = translate + " " + scale; canvas.setAttribute('transform', transform); canvasTransformed = true; } } else if (canvasTransformed) { clearTransform(); } }; function defineStartTime() { logger.info("==Defining start time"); if (params.t === undefined) return 0; let extractNumber = /\d+/g; let extractUnit = /\D+/g; let startTime = 0; while (true) { let param1 = extractUnit.exec(params.t); let param2 = extractNumber.exec(params.t); if (param1 == null || param2 == null) break; let unit = String(param1).toLowerCase(); let value = parseInt(String(param2)); if (unit == "h") value *= 3600; else if (unit == "m") value *= 60; startTime += value; } logger.info("==Start time", startTime); return startTime; }; function asyncRequest(method, url) { return new Promise(function (resolve, reject) { let xhr = new XMLHttpRequest(); xhr.open(method, url); xhr.onload = function () { if (xhr.readyState !== xhr.DONE) { return; } if (xhr.status >= 200 && xhr.status < 300) { resolve(xhr); } else { reject({status: xhr.status, statusText: xhr.statusText}); } }; xhr.onerror = function () { reject({status: xhr.status, statusText: xhr.statusText}); }; xhr.send(); }); }; function loadMetadata() { asyncRequest('GET', metadataXML).then(function (response) { processMetadataXML(response); }).catch(function(error) { logger.error("==Couldn't load metadata.xml", error); onLoadComplete(false); }); }; function processMetadataXML(response) { logger.info("==Processing metadata.xml"); metadataXMLContent = response.responseXML; let metadata = metadataXMLContent.getElementsByTagName("meta"); if (metadata.length > 0) { metadata = metadata[0]; let meetingName = metadata.getElementsByTagName("meetingName"); if (meetingName.length > 0) { $("#recording-title").text(meetingName[0].textContent); $("#recording-title").attr("title", meetingName[0].textContent); } } document.dispatchEvent(new CustomEvent('content-ready', {'detail': 'metadata'})); }; function loadData() { asyncRequest('GET', shapesSVG).then(function (response) { processShapesSVG(response); }).catch(function(error) { logger.error("==Couldn't load shapes.svg", error); }); asyncRequest('GET', panzoomsXML).then(function (response) { processPanzoomsXML(response); }).catch(function(error) { logger.error("==Couldn't load panzoom.xml", error); }); asyncRequest('GET', cursorXML).then(function (response) { processCursorXML(response); }).catch(function(error) { logger.error("==Couldn't load cursor.xml", error); }); asyncRequest('GET', deskshareXML).then(function (response) { processDeskshareXML(response); }).catch(function(error) { if (error.status == 404) { logger.warn("==Couldn't find deskshare.xml, assuming there's no deskshare"); document.dispatchEvent(new CustomEvent('data-ready', {'detail': 'deskshare-xml'})); } else { logger.error("==Couldn't load deskshare.xml", error); } }); }; function processPanzoomsXML(response) { logger.info("==Processing panzooms.xml"); let panelements = response.responseXML.getElementsByTagName("recording"); let panZoomArray = panelements[0].getElementsByTagName("event"); let viewBoxes = response.responseXML.getElementsByTagName("viewBox"); let pzlen = panZoomArray.length; let secondVal; for (var k = 0;k < pzlen; k++) { if (panZoomArray[k+1] == undefined) { secondVal = "end"; } else { secondVal = panZoomArray[k+1].getAttribute("timestamp"); } vboxValues[[panZoomArray[k].getAttribute("timestamp"), secondVal]] = viewBoxes[k].childNodes[0].data; } document.dispatchEvent(new CustomEvent('data-ready', {'detail': 'panzoom'})); }; function processCursorXML(response) { logger.info("==Processing cursor.xml"); let curelements = response.responseXML.getElementsByTagName("recording"); let cursorArray = curelements[0].getElementsByTagName("event"); let coords = response.responseXML.getElementsByTagName("cursor"); let clen = cursorArray.length; if (cursorArray.length != 0) cursorValues["0"] = "0 0"; for (var m = 0; m < clen; m++) { cursorValues[cursorArray[m].getAttribute("timestamp")] = coords[m].childNodes[0].data; } document.dispatchEvent(new CustomEvent('data-ready', {'detail': 'cursor'})); }; function processShapesSVG(response) { logger.info("==Processing shapes.svg"); let svgobj = document.createElement('div'); $(svgobj).css('height', '100%'); $(svgobj).css('width', '100%'); // for some reason, innerHTML was dropping part of the svg file, while it works // fine when using Ajax html method // svgobj.innerHTML = response.responseText; $(svgobj).html(response.responseText); // Update the links inside of the presentation to include the full URL $(svgobj).find('image').each(function() { let href = $(this).attr('xlink:href'); href = url + '/' + href; $(this).attr('xlink:href', href); }); // Clear the style, we're setting it via css $(svgobj).find('svg').attr('style', ''); document.getElementById('slide').appendChild(svgobj); $("#svgfile").css('height', '100%'); $("#svgfile").css('width', '100%'); shapesSVGContent = response.responseXML; // Getting all the event tags let shapeelement = shapesSVGContent.getElementsByTagName("svg")[0]; // Get an array of the elements for each "shape" in the drawing shapesArray = shapesSVGContent.querySelectorAll('g[class="shape"]'); // To assist in finding the version of a shape shown at a particular time // (while being drawn, during updates), provide a lookup from time to id // Also save the id of the last version of each shape as its main id for (var j = 0; j < shapesArray.length; j++) { shape = shapesArray[j]; var id = shape.getAttribute('id'); var shape_i = shape.getAttribute('shape'); var time = (parseFloat(shape.getAttribute('timestamp')) * 10) | 0; if (timestampToId[time] == undefined) { timestampToId[time] = []; timestampToIdKeys.push(time); } timestampToId[time].push({id: id, shape: shape_i}) mainShapeIds[shape_i] = id; } asyncRequest('GET', textJSON).then(function (response) { processTextJSON(response); processSlideAspectTimes(); document.dispatchEvent(new CustomEvent('data-ready', {'detail': 'text'})); }).catch(function(error) { logger.warn("==Couldn't load presentation_text.json", error); processTextFallback(); processSlideAspectTimes(); document.dispatchEvent(new CustomEvent('data-ready', {'detail': 'text'})); }); document.dispatchEvent(new CustomEvent('data-ready', {'detail': 'svg'})); }; function processDeskshareXML(response) { logger.info("==Processing deskshare.xml"); let deskshareElements = response.responseXML.getElementsByTagName("recording"); let deskshareArray = deskshareElements[0].getElementsByTagName("event"); if (deskshareArray != null && deskshareArray.length != 0) { for (var m = 0; m < deskshareArray.length; m++) { let deskshareEvent = [ parseFloat(deskshareArray[m].getAttribute("start_timestamp")), parseFloat(deskshareArray[m].getAttribute("stop_timestamp")), parseFloat(deskshareArray[m].getAttribute("video_width")), parseFloat(deskshareArray[m].getAttribute("video_height")) ]; deskshareEvents[m] = deskshareEvent; } } document.dispatchEvent(new CustomEvent('data-ready', {'detail': 'deskshare-xml'})); }; function checkMediaURL(url) { var xhr = new XMLHttpRequest(); xhr.open('HEAD', url, true); xhr.onreadystatechange = function (e) { if (xhr.readyState === 4) { if (xhr.status == 200 || xhr.status == 206) { var pathname = new URL(xhr.responseURL).pathname; if (pathname.endsWith("webcams.webm") || pathname.endsWith("webcams.mp4")) { logger.info("==Found video", pathname); hasVideo = true; } else if (pathname.endsWith("deskshare.webm") || pathname.endsWith("deskshare.mp4")) { logger.info("==Found deskshare", pathname); hasDeskshare = true; } } mediasToCheck--; if (mediasToCheck == 0) { document.dispatchEvent(new CustomEvent('content-ready', {'detail': 'medias-checked'})); } } }; xhr.send(); }; function checkMedias() { for (var i = 0 ; i < mediasURL.length ; i++) { checkMediaURL(url + mediasURL[i]); } }; function initPopcorn() { firstLoad = false; generateThumbnails(); var startTime = defineStartTime(); Popcorn("#video").currentTime(startTime); if (hasDeskshare) Popcorn("#deskshare-video").currentTime(startTime); // Popcorn documentation suggests this way to get the duration, // since this information does not come with 'loadedmetadata' event. Popcorn("#video").cue(2, function() { meetingDuration = parseFloat(Popcorn("#video").duration().toFixed(1)); logger.info("==Meeting duration (seconds)", meetingDuration); }); }; function processTextJSON(response) { logger.info("==Processing presentation_text.json"); let slidesText = JSON.parse(response.responseText); let shapeElements = shapesSVGContent.getElementsByTagName("svg"); let images = shapeElements[0].getElementsByClassName("slide"); for (var m = 0; m < images.length; m++) { let len = images[m].getAttribute("in").split(" ").length; for (var n = 0; n < len; n++) { imageAtTime[[images[m].getAttribute("in").split(" ")[n], images[m].getAttribute("out").split(" ")[n]]] = images[m].getAttribute("id"); } // The logo at the start has no text attribute if (images[m].getAttribute("text")) { // Have to save the value because images array might go out of scope var imgId = images[m].getAttribute("id"); // Text format: presentation/PRESENTATION_ID/textfiles/SLIDE_ID.txt var imgTxt = images[m].getAttribute("text").split("/"); var presentationId = imgTxt[1]; var slideId = imgTxt[3].split(".")[0]; if (slidesText[presentationId] && slidesText[presentationId][slideId]) { slidePlainText[imgId] = $('
').text(slidesText[presentationId][slideId]).html(); } else { slidePlainText[imgId] = $('
') } } } }; function processTextFallback() { logger.info("==Processing slides.txt"); var textPathToImgId = {}; let shapeElements = shapesSVGContent.getElementsByTagName("svg"); let images = shapeElements[0].getElementsByClassName("slide"); for (var m = 0; m < images.length; m++) { let len = images[m].getAttribute("in").split(" ").length; for (var n = 0; n < len; n++) { imageAtTime[[images[m].getAttribute("in").split(" ")[n], images[m].getAttribute("out").split(" ")[n]]] = images[m].getAttribute("id"); } // The logo at the start has no text attribute if (images[m].getAttribute("text")) { var textPath = url + "/" + images[m].getAttribute("text"); textPathToImgId[textPath] = images[m].getAttribute("id"); getTextAsync(textPath,textPathToImgId); } } }; function getTextAsync(textPath, textPathToImgId) { let xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if (xhr.status == 200 || xhr.status == 206) { var pathname = new URL(xhr.responseURL).pathname; var imgId = textPathToImgId[pathname]; slidePlainText[imgId] = $('
').text(xhr.responseText).html(); } } }; xhr.open("GET", textPath, true); xhr.send(null); }; function processSlideAspectTimes() { var lastAspectValue = 0; for (var key in vboxValues) { if (vboxValues.hasOwnProperty(key)) { var startTimestamp = key.split(",")[0]; var stopTimestamp = key.split(",")[1]; var vboxWidth = parseFloat(vboxValues[key].split(" ")[2]); var vboxHeight = parseFloat(vboxValues[key].split(" ")[3]); var aspectValue = processAspectValue(vboxWidth, vboxHeight, startTimestamp, lastAspectValue); slideAspectValues[[startTimestamp, stopTimestamp]] = aspectValue; lastAspectValue = aspectValue; } } }; function processAspectValue(vboxWidth, vboxHeight, time, lastAspectValue) { logger.debug("==Processing presentation aspect"); let imageId; if (time == "0.0") { // A little hack 'cause function getImageAtTime with time = 0.0 returns the background image... // We need the first slide instead logger.debug("==First image"); imageId = "image1"; } else { imageId = getImageAtTime(time); logger.debug("==Image", imageId); } if (imageId !== undefined) { let image = getSVGFile().getElementById(imageId); if (image) { if (getDeskshareAtTime(parseFloat(time))) { return lastAspectValue; } let imageWidth = parseFloat(image.getAttribute("width")); let imageHeight = parseFloat(image.getAttribute("height")); // Fit-to-width: returning vbox aspect if (vboxWidth == imageWidth && vboxHeight < imageHeight) { logger.debug("==Fit-to-width aspect detected"); return parseFloat(vboxWidth/vboxHeight); } else if (vboxWidth == imageWidth && vboxHeight == imageHeight) { // Fit-to-page: returning image aspect logger.debug("==Fit-to-page aspect detected"); return parseFloat(imageWidth/imageHeight); } else { // If it's not fit-to-width neither fit-to-page we return the previous aspect return lastAspectValue; } } else { logger.error("==No image for id", imageId); return lastAspectValue; } } else { logger.error("==Image undefined"); return lastAspectValue; } }; // A small hack to hide the cursor when resizing the window, so it's not // misplaced while the window is being resized window.onresize = function(event) { showCursor(false); resizeComponents(); resizeSlide(); resizeDeskshare(); }; // Resize the container that has the slides (and whiteboard) to be the maximum // size possible but still maintaining the aspect ratio of the slides. // // This is done here only because of pan and zoom. Pan/zoom is done by modifiyng // the svg's viewBox, and that requires the container that has the svg to be the // exact size we want to display the slides so that parts of the svg that are outside // of its parent's area are hidden. If we let the svg occupy all presentation area // (letting the svg do the image resizing), the slides will move and zoom around the // entire area when pan/zoom is activated, usually displaying more of the slide // than we want to (i.e. more than was displayed in the conference). function resizeSlide() { logger.debug("==Resizing slide"); if (currentImage) { let $slide = $("#slide"); let maxWidth = currentSlideAspect * $slide.parent().outerHeight(); $slide.css("max-width", maxWidth); let maxHeight = $slide.parent().width() / currentSlideAspect; $slide.css("max-height", maxHeight); logger.debug("==Size", {maxWidth, maxHeight}); } else { logger.debug("==Slide not ready"); } }; function resizeDeskshare() { if (!hasDeskshare) return; logger.debug("==Resizing deskshare"); let deskshareVideo = document.getElementById("deskshare-video"); let $deskshareVideo = $("#deskshare-video"); // Deskshare may exist and not be initialized yet if ($deskshareVideo && deskshareVideo) { let videoWidth = parseInt(deskshareVideo.videoWidth, 10); let videoHeight = parseInt(deskshareVideo.videoHeight, 10); let aspectRatio = videoWidth/videoHeight; let maxWidth = aspectRatio * $deskshareVideo.parent().outerHeight(); $deskshareVideo.css("max-width", maxWidth); let maxHeight = $deskshareVideo.parent().width() / aspectRatio; $deskshareVideo.css("max-height", maxHeight); logger.debug("==Size", {maxWidth, maxHeight}); } else { logger.debug("==Deskshare not ready"); } }; function getSVGFile() { return $('svg')[0]; }; function linkChatToMedia() { logger.info("==Linking chat to media"); // Popcorn lib uses the 'mediaLoaded' event to link the chat timeline to the video var event = document.createEvent("HTMLEvents"); event.initEvent("mediaLoaded", true, true); document.dispatchEvent(event); } document.addEventListener('media-ready', function(event) { logger.debug("==Media ready", event.detail); if (mediaReady) return; switch(event.detail) { case 'video': videoReady = true; break; case 'captions': captionsReady = true; break; case 'audio': audioReady = true; break; case 'deskshare': deskshareReady = true; break; default: logger.warn("==Unhandled media-ready event", event.detail); } if ((audioReady || (videoReady && captionsReady)) && deskshareReady) { logger.info("==All medias can be played"); setMediaSync(); linkChatToMedia(); document.dispatchEvent(new CustomEvent('playback-ready', {'detail': 'media'})); } }, false); document.addEventListener('data-ready', function(event) { logger.debug("==Data ready", event.detail); if (dataReady) return; switch(event.detail) { case 'svg': svgReady = true; break; case 'text': textReady = true; break; case 'panzoom': panzoomReady = true; break; case 'cursor': cursorReady = true; break; case 'deskshare-xml': deskshareXMLReady = true; break; default: logger.warn("==Unhandled data-ready event", event.detail); } if (svgReady && textReady && panzoomReady && cursorReady && deskshareXMLReady) { document.dispatchEvent(new CustomEvent('playback-ready', {'detail': 'data'})); } }, false);