Caspar Template
This commit is contained in:
315
public/caspar.js
Normal file
315
public/caspar.js
Normal file
@@ -0,0 +1,315 @@
|
||||
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");
|
||||
};
|
||||
Reference in New Issue
Block a user