@xiboplayer/pwa 0.7.21 → 0.7.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/__vite-browser-external-DeMPM02e.js +2 -0
- package/dist/assets/__vite-browser-external-DeMPM02e.js.map +1 -0
- package/dist/assets/chunk-DzMEjpoC.js +1 -0
- package/dist/assets/{html2canvas-BAfZNSwU.js → html2canvas-EikzC5d8.js} +2 -2
- package/dist/assets/{html2canvas-BAfZNSwU.js.map → html2canvas-EikzC5d8.js.map} +1 -1
- package/dist/assets/main-CRdq5ifQ.js +3 -0
- package/dist/assets/{main-vwJkNw4Y.js.map → main-CRdq5ifQ.js.map} +1 -1
- package/dist/assets/main-DTR2QDcF.js +108 -0
- package/dist/assets/main-DTR2QDcF.js.map +1 -0
- package/dist/assets/{pdf-Bxz9Nzto.js → pdf-CMz6puSt.js} +1 -1
- package/dist/assets/{pdf-Bxz9Nzto.js.map → pdf-CMz6puSt.js.map} +1 -1
- package/dist/assets/{setup-B4gZX38p.js → setup-Bw8T9Qq6.js} +2 -2
- package/dist/assets/{setup-B4gZX38p.js.map → setup-Bw8T9Qq6.js.map} +1 -1
- package/dist/assets/src-A5KHvitf.js +2 -0
- package/dist/assets/{src-CROvYSP8.js.map → src-A5KHvitf.js.map} +1 -1
- package/dist/assets/{src-DAB0dqGG.js → src-BHsN2u2P.js} +2 -2
- package/dist/assets/{src-DAB0dqGG.js.map → src-BHsN2u2P.js.map} +1 -1
- package/dist/assets/src-BLUMUwZR.js +1 -0
- package/dist/assets/src-BXXcWcHh.js +1 -0
- package/dist/assets/src-BxSOopk7.js +1 -0
- package/dist/assets/{src-WDu491CE.js → src-BxaX1gGg.js} +2 -2
- package/dist/assets/{src-WDu491CE.js.map → src-BxaX1gGg.js.map} +1 -1
- package/dist/assets/{src-BtVLiVYZ.js → src-CCAyzQUp.js} +3 -3
- package/dist/assets/{src-BtVLiVYZ.js.map → src-CCAyzQUp.js.map} +1 -1
- package/dist/assets/src-CWJcD3kA.js +1 -0
- package/dist/assets/{src-Cx3tXAAu.js → src-CZ1k5h23.js} +3 -3
- package/dist/assets/{src-Cx3tXAAu.js.map → src-CZ1k5h23.js.map} +1 -1
- package/dist/assets/src-ClrziKzV.js +16 -0
- package/dist/assets/src-ClrziKzV.js.map +1 -0
- package/dist/assets/{src-C_Lx4lXp.js → src-CtjjclS4.js} +2 -2
- package/dist/assets/{src-C_Lx4lXp.js.map → src-CtjjclS4.js.map} +1 -1
- package/dist/assets/src-CuVaZcMo.js +2 -0
- package/dist/assets/{src-B_BNICay.js.map → src-CuVaZcMo.js.map} +1 -1
- package/dist/assets/src-Cy5OUviT.js +1 -0
- package/dist/assets/src-DK5BYonP.js +630 -0
- package/dist/assets/src-DK5BYonP.js.map +1 -0
- package/dist/assets/src-Dk-W3N33.js +1 -0
- package/dist/assets/{src-cUopH0nN.js → src-xPTO7Ts6.js} +3 -3
- package/dist/assets/{src-cUopH0nN.js.map → src-xPTO7Ts6.js.map} +1 -1
- package/dist/assets/sync-manager-zf1tikPt.js +2 -0
- package/dist/assets/sync-manager-zf1tikPt.js.map +1 -0
- package/dist/index.html +1 -1
- package/dist/setup.html +3 -4
- package/dist/sw-pwa.js +2 -2
- package/dist/sw-pwa.js.map +1 -1
- package/package.json +15 -13
- package/dist/assets/chunk-7ZXdHUL4.js +0 -1
- package/dist/assets/main-oacre7st.js +0 -108
- package/dist/assets/main-oacre7st.js.map +0 -1
- package/dist/assets/main-vwJkNw4Y.js +0 -3
- package/dist/assets/src-B_BNICay.js +0 -2
- package/dist/assets/src-Bjt9ooXK.js +0 -16
- package/dist/assets/src-Bjt9ooXK.js.map +0 -1
- package/dist/assets/src-CKpVxGpH.js +0 -629
- package/dist/assets/src-CKpVxGpH.js.map +0 -1
- package/dist/assets/src-CROvYSP8.js +0 -2
- package/dist/assets/sync-manager-8Z-qwkod.js +0 -2
- package/dist/assets/sync-manager-8Z-qwkod.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"mappings":"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","names":["log","pkg"],"ignoreList":[],"sources":["../../../renderer/src/layout-pool.js","../../../renderer/src/renderer-lite.js","../../../renderer/src/layout.js","../../../renderer/src/index.js"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * LayoutPool - Maintains a pool of pre-built layout containers\n * for instant layout transitions.\n *\n * Instead of tearing down and rebuilding the DOM on every layout switch,\n * the pool keeps up to `maxSize` layout containers alive. The current\n * layout is marked 'hot' (visible); pre-loaded layouts are 'warm' (hidden).\n * When transitioning, visibility is swapped instantly - no DOM rebuild.\n *\n * Pool entries:\n * layoutId -> { container, layout, regions, blobUrls, mediaUrlCache, status, lastAccess }\n *\n * Status: 'hot' (currently visible) or 'warm' (preloaded, hidden)\n */\n\nimport { createLogger } from '@xiboplayer/utils';\n\nconst log = createLogger('LayoutPool');\n\nexport class LayoutPool {\n /**\n * @param {number} maxSize - Maximum number of layouts to keep in pool (default: 2)\n */\n constructor(maxSize = 2) {\n /** @type {Map<number, Object>} */\n this.layouts = new Map();\n this.maxSize = maxSize;\n /** @type {number|null} */\n this.hotLayoutId = null;\n }\n\n /**\n * Check if a layout is in the pool\n * @param {number} layoutId\n * @returns {boolean}\n */\n has(layoutId) {\n return this.layouts.has(layoutId);\n }\n\n /**\n * Get a pool entry\n * @param {number} layoutId\n * @returns {Object|undefined}\n */\n get(layoutId) {\n return this.layouts.get(layoutId);\n }\n\n /**\n * Add a layout entry to the pool.\n * If pool is full, evicts the least-recently-used warm entry.\n *\n * @param {number} layoutId\n * @param {Object} entry - Pool entry\n * @param {HTMLElement} entry.container - Layout container DOM element\n * @param {Object} entry.layout - Parsed layout object\n * @param {Map} entry.regions - Region map (regionId => region state)\n * @param {Set<string>} entry.blobUrls - Tracked blob URLs for this layout\n * @param {Map} [entry.mediaUrlCache] - Media URL cache (fileId => url)\n */\n add(layoutId, entry) {\n // If already in pool, update in place\n if (this.layouts.has(layoutId)) {\n const existing = this.layouts.get(layoutId);\n Object.assign(existing, entry);\n existing.lastAccess = Date.now();\n return;\n }\n\n // If pool is full, evict LRU warm entry\n if (this.layouts.size >= this.maxSize) {\n this.evictLRU();\n }\n\n entry.status = 'warm';\n entry.lastAccess = Date.now();\n this.layouts.set(layoutId, entry);\n log.info(`Added layout ${layoutId} to pool (size: ${this.layouts.size}/${this.maxSize})`);\n }\n\n /**\n * Mark a layout as active (visible).\n * The previous hot layout is demoted to warm.\n * @param {number} layoutId\n */\n setHot(layoutId) {\n // Demote previous hot layout to warm\n if (this.hotLayoutId !== null && this.layouts.has(this.hotLayoutId)) {\n this.layouts.get(this.hotLayoutId).status = 'warm';\n }\n\n if (this.layouts.has(layoutId)) {\n const entry = this.layouts.get(layoutId);\n entry.status = 'hot';\n entry.lastAccess = Date.now();\n }\n\n this.hotLayoutId = layoutId;\n }\n\n /**\n * Evict a specific layout from the pool.\n * Releases video/audio resources, revokes blob URLs, and removes the container from the DOM.\n * @param {number} layoutId\n */\n evict(layoutId) {\n const entry = this.layouts.get(layoutId);\n if (!entry) return;\n\n log.info(`Evicting layout ${layoutId} from pool`);\n\n // Stop any active region timers\n if (entry.regions) {\n for (const [regionId, region] of entry.regions) {\n if (region.timer) {\n clearTimeout(region.timer);\n region.timer = null;\n }\n }\n }\n\n // Release all video/audio resources BEFORE removing from DOM.\n // Removing a <video> with an active src leaks decoded frame buffers.\n if (entry.container) {\n LayoutPool.releaseMediaElements(entry.container);\n }\n\n // Revoke blob URLs\n if (entry.blobUrls && entry.blobUrls.size > 0) {\n entry.blobUrls.forEach(url => {\n URL.revokeObjectURL(url);\n });\n log.info(`Revoked ${entry.blobUrls.size} blob URLs for layout ${layoutId}`);\n }\n\n // Revoke media URL cache blob URLs\n if (entry.mediaUrlCache) {\n for (const [fileId, blobUrl] of entry.mediaUrlCache) {\n if (blobUrl && typeof blobUrl === 'string' && blobUrl.startsWith('blob:')) {\n URL.revokeObjectURL(blobUrl);\n }\n }\n }\n\n // Remove container from DOM\n if (entry.container && entry.container.parentNode) {\n entry.container.remove();\n }\n\n this.layouts.delete(layoutId);\n\n // Clear hot reference if this was the hot layout\n if (this.hotLayoutId === layoutId) {\n this.hotLayoutId = null;\n }\n }\n\n /**\n * Release all video and audio elements inside a container.\n * Must be called BEFORE removing the container from the DOM —\n * browsers keep decoded frame buffers alive for detached <video> elements\n * that still have a src.\n *\n * @param {HTMLElement} container\n */\n static releaseMediaElements(container) {\n // Defer the actual release by one animation frame to give the GPU compositor\n // time to stop referencing textures from the old layout. Without this delay,\n // the compositor may still hold stale mailbox references when we destroy the\n // video backing, causing SharedImageManager::ProduceSkia \"non-existent mailbox\"\n // errors (Chrome bug: race in shared_image_manager.cc acknowledged in a TODO).\n requestAnimationFrame(() => LayoutPool._releaseMediaElementsSync(container));\n }\n\n static _releaseMediaElementsSync(container) {\n let videoCount = 0;\n let hlsCount = 0;\n\n container.querySelectorAll('video').forEach(v => {\n // Destroy hls.js instance if attached (stored by renderVideo)\n if (v._hlsInstance) {\n v._hlsInstance.destroy();\n v._hlsInstance = null;\n hlsCount++;\n }\n // Stop MediaStream tracks (webcam/mic)\n if (v._mediaStream) {\n v._mediaStream.getTracks().forEach(t => t.stop());\n v._mediaStream = null;\n v.srcObject = null;\n }\n v.pause();\n v.removeAttribute('src');\n v.load(); // Forces browser to release decoded buffers\n videoCount++;\n });\n\n container.querySelectorAll('audio').forEach(a => {\n a.pause();\n a.removeAttribute('src');\n a.load();\n });\n\n // Release media inside iframes (embedded widgets with HLS streams, webcams, etc.)\n // We can't querySelectorAll('video') across iframe boundaries, but we can:\n // 1. Try to access same-origin iframe contentDocument\n // 2. Force-remove the iframe src to stop all network activity\n let iframeCount = 0;\n container.querySelectorAll('iframe').forEach(iframe => {\n try {\n // Same-origin iframes: reach inside and release videos\n const doc = iframe.contentDocument || iframe.contentWindow?.document;\n if (doc) {\n doc.querySelectorAll('video').forEach(v => {\n v.pause();\n v.removeAttribute('src');\n v.load();\n videoCount++;\n });\n doc.querySelectorAll('audio').forEach(a => {\n a.pause();\n a.removeAttribute('src');\n a.load();\n });\n }\n } catch (_) {\n // Cross-origin: can't access contentDocument\n }\n // Force stop all iframe network activity (HLS segments, SSE, WebSocket, etc.)\n iframe.src = 'about:blank';\n iframeCount++;\n });\n\n // Destroy PDF documents and release GPU canvas backing stores\n container.querySelectorAll('.pdf-widget').forEach(el => {\n if (el._pdfDestroy) el._pdfDestroy();\n });\n\n if (videoCount > 0 || iframeCount > 0) {\n log.info(`Released ${videoCount} video(s)${hlsCount ? ` (${hlsCount} HLS)` : ''}${iframeCount ? `, ${iframeCount} iframe(s)` : ''}`);\n }\n }\n\n /**\n * Evict the least-recently-used warm entry.\n * Only warm entries are eligible for eviction (never the hot layout).\n */\n evictLRU() {\n let oldest = null;\n let oldestTime = Infinity;\n\n for (const [id, entry] of this.layouts) {\n if (entry.status === 'warm' && entry.lastAccess < oldestTime) {\n oldest = id;\n oldestTime = entry.lastAccess;\n }\n }\n\n if (oldest !== null) {\n this.evict(oldest);\n }\n }\n\n /**\n * Clear all warm (preloaded) entries, keeping the hot layout.\n * @returns {number} Number of entries cleared\n */\n clearWarm() {\n let count = 0;\n const warmIds = [];\n\n for (const [id, entry] of this.layouts) {\n if (entry.status === 'warm') {\n warmIds.push(id);\n }\n }\n\n for (const id of warmIds) {\n this.evict(id);\n count++;\n }\n\n if (count > 0) {\n log.info(`Cleared ${count} warm layout(s) from pool`);\n }\n\n return count;\n }\n\n /**\n * Clear warm entries NOT in the given set of layout IDs.\n * Keeps warm entries that are still scheduled.\n * @param {Set<number>} keepIds - Layout IDs to keep\n * @returns {number} Number of entries cleared\n */\n clearWarmNotIn(keepIds) {\n let count = 0;\n const evictIds = [];\n\n for (const [id, entry] of this.layouts) {\n if (entry.status === 'warm' && !keepIds.has(id)) {\n evictIds.push(id);\n }\n }\n\n for (const id of evictIds) {\n this.evict(id);\n count++;\n }\n\n if (count > 0) {\n log.info(`Cleared ${count} warm layout(s) no longer in schedule`);\n }\n\n return count;\n }\n\n /**\n * Get the most recently added layout ID.\n * @returns {number|undefined}\n */\n getLatest() {\n let latest;\n for (const id of this.layouts.keys()) {\n latest = id;\n }\n return latest;\n }\n\n /**\n * Clear all entries (both hot and warm).\n */\n clear() {\n const ids = Array.from(this.layouts.keys());\n for (const id of ids) {\n this.evict(id);\n }\n this.hotLayoutId = null;\n }\n\n /**\n * Get the number of entries in the pool.\n * @returns {number}\n */\n get size() {\n return this.layouts.size;\n }\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * RendererLite - Lightweight XLF Layout Renderer\n *\n * A standalone, reusable JavaScript library for rendering Xibo Layout Format (XLF) files.\n * Provides layout rendering without dependencies on XLR, suitable for any platform.\n *\n * Features:\n * - Parse XLF XML layout files\n * - Create region DOM elements with positioning\n * - Render widgets (text, image, video, audio, PDF, webpage)\n * - Handle widget duration timers\n * - Apply CSS transitions (fade, fly)\n * - Event emitter for lifecycle hooks\n * - Manage layout lifecycle\n *\n * Usage pattern (similar to xmr-wrapper.js):\n *\n * ```javascript\n * import { RendererLite } from './renderer-lite.js';\n *\n * const container = document.getElementById('player-container');\n * const renderer = new RendererLite({ cmsUrl: '...', hardwareKey: '...' }, container);\n *\n * // Listen to events\n * renderer.on('layoutStart', (layoutId) => console.log('Layout started:', layoutId));\n * renderer.on('layoutEnd', (layoutId) => console.log('Layout ended:', layoutId));\n * renderer.on('widgetStart', (widget) => console.log('Widget started:', widget));\n * renderer.on('widgetEnd', (widget) => console.log('Widget ended:', widget));\n * renderer.on('error', (error) => console.error('Error:', error));\n *\n * // Render a layout\n * await renderer.renderLayout(layoutXml, duration);\n *\n * // Stop current layout\n * renderer.stopCurrentLayout();\n *\n * // Cleanup\n * renderer.cleanup();\n * ```\n */\n\nimport { EventEmitter } from '@xiboplayer/utils';\nimport { createLogger, isDebug, PLAYER_API } from '@xiboplayer/utils';\nimport { parseLayoutDuration } from '@xiboplayer/schedule';\nimport { LayoutPool } from './layout-pool.js';\n\n/**\n * Transition utilities for widget animations\n */\nexport const Transitions = {\n /**\n * Apply fade in transition\n */\n fadeIn(element, duration) {\n const keyframes = [\n { opacity: 0 },\n { opacity: 1 }\n ];\n const timing = {\n duration: duration,\n easing: 'linear',\n fill: 'forwards'\n };\n return element.animate(keyframes, timing);\n },\n\n /**\n * Apply fade out transition\n */\n fadeOut(element, duration) {\n const keyframes = [\n { opacity: 1 },\n { opacity: 0 }\n ];\n const timing = {\n duration: duration,\n easing: 'linear',\n fill: 'forwards'\n };\n return element.animate(keyframes, timing);\n },\n\n /**\n * Get fly keyframes based on compass direction\n */\n getFlyKeyframes(direction, width, height, isIn) {\n const dirMap = {\n 'N': { x: 0, y: isIn ? -height : height },\n 'NE': { x: isIn ? width : -width, y: isIn ? -height : height },\n 'E': { x: isIn ? width : -width, y: 0 },\n 'SE': { x: isIn ? width : -width, y: isIn ? height : -height },\n 'S': { x: 0, y: isIn ? height : -height },\n 'SW': { x: isIn ? -width : width, y: isIn ? height : -height },\n 'W': { x: isIn ? -width : width, y: 0 },\n 'NW': { x: isIn ? -width : width, y: isIn ? -height : height }\n };\n\n const offset = dirMap[direction] || dirMap['N'];\n\n if (isIn) {\n return {\n from: {\n transform: `translate(${offset.x}px, ${offset.y}px)`,\n opacity: 0\n },\n to: {\n transform: 'translate(0, 0)',\n opacity: 1\n }\n };\n } else {\n return {\n from: {\n transform: 'translate(0, 0)',\n opacity: 1\n },\n to: {\n transform: `translate(${offset.x}px, ${offset.y}px)`,\n opacity: 0\n }\n };\n }\n },\n\n /**\n * Apply fly in transition\n */\n flyIn(element, duration, direction, regionWidth, regionHeight) {\n const keyframes = this.getFlyKeyframes(direction, regionWidth, regionHeight, true);\n const timing = {\n duration: duration,\n easing: 'ease-out',\n fill: 'forwards'\n };\n return element.animate([keyframes.from, keyframes.to], timing);\n },\n\n /**\n * Apply fly out transition\n */\n flyOut(element, duration, direction, regionWidth, regionHeight) {\n const keyframes = this.getFlyKeyframes(direction, regionWidth, regionHeight, false);\n const timing = {\n duration: duration,\n easing: 'ease-in',\n fill: 'forwards'\n };\n return element.animate([keyframes.from, keyframes.to], timing);\n },\n\n /**\n * Apply slide-in transition (layout-level #337).\n *\n * Identical in shape to flyIn but keeps opacity at 1 throughout —\n * slides are pure positional animations for layout-to-layout\n * transitions where both layouts are fully rendered and the effect\n * is a carousel-style push/pull.\n */\n slideIn(element, duration, direction, width, height) {\n const dirMap = {\n N: { x: 0, y: -height },\n NE: { x: width, y: -height },\n E: { x: width, y: 0 },\n SE: { x: width, y: height },\n S: { x: 0, y: height },\n SW: { x: -width, y: height },\n W: { x: -width, y: 0 },\n NW: { x: -width, y: -height }\n };\n const offset = dirMap[direction] || dirMap.E;\n return element.animate(\n [\n { transform: `translate(${offset.x}px, ${offset.y}px)` },\n { transform: 'translate(0, 0)' }\n ],\n { duration, easing: 'ease-out', fill: 'forwards' }\n );\n },\n\n /**\n * Apply slide-out transition (layout-level #337).\n *\n * Pushes the outgoing layout off in the given direction.\n */\n slideOut(element, duration, direction, width, height) {\n const dirMap = {\n N: { x: 0, y: -height },\n NE: { x: width, y: -height },\n E: { x: width, y: 0 },\n SE: { x: width, y: height },\n S: { x: 0, y: height },\n SW: { x: -width, y: height },\n W: { x: -width, y: 0 },\n NW: { x: -width, y: -height }\n };\n const offset = dirMap[direction] || dirMap.W;\n return element.animate(\n [\n { transform: 'translate(0, 0)' },\n { transform: `translate(${offset.x}px, ${offset.y}px)` }\n ],\n { duration, easing: 'ease-in', fill: 'forwards' }\n );\n },\n\n /**\n * Apply wipe-in transition (layout-level #337).\n *\n * Reveals the incoming layout by progressively shrinking a\n * clip-path inset from one edge. The `direction` picks which edge\n * is the \"start\" of the reveal — E means \"wipe reveals starting\n * from the left edge moving east\", matching the barWipe convention.\n */\n wipeIn(element, duration, direction) {\n // inset(<top> <right> <bottom> <left>) — 100% on an edge hides\n // everything past that edge, 0% on an edge reveals everything.\n const insetByDirection = {\n E: { from: 'inset(0 100% 0 0)', to: 'inset(0 0 0 0)' },\n W: { from: 'inset(0 0 0 100%)', to: 'inset(0 0 0 0)' },\n S: { from: 'inset(0 0 100% 0)', to: 'inset(0 0 0 0)' },\n N: { from: 'inset(100% 0 0 0)', to: 'inset(0 0 0 0)' },\n // Diagonals: wipe from the named corner to its opposite\n SE: { from: 'inset(0 100% 100% 0)', to: 'inset(0 0 0 0)' },\n SW: { from: 'inset(0 0 100% 100%)', to: 'inset(0 0 0 0)' },\n NE: { from: 'inset(100% 100% 0 0)', to: 'inset(0 0 0 0)' },\n NW: { from: 'inset(100% 0 0 100%)', to: 'inset(0 0 0 0)' }\n };\n const clip = insetByDirection[direction] || insetByDirection.E;\n return element.animate(\n [{ clipPath: clip.from }, { clipPath: clip.to }],\n { duration, easing: 'ease-out', fill: 'forwards' }\n );\n },\n\n /**\n * Apply transition based on type\n */\n apply(element, transitionConfig, isIn, regionWidth, regionHeight) {\n if (!transitionConfig || !transitionConfig.type) {\n return null;\n }\n\n const type = transitionConfig.type.toLowerCase();\n const duration = transitionConfig.duration || 1000;\n const direction = transitionConfig.direction || 'N';\n\n switch (type) {\n case 'fade':\n return isIn ? this.fadeIn(element, duration) : this.fadeOut(element, duration);\n case 'fadein':\n return isIn ? this.fadeIn(element, duration) : null;\n case 'fadeout':\n return isIn ? null : this.fadeOut(element, duration);\n case 'fly':\n return isIn\n ? this.flyIn(element, duration, direction, regionWidth, regionHeight)\n : this.flyOut(element, duration, direction, regionWidth, regionHeight);\n case 'flyin':\n return isIn ? this.flyIn(element, duration, direction, regionWidth, regionHeight) : null;\n case 'flyout':\n return isIn ? null : this.flyOut(element, duration, direction, regionWidth, regionHeight);\n case 'slide':\n return isIn\n ? this.slideIn(element, duration, direction, regionWidth, regionHeight)\n : this.slideOut(element, duration, direction, regionWidth, regionHeight);\n case 'wipe':\n // Wipe is a reveal-only effect — the outgoing layout isn't\n // animated, the incoming one \"uncovers\" itself on top.\n return isIn ? this.wipeIn(element, duration, direction) : null;\n default:\n return null;\n }\n }\n};\n\n/**\n * RendererLite - Lightweight XLF renderer\n */\nexport class RendererLite {\n /**\n * @param {Object} config - Player configuration\n * @param {string} config.cmsUrl - CMS base URL\n * @param {string} config.hardwareKey - Display hardware key\n * @param {HTMLElement} container - DOM container for rendering\n * @param {Object} options - Renderer options\n * @param {Map<string,string>} [options.fileIdToSaveAs] - Map from numeric file ID to storedAs filename (for layout backgrounds)\n * @param {Function} options.getWidgetHtml - Function to get widget HTML (layoutId, regionId, widgetId) => html\n */\n constructor(config, container, options = {}) {\n this.config = config;\n this.container = container;\n this.options = options;\n\n // Logger with configurable level\n this.log = createLogger('RendererLite', options.logLevel);\n\n // Event emitter for lifecycle hooks\n this.emitter = new EventEmitter();\n\n // State\n this.currentLayout = null;\n this.currentLayoutId = null;\n this._preloadingLayoutId = null; // Set during preload for blob URL tracking\n this._preloadingPromise = null; // Promise for in-flight preload (await instead of skip)\n this.regions = new Map(); // regionId => { element, widgets, currentIndex, timer }\n this.layoutTimer = null;\n this.layoutEndEmitted = false; // Prevents double layoutEnd on stop after timer\n this._deferredTimerLayoutId = null; // Set when timer is deferred for dynamic layouts\n this._deferredTimerFallback = null; // Safety timeout: starts layout timer if metadata never arrives\n this._paused = false;\n this._layoutTimerStartedAt = null; // Date.now() when layout timer started\n this._layoutTimerDurationMs = null; // Total layout duration in ms\n this.layoutBlobUrls = new Map(); // layoutId => Set<blobUrl> (for lifecycle tracking)\n this.audioOverlays = new Map(); // widgetId => [HTMLAudioElement] (audio overlays for widgets)\n\n // Bound methods (avoid lambda allocation per call in startRegion/_advanceRegion)\n this._stopWidgetBound = (rid, idx) => this.stopWidget(rid, idx);\n this._renderWidgetBound = (rid, idx) => this.renderWidget(rid, idx);\n\n // Scale state (for fitting layout to screen)\n this.scaleFactor = 1;\n this.offsetX = 0;\n this.offsetY = 0;\n\n // Overlay state\n this.overlayContainer = null;\n this.activeOverlays = new Map(); // layoutId => { container, layout, timer, regions }\n\n // Interactive action state\n this._keydownHandler = null; // Document keydown listener (single, shared)\n this._keyboardActions = []; // Active keyboard actions for current layout\n\n // Sub-playlist cycle state (round-robin per parentWidgetId group)\n this._subPlaylistCycleIndex = new Map();\n\n // Widget lifecycle tracking — ensures symmetric start/stop\n this._startedWidgets = new Set(); // \"regionId:widgetIndex\" keys\n\n // Layout preload pool (2-layout pool for instant transitions)\n this.layoutPool = new LayoutPool(2);\n this.preloadTimer = null;\n this._preloadRetryTimer = null;\n\n // Layout-to-layout transition default (#337). Applied when the\n // incoming layout has no per-layout layoutTransitionIn override.\n // Setting type to 'instant' preserves the pre-#337 hard-cut\n // behaviour byte-for-byte.\n this.layoutTransition = this._normalizeLayoutTransition(\n options.layoutTransition\n );\n\n // Setup container styles\n this.setupContainer();\n\n // Interactive Control (XIC) event handlers\n this.emitter.on('interactiveTrigger', (data) => this._handleInteractiveTrigger(data));\n this.emitter.on('widgetExpire', (data) => this._handleWidgetExpire(data));\n this.emitter.on('widgetExtendDuration', (data) => this._handleWidgetExtendDuration(data));\n this.emitter.on('widgetSetDuration', (data) => this._handleWidgetSetDuration(data));\n\n this.log.info('Initialized');\n }\n\n /**\n * Setup container element\n */\n setupContainer() {\n this.container.style.position = 'relative';\n this.container.style.width = '100%';\n this.container.style.height = '100vh'; // Use viewport height, not percentage\n this.container.style.overflow = 'hidden';\n\n // Watch for container resize to rescale layout (debounced to avoid spam)\n this._resizeSuppressed = false;\n if (typeof ResizeObserver !== 'undefined') {\n let resizeTimer = null;\n this.resizeObserver = new ResizeObserver(() => {\n if (this._resizeSuppressed) return;\n if (resizeTimer) clearTimeout(resizeTimer);\n resizeTimer = setTimeout(() => this.rescaleRegions(), 150);\n });\n this.resizeObserver.observe(this.container);\n }\n\n // Create overlay container for overlay layouts (higher z-index than main content)\n this.overlayContainer = document.createElement('div');\n this.overlayContainer.id = 'overlay-container';\n this.overlayContainer.style.position = 'absolute';\n this.overlayContainer.style.top = '0';\n this.overlayContainer.style.left = '0';\n this.overlayContainer.style.width = '100%';\n this.overlayContainer.style.height = '100%';\n this.overlayContainer.style.zIndex = '1000'; // Above main layout (z-index 0-999)\n this.overlayContainer.style.pointerEvents = 'none'; // Don't block clicks on main layout\n this.container.appendChild(this.overlayContainer);\n }\n\n /**\n * Calculate scale factor to fit layout into container\n * Centers the layout and scales regions proportionally.\n * @param {Object} layout - Parsed layout with width/height\n */\n calculateScale(layout) {\n const screenWidth = this.container.clientWidth;\n const screenHeight = this.container.clientHeight;\n\n if (!screenWidth || !screenHeight) return;\n\n const scaleX = screenWidth / layout.width;\n const scaleY = screenHeight / layout.height;\n this.scaleFactor = Math.min(scaleX, scaleY);\n this.offsetX = (screenWidth - layout.width * this.scaleFactor) / 2;\n this.offsetY = (screenHeight - layout.height * this.scaleFactor) / 2;\n\n this.log.info(`Scale: ${this.scaleFactor.toFixed(3)} (${layout.width}x${layout.height} → ${screenWidth}x${screenHeight}, offset ${Math.round(this.offsetX)},${Math.round(this.offsetY)})`);\n }\n\n /**\n * Apply scale to a region element\n * @param {HTMLElement} regionEl - Region DOM element\n * @param {Object} regionConfig - Region config with left, top, width, height\n */\n applyRegionScale(regionEl, regionConfig) {\n const sf = this.scaleFactor;\n regionEl.style.left = `${regionConfig.left * sf + this.offsetX}px`;\n regionEl.style.top = `${regionConfig.top * sf + this.offsetY}px`;\n regionEl.style.width = `${regionConfig.width * sf}px`;\n regionEl.style.height = `${regionConfig.height * sf}px`;\n }\n\n /**\n * Reapply scale to all current regions (e.g., on window resize)\n */\n rescaleRegions() {\n if (!this.currentLayout) return;\n\n this.calculateScale(this.currentLayout);\n\n for (const [regionId, region] of this.regions) {\n this.applyRegionScale(region.element, region.config);\n // Update region dimensions for transition calculations\n region.width = region.config.width * this.scaleFactor;\n region.height = region.config.height * this.scaleFactor;\n }\n\n // Rescale active overlays too\n for (const [overlayId, overlay] of this.activeOverlays) {\n this.calculateScale(overlay.layout);\n for (const [regionId, region] of overlay.regions) {\n this.applyRegionScale(region.element, region.config);\n region.width = region.config.width * this.scaleFactor;\n region.height = region.config.height * this.scaleFactor;\n }\n }\n }\n\n /**\n * Event emitter interface (like XMR wrapper)\n */\n on(event, callback) {\n this.emitter.on(event, callback);\n }\n\n emit(event, ...args) {\n this.emitter.emit(event, ...args);\n }\n\n /**\n * Parse action elements from an XLF parent element (region or media)\n * @param {Element} parentEl - Parent XML element containing <action> children\n * @returns {Array} Parsed actions\n */\n parseActions(parentEl) {\n const actions = [];\n for (const actionEl of parentEl.children) {\n if (actionEl.tagName !== 'action') continue;\n actions.push({\n id: actionEl.getAttribute('id') || '',\n actionType: actionEl.getAttribute('actionType') || '',\n triggerType: actionEl.getAttribute('triggerType') || '',\n triggerCode: actionEl.getAttribute('triggerCode') || '',\n source: actionEl.getAttribute('source') || '',\n sourceId: actionEl.getAttribute('sourceId') || '',\n target: actionEl.getAttribute('target') || '',\n targetId: actionEl.getAttribute('targetId') || '',\n widgetId: actionEl.getAttribute('widgetId') || '',\n layoutCode: actionEl.getAttribute('layoutCode') || '',\n commandCode: actionEl.getAttribute('commandCode') || ''\n });\n }\n return actions;\n }\n\n /**\n * Normalize a layout transition spec from constructor options.\n *\n * Accepts either a partial object, null, or undefined, and returns a\n * canonical shape: {type, duration, direction}. Defaults to the\n * backwards-compatible `instant` type so existing callers see no\n * behavioural change.\n *\n * @param {Object|null|undefined} spec\n * @returns {{type: string, duration: number, direction: string|undefined}}\n */\n _normalizeLayoutTransition(spec) {\n const type = spec?.type || 'instant';\n const duration = Number.isFinite(spec?.duration) ? spec.duration : 500;\n const direction = spec?.direction || undefined;\n return { type, duration, direction };\n }\n\n /**\n * Resolve the effective layout transition spec for an incoming\n * layout. Per-layout overrides (from parseXlf) beat the\n * renderer-wide default; unspecified fields fall back to the\n * default's values.\n *\n * @param {Object} incomingLayout - parsed layout object\n * @returns {{type: string, duration: number, direction: string|undefined}}\n */\n _resolveLayoutTransition(incomingLayout) {\n const layoutOverride = incomingLayout?.layoutTransitionIn;\n if (!layoutOverride || !layoutOverride.type) {\n return this.layoutTransition;\n }\n return {\n type: layoutOverride.type,\n duration: Number.isFinite(layoutOverride.duration)\n ? layoutOverride.duration\n : this.layoutTransition.duration,\n direction: layoutOverride.direction || this.layoutTransition.direction,\n };\n }\n\n /**\n * Parse XLF XML to layout object\n * @param {string} xlfXml - XLF XML content\n * @returns {Object} Parsed layout\n */\n parseXlf(xlfXml) {\n const parser = new DOMParser();\n const doc = parser.parseFromString(xlfXml, 'text/xml');\n\n const layoutEl = doc.querySelector('layout');\n if (!layoutEl) {\n throw new Error('Invalid XLF: no <layout> element');\n }\n\n const layoutDurationAttr = layoutEl.getAttribute('duration');\n\n // Layout-to-layout transitions (#337). When present, these attributes\n // describe the visual effect to apply when this layout becomes the\n // active one. The \"In\" suffix mirrors the transIn/transOut\n // convention on <media> widgets. Absent = use the renderer's\n // configured default (or \"instant\" if no default is set).\n //\n // Supported types: instant (default, hard cut), fade, slide, wipe.\n // Direction (slide + wipe) uses the same 8-way compass as widget fly:\n // N, NE, E, SE, S, SW, W, NW.\n const layoutTransitionInType = layoutEl.getAttribute('layoutTransitionIn');\n const layoutTransitionInDurationAttr = layoutEl.getAttribute(\n 'layoutTransitionInDuration'\n );\n const layoutTransitionInDirection = layoutEl.getAttribute(\n 'layoutTransitionInDirection'\n );\n\n const layout = {\n schemaVersion: parseInt(layoutEl.getAttribute('schemaVersion') || '1'),\n width: parseInt(layoutEl.getAttribute('width') || '1920'),\n height: parseInt(layoutEl.getAttribute('height') || '1080'),\n duration: layoutDurationAttr ? parseInt(layoutDurationAttr) : 0, // 0 = calculate from widgets\n bgcolor: layoutEl.getAttribute('backgroundColor') || layoutEl.getAttribute('bgcolor') || '#000000',\n background: layoutEl.getAttribute('background') || null, // Background image fileId\n enableStat: layoutEl.getAttribute('enableStat') !== '0', // absent or \"1\" = enabled\n actions: this.parseActions(layoutEl),\n layoutTransitionIn: layoutTransitionInType\n ? {\n type: layoutTransitionInType,\n duration: layoutTransitionInDurationAttr\n ? parseInt(layoutTransitionInDurationAttr)\n : undefined,\n direction: layoutTransitionInDirection || undefined,\n }\n : null,\n regions: []\n };\n\n if (layout.schemaVersion > 1) {\n this.log.debug(`XLF schema version: ${layout.schemaVersion}`);\n }\n\n if (layoutDurationAttr) {\n this.log.info(`Layout duration from XLF: ${layout.duration}s`);\n } else {\n this.log.info(`Layout duration NOT in XLF, will calculate from widgets`);\n }\n\n // Parse regions and drawers (drawers are invisible regions for interactive actions)\n const regionAndDrawerEls = layoutEl.querySelectorAll(':scope > region, :scope > drawer');\n for (const regionEl of regionAndDrawerEls) {\n const isDrawer = regionEl.tagName === 'drawer';\n const regionType = regionEl.getAttribute('type') || null;\n const region = {\n id: regionEl.getAttribute('id'),\n width: parseInt(regionEl.getAttribute('width') || '0'),\n height: parseInt(regionEl.getAttribute('height') || '0'),\n top: parseInt(regionEl.getAttribute('top') || '0'),\n left: parseInt(regionEl.getAttribute('left') || '0'),\n zindex: parseInt(regionEl.getAttribute('zindex') || (isDrawer ? '2000' : '0')),\n enableStat: regionEl.getAttribute('enableStat') !== '0',\n actions: this.parseActions(regionEl),\n exitTransition: null,\n transitionType: null, // Region-level default widget transition type\n transitionDuration: null,\n transitionDirection: null,\n loop: true, // Default: cycle widgets. Spec: loop=0 means single media stays visible\n isDrawer,\n isCanvas: regionType === 'canvas', // Canvas regions render all widgets simultaneously\n widgets: []\n };\n\n // Parse region-level options (exit transitions, loop)\n // Use direct children only to avoid matching <options> inside <media>\n const regionOptionsEl = Array.from(regionEl.children).find(el => el.tagName === 'options');\n if (regionOptionsEl) {\n const exitTransType = regionOptionsEl.querySelector('exitTransType');\n if (exitTransType && exitTransType.textContent) {\n const exitTransDuration = regionOptionsEl.querySelector('exitTransDuration');\n const exitTransDirection = regionOptionsEl.querySelector('exitTransDirection');\n region.exitTransition = {\n type: exitTransType.textContent,\n duration: parseInt((exitTransDuration && exitTransDuration.textContent) || '1000'),\n direction: (exitTransDirection && exitTransDirection.textContent) || 'N'\n };\n }\n\n // Region loop option: 0 = single media stays on screen, 1 = cycles (default)\n const loopEl = regionOptionsEl.querySelector('loop');\n if (loopEl) {\n region.loop = loopEl.textContent !== '0';\n }\n\n // Region-level default transition for widgets (applied if widget has no own transition)\n const transType = regionOptionsEl.querySelector('transitionType');\n if (transType && transType.textContent) {\n region.transitionType = transType.textContent;\n const transDuration = regionOptionsEl.querySelector('transitionDuration');\n const transDirection = regionOptionsEl.querySelector('transitionDirection');\n region.transitionDuration = parseInt((transDuration && transDuration.textContent) || '1000');\n region.transitionDirection = (transDirection && transDirection.textContent) || 'N';\n }\n }\n\n // Parse media/widgets (use direct children to avoid nested matches)\n for (const child of regionEl.children) {\n if (child.tagName !== 'media') continue;\n const widget = this.parseWidget(child);\n region.widgets.push(widget);\n }\n\n // Auto-detect canvas from CMS \"global\" widget (CMS bundles canvas sub-widgets\n // into a single type=\"global\" media element in the XLF)\n if (!region.isCanvas && region.widgets.some(w => w.type === 'global')) {\n region.isCanvas = true;\n }\n\n layout.regions.push(region);\n\n if (isDrawer) {\n this.log.info(`Parsed drawer: id=${region.id} with ${region.widgets.length} widgets`);\n }\n\n if (region.isCanvas) {\n this.log.info(`Parsed canvas region: id=${region.id} with ${region.widgets.length} widgets (all render simultaneously)`);\n }\n }\n\n // Calculate layout duration if not specified (duration=0)\n // Uses shared parseLayoutDuration() — single source of truth for XLF-based duration calc\n if (layout.duration === 0) {\n const { duration, isDynamic } = parseLayoutDuration(xlfXml);\n layout.duration = duration;\n layout.isDynamic = isDynamic;\n this.log.info(`Calculated layout duration: ${layout.duration}s (not specified in XLF)${isDynamic ? ' [dynamic — has useDuration=0 video]' : ''}`);\n }\n\n return layout;\n }\n\n /**\n * Parse widget from media element\n * @param {Element} mediaEl - Media XML element\n * @returns {Object} Widget config\n */\n parseWidget(mediaEl) {\n const type = mediaEl.getAttribute('type');\n const duration = parseInt(mediaEl.getAttribute('duration') || '10');\n const useDuration = parseInt(mediaEl.getAttribute('useDuration') || '1');\n const id = mediaEl.getAttribute('id');\n const fileId = mediaEl.getAttribute('fileId'); // Media library file ID\n\n // Parse options\n const options = {};\n const optionsEl = mediaEl.querySelector('options');\n if (optionsEl) {\n for (const child of optionsEl.children) {\n options[child.tagName] = child.textContent;\n }\n }\n\n // Parse raw content\n const rawEl = mediaEl.querySelector('raw');\n const raw = rawEl ? rawEl.textContent : '';\n\n // Parse transitions\n const transitions = {\n in: null,\n out: null\n };\n\n if (options.transIn) {\n transitions.in = {\n type: options.transIn,\n duration: parseInt(options.transInDuration || '1000'),\n direction: options.transInDirection || 'N'\n };\n }\n\n if (options.transOut) {\n transitions.out = {\n type: options.transOut,\n duration: parseInt(options.transOutDuration || '1000'),\n direction: options.transOutDirection || 'N'\n };\n }\n\n // Parse widget-level actions\n const actions = this.parseActions(mediaEl);\n\n // Parse audio overlay nodes (<audio> child elements on the widget)\n // Spec format: <audio><uri volume=\"\" loop=\"\" mediaId=\"\">filename.mp3</uri></audio>\n // Also supports flat format: <audio mediaId=\"\" uri=\"\" volume=\"\" loop=\"\">\n const audioNodes = [];\n for (const child of mediaEl.children) {\n if (child.tagName.toLowerCase() === 'audio') {\n const uriEl = child.querySelector('uri');\n if (uriEl) {\n // Spec format: attributes on <uri>, filename as text content\n audioNodes.push({\n mediaId: uriEl.getAttribute('mediaId') || null,\n uri: uriEl.textContent || '',\n volume: parseInt(uriEl.getAttribute('volume') || '100'),\n loop: uriEl.getAttribute('loop') === '1'\n });\n } else {\n // Flat format fallback: attributes directly on <audio>\n audioNodes.push({\n mediaId: child.getAttribute('mediaId') || null,\n uri: child.getAttribute('uri') || '',\n volume: parseInt(child.getAttribute('volume') || '100'),\n loop: child.getAttribute('loop') === '1'\n });\n }\n }\n }\n\n // Parse commands on media (shell/native commands triggered on widget start)\n // Spec: <commands><command commandCode=\"code\" commandString=\"args\"/></commands>\n const commands = [];\n const commandsEl = Array.from(mediaEl.children).find(el => el.tagName === 'commands');\n if (commandsEl) {\n for (const cmdEl of commandsEl.children) {\n if (cmdEl.tagName === 'command') {\n commands.push({\n commandCode: cmdEl.getAttribute('commandCode') || '',\n commandString: cmdEl.getAttribute('commandString') || ''\n });\n }\n }\n }\n\n // Sub-playlist attributes (widgets grouped by parentWidgetId)\n const parentWidgetId = mediaEl.getAttribute('parentWidgetId') || null;\n const displayOrder = parseInt(mediaEl.getAttribute('displayOrder') || '0');\n const cyclePlayback = mediaEl.getAttribute('cyclePlayback') === '1';\n const playCount = parseInt(mediaEl.getAttribute('playCount') || '0');\n const isRandom = mediaEl.getAttribute('isRandom') === '1';\n\n // Media expiry dates (per-widget time-gating within a layout)\n const fromDt = mediaEl.getAttribute('fromDt') || mediaEl.getAttribute('fromdt') || null;\n const toDt = mediaEl.getAttribute('toDt') || mediaEl.getAttribute('todt') || null;\n\n // Render mode: 'native' (player renders directly) or 'html' (use GetResource)\n const render = mediaEl.getAttribute('render') || null;\n\n return {\n type,\n duration,\n useDuration, // Whether to use specified duration (1) or media length (0)\n id,\n fileId, // Media library file ID for cache lookup\n render, // 'native' or 'html' — null means use type-based dispatch\n fromDt, // Widget valid-from date (Y-m-d H:i:s)\n toDt, // Widget valid-to date (Y-m-d H:i:s)\n enableStat: mediaEl.getAttribute('enableStat') !== '0', // absent or \"1\" = enabled\n webhookUrl: options.webhookUrl || null,\n options,\n raw,\n transitions,\n actions,\n audioNodes, // Audio overlays attached to this widget\n commands, // Shell commands triggered on widget start\n parentWidgetId,\n displayOrder,\n cyclePlayback,\n playCount,\n isRandom\n };\n }\n\n /**\n * Track blob URL for lifecycle management\n * @param {string} blobUrl - Blob URL to track\n */\n trackBlobUrl(blobUrl) {\n const layoutId = this._preloadingLayoutId || this.currentLayoutId || 0;\n\n if (!layoutId) {\n this.log.warn('trackBlobUrl called without currentLayoutId, tracking under key 0');\n }\n\n if (!this.layoutBlobUrls.has(layoutId)) {\n this.layoutBlobUrls.set(layoutId, new Set());\n }\n\n this.layoutBlobUrls.get(layoutId).add(blobUrl);\n }\n\n /**\n * Revoke all blob URLs for a specific layout\n * @param {number} layoutId - Layout ID\n */\n revokeBlobUrlsForLayout(layoutId) {\n const blobUrls = this.layoutBlobUrls.get(layoutId);\n if (blobUrls) {\n blobUrls.forEach(url => {\n URL.revokeObjectURL(url);\n });\n this.layoutBlobUrls.delete(layoutId);\n this.log.info(`Revoked ${blobUrls.size} blob URLs for layout ${layoutId}`);\n }\n }\n\n /**\n * Update layout duration based on actual widget durations\n * Called when video metadata loads and we discover actual duration\n */\n updateLayoutDuration() {\n if (!this.currentLayout) return;\n\n // Calculate maximum region duration\n let maxRegionDuration = 0;\n\n for (const region of this.currentLayout.regions) {\n if (region.isDrawer) continue;\n let regionDuration = 0;\n\n for (const widget of region.widgets) {\n if (widget.duration > 0) {\n regionDuration += widget.duration;\n }\n }\n\n maxRegionDuration = Math.max(maxRegionDuration, regionDuration);\n }\n\n // Update layout duration if recalculated value differs.\n // Both upgrades (video metadata revealing longer duration) and downgrades\n // (DURATION comment correcting an overestimate) are legitimate.\n if (maxRegionDuration > 0 && maxRegionDuration !== this.currentLayout.duration) {\n const oldDuration = this.currentLayout.duration;\n this.currentLayout.duration = maxRegionDuration;\n this.currentLayout._durationFromMetadata = true;\n\n this.log.info(`Layout duration updated: ${oldDuration}s → ${maxRegionDuration}s (based on video metadata)`);\n const final_ = !this._hasUnprobedVideos();\n this.emit('layoutDurationUpdated', this.currentLayoutId, maxRegionDuration, final_);\n\n // Deferred timer: video metadata arrived, start the timer now\n if (this._deferredTimerLayoutId === this.currentLayoutId && !this.layoutTimer) {\n if (this._hasUnprobedVideos()) {\n this.log.info(`Layout duration updated to ${maxRegionDuration}s but still has unprobed videos — keeping timer deferred`);\n } else {\n // Cancel safety fallback — metadata arrived in time\n if (this._deferredTimerFallback) {\n clearTimeout(this._deferredTimerFallback);\n this._deferredTimerFallback = null;\n }\n const elapsed = Date.now() - (this._layoutTimerStartedAt || Date.now());\n const remainingMs = Math.max(1000, maxRegionDuration * 1000 - elapsed);\n this._deferredTimerLayoutId = null;\n this._layoutTimerDurationMs = remainingMs;\n this.layoutTimer = setTimeout(() => {\n this.log.info(`Layout ${this.currentLayoutId} duration expired (${this.currentLayout.duration}s)`);\n if (this.currentLayoutId) {\n this.layoutEndEmitted = true;\n this.emit('layoutEnd', this.currentLayoutId);\n }\n }, remainingMs);\n this.log.info(`All video durations resolved — deferred timer started: ${(remainingMs / 1000).toFixed(1)}s remaining (waited ${(elapsed / 1000).toFixed(1)}s for metadata)`);\n }\n } else if (this.layoutTimer) {\n // Reset layout timer with REMAINING time — not full duration.\n clearTimeout(this.layoutTimer);\n\n const elapsed = Date.now() - (this._layoutTimerStartedAt || Date.now());\n const remainingMs = Math.max(1000, this.currentLayout.duration * 1000 - elapsed);\n this.layoutTimer = setTimeout(() => {\n this.log.info(`Layout ${this.currentLayoutId} duration expired (${this.currentLayout.duration}s)`);\n if (this.currentLayoutId) {\n this.layoutEndEmitted = true;\n this.emit('layoutEnd', this.currentLayoutId);\n }\n }, remainingMs);\n\n this.log.info(`Layout timer adjusted to ${(remainingMs / 1000).toFixed(1)}s remaining (elapsed ${(elapsed / 1000).toFixed(1)}s of ${this.currentLayout.duration}s)`);\n } else {\n this.log.info(`Layout duration updated to ${maxRegionDuration}s (timer not yet started, will use new value)`);\n }\n\n // Reschedule preload timer — the initial preload was based on the old\n // duration estimate (e.g. 45s for 60s default). With the real duration\n // (e.g. 375s), the preload should fire much later so that schedule\n // cooldowns (maxPlaysPerHour) have time to expire.\n this._scheduleNextLayoutPreload(this.currentLayout);\n }\n }\n\n // ── Interactive Actions ──────────────────────────────────────────────\n\n /**\n * Attach interactive action event listeners for a layout.\n * Binds touch/click on region/widget elements and a single document keydown handler.\n */\n attachActionListeners(layout) {\n const allKeyboardActions = [];\n let touchActionCount = 0;\n\n // Layout-level actions (attached to the main container)\n for (const action of (layout.actions || [])) {\n if (action.triggerType === 'touch') {\n this.attachTouchAction(this.container, action, null, null);\n touchActionCount++;\n } else if (action.triggerType?.startsWith('keyboard:')) {\n allKeyboardActions.push(action);\n }\n }\n\n for (const regionConfig of layout.regions) {\n const region = this.regions.get(regionConfig.id);\n if (!region) continue;\n\n // Region-level actions\n for (const action of (regionConfig.actions || [])) {\n if (action.triggerType === 'touch') {\n this.attachTouchAction(region.element, action, regionConfig.id, null);\n touchActionCount++;\n } else if (action.triggerType.startsWith('keyboard:')) {\n allKeyboardActions.push(action);\n }\n }\n\n // Widget-level actions\n for (const widget of regionConfig.widgets) {\n if (!widget.actions || widget.actions.length === 0) continue;\n const widgetEl = region.widgetElements.get(widget.id);\n if (!widgetEl) continue;\n\n for (const action of widget.actions) {\n if (action.triggerType === 'touch') {\n this.attachTouchAction(widgetEl, action, regionConfig.id, widget.id);\n touchActionCount++;\n } else if (action.triggerType.startsWith('keyboard:')) {\n allKeyboardActions.push(action);\n }\n }\n }\n }\n\n this.setupKeyboardListener(allKeyboardActions);\n\n if (touchActionCount > 0 || allKeyboardActions.length > 0) {\n this.log.info(`Actions attached: ${touchActionCount} touch, ${allKeyboardActions.length} keyboard`);\n }\n }\n\n /**\n * Attach a click listener to an element for a touch-triggered action.\n */\n attachTouchAction(element, action, regionId, widgetId) {\n element.style.cursor = 'pointer';\n\n const handler = (event) => {\n event.stopPropagation();\n const source = widgetId ? `widget ${widgetId}` : `region ${regionId}`;\n this.log.info(`Touch action fired on ${source}: ${action.actionType}`);\n\n this.emit('action-trigger', {\n actionType: action.actionType,\n triggerType: 'touch',\n triggerCode: action.triggerCode,\n layoutCode: action.layoutCode,\n targetId: action.targetId,\n commandCode: action.commandCode,\n source: { regionId, widgetId }\n });\n };\n\n element.addEventListener('click', handler);\n if (!element._actionHandlers) element._actionHandlers = [];\n element._actionHandlers.push(handler);\n }\n\n /**\n * Setup document-level keyboard listener for keyboard-triggered actions.\n */\n setupKeyboardListener(keyboardActions) {\n this.removeKeyboardListener();\n this._keyboardActions = keyboardActions;\n if (keyboardActions.length === 0) return;\n\n this._keydownHandler = (event) => {\n const pressedKey = event.key;\n for (const action of this._keyboardActions) {\n const keycode = action.triggerType.substring('keyboard:'.length);\n if (pressedKey === keycode) {\n this.log.info(`Keyboard action (key: ${pressedKey}): ${action.actionType}`);\n this.emit('action-trigger', {\n actionType: action.actionType,\n triggerType: action.triggerType,\n triggerCode: action.triggerCode,\n layoutCode: action.layoutCode,\n targetId: action.targetId,\n commandCode: action.commandCode,\n source: { key: pressedKey }\n });\n break;\n }\n }\n };\n\n document.addEventListener('keydown', this._keydownHandler);\n }\n\n /** Remove the document-level keyboard listener */\n removeKeyboardListener() {\n if (this._keydownHandler) {\n document.removeEventListener('keydown', this._keydownHandler);\n this._keydownHandler = null;\n }\n this._keyboardActions = [];\n }\n\n /** Remove all action listeners (touch + keyboard) */\n removeActionListeners() {\n for (const [, region] of this.regions) {\n this._cleanElementActionHandlers(region.element);\n for (const [, widgetEl] of region.widgetElements) {\n this._cleanElementActionHandlers(widgetEl);\n }\n }\n this.removeKeyboardListener();\n }\n\n _cleanElementActionHandlers(element) {\n if (element._actionHandlers) {\n for (const handler of element._actionHandlers) {\n element.removeEventListener('click', handler);\n }\n delete element._actionHandlers;\n element.style.cursor = '';\n }\n }\n\n // ── Interactive Control (XIC) ─────────────────────────────────────\n\n /**\n * Find a region containing a widget by widget ID.\n * Searches main regions first, then overlay regions.\n * @param {string} widgetId\n * @returns {{ regionId: string, region: Object, widget: Object, widgetIndex: number, regionMap: Map }|null}\n */\n _findRegionByWidgetId(widgetId) {\n // Search main regions\n for (const [regionId, region] of this.regions) {\n const widgetIndex = region.widgets.findIndex(w => w.id === widgetId);\n if (widgetIndex !== -1) {\n return { regionId, region, widget: region.widgets[widgetIndex], widgetIndex, regionMap: this.regions };\n }\n }\n // Search overlay regions\n for (const overlay of this.activeOverlays.values()) {\n if (!overlay.regions) continue;\n for (const [regionId, region] of overlay.regions) {\n const widgetIndex = region.widgets.findIndex(w => w.id === widgetId);\n if (widgetIndex !== -1) {\n return { regionId, region, widget: region.widgets[widgetIndex], widgetIndex, regionMap: overlay.regions };\n }\n }\n }\n return null;\n }\n\n /**\n * Advance a region to its next widget using the standard cycle.\n * @param {string} regionId\n * @param {Map} regionMap - The Map containing this region (main or overlay)\n */\n _advanceRegion(regionId, regionMap) {\n const region = regionMap.get(regionId);\n if (!region) return;\n region.currentIndex = (region.currentIndex + 1) % region.widgets.length;\n const isMain = regionMap === this.regions;\n this._startRegionCycle(\n region, regionId,\n isMain ? this._renderWidgetBound : this._renderWidgetBound,\n isMain ? this._stopWidgetBound : this._stopWidgetBound,\n isMain ? () => this.checkLayoutComplete() : undefined\n );\n }\n\n /**\n * Handle interactiveTrigger XIC event — navigate to a target widget.\n * @param {{ targetId: string, triggerCode: string }} data\n */\n _handleInteractiveTrigger({ targetId, triggerCode }) {\n this.log.info(`XIC interactiveTrigger: target=${targetId} code=${triggerCode}`);\n const found = this._findRegionByWidgetId(targetId);\n if (found) {\n this.navigateToWidget(targetId);\n } else {\n this.log.warn(`XIC interactiveTrigger: widget ${targetId} not found`);\n }\n }\n\n /**\n * Handle widgetExpire XIC event — immediately expire a widget and advance.\n * @param {{ widgetId: string }} data\n */\n _handleWidgetExpire({ widgetId }) {\n const found = this._findRegionByWidgetId(widgetId);\n if (!found) {\n this.log.warn(`XIC widgetExpire: widget ${widgetId} not found`);\n return;\n }\n const { regionId, region, widgetIndex, regionMap } = found;\n this.log.info(`XIC widgetExpire: widget=${widgetId} region=${regionId}`);\n if (region.timer) {\n clearTimeout(region.timer);\n region.timer = null;\n }\n this.stopWidget(regionId, widgetIndex);\n this._advanceRegion(regionId, regionMap);\n }\n\n /**\n * Handle widgetExtendDuration XIC event — extend the current widget timer.\n * @param {{ widgetId: string, duration: number }} data - duration in seconds (added to remaining)\n */\n _handleWidgetExtendDuration({ widgetId, duration }) {\n const found = this._findRegionByWidgetId(widgetId);\n if (!found) {\n this.log.warn(`XIC widgetExtendDuration: widget ${widgetId} not found`);\n return;\n }\n const { regionId, region } = found;\n this.log.info(`XIC widgetExtendDuration: widget=${widgetId} +${duration}s`);\n if (region.timer) {\n clearTimeout(region.timer);\n region.timer = null;\n }\n // Re-arm timer with the extended duration\n region.timer = setTimeout(() => {\n this.stopWidget(regionId, region.currentIndex);\n this._advanceRegion(regionId, found.regionMap);\n }, duration * 1000);\n }\n\n /**\n * Handle widgetSetDuration XIC event — replace the widget timer with an absolute duration.\n * @param {{ widgetId: string, duration: number }} data - duration in seconds (absolute)\n */\n _handleWidgetSetDuration({ widgetId, duration }) {\n const found = this._findRegionByWidgetId(widgetId);\n if (!found) {\n this.log.warn(`XIC widgetSetDuration: widget ${widgetId} not found`);\n return;\n }\n const { regionId, region } = found;\n this.log.info(`XIC widgetSetDuration: widget=${widgetId} ${duration}s`);\n if (region.timer) {\n clearTimeout(region.timer);\n region.timer = null;\n }\n // Set timer with the absolute duration\n region.timer = setTimeout(() => {\n this.stopWidget(regionId, region.currentIndex);\n this._advanceRegion(regionId, found.regionMap);\n }, duration * 1000);\n }\n\n /**\n * Navigate to a specific widget within a region (for navWidget actions)\n */\n navigateToWidget(targetWidgetId) {\n for (const [regionId, region] of this.regions) {\n const widgetIndex = region.widgets.findIndex(w => w.id === targetWidgetId);\n if (widgetIndex === -1) continue;\n\n this.log.info(`Navigating to widget ${targetWidgetId} in region ${regionId} (index ${widgetIndex})`);\n\n // Show drawer region if hidden (drawers start display:none)\n if (region.isDrawer && region.element.style.display === 'none') {\n region.element.style.display = '';\n this.log.info(`Drawer region ${regionId} revealed`);\n }\n\n if (region.timer) {\n clearTimeout(region.timer);\n region.timer = null;\n }\n\n this.stopWidget(regionId, region.currentIndex);\n region.currentIndex = widgetIndex;\n this.renderWidget(regionId, widgetIndex);\n\n if (region.widgets.length > 1) {\n const widget = region.widgets[widgetIndex];\n const duration = widget.duration * 1000;\n region.timer = setTimeout(() => {\n this.stopWidget(regionId, widgetIndex);\n const nextIndex = (widgetIndex + 1) % region.widgets.length;\n region.currentIndex = nextIndex;\n // For drawers, hide again after last widget; for normal regions, continue cycling\n if (region.isDrawer && nextIndex === 0) {\n region.element.style.display = 'none';\n this.log.info(`Drawer region ${regionId} hidden (cycle complete)`);\n } else if (region.isDrawer) {\n // Continue cycling through remaining drawer widgets (will hide on wrap to 0)\n this.navigateToWidget(region.widgets[nextIndex].id);\n } else {\n this.startRegion(regionId);\n }\n }, duration);\n } else if (region.isDrawer) {\n // Single-widget drawer: hide after widget duration\n const widget = region.widgets[widgetIndex];\n const duration = widget.duration * 1000;\n region.timer = setTimeout(() => {\n this.stopWidget(regionId, widgetIndex);\n region.element.style.display = 'none';\n this.log.info(`Drawer region ${regionId} hidden (single widget done)`);\n }, duration);\n }\n return;\n }\n this.log.warn(`Target widget ${targetWidgetId} not found in any region`);\n }\n\n /**\n * Navigate to the next widget in a region (wraps around)\n * @param {string} [regionId] - Target region. If omitted, uses the first region.\n */\n nextWidget(regionId) {\n const region = regionId ? this.regions.get(regionId) : this.regions.values().next().value;\n if (!region || region.widgets.length <= 1) return;\n\n const nextIndex = (region.currentIndex + 1) % region.widgets.length;\n const targetWidget = region.widgets[nextIndex];\n this.log.info(`nextWidget → index ${nextIndex} (widget ${targetWidget.id})`);\n this.navigateToWidget(targetWidget.id);\n }\n\n /**\n * Navigate to the previous widget in a region (wraps around)\n * @param {string} [regionId] - Target region. If omitted, uses the first region.\n */\n previousWidget(regionId) {\n const region = regionId ? this.regions.get(regionId) : this.regions.values().next().value;\n if (!region || region.widgets.length <= 1) return;\n\n const prevIndex = (region.currentIndex - 1 + region.widgets.length) % region.widgets.length;\n const targetWidget = region.widgets[prevIndex];\n this.log.info(`previousWidget → index ${prevIndex} (widget ${targetWidget.id})`);\n this.navigateToWidget(targetWidget.id);\n }\n\n // ── Layout Helpers ───────────────────────────────────────────────\n\n /**\n * Get media file URL for storedAs filename.\n * @param {string} storedAs - The storedAs filename (e.g. \"42_abc123.jpg\")\n * @returns {string} Full URL for the media file\n */\n _mediaFileUrl(storedAs) {\n return `${window.location.origin}${PLAYER_API}/media/file/${storedAs}`;\n }\n\n /**\n * Position a widget element to fill its region (hidden by default).\n * @param {HTMLElement} element\n */\n _positionWidgetElement(element) {\n Object.assign(element.style, {\n position: 'absolute',\n top: '0',\n left: '0',\n width: '100%',\n height: '100%',\n visibility: 'hidden',\n opacity: '0',\n });\n }\n\n /**\n * Apply a background image with cover styling.\n * @param {HTMLElement} element\n * @param {string} url - Image URL\n */\n _applyBackgroundImage(element, url) {\n Object.assign(element.style, {\n backgroundImage: `url(${url})`,\n backgroundSize: 'cover',\n backgroundPosition: 'center',\n backgroundRepeat: 'no-repeat',\n });\n }\n\n /**\n * Clear all region timers in a region map.\n * @param {Map} regions - Region map (regionId → region)\n */\n _clearRegionTimers(regions) {\n for (const [, region] of regions) {\n if (region.timer) {\n clearTimeout(region.timer);\n region.timer = null;\n }\n }\n }\n\n // ── Layout Rendering ──────────────────────────────────────────────\n\n /**\n * Render a layout\n * @param {string} xlfXml - XLF XML content\n * @param {number} layoutId - Layout ID\n * @returns {Promise<void>}\n */\n async renderLayout(xlfXml, layoutId) {\n try {\n this.log.info(`Rendering layout ${layoutId}`);\n\n // Check if we're replaying the same layout\n const isSameLayout = this.currentLayoutId === layoutId;\n\n if (isSameLayout) {\n // OPTIMIZATION: Reuse existing elements for same layout (Arexibo pattern)\n this.log.info(`Replaying layout ${layoutId} - reusing elements (no recreation!)`);\n\n // Stop all region timers and widgets, then reset to first widget\n this._clearRegionTimers(this.regions);\n this._stopAllRegionWidgets(this.regions, this._stopWidgetBound);\n for (const [, region] of this.regions) {\n region.currentIndex = 0;\n region.complete = false;\n }\n\n // Clear layout timer\n if (this.layoutTimer) {\n clearTimeout(this.layoutTimer);\n this.layoutTimer = null;\n }\n\n this.layoutEndEmitted = false;\n this._deferredTimerLayoutId = null;\n if (this._deferredTimerFallback) {\n clearTimeout(this._deferredTimerFallback);\n this._deferredTimerFallback = null;\n }\n\n // DON'T call stopCurrentLayout() - keep elements alive!\n // DON'T recreate regions/elements - already exist!\n\n // Emit layout start event\n this.emit('layoutStart', layoutId, this.currentLayout);\n\n // Restart all regions from widget 0 (except drawers)\n for (const [regionId, region] of this.regions) {\n if (region.isDrawer) continue;\n this.startRegion(regionId);\n }\n\n // Wait for all initial widgets to be ready then start layout timer\n this.startLayoutTimerWhenReady(layoutId, this.currentLayout);\n\n this.log.info(`Layout ${layoutId} restarted (reused elements)`);\n\n // Schedule next layout preload for same-layout replay\n this._scheduleNextLayoutPreload(this.currentLayout);\n\n return; // EARLY RETURN - skip recreation below\n }\n\n // Check if this layout was preloaded in the pool\n if (this.layoutPool.has(layoutId)) {\n this.log.info(`Layout ${layoutId} found in preload pool - instant swap!`);\n await this._swapToPreloadedLayout(layoutId);\n return; // EARLY RETURN - preloaded layout swapped in\n }\n\n // Different layout - full teardown and rebuild\n this.log.info(`Switching to new layout ${layoutId}`);\n this.stopCurrentLayout();\n\n // Parse XLF\n const layout = this.parseXlf(xlfXml);\n this.currentLayout = layout;\n this.currentLayoutId = layoutId;\n\n // Calculate scale factor to fit layout into screen\n this.calculateScale(layout);\n\n // Set container background\n this.container.style.backgroundColor = layout.bgcolor;\n this.container.style.backgroundImage = ''; // Reset previous\n\n // Apply background image if specified in XLF\n // With storedAs refactor, background may be a filename (e.g. \"43.png\") or a numeric fileId\n if (layout.background) {\n const saveAs = this.options.fileIdToSaveAs?.get(String(layout.background)) || layout.background;\n this._applyBackgroundImage(this.container, this._mediaFileUrl(saveAs));\n this.log.info(`Background image set: ${layout.background} → ${saveAs}`);\n }\n\n // Create regions\n for (const regionConfig of layout.regions) {\n await this.createRegion(regionConfig);\n }\n\n // PRE-CREATE: Build all widget elements upfront (Arexibo pattern)\n this.log.info('Pre-creating widget elements for instant transitions...');\n for (const [regionId, region] of this.regions) {\n for (let i = 0; i < region.widgets.length; i++) {\n const widget = region.widgets[i];\n widget.layoutId = this.currentLayoutId;\n widget.regionId = regionId;\n\n try {\n const element = await this.createWidgetElement(widget, region);\n this._positionWidgetElement(element);\n region.element.appendChild(element);\n region.widgetElements.set(widget.id, element);\n } catch (error) {\n this.log.error(`Failed to pre-create widget ${widget.id}:`, error);\n }\n }\n }\n this.log.info('All widget elements pre-created');\n\n // Attach interactive action listeners (touch/click and keyboard)\n this.attachActionListeners(layout);\n\n // Emit layout start event\n this.emit('layoutStart', layoutId, layout);\n\n // Report calculated duration so the schedule queue/timeline uses it\n // instead of the 60s default. For layouts with unprobed videos, this\n // is an estimate that will be corrected by updateLayoutDuration().\n if (layout.duration > 0) {\n const final_ = !this._hasUnprobedVideos();\n this.emit('layoutDurationUpdated', layoutId, layout.duration, final_);\n }\n\n // Start all regions (except drawers — they're action-triggered)\n for (const [regionId, region] of this.regions) {\n if (region.isDrawer) continue;\n this.startRegion(regionId);\n }\n\n // Wait for all initial widgets to be ready (videos playing, images loaded)\n // THEN start the layout timer — ensures videos play to their last frame\n this.startLayoutTimerWhenReady(layoutId, layout);\n\n // Schedule preloading of the next layout at 75% of current duration\n this._scheduleNextLayoutPreload(layout);\n\n this.log.info(`Layout ${layoutId} started`);\n\n } catch (error) {\n this.log.error('Error rendering layout:', error);\n this.emit('error', { type: 'layoutError', error, layoutId });\n throw error;\n }\n }\n\n /**\n * Build a region DOM element and state entry.\n * Shared by createRegion, preloadLayout, and renderOverlay.\n *\n * @param {Object} regionConfig - Region configuration from parsed XLF\n * @param {string} elementId - DOM element ID for the region div\n * @param {HTMLElement} parentEl - Parent element to append the region to\n * @param {Object} [extraState] - Additional properties merged into region state\n * @returns {Object} Region state object { element, config, widgets, ... }\n */\n _createRegionEntry(regionConfig, elementId, parentEl, extraState = {}) {\n const { className = 'renderer-lite-region', ...stateProps } = extraState;\n\n const regionEl = document.createElement('div');\n regionEl.id = elementId;\n regionEl.className = className;\n regionEl.style.position = 'absolute';\n regionEl.style.zIndex = String(regionConfig.zindex);\n regionEl.style.overflow = 'hidden';\n\n // Apply scaled positioning\n this.applyRegionScale(regionEl, regionConfig);\n\n parentEl.appendChild(regionEl);\n\n const sf = this.scaleFactor;\n return {\n element: regionEl,\n config: regionConfig,\n widgets: regionConfig.widgets,\n currentIndex: 0,\n timer: null,\n width: regionConfig.width * sf,\n height: regionConfig.height * sf,\n complete: false,\n widgetElements: new Map(),\n ...stateProps,\n };\n }\n\n /**\n * Create a region element\n * @param {Object} regionConfig - Region configuration\n */\n async createRegion(regionConfig) {\n const region = this._createRegionEntry(\n regionConfig,\n `region_${regionConfig.id}`,\n this.container,\n {\n isDrawer: regionConfig.isDrawer || false,\n isCanvas: regionConfig.isCanvas || false,\n }\n );\n\n // Drawer regions start fully hidden — shown only by navWidget actions\n if (regionConfig.isDrawer) {\n region.element.style.display = 'none';\n }\n\n // Filter expired widgets (fromDt/toDt time-gating within XLF)\n let widgets = regionConfig.widgets.filter(w => this._isWidgetActive(w));\n\n // For regions with sub-playlist cycle playback, select which widgets play this cycle\n if (widgets.some(w => w.cyclePlayback)) {\n widgets = this._applyCyclePlayback(widgets);\n }\n region.widgets = widgets;\n\n this.regions.set(regionConfig.id, region);\n }\n\n /**\n * Start playing a region's widgets\n * @param {string} regionId - Region ID\n */\n startRegion(regionId) {\n const region = this.regions.get(regionId);\n this._startRegionCycle(\n region, regionId,\n this._renderWidgetBound,\n this._stopWidgetBound,\n () => {\n this.log.info(`Region ${regionId} completed one full cycle`);\n this.checkLayoutComplete();\n }\n );\n }\n\n /**\n * Create a widget element (extracted for pre-creation)\n * @param {Object} widget - Widget config\n * @param {Object} region - Region state\n * @returns {Promise<HTMLElement>} Widget DOM element\n */\n async createWidgetElement(widget, region) {\n // render=\"html\" forces GetResource iframe regardless of native type,\n // EXCEPT for types we handle natively (PDF: CMS bundle can't work cross-origin)\n if (widget.render === 'html' && widget.type !== 'pdf') {\n return await this.renderGenericWidget(widget, region);\n }\n\n switch (widget.type) {\n case 'image':\n return await this.renderImage(widget, region);\n case 'video':\n return await this.renderVideo(widget, region);\n case 'audio':\n return await this.renderAudio(widget, region);\n case 'text':\n case 'ticker':\n return await this.renderTextWidget(widget, region);\n case 'pdf':\n return await this.renderPdf(widget, region);\n case 'webpage':\n return await this.renderWebpage(widget, region);\n case 'localvideo':\n return await this.renderVideo(widget, region);\n case 'videoin':\n return await this.renderVideoIn(widget, region);\n case 'powerpoint':\n case 'flash':\n // Legacy Windows-only types — show placeholder instead of failing silently\n this.log.warn(`Widget type '${widget.type}' is not supported on web players (widget ${widget.id})`);\n return this._renderUnsupportedPlaceholder(widget, region);\n default:\n // Generic widget (clock, calendar, weather, etc.)\n return await this.renderGenericWidget(widget, region);\n }\n }\n\n /**\n * Helper: Find media element within widget (works for both direct and wrapped elements)\n * @param {HTMLElement} element - Widget element (might BE the media element or contain it)\n * @param {string} tagName - Tag name to find ('VIDEO', 'AUDIO', 'IMG', 'IFRAME')\n * @returns {HTMLElement|null}\n */\n findMediaElement(element, tagName) {\n // Check if element IS the tag, or contains it as a descendant\n return element.tagName === tagName ? element : element.querySelector(tagName.toLowerCase());\n }\n\n /**\n * Update media element for dynamic content (videos/audio need restart)\n * @param {HTMLElement} element - Widget element\n * @param {Object} widget - Widget config\n */\n updateMediaElement(element, widget) {\n // Restart video or audio on widget show (even if looping)\n const mediaEl = this.findMediaElement(element, 'VIDEO') || this.findMediaElement(element, 'AUDIO');\n if (mediaEl) {\n // Re-acquire webcam stream if it was stopped during _hideWidget()\n if (mediaEl.tagName === 'VIDEO' && mediaEl._mediaConstraints && !mediaEl._mediaStream) {\n navigator.mediaDevices.getUserMedia(mediaEl._mediaConstraints).then(stream => {\n mediaEl.srcObject = stream;\n mediaEl._mediaStream = stream;\n this.log.info(`Webcam stream re-acquired for widget ${widget.id}`);\n }).catch(e => {\n this.log.warn('Failed to re-acquire webcam stream:', e.message);\n });\n return; // srcObject auto-plays, no need for _restartMediaElement\n }\n\n this._restartMediaElement(mediaEl);\n this.log.info(`${mediaEl.tagName === 'VIDEO' ? 'Video' : 'Audio'} restarted: ${widget.fileId || widget.id}`);\n }\n }\n\n /**\n * Restart a media element from the beginning.\n * Waits for seek to complete before playing — avoids DOMException\n * \"The play() request was interrupted\" when calling play() mid-seek.\n */\n _restartMediaElement(el) {\n el.currentTime = 0;\n const playAfterSeek = () => {\n el.removeEventListener('seeked', playAfterSeek);\n el.play().catch(() => {});\n };\n el.addEventListener('seeked', playAfterSeek);\n // Always call play() — for preloaded-then-paused videos, seeked may not\n // fire (currentTime already 0) and readyState may be < 2 (not buffered yet).\n // play() handles both cases: if not ready, it queues; if ready, it plays.\n el.play().catch(() => {});\n }\n\n /**\n * Wait for a widget's media to be ready for playback.\n * - Video: resolves when 'playing' fires (buffered enough to render frames)\n * - Image: resolves when 'load' fires (decoded and paintable)\n * - Text/embedded/clock: resolves immediately (inline content, no async load)\n * @param {HTMLElement} element - Widget DOM element\n * @param {Object} widget - Widget config\n * @returns {Promise<void>}\n */\n waitForWidgetReady(element, widget) {\n const READY_TIMEOUT = 10000; // 10s max wait — don't block forever on broken media\n\n // Video widgets: wait for actual playback\n const videoEl = this.findMediaElement(element, 'VIDEO');\n if (videoEl) {\n // Already playing (replay case where video was kept alive)\n if (!videoEl.paused && videoEl.readyState >= 3) {\n return Promise.resolve();\n }\n return new Promise((resolve) => {\n const timer = setTimeout(() => {\n this.log.warn(`Video ready timeout (${READY_TIMEOUT}ms) for widget ${widget.id}`);\n resolve();\n }, READY_TIMEOUT);\n const onPlaying = () => {\n videoEl.removeEventListener('playing', onPlaying);\n clearTimeout(timer);\n this.log.info(`Video widget ${widget.id} ready (playing)`);\n resolve();\n };\n videoEl.addEventListener('playing', onPlaying);\n });\n }\n\n // Audio widgets: wait for playback to start\n const audioEl = this.findMediaElement(element, 'AUDIO');\n if (audioEl) {\n if (!audioEl.paused && audioEl.readyState >= 3) {\n return Promise.resolve();\n }\n return new Promise((resolve) => {\n const timer = setTimeout(() => {\n this.log.warn(`Audio ready timeout (${READY_TIMEOUT}ms) for widget ${widget.id}`);\n resolve();\n }, READY_TIMEOUT);\n const onPlaying = () => {\n audioEl.removeEventListener('playing', onPlaying);\n clearTimeout(timer);\n this.log.info(`Audio widget ${widget.id} ready (playing)`);\n resolve();\n };\n audioEl.addEventListener('playing', onPlaying);\n });\n }\n\n // Image widgets: wait for image decode\n const imgEl = this.findMediaElement(element, 'IMG');\n if (imgEl) {\n if (imgEl.complete && imgEl.naturalWidth > 0) {\n return Promise.resolve();\n }\n return new Promise((resolve) => {\n const onLoad = () => {\n imgEl.removeEventListener('load', onLoad);\n clearTimeout(timer);\n resolve();\n };\n const timer = setTimeout(() => {\n imgEl.removeEventListener('load', onLoad);\n this.log.warn(`Image ready timeout for widget ${widget.id}`);\n resolve();\n }, READY_TIMEOUT);\n imgEl.addEventListener('load', onLoad);\n });\n }\n\n // Text, embedded, clock, etc. — ready immediately\n return Promise.resolve();\n }\n\n /**\n * Start the layout timer only after all initial widgets are ready.\n * This ensures that the layout duration counts from when content is\n * actually visible, so videos play their full duration to the last frame.\n * @param {number|string} layoutId - Layout ID\n * @param {Object} layout - Layout config with .duration\n */\n async startLayoutTimerWhenReady(layoutId, layout) {\n if (!layout || layout.duration <= 0) return;\n\n // Collect readiness promises for each region's first (current) widget\n const readyPromises = [];\n for (const [regionId, region] of this.regions) {\n if (region.widgets.length === 0) continue;\n const widget = region.widgets[region.currentIndex || 0];\n const element = region.widgetElements.get(widget.id);\n if (element) {\n readyPromises.push(this.waitForWidgetReady(element, widget));\n }\n }\n\n if (readyPromises.length > 0) {\n this.log.info(`Waiting for ${readyPromises.length} widget(s) to be ready before starting layout timer...`);\n await Promise.all(readyPromises);\n this.log.info(`All widgets ready — starting layout timer`);\n }\n\n // Guard: layout may have changed while we were waiting\n if (this.currentLayoutId !== layoutId) {\n this.log.warn(`Layout changed while waiting for widgets — skipping timer for ${layoutId}`);\n return;\n }\n\n // Dynamic layouts (useDuration=0 videos): defer timer until video metadata\n // provides real durations. Safety timeout ensures corrupt/missing videos\n // don't freeze the display forever.\n // Skip deferral if updateLayoutDuration() already set the duration from\n // video metadata (e.g. during preload or a previous play of this layout).\n if (layout.isDynamic && !layout._durationFromMetadata && this._hasUnprobedVideos()) {\n this._deferredTimerLayoutId = layoutId;\n this._layoutTimerStartedAt = Date.now();\n this.log.info(`Layout ${layoutId} has unprobed videos — deferring timer until metadata loads`);\n\n // Safety: if metadata never arrives (corrupt file, codec error), start\n // the timer with the estimated duration after 30s so the display keeps cycling.\n this._deferredTimerFallback = setTimeout(() => {\n this._deferredTimerFallback = null;\n if (this._deferredTimerLayoutId === layoutId && !this.layoutTimer) {\n this.log.warn(`Layout ${layoutId}: metadata timeout after 30s — starting timer with ${layout.duration}s estimate`);\n this._deferredTimerLayoutId = null;\n this._startLayoutTimer(layoutId, layout);\n }\n }, 30000);\n\n return;\n }\n\n this._startLayoutTimer(layoutId, layout);\n }\n\n /**\n * Check if any region's longest-running video widget (useDuration=0) hasn't\n * been probed yet. Used to decide whether to defer the layout timer.\n *\n * Only checks widgets that have had <video> elements created (during preload\n * or show). Widgets that haven't been displayed yet can never be probed —\n * checking them would always force a 30s timeout on layouts with multiple\n * video widgets per region.\n *\n * Returns false if the layout duration has already been updated from video\n * metadata (meaning at least one probe succeeded and updateLayoutDuration\n * computed a real duration), since the timer can start with that value.\n */\n _hasUnprobedVideos() {\n // If any video was probed and updateLayoutDuration ran, the layout duration\n // is already based on real metadata — no need to defer further.\n for (const [, region] of this.regions) {\n for (const widget of region.widgets) {\n if (widget.type === 'video' && widget.useDuration === 0 && widget._probed) return false;\n }\n }\n // No videos probed at all — check if there are any that need probing\n for (const [, region] of this.regions) {\n for (const widget of region.widgets) {\n if (widget.type === 'video' && widget.useDuration === 0) return true;\n }\n }\n return false;\n }\n\n /**\n * Actually start the layout timer. Called directly or after deferred timer resolves.\n */\n _startLayoutTimer(layoutId, layout) {\n this._deferredTimerLayoutId = null;\n if (this._deferredTimerFallback) {\n clearTimeout(this._deferredTimerFallback);\n this._deferredTimerFallback = null;\n }\n const layoutDurationMs = layout.duration * 1000;\n this.log.info(`Layout ${layoutId} will end after ${layout.duration}s`);\n\n this._layoutTimerStartedAt = Date.now();\n this._layoutTimerDurationMs = layoutDurationMs;\n this.layoutTimer = setTimeout(() => {\n this.log.info(`Layout ${layoutId} duration expired (${layout.duration}s)`);\n if (this.currentLayoutId) {\n this.layoutEndEmitted = true;\n this.emit('layoutEnd', this.currentLayoutId);\n }\n }, layoutDurationMs);\n }\n\n /**\n * Render a widget in a region (using element reuse)\n * @param {string} regionId - Region ID\n * @param {number} widgetIndex - Widget index in region\n */\n /**\n * Core: show a widget in a region (shared by main layout + overlay)\n * Returns the widget object on success, null on failure.\n */\n async _showWidget(region, widgetIndex) {\n const widget = region.widgets[widgetIndex];\n if (!widget) return null;\n\n let element = region.widgetElements.get(widget.id);\n\n if (!element) {\n this.log.warn(`Widget ${widget.id} not pre-created, creating now`);\n element = await this.createWidgetElement(widget, region);\n element.style.position = 'absolute';\n element.style.top = '0';\n element.style.left = '0';\n element.style.width = '100%';\n element.style.height = '100%';\n region.widgetElements.set(widget.id, element);\n region.element.appendChild(element);\n }\n\n // Hide all other widgets in region (skip for canvas — all widgets stay visible)\n // Cancel fill:forwards animations first — they override inline styles\n if (!region.isCanvas) {\n for (const [widgetId, widgetEl] of region.widgetElements) {\n if (widgetId !== widget.id) {\n widgetEl.getAnimations?.().forEach(a => a.cancel());\n widgetEl.style.visibility = 'hidden';\n widgetEl.style.opacity = '0';\n }\n }\n }\n\n this.updateMediaElement(element, widget);\n element.getAnimations?.().forEach(a => a.cancel());\n element.style.visibility = 'visible';\n\n if (widget.transitions.in) {\n Transitions.apply(element, widget.transitions.in, true, region.width, region.height);\n } else {\n element.style.opacity = '1';\n }\n\n // Resume PDF page cycling if this widget was previously paused\n if (element._pdfResume) {\n element._pdfResume();\n }\n\n // Start audio overlays attached to this widget\n this._startAudioOverlays(widget);\n\n return widget;\n }\n\n /**\n * Start audio overlay elements for a widget.\n * Audio overlays are <audio> child nodes in the XLF that play alongside\n * the visual widget (e.g. background music for an image slideshow).\n * @param {Object} widget - Widget config with audioNodes array\n */\n _startAudioOverlays(widget) {\n if (!widget.audioNodes || widget.audioNodes.length === 0) return;\n\n // Stop any existing audio overlays for this widget first\n this._stopAudioOverlays(widget.id);\n\n const audioElements = [];\n for (const audioNode of widget.audioNodes) {\n if (!audioNode.uri) continue;\n\n const audio = document.createElement('audio');\n audio.autoplay = true;\n audio.loop = audioNode.loop;\n audio.volume = Math.max(0, Math.min(1, audioNode.volume / 100));\n\n // Direct URL from storedAs filename\n audio.src = audioNode.uri ? this._mediaFileUrl(audioNode.uri) : '';\n\n // Append to DOM to prevent garbage collection in some browsers\n audio.style.display = 'none';\n this.container.appendChild(audio);\n\n // Handle autoplay restrictions gracefully (play() may return undefined in some envs)\n const playPromise = audio.play();\n if (playPromise && playPromise.catch) playPromise.catch(() => {});\n\n audioElements.push(audio);\n this.log.info(`Audio overlay started for widget ${widget.id}: ${audioNode.uri} (loop=${audioNode.loop}, vol=${audioNode.volume})`);\n }\n\n if (audioElements.length > 0) {\n this.audioOverlays.set(widget.id, audioElements);\n }\n }\n\n /**\n * Stop and clean up audio overlay elements for a widget.\n * @param {string} widgetId - Widget ID\n */\n _stopAudioOverlays(widgetId) {\n const audioElements = this.audioOverlays.get(widgetId);\n if (!audioElements) return;\n\n for (const audio of audioElements) {\n audio.pause();\n audio.removeAttribute('src');\n audio.load(); // Release resources\n if (audio.parentNode) audio.parentNode.removeChild(audio); // Remove from DOM\n }\n\n this.audioOverlays.delete(widgetId);\n this.log.info(`Audio overlays stopped for widget ${widgetId}`);\n }\n\n /**\n * Core: hide a widget in a region (shared by main layout + overlay).\n * Returns { widget, animPromise } synchronously — callers await animPromise if needed.\n * NOT async, so callers that don't need the animation stay on the same microtask.\n */\n _hideWidget(region, widgetIndex) {\n const widget = region.widgets[widgetIndex];\n if (!widget) return { widget: null, animPromise: null };\n\n const widgetElement = region.widgetElements.get(widget.id);\n if (!widgetElement) return { widget: null, animPromise: null };\n\n let animPromise = null;\n if (widget.transitions.out) {\n const animation = Transitions.apply(\n widgetElement, widget.transitions.out, false, region.width, region.height\n );\n if (animation) {\n animPromise = new Promise(resolve => { animation.onfinish = resolve; });\n }\n }\n\n const videoEl = widgetElement.querySelector('video');\n if (videoEl) {\n videoEl.pause();\n\n // Stop MediaStream tracks (webcam/mic) to release the device\n if (videoEl._mediaStream) {\n videoEl._mediaStream.getTracks().forEach(t => t.stop());\n videoEl._mediaStream = null;\n videoEl.srcObject = null;\n }\n\n // Destroy HLS.js instance to free worker + buffers\n if (videoEl._hlsInstance) {\n videoEl._hlsInstance.destroy();\n videoEl._hlsInstance = null;\n }\n\n // Release decoded video buffers (GPU dmabufs) — without this, paused\n // videos hold texture memory until the layout is evicted from the pool.\n // removeAttribute('src') + load() forces the browser to drop the decoded\n // frame, releasing GPU dmabufs immediately instead of at pool eviction.\n videoEl.removeAttribute('src');\n videoEl.load();\n\n // Remove event listeners to prevent accumulation across widget cycles\n if (videoEl._eventCleanup) {\n for (const [event, handler] of videoEl._eventCleanup) {\n videoEl.removeEventListener(event, handler);\n }\n videoEl._eventCleanup = null;\n }\n }\n\n const audioEl = widgetElement.querySelector('audio');\n if (audioEl && widget.options.loop !== '1') audioEl.pause();\n\n // Remove audio event listeners\n if (audioEl?._eventCleanup) {\n for (const [event, handler] of audioEl._eventCleanup) {\n audioEl.removeEventListener(event, handler);\n }\n audioEl._eventCleanup = null;\n }\n\n // Stop audio overlays attached to this widget\n this._stopAudioOverlays(widget.id);\n\n // Stop PDF page cycling timers\n if (widgetElement._pdfCleanup) {\n widgetElement._pdfCleanup();\n }\n\n // Stop embedded widget iframes (HLS live streams, webcams, etc.)\n // Setting src=about:blank kills all network activity (HLS segment fetches,\n // WebSocket connections, SSE streams) and releases video decode buffers.\n const iframes = widgetElement.querySelectorAll('iframe');\n for (const iframe of iframes) {\n try {\n const doc = iframe.contentDocument || iframe.contentWindow?.document;\n if (doc) {\n doc.querySelectorAll('video').forEach(v => { v.pause(); v.removeAttribute('src'); v.load(); });\n doc.querySelectorAll('audio').forEach(a => { a.pause(); a.removeAttribute('src'); a.load(); });\n }\n } catch (_) {}\n iframe.src = 'about:blank';\n }\n\n return { widget, animPromise };\n }\n\n /**\n * Check if a widget is within its valid time window (fromDt/toDt).\n * Widgets without dates are always active.\n * @param {Object} widget - Widget config with optional fromDt/toDt\n * @returns {boolean}\n */\n _isWidgetActive(widget) {\n const now = new Date();\n if (widget.fromDt) {\n const from = new Date(widget.fromDt);\n if (now < from) return false;\n }\n if (widget.toDt) {\n const to = new Date(widget.toDt);\n if (now > to) return false;\n }\n return true;\n }\n\n /**\n * Parse NUMITEMS and DURATION HTML comments from GetResource responses.\n * CMS embeds these in widget HTML to override duration for dynamic content\n * (e.g. DataSet tickers, RSS feeds). Format: <!-- NUMITEMS=5 --> <!-- DURATION=30 -->\n * DURATION takes precedence; otherwise NUMITEMS × widget.duration is used.\n * @param {string} html - Widget HTML content\n * @param {Object} widget - Widget config (duration may be updated)\n */\n _parseDurationComments(html, widget) {\n const oldDuration = widget.duration;\n\n const durationMatch = html.match(/<!--\\s*DURATION=(\\d+)\\s*-->/);\n if (durationMatch) {\n const newDuration = parseInt(durationMatch[1], 10);\n if (newDuration > 0) {\n this.log.info(`Widget ${widget.id}: DURATION comment overrides duration ${widget.duration}→${newDuration}s`);\n widget.duration = newDuration;\n if (widget.duration !== oldDuration) this.updateLayoutDuration();\n return;\n }\n }\n\n const numItemsMatch = html.match(/<!--\\s*NUMITEMS=(\\d+)\\s*-->/);\n if (numItemsMatch) {\n const numItems = parseInt(numItemsMatch[1], 10);\n if (numItems > 0 && widget.duration > 0) {\n const newDuration = numItems * widget.duration;\n this.log.info(`Widget ${widget.id}: NUMITEMS=${numItems} × ${widget.duration}s = ${newDuration}s`);\n widget.duration = newDuration;\n }\n }\n\n if (widget.duration !== oldDuration) this.updateLayoutDuration();\n }\n\n /**\n * Apply sub-playlist cycle playback filtering.\n * Groups widgets by parentWidgetId, then selects one widget per group for this cycle.\n * Non-grouped widgets pass through unchanged.\n *\n * @param {Array} widgets - All widgets in the region\n * @returns {Array} Filtered widgets for this playback cycle\n */\n _applyCyclePlayback(widgets) {\n // Track cycle indices per group for deterministic round-robin\n if (!this._subPlaylistCycleIndex) {\n this._subPlaylistCycleIndex = new Map();\n }\n\n // Group widgets by parentWidgetId\n const groups = new Map(); // parentWidgetId → [widgets]\n const result = [];\n\n for (const widget of widgets) {\n if (widget.parentWidgetId && widget.cyclePlayback) {\n if (!groups.has(widget.parentWidgetId)) {\n groups.set(widget.parentWidgetId, []);\n }\n groups.get(widget.parentWidgetId).push(widget);\n } else {\n // Non-grouped widget: add a placeholder to preserve order\n result.push({ type: 'direct', widget });\n }\n }\n\n // For each group, select one widget for this cycle\n for (const [groupId, groupWidgets] of groups) {\n // Sort by displayOrder\n groupWidgets.sort((a, b) => a.displayOrder - b.displayOrder);\n\n let selectedWidget;\n if (groupWidgets.some(w => w.isRandom)) {\n // Random selection\n const idx = Math.floor(Math.random() * groupWidgets.length);\n selectedWidget = groupWidgets[idx];\n } else {\n // Round-robin based on cycle index, respecting playCount\n const state = this._subPlaylistCycleIndex.get(groupId) || { widgetIdx: 0, playsDone: 0 };\n selectedWidget = groupWidgets[state.widgetIdx % groupWidgets.length];\n const effectivePlayCount = selectedWidget.playCount || 1;\n\n state.playsDone++;\n if (state.playsDone >= effectivePlayCount) {\n state.widgetIdx++;\n state.playsDone = 0;\n }\n this._subPlaylistCycleIndex.set(groupId, state);\n }\n\n this.log.info(`Sub-playlist cycle: group ${groupId} selected widget ${selectedWidget.id} (${groupWidgets.length} in group)`);\n result.push({ type: 'direct', widget: selectedWidget });\n }\n\n return result.map(r => r.widget);\n }\n\n /**\n * Core: cycle through widgets in a region (shared by main layout + overlay)\n * @param {Object} region - Region state object\n * @param {string} regionId - Region ID\n * @param {Function} showFn - (regionId, widgetIndex) => show widget\n * @param {Function} hideFn - (regionId, widgetIndex) => hide widget\n * @param {Function} [onCycleComplete] - Called when region completes one full cycle\n */\n _startRegionCycle(region, regionId, showFn, hideFn, onCycleComplete) {\n if (!region || region.widgets.length === 0) return;\n\n // Canvas regions: render ALL widgets simultaneously (stacked), no cycling.\n // Duration = max widget duration; region completes when the longest widget expires.\n if (region.isCanvas) {\n this._startCanvasRegion(region, regionId, showFn, onCycleComplete);\n return;\n }\n\n // Non-looping region with a single widget: show it and stay (spec: loop=0)\n if (region.widgets.length === 1) {\n showFn(regionId, 0);\n return;\n }\n\n const playNext = () => {\n const widgetIndex = region.currentIndex;\n const widget = region.widgets[widgetIndex];\n\n showFn(regionId, widgetIndex);\n\n const duration = widget.duration * 1000;\n this.log.info(`Region ${regionId} widget ${widget.id} (${widget.type}) playing for ${widget.duration}s (useDuration=${widget.useDuration}, index ${widgetIndex}/${region.widgets.length})`);\n region.timer = setTimeout(() => {\n this._handleWidgetCycleEnd(widget, region, regionId, widgetIndex, showFn, hideFn, onCycleComplete, playNext);\n }, duration);\n };\n\n playNext();\n }\n\n /**\n * Start a canvas region — render all widgets simultaneously (stacked).\n * Canvas regions show every widget at once rather than cycling through them.\n * The region duration is the maximum widget duration.\n * @param {Object} region - Region state\n * @param {string} regionId - Region ID\n * @param {Function} showFn - Show widget function (regionId, widgetIndex)\n * @param {Function} onCycleComplete - Callback when region completes\n */\n _startCanvasRegion(region, regionId, showFn, onCycleComplete) {\n // Show all widgets at once\n for (let i = 0; i < region.widgets.length; i++) {\n showFn(regionId, i);\n }\n\n // Mark region as complete after max widget duration\n const maxDuration = Math.max(...region.widgets.map(w => w.duration)) * 1000;\n if (maxDuration > 0) {\n region.timer = setTimeout(() => {\n if (!region.complete) {\n region.complete = true;\n onCycleComplete?.();\n }\n }, maxDuration);\n } else {\n // No duration — immediately complete\n region.complete = true;\n onCycleComplete?.();\n }\n }\n\n /**\n * Handle widget cycle end — shared logic for timer-based and event-based cycling\n */\n _handleWidgetCycleEnd(widget, region, regionId, widgetIndex, showFn, hideFn, onCycleComplete, playNext) {\n // Emit widgetAction if widget has a webhook URL configured\n if (widget.webhookUrl) {\n this.emit('widgetAction', {\n type: 'durationEnd',\n widgetId: widget.id,\n layoutId: this.currentLayoutId,\n regionId,\n url: widget.webhookUrl\n });\n }\n\n hideFn(regionId, widgetIndex);\n\n const nextIndex = (region.currentIndex + 1) % region.widgets.length;\n if (nextIndex === 0 && !region.complete) {\n region.complete = true;\n onCycleComplete?.();\n }\n\n // Non-looping single-widget region (loop=0): don't replay.\n // Multi-widget regions (playlists) always cycle regardless of loop setting —\n // in Xibo, loop=0 only means \"don't repeat a single media item.\"\n if (nextIndex === 0 && region.config?.loop === false && region.widgets.length === 1) {\n showFn(regionId, 0);\n return;\n }\n\n // Don't start next widget if layout has already ended (race with layout timer)\n if (this.layoutEndEmitted) return;\n\n region.currentIndex = nextIndex;\n playNext();\n }\n\n async renderWidget(regionId, widgetIndex) {\n const region = this.regions.get(regionId);\n if (!region) return;\n\n try {\n const widget = await this._showWidget(region, widgetIndex);\n if (widget) {\n this.log.info(`Showing widget ${widget.type} (${widget.id}) in region ${regionId}`);\n this._startedWidgets.add(`${regionId}:${widgetIndex}`);\n this.emit('widgetStart', {\n widgetId: widget.id, regionId, layoutId: this.currentLayoutId,\n mediaId: parseInt(widget.fileId || widget.id) || null,\n type: widget.type, duration: widget.duration,\n enableStat: widget.enableStat\n });\n\n // Execute commands attached to this widget (shell/native commands)\n if (widget.commands && widget.commands.length > 0) {\n for (const cmd of widget.commands) {\n this.emit('widgetCommand', {\n commandCode: cmd.commandCode,\n commandString: cmd.commandString,\n widgetId: widget.id,\n regionId,\n layoutId: this.currentLayoutId\n });\n }\n }\n }\n } catch (error) {\n this.log.error(`Error rendering widget:`, error);\n this.emit('error', { type: 'widgetError', error, widgetId: region.widgets[widgetIndex]?.id, regionId });\n }\n }\n\n /**\n * Stop a widget (with element reuse - don't revoke blob URLs!)\n * @param {string} regionId - Region ID\n * @param {number} widgetIndex - Widget index\n */\n async stopWidget(regionId, widgetIndex) {\n const key = `${regionId}:${widgetIndex}`;\n if (!this._startedWidgets.delete(key)) return; // idempotent: already stopped\n\n const region = this.regions.get(regionId);\n if (!region) return;\n\n const { widget, animPromise } = this._hideWidget(region, widgetIndex);\n // Emit widgetEnd immediately — don't wait for exit animation.\n // If we await animPromise first, a pool eviction can remove the DOM element,\n // causing the animation's onfinish to never fire and widgetEnd to be lost.\n if (widget) {\n this.emit('widgetEnd', {\n widgetId: widget.id, regionId, layoutId: this.currentLayoutId,\n mediaId: parseInt(widget.fileId || widget.id) || null,\n type: widget.type,\n enableStat: widget.enableStat\n });\n }\n if (animPromise) await animPromise;\n }\n\n /**\n * Stop all started widgets across regions (symmetric counterpart to startRegion)\n * Canvas regions start ALL widgets; non-canvas regions have one active widget.\n * @param {Map} regions - Region map\n * @param {Function} stopFn - (regionId, widgetIndex) => void\n */\n _stopAllRegionWidgets(regions, stopFn) {\n for (const [regionId, region] of regions) {\n if (region.isCanvas) {\n for (let i = 0; i < region.widgets.length; i++) {\n stopFn(regionId, i);\n }\n } else if (region.widgets.length > 0) {\n stopFn(regionId, region.currentIndex);\n }\n }\n }\n\n /**\n * Render image widget\n */\n async renderImage(widget, region) {\n const img = document.createElement('img');\n img.className = 'renderer-lite-widget';\n img.style.width = '100%';\n img.style.height = '100%';\n // Scale type mapping (CMS image.xml):\n // center (default) → contain: scale proportionally to fit region, centered\n // stretch → fill: ignore aspect ratio, fill entire region\n // fit → cover: scale proportionally to fill region, crop excess\n const scaleType = widget.options.scaleType;\n const fitMap = { stretch: 'fill', center: 'contain', fit: 'cover' };\n img.style.objectFit = fitMap[scaleType] || 'contain';\n\n // Alignment: map alignId/valignId to CSS object-position\n // XLF tags are <alignId> and <valignId> (from CMS image.xml property ids)\n const alignMap = { left: 'left', center: 'center', right: 'right' };\n const valignMap = { top: 'top', middle: 'center', bottom: 'bottom' };\n const hPos = alignMap[widget.options.alignId] || 'center';\n const vPos = valignMap[widget.options.valignId] || 'center';\n img.style.objectPosition = `${hPos} ${vPos}`;\n\n img.style.opacity = '0';\n\n // Direct URL from storedAs filename — store key = widget reference = serve URL\n const src = widget.options.uri\n ? this._mediaFileUrl(widget.options.uri)\n : '';\n\n img.src = src;\n return img;\n }\n\n /**\n * Render video widget\n */\n async renderVideo(widget, region) {\n const video = document.createElement('video');\n video.className = 'renderer-lite-widget';\n video.style.width = '100%';\n video.style.height = '100%';\n const vScaleType = widget.options.scaleType;\n const vFitMap = { stretch: 'fill', center: 'none', fit: 'contain' };\n video.style.objectFit = vFitMap[vScaleType] || 'contain';\n video.style.opacity = '1'; // Immediately visible\n video.autoplay = true;\n video.preload = 'auto'; // Eagerly buffer - chunks are pre-warmed in SW BlobCache\n video.muted = widget.options.mute === '1';\n video.loop = false; // Don't use native loop - we handle it manually to avoid black frames\n video.controls = false; // Hidden by default — toggle with V key in PWA\n video.playsInline = true; // Prevent fullscreen on mobile\n\n // Direct URL from storedAs filename\n const storedAs = widget.options.uri || '';\n const fileId = widget.fileId || widget.id;\n\n // Handle video end - pause on last frame instead of showing black\n // Widget cycling will restart the video via updateMediaElement()\n const onEnded = () => {\n if (widget.options.loop === '1') {\n video.currentTime = 0;\n this.log.info(`Video ${storedAs} ended - reset to start, waiting for widget cycle to replay`);\n } else {\n this.log.info(`Video ${storedAs} ended - paused on last frame`);\n }\n };\n video.addEventListener('ended', onEnded);\n const videoSrc = storedAs ? this._mediaFileUrl(storedAs) : '';\n\n // HLS/DASH streaming support\n const isHlsStream = videoSrc.includes('.m3u8');\n if (isHlsStream) {\n // Try native HLS first (Safari, iOS, some Android)\n if (video.canPlayType('application/vnd.apple.mpegurl')) {\n this.log.info(`HLS stream (native): ${fileId}`);\n video.src = videoSrc;\n } else {\n // Dynamic import hls.js for Chrome/Firefox (code-split, not in main bundle)\n try {\n const { default: Hls } = await import('hls.js');\n if (Hls.isSupported()) {\n const hls = new Hls({ enableWorker: true, lowLatencyMode: true });\n hls.loadSource(videoSrc);\n hls.attachMedia(video);\n video._hlsInstance = hls; // Store for cleanup on eviction\n hls.on(Hls.Events.ERROR, (_event, data) => {\n if (data.fatal) {\n this.log.error(`HLS fatal error: ${data.type}`, data.details);\n hls.destroy();\n video._hlsInstance = null;\n }\n });\n this.log.info(`HLS stream (hls.js): ${fileId}`);\n } else {\n this.log.warn(`HLS not supported on this browser for ${fileId}`);\n video.src = videoSrc; // Fallback — may not work\n }\n } catch (e) {\n this.log.warn(`hls.js not available, falling back to native: ${e.message}`);\n video.src = videoSrc;\n }\n }\n } else {\n video.src = videoSrc;\n }\n\n // Detect video duration for dynamic layout timing (when useDuration=0)\n // Capture the layout ID at creation time — during preload, _preloadingLayoutId\n // is the target layout (currentLayoutId is still the playing layout).\n const createdForLayoutId = this._preloadingLayoutId || this.currentLayoutId;\n const onLoadedMetadata = () => {\n const videoDuration = video.duration;\n this.log.info(`Video ${storedAs} duration detected: ${videoDuration}s`);\n\n if (widget.duration === 0 || widget.useDuration === 0) {\n widget.duration = videoDuration;\n widget._probed = true;\n this.log.info(`Updated widget ${widget.id} duration to ${videoDuration}s (useDuration=0)`);\n\n if (this.currentLayoutId === createdForLayoutId) {\n this.updateLayoutDuration();\n } else {\n this.log.info(`Video ${storedAs} duration set but layout timer not updated (preloaded for layout ${createdForLayoutId}, current is ${this.currentLayoutId})`);\n }\n }\n };\n video.addEventListener('loadedmetadata', onLoadedMetadata);\n\n const onLoadedData = () => {\n this.log.info('Video loaded and ready:', storedAs);\n };\n video.addEventListener('loadeddata', onLoadedData);\n\n const onError = () => {\n const error = video.error;\n const errorCode = error?.code;\n const errorMessage = error?.message || 'Unknown error';\n this.log.warn(`Video error: ${storedAs}, code: ${errorCode}, time: ${video.currentTime.toFixed(1)}s, message: ${errorMessage}`);\n\n // Set fallback duration so the deferred timer can proceed.\n // Without this, a corrupt video leaves widget.duration=0 forever,\n // _hasUnprobedVideos() stays true, and the deferred timer never unblocks.\n if (widget.useDuration === 0 && widget.duration === 0) {\n widget.duration = 60;\n this.log.info(`Set fallback duration 60s for errored widget ${widget.id}`);\n if (this.currentLayoutId === createdForLayoutId) {\n this.updateLayoutDuration();\n }\n }\n\n this.emit('videoError', { storedAs, fileId, errorCode, errorMessage, currentTime: video.currentTime });\n };\n video.addEventListener('error', onError);\n\n const onPlaying = () => {\n this.log.info('Video playing:', storedAs);\n };\n video.addEventListener('playing', onPlaying);\n\n // Store listener references for cleanup in _hideWidget()\n video._eventCleanup = [\n ['ended', onEnded],\n ['loadedmetadata', onLoadedMetadata],\n ['loadeddata', onLoadedData],\n ['error', onError],\n ['playing', onPlaying],\n ];\n\n this.log.info('Video element created:', storedAs, video.src);\n\n return video;\n }\n\n /**\n * Render videoin (webcam/microphone) widget.\n * Uses getUserMedia() to capture live video from camera hardware.\n * @param {Object} widget - Widget config with options (sourceId, showFullScreen, mirror, mute, captureAudio)\n * @param {Object} region - Region dimensions (width, height)\n * @returns {HTMLVideoElement}\n */\n async renderVideoIn(widget, region) {\n const video = document.createElement('video');\n video.className = 'renderer-lite-widget';\n video.style.width = '100%';\n video.style.height = '100%';\n video.style.objectFit = widget.options.showFullScreen === '1' ? 'cover' : 'contain';\n video.autoplay = true;\n video.playsInline = true;\n video.controls = false;\n video.muted = widget.options.mute !== '0'; // Muted by default to prevent audio feedback\n\n // Mirror mode (front-facing camera)\n if (widget.options.mirror === '1') {\n video.style.transform = 'scaleX(-1)';\n }\n\n // Build getUserMedia constraints\n const videoConstraints = {\n width: { ideal: region.width },\n height: { ideal: region.height },\n };\n const deviceId = widget.options.sourceId || widget.options.deviceId;\n if (deviceId) {\n videoConstraints.deviceId = { exact: deviceId };\n } else {\n videoConstraints.facingMode = widget.options.facingMode || 'environment';\n }\n\n const constraints = {\n video: videoConstraints,\n audio: widget.options.captureAudio === '1',\n };\n\n // Store constraints for re-acquisition after layout transitions\n video._mediaConstraints = constraints;\n\n try {\n const stream = await navigator.mediaDevices.getUserMedia(constraints);\n video.srcObject = stream;\n video._mediaStream = stream;\n this.log.info(`Webcam stream acquired for widget ${widget.id} (tracks: ${stream.getTracks().length})`);\n } catch (e) {\n this.log.warn(`getUserMedia failed for widget ${widget.id}: ${e.message}`);\n return this._renderUnsupportedPlaceholder(\n { ...widget, type: 'Camera unavailable' },\n region\n );\n }\n\n return video;\n }\n\n /**\n * Render audio widget\n */\n async renderAudio(widget, region) {\n const container = document.createElement('div');\n container.className = 'renderer-lite-widget audio-widget';\n container.style.width = '100%';\n container.style.height = '100%';\n container.style.display = 'flex';\n container.style.flexDirection = 'column';\n container.style.alignItems = 'center';\n container.style.justifyContent = 'center';\n container.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';\n container.style.opacity = '0';\n\n // Audio element\n const audio = document.createElement('audio');\n audio.autoplay = true;\n audio.loop = widget.options.loop === '1';\n audio.volume = parseFloat(widget.options.volume || '100') / 100;\n\n // Direct URL from storedAs filename\n const storedAs = widget.options.uri || '';\n const fileId = widget.fileId || widget.id;\n audio.src = storedAs ? this._mediaFileUrl(storedAs) : '';\n\n // Handle audio end - similar to video ended handling\n const onAudioEnded = () => {\n if (widget.options.loop === '1') {\n audio.currentTime = 0;\n this.log.info(`Audio ${storedAs} ended - reset to start, waiting for widget cycle to replay`);\n } else {\n this.log.info(`Audio ${storedAs} ended - playback complete`);\n }\n };\n audio.addEventListener('ended', onAudioEnded);\n\n // Detect audio duration for dynamic layout timing (when useDuration=0)\n const audioCreatedForLayoutId = this._preloadingLayoutId || this.currentLayoutId;\n const onAudioLoadedMetadata = () => {\n const audioDuration = Math.floor(audio.duration);\n this.log.info(`Audio ${storedAs} duration detected: ${audioDuration}s`);\n\n if (widget.duration === 0 || widget.useDuration === 0) {\n widget.duration = audioDuration;\n this.log.info(`Updated widget ${widget.id} duration to ${audioDuration}s (useDuration=0)`);\n\n if (this.currentLayoutId === audioCreatedForLayoutId) {\n this.updateLayoutDuration();\n } else {\n this.log.info(`Audio ${storedAs} duration set but layout timer not updated (preloaded for layout ${audioCreatedForLayoutId}, current is ${this.currentLayoutId})`);\n }\n }\n };\n audio.addEventListener('loadedmetadata', onAudioLoadedMetadata);\n\n // Handle audio errors\n const onAudioError = () => {\n const error = audio.error;\n this.log.warn(`Audio error (non-fatal): ${storedAs}, code: ${error?.code}, message: ${error?.message || 'Unknown'}`);\n };\n audio.addEventListener('error', onAudioError);\n\n // Store listener references for cleanup in _hideWidget()\n audio._eventCleanup = [\n ['ended', onAudioEnded],\n ['loadedmetadata', onAudioLoadedMetadata],\n ['error', onAudioError],\n ];\n\n // Visual feedback\n const icon = document.createElement('div');\n icon.innerHTML = '♪';\n icon.style.fontSize = '120px';\n icon.style.color = 'white';\n icon.style.marginBottom = '20px';\n\n const info = document.createElement('div');\n info.style.color = 'white';\n info.style.fontSize = '24px';\n info.textContent = 'Playing Audio';\n\n const filename = document.createElement('div');\n filename.style.color = 'rgba(255,255,255,0.7)';\n filename.style.fontSize = '16px';\n filename.style.marginTop = '10px';\n filename.textContent = widget.options.uri;\n\n container.appendChild(audio);\n container.appendChild(icon);\n container.appendChild(info);\n container.appendChild(filename);\n\n return container;\n }\n\n /**\n * Render text/ticker widget\n */\n async renderTextWidget(widget, region) {\n return await this._renderIframeWidget(widget, region);\n }\n\n /**\n * Render PDF widget — single reusable canvas, page-by-page cycling.\n *\n * Memory strategy:\n * - One canvas is created and reused for all pages (no DOM churn)\n * - Each page is rendered sequentially (avoids concurrent render errors)\n * - page.cleanup() releases PDF.js internal page buffers after each render\n * - pdf.destroy() releases the entire document on widget teardown\n * - Active renderTask is cancelled on cleanup to prevent stale renders\n */\n async renderPdf(widget, region) {\n const container = document.createElement('div');\n container.className = 'renderer-lite-widget pdf-widget';\n container.style.width = '100%';\n container.style.height = '100%';\n container.style.backgroundColor = 'transparent';\n container.style.opacity = '0';\n container.style.position = 'relative';\n\n // Load PDF.js if available\n if (typeof window.pdfjsLib === 'undefined') {\n try {\n const pdfjsModule = await import('pdfjs-dist');\n window.pdfjsLib = pdfjsModule;\n // Derive worker path from current page location (works for /player/pwa/ and /player/)\n const basePath = window.location.pathname.replace(/\\/[^/]*$/, '/');\n window.pdfjsLib.GlobalWorkerOptions.workerSrc = `${window.location.origin}${basePath}pdf.worker.min.mjs`;\n } catch (error) {\n this.log.error('PDF.js not available:', error);\n container.innerHTML = '<div style=\"color:white;padding:20px;text-align:center;\">PDF viewer unavailable</div>';\n container.style.opacity = '1';\n return container;\n }\n }\n\n // Direct URL from storedAs filename\n const pdfUrl = widget.options.uri\n ? this._mediaFileUrl(widget.options.uri)\n : '';\n\n // Render PDF with multi-page cycling\n try {\n const loadingTask = window.pdfjsLib.getDocument(pdfUrl);\n const pdf = await loadingTask.promise;\n const totalPages = pdf.numPages;\n const duration = widget.duration || 60;\n const timePerPage = (duration * 1000) / totalPages;\n this.log.info(`[pdf] PDF loaded: ${totalPages} pages, ${duration}s duration, ${(timePerPage / 1000).toFixed(1)}s/page`);\n\n // Measure page size from first page to set up the single reusable canvas\n const page1 = await pdf.getPage(1);\n const viewport0 = page1.getViewport({ scale: 1 });\n const scale = Math.min(region.width / viewport0.width, region.height / viewport0.height);\n page1.cleanup();\n\n const canvas = document.createElement('canvas');\n canvas.className = 'pdf-page';\n canvas.width = Math.floor(viewport0.width * scale);\n canvas.height = Math.floor(viewport0.height * scale);\n canvas.style.cssText = 'display:block;margin:auto;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);';\n const ctx = canvas.getContext('2d');\n container.appendChild(canvas);\n\n // Page indicator (bottom-right, v1-style pill) — debug only\n const indicator = document.createElement('div');\n indicator.style.cssText = 'position:absolute;bottom:10px;right:10px;background:rgba(0,0,0,0.7);color:white;padding:8px 12px;border-radius:4px;font:14px system-ui;z-index:1;';\n if (!isDebug()) indicator.style.display = 'none';\n container.appendChild(indicator);\n\n let currentPage = 1;\n let cycleTimer = null;\n let activeRenderTask = null;\n let stopped = false;\n\n // Render one page at a time on the single canvas. Sequential scheduling\n // (setTimeout after render completes) avoids the \"Cannot use the same\n // canvas during multiple render() operations\" error from PDF.js.\n const cyclePage = async () => {\n if (stopped) return;\n indicator.textContent = `Page ${currentPage} / ${totalPages}`;\n\n const page = await pdf.getPage(currentPage);\n const scaledViewport = page.getViewport({ scale });\n\n // Clear and render on the reusable canvas\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n activeRenderTask = page.render({ canvasContext: ctx, viewport: scaledViewport });\n try {\n await activeRenderTask.promise;\n } catch (e) {\n // RenderingCancelledException is expected when stopped during render\n if (stopped) return;\n throw e;\n }\n activeRenderTask = null;\n page.cleanup(); // Release PDF.js internal page buffers\n\n // Schedule next page (only after current render completes)\n if (totalPages > 1 && !stopped) {\n cycleTimer = setTimeout(() => {\n currentPage = currentPage >= totalPages ? 1 : currentPage + 1;\n cyclePage();\n }, timePerPage);\n }\n };\n\n await cyclePage();\n\n // Pause: stop page cycling (called by _hideWidget during region cycling / replay)\n // Returns a promise that resolves when the active render is fully cancelled.\n let cancelPromise = null;\n container._pdfCleanup = () => {\n stopped = true;\n if (cycleTimer) clearTimeout(cycleTimer);\n cycleTimer = null;\n if (activeRenderTask) {\n const task = activeRenderTask;\n activeRenderTask = null;\n task.cancel();\n cancelPromise = task.promise.catch(() => {}); // wait for cancellation to propagate\n }\n };\n\n // Resume: restart page cycling from page 1 (called by _showWidget on reuse)\n // Always cleanup first — the PDF may still be rendering from preload\n // (pre-create starts cyclePage immediately, but the widget isn't \"shown\"\n // until the layout swap, so _pdfCleanup was never called).\n container._pdfResume = async () => {\n container._pdfCleanup(); // stop any in-flight render\n if (cancelPromise) { await cancelPromise; cancelPromise = null; }\n stopped = false;\n currentPage = 1;\n cyclePage();\n };\n\n // Destroy: release GPU + PDF resources (called on element removal / eviction)\n container._pdfDestroy = () => {\n container._pdfCleanup();\n canvas.width = 0;\n canvas.height = 0;\n pdf.destroy();\n };\n\n } catch (error) {\n this.log.error('PDF render failed:', error);\n container.innerHTML = '<div style=\"color:white;padding:20px;text-align:center;\">Failed to load PDF</div>';\n }\n\n container.style.opacity = '1';\n return container;\n }\n\n /**\n * Render webpage widget\n */\n async renderWebpage(widget, region) {\n // modeId=1 (or absent) = Open Natively (direct URL), modeId=0 = Manual/GetResource\n const modeId = parseInt(widget.options.modeId || '1');\n if (modeId === 0) {\n // GetResource mode: treat like a generic widget (fetch HTML from CMS)\n return await this.renderGenericWidget(widget, region);\n }\n\n const iframe = document.createElement('iframe');\n iframe.className = 'renderer-lite-widget';\n iframe.style.width = '100%';\n iframe.style.height = '100%';\n iframe.style.border = 'none';\n iframe.style.opacity = '0';\n // CMS may percent-encode the URI in XLF (e.g. https%3A%2F%2F → https://)\n const uri = decodeURIComponent(widget.options.uri || '');\n iframe.src = uri;\n\n return iframe;\n }\n\n /**\n * Render generic widget (clock, calendar, weather, etc.)\n */\n async renderGenericWidget(widget, region) {\n return await this._renderIframeWidget(widget, region);\n }\n\n /**\n * Shared iframe rendering for text/ticker and generic widgets.\n * Creates an iframe, resolves widget HTML via getWidgetHtml (cache URL or blob),\n * and parses NUMITEMS/DURATION comments for dynamic widget duration.\n */\n async _renderIframeWidget(widget, region) {\n const iframe = document.createElement('iframe');\n iframe.className = 'renderer-lite-widget';\n iframe.style.width = '100%';\n iframe.style.height = '100%';\n iframe.style.border = 'none';\n iframe.style.opacity = '0';\n\n // Get widget HTML (may return { url } for cache-path loading or string for blob)\n let html = widget.raw;\n if (this.options.getWidgetHtml) {\n const result = await this.options.getWidgetHtml(widget);\n if (result && typeof result === 'object' && result.url) {\n // Use cache URL — SW serves HTML and intercepts sub-resources\n iframe.src = result.url;\n\n // Parse NUMITEMS/DURATION from fallback HTML (cache path)\n if (result.fallback) {\n this._parseDurationComments(result.fallback, widget);\n }\n\n return iframe;\n }\n html = result;\n }\n\n if (html) {\n // Parse NUMITEMS/DURATION HTML comments for dynamic widget duration\n // Format: <!-- NUMITEMS=5 --> and <!-- DURATION=30 -->\n this._parseDurationComments(html, widget);\n\n const blob = new Blob([html], { type: 'text/html' });\n const blobUrl = URL.createObjectURL(blob);\n iframe.src = blobUrl;\n\n // Track blob URL for lifecycle management\n this.trackBlobUrl(blobUrl);\n } else {\n this.log.warn(`No HTML for widget ${widget.id}`);\n iframe.srcdoc = '<div style=\"padding:20px;\">Widget content unavailable</div>';\n }\n\n return iframe;\n }\n\n /**\n * Render a placeholder for unsupported widget types (powerpoint, flash)\n */\n _renderUnsupportedPlaceholder(widget, region) {\n const div = document.createElement('div');\n div.className = 'renderer-lite-widget';\n div.style.width = '100%';\n div.style.height = '100%';\n div.style.display = 'flex';\n div.style.alignItems = 'center';\n div.style.justifyContent = 'center';\n div.style.backgroundColor = '#111';\n div.style.color = '#666';\n div.style.fontSize = '14px';\n div.textContent = `Unsupported: ${widget.type}`;\n return div;\n }\n\n // ── Layout Preload Pool ─────────────────────────────────────────────\n\n /**\n * Schedule preloading of the next layout at 75% of current layout duration.\n * Emits 'request-next-layout-preload' so the platform layer can peek at the\n * schedule and call preloadLayout() with the next layout's XLF.\n * @param {Object} layout - Current layout object with .duration\n */\n _scheduleNextLayoutPreload(layout) {\n if (this.preloadTimer) {\n clearTimeout(this.preloadTimer);\n this.preloadTimer = null;\n }\n if (this._preloadRetryTimer) {\n clearTimeout(this._preloadRetryTimer);\n this._preloadRetryTimer = null;\n }\n\n const duration = layout.duration || 60; // seconds\n const preloadDelay = duration * 1000 * 0.75; // 75% through\n const retryDelay = duration * 1000 * 0.90; // 90% retry\n\n this.log.info(`Scheduling next layout preload in ${(preloadDelay / 1000).toFixed(1)}s (75% of ${duration}s)`);\n\n this.preloadTimer = setTimeout(() => {\n this.preloadTimer = null;\n this.emit('request-next-layout-preload');\n }, preloadDelay);\n\n // Retry at 90% if the 75% attempt couldn't find a layout (e.g. cooldowns\n // hadn't expired yet). The platform handler is idempotent — if a layout\n // is already in the pool it skips, so this is safe even if 75% succeeded.\n this._preloadRetryTimer = setTimeout(() => {\n this._preloadRetryTimer = null;\n this.emit('request-next-layout-preload');\n }, retryDelay);\n }\n\n /**\n * Preload a layout into the pool as a warm (hidden) entry.\n * Creates the full DOM hierarchy (regions + widgets) in a hidden container,\n * pre-fetches media, but does NOT start widget cycling or layout timer.\n *\n * This is called by the platform layer in response to 'request-next-layout-preload'.\n *\n * @param {string} xlfXml - XLF XML content for the layout\n * @param {number} layoutId - Layout ID\n * @returns {Promise<boolean>} true if preload succeeded, false on failure\n */\n hasPreloadedLayout(layoutId) {\n return this.layoutPool.has(layoutId);\n }\n\n async preloadLayout(xlfXml, layoutId) {\n // Don't preload if already in pool\n if (this.layoutPool.has(layoutId)) {\n this.log.info(`Layout ${layoutId} already in preload pool, skipping`);\n return true;\n }\n\n // Don't preload the currently playing layout\n if (this.currentLayoutId === layoutId) {\n this.log.info(`Layout ${layoutId} is current, skipping preload`);\n return true;\n }\n\n // If already in-flight, wait for it instead of skipping (prevents the race\n // where showLayout is called before the background preload finishes adding\n // the layout to the pool).\n if (this._preloadingLayoutId === layoutId && this._preloadingPromise) {\n this.log.info(`Layout ${layoutId} preload in-flight, waiting for it...`);\n return this._preloadingPromise;\n }\n\n // Store the preload promise so concurrent callers can await it\n this._preloadingPromise = this._doPreloadLayout(xlfXml, layoutId);\n return this._preloadingPromise;\n }\n\n async _doPreloadLayout(xlfXml, layoutId) {\n try {\n this.log.info(`Preloading layout ${layoutId} into pool...`);\n\n // Parse XLF\n const layout = this.parseXlf(xlfXml);\n\n // Calculate scale factor\n this.calculateScale(layout);\n\n // Create a hidden wrapper container for the preloaded layout\n const wrapper = document.createElement('div');\n wrapper.id = `preload_layout_${layoutId}`;\n wrapper.className = 'renderer-lite-preload-wrapper';\n wrapper.style.position = 'absolute';\n wrapper.style.top = '0';\n wrapper.style.left = '0';\n wrapper.style.width = '100%';\n wrapper.style.height = '100%';\n wrapper.style.visibility = 'hidden';\n wrapper.style.zIndex = '-1'; // Behind everything\n\n // Set background\n wrapper.style.backgroundColor = layout.bgcolor;\n\n // Apply background image if specified\n // With storedAs refactor, background may be a filename or a numeric fileId\n if (layout.background) {\n const saveAs = this.options.fileIdToSaveAs?.get(String(layout.background)) || layout.background;\n this._applyBackgroundImage(wrapper, this._mediaFileUrl(saveAs));\n }\n\n const savedCurrentLayoutId = this.currentLayoutId;\n\n // Create regions in the hidden wrapper\n const preloadRegions = new Map();\n for (const regionConfig of layout.regions) {\n const region = this._createRegionEntry(\n regionConfig,\n `preload_region_${layoutId}_${regionConfig.id}`,\n wrapper\n );\n preloadRegions.set(regionConfig.id, region);\n }\n\n // Track blob URLs for the preloaded layout separately\n const preloadBlobUrls = new Set();\n const savedLayoutBlobUrls = this.layoutBlobUrls;\n this.layoutBlobUrls = new Map();\n this.layoutBlobUrls.set(layoutId, preloadBlobUrls);\n\n // Set _preloadingLayoutId so trackBlobUrl routes to the correct layout\n // without corrupting currentLayoutId (which other code reads during awaits)\n this._preloadingLayoutId = layoutId;\n\n // Pre-create all widget elements\n for (const [regionId, region] of preloadRegions) {\n for (let i = 0; i < region.widgets.length; i++) {\n const widget = region.widgets[i];\n widget.layoutId = layoutId;\n widget.regionId = regionId;\n\n try {\n const element = await this.createWidgetElement(widget, region);\n this._positionWidgetElement(element);\n region.element.appendChild(element);\n region.widgetElements.set(widget.id, element);\n } catch (error) {\n this.log.error(`Preload: Failed to create widget ${widget.id}:`, error);\n }\n }\n }\n\n // Restore state\n this.currentLayoutId = savedCurrentLayoutId;\n\n // Pause all videos in preloaded layout (autoplay starts them even when hidden)\n wrapper.querySelectorAll('video').forEach(v => v.pause());\n\n // Collect any blob URLs tracked during preload\n const trackedBlobUrls = this.layoutBlobUrls.get(layoutId) || new Set();\n trackedBlobUrls.forEach(url => preloadBlobUrls.add(url));\n\n // Restore original layoutBlobUrls\n this.layoutBlobUrls = savedLayoutBlobUrls;\n\n // Add wrapper to main container (hidden)\n this.container.appendChild(wrapper);\n\n // Add to pool as warm\n this.layoutPool.add(layoutId, {\n container: wrapper,\n layout,\n regions: preloadRegions,\n blobUrls: preloadBlobUrls,\n });\n\n this.log.info(`Layout ${layoutId} preloaded into pool (${preloadRegions.size} regions)`);\n return true;\n\n } catch (error) {\n this.log.error(`Preload failed for layout ${layoutId}:`, error);\n return false;\n } finally {\n if (this._preloadingLayoutId === layoutId) {\n this._preloadingLayoutId = null;\n this._preloadingPromise = null;\n }\n }\n }\n\n /**\n * Swap to a preloaded layout from the pool.\n *\n * Dispatches on the resolved layout transition spec:\n * - `instant` (default) → hard cut via _swapToPreloadedLayoutInstant\n * - `fade|slide|wipe|...` → cross-fade overlap via\n * _swapToPreloadedLayoutWithTransition\n *\n * Per-layout overrides (from the XLF `layoutTransitionIn` attribute)\n * beat the renderer's configured default. See #337.\n *\n * @param {number} layoutId - Layout ID to swap to\n */\n async _swapToPreloadedLayout(layoutId) {\n const preloaded = this.layoutPool.get(layoutId);\n if (!preloaded) {\n this.log.error(`Cannot swap: layout ${layoutId} not in pool`);\n return;\n }\n\n const spec = this._resolveLayoutTransition(preloaded.layout);\n\n if (spec.type === 'instant') {\n return this._swapToPreloadedLayoutInstant(layoutId, preloaded);\n }\n return this._swapToPreloadedLayoutWithTransition(layoutId, preloaded, spec);\n }\n\n /**\n * Instant swap path — the pre-#337 fast swap that hard-cuts from\n * old to new with zero animation overhead. This is the default and\n * covers the common \"no transition configured\" case.\n *\n * Kept as a dedicated method (rather than a branch) so the\n * transition path can extract shared helpers without destabilising\n * the fast path's behaviour.\n *\n * @param {number} layoutId\n * @param {Object} preloaded - pool entry from layoutPool.get(layoutId)\n */\n async _swapToPreloadedLayoutInstant(layoutId, preloaded) {\n // ── Tear down old layout ──\n this.removeActionListeners();\n this._clearLayoutTimers();\n\n const oldLayoutId = this.currentLayoutId;\n const alreadyEmittedEnd = this.layoutEndEmitted;\n\n this.layoutEndEmitted = false;\n // Keep currentLayout/currentLayoutId until widgets are stopped,\n // so widgetEnd events carry the correct layoutId (not null).\n\n if (oldLayoutId && this.layoutPool.has(oldLayoutId)) {\n // Stop all widgets before evicting (symmetric widgetEnd events)\n this._clearRegionTimers(this.regions);\n this._stopAllRegionWidgets(this.regions, this._stopWidgetBound);\n // Old layout was preloaded — evict from pool (safe: removes its wrapper div)\n this.layoutPool.evict(oldLayoutId);\n } else {\n // Old layout was rendered normally — manual cleanup.\n // Region elements live directly in this.container (not a wrapper),\n // so we must remove them individually.\n this._clearRegionTimers(this.regions);\n this._stopAllRegionWidgets(this.regions, this._stopWidgetBound);\n for (const [, region] of this.regions) {\n // Release video/audio resources before removing from DOM\n LayoutPool.releaseMediaElements(region.element);\n // Apply region exit transition if configured, then remove\n if (region.config?.exitTransition) {\n const animation = Transitions.apply(\n region.element, region.config.exitTransition, false,\n region.width, region.height\n );\n if (animation) {\n const el = region.element;\n animation.onfinish = () => el.remove();\n } else {\n region.element.remove();\n }\n } else {\n region.element.remove();\n }\n }\n // Revoke blob URLs\n if (oldLayoutId) {\n this.revokeBlobUrlsForLayout(oldLayoutId);\n }\n }\n\n // Now safe to clear old layout state — widgets have been stopped with correct layoutId\n this.currentLayout = null;\n this.currentLayoutId = null;\n this.regions.clear();\n\n // ── Activate preloaded layout ──\n this._activatePreloadedLayout(layoutId, preloaded, oldLayoutId, alreadyEmittedEnd);\n\n this.log.info(`Swapped to preloaded layout ${layoutId} (instant transition)`);\n this._logResourceStats(layoutId);\n }\n\n /**\n * Transition swap path — cross-fade / slide / wipe between layouts\n * using the LayoutPool's overlap architecture (#337).\n *\n * The preloaded wrapper already lives inside `this.container` at\n * zIndex=-1 (hidden). For the transition we:\n *\n * 1. Stop old widgets with the OLD layoutId still set (so their\n * widgetEnd events carry the correct layoutId).\n * 2. Raise the new wrapper above the old (zIndex=1).\n * 3. Update renderer state to the new layout and start its\n * widgets — they play over the top of the still-visible old\n * content during the transition window.\n * 4. Kick off the incoming animation on the new wrapper and,\n * for fade/slide, a matching outgoing animation on the old\n * container. `wipe` is reveal-only — the old container\n * disappears instantly when the incoming wipe completes.\n * 5. On the incoming animation's onfinish, tear down the old\n * layout the same way the instant path would have done\n * synchronously.\n *\n * Notes:\n * - Audio from the old layout keeps playing during the overlap.\n * Authors who want silent transitions should use `instant` or\n * mute the last audio widget on the outgoing layout.\n * - `layoutEnd` for the old layout is emitted up-front (same\n * point as the instant path, right after currentLayoutId\n * updates) so stats accounting isn't gated on the animation\n * clock. The DOM/media cleanup still waits for onfinish.\n * - Multi-display sync (#337 DoD): no sync-manager changes are\n * needed. `onLayoutShow` fires on every display at the same\n * moment via the lead's `showAt` contract, each display\n * applies its choreography stagger, and the transition spec\n * comes from the layout XLF (same on every display) so all\n * displays start and finish the transition in lock-step.\n *\n * @param {number} layoutId\n * @param {Object} preloaded - pool entry from layoutPool.get(layoutId)\n * @param {{type:string,duration:number,direction?:string}} spec\n */\n async _swapToPreloadedLayoutWithTransition(layoutId, preloaded, spec) {\n this.removeActionListeners();\n this._clearLayoutTimers();\n\n const oldLayoutId = this.currentLayoutId;\n const alreadyEmittedEnd = this.layoutEndEmitted;\n this.layoutEndEmitted = false;\n\n // Capture old state before we mutate `this.regions` so the\n // deferred teardown in onfinish can still reach it.\n const oldRegions = this.regions;\n const oldIsPooled =\n oldLayoutId !== null && this.layoutPool.has(oldLayoutId);\n const oldContainer = oldIsPooled\n ? this.layoutPool.get(oldLayoutId).container\n : null;\n\n // Phase 1 — stop old widgets while currentLayoutId still points\n // at the old layout (widgetEnd events fire with the correct id).\n this._clearRegionTimers(oldRegions);\n this._stopAllRegionWidgets(oldRegions, this._stopWidgetBound);\n\n // Clear old state AFTER widgets have emitted. The DOM is still\n // alive and will be removed by _teardownOldLayoutAfterTransition\n // once the animation finishes.\n this.currentLayout = null;\n this.currentLayoutId = null;\n this.regions = new Map();\n\n // Phase 2 — raise the preloaded wrapper above the old content.\n // The preload path appends wrappers at zIndex=-1 hidden; the\n // instant path sets them to zIndex=0 on activation. For the\n // overlap transition we use zIndex=1 so the new layout visibly\n // paints on top, and restore it to 0 at the end of the animation\n // (matches the steady-state convention of the instant path).\n preloaded.container.style.visibility = 'visible';\n preloaded.container.style.zIndex = '1';\n // Start the incoming layout at the animation's \"from\" state:\n // opacity 0 for fade, translated off-screen for slide, fully\n // clipped for wipe. The Transitions.apply() call will drive the\n // animation from there.\n if (spec.type === 'fade') {\n preloaded.container.style.opacity = '0';\n }\n\n // Phase 3 — activate new layout state + start its widgets.\n this._activatePreloadedLayout(layoutId, preloaded, oldLayoutId, alreadyEmittedEnd);\n\n // Phase 4 — kick off the animations. The incoming animation drives\n // the teardown timing in its onfinish; the outgoing one runs in\n // parallel purely for visual effect.\n const layoutWidth = preloaded.layout.width;\n const layoutHeight = preloaded.layout.height;\n\n const incoming = Transitions.apply(\n preloaded.container,\n spec,\n true,\n layoutWidth,\n layoutHeight\n );\n let outgoing = null;\n if (oldContainer && (spec.type === 'fade' || spec.type === 'slide')) {\n outgoing = Transitions.apply(\n oldContainer,\n spec,\n false,\n layoutWidth,\n layoutHeight\n );\n }\n\n const finalizeTeardown = () => {\n // Restore the preloaded wrapper to the same zIndex the instant\n // path leaves it at, so any subsequent swap finds the DOM in a\n // consistent state.\n preloaded.container.style.zIndex = '0';\n preloaded.container.style.opacity = '';\n\n this._teardownOldLayoutAfterTransition(\n oldLayoutId,\n oldRegions,\n oldIsPooled\n );\n\n // Cancel any still-running outgoing animation in case the\n // incoming finished first (different durations) — prevents the\n // old container from becoming visible again mid-cleanup.\n if (outgoing) {\n try { outgoing.cancel(); } catch (_) { /* no-op */ }\n }\n\n this.log.info(\n `Swapped to preloaded layout ${layoutId} (${spec.type} transition, ${spec.duration}ms)`\n );\n this._logResourceStats(layoutId);\n };\n\n if (incoming) {\n incoming.onfinish = finalizeTeardown;\n // Safety net: if onfinish never fires (browser bug, tab\n // backgrounded, etc.), force cleanup after duration + 50%.\n // This matches the pre-#337 worst case where cleanup was\n // synchronous — we'd rather cut abruptly than leak DOM.\n setTimeout(finalizeTeardown, Math.ceil(spec.duration * 1.5));\n } else {\n // Browser doesn't support the requested transition type —\n // fall back to an instant cleanup so nothing leaks.\n finalizeTeardown();\n }\n }\n\n /**\n * Shared \"activate preloaded layout\" block. Extracted from the\n * instant path so both swap paths produce identical state after\n * activation (state updates, event emission, background copy,\n * scale, widget start). See _swapToPreloadedLayoutInstant for the\n * historical inline version — this is a straight cut-and-lift,\n * not a rewrite.\n *\n * Preconditions: the old layout's widgets have been stopped and\n * the old state has been cleared from this.currentLayout / this.regions.\n *\n * @param {number} layoutId\n * @param {Object} preloaded - pool entry\n * @param {number|null} oldLayoutId - id of the layout we're leaving\n * @param {boolean} alreadyEmittedEnd - whether layoutEnd was already emitted for the old layout\n */\n _activatePreloadedLayout(layoutId, preloaded, oldLayoutId, alreadyEmittedEnd) {\n preloaded.container.style.visibility = 'visible';\n // The transition path raises zIndex to 1 during the animation and\n // restores it to 0 in finalizeTeardown — don't clobber that here.\n if (preloaded.container.style.zIndex !== '1') {\n preloaded.container.style.zIndex = '0';\n }\n\n // Update renderer state to the preloaded layout\n this.layoutPool.setHot(layoutId);\n this.currentLayout = preloaded.layout;\n this.currentLayoutId = layoutId;\n this.regions = preloaded.regions;\n\n // Emit layoutEnd for old layout AFTER setting new currentLayoutId —\n // the listener guard in main.ts sees the new layout already playing\n // and skips advance, while stats/tracking still run.\n // Skip if the layout timer already emitted layoutEnd (avoids double stats).\n if (oldLayoutId && !alreadyEmittedEnd) {\n this.emit('layoutEnd', oldLayoutId);\n }\n\n // Update container background to match preloaded layout\n this.container.style.backgroundColor = preloaded.layout.bgcolor;\n if (preloaded.container.style.backgroundImage) {\n // Copy background styles from preloaded wrapper to main container\n for (const prop of ['backgroundImage', 'backgroundSize', 'backgroundPosition', 'backgroundRepeat']) {\n this.container.style[prop] = preloaded.container.style[prop];\n }\n } else {\n this.container.style.backgroundImage = '';\n }\n\n // Recalculate scale for the preloaded layout\n this.calculateScale(preloaded.layout);\n\n // Attach interactive action listeners\n this.attachActionListeners(preloaded.layout);\n\n // Emit layout start event\n this.emit('layoutStart', layoutId, preloaded.layout);\n\n // Reset all regions and start widget cycling\n for (const [regionId, region] of this.regions) {\n region.currentIndex = 0;\n region.complete = false;\n this.startRegion(regionId);\n }\n\n // Recalculate layout duration from widget durations.\n // During preload, video loadedmetadata updated widget.duration but\n // updateLayoutDuration() updated this.currentLayout (the old layout),\n // so preloaded.layout.duration may still be the XLF default (e.g. 60s).\n this.updateLayoutDuration();\n\n // Wait for widgets to be ready then start layout timer\n this.startLayoutTimerWhenReady(layoutId, preloaded.layout);\n\n // Schedule next preload (unless updateLayoutDuration already did it)\n if (!this.preloadTimer) {\n this._scheduleNextLayoutPreload(preloaded.layout);\n }\n }\n\n /**\n * Tear down the old layout after a transition animation finishes.\n *\n * Mirrors the synchronous teardown in _swapToPreloadedLayoutInstant\n * but runs from an animation's onfinish callback (or the safety\n * timeout) so the DOM, videos, and blob URLs live long enough for\n * the visual transition to complete.\n *\n * @param {number|null} oldLayoutId\n * @param {Map} oldRegions - the this.regions captured before swap\n * @param {boolean} oldIsPooled - whether the old layout was in the pool\n */\n _teardownOldLayoutAfterTransition(oldLayoutId, oldRegions, oldIsPooled) {\n if (oldIsPooled && oldLayoutId !== null) {\n // Old layout was preloaded — evict from pool (removes wrapper).\n this.layoutPool.evict(oldLayoutId);\n return;\n }\n\n // Old layout was rendered normally — manual cleanup.\n for (const [, region] of oldRegions) {\n // Release video/audio resources before removing from DOM\n LayoutPool.releaseMediaElements(region.element);\n // Apply region exit transition if configured, then remove\n if (region.config?.exitTransition) {\n const animation = Transitions.apply(\n region.element, region.config.exitTransition, false,\n region.width, region.height\n );\n if (animation) {\n const el = region.element;\n animation.onfinish = () => el.remove();\n } else {\n region.element.remove();\n }\n } else {\n region.element.remove();\n }\n }\n if (oldLayoutId) {\n this.revokeBlobUrlsForLayout(oldLayoutId);\n }\n }\n\n /**\n * Log resource allocation stats for debugging memory/GPU leaks.\n * Called after every layout swap to track DOM node accumulation,\n * video element lifecycle, and pool state.\n */\n _logResourceStats(layoutId) {\n const domNodes = document.querySelectorAll('*').length;\n const videos = document.querySelectorAll('video').length;\n const videosSrc = document.querySelectorAll('video[src]').length;\n const canvases = document.querySelectorAll('canvas').length;\n const iframes = document.querySelectorAll('iframe').length;\n const images = document.querySelectorAll('img').length;\n const poolSize = this.layoutPool ? this.layoutPool.size : 0;\n const regionCount = this.regions ? this.regions.size : 0;\n const widgetElements = [...(this.regions?.values() || [])].reduce(\n (sum, r) => sum + (r.widgetElements?.size || 0), 0\n );\n const jsHeap = performance?.memory ? {\n used: Math.round(performance.memory.usedJSHeapSize / 1048576),\n total: Math.round(performance.memory.totalJSHeapSize / 1048576),\n limit: Math.round(performance.memory.jsHeapSizeLimit / 1048576),\n } : null;\n\n // Count blob URLs still tracked (potential leak indicator)\n const blobUrls = this._blobUrls ? [...this._blobUrls.values()].reduce((s, set) => s + set.size, 0) : 0;\n const blobLayouts = this._blobUrls ? this._blobUrls.size : 0;\n\n // Preload wrapper divs in DOM (should be 0-1 in normal operation)\n const preloadWrappers = document.querySelectorAll('.renderer-lite-preload-wrapper').length;\n\n // Audio overlay elements\n const audioEls = document.querySelectorAll('audio').length;\n\n const heapStr = jsHeap ? `heap=${jsHeap.used}/${jsHeap.total}MB (limit ${jsHeap.limit}MB)` : 'heap=N/A';\n this.log.info(\n `[Resources] layout=${layoutId} dom=${domNodes} videos=${videos}(src=${videosSrc}) ` +\n `canvas=${canvases} iframe=${iframes} img=${images} audio=${audioEls} ` +\n `pool=${poolSize} preloadWrappers=${preloadWrappers} ` +\n `regions=${regionCount} widgets=${widgetElements} ` +\n `blobs=${blobUrls}(${blobLayouts} layouts) ${heapStr}`\n );\n }\n\n /**\n * Get the currently showing layout ID.\n * @returns {number|null}\n */\n getCurrentLayoutId() {\n return this.currentLayoutId;\n }\n\n /**\n * Show a preloaded layout (swap from pool to visible).\n * If no layoutId, shows the most recently preloaded layout.\n * No-ops if the layout is not in the pool.\n * @param {number} [layoutId]\n */\n showLayout(layoutId) {\n if (layoutId === undefined) {\n layoutId = this.layoutPool.getLatest();\n if (layoutId === undefined) {\n this.log.warn('showLayout: no preloaded layout to show');\n return;\n }\n }\n // Same layout already showing — skip swap (self-swap would evict then fail).\n // Same-layout replay is handled by renderLayout's replay path instead.\n if (this.currentLayoutId === layoutId) {\n this.log.info(`showLayout: layout ${layoutId} already showing`);\n return;\n }\n if (!this.layoutPool.has(layoutId)) {\n this.log.warn(`showLayout: layout ${layoutId} not in preload pool`);\n return;\n }\n this._swapToPreloadedLayout(layoutId);\n }\n\n /**\n * Check if the layout timer is active (running or deferred waiting for metadata).\n * Used to detect stalled layouts that need timer restart.\n * @returns {boolean}\n */\n hasActiveLayoutTimer() {\n return this.layoutTimer !== null || this._deferredTimerLayoutId !== null;\n }\n\n /**\n * Check if all regions have completed one full cycle\n * This is informational only - layout timer is authoritative\n */\n checkLayoutComplete() {\n // Check if all regions with multiple widgets have completed one cycle\n let allComplete = true;\n for (const [regionId, region] of this.regions) {\n // Only check multi-widget regions\n if (region.widgets.length > 1 && !region.complete) {\n allComplete = false;\n break;\n }\n }\n\n if (allComplete && this.currentLayoutId) {\n this.log.info(`All multi-widget regions completed one cycle`);\n // NOTE: We DON'T emit layoutEnd here - layout timer is authoritative\n // This is just informational logging for debugging\n }\n }\n\n /**\n * Clear all layout-level timers (layout duration, preload, preload retry).\n */\n _clearLayoutTimers() {\n if (this.layoutTimer) {\n clearTimeout(this.layoutTimer);\n this.layoutTimer = null;\n }\n if (this.preloadTimer) {\n clearTimeout(this.preloadTimer);\n this.preloadTimer = null;\n }\n if (this._preloadRetryTimer) {\n clearTimeout(this._preloadRetryTimer);\n this._preloadRetryTimer = null;\n }\n }\n\n /**\n * Stop current layout\n */\n stopCurrentLayout() {\n if (!this.currentLayout) return;\n\n this.log.info(`Stopping layout ${this.currentLayoutId}`);\n\n const endedLayoutId = this.currentLayoutId;\n const shouldEmit = endedLayoutId && !this.layoutEndEmitted;\n\n this.layoutEndEmitted = false;\n this._deferredTimerLayoutId = null;\n if (this._deferredTimerFallback) {\n clearTimeout(this._deferredTimerFallback);\n this._deferredTimerFallback = null;\n }\n this.currentLayout = null;\n this.currentLayoutId = null;\n\n // Clear timers\n this._clearLayoutTimers();\n\n // Remove interactive action listeners before teardown\n this.removeActionListeners();\n\n // If layout was preloaded (has its own wrapper div in pool), evict safely.\n // Normally-rendered layouts are NOT in the pool, so we do manual cleanup.\n if (endedLayoutId && this.layoutPool.has(endedLayoutId)) {\n this.layoutPool.evict(endedLayoutId);\n } else {\n // Normally-rendered layout - manual cleanup (regions are in this.container)\n\n // Revoke all blob URLs for this layout (tracked lifecycle management)\n if (endedLayoutId) {\n this.revokeBlobUrlsForLayout(endedLayoutId);\n }\n\n // Stop all regions — use helper to stop ALL started widgets (canvas fix)\n this._clearRegionTimers(this.regions);\n this._stopAllRegionWidgets(this.regions, this._stopWidgetBound);\n for (const [, region] of this.regions) {\n // Release video/audio resources before removing from DOM\n LayoutPool.releaseMediaElements(region.element);\n\n // Apply region exit transition if configured, then remove\n if (region.config?.exitTransition) {\n const animation = Transitions.apply(\n region.element, region.config.exitTransition, false,\n region.width, region.height\n );\n if (animation) {\n const el = region.element;\n animation.onfinish = () => el.remove();\n } else {\n region.element.remove();\n }\n } else {\n region.element.remove();\n }\n }\n\n }\n\n this.regions.clear();\n\n // Emit LAST — re-entrant renderLayout() sees currentLayout=null,\n // so stopCurrentLayout() returns early. No cascade.\n if (shouldEmit) {\n this.emit('layoutEnd', endedLayoutId);\n }\n }\n\n /**\n * Render an overlay layout on top of the main layout\n * @param {string} xlfXml - XLF XML content for overlay\n * @param {number} layoutId - Overlay layout ID\n * @param {number} priority - Overlay priority (higher = on top)\n * @returns {Promise<void>}\n */\n async renderOverlay(xlfXml, layoutId, priority = 0) {\n try {\n this.log.info(`Rendering overlay ${layoutId} (priority ${priority})`);\n\n // Check if this overlay is already active\n if (this.activeOverlays.has(layoutId)) {\n this.log.warn(`Overlay ${layoutId} already active, skipping`);\n return;\n }\n\n // Parse XLF\n const layout = this.parseXlf(xlfXml);\n\n // Create overlay container\n const overlayDiv = document.createElement('div');\n overlayDiv.id = `overlay_${layoutId}`;\n overlayDiv.className = 'renderer-lite-overlay';\n overlayDiv.style.position = 'absolute';\n overlayDiv.style.top = '0';\n overlayDiv.style.left = '0';\n overlayDiv.style.width = '100%';\n overlayDiv.style.height = '100%';\n overlayDiv.style.zIndex = String(1000 + priority); // Higher priority = higher z-index\n overlayDiv.style.pointerEvents = 'auto'; // Enable clicks on overlay\n overlayDiv.style.backgroundColor = layout.bgcolor;\n\n // Calculate scale for overlay layout\n this.calculateScale(layout);\n\n // Create regions for overlay\n const overlayRegions = new Map();\n for (const regionConfig of layout.regions) {\n const region = this._createRegionEntry(\n regionConfig,\n `overlay_${layoutId}_region_${regionConfig.id}`,\n overlayDiv,\n {\n className: 'renderer-lite-region overlay-region',\n isCanvas: regionConfig.isCanvas || false,\n }\n );\n overlayRegions.set(regionConfig.id, region);\n }\n\n // Pre-create widget elements for overlay\n for (const [regionId, region] of overlayRegions) {\n for (const widget of region.widgets) {\n widget.layoutId = layoutId;\n widget.regionId = regionId;\n\n try {\n const element = await this.createWidgetElement(widget, region);\n this._positionWidgetElement(element);\n region.element.appendChild(element);\n region.widgetElements.set(widget.id, element);\n } catch (error) {\n this.log.error(`Failed to pre-create overlay widget ${widget.id}:`, error);\n }\n }\n }\n\n // Add overlay to container\n this.overlayContainer.appendChild(overlayDiv);\n\n // Store overlay state\n this.activeOverlays.set(layoutId, {\n container: overlayDiv,\n layout: layout,\n regions: overlayRegions,\n timer: null,\n priority: priority\n });\n\n // Emit overlay start event\n this.emit('overlayStart', layoutId, layout);\n\n // Start all overlay regions\n for (const [regionId, region] of overlayRegions) {\n this.startOverlayRegion(layoutId, regionId);\n }\n\n // Set overlay timer based on duration\n if (layout.duration > 0) {\n const durationMs = layout.duration * 1000;\n const overlayState = this.activeOverlays.get(layoutId);\n if (overlayState) {\n overlayState.timer = setTimeout(() => {\n this.log.info(`Overlay ${layoutId} duration expired (${layout.duration}s)`);\n this.emit('overlayEnd', layoutId);\n }, durationMs);\n }\n }\n\n this.log.info(`Overlay ${layoutId} started`);\n\n } catch (error) {\n this.log.error('Error rendering overlay:', error);\n this.emit('error', { type: 'overlayError', error, layoutId });\n throw error;\n }\n }\n\n /**\n * Start playing an overlay region's widgets\n * @param {number} overlayId - Overlay layout ID\n * @param {string} regionId - Region ID\n */\n startOverlayRegion(overlayId, regionId) {\n const overlayState = this.activeOverlays.get(overlayId);\n if (!overlayState) return;\n\n const region = overlayState.regions.get(regionId);\n this._startRegionCycle(\n region, regionId,\n (rid, idx) => this.renderOverlayWidget(overlayId, rid, idx),\n (rid, idx) => this.stopOverlayWidget(overlayId, rid, idx),\n () => this.log.info(`Overlay ${overlayId} region ${regionId} completed one full cycle`)\n );\n }\n\n /**\n * Render a widget in an overlay region\n * @param {number} overlayId - Overlay layout ID\n * @param {string} regionId - Region ID\n * @param {number} widgetIndex - Widget index in region\n */\n async renderOverlayWidget(overlayId, regionId, widgetIndex) {\n const overlayState = this.activeOverlays.get(overlayId);\n if (!overlayState) return;\n\n const region = overlayState.regions.get(regionId);\n if (!region) return;\n\n try {\n const widget = await this._showWidget(region, widgetIndex);\n if (widget) {\n this.log.info(`Showing overlay widget ${widget.type} (${widget.id}) in overlay ${overlayId} region ${regionId}`);\n this._startedWidgets.add(`overlay:${overlayId}:${regionId}:${widgetIndex}`);\n this.emit('overlayWidgetStart', {\n overlayId, widgetId: widget.id, regionId,\n type: widget.type, duration: widget.duration\n });\n }\n } catch (error) {\n this.log.error(`Error rendering overlay widget:`, error);\n this.emit('error', { type: 'overlayWidgetError', error, widgetId: region.widgets[widgetIndex]?.id, regionId, overlayId });\n }\n }\n\n /**\n * Stop an overlay widget\n * @param {number} overlayId - Overlay layout ID\n * @param {string} regionId - Region ID\n * @param {number} widgetIndex - Widget index\n */\n async stopOverlayWidget(overlayId, regionId, widgetIndex) {\n const key = `overlay:${overlayId}:${regionId}:${widgetIndex}`;\n if (!this._startedWidgets.delete(key)) return; // idempotent\n\n const overlayState = this.activeOverlays.get(overlayId);\n if (!overlayState) return;\n\n const region = overlayState.regions.get(regionId);\n if (!region) return;\n\n const { widget, animPromise } = this._hideWidget(region, widgetIndex);\n // Emit immediately — don't wait for exit animation (same fix as stopWidget)\n if (widget) {\n this.emit('overlayWidgetEnd', {\n overlayId, widgetId: widget.id, regionId, type: widget.type\n });\n }\n if (animPromise) await animPromise;\n }\n\n /**\n * Stop and remove an overlay layout\n * @param {number} layoutId - Overlay layout ID\n */\n stopOverlay(layoutId) {\n const overlayState = this.activeOverlays.get(layoutId);\n if (!overlayState) {\n this.log.warn(`Overlay ${layoutId} not active`);\n return;\n }\n\n this.log.info(`Stopping overlay ${layoutId}`);\n\n // Clear overlay timer\n if (overlayState.timer) {\n clearTimeout(overlayState.timer);\n overlayState.timer = null;\n }\n\n // Stop all overlay regions\n for (const [, region] of overlayState.regions) {\n if (region.timer) { clearTimeout(region.timer); region.timer = null; }\n }\n this._stopAllRegionWidgets(overlayState.regions,\n (rid, idx) => this.stopOverlayWidget(layoutId, rid, idx));\n\n // Remove overlay container from DOM\n if (overlayState.container) {\n overlayState.container.remove();\n }\n\n // Revoke blob URLs for this overlay\n this.revokeBlobUrlsForLayout(layoutId);\n\n // Remove from active overlays\n this.activeOverlays.delete(layoutId);\n\n // Emit overlay end event\n this.emit('overlayEnd', layoutId);\n\n this.log.info(`Overlay ${layoutId} stopped`);\n }\n\n /**\n * Stop all active overlays\n */\n stopAllOverlays() {\n const overlayIds = Array.from(this.activeOverlays.keys());\n for (const overlayId of overlayIds) {\n this.stopOverlay(overlayId);\n }\n this.log.info('All overlays stopped');\n }\n\n /**\n * Get active overlay IDs\n * @returns {Array<number>}\n */\n getActiveOverlays() {\n return Array.from(this.activeOverlays.keys());\n }\n\n /**\n * Pause playback: pause all media, stop widget cycling.\n * The layout timer keeps running — schedule is authoritative.\n */\n pause() {\n if (this._paused) return;\n this._paused = true;\n\n // Stop all region widget-cycling timers\n for (const [, region] of this.regions) {\n if (region.timer) {\n clearTimeout(region.timer);\n region.timer = null;\n }\n }\n\n // Pause all video/audio elements\n this._forEachMedia(el => el.pause());\n\n this.emit('paused');\n this.log.info('Playback paused (layout timer continues)');\n }\n\n /**\n * Check if playback is currently paused.\n */\n isPaused() {\n return this._paused;\n }\n\n /**\n * Resume playback: resume media and widget cycling.\n * Layout timer was never paused — no need to restore it.\n */\n resume() {\n if (!this._paused) return;\n this._paused = false;\n\n // Resume all video/audio\n this._forEachMedia(el => el.play().catch(() => {}));\n\n // Restart region widget cycling (re-enters cycle from current widget)\n for (const [regionId] of this.regions) {\n this.startRegion(regionId);\n }\n\n this.emit('resumed');\n this.log.info('Playback resumed');\n }\n\n /**\n * Apply a function to every video/audio element in all regions.\n */\n _forEachMedia(fn) {\n for (const [, region] of this.regions) {\n region.element?.querySelectorAll('video, audio').forEach(fn);\n }\n }\n\n /**\n * Cleanup renderer\n */\n cleanup() {\n this.stopAllOverlays();\n this.stopCurrentLayout();\n this._startedWidgets.clear();\n\n // Clean up any remaining audio overlays\n for (const widgetId of this.audioOverlays.keys()) {\n this._stopAudioOverlays(widgetId);\n }\n\n // Clear the layout preload pool\n this.layoutPool.clear();\n\n if (this.preloadTimer) {\n clearTimeout(this.preloadTimer);\n this.preloadTimer = null;\n }\n if (this._preloadRetryTimer) {\n clearTimeout(this._preloadRetryTimer);\n this._preloadRetryTimer = null;\n }\n\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n this.resizeObserver = null;\n }\n\n this.container.innerHTML = '';\n this.log.info('Cleaned up');\n }\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Layout translator - XLF to HTML\n * Based on arexibo layout.rs\n */\n\nimport { cacheWidgetHtml } from '@xiboplayer/cache';\nimport { createLogger, isDebug, PLAYER_API } from '@xiboplayer/utils';\n\nconst log = createLogger('Layout');\n\n// ── Safe interpolation helpers for HTML generation ─────────────────────────\n// Use these instead of raw ${value} in template literals to prevent XSS.\n// Our LayoutTranslator generates complete HTML documents as strings (unlike\n// upstream Xibo players that use DOM APIs). This gives us pre-rendering and\n// cross-context support (Node.js, arexibo, service worker) but requires\n// manual sanitization at every interpolation point.\nconst SAFE_CSS_COLOR = /^(#[0-9a-fA-F]{3,8}|rgba?\\(\\s*[\\d.,\\s%]+\\)|[a-zA-Z]{1,20}|transparent|inherit)$/;\nexport const safeCssColor = (v) => SAFE_CSS_COLOR.test(v) ? v : '#000000';\nexport const safeJsString = (v) => JSON.stringify(v);\nexport const safeHtmlAttr = (v) => String(v).replace(/[&\"'<>]/g, c =>\n ({ '&': '&', '\"': '"', \"'\": ''', '<': '<', '>': '>' }[c]));\n\nexport class LayoutTranslator {\n constructor(xmds) {\n this.xmds = xmds;\n }\n\n /**\n * Translate XLF XML to playable HTML\n */\n async translateXLF(layoutId, xlfXml) {\n const parser = new DOMParser();\n const doc = parser.parseFromString(xlfXml, 'text/xml');\n\n const layoutEl = doc.querySelector('layout');\n if (!layoutEl) {\n throw new Error('Invalid XLF: no <layout> element');\n }\n\n const width = parseInt(layoutEl.getAttribute('width') || '1920');\n const height = parseInt(layoutEl.getAttribute('height') || '1080');\n const bgcolor = safeCssColor(layoutEl.getAttribute('bgcolor') || '#000000');\n\n const regions = [];\n for (const regionEl of doc.querySelectorAll('region')) {\n regions.push(await this.translateRegion(layoutId, regionEl));\n }\n\n return this.generateHTML(width, height, bgcolor, regions);\n }\n\n /**\n * Translate a single region\n */\n async translateRegion(layoutId, regionEl) {\n const id = regionEl.getAttribute('id');\n const width = parseInt(regionEl.getAttribute('width'));\n const height = parseInt(regionEl.getAttribute('height'));\n const top = parseInt(regionEl.getAttribute('top'));\n const left = parseInt(regionEl.getAttribute('left'));\n const zindex = parseInt(regionEl.getAttribute('zindex') || '0');\n\n const media = [];\n for (const mediaEl of regionEl.querySelectorAll('media')) {\n media.push(await this.translateMedia(layoutId, id, mediaEl));\n }\n\n return {\n id,\n width,\n height,\n top,\n left,\n zindex,\n media\n };\n }\n\n /**\n * Translate a single media item\n */\n async translateMedia(layoutId, regionId, mediaEl) {\n const type = mediaEl.getAttribute('type');\n const duration = parseInt(mediaEl.getAttribute('duration') || '10');\n const id = mediaEl.getAttribute('id');\n\n const optionsEl = mediaEl.querySelector('options');\n const rawEl = mediaEl.querySelector('raw');\n\n const options = {};\n if (optionsEl) {\n for (const child of optionsEl.children) {\n options[child.tagName] = child.textContent;\n }\n }\n\n // Parse transition information\n const transitions = {\n in: null,\n out: null\n };\n\n const transInEl = mediaEl.querySelector('options > transIn');\n const transOutEl = mediaEl.querySelector('options > transOut');\n const transInDurationEl = mediaEl.querySelector('options > transInDuration');\n const transOutDurationEl = mediaEl.querySelector('options > transOutDuration');\n const transInDirectionEl = mediaEl.querySelector('options > transInDirection');\n const transOutDirectionEl = mediaEl.querySelector('options > transOutDirection');\n\n if (transInEl && transInEl.textContent) {\n transitions.in = {\n type: transInEl.textContent,\n duration: parseInt(transInDurationEl?.textContent || '1000'),\n direction: transInDirectionEl?.textContent || 'N'\n };\n }\n\n if (transOutEl && transOutEl.textContent) {\n transitions.out = {\n type: transOutEl.textContent,\n duration: parseInt(transOutDurationEl?.textContent || '1000'),\n direction: transOutDirectionEl?.textContent || 'N'\n };\n }\n\n // All videos use cache URL pattern\n // Large videos download in background, small videos are already cached\n // Service Worker handles both cases appropriately\n\n let raw = rawEl ? rawEl.textContent : '';\n\n // For widgets (clock, calendar, etc.), fetch rendered HTML from CMS\n const widgetTypes = ['clock', 'clock-digital', 'clock-analogue', 'calendar', 'weather',\n 'currencies', 'stocks', 'twitter', 'global', 'embedded', 'text', 'ticker'];\n if (widgetTypes.some(w => type.includes(w))) {\n // Try to get widget HTML with retry logic for kiosk reliability\n const retries = 3;\n let lastError = null;\n\n for (let attempt = 1; attempt <= retries; attempt++) {\n try {\n log.info(`Fetching resource for ${type} widget (layout=${layoutId}, region=${regionId}, media=${id}) - attempt ${attempt}/${retries}`);\n raw = await this.xmds.getResource(layoutId, regionId, id);\n log.info(`Got resource HTML (${raw.length} chars)`);\n\n // Store widget HTML in cache and save cache key for iframe src generation\n const widgetCacheKey = await cacheWidgetHtml(layoutId, regionId, id, raw);\n options.widgetCacheKey = widgetCacheKey;\n\n // Success - break retry loop\n break;\n\n } catch (error) {\n lastError = error;\n log.warn(`Failed to get resource (attempt ${attempt}/${retries}):`, error.message);\n\n // If not last attempt, wait before retry\n if (attempt < retries) {\n const delay = attempt * 2000; // 2s, 4s backoff\n log.info(`Retrying in ${delay}ms...`);\n await new Promise(resolve => setTimeout(resolve, delay));\n }\n }\n }\n\n // If all retries failed, try to use cached version as fallback\n if (!raw && lastError) {\n log.warn('All retries failed, checking for cached widget HTML...');\n\n // Try to get cached widget HTML from ContentStore via proxy\n try {\n const resp = await fetch(`/store${PLAYER_API}/widgets/${layoutId}/${regionId}/${id}`);\n if (resp.ok) {\n raw = await resp.text();\n options.widgetCacheKey = `${PLAYER_API}/widgets/${layoutId}/${regionId}/${id}`;\n log.info(`Using stored widget HTML (${raw.length} chars) - CMS update pending`);\n } else {\n log.error(`No stored version available for widget ${id}`);\n raw = `<div style=\"display:flex;align-items:center;justify-content:center;height:100%;color:#999;font-size:18px;\">Content updating...</div>`;\n }\n } catch (storeError) {\n log.error('Store fallback failed:', storeError);\n raw = `<div style=\"display:flex;align-items:center;justify-content:center;height:100%;color:#999;font-size:18px;\">Content updating...</div>`;\n }\n }\n }\n\n return {\n type,\n duration,\n id,\n options,\n raw,\n transitions\n };\n }\n\n /**\n * Generate HTML from parsed layout\n */\n generateHTML(width, height, bgcolor, regions) {\n const regionHTML = regions.map(r => this.generateRegionHTML(r)).join('\\n');\n const regionJS = regions.map(r => this.generateRegionJS(r)).join(',\\n');\n\n return `<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=${width}, height=${height}\">\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n html, body { width: 100%; height: 100%; overflow: hidden; }\n body { background-color: ${bgcolor}; }\n .region {\n position: absolute;\n overflow: hidden;\n }\n .media {\n width: 100%;\n height: 100%;\n object-fit: contain;\n }\n iframe {\n border: none;\n width: 100%;\n height: 100%;\n }\n </style>\n</head>\n<body>\n${regionHTML}\n<script>\n// Transition utilities\nwindow.Transitions = {\n fadeIn(element, duration) {\n const keyframes = [\n { opacity: 0 },\n { opacity: 1 }\n ];\n const timing = {\n duration: duration,\n easing: 'linear',\n fill: 'forwards'\n };\n return element.animate(keyframes, timing);\n },\n\n fadeOut(element, duration) {\n const keyframes = [\n { opacity: 1 },\n { opacity: 0 }\n ];\n const timing = {\n duration: duration,\n easing: 'linear',\n fill: 'forwards'\n };\n return element.animate(keyframes, timing);\n },\n\n getFlyKeyframes(direction, width, height, isIn) {\n const dirMap = {\n 'N': { x: 0, y: isIn ? -height : height },\n 'NE': { x: isIn ? width : -width, y: isIn ? -height : height },\n 'E': { x: isIn ? width : -width, y: 0 },\n 'SE': { x: isIn ? width : -width, y: isIn ? height : -height },\n 'S': { x: 0, y: isIn ? height : -height },\n 'SW': { x: isIn ? -width : width, y: isIn ? height : -height },\n 'W': { x: isIn ? -width : width, y: 0 },\n 'NW': { x: isIn ? -width : width, y: isIn ? -height : height }\n };\n\n const offset = dirMap[direction] || dirMap['N'];\n\n if (isIn) {\n return [\n { transform: \\`translate(\\${offset.x}px, \\${offset.y}px)\\`, opacity: 0 },\n { transform: 'translate(0, 0)', opacity: 1 }\n ];\n } else {\n return [\n { transform: 'translate(0, 0)', opacity: 1 },\n { transform: \\`translate(\\${offset.x}px, \\${offset.y}px)\\`, opacity: 0 }\n ];\n }\n },\n\n flyIn(element, duration, direction, regionWidth, regionHeight) {\n const keyframes = this.getFlyKeyframes(direction, regionWidth, regionHeight, true);\n const timing = {\n duration: duration,\n easing: 'ease-out',\n fill: 'forwards'\n };\n return element.animate(keyframes, timing);\n },\n\n flyOut(element, duration, direction, regionWidth, regionHeight) {\n const keyframes = this.getFlyKeyframes(direction, regionWidth, regionHeight, false);\n const timing = {\n duration: duration,\n easing: 'ease-in',\n fill: 'forwards'\n };\n return element.animate(keyframes, timing);\n },\n\n apply(element, transitionConfig, isIn, regionWidth, regionHeight) {\n if (!transitionConfig || !transitionConfig.type) {\n return null;\n }\n\n const type = transitionConfig.type.toLowerCase();\n const duration = transitionConfig.duration || 1000;\n const direction = transitionConfig.direction || 'N';\n\n switch (type) {\n case 'fade':\n return isIn ? this.fadeIn(element, duration) : this.fadeOut(element, duration);\n case 'fadein':\n return isIn ? this.fadeIn(element, duration) : null;\n case 'fadeout':\n return isIn ? null : this.fadeOut(element, duration);\n case 'fly':\n return isIn\n ? this.flyIn(element, duration, direction, regionWidth, regionHeight)\n : this.flyOut(element, duration, direction, regionWidth, regionHeight);\n case 'flyin':\n return isIn ? this.flyIn(element, duration, direction, regionWidth, regionHeight) : null;\n case 'flyout':\n return isIn ? null : this.flyOut(element, duration, direction, regionWidth, regionHeight);\n default:\n return null;\n }\n }\n};\n\nconst regions = {\n${regionJS}\n};\n\n// Auto-start all regions\nObject.keys(regions).forEach(id => {\n playRegion(id);\n});\n\n// Track active timers per region so layout teardown can cancel them\nconst regionTimers = {};\n\nfunction playRegion(id) {\n const region = regions[id];\n if (!region || region.media.length === 0) return;\n\n regionTimers[id] = [];\n\n // If only one media item, just show it and don't cycle (arexibo behavior)\n if (region.media.length === 1) {\n const media = region.media[0];\n if (media.start) media.start();\n return; // Don't schedule stop/restart\n }\n\n // Multiple media items - cycle normally\n let currentIndex = 0;\n\n function playNext() {\n const media = region.media[currentIndex];\n if (media.start) media.start();\n\n const duration = media.duration || 10;\n const timerId = setTimeout(() => {\n if (media.stop) media.stop();\n currentIndex = (currentIndex + 1) % region.media.length;\n playNext();\n }, duration * 1000);\n regionTimers[id].push(timerId);\n }\n\n playNext();\n}\n\n// Cleanup function — called before layout teardown\nwindow._stopAllRegions = function() {\n Object.values(regionTimers).forEach(timers => timers.forEach(t => clearTimeout(t)));\n};\n</script>\n</body>\n</html>`;\n }\n\n /**\n * Generate HTML for a region container\n */\n generateRegionHTML(region) {\n return ` <div id=\"region_${region.id}\" class=\"region\" style=\"\n left: ${region.left}px;\n top: ${region.top}px;\n width: ${region.width}px;\n height: ${region.height}px;\n z-index: ${region.zindex};\n \"></div>`;\n }\n\n /**\n * Generate JavaScript for region media control\n */\n generateRegionJS(region) {\n const mediaJS = region.media.map(m => this.generateMediaJS(m, region.id)).join(',\\n ');\n\n return ` '${region.id}': {\n media: [\n${mediaJS}\n ]\n }`;\n }\n\n /**\n * Generate iframe widget JS for text/ticker and generic widget types.\n * Returns { startFn, stopFn } strings for the media item.\n */\n _generateIframeWidgetJS(regionId, mediaId, widgetUrl, transIn, transOut) {\n const iframeId = `widget_${regionId}_${mediaId}`;\n const startFn = `() => {\n const region = document.getElementById('region_${regionId}');\n let iframe = document.getElementById('${iframeId}');\n if (!iframe) {\n iframe = document.createElement('iframe');\n iframe.id = '${iframeId}';\n iframe.src = ${safeJsString(widgetUrl)};\n iframe.style.width = '100%';\n iframe.style.height = '100%';\n iframe.style.border = 'none';\n iframe.scrolling = 'no';\n iframe.style.opacity = '0';\n region.innerHTML = '';\n region.appendChild(iframe);\n\n // Apply transition after iframe loads\n iframe.onload = () => {\n const transIn = ${transIn};\n if (transIn && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n window.Transitions.apply(iframe, transIn, true, regionRect.width, regionRect.height);\n } else {\n iframe.style.opacity = '1';\n }\n };\n } else {\n iframe.style.display = 'block';\n iframe.style.opacity = '1';\n }\n }`;\n const stopFn = `() => {\n const region = document.getElementById('region_${regionId}');\n const iframe = document.getElementById('${iframeId}');\n if (iframe) {\n const transOut = ${transOut};\n if (transOut && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n const animation = window.Transitions.apply(iframe, transOut, false, regionRect.width, regionRect.height);\n if (animation) {\n animation.onfinish = () => {\n iframe.style.display = 'none';\n };\n return;\n }\n }\n iframe.style.display = 'none';\n }\n }`;\n return { startFn, stopFn };\n }\n\n /**\n * Generate JavaScript for a single media item\n */\n generateMediaJS(media, regionId) {\n const duration = media.duration || 10;\n const transIn = media.transitions?.in ? JSON.stringify(media.transitions.in) : 'null';\n const transOut = media.transitions?.out ? JSON.stringify(media.transitions.out) : 'null';\n let startFn = 'null';\n let stopFn = 'null';\n\n switch (media.type) {\n case 'image': {\n // Use absolute URL within service worker scope\n const imageSrc = `${window.location.origin}${PLAYER_API}/media/${media.options.uri}`;\n startFn = `() => {\n const region = document.getElementById('region_${regionId}');\n const img = document.createElement('img');\n img.className = 'media';\n img.src = ${safeJsString(imageSrc)};\n img.style.opacity = '0';\n region.innerHTML = '';\n region.appendChild(img);\n\n // Apply transition\n const transIn = ${transIn};\n if (transIn && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n window.Transitions.apply(img, transIn, true, regionRect.width, regionRect.height);\n } else {\n img.style.opacity = '1';\n }\n }`;\n break;\n }\n\n case 'video': {\n // All videos use cache URL pattern\n // Background-downloaded videos will auto-reload when cache completes\n const videoSrc = `${window.location.origin}${PLAYER_API}/media/${media.options.uri}`;\n const videoFilename = media.options.uri;\n\n startFn = `() => {\n const region = document.getElementById('region_${regionId}');\n const video = document.createElement('video');\n video.className = 'media';\n video.src = ${safeJsString(videoSrc)};\n video.dataset.filename = ${safeJsString(videoFilename)};\n video.autoplay = true;\n video.muted = ${media.options.mute === '1' ? 'true' : 'false'};\n video.loop = false;\n video.style.width = '100%';\n video.style.height = '100%';\n video.style.objectFit = 'contain';\n video.style.opacity = '0';\n\n // Retry loading if cache completes while video is playing\n const retryOnCache = (event) => {\n if (event.detail.filename === ${safeJsString(videoFilename)} && video.error) {\n console.log('[Video] Cache complete, reloading:', '${videoFilename}');\n video.load();\n video.play();\n }\n };\n video._retryOnCache = retryOnCache;\n window.addEventListener('media-cached', retryOnCache);\n\n region.innerHTML = '';\n region.appendChild(video);\n\n // Apply transition\n const transIn = ${transIn};\n if (transIn && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n window.Transitions.apply(video, transIn, true, regionRect.width, regionRect.height);\n } else {\n video.style.opacity = '1';\n }\n\n console.log('[Video] Playing:', '${media.options.uri}');\n }`;\n stopFn = `() => {\n const region = document.getElementById('region_${regionId}');\n const video = document.querySelector('#region_${regionId} video');\n if (video) {\n // Remove global media-cached listener to prevent leak\n if (video._retryOnCache) {\n window.removeEventListener('media-cached', video._retryOnCache);\n video._retryOnCache = null;\n }\n const transOut = ${transOut};\n if (transOut && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n const animation = window.Transitions.apply(video, transOut, false, regionRect.width, regionRect.height);\n if (animation) {\n animation.onfinish = () => {\n video.pause();\n video.remove();\n };\n return;\n }\n }\n video.pause();\n video.remove();\n }\n }`;\n break;\n }\n\n case 'text':\n case 'ticker':\n // Text/ticker widgets use the same iframe pattern as default widgets.\n // If no widgetCacheKey, fall through to the default case which handles unsupported types.\n if (media.options.widgetCacheKey) {\n const textUrl = `${window.location.origin}${media.options.widgetCacheKey}`;\n const iframe = this._generateIframeWidgetJS(regionId, media.id, textUrl, transIn, transOut);\n startFn = iframe.startFn;\n stopFn = iframe.stopFn;\n break;\n }\n // Fall through to default (handles missing widgetCacheKey as unsupported)\n\n case 'audio': {\n const audioSrc = `${window.location.origin}${PLAYER_API}/media/${media.options.uri}`;\n const audioId = `audio_${regionId}_${media.id}`;\n const audioLoop = media.options.loop === '1';\n const audioVolume = (parseInt(media.options.volume || '100') / 100).toFixed(2);\n\n startFn = `() => {\n const region = document.getElementById('region_${regionId}');\n\n // Create audio element\n const audio = document.createElement('audio');\n audio.id = '${audioId}';\n audio.className = 'media';\n audio.src = ${safeJsString(audioSrc)};\n audio.autoplay = true;\n audio.loop = ${audioLoop};\n audio.volume = ${audioVolume};\n\n // Create visual feedback container\n const visualContainer = document.createElement('div');\n visualContainer.className = 'audio-visual';\n visualContainer.style.cssText = \\`\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n opacity: 0;\n \\`;\n\n // Audio icon\n const icon = document.createElement('div');\n icon.innerHTML = '♪';\n icon.style.cssText = \\`\n font-size: 120px;\n color: white;\n margin-bottom: 20px;\n animation: pulse 2s ease-in-out infinite;\n \\`;\n\n // Audio info\n const info = document.createElement('div');\n info.style.cssText = \\`\n color: white;\n font-size: 24px;\n text-align: center;\n padding: 0 20px;\n \\`;\n info.textContent = 'Playing Audio';\n\n // Filename\n const filename = document.createElement('div');\n filename.style.cssText = \\`\n color: rgba(255,255,255,0.7);\n font-size: 16px;\n margin-top: 10px;\n \\`;\n filename.textContent = '${media.options.uri}';\n\n visualContainer.appendChild(icon);\n visualContainer.appendChild(info);\n visualContainer.appendChild(filename);\n\n region.innerHTML = '';\n region.appendChild(audio);\n region.appendChild(visualContainer);\n\n // Add pulse animation\n const style = document.createElement('style');\n style.textContent = \\`\n @keyframes pulse {\n 0%, 100% { transform: scale(1); opacity: 1; }\n 50% { transform: scale(1.1); opacity: 0.8; }\n }\n \\`;\n document.head.appendChild(style);\n\n // Apply transition\n const transIn = ${transIn};\n if (transIn && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n window.Transitions.apply(visualContainer, transIn, true, regionRect.width, regionRect.height);\n } else {\n visualContainer.style.opacity = '1';\n }\n\n console.log('[Audio] Playing:', '${audioSrc}', 'Volume:', ${audioVolume}, 'Loop:', ${audioLoop});\n }`;\n\n stopFn = `() => {\n const audio = document.getElementById('${audioId}');\n if (audio) {\n audio.pause();\n audio.remove();\n }\n const region = document.getElementById('region_${regionId}');\n if (region) {\n const visualContainer = region.querySelector('.audio-visual');\n if (visualContainer) {\n const transOut = ${transOut};\n if (transOut && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n const animation = window.Transitions.apply(visualContainer, transOut, false, regionRect.width, regionRect.height);\n if (animation) {\n animation.onfinish = () => visualContainer.remove();\n return;\n }\n }\n visualContainer.remove();\n }\n }\n }`;\n break;\n }\n\n case 'pdf': {\n const pdfSrc = `${window.location.origin}${PLAYER_API}/media/${media.options.uri}`;\n const pdfContainerId = `pdf_${regionId}_${media.id}`;\n const pdfDuration = duration; // Total duration for entire PDF\n\n startFn = `async () => {\n const container = document.createElement('div');\n container.className = 'media pdf-container';\n container.id = '${pdfContainerId}';\n container.style.width = '100%';\n container.style.height = '100%';\n container.style.overflow = 'hidden';\n container.style.backgroundColor = '#525659';\n container.style.opacity = '0';\n container.style.position = 'relative';\n\n const region = document.getElementById('region_${regionId}');\n region.innerHTML = '';\n region.appendChild(container);\n\n // Load PDF.js if not already loaded\n if (typeof pdfjsLib === 'undefined') {\n try {\n const pdfjsModule = await import('pdfjs-dist');\n window.pdfjsLib = pdfjsModule;\n pdfjsLib.GlobalWorkerOptions.workerSrc = '${window.location.origin}/player/pdf.worker.min.mjs';\n } catch (error) {\n console.error('[PDF] Failed to load PDF.js:', error);\n container.innerHTML = '<div style=\"color:white;padding:20px;text-align:center;\">PDF viewer unavailable</div>';\n return;\n }\n }\n\n // Render PDF with multi-page support\n try {\n const loadingTask = pdfjsLib.getDocument(${safeJsString(pdfSrc)});\n const pdf = await loadingTask.promise;\n const totalPages = pdf.numPages;\n\n // Calculate time per page (distribute total duration across all pages)\n const timePerPage = (${pdfDuration} * 1000) / totalPages; // milliseconds per page\n\n console.log(\\`[PDF] Loading: \\${totalPages} pages, \\${timePerPage}ms per page\\`);\n\n const containerWidth = container.offsetWidth || ${width};\n const containerHeight = container.offsetHeight || ${height};\n\n // Create page indicator\n const pageIndicator = document.createElement('div');\n pageIndicator.className = 'pdf-page-indicator';\n pageIndicator.style.cssText = \\`\n position: absolute;\n bottom: 10px;\n right: 10px;\n background: rgba(0,0,0,0.7);\n color: white;\n padding: 8px 12px;\n border-radius: 4px;\n font-size: 14px;\n z-index: 10;\n display: ${isDebug() ? 'block' : 'none'};\n \\`;\n container.appendChild(pageIndicator);\n\n let currentPage = 1;\n let pageTimers = [];\n\n // Function to render a single page\n async function renderPage(pageNum) {\n const page = await pdf.getPage(pageNum);\n const viewport = page.getViewport({ scale: 1 });\n\n // Calculate scale to fit page within container\n const scaleX = containerWidth / viewport.width;\n const scaleY = containerHeight / viewport.height;\n const scale = Math.min(scaleX, scaleY);\n\n const scaledViewport = page.getViewport({ scale });\n\n const canvas = document.createElement('canvas');\n canvas.className = 'pdf-page';\n const context = canvas.getContext('2d');\n canvas.width = scaledViewport.width;\n canvas.height = scaledViewport.height;\n\n // Center canvas in container\n canvas.style.cssText = \\`\n display: block;\n margin: auto;\n margin-top: \\${Math.max(0, (containerHeight - scaledViewport.height) / 2)}px;\n position: absolute;\n top: 0;\n left: 50%;\n transform: translateX(-50%);\n opacity: 0;\n transition: opacity 0.5s ease-in-out;\n \\`;\n\n container.appendChild(canvas);\n\n await page.render({\n canvasContext: context,\n viewport: scaledViewport\n }).promise;\n\n // Fade in new page\n setTimeout(() => canvas.style.opacity = '1', 50);\n\n return canvas;\n }\n\n // Function to cycle through pages\n async function cyclePage() {\n // Update page indicator\n pageIndicator.textContent = \\`Page \\${currentPage} / \\${totalPages}\\`;\n\n // Remove old pages\n const oldPages = container.querySelectorAll('.pdf-page');\n oldPages.forEach(oldPage => {\n if (oldPage !== container.lastChild) {\n oldPage.style.opacity = '0';\n setTimeout(() => oldPage.remove(), 500);\n }\n });\n\n // Render current page\n await renderPage(currentPage);\n\n console.log(\\`[PDF] Showing page \\${currentPage}/\\${totalPages}\\`);\n\n // Schedule next page\n if (totalPages > 1) {\n const timer = setTimeout(() => {\n currentPage = currentPage >= totalPages ? 1 : currentPage + 1;\n cyclePage();\n }, timePerPage);\n pageTimers.push(timer);\n }\n }\n\n // Store live timer array on element for cleanup (not JSON — stays current)\n container._pageTimers = pageTimers;\n\n // Start cycling\n await cyclePage();\n\n // Apply transition to container\n const transIn = ${transIn};\n if (transIn && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n window.Transitions.apply(container, transIn, true, regionRect.width, regionRect.height);\n } else {\n container.style.opacity = '1';\n }\n\n } catch (error) {\n console.error('[PDF] Render failed:', error);\n container.innerHTML = '<div style=\"color:white;padding:20px;text-align:center;\">Failed to load PDF</div>';\n container.style.opacity = '1';\n }\n }`;\n\n stopFn = `() => {\n const region = document.getElementById('region_${regionId}');\n const container = document.getElementById('${pdfContainerId}');\n if (container) {\n // Clear page cycling timers (live array, always current)\n if (container._pageTimers) {\n container._pageTimers.forEach(t => clearTimeout(t));\n container._pageTimers.length = 0;\n }\n\n const transOut = ${transOut};\n if (transOut && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n const animation = window.Transitions.apply(container, transOut, false, regionRect.width, regionRect.height);\n if (animation) {\n animation.onfinish = () => {\n container.remove();\n };\n return;\n }\n }\n container.remove();\n }\n }`;\n break;\n }\n\n case 'webpage': {\n const url = decodeURIComponent(media.options.uri || '');\n startFn = `() => {\n const region = document.getElementById('region_${regionId}');\n const iframe = document.createElement('iframe');\n iframe.src = ${safeJsString(url)};\n iframe.style.opacity = '0';\n region.innerHTML = '';\n region.appendChild(iframe);\n\n // Apply transition after iframe loads\n iframe.onload = () => {\n const transIn = ${transIn};\n if (transIn && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n window.Transitions.apply(iframe, transIn, true, regionRect.width, regionRect.height);\n } else {\n iframe.style.opacity = '1';\n }\n };\n }`;\n break;\n }\n\n default:\n // Widgets (clock, calendar, weather, etc.) - use cache URL pattern in /player/ scope for SW\n // Keep widget iframes alive across duration cycles (arexibo behavior)\n if (media.options.widgetCacheKey) {\n const widgetUrl = `${window.location.origin}${media.options.widgetCacheKey}`;\n const iframe = this._generateIframeWidgetJS(regionId, media.id, widgetUrl, transIn, transOut);\n startFn = iframe.startFn;\n stopFn = iframe.stopFn;\n } else {\n log.warn(`Unsupported media type: ${media.type}`);\n startFn = `() => console.log('Unsupported media type: ${media.type}')`;\n }\n }\n\n return ` {\n start: ${startFn},\n stop: ${stopFn},\n duration: ${duration}\n }`;\n }\n}\n\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n// @xiboplayer/renderer - Layout rendering\nimport pkg from '../package.json' with { type: 'json' };\nexport const VERSION = pkg.version;\nexport { RendererLite } from './renderer-lite.js';\nexport { LayoutPool } from './layout-pool.js';\nexport { LayoutTranslator } from './layout.js';\n"],"file":"src-CKpVxGpH.js"}
|