316 lines
8.3 KiB
JavaScript
316 lines
8.3 KiB
JavaScript
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");
|
|
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");
|
|
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");
|
|
};
|