Skip to content

Instantly share code, notes, and snippets.

@chrismcfee
Created May 15, 2024 14:54
Show Gist options
  • Select an option

  • Save chrismcfee/8b811b376cfbee426bc945cbfd485e17 to your computer and use it in GitHub Desktop.

Select an option

Save chrismcfee/8b811b376cfbee426bc945cbfd485e17 to your computer and use it in GitHub Desktop.
define([
"renderer/modules/workerPromise",
"renderer/modules/monitor.webgl",
"renderer/modules/monitor",
], function (WorkerPromise) {
"use strict";
const ZCAST_PLAYERS = [];
const ONE_MINUTE = 60000;
const ONE_HOUR = 60 * ONE_MINUTE;
const ONE_DAY = 24 * ONE_HOUR;
const ONE_WEEK = 7 * ONE_DAY;
const ONE_YEAR = 365 * ONE_DAY;
const DRIFT_ALLOWANCE = 500;
let playerRAF = null;
let startPerf = Math.floor(performance.now());
let startNow = Date.now();
class ZCast {
static get fonts() {
return [
"Arial",
"Latin Modern Roman",
"Open Sans",
"Courier Prime",
"Comic Neue",
"League Gothic",
"MS PGothic",
"Microsoft Yahei",
"Handsome",
"Oswald",
"Gotham Medium",
"Gotham Book",
"Gotham Light",
"Comme",
"Oxygen",
"Pixymbols Digit",
];
}
static play() {
const now = Date.now();
const perf = Math.floor(performance.now());
let diff;
playerRAF = requestAnimationFrame(ZCast.play);
for (let i = 0, total = ZCAST_PLAYERS.length; i < total; i++) {
ZCAST_PLAYERS[i].render(now);
}
diff = perf - startPerf - (now - startNow);
if (Math.abs(diff) > DRIFT_ALLOWANCE) {
playerRAF && cancelAnimationFrame(playerRAF);
playerLog(
"System clock changed by " +
(diff / 1000).toFixed(3) +
" seconds. Max allowed is " +
(DRIFT_ALLOWANCE / 1000).toFixed(3) +
" seconds. Reloading..."
);
window.location.reload();
}
}
static stop() {
playerRAF && cancelAnimationFrame(playerRAF);
for (var i = 0, total = ZCAST_PLAYERS.length; i < total; i++) {
ZCAST_PLAYERS[i].pause();
}
}
static empty() {
for (var i = 0, total = ZCAST_PLAYERS.length; i < total; i++) {
ZCAST_PLAYERS[i].empty();
}
}
constructor(stageElement, serverUrl) {
const device = this;
this.stageElement = stageElement;
this.serverUrl = serverUrl || window.location.origin;
this.serverTimeDelta = 0;
this.RAF = null;
this.data = null;
this.monitors = {};
this.rss = {};
this.waitingForBackground = false;
this.worker = new Worker(
ServerInfo.generateStaticPathFor("renderer/zcast.worker.js")
);
this.worker.onmessage = function (e) {
var els;
if ("WORKER_PROMISE_ID" in e.data) {
WorkerPromise.processResult(e.data);
} else if (e.data.action === "rssUpdate") {
device.rss = e.data.feedInfo;
Array.from(
device.stageElement.querySelectorAll("rss-renderer")
).forEach(function (el) {
if (el.feedId in device.rss) {
el.feed = device.rss[el.feedId];
}
});
for (var id in device.monitors) {
device.monitors[id].updateWidgets();
}
// els = Array.from(device.stageElement.querySelectorAll('widget-renderer'));
// if (els.length > 0) {
// playerLog('Updating widgets...');
// els.forEach(function (el) {
// el.updateWidget();
// });
// }
}
};
this.alertWorker = new Worker(
ServerInfo.generateStaticPathFor("renderer/workers/alerts.js")
);
ZCAST_PLAYERS.push(this);
}
haltRSS() {
this.worker.postMessage({ action: "rssStop" });
}
checkServerTime() {
const device = this;
const syncUrl =
device.serverUrl + "/zcast/command/?action=serverTime&model=device";
if (!navigator.onLine) {
playerLog("Device is offline, unable to sync.");
return Promise.resolve(0);
}
playerLog("Syncing device with server time...");
return new Promise(function (resolve, reject) {
// Sync renderer with server if available
getServerSyncDelta(syncUrl, device.serverTimeDelta).then(
function (delta) {
device.serverTimeDelta = delta;
//window.ZCastWidgets.timeOffset = delta;
showTimeDifference(device.serverTimeDelta);
resolve(device.serverTimeDelta);
},
function (err) {
console.log(err);
playerLog(err);
showTimeDifference(device.serverTimeDelta);
resolve(device.serverTimeDelta);
}
);
});
}
load(url, IS_DEVICE) {
const device = this;
playerLog("Fetching player data...");
return WorkerPromise.send(device.worker, {
action: "loadSchedule",
url: url,
server: device.serverUrl,
IS_DEVICE: IS_DEVICE,
}).then(function (data) {
const backgroundElement = document.getElementById(
"zcast-background-url"
);
device.data = data;
if ("STATIC_URL" in data) {
window.STATIC_URL = data.STATIC_URL;
}
if ("MEDIA_URL" in data) {
window.MEDIA_URL = data.MEDIA_URL;
}
if ("SITE_URL" in data) {
window.SITE_URL = data.SITE_URL;
device.serverUrl = data.SITE_URL;
}
if ("version" in data) {
window.ZC_VERSION = data.version;
}
if ("device" in data) {
if (data.device.assignedFields) {
if (backgroundElement) {
if ("backgroundUrl" in data.device.assignedFields) {
if (!navigator.onLine) {
playerLog(
"Defer loading background URL " +
data.device.assignedFields.backgroundUrl +
" until online..."
);
if (!this.waitingForBackground) {
window.addEventListener("online", function (e) {
backgroundElement.setAttribute(
"src",
device.data.device.assignedFields.backgroundUrl
);
backgroundElement.setAttribute("style", "");
});
this.waitingForBackground = true;
}
} else {
backgroundElement.setAttribute(
"src",
data.device.assignedFields.backgroundUrl
);
backgroundElement.setAttribute("style", "");
}
} else {
backgroundElement.removeAttribute("src");
backgroundElement.setAttribute("style", "display: none");
}
}
if ("alertSource" in data.device.assignedFields) {
device.alertWorker.postMessage(
Object.assign(
{ serverUrl: device.serverUrl },
data.device.assignedFields
)
);
}
}
}
playerLog("Updating monitors...");
return Promise.all(data.monitors.map(device.updateDisplay, device));
});
}
createDisplay(id, webgl) {
const monitor = webgl
? document.createElement("monitor-webgl")
: document.createElement("monitor-display");
monitor.id = "renderer-monitor-" + id;
monitor.displayId = id;
this.stageElement.appendChild(monitor);
this.monitors[id] = monitor;
return monitor;
}
updateDisplay(data) {
var monitor = this.monitors[data.id];
var assignedFields = data.assignedFields || {};
if (!monitor) {
monitor = this.createDisplay(
data.id,
assignedFields.webGL ? true : false
);
}
monitor.displayName = data.name;
monitor.setAttribute("data-name", data.name);
monitor.width = data.displayWidth
? data.displayWidth
: data.resolutionWidth;
monitor.height = data.displayHeight
? data.displayHeight
: data.resolutionHeight;
monitor.left = data.displayLeft;
monitor.top = data.displayTop;
if (data.transform) {
monitor.style.transform = data.transform;
}
monitor.assignedFields = assignedFields;
monitor.setAttribute("data-scheduled", data.scheduled);
return monitor.update(data.content, this);
}
loadDisplays(data) {
const device = this;
device.cleanDisplays(data);
return Promise.all(data.map(device.updateDisplay, device));
}
cleanDisplays(newMonitors) {
for (var id in this.monitors) {
if (!(id in newMonitors)) {
this.removeDisplay(id);
}
}
}
removeDisplay(id) {
this.monitors[id].clear();
this.stageElement.removeChild(this.monitors[id]);
delete this.monitors[id];
}
renderMonitor(id) {
this.monitors[id].render(Date.now() + this.serverTimeDelta);
}
render(now) {
for (var id in this.monitors) {
this.monitors[id].render(now || Date.now());
}
}
empty() {
for (var id in this.monitors) {
this.removeDisplay(id);
}
}
start() {
ZCast.play();
}
pause() {
for (var id in this.monitors) {
this.monitors[id].pause();
}
}
}
function toJSONDataUrl(data) {
return "data:application/json;base64," + btoa(JSON.stringify(data));
}
function playerLog(txt) {
console.log(new Date() + " - " + txt);
}
function showTimeDifference(delta) {
var currentDeviceDate = new Date();
var currentServerDate = new Date(Date.now() + delta);
var diffString;
if (delta !== 0) {
diffString =
"Device is at a " +
timeDifferenceToString(delta) +
" time difference from the server.";
} else {
diffString = "Device and server are in time sync.";
}
playerLog(
diffString +
"\nCurrent device datetime: " +
currentDeviceDate.toLocaleString() +
"\nCurrent server datetime: " +
currentServerDate.toLocaleString()
);
}
function timeDifferenceToString(delta) {
var t = Math.abs(delta);
var h = Math.floor(t / ONE_HOUR);
var m = Math.floor((t - h * ONE_HOUR) / ONE_MINUTE);
var s = (t % ONE_MINUTE) / 1000;
var output = [];
if (h > 0) {
output.push(h + (h > 1 ? " hours" : " hour"));
}
if (m > 0) {
output.push(m + (m > 1 ? " minutes" : " minute"));
}
if (s > 0) {
output.push(s + (s > 1 ? " seconds" : " second"));
}
return output.join(", ") + (delta > 0 ? " behind" : " ahead of");
}
function ajax(opts) {
var xhr = new XMLHttpRequest();
var url = opts.url;
if (!opts.cache) {
url = (url.indexOf("?") === -1 ? "?" : "&") + "_=" + Date.now();
}
try {
xhr.responseType = opts.dataType;
} catch (err) {}
xhr.open(opts.type, opts.url, true);
xhr.onreadystatechange = function (e) {
opts.readyStateChanged && opts.readyStateChanged.call(this, this);
};
xhr.onload = function (e) {
var status, response;
if (xhr.status === 200) {
status = "success";
response =
opts.dataType === "document" ? this.responseXML : this.response;
if (opts.dataType === "json" && typeof response === "string") {
try {
response = JSON.parse(response);
opts.success &&
opts.success.call(this, response, this.status, this);
} catch (err) {
opts.error && opts.error.call(this, this, "error", err.toString());
}
} else {
opts.success && opts.success.call(this, response, this.status, this);
}
} else {
status = "error";
opts.error && opts.error.call(this, this, "error", xhr.statusText);
}
opts.complete && opts.complete.call(this, this, status);
};
xhr.onerror = function (e) {
opts.error && opts.error.call(this, this, "error", this.response);
opts.complete && opts.complete.call(this, this, "error");
};
opts.beforeSend && opts.beforeSend.call(xhr, xhr, { xhr: xhr });
xhr.send();
}
function getServerSyncDelta(syncURL, currentDelta) {
return new Promise(function (resolve, reject) {
var maxSuccesses = 5;
var maxFails = 3;
var successes = 0;
var fails = 0;
var syncData = [];
var sentTime;
var receivedTime;
var syncProps = {
url: syncURL,
type: "GET",
dataType: "json",
cache: false,
beforeSend: function (jqXHR, settings) {
sentTime = Date.now();
},
readyStateChanged: function (xhr) {
if (xhr.readyState === 2) {
receivedTime = Date.now();
}
},
success: function (data, textStatus, jqXHR) {
var tripTime = receivedTime - sentTime;
var clientTime = receivedTime - tripTime / 3;
var serverTime = Math.round(data.serverTime * 1000);
syncData.push(Math.floor(serverTime - clientTime));
successes++;
},
error: function (jqXHR, textStatus, errorThrown) {
fails++;
},
complete: function (jqXHR, textStatus) {
if (successes === maxSuccesses) {
resolve(Math.min.apply(null, syncData));
} else if (fails === maxFails) {
reject("Server unresponsive. Device failed to sync.");
} else {
ajax(syncProps);
}
},
};
ajax(syncProps);
});
}
window.addEventListener("unload", function (e) {
playerLog("Clearing content for restart...");
ZCast.stop();
ZCast.empty();
});
window.ZCast = ZCast;
return ZCast;
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment