Merge branch 'master' of github.com:bigbluebutton/bigbluebutton into adding-accessibility-changes
This commit is contained in:
commit
bcc9d78748
1
bigbluebutton-config/bigbluebutton-release
Normal file
1
bigbluebutton-config/bigbluebutton-release
Normal file
@ -0,0 +1 @@
|
||||
BIGBLUEBUTTON_RELEASE=0.81-dev
|
@ -92,7 +92,8 @@
|
||||
|
||||
<!-- Footer Start -->
|
||||
<div id="footer">
|
||||
<p id="legal">Copyright © 2012 <a href="http://bigbluebutton.org/" title="BigBlueButton" >BigBlueButton Inc.</a> <br> <a href="http://code.google.com/p/bigbluebutton/wiki/ReleaseNotes" title="BigBlueButton Release Notes" target="_blank">version 0.8</a></p>
|
||||
<p id="legal">Copyright © 2012 <a href="http://bigbluebutton.org/" title="BigBlueButton" >BigBlueButton Inc.</a> <br>
|
||||
<a href="http://code.google.com/p/bigbluebutton/wiki/ReleaseNotes" title="BigBlueButton Release Notes" target="_blank">version 0.81-dev</a></p>
|
||||
</div>
|
||||
<!-- Footer End -->
|
||||
|
||||
|
98
record-and-playback/presentation/playback/presentation/css/bbb.playback.css
Executable file
98
record-and-playback/presentation/playback/presentation/css/bbb.playback.css
Executable file
@ -0,0 +1,98 @@
|
||||
/*
|
||||
|
||||
BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
|
||||
Copyright (c) 2012 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
html{
|
||||
height:100%;
|
||||
}
|
||||
body {
|
||||
font-family: Verdana;
|
||||
height:100%;
|
||||
}
|
||||
h1 {
|
||||
text-align:center
|
||||
}
|
||||
br{
|
||||
display:none
|
||||
}
|
||||
|
||||
#playbackArea{
|
||||
width: 800px;
|
||||
height: 600px;
|
||||
margin: 0px auto;
|
||||
/* border:5px solid #a1a1a1;
|
||||
border-radius:10px; */
|
||||
}
|
||||
|
||||
#slide{
|
||||
width: 800px;
|
||||
height: 600px;
|
||||
}
|
||||
|
||||
#video{
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.chatcontainer{
|
||||
width:420px;
|
||||
height:600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
position: relative;
|
||||
top:-650px;
|
||||
left:610px;
|
||||
}
|
||||
#chat{
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
position:relative;
|
||||
top:5px;
|
||||
padding-left:10px;
|
||||
padding-right:10px;
|
||||
}
|
||||
#chat div p{
|
||||
margin-bottom:-10px;
|
||||
padding:0px;
|
||||
font-size:13px
|
||||
}
|
||||
#big {display:none}
|
||||
#mid {display:none}
|
||||
|
||||
#footer{
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-top: 25px;
|
||||
font-size:0.75em;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.circle {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
|
||||
#cursor {
|
||||
position: absolute;
|
||||
background: red;
|
||||
}
|
200
record-and-playback/presentation/playback/presentation/lib/popcorn-complete.min.js
vendored
Executable file
200
record-and-playback/presentation/playback/presentation/lib/popcorn-complete.min.js
vendored
Executable file
File diff suppressed because one or more lines are too long
346
record-and-playback/presentation/playback/presentation/lib/writing.js
Executable file
346
record-and-playback/presentation/playback/presentation/lib/writing.js
Executable file
@ -0,0 +1,346 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2012 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
// - - - START OF GLOBAL VARIABLES - - - //
|
||||
"use strict";
|
||||
|
||||
function getUrlParameters() {
|
||||
var map = {};
|
||||
window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (m, key, value) { map[key] = value; });
|
||||
return map;
|
||||
}
|
||||
|
||||
// - - - END OF GLOBAL VARIABLES - - - //
|
||||
|
||||
// - - - START OF JAVASCRIPT FUNCTIONS - - - //
|
||||
|
||||
// Draw the cursor at a specific point
|
||||
function draw(x, y) {
|
||||
cursorStyle = document.getElementById("cursor").style;
|
||||
//move to the next place
|
||||
cursorStyle.left = (parseInt(document.getElementById("slide").offsetLeft, 10) + parseInt(x, 10)) + "px";
|
||||
cursorStyle.top = (parseInt(document.getElementById("slide").offsetTop, 10) + parseInt(y, 10)) + "px";
|
||||
}
|
||||
|
||||
// Shows or hides the cursor object depending on true/false parameter passed.
|
||||
function showCursor(boolVal) {
|
||||
cursorStyle = document.getElementById("cursor").style;
|
||||
if(boolVal === false) {
|
||||
cursorStyle.height = "0px";
|
||||
cursorStyle.width = "0px";
|
||||
}
|
||||
else {
|
||||
cursorStyle.height = "10px";
|
||||
cursorStyle.width = "10px";
|
||||
}
|
||||
}
|
||||
|
||||
function setViewBox(val) {
|
||||
if(svgobj.contentDocument) svgfile = svgobj.contentDocument.getElementById("svgfile");
|
||||
else svgfile = svgobj.getSVGDocument('svgfile').getElementById("svgfile");
|
||||
svgfile.setAttribute('viewBox', val);
|
||||
}
|
||||
|
||||
function setCursor(val) {
|
||||
draw(val[0], val[1]);
|
||||
}
|
||||
|
||||
function getImageAtTime(time) {
|
||||
var curr_t = parseFloat(time);
|
||||
var key;
|
||||
for (key in imageAtTime) {
|
||||
if(imageAtTime.hasOwnProperty(key)) {
|
||||
var arry = key.split(",");
|
||||
if ((parseFloat(arry[0]) <= curr_t) && (parseFloat(arry[1]) >= curr_t)) {
|
||||
return imageAtTime[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getViewboxAtTime(time) {
|
||||
var curr_t = parseFloat(time);
|
||||
var key;
|
||||
for (key in vboxValues) {
|
||||
if(vboxValues.hasOwnProperty(key)) {
|
||||
var arry = key.split(",");
|
||||
if(arry[1] == "end") {
|
||||
return vboxValues[key];
|
||||
}
|
||||
else if ((parseFloat(arry[0]) <= curr_t) && (parseFloat(arry[1]) >= curr_t)) {
|
||||
return vboxValues[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getCursorAtTime(time) {
|
||||
var coords = cursorValues[time];
|
||||
if(coords) return coords.split(' ');
|
||||
}
|
||||
|
||||
// - - - END OF JAVASCRIPT FUNCTIONS - - - //
|
||||
|
||||
function runPopcorn() {
|
||||
console.log("SVG loaded");
|
||||
if(svgobj.contentDocument) svgfile = svgobj.contentDocument.getElementById("svgfile");
|
||||
else svgfile = svgobj.getSVGDocument('svgfile');
|
||||
|
||||
//making the object for requesting the read of the XML files.
|
||||
if (window.XMLHttpRequest) {
|
||||
// code for IE7+, Firefox, Chrome, Opera, Safari
|
||||
var xmlhttp = new XMLHttpRequest();
|
||||
} else {
|
||||
// code for IE6, IE5
|
||||
var xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
|
||||
}
|
||||
|
||||
// PROCESS SHAPES.SVG (in XML format).
|
||||
xmlhttp.open("GET", shapes_svg, false);
|
||||
xmlhttp.send();
|
||||
var xmlDoc = xmlhttp.responseXML;
|
||||
//getting all the event tags
|
||||
var shapeelements = xmlDoc.getElementsByTagName("svg");
|
||||
|
||||
//get the array of values for the first shape (getDataPoints(0) is the first shape).
|
||||
var array = shapeelements[0].getElementsByClassName("shape"); //get all the lines from the svg file
|
||||
//var pages = shapeelements[0].getElementsByClassName("page");
|
||||
var images = shapeelements[0].getElementsByTagName("image");
|
||||
|
||||
//console.log(images);
|
||||
|
||||
//fill the times array with the times of the svg images.
|
||||
for (var j = 0; j < array.length; j++) {
|
||||
times[j] = array[j].getAttribute("id").substr(4);
|
||||
}
|
||||
|
||||
var times_length = times.length; //get the length of the times array.
|
||||
|
||||
for(var m = 0; m < images.length; m++) {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
// PROCESS PANZOOMS.XML
|
||||
xmlhttp.open("GET", events_xml, false);
|
||||
xmlhttp.send();
|
||||
xmlDoc = xmlhttp.responseXML;
|
||||
//getting all the event tags
|
||||
var panelements = xmlDoc.getElementsByTagName("recording");
|
||||
var panZoomArray = panelements[0].getElementsByTagName("event");
|
||||
viewBoxes = xmlDoc.getElementsByTagName("viewBox");
|
||||
|
||||
var pzlen = panZoomArray.length;
|
||||
var second_val;
|
||||
//fill the times array with the times of the svg images.
|
||||
for (var k = 0;k < pzlen; k++) {
|
||||
if(panZoomArray[k+1] == undefined) {
|
||||
second_val = "end";
|
||||
}
|
||||
else second_val = panZoomArray[k+1].getAttribute("timestamp");
|
||||
vboxValues[[panZoomArray[k].getAttribute("timestamp"), second_val]] = viewBoxes[k].childNodes[0].data;
|
||||
}
|
||||
|
||||
|
||||
// PROCESS CURSOR.XML
|
||||
xmlhttp.open("GET", cursor_xml, false);
|
||||
xmlhttp.send();
|
||||
xmlDoc = xmlhttp.responseXML;
|
||||
//getting all the event tags
|
||||
var curelements = xmlDoc.getElementsByTagName("recording");
|
||||
var cursorArray = curelements[0].getElementsByTagName("event");
|
||||
coords = xmlDoc.getElementsByTagName("cursor");
|
||||
|
||||
var clen = cursorArray.length;
|
||||
//fill the times array with the times of the svg images.
|
||||
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;
|
||||
}
|
||||
|
||||
svgobj.style.left = document.getElementById("slide").offsetLeft + "px";
|
||||
svgobj.style.top = "8px";
|
||||
var next_shape;
|
||||
var shape;
|
||||
for (var i = 0, len = times_length; i < len-1; i++) { //iterate through all the shapes and pick out the main ones
|
||||
var time = times[i];
|
||||
shape = svgobj.contentDocument.getElementById("draw" + time).getAttribute("shape");
|
||||
next_shape = svgobj.contentDocument.getElementById("draw" + times[i+1]).getAttribute("shape");
|
||||
|
||||
if(shape !== next_shape) {
|
||||
main_shapes_times[main_shapes_times.length] = time;
|
||||
}
|
||||
}
|
||||
if(times.length !== 0) {
|
||||
main_shapes_times[main_shapes_times.length] = times[times.length-1]; //put last value into this array always!
|
||||
}
|
||||
|
||||
var p = new Popcorn("#video");
|
||||
//update 60x / second the position of the next value.
|
||||
p.code({
|
||||
start: 1, // start time
|
||||
end: p.duration(),
|
||||
onFrame: function(options) {
|
||||
if(!((p.paused() === true) && (p.seeking() === false))) {
|
||||
var t = p.currentTime().toFixed(1); //get the time and round to 1 decimal place
|
||||
|
||||
if(svgobj.contentDocument) current_shape = svgobj.contentDocument.getElementById("draw" + t);
|
||||
else current_shape = svgobj.getSVGDocument('svgfile').getElementById("draw" + t);
|
||||
if(current_shape !== null) { //if there is actually a new shape to be displayed
|
||||
current_shape = current_shape.getAttribute("shape"); //get actual shape tag for this specific time of playback
|
||||
}
|
||||
//redraw everything (only way to make everything elegant)
|
||||
for (var i = 0, len = times_length; i < len; i++) {
|
||||
var time_s = times[i];
|
||||
var time_f = parseFloat(time_s);
|
||||
if(svgobj.contentDocument) shape = svgobj.contentDocument.getElementById("draw" + time_s);
|
||||
else shape = svgobj.getSVGDocument('svgfile').getElementById("draw" + time_s);
|
||||
var shape_i = shape.getAttribute("shape");
|
||||
|
||||
if (time_f < t) {
|
||||
if(shape_i === current_shape) { //currently drawing the same shape so don't draw the older steps
|
||||
shape.style.visibility = "hidden"; //hide older steps to shape
|
||||
}
|
||||
else if(main_shapes_times.indexOf(time_s) !== -1) { //as long as it is a main shape, it can be drawn... no intermediate steps.
|
||||
if(parseFloat(shape.getAttribute("undo")) === -1) { //As long as the undo event hasn't happened yet...
|
||||
shape.style.visibility = "visible";
|
||||
}
|
||||
else if (parseFloat(shape.getAttribute("undo")) > t) {
|
||||
shape.style.visibility = "visible";
|
||||
}
|
||||
else {
|
||||
shape.style.visibility = "hidden";
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(time_s === t) { //for the shape with the time specific to the current time
|
||||
shape.style.visibility = "visible";
|
||||
}
|
||||
else { //for shapes that shouldn't be drawn yet (larger time than current time), don't draw them.
|
||||
shape.style.visibility = "hidden";
|
||||
}
|
||||
}
|
||||
|
||||
var next_image = getImageAtTime(t); //fetch the name of the image at this time.
|
||||
var imageXOffset = 0;
|
||||
var imageYOffset = 0;
|
||||
if((current_image !== next_image) && (next_image !== undefined)){ //changing slide image
|
||||
if(svgobj.contentDocument) {
|
||||
svgobj.contentDocument.getElementById(current_image).style.visibility = "hidden";
|
||||
var ni = svgobj.contentDocument.getElementById(next_image);
|
||||
}
|
||||
else {
|
||||
svgobj.getSVGDocument('svgfile').getElementById(current_image).style.visibility = "hidden";
|
||||
var ni = svgobj.getSVGDocument('svgfile').getElementById(next_image);
|
||||
}
|
||||
ni.style.visibility = "visible";
|
||||
|
||||
var num_current = current_image.substr(5);
|
||||
var num_next = next_image.substr(5);
|
||||
if(svgobj.contentDocument) currentcanvas = svgobj.contentDocument.getElementById("canvas" + num_current);
|
||||
else currentcanvas = svgobj.getSVGDocument('svgfile').getElementById("canvas" + num_current);
|
||||
if(currentcanvas !== null) {
|
||||
currentcanvas.setAttribute("display", "none");
|
||||
}
|
||||
if(svgobj.contentDocument) nextcanvas = svgobj.contentDocument.getElementById("canvas" + num_next);
|
||||
else nextcanvas = svgobj.getSVGDocument('svgfile').getElementById("canvas" + num_next);
|
||||
if((nextcanvas !== undefined) && (nextcanvas != null)) {
|
||||
nextcanvas.setAttribute("display", "");
|
||||
}
|
||||
current_image = next_image;
|
||||
}
|
||||
|
||||
if(svgobj.contentDocument) var thisimg = svgobj.contentDocument.getElementById(current_image);
|
||||
else var thisimg = svgobj.getSVGDocument('svgfile').getElementById(current_image);
|
||||
var offsets = thisimg.getBoundingClientRect();
|
||||
imageXOffset = (1600 - parseInt(thisimg.getAttribute("width"), 10))/2;
|
||||
imageYOffset = (1200 - parseInt(thisimg.getAttribute("height"), 10))/2;
|
||||
|
||||
var vboxVal = getViewboxAtTime(t);
|
||||
if(vboxVal !== undefined) {
|
||||
setViewBox(vboxVal);
|
||||
}
|
||||
|
||||
var cursorVal = getCursorAtTime(t);
|
||||
var cursor_on = false;
|
||||
if(cursorVal != null) {
|
||||
if(!cursor_on) {
|
||||
document.getElementById("cursor").style.visibility = 'visible';
|
||||
cursor_on = true;
|
||||
}
|
||||
setCursor([parseFloat(cursorVal[0]) + imageXOffset - 6, parseFloat(cursorVal[1]) + imageYOffset - 6]); //-6 is for radius of cursor offset
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
var current_canvas = "canvas0";
|
||||
var current_image = "image0";
|
||||
var currentcanvas;
|
||||
var shape;
|
||||
var nextcanvas;
|
||||
var next_image;
|
||||
var next_pgid;
|
||||
var curr_pgid;
|
||||
var svgfile;
|
||||
//current time
|
||||
var t;
|
||||
var len;
|
||||
var current_shape;
|
||||
//coordinates for x and y for each second
|
||||
var panAndZoomTimes = [];
|
||||
var viewBoxes = [];
|
||||
var coords = [];
|
||||
var times = [];
|
||||
var clearTimes = [];
|
||||
var main_shapes_times = [];
|
||||
var vboxValues = {};
|
||||
var cursorValues = {};
|
||||
var imageAtTime = {};
|
||||
var cursorStyle;
|
||||
|
||||
var params = getUrlParameters();
|
||||
var MEETINGID = params.meetingId;
|
||||
var HOST = window.location.hostname;
|
||||
var url = "http://" + HOST + "/presentation/" + MEETINGID;
|
||||
var shapes_svg = url + '/shapes.svg';
|
||||
var events_xml = url + '/panzooms.xml';
|
||||
var cursor_xml = url + '/cursor.xml';
|
||||
|
||||
var svgobj = document.createElement('object');
|
||||
|
||||
svgobj.setAttribute('data', shapes_svg);
|
||||
svgobj.setAttribute('height', '600px');
|
||||
svgobj.setAttribute('width', '800px');
|
||||
svgobj.addEventListener('load', runPopcorn, false);
|
||||
document.getElementById('slide').appendChild(svgobj);
|
||||
|
||||
//immediately load the content
|
||||
|
||||
|
||||
window.onresize = function(event) {
|
||||
svgobj.style.left = document.getElementById("slide").offsetLeft + "px";
|
||||
svgobj.style.top = "8px";
|
||||
};
|
||||
console.log("writing.js is loaded.");
|
BIN
record-and-playback/presentation/playback/presentation/logo.png
Executable file
BIN
record-and-playback/presentation/playback/presentation/logo.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
70
record-and-playback/presentation/playback/presentation/playback.html
Executable file
70
record-and-playback/presentation/playback/presentation/playback.html
Executable file
@ -0,0 +1,70 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
|
||||
Copyright (c) 2012 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 <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<title>BigBlueButton Playback</title>
|
||||
<link rel="stylesheet" href="css/bbb.playback.css">
|
||||
<script type="text/javascript">
|
||||
document.addEventListener( "DOMContentLoaded", function() {
|
||||
function getUrlParameters() {
|
||||
var map = {};
|
||||
var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) { map[key] = value; });
|
||||
return map;
|
||||
}
|
||||
var audio;
|
||||
var slides;
|
||||
var params = getUrlParameters();
|
||||
var MEETINGID = params['meetingId'];
|
||||
var HOST = window.location.hostname;
|
||||
var RECORDINGS = "http://" + HOST + "/presentation/" + MEETINGID;
|
||||
var SLIDES_XML = "http://" + HOST + "/presentation/" + MEETINGID + '/slides_new.xml';
|
||||
var appName = navigator.appName;
|
||||
var appVersion = navigator.appVersion;
|
||||
audio = document.getElementById("video");
|
||||
if(appName == "Microsoft Internet Explorer"){
|
||||
var message = "Your browser does not support this playback. Please use other browser: Firefox, Safari, Chrome, Opera";
|
||||
var line = document.createElement("p");
|
||||
line.appendChild(document.createTextNode(message));
|
||||
document.getElementById("chat").appendChild(line);
|
||||
}else if (appVersion.match("Safari") != null && appVersion.match("Chrome") == null){
|
||||
audio.setAttribute('src', RECORDINGS + '/audio/recording.wav');
|
||||
audio.setAttribute('type','audio/x-wav');
|
||||
}else{
|
||||
audio.setAttribute('src', RECORDINGS + '/audio/audio.ogg');
|
||||
audio.setAttribute('type','audio/ogg');
|
||||
}
|
||||
audio.setAttribute('data-timeline-sources', SLIDES_XML)
|
||||
}, false);
|
||||
</script>
|
||||
<script src="lib/popcorn-complete.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="playbackArea">
|
||||
<div id="slide"></div>
|
||||
<audio id="video" controls > You must use an HTML5 capable browser to view this page.
|
||||
This playback requires modern browser, please use FF, Safari, Chrome</audio>
|
||||
<div class="circle" id="cursor" style="visibility:hidden;"></div>
|
||||
<div class="chatcontainer">
|
||||
<div id="chat"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p id="footer">Recorded with <a href="http://bigbluebutton.org/">BigBlueButton</a>.</p>
|
||||
<script src='lib/writing.js'></script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,9 @@
|
||||
location /playback/presentation {
|
||||
root /var/bigbluebutton;
|
||||
index index.html index.htm;
|
||||
}
|
||||
|
||||
location /presentation {
|
||||
root /var/bigbluebutton/published;
|
||||
index index.html index.htm;
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
|
||||
playback_host: 192.168.0.183
|
||||
publish_dir: /var/bigbluebutton/published/presentation
|
96
record-and-playback/presentation/scripts/process/presentation.rb
Executable file
96
record-and-playback/presentation/scripts/process/presentation.rb
Executable file
@ -0,0 +1,96 @@
|
||||
# Set encoding to utf-8
|
||||
# encoding: UTF-8
|
||||
|
||||
#
|
||||
# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
#
|
||||
# Copyright (c) 2012 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
require '../../core/lib/recordandplayback'
|
||||
require 'rubygems'
|
||||
require 'trollop'
|
||||
require 'yaml'
|
||||
|
||||
opts = Trollop::options do
|
||||
opt :meeting_id, "Meeting id to archive", :default => '58f4a6b3-cd07-444d-8564-59116cb53974', :type => String
|
||||
end
|
||||
|
||||
meeting_id = opts[:meeting_id]
|
||||
|
||||
# This script lives in scripts/archive/steps while properties.yaml lives in scripts/
|
||||
props = YAML::load(File.open('../../core/scripts/bigbluebutton.yml'))
|
||||
|
||||
recording_dir = props['recording_dir']
|
||||
raw_archive_dir = "#{recording_dir}/raw/#{meeting_id}"
|
||||
|
||||
|
||||
|
||||
target_dir = "#{recording_dir}/process/presentation/#{meeting_id}"
|
||||
if not FileTest.directory?(target_dir)
|
||||
logger = Logger.new("/var/log/bigbluebutton/slides/process-#{meeting_id}.log", 'daily' )
|
||||
BigBlueButton.logger = logger
|
||||
BigBlueButton.logger.info("Processing script presentation.rb")
|
||||
FileUtils.mkdir_p target_dir
|
||||
|
||||
# Create a copy of the raw archives
|
||||
temp_dir = "#{target_dir}/temp"
|
||||
FileUtils.mkdir_p temp_dir
|
||||
FileUtils.cp_r(raw_archive_dir, temp_dir)
|
||||
|
||||
BigBlueButton::AudioProcessor.process("#{temp_dir}/#{meeting_id}", "#{target_dir}/audio.ogg")
|
||||
events_xml = "#{temp_dir}/#{meeting_id}/events.xml"
|
||||
FileUtils.cp(events_xml, target_dir)
|
||||
|
||||
presentation_dir = "#{temp_dir}/#{meeting_id}/presentation"
|
||||
presentations = BigBlueButton::Presentation.get_presentations(events_xml)
|
||||
|
||||
processed_pres_dir = "#{target_dir}/presentation"
|
||||
FileUtils.mkdir_p processed_pres_dir
|
||||
|
||||
presentations.each do |pres|
|
||||
pres_dir = "#{presentation_dir}/#{pres}"
|
||||
num_pages = BigBlueButton::Presentation.get_number_of_pages_for(pres_dir)
|
||||
pres_pdf = "#{pres_dir}/#{pres}.pdf"
|
||||
|
||||
target_pres_dir = "#{processed_pres_dir}/#{pres}"
|
||||
FileUtils.mkdir_p target_pres_dir
|
||||
|
||||
images=Dir.glob("#{pres_dir}/#{pres}.{jpg,png,gif}")
|
||||
if images.empty?
|
||||
1.upto(num_pages) do |page|
|
||||
pdf_page = "#{pres_dir}/slide-#{page}.pdf"
|
||||
BigBlueButton::Presentation.extract_page_from_pdf(page, pres_pdf, pdf_page)
|
||||
#BigBlueButton::Presentation.convert_pdf_to_png(pdf_page, "#{target_pres_dir}/slide-#{page}.png")
|
||||
command = "convert -density 300x300 -resize 1600x1200 -quality 90 +dither -depth 8 -colors 256 #{pdf_page} #{target_pres_dir}/slide-#{page}.png"
|
||||
BigBlueButton.execute(command)
|
||||
end
|
||||
else
|
||||
ext = File.extname("#{images[0]}")
|
||||
#BigBlueButton::Presentation.convert_image_to_png(images[0],"#{target_pres_dir}/slide-1.png")
|
||||
command="convert -resize 1600x1200 #{images[0]} #{target_pres_dir}/slide-1.png"
|
||||
BigBlueButton.execute(command)
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
process_done = File.new("#{recording_dir}/status/processed/#{meeting_id}-presentation.done", "w")
|
||||
process_done.write("Processed #{meeting_id}")
|
||||
process_done.close
|
||||
#else
|
||||
# BigBlueButton.logger.debug("Skipping #{meeting_id} as it has already been processed.")
|
||||
end
|
||||
|
671
record-and-playback/presentation/scripts/publish/presentation.rb
Executable file
671
record-and-playback/presentation/scripts/publish/presentation.rb
Executable file
@ -0,0 +1,671 @@
|
||||
# Set encoding to utf-8
|
||||
# encoding: UTF-8
|
||||
|
||||
#
|
||||
# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
#
|
||||
# Copyright (c) 2012 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
performance_start = Time.now
|
||||
|
||||
require '../../core/lib/recordandplayback'
|
||||
require 'rubygems'
|
||||
require 'trollop'
|
||||
require 'yaml'
|
||||
require 'builder'
|
||||
require 'fastimage' # require fastimage to get the image size of the slides (gem install fastimage)
|
||||
|
||||
# used to convert the colours to hex
|
||||
class String
|
||||
def convert_base(from, to)
|
||||
self.to_i(from).to_s(to)
|
||||
end
|
||||
end
|
||||
|
||||
def processPanAndZooms
|
||||
#Create panzooms.xml
|
||||
BigBlueButton.logger.info("Creating panzooms.xml")
|
||||
$panzooms_xml = Nokogiri::XML::Builder.new do |xml|
|
||||
$xml = xml
|
||||
$xml.recording('id' => 'panzoom_events') do
|
||||
h_ratio_prev = nil
|
||||
w_ratio_prev = nil
|
||||
x_prev = nil
|
||||
y_prev = nil
|
||||
timestamp_orig_prev = nil
|
||||
timestamp_prev = nil
|
||||
last_time = $panzoom_events.last[:timestamp].to_f
|
||||
$panzoom_events.each do |panZoomEvent|
|
||||
# Get variables
|
||||
timestamp_orig = panZoomEvent[:timestamp].to_f
|
||||
|
||||
timestamp = ((timestamp_orig-$join_time)/1000).round(1)
|
||||
h_ratio = panZoomEvent.xpath(".//heightRatio")[0].text()
|
||||
w_ratio = panZoomEvent.xpath(".//widthRatio")[0].text()
|
||||
x = panZoomEvent.xpath(".//xOffset")[0].text()
|
||||
y = panZoomEvent.xpath(".//yOffset")[0].text()
|
||||
|
||||
if(timestamp_prev == timestamp)
|
||||
if(timestamp_orig == last_time)
|
||||
if(h_ratio && w_ratio && x && y)
|
||||
$xml.event(:timestamp => timestamp, :orig => timestamp_orig) do
|
||||
$ss.each do |key,val|
|
||||
$val = val
|
||||
if key === timestamp
|
||||
$vbox_width = $val[0]
|
||||
$vbox_height = $val[1]
|
||||
end
|
||||
end
|
||||
$xml.viewBox "#{($vbox_width-((1-((x.to_f.abs)*$magic_mystery_number/100.0))*$vbox_width))} #{($vbox_height-((1-((y.to_f.abs)*$magic_mystery_number/100.0))*$vbox_height)).round(2)} #{((w_ratio.to_f/100.0)*$vbox_width).round(1)} #{((h_ratio.to_f/100.0)*$vbox_height).round(1)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
# do nothing because playback can't react that fast
|
||||
else
|
||||
if(h_ratio_prev && w_ratio_prev && x_prev && y_prev)
|
||||
$xml.event(:timestamp => timestamp_prev, :orig => timestamp_orig_prev) do
|
||||
$ss.each do |key,val|
|
||||
$val = val
|
||||
if key === timestamp_prev
|
||||
$vbox_width = $val[0]
|
||||
$vbox_height = $val[1]
|
||||
end
|
||||
end
|
||||
$xml.viewBox "#{($vbox_width-((1-((x_prev.to_f.abs)*$magic_mystery_number/100.0))*$vbox_width))} #{($vbox_height-((1-((y_prev.to_f.abs)*$magic_mystery_number/100.0))*$vbox_height)).round(2)} #{((w_ratio_prev.to_f/100.0)*$vbox_width).round(1)} #{((h_ratio_prev.to_f/100.0)*$vbox_height).round(1)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
timestamp_prev = timestamp
|
||||
timestamp_orig_prev = timestamp_orig
|
||||
h_ratio_prev = h_ratio
|
||||
w_ratio_prev = w_ratio
|
||||
x_prev = x
|
||||
y_prev = y
|
||||
end
|
||||
end
|
||||
end
|
||||
BigBlueButton.logger.info("Finished creating panzooms.xml")
|
||||
end
|
||||
|
||||
def processCursorEvents
|
||||
BigBlueButton.logger.info("Processing cursor events")
|
||||
$cursor_xml = Nokogiri::XML::Builder.new do |xml|
|
||||
$xml = xml
|
||||
$xml.recording('id' => 'cursor_events') do
|
||||
x_prev = nil
|
||||
y_prev = nil
|
||||
timestamp_orig_prev = nil
|
||||
timestamp_prev = nil
|
||||
if(!$cursor_events.empty?)
|
||||
last_time = $cursor_events.last[:timestamp].to_f
|
||||
$cursor_events.each do |cursorEvent|
|
||||
timestamp_orig = cursorEvent[:timestamp].to_f
|
||||
timestamp = ((timestamp_orig-$join_time)/1000).round(1)
|
||||
|
||||
x = cursorEvent.xpath(".//xOffset")[0].text()
|
||||
y = cursorEvent.xpath(".//yOffset")[0].text()
|
||||
if(timestamp_prev == timestamp)
|
||||
|
||||
else
|
||||
if(x_prev && y_prev)
|
||||
$xml.event(:timestamp => timestamp_prev, :orig => timestamp_orig_prev) do
|
||||
$ss.each do |key,val|
|
||||
$val = val
|
||||
if key === timestamp_prev
|
||||
$vbox_width = $val[0]/2 # because the image size is twice as big as the viewbox
|
||||
$vbox_height = $val[1]/2 # because the image size is twice as big as the viewbox
|
||||
end
|
||||
end
|
||||
$xml.cursor "#{($vbox_width.to_f*x.to_f).round(1)} #{($vbox_height.to_f*y.to_f).round(1)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
timestamp_prev = timestamp
|
||||
timestamp_orig_prev = timestamp_orig
|
||||
x_prev = x
|
||||
y_prev = y
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
BigBlueButton.logger.info("Finished processing cursor events")
|
||||
end
|
||||
|
||||
def processClearEvents
|
||||
# process all the cleared pages events.
|
||||
$clear_page_events.each do |clearEvent|
|
||||
clearTime = ((clearEvent[:timestamp].to_f - $join_time)/1000).round(1)
|
||||
$pageCleared = clearEvent.xpath(".//pageNumber")[0].text()
|
||||
slideFolder = clearEvent.xpath(".//presentation")[0].text()
|
||||
#$clearPageTimes[clearTime] = [$pageCleared, $canvas_number, "presentation/#{slideFolder}/slide-#{$pageCleared.to_i+1}.png", nil]
|
||||
$clearPageTimes[($prev_clear_time..clearTime)] = [$pageCleared, $canvas_number, "presentation/#{slideFolder}/slide-#{$pageCleared.to_i+1}.png", nil]
|
||||
$prev_clear_time = clearTime
|
||||
$canvas_number+=1
|
||||
end
|
||||
end
|
||||
|
||||
def processUndoEvents
|
||||
# Processing the undo events, creating/filling a hashmap called "undos".
|
||||
BigBlueButton.logger.info("Process undo events.")
|
||||
$undo_events.each do |undo|
|
||||
closest_shape = nil # Initialize as nil to prime the loop.
|
||||
t = undo[:timestamp].to_f
|
||||
$shape_events.each do |shape|
|
||||
# The undo cannot be for a shape that hasn't been drawn yet.
|
||||
if shape[:timestamp].to_f < t
|
||||
# It must be the closest shape drawn that hasn't already been undone.
|
||||
if (closest_shape == nil) || (shape[:timestamp].to_f > closest_shape[:timestamp].to_f)
|
||||
# It cannot be an undo for another shape already.
|
||||
if !($undos.has_key? shape)
|
||||
# Must be part of this presentation of course
|
||||
if shape.xpath(".//pageNumber")[0].text() == undo.xpath(".//pageNumber")[0].text()
|
||||
# Must be a shape in this page too.
|
||||
if shape.xpath(".//presentation")[0].text() == undo.xpath(".//presentation")[0].text()
|
||||
if ((shape.xpath(".//type")[0].text() == "rectangle") || (shape.xpath(".//type")[0].text() == "ellipse"))
|
||||
shape_already_processed = false
|
||||
if($undos.length == 0)
|
||||
shape_already_processed = false
|
||||
else
|
||||
$undos.each do |u, v|
|
||||
if shape.xpath(".//dataPoints")[0].text().split(",")[0] == u.xpath(".//dataPoints")[0].text().split(",")[0]
|
||||
if shape.xpath(".//dataPoints")[0].text().split(",")[1] == u.xpath(".//dataPoints")[0].text().split(",")[1]
|
||||
shape_already_processed = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if !(shape_already_processed)
|
||||
closest_shape = shape
|
||||
end
|
||||
else
|
||||
closest_shape = shape
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if(closest_shape != nil)
|
||||
$undos[closest_shape] = ((undo[:timestamp].to_f - $join_time)/1000).round(1)
|
||||
end
|
||||
end
|
||||
|
||||
$undos_temp = {}
|
||||
$undos.each do |un, val|
|
||||
$undos_temp[((un[:timestamp].to_f - $join_time)/1000).round(1)] = val
|
||||
end
|
||||
$undos = $undos_temp
|
||||
BigBlueButton.logger.info("Undos: #{$undos}")
|
||||
end
|
||||
|
||||
def processClearImages
|
||||
BigBlueButton.logger.info("Put image numbers in clearPageTimes")
|
||||
$slides_compiled.each do |key, val|
|
||||
$clearPageTimes.each do |cpt, pgCanvasUrl|
|
||||
# check if the src of the slide matches the url of the clear event
|
||||
if key[0] == pgCanvasUrl[2]
|
||||
# put the image number into the $clearPageTimes
|
||||
pgCanvasUrl[3] = "image#{val[2].to_i}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def storePencilShape
|
||||
$line_count = $line_count + 1 # always update the line count!
|
||||
$xml.g(:class => :shape, :id=>"draw#{$shapeCreationTime}", :undo => $shapeUndoTime, :shape =>"line#{$line_count}", :style => "stroke:\##{$colour_hex}; stroke-width:#{$shapeThickness}; visibility:hidden; stroke-linecap: round; ") do
|
||||
for i in (0...($shapeDataPoints.length/2)-1) do
|
||||
$xml.line(:x1 => (($shapeDataPoints[i*2].to_f)/100)*$vbox_width, :y1 => (($shapeDataPoints[(i*2)+1].to_f)/100)*$vbox_height, :x2 => (($shapeDataPoints[(i*2)+2].to_f)/100)*$vbox_width, :y2 => (($shapeDataPoints[(i*2)+3].to_f)/100)*$vbox_height)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def storeRectShape
|
||||
if($shapeCreationTime != $prev_time)
|
||||
if(($originalOriginX == (($shapeDataPoints[0].to_f)/100)*$vbox_width) && ($originalOriginY == (($shapeDataPoints[1].to_f)/100)*$vbox_height))
|
||||
# do not update the rectangle count
|
||||
else
|
||||
$rectangle_count = $rectangle_count + 1
|
||||
end
|
||||
$xml.g(:class => :shape, :id => "draw#{$shapeCreationTime}", :undo => $shapeUndoTime, :shape => "rect#{$rectangle_count}", :style => "stroke:\##{$colour_hex}; stroke-width:#{$shapeThickness}; visibility:hidden; fill:none") do
|
||||
$originX = (($shapeDataPoints[0].to_f)/100)*$vbox_width
|
||||
$originY = (($shapeDataPoints[1].to_f)/100)*$vbox_height
|
||||
$originalOriginX = $originX
|
||||
$originalOriginY = $originY
|
||||
rectWidth = (($shapeDataPoints[2].to_f - $shapeDataPoints[0].to_f)/100)*$vbox_width
|
||||
rectHeight = (($shapeDataPoints[3].to_f - $shapeDataPoints[1].to_f)/100)*$vbox_height
|
||||
|
||||
# Cannot have a negative height or width so we adjust
|
||||
if(rectHeight < 0)
|
||||
$originY = $originY + rectHeight
|
||||
rectHeight = rectHeight.abs
|
||||
end
|
||||
if(rectWidth < 0)
|
||||
$originX = $originX + rectWidth
|
||||
rectWidth = rectWidth.abs
|
||||
end
|
||||
$xml.rect(:x => $originX, :y => $originY, :width => rectWidth, :height => rectHeight)
|
||||
$prev_time = $shapeCreationTime
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def storeEllipseShape
|
||||
if($shapeCreationTime != $prev_time)
|
||||
if(($originalOriginX == (($shapeDataPoints[0].to_f)/100)*$vbox_width) && ($originalOriginY == (($shapeDataPoints[1].to_f)/100)*$vbox_height))
|
||||
# do not update the rectangle count
|
||||
else
|
||||
$ellipse_count = $ellipse_count + 1
|
||||
end # end (($originalOriginX == (($shapeDataPoints[0].to_f)/100)*$vbox_width) && ($originalOriginY == (($shapeDataPoints[1].to_f)/100)*$vbox_height))
|
||||
$xml.g(:class => :shape, :id => "draw#{$shapeCreationTime}", :undo => $shapeUndoTime, :shape => "ellipse#{$ellipse_count}", :style =>"stroke:\##{$colour_hex}; stroke-width:#{$shapeThickness}; visibility:hidden; fill:none") do
|
||||
$originX = (($shapeDataPoints[0].to_f)/100)*$vbox_width
|
||||
$originY = (($shapeDataPoints[1].to_f)/100)*$vbox_height
|
||||
$originalOriginX = $originX
|
||||
$originalOriginY = $originY
|
||||
ellipseWidth = (($shapeDataPoints[2].to_f - $shapeDataPoints[0].to_f)/100)*$vbox_width
|
||||
ellipseHeight = (($shapeDataPoints[3].to_f - $shapeDataPoints[1].to_f)/100)*$vbox_height
|
||||
if(ellipseHeight < 0)
|
||||
$originY = $originY + ellipseHeight
|
||||
ellipseHeight = ellipseHeight.abs
|
||||
end
|
||||
if(ellipseWidth < 0)
|
||||
$originX = $originX + ellipseWidth
|
||||
ellipseWidth = ellipseWidth.abs
|
||||
end
|
||||
$xml.ellipse(:cx => $originX+(ellipseWidth/2), :cy => $originY+(ellipseHeight/2), :rx => ellipseWidth/2, :ry => ellipseHeight/2)
|
||||
$prev_time = $shapeCreationTime
|
||||
end # end xml.g
|
||||
end # end if($shapeCreationTime != $prev_time)
|
||||
end
|
||||
|
||||
|
||||
def storeTextShape
|
||||
if($shapeCreationTime != $prev_time)
|
||||
$xml.g(:class => :shape, :id => "draw#{$shapeCreationTime}", :undo => $shapeUndoTime, :shape => "text#{$text_count}", :style => "fill:\##{$colour_hex}; visibility:hidden; font-family: #{$textFontType}; font-size: #{$textFontSize};") do
|
||||
$xml.text_(:x => "#{(($shapeDataPoints[0].to_f)/100)*$vbox_width}", :y => "#{(($shapeDataPoints[1].to_f)/100)*$vbox_height}") do
|
||||
$xml.text($textValue)
|
||||
end
|
||||
$prev_time = $shapeCreationTime
|
||||
end # end xml.g
|
||||
end # end if($shapeCreationTime != $prev_time)
|
||||
end
|
||||
|
||||
def processSlideEvents
|
||||
BigBlueButton.logger.info("Slide events processing")
|
||||
# For each slide (there is only one image per slide)
|
||||
$slides_events.each do |node|
|
||||
eventname = node['eventname']
|
||||
if eventname == "SharePresentationEvent"
|
||||
$presentation_name = node.xpath(".//presentationName")[0].text()
|
||||
else
|
||||
slide_timestamp = node[:timestamp]
|
||||
slide_start = ((slide_timestamp.to_f - $meeting_start.to_f) / 1000).round(1)
|
||||
slide_number = node.xpath(".//slide")[0].text()
|
||||
slide_src = "presentation/#{$presentation_name}/slide-#{slide_number.to_i + 1}.png"
|
||||
|
||||
image_url = "#{$process_dir}/#{slide_src}"
|
||||
slide_size = FastImage.size(image_url)
|
||||
current_index = $slides_events.index(node)
|
||||
if(current_index + 1 < $slides_events.length)
|
||||
slide_end = (( $slides_events[current_index + 1][:timestamp].to_f - $meeting_start.to_f ) / 1000).round(1)
|
||||
else
|
||||
slide_end = (( $meeting_end.to_f - $meeting_start.to_f ) / 1000).round(1)
|
||||
end
|
||||
|
||||
BigBlueButton.logger.info("Processing slide image")
|
||||
# Is this a new image or one previously viewed?
|
||||
if($slides_compiled[[slide_src, slide_size[1], slide_size[0]]] == nil)
|
||||
# If it is, add it to the list with all the data.
|
||||
$slides_compiled[[slide_src, slide_size[1], slide_size[0]]] = [[slide_start], [slide_end], $global_slide_count]
|
||||
$global_slide_count = $global_slide_count + 1
|
||||
elsif
|
||||
# If not, append new in and out times to the old entry
|
||||
$slides_compiled[[slide_src, slide_size[1], slide_size[0]]][0] << slide_start
|
||||
$slides_compiled[[slide_src, slide_size[1], slide_size[0]]][1] << slide_end
|
||||
end
|
||||
|
||||
$ss[(slide_start..slide_end)] = slide_size # store the size of the slide at that range of time
|
||||
puts "#{slide_src} : #{slide_start} -> #{slide_end}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def processShapesAndClears
|
||||
# Create shapes.svg file from the events.xml
|
||||
BigBlueButton.logger.info("Creating shapes.svg")
|
||||
$shapes_svg = Nokogiri::XML::Builder.new do |xml|
|
||||
$xml = xml
|
||||
|
||||
processClearEvents()
|
||||
processUndoEvents()
|
||||
|
||||
# Put in the last clear events numbers (previous clear to the end of the slideshow)
|
||||
endPresentationTime = (($end_time - $join_time)/1000).round(1)
|
||||
$clearPageTimes[($prev_clear_time..endPresentationTime)] = [$pageCleared, $canvas_number, nil, nil]
|
||||
|
||||
# Put the headers on the svg xml file.
|
||||
$xml.doc.create_internal_subset('svg', "-//W3C//DTD SVG 1.1//EN", "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd")
|
||||
$xml.svg(:id => :svgfile, :style => 'position:absolute; height:600px; width:800px;', :xmlns => 'http://www.w3.org/2000/svg', 'xmlns:xlink' => 'http://www.w3.org/1999/xlink', :version => '1.1', :viewBox => :'0 0 800 600') do
|
||||
|
||||
# This is for the first image. It is a placeholder for an image that doesn't exist.
|
||||
$xml.image(:id => :image0, :in => 0, :out => $first_slide_start, :src => "logo.png", :width => 800)
|
||||
$xml.g(:class => :canvas, :id => :canvas0, :image => :image0, :display => :none)
|
||||
$presentation_name = ""
|
||||
|
||||
processSlideEvents()
|
||||
processClearImages()
|
||||
|
||||
BigBlueButton.logger.info("Printing out the gathered images")
|
||||
# Print out the gathered/detected images.
|
||||
$slides_compiled.each do |key, val|
|
||||
$val = val
|
||||
$xml.image(:id => "image#{$val[2].to_i}", :in => $val[0].join(' '), :out => $val[1].join(' '), 'xlink:href' => key[0], :height => key[1], :width => key[2], :visibility => :hidden)
|
||||
$canvas_number+=1
|
||||
$xml.g(:class => :canvas, :id => "canvas#{$val[2].to_i}", :image => "image#{$val[2].to_i}", :display => :none) do
|
||||
|
||||
BigBlueButton.logger.info("Processing shapes within the image")
|
||||
# Select and print the shapes within the current image
|
||||
$shape_events.each do |shape|
|
||||
$shapeTimestamp = shape[:timestamp].to_f
|
||||
$shapeCreationTime = (($shapeTimestamp-$join_time)/1000).round(1)
|
||||
in_this_image = false
|
||||
index = 0
|
||||
numOfTimes = $val[0].length
|
||||
|
||||
# Checks to see if the current shapes are to be drawn in this particular image
|
||||
while((in_this_image == false) && (index < numOfTimes)) do
|
||||
if((($val[0][index].to_f)..($val[1][index].to_f)) === $shapeCreationTime) # is the shape within the certain time of the image
|
||||
in_this_image = true
|
||||
end
|
||||
index+=1
|
||||
end
|
||||
|
||||
if(in_this_image)
|
||||
# Get variables
|
||||
$shapeType = shape.xpath(".//type")[0].text()
|
||||
$shapeThickness = shape.xpath(".//thickness")[0].text()
|
||||
$pageNumber = shape.xpath(".//pageNumber")[0].text()
|
||||
$shapeDataPoints = shape.xpath(".//dataPoints")[0].text().split(",")
|
||||
colour = shape.xpath(".//color")[0].text()
|
||||
|
||||
if($shapeType == "text")
|
||||
$textValue = shape.xpath(".//text")[0].text()
|
||||
$textFontType = shape.xpath(".//font")[0].text()
|
||||
$textFontSize = shape.xpath(".//fontsize")[0].text()
|
||||
end
|
||||
|
||||
# figure out undo time
|
||||
BigBlueButton.logger.info("Figuring out undo time")
|
||||
if($undos.has_key? ((shape[:timestamp].to_f - $join_time)/1000).round(1))
|
||||
$shapeUndoTime = $undos[((shape[:timestamp].to_f - $join_time)/1000).round(1)]
|
||||
else
|
||||
$shapeUndoTime = -1
|
||||
end
|
||||
|
||||
clear_time = -1
|
||||
$clearPageTimes.each do |clearTimeInstance, pageAndCanvasNumbers|
|
||||
$clearTimeInstance = clearTimeInstance
|
||||
$pageAndCanvasNumbers = pageAndCanvasNumbers
|
||||
if(($clearTimeInstance.last > $shapeCreationTime) && ($pageAndCanvasNumbers[3] == "image#{$val[2].to_i}"))
|
||||
if((clear_time > $clearTimeInstance.last) || (clear_time == -1))
|
||||
clear_time = $clearTimeInstance.last
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if($shapeUndoTime == -1)
|
||||
if(clear_time == -1)
|
||||
$shapeUndoTime = -1 # nothing changes
|
||||
elsif(clear_time != -1)
|
||||
$shapeUndoTime = clear_time
|
||||
end
|
||||
elsif($shapeUndoTime != -1)
|
||||
if(clear_time == -1)
|
||||
$shapeUndoTime = $shapeUndoTime #nothing changes
|
||||
elsif (clear_time != -1)
|
||||
if(clear_time < $shapeUndoTime)
|
||||
$shapeUndoTime = clear_time
|
||||
else
|
||||
$shapeUndoTime = $shapeUndoTime # nothing changes
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Process colours
|
||||
$colour_hex = colour.to_i.to_s(16) # convert from base 10 to base 16 (hex)
|
||||
$colour_hex='0'*(6-$colour_hex.length) + $colour_hex # pad the number with 0's to give it a length of 6
|
||||
|
||||
# resolve the current image height and width
|
||||
$ss.each do |t,size|
|
||||
if t === $shapeCreationTime
|
||||
$vbox_width = size[0]
|
||||
$vbox_height = size[1]
|
||||
end
|
||||
end
|
||||
|
||||
# Process the pencil shapes.
|
||||
if $shapeType.eql? "pencil"
|
||||
storePencilShape()
|
||||
|
||||
# Process the rectangle shapes
|
||||
elsif $shapeType.eql? "rectangle"
|
||||
storeRectShape()
|
||||
|
||||
# Process the ellipse shapes
|
||||
elsif $shapeType.eql? "ellipse"
|
||||
storeEllipseShape()
|
||||
|
||||
elsif $shapeType.eql? "text"
|
||||
storeTextShape()
|
||||
end # end if pencil (and other shapes)
|
||||
end # end if((in_this_image) && (in_this_canvas))
|
||||
end # end shape_events.each do |shape|
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def processChatMessages
|
||||
BigBlueButton.logger.info("Processing chat events")
|
||||
# Create slides.xml and chat.
|
||||
$slides_doc = Nokogiri::XML::Builder.new do |xml|
|
||||
$xml = xml
|
||||
$xml.popcorn {
|
||||
# Process chat events.
|
||||
$chat_events.each do |node|
|
||||
chat_timestamp = node[:timestamp]
|
||||
chat_sender = node.xpath(".//sender")[0].text()
|
||||
chat_message = node.xpath(".//message")[0].text()
|
||||
chat_start = (chat_timestamp.to_i - $meeting_start.to_i) / 1000
|
||||
$xml.timeline(:in => chat_start, :direction => :down, :innerHTML => "<span><strong>#{chat_sender}:</strong> #{chat_message}</span>", :target => :chat )
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
$vbox_width = 800
|
||||
$vbox_height = 600
|
||||
$magic_mystery_number = 2
|
||||
$shapesold_svg_filename = 'shapes_old.svg'
|
||||
$shapes_svg_filename = 'shapes.svg'
|
||||
$panzooms_xml_filename = 'panzooms.xml'
|
||||
$cursor_xml_filename = 'cursor.xml'
|
||||
|
||||
$originX = "NaN"
|
||||
$originY = "NaN"
|
||||
$originalOriginX = "NaN"
|
||||
$originalOriginY = "NaN"
|
||||
|
||||
$rectangle_count = 0
|
||||
$line_count = 0
|
||||
$ellipse_count = 0
|
||||
$text_count = 0
|
||||
$global_slide_count = 1
|
||||
$global_page_count = 0
|
||||
$canvas_number = 0
|
||||
$prev_clear_time = 0
|
||||
$pageCleared = "0"
|
||||
$page_number = 0
|
||||
$prev_canvas_time_start = 0 # initial start is 0 seconds. (beginning of video)
|
||||
|
||||
$prev_time = "NaN"
|
||||
|
||||
$ss = {}
|
||||
$clearPageTimes = {}
|
||||
$slides_compiled = {}
|
||||
$undos = {}
|
||||
|
||||
opts = Trollop::options do
|
||||
opt :meeting_id, "Meeting id to archive", :default => '58f4a6b3-cd07-444d-8564-59116cb53974', :type => String
|
||||
end
|
||||
|
||||
$meeting_id = opts[:meeting_id]
|
||||
puts $meeting_id
|
||||
match = /(.*)-(.*)/.match $meeting_id
|
||||
$meeting_id = match[1]
|
||||
$playback = match[2]
|
||||
|
||||
puts $meeting_id
|
||||
puts $playback
|
||||
if ($playback == "slides")
|
||||
logger = Logger.new("/var/log/bigbluebutton/slides/publish-#{$meeting_id}.log", 'daily' )
|
||||
BigBlueButton.logger = logger
|
||||
BigBlueButton.logger.info("RUNNING SLIDES_NEW.RB - Publishing #{$meeting_id}")
|
||||
# This script lives in scripts/archive/steps while properties.yaml lives in scripts/
|
||||
bbb_props = YAML::load(File.open('../../core/scripts/bigbluebutton.yml'))
|
||||
simple_props = YAML::load(File.open('presentation.yml'))
|
||||
BigBlueButton.logger.info("Setting recording dir")
|
||||
recording_dir = bbb_props['recording_dir']
|
||||
BigBlueButton.logger.info("Setting process dir")
|
||||
$process_dir = "#{recording_dir}/process/presentation/#{$meeting_id}"
|
||||
BigBlueButton.logger.info("setting publish dir")
|
||||
publish_dir = simple_props['publish_dir']
|
||||
BigBlueButton.logger.info("setting playback host")
|
||||
playback_host = simple_props['playback_host']
|
||||
BigBlueButton.logger.info("setting target dir")
|
||||
target_dir = "#{recording_dir}/publish/presentation/#{$meeting_id}"
|
||||
if not FileTest.directory?(target_dir)
|
||||
BigBlueButton.logger.info("Making dir target_dir")
|
||||
FileUtils.mkdir_p target_dir
|
||||
|
||||
package_dir = "#{target_dir}/#{$meeting_id}"
|
||||
BigBlueButton.logger.info("Making dir package_dir")
|
||||
FileUtils.mkdir_p package_dir
|
||||
|
||||
audio_dir = "#{package_dir}/audio"
|
||||
BigBlueButton.logger.info("Making audio dir")
|
||||
FileUtils.mkdir_p audio_dir
|
||||
BigBlueButton.logger.info("Made audio dir - coping this -> #{$process_dir}/audio.ogg to -> #{audio_dir}")
|
||||
FileUtils.cp("#{$process_dir}/audio.ogg", audio_dir)
|
||||
BigBlueButton.logger.info("copied file 1")
|
||||
FileUtils.cp("#{$process_dir}/temp/#{$meeting_id}/audio/recording.wav", audio_dir)
|
||||
BigBlueButton.logger.info("copied file 2")
|
||||
FileUtils.cp("#{$process_dir}/events.xml", package_dir)
|
||||
BigBlueButton.logger.info("copied file 3")
|
||||
BigBlueButton.logger.info("Copying files to package dir")
|
||||
FileUtils.cp_r("#{$process_dir}/presentation", package_dir)
|
||||
BigBlueButton.logger.info("Copied files to package dir")
|
||||
BigBlueButton.logger.info("Creating metadata.xml")
|
||||
# Create metadata.xml
|
||||
b = Builder::XmlMarkup.new(:indent => 2)
|
||||
|
||||
metaxml = b.recording {
|
||||
b.id($meeting_id)
|
||||
b.state("available")
|
||||
b.published(true)
|
||||
# Date Format for recordings: Thu Mar 04 14:05:56 UTC 2010
|
||||
b.start_time(BigBlueButton::Events.first_event_timestamp("#{$process_dir}/events.xml"))
|
||||
b.end_time(BigBlueButton::Events.last_event_timestamp("#{$process_dir}/events.xml"))
|
||||
b.playback {
|
||||
b.format("presentation")
|
||||
b.link("http://#{playback_host}/playback/presentation/playback.html?meetingId=#{$meeting_id}")
|
||||
}
|
||||
b.meta {
|
||||
BigBlueButton::Events.get_meeting_metadata("#{$process_dir}/events.xml").each { |k,v| b.method_missing(k,v) }
|
||||
}
|
||||
}
|
||||
metadata_xml = File.new("#{package_dir}/metadata.xml","w")
|
||||
metadata_xml.write(metaxml)
|
||||
metadata_xml.close
|
||||
BigBlueButton.logger.info("Generating xml for slides and chat")
|
||||
|
||||
#Create slides.xml
|
||||
# presentation_url = "/slides/" + $meeting_id + "/presentation"
|
||||
@doc = Nokogiri::XML(File.open("#{$process_dir}/events.xml"))
|
||||
|
||||
$meeting_start = @doc.xpath("//event[@eventname='ParticipantJoinEvent']")[0][:timestamp]
|
||||
$meeting_end = @doc.xpath("//event[@eventname='EndAndKickAllEvent']").last()[:timestamp]
|
||||
|
||||
first_presentation_start_node = @doc.xpath("//event[@eventname='SharePresentationEvent']")
|
||||
first_presentation_start = $meeting_end
|
||||
if not first_presentation_start_node.empty?
|
||||
first_presentation_start = first_presentation_start_node[0][:timestamp]
|
||||
end
|
||||
$first_slide_start = ((first_presentation_start.to_f - $meeting_start.to_f) / 1000).round(1)
|
||||
|
||||
# Gathering all the events from the events.xml
|
||||
$slides_events = @doc.xpath("//event[@eventname='GotoSlideEvent' or @eventname='SharePresentationEvent']")
|
||||
$chat_events = @doc.xpath("//event[@eventname='PublicChatEvent']")
|
||||
$shape_events = @doc.xpath("//event[@eventname='AddShapeEvent']") # for the creation of shapes
|
||||
$panzoom_events = @doc.xpath("//event[@eventname='ResizeAndMoveSlideEvent']") # for the action of panning and/or zooming
|
||||
$cursor_events = @doc.xpath("//event[@eventname='CursorMoveEvent']")
|
||||
$clear_page_events = @doc.xpath("//event[@eventname='ClearPageEvent']") # for clearing the svg image
|
||||
$undo_events = @doc.xpath("//event[@eventname='UndoShapeEvent']") # for undoing shapes.
|
||||
$join_time = @doc.xpath("//event[@eventname='ParticipantJoinEvent']")[0][:timestamp].to_f
|
||||
$end_time = @doc.xpath("//event[@eventname='EndAndKickAllEvent']")[0][:timestamp].to_f
|
||||
|
||||
processChatMessages()
|
||||
|
||||
processShapesAndClears()
|
||||
|
||||
processPanAndZooms()
|
||||
|
||||
BigBlueButton.logger.info("Cursor events empty: #{$cursor_events.empty?}")
|
||||
processCursorEvents()
|
||||
|
||||
|
||||
|
||||
BigBlueButton.logger.info("writing slides_new.xml")
|
||||
# Write slides.xml to file
|
||||
File.open("#{package_dir}/slides_new.xml", 'w') { |f| f.puts $slides_doc.to_xml }
|
||||
|
||||
BigBlueButton.logger.info("Wrote slides_new.xml")
|
||||
|
||||
# Write shapes.svg to file
|
||||
File.open("#{package_dir}/#{$shapes_svg_filename}", 'w') { |f| f.puts $shapes_svg.to_xml.gsub(%r"\s*\<g.*/\>", "") } #.gsub(%r"\s*\<g.*\>\s*\</g\>", "") }
|
||||
|
||||
# Write panzooms.xml to file
|
||||
File.open("#{package_dir}/#{$panzooms_xml_filename}", 'w') { |f| f.puts $panzooms_xml.to_xml }
|
||||
|
||||
# Write panzooms.xml to file
|
||||
File.open("#{package_dir}/#{$cursor_xml_filename}", 'w') { |f| f.puts $cursor_xml.to_xml }
|
||||
|
||||
BigBlueButton.logger.info("Publishing slides")
|
||||
# Now publish this recording files by copying them into the publish folder.
|
||||
if not FileTest.directory?(publish_dir)
|
||||
FileUtils.mkdir_p publish_dir
|
||||
end
|
||||
FileUtils.cp_r(package_dir, publish_dir) # Copy all the files.
|
||||
BigBlueButton.logger.info("Finished publishing script presentation.rb successfully.")
|
||||
else
|
||||
BigBlueButton.logger.info("#{target_dir} is already there")
|
||||
end
|
||||
end
|
||||
|
||||
performance_end = Time.now
|
||||
|
Loading…
Reference in New Issue
Block a user