178 lines
6.0 KiB
JavaScript
178 lines
6.0 KiB
JavaScript
const moduleTypes = new Map();
|
|
const { obsBrowserSourceCss } = require("./obs-browser-defaults");
|
|
|
|
function number(value, fallback, min, max) {
|
|
const parsed = Number(value);
|
|
if (!Number.isFinite(parsed)) return fallback;
|
|
return Math.min(max, Math.max(min, parsed));
|
|
}
|
|
|
|
function text(value, fallback = "", max = 2000) {
|
|
return String(value === undefined || value === null ? fallback : value).slice(0, max);
|
|
}
|
|
|
|
function color(value, fallback = "#ffffff") {
|
|
const normalized = text(value, fallback, 32).trim();
|
|
return /^(#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})|rgba?\([0-9.,%\s]+\)|transparent)$/i.test(normalized)
|
|
? normalized
|
|
: fallback;
|
|
}
|
|
|
|
function url(value, { allowEmpty = true } = {}) {
|
|
const normalized = text(value, "", 2048).trim();
|
|
if (!normalized && allowEmpty) return "";
|
|
try {
|
|
const parsed = new URL(normalized);
|
|
return ["http:", "https:"].includes(parsed.protocol) ? parsed.toString() : "";
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function commonConfig(input = {}) {
|
|
const anchors = [
|
|
"top-left", "top-center", "top-right",
|
|
"center-left", "center", "center-right",
|
|
"bottom-left", "bottom-center", "bottom-right"
|
|
];
|
|
return {
|
|
x: number(input.x, 0, 0, 100),
|
|
y: number(input.y, 0, 0, 100),
|
|
width: number(input.width, 100, 0.1, 100),
|
|
height: number(input.height, 100, 0.1, 100),
|
|
opacity: number(input.opacity, 1, 0, 1),
|
|
anchor: anchors.includes(input.anchor) ? input.anchor : "top-left"
|
|
};
|
|
}
|
|
|
|
function mediaConfig(input = {}, { includeFit = false } = {}) {
|
|
const playBehavior = ["once", "loop", "manual"].includes(input.playBehavior)
|
|
? input.playBehavior
|
|
: "once";
|
|
return {
|
|
...commonConfig(input),
|
|
url: url(input.url),
|
|
playBehavior,
|
|
muted: input.muted === true || input.muted === "on",
|
|
volume: number(input.volume, 1, 0, 1),
|
|
playbackRate: number(input.playbackRate, 1, 0.25, 4),
|
|
startAt: number(input.startAt, 0, 0, 86400),
|
|
showControls: playBehavior === "manual" || input.showControls === true || input.showControls === "on",
|
|
...(includeFit ? { fit: ["contain", "cover", "fill"].includes(input.fit) ? input.fit : "contain" } : {})
|
|
};
|
|
}
|
|
|
|
function registerOverlayModuleType(type) {
|
|
if (!type?.id || typeof type.normalize !== "function") {
|
|
throw new Error("Overlay module types require an id and normalize function.");
|
|
}
|
|
const renderType = type.renderType || type.id;
|
|
if (!["text", "image", "video", "audio", "web"].includes(renderType)) {
|
|
throw new Error("Overlay module renderType must be text, image, video, audio, or web.");
|
|
}
|
|
moduleTypes.set(type.id, Object.freeze({ ...type, renderType }));
|
|
return () => moduleTypes.delete(type.id);
|
|
}
|
|
|
|
function getOverlayModuleType(id) {
|
|
return moduleTypes.get(String(id || "")) || null;
|
|
}
|
|
|
|
function listOverlayModuleTypes() {
|
|
return Array.from(moduleTypes.values()).map(({ id, label, description }) => ({ id, label, description }));
|
|
}
|
|
|
|
function normalizeModuleConfig(typeId, input) {
|
|
const type = getOverlayModuleType(typeId);
|
|
if (!type) throw new Error("Unsupported overlay module type.");
|
|
return type.normalize(input || {});
|
|
}
|
|
|
|
registerOverlayModuleType({
|
|
id: "text",
|
|
label: "Text",
|
|
description: "Positioned text with configurable colors and sizing.",
|
|
normalize(input) {
|
|
return {
|
|
...commonConfig(input),
|
|
text: text(input.text, "New text", 4000),
|
|
fontSize: number(input.fontSize, 48, 8, 400),
|
|
fontWeight: number(input.fontWeight, 700, 100, 900),
|
|
color: color(input.color, "#ffffff"),
|
|
background: color(input.background, "transparent"),
|
|
horizontalAlign: ["left", "center", "right"].includes(input.horizontalAlign)
|
|
? input.horizontalAlign
|
|
: (["left", "center", "right"].includes(input.align) ? input.align : "left"),
|
|
verticalAlign: ["top", "center", "bottom"].includes(input.verticalAlign) ? input.verticalAlign : "center",
|
|
overflow: ["wrap", "shrink", "clip", "single-line", "scroll"].includes(input.overflow)
|
|
? input.overflow
|
|
: "wrap",
|
|
padding: number(input.padding, 0, 0, 200)
|
|
};
|
|
}
|
|
});
|
|
|
|
registerOverlayModuleType({
|
|
id: "image",
|
|
label: "Image",
|
|
description: "A responsive image from an HTTPS or HTTP URL.",
|
|
normalize(input) {
|
|
return {
|
|
...commonConfig(input),
|
|
url: url(input.url),
|
|
fit: ["contain", "cover", "fill"].includes(input.fit) ? input.fit : "contain"
|
|
};
|
|
}
|
|
});
|
|
|
|
registerOverlayModuleType({
|
|
id: "video",
|
|
label: "Video / media",
|
|
description: "Play a video file, including its audio track, with scene-aware playback controls.",
|
|
renderType: "video",
|
|
normalize(input) {
|
|
return mediaConfig(input, { includeFit: true });
|
|
}
|
|
});
|
|
|
|
registerOverlayModuleType({
|
|
id: "audio",
|
|
label: "Audio",
|
|
description: "Play an audio file through the OBS Browser Source.",
|
|
renderType: "audio",
|
|
normalize(input) {
|
|
return mediaConfig(input);
|
|
}
|
|
});
|
|
|
|
registerOverlayModuleType({
|
|
id: "web",
|
|
label: "Website or alert overlay",
|
|
description: "Add a third-party alert box, chat overlay, or other web page.",
|
|
normalize(input) {
|
|
return {
|
|
...commonConfig(input),
|
|
url: url(input.url),
|
|
allowPointerEvents: input.allowPointerEvents === true || input.allowPointerEvents === "on",
|
|
autoRefresh: input.autoRefresh === true || input.autoRefresh === "on",
|
|
healthCheck: input.healthCheck !== false && input.healthCheck !== "false" && input.healthCheck !== "off",
|
|
refreshIntervalSeconds: number(input.refreshIntervalSeconds, 300, 30, 3600),
|
|
retryLimit: Math.round(number(input.retryLimit, 3, 0, 5)),
|
|
cropTop: number(input.cropTop, 0, 0, 95),
|
|
cropRight: number(input.cropRight, 0, 0, 95),
|
|
cropBottom: number(input.cropBottom, 0, 0, 95),
|
|
cropLeft: number(input.cropLeft, 0, 0, 95),
|
|
zoom: number(input.zoom, 1, 0.1, 5),
|
|
injectPageCss: input.injectPageCss !== false && input.injectPageCss !== "false" && input.injectPageCss !== "off",
|
|
customCss: obsBrowserSourceCss(text(input.customCss, "", 12000))
|
|
};
|
|
}
|
|
});
|
|
|
|
module.exports = {
|
|
getOverlayModuleType,
|
|
listOverlayModuleTypes,
|
|
normalizeModuleConfig,
|
|
registerOverlayModuleType
|
|
};
|