Created
May 15, 2024 14:54
-
-
Save chrismcfee/8b811b376cfbee426bc945cbfd485e17 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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