1// overlays.js â config-driven overlay framework for the live-do player. 2// Reads the resolved SNOW_LIVE.overlays and mounts each enabled overlay as a 3// registered component into #wrap. Components talk to the transport via 4// window.SNOW_CORE (events + commands). Fail-silent: a decorative overlay must 5// never break the feed. 6(function () { 7 const REGISTRY = new Map(); // type -> factory(container, opts, core) => { update?, destroy? } 8 const MOUNTED = new Map(); // id -> { update?, destroy? } instance handle 9 function register(type, factory) { REGISTRY.set(type, factory); } 10 window.SNOW_OVERLAYS = { register, mounted: MOUNTED }; // components self-register (loaded after this file) 11 12 function boot() { 13 if (window.__ovBooted) return; window.__ovBooted = true; // idempotent: never double-mount 14 // On the preview surfaces keep navigation IN preview: rewrite the inter-view nav links to 15 // their *-preview counterparts so switching live<->snapshot stays on the preview pages. 16 // live-preview sets SNOW_PREVIEW; snapshot-preview sets SNOW_VARIANT. (status.html has no 17 // preview, so it's left alone.) Graceful â if this never runs the tabs fall back to live. 18 if (window.SNOW_VARIANT || window.SNOW_PREVIEW) { 19 const PREVIEW = { "live.html": "live-preview.html", "snapshot.html": "snapshot-preview.html" }; 20 for (const a of document.querySelectorAll("nav.tabs a.tab")) { 21 const to = PREVIEW[a.getAttribute("href")]; 22 if (to) a.setAttribute("href", to); 23 } 24 } 25 const overlays = (window.SNOW_LIVE && window.SNOW_LIVE.overlays) || null; 26 if (!overlays || typeof overlays !== "object") return; 27 const wrap = document.getElementById("wrap"); 28 const core = window.SNOW_CORE; 29 if (!wrap) return; 30 const regions = { 31 top: document.querySelector(".chrome-top"), 32 video: wrap, 33 bottom: document.querySelector(".chrome-bottom"), 34 }; 35 // Responsive region: framed (chrome in bars) vs immersive (overlays on the video). On the 36 // breakpoint crossing we RE-PARENT each container (a DOM move) â never destroy/recreate â so 37 // inner DOM, listeners, and SNOW_CORE subscriptions survive the rotate/resize. 38 // Framed when EITHER: 39 // ⢠a wide-landscape screen (â¥700Ã601) â desktop/tablet; OR 40 // ⢠ANY portrait â a landscape video letterboxes into a band, leaving empty space above & 41 // below for the bars; without framing those overlays pile onto the small video (the 42 // phone-portrait complaint). 43 // Immersive stays only for LANDSCAPE phones (short), which the "(orientation:landscape) and 44 // (max-height:600px)" full-bleed query in player.css fills edge-to-edge. The min-height 601 45 // keeps the wide-landscape clause ABOVE that full-bleed query so framed + full-bleed never overlap. 46 const framedQ = window.matchMedia("(min-width: 700px) and (min-height: 601px), (orientation: portrait)"); 47 // Embedded players pin the framed layout regardless of iframe size, so the chrome always frames 48 // the video (a small iframe would otherwise trip framedQ into the immersive on-video layout). 49 const isFramed = () => !!window.SNOW_EMBED || framedQ.matches; 50 const effRegion = (o) => (isFramed() ? (o.region || "video") : "video"); 51 // Single source of truth for "resolve region â resolve target â guarded 52 // appendChild â rebuild className". Used at mount AND on breakpoint change so the 53 // two stay in lockstep. The guarded appendChild is a DOM move (re-parent), never 54 // a destroy/recreate â inner DOM, listeners, and SNOW_CORE subscriptions survive. 55 const place = (container, o) => { 56 const region = effRegion(o); 57 const target = regions[region] || wrap; // unknown region â video 58 if (container.parentNode !== target) target.appendChild(container); // DOM move: keeps inner DOM + listeners + subs 59 container.className = "overlay overlay-" + o.type + " ov-region-" + region; 60 }; 61 const PLACED = []; // { id, container, o } â for re-parenting on breakpoint change 62 for (const [id, o] of Object.entries(overlays)) { 63 if (!o || o.enabled !== true) continue; 64 const factory = REGISTRY.get(o.type); 65 if (!factory) { console.warn("overlays: unsupported type", o.type, "for", id
65); continue; } 66 try { 67 const container = document.createElement("div"); 68 container.id = "ov-" + id; 69 place(container, o); // in the DOM with its class before the factory runs 70 const inst = factory(container, o, core) || {}; // component fills the container + wires core 71 MOUNTED.set(id, inst); // keep the lifecycle handle for later tasks 72 PLACED.push({ id, container, o }); // retain for re-parenting 73 } catch (e) { console.warn("overlays: failed to mount", id, e); } 74 } 75 framedQ.addEventListener("change", () => { 76 for (const p of PLACED) place(p.container, p.o); 77 }); 78 79 // ââ frame aspect ratio: the #wrap box is a FIXED 16:9 (declared in player.css). The 80 // video/image letterboxes INSIDE it (object-fit:contain); we deliberately do NOT reshape 81 // the box to the content, so the viewable screen keeps a stable shape across 82 // live / offline / snapshot (the camera's delivery sub-stream is ~4:3, the snapshots are 83 // ~16:9). fitStage just sizes that fixed-shape box to fit the space between the bars. 84 const FRAME_AR = 16 / 9; 85 86 // ââ fitStage: size the framed video + bars to the video's DISPLAYED width ââ 87 // The on-video box letterboxes when the viewport is short/wide, so the chrome bars (full 88 // width) would overhang the video. In framed mode we set the video and bars to the video's 89 // actual width (given the height left between the bars + its real aspect) so they track the 90 // video exactly. Pure CSS can't size this â the video's size depends on the bars' height and 91 // vice-versa. No-op (clears the inline widths, leaving the stable CSS) when not framed. 92 const sized = [wrap, regions.top, regions.bottom].filter(Boolean); 93 function fitStage() { 94 // While #wrap is MAXIMIZED (CSS pseudo-FS or the native fullscreen element) it must fill the 95 // viewport via CSS, so release any framed inline width and bail â never size it to the bars. 96 const maximized = wrap.classList.contains("snow-fs") || document.fullscreenElement === wrap; 97 const framed = !maximized && isFramed() && 98 ((regions.top && regions.top.childElementCount) || (regions.bottom && regions.bottom.childElementCount)); 99 if (!framed) { for (const el of sized) el.style.width = ""; return; } 100 const main = wrap.parentElement, cs = getComputedStyle(main); 101 const availH = main.clientHeight - parseFloat(cs.paddingTop) - parseFloat(cs.paddingBottom); 102 const availW = main.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight); 103 const barsH = (regions.top ? regions.top.offsetHeight : 0) + (regions.bottom ? regions.bottom.offsetHeight : 0); 104 const videoW = Math.max(0, Math.floor(Math.min(availW, (availH - barsH) * FRAME_AR, 1280))); 105 for (const el of sized) el.style.width = videoW + "px"; // wrap height follows via aspect-ratio 106 } 107 let rafId = null; 108 const scheduleFit = () => { if (rafId) return; rafId = requestAnimationFrame(() => { rafId = null; fitStage(); }); }; 109 window.addEventListener("resize", scheduleFit); 110 framedQ.addEventListener("change", scheduleFit); // re-parent listener runs first; this rAF runs after 111 // Re-fit whenever the bar heights change: any image load under <main> â chiefly the brandbar 112 // PNG, whose height feeds back into the bar height and so into the video size. (load doesn't 113 // bubble, so listen in the capture phase on <main>.) 114 (wrap.parentElement || wrap).addEventListener("load", scheduleFit, true); 115 window.SNOW_OVERLAYS.fitStage = fitStage; // exposed for tests 116 scheduleFit(); 117 } 118 if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot); 119 else boot(); 120 121 // ââ built-in: image (branding) â preserves the prior behavior âââââââââââââââ 122 register("image", function (container, o) { 123 if (!o.src) { console.warn("overlays: image has no src"); container.remove(); return; } 124 const img = document.createElement("img"); 125 img.className = "overlay-image-img"; 126 img.alt = ""; img.decoding = "async"; 127 Object.assign(container.style, { position: "absolute", inset: "0", pointerEvents: "none", zIndex: "1" });
128 Object.assign(img.style, { 129 position: "absolute", inset: "0", width: "100%", height: "100%", 130 objectFit: o.fit === "cover" ? "cover" : "contain", 131 opacity: String(o.opacity ?? 1), pointerEvents: "none", 132 }); 133 img.onerror = () => container.remove(); 134 // o.src is trusted: config.js is a same-origin static asset. Validate the URL scheme here if overlay config ever comes from an untrusted source. 135 img.src = o.src; 136 container.appendChild(img); 137 }); 138})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.