const DEFAULTS = { width: 1920, height: 1080, scale: 1, fit: "contain", maxwidth: 500, maxheight: 480, autoplay: true, muted: true, transparent: true, chroma: "#00ff00", readyDelay: 1000, maxWait: 10000, }; let currentReadyTimer; let currentMaxWaitTimer; let autoplayTimer; function toBoolean(value, defaultValue = false) { if (value === undefined || value === null || value === "") { return defaultValue; } if (typeof value === "boolean") { return value; } return ["1", "true", "yes", "on"].includes(String(value).toLowerCase()); } function toNumber(value, defaultValue, min, max) { const number = Number(value); if (!Number.isFinite(number)) { return defaultValue; } return Math.min(Math.max(number, min), max); } function slugify(value = "") { return String(value) .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-|-$/g, "") || "unknown"; } function parseJsonPayload(payload) { try { const parsed = JSON.parse(payload); if (parsed && typeof parsed === "object") { return parsed; } } catch { return null; } return null; } function parseXmlPayload(payload) { const data = {}; const documentXml = new DOMParser().parseFromString(payload, "application/xml"); if (documentXml.querySelector("parsererror")) { return data; } for (const component of documentXml.querySelectorAll("componentData")) { const key = component.getAttribute("id"); const valueNode = component.querySelector("data[id='text'], data[id='value'], data"); const value = valueNode?.getAttribute("value") || valueNode?.textContent; if (key && value) { data[key] = value; } } for (const element of documentXml.documentElement.querySelectorAll("*")) { if (element.children.length === 0 && element.textContent.trim()) { data[element.tagName] = element.textContent.trim(); } } return data; } function parseUpdatePayload(payload) { if (payload && typeof payload === "object") { return payload; } const text = String(payload || "").trim(); if (!text) { return {}; } const json = parseJsonPayload(text); if (json) { return json; } if (/^https?:\/\//i.test(text)) { return { url: text }; } return parseXmlPayload(text); } function normalizeOptions(data) { return { url: data.url || data.URL || data.link || data.source || "", width: toNumber(data.width, DEFAULTS.width, 320, 3840), height: toNumber(data.height, DEFAULTS.height, 240, 2160), scale: toNumber(data.scale, DEFAULTS.scale, 0.25, 3), maxwidth: toNumber(data.maxwidth || data.maxWidth, DEFAULTS.maxwidth, 220, 500), maxheight: toNumber(data.maxheight || data.maxHeight, DEFAULTS.maxheight, 220, 2160), readyDelay: toNumber(data.readyDelay, DEFAULTS.readyDelay, 0, 30000), maxWait: toNumber(data.maxWait, DEFAULTS.maxWait, 1000, 60000), fit: data.fit === "cover" ? "cover" : "contain", autoplay: toBoolean(data.autoplay, DEFAULTS.autoplay), muted: toBoolean(data.muted, DEFAULTS.muted), transparent: toBoolean(data.transparent, DEFAULTS.transparent), chroma: data.chroma || DEFAULTS.chroma, }; } function addAutoplayToUrl(src, options) { try { const url = new URL(src, window.location.href); url.searchParams.set("autoplay", "1"); url.searchParams.set("playsinline", "1"); if (options.muted) { url.searchParams.set("mute", "1"); url.searchParams.set("muted", "1"); } return url.toString(); } catch { return src; } } function normalizeInsertedMedia(root, options) { for (const iframe of root.querySelectorAll("iframe")) { const permissions = new Set( (iframe.getAttribute("allow") || "") .split(";") .map((permission) => permission.trim()) .filter(Boolean), ); permissions.add("autoplay"); permissions.add("fullscreen"); permissions.add("picture-in-picture"); iframe.setAttribute("allow", Array.from(permissions).join("; ")); if (options.autoplay && iframe.src) { iframe.src = addAutoplayToUrl(iframe.src, options); } } for (const video of root.querySelectorAll("video")) { video.autoplay = options.autoplay; video.playsInline = true; video.setAttribute("playsinline", ""); if (options.autoplay) { video.setAttribute("autoplay", ""); } if (options.muted) { video.muted = true; video.defaultMuted = true; video.setAttribute("muted", ""); } } } function executeScripts(root) { for (const script of root.querySelectorAll("script")) { const replacement = document.createElement("script"); for (const attribute of script.attributes) { replacement.setAttribute(attribute.name, attribute.value); } replacement.textContent = script.textContent; script.replaceWith(replacement); } } function applyStageOptions(options) { document.body.style.setProperty("--stage-width", `${options.width}px`); document.body.style.setProperty("--stage-height", `${options.height}px`); document.body.style.setProperty("--scale", String(options.scale)); document.body.style.setProperty("--chroma", options.chroma); document.body.classList.toggle("transparent", options.transparent); document.documentElement.classList.toggle("transparent", options.transparent); const stage = document.querySelector(".stage"); if (!stage) { return; } stage.classList.toggle("fit-cover", options.fit === "cover"); stage.classList.toggle("fit-contain", options.fit !== "cover"); } function hideUntilReady(options) { clearTimeout(currentReadyTimer); clearTimeout(currentMaxWaitTimer); document.body.classList.remove("is-ready"); document.body.classList.add("is-loading"); const reveal = () => { document.body.classList.remove("is-loading"); document.body.classList.add("is-ready"); }; currentReadyTimer = setTimeout(reveal, options.readyDelay); currentMaxWaitTimer = setTimeout(reveal, options.maxWait); } function startAutoplayAssist(root, options) { clearInterval(autoplayTimer); if (!options.autoplay) { return; } const scan = () => { for (const video of root.querySelectorAll("video")) { const promise = video.play?.(); promise?.catch?.(() => {}); } }; scan(); autoplayTimer = setInterval(scan, 1000); } function renderEmbed(oembed, sourceUrl, options) { const embed = document.querySelector(".embed"); if (!embed) { return; } const providerClass = `provider-${slugify(oembed.provider_name)}`; const embedWidth = Math.min(Number(oembed.width) || 500, 500); const embedHeight = Number(oembed.height); embed.className = `embed ${providerClass}`; embed.dataset.source = sourceUrl; embed.style.setProperty("--embed-width", `${embedWidth}px`); if (Number.isFinite(embedHeight) && embedHeight > 0) { embed.style.setProperty("--embed-height", `${embedHeight}px`); } else { embed.style.removeProperty("--embed-height"); } if (oembed.type === "photo" && oembed.url) { const image = document.createElement("img"); image.src = oembed.url; image.alt = oembed.title || ""; embed.replaceChildren(image); } else { embed.innerHTML = oembed.html || ""; } normalizeInsertedMedia(embed, options); executeScripts(embed); startAutoplayAssist(embed, options); hideUntilReady(options); } async function loadEmbed(options) { if (!options.url) { return; } applyStageOptions(options); const requestUrl = new URL("/api/oembed", window.location.origin); requestUrl.searchParams.set("url", options.url); requestUrl.searchParams.set("maxwidth", String(options.maxwidth)); requestUrl.searchParams.set("maxheight", String(options.maxheight)); const response = await fetch(requestUrl); const result = await response.json(); if (!response.ok) { throw new Error(result.error || "Could not load oEmbed data."); } renderEmbed(result.data, options.url, options); } window.update = (payload) => { const options = normalizeOptions(parseUpdatePayload(payload)); loadEmbed(options).catch((error) => { console.error(error); document.body.classList.remove("is-ready"); document.body.classList.add("is-loading"); }); }; window.play = () => { document.body.classList.remove("is-loading"); document.body.classList.add("is-ready"); }; window.stop = () => { clearInterval(autoplayTimer); document.body.classList.remove("is-ready"); document.body.classList.add("is-loading"); };