window.conference.live = (() => {
let config;
let lang;
let data = {};
let confStart;
let confEnd;
let confDur;
let stream;
let streamPause;
let streamPrepend;
let streamExtend;
let demo;
let demoStart;
let demoEnd;
let demoDur;
let demoPause;
let freezeTime = false;
let timeFrozen = 0;
let timeOffset = 0;
let liveTimer;
let streamVideoTimer;
let streamInfoTimer;
let streamModal;
const loadData = () => {
// Load schedule
const request = new Request(window.conference.config.baseurl + '/assets/js/data.json');
fetch(request)
.then(response =>
response.json()
)
.then(d => {
data = d;
})
.catch((error) => {
console.log(error);
});
};
const getData = () => {
// Return data
return data;
};
const mod = (n, m) => {
// Absolute modulo
return ((n % m) + m) % m;
};
const timeNow = () => {
// Current timestamp in seconds
return Math.floor(Date.now() / 1000);
};
const timeCont = () => {
// Continuous time (respecting previous pauses)
return timeNow() - timeOffset;
};
const timeCycle = () => {
// Cyclic timestamp in seconds
const actTime = timeNow();
const relTime = mod(actTime, demoDur + 2*demoPause) / (demoDur + 2*demoPause);
const cycleTime = mod((demoEnd - demoStart) * relTime - timeOffset, (demoEnd - demoStart)) + demoStart;
return cycleTime;
};
const time = () => {
// Return app time
if (freezeTime) {
return timeFrozen;
}
else if (demo) {
return timeCycle();
}
else {
return timeCont();
}
};
const pauseTime = () => {
// Pause app time
if (!freezeTime) {
timeFrozen = time();
freezeTime = true;
stopUpdate();
}
};
const continueTime = () => {
// Continue app time
if (freezeTime) {
freezeTime = false;
timeOffset += time() - timeFrozen;
startUpdate();
}
};
const resetTime = () => {
// Reset app time
timeOffset = 0;
freezeTime = false;
startUpdate();
};
const setTime = (newTime, newDay) => {
// Set and pause app time
pauseTime();
if (!('days' in data)) {
console.log('Data is not loaded yet!')
return
}
let dayIdx;
if (!newDay) {
dayIdx = 0;
}
else if (Number.isInteger(newDay)) {
dayIdx = newDay-1;
}
else if (/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(newDay)) {
dayIdx = data.days.find(o => o.name === newDay);
}
else {
dayIdx = data.days.find(o => o.name === newDay);
}
const newDate = data.days[dayIdx].date;
let d = new Date(newDate);
newTime = newTime.split(':');
d.setHours(newTime[0], newTime[1]);
timeFrozen = Math.floor(d.getTime() / 1000);
update();
};
const getTime = () => {
// Return app time as string
const tConvert = time();
const d = new Date(tConvert * 1000);
const dStr = d.toISOString().slice(0,10);
const h = d.getHours();
const m = d.getMinutes();
return dStr +" "+ h +":"+ (m < 10 ? "0" : "") + m;
};
const timeUnit = () => {
// App time refresh rate
if (demo) {
return 0.1;
}
else {
return 60;
}
};
const delayStart = (startTime) => {
// Seconds until given startTime occurs
const tNow = time();
const tUnit = timeUnit();
if (demo) {
// Convert virtual duration to real duration
return mod(startTime - tNow, demoEnd - demoStart) / (demoEnd - demoStart) * (demoDur + 2*demoPause);
}
else {
if (startTime > tNow) {
return startTime - tNow;
}
else {
// Start on the unit
return (tUnit - (tNow % tUnit));
}
}
};
const model = {
set demo(value) {
demo = value;
resetTime();
},
get demo() {
return demo;
}
};
const updateLive = () => {
// Update status all live elements in DOM
const tNow = time();
const liveShow = document.getElementsByClassName('live-show');
const liveHide = document.getElementsByClassName('live-hide');
const liveTime = document.getElementsByClassName('live-time');
const livePast = document.getElementsByClassName('live-past');
// Show elements for a given period
for (let i = 0; i < liveShow.length; i++) {
const tStarts = liveShow[i].dataset.start.split(',');
const tEnds = liveShow[i].dataset.end.split(',');
for (let k = 0; k < tStarts.length; k++) {
if (tNow >= tStarts[k] && tNow < tEnds[k]) {
// Show when active
liveShow[i].classList.remove('d-none');
break;
}
else if (!liveShow[i].classList.contains('d-none')) {
// Hide otherwise
liveShow[i].classList.add('d-none');
}
}
}
// Hide elements for a given period
for (let i = 0; i < liveHide.length; i++) {
const tStarts = liveHide[i].dataset.start.split(',');
const tEnds = liveHide[i].dataset.end.split(',');
for (let k = 0; k < tStarts.length; k++) {
if (tNow >= tStarts[k] && tNow < tEnds[k]) {
// Hide when active
if (!liveHide[i].classList.contains('d-none')) {
liveHide[i].classList.add('d-none');
break;
}
}
else {
// Show otherwise
liveHide[i].classList.remove('d-none');
}
}
}
// Update duration string for given elements
for (let i = 0; i < liveTime.length; i++) {
const t = liveTime[i].dataset.time;
if (typeof t == "undefined") {
break;
}
let tRel = tNow - t;
let tStr;
if (tRel >= -60 && tRel < 0) {
tStr = lang.time.soon;
}
else if (tRel >= 0 && tRel < 60) {
tStr = lang.time.now;
}
else {
if (tRel < 0) {
tStr = lang.time.in;
}
else {
tStr = lang.time.since;
}
tRel = Math.abs(tRel);
let dWeeks = Math.floor(tRel / (7*24*60*60));
let dDays = Math.floor(tRel / (24*60*60));
let dHours = Math.floor(tRel / (60*60));
let dMins = Math.floor(tRel / (60));
if (dWeeks > 4) {
break;
}
else if (dWeeks > 1) {
tStr += dWeeks +' '+ lang.time.weeks;
}
else if (dWeeks == 1) {
tStr += '1 '+ lang.time.week;
}
else if (dDays > 1) {
tStr += dDays +' '+ lang.time.days;
}
else if (dDays == 1) {
tStr += '1 '+ lang.time.day;
}
else if (dHours > 1) {
tStr += dHours +' '+ lang.time.hours;
}
else if (dHours == 1) {
tStr += '1 '+ lang.time.hour;
}
else if (dMins > 1) {
tStr += dMins +' '+ lang.time.minutes;
}
else {
tStr += '1 '+ lang.time.minute;
}
}
liveTime[i].innerHTML = tStr;
}
// Disable elements for a given period
for (let i = 0; i < livePast.length; i++) {
const t = livePast[i].dataset.time;
if (typeof t == "undefined") {
break;
}
let tRel = tNow - t;
if (tRel < 0) {
if (livePast[i].nodeName == 'A' || livePast[i].nodeName == 'BUTTON') {
// Disable when in past
if (!livePast[i].classList.contains('disabled')) {
livePast[i].classList.add('disabled');
}
}
else {
// Grey out when in past
if (!livePast[i].classList.contains('text-secondary')) {
livePast[i].classList.add('text-secondary');
}
}
}
else {
// Show normal otherwise
livePast[i].classList.remove('disabled');
livePast[i].classList.remove('text-secondary');
}
}
// Cancel timer after program is over
if (tNow > confEnd && !demo) {
stopUpdateLive();
}
};
const startUpdateLive = () => {
// Start update timer to update live elements in DOM
stopUpdateLive();
updateLive();
if (demo) {
// Immediate start required since delayStart would wait for next wrap around
liveTimer = setInterval(updateLive, timeUnit() * 1000);
}
else {
setTimeout(() => {
liveTimer = setInterval(updateLive, timeUnit() * 1000);
updateLive();
}, delayStart(confStart) * 1000);
}
};
const stopUpdateLive = () => {
// stopUpdate update timer to update live elements in DOM
if (typeof liveTimer !== "undefined") {
clearInterval(liveTimer);
}
};
const getRoom = (roomName) => {
// Verify if data is already loaded and object populated
if ('rooms' in data && Object.keys(data.rooms).length > 0) {
// Return room object for given room name
if (roomName in data.rooms) {
return data.rooms[roomName];
}
else {
return data.rooms[Object.keys(data.rooms)[0]];
}
}
else {
console.log('Cannot read rooms as data is not loaded yet!')
return {}
}
};
const getAllTalks = () => {
if ('talks' in data && Object.keys(data.talks).length > 0) {
return data.talks
}
else {
console.log('Cannot read talks as data is not loaded yet!')
return {}
}
};
const getTalks = (roomName) => {
if (roomName in getAllTalks()) {
return data.talks[roomName].map((talk) => {
// For talks with live links, add some grace period to the end
// time in order to prevent that the next talk is announced
// immediately
const end = talk.live_links && talk.live_links.length > 0 ?
talk.end + streamExtend * 60 : talk.end;
return { ...talk, end };
});
}
else {
return [];
}
};
const getNextTalk = (roomName) => {
// Get talk object for next talk in given room
const timeNow = time();
const talksHere = getTalks(roomName);
if (talksHere.length > 0) {
if (timeNow < talksHere[talksHere.length-1].end) {
for (let i = 0; i < talksHere.length; i++) {
if (timeNow < talksHere[i].end) {
return talksHere[i];
}
}
}
}
return {};
};
const getSpeaker = (speakerName) => {
if ('speakers' in data && Object.keys(data.speakers).length > 0) {
if (speakerName in data.speakers) {
return data.speakers[speakerName]
}
}
console.log('Cannot read speakers as data is not loaded yet!')
return {}
};
const getNextPause = (roomName) => {
// Get time object for next pause in given room
const timeNow = time();
const talksHere = getTalks(roomName);
if (talksHere.length > 0) {
if (timeNow < talksHere[talksHere.length-1].end) {
for (let i = 1; i < talksHere.length; i++) {
if (timeNow < talksHere[i].start && streamPause*60 <= talksHere[i].start - talksHere[i-1].end) {
return {
'start': talksHere[i-1].end,
'end': talksHere[i].start,
};
}
}
}
}
return false;
};
const setStreamIframeContent = (content) => {
// Set stream modal iframe to show given text
streamModal.find('iframe').attr('src', '');
streamModal.find('iframe').addClass('d-none');
streamModal.find('#stream-placeholder > div').text(content);
streamModal.find('#stream-placeholder').addClass('d-flex');
};
const setStreamIframeSrc = (href) => {
// Set stream modal iframe to show given URL
streamModal.find('iframe').attr('src', href);
streamModal.find('#stream-placeholder').addClass('d-none').removeClass('d-flex');
streamModal.find('iframe').removeClass('d-none');
};
const setStreamVideo = (roomName) => {
// Update stream modal iframe:
// Show stream with start/pause/end message (for given room) and keep updated
const timeNow = time();
let roomStart, roomEnd;
let talksHere = getTalks(roomName);
if (talksHere.length > 0) {
roomStart = talksHere[0].start;
roomEnd = talksHere[talksHere.length-1].end;
}
else {
// If no program for given room, take overall first and last talk
roomStart = 0;
roomEnd = 0;
for (let roomNameTalk in getAllTalks()) {
talksHere = getTalks(roomNameTalk);
if (talksHere.length > 0) {
const crntRoomStart = talksHere[0].start;
const crntRoomEnd = talksHere[talksHere.length-1].end;
if (roomStart == 0 || roomStart > crntRoomStart) {
roomStart = crntRoomStart;
}
if (roomEnd == 0 || roomEnd < crntRoomEnd) {
roomEnd = crntRoomEnd;
}
}
}
}
if (typeof streamVideoTimer !== "undefined") {
clearInterval(streamVideoTimer);
}
// Conference not yet started
if (timeNow <= roomStart - streamPrepend*60) {
setStreamIframeContent(lang.pre_stream);
if (!freezeTime) {
streamVideoTimer = setTimeout(setStreamVideo, delayStart(roomStart - streamPrepend*60) * 1000, roomName);
}
}
// Conference is over
else if (timeNow >= roomEnd + streamExtend*60) {
setStreamIframeContent(lang.post_stream);
if (!freezeTime && demo) {
streamVideoTimer = setTimeout(setStreamVideo, delayStart(roomEnd - streamPrepend*60) * 1000, roomName);
}
}
// Conference ongoing
else {
const pauseNext = getNextPause(roomName);
// Currently stream is paused
if (pauseNext && timeNow >= pauseNext.start + streamExtend*60 && timeNow <= pauseNext.end - streamPrepend*60) {
setStreamIframeContent(lang.pause_stream);
if (!freezeTime) {
streamVideoTimer = setTimeout(setStreamVideo, delayStart(pauseNext.end - streamPrepend*60) * 1000, roomName);
}
}
// Currently a talk is active
else {
const room = getRoom(roomName);
if (room !== {}) {
setStreamIframeSrc(room.href);
if (!freezeTime) {
if (pauseNext) {
streamVideoTimer = setTimeout(setStreamVideo, delayStart(pauseNext.start + streamExtend*60) * 1000, roomName);
}
else {
streamVideoTimer = setTimeout(setStreamVideo, delayStart(roomEnd + streamExtend*60) * 1000, roomName);
}
}
}
}
}
};
const setStreamInfo = (roomName) => {
// Update stream modal info bar:
// Show next talk and speaker (for given room) and keep updated
const timeNow = time();
const talkNext = getNextTalk(roomName);
if (typeof streamInfoTimer !== "undefined") {
clearInterval(streamInfoTimer);
}
if (talkNext !== {} && timeNow >= talkNext.start - streamPause*60) {
document.getElementById('stream-info').dataset.time = talkNext.start;
document.getElementById('stream-info-time').dataset.time = talkNext.start;
streamModal.find('#stream-info-color').removeClass((index, className) => {
return (className.match(/(^|\s)border-soft-\S+/g) || []).join(' ');
});
streamModal.find('#stream-info-color').addClass('border-soft-' + talkNext.color);
streamModal.find('#stream-info-talk').text(talkNext.name).attr('href', talkNext.href);
let speakerStr = '';
for (let i = 0; i < talkNext.speakers.length; i++) {
let speaker = getSpeaker(talkNext.speakers[i]);
if (speaker == {}) {
speakerStr += talkNext.speakers[i] +', '
}
else if (speaker.href == '') {
speakerStr += speaker.name +', '
}
else {
speakerStr += ''+ speaker.name +', ';
}
}
speakerStr = speakerStr.slice(0, -2);
streamModal.find('#stream-info-speakers').html(speakerStr);
if (talkNext.live_links) {
let linksStr = '';
for (let i = 0; i < talkNext.live_links.length; i++) {
const link = talkNext.live_links[i];
// Skip empty links
if ((link.name == '' && !link.icon) || link.href == '') {
continue;
}
linksStr += '';
if (link.icon) {
linksStr += ' ';
}
linksStr += link.name + '';
}
streamModal.find('#stream-info-links').html(linksStr).removeClass('d-none');
}
else {
streamModal.find('#stream-info-links').addClass('d-none');
}
streamModal.find('#stream-info').removeClass('d-none');
updateLive();
if (!freezeTime) {
streamInfoTimer = setTimeout(setStreamInfo, delayStart(talkNext.end) * 1000, roomName);
}
}
else {
streamModal.find('#stream-info').addClass('d-none');
if (!freezeTime) {
if (talkNext) {
streamInfoTimer = setTimeout(setStreamInfo, delayStart(talkNext.start - streamPause*60) * 1000, roomName);
}
else if (demo) {
let talksHere = getTalks(roomName);
if (talksHere.length > 0) {
streamInfoTimer = setTimeout(setStreamInfo, delayStart(talksHere[0].start - streamPrepend*60) * 1000, roomName);
}
}
}
}
};
const setStream = (roomName) => {
// Update stream modal (iframe and info bar) for given room
streamModal.find('.modal-footer .btn').removeClass('active');
streamModal.find('#stream-select').val(0);
// Recover room name in case of empty default
const room = getRoom(roomName);
if (room !== {}) {
roomName = room.name;
setStreamVideo(roomName);
setStreamInfo(roomName);
streamModal.find('#stream-button' + room.id).addClass('active');
streamModal.find('#stream-select').val(room.id);
}
};
const updateStream = () => {
// Update stream modal for currently active room button
if (streamModal.hasClass('show')) {
let activeButton = streamModal.find('.modal-footer .btn.active');
let roomName = activeButton.data('room');
if (typeof roomName !== "undefined") {
setStream(roomName);
}
}
};
const stopUpdateStream = () => {
// Stop stream modal update timer
if (typeof streamVideoTimer !== "undefined") {
clearInterval(streamVideoTimer);
}
if (typeof streamInfoTimer !== "undefined") {
clearInterval(streamInfoTimer);
}
};
const hideModal = () => {
// Close stream modal
streamModal.find('iframe').attr('src', '');
streamModal.find('.modal-footer .btn').removeClass('active');
streamModal.find('#stream-select').selectedIndex = -1;
};
const setupStream = () => {
// Setup events when modal opens/closes
streamModal = $('#stream-modal');
// configure modal opening buttons
streamModal.on('show.bs.modal', (event) => {
let button = $(event.relatedTarget);
let roomName = button.data('room');
setStream(roomName);
});
streamModal.on('hide.bs.modal', () => {
hideModal();
});
// configure room selection buttons in modal
streamModal.find('.modal-footer .btn').on('click', function (event) {
event.preventDefault();
let roomName = $(this).data('room');
setStream(roomName);
});
// configure room selection menu in modal
streamModal.find('#stream-select').on('change', function (event) {
event.preventDefault();
let roomName = $(this).children('option:selected').text();
setStream(roomName);
});
};
const init = (c, l) => {
config = c;
lang = l;
confStart = config.time.start;
confEnd = config.time.end;
confDur = confEnd - confStart;
demo = config.demo.enable;
demoDur = config.demo.duration;
demoPause = config.demo.pause;
demoStart = confStart - confDur/demoDur*demoPause;
demoEnd = confEnd + confDur/demoDur*demoPause;
stream = config.streaming.enable;
streamPause = config.streaming.pause;
streamPrepend = config.streaming.prepend;
streamExtend = config.streaming.extend;
loadData();
startUpdateLive();
if (stream) {
setupStream();
}
};
const update = () => {
updateLive();
if (stream) {
updateStream();
}
};
const startUpdate = () => {
startUpdateLive();
if (stream) {
updateStream();
}
};
const stopUpdate = () => {
stopUpdateLive();
if (stream) {
stopUpdateStream();
}
};
return {
init: init,
getData: getData,
pauseTime: pauseTime,
continueTime: continueTime,
resetTime: resetTime,
setTime: setTime,
getTime: getTime,
demo: model.demo
};
})();