diff --git a/frontend.py b/frontend.py index 9fb71e2..e80ffe4 100644 --- a/frontend.py +++ b/frontend.py @@ -61,6 +61,8 @@ socket.setdefaulttimeout(3) # for mqtt +APP_STARTUP_TIME = int(datetime.now().timestamp()) + class SubmissionsCollector(Collector): def collect(self) -> Iterable[Metric]: @@ -480,6 +482,32 @@ def metrics(): return generate_latest() +@app.route("/slideshow") +def slideshow(): + return render_template("slideshow.jinja", APP_STARTUP_TIME=APP_STARTUP_TIME) + + +@app.route("/api/slideshow/content") +def api_slideshow_content(): + assets = [a.to_dict() for a in get_all_live_assets()] + resp = jsonify( + { + a["id"]: { + "url": a["url"], + "type": a["filetype"], + } + for a in assets + } + ) + resp.headers["Cache-Control"] = "public, max-age=30" + return resp + + +@app.route("/api/startup") +def app_startup_time(): + return str(APP_STARTUP_TIME) + + # @app.route("/content/last") # def content_last(): # assets = get_all_live_assets() diff --git a/static/slideshow.css b/static/slideshow.css new file mode 100644 index 0000000..1491262 --- /dev/null +++ b/static/slideshow.css @@ -0,0 +1,26 @@ +* { + margin: 0; + padding: 0; + background-color: black; +} + +#slideshow, #error { + margin: auto 0; + text-align: center; +} + +#error { + font-size: 100px; + margin-top: 200px; + color: white; + font-family: sans-serif; +} + +img, video { + display: block; + width: auto; + height: auto; + max-width: 100vw; + max-height: 100vh; + margin: 0 auto; +} diff --git a/static/slideshow.js b/static/slideshow.js new file mode 100644 index 0000000..725dc14 --- /dev/null +++ b/static/slideshow.js @@ -0,0 +1,128 @@ +document.getElementById("slideshow").style.display = "none"; + +// the list of live assets, as we got it from the api +content = {}; + +// the list of live asset ids, shuffled at the start of each run +content_shuffled = []; +currently_showing = 0; + +function xhr_get(url, callback_func) { + req = new XMLHttpRequest(); + req.timeout = 10000; + req.open('GET', url); + req.setRequestHeader('Accept', 'application/json'); + req.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + req.addEventListener('load', function(event) { + if (req.status != 200) { + return; + } + + callback_func(event); + }); + req.send(); +} + +// from https://stackoverflow.com/a/12646864 +function shuffle_content() { + array = Object.keys(content); + for (let i = array.length - 1; i >= 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + content_shuffled = array; +} + +// Auto-reload slideshow in case the backend has restarted. We need this +// to ensure we're running the latest code, without needing to press +// reload on every display individually. +window.setInterval(function() { + console.info('checking if slideshow needs reloading because server has restarted'); + xhr_get('/api/startup', function() { + startup = parseInt(req.responseText); + if (startup > 0 && app_startup < startup) { + console.warn('startup time has changed, reloading GUI'); + window.location.reload(); + } else { + console.info('slideshow does not need reloading'); + } + }); +}, 42000); + +// Load the list of live assets. +function get_live_assets() { + console.info('loading live assets'); + xhr_get('/api/slideshow/content', function() { + content = JSON.parse(req.responseText); + console.info("got live assets, " + Object.keys(content).length + " assets in total"); + }); +} + +get_live_assets(); +window.setInterval(get_live_assets, 30000); + +// The actual magic starts here. This function knows about the current +// position in the slideshow and automatically selects the next available +// picture. If the current picture cannot be found in the list (aka it was +// deleted or expired) or we reach the end of the list, we start again +// from the beginning. In any case, we load the image into a temporary +// element, wait for it to finish loading, then replace the currently +// showing image. This ensures there's always an image showing. +function get_next_asset_to_show() { + if (content_shuffled.length === 0) { + shuffle_content(); + } + + // iterate over the shuffled content array to find the current asset, + // then return the next one + for (let i = 0; i < content_shuffled.length-1; i++) { + if (currently_showing == content_shuffled[i]) { + next_asset = content_shuffled[i + 1]; + currently_showing = next_asset; + return content[next_asset]; + } + } + + shuffle_content(); + next_asset = content_shuffled[0]; + currently_showing = next_asset; + return content[next_asset]; +} + +window.setInterval(function() { + document.getElementById("slideshow").style.display = "block"; + document.getElementById("error").style.display = "none"; + + next_asset = get_next_asset_to_show(); + console.info("next asset is " + next_asset['url'] + " of type " + next_asset['type']); + + image = document.getElementById("slideshow-image"); + video = document.getElementById("slideshow-video"); + + if (next_asset['type'] == 'image') { + img = document.createElement("img"); + img.onload = function() { + video.style.display = "none"; + image.style.display = "none"; + image.src = this.src; + image.style.display = "block"; + } + img.src = next_asset['url']; + } else /* if (next_asset['type'] == 'video') { + vid = document.createElement("video"); + vid.onload = function() { + image.style.display = "none"; + video.getElementsByTagName("source")[0].src = this.src; + video.style.display = "block"; + video.play(); + } + vid.src = next_asset['url']; + } else*/ { + /* + document.getElementById("slideshow").style.display = "none"; + document.getElementById("error").style.display = "block"; + document.getElementById("error-text").innerHTML = "unknown asset type " + next_asset["type"]; + */ + console.warn("unknown asset type " + next_asset["type"] + " for asset " + next_asset["url"]); + } +}, 10000); diff --git a/templates/layout.jinja b/templates/layout.jinja index 4581ac1..69c9434 100644 --- a/templates/layout.jinja +++ b/templates/layout.jinja @@ -29,6 +29,7 @@