@xiboplayer/pwa 0.7.12 → 0.7.13
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/{html2canvas-D9YrxbSe.js → html2canvas-BdjGaL7S.js} +2 -2
- package/dist/assets/{html2canvas-D9YrxbSe.js.map → html2canvas-BdjGaL7S.js.map} +1 -1
- package/dist/assets/{main-BytxDgA6.js → main-CT7JIMxf.js} +5 -5
- package/dist/assets/{main-BytxDgA6.js.map → main-CT7JIMxf.js.map} +1 -1
- package/dist/assets/{pdf-Cim3XbjS.js → pdf-ByOGXKA2.js} +2 -2
- package/dist/assets/{pdf-Cim3XbjS.js.map → pdf-ByOGXKA2.js.map} +1 -1
- package/dist/assets/{setup-BFWT01-R.js → setup-tmBa6Ubb.js} +2 -2
- package/dist/assets/{setup-BFWT01-R.js.map → setup-tmBa6Ubb.js.map} +1 -1
- package/dist/assets/{src-wmNLeJQ3.js → src-B9Yas4SH.js} +1 -1
- package/dist/assets/{src-BEkBRqOh.js → src-BfTQUSd6.js} +2 -2
- package/dist/assets/{src-BEkBRqOh.js.map → src-BfTQUSd6.js.map} +1 -1
- package/dist/assets/{src-Cplt1eH9.js → src-BvglFNkK.js} +2 -2
- package/dist/assets/{src-Cplt1eH9.js.map → src-BvglFNkK.js.map} +1 -1
- package/dist/assets/{src-DhbgZf-p.js → src-Ct_7vG0E.js} +1 -1
- package/dist/assets/{src-BCSDJZKF.js → src-Cyk0X_mz.js} +2 -2
- package/dist/assets/{src-BCSDJZKF.js.map → src-Cyk0X_mz.js.map} +1 -1
- package/dist/assets/{src-BgXR6vhE.js → src-DGSLOm9k.js} +2 -2
- package/dist/assets/{src-BgXR6vhE.js.map → src-DGSLOm9k.js.map} +1 -1
- package/dist/assets/{src-BxHs3cOi.js → src-DNqc--R2.js} +2 -2
- package/dist/assets/{src-BxHs3cOi.js.map → src-DNqc--R2.js.map} +1 -1
- package/dist/assets/{src-mns1H4Qq.js → src-DgZHc2Af.js} +2 -2
- package/dist/assets/{src-mns1H4Qq.js.map → src-DgZHc2Af.js.map} +1 -1
- package/dist/assets/{src-CYB7yWaE.js → src-DmdLCQQf.js} +1 -1
- package/dist/assets/{src-B1CMJkFl.js → src-Dr4QlWKq.js} +2 -2
- package/dist/assets/{src-B1CMJkFl.js.map → src-Dr4QlWKq.js.map} +1 -1
- package/dist/assets/{src-CkqT69S8.js → src-DwSNRd1O.js} +2 -2
- package/dist/assets/{src-CkqT69S8.js.map → src-DwSNRd1O.js.map} +1 -1
- package/dist/assets/{sync-manager-Djwpg3Cy.js → sync-manager-zTULh3xK.js} +2 -2
- package/dist/assets/{sync-manager-Djwpg3Cy.js.map → sync-manager-zTULh3xK.js.map} +1 -1
- package/dist/index.html +4 -4
- package/dist/setup.html +6 -6
- package/dist/sw-pwa.js +2 -2
- package/dist/sw-pwa.js.map +1 -1
- package/package.json +13 -13
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"mappings":"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","names":["log","cacheWidgetHtml","VERSION","pkg","E"],"ignoreList":[],"sources":["../../../renderer/src/layout-pool.js","../../../renderer/src/renderer-lite.js","../../../renderer/src/layout.js","../../../renderer/src/index.js","../../../core/src/data-connectors.js","../../../core/src/layout-blacklist.js","../../../core/src/events.js","../../../core/src/player-core.js","../../../core/src/index.js","../../src/download-overlay.ts","../../src/timeline-overlay.ts","../../src/setup-overlay.ts","../../src/main.ts"],"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 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 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 // 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 * 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 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 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 (instant transition).\n * Hides the current layout container and shows the preloaded one,\n * then starts widget cycling and layout timer.\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 // ── 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 preloaded.container.style.visibility = 'visible';\n preloaded.container.style.zIndex = '0';\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 this.log.info(`Swapped to preloaded layout ${layoutId} (instant transition)`);\n this._logResourceStats(layoutId);\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\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 = 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 = '${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 = '${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 = '${videoSrc}';\n video.dataset.filename = '${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 === '${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 = '${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('${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 = '${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","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * DataConnectorManager - Manages real-time data connectors from CMS\n *\n * Data connectors allow widgets to receive real-time data from CMS-configured\n * data sources. The CMS sends data connector configuration via the schedule XML,\n * and this manager periodically polls the data source URLs, stores the data,\n * and emits events so the IC /realtime route can serve it to widgets.\n *\n * Usage:\n * const manager = new DataConnectorManager();\n * manager.setConnectors(schedule.dataConnectors);\n * manager.startPolling();\n *\n * // Get data for a widget\n * const data = manager.getData('weather_data');\n *\n * // Listen for updates\n * manager.on('data-updated', (dataKey, data) => { ... });\n */\n\nimport { EventEmitter, createLogger, fetchWithRetry } from '@xiboplayer/utils';\n\nconst log = createLogger('DataConnector');\n\nconst MAX_BACKOFF_MS = 300000; // 5 minutes\nconst CIRCUIT_BREAKER_THRESHOLD = 3;\n\nexport class DataConnectorManager extends EventEmitter {\n constructor() {\n super();\n\n // dataKey -> { config, data, timer, lastFetch, failures }\n this.connectors = new Map();\n }\n\n /**\n * Set active connectors from schedule\n * Stops any existing polling and reconfigures with new connector list.\n * @param {Array} connectors - Array of connector config objects from schedule XML\n * Each: { id, dataConnectorId, dataKey, url, updateInterval }\n */\n setConnectors(connectors) {\n // Stop existing polling before reconfiguring\n this.stopPolling();\n\n // Clear previous connectors\n this.connectors.clear();\n\n if (!connectors || connectors.length === 0) {\n log.debug('No data connectors configured');\n return;\n }\n\n for (const connector of connectors) {\n if (!connector.dataKey || !connector.url) {\n log.warn('Skipping data connector with missing dataKey or url:', connector);\n continue;\n }\n\n this.connectors.set(connector.dataKey, {\n config: connector,\n data: null,\n timer: null,\n lastFetch: null,\n failures: 0\n });\n\n log.info(`Registered data connector: ${connector.dataKey} (interval: ${connector.updateInterval}s)`);\n }\n\n log.info(`${this.connectors.size} data connector(s) configured`);\n }\n\n /**\n * Start polling for all active connectors\n * Performs an initial fetch immediately, then sets up periodic polling.\n */\n startPolling() {\n for (const [dataKey, entry] of this.connectors.entries()) {\n const { config } = entry;\n const intervalMs = (config.updateInterval || 300) * 1000;\n\n // Fetch immediately on start\n this.fetchData(entry).catch(err => {\n log.error(`Initial fetch failed for ${dataKey}:`, err);\n });\n\n // Set up periodic polling\n entry.timer = setInterval(() => {\n this.fetchData(entry).catch(err => {\n log.error(`Polling fetch failed for ${dataKey}:`, err);\n });\n }, intervalMs);\n\n log.debug(`Started polling for ${dataKey} every ${config.updateInterval}s`);\n }\n }\n\n /**\n * Stop all polling timers\n */\n stopPolling() {\n for (const [dataKey, entry] of this.connectors.entries()) {\n if (entry.timer) {\n clearInterval(entry.timer);\n entry.timer = null;\n log.debug(`Stopped polling for ${dataKey}`);\n }\n }\n }\n\n /**\n * Get current data for a dataKey\n * @param {string} dataKey - The data key to look up\n * @returns {Object|null} The stored data, or null if not available\n */\n getData(dataKey) {\n const entry = this.connectors.get(dataKey);\n if (!entry) {\n log.debug(`No data connector found for key: ${dataKey}`);\n return null;\n }\n return entry.data;\n }\n\n /**\n * Get all data keys that have data available\n * @returns {string[]} Array of data keys with data\n */\n getAvailableKeys() {\n const keys = [];\n for (const [dataKey, entry] of this.connectors.entries()) {\n if (entry.data !== null) {\n keys.push(dataKey);\n }\n }\n return keys;\n }\n\n /**\n * Internal: fetch data from CMS data source\n * @param {Object} entry - Connector entry from this.connectors\n */\n async fetchData(entry) {\n const { config } = entry;\n const { dataKey, url } = config;\n\n log.debug(`Fetching data for ${dataKey}: ${url}`);\n\n try {\n const response = await fetchWithRetry(url, {\n method: 'GET',\n headers: {\n 'Accept': 'application/json'\n }\n }, { maxRetries: 2, baseDelayMs: 2000 });\n\n if (!response.ok) {\n log.warn(`Data connector ${dataKey} returned ${response.status}: ${response.statusText}`);\n return;\n }\n\n const contentType = response.headers.get('Content-Type') || '';\n let data;\n\n if (contentType.includes('application/json')) {\n data = await response.json();\n } else {\n // Store as raw text if not JSON\n data = await response.text();\n }\n\n const previousData = entry.data;\n entry.data = data;\n entry.lastFetch = Date.now();\n entry.failures = 0; // Reset on success\n\n log.debug(`Data updated for ${dataKey} (fetched at ${new Date(entry.lastFetch).toISOString()})`);\n\n // Restore normal polling interval if it was backed off\n this._ensureNormalPolling(entry);\n\n // Emit event for listeners (IC route, platform layer)\n this.emit('data-updated', dataKey, data);\n\n // Emit a specific event if data actually changed\n if (JSON.stringify(previousData) !== JSON.stringify(data)) {\n this.emit('data-changed', dataKey, data);\n }\n\n } catch (error) {\n entry.failures = (entry.failures || 0) + 1;\n log.error(`Failed to fetch data for ${dataKey} (${entry.failures}x):`, error);\n this.emit('fetch-error', dataKey, error);\n\n // Circuit breaker: slow down polling after repeated failures\n if (entry.failures >= CIRCUIT_BREAKER_THRESHOLD && entry.timer) {\n const baseMs = (config.updateInterval || 300) * 1000;\n const backoffMs = Math.min(baseMs * 2 ** (entry.failures - CIRCUIT_BREAKER_THRESHOLD + 1), MAX_BACKOFF_MS);\n clearInterval(entry.timer);\n entry.timer = setTimeout(() => {\n this.fetchData(entry).catch(() => {});\n // Re-arm with backoff interval\n entry.timer = setInterval(() => {\n this.fetchData(entry).catch(() => {});\n }, backoffMs);\n }, backoffMs);\n log.warn(`Circuit breaker: ${dataKey} backing off to ${Math.round(backoffMs / 1000)}s`);\n }\n }\n }\n\n /**\n * Restore normal polling interval after circuit breaker backoff.\n * @private\n */\n _ensureNormalPolling(entry) {\n if (entry.failures === 0 && entry.timer) {\n const baseMs = (entry.config.updateInterval || 300) * 1000;\n // Clear any backed-off timer and restore the normal interval\n clearInterval(entry.timer);\n clearTimeout(entry.timer);\n entry.timer = setInterval(() => {\n this.fetchData(entry).catch(() => {});\n }, baseMs);\n }\n }\n\n /**\n * Force refresh all connectors — re-fetch immediately and restart polling.\n * Called by XMR dataUpdate command.\n */\n refreshAll() {\n if (this.connectors.size === 0) return;\n\n log.info(`Refreshing all ${this.connectors.size} data connector(s)`);\n this.stopPolling();\n this.startPolling();\n }\n\n /**\n * Cleanup - stop all polling and remove listeners\n */\n cleanup() {\n this.stopPolling();\n this.connectors.clear();\n this.removeAllListeners();\n log.debug('DataConnectorManager 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 blacklist — tracks consecutive rendering failures and\n * blacklists layouts that fail repeatedly to prevent crash loops.\n */\n\nimport { createLogger } from '@xiboplayer/utils';\n\nconst log = createLogger('Blacklist');\n\nexport class LayoutBlacklist {\n /**\n * @param {number} [threshold=3] - Consecutive failures before blacklisting\n */\n constructor(threshold = 3) {\n this._entries = new Map();\n this._threshold = threshold;\n }\n\n /**\n * Record a layout rendering failure.\n * @param {number} layoutId\n * @param {string} reason\n * @returns {{ blacklisted: boolean, failures: number }} Current state after recording\n */\n recordFailure(layoutId, reason) {\n const id = Number(layoutId);\n const entry = this._entries.get(id) || { failures: 0, blacklisted: false, reason: '' };\n entry.failures++;\n entry.reason = reason;\n\n if (!entry.blacklisted && entry.failures >= this._threshold) {\n entry.blacklisted = true;\n log.warn(`Layout ${id} blacklisted after ${entry.failures} consecutive failures: ${reason}`);\n } else if (!entry.blacklisted) {\n log.info(`Layout ${id} failure ${entry.failures}/${this._threshold}: ${reason}`);\n }\n\n this._entries.set(id, entry);\n return { blacklisted: entry.blacklisted, failures: entry.failures };\n }\n\n /**\n * Record a successful layout render. Resets failure counter.\n * @param {number} layoutId\n * @returns {boolean} true if the layout was previously blacklisted (now restored)\n */\n recordSuccess(layoutId) {\n const id = Number(layoutId);\n if (!this._entries.has(id)) return false;\n\n const was = this._entries.get(id);\n this._entries.delete(id);\n\n if (was.blacklisted) {\n log.info(`Layout ${id} removed from blacklist (rendered successfully)`);\n return true;\n }\n return false;\n }\n\n /**\n * Check if a layout is currently blacklisted.\n * @param {number} layoutId\n * @returns {boolean}\n */\n isBlacklisted(layoutId) {\n const entry = this._entries.get(Number(layoutId));\n return entry?.blacklisted === true;\n }\n\n /**\n * Get all currently blacklisted layout IDs.\n * @returns {number[]}\n */\n getBlacklistedIds() {\n const result = [];\n for (const [id, entry] of this._entries) {\n if (entry.blacklisted) result.push(id);\n }\n return result;\n }\n\n /**\n * Reset the blacklist. Called when RequiredFiles changes.\n * @returns {number} Number of entries cleared\n */\n reset() {\n const count = this._entries.size;\n if (count > 0) {\n log.info(`Blacklist reset (${count} entries cleared)`);\n this._entries.clear();\n }\n return count;\n }\n\n get size() {\n return this._entries.size;\n }\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Core event name constants — shared between PlayerCore and platform layers.\n * Using constants prevents silent typo bugs at the core/platform boundary.\n */\n\nexport const CORE_EVENTS = Object.freeze({\n // Collection lifecycle\n COLLECTION_START: 'collection-start',\n COLLECTION_COMPLETE: 'collection-complete',\n COLLECTION_ERROR: 'collection-error',\n\n // Registration\n REGISTER_COMPLETE: 'register-complete',\n\n // Schedule\n SCHEDULE_RECEIVED: 'schedule-received',\n LAYOUTS_SCHEDULED: 'layouts-scheduled',\n NO_LAYOUTS_SCHEDULED: 'no-layouts-scheduled',\n TIMELINE_UPDATED: 'timeline-updated',\n\n // Layout lifecycle\n LAYOUT_PREPARE_REQUEST: 'layout-prepare-request',\n LAYOUT_EXPIRE_CURRENT: 'layout-expire-current',\n LAYOUT_ALREADY_PLAYING: 'layout-already-playing',\n CHECK_PENDING_LAYOUT: 'check-pending-layout',\n\n // Downloads\n FILES_RECEIVED: 'files-received',\n DOWNLOAD_REQUEST: 'download-request',\n\n // Overlay\n OVERLAY_LAYOUT_REQUEST: 'overlay-layout-request',\n REVERT_TO_SCHEDULE: 'revert-to-schedule',\n\n // Sync\n SYNC_CONFIG: 'sync-config',\n\n // XMR\n XMR_CONNECTED: 'xmr-connected',\n XMR_RECONNECTED: 'xmr-reconnected',\n XMR_MISCONFIGURED: 'xmr-misconfigured',\n\n // Navigation\n NAVIGATE_TO_WIDGET: 'navigate-to-widget',\n\n // Commands\n EXECUTE_NATIVE_COMMAND: 'execute-native-command',\n SCHEDULED_COMMAND: 'scheduled-command',\n COMMAND_RESULT: 'command-result',\n\n // Screenshots\n SCREENSHOT_REQUEST: 'screenshot-request',\n\n // Stats/Logs/Faults\n SUBMIT_STATS_REQUEST: 'submit-stats-request',\n SUBMIT_LOGS_REQUEST: 'submit-logs-request',\n SUBMIT_FAULTS_REQUEST: 'submit-faults-request',\n\n // Cache\n CACHE_ANALYSIS: 'cache-analysis',\n\n // Collection\n COLLECTION_INTERVAL_SET: 'collection-interval-set',\n COLLECTION_INTERVAL_UPDATED: 'collection-interval-updated',\n\n // Settings\n LOG_LEVEL_CHANGED: 'log-level-changed',\n OFFLINE_MODE: 'offline-mode',\n\n // Purge\n PURGE_REQUEST: 'purge-request',\n PURGE_ALL_REQUEST: 'purge-all-request',\n});\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * PlayerCore - Platform-independent orchestration module\n *\n * Pure orchestration logic without platform-specific concerns (UI, DOM, storage).\n * Can be reused across PWA, Electron, mobile platforms.\n *\n * Architecture:\n * ┌─────────────────────────────────────────────────────┐\n * │ PlayerCore (Pure Orchestration) │\n * │ - Collection cycle coordination │\n * │ - Schedule checking │\n * │ - Layout transition logic │\n * │ - Event emission (not DOM manipulation) │\n * │ - XMDS communication │\n * │ - XMR integration │\n * └─────────────────────────────────────────────────────┘\n * ↓\n * ┌─────────────────────────────────────────────────────┐\n * │ Platform Layer (PWA/Electron/Mobile) │\n * │ - UI updates (status display, progress bars) │\n * │ - DOM manipulation │\n * │ - Platform-specific storage │\n * │ - Blob URL management │\n * │ - Event listeners for PlayerCore events │\n * └─────────────────────────────────────────────────────┘\n *\n * Usage:\n * const core = new PlayerCore({\n * config,\n * xmds,\n * cache,\n * schedule,\n * renderer,\n * xmrWrapper\n * });\n *\n * // Listen to events\n * core.on('collection-start', () => { ... });\n * core.on('layout-ready', (layoutId) => { ... });\n *\n * // Start collection\n * await core.collect();\n */\n\nimport { EventEmitter, createLogger, applyCmsLogLevel, openIDB } from '@xiboplayer/utils';\nimport { calculateTimeline, parseLayoutFile } from '@xiboplayer/schedule';\nimport { CacheAnalyzer } from '@xiboplayer/cache';\nimport { DataConnectorManager } from './data-connectors.js';\nimport { LayoutBlacklist } from './layout-blacklist.js';\nimport { CORE_EVENTS as E } from './events.js';\n\nconst log = createLogger('PlayerCore');\n\n/**\n * Discover a local/LAN IP address.\n * Electron: os.networkInterfaces() via preload (reliable, skips VPN/Docker).\n * Chromium/browser: proxy endpoint GET /system/lan-ip (Node.js has os.networkInterfaces()).\n */\nasync function discoverLanIp() {\n if (typeof window !== 'undefined' && window.electronAPI?.getLanIpAddress) {\n try { return await window.electronAPI.getLanIpAddress(); } catch (_) {}\n }\n // Fallback: ask the proxy server (works in Chromium kiosk and any browser)\n try {\n const fetcher = globalThis.__nativeFetch || globalThis.fetch;\n const res = await fetcher('/system/lan-ip');\n if (res.ok) {\n const { ip } = await res.json();\n if (ip) return ip;\n }\n } catch (_) {}\n return '';\n}\n\n// IndexedDB database/store for offline cache\nconst OFFLINE_DB_BASE = 'xibo-offline-cache';\nconst OFFLINE_DB_VERSION = 1;\nconst OFFLINE_STORE = 'cache';\n\n\n/** Open the offline cache IndexedDB (creates store on first use) */\nfunction openOfflineDb(cmsId) {\n const dbName = cmsId ? `${OFFLINE_DB_BASE}-${cmsId}` : OFFLINE_DB_BASE;\n return openIDB(dbName, OFFLINE_DB_VERSION, OFFLINE_STORE);\n}\n\nexport class PlayerCore extends EventEmitter {\n constructor(options) {\n super();\n\n // Required dependencies (injected)\n this.config = options.config;\n this.xmds = options.xmds;\n this.cache = options.cache;\n this.schedule = options.schedule;\n this.renderer = options.renderer;\n this.XmrWrapper = options.xmrWrapper;\n this.statsCollector = options.statsCollector; // Optional: proof of play tracking\n this.displaySettings = options.displaySettings; // Optional: CMS display settings manager\n\n // CMS ID for namespaced IndexedDB databases\n this._cmsId = options.cmsId || null;\n\n // Data connectors manager (real-time data for widgets)\n this.dataConnectorManager = new DataConnectorManager();\n\n // Discover LAN IP early (async, non-blocking)\n discoverLanIp().then((ip) => {\n this._lanIpAddress = ip;\n log.info('LAN IP:', ip || '(not discovered)');\n });\n\n // State\n this.xmr = null;\n this.currentLayoutId = null;\n this.collecting = false;\n this.collectionInterval = null;\n this.pendingLayouts = new Map(); // layoutId -> required media IDs\n this._layoutMediaStatus = new Map(); // layoutFile → { ready: boolean, missing: string[] }\n this.offlineMode = false; // Track whether we're currently in offline mode\n this._normalCollectInterval = null; // Saved interval to restore after offline retry\n this._offlineRetrySeconds = 0; // Current backoff interval (0 = not retrying)\n\n // CRC32 checksums for skip optimization (avoid redundant XMDS calls)\n this._lastCheckRf = null;\n this._lastCheckSchedule = null;\n\n // Timeline recalculation guard — skip when inputs haven't changed\n this._lastTimelineFingerprint = null;\n this._lastTimeline = null;\n\n // Layout override state (for changeLayout/overlayLayout via XMR → revertToSchedule)\n this._layoutOverride = null; // { layoutId, type: 'change'|'overlay' }\n this._lastRequiredFiles = []; // Track files for MediaInventory\n\n // Scheduled commands tracking (avoid re-executing same command)\n this._executedCommands = new Set();\n\n // Display commands from RegisterDisplay (used by XMR commandAction)\n this.displayCommands = null;\n\n // Fault reporting agent (independent timer, faster than collection cycle)\n this._faultReportingInterval = null;\n this._faultReportingSeconds = 60; // Default: check for faults every 60s\n\n // Unsafe layout blacklist: layoutId → { failures: number, blacklisted: boolean, reason: string }\n this._layoutBlacklist = new LayoutBlacklist(3);\n\n // Status tracking for NotifyStatus enrichment\n this._lastLayoutChangeTime = null; // ISO timestamp of last layout switch\n this._statusCode = 2; // 1=running, 2=downloading, 3=error\n\n // Dynamic layout tracking (useDuration=0 videos — must play to natural end)\n this._dynamicLayouts = new Set();\n\n // Multi-display sync configuration (from RegisterDisplay syncGroup settings)\n this.syncConfig = null;\n this.syncManager = null; // Optional: set via setSyncManager() after RegisterDisplay\n\n // Layout durations for timeline calculation (layoutFile/layoutId → seconds)\n this._layoutDurations = new Map();\n this._finalDurations = new Set(); // layoutFiles whose duration is definitive (all videos probed)\n\n // Guard: layout currently being prepared (async prepareAndRenderLayout in flight)\n this._preparingLayoutId = null;\n\n // Cache analyzer for stale media detection and storage health\n this.cacheAnalyzer = this.cache ? new CacheAnalyzer(this.cache) : null;\n\n // In-memory offline cache (populated from IndexedDB on first load)\n this._offlineCache = { schedule: null, settings: null, requiredFiles: null };\n this._offlineDbReady = this._initOfflineCache();\n }\n\n /** Schedule queue options — avoids repeating this object in 8 call sites */\n get _queueOptions() {\n return { dynamicLayouts: this._dynamicLayouts };\n }\n\n /**\n * Schedule an auto-revert timer for layout/overlay overrides.\n * @param {number} id - Layout ID\n * @param {number} duration - Duration in seconds (0 = no timer)\n * @param {string} label - Description for logging\n */\n _scheduleAutoRevert(id, duration, label) {\n if (duration > 0) {\n setTimeout(() => {\n if (this._layoutOverride?.layoutId === id) {\n log.info(`${label} duration expired (${duration}s), reverting to schedule`);\n this.revertToSchedule();\n }\n }, duration * 1000);\n }\n }\n\n // ── Offline Cache (IndexedDB) ──────────────────────────────────────\n\n /** Load offline cache from IndexedDB into memory on startup */\n async _initOfflineCache() {\n try {\n const db = await openOfflineDb(this._cmsId);\n const tx = db.transaction(OFFLINE_STORE, 'readonly');\n const store = tx.objectStore(OFFLINE_STORE);\n\n const [schedule, settings, requiredFiles, durations, finalDurations, durVersion] = await Promise.all([\n new Promise(r => { const req = store.get('schedule'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n new Promise(r => { const req = store.get('settings'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n new Promise(r => { const req = store.get('requiredFiles'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n new Promise(r => { const req = store.get('durations'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n new Promise(r => { const req = store.get('finalDurations'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n new Promise(r => { const req = store.get('durationsVersion'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n ]);\n\n if (Array.isArray(durations) && durations.length > 0) {\n for (const [k, v] of durations) this._layoutDurations.set(k, v);\n log.info(`[Timeline] Restored ${durations.length} cached durations from IDB`);\n }\n // v2: clear stale final durations from before the fix.\n // Final durations are only valid when set by video metadata / probeLayoutDurations,\n // not by XLF estimates. Old IDB data has 60s defaults marked as final.\n if (durVersion >= 2 && Array.isArray(finalDurations) && finalDurations.length > 0) {\n for (const k of finalDurations) this._finalDurations.add(k);\n log.info(`[Timeline] Restored ${finalDurations.length} final duration keys from IDB`);\n } else if (Array.isArray(finalDurations) && finalDurations.length > 0) {\n log.info(`[Timeline] Discarded ${finalDurations.length} stale final duration keys (pre-v2)`);\n }\n\n this._offlineCache = { schedule, settings, requiredFiles };\n this._offlineDb = db; // Keep handle open for _offlineSave (avoids reopen per write)\n log.info('Offline cache loaded from IndexedDB',\n schedule ? '(has schedule)' : '(empty)');\n } catch (e) {\n log.warn('Failed to load offline cache from IndexedDB:', e);\n }\n }\n\n /** Save a key to both in-memory cache and IndexedDB (fire-and-forget) */\n async _offlineSave(key, data) {\n this._offlineCache[key] = data;\n try {\n // Reuse persistent handle from _initOfflineCache (avoids 6 open/close per cycle)\n if (!this._offlineDb) {\n this._offlineDb = await openOfflineDb(this._cmsId);\n }\n const tx = this._offlineDb.transaction(OFFLINE_STORE, 'readwrite');\n tx.objectStore(OFFLINE_STORE).put(data, key);\n await new Promise((resolve, reject) => {\n tx.oncomplete = resolve;\n tx.onerror = () => reject(tx.error);\n });\n } catch (e) {\n // Handle closed/invalid DB — reopen on next attempt\n this._offlineDb = null;\n log.warn('Failed to save offline cache:', key, e);\n }\n }\n\n /** Check if we have any cached data to fall back on */\n hasCachedData() {\n return this._offlineCache.schedule !== null;\n }\n\n /** Check if the browser reports being offline */\n isOffline() {\n return typeof navigator !== 'undefined' && navigator.onLine === false;\n }\n\n /** Check if currently in offline mode */\n isInOfflineMode() {\n return this.offlineMode;\n }\n\n /**\n * Run an offline collection cycle using cached data.\n * Evaluates the cached schedule and continues playback.\n */\n collectOffline() {\n log.warn('Offline mode — using cached schedule');\n\n if (!this.offlineMode) {\n this.offlineMode = true;\n this.emit(E.OFFLINE_MODE, true);\n }\n\n // Exponential backoff: 30s → 60s → 120s → ... → capped at normal interval\n // Recovers quickly from brief outages but doesn't hammer when truly offline\n if (this.collectionInterval) {\n if (!this._normalCollectInterval) {\n this._normalCollectInterval = this._currentCollectInterval;\n this._offlineRetrySeconds = 30;\n } else {\n // Double the backoff, cap at normal interval\n this._offlineRetrySeconds = Math.min(\n this._offlineRetrySeconds * 2,\n this._normalCollectInterval\n );\n }\n this._setCollectionTimer(this._offlineRetrySeconds);\n log.info(`Offline: retry in ${this._offlineRetrySeconds}s`);\n }\n\n // Load cached settings for collection interval (first run only)\n if (!this.collectionInterval) {\n const cachedReg = this._offlineCache.settings;\n if (cachedReg?.settings) {\n this.setupCollectionInterval(cachedReg.settings);\n this._normalCollectInterval = this._currentCollectInterval;\n this._offlineRetrySeconds = 30;\n this._setCollectionTimer(this._offlineRetrySeconds);\n log.info(`Offline: retry in ${this._offlineRetrySeconds}s`);\n }\n }\n\n // Load cached schedule and apply it\n const cachedSchedule = this._offlineCache.schedule;\n if (cachedSchedule) {\n this.schedule.setSchedule(cachedSchedule);\n this.emit(E.SCHEDULE_RECEIVED, cachedSchedule);\n }\n\n // Evaluate current schedule\n const layoutFiles = this.schedule.getCurrentLayouts();\n log.info('Offline layouts:', layoutFiles);\n this.emit(E.LAYOUTS_SCHEDULED, layoutFiles);\n\n this._evaluateAndSwitchLayout(layoutFiles, 'Offline');\n\n this.emit(E.COLLECTION_COMPLETE);\n }\n\n /**\n * Evaluate the current schedule and switch layouts if needed.\n * Shared by both collect() and collectOffline() after emitting 'layouts-scheduled'.\n * @param {string[]} layoutFiles - Currently scheduled layout filenames\n * @param {string} context - Log context label (e.g. 'Offline' or '')\n */\n _evaluateAndSwitchLayout(layoutFiles, context) {\n const prefix = context ? `${context}: ` : '';\n\n // Use the queue (not raw layoutFiles) for play/expire decisions.\n // The queue has all constraints baked in (maxPlaysPerHour, priorities, dayparting).\n // The player is a dumb consumer — it only expires when the queue rebuilds\n // with a different layout set (new CMS schedule, daypart boundary crossed).\n const { queue } = this.schedule.getScheduleQueue(this._layoutDurations, this._queueOptions);\n\n if (queue.length > 0) {\n if (this.currentLayoutId) {\n const stillInQueue = queue.some(e => parseLayoutFile(e.layoutId) === this.currentLayoutId);\n\n if (!stillInQueue) {\n // Schedule changed and current layout is no longer in the queue — expire immediately.\n // Clear currentLayoutId and emit expire event so the renderer can teardown.\n // The renderer's layoutEnd → advanceToNextLayout flow handles the switch.\n log.info(`Layout ${this.currentLayoutId} no longer in queue — expiring`);\n this.currentLayoutId = null;\n this.emit(E.LAYOUT_EXPIRE_CURRENT);\n } else {\n // Layout is still in queue — don't interrupt, just rebuild queue in background.\n // The playing layout ends when its timer fires (layoutEnd event),\n // at which point advanceToNextLayout() pops from the already-updated queue.\n log.info(`Layout ${this.currentLayoutId} playing — queue updated in background, playback continues`);\n this.emit(E.LAYOUT_ALREADY_PLAYING, this.currentLayoutId);\n }\n } else if (!this._preparingLayoutId) {\n // No layout playing or being prepared — start one from the queue.\n // Guard with _preparingLayoutId to prevent a second _evaluateAndSwitchLayout\n // call (e.g. offline-restore then online-collect) from popping another layout\n // before the async prepareAndRenderLayout completes.\n const next = this.getNextLayout();\n if (next) {\n this._preparingLayoutId = next.layoutId;\n log.info(`${prefix}switching to layout ${next.layoutId}`);\n this.emit(E.LAYOUT_PREPARE_REQUEST, next.layoutId);\n }\n } else {\n log.info(`${prefix}layout ${this._preparingLayoutId} already being prepared, skipping`);\n }\n } else {\n log.info(`${context ? `${context}: n` : 'N'}o layouts${context ? ' in cached schedule' : ' scheduled, falling back to default'}`);\n this.emit(E.NO_LAYOUTS_SCHEDULED);\n }\n\n this.logUpcomingTimeline();\n }\n\n /**\n * Force an immediate collection (used by platform layer on 'online' event)\n */\n async collectNow() {\n this._lastCheckRf = null;\n this._lastCheckSchedule = null;\n return this.collect();\n }\n\n /**\n * Start collection cycle\n * Pure orchestration - emits events instead of updating UI\n */\n async collect() {\n // Prevent concurrent collections\n if (this.collecting) {\n log.debug('Collection already in progress, skipping');\n return;\n }\n\n this.collecting = true;\n\n try {\n // Ensure offline cache is loaded from IndexedDB before checking\n await this._offlineDbReady;\n\n log.info('Starting collection cycle...');\n this.emit(E.COLLECTION_START);\n\n // Check if browser reports offline\n if (this.isOffline()) {\n if (this.hasCachedData()) {\n this.collecting = false;\n return this.collectOffline();\n }\n throw new Error('Offline with no cached data — cannot start playback');\n }\n\n // Ensure RSA key pair exists before registering\n if (this.config.ensureXmrKeyPair) {\n await this.config.ensureXmrKeyPair();\n }\n\n // Register display\n log.debug('Collection step: registerDisplay');\n const regResult = await this.xmds.registerDisplay();\n log.info(`Display registered: ${regResult.code}${regResult.tags?.length ? `, tags: ${regResult.tags.join(', ')}` : ''}`);\n log.debug('Register result:', JSON.stringify(regResult));\n\n this._processRegistration(regResult);\n\n // Initialize XMR if available\n log.debug('Collection step: initializeXmr');\n await this.initializeXmr(regResult);\n\n // CRC32 skip optimization: only fetch RequiredFiles/Schedule when CMS data changed\n const checkRf = regResult.checkRf || '';\n const checkSchedule = regResult.checkSchedule || '';\n\n // Get required files (skip if CRC unchanged)\n if (!this._lastCheckRf || this._lastCheckRf !== checkRf) {\n // RequiredFiles changed — CMS may have fixed broken layouts\n this.resetBlacklist();\n\n log.debug('Collection step: requiredFiles');\n const rfResult = await this.xmds.requiredFiles();\n // RequiredFiles returns { files, purge } — files to download, items to delete\n const files = rfResult.files || rfResult;\n const purgeItems = rfResult.purge || [];\n log.info('Required files:', files.length, purgeItems.length > 0 ? `(+ ${purgeItems.length} purge)` : '');\n this._lastCheckRf = checkRf;\n this.emit(E.FILES_RECEIVED, files);\n\n // Cache required files for offline use\n this._offlineSave('requiredFiles', rfResult);\n\n if (purgeItems.length > 0) {\n this.emit(E.PURGE_REQUEST, purgeItems);\n }\n\n // Get schedule (skip if CRC unchanged)\n if (!this._lastCheckSchedule || this._lastCheckSchedule !== checkSchedule) {\n log.debug('Collection step: schedule');\n const schedule = await this.xmds.schedule();\n log.info('Schedule received');\n this._lastCheckSchedule = checkSchedule;\n log.debug('Collection step: processing schedule');\n this._applyNewSchedule(schedule);\n this.logUpcomingTimeline();\n }\n\n log.debug('Collection step: download-request + mediaInventory');\n const currentLayouts = this.schedule.getCurrentLayouts();\n\n // Layout IDs in playback order (from the pre-calculated queue)\n const { queue } = this.schedule.getScheduleQueue(\n this._layoutDurations,\n this._queueOptions\n );\n const layoutOrder = [...new Set(queue.map(e => parseLayoutFile(e.layoutId)))];\n\n this._lastRequiredFiles = files;\n\n // Download window enforcement (#81) — skip downloads outside configured window\n if (this.displaySettings?.isInDownloadWindow && !this.displaySettings.isInDownloadWindow()) {\n const nextWindow = this.displaySettings.getNextDownloadWindow?.();\n log.info(`Outside download window, skipping downloads${nextWindow ? ` (next: ${nextWindow.toLocaleTimeString()})` : ''}`);\n } else {\n this.emit(E.DOWNLOAD_REQUEST, { layoutOrder, files, layoutDependants: Object.fromEntries(this.schedule.getDependantsMap()) });\n }\n\n // Non-blocking cache analysis (stale media detection)\n if (this.cacheAnalyzer) {\n this.cacheAnalyzer.analyze(files).then(report => {\n this.emit(E.CACHE_ANALYSIS, report);\n }).catch(err => log.warn('Cache analysis failed:', err));\n }\n\n // Submit media inventory to CMS (reports cached files)\n this.submitMediaInventory(files);\n } else {\n if (checkRf) {\n log.info('RequiredFiles CRC unchanged, skipping download check');\n }\n if (this._lastCheckSchedule !== checkSchedule) {\n const schedule = await this.xmds.schedule();\n log.info('Schedule received (RF unchanged but schedule changed)');\n this._lastCheckSchedule = checkSchedule;\n this._applyNewSchedule(schedule);\n } else if (checkSchedule) {\n log.info('Schedule CRC unchanged, skipping');\n }\n }\n\n // Fetch weather data for schedule criteria evaluation (#15)\n await this._fetchWeatherData();\n\n log.debug('Collection step: evaluateSchedule');\n // Evaluate current schedule\n const layoutFiles = this.schedule.getCurrentLayouts();\n log.info('Current layouts:', layoutFiles);\n this.emit(E.LAYOUTS_SCHEDULED, layoutFiles);\n\n this._evaluateAndSwitchLayout(layoutFiles, '');\n\n // Process scheduled commands (auto-execute commands whose time has arrived)\n this._processScheduledCommands();\n\n // Submit stats if enabled and collector is available\n if (regResult.settings?.statsEnabled === 'On' || regResult.settings?.statsEnabled === '1') {\n if (this.statsCollector) {\n log.info('Stats enabled, submitting proof of play');\n this.emit(E.SUBMIT_STATS_REQUEST);\n } else {\n log.warn('Stats enabled but no StatsCollector provided');\n }\n }\n\n // Submit logs to CMS (always, regardless of stats setting)\n this.emit(E.SUBMIT_LOGS_REQUEST);\n\n // Submit faults immediately (higher priority than logs)\n this.emit(E.SUBMIT_FAULTS_REQUEST);\n\n // Setup collection interval on first run\n if (!this.collectionInterval && regResult.settings) {\n this.setupCollectionInterval(regResult.settings);\n }\n\n // Start fault reporting agent (independent of collection cycle)\n if (!this._faultReportingInterval) {\n this._startFaultReportingAgent();\n }\n\n // Recalculate timeline after every collection cycle completes,\n // even if schedule CRC was unchanged — durations or time may have shifted.\n this.logUpcomingTimeline();\n\n this.emit(E.COLLECTION_COMPLETE);\n\n } catch (error) {\n // Offline fallback: if network failed but we have cached data, use it\n if (this.hasCachedData()) {\n log.warn('Collection failed, falling back to cached data:', error?.message || error);\n this.emit(E.COLLECTION_ERROR, error);\n this.collecting = false;\n return this.collectOffline();\n }\n\n log.error('Collection error:', error);\n this.emit(E.COLLECTION_ERROR, error);\n throw error;\n } finally {\n this.collecting = false;\n }\n }\n\n /**\n * Process registration result — offline exit, settings, sync config, tags, commands.\n */\n _processRegistration(regResult) {\n // Cache settings for offline use\n this._offlineSave('settings', regResult);\n\n // Exit offline mode if we were in it\n if (this.offlineMode) {\n this.offlineMode = false;\n log.info('Back online — resuming normal collection');\n this.emit(E.OFFLINE_MODE, false);\n\n // Restore normal collection interval (was shortened for offline retry)\n if (this._normalCollectInterval) {\n this._setCollectionTimer(this._normalCollectInterval);\n this._normalCollectInterval = null;\n this._offlineRetrySeconds = 0;\n }\n }\n\n // Apply display settings if DisplaySettings manager is available\n if (this.displaySettings && regResult.settings) {\n const result = this.displaySettings.applySettings(regResult.settings);\n if (result.changed.includes('collectInterval')) {\n this.updateCollectionInterval(result.settings.collectInterval);\n }\n\n // Apply CMS logLevel (respects local overrides)\n if (regResult.settings.logLevel) {\n const applied = applyCmsLogLevel(regResult.settings.logLevel);\n if (applied) {\n log.info('Log level updated from CMS:', regResult.settings.logLevel);\n this.emit(E.LOG_LEVEL_CHANGED, regResult.settings.logLevel);\n }\n }\n }\n\n // Pass display properties to schedule for criteria evaluation\n if (this.schedule?.setDisplayProperties && regResult.settings) {\n this.schedule.setDisplayProperties(regResult.settings);\n }\n\n // Store sync config if display is in a sync group — only emit if CMS config changed\n // (compare raw CMS response, not the mutated config with relayUrl/syncGroupId added by PWA)\n if (regResult.syncConfig) {\n const rawKey = JSON.stringify(regResult.syncConfig);\n if (rawKey !== this._lastRawSyncConfig) {\n this._lastRawSyncConfig = rawKey;\n this.syncConfig = regResult.syncConfig;\n log.info('Sync group:', regResult.syncConfig.isLead ? 'LEAD' : `follower → ${regResult.syncConfig.syncGroup}`,\n `(switchDelay: ${regResult.syncConfig.syncSwitchDelay}ms, videoPauseDelay: ${regResult.syncConfig.syncVideoPauseDelay}ms)`);\n this.emit(E.SYNC_CONFIG, regResult.syncConfig);\n }\n }\n\n // Extract config from display tags (key|value convention)\n this._applyTagConfig(regResult.tags);\n\n // Store display commands for XMR commandAction resolution\n if (regResult.commands && regResult.commands.length > 0) {\n this.displayCommands = {};\n for (const cmd of regResult.commands) {\n this.displayCommands[cmd.commandCode] = cmd;\n }\n log.debug('Display commands:', Object.keys(this.displayCommands).join(', '));\n }\n\n this.emit(E.REGISTER_COMPLETE, regResult);\n }\n\n /**\n * Apply a new schedule from CMS — emit event, update schedule manager,\n * reset executed commands, refresh data connectors, and cache offline.\n */\n _applyNewSchedule(schedule) {\n this.emit(E.SCHEDULE_RECEIVED, schedule);\n this.schedule.setSchedule(schedule);\n this._executedCommands.clear();\n this.updateDataConnectors();\n this._offlineSave('schedule', schedule);\n }\n\n /**\n * Initialize XMR WebSocket connection\n */\n async initializeXmr(regResult) {\n const xmrUrl = regResult.settings?.xmrWebSocketAddress || regResult.settings?.xmrNetworkAddress;\n if (!xmrUrl) {\n log.warn('XMR not configured: no xmrWebSocketAddress or xmrNetworkAddress in CMS settings');\n this.emit(E.XMR_MISCONFIGURED, {\n reason: 'missing',\n message: 'XMR address not configured in CMS. Go to CMS Admin → Settings → Configuration → XMR and set the WebSocket address.',\n });\n return;\n }\n\n // Validate URL protocol — PWA players need ws:// or wss://, not tcp://\n if (xmrUrl.startsWith('tcp://')) {\n log.warn(`XMR address uses tcp:// protocol which is not supported by PWA players: ${xmrUrl}`);\n log.warn('Configure XMR_WS_ADDRESS in CMS Admin → Settings → Configuration → XMR (e.g. wss://your-domain/xmr)');\n this.emit(E.XMR_MISCONFIGURED, {\n reason: 'wrong-protocol',\n url: xmrUrl,\n message: `XMR uses tcp:// protocol (not supported by PWA). Set XMR WebSocket Address to wss://your-domain/xmr in CMS Settings.`,\n });\n return;\n }\n\n // Detect placeholder/example URLs\n if (/example\\.(org|com|net)/i.test(xmrUrl)) {\n log.warn(`XMR address contains placeholder domain: ${xmrUrl}`);\n log.warn('Configure the real XMR address in CMS Admin → Settings → Configuration → XMR');\n this.emit(E.XMR_MISCONFIGURED, {\n reason: 'placeholder',\n url: xmrUrl,\n message: `XMR address is still the default placeholder (${xmrUrl}). Update it in CMS Settings.`,\n });\n return;\n }\n\n const xmrCmsKey = regResult.settings?.xmrCmsKey || regResult.settings?.serverKey || this.config.serverKey;\n log.debug('XMR CMS Key:', xmrCmsKey ? 'present' : 'missing');\n\n if (!this.xmr) {\n log.info('Initializing XMR WebSocket:', xmrUrl);\n this.xmr = new this.XmrWrapper(this.config, this);\n await this.xmr.start(xmrUrl, xmrCmsKey);\n this.emit(E.XMR_CONNECTED, xmrUrl);\n } else if (!this.xmr.isConnected()) {\n log.info('XMR disconnected, attempting to reconnect...');\n await this.xmr.start(xmrUrl, xmrCmsKey);\n this.emit(E.XMR_RECONNECTED, xmrUrl);\n } else {\n log.debug('XMR already connected');\n }\n }\n\n /**\n * Setup collection interval\n */\n setupCollectionInterval(settings) {\n // Use DisplaySettings if available, otherwise fallback to raw settings\n const collectIntervalSeconds = this.displaySettings\n ? this.displaySettings.getCollectInterval()\n : parseInt(settings.collectInterval || '300', 10);\n\n this._setCollectionTimer(collectIntervalSeconds);\n this.emit(E.COLLECTION_INTERVAL_SET, collectIntervalSeconds);\n }\n\n /**\n * Update collection interval dynamically\n * Called when CMS changes the collection interval\n */\n updateCollectionInterval(newIntervalSeconds) {\n if (this.collectionInterval) {\n this._setCollectionTimer(newIntervalSeconds);\n this.emit(E.COLLECTION_INTERVAL_UPDATED, newIntervalSeconds);\n }\n }\n\n /**\n * Start the fault reporting agent.\n * Runs on an independent timer (default 60s) to submit faults faster\n * than the normal collection cycle (300s). This ensures the CMS dashboard\n * gets fault alerts with lower latency.\n */\n _startFaultReportingAgent() {\n if (this._faultReportingInterval) clearInterval(this._faultReportingInterval);\n\n log.info(`Fault reporting agent started (interval: ${this._faultReportingSeconds}s)`);\n this._faultReportingInterval = setInterval(() => {\n this.emit(E.SUBMIT_FAULTS_REQUEST);\n }, this._faultReportingSeconds * 1000);\n }\n\n /** Internal: (re)create the collection setInterval timer */\n _setCollectionTimer(seconds) {\n if (this.collectionInterval) clearInterval(this.collectionInterval);\n this._currentCollectInterval = seconds;\n log.info(`Collection interval: ${seconds}s`);\n this.collectionInterval = setInterval(() => {\n log.debug('Running scheduled collection cycle...');\n this.collect().catch(error => {\n log.error('Collection error:', error);\n this.emit(E.COLLECTION_ERROR, error);\n });\n }, seconds * 1000);\n }\n\n /**\n * Request layout change (called by XMR or schedule)\n * Pure orchestration - emits events for platform to handle\n */\n async requestLayoutChange(layoutId) {\n log.info(`Layout change requested: ${layoutId}`);\n\n // Clear current layout tracking so it will switch\n this.currentLayoutId = null;\n\n this.emit('layout-change-requested', layoutId);\n }\n\n /**\n * Mark layout as ready and current\n * Called by platform after it successfully renders the layout\n */\n /**\n * Clear the preparing-layout guard.\n * Called by platform layer when preparation is cancelled or skipped.\n */\n clearPreparingLayout() {\n this._preparingLayoutId = null;\n }\n\n setCurrentLayout(layoutId) {\n this.currentLayoutId = layoutId;\n this._preparingLayoutId = null;\n this._lastLayoutChangeTime = new Date().toISOString();\n this._statusCode = 1; // Running\n this.pendingLayouts.delete(layoutId);\n // Layout proved playable — clear media status (no longer missing)\n this._layoutMediaStatus.delete(`${layoutId}.xlf`);\n this.emit('layout-current', layoutId);\n // Force timeline recalc on layout change (fingerprint reset)\n this._lastTimelineFingerprint = null;\n this.logUpcomingTimeline();\n }\n\n /**\n * Mark layout as pending (waiting for media)\n * Called by platform when layout needs media downloads\n */\n setPendingLayout(layoutId, requiredMediaIds) {\n this.pendingLayouts.set(layoutId, requiredMediaIds);\n this.emit('layout-pending', layoutId, requiredMediaIds);\n }\n\n /**\n * Clear current layout (for replay)\n * Called by platform when layout ends\n */\n clearCurrentLayout() {\n this.currentLayoutId = null;\n this.emit('layout-cleared');\n }\n\n /**\n * Get the next layout from the pre-calculated schedule queue.\n * Pops the next entry, skipping blacklisted layouts.\n * Returns { layoutId, layoutFile } or null.\n */\n getNextLayout() {\n const entry = this.schedule.popNextFromQueue(\n this._layoutDurations,\n this._queueOptions\n );\n\n if (!entry) {\n // No queue entries — try default\n const defaultFile = this.schedule.schedule?.default;\n if (defaultFile) {\n const layoutId = parseLayoutFile(defaultFile);\n return { layoutId, layoutFile: defaultFile };\n }\n return null;\n }\n\n const layoutId = parseLayoutFile(entry.layoutId);\n\n if (this.isLayoutBlacklisted(layoutId)) {\n // Try next entries (up to queue length) to find a non-blacklisted one\n const { queue } = this.schedule.getScheduleQueue(\n this._layoutDurations,\n this._queueOptions\n );\n for (let i = 0; i < queue.length - 1; i++) {\n const next = this.schedule.popNextFromQueue(\n this._layoutDurations,\n this._queueOptions\n );\n if (next) {\n const nextId = parseLayoutFile(next.layoutId);\n if (!this.isLayoutBlacklisted(nextId)) {\n return { layoutId: nextId, layoutFile: next.layoutId };\n }\n }\n }\n // All blacklisted — return this one anyway to avoid blank screen\n log.warn('All queued layouts are blacklisted, using current entry as fallback');\n }\n\n return { layoutId, layoutFile: entry.layoutId };\n }\n\n /**\n * Peek at the next layout in the schedule queue without advancing.\n * Used by the preload system to know which layout to pre-build.\n * Returns { layoutId, layoutFile } or null if no next layout or same as current.\n */\n peekNextLayout() {\n const entry = this.schedule.peekNextInQueue(\n this._layoutDurations,\n this._queueOptions\n );\n\n if (!entry) return null;\n\n const layoutId = parseLayoutFile(entry.layoutId);\n\n // Don't preload if it's the same as current\n if (layoutId === this.currentLayoutId) {\n // Try the one after that\n const after = this.schedule.peekAfterNext(\n this._layoutDurations,\n this._queueOptions\n );\n if (!after) return null;\n const afterId = parseLayoutFile(after.layoutId);\n if (afterId === this.currentLayoutId || this.isLayoutBlacklisted(afterId)) return null;\n return { layoutId: afterId, layoutFile: after.layoutId };\n }\n\n if (this.isLayoutBlacklisted(layoutId)) return null;\n\n return { layoutId, layoutFile: entry.layoutId };\n }\n\n /**\n * Advance to the next layout in the pre-calculated schedule queue.\n * Called by platform layer when a layout finishes (layoutEnd event).\n * Pops the next entry from the queue and emits layout-prepare-request.\n */\n advanceToNextLayout() {\n // Don't cycle if we're in a layout override (XMR changeLayout/overlayLayout)\n if (this._layoutOverride) {\n log.info('Layout override active, not advancing schedule');\n return;\n }\n\n const next = this.getNextLayout();\n\n // ── Never-stop guarantee ────────────────────────────────────────\n if (!next) {\n if (this.currentLayoutId) {\n log.info(`No layouts in queue, replaying ${this.currentLayoutId} to avoid blank screen`);\n const replayId = this.currentLayoutId;\n this.currentLayoutId = null;\n this._preparingLayoutId = replayId;\n this.emit(E.LAYOUT_PREPARE_REQUEST, replayId);\n } else {\n log.info('No layouts scheduled during advance');\n this.emit(E.NO_LAYOUTS_SCHEDULED);\n }\n return;\n }\n\n const { layoutId, layoutFile } = next;\n const dur = this._layoutDurations.get(layoutFile) || '?';\n\n // Debug: log incoming layout vs timeline overlay top entries\n if (this._lastTimeline && this._lastTimeline.length > 0) {\n const top2 = this._lastTimeline.slice(0, 2).map(e => {\n const t = e.startTime.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });\n return `${e.layoutFile}(${e.duration}s@${t})`;\n });\n log.debug(`[Timeline] Layout transition: entering ${layoutFile} (${dur}s), overlay top: [${top2.join(', ')}]`);\n\n // Warn if the entering layout doesn't match the first timeline entry\n if (this._lastTimeline[0].layoutFile !== layoutFile) {\n log.warn(`[Timeline] Mismatch: entering ${layoutFile} but overlay expects ${this._lastTimeline[0].layoutFile}`);\n }\n } else {\n log.debug(`[Timeline] Layout transition: entering ${layoutFile} (${dur}s), no timeline data`);\n }\n\n // Multi-display sync: if this is a sync event and we have a SyncManager,\n // delegate layout transitions to the sync protocol\n if (this.syncManager && this.schedule.isSyncEvent(layoutFile)) {\n if (this.isSyncLead()) {\n log.info(`[Sync] Lead requesting coordinated layout change: ${layoutId}`);\n // Lead must render the layout itself (not just coordinate followers).\n // Emit layout-prepare-request so the renderer builds it, while\n // requestLayoutChange coordinates the show timing with followers.\n this._preparingLayoutId = layoutId;\n this.emit(E.LAYOUT_PREPARE_REQUEST, layoutId);\n this.syncManager.requestLayoutChange(layoutId).catch(err => {\n log.error('[Sync] Layout change failed:', err);\n });\n return;\n } else if (this.syncManager.transport?.connected) {\n log.info(`[Sync] Follower waiting for lead signal (not advancing independently)`);\n return;\n } else {\n log.warn(`[Sync] Follower: lead unreachable, advancing independently`);\n }\n }\n\n if (layoutId === this.currentLayoutId) {\n log.info(`Next layout ${layoutId} is same as current, triggering replay`);\n this.currentLayoutId = null;\n }\n\n const { queue } = this.schedule.getScheduleQueue(\n this._layoutDurations,\n this._queueOptions\n );\n const pos = this.schedule.getQueuePosition();\n log.info(`Advancing to layout ${layoutId} (queue pos ${pos}/${queue.length})`);\n\n // Set _preparingLayoutId BEFORE emitting to prevent collect() cycles\n // from seeing both currentLayoutId=null and _preparingLayoutId=null\n // and popping another layout from the queue (double-pop race).\n this._preparingLayoutId = layoutId;\n this.emit(E.LAYOUT_PREPARE_REQUEST, layoutId);\n }\n\n /**\n * Go back to the previous layout in the schedule queue (wraps around).\n * Called by platform layer in response to manual navigation (keyboard/remote).\n * Skips sync-manager logic — manual navigation is local only.\n */\n advanceToPreviousLayout() {\n if (this._layoutOverride) {\n log.info('Layout override active, not going back');\n return;\n }\n\n const { queue } = this.schedule.getScheduleQueue(\n this._layoutDurations,\n this._queueOptions\n );\n if (queue.length <= 1) {\n log.info('Single or empty queue, nothing to go back to');\n return;\n }\n\n // Go back 2 positions (current was already popped, so -2 from current pos)\n const entry = this.schedule.rewindQueue(2, this._layoutDurations, this._queueOptions);\n if (!entry) return;\n\n const layoutId = parseLayoutFile(entry.layoutId);\n\n if (layoutId === this.currentLayoutId) {\n log.info('Previous layout is same as current, nothing to go back to');\n return;\n }\n\n log.info(`Going back to layout ${layoutId}`);\n this.emit(E.LAYOUT_PREPARE_REQUEST, layoutId);\n }\n\n /**\n * Notify that a file is ready (called by platform for both layout and media files)\n * Checks if any pending layouts can now be rendered\n */\n notifyMediaReady(fileId, fileType = 'media') {\n log.debug(`File ${fileId} ready (${fileType})`);\n\n // Check if any pending layouts are now complete\n for (const [layoutId, requiredFiles] of this.pendingLayouts.entries()) {\n // Check if this file is needed by this layout\n // For layout files: match layout ID with file ID (layout 78 needs layout/78)\n // For media files: check if fileId is in requiredFiles array\n const isLayoutFile = fileType === 'layout' && layoutId === parseInt(fileId);\n const isRequiredMedia = fileType === 'media' && requiredFiles.includes(fileId);\n\n if (isLayoutFile || isRequiredMedia) {\n log.debug(`${fileType} ${fileId} was needed by pending layout ${layoutId}, checking if ready...`);\n this.emit(E.CHECK_PENDING_LAYOUT, layoutId, requiredFiles);\n }\n }\n }\n\n /**\n * Notify layout status to CMS\n */\n async notifyLayoutStatus(layoutId) {\n try {\n const status = {\n currentLayoutId: layoutId,\n deviceName: this.config?.displayName || '',\n displayName: this.config?.displayName || '',\n lastCommandSuccess: this._lastCommandSuccess ?? true,\n code: this._statusCode,\n lastLayoutChangeTime: this._lastLayoutChangeTime || new Date().toISOString(),\n };\n\n // Add geo-location if available\n if (this.config?.latitude) status.latitude = this.config.latitude;\n if (this.config?.longitude) status.longitude = this.config.longitude;\n\n // Report LAN IP so CMS can tell sync followers where the lead is\n if (this._lanIpAddress) status.lanIpAddress = this._lanIpAddress;\n\n await this.xmds.notifyStatus(status);\n this.emit('status-notified', layoutId);\n } catch (error) {\n log.warn('Failed to notify status:', error);\n this.emit('status-notify-failed', layoutId, error);\n }\n }\n\n /**\n * Report geo location (called by XMR when CMS pushes coordinates)\n * Updates schedule location for geo-fencing and triggers schedule re-evaluation.\n * @param {Object} data - { latitude, longitude }\n */\n reportGeoLocation(data) {\n const lat = parseFloat(data?.latitude);\n const lng = parseFloat(data?.longitude);\n\n if (isNaN(lat) || isNaN(lng)) {\n log.warn('reportGeoLocation: invalid coordinates', data);\n return;\n }\n\n log.info(`Geo location from CMS: ${lat.toFixed(4)}, ${lng.toFixed(4)}`);\n\n if (this.schedule?.setLocation) {\n this.schedule.setLocation(lat, lng);\n }\n\n this.emit('location-updated', { latitude: lat, longitude: lng, source: 'cms' });\n this.checkSchedule();\n }\n\n /**\n * Request geo location using a fallback chain:\n * 1. Browser Geolocation API (GPS / OS-level)\n * 2. Google Geolocation API (if GOOGLE_GEO_API_KEY is configured)\n * 3. IP-based geolocation (free, no key required)\n * @returns {Promise<{latitude: number, longitude: number}|null>}\n */\n async requestGeoLocation() {\n // Return cached location if still fresh (re-resolve every 30 minutes)\n const GEO_CACHE_MS = 30 * 60 * 1000;\n if (this._geoCache && (Date.now() - this._geoCache.ts) < GEO_CACHE_MS) {\n return this._geoCache.location;\n }\n\n // Try browser geolocation (works with GPS or Google API key baked into Chromium).\n // Skip if it already failed — Electron without a Google API key will never succeed.\n if (!this._browserGeoFailed) {\n const browser = await this._tryBrowserGeolocation();\n if (browser) {\n return this._cacheGeo(this._applyLocation(browser.latitude, browser.longitude, 'browser'));\n }\n this._browserGeoFailed = true;\n }\n\n // Try Google Geolocation API if key is configured\n const apiKey = this.config?.googleGeoApiKey;\n if (apiKey) {\n const google = await this._tryGoogleGeolocation(apiKey);\n if (google) {\n return this._cacheGeo(this._applyLocation(google.latitude, google.longitude, 'google-api'));\n }\n }\n\n // Fall back to IP-based geolocation (free, no key)\n const ip = await this._tryIpGeolocation();\n if (ip) {\n return this._cacheGeo(this._applyLocation(ip.latitude, ip.longitude, 'ip-geolocation'));\n }\n\n log.warn('All geolocation methods failed');\n return null;\n }\n\n /** Cache a resolved geolocation result. @private */\n _cacheGeo(location) {\n this._geoCache = { location, ts: Date.now() };\n return location;\n }\n\n /**\n * Extract config values from CMS display tags using key|value convention.\n * Tags like \"geoApiKey|AIzaSy...\" are parsed and applied to player config.\n * @param {string[]} tags - Array of tag strings from RegisterDisplay\n * @private\n */\n _applyTagConfig(tags) {\n if (!Array.isArray(tags) || tags.length === 0) return;\n\n const TAG_CONFIG_MAP = {\n 'geoApiKey': 'googleGeoApiKey',\n };\n\n for (const tag of tags) {\n const pipeIdx = tag.indexOf('|');\n if (pipeIdx === -1) continue;\n\n const key = tag.substring(0, pipeIdx);\n const value = tag.substring(pipeIdx + 1);\n const configKey = TAG_CONFIG_MAP[key];\n\n if (configKey && value && this.config) {\n log.info(`Config from CMS tag: ${key} → ${configKey}`);\n this.config[configKey] = value;\n }\n }\n }\n\n _applyLocation(lat, lng, source) {\n log.info(`Geolocation (${source}): ${lat.toFixed(4)}, ${lng.toFixed(4)}`);\n\n if (this.schedule?.setLocation) {\n this.schedule.setLocation(lat, lng);\n }\n\n this.emit('location-updated', { latitude: lat, longitude: lng, source });\n this.checkSchedule();\n\n return { latitude: lat, longitude: lng };\n }\n\n /**\n * Try the browser Geolocation API (navigator.geolocation).\n * @returns {Promise<{latitude: number, longitude: number}|null>}\n * @private\n */\n async _tryBrowserGeolocation() {\n if (typeof navigator === 'undefined' || !navigator.geolocation) return null;\n\n try {\n const position = await new Promise((resolve, reject) => {\n navigator.geolocation.getCurrentPosition(resolve, reject, {\n timeout: 10000,\n maximumAge: 300000, // 5 minutes\n enableHighAccuracy: false\n });\n });\n return { latitude: position.coords.latitude, longitude: position.coords.longitude };\n } catch (error) {\n log.warn('Browser geolocation failed:', error?.message || error);\n return null;\n }\n }\n\n /**\n * Try Google Geolocation API (direct HTTPS POST, bypasses Chromium's built-in service).\n * @param {string} apiKey - Google API key\n * @returns {Promise<{latitude: number, longitude: number}|null>}\n * @private\n */\n async _tryGoogleGeolocation(apiKey) {\n try {\n const res = await fetch(\n `https://www.googleapis.com/geolocation/v1/geolocate?key=${apiKey}`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ considerIp: true }),\n signal: AbortSignal.timeout(5000)\n }\n );\n if (!res.ok) {\n log.warn(`Google Geolocation API returned ${res.status}`);\n return null;\n }\n const data = await res.json();\n if (data.location?.lat != null && data.location?.lng != null) {\n return { latitude: data.location.lat, longitude: data.location.lng };\n }\n return null;\n } catch (error) {\n log.warn('Google Geolocation API failed:', error?.message || error);\n return null;\n }\n }\n\n /**\n * Try IP-based geolocation using free HTTPS providers (no API key needed).\n * Tries ipapi.co first, then freeipapi.com as fallback.\n * @returns {Promise<{latitude: number, longitude: number}|null>}\n * @private\n */\n async _tryIpGeolocation() {\n const providers = [\n {\n url: 'https://ipapi.co/json/',\n parse: (data) => data.latitude != null && data.longitude != null\n ? { latitude: data.latitude, longitude: data.longitude }\n : null\n },\n {\n url: 'https://freeipapi.com/api/json',\n parse: (data) => data.latitude != null && data.longitude != null\n ? { latitude: data.latitude, longitude: data.longitude }\n : null\n }\n ];\n\n for (const provider of providers) {\n try {\n const res = await fetch(provider.url, { signal: AbortSignal.timeout(5000) });\n if (!res.ok) continue;\n const data = await res.json();\n const location = provider.parse(data);\n if (location) return location;\n } catch (error) {\n log.warn(`IP geolocation (${provider.url}) failed:`, error?.message || error);\n }\n }\n return null;\n }\n\n /**\n * Re-evaluate current schedule and switch layouts if needed.\n * Called after location updates or other schedule-affecting changes.\n */\n checkSchedule() {\n const layoutFiles = this.schedule.getCurrentLayouts();\n this.emit(E.LAYOUTS_SCHEDULED, layoutFiles);\n this._evaluateAndSwitchLayout(layoutFiles, '');\n }\n\n /**\n * Capture screenshot (called by XMR wrapper)\n * Emits event for platform layer to handle\n */\n async captureScreenshot() {\n log.info('Screenshot requested');\n this.emit(E.SCREENSHOT_REQUEST);\n }\n\n /**\n * Change to a specific layout (called by XMR wrapper)\n * Tracks override state so revertToSchedule() can undo it.\n */\n async changeLayout(layoutId, options) {\n log.info('Layout change requested via XMR:', layoutId);\n const id = parseInt(layoutId, 10);\n const duration = options?.duration || 0;\n const changeMode = options?.changeMode || 'replace';\n this._layoutOverride = { layoutId: id, type: 'change', duration, changeMode };\n this.currentLayoutId = null; // Force re-render\n this.emit(E.LAYOUT_PREPARE_REQUEST, id);\n this._scheduleAutoRevert(id, duration, 'Layout override');\n }\n\n /**\n * Push an overlay layout on top of current content (called by XMR wrapper)\n * @param {number|string} layoutId - Layout to overlay\n */\n async overlayLayout(layoutId, options) {\n log.info('Overlay layout requested via XMR:', layoutId);\n const id = parseInt(layoutId, 10);\n const duration = options?.duration || 0;\n this._layoutOverride = { layoutId: id, type: 'overlay', duration };\n this.emit(E.OVERLAY_LAYOUT_REQUEST, id);\n this._scheduleAutoRevert(id, duration, 'Overlay');\n }\n\n /**\n * Revert to scheduled content after changeLayout/overlayLayout override\n */\n async revertToSchedule() {\n log.info('Reverting to scheduled content');\n this._layoutOverride = null;\n this.currentLayoutId = null;\n this.emit(E.REVERT_TO_SCHEDULE);\n\n // Re-evaluate schedule to get the right layout\n const layoutFiles = this.schedule.getCurrentLayouts();\n if (layoutFiles.length > 0) {\n const layoutFile = layoutFiles[0];\n const layoutId = parseLayoutFile(layoutFile);\n this.emit(E.LAYOUT_PREPARE_REQUEST, layoutId);\n } else {\n this.emit(E.NO_LAYOUTS_SCHEDULED);\n }\n }\n\n /**\n * Purge all cached content and re-download (called by XMR wrapper)\n */\n async purgeAll() {\n log.info('Purge all cache requested via XMR');\n this._lastCheckRf = null;\n this._lastCheckSchedule = null;\n this.emit(E.PURGE_ALL_REQUEST);\n // Trigger immediate re-collection after purge\n return this.collectNow();\n }\n\n /**\n * Execute a command (HTTP only in browser context)\n * @param {string} commandCode - The command code from CMS\n * @param {Object} commands - Commands map from display settings\n */\n async executeCommand(commandCode, commands) {\n log.info('Execute command requested:', commandCode);\n\n if (!commands || !commands[commandCode]) {\n log.warn('Unknown command code:', commandCode);\n this._lastCommandSuccess = false;\n this.emit(E.COMMAND_RESULT, { code: commandCode, success: false, reason: 'Unknown command' });\n return;\n }\n\n const command = commands[commandCode];\n const commandString = command.commandString || command.value || '';\n\n // Only HTTP commands are possible in a browser\n if (commandString.startsWith('http|')) {\n const parts = commandString.split('|');\n const url = parts[1];\n const contentType = parts[2] || 'application/json';\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': contentType },\n signal: AbortSignal.timeout(10000),\n });\n const success = response.ok;\n this._lastCommandSuccess = success;\n log.info(`HTTP command ${commandCode} result: ${response.status}`);\n this.emit(E.COMMAND_RESULT, { code: commandCode, success, status: response.status });\n } catch (error) {\n this._lastCommandSuccess = false;\n log.error(`HTTP command ${commandCode} failed:`, error);\n this.emit(E.COMMAND_RESULT, { code: commandCode, success: false, reason: error.message });\n }\n } else {\n // Emit event for platform layer (Electron/Chromium) to handle native commands\n // (shell, RS232, Android intent, etc.)\n log.info('Delegating non-HTTP command to platform layer:', commandCode);\n this.emit(E.EXECUTE_NATIVE_COMMAND, { code: commandCode, commandString });\n }\n }\n\n /**\n * Trigger a webhook action (called by XMR wrapper)\n * @param {string} triggerCode - The trigger code to fire\n */\n triggerWebhook(triggerCode) {\n log.info('Webhook trigger from XMR:', triggerCode);\n this.handleTrigger(triggerCode);\n }\n\n /**\n * Force refresh of data connectors (called by XMR wrapper)\n */\n refreshDataConnectors() {\n log.info('Data connector refresh requested via XMR');\n this.dataConnectorManager.refreshAll();\n this.emit('data-connectors-refreshed');\n }\n\n /**\n * Submit media inventory to CMS\n * Reports which files are cached and complete.\n * @param {Array} files - List of files from RequiredFiles\n */\n async submitMediaInventory(files) {\n if (!files || files.length === 0) return;\n\n try {\n // Build inventory XML: <files><file type=\"media\" id=\"1\" complete=\"1\" md5=\"abc\" lastChecked=\"123\"/></files>\n // complete: use file.complete if set by caller (cache layer), default to \"1\"\n const now = Math.floor(Date.now() / 1000);\n const fileEntries = files\n .filter(f => ['media', 'layout', 'resource', 'dependency', 'widget'].includes(f.type))\n .map(f => {\n const complete = f.complete !== undefined ? (f.complete ? '1' : '0') : '1';\n const fileType = f.fileType ? ` fileType=\"${f.fileType}\"` : '';\n return `<file type=\"${f.type}\" id=\"${f.id}\" complete=\"${complete}\" md5=\"${f.md5 || ''}\" lastChecked=\"${now}\"${fileType}/>`;\n })\n .join('');\n const inventoryXml = `<files>${fileEntries}</files>`;\n\n await this.xmds.mediaInventory(inventoryXml);\n log.info(`Media inventory submitted: ${files.length} files`);\n this.emit('media-inventory-submitted', files.length);\n } catch (error) {\n log.warn('MediaInventory submission failed:', error);\n }\n }\n\n /**\n * BlackList a media file (report broken media to CMS)\n * @param {string|number} mediaId - The media ID\n * @param {string} type - File type ('media' or 'layout')\n * @param {string} reason - Reason for blacklisting\n */\n async blackList(mediaId, type, reason) {\n try {\n await this.xmds.blackList(mediaId, type, reason);\n this.emit('media-blacklisted', { mediaId, type, reason });\n } catch (error) {\n log.warn('BlackList failed:', error);\n }\n }\n\n /**\n * Report a layout render failure. After N consecutive failures\n * (default 3), the layout is blacklisted and skipped in schedule\n * evaluation. Blacklisted layouts are reported to CMS via the\n * BlackList XMDS method.\n *\n * @param {number} layoutId - The layout that failed\n * @param {string} reason - Human-readable failure description\n */\n reportLayoutFailure(layoutId, reason) {\n const id = Number(layoutId);\n this._statusCode = 3; // Error — layout failed to render\n\n const { blacklisted, failures } = this._layoutBlacklist.recordFailure(id, reason);\n if (blacklisted && failures === 3) {\n // Newly blacklisted (threshold just reached)\n this.emit('layout-blacklisted', { layoutId: id, reason, failures });\n this.blackList(id, 'layout', reason);\n }\n }\n\n reportLayoutSuccess(layoutId) {\n const wasBlacklisted = this._layoutBlacklist.recordSuccess(Number(layoutId));\n if (wasBlacklisted) {\n this.emit('layout-unblacklisted', { layoutId: Number(layoutId) });\n }\n }\n\n isLayoutBlacklisted(layoutId) {\n return this._layoutBlacklist.isBlacklisted(layoutId);\n }\n\n getBlacklistedLayouts() {\n return this._layoutBlacklist.getBlacklistedIds();\n }\n\n resetBlacklist() {\n if (this._layoutBlacklist.reset() > 0) {\n this.emit('blacklist-reset');\n }\n }\n\n /**\n * Check if currently in a layout override (from XMR changeLayout/overlayLayout)\n */\n isLayoutOverridden() {\n return this._layoutOverride !== null;\n }\n\n /**\n * Handle interactive trigger (from IC or touch events)\n * Looks up matching action in schedule and executes it\n * @param {string} triggerCode - The trigger code from the IC request\n */\n handleTrigger(triggerCode) {\n const action = this.schedule.findActionByTrigger(triggerCode);\n if (!action) {\n log.debug('No scheduled action matches trigger:', triggerCode);\n return;\n }\n\n log.info(`Action triggered: ${action.actionType} (trigger: ${triggerCode})`);\n\n switch (action.actionType) {\n case 'navLayout':\n case 'navigateToLayout':\n if (action.layoutCode) {\n this.changeLayout(action.layoutCode);\n }\n break;\n case 'navWidget':\n case 'navigateToWidget':\n this.emit(E.NAVIGATE_TO_WIDGET, action);\n break;\n case 'command':\n this.emit('execute-command', action.commandCode);\n break;\n default:\n log.warn('Unknown action type:', action.actionType);\n }\n }\n\n /**\n * Update data connectors from current schedule\n * Reconfigures and restarts polling when schedule changes.\n */\n updateDataConnectors() {\n const connectors = this.schedule.getDataConnectors();\n\n if (connectors.length > 0) {\n log.info(`Configuring ${connectors.length} data connector(s)`);\n }\n\n this.dataConnectorManager.setConnectors(connectors);\n\n if (connectors.length > 0) {\n this.dataConnectorManager.startPolling();\n this.emit('data-connectors-started', connectors.length);\n }\n }\n\n /**\n * Process scheduled commands from the CMS schedule.\n * Checks for command events whose scheduled date has arrived and executes them.\n * Each command is only executed once (tracked by code+date key in _executedCommands).\n */\n _processScheduledCommands() {\n if (!this.schedule?.getCommands) return;\n\n const commands = this.schedule.getCommands();\n if (commands.length === 0) return;\n\n const now = new Date();\n\n for (const command of commands) {\n if (!command.code || !command.date) continue;\n\n // Unique key to track execution (same command can be scheduled multiple times)\n const commandKey = `${command.code}|${command.date}`;\n\n // Skip already executed commands\n if (this._executedCommands.has(commandKey)) continue;\n\n // Check if the command's scheduled time has arrived\n const commandDate = new Date(command.date);\n if (isNaN(commandDate.getTime())) {\n log.warn('Scheduled command has invalid date:', command.date);\n continue;\n }\n\n if (now >= commandDate) {\n log.info(`Executing scheduled command: ${command.code} (scheduled: ${command.date})`);\n this._executedCommands.add(commandKey);\n\n // Handle built-in commands directly\n if (command.code === 'collectNow') {\n // Trigger immediate collection on next tick (avoid re-entrance)\n setTimeout(() => this.collectNow().catch(e => log.error('collectNow command failed:', e)), 0);\n } else {\n // Emit event for platform layer to handle (reboot, restart, etc.)\n this.emit(E.SCHEDULED_COMMAND, command);\n }\n }\n }\n }\n\n /**\n * Fetch weather data from CMS and pass to schedule for criteria evaluation.\n * Non-blocking: weather fetch failure doesn't prevent schedule evaluation.\n */\n async _fetchWeatherData() {\n if (!this.xmds?.getWeather || !this.schedule?.setWeatherData) return;\n\n try {\n const weatherJson = await this.xmds.getWeather();\n const weatherData = typeof weatherJson === 'string' ? JSON.parse(weatherJson) : weatherJson;\n this.schedule.setWeatherData(weatherData);\n log.info('Weather data updated:', Object.keys(weatherData).join(', '));\n } catch (e) {\n log.warn('GetWeather failed (non-critical):', e?.message || e);\n }\n }\n\n /**\n * Get the DataConnectorManager instance\n * Used by platform layer to serve data to widgets via IC /realtime\n * @returns {DataConnectorManager}\n */\n getDataConnectorManager() {\n return this.dataConnectorManager;\n }\n\n /**\n * Set the SyncManager instance for multi-display coordination.\n * Called by platform layer after RegisterDisplay returns syncConfig.\n *\n * @param {SyncManager} syncManager - SyncManager instance\n */\n setSyncManager(syncManager) {\n this.syncManager = syncManager;\n log.info('SyncManager attached:', syncManager.isLead ? 'LEAD' : 'FOLLOWER');\n }\n\n /**\n * Check if this display is part of a sync group\n * @returns {boolean}\n */\n isInSyncGroup() {\n return this.syncConfig !== null;\n }\n\n /**\n * Check if this display is the sync group leader\n * @returns {boolean}\n */\n isSyncLead() {\n return this.syncConfig?.isLead === true;\n }\n\n /**\n * Get sync configuration\n * @returns {Object|null} { syncGroup, syncPublisherPort, syncSwitchDelay, syncVideoPauseDelay, isLead }\n */\n getSyncConfig() {\n return this.syncConfig;\n }\n\n // ── Timeline (offline schedule prediction) ─────────────────────────\n\n // Duration flow: renderer is the single source of truth.\n // 1. Renderer calculates duration from widgets → emits layoutDurationUpdated\n // 2. recordLayoutDuration stores it (with final flag) → persisted to IDB\n // 3. On restart, IDB restores correct durations → queue uses them immediately\n // No XLF parsing needed in core — the renderer already does this.\n\n /**\n * Calculate and log the upcoming playback timeline (next 2 hours).\n * Emits 'timeline-updated' with the full timeline array.\n */\n logUpcomingTimeline() {\n if (!this.schedule.getLayoutsAtTime) return; // Schedule doesn't support time queries\n\n // Fingerprint inputs: schedule CRC + sorted durations + current layout + media status.\n // When unchanged, re-emit the cached timeline — avoids time drift from\n // re-simulating with a new Date.now() anchor on every collection cycle.\n const durationEntries = [...this._layoutDurations.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([k, v]) => `${k}:${v}`)\n .join('|');\n const mediaStatusEntries = [...this._layoutMediaStatus.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([k, v]) => `${k}:${v.ready}:${v.missingKey}`)\n .join('|');\n const pendingEntries = [...this.pendingLayouts.keys()].sort().join(',');\n const queuePos = this.schedule.getQueuePosition() || 0;\n const fingerprint = `${this._lastCheckSchedule}|${durationEntries}|${this.currentLayoutId}|${queuePos}|${mediaStatusEntries}|${pendingEntries}`;\n\n if (fingerprint === this._lastTimelineFingerprint && this._lastTimeline) {\n this.emit(E.TIMELINE_UPDATED, this._lastTimeline);\n return;\n }\n\n const { queue } = this.schedule.getScheduleQueue(this._layoutDurations, this._queueOptions);\n const timeline = calculateTimeline(queue, this.schedule.getQueuePosition(), {\n currentLayoutStartedAt: this._lastLayoutChangeTime ? new Date(this._lastLayoutChangeTime) : null,\n defaultLayout: this.schedule.schedule?.default || null,\n durations: this._layoutDurations,\n });\n if (timeline.length === 0) return;\n\n // Annotate entries with missingMedia from pendingLayouts (high authority)\n // and _layoutMediaStatus (proactive check, lower authority)\n for (const entry of timeline) {\n const layoutId = parseInt(entry.layoutFile.replace('.xlf', ''), 10);\n const pendingMedia = this.pendingLayouts.get(layoutId);\n if (pendingMedia && pendingMedia.length > 0) {\n // pendingLayouts takes priority — definitively missing\n entry.missingMedia = pendingMedia.map(String);\n } else {\n const status = this._layoutMediaStatus.get(entry.layoutFile);\n if (status && !status.ready && status.missing.length > 0) {\n entry.missingMedia = status.missing.map(String);\n }\n }\n }\n\n this._lastTimelineFingerprint = fingerprint;\n this._lastTimeline = timeline;\n\n const lines = timeline.slice(0, 20).map(e => {\n const s = e.startTime.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });\n const end = e.endTime.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });\n const missingTag = e.missingMedia ? ` [MISSING: ${e.missingMedia.length} files]` : '';\n return ` ${s}-${end} Layout ${e.layoutFile} (${e.duration}s)${e.isDefault ? ' [default]' : ''}${missingTag}`;\n });\n\n // Log warnings for layouts with missing media\n for (const entry of timeline) {\n if (entry.missingMedia) {\n log.warn(`[Timeline] Layout ${entry.layoutFile}: ${entry.missingMedia.length} files missing`);\n }\n }\n\n log.info(`[Timeline] Next ${timeline.length} plays:\\n${lines.join('\\n')}`);\n this.emit(E.TIMELINE_UPDATED, timeline);\n }\n\n /**\n * Set media readiness status for a layout (proactive async check from platform layer).\n * No-ops if value is unchanged to avoid fingerprint churn.\n * @param {string} layoutFile - Layout file (e.g. '100.xlf')\n * @param {boolean} ready - Whether all media is cached\n * @param {string[]} [missing] - Array of missing media IDs/filenames\n */\n setLayoutMediaStatus(layoutFile, ready, missing = []) {\n const existing = this._layoutMediaStatus.get(layoutFile);\n const missingKey = missing.slice().sort().join(',');\n if (existing && existing.ready === ready && existing.missingKey === missingKey) return;\n\n this._layoutMediaStatus.set(layoutFile, { ready, missing, missingKey });\n // Invalidate fingerprint to force timeline recalculation\n this._lastTimelineFingerprint = null;\n }\n\n /**\n * Record/correct a layout's actual duration (e.g., from video loadedmetadata).\n * Updates the durations map and re-logs the timeline if it changed.\n * @param {string} file - Layout file or layout ID string\n * @param {number} duration - Actual duration in seconds\n * @param {boolean} [final=false] - True when all videos in the layout have been probed\n */\n recordLayoutDuration(file, duration, final = false) {\n // Normalize: store under both \"492\" and \"492.xlf\" forms so that\n // calculateTimeline (which looks up \"492.xlf\") and other callers\n // (which use \"492\") always find the corrected value.\n const id = String(file).replace('.xlf', '');\n const xlfKey = id + '.xlf';\n\n // Definitive duration — never overwrite once set\n if (this._finalDurations.has(id)) return;\n\n const prev = this._layoutDurations.get(file);\n if (prev === duration && !final) return; // No change\n\n this._layoutDurations.set(id, duration);\n this._layoutDurations.set(xlfKey, duration);\n\n if (final) {\n this._finalDurations.add(id);\n this._finalDurations.add(xlfKey);\n }\n\n log.debug(`[Timeline] Duration corrected: layout ${file} ${prev || '?'}s → ${duration}s${final ? ' (final)' : ''}`);\n\n // Invalidate the cached schedule queue so the next getScheduleQueue() call\n // rebuilds with corrected durations (affects queue log and period calculation).\n this.schedule.invalidateQueue();\n\n // Debounce timeline recalculation — multiple video loadedmetadata events\n // can fire within milliseconds; collapse them into one recalculation.\n if (this._timelineRecalcTimer) clearTimeout(this._timelineRecalcTimer);\n this._timelineRecalcTimer = setTimeout(() => {\n this._timelineRecalcTimer = null;\n this.logUpcomingTimeline();\n this._offlineSave('durations', [...this._layoutDurations.entries()]);\n this._offlineSave('finalDurations', [...this._finalDurations]);\n this._offlineSave('durationsVersion', 2);\n }, 500);\n }\n\n /**\n * Cleanup\n */\n cleanup() {\n if (this.collectionInterval) {\n clearInterval(this.collectionInterval);\n this.collectionInterval = null;\n }\n\n if (this._faultReportingInterval) {\n clearInterval(this._faultReportingInterval);\n this._faultReportingInterval = null;\n }\n\n if (this._timelineRecalcTimer) {\n clearTimeout(this._timelineRecalcTimer);\n this._timelineRecalcTimer = null;\n }\n\n if (this.xmr) {\n this.xmr.stop();\n this.xmr = null;\n }\n\n // Stop multi-display sync\n if (this.syncManager) {\n this.syncManager.stop();\n this.syncManager = null;\n }\n\n // Stop data connector polling\n this.dataConnectorManager.cleanup();\n\n // Emit cleanup-complete before removing listeners\n this.emit('cleanup-complete');\n this.removeAllListeners();\n }\n\n /**\n * Get current layout ID\n */\n getCurrentLayoutId() {\n return this.currentLayoutId;\n }\n\n /**\n * Get known duration for a layout (from video metadata or XLF parse).\n * @param {number|string} layoutId\n * @returns {number|undefined}\n */\n getLayoutDuration(layoutId) {\n const id = String(layoutId);\n return this._layoutDurations.get(`${id}.xlf`) || this._layoutDurations.get(id);\n }\n\n /**\n * Check if collecting\n */\n isCollecting() {\n return this.collecting;\n }\n\n /**\n * Get pending layouts\n */\n getPendingLayouts() {\n return Array.from(this.pendingLayouts.keys());\n }\n\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n// @xiboplayer/core - Player core orchestration\nimport pkg from '../package.json' with { type: 'json' };\nexport const VERSION = pkg.version;\nexport { PlayerCore } from './player-core.js';\nexport { DataConnectorManager } from './data-connectors.js';\nexport { CORE_EVENTS } from './events.js';\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Download Progress Overlay\n *\n * Shows download status on hover (configurable, debug feature)\n * Displays: active downloads, progress, chunk status, queue info\n */\n\nexport interface DownloadOverlayConfig {\n enabled: boolean;\n updateInterval?: number; // ms between updates\n autoHide?: boolean; // Hide when no downloads\n}\n\nexport class DownloadOverlay {\n private overlay: HTMLElement | null = null;\n private config: DownloadOverlayConfig;\n private updateTimer: number | null = null;\n private _visible: boolean = false; // User-toggled visibility (D key)\n private _getProgress: (() => Record<string, any>) | null = null;\n\n constructor(config: DownloadOverlayConfig) {\n this.config = {\n updateInterval: 1000,\n autoHide: true,\n ...config\n };\n\n if (this.config.enabled) {\n this.createOverlay();\n // Start hidden — only shown when downloads are active or user presses D\n this.overlay!.style.display = 'none';\n }\n }\n\n private createOverlay() {\n this.overlay = document.createElement('div');\n this.overlay.id = 'download-overlay';\n // Style like top status messages - always visible, clean design\n this.overlay.style.cssText = `\n position: fixed;\n top: 1.5vh;\n left: 1.5vw;\n background: rgba(0, 0, 0, 0.88);\n color: #fff;\n font-family: system-ui, -apple-system, sans-serif;\n font-size: 1.4vw;\n padding: 1vh 1.2vw;\n border-radius: 0.4vw;\n border: 1px solid rgba(255, 255, 255, 0.25);\n z-index: 999999;\n max-width: 35vw;\n box-shadow: 0 0.3vh 1.2vw rgba(0, 0, 0, 0.5);\n `;\n\n document.body.appendChild(this.overlay);\n }\n\n /**\n * Set the progress callback. Called by PwaPlayer after DownloadManager is created.\n */\n public setProgressCallback(fn: () => Record<string, any>) {\n this._getProgress = fn;\n }\n\n private updateOverlay() {\n if (!this.overlay) return;\n\n const progress = this._getProgress ? this._getProgress() : {};\n const html = this.renderStatus(progress);\n const hasDownloads = !!html;\n\n if (hasDownloads) {\n // Active downloads — show overlay (auto or user-toggled)\n this.overlay.innerHTML = html;\n this.overlay.style.display = 'block';\n } else if (this._visible) {\n // User toggled on (D key) but no downloads — show idle status\n this.overlay.innerHTML = '<div style=\"color: #6c6; font-size: 1.4vw;\">✓ All downloads complete</div>';\n this.overlay.style.display = 'block';\n } else {\n // No downloads and not user-toggled — stop polling, hide\n this.stopUpdating();\n this.overlay.style.display = 'none';\n }\n }\n\n private renderStatus(progress: any): string {\n const downloads = progress || {};\n\n if (Object.keys(downloads).length === 0) {\n if (this.config.autoHide) {\n return ''; // Hide when no downloads\n }\n return `<div style=\"color: #6c6;\">✓ No downloads</div>`;\n }\n\n const numDownloads = Object.keys(downloads).length;\n let html = `<div style=\"font-weight: 600; margin-bottom: 0.8vh; font-size: 1.4vw;\">Downloads: ${numDownloads} active</div>`;\n\n for (const [url, progress] of Object.entries(downloads)) {\n const filename = this.extractFilename(url);\n const percent = Math.round((progress as any).percent || 0);\n const downloaded = this.formatBytes((progress as any).downloaded || 0);\n const total = this.formatBytes((progress as any).total || 0);\n\n html += `\n <div style=\"margin-bottom: 0.6vh; padding-bottom: 0.6vh; border-bottom: 1px solid rgba(255,255,255,0.1);\">\n <div style=\"font-size: 1.2vw; margin-bottom: 0.2vh;\">${filename}</div>\n <div style=\"background: rgba(255,255,255,0.1); height: 0.4vh; border-radius: 0.2vw; overflow: hidden;\">\n <div style=\"width: ${percent}%; height: 100%; background: #4a9eff; transition: width 0.3s;\"></div>\n </div>\n <div style=\"color: #999; font-size: 1.1vw; margin-top: 0.2vh;\">\n ${percent}% · ${downloaded} / ${total}\n </div>\n </div>\n `;\n }\n\n return html;\n }\n\n private extractFilename(key: string): string {\n // Key is now \"type/id\" (e.g. \"media/5\", \"layout/12\") — no URL parsing needed\n return key || 'unknown';\n }\n\n private formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n const kb = bytes / 1024;\n if (kb < 1024) return `${kb.toFixed(1)} KB`;\n const mb = kb / 1024;\n if (mb < 1024) return `${mb.toFixed(1)} MB`;\n return `${(mb / 1024).toFixed(1)} GB`;\n }\n\n /**\n * Toggle overlay visibility (D key).\n * When toggled on, starts polling. When toggled off, hides immediately.\n */\n public toggle() {\n if (!this.overlay) return;\n this._visible = !this._visible;\n if (this._visible) {\n this.overlay.style.display = 'block';\n this.updateOverlay(); // Immediate update\n this.startUpdating();\n } else {\n this.overlay.style.display = 'none';\n this.stopUpdating();\n }\n }\n\n /**\n * Start polling for download progress.\n * Safe to call multiple times — won't create duplicate timers.\n * Does NOT set _visible — the overlay auto-shows when downloads are active\n * and auto-hides when they finish. Use toggle() for user-controlled visibility.\n */\n public startUpdating() {\n if (this.updateTimer) return; // Already polling\n this.updateTimer = window.setInterval(() => {\n this.updateOverlay();\n }, this.config.updateInterval);\n this.updateOverlay(); // Immediate first update\n }\n\n /**\n * Stop polling. Called automatically when no downloads are active.\n */\n private stopUpdating() {\n if (this.updateTimer) {\n clearInterval(this.updateTimer);\n this.updateTimer = null;\n }\n }\n\n public destroy() {\n this.stopUpdating();\n if (this.overlay) {\n this.overlay.remove();\n this.overlay = null;\n }\n }\n\n public setEnabled(enabled: boolean) {\n this.config.enabled = enabled;\n\n if (enabled && !this.overlay) {\n this.createOverlay();\n // Polling starts on demand via startUpdating()\n } else if (!enabled && this.overlay) {\n this.destroy();\n }\n }\n}\n\n/**\n * Get default configuration based on environment\n */\nexport function getDefaultOverlayConfig(): DownloadOverlayConfig {\n // Check URL parameter override\n const urlParams = new URLSearchParams(window.location.search);\n const showDownloads = urlParams.get('showDownloads');\n\n if (showDownloads !== null) {\n return { enabled: showDownloads !== '0' && showDownloads !== 'false' };\n }\n\n // Check localStorage preference\n const savedPref = localStorage.getItem('xibo_show_download_overlay');\n if (savedPref !== null) {\n return { enabled: savedPref === 'true' };\n }\n\n // Default: disabled — toggle with D key or ?showDownloads=1\n return { enabled: false };\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Timeline Overlay\n *\n * Toggleable debug overlay showing upcoming schedule timeline.\n * Displays: layout IDs, time ranges, durations, current layout highlight.\n * Positioned bottom-left (download overlay is top-left).\n */\n\nimport { parseLayoutFile } from '@xiboplayer/schedule';\n\ninterface HiddenLayout {\n file: string;\n priority: number;\n}\n\ninterface TimelineEntry {\n layoutFile: string;\n startTime: Date;\n endTime: Date;\n duration: number;\n isDefault: boolean;\n hidden?: HiddenLayout[];\n missingMedia?: string[];\n}\n\nexport class TimelineOverlay {\n private overlay: HTMLElement | null = null;\n private visible: boolean;\n private timeline: TimelineEntry[] = [];\n private currentLayoutId: number | null = null;\n private layoutStartedAt: number | null = null; // wall-clock ms when layout began\n private currentDuration: number | null = null;\n private currentIsDefault: boolean = false;\n private previousLayout: { id: number; duration: number; startedAt: number } | null = null;\n private offline: boolean = false;\n private onLayoutClick: ((layoutId: number) => void) | null = null;\n private refreshTimer: ReturnType<typeof setInterval> | null = null;\n\n constructor(visible = false, onLayoutClick?: (layoutId: number) => void) {\n this.visible = visible;\n this.onLayoutClick = onLayoutClick || null;\n this.createOverlay();\n if (!this.visible) {\n this.overlay!.style.display = 'none';\n }\n // Re-render every 5s to update the remaining-time countdown on the current layout\n this.refreshTimer = setInterval(() => this.render(), 5000);\n }\n\n private createOverlay() {\n this.overlay = document.createElement('div');\n this.overlay.id = 'timeline-overlay';\n this.overlay.style.cssText = `\n position: fixed;\n bottom: 1.5vh;\n left: 1.5vw;\n background: rgba(0, 0, 0, 0.88);\n color: #fff;\n font-family: system-ui, -apple-system, sans-serif;\n font-size: 1.4vw;\n padding: 1vh 1.2vw;\n border-radius: 0.4vw;\n border: 1px solid rgba(255, 255, 255, 0.25);\n z-index: 999999;\n max-width: 35vw;\n box-shadow: 0 0.3vh 1.2vw rgba(0, 0, 0, 0.5);\n pointer-events: auto;\n `;\n // Click-to-skip: delegate click events on layout entries\n this.overlay.addEventListener('click', (e: MouseEvent) => {\n const target = (e.target as HTMLElement).closest('[data-layout-id]') as HTMLElement | null;\n if (!target || !this.onLayoutClick) return;\n const layoutId = parseInt(target.dataset.layoutId!, 10);\n if (isNaN(layoutId) || layoutId === this.currentLayoutId) return;\n this.onLayoutClick(layoutId);\n });\n\n document.body.appendChild(this.overlay);\n }\n\n toggle() {\n this.visible = !this.visible;\n if (this.overlay) {\n this.overlay.style.display = this.visible ? 'block' : 'none';\n }\n // Re-render when becoming visible (render() skips while hidden)\n if (this.visible) {\n this.render();\n }\n // Persist preference\n localStorage.setItem('xibo_show_timeline_overlay', String(this.visible));\n }\n\n /**\n * Update the overlay with new timeline data and/or current layout highlight.\n * Pass timeline=null to keep existing timeline and only update the highlight.\n */\n setOffline(offline: boolean) {\n this.offline = offline;\n this.render();\n }\n\n update(timeline: TimelineEntry[] | null, currentLayoutId: number | null, currentDuration?: number) {\n if (currentLayoutId !== null) {\n // Detect layout change — save previous for history display\n if (currentLayoutId !== this.currentLayoutId) {\n if (this.currentLayoutId !== null && this.currentDuration !== null && this.layoutStartedAt !== null) {\n this.previousLayout = { id: this.currentLayoutId, duration: this.currentDuration, startedAt: this.layoutStartedAt };\n }\n this.currentLayoutId = currentLayoutId;\n this.currentIsDefault = false;\n }\n // Always reset start time — same-layout replays emit layoutStart too\n this.layoutStartedAt = Date.now();\n // Duration is known at layout start — set it directly rather than\n // searching the timeline (which only contains future layouts).\n if (currentDuration !== undefined) {\n this.currentDuration = currentDuration;\n }\n }\n\n if (timeline !== null) {\n this.timeline = timeline;\n }\n\n this.render();\n }\n\n private render() {\n if (!this.overlay || !this.visible) return;\n\n if (this.timeline.length === 0 && !this.previousLayout && !this.currentLayoutId) {\n this.overlay.innerHTML = '<div style=\"color: #999;\">Timeline — no upcoming layouts</div>';\n return;\n }\n\n const now = Date.now();\n const clickable = this.onLayoutClick !== null;\n\n // Build upcoming list: timeline entries minus the first occurrence of the current layout\n let skippedCurrent = false;\n const upcoming: TimelineEntry[] = [];\n for (const entry of this.timeline) {\n const layoutId = parseLayoutFile(entry.layoutFile);\n if (!skippedCurrent && layoutId === this.currentLayoutId) {\n skippedCurrent = true;\n continue;\n }\n upcoming.push(entry);\n }\n\n // Count: previous (if any) + current (if any) + upcoming\n const totalCount = (this.previousLayout ? 1 : 0) + (this.currentLayoutId ? 1 : 0) + upcoming.length;\n const offlineBadge = this.offline ? ' <span style=\"color: #ff4444; font-size: 1.1vw;\">OFFLINE</span>' : '';\n let html = `<div style=\"font-weight: 600; margin-bottom: 0.8vh; font-size: 1.4vw; color: #ccc;\">Timeline (${totalCount} scheduled)${offlineBadge}</div>`;\n\n const maxVisible = 8;\n let rendered = 0;\n\n // 1. Previous layout (dimmed, strikethrough)\n if (this.previousLayout && rendered < maxVisible) {\n const prev = this.previousLayout;\n const durStr = this.formatDuration(prev.duration);\n const durPad = durStr.padStart(7).replace(/ /g, ' ');\n const idCol = `#${prev.id}`.padEnd(6).replace(/ /g, ' ');\n const startDate = new Date(prev.startedAt);\n const endDate = new Date(prev.startedAt + prev.duration * 1000);\n const timeRange = `${this.formatTime(startDate)}–${this.formatTime(endDate)} `;\n const cursor = clickable ? 'cursor: pointer;' : '';\n const hover = clickable ? 'onmouseover=\"this.style.background=\\'rgba(255,255,255,0.1)\\'\" onmouseout=\"this.style.background=\\'none\\'\"' : '';\n html += `<div data-layout-id=\"${prev.id}\" style=\"border-left: 0.25vw solid #555; padding-left: 0.6vw; color: #666; text-decoration: line-through; ${cursor} margin-bottom: 0.3vh; font-family: monospace; font-size: 1.3vw; line-height: 1.5; white-space: nowrap;\" ${hover}>`;\n html += `${timeRange}${idCol}${durPad}`;\n html += '</div>';\n rendered++;\n }\n\n // 2. Current layout (blue highlight, countdown from wall-clock start, with time range)\n if (this.currentLayoutId !== null && rendered < maxVisible) {\n let durStr: string;\n let timeRange = '';\n if (this.currentDuration !== null && this.layoutStartedAt !== null) {\n const elapsed = (now - this.layoutStartedAt) / 1000;\n const remainingSec = Math.max(0, Math.round(this.currentDuration - elapsed));\n durStr = this.formatDuration(remainingSec);\n const startDate = new Date(this.layoutStartedAt);\n const endDate = new Date(this.layoutStartedAt + this.currentDuration * 1000);\n timeRange = `${this.formatTime(startDate)}–${this.formatTime(endDate)} `;\n } else {\n durStr = '---';\n }\n const durPad = durStr.padStart(7).replace(/ /g, ' ');\n const idCol = `#${this.currentLayoutId}`.padEnd(6).replace(/ /g, ' ');\n html += `<div data-layout-id=\"${this.currentLayoutId}\" style=\"border-left: 0.25vw solid #4a9eff; padding-left: 0.6vw; color: #fff; font-weight: 600; margin-bottom: 0.3vh; font-family: monospace; font-size: 1.3vw; line-height: 1.5; white-space: nowrap;\">`;\n html += `${timeRange}${idCol}${durPad}`;\n if (this.currentIsDefault) html += ' <span style=\"color: #888;\">[def]</span>';\n html += '</div>';\n rendered++;\n }\n\n // 3. Upcoming layouts — compute times by chaining from current layout end\n let nextStartMs = (this.layoutStartedAt !== null && this.currentDuration !== null)\n ? this.layoutStartedAt + this.currentDuration * 1000\n : now;\n for (const entry of upcoming) {\n if (rendered >= maxVisible) break;\n const layoutId = parseLayoutFile(entry.layoutFile);\n const hasMissing = entry.missingMedia && entry.missingMedia.length > 0;\n const durStr = this.formatDuration(entry.duration);\n const entryEndMs = nextStartMs + entry.duration * 1000;\n const startStr = this.formatTime(new Date(nextStartMs));\n const endStr = this.formatTime(new Date(entryEndMs));\n\n let borderLeft: string;\n let color: string;\n if (hasMissing) {\n borderLeft = 'border-left: 0.25vw solid #ff4444; padding-left: 0.6vw;';\n color = 'color: #ff6666;';\n } else {\n borderLeft = 'padding-left: 0.85vw;';\n color = 'color: #aaa;';\n }\n const cursor = clickable ? 'cursor: pointer;' : '';\n const hover = clickable ? 'onmouseover=\"this.style.background=\\'rgba(255,255,255,0.1)\\'\" onmouseout=\"this.style.background=\\'none\\'\"' : '';\n\n html += `<div data-layout-id=\"${layoutId}\" style=\"${borderLeft} ${color} ${cursor} margin-bottom: 0.3vh; font-family: monospace; font-size: 1.3vw; line-height: 1.5; white-space: nowrap;\" ${hover}>`;\n const idCol = `#${layoutId}`.padEnd(6).replace(/ /g, ' ');\n const durPad = durStr.padStart(7).replace(/ /g, ' ');\n html += `${startStr}–${endStr} ${idCol}${durPad}`;\n if (entry.isDefault) html += ' <span style=\"color: #888;\">[def]</span>';\n if (hasMissing) {\n const missingList = entry.missingMedia!.join(', ');\n html += ` <span style=\"color: #ff4444; font-size: 1.1vw;\" title=\"Missing: ${missingList}\">⚠ ${entry.missingMedia!.length}</span>`;\n }\n if (entry.hidden && entry.hidden.length > 0) {\n const hiddenIds = entry.hidden.map(h => `#${h.file.replace('.xlf', '')} (p${h.priority})`).join(', ');\n html += ` <span style=\"color: #8899aa; font-size: 1.1vw;\" title=\"Also scheduled: ${hiddenIds}\">+${entry.hidden.length}</span>`;\n }\n html += '</div>';\n nextStartMs = entryEndMs;\n rendered++;\n }\n\n if (totalCount > maxVisible) {\n html += `<div style=\"padding-left: 0.85vw; color: #888; font-size: 1.1vw; margin-top: 0.3vh;\">+${totalCount - maxVisible} more</div>`;\n }\n\n this.overlay.innerHTML = html;\n }\n\n private formatTime(date: Date): string {\n return date.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });\n }\n\n private formatDuration(seconds: number): string {\n const m = Math.floor(seconds / 60);\n const s = Math.round(seconds % 60);\n return m > 0 ? `${m}m ${s.toString().padStart(2, '0')}s` : `${s}s`;\n }\n\n destroy() {\n if (this.refreshTimer) {\n clearInterval(this.refreshTimer);\n this.refreshTimer = null;\n }\n if (this.overlay) {\n this.overlay.remove();\n this.overlay = null;\n }\n }\n}\n\n/**\n * Determine initial visibility from URL param or localStorage.\n */\nexport function isTimelineVisible(): boolean {\n const urlParams = new URLSearchParams(window.location.search);\n const showTimeline = urlParams.get('showTimeline');\n if (showTimeline !== null) {\n return showTimeline !== '0' && showTimeline !== 'false';\n }\n\n const saved = localStorage.getItem('xibo_show_timeline_overlay');\n if (saved !== null) {\n return saved === 'true';\n }\n\n return false;\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Setup Overlay\n *\n * Two-phase overlay that never navigates away from the player:\n * 1. CMS key gate — verifies identity\n * 2. Full setup form — setup.html in a fullscreen iframe\n *\n * Dismissible with Esc or Cancel at both phases. On successful setup\n * the iframe redirects to index.html, which we intercept to reload.\n */\n\nimport { createLogger, config } from '@xiboplayer/utils';\n\nconst log = createLogger('SetupOverlay');\n\nexport class SetupOverlay {\n private backdrop: HTMLElement | null = null;\n private gateCard: HTMLElement | null = null;\n private iframe: HTMLIFrameElement | null = null;\n private cancelBtn: HTMLElement | null = null;\n private visible = false;\n\n show() {\n if (this.visible) return;\n this.visible = true;\n\n if (!this.backdrop) {\n this.create();\n }\n\n // Always start with the gate phase\n this.showGate();\n this.backdrop!.style.display = 'flex';\n log.info('[SetupOverlay] Opened');\n }\n\n hide() {\n if (!this.visible) return;\n this.visible = false;\n\n if (this.backdrop) {\n this.backdrop.style.display = 'none';\n }\n // Clear iframe to stop any polling timers inside setup.html\n if (this.iframe) {\n this.iframe.src = 'about:blank';\n this.iframe.style.display = 'none';\n }\n log.info('[SetupOverlay] Closed');\n }\n\n toggle() {\n if (this.visible) {\n this.hide();\n } else {\n this.show();\n }\n }\n\n isVisible() {\n return this.visible;\n }\n\n /** Show the CMS key gate card, hide the iframe */\n private showGate() {\n if (this.gateCard) this.gateCard.style.display = 'block';\n if (this.iframe) this.iframe.style.display = 'none';\n if (this.cancelBtn) this.cancelBtn.style.display = 'none';\n\n const input = this.gateCard?.querySelector('#gate-key') as HTMLInputElement;\n if (input) {\n input.value = '';\n requestAnimationFrame(() => input.focus());\n }\n const err = this.gateCard?.querySelector('#gate-error') as HTMLElement;\n if (err) err.style.display = 'none';\n }\n\n /** Show the setup iframe, hide the gate card */\n private showSetup() {\n if (this.gateCard) this.gateCard.style.display = 'none';\n if (this.cancelBtn) this.cancelBtn.style.display = 'block';\n if (this.iframe) {\n this.iframe.style.display = 'block';\n this.iframe.src = './setup.html?unlocked=1';\n }\n }\n\n private create() {\n // ── Backdrop ──\n this.backdrop = document.createElement('div');\n this.backdrop.id = 'setup-overlay-backdrop';\n this.backdrop.style.cssText = `\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.85);\n z-index: 1000000;\n display: none;\n align-items: center;\n justify-content: center;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n `;\n\n // ── Cancel button (visible in iframe phase) ──\n this.cancelBtn = document.createElement('button');\n this.cancelBtn.textContent = 'Cancel';\n this.cancelBtn.style.cssText = `\n position: absolute;\n top: 12px;\n right: 16px;\n background: transparent;\n border: 1px solid rgba(255, 255, 255, 0.3);\n color: #aaa;\n font-size: 14px;\n padding: 6px 18px;\n border-radius: 6px;\n cursor: pointer;\n z-index: 1000001;\n display: none;\n transition: background 0.2s, color 0.2s;\n `;\n this.cancelBtn.addEventListener('mouseenter', () => {\n this.cancelBtn!.style.background = 'rgba(255,255,255,0.1)';\n this.cancelBtn!.style.color = '#fff';\n });\n this.cancelBtn.addEventListener('mouseleave', () => {\n this.cancelBtn!.style.background = 'transparent';\n this.cancelBtn!.style.color = '#aaa';\n });\n this.cancelBtn.addEventListener('click', () => this.hide());\n\n // ── Gate card (matches setup.html .container) ──\n this.gateCard = document.createElement('div');\n this.gateCard.style.cssText = `\n background: #2A2A2A;\n border-radius: 16px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);\n padding: 48px;\n max-width: 480px;\n width: 90vw;\n color: #E0E0E0;\n `;\n this.gateCard.innerHTML = `\n <div style=\"text-align: center; margin-bottom: 32px;\">\n <div style=\"font-size: 36px; font-weight: 700; color: #fff; letter-spacing: -0.5px;\">\n <span style=\"color: #0097D8;\">xibo</span> player\n </div>\n <div style=\"font-size: 14px; color: #888; margin-top: 4px;\">PWA Digital Signage</div>\n </div>\n <div style=\"font-size: 18px; font-weight: 600; color: #fff; margin-bottom: 8px; text-align: center;\">\n Reconfigure Display\n </div>\n <div style=\"font-size: 13px; color: #888; margin-bottom: 20px; text-align: center; line-height: 1.5;\">\n Enter the current CMS Key to change settings.\n </div>\n <form id=\"gate-form\">\n <div style=\"margin-bottom: 20px;\">\n <label style=\"display: block; margin-bottom: 6px; color: #AAA; font-size: 13px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;\">\n CMS Key\n </label>\n <input type=\"password\" id=\"gate-key\" placeholder=\"Current CMS key\" required\n style=\"width: 100%; padding: 12px 14px; background: #1D1D1D; border: 2px solid #3A3A3A; border-radius: 8px; font-size: 15px; color: #E0E0E0; transition: border-color 0.2s; box-sizing: border-box;\">\n </div>\n <button type=\"submit\" style=\"width: 100%; padding: 14px; background: #0097D8; color: white; border: none; border-radius: 8px; font-size: 15px; font-weight: 600; cursor: pointer; transition: background 0.2s, transform 0.1s;\">\n Unlock\n </button>\n </form>\n <div id=\"gate-error\" style=\"margin-top: 16px; padding: 12px 14px; background: rgba(244, 67, 54, 0.15); border: 1px solid rgba(244, 67, 54, 0.3); border-radius: 8px; color: #EF9A9A; font-size: 14px; display: none;\"></div>\n <button id=\"gate-cancel\" style=\"width: 100%; padding: 14px; background: transparent; border: 1px solid #3A3A3A; color: #AAA; border-radius: 8px; font-size: 15px; font-weight: 600; cursor: pointer; margin-top: 8px; transition: background 0.2s;\">\n Cancel\n </button>\n `;\n\n // ── Iframe (fullscreen, same look as setup.html) ──\n this.iframe = document.createElement('iframe');\n this.iframe.style.cssText = `\n width: 100%;\n height: 100%;\n border: none;\n background: #1D1D1D;\n display: none;\n `;\n\n // Detect success redirect: setup.html navigates to index.html → reload player\n this.iframe.addEventListener('load', () => {\n try {\n const href = this.iframe!.contentWindow?.location?.href || '';\n if (href.includes('index.html')) {\n this.hide();\n window.location.reload();\n return;\n }\n\n // Esc inside the iframe dismisses the overlay\n const iframeDoc = this.iframe!.contentDocument;\n if (!iframeDoc) return;\n iframeDoc.addEventListener('keydown', (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n e.preventDefault();\n this.hide();\n }\n });\n } catch { /* not loaded yet */ }\n });\n\n this.backdrop.appendChild(this.cancelBtn);\n this.backdrop.appendChild(this.gateCard);\n this.backdrop.appendChild(this.iframe);\n document.body.appendChild(this.backdrop);\n\n // ── Gate event handlers ──\n const form = this.gateCard.querySelector('#gate-form') as HTMLFormElement;\n const input = this.gateCard.querySelector('#gate-key') as HTMLInputElement;\n const errorEl = this.gateCard.querySelector('#gate-error') as HTMLElement;\n const gateCancelBtn = this.gateCard.querySelector('#gate-cancel') as HTMLButtonElement;\n\n input.addEventListener('focus', () => { input.style.borderColor = '#0097D8'; });\n input.addEventListener('blur', () => { input.style.borderColor = '#3A3A3A'; });\n\n form.addEventListener('submit', (e: Event) => {\n e.preventDefault();\n const entered = input.value.trim();\n\n if (entered === config.cmsKey) {\n this.showSetup();\n } else {\n errorEl.textContent = 'Incorrect CMS key';\n errorEl.style.display = 'block';\n input.focus();\n input.select();\n }\n });\n\n gateCancelBtn.addEventListener('click', () => this.hide());\n\n // Esc closes overlay; stopPropagation blocks player shortcuts\n this.backdrop.addEventListener('keydown', (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n e.preventDefault();\n this.hide();\n }\n e.stopPropagation();\n });\n }\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * PWA Player with RendererLite\n *\n * Lightweight PWA player using modular PlayerCore orchestration.\n * Platform layer handles UI, DOM manipulation, and platform-specific features.\n */\n\nimport { RendererLite } from '@xiboplayer/renderer';\nimport { StoreClient, DownloadManager, BARRIER } from '@xiboplayer/cache';\nimport { PlayerCore, CORE_EVENTS as E } from '@xiboplayer/core';\nimport { parseLayoutDuration, parseLayoutFile } from '@xiboplayer/schedule';\nimport { createLogger, registerLogSink, PLAYER_API } from '@xiboplayer/utils';\nimport { DownloadOverlay, getDefaultOverlayConfig } from './download-overlay.js';\nimport { TimelineOverlay, isTimelineVisible } from './timeline-overlay.js';\nimport { SetupOverlay } from './setup-overlay.js';\n\ndeclare const __APP_VERSION__: string;\ndeclare const __BUILD_DATE__: string;\n\nconst log = createLogger('PWA');\n\n// ContentStore key prefix — mirrors PLAYER_API without leading slash\nconst STORE_PREFIX = PLAYER_API.slice(1);\n\n// Dynamic base path — same build serves /player/pwa/, /player/pwa-xmds/, /player/pwa-xlr/\nconst PLAYER_BASE = new URL('./', window.location.href).pathname.replace(/\\/$/, '');\n\n// Import core modules (will be loaded at runtime)\nlet cacheWidgetHtml: any;\nlet scheduleManager: any;\nlet config: any;\nlet RestClient: any;\nlet XmdsClient: any;\nlet ProtocolDetector: any;\nlet XmrWrapper: any;\nlet store: StoreClient;\nlet downloadManager: DownloadManager;\nlet StatsCollector: any;\nlet formatStats: any;\nlet LogReporter: any;\nlet formatLogs: any;\nlet DisplaySettings: any;\nlet SyncManager: any;\nlet computeStagger: any;\n\n// SDK package versions (populated in loadCoreModules)\nconst sdkVersions: Record<string, string> = {};\n\nclass PwaPlayer {\n private renderer!: RendererLite;\n private core!: PlayerCore;\n private xmds!: any;\n private downloadOverlay: DownloadOverlay | null = null;\n private timelineOverlay: TimelineOverlay | null = null;\n private setupOverlay: SetupOverlay | null = null;\n private statsCollector: any = null;\n private logReporter: any = null;\n private displaySettings: any = null;\n private currentScheduleId: number = -1; // Track scheduleId for stats\n private scheduledLayoutIds: Set<number> = new Set(); // Layout IDs from current schedule\n private preparingLayoutId: number | null = null; // Guard against concurrent prepareLayout calls\n private _pendingRetryLayoutId: number | null = null; // Queued retry when check-pending-layout arrives during preparation\n private _screenshotInterval: any = null;\n private _screenshotMethod: 'electron' | 'displayMedia' | 'html2canvas' | null = null;\n private _html2canvasMod: any = null;\n private _screenshotInFlight = false; // Concurrency guard — one capture at a time\n private _wakeLock: any = null; // Screen Wake Lock sentinel\n private syncManager: any = null; // Multi-display sync coordinator\n private _currentLayoutEnableStat: boolean = true; // enableStat from current layout XLF\n private _probeTimer: any = null; // Debounce timer for duration probing\n private _mediaStatusTimer: ReturnType<typeof setTimeout> | null = null; // Debounce timer for media status check\n private _pendingFollowerStats: any[] | null = null; // In-flight stats delegated to lead\n private _pendingFollowerLogs: any[] | null = null; // In-flight logs delegated to lead\n private _iframeObserver: MutationObserver | null = null; // Iframe key-forwarding observer\n private _swIcHandler: any = null; // SW Interactive Control message handler\n private _chunkConfig: any = null; // Device-adaptive chunk configuration\n private _fileIdToSaveAs: Map<string, string> = new Map(); // Numeric file ID → storedAs filename\n private _cachedMediaKeys: Set<string> = new Set(); // saveAs keys confirmed cached (avoids HEAD 404s)\n private protocolDetector: any = null; // CMS protocol auto-detector\n\n async init() {\n log.info('Initializing player with RendererLite + PlayerCore...');\n\n // Load core modules\n await this.loadCoreModules();\n\n // Register Service Worker for offline-first kiosk mode\n if ('serviceWorker' in navigator) {\n try {\n const registration = await navigator.serviceWorker.register(`${PLAYER_BASE}/sw-pwa.js?v=${Date.now()}`, {\n scope: `${PLAYER_BASE}/`,\n type: 'module',\n updateViaCache: 'none'\n });\n log.info('Service Worker registered for offline mode:', registration.scope);\n\n // Request persistent storage (kiosk requirement)\n if (navigator.storage && navigator.storage.persist) {\n const persistent = await navigator.storage.persist();\n if (persistent) {\n log.info('Persistent storage granted - cache won\\'t be evicted');\n } else {\n log.warn('Persistent storage denied - cache may be evicted');\n }\n }\n } catch (error) {\n log.warn('Service Worker registration failed:', error);\n }\n }\n\n // Initialize StoreClient (REST) + DownloadManager (main thread)\n log.info('Initializing cache clients...');\n store = new StoreClient();\n const { calculateChunkConfig } = await import('@xiboplayer/sw');\n this._chunkConfig = calculateChunkConfig(log);\n downloadManager = new DownloadManager({\n concurrency: this._chunkConfig.concurrency,\n chunkSize: this._chunkConfig.chunkSize,\n chunksPerFile: 2,\n });\n log.info('Cache clients ready — StoreClient + DownloadManager');\n\n // Create renderer\n const container = document.getElementById('player-container');\n if (!container) {\n throw new Error('No #player-container found');\n }\n\n this.renderer = new RendererLite(\n {\n cmsUrl: config.cmsUrl,\n hardwareKey: config.hardwareKey\n },\n container,\n {\n // Provide fileId→saveAs map for layout background resolution\n fileIdToSaveAs: this._fileIdToSaveAs,\n\n // Provide widget HTML resolver — check ContentStore via proxy\n getWidgetHtml: async (widget: any) => {\n const widgetPath = `${PLAYER_API}/widgets/${widget.layoutId}/${widget.regionId}/${widget.id}`;\n log.debug(`Looking for widget HTML at: ${widgetPath}`, widget);\n\n try {\n const exists = await store.has(`${STORE_PREFIX}/widgets`, `${widget.layoutId}/${widget.regionId}/${widget.id}`);\n if (exists) {\n log.debug(`Widget HTML found in store, using mirror URL for iframe`);\n return { url: widgetPath, fallback: widget.raw || '' };\n } else {\n log.warn(`No widget HTML found in store: ${widgetPath}`);\n }\n } catch (error) {\n log.error(`Failed to check widget HTML for ${widget.id}:`, error);\n }\n\n // Fallback to widget.raw (XLF template)\n log.warn(`Using widget.raw fallback for ${widget.id}`);\n return widget.raw || '';\n }\n }\n );\n\n // Create PlayerCore (with CMS-namespaced offline cache DB)\n this.core = new PlayerCore({\n config,\n xmds: this.xmds,\n cache: store,\n schedule: scheduleManager,\n renderer: this.renderer,\n xmrWrapper: XmrWrapper,\n statsCollector: this.statsCollector,\n displaySettings: this.displaySettings,\n cmsId: config.activeCmsId,\n });\n\n // Setup platform-specific event handlers\n this.setupCoreEventHandlers();\n this.setupRendererEventHandlers();\n this.setupInteractiveControl();\n this.setupDataConnectorNotify();\n this.setupRemoteControls();\n\n // Setup UI\n this.updateConfigDisplay();\n\n // Online/offline event listeners for seamless offline mode\n window.addEventListener('online', () => {\n log.info('Browser reports online — triggering immediate collection');\n this.updateStatus('Back online, syncing...');\n this.removeOfflineIndicator();\n this.core.collectNow().catch((error: any) => {\n log.error('Failed to collect after coming online:', error);\n });\n });\n window.addEventListener('offline', () => {\n log.warn('Browser reports offline — continuing playback with cached data');\n this.updateStatus('Offline mode — using cached content');\n this.showOfflineIndicator();\n });\n\n // Initialize download progress overlay (configurable debug feature)\n // Respect controls.keyboard.debugOverlays — if disabled, don't restore overlays\n const controls = this.getControls();\n const debugOverlaysEnabled = (controls.keyboard || {}).debugOverlays === true;\n\n const overlayConfig = getDefaultOverlayConfig();\n if (overlayConfig.enabled && debugOverlaysEnabled) {\n this.downloadOverlay = new DownloadOverlay(overlayConfig);\n this.downloadOverlay.setProgressCallback(() => downloadManager.getProgress());\n log.info('Download overlay enabled (hover bottom-right corner)');\n }\n\n // Timeline overlay — created on first T key press (or if previously visible)\n if (isTimelineVisible() && debugOverlaysEnabled) {\n this.timelineOverlay = new TimelineOverlay(true, (layoutId) => this.skipToLayout(layoutId));\n }\n\n // Listen for certificate warnings from Electron main process\n this.setupCertWarnings();\n\n // Listen for XMR connection status changes\n this.setupXmrWarning();\n\n // Request Screen Wake Lock to prevent display sleep\n await this.requestWakeLock();\n\n // Re-acquire wake lock when tab becomes visible again\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'visible') {\n this.requestWakeLock();\n }\n });\n\n // Start collection cycle\n await this.core.collect();\n\n log.info('Player initialized successfully');\n }\n\n /**\n * Request Screen Wake Lock to prevent display from sleeping\n * Re-acquired on visibility change (browser releases it when tab is hidden)\n */\n private async requestWakeLock() {\n if (!('wakeLock' in navigator)) {\n log.debug('Wake Lock API not supported');\n return;\n }\n\n try {\n this._wakeLock = await (navigator as any).wakeLock.request('screen');\n log.info('Screen Wake Lock acquired — display will stay on');\n\n this._wakeLock.addEventListener('release', () => {\n log.debug('Screen Wake Lock released');\n this._wakeLock = null;\n });\n } catch (error: any) {\n log.warn('Wake Lock request failed:', error?.message);\n }\n }\n\n /**\n * Listen for certificate warnings from Electron and show in the top bar.\n * The #overlay bar (defined in index.html) is the status bar with\n * #config-info (left) and #status (right). If it was removed (statusBarOnHover\n * not set), we recreate it. Cert warnings make the bar always visible.\n */\n private setupCertWarnings() {\n const warnedHosts = new Set<string>();\n\n window.addEventListener('cert-warning', ((e: CustomEvent) => {\n const { host, error } = e.detail;\n if (warnedHosts.has(host)) return;\n warnedHosts.add(host);\n\n log.warn(`Invalid SSL certificate accepted for stream: ${host} (${error})`);\n\n // Find or recreate the top bar\n let overlay = document.getElementById('overlay');\n let created = false;\n if (!overlay) {\n overlay = document.createElement('div');\n overlay.id = 'overlay';\n // Recreate child structure: config-info | status\n const info = document.createElement('div');\n info.id = 'config-info';\n overlay.appendChild(info);\n const status = document.createElement('div');\n status.id = 'status';\n overlay.appendChild(status);\n document.body.appendChild(overlay);\n created = true;\n }\n\n // Find or create the cert warning span between #config-info and #status\n let certSpan = document.getElementById('cert-warnings');\n if (!certSpan) {\n certSpan = document.createElement('span');\n certSpan.id = 'cert-warnings';\n certSpan.style.cssText = 'color: #ffaa33; flex: 0 0 auto;';\n const statusEl = document.getElementById('status');\n overlay.insertBefore(certSpan, statusEl);\n }\n\n const hosts = [...warnedHosts].join(', ');\n certSpan.textContent = `\\u26A0 SSL: ${hosts}`;\n\n // Don't force always-visible — let hover-only CSS handle show/hide\n\n // If we recreated the overlay, repopulate config info\n if (created) this.updateConfigDisplay();\n }) as EventListener);\n }\n\n /**\n * Show/hide an XMR disconnected warning in the top bar.\n * Placed before #cert-warnings (or before #status if no cert warnings).\n */\n private setupXmrWarning() {\n this.core.on('xmr-status', ({ connected }: { connected: boolean }) => {\n const overlay = document.getElementById('overlay');\n if (!overlay) return;\n\n let span = document.getElementById('xmr-warning');\n\n if (!connected) {\n if (!span) {\n span = document.createElement('span');\n span.id = 'xmr-warning';\n span.style.cssText = 'color: #ff6666; flex: 0 0 auto;';\n // Insert before cert-warnings or status (whichever comes first)\n const anchor = document.getElementById('cert-warnings') || document.getElementById('status');\n overlay.insertBefore(span, anchor);\n }\n span.textContent = '\\u26A0 XMR disconnected';\n } else {\n span?.remove();\n }\n });\n }\n\n /**\n * Load core modules\n */\n private async loadCoreModules() {\n try {\n const [\n cacheModule, xmdsModule, scheduleModule, configModule,\n xmrModule, statsModule, displaySettingsModule, coreModule,\n rendererModule, syncModule,\n ] = await Promise.all([\n import('@xiboplayer/cache'),\n import('@xiboplayer/xmds'),\n import('@xiboplayer/schedule'),\n import('@xiboplayer/utils'),\n import('@xiboplayer/xmr'),\n import('@xiboplayer/stats'),\n import('@xiboplayer/settings'),\n import('@xiboplayer/core'),\n import('@xiboplayer/renderer'),\n import('@xiboplayer/sync'),\n ]);\n\n cacheWidgetHtml = cacheModule.cacheWidgetHtml;\n SyncManager = syncModule.SyncManager;\n computeStagger = syncModule.computeStagger;\n scheduleManager = scheduleModule.scheduleManager;\n config = configModule.config;\n RestClient = xmdsModule.RestClient;\n XmdsClient = xmdsModule.XmdsClient;\n ProtocolDetector = xmdsModule.ProtocolDetector;\n XmrWrapper = xmrModule.XmrWrapper;\n StatsCollector = statsModule.StatsCollector;\n formatStats = statsModule.formatStats;\n LogReporter = statsModule.LogReporter;\n formatLogs = statsModule.formatLogs;\n DisplaySettings = displaySettingsModule.DisplaySettings;\n\n // Capture SDK package versions\n sdkVersions.core = coreModule.VERSION || '?';\n sdkVersions.cache = cacheModule.VERSION || '?';\n sdkVersions.renderer = rendererModule.VERSION || '?';\n sdkVersions.schedule = scheduleModule.VERSION || '?';\n sdkVersions.xmds = xmdsModule.VERSION || '?';\n sdkVersions.xmr = xmrModule.VERSION || '?';\n sdkVersions.utils = configModule.VERSION || '?';\n sdkVersions.stats = statsModule.VERSION || '?';\n sdkVersions.settings = displaySettingsModule.VERSION || '?';\n\n // Get MAC address from Electron if available (for WOL support)\n if ((window as any).electronAPI?.getSystemInfo) {\n try {\n const sysInfo = await (window as any).electronAPI.getSystemInfo();\n if (sysInfo.macAddress) {\n config.macAddress = sysInfo.macAddress;\n }\n } catch (_) { /* pure PWA — no Electron API */ }\n }\n\n // Transport selection:\n // transport: \"rest\" → forced REST API\n // transport: \"xmds\" → forced SOAP\n // transport: \"auto\" → probe REST → SOAP fallback (default)\n // /player/pwa-xmds/ → forced SOAP (URL-based override)\n // ?transport=xmds → forced SOAP (query param override)\n const cfgTransport = config.transport !== 'auto' ? config.transport : undefined;\n const urlTransport = new URLSearchParams(window.location.search).get('transport');\n const transport = urlTransport\n || (PLAYER_BASE.includes('pwa-xmds') ? 'xmds' : null)\n || cfgTransport\n || 'auto';\n\n // Use ProtocolDetector for auto-detection with re-probe support\n this.protocolDetector = new ProtocolDetector(config.cmsUrl, RestClient, XmdsClient);\n const forceProtocol = (transport === 'auto') ? undefined : transport;\n const { client } = await this.protocolDetector.detect(config, forceProtocol);\n this.xmds = client;\n\n // Initialize stats collector (namespaced by CMS ID)\n const cmsId = config.activeCmsId;\n this.statsCollector = new StatsCollector(cmsId);\n await this.statsCollector.init();\n log.info(`Stats collector initialized${cmsId ? ` (CMS: ${cmsId})` : ''}`);\n\n // Initialize log reporter for CMS log submission (namespaced by CMS ID)\n this.logReporter = new LogReporter(cmsId);\n await this.logReporter.init();\n log.info(`Log reporter initialized${cmsId ? ` (CMS: ${cmsId})` : ''}`);\n\n // Serialize log args to string (shared by log reporter and console forwarder)\n const serializeArgs = (args: any[]) => args.map((a: any) => typeof a === 'string' ? a : JSON.stringify(a)).join(' ');\n\n // Bridge logger output to LogReporter for CMS submission\n registerLogSink(({ level, name, args }: { level: string; name: string; args: any[] }) => {\n if (!this.logReporter) return;\n const message = serializeArgs(args);\n this.logReporter.log(level, `[${name}] ${message}`, 'PLAYER').catch(() => {});\n });\n\n // Forward console logs to proxy stdout (for journald/log analysis).\n // Controlled by debug.consoleLogs in config.json.\n // Optional debug.consoleLogsInterval (seconds) sets the batch flush interval (default 10s).\n const debugConfig = config.debug;\n if (debugConfig?.consoleLogs) {\n const flushIntervalMs = (debugConfig.consoleLogsInterval || 10) * 1000;\n let batch: Array<{ level: string; name: string; message: string; ts: string }> = [];\n let flushTimer: ReturnType<typeof setTimeout> | null = null;\n\n const flushLogs = () => {\n if (batch.length === 0) return;\n const payload = batch;\n batch = [];\n flushTimer = null;\n // Fire-and-forget POST — log forwarding must never block the player\n fetch('/debug/log', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n }).catch(() => {});\n };\n\n registerLogSink(({ level, name, args }: { level: string; name: string; args: any[] }) => {\n const message = serializeArgs(args);\n batch.push({ level, name, message, ts: new Date().toISOString() });\n if (!flushTimer) {\n flushTimer = setTimeout(flushLogs, flushIntervalMs);\n }\n });\n\n log.info(`Console log forwarding to proxy enabled (flush every ${flushIntervalMs / 1000}s)`);\n }\n\n // Initialize display settings manager\n this.displaySettings = new DisplaySettings();\n log.info('Display settings manager initialized');\n\n // Log version and environment information for debugging\n const buildDate = typeof __BUILD_DATE__ !== 'undefined' ? __BUILD_DATE__ : '?';\n const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '?';\n log.info(`v${appVersion} built ${buildDate}`);\n const versionParts = Object.entries(sdkVersions).map(([k, v]) => `${k}=${v}`).join(' ');\n log.info(`SDK: ${versionParts}`);\n const isElectron = !!(window as any).electronAPI;\n const electronVersion = isElectron ? (navigator.userAgent.match(/Electron\\/([\\d.]+)/)?.[1] || '?') : null;\n const chromeVersion = navigator.userAgent.match(/Chrome\\/([\\d.]+)/)?.[1] || '?';\n const platform = isElectron ? `Electron ${electronVersion} / Chrome ${chromeVersion}` : `Chrome ${chromeVersion}`;\n log.info(`Env: PWA v${appVersion} | ${platform} | ${navigator.platform} | ${screen.width}x${screen.height}`);\n\n log.info('Core modules loaded');\n } catch (error) {\n log.error('Failed to load core modules:', error);\n throw error;\n }\n }\n\n /**\n * Setup PlayerCore event handlers (Platform-specific UI updates)\n */\n private setupCoreEventHandlers() {\n // Delegate to focused handler groups\n this.setupSyncEventHandlers();\n this.setupDownloadEventHandlers();\n this.setupCommandEventHandlers();\n\n // Collection events\n this.core.on(E.COLLECTION_START, () => {\n this.updateStatus('Collecting data from CMS...');\n });\n\n this.core.on(E.REGISTER_COMPLETE, (regResult: any) => {\n const displayName = this.displaySettings?.getDisplayName() || regResult.displayName || config.hardwareKey;\n this.updateStatus(`Registered: ${displayName}`);\n\n // Update page title with display name\n if (this.displaySettings) {\n document.title = `Xibo Player - ${this.displaySettings.getDisplayName()}`;\n }\n\n // Set display location from CMS settings\n const lat = parseFloat(regResult?.settings?.latitude);\n const lng = parseFloat(regResult?.settings?.longitude);\n if (lat && lng && !isNaN(lat) && !isNaN(lng)) {\n log.info(`Display location from CMS: ${lat.toFixed(4)}, ${lng.toFixed(4)}`);\n if (scheduleManager?.setLocation) {\n scheduleManager.setLocation(lat, lng);\n }\n } else if (this.core.requestGeoLocation) {\n // No CMS coordinates — try browser Geolocation API as fallback\n log.info('No CMS coordinates, requesting browser geolocation...');\n this.core.requestGeoLocation();\n }\n\n // Multi-display sync: local config fallback when CMS doesn't provide syncConfig\n if (!regResult.syncConfig && config.data?.sync) {\n log.info('[Sync] Using local sync config (CMS did not provide syncConfig)');\n this.core.syncConfig = config.data.sync;\n this.core.emit(E.SYNC_CONFIG, config.data.sync);\n }\n });\n\n // NOTE: Two OFFLINE_MODE listeners are intentional — this one handles UI,\n // setupSyncEventHandlers() registers a second one for sync bootstrap.\n this.core.on(E.OFFLINE_MODE, (isOffline: boolean) => {\n if (isOffline) {\n this.updateStatus('Offline mode — using cached content');\n this.showOfflineIndicator();\n } else {\n this.updateStatus('Back online');\n this.removeOfflineIndicator();\n }\n });\n\n this.core.on(E.SCHEDULE_RECEIVED, (schedule: any) => {\n this.updateStatus('Processing schedule...');\n\n // Extract scheduleId for stats tracking\n // Check layouts or campaigns for scheduleId\n if (schedule.layouts && schedule.layouts.length > 0) {\n this.currentScheduleId = parseInt(schedule.layouts[0].scheduleid) || -1;\n } else if (schedule.campaigns && schedule.campaigns.length > 0) {\n this.currentScheduleId = parseInt(schedule.campaigns[0].scheduleid) || -1;\n }\n\n // Selectively clear preloaded layouts not in the new schedule.\n // Keep warm entries whose layout ID is still scheduled — their DOM is still valid.\n // (The CMS schedule CRC changes every collection due to timestamps, even when\n // the actual layout list hasn't changed. Blindly clearing would destroy preloads.)\n if (this.renderer?.layoutPool) {\n const scheduledIds = new Set<number>();\n if (schedule.layouts) {\n for (const l of schedule.layouts) {\n const id = parseLayoutFile(l.file || l.id || l);\n if (id) scheduledIds.add(id);\n }\n }\n if (schedule.campaigns) {\n for (const c of schedule.campaigns) {\n if (c.layouts) {\n for (const l of c.layouts) {\n const id = parseLayoutFile(l.file || l.id || l);\n if (id) scheduledIds.add(id);\n }\n }\n }\n }\n const cleared = this.renderer.layoutPool.clearWarmNotIn(scheduledIds);\n if (cleared > 0) {\n log.info(`Cleared ${cleared} preloaded layout(s) no longer in schedule`);\n }\n this.scheduledLayoutIds = scheduledIds;\n }\n\n log.debug('Current scheduleId for stats:', this.currentScheduleId);\n });\n\n this.core.on(E.LAYOUT_PREPARE_REQUEST, async (layoutId: number) => {\n await this.prepareLayout(layoutId);\n // Non-sync or no layout playing yet: show immediately.\n // Sync transitions: onLayoutShow handles showing with stagger.\n if (!this.syncManager || this.renderer.getCurrentLayoutId() === null) {\n this.renderer.showLayout(layoutId);\n }\n });\n\n this.core.on(E.LAYOUT_ALREADY_PLAYING, (layoutId: number) => {\n // The schedule says this layout should still be playing. Verify the renderer\n // actually has an active timer — if not, the renderer stalled (e.g. after a\n // GPU crash/recovery or restart) and we need to force a re-show.\n if (!this.renderer.hasActiveLayoutTimer()) {\n log.warn(`Layout ${layoutId} has no active timer — restarting layout`);\n this.renderer.stopCurrentLayout();\n // stopCurrentLayout → layoutEnd → advanceToNextLayout → re-prepares + shows\n }\n });\n\n this.core.on(E.LAYOUT_EXPIRE_CURRENT, () => {\n log.info('Schedule changed — expiring current layout');\n this.renderer.stopCurrentLayout();\n // stopCurrentLayout() emits layoutEnd → the layoutEnd handler\n // calls advanceToNextLayout() which picks the next scheduled layout\n });\n\n this.core.on(E.NO_LAYOUTS_SCHEDULED, () => {\n this.updateStatus('No layouts scheduled');\n });\n\n this.core.on(E.COLLECTION_COMPLETE, () => {\n const layoutId = this.core.getCurrentLayoutId();\n if (layoutId) {\n this.updateStatus(`Playing layout ${layoutId}`);\n } else if (this.preparingLayoutId) {\n this.updateStatus(`Downloading layout ${this.preparingLayoutId}...`);\n }\n\n // Duration probing is handled by the debounced re-probe (3s after last\n // file cached) — avoids 404s from probing before downloads complete.\n });\n\n this.core.on(E.COLLECTION_ERROR, async (error: any) => {\n this.updateStatus(`Collection error: ${error}`, 'error');\n\n // Display not found / not authorized — show setup screen so user can re-register\n const msg = error?.message || String(error);\n if (msg.includes('403') && (msg.includes('Display not found') || msg.includes('not authorized'))) {\n log.warn('Display not registered or not authorized — showing setup screen');\n if (!this.setupOverlay) {\n this.setupOverlay = new SetupOverlay();\n }\n this.setupOverlay.show();\n return;\n }\n\n // Report fault to CMS (triggers dashboard alert)\n this.reportFault('COLLECTION_FAILED', `Collection cycle failed: ${error?.message || error}`);\n });\n\n this.core.on(E.XMR_CONNECTED, (url: string) => {\n log.info('XMR connected:', url);\n });\n\n this.core.on(E.XMR_MISCONFIGURED, (info: { reason: string; url?: string; message: string }) => {\n log.warn(`XMR misconfigured (${info.reason}): ${info.message}`);\n });\n\n // Log level changes from CMS (overlays are controlled by config.controls, not log level)\n this.core.on(E.LOG_LEVEL_CHANGED, () => {\n log.info(`Log level changed`);\n });\n\n // Overlay layout push from XMR\n this.core.on(E.OVERLAY_LAYOUT_REQUEST, async (layoutId: number) => {\n log.info('Overlay layout requested:', layoutId);\n // Re-use existing overlay rendering (schedule-driven overlays already work)\n // Just need to prepare and render the overlay layout\n await this.prepareLayout(layoutId);\n this.renderer.showLayout(layoutId);\n });\n\n // Revert to schedule (undo XMR layout override)\n this.core.on(E.REVERT_TO_SCHEDULE, () => {\n log.info('Reverting to scheduled content');\n this.updateStatus('Reverting to schedule...');\n });\n\n // Display settings events\n if (this.displaySettings) {\n this.displaySettings.on('interval-changed', (newInterval: number) => {\n log.info(`Collection interval changed to ${newInterval}s`);\n });\n\n this.displaySettings.on('settings-applied', (_settings: any, changes: string[]) => {\n if (changes.length > 0) {\n log.info('Settings updated from CMS:', changes.join(', '));\n }\n // Start periodic screenshots once we have settings (only first time)\n if (!this._screenshotInterval) {\n this.startScreenshotInterval();\n }\n });\n }\n\n // Stats submission\n this.core.on(E.SUBMIT_STATS_REQUEST, async () => {\n await this.submitStats();\n });\n\n // Log submission to CMS\n this.core.on(E.SUBMIT_LOGS_REQUEST, async () => {\n await this.submitLogs();\n });\n\n // Screenshot capture (triggered by XMR or periodic interval)\n this.core.on(E.SCREENSHOT_REQUEST, async () => {\n await this.captureAndSubmitScreenshot();\n });\n\n // Handle check-pending-layout events — layout was pending download, now ready\n this.core.on(E.CHECK_PENDING_LAYOUT, async (layoutId: number) => {\n await this.prepareLayout(layoutId);\n this.renderer.showLayout(layoutId);\n });\n\n // Navigate to widget (navWidget action via triggerCode from schedule-level actions)\n this.core.on(E.NAVIGATE_TO_WIDGET, (action: any) => {\n if (action.targetId) {\n this.renderer.navigateToWidget(action.targetId);\n } else {\n log.warn('navigate-to-widget action has no targetId:', action);\n }\n });\n\n // Timeline overlay — visualize upcoming schedule\n this.core.on(E.TIMELINE_UPDATED, (timeline: any[]) => {\n const id = this.core.getCurrentLayoutId();\n const dur = id ? this.core.getLayoutDuration(id) : undefined;\n this.timelineOverlay?.update(timeline, id, dur);\n });\n }\n\n /**\n * Setup multi-display sync event handlers.\n * Handles SYNC_CONFIG and offline sync fallback via OFFLINE_MODE.\n */\n private setupSyncEventHandlers() {\n // Offline sync: if CMS is unreachable but local config has sync settings,\n // start SyncManager so LAN-only displays can still sync with each other.\n this.core.on(E.OFFLINE_MODE, (isOffline: boolean) => {\n if (isOffline && !this.syncManager && config.data?.sync) {\n log.info('[Sync] Offline mode with local sync config — starting sync');\n this.core.syncConfig = config.data.sync;\n this.core.emit(E.SYNC_CONFIG, config.data.sync);\n }\n });\n\n // Multi-display sync: create SyncManager when CMS provides sync config (or local fallback)\n this.core.on(E.SYNC_CONFIG, async (syncConfig: any) => {\n if (this.syncManager) {\n this.syncManager.stop();\n }\n\n // Cross-device sync: build WebSocket relay URL.\n // Always rebuild for followers (mDNS re-discovers lead IP/port each cycle).\n // Lead connects to its own relay (localhost).\n if (syncConfig.syncPublisherPort) {\n if (syncConfig.syncGroupId) {\n syncConfig.syncGroup = String(syncConfig.syncGroupId);\n }\n\n if (syncConfig.isLead) {\n syncConfig.relayUrl = `ws://localhost:${syncConfig.syncPublisherPort}/sync`;\n // Trigger mDNS advertisement so followers can discover us\n fetch('/system/advertise-sync', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ syncGroupId: syncConfig.syncGroupId, port: syncConfig.syncPublisherPort, displayId: config.hardwareKey }),\n }).catch(() => {});\n } else {\n // Try mDNS discovery first, fall back to CMS-provided IP\n let leadHost = syncConfig.syncGroup;\n try {\n const res = await fetch(`/system/discover-lead?syncGroupId=${syncConfig.syncGroupId}`);\n if (res.ok) {\n const { host, port } = await res.json();\n leadHost = host;\n log.info(`mDNS discovered lead at ${host}:${port}`);\n }\n } catch (_) {\n log.warn('mDNS discovery failed, using CMS-provided IP');\n }\n syncConfig.relayUrl = `ws://${leadHost}:${syncConfig.syncPublisherPort}/sync`;\n }\n }\n\n // Persist resolved sync config to config.json so offline restarts\n // can sync over LAN without CMS. Strips runtime-only fields.\n // Merge with existing sync config to preserve local-only fields\n // (topology, choreography, staggerMs, gridCols, gridRows).\n const { syncToken, ...persistable } = syncConfig;\n const merged = { ...(config.data?.sync || {}), ...persistable };\n if ((window as any).electronAPI?.setConfig) {\n (window as any).electronAPI.setConfig({ sync: merged });\n } else {\n fetch('/config', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sync: merged }),\n }).catch(() => {});\n }\n\n // Pass CMS server key as sync token for relay auth (shared by all displays on this CMS)\n if (!syncConfig.syncToken) {\n syncConfig.syncToken = config.cmsKey;\n }\n\n this.syncManager = new SyncManager({\n displayId: config.hardwareKey,\n syncConfig,\n onLayoutChange: async (layoutId: string) => {\n // Wall mode: map lead's layout to this display's position-specific layout\n const layoutMap = syncConfig.layoutMap || config.sync?.layoutMap;\n const mappedId = layoutMap?.[layoutId] ?? layoutId;\n if (mappedId !== layoutId) {\n log.info(`[Sync] Wall mode: lead layout ${layoutId} → local layout ${mappedId}`);\n }\n // Follower: preload layout hidden, show comes from onLayoutShow\n log.info(`[Sync] Preparing layout ${mappedId} (waiting for show signal)`);\n await this.prepareLayout(parseInt(String(mappedId), 10));\n // Report ready to lead (use lead's layoutId so lead can track readiness)\n this.syncManager?.reportReady(layoutId);\n },\n onLayoutShow: (layoutId: string) => {\n // Map lead's layout ID to this display's layout (wall mode)\n const layoutMap = syncConfig.layoutMap || config.sync?.layoutMap;\n const mappedId = layoutMap?.[layoutId] ?? layoutId;\n const numericId = parseInt(String(mappedId), 10);\n\n // Compute choreography stagger delay (0 if no choreography configured)\n const choreo = syncConfig.choreography || 'simultaneous';\n const staggerMs = syncConfig.staggerMs ?? 150;\n\n // Build stagger options: prefer 2D topology, fall back to 1D position\n const staggerOpts: any = { choreography: choreo, staggerMs };\n if (syncConfig.topology) {\n staggerOpts.topology = syncConfig.topology;\n staggerOpts.gridCols = syncConfig.gridCols ?? 1;\n staggerOpts.gridRows = syncConfig.gridRows ?? 1;\n } else {\n staggerOpts.position = syncConfig.position ?? 0;\n staggerOpts.totalDisplays = syncConfig.totalDisplays ?? 1;\n }\n const stagger = computeStagger(staggerOpts);\n\n if (stagger > 0) {\n log.info(`[Sync] Show layout ${numericId} with ${stagger}ms choreography delay (${choreo})`);\n setTimeout(() => this.renderer.showLayout(numericId), stagger);\n } else {\n log.info(`[Sync] Show layout ${numericId}`);\n this.renderer.showLayout(numericId);\n }\n },\n onVideoStart: (layoutId: string, regionId: string) => {\n // Resume paused video in the specified region\n log.info(`[Sync] Video start: layout ${layoutId} region ${regionId}`);\n this.renderer.resumeRegionMedia?.(regionId);\n },\n // Lead: follower delegated stats — submit on their behalf\n onStatsReport: async (followerId: string, statsXml: string, ack: () => void) => {\n log.info(`[Sync] Submitting stats for follower ${followerId}`);\n try {\n const success = await this.xmds.submitStats(statsXml, followerId);\n if (success) ack();\n } catch (err: any) {\n log.warn(`[Sync] Stats submission failed for follower ${followerId}:`, err);\n }\n },\n // Lead: follower delegated logs — submit on their behalf\n onLogsReport: async (followerId: string, logsXml: string, ack: () => void) => {\n log.info(`[Sync] Submitting logs for follower ${followerId}`);\n try {\n const success = await this.xmds.submitLog(logsXml, followerId);\n if (success) ack();\n } catch (err: any) {\n log.warn(`[Sync] Log submission failed for follower ${followerId}:`, err);\n }\n },\n // Follower: lead confirmed our stats were submitted\n onStatsAck: async (_displayId: string) => {\n log.info('[Sync] Lead confirmed stats submission');\n if (this._pendingFollowerStats && this.statsCollector) {\n await this.statsCollector.clearSubmittedStats(this._pendingFollowerStats);\n this._pendingFollowerStats = null;\n }\n },\n // Follower: lead confirmed our logs were submitted\n onLogsAck: async (_displayId: string) => {\n log.info('[Sync] Lead confirmed logs submission');\n if (this._pendingFollowerLogs && this.logReporter) {\n await this.logReporter.clearSubmittedLogs(this._pendingFollowerLogs);\n this._pendingFollowerLogs = null;\n }\n },\n // Relay: group membership changed (auto-detect totalDisplays)\n onGroupUpdate: (totalDisplays: number, topology: Record<string, any>) => {\n log.info(`[Sync] Group update: ${totalDisplays} displays, topology: ${JSON.stringify(topology)}`);\n syncConfig.totalDisplays = totalDisplays;\n },\n });\n this.core.setSyncManager(this.syncManager);\n this.syncManager.start();\n log.info(`[Sync] SyncManager started as ${syncConfig.isLead ? 'LEAD' : 'FOLLOWER'}`);\n this.updateConfigDisplay();\n });\n }\n\n /**\n * Setup download and cache event handlers.\n * Handles FILES_RECEIVED, DOWNLOAD_REQUEST, PURGE_REQUEST, PURGE_ALL_REQUEST.\n */\n private setupDownloadEventHandlers() {\n this.core.on(E.FILES_RECEIVED, (files: any[]) => {\n this.updateStatus(`Downloading ${files.length} files...`);\n });\n\n this.core.on(E.DOWNLOAD_REQUEST, async (groupedFiles: any) => {\n // Download orchestration runs in main thread — no SW messaging\n this.downloadOverlay?.startUpdating();\n try {\n // Push current JWT token to proxy for cache-through CMS requests\n const token = this.xmds?.getToken?.() || null;\n if (token) {\n await fetch('/auth-token', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ token }),\n });\n }\n await this.enqueueDownloads(groupedFiles);\n log.info('Download enqueue complete');\n } catch (error) {\n log.error('Download request failed:', error);\n this.updateStatus('Download failed: ' + error, 'error');\n }\n });\n\n this.core.on(E.PURGE_REQUEST, async (purgeFiles: any[]) => {\n try {\n const result = await store.remove(purgeFiles);\n log.info(`Purge complete: ${result.deleted}/${result.total} files deleted`);\n } catch (error) {\n log.warn('Purge failed:', error);\n }\n });\n\n this.core.on(E.PURGE_ALL_REQUEST, async () => {\n log.info('Purging all cached content...');\n this.updateStatus('Purging cache...');\n try {\n // Delete all files from ContentStore\n const allFiles = await store.list();\n if (allFiles.length > 0) {\n const result = await store.remove(allFiles);\n log.info(`Purged ${result.deleted} files from ContentStore`);\n }\n // Clean up any legacy Cache API caches (pre-ContentStore migration)\n const cacheNames = await caches.keys();\n if (cacheNames.length > 0) {\n await Promise.all(cacheNames.map(name => caches.delete(name)));\n log.info(`Purged ${cacheNames.length} legacy caches`);\n }\n } catch (error) {\n log.error('Cache purge failed:', error);\n }\n });\n }\n\n /**\n * Setup command execution event handlers.\n * Handles EXECUTE_NATIVE_COMMAND, COMMAND_RESULT, SCHEDULED_COMMAND.\n */\n private setupCommandEventHandlers() {\n // Native command execution (#202) — shell commands delegated by PlayerCore\n // Electron: use IPC (in-process, faster). Chromium/other: HTTP to proxy server.\n this.core.on(E.EXECUTE_NATIVE_COMMAND, async (data: any) => {\n let result;\n if ((window as any).electronAPI?.executeShellCommand) {\n result = await (window as any).electronAPI.executeShellCommand({\n commandString: data.commandString,\n });\n } else {\n try {\n const resp = await fetch('/shell-command', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ commandString: data.commandString }),\n });\n result = await resp.json();\n } catch (err: any) {\n result = { success: false, reason: err.message };\n }\n }\n this.core.emit(E.COMMAND_RESULT, { code: data.code, ...result });\n });\n\n // Command execution result\n this.core.on(E.COMMAND_RESULT, (result: any) => {\n log.info('Command result:', result);\n if (!result.success) {\n this.reportFault('COMMAND_FAILED', `Command ${result.code} failed: ${result.reason || 'unknown'}`);\n }\n });\n\n // Scheduled commands (#17) — execute commands whose scheduled time has arrived\n this.core.on(E.SCHEDULED_COMMAND, (command: any) => {\n log.info(`Scheduled command: ${command.code}`);\n this.core.executeCommand(command.code);\n });\n }\n\n\n /**\n * Setup Interactive Control handler (receives messages from SW for widget IC requests)\n * IC library in widget iframes makes XHR to /player/pwa/ic/*, SW forwards here.\n */\n private setupInteractiveControl() {\n this._swIcHandler = (event: any) => {\n if (event.data?.type !== 'INTERACTIVE_CONTROL') return;\n\n const { method, path, search, body } = event.data;\n const port = event.ports?.[0];\n if (!port) return;\n\n const response = this.handleInteractiveControl(method, path, search, body);\n port.postMessage(response);\n };\n navigator.serviceWorker?.addEventListener('message', this._swIcHandler);\n }\n\n /**\n * Notify widget iframes when DataConnector data changes.\n * XIC library listens for postMessage { ctrl: 'rtNotifyData', data: { dataKey } }\n * and calls the widget's registered notifyData callback.\n */\n private setupDataConnectorNotify() {\n const dcManager = this.core.getDataConnectorManager();\n dcManager.on('data-changed', (dataKey: string) => {\n const iframes = document.querySelectorAll<HTMLIFrameElement>('iframe');\n const message = { ctrl: 'rtNotifyData', data: { dataKey } };\n for (const iframe of iframes) {\n try {\n iframe.contentWindow?.postMessage(message, '*');\n } catch { /* cross-origin iframe, ignore */ }\n }\n });\n }\n\n /**\n * Setup keyboard and presenter remote controls.\n * Handles arrow keys, page up/down, space for next/prev/pause,\n * and MediaSession API for multimedia keyboard keys.\n */\n private setupRemoteControls() {\n // Keep focus on main document so keyboard shortcuts work even with widget iframes.\n // Iframes steal focus — this pulls it back after a short delay so interactive\n // widgets still work momentarily but keyboard control returns to the player.\n window.addEventListener('blur', () => {\n // Don't steal focus when setup overlay is open (user is typing in iframe inputs)\n if (this.setupOverlay?.isVisible()) return;\n setTimeout(() => window.focus(), 200);\n });\n\n // Forward keyboard events from widget iframes to the main document.\n // Iframes have their own document, so keydown on the parent never fires\n // when an iframe has focus. We observe new iframes and attach forwarders.\n const attachIframeKeyForwarder = (iframe: HTMLIFrameElement) => {\n const tryAttach = () => {\n try {\n const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;\n if (!iframeDoc) return;\n if ((iframe as any).__keyForwarderAttached) return;\n (iframe as any).__keyForwarderAttached = true;\n iframeDoc.addEventListener('keydown', (e: KeyboardEvent) => {\n // Don't forward keys from setup overlay — user is typing in form inputs\n if (this.setupOverlay?.isVisible()) return;\n // Re-dispatch on the main document so our handler fires\n const clone = new KeyboardEvent('keydown', {\n key: e.key, code: e.code, keyCode: e.keyCode,\n ctrlKey: e.ctrlKey, shiftKey: e.shiftKey, altKey: e.altKey, metaKey: e.metaKey,\n bubbles: true, cancelable: true,\n });\n if (document.dispatchEvent(clone)) return; // not prevented\n e.preventDefault();\n });\n } catch { /* cross-origin iframe, ignore */ }\n };\n iframe.addEventListener('load', tryAttach);\n tryAttach();\n };\n\n // Attach to existing and future iframes\n Array.from(document.querySelectorAll('iframe')).forEach(f => attachIframeKeyForwarder(f as HTMLIFrameElement));\n this._iframeObserver = new MutationObserver((mutations) => {\n for (const m of mutations) {\n for (const node of m.addedNodes) {\n if (node instanceof HTMLIFrameElement) attachIframeKeyForwarder(node);\n if (node instanceof HTMLElement) {\n node.querySelectorAll('iframe').forEach(f => attachIframeKeyForwarder(f as HTMLIFrameElement));\n }\n }\n }\n });\n this._iframeObserver.observe(document.body, { childList: true, subtree: true });\n\n // Read control toggles from config (injected by proxy into localStorage)\n const controls = this.getControls();\n const { keyboard: kb = {} } = controls;\n const debugOverlays = kb.debugOverlays === true;\n const setupKey = kb.setupKey === true;\n const playbackControl = kb.playbackControl === true;\n const videoControls = kb.videoControls === true;\n\n // Keyboard / presenter remote (clicker) controls\n document.addEventListener('keydown', (e: KeyboardEvent) => {\n // Ctrl+Q — quit (Chromium kiosk: calls server /quit; Electron: handled by menu accelerator)\n if (e.key === 'q' && (e.ctrlKey || e.metaKey)) {\n e.preventDefault();\n log.info('[Remote] Quit requested (Ctrl+Q)');\n fetch('/quit', { method: 'POST' }).catch(() => {});\n return;\n }\n\n switch (e.key) {\n case 't':\n case 'T':\n if (!debugOverlays) break;\n if (!this.timelineOverlay) {\n this.timelineOverlay = new TimelineOverlay(true, (layoutId) => this.skipToLayout(layoutId));\n }\n this.timelineOverlay.toggle();\n break;\n case 'd':\n case 'D':\n if (!debugOverlays) break;\n if (!this.downloadOverlay) {\n this.downloadOverlay = new DownloadOverlay({ enabled: true, autoHide: false });\n this.downloadOverlay.setProgressCallback(() => downloadManager.getProgress());\n }\n this.downloadOverlay.toggle();\n break;\n case 'v':\n case 'V': {\n if (!videoControls) break;\n // Collect videos from parent + all same-origin iframes (widget regions)\n const allVideos: HTMLVideoElement[] = [...document.querySelectorAll<HTMLVideoElement>('video')];\n document.querySelectorAll<HTMLIFrameElement>('iframe').forEach(iframe => {\n try { allVideos.push(...iframe.contentDocument!.querySelectorAll<HTMLVideoElement>('video')); } catch {}\n });\n const show = allVideos.length > 0 && !allVideos[0].controls;\n allVideos.forEach(v => v.controls = show);\n break;\n }\n // Playback control: next/prev/pause\n case 'ArrowRight':\n case 'PageDown':\n if (!playbackControl) break;\n log.info('[Remote] Next layout (keyboard)');\n this.core.advanceToNextLayout();\n e.preventDefault();\n break;\n case 'ArrowLeft':\n case 'PageUp':\n if (!playbackControl) break;\n log.info('[Remote] Previous layout (keyboard)');\n this.core.advanceToPreviousLayout();\n e.preventDefault();\n break;\n case ' ':\n if (!playbackControl) break;\n log.info('[Remote] Toggle pause (keyboard)');\n if (this.renderer.isPaused()) {\n this.renderer.resume();\n } else {\n this.renderer.pause();\n }\n e.preventDefault();\n break;\n case 'r':\n case 'R':\n if (!playbackControl) break;\n if (this.core.isLayoutOverridden()) {\n log.info('[Remote] Revert to schedule (keyboard)');\n this.core.revertToSchedule();\n }\n break;\n case 's':\n case 'S':\n if (!setupKey) break;\n if (!this.setupOverlay) {\n this.setupOverlay = new SetupOverlay();\n }\n this.setupOverlay.toggle();\n e.preventDefault(); // prevent 's' from being typed into the focused input\n break;\n }\n });\n\n // MediaSession API for multimedia keys (only fires when media is active)\n if (playbackControl && 'mediaSession' in navigator) {\n navigator.mediaSession.setActionHandler('nexttrack', () => {\n log.info('[Remote] Next layout (MediaSession)');\n this.core.advanceToNextLayout();\n });\n navigator.mediaSession.setActionHandler('previoustrack', () => {\n log.info('[Remote] Previous layout (MediaSession)');\n this.core.advanceToPreviousLayout();\n });\n navigator.mediaSession.setActionHandler('pause', () => {\n log.info('[Remote] Pause (MediaSession)');\n this.renderer.pause();\n });\n navigator.mediaSession.setActionHandler('play', () => {\n log.info('[Remote] Resume (MediaSession)');\n this.renderer.resume();\n });\n }\n\n log.info('Remote controls initialized (keyboard + MediaSession)');\n }\n\n /** Read controls config (injected by proxy from config.json into localStorage). */\n private getControls(): Record<string, any> {\n return config.controls;\n }\n\n /**\n * Skip to a specific layout by ID (from timeline click or XMR command).\n * Uses changeLayout() which sets a layout override — press R to revert to schedule.\n */\n private skipToLayout(layoutId: number) {\n log.info(`Skipping to layout ${layoutId} (timeline click)`);\n this.core.changeLayout(layoutId);\n }\n\n private parseBody(body: string | null): any {\n try { return body ? JSON.parse(body) : {}; } catch (_) { return {}; }\n }\n\n /**\n * Handle an Interactive Control request from a widget\n */\n private handleInteractiveControl(method: string, path: string, search: string, body: string | null): any {\n log.debug('IC request:', method, path, search);\n\n switch (path) {\n case '/info':\n return {\n status: 200,\n body: JSON.stringify({\n hardwareKey: config.hardwareKey,\n displayName: config.displayName,\n playerType: 'pwa',\n currentLayoutId: this.core.getCurrentLayoutId()\n })\n };\n\n case '/trigger': {\n const data = this.parseBody(body);\n // Forward to renderer for layout-level actions (widget navigation)\n this.renderer.emit('interactiveTrigger', {\n targetId: data.id,\n triggerCode: data.trigger\n });\n // Forward to core for schedule-level actions (layout navigation)\n if (data.trigger) {\n this.core.handleTrigger(data.trigger);\n }\n return { status: 200, body: 'OK' };\n }\n\n case '/duration/expire': {\n const data = this.parseBody(body);\n log.info('IC: Widget duration expire requested for', data.id);\n this.renderer.emit('widgetExpire', { widgetId: data.id });\n return { status: 200, body: 'OK' };\n }\n\n case '/duration/extend': {\n const data = this.parseBody(body);\n log.info('IC: Widget duration extend by', data.duration, 'for', data.id);\n this.renderer.emit('widgetExtendDuration', {\n widgetId: data.id,\n duration: parseInt(data.duration)\n });\n return { status: 200, body: 'OK' };\n }\n\n case '/duration/set': {\n const data = this.parseBody(body);\n log.info('IC: Widget duration set to', data.duration, 'for', data.id);\n this.renderer.emit('widgetSetDuration', {\n widgetId: data.id,\n duration: parseInt(data.duration)\n });\n return { status: 200, body: 'OK' };\n }\n\n case '/fault': {\n const data = this.parseBody(body);\n this.reportFault(data.code || 'WIDGET_FAULT', data.reason || 'Widget reported fault', {\n layoutId: data.layoutId,\n regionId: data.regionId,\n widgetId: data.widgetId\n });\n return { status: 200, body: 'OK' };\n }\n\n case '/realtime': {\n const params = new URLSearchParams(search);\n const dataKey = params.get('dataKey');\n log.debug('IC: Realtime data request for key:', dataKey);\n\n if (!dataKey) {\n return { status: 400, body: JSON.stringify({ error: 'Missing dataKey parameter' }) };\n }\n\n const dcManager = this.core.getDataConnectorManager();\n const connectorData = dcManager.getData(dataKey);\n\n if (connectorData === null) {\n return { status: 404, body: JSON.stringify({ error: `No data available for key: ${dataKey}` }) };\n }\n\n const responseBody = typeof connectorData === 'string' ? connectorData : JSON.stringify(connectorData);\n return { status: 200, body: responseBody };\n }\n\n case '/criteria': {\n // Return display properties/criteria that widgets can query\n // Used by widgets to adapt content based on display characteristics\n return {\n status: 200,\n body: JSON.stringify({\n displayId: config.displayId,\n hardwareKey: config.hardwareKey,\n displayName: config.displayName,\n width: window.innerWidth,\n height: window.innerHeight,\n latitude: config.latitude || null,\n longitude: config.longitude || null,\n playerType: 'pwa'\n })\n };\n }\n\n default:\n return { status: 404, body: JSON.stringify({ error: 'Unknown IC route' }) };\n }\n }\n\n /**\n * Notify PlayerCore that a file download completed.\n * Called directly from enqueueDownloads() — no SW messaging needed.\n */\n private notifyFileCached(fileId: string, fileType: string) {\n log.debug(`Download complete: ${fileType}/${fileId}`);\n\n if (fileType === 'layout') {\n this.core.notifyMediaReady(parseInt(fileId), fileType);\n } else if (fileType === 'media') {\n // Pass saveAs string for media files (matches pendingLayouts entries)\n const saveAs = this._fileIdToSaveAs.get(fileId) || fileId;\n this._cachedMediaKeys.add(saveAs);\n this.core.notifyMediaReady(saveAs, fileType);\n } else {\n // Dependencies, widgets, datasets — track by storeKey\n this._cachedMediaKeys.add(fileId);\n }\n\n // Debounced duration probe — run after downloads settle\n if (this._probeTimer) clearTimeout(this._probeTimer);\n this._probeTimer = setTimeout(() => {\n this._probeTimer = null;\n this.probeLayoutDurations().catch(() => {});\n }, 3000);\n\n // Debounced media status check — update timeline missing-media annotations\n if (this._mediaStatusTimer) clearTimeout(this._mediaStatusTimer);\n this._mediaStatusTimer = setTimeout(() => {\n this._mediaStatusTimer = null;\n this.checkTimelineMediaStatus().catch(() => {});\n }, 2000);\n }\n\n /**\n * Enqueue files for download — runs in main thread, no SW messaging.\n * Ported from MessageHandler.handleDownloadFiles() with direct callbacks.\n */\n private async enqueueDownloads(data: any) {\n const { extractMediaIdsFromXlf } = await import('@xiboplayer/sw');\n const { layoutOrder, files, layoutDependants } = data;\n // Use DownloadManager facade methods (not direct queue access)\n\n /** Store key = URL path without leading / and query params */\n const storeKeyFrom = (f: any) => (f.path || '').split('?')[0].replace(/^\\/+/, '') || `${f.type || 'media'}/${f.id}`;\n\n // Build fileId→saveAs map from CMS RequiredFiles data\n for (const f of files) {\n if (f.saveAs) {\n this._fileIdToSaveAs.set(String(f.id), f.saveAs);\n }\n }\n // Build lookup maps from flat CMS file list\n const xlfFiles = new Map();\n const resources: any[] = [];\n const mediaFiles = new Map();\n const idToKeys = new Map();\n for (const f of files) {\n if (f.type === 'layout') {\n xlfFiles.set(parseInt(f.id), f);\n } else if (f.type === 'static') {\n resources.push(f);\n } else {\n const key = `${f.type}:${f.id}`;\n mediaFiles.set(key, f);\n const bareId = String(f.id);\n if (!idToKeys.has(bareId)) idToKeys.set(bareId, []);\n idToKeys.get(bareId).push(key);\n }\n }\n\n log.info(`Download: ${layoutOrder.length} layouts, ${mediaFiles.size} media, ${resources.length} resources`);\n\n // ── Step 1: Fetch + parse all XLFs (cache-through handles store/CMS) ──\n const layoutMediaMap = new Map();\n const allXlfIds = [...layoutOrder, ...[...xlfFiles.keys()].filter((id: number) => !layoutOrder.includes(id))];\n const xlfPromises = allXlfIds.map(async (layoutId: number) => {\n const xlfFile = xlfFiles.get(layoutId);\n if (!xlfFile?.path) return;\n\n let xlfText: string | undefined;\n\n // Try store first, then cache-through fetches from CMS on miss\n try {\n const headers: Record<string, string> = {};\n if (xlfFile.cmsDownloadUrl) headers['X-Cms-Download-Url'] = xlfFile.cmsDownloadUrl;\n const resp = await fetch(xlfFile.path, Object.keys(headers).length ? { headers } : undefined);\n if (resp.ok) {\n xlfText = await resp.text();\n log.info(`Fetched XLF ${layoutId} (${xlfText.length} bytes)`);\n // Store XLF in content store so prepareLayout() can find it via store.get()\n await store.put(`${STORE_PREFIX}/layouts`, String(layoutId), new Blob([xlfText], { type: 'text/xml' }));\n this.notifyFileCached(String(layoutId), 'layout');\n }\n } catch (_) {}\n\n if (xlfText) {\n layoutMediaMap.set(layoutId, extractMediaIdsFromXlf(xlfText, log));\n }\n });\n await Promise.allSettled(xlfPromises);\n log.info(`Parsed ${layoutMediaMap.size} XLFs`);\n\n // Helper: enqueue a file, attach completion callback\n const enqueueFile = async (builder: any, file: any): Promise<boolean> => {\n if (!file.path || file.path === 'null' || file.path === 'undefined') return false;\n\n const storeKey = storeKeyFrom(file);\n\n // Check if already stored on disk (200 = cached, 204 = not in store)\n try {\n const headResp = await fetch(`/store/${storeKey}`, { method: 'HEAD' });\n if (headResp.status === 200) return false;\n } catch (_) {}\n\n // Check if already downloading (download manager keys are type/id, not URL paths)\n const dmKey = `${file.type}/${file.id}`;\n if (downloadManager.getTask(dmKey)) return false;\n\n // Check for existing chunks — skip already-downloaded ones\n try {\n const mcResp = await fetch(`/store/missing-chunks/${storeKey}`);\n if (mcResp.ok) {\n const { missing, numChunks } = await mcResp.json();\n if (numChunks > 0 && missing.length < numChunks) {\n const existing = new Set<number>();\n for (let i = 0; i < numChunks; i++) {\n if (!missing.includes(i)) existing.add(i);\n }\n file.skipChunks = existing;\n log.info(`Resuming ${storeKey}: ${existing.size}/${numChunks} chunks cached, ${missing.length} to download`);\n }\n }\n } catch (_) {}\n\n const fileDownload = builder.addFile(file);\n if (fileDownload.state !== 'pending') return false;\n\n // Direct callback — no postMessage needed\n fileDownload.wait().then((blob: any) => {\n const fileSize = parseInt(file.size) || blob.size;\n log.info('Download complete:', storeKey, `(${fileSize} bytes)`);\n\n // Mark chunked files as complete\n if (fileSize > this._chunkConfig.chunkSize) {\n fetch('/store/mark-complete', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ storeKey }),\n }).catch((e: any) => log.warn('mark-complete failed:', storeKey, e.message));\n }\n\n this.notifyFileCached(String(file.id), file.type);\n downloadManager.removeCompleted(dmKey);\n }).catch((err: any) => {\n log.error('Download failed:', file.id, err);\n downloadManager.removeCompleted(dmKey);\n });\n return true;\n };\n\n // ── Step 2: Enqueue resources (parallel HEAD checks) ──\n const resourceBuilder = downloadManager.createTaskBuilder();\n await Promise.all(resources.map(file => enqueueFile(resourceBuilder, file)));\n const resourceTasks = await resourceBuilder.build();\n if (resourceTasks.length > 0) {\n resourceTasks.push(BARRIER);\n downloadManager.enqueueOrderedTasks(resourceTasks);\n }\n\n // ── Step 3: For each layout in play order, merge XLF + dependants ──\n const claimed = new Set();\n const nonScheduledIds = [...layoutMediaMap.keys()].filter((id: number) => !layoutOrder.includes(id));\n const filenameToMediaId = new Map();\n for (const [key, file] of mediaFiles) {\n if (file.saveAs) filenameToMediaId.set(file.saveAs, key);\n }\n\n const depMap = new Map();\n if (layoutDependants) {\n for (const [id, filenames] of Object.entries(layoutDependants)) {\n depMap.set(parseInt(id, 10), filenames);\n }\n }\n\n for (const layoutId of layoutOrder) {\n const xlfMediaIds = layoutMediaMap.get(layoutId);\n if (!xlfMediaIds) continue;\n\n const bareIds = new Set(xlfMediaIds);\n for (const nsId of nonScheduledIds) {\n const nsMediaIds = layoutMediaMap.get(nsId);\n if (nsMediaIds) {\n for (const id of nsMediaIds) bareIds.add(id);\n }\n }\n const deps = depMap.get(layoutId) || [];\n for (const filename of deps) {\n const key = filenameToMediaId.get(filename);\n if (key) bareIds.add(key);\n }\n\n const matched: any[] = [];\n for (const bareId of bareIds) {\n if (mediaFiles.has(bareId) && !claimed.has(bareId)) {\n matched.push(mediaFiles.get(bareId));\n claimed.add(bareId);\n continue;\n }\n const keys = idToKeys.get(String(bareId)) || [];\n for (const key of keys) {\n if (claimed.has(key)) continue;\n matched.push(mediaFiles.get(key));\n claimed.add(key);\n }\n }\n if (matched.length === 0) continue;\n\n log.info(`Layout ${layoutId}: ${matched.length} media`);\n matched.sort((a: any, b: any) => (a.size || 0) - (b.size || 0));\n const builder = downloadManager.createTaskBuilder();\n await Promise.all(matched.map(file => enqueueFile(builder, file)));\n const orderedTasks = await builder.build();\n if (orderedTasks.length > 0) {\n orderedTasks.push(BARRIER);\n downloadManager.enqueueOrderedTasks(orderedTasks);\n }\n }\n\n // Enqueue unclaimed media\n const unclaimed = [...mediaFiles.keys()].filter((id: string) => !claimed.has(id));\n if (unclaimed.length > 0) {\n log.info(`${unclaimed.length} media not in any XLF`);\n const builder = downloadManager.createTaskBuilder();\n await Promise.all(unclaimed.map(id => {\n const file = mediaFiles.get(id);\n return file ? enqueueFile(builder, file) : Promise.resolve(false);\n }));\n const orderedTasks = await builder.build();\n if (orderedTasks.length > 0) {\n downloadManager.enqueueOrderedTasks(orderedTasks);\n }\n }\n\n log.info('Downloads active:', downloadManager.running, ', queued:', downloadManager.queued);\n }\n\n /**\n * Setup renderer event handlers\n */\n private setupRendererEventHandlers() {\n this.renderer.on('layoutStart', (layoutId: number, _layout: any) => {\n log.info('Layout started:', layoutId);\n this.updateStatus(`Playing layout ${layoutId}`);\n\n this.core.setCurrentLayout(layoutId);\n\n // Store layout-level enableStat for use in layoutEnd\n this._currentLayoutEnableStat = _layout?.enableStat !== false;\n\n // Update timeline overlay with current layout's known duration\n const layoutDur = this.core.getLayoutDuration(layoutId) || _layout?.duration;\n this.timelineOverlay?.update(null, layoutId, layoutDur);\n\n // Track stats: start layout (only if enableStat is not disabled)\n if (this.statsCollector && this._currentLayoutEnableStat) {\n this.statsCollector.startLayout(layoutId, this.currentScheduleId).catch((err: any) => {\n log.error('Failed to start layout stat:', err);\n });\n }\n });\n\n this.renderer.on('layoutEnd', (layoutId: number) => {\n log.info('Layout ended:', layoutId);\n\n // Record play at END so maxPlaysPerHour doesn't interrupt the current play.\n // Previously recorded at layoutStart, which caused periodic collections to\n // filter the layout mid-playback (e.g., 200s video cut at 168s).\n scheduleManager?.recordPlay(layoutId.toString());\n\n // Track stats: end layout (only if enableStat was not disabled)\n if (this.statsCollector && this._currentLayoutEnableStat) {\n this.statsCollector.endLayout(layoutId, this.currentScheduleId).catch((err: any) => {\n log.error('Failed to end layout stat:', err);\n });\n }\n\n // If a new layout is already rendering or being prepared (async fetch),\n // skip advance — the transition was already handled by the caller.\n // Stats/play recording above still run for proper tracking.\n if (this.renderer.getCurrentLayoutId() && this.renderer.getCurrentLayoutId() !== layoutId) {\n log.debug(`Layout ${layoutId} ended but ${this.renderer.getCurrentLayoutId()} already playing, skipping advance`);\n return;\n }\n if (this.preparingLayoutId && this.preparingLayoutId !== layoutId) {\n log.debug(`Layout ${layoutId} ended but ${this.preparingLayoutId} being prepared, skipping advance`);\n return;\n }\n\n // Report to CMS\n this.core.notifyLayoutStatus(layoutId);\n\n // Clear current layout to allow replay/advance\n this.core.clearCurrentLayout();\n\n // If a new layout is already pending download, don't advance\n // (avoids redundant XMDS calls and duplicate download requests)\n const pending = this.core.getPendingLayouts();\n if (pending.length > 0) {\n log.info(`Layout ${pending[0]} pending download, skipping advance`);\n return;\n }\n\n // Advance to the next layout in the schedule (round-robin cycling)\n // This avoids a full collect() cycle — just picks the next layout and renders it.\n // Periodic collect() cycles still run on the collection interval to sync with CMS.\n log.info('Layout cycle completed, advancing to next layout...');\n this.core.advanceToNextLayout();\n });\n\n this.renderer.on('widgetStart', (data: any) => {\n const { widgetId, layoutId, mediaId } = data;\n log.debug('Widget started:', data.type, widgetId, 'media:', mediaId);\n\n // Track stats: start widget/media (only if enableStat is not disabled)\n if (this.statsCollector && mediaId && data.enableStat !== false) {\n this.statsCollector.startWidget(mediaId, layoutId, this.currentScheduleId).catch((err: any) => {\n log.error('Failed to start widget stat:', err);\n });\n }\n });\n\n this.renderer.on('widgetEnd', (data: any) => {\n const { widgetId, layoutId, mediaId } = data;\n log.debug('Widget ended:', data.type, widgetId, 'media:', mediaId);\n\n // Track stats: end widget/media (only if enableStat is not disabled)\n if (this.statsCollector && mediaId && data.enableStat !== false) {\n this.statsCollector.endWidget(mediaId, layoutId, this.currentScheduleId).catch((err: any) => {\n log.error('Failed to end widget stat:', err);\n });\n }\n });\n\n // Widget commands (#202) — execute commands embedded in layout widgets\n this.renderer.on('widgetCommand', (data: any) => {\n log.info('Widget command:', data.commandCode);\n const commands = { [data.commandCode]: { commandString: data.commandString } };\n this.core.executeCommand(data.commandCode, commands);\n });\n\n this.renderer.on('error', (error: any) => {\n log.error('Renderer error:', error);\n this.updateStatus(`Error: ${error.type}`, 'error');\n\n // Report fault to CMS (triggers dashboard alert)\n this.reportFault(error.type || 'RENDERER_ERROR', `Renderer error: ${error.message || error.type}`, {\n layoutId: error.layoutId,\n regionId: error.regionId,\n widgetId: error.widgetId\n });\n });\n\n // Handle interactive actions from touch/click and keyboard triggers\n this.renderer.on('action-trigger', (data: any) => {\n const { actionType, triggerCode, layoutCode, targetId, commandCode } = data;\n log.info('Action trigger:', actionType, data);\n\n switch (actionType) {\n case 'navLayout':\n case 'navigateToLayout':\n if (triggerCode) {\n this.core.handleTrigger(triggerCode);\n } else if (layoutCode) {\n this.core.changeLayout(layoutCode);\n }\n break;\n\n case 'navWidget':\n case 'navigateToWidget':\n if (triggerCode) {\n this.core.handleTrigger(triggerCode);\n } else if (targetId) {\n this.renderer.navigateToWidget(targetId);\n }\n break;\n\n case 'previousWidget':\n this.renderer.previousWidget(data.source?.regionId);\n break;\n\n case 'nextWidget':\n this.renderer.nextWidget(data.source?.regionId);\n break;\n\n case 'command':\n if (commandCode) {\n this.core.executeCommand(commandCode);\n }\n break;\n\n default:\n log.warn('Unknown action type:', actionType);\n }\n\n // Record interaction event for proof of play (#19)\n if (this.statsCollector) {\n this.statsCollector.recordEvent('touch', this.core.getCurrentLayoutId(), data.targetId || null, this.currentScheduleId);\n }\n });\n\n // Widget duration webhooks (#16) — fire HTTP POST when widget duration expires\n this.renderer.on('widgetAction', (data: any) => {\n if (data.type === 'durationEnd' && data.url) {\n log.info(`Widget ${data.widgetId} duration ended, calling webhook: ${data.url}`);\n\n // Record webhook event for proof of play (#19)\n if (this.statsCollector) {\n this.statsCollector.recordEvent('webhook', data.layoutId, data.widgetId, this.currentScheduleId);\n }\n\n fetch(data.url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n widgetId: data.widgetId,\n layoutId: data.layoutId,\n regionId: data.regionId,\n event: 'durationEnd',\n timestamp: new Date().toISOString()\n })\n }).catch(err => log.warn('Webhook failed (non-critical):', err));\n }\n });\n\n // Correct timeline duration when video metadata reveals actual duration\n this.renderer.on('layoutDurationUpdated', (layoutId: number, duration: number, final: boolean) => {\n this.core.recordLayoutDuration(String(layoutId), duration, final);\n });\n\n // Handle next layout preload request from renderer\n // Fired at 75% of current layout duration to pre-build the next layout's DOM\n this.renderer.on('request-next-layout-preload', async () => {\n try {\n // Peek at the next layout without advancing the schedule index\n const next = this.core.peekNextLayout();\n if (!next) {\n log.debug('No next layout to preload (single layout schedule or same layout)');\n return;\n }\n\n const nextLayoutId = next.layoutId;\n\n // Skip if already preloaded or preload in-flight\n if (this.renderer.layoutPool.has(nextLayoutId)) {\n log.debug(`Layout ${nextLayoutId} already in preload pool`);\n return;\n }\n if ((this.renderer as any)._preloadingLayoutId === nextLayoutId) {\n log.debug(`Layout ${nextLayoutId} preload already in-flight`);\n return;\n }\n\n log.info(`Preloading next layout ${nextLayoutId}...`);\n\n // Get XLF from cache\n const xlfBlob = await store.get(`${STORE_PREFIX}/layouts`, nextLayoutId);\n if (!xlfBlob) {\n log.debug(`Layout ${nextLayoutId} XLF not cached, skipping preload`);\n return;\n }\n\n const xlfXml = await xlfBlob.text();\n const doc = new DOMParser().parseFromString(xlfXml, 'text/xml');\n\n // Check if all required media is cached\n const { allMedia: requiredMedia } = this.getMediaIds(doc);\n const allMediaCached = await this.checkAllMediaCached(requiredMedia);\n\n if (!allMediaCached) {\n log.debug(`Media not fully cached for layout ${nextLayoutId}, skipping preload`);\n return;\n }\n\n // Fetch widget HTML before preloading (same as prepareLayout)\n await this.fetchWidgetHtml(doc, nextLayoutId);\n\n // Preload the layout into the renderer's pool\n const success = await this.renderer.preloadLayout(xlfXml, nextLayoutId);\n if (success) {\n log.info(`Layout ${nextLayoutId} preloaded successfully`);\n } else {\n log.warn(`Layout ${nextLayoutId} preload failed (will fall back to normal render)`);\n }\n } catch (error) {\n log.warn('Layout preload failed (non-blocking):', error);\n // Non-blocking: preload failure is graceful, normal render path will be used\n }\n });\n\n // Handle video playback errors — re-download only missing chunks\n this.renderer.on('videoError', async ({ storedAs }: any) => {\n if (!storedAs) return;\n const storeKey = `${PLAYER_API.slice(1)}/media/file/${storedAs}`;\n try {\n const resp = await fetch(`/store/missing-chunks/${storeKey}`);\n const { missing } = await resp.json();\n if (missing.length === 0) {\n log.warn(`Video ${storedAs}: corrupt file (all chunks present), deleting for re-download`);\n await fetch('/store/delete', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ files: [{ key: storeKey }] }),\n });\n const layoutId = this.core.getCurrentLayoutId();\n if (layoutId) {\n this.core.setPendingLayout(layoutId, [storedAs]);\n }\n this.core.collectNow().catch((err: any) => {\n log.error(`Failed to trigger re-download for ${storedAs}:`, err.message);\n });\n return;\n }\n log.warn(`Video ${storedAs}: ${missing.length} missing chunks (${missing.join(', ')}), re-downloading`);\n\n // Unmark completion (keeps existing chunks on disk) so HEAD returns 404\n await fetch('/store/unmark-complete', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ storeKey }),\n });\n\n // Trigger collection — enqueueFile will populate skipChunks for existing chunks\n this.core.collectNow().catch((err: any) => {\n log.error(`Failed to trigger re-download for ${storedAs}:`, err.message);\n });\n } catch (err: any) {\n log.error(`Failed to check/re-download ${storedAs}:`, err.message);\n }\n });\n }\n\n /**\n * Prepare and render layout (Platform-specific logic)\n */\n private async prepareLayout(layoutId: number) {\n // Same layout replay — use renderer's built-in replay path which\n // re-emits layoutStart, restarts timer and widget cycling.\n if (this.renderer.getCurrentLayoutId() === layoutId) {\n log.debug(`Layout ${layoutId} replay`);\n this.core.clearPreparingLayout();\n // Renderer's same-layout replay path reuses existing DOM — XLF not re-parsed\n await this.renderer.renderLayout('', layoutId);\n return;\n }\n\n // Guard: prevent concurrent preparations of the same layout.\n // Instead of dropping the event (which caused permanent stalls when the\n // first attempt failed due to a store race), schedule a retry after\n // the current preparation finishes.\n if (this.preparingLayoutId === layoutId) {\n log.debug(`Layout ${layoutId} preparation in progress, will retry after it completes`);\n this._pendingRetryLayoutId = layoutId;\n return;\n }\n\n this.preparingLayoutId = layoutId;\n try {\n // Get XLF from cache\n const xlfBlob = await store.get(`${STORE_PREFIX}/layouts`, layoutId);\n if (!xlfBlob) {\n log.info('Layout not in cache yet, marking as pending:', layoutId);\n // Mark layout as pending so when it downloads, we'll retry\n // Use layoutId as required file (will trigger on layout file cached)\n this.core.setPendingLayout(layoutId, [String(layoutId)]);\n this.updateStatus(`Downloading layout ${layoutId}...`);\n return;\n }\n\n const xlfXml = await xlfBlob.text();\n\n // Parse XLF once — reuse Document for media check and widget HTML fetch\n const xlfDoc = new DOMParser().parseFromString(xlfXml, 'text/xml');\n\n // Check if all required media is cached\n const { allMedia: requiredMedia } = this.getMediaIds(xlfDoc);\n const allMediaCached = await this.checkAllMediaCached(requiredMedia);\n\n if (!allMediaCached) {\n // Reorder download queue: current layout's media first, hold others.\n // All files (including all chunks) must complete before other layouts start.\n downloadManager.prioritizeLayoutFiles(requiredMedia.map(String));\n\n log.info(`Waiting for media to finish downloading for layout ${layoutId}`);\n this.updateStatus(`Preparing layout ${layoutId}...`);\n this.core.setPendingLayout(layoutId, requiredMedia);\n return; // Keep playing current layout until media is ready\n }\n\n // Fetch widget HTML (skip if already preloaded — was fetched during preload)\n if (!this.renderer.hasPreloadedLayout(layoutId)) {\n await this.fetchWidgetHtml(xlfDoc, layoutId);\n }\n\n // Preload layout into pool (hidden). Caller decides when to show.\n await this.renderer.preloadLayout(xlfXml, layoutId);\n\n // Clear pending status — layout prepared successfully, all media cached.\n // Without this, pendingLayouts keeps the layout marked as ⚠ missing\n // until it actually plays (which may be minutes later in the rotation).\n this.core.pendingLayouts.delete(layoutId);\n\n log.info(`Layout ${layoutId} ready`);\n\n } catch (error: any) {\n log.error('Failed to prepare layout:', layoutId, error);\n this.updateStatus(`Failed to load layout ${layoutId}`, 'error');\n\n // Report fault to CMS (triggers dashboard alert)\n this.reportFault('LAYOUT_LOAD_FAILED', `Failed to prepare layout ${layoutId}: ${error?.message || error}`, {\n layoutId\n });\n } finally {\n this.preparingLayoutId = null;\n this.core.clearPreparingLayout();\n\n // If another check-pending-layout arrived while we were preparing,\n // retry after a short delay to let the ContentStore settle.\n // This fixes the race where FILE_CACHED notification arrives before\n // the PUT to ContentStore is visible to HEAD requests.\n const retryId = this._pendingRetryLayoutId;\n this._pendingRetryLayoutId = null;\n if (retryId !== null && retryId !== undefined && this.core.getCurrentLayoutId() !== retryId) {\n log.debug(`Retrying preparation for layout ${retryId} after 500ms`);\n setTimeout(() => this.prepareLayout(retryId), 500);\n }\n }\n }\n\n /**\n * Get all required media saveAs filenames and video-specific ones from layout XLF.\n * Returns saveAs strings (via _fileIdToSaveAs map) for store key matching.\n */\n private getMediaIds(xlfXmlOrDoc: string | Document): { allMedia: string[]; videoMedia: string[] } {\n const doc = typeof xlfXmlOrDoc === 'string'\n ? new DOMParser().parseFromString(xlfXmlOrDoc, 'text/xml')\n : xlfXmlOrDoc;\n const allMedia: string[] = [];\n const videoMedia: string[] = [];\n\n doc.querySelectorAll('media[fileId]').forEach(el => {\n const fileId = el.getAttribute('fileId');\n if (fileId) {\n const saveAs = this._fileIdToSaveAs.get(fileId) || fileId;\n // Skip layout XLF references — stored in layouts/ store, not media/file/\n if (saveAs.endsWith('.xlf')) return;\n allMedia.push(saveAs);\n if (el.getAttribute('type') === 'video') {\n videoMedia.push(saveAs);\n }\n }\n });\n\n // Include background image file ID from layout element\n const bgFileId = doc.querySelector('layout')?.getAttribute('background');\n if (bgFileId) {\n const saveAs = this._fileIdToSaveAs.get(bgFileId) || bgFileId;\n if (!allMedia.includes(saveAs)) {\n allMedia.push(saveAs);\n }\n }\n\n return { allMedia, videoMedia };\n }\n\n /**\n * Check if all required media files are cached and ready.\n * Uses StoreClient.has() → HEAD /store${PLAYER_API}/media/:id to check ContentStore.\n */\n /**\n * Check if all required media files are cached and ready.\n * Uses storedAs filenames for store key matching: /media/file/{saveAs}\n */\n private async checkAllMediaCached(mediaSaveAs: string[]): Promise<boolean> {\n // Check in-memory set first — avoids all HEAD requests for known-cached files\n const unknown = mediaSaveAs.filter(s => !this._cachedMediaKeys.has(s));\n if (unknown.length === 0) return true;\n\n // HEAD-check all unknown files against the content store.\n // Always check the store directly — the download queue may have stale tasks\n // for files that are already cached (race between download completion and\n // task cleanup). The HEAD check is fast (<1ms for local store) and authoritative.\n const toCheck = unknown;\n\n const results = await Promise.all(\n toCheck.map(async (saveAs) => {\n try {\n const cached = await store.has(STORE_PREFIX, `media/file/${saveAs}`);\n if (cached) this._cachedMediaKeys.add(saveAs);\n return cached;\n } catch {\n log.warn(`Unable to verify media ${saveAs}, assuming cached (offline mode)`);\n return true;\n }\n })\n );\n const missing = toCheck.filter((_, i) => !results[i]);\n if (missing.length > 0) {\n log.debug(`Media not yet cached: ${missing.join(', ')}`);\n return false;\n }\n return true;\n }\n\n /**\n * Fetch widget HTML for all widgets in layout (parallel)\n */\n private async fetchWidgetHtml(xlfXmlOrDoc: string | Document, layoutId: number) {\n const doc = typeof xlfXmlOrDoc === 'string'\n ? new DOMParser().parseFromString(xlfXmlOrDoc, 'text/xml')\n : xlfXmlOrDoc;\n\n const fetchPromises: Promise<void>[] = [];\n\n for (const regionEl of doc.querySelectorAll('region')) {\n const regionId = regionEl.getAttribute('id');\n\n for (const mediaEl of regionEl.querySelectorAll('media')) {\n const type = mediaEl.getAttribute('type');\n const widgetId = mediaEl.getAttribute('id');\n const render = mediaEl.getAttribute('render');\n\n // XLF render=\"html\" means CMS provides pre-rendered HTML via getResource.\n // render=\"native\" means player handles the media directly (video, image, audio).\n if (render === 'html') {\n fetchPromises.push(\n (async () => {\n try {\n // Check ContentStore for existing widget HTML\n const storeId = `${layoutId}/${regionId}/${widgetId}`;\n let html: string | null = null;\n\n const existing = await store.get(`${STORE_PREFIX}/widgets`, storeId);\n if (existing) {\n html = await existing.text();\n log.debug(`Found cached widget HTML for ${type} ${widgetId}`);\n }\n\n if (!html) {\n html = await this.xmds.getResource(layoutId, regionId, widgetId);\n log.debug(`Retrieved widget HTML for ${type} ${widgetId} from CMS`);\n }\n // Always process: injects <base> tag, rewrites IC hostAddress.\n // cacheWidgetHtml is idempotent — already-rewritten URLs won't re-match.\n const result = await cacheWidgetHtml(layoutId, regionId, widgetId, html);\n html = result.html;\n\n // Update raw content in XLF\n const rawEl = mediaEl.querySelector('raw');\n if (rawEl) {\n rawEl.textContent = html;\n } else {\n const newRaw = doc.createElement('raw');\n newRaw.textContent = html;\n mediaEl.appendChild(newRaw);\n }\n } catch (error) {\n log.warn(`Failed to get widget HTML for ${type} ${widgetId}:`, error);\n }\n })()\n );\n }\n }\n }\n\n if (fetchPromises.length > 0) {\n log.info(`Fetching ${fetchPromises.length} widget HTML resources in parallel...`);\n await Promise.all(fetchPromises);\n log.debug('All widget HTML fetched');\n }\n }\n\n /**\n * Check media cache status for all scheduled layouts.\n * For each layout: load XLF from cache, extract media IDs, check each with store.has().\n * Feeds results into PlayerCore.setLayoutMediaStatus() for timeline annotation.\n */\n private async checkTimelineMediaStatus() {\n if (this.scheduledLayoutIds.size === 0) return;\n\n for (const layoutId of this.scheduledLayoutIds) {\n const layoutFile = `${layoutId}.xlf`;\n try {\n const xlfBlob = await store.get(`${STORE_PREFIX}/layouts`, layoutId);\n if (!xlfBlob) continue;\n\n const xlfXml = await xlfBlob.text();\n const { allMedia } = this.getMediaIds(xlfXml);\n\n if (allMedia.length === 0) {\n this.core.setLayoutMediaStatus(layoutFile, true);\n continue;\n }\n\n const missing: string[] = [];\n for (const saveAs of allMedia) {\n if (this._cachedMediaKeys.has(saveAs)) continue;\n // If in download queue, it's not cached — skip HEAD\n const storeKey = `${STORE_PREFIX}/media/file/${saveAs}`;\n if (downloadManager.getTask(storeKey)) { missing.push(saveAs); continue; }\n try {\n const cached = await store.has(STORE_PREFIX, `media/file/${saveAs}`);\n if (cached) this._cachedMediaKeys.add(saveAs);\n else missing.push(saveAs);\n } catch {\n // Assume cached on error (offline mode)\n }\n }\n\n this.core.setLayoutMediaStatus(layoutFile, missing.length === 0, missing);\n } catch {\n // Skip layouts we can't load\n }\n }\n\n // Re-emit annotated timeline\n this.core.logUpcomingTimeline();\n }\n\n /**\n * Probe video durations for all scheduled layouts.\n * Uses preload=\"metadata\" — only fetches headers (~50KB), not the full video.\n * Feeds discovered durations into PlayerCore for accurate timeline calculation.\n */\n private async probeLayoutDurations() {\n if (this.scheduledLayoutIds.size === 0) return;\n\n for (const layoutId of this.scheduledLayoutIds) {\n\n try {\n const xlfBlob = await store.get(`${STORE_PREFIX}/layouts`, layoutId);\n if (!xlfBlob) continue;\n\n const xlfXml = await xlfBlob.text();\n const { videoMedia } = this.getMediaIds(xlfXml);\n if (videoMedia.length === 0) continue;\n\n // Parse XLF to find video widgets with duration=0 (use media length)\n const parser = new DOMParser();\n const doc = parser.parseFromString(xlfXml, 'text/xml');\n\n // Probe actual video durations, keyed by fileId\n const videoDurations = new Map<string, number>();\n let dynamicVideoCount = 0;\n for (const mediaEl of doc.querySelectorAll('media[type=\"video\"]')) {\n const useDuration = mediaEl.getAttribute('useDuration');\n if (useDuration === '1') continue; // Has explicit CMS duration, skip\n\n const fileId = mediaEl.getAttribute('fileId');\n if (!fileId) continue;\n dynamicVideoCount++;\n\n const saveAs = this._fileIdToSaveAs.get(fileId) || fileId;\n const exists = await store.has(STORE_PREFIX, `media/file/${saveAs}`);\n if (!exists) continue;\n\n // Probe metadata only — does NOT download the full video\n const duration = await this.probeVideoDuration(`${window.location.origin}${PLAYER_API}/media/file/${saveAs}`);\n if (duration > 0) {\n videoDurations.set(fileId, duration);\n }\n }\n\n if (videoDurations.size === 0) continue;\n\n // Only mark final if ALL dynamic videos were successfully probed\n const allProbed = videoDurations.size >= dynamicVideoCount;\n\n // Phase 2: refine layout duration with probed video lengths\n const { duration: probedDuration } = parseLayoutDuration(xlfXml, videoDurations);\n if (probedDuration > 0) {\n this.core.recordLayoutDuration(String(layoutId), probedDuration, allProbed);\n }\n } catch (err) {\n log.debug(`Duration probe failed for layout ${layoutId}:`, err);\n }\n }\n }\n\n /**\n * Probe a single video's duration using metadata only.\n * Creates a temporary <video preload=\"metadata\"> element, reads duration, destroys it.\n */\n private probeVideoDuration(url: string): Promise<number> {\n return new Promise((resolve) => {\n const video = document.createElement('video');\n video.preload = 'metadata';\n video.muted = true;\n\n const cleanup = () => {\n video.removeAttribute('src');\n video.load(); // Release resources\n };\n\n video.addEventListener('loadedmetadata', () => {\n const dur = video.duration;\n cleanup();\n resolve(dur);\n }, { once: true });\n\n video.addEventListener('error', () => {\n cleanup();\n resolve(0);\n }, { once: true });\n\n // Safety timeout — don't block forever\n setTimeout(() => {\n cleanup();\n resolve(0);\n }, 5000);\n\n video.src = url;\n });\n }\n\n /**\n * Update config display\n */\n private updateConfigDisplay() {\n const configEl = document.getElementById('config-info');\n if (configEl) {\n const version = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '?';\n const buildDate = typeof __BUILD_DATE__ !== 'undefined' ? __BUILD_DATE__.replace('T', ' ').replace(/\\.\\d+Z$/, '') : '';\n const versionStr = buildDate ? `v${version} (${buildDate})` : `v${version}`;\n let text = `${versionStr} | CMS: ${config.cmsUrl} | Display: ${config.displayName || 'Unknown'} | HW: ${config.hardwareKey}`;\n const sc = this.core?.getSyncConfig?.();\n if (sc) {\n const relay = sc.relayUrl ? new URL(sc.relayUrl).host : '';\n text += ` | Sync: ${sc.isLead ? 'LEAD' : `FOLLOWER → ${relay}`} (group ${sc.syncGroupId || sc.syncGroup})`;\n }\n configEl.textContent = text;\n }\n }\n\n /**\n * Generic submission pipeline for stats and logs.\n * Handles in-flight guard, sync delegation, CMS submission, and cleanup.\n */\n private async submitCollectedData(options: {\n name: string;\n pendingFlag: '_pendingFollowerStats' | '_pendingFollowerLogs';\n getItems: () => Promise<any[]>;\n formatFn: (items: any[]) => string;\n delegateFn: (xml: string) => void;\n submitFn: (xml: string) => Promise<any>;\n clearFn: (items: any[]) => Promise<void>;\n }): Promise<void> {\n const { name, pendingFlag, getItems, formatFn, delegateFn, submitFn, clearFn } = options;\n\n // Guard: don't start a new delegation while one is in-flight\n if (this[pendingFlag] !== null) {\n log.debug(`${name} delegation in-flight, skipping`);\n return;\n }\n\n try {\n const items = await getItems();\n\n if (items.length === 0) {\n log.debug(`No ${name} to submit`);\n return;\n }\n\n const xml = formatFn(items);\n\n // Follower with live lead: delegate via BroadcastChannel\n if (this.syncManager && !this.syncManager.isLead && this._syncLeadAlive()) {\n log.info(`[Sync] Delegating ${items.length} ${name} to lead`);\n this[pendingFlag] = items;\n delegateFn(xml);\n return;\n }\n\n // Lead, standalone, or lead-dead follower: submit directly\n if (this.syncManager && !this.syncManager.isLead) {\n log.warn(`[Sync] Lead not alive, submitting ${name} directly`);\n }\n\n log.info(`Submitting ${items.length} ${name} to CMS...`);\n\n const success = await submitFn(xml);\n\n if (success) {\n log.info(`${name} submitted successfully`);\n await clearFn(items);\n } else {\n log.warn(`${name} submission failed (CMS returned false)`);\n }\n } catch (error) {\n log.error(`Failed to submit ${name}:`, error);\n }\n }\n\n /**\n * Submit proof of play stats to CMS\n */\n private async submitStats(): Promise<void> {\n if (!this.statsCollector) {\n log.warn('Stats collector not initialized');\n return;\n }\n\n const aggregationLevel = this.displaySettings?.getSetting('aggregationLevel') || 'Individual';\n\n await this.submitCollectedData({\n name: 'stats',\n pendingFlag: '_pendingFollowerStats',\n getItems: async () => aggregationLevel === 'Aggregate'\n ? this.statsCollector.getAggregatedStatsForSubmission(50)\n : this.statsCollector.getStatsForSubmission(50),\n formatFn: formatStats,\n delegateFn: (xml) => this.syncManager.reportStats(xml),\n submitFn: (xml) => this.xmds.submitStats(xml),\n clearFn: (items) => this.statsCollector.clearSubmittedStats(items),\n });\n }\n\n /**\n * Submit player logs to CMS for remote debugging\n */\n private async submitLogs(): Promise<void> {\n if (!this.logReporter) return;\n\n await this.submitCollectedData({\n name: 'logs',\n pendingFlag: '_pendingFollowerLogs',\n getItems: () => this.logReporter.getLogsForSubmission(),\n formatFn: formatLogs,\n delegateFn: (xml) => this.syncManager.reportLogs(xml),\n submitFn: (xml) => this.xmds.submitLog(xml),\n clearFn: (items) => this.logReporter.clearSubmittedLogs(items),\n });\n }\n\n /**\n * Report a fault to both the log dashboard and the player_faults dashboard.\n * Combines logReporter.reportFault() (log dashboard) with submitFault() (faults dashboard).\n */\n private reportFault(code: string, reason: string, details?: { layoutId?: number; regionId?: string; widgetId?: string }): void {\n this.logReporter?.reportFault(code, reason);\n this.submitFault(code, reason, details);\n }\n\n /**\n * Submit a fault report to CMS for the player_faults dashboard.\n */\n private submitFault(code: string, reason: string, details?: { layoutId?: number; regionId?: string; widgetId?: string }) {\n if (!this.xmds) return;\n\n const fault = JSON.stringify([{\n code,\n reason,\n date: new Date().toISOString().replace('T', ' ').substring(0, 19),\n ...details\n }]);\n\n this.xmds.reportFaults(fault).catch((err: any) => {\n log.debug('reportFaults failed (non-critical):', err);\n });\n }\n\n /**\n * Capture screenshot and submit to CMS.\n *\n * Strategy (best available, tried in order):\n * 0. Electron IPC — webContents.capturePage() via preload bridge.\n * Pixel-perfect, captures video/WebGL/composited layers, zero DOM cost.\n * Only available when running inside the Electron shell.\n * 1. getDisplayMedia() — native pixel capture via screen sharing API.\n * Pixel-perfect, zero DOM manipulation. Chromium kiosk auto-approves\n * via --auto-select-desktop-capture-source flag.\n * 2. Direct canvas drawing — fallback that draws img/video/canvas elements\n * directly. Text-only widgets (clocks, tickers) won't appear.\n *\n * The first successful method is cached for subsequent calls.\n */\n private async captureAndSubmitScreenshot() {\n // Concurrency guard — skip if a capture is already in flight\n if (this._screenshotInFlight) {\n log.debug('Screenshot capture already in progress, skipping');\n return;\n }\n this._screenshotInFlight = true;\n\n try {\n let base64: string;\n\n // Electron path: use native webContents.capturePage() via IPC\n if (this._screenshotMethod === 'electron' ||\n (this._screenshotMethod === null && (window as any).electronAPI?.captureScreenshot)) {\n const electronResult = await (window as any).electronAPI.captureScreenshot();\n if (electronResult) {\n this._screenshotMethod = 'electron';\n base64 = electronResult;\n } else {\n // Electron capture returned null (window not yet painted).\n // Do NOT fall through to getDisplayMedia — it triggers a\n // permission dialog that blocks the whole UI. Skip this\n // cycle; capturePage() will succeed on the next interval.\n log.debug('Electron screenshot not ready yet, will retry next interval');\n return;\n }\n } else if (this._screenshotMethod === 'displayMedia' ||\n (this._screenshotMethod === null && typeof navigator.mediaDevices?.getDisplayMedia === 'function')) {\n // Try getDisplayMedia — pixel-perfect screen capture, zero DOM cost.\n // Chromium kiosk auto-approves via --auto-accept-this-tab-capture.\n try {\n base64 = await this.captureDisplayMedia();\n this._screenshotMethod = 'displayMedia';\n } catch (e: any) {\n log.warn('getDisplayMedia failed, falling back to html2canvas:', e.message || e);\n this._screenshotMethod = null;\n base64 = await this.captureHtml2CanvasIsolated();\n this._screenshotMethod = 'html2canvas';\n }\n } else {\n // Tier 3: html2canvas hybrid (Firefox, other browsers)\n // Direct draw for img/video/canvas + per-iframe html2canvas for HTML widgets\n this._screenshotMethod = 'html2canvas';\n base64 = await this.captureHtml2CanvasIsolated();\n }\n\n const success = await this.xmds.submitScreenShot(base64);\n if (success) {\n log.info(`Screenshot submitted (${this._screenshotMethod})`);\n } else {\n log.warn('Screenshot submission failed');\n }\n } catch (error) {\n log.error('Failed to capture screenshot:', error);\n } finally {\n this._screenshotInFlight = false;\n }\n }\n\n /**\n * Capture screenshot via getDisplayMedia (screen sharing API).\n * Pixel-perfect, captures everything the GPU renders including video,\n * WebGL, composited layers, and all widget content.\n * Chromium kiosk auto-approves via --auto-select-desktop-capture-source.\n */\n private async captureDisplayMedia(): Promise<string> {\n const stream = await navigator.mediaDevices.getDisplayMedia({\n video: true,\n audio: false,\n preferCurrentTab: true,\n } as any);\n\n try {\n const track = stream.getVideoTracks()[0];\n const imageCapture = new (window as any).ImageCapture(track);\n const bitmap = await imageCapture.grabFrame();\n\n const canvas = document.createElement('canvas');\n canvas.width = bitmap.width;\n canvas.height = bitmap.height;\n const ctx = canvas.getContext('2d')!;\n ctx.drawImage(bitmap, 0, 0);\n bitmap.close();\n\n return canvas.toDataURL('image/jpeg', 0.8).split(',')[1];\n } finally {\n stream.getTracks().forEach(t => t.stop());\n }\n }\n\n /**\n * Capture screenshot via html2canvas hybrid approach.\n * Draws images/video/canvas directly, uses per-iframe html2canvas for\n * HTML widgets (clocks, tickers). CSS contain: strict on capture divs\n * prevents ResizeObserver glitches.\n */\n private async captureHtml2CanvasIsolated(): Promise<string> {\n const canvas = document.createElement('canvas');\n canvas.width = window.innerWidth;\n canvas.height = window.innerHeight;\n const ctx = canvas.getContext('2d')!;\n\n ctx.fillStyle = '#000';\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n\n const container = document.getElementById('player-container');\n if (!container) {\n return canvas.toDataURL('image/jpeg', 0.8).split(',')[1];\n }\n\n // Lazy-load html2canvas\n if (!this._html2canvasMod) {\n this._html2canvasMod = (await import('html2canvas')).default;\n }\n\n // Suppress resize during capture\n if (this.renderer) {\n this.renderer._resizeSuppressed = true;\n }\n\n // Protect the player container from external DOM changes.\n // html2canvas appends/removes elements to document.body which can trigger\n // Chromium reflow affecting the player. contain:strict on the player itself\n // makes it immune to any layout changes outside it.\n const prevContain = container.style.contain || '';\n container.style.contain = 'strict';\n\n try {\n // Draw container background\n const containerRect = container.getBoundingClientRect();\n const containerStyle = getComputedStyle(container);\n const bgColor = containerStyle.backgroundColor;\n if (bgColor && bgColor !== 'transparent' && bgColor !== 'rgba(0, 0, 0, 0)') {\n ctx.fillStyle = bgColor;\n ctx.fillRect(containerRect.left, containerRect.top, containerRect.width, containerRect.height);\n }\n const bgImage = containerStyle.backgroundImage;\n if (bgImage && bgImage !== 'none') {\n const urlMatch = bgImage.match(/url\\([\"']?(.*?)[\"']?\\)/);\n if (urlMatch) {\n try {\n const bgImg = new Image();\n bgImg.crossOrigin = 'anonymous';\n await new Promise<void>((resolve) => {\n bgImg.onload = () => resolve();\n bgImg.onerror = () => resolve();\n setTimeout(() => resolve(), 2000);\n bgImg.src = urlMatch[1];\n });\n if (bgImg.naturalWidth) {\n ctx.drawImage(bgImg, containerRect.left, containerRect.top, containerRect.width, containerRect.height);\n }\n } catch (_) { /* skip failed background */ }\n }\n }\n\n // Draw each visible widget element\n const elements = container.querySelectorAll('img, video, iframe, canvas');\n let drawn = 0;\n\n for (const el of elements) {\n const htmlEl = el as HTMLElement;\n if (htmlEl.style.visibility === 'hidden') continue;\n if (htmlEl.style.display === 'none') continue;\n const rect = el.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) continue;\n\n try {\n if (el instanceof HTMLImageElement) {\n if (!el.complete || !el.naturalWidth) continue;\n const fit = getComputedStyle(el).objectFit;\n if (fit === 'contain' && el.naturalWidth && el.naturalHeight) {\n const d = this.containedRect(el.naturalWidth, el.naturalHeight, rect);\n ctx.drawImage(el, d.x, d.y, d.w, d.h);\n } else {\n ctx.drawImage(el, rect.left, rect.top, rect.width, rect.height);\n }\n drawn++;\n } else if (el instanceof HTMLVideoElement) {\n if (el.readyState < 2) continue;\n const fit = getComputedStyle(el).objectFit;\n if (fit === 'contain' && el.videoWidth && el.videoHeight) {\n const d = this.containedRect(el.videoWidth, el.videoHeight, rect);\n ctx.drawImage(el, d.x, d.y, d.w, d.h);\n } else {\n ctx.drawImage(el, rect.left, rect.top, rect.width, rect.height);\n }\n drawn++;\n } else if (el instanceof HTMLCanvasElement) {\n ctx.drawImage(el, rect.left, rect.top, rect.width, rect.height);\n drawn++;\n } else if (el instanceof HTMLIFrameElement) {\n const iDoc = el.contentDocument;\n if (!iDoc?.body) continue;\n\n // Clone iframe DOM into main document for html2canvas rendering.\n // contain: strict prevents layout effects on the live player.\n const captureDiv = document.createElement('div');\n captureDiv.style.cssText = `position:fixed;left:-9999px;top:0;width:${rect.width}px;height:${rect.height}px;overflow:hidden;`;\n\n // Clone stylesheets with absolute URLs\n const linkPromises: Promise<void>[] = [];\n for (const styleEl of iDoc.querySelectorAll('style')) {\n captureDiv.appendChild(styleEl.cloneNode(true));\n }\n for (const linkEl of iDoc.querySelectorAll('link[rel=\"stylesheet\"]')) {\n const newLink = document.createElement('link');\n newLink.rel = 'stylesheet';\n newLink.href = new URL(linkEl.getAttribute('href') || '', iDoc.baseURI).href;\n captureDiv.appendChild(newLink);\n linkPromises.push(new Promise<void>(resolve => {\n newLink.onload = () => resolve();\n newLink.onerror = () => resolve();\n }));\n }\n\n // Clone body content with absolute img URLs\n const clonedBody = iDoc.body.cloneNode(true) as HTMLElement;\n for (const img of clonedBody.querySelectorAll('img[src]')) {\n const src = img.getAttribute('src') || '';\n if (src && !src.startsWith('http') && !src.startsWith('data:') && !src.startsWith('blob:')) {\n img.setAttribute('src', new URL(src, iDoc.baseURI).href);\n }\n }\n captureDiv.appendChild(clonedBody);\n document.body.appendChild(captureDiv);\n\n // Collect natural dimensions from original iframe images\n const origImgs = iDoc.querySelectorAll('img');\n const imgNaturals = new Map<string, { nw: number; nh: number }>();\n origImgs.forEach((img, i) => {\n if (img.naturalWidth && img.naturalHeight) {\n imgNaturals.set(String(i), { nw: img.naturalWidth, nh: img.naturalHeight });\n }\n });\n\n if (linkPromises.length > 0) {\n await Promise.race([\n Promise.all(linkPromises),\n new Promise(r => setTimeout(r, 500)),\n ]);\n }\n\n const iframeCanvas = await this._html2canvasMod(captureDiv, {\n useCORS: true, allowTaint: true, logging: false,\n backgroundColor: null,\n width: rect.width, height: rect.height,\n onclone: (clonedDoc: Document) => {\n // Force visible — CSS animations reset to opacity:0 in cloned DOM\n const s = clonedDoc.createElement('style');\n s.textContent = '*, *::before, *::after { animation: none !important; transition: none !important; opacity: 1 !important; }';\n clonedDoc.head.appendChild(s);\n\n // Fix object-fit: contain sizing for html2canvas\n const clonedImgs = clonedDoc.querySelectorAll('img');\n clonedImgs.forEach((cImg, i) => {\n const style = clonedDoc.defaultView?.getComputedStyle(cImg);\n if (!style || style.objectFit !== 'contain') return;\n const dims = imgNaturals.get(String(i));\n if (!dims) return;\n\n const cW = cImg.clientWidth || parseFloat(style.width) || 0;\n const cH = cImg.clientHeight || parseFloat(style.height) || 0;\n if (!cW || !cH) return;\n\n const srcAspect = dims.nw / dims.nh;\n const dstAspect = cW / cH;\n let drawW: number, drawH: number;\n if (srcAspect > dstAspect) {\n drawW = cW; drawH = cW / srcAspect;\n } else {\n drawH = cH; drawW = cH * srcAspect;\n }\n\n const wrapper = clonedDoc.createElement('div');\n wrapper.style.cssText = `width:${cW}px;height:${cH}px;display:flex;align-items:center;justify-content:center;overflow:hidden;`;\n cImg.style.objectFit = 'fill';\n cImg.style.width = `${drawW}px`;\n cImg.style.height = `${drawH}px`;\n cImg.parentNode?.insertBefore(wrapper, cImg);\n wrapper.appendChild(cImg);\n });\n },\n });\n\n document.body.removeChild(captureDiv);\n ctx.drawImage(iframeCanvas, rect.left, rect.top, rect.width, rect.height);\n\n // Draw videos directly — html2canvas can't render <video> elements.\n // Draw on top of the html2canvas result so video overlays black placeholder.\n const iframeRect = el.getBoundingClientRect();\n for (const vid of iDoc.querySelectorAll('video') as NodeListOf<HTMLVideoElement>) {\n if (vid.readyState < 2) continue;\n const vr = vid.getBoundingClientRect();\n if (vr.width === 0 || vr.height === 0) continue;\n try {\n const fit = iDoc.defaultView?.getComputedStyle(vid)?.objectFit;\n if (fit === 'contain' && vid.videoWidth && vid.videoHeight) {\n const d = this.containedRect(vid.videoWidth, vid.videoHeight,\n new DOMRect(iframeRect.left + vr.left, iframeRect.top + vr.top, vr.width, vr.height));\n ctx.drawImage(vid, d.x, d.y, d.w, d.h);\n } else {\n ctx.drawImage(vid, iframeRect.left + vr.left, iframeRect.top + vr.top, vr.width, vr.height);\n }\n } catch (_) { /* tainted video */ }\n }\n\n // Draw canvas elements directly (PDF pages, charts rendered in iframe)\n for (const c of iDoc.querySelectorAll('canvas') as NodeListOf<HTMLCanvasElement>) {\n const cr = c.getBoundingClientRect();\n if (cr.width === 0 || cr.height === 0) continue;\n try {\n ctx.drawImage(c, iframeRect.left + cr.left, iframeRect.top + cr.top, cr.width, cr.height);\n } catch (_) { /* tainted canvas */ }\n }\n\n drawn++;\n }\n } catch (e: any) {\n log.warn('Screenshot: failed to draw element', el.tagName, e);\n }\n }\n\n log.debug(`Screenshot: composed ${drawn}/${elements.length} elements`);\n return canvas.toDataURL('image/jpeg', 0.8).split(',')[1];\n } finally {\n container.style.contain = prevContain;\n if (this.renderer) {\n this.renderer._resizeSuppressed = false;\n }\n }\n }\n\n /**\n * Calculate the destination rect for object-fit: contain.\n * Returns the centered rect that preserves the source aspect ratio\n * within the bounding rect (letterbox/pillarbox).\n */\n private containedRect(\n srcW: number, srcH: number, rect: DOMRect\n ): { x: number; y: number; w: number; h: number } {\n const srcAspect = srcW / srcH;\n const dstAspect = rect.width / rect.height;\n let w: number, h: number;\n if (srcAspect > dstAspect) {\n // Source is wider — fit to width, letterbox top/bottom\n w = rect.width;\n h = rect.width / srcAspect;\n } else {\n // Source is taller — fit to height, pillarbox left/right\n h = rect.height;\n w = rect.height * srcAspect;\n }\n return {\n x: rect.left + (rect.width - w) / 2,\n y: rect.top + (rect.height - h) / 2,\n w, h,\n };\n }\n\n /**\n * Start periodic screenshot submission\n */\n private startScreenshotInterval() {\n const intervalSecs = this.displaySettings?.getSetting('screenshotInterval') || 0;\n if (!intervalSecs || intervalSecs <= 0) return;\n\n // Pre-load html2canvas for non-Electron/non-Chromium browsers\n if (!this._html2canvasMod && !(window as any).electronAPI) {\n import('html2canvas').then(m => { this._html2canvasMod = m.default; });\n }\n\n const intervalMs = intervalSecs * 1000;\n log.info(`Starting periodic screenshots every ${intervalSecs}s`);\n this._screenshotInterval = setInterval(() => {\n this.captureAndSubmitScreenshot();\n }, intervalMs);\n }\n\n /**\n * Update status message (Platform-specific UI)\n */\n private updateStatus(message: string, type: 'info' | 'error' = 'info') {\n const statusEl = document.getElementById('status');\n if (statusEl) {\n statusEl.textContent = message;\n statusEl.className = `status status-${type}`;\n }\n if (type === 'error') {\n log.error('Status:', message);\n } else {\n log.info('Status:', message);\n }\n }\n\n private showOfflineIndicator() {\n this.timelineOverlay?.setOffline(true);\n }\n\n private removeOfflineIndicator() {\n this.timelineOverlay?.setOffline(false);\n }\n\n /**\n * Check if the sync lead is alive (for follower delegation).\n * Returns true if any peer with role 'lead' has been seen in the last 15s.\n */\n private _syncLeadAlive(): boolean {\n if (!this.syncManager) return false;\n for (const [, peer] of this.syncManager.followers) {\n if (peer.role === 'lead' && Date.now() - peer.lastSeen < 15000) {\n return true;\n }\n }\n return false;\n }\n\n /**\n * Cleanup\n */\n cleanup() {\n this.core.cleanup();\n this.renderer.cleanup();\n\n if (this._screenshotInterval) {\n clearInterval(this._screenshotInterval);\n this._screenshotInterval = null;\n }\n\n if (this._wakeLock) {\n this._wakeLock.release();\n this._wakeLock = null;\n }\n\n if (this.downloadOverlay) {\n this.downloadOverlay.destroy();\n }\n\n if (this.timelineOverlay) {\n this.timelineOverlay.destroy();\n }\n\n // Disconnect iframe observer\n if (this._iframeObserver) {\n this._iframeObserver.disconnect();\n this._iframeObserver = null;\n }\n\n // Remove SW message listeners\n if (navigator.serviceWorker) {\n if (this._swIcHandler) {\n navigator.serviceWorker.removeEventListener('message', this._swIcHandler);\n this._swIcHandler = null;\n }\n }\n\n // Clean up DownloadManager\n downloadManager?.clear();\n\n if (this._probeTimer) {\n clearTimeout(this._probeTimer);\n this._probeTimer = null;\n }\n\n if (this._mediaStatusTimer) {\n clearTimeout(this._mediaStatusTimer);\n this._mediaStatusTimer = null;\n }\n }\n}\n\nfunction startPlayer() {\n const player = new PwaPlayer();\n player.init().catch(error => {\n log.error('Failed to initialize:', error);\n // First boot with bad config — redirect to setup so user can fix it\n log.warn('Redirecting to setup screen...');\n window.location.href = './setup.html';\n });\n window.addEventListener('beforeunload', () => {\n player.cleanup();\n });\n}\n\nif (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', startPlayer);\n} else {\n startPlayer();\n}\n"],"file":"main-BytxDgA6.js"}
|
|
1
|
+
{"version":3,"mappings":"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","names":["log","cacheWidgetHtml","VERSION","pkg","E"],"ignoreList":[],"sources":["../../../renderer/src/layout-pool.js","../../../renderer/src/renderer-lite.js","../../../renderer/src/layout.js","../../../renderer/src/index.js","../../../core/src/data-connectors.js","../../../core/src/layout-blacklist.js","../../../core/src/events.js","../../../core/src/player-core.js","../../../core/src/index.js","../../src/download-overlay.ts","../../src/timeline-overlay.ts","../../src/setup-overlay.ts","../../src/main.ts"],"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 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 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 // 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 * 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 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 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 (instant transition).\n * Hides the current layout container and shows the preloaded one,\n * then starts widget cycling and layout timer.\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 // ── 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 preloaded.container.style.visibility = 'visible';\n preloaded.container.style.zIndex = '0';\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 this.log.info(`Swapped to preloaded layout ${layoutId} (instant transition)`);\n this._logResourceStats(layoutId);\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\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 = 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 = '${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 = '${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 = '${videoSrc}';\n video.dataset.filename = '${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 === '${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 = '${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('${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 = '${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","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * DataConnectorManager - Manages real-time data connectors from CMS\n *\n * Data connectors allow widgets to receive real-time data from CMS-configured\n * data sources. The CMS sends data connector configuration via the schedule XML,\n * and this manager periodically polls the data source URLs, stores the data,\n * and emits events so the IC /realtime route can serve it to widgets.\n *\n * Usage:\n * const manager = new DataConnectorManager();\n * manager.setConnectors(schedule.dataConnectors);\n * manager.startPolling();\n *\n * // Get data for a widget\n * const data = manager.getData('weather_data');\n *\n * // Listen for updates\n * manager.on('data-updated', (dataKey, data) => { ... });\n */\n\nimport { EventEmitter, createLogger, fetchWithRetry } from '@xiboplayer/utils';\n\nconst log = createLogger('DataConnector');\n\nconst MAX_BACKOFF_MS = 300000; // 5 minutes\nconst CIRCUIT_BREAKER_THRESHOLD = 3;\n\nexport class DataConnectorManager extends EventEmitter {\n constructor() {\n super();\n\n // dataKey -> { config, data, timer, lastFetch, failures }\n this.connectors = new Map();\n }\n\n /**\n * Set active connectors from schedule\n * Stops any existing polling and reconfigures with new connector list.\n * @param {Array} connectors - Array of connector config objects from schedule XML\n * Each: { id, dataConnectorId, dataKey, url, updateInterval }\n */\n setConnectors(connectors) {\n // Stop existing polling before reconfiguring\n this.stopPolling();\n\n // Clear previous connectors\n this.connectors.clear();\n\n if (!connectors || connectors.length === 0) {\n log.debug('No data connectors configured');\n return;\n }\n\n for (const connector of connectors) {\n if (!connector.dataKey || !connector.url) {\n log.warn('Skipping data connector with missing dataKey or url:', connector);\n continue;\n }\n\n this.connectors.set(connector.dataKey, {\n config: connector,\n data: null,\n timer: null,\n lastFetch: null,\n failures: 0\n });\n\n log.info(`Registered data connector: ${connector.dataKey} (interval: ${connector.updateInterval}s)`);\n }\n\n log.info(`${this.connectors.size} data connector(s) configured`);\n }\n\n /**\n * Start polling for all active connectors\n * Performs an initial fetch immediately, then sets up periodic polling.\n */\n startPolling() {\n for (const [dataKey, entry] of this.connectors.entries()) {\n const { config } = entry;\n const intervalMs = (config.updateInterval || 300) * 1000;\n\n // Fetch immediately on start\n this.fetchData(entry).catch(err => {\n log.error(`Initial fetch failed for ${dataKey}:`, err);\n });\n\n // Set up periodic polling\n entry.timer = setInterval(() => {\n this.fetchData(entry).catch(err => {\n log.error(`Polling fetch failed for ${dataKey}:`, err);\n });\n }, intervalMs);\n\n log.debug(`Started polling for ${dataKey} every ${config.updateInterval}s`);\n }\n }\n\n /**\n * Stop all polling timers\n */\n stopPolling() {\n for (const [dataKey, entry] of this.connectors.entries()) {\n if (entry.timer) {\n clearInterval(entry.timer);\n entry.timer = null;\n log.debug(`Stopped polling for ${dataKey}`);\n }\n }\n }\n\n /**\n * Get current data for a dataKey\n * @param {string} dataKey - The data key to look up\n * @returns {Object|null} The stored data, or null if not available\n */\n getData(dataKey) {\n const entry = this.connectors.get(dataKey);\n if (!entry) {\n log.debug(`No data connector found for key: ${dataKey}`);\n return null;\n }\n return entry.data;\n }\n\n /**\n * Get all data keys that have data available\n * @returns {string[]} Array of data keys with data\n */\n getAvailableKeys() {\n const keys = [];\n for (const [dataKey, entry] of this.connectors.entries()) {\n if (entry.data !== null) {\n keys.push(dataKey);\n }\n }\n return keys;\n }\n\n /**\n * Internal: fetch data from CMS data source\n * @param {Object} entry - Connector entry from this.connectors\n */\n async fetchData(entry) {\n const { config } = entry;\n const { dataKey, url } = config;\n\n log.debug(`Fetching data for ${dataKey}: ${url}`);\n\n try {\n const response = await fetchWithRetry(url, {\n method: 'GET',\n headers: {\n 'Accept': 'application/json'\n }\n }, { maxRetries: 2, baseDelayMs: 2000 });\n\n if (!response.ok) {\n log.warn(`Data connector ${dataKey} returned ${response.status}: ${response.statusText}`);\n return;\n }\n\n const contentType = response.headers.get('Content-Type') || '';\n let data;\n\n if (contentType.includes('application/json')) {\n data = await response.json();\n } else {\n // Store as raw text if not JSON\n data = await response.text();\n }\n\n const previousData = entry.data;\n entry.data = data;\n entry.lastFetch = Date.now();\n entry.failures = 0; // Reset on success\n\n log.debug(`Data updated for ${dataKey} (fetched at ${new Date(entry.lastFetch).toISOString()})`);\n\n // Restore normal polling interval if it was backed off\n this._ensureNormalPolling(entry);\n\n // Emit event for listeners (IC route, platform layer)\n this.emit('data-updated', dataKey, data);\n\n // Emit a specific event if data actually changed\n if (JSON.stringify(previousData) !== JSON.stringify(data)) {\n this.emit('data-changed', dataKey, data);\n }\n\n } catch (error) {\n entry.failures = (entry.failures || 0) + 1;\n log.error(`Failed to fetch data for ${dataKey} (${entry.failures}x):`, error);\n this.emit('fetch-error', dataKey, error);\n\n // Circuit breaker: slow down polling after repeated failures\n if (entry.failures >= CIRCUIT_BREAKER_THRESHOLD && entry.timer) {\n const baseMs = (config.updateInterval || 300) * 1000;\n const backoffMs = Math.min(baseMs * 2 ** (entry.failures - CIRCUIT_BREAKER_THRESHOLD + 1), MAX_BACKOFF_MS);\n clearInterval(entry.timer);\n entry.timer = setTimeout(() => {\n this.fetchData(entry).catch(() => {});\n // Re-arm with backoff interval\n entry.timer = setInterval(() => {\n this.fetchData(entry).catch(() => {});\n }, backoffMs);\n }, backoffMs);\n log.warn(`Circuit breaker: ${dataKey} backing off to ${Math.round(backoffMs / 1000)}s`);\n }\n }\n }\n\n /**\n * Restore normal polling interval after circuit breaker backoff.\n * @private\n */\n _ensureNormalPolling(entry) {\n if (entry.failures === 0 && entry.timer) {\n const baseMs = (entry.config.updateInterval || 300) * 1000;\n // Clear any backed-off timer and restore the normal interval\n clearInterval(entry.timer);\n clearTimeout(entry.timer);\n entry.timer = setInterval(() => {\n this.fetchData(entry).catch(() => {});\n }, baseMs);\n }\n }\n\n /**\n * Force refresh all connectors — re-fetch immediately and restart polling.\n * Called by XMR dataUpdate command.\n */\n refreshAll() {\n if (this.connectors.size === 0) return;\n\n log.info(`Refreshing all ${this.connectors.size} data connector(s)`);\n this.stopPolling();\n this.startPolling();\n }\n\n /**\n * Cleanup - stop all polling and remove listeners\n */\n cleanup() {\n this.stopPolling();\n this.connectors.clear();\n this.removeAllListeners();\n log.debug('DataConnectorManager 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 blacklist — tracks consecutive rendering failures and\n * blacklists layouts that fail repeatedly to prevent crash loops.\n */\n\nimport { createLogger } from '@xiboplayer/utils';\n\nconst log = createLogger('Blacklist');\n\nexport class LayoutBlacklist {\n /**\n * @param {number} [threshold=3] - Consecutive failures before blacklisting\n */\n constructor(threshold = 3) {\n this._entries = new Map();\n this._threshold = threshold;\n }\n\n /**\n * Record a layout rendering failure.\n * @param {number} layoutId\n * @param {string} reason\n * @returns {{ blacklisted: boolean, failures: number }} Current state after recording\n */\n recordFailure(layoutId, reason) {\n const id = Number(layoutId);\n const entry = this._entries.get(id) || { failures: 0, blacklisted: false, reason: '' };\n entry.failures++;\n entry.reason = reason;\n\n if (!entry.blacklisted && entry.failures >= this._threshold) {\n entry.blacklisted = true;\n log.warn(`Layout ${id} blacklisted after ${entry.failures} consecutive failures: ${reason}`);\n } else if (!entry.blacklisted) {\n log.info(`Layout ${id} failure ${entry.failures}/${this._threshold}: ${reason}`);\n }\n\n this._entries.set(id, entry);\n return { blacklisted: entry.blacklisted, failures: entry.failures };\n }\n\n /**\n * Record a successful layout render. Resets failure counter.\n * @param {number} layoutId\n * @returns {boolean} true if the layout was previously blacklisted (now restored)\n */\n recordSuccess(layoutId) {\n const id = Number(layoutId);\n if (!this._entries.has(id)) return false;\n\n const was = this._entries.get(id);\n this._entries.delete(id);\n\n if (was.blacklisted) {\n log.info(`Layout ${id} removed from blacklist (rendered successfully)`);\n return true;\n }\n return false;\n }\n\n /**\n * Check if a layout is currently blacklisted.\n * @param {number} layoutId\n * @returns {boolean}\n */\n isBlacklisted(layoutId) {\n const entry = this._entries.get(Number(layoutId));\n return entry?.blacklisted === true;\n }\n\n /**\n * Get all currently blacklisted layout IDs.\n * @returns {number[]}\n */\n getBlacklistedIds() {\n const result = [];\n for (const [id, entry] of this._entries) {\n if (entry.blacklisted) result.push(id);\n }\n return result;\n }\n\n /**\n * Reset the blacklist. Called when RequiredFiles changes.\n * @returns {number} Number of entries cleared\n */\n reset() {\n const count = this._entries.size;\n if (count > 0) {\n log.info(`Blacklist reset (${count} entries cleared)`);\n this._entries.clear();\n }\n return count;\n }\n\n get size() {\n return this._entries.size;\n }\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Core event name constants — shared between PlayerCore and platform layers.\n * Using constants prevents silent typo bugs at the core/platform boundary.\n */\n\nexport const CORE_EVENTS = Object.freeze({\n // Collection lifecycle\n COLLECTION_START: 'collection-start',\n COLLECTION_COMPLETE: 'collection-complete',\n COLLECTION_ERROR: 'collection-error',\n\n // Registration\n REGISTER_COMPLETE: 'register-complete',\n\n // Schedule\n SCHEDULE_RECEIVED: 'schedule-received',\n LAYOUTS_SCHEDULED: 'layouts-scheduled',\n NO_LAYOUTS_SCHEDULED: 'no-layouts-scheduled',\n TIMELINE_UPDATED: 'timeline-updated',\n\n // Layout lifecycle\n LAYOUT_PREPARE_REQUEST: 'layout-prepare-request',\n LAYOUT_EXPIRE_CURRENT: 'layout-expire-current',\n LAYOUT_ALREADY_PLAYING: 'layout-already-playing',\n CHECK_PENDING_LAYOUT: 'check-pending-layout',\n\n // Downloads\n FILES_RECEIVED: 'files-received',\n DOWNLOAD_REQUEST: 'download-request',\n\n // Overlay\n OVERLAY_LAYOUT_REQUEST: 'overlay-layout-request',\n REVERT_TO_SCHEDULE: 'revert-to-schedule',\n\n // Sync\n SYNC_CONFIG: 'sync-config',\n\n // XMR\n XMR_CONNECTED: 'xmr-connected',\n XMR_RECONNECTED: 'xmr-reconnected',\n XMR_MISCONFIGURED: 'xmr-misconfigured',\n\n // Navigation\n NAVIGATE_TO_WIDGET: 'navigate-to-widget',\n\n // Commands\n EXECUTE_NATIVE_COMMAND: 'execute-native-command',\n SCHEDULED_COMMAND: 'scheduled-command',\n COMMAND_RESULT: 'command-result',\n\n // Screenshots\n SCREENSHOT_REQUEST: 'screenshot-request',\n\n // Stats/Logs/Faults\n SUBMIT_STATS_REQUEST: 'submit-stats-request',\n SUBMIT_LOGS_REQUEST: 'submit-logs-request',\n SUBMIT_FAULTS_REQUEST: 'submit-faults-request',\n\n // Cache\n CACHE_ANALYSIS: 'cache-analysis',\n\n // Collection\n COLLECTION_INTERVAL_SET: 'collection-interval-set',\n COLLECTION_INTERVAL_UPDATED: 'collection-interval-updated',\n\n // Settings\n LOG_LEVEL_CHANGED: 'log-level-changed',\n OFFLINE_MODE: 'offline-mode',\n\n // Purge\n PURGE_REQUEST: 'purge-request',\n PURGE_ALL_REQUEST: 'purge-all-request',\n});\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * PlayerCore - Platform-independent orchestration module\n *\n * Pure orchestration logic without platform-specific concerns (UI, DOM, storage).\n * Can be reused across PWA, Electron, mobile platforms.\n *\n * Architecture:\n * ┌─────────────────────────────────────────────────────┐\n * │ PlayerCore (Pure Orchestration) │\n * │ - Collection cycle coordination │\n * │ - Schedule checking │\n * │ - Layout transition logic │\n * │ - Event emission (not DOM manipulation) │\n * │ - XMDS communication │\n * │ - XMR integration │\n * └─────────────────────────────────────────────────────┘\n * ↓\n * ┌─────────────────────────────────────────────────────┐\n * │ Platform Layer (PWA/Electron/Mobile) │\n * │ - UI updates (status display, progress bars) │\n * │ - DOM manipulation │\n * │ - Platform-specific storage │\n * │ - Blob URL management │\n * │ - Event listeners for PlayerCore events │\n * └─────────────────────────────────────────────────────┘\n *\n * Usage:\n * const core = new PlayerCore({\n * config,\n * xmds,\n * cache,\n * schedule,\n * renderer,\n * xmrWrapper\n * });\n *\n * // Listen to events\n * core.on('collection-start', () => { ... });\n * core.on('layout-ready', (layoutId) => { ... });\n *\n * // Start collection\n * await core.collect();\n */\n\nimport { EventEmitter, createLogger, applyCmsLogLevel, openIDB } from '@xiboplayer/utils';\nimport { calculateTimeline, parseLayoutFile } from '@xiboplayer/schedule';\nimport { CacheAnalyzer } from '@xiboplayer/cache';\nimport { DataConnectorManager } from './data-connectors.js';\nimport { LayoutBlacklist } from './layout-blacklist.js';\nimport { CORE_EVENTS as E } from './events.js';\n\nconst log = createLogger('PlayerCore');\n\n/**\n * Discover a local/LAN IP address.\n * Electron: os.networkInterfaces() via preload (reliable, skips VPN/Docker).\n * Chromium/browser: proxy endpoint GET /system/lan-ip (Node.js has os.networkInterfaces()).\n */\nasync function discoverLanIp() {\n if (typeof window !== 'undefined' && window.electronAPI?.getLanIpAddress) {\n try { return await window.electronAPI.getLanIpAddress(); } catch (_) {}\n }\n // Fallback: ask the proxy server (works in Chromium kiosk and any browser)\n try {\n const fetcher = globalThis.__nativeFetch || globalThis.fetch;\n const res = await fetcher('/system/lan-ip');\n if (res.ok) {\n const { ip } = await res.json();\n if (ip) return ip;\n }\n } catch (_) {}\n return '';\n}\n\n// IndexedDB database/store for offline cache\nconst OFFLINE_DB_BASE = 'xibo-offline-cache';\nconst OFFLINE_DB_VERSION = 1;\nconst OFFLINE_STORE = 'cache';\n\n\n/** Open the offline cache IndexedDB (creates store on first use) */\nfunction openOfflineDb(cmsId) {\n const dbName = cmsId ? `${OFFLINE_DB_BASE}-${cmsId}` : OFFLINE_DB_BASE;\n return openIDB(dbName, OFFLINE_DB_VERSION, OFFLINE_STORE);\n}\n\nexport class PlayerCore extends EventEmitter {\n constructor(options) {\n super();\n\n // Required dependencies (injected)\n this.config = options.config;\n this.xmds = options.xmds;\n this.cache = options.cache;\n this.schedule = options.schedule;\n this.renderer = options.renderer;\n this.XmrWrapper = options.xmrWrapper;\n this.statsCollector = options.statsCollector; // Optional: proof of play tracking\n this.displaySettings = options.displaySettings; // Optional: CMS display settings manager\n\n // CMS ID for namespaced IndexedDB databases\n this._cmsId = options.cmsId || null;\n\n // Data connectors manager (real-time data for widgets)\n this.dataConnectorManager = new DataConnectorManager();\n\n // Discover LAN IP early (async, non-blocking)\n discoverLanIp().then((ip) => {\n this._lanIpAddress = ip;\n log.info('LAN IP:', ip || '(not discovered)');\n });\n\n // State\n this.xmr = null;\n this.currentLayoutId = null;\n this.collecting = false;\n this.collectionInterval = null;\n this.pendingLayouts = new Map(); // layoutId -> required media IDs\n this._layoutMediaStatus = new Map(); // layoutFile → { ready: boolean, missing: string[] }\n this.offlineMode = false; // Track whether we're currently in offline mode\n this._normalCollectInterval = null; // Saved interval to restore after offline retry\n this._offlineRetrySeconds = 0; // Current backoff interval (0 = not retrying)\n\n // CRC32 checksums for skip optimization (avoid redundant XMDS calls)\n this._lastCheckRf = null;\n this._lastCheckSchedule = null;\n\n // Timeline recalculation guard — skip when inputs haven't changed\n this._lastTimelineFingerprint = null;\n this._lastTimeline = null;\n\n // Layout override state (for changeLayout/overlayLayout via XMR → revertToSchedule)\n this._layoutOverride = null; // { layoutId, type: 'change'|'overlay' }\n this._lastRequiredFiles = []; // Track files for MediaInventory\n\n // Scheduled commands tracking (avoid re-executing same command)\n this._executedCommands = new Set();\n\n // Display commands from RegisterDisplay (used by XMR commandAction)\n this.displayCommands = null;\n\n // Fault reporting agent (independent timer, faster than collection cycle)\n this._faultReportingInterval = null;\n this._faultReportingSeconds = 60; // Default: check for faults every 60s\n\n // Unsafe layout blacklist: layoutId → { failures: number, blacklisted: boolean, reason: string }\n this._layoutBlacklist = new LayoutBlacklist(3);\n\n // Status tracking for NotifyStatus enrichment\n this._lastLayoutChangeTime = null; // ISO timestamp of last layout switch\n this._statusCode = 2; // 1=running, 2=downloading, 3=error\n\n // Dynamic layout tracking (useDuration=0 videos — must play to natural end)\n this._dynamicLayouts = new Set();\n\n // Multi-display sync configuration (from RegisterDisplay syncGroup settings)\n this.syncConfig = null;\n this.syncManager = null; // Optional: set via setSyncManager() after RegisterDisplay\n\n // Layout durations for timeline calculation (layoutFile/layoutId → seconds)\n this._layoutDurations = new Map();\n this._finalDurations = new Set(); // layoutFiles whose duration is definitive (all videos probed)\n\n // Guard: layout currently being prepared (async prepareAndRenderLayout in flight)\n this._preparingLayoutId = null;\n\n // Cache analyzer for stale media detection and storage health\n this.cacheAnalyzer = this.cache ? new CacheAnalyzer(this.cache) : null;\n\n // In-memory offline cache (populated from IndexedDB on first load)\n this._offlineCache = { schedule: null, settings: null, requiredFiles: null };\n this._offlineDbReady = this._initOfflineCache();\n }\n\n /** Schedule queue options — avoids repeating this object in 8 call sites */\n get _queueOptions() {\n return { dynamicLayouts: this._dynamicLayouts };\n }\n\n /**\n * Schedule an auto-revert timer for layout/overlay overrides.\n * @param {number} id - Layout ID\n * @param {number} duration - Duration in seconds (0 = no timer)\n * @param {string} label - Description for logging\n */\n _scheduleAutoRevert(id, duration, label) {\n if (duration > 0) {\n setTimeout(() => {\n if (this._layoutOverride?.layoutId === id) {\n log.info(`${label} duration expired (${duration}s), reverting to schedule`);\n this.revertToSchedule();\n }\n }, duration * 1000);\n }\n }\n\n // ── Offline Cache (IndexedDB) ──────────────────────────────────────\n\n /** Load offline cache from IndexedDB into memory on startup */\n async _initOfflineCache() {\n try {\n const db = await openOfflineDb(this._cmsId);\n const tx = db.transaction(OFFLINE_STORE, 'readonly');\n const store = tx.objectStore(OFFLINE_STORE);\n\n const [schedule, settings, requiredFiles, durations, finalDurations, durVersion] = await Promise.all([\n new Promise(r => { const req = store.get('schedule'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n new Promise(r => { const req = store.get('settings'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n new Promise(r => { const req = store.get('requiredFiles'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n new Promise(r => { const req = store.get('durations'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n new Promise(r => { const req = store.get('finalDurations'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n new Promise(r => { const req = store.get('durationsVersion'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n ]);\n\n if (Array.isArray(durations) && durations.length > 0) {\n for (const [k, v] of durations) this._layoutDurations.set(k, v);\n log.info(`[Timeline] Restored ${durations.length} cached durations from IDB`);\n }\n // v2: clear stale final durations from before the fix.\n // Final durations are only valid when set by video metadata / probeLayoutDurations,\n // not by XLF estimates. Old IDB data has 60s defaults marked as final.\n if (durVersion >= 2 && Array.isArray(finalDurations) && finalDurations.length > 0) {\n for (const k of finalDurations) this._finalDurations.add(k);\n log.info(`[Timeline] Restored ${finalDurations.length} final duration keys from IDB`);\n } else if (Array.isArray(finalDurations) && finalDurations.length > 0) {\n log.info(`[Timeline] Discarded ${finalDurations.length} stale final duration keys (pre-v2)`);\n }\n\n this._offlineCache = { schedule, settings, requiredFiles };\n this._offlineDb = db; // Keep handle open for _offlineSave (avoids reopen per write)\n log.info('Offline cache loaded from IndexedDB',\n schedule ? '(has schedule)' : '(empty)');\n } catch (e) {\n log.warn('Failed to load offline cache from IndexedDB:', e);\n }\n }\n\n /** Save a key to both in-memory cache and IndexedDB (fire-and-forget) */\n async _offlineSave(key, data) {\n this._offlineCache[key] = data;\n try {\n // Reuse persistent handle from _initOfflineCache (avoids 6 open/close per cycle)\n if (!this._offlineDb) {\n this._offlineDb = await openOfflineDb(this._cmsId);\n }\n const tx = this._offlineDb.transaction(OFFLINE_STORE, 'readwrite');\n tx.objectStore(OFFLINE_STORE).put(data, key);\n await new Promise((resolve, reject) => {\n tx.oncomplete = resolve;\n tx.onerror = () => reject(tx.error);\n });\n } catch (e) {\n // Handle closed/invalid DB — reopen on next attempt\n this._offlineDb = null;\n log.warn('Failed to save offline cache:', key, e);\n }\n }\n\n /** Check if we have any cached data to fall back on */\n hasCachedData() {\n return this._offlineCache.schedule !== null;\n }\n\n /** Check if the browser reports being offline */\n isOffline() {\n return typeof navigator !== 'undefined' && navigator.onLine === false;\n }\n\n /** Check if currently in offline mode */\n isInOfflineMode() {\n return this.offlineMode;\n }\n\n /**\n * Run an offline collection cycle using cached data.\n * Evaluates the cached schedule and continues playback.\n */\n collectOffline() {\n log.warn('Offline mode — using cached schedule');\n\n if (!this.offlineMode) {\n this.offlineMode = true;\n this.emit(E.OFFLINE_MODE, true);\n }\n\n // Exponential backoff: 30s → 60s → 120s → ... → capped at normal interval\n // Recovers quickly from brief outages but doesn't hammer when truly offline\n if (this.collectionInterval) {\n if (!this._normalCollectInterval) {\n this._normalCollectInterval = this._currentCollectInterval;\n this._offlineRetrySeconds = 30;\n } else {\n // Double the backoff, cap at normal interval\n this._offlineRetrySeconds = Math.min(\n this._offlineRetrySeconds * 2,\n this._normalCollectInterval\n );\n }\n this._setCollectionTimer(this._offlineRetrySeconds);\n log.info(`Offline: retry in ${this._offlineRetrySeconds}s`);\n }\n\n // Load cached settings for collection interval (first run only)\n if (!this.collectionInterval) {\n const cachedReg = this._offlineCache.settings;\n if (cachedReg?.settings) {\n this.setupCollectionInterval(cachedReg.settings);\n this._normalCollectInterval = this._currentCollectInterval;\n this._offlineRetrySeconds = 30;\n this._setCollectionTimer(this._offlineRetrySeconds);\n log.info(`Offline: retry in ${this._offlineRetrySeconds}s`);\n }\n }\n\n // Load cached schedule and apply it\n const cachedSchedule = this._offlineCache.schedule;\n if (cachedSchedule) {\n this.schedule.setSchedule(cachedSchedule);\n this.emit(E.SCHEDULE_RECEIVED, cachedSchedule);\n }\n\n // Evaluate current schedule\n const layoutFiles = this.schedule.getCurrentLayouts();\n log.info('Offline layouts:', layoutFiles);\n this.emit(E.LAYOUTS_SCHEDULED, layoutFiles);\n\n this._evaluateAndSwitchLayout(layoutFiles, 'Offline');\n\n this.emit(E.COLLECTION_COMPLETE);\n }\n\n /**\n * Evaluate the current schedule and switch layouts if needed.\n * Shared by both collect() and collectOffline() after emitting 'layouts-scheduled'.\n * @param {string[]} layoutFiles - Currently scheduled layout filenames\n * @param {string} context - Log context label (e.g. 'Offline' or '')\n */\n _evaluateAndSwitchLayout(layoutFiles, context) {\n const prefix = context ? `${context}: ` : '';\n\n // Use the queue (not raw layoutFiles) for play/expire decisions.\n // The queue has all constraints baked in (maxPlaysPerHour, priorities, dayparting).\n // The player is a dumb consumer — it only expires when the queue rebuilds\n // with a different layout set (new CMS schedule, daypart boundary crossed).\n const { queue } = this.schedule.getScheduleQueue(this._layoutDurations, this._queueOptions);\n\n if (queue.length > 0) {\n if (this.currentLayoutId) {\n const stillInQueue = queue.some(e => parseLayoutFile(e.layoutId) === this.currentLayoutId);\n\n if (!stillInQueue) {\n // Schedule changed and current layout is no longer in the queue — expire immediately.\n // Clear currentLayoutId and emit expire event so the renderer can teardown.\n // The renderer's layoutEnd → advanceToNextLayout flow handles the switch.\n log.info(`Layout ${this.currentLayoutId} no longer in queue — expiring`);\n this.currentLayoutId = null;\n this.emit(E.LAYOUT_EXPIRE_CURRENT);\n } else {\n // Layout is still in queue — don't interrupt, just rebuild queue in background.\n // The playing layout ends when its timer fires (layoutEnd event),\n // at which point advanceToNextLayout() pops from the already-updated queue.\n log.info(`Layout ${this.currentLayoutId} playing — queue updated in background, playback continues`);\n this.emit(E.LAYOUT_ALREADY_PLAYING, this.currentLayoutId);\n }\n } else if (!this._preparingLayoutId) {\n // No layout playing or being prepared — start one from the queue.\n // Guard with _preparingLayoutId to prevent a second _evaluateAndSwitchLayout\n // call (e.g. offline-restore then online-collect) from popping another layout\n // before the async prepareAndRenderLayout completes.\n const next = this.getNextLayout();\n if (next) {\n this._preparingLayoutId = next.layoutId;\n log.info(`${prefix}switching to layout ${next.layoutId}`);\n this.emit(E.LAYOUT_PREPARE_REQUEST, next.layoutId);\n }\n } else {\n log.info(`${prefix}layout ${this._preparingLayoutId} already being prepared, skipping`);\n }\n } else {\n log.info(`${context ? `${context}: n` : 'N'}o layouts${context ? ' in cached schedule' : ' scheduled, falling back to default'}`);\n this.emit(E.NO_LAYOUTS_SCHEDULED);\n }\n\n this.logUpcomingTimeline();\n }\n\n /**\n * Force an immediate collection (used by platform layer on 'online' event)\n */\n async collectNow() {\n this._lastCheckRf = null;\n this._lastCheckSchedule = null;\n return this.collect();\n }\n\n /**\n * Start collection cycle\n * Pure orchestration - emits events instead of updating UI\n */\n async collect() {\n // Prevent concurrent collections\n if (this.collecting) {\n log.debug('Collection already in progress, skipping');\n return;\n }\n\n this.collecting = true;\n\n try {\n // Ensure offline cache is loaded from IndexedDB before checking\n await this._offlineDbReady;\n\n log.info('Starting collection cycle...');\n this.emit(E.COLLECTION_START);\n\n // Check if browser reports offline\n if (this.isOffline()) {\n if (this.hasCachedData()) {\n this.collecting = false;\n return this.collectOffline();\n }\n throw new Error('Offline with no cached data — cannot start playback');\n }\n\n // Ensure RSA key pair exists before registering\n if (this.config.ensureXmrKeyPair) {\n await this.config.ensureXmrKeyPair();\n }\n\n // Register display\n log.debug('Collection step: registerDisplay');\n const regResult = await this.xmds.registerDisplay();\n log.info(`Display registered: ${regResult.code}${regResult.tags?.length ? `, tags: ${regResult.tags.join(', ')}` : ''}`);\n log.debug('Register result:', JSON.stringify(regResult));\n\n this._processRegistration(regResult);\n\n // Initialize XMR if available\n log.debug('Collection step: initializeXmr');\n await this.initializeXmr(regResult);\n\n // CRC32 skip optimization: only fetch RequiredFiles/Schedule when CMS data changed\n const checkRf = regResult.checkRf || '';\n const checkSchedule = regResult.checkSchedule || '';\n\n // Get required files (skip if CRC unchanged)\n if (!this._lastCheckRf || this._lastCheckRf !== checkRf) {\n // RequiredFiles changed — CMS may have fixed broken layouts\n this.resetBlacklist();\n\n log.debug('Collection step: requiredFiles');\n const rfResult = await this.xmds.requiredFiles();\n // RequiredFiles returns { files, purge } — files to download, items to delete\n const files = rfResult.files || rfResult;\n const purgeItems = rfResult.purge || [];\n log.info('Required files:', files.length, purgeItems.length > 0 ? `(+ ${purgeItems.length} purge)` : '');\n this._lastCheckRf = checkRf;\n this.emit(E.FILES_RECEIVED, files);\n\n // Cache required files for offline use\n this._offlineSave('requiredFiles', rfResult);\n\n if (purgeItems.length > 0) {\n this.emit(E.PURGE_REQUEST, purgeItems);\n }\n\n // Get schedule (skip if CRC unchanged)\n if (!this._lastCheckSchedule || this._lastCheckSchedule !== checkSchedule) {\n log.debug('Collection step: schedule');\n const schedule = await this.xmds.schedule();\n log.info('Schedule received');\n this._lastCheckSchedule = checkSchedule;\n log.debug('Collection step: processing schedule');\n this._applyNewSchedule(schedule);\n this.logUpcomingTimeline();\n }\n\n log.debug('Collection step: download-request + mediaInventory');\n const currentLayouts = this.schedule.getCurrentLayouts();\n\n // Layout IDs in playback order (from the pre-calculated queue)\n const { queue } = this.schedule.getScheduleQueue(\n this._layoutDurations,\n this._queueOptions\n );\n const layoutOrder = [...new Set(queue.map(e => parseLayoutFile(e.layoutId)))];\n\n this._lastRequiredFiles = files;\n\n // Download window enforcement (#81) — skip downloads outside configured window\n if (this.displaySettings?.isInDownloadWindow && !this.displaySettings.isInDownloadWindow()) {\n const nextWindow = this.displaySettings.getNextDownloadWindow?.();\n log.info(`Outside download window, skipping downloads${nextWindow ? ` (next: ${nextWindow.toLocaleTimeString()})` : ''}`);\n } else {\n this.emit(E.DOWNLOAD_REQUEST, { layoutOrder, files, layoutDependants: Object.fromEntries(this.schedule.getDependantsMap()) });\n }\n\n // Non-blocking cache analysis (stale media detection)\n if (this.cacheAnalyzer) {\n this.cacheAnalyzer.analyze(files).then(report => {\n this.emit(E.CACHE_ANALYSIS, report);\n }).catch(err => log.warn('Cache analysis failed:', err));\n }\n\n // Submit media inventory to CMS (reports cached files)\n this.submitMediaInventory(files);\n } else {\n if (checkRf) {\n log.info('RequiredFiles CRC unchanged, skipping download check');\n }\n if (this._lastCheckSchedule !== checkSchedule) {\n const schedule = await this.xmds.schedule();\n log.info('Schedule received (RF unchanged but schedule changed)');\n this._lastCheckSchedule = checkSchedule;\n this._applyNewSchedule(schedule);\n } else if (checkSchedule) {\n log.info('Schedule CRC unchanged, skipping');\n }\n }\n\n // Fetch weather data for schedule criteria evaluation (#15)\n await this._fetchWeatherData();\n\n log.debug('Collection step: evaluateSchedule');\n // Evaluate current schedule\n const layoutFiles = this.schedule.getCurrentLayouts();\n log.info('Current layouts:', layoutFiles);\n this.emit(E.LAYOUTS_SCHEDULED, layoutFiles);\n\n this._evaluateAndSwitchLayout(layoutFiles, '');\n\n // Process scheduled commands (auto-execute commands whose time has arrived)\n this._processScheduledCommands();\n\n // Submit stats if enabled and collector is available\n if (regResult.settings?.statsEnabled === 'On' || regResult.settings?.statsEnabled === '1') {\n if (this.statsCollector) {\n log.info('Stats enabled, submitting proof of play');\n this.emit(E.SUBMIT_STATS_REQUEST);\n } else {\n log.warn('Stats enabled but no StatsCollector provided');\n }\n }\n\n // Submit logs to CMS (always, regardless of stats setting)\n this.emit(E.SUBMIT_LOGS_REQUEST);\n\n // Submit faults immediately (higher priority than logs)\n this.emit(E.SUBMIT_FAULTS_REQUEST);\n\n // Setup collection interval on first run\n if (!this.collectionInterval && regResult.settings) {\n this.setupCollectionInterval(regResult.settings);\n }\n\n // Start fault reporting agent (independent of collection cycle)\n if (!this._faultReportingInterval) {\n this._startFaultReportingAgent();\n }\n\n // Recalculate timeline after every collection cycle completes,\n // even if schedule CRC was unchanged — durations or time may have shifted.\n this.logUpcomingTimeline();\n\n this.emit(E.COLLECTION_COMPLETE);\n\n } catch (error) {\n // Offline fallback: if network failed but we have cached data, use it\n if (this.hasCachedData()) {\n log.warn('Collection failed, falling back to cached data:', error?.message || error);\n this.emit(E.COLLECTION_ERROR, error);\n this.collecting = false;\n return this.collectOffline();\n }\n\n log.error('Collection error:', error);\n this.emit(E.COLLECTION_ERROR, error);\n throw error;\n } finally {\n this.collecting = false;\n }\n }\n\n /**\n * Process registration result — offline exit, settings, sync config, tags, commands.\n */\n _processRegistration(regResult) {\n // Cache settings for offline use\n this._offlineSave('settings', regResult);\n\n // Exit offline mode if we were in it\n if (this.offlineMode) {\n this.offlineMode = false;\n log.info('Back online — resuming normal collection');\n this.emit(E.OFFLINE_MODE, false);\n\n // Restore normal collection interval (was shortened for offline retry)\n if (this._normalCollectInterval) {\n this._setCollectionTimer(this._normalCollectInterval);\n this._normalCollectInterval = null;\n this._offlineRetrySeconds = 0;\n }\n }\n\n // Apply display settings if DisplaySettings manager is available\n if (this.displaySettings && regResult.settings) {\n const result = this.displaySettings.applySettings(regResult.settings);\n if (result.changed.includes('collectInterval')) {\n this.updateCollectionInterval(result.settings.collectInterval);\n }\n\n // Apply CMS logLevel (respects local overrides)\n if (regResult.settings.logLevel) {\n const applied = applyCmsLogLevel(regResult.settings.logLevel);\n if (applied) {\n log.info('Log level updated from CMS:', regResult.settings.logLevel);\n this.emit(E.LOG_LEVEL_CHANGED, regResult.settings.logLevel);\n }\n }\n }\n\n // Pass display properties to schedule for criteria evaluation\n if (this.schedule?.setDisplayProperties && regResult.settings) {\n this.schedule.setDisplayProperties(regResult.settings);\n }\n\n // Store sync config if display is in a sync group — only emit if CMS config changed\n // (compare raw CMS response, not the mutated config with relayUrl/syncGroupId added by PWA)\n if (regResult.syncConfig) {\n const rawKey = JSON.stringify(regResult.syncConfig);\n if (rawKey !== this._lastRawSyncConfig) {\n this._lastRawSyncConfig = rawKey;\n this.syncConfig = regResult.syncConfig;\n log.info('Sync group:', regResult.syncConfig.isLead ? 'LEAD' : `follower → ${regResult.syncConfig.syncGroup}`,\n `(switchDelay: ${regResult.syncConfig.syncSwitchDelay}ms, videoPauseDelay: ${regResult.syncConfig.syncVideoPauseDelay}ms)`);\n this.emit(E.SYNC_CONFIG, regResult.syncConfig);\n }\n }\n\n // Extract config from display tags (key|value convention)\n this._applyTagConfig(regResult.tags);\n\n // Store display commands for XMR commandAction resolution\n if (regResult.commands && regResult.commands.length > 0) {\n this.displayCommands = {};\n for (const cmd of regResult.commands) {\n this.displayCommands[cmd.commandCode] = cmd;\n }\n log.debug('Display commands:', Object.keys(this.displayCommands).join(', '));\n }\n\n this.emit(E.REGISTER_COMPLETE, regResult);\n }\n\n /**\n * Apply a new schedule from CMS — emit event, update schedule manager,\n * reset executed commands, refresh data connectors, and cache offline.\n */\n _applyNewSchedule(schedule) {\n this.emit(E.SCHEDULE_RECEIVED, schedule);\n this.schedule.setSchedule(schedule);\n this._executedCommands.clear();\n this.updateDataConnectors();\n this._offlineSave('schedule', schedule);\n }\n\n /**\n * Initialize XMR WebSocket connection\n */\n async initializeXmr(regResult) {\n const xmrUrl = regResult.settings?.xmrWebSocketAddress || regResult.settings?.xmrNetworkAddress;\n if (!xmrUrl) {\n log.warn('XMR not configured: no xmrWebSocketAddress or xmrNetworkAddress in CMS settings');\n this.emit(E.XMR_MISCONFIGURED, {\n reason: 'missing',\n message: 'XMR address not configured in CMS. Go to CMS Admin → Settings → Configuration → XMR and set the WebSocket address.',\n });\n return;\n }\n\n // Validate URL protocol — PWA players need ws:// or wss://, not tcp://\n if (xmrUrl.startsWith('tcp://')) {\n log.warn(`XMR address uses tcp:// protocol which is not supported by PWA players: ${xmrUrl}`);\n log.warn('Configure XMR_WS_ADDRESS in CMS Admin → Settings → Configuration → XMR (e.g. wss://your-domain/xmr)');\n this.emit(E.XMR_MISCONFIGURED, {\n reason: 'wrong-protocol',\n url: xmrUrl,\n message: `XMR uses tcp:// protocol (not supported by PWA). Set XMR WebSocket Address to wss://your-domain/xmr in CMS Settings.`,\n });\n return;\n }\n\n // Detect placeholder/example URLs\n if (/example\\.(org|com|net)/i.test(xmrUrl)) {\n log.warn(`XMR address contains placeholder domain: ${xmrUrl}`);\n log.warn('Configure the real XMR address in CMS Admin → Settings → Configuration → XMR');\n this.emit(E.XMR_MISCONFIGURED, {\n reason: 'placeholder',\n url: xmrUrl,\n message: `XMR address is still the default placeholder (${xmrUrl}). Update it in CMS Settings.`,\n });\n return;\n }\n\n const xmrCmsKey = regResult.settings?.xmrCmsKey || regResult.settings?.serverKey || this.config.serverKey;\n log.debug('XMR CMS Key:', xmrCmsKey ? 'present' : 'missing');\n\n if (!this.xmr) {\n log.info('Initializing XMR WebSocket:', xmrUrl);\n this.xmr = new this.XmrWrapper(this.config, this);\n await this.xmr.start(xmrUrl, xmrCmsKey);\n this.emit(E.XMR_CONNECTED, xmrUrl);\n } else if (!this.xmr.isConnected()) {\n log.info('XMR disconnected, attempting to reconnect...');\n await this.xmr.start(xmrUrl, xmrCmsKey);\n this.emit(E.XMR_RECONNECTED, xmrUrl);\n } else {\n log.debug('XMR already connected');\n }\n }\n\n /**\n * Setup collection interval\n */\n setupCollectionInterval(settings) {\n // Use DisplaySettings if available, otherwise fallback to raw settings\n const collectIntervalSeconds = this.displaySettings\n ? this.displaySettings.getCollectInterval()\n : parseInt(settings.collectInterval || '300', 10);\n\n this._setCollectionTimer(collectIntervalSeconds);\n this.emit(E.COLLECTION_INTERVAL_SET, collectIntervalSeconds);\n }\n\n /**\n * Update collection interval dynamically\n * Called when CMS changes the collection interval\n */\n updateCollectionInterval(newIntervalSeconds) {\n if (this.collectionInterval) {\n this._setCollectionTimer(newIntervalSeconds);\n this.emit(E.COLLECTION_INTERVAL_UPDATED, newIntervalSeconds);\n }\n }\n\n /**\n * Start the fault reporting agent.\n * Runs on an independent timer (default 60s) to submit faults faster\n * than the normal collection cycle (300s). This ensures the CMS dashboard\n * gets fault alerts with lower latency.\n */\n _startFaultReportingAgent() {\n if (this._faultReportingInterval) clearInterval(this._faultReportingInterval);\n\n log.info(`Fault reporting agent started (interval: ${this._faultReportingSeconds}s)`);\n this._faultReportingInterval = setInterval(() => {\n this.emit(E.SUBMIT_FAULTS_REQUEST);\n }, this._faultReportingSeconds * 1000);\n }\n\n /** Internal: (re)create the collection setInterval timer */\n _setCollectionTimer(seconds) {\n if (this.collectionInterval) clearInterval(this.collectionInterval);\n this._currentCollectInterval = seconds;\n log.info(`Collection interval: ${seconds}s`);\n this.collectionInterval = setInterval(() => {\n log.debug('Running scheduled collection cycle...');\n this.collect().catch(error => {\n log.error('Collection error:', error);\n this.emit(E.COLLECTION_ERROR, error);\n });\n }, seconds * 1000);\n }\n\n /**\n * Request layout change (called by XMR or schedule)\n * Pure orchestration - emits events for platform to handle\n */\n async requestLayoutChange(layoutId) {\n log.info(`Layout change requested: ${layoutId}`);\n\n // Clear current layout tracking so it will switch\n this.currentLayoutId = null;\n\n this.emit('layout-change-requested', layoutId);\n }\n\n /**\n * Mark layout as ready and current\n * Called by platform after it successfully renders the layout\n */\n /**\n * Clear the preparing-layout guard.\n * Called by platform layer when preparation is cancelled or skipped.\n */\n clearPreparingLayout() {\n this._preparingLayoutId = null;\n }\n\n setCurrentLayout(layoutId) {\n this.currentLayoutId = layoutId;\n this._preparingLayoutId = null;\n this._lastLayoutChangeTime = new Date().toISOString();\n this._statusCode = 1; // Running\n this.pendingLayouts.delete(layoutId);\n // Layout proved playable — clear media status (no longer missing)\n this._layoutMediaStatus.delete(`${layoutId}.xlf`);\n this.emit('layout-current', layoutId);\n // Force timeline recalc on layout change (fingerprint reset)\n this._lastTimelineFingerprint = null;\n this.logUpcomingTimeline();\n }\n\n /**\n * Mark layout as pending (waiting for media)\n * Called by platform when layout needs media downloads\n */\n setPendingLayout(layoutId, requiredMediaIds) {\n this.pendingLayouts.set(layoutId, requiredMediaIds);\n this.emit('layout-pending', layoutId, requiredMediaIds);\n }\n\n /**\n * Clear current layout (for replay)\n * Called by platform when layout ends\n */\n clearCurrentLayout() {\n this.currentLayoutId = null;\n this.emit('layout-cleared');\n }\n\n /**\n * Get the next layout from the pre-calculated schedule queue.\n * Pops the next entry, skipping blacklisted layouts.\n * Returns { layoutId, layoutFile } or null.\n */\n getNextLayout() {\n const entry = this.schedule.popNextFromQueue(\n this._layoutDurations,\n this._queueOptions\n );\n\n if (!entry) {\n // No queue entries — try default\n const defaultFile = this.schedule.schedule?.default;\n if (defaultFile) {\n const layoutId = parseLayoutFile(defaultFile);\n return { layoutId, layoutFile: defaultFile };\n }\n return null;\n }\n\n const layoutId = parseLayoutFile(entry.layoutId);\n\n if (this.isLayoutBlacklisted(layoutId)) {\n // Try next entries (up to queue length) to find a non-blacklisted one\n const { queue } = this.schedule.getScheduleQueue(\n this._layoutDurations,\n this._queueOptions\n );\n for (let i = 0; i < queue.length - 1; i++) {\n const next = this.schedule.popNextFromQueue(\n this._layoutDurations,\n this._queueOptions\n );\n if (next) {\n const nextId = parseLayoutFile(next.layoutId);\n if (!this.isLayoutBlacklisted(nextId)) {\n return { layoutId: nextId, layoutFile: next.layoutId };\n }\n }\n }\n // All blacklisted — return this one anyway to avoid blank screen\n log.warn('All queued layouts are blacklisted, using current entry as fallback');\n }\n\n return { layoutId, layoutFile: entry.layoutId };\n }\n\n /**\n * Peek at the next layout in the schedule queue without advancing.\n * Used by the preload system to know which layout to pre-build.\n * Returns { layoutId, layoutFile } or null if no next layout or same as current.\n */\n peekNextLayout() {\n const entry = this.schedule.peekNextInQueue(\n this._layoutDurations,\n this._queueOptions\n );\n\n if (!entry) return null;\n\n const layoutId = parseLayoutFile(entry.layoutId);\n\n // Don't preload if it's the same as current\n if (layoutId === this.currentLayoutId) {\n // Try the one after that\n const after = this.schedule.peekAfterNext(\n this._layoutDurations,\n this._queueOptions\n );\n if (!after) return null;\n const afterId = parseLayoutFile(after.layoutId);\n if (afterId === this.currentLayoutId || this.isLayoutBlacklisted(afterId)) return null;\n return { layoutId: afterId, layoutFile: after.layoutId };\n }\n\n if (this.isLayoutBlacklisted(layoutId)) return null;\n\n return { layoutId, layoutFile: entry.layoutId };\n }\n\n /**\n * Advance to the next layout in the pre-calculated schedule queue.\n * Called by platform layer when a layout finishes (layoutEnd event).\n * Pops the next entry from the queue and emits layout-prepare-request.\n */\n advanceToNextLayout() {\n // Don't cycle if we're in a layout override (XMR changeLayout/overlayLayout)\n if (this._layoutOverride) {\n log.info('Layout override active, not advancing schedule');\n return;\n }\n\n const next = this.getNextLayout();\n\n // ── Never-stop guarantee ────────────────────────────────────────\n if (!next) {\n if (this.currentLayoutId) {\n log.info(`No layouts in queue, replaying ${this.currentLayoutId} to avoid blank screen`);\n const replayId = this.currentLayoutId;\n this.currentLayoutId = null;\n this._preparingLayoutId = replayId;\n this.emit(E.LAYOUT_PREPARE_REQUEST, replayId);\n } else {\n log.info('No layouts scheduled during advance');\n this.emit(E.NO_LAYOUTS_SCHEDULED);\n }\n return;\n }\n\n const { layoutId, layoutFile } = next;\n const dur = this._layoutDurations.get(layoutFile) || '?';\n\n // Debug: log incoming layout vs timeline overlay top entries\n if (this._lastTimeline && this._lastTimeline.length > 0) {\n const top2 = this._lastTimeline.slice(0, 2).map(e => {\n const t = e.startTime.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });\n return `${e.layoutFile}(${e.duration}s@${t})`;\n });\n log.debug(`[Timeline] Layout transition: entering ${layoutFile} (${dur}s), overlay top: [${top2.join(', ')}]`);\n\n // Warn if the entering layout doesn't match the first timeline entry\n if (this._lastTimeline[0].layoutFile !== layoutFile) {\n log.warn(`[Timeline] Mismatch: entering ${layoutFile} but overlay expects ${this._lastTimeline[0].layoutFile}`);\n }\n } else {\n log.debug(`[Timeline] Layout transition: entering ${layoutFile} (${dur}s), no timeline data`);\n }\n\n // Multi-display sync: if this is a sync event and we have a SyncManager,\n // delegate layout transitions to the sync protocol\n if (this.syncManager && this.schedule.isSyncEvent(layoutFile)) {\n if (this.isSyncLead()) {\n log.info(`[Sync] Lead requesting coordinated layout change: ${layoutId}`);\n // Lead must render the layout itself (not just coordinate followers).\n // Emit layout-prepare-request so the renderer builds it, while\n // requestLayoutChange coordinates the show timing with followers.\n this._preparingLayoutId = layoutId;\n this.emit(E.LAYOUT_PREPARE_REQUEST, layoutId);\n this.syncManager.requestLayoutChange(layoutId).catch(err => {\n log.error('[Sync] Layout change failed:', err);\n });\n return;\n } else if (this.syncManager.transport?.connected) {\n log.info(`[Sync] Follower waiting for lead signal (not advancing independently)`);\n return;\n } else {\n log.warn(`[Sync] Follower: lead unreachable, advancing independently`);\n }\n }\n\n if (layoutId === this.currentLayoutId) {\n log.info(`Next layout ${layoutId} is same as current, triggering replay`);\n this.currentLayoutId = null;\n }\n\n const { queue } = this.schedule.getScheduleQueue(\n this._layoutDurations,\n this._queueOptions\n );\n const pos = this.schedule.getQueuePosition();\n log.info(`Advancing to layout ${layoutId} (queue pos ${pos}/${queue.length})`);\n\n // Set _preparingLayoutId BEFORE emitting to prevent collect() cycles\n // from seeing both currentLayoutId=null and _preparingLayoutId=null\n // and popping another layout from the queue (double-pop race).\n this._preparingLayoutId = layoutId;\n this.emit(E.LAYOUT_PREPARE_REQUEST, layoutId);\n }\n\n /**\n * Go back to the previous layout in the schedule queue (wraps around).\n * Called by platform layer in response to manual navigation (keyboard/remote).\n * Skips sync-manager logic — manual navigation is local only.\n */\n advanceToPreviousLayout() {\n if (this._layoutOverride) {\n log.info('Layout override active, not going back');\n return;\n }\n\n const { queue } = this.schedule.getScheduleQueue(\n this._layoutDurations,\n this._queueOptions\n );\n if (queue.length <= 1) {\n log.info('Single or empty queue, nothing to go back to');\n return;\n }\n\n // Go back 2 positions (current was already popped, so -2 from current pos)\n const entry = this.schedule.rewindQueue(2, this._layoutDurations, this._queueOptions);\n if (!entry) return;\n\n const layoutId = parseLayoutFile(entry.layoutId);\n\n if (layoutId === this.currentLayoutId) {\n log.info('Previous layout is same as current, nothing to go back to');\n return;\n }\n\n log.info(`Going back to layout ${layoutId}`);\n this.emit(E.LAYOUT_PREPARE_REQUEST, layoutId);\n }\n\n /**\n * Notify that a file is ready (called by platform for both layout and media files)\n * Checks if any pending layouts can now be rendered\n */\n notifyMediaReady(fileId, fileType = 'media') {\n log.debug(`File ${fileId} ready (${fileType})`);\n\n // Check if any pending layouts are now complete\n for (const [layoutId, requiredFiles] of this.pendingLayouts.entries()) {\n // Check if this file is needed by this layout\n // For layout files: match layout ID with file ID (layout 78 needs layout/78)\n // For media files: check if fileId is in requiredFiles array\n const isLayoutFile = fileType === 'layout' && layoutId === parseInt(fileId);\n const isRequiredMedia = fileType === 'media' && requiredFiles.includes(fileId);\n\n if (isLayoutFile || isRequiredMedia) {\n log.debug(`${fileType} ${fileId} was needed by pending layout ${layoutId}, checking if ready...`);\n this.emit(E.CHECK_PENDING_LAYOUT, layoutId, requiredFiles);\n }\n }\n }\n\n /**\n * Notify layout status to CMS\n */\n async notifyLayoutStatus(layoutId) {\n try {\n const status = {\n currentLayoutId: layoutId,\n deviceName: this.config?.displayName || '',\n displayName: this.config?.displayName || '',\n lastCommandSuccess: this._lastCommandSuccess ?? true,\n code: this._statusCode,\n lastLayoutChangeTime: this._lastLayoutChangeTime || new Date().toISOString(),\n };\n\n // Add geo-location if available\n if (this.config?.latitude) status.latitude = this.config.latitude;\n if (this.config?.longitude) status.longitude = this.config.longitude;\n\n // Report LAN IP so CMS can tell sync followers where the lead is\n if (this._lanIpAddress) status.lanIpAddress = this._lanIpAddress;\n\n await this.xmds.notifyStatus(status);\n this.emit('status-notified', layoutId);\n } catch (error) {\n log.warn('Failed to notify status:', error);\n this.emit('status-notify-failed', layoutId, error);\n }\n }\n\n /**\n * Report geo location (called by XMR when CMS pushes coordinates)\n * Updates schedule location for geo-fencing and triggers schedule re-evaluation.\n * @param {Object} data - { latitude, longitude }\n */\n reportGeoLocation(data) {\n const lat = parseFloat(data?.latitude);\n const lng = parseFloat(data?.longitude);\n\n if (isNaN(lat) || isNaN(lng)) {\n log.warn('reportGeoLocation: invalid coordinates', data);\n return;\n }\n\n log.info(`Geo location from CMS: ${lat.toFixed(4)}, ${lng.toFixed(4)}`);\n\n if (this.schedule?.setLocation) {\n this.schedule.setLocation(lat, lng);\n }\n\n this.emit('location-updated', { latitude: lat, longitude: lng, source: 'cms' });\n this.checkSchedule();\n }\n\n /**\n * Request geo location using a fallback chain:\n * 1. Browser Geolocation API (GPS / OS-level)\n * 2. Google Geolocation API (if GOOGLE_GEO_API_KEY is configured)\n * 3. IP-based geolocation (free, no key required)\n * @returns {Promise<{latitude: number, longitude: number}|null>}\n */\n async requestGeoLocation() {\n // Return cached location if still fresh (re-resolve every 30 minutes)\n const GEO_CACHE_MS = 30 * 60 * 1000;\n if (this._geoCache && (Date.now() - this._geoCache.ts) < GEO_CACHE_MS) {\n return this._geoCache.location;\n }\n\n // Try browser geolocation (works with GPS or Google API key baked into Chromium).\n // Skip if it already failed — Electron without a Google API key will never succeed.\n if (!this._browserGeoFailed) {\n const browser = await this._tryBrowserGeolocation();\n if (browser) {\n return this._cacheGeo(this._applyLocation(browser.latitude, browser.longitude, 'browser'));\n }\n this._browserGeoFailed = true;\n }\n\n // Try Google Geolocation API if key is configured\n const apiKey = this.config?.googleGeoApiKey;\n if (apiKey) {\n const google = await this._tryGoogleGeolocation(apiKey);\n if (google) {\n return this._cacheGeo(this._applyLocation(google.latitude, google.longitude, 'google-api'));\n }\n }\n\n // Fall back to IP-based geolocation (free, no key)\n const ip = await this._tryIpGeolocation();\n if (ip) {\n return this._cacheGeo(this._applyLocation(ip.latitude, ip.longitude, 'ip-geolocation'));\n }\n\n log.warn('All geolocation methods failed');\n return null;\n }\n\n /** Cache a resolved geolocation result. @private */\n _cacheGeo(location) {\n this._geoCache = { location, ts: Date.now() };\n return location;\n }\n\n /**\n * Extract config values from CMS display tags using key|value convention.\n * Tags like \"geoApiKey|AIzaSy...\" are parsed and applied to player config.\n * @param {string[]} tags - Array of tag strings from RegisterDisplay\n * @private\n */\n _applyTagConfig(tags) {\n if (!Array.isArray(tags) || tags.length === 0) return;\n\n const TAG_CONFIG_MAP = {\n 'geoApiKey': 'googleGeoApiKey',\n };\n\n for (const tag of tags) {\n const pipeIdx = tag.indexOf('|');\n if (pipeIdx === -1) continue;\n\n const key = tag.substring(0, pipeIdx);\n const value = tag.substring(pipeIdx + 1);\n const configKey = TAG_CONFIG_MAP[key];\n\n if (configKey && value && this.config) {\n log.info(`Config from CMS tag: ${key} → ${configKey}`);\n this.config[configKey] = value;\n }\n }\n }\n\n _applyLocation(lat, lng, source) {\n log.info(`Geolocation (${source}): ${lat.toFixed(4)}, ${lng.toFixed(4)}`);\n\n if (this.schedule?.setLocation) {\n this.schedule.setLocation(lat, lng);\n }\n\n this.emit('location-updated', { latitude: lat, longitude: lng, source });\n this.checkSchedule();\n\n return { latitude: lat, longitude: lng };\n }\n\n /**\n * Try the browser Geolocation API (navigator.geolocation).\n * @returns {Promise<{latitude: number, longitude: number}|null>}\n * @private\n */\n async _tryBrowserGeolocation() {\n if (typeof navigator === 'undefined' || !navigator.geolocation) return null;\n\n try {\n const position = await new Promise((resolve, reject) => {\n navigator.geolocation.getCurrentPosition(resolve, reject, {\n timeout: 10000,\n maximumAge: 300000, // 5 minutes\n enableHighAccuracy: false\n });\n });\n return { latitude: position.coords.latitude, longitude: position.coords.longitude };\n } catch (error) {\n log.warn('Browser geolocation failed:', error?.message || error);\n return null;\n }\n }\n\n /**\n * Try Google Geolocation API (direct HTTPS POST, bypasses Chromium's built-in service).\n * @param {string} apiKey - Google API key\n * @returns {Promise<{latitude: number, longitude: number}|null>}\n * @private\n */\n async _tryGoogleGeolocation(apiKey) {\n try {\n const res = await fetch(\n `https://www.googleapis.com/geolocation/v1/geolocate?key=${apiKey}`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ considerIp: true }),\n signal: AbortSignal.timeout(5000)\n }\n );\n if (!res.ok) {\n log.warn(`Google Geolocation API returned ${res.status}`);\n return null;\n }\n const data = await res.json();\n if (data.location?.lat != null && data.location?.lng != null) {\n return { latitude: data.location.lat, longitude: data.location.lng };\n }\n return null;\n } catch (error) {\n log.warn('Google Geolocation API failed:', error?.message || error);\n return null;\n }\n }\n\n /**\n * Try IP-based geolocation using free HTTPS providers (no API key needed).\n * Tries ipapi.co first, then freeipapi.com as fallback.\n * @returns {Promise<{latitude: number, longitude: number}|null>}\n * @private\n */\n async _tryIpGeolocation() {\n const providers = [\n {\n url: 'https://ipapi.co/json/',\n parse: (data) => data.latitude != null && data.longitude != null\n ? { latitude: data.latitude, longitude: data.longitude }\n : null\n },\n {\n url: 'https://freeipapi.com/api/json',\n parse: (data) => data.latitude != null && data.longitude != null\n ? { latitude: data.latitude, longitude: data.longitude }\n : null\n }\n ];\n\n for (const provider of providers) {\n try {\n const res = await fetch(provider.url, { signal: AbortSignal.timeout(5000) });\n if (!res.ok) continue;\n const data = await res.json();\n const location = provider.parse(data);\n if (location) return location;\n } catch (error) {\n log.warn(`IP geolocation (${provider.url}) failed:`, error?.message || error);\n }\n }\n return null;\n }\n\n /**\n * Re-evaluate current schedule and switch layouts if needed.\n * Called after location updates or other schedule-affecting changes.\n */\n checkSchedule() {\n const layoutFiles = this.schedule.getCurrentLayouts();\n this.emit(E.LAYOUTS_SCHEDULED, layoutFiles);\n this._evaluateAndSwitchLayout(layoutFiles, '');\n }\n\n /**\n * Capture screenshot (called by XMR wrapper)\n * Emits event for platform layer to handle\n */\n async captureScreenshot() {\n log.info('Screenshot requested');\n this.emit(E.SCREENSHOT_REQUEST);\n }\n\n /**\n * Change to a specific layout (called by XMR wrapper)\n * Tracks override state so revertToSchedule() can undo it.\n */\n async changeLayout(layoutId, options) {\n log.info('Layout change requested via XMR:', layoutId);\n const id = parseInt(layoutId, 10);\n const duration = options?.duration || 0;\n const changeMode = options?.changeMode || 'replace';\n this._layoutOverride = { layoutId: id, type: 'change', duration, changeMode };\n this.currentLayoutId = null; // Force re-render\n this.emit(E.LAYOUT_PREPARE_REQUEST, id);\n this._scheduleAutoRevert(id, duration, 'Layout override');\n }\n\n /**\n * Push an overlay layout on top of current content (called by XMR wrapper)\n * @param {number|string} layoutId - Layout to overlay\n */\n async overlayLayout(layoutId, options) {\n log.info('Overlay layout requested via XMR:', layoutId);\n const id = parseInt(layoutId, 10);\n const duration = options?.duration || 0;\n this._layoutOverride = { layoutId: id, type: 'overlay', duration };\n this.emit(E.OVERLAY_LAYOUT_REQUEST, id);\n this._scheduleAutoRevert(id, duration, 'Overlay');\n }\n\n /**\n * Revert to scheduled content after changeLayout/overlayLayout override\n */\n async revertToSchedule() {\n log.info('Reverting to scheduled content');\n this._layoutOverride = null;\n this.currentLayoutId = null;\n this.emit(E.REVERT_TO_SCHEDULE);\n\n // Re-evaluate schedule to get the right layout\n const layoutFiles = this.schedule.getCurrentLayouts();\n if (layoutFiles.length > 0) {\n const layoutFile = layoutFiles[0];\n const layoutId = parseLayoutFile(layoutFile);\n this.emit(E.LAYOUT_PREPARE_REQUEST, layoutId);\n } else {\n this.emit(E.NO_LAYOUTS_SCHEDULED);\n }\n }\n\n /**\n * Purge all cached content and re-download (called by XMR wrapper)\n */\n async purgeAll() {\n log.info('Purge all cache requested via XMR');\n this._lastCheckRf = null;\n this._lastCheckSchedule = null;\n this.emit(E.PURGE_ALL_REQUEST);\n // Trigger immediate re-collection after purge\n return this.collectNow();\n }\n\n /**\n * Execute a command (HTTP only in browser context)\n * @param {string} commandCode - The command code from CMS\n * @param {Object} commands - Commands map from display settings\n */\n async executeCommand(commandCode, commands) {\n log.info('Execute command requested:', commandCode);\n\n if (!commands || !commands[commandCode]) {\n log.warn('Unknown command code:', commandCode);\n this._lastCommandSuccess = false;\n this.emit(E.COMMAND_RESULT, { code: commandCode, success: false, reason: 'Unknown command' });\n return;\n }\n\n const command = commands[commandCode];\n const commandString = command.commandString || command.value || '';\n\n // Only HTTP commands are possible in a browser\n if (commandString.startsWith('http|')) {\n const parts = commandString.split('|');\n const url = parts[1];\n const contentType = parts[2] || 'application/json';\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': contentType },\n signal: AbortSignal.timeout(10000),\n });\n const success = response.ok;\n this._lastCommandSuccess = success;\n log.info(`HTTP command ${commandCode} result: ${response.status}`);\n this.emit(E.COMMAND_RESULT, { code: commandCode, success, status: response.status });\n } catch (error) {\n this._lastCommandSuccess = false;\n log.error(`HTTP command ${commandCode} failed:`, error);\n this.emit(E.COMMAND_RESULT, { code: commandCode, success: false, reason: error.message });\n }\n } else {\n // Emit event for platform layer (Electron/Chromium) to handle native commands\n // (shell, RS232, Android intent, etc.)\n log.info('Delegating non-HTTP command to platform layer:', commandCode);\n this.emit(E.EXECUTE_NATIVE_COMMAND, { code: commandCode, commandString });\n }\n }\n\n /**\n * Trigger a webhook action (called by XMR wrapper)\n * @param {string} triggerCode - The trigger code to fire\n */\n triggerWebhook(triggerCode) {\n log.info('Webhook trigger from XMR:', triggerCode);\n this.handleTrigger(triggerCode);\n }\n\n /**\n * Force refresh of data connectors (called by XMR wrapper)\n */\n refreshDataConnectors() {\n log.info('Data connector refresh requested via XMR');\n this.dataConnectorManager.refreshAll();\n this.emit('data-connectors-refreshed');\n }\n\n /**\n * Submit media inventory to CMS\n * Reports which files are cached and complete.\n * @param {Array} files - List of files from RequiredFiles\n */\n async submitMediaInventory(files) {\n if (!files || files.length === 0) return;\n\n try {\n // Build inventory XML: <files><file type=\"media\" id=\"1\" complete=\"1\" md5=\"abc\" lastChecked=\"123\"/></files>\n // complete: use file.complete if set by caller (cache layer), default to \"1\"\n const now = Math.floor(Date.now() / 1000);\n const fileEntries = files\n .filter(f => ['media', 'layout', 'resource', 'dependency', 'widget'].includes(f.type))\n .map(f => {\n const complete = f.complete !== undefined ? (f.complete ? '1' : '0') : '1';\n const fileType = f.fileType ? ` fileType=\"${f.fileType}\"` : '';\n return `<file type=\"${f.type}\" id=\"${f.id}\" complete=\"${complete}\" md5=\"${f.md5 || ''}\" lastChecked=\"${now}\"${fileType}/>`;\n })\n .join('');\n const inventoryXml = `<files>${fileEntries}</files>`;\n\n await this.xmds.mediaInventory(inventoryXml);\n log.info(`Media inventory submitted: ${files.length} files`);\n this.emit('media-inventory-submitted', files.length);\n } catch (error) {\n log.warn('MediaInventory submission failed:', error);\n }\n }\n\n /**\n * BlackList a media file (report broken media to CMS)\n * @param {string|number} mediaId - The media ID\n * @param {string} type - File type ('media' or 'layout')\n * @param {string} reason - Reason for blacklisting\n */\n async blackList(mediaId, type, reason) {\n try {\n await this.xmds.blackList(mediaId, type, reason);\n this.emit('media-blacklisted', { mediaId, type, reason });\n } catch (error) {\n log.warn('BlackList failed:', error);\n }\n }\n\n /**\n * Report a layout render failure. After N consecutive failures\n * (default 3), the layout is blacklisted and skipped in schedule\n * evaluation. Blacklisted layouts are reported to CMS via the\n * BlackList XMDS method.\n *\n * @param {number} layoutId - The layout that failed\n * @param {string} reason - Human-readable failure description\n */\n reportLayoutFailure(layoutId, reason) {\n const id = Number(layoutId);\n this._statusCode = 3; // Error — layout failed to render\n\n const { blacklisted, failures } = this._layoutBlacklist.recordFailure(id, reason);\n if (blacklisted && failures === 3) {\n // Newly blacklisted (threshold just reached)\n this.emit('layout-blacklisted', { layoutId: id, reason, failures });\n this.blackList(id, 'layout', reason);\n }\n }\n\n reportLayoutSuccess(layoutId) {\n const wasBlacklisted = this._layoutBlacklist.recordSuccess(Number(layoutId));\n if (wasBlacklisted) {\n this.emit('layout-unblacklisted', { layoutId: Number(layoutId) });\n }\n }\n\n isLayoutBlacklisted(layoutId) {\n return this._layoutBlacklist.isBlacklisted(layoutId);\n }\n\n getBlacklistedLayouts() {\n return this._layoutBlacklist.getBlacklistedIds();\n }\n\n resetBlacklist() {\n if (this._layoutBlacklist.reset() > 0) {\n this.emit('blacklist-reset');\n }\n }\n\n /**\n * Check if currently in a layout override (from XMR changeLayout/overlayLayout)\n */\n isLayoutOverridden() {\n return this._layoutOverride !== null;\n }\n\n /**\n * Handle interactive trigger (from IC or touch events)\n * Looks up matching action in schedule and executes it\n * @param {string} triggerCode - The trigger code from the IC request\n */\n handleTrigger(triggerCode) {\n const action = this.schedule.findActionByTrigger(triggerCode);\n if (!action) {\n log.debug('No scheduled action matches trigger:', triggerCode);\n return;\n }\n\n log.info(`Action triggered: ${action.actionType} (trigger: ${triggerCode})`);\n\n switch (action.actionType) {\n case 'navLayout':\n case 'navigateToLayout':\n if (action.layoutCode) {\n this.changeLayout(action.layoutCode);\n }\n break;\n case 'navWidget':\n case 'navigateToWidget':\n this.emit(E.NAVIGATE_TO_WIDGET, action);\n break;\n case 'command':\n this.emit('execute-command', action.commandCode);\n break;\n default:\n log.warn('Unknown action type:', action.actionType);\n }\n }\n\n /**\n * Update data connectors from current schedule\n * Reconfigures and restarts polling when schedule changes.\n */\n updateDataConnectors() {\n const connectors = this.schedule.getDataConnectors();\n\n if (connectors.length > 0) {\n log.info(`Configuring ${connectors.length} data connector(s)`);\n }\n\n this.dataConnectorManager.setConnectors(connectors);\n\n if (connectors.length > 0) {\n this.dataConnectorManager.startPolling();\n this.emit('data-connectors-started', connectors.length);\n }\n }\n\n /**\n * Process scheduled commands from the CMS schedule.\n * Checks for command events whose scheduled date has arrived and executes them.\n * Each command is only executed once (tracked by code+date key in _executedCommands).\n */\n _processScheduledCommands() {\n if (!this.schedule?.getCommands) return;\n\n const commands = this.schedule.getCommands();\n if (commands.length === 0) return;\n\n const now = new Date();\n\n for (const command of commands) {\n if (!command.code || !command.date) continue;\n\n // Unique key to track execution (same command can be scheduled multiple times)\n const commandKey = `${command.code}|${command.date}`;\n\n // Skip already executed commands\n if (this._executedCommands.has(commandKey)) continue;\n\n // Check if the command's scheduled time has arrived\n const commandDate = new Date(command.date);\n if (isNaN(commandDate.getTime())) {\n log.warn('Scheduled command has invalid date:', command.date);\n continue;\n }\n\n if (now >= commandDate) {\n log.info(`Executing scheduled command: ${command.code} (scheduled: ${command.date})`);\n this._executedCommands.add(commandKey);\n\n // Handle built-in commands directly\n if (command.code === 'collectNow') {\n // Trigger immediate collection on next tick (avoid re-entrance)\n setTimeout(() => this.collectNow().catch(e => log.error('collectNow command failed:', e)), 0);\n } else {\n // Emit event for platform layer to handle (reboot, restart, etc.)\n this.emit(E.SCHEDULED_COMMAND, command);\n }\n }\n }\n }\n\n /**\n * Fetch weather data from CMS and pass to schedule for criteria evaluation.\n * Non-blocking: weather fetch failure doesn't prevent schedule evaluation.\n */\n async _fetchWeatherData() {\n if (!this.xmds?.getWeather || !this.schedule?.setWeatherData) return;\n\n try {\n const weatherJson = await this.xmds.getWeather();\n const weatherData = typeof weatherJson === 'string' ? JSON.parse(weatherJson) : weatherJson;\n this.schedule.setWeatherData(weatherData);\n log.info('Weather data updated:', Object.keys(weatherData).join(', '));\n } catch (e) {\n log.warn('GetWeather failed (non-critical):', e?.message || e);\n }\n }\n\n /**\n * Get the DataConnectorManager instance\n * Used by platform layer to serve data to widgets via IC /realtime\n * @returns {DataConnectorManager}\n */\n getDataConnectorManager() {\n return this.dataConnectorManager;\n }\n\n /**\n * Set the SyncManager instance for multi-display coordination.\n * Called by platform layer after RegisterDisplay returns syncConfig.\n *\n * @param {SyncManager} syncManager - SyncManager instance\n */\n setSyncManager(syncManager) {\n this.syncManager = syncManager;\n log.info('SyncManager attached:', syncManager.isLead ? 'LEAD' : 'FOLLOWER');\n }\n\n /**\n * Check if this display is part of a sync group\n * @returns {boolean}\n */\n isInSyncGroup() {\n return this.syncConfig !== null;\n }\n\n /**\n * Check if this display is the sync group leader\n * @returns {boolean}\n */\n isSyncLead() {\n return this.syncConfig?.isLead === true;\n }\n\n /**\n * Get sync configuration\n * @returns {Object|null} { syncGroup, syncPublisherPort, syncSwitchDelay, syncVideoPauseDelay, isLead }\n */\n getSyncConfig() {\n return this.syncConfig;\n }\n\n // ── Timeline (offline schedule prediction) ─────────────────────────\n\n // Duration flow: renderer is the single source of truth.\n // 1. Renderer calculates duration from widgets → emits layoutDurationUpdated\n // 2. recordLayoutDuration stores it (with final flag) → persisted to IDB\n // 3. On restart, IDB restores correct durations → queue uses them immediately\n // No XLF parsing needed in core — the renderer already does this.\n\n /**\n * Calculate and log the upcoming playback timeline (next 2 hours).\n * Emits 'timeline-updated' with the full timeline array.\n */\n logUpcomingTimeline() {\n if (!this.schedule.getLayoutsAtTime) return; // Schedule doesn't support time queries\n\n // Fingerprint inputs: schedule CRC + sorted durations + current layout + media status.\n // When unchanged, re-emit the cached timeline — avoids time drift from\n // re-simulating with a new Date.now() anchor on every collection cycle.\n const durationEntries = [...this._layoutDurations.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([k, v]) => `${k}:${v}`)\n .join('|');\n const mediaStatusEntries = [...this._layoutMediaStatus.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([k, v]) => `${k}:${v.ready}:${v.missingKey}`)\n .join('|');\n const pendingEntries = [...this.pendingLayouts.keys()].sort().join(',');\n const queuePos = this.schedule.getQueuePosition() || 0;\n const fingerprint = `${this._lastCheckSchedule}|${durationEntries}|${this.currentLayoutId}|${queuePos}|${mediaStatusEntries}|${pendingEntries}`;\n\n if (fingerprint === this._lastTimelineFingerprint && this._lastTimeline) {\n this.emit(E.TIMELINE_UPDATED, this._lastTimeline);\n return;\n }\n\n const { queue } = this.schedule.getScheduleQueue(this._layoutDurations, this._queueOptions);\n const timeline = calculateTimeline(queue, this.schedule.getQueuePosition(), {\n currentLayoutStartedAt: this._lastLayoutChangeTime ? new Date(this._lastLayoutChangeTime) : null,\n defaultLayout: this.schedule.schedule?.default || null,\n durations: this._layoutDurations,\n });\n if (timeline.length === 0) return;\n\n // Annotate entries with missingMedia from pendingLayouts (high authority)\n // and _layoutMediaStatus (proactive check, lower authority)\n for (const entry of timeline) {\n const layoutId = parseInt(entry.layoutFile.replace('.xlf', ''), 10);\n const pendingMedia = this.pendingLayouts.get(layoutId);\n if (pendingMedia && pendingMedia.length > 0) {\n // pendingLayouts takes priority — definitively missing\n entry.missingMedia = pendingMedia.map(String);\n } else {\n const status = this._layoutMediaStatus.get(entry.layoutFile);\n if (status && !status.ready && status.missing.length > 0) {\n entry.missingMedia = status.missing.map(String);\n }\n }\n }\n\n this._lastTimelineFingerprint = fingerprint;\n this._lastTimeline = timeline;\n\n const lines = timeline.slice(0, 20).map(e => {\n const s = e.startTime.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });\n const end = e.endTime.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });\n const missingTag = e.missingMedia ? ` [MISSING: ${e.missingMedia.length} files]` : '';\n return ` ${s}-${end} Layout ${e.layoutFile} (${e.duration}s)${e.isDefault ? ' [default]' : ''}${missingTag}`;\n });\n\n // Log warnings for layouts with missing media\n for (const entry of timeline) {\n if (entry.missingMedia) {\n log.warn(`[Timeline] Layout ${entry.layoutFile}: ${entry.missingMedia.length} files missing`);\n }\n }\n\n log.info(`[Timeline] Next ${timeline.length} plays:\\n${lines.join('\\n')}`);\n this.emit(E.TIMELINE_UPDATED, timeline);\n }\n\n /**\n * Set media readiness status for a layout (proactive async check from platform layer).\n * No-ops if value is unchanged to avoid fingerprint churn.\n * @param {string} layoutFile - Layout file (e.g. '100.xlf')\n * @param {boolean} ready - Whether all media is cached\n * @param {string[]} [missing] - Array of missing media IDs/filenames\n */\n setLayoutMediaStatus(layoutFile, ready, missing = []) {\n const existing = this._layoutMediaStatus.get(layoutFile);\n const missingKey = missing.slice().sort().join(',');\n if (existing && existing.ready === ready && existing.missingKey === missingKey) return;\n\n this._layoutMediaStatus.set(layoutFile, { ready, missing, missingKey });\n // Invalidate fingerprint to force timeline recalculation\n this._lastTimelineFingerprint = null;\n }\n\n /**\n * Record/correct a layout's actual duration (e.g., from video loadedmetadata).\n * Updates the durations map and re-logs the timeline if it changed.\n * @param {string} file - Layout file or layout ID string\n * @param {number} duration - Actual duration in seconds\n * @param {boolean} [final=false] - True when all videos in the layout have been probed\n */\n recordLayoutDuration(file, duration, final = false) {\n // Normalize: store under both \"492\" and \"492.xlf\" forms so that\n // calculateTimeline (which looks up \"492.xlf\") and other callers\n // (which use \"492\") always find the corrected value.\n const id = String(file).replace('.xlf', '');\n const xlfKey = id + '.xlf';\n\n // Definitive duration — never overwrite once set\n if (this._finalDurations.has(id)) return;\n\n const prev = this._layoutDurations.get(file);\n if (prev === duration && !final) return; // No change\n\n this._layoutDurations.set(id, duration);\n this._layoutDurations.set(xlfKey, duration);\n\n if (final) {\n this._finalDurations.add(id);\n this._finalDurations.add(xlfKey);\n }\n\n log.debug(`[Timeline] Duration corrected: layout ${file} ${prev || '?'}s → ${duration}s${final ? ' (final)' : ''}`);\n\n // Invalidate the cached schedule queue so the next getScheduleQueue() call\n // rebuilds with corrected durations (affects queue log and period calculation).\n this.schedule.invalidateQueue();\n\n // Debounce timeline recalculation — multiple video loadedmetadata events\n // can fire within milliseconds; collapse them into one recalculation.\n if (this._timelineRecalcTimer) clearTimeout(this._timelineRecalcTimer);\n this._timelineRecalcTimer = setTimeout(() => {\n this._timelineRecalcTimer = null;\n this.logUpcomingTimeline();\n this._offlineSave('durations', [...this._layoutDurations.entries()]);\n this._offlineSave('finalDurations', [...this._finalDurations]);\n this._offlineSave('durationsVersion', 2);\n }, 500);\n }\n\n /**\n * Cleanup\n */\n cleanup() {\n if (this.collectionInterval) {\n clearInterval(this.collectionInterval);\n this.collectionInterval = null;\n }\n\n if (this._faultReportingInterval) {\n clearInterval(this._faultReportingInterval);\n this._faultReportingInterval = null;\n }\n\n if (this._timelineRecalcTimer) {\n clearTimeout(this._timelineRecalcTimer);\n this._timelineRecalcTimer = null;\n }\n\n if (this.xmr) {\n this.xmr.stop();\n this.xmr = null;\n }\n\n // Stop multi-display sync\n if (this.syncManager) {\n this.syncManager.stop();\n this.syncManager = null;\n }\n\n // Stop data connector polling\n this.dataConnectorManager.cleanup();\n\n // Emit cleanup-complete before removing listeners\n this.emit('cleanup-complete');\n this.removeAllListeners();\n }\n\n /**\n * Get current layout ID\n */\n getCurrentLayoutId() {\n return this.currentLayoutId;\n }\n\n /**\n * Get known duration for a layout (from video metadata or XLF parse).\n * @param {number|string} layoutId\n * @returns {number|undefined}\n */\n getLayoutDuration(layoutId) {\n const id = String(layoutId);\n return this._layoutDurations.get(`${id}.xlf`) || this._layoutDurations.get(id);\n }\n\n /**\n * Check if collecting\n */\n isCollecting() {\n return this.collecting;\n }\n\n /**\n * Get pending layouts\n */\n getPendingLayouts() {\n return Array.from(this.pendingLayouts.keys());\n }\n\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n// @xiboplayer/core - Player core orchestration\nimport pkg from '../package.json' with { type: 'json' };\nexport const VERSION = pkg.version;\nexport { PlayerCore } from './player-core.js';\nexport { DataConnectorManager } from './data-connectors.js';\nexport { CORE_EVENTS } from './events.js';\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Download Progress Overlay\n *\n * Shows download status on hover (configurable, debug feature)\n * Displays: active downloads, progress, chunk status, queue info\n */\n\nexport interface DownloadOverlayConfig {\n enabled: boolean;\n updateInterval?: number; // ms between updates\n autoHide?: boolean; // Hide when no downloads\n}\n\nexport class DownloadOverlay {\n private overlay: HTMLElement | null = null;\n private config: DownloadOverlayConfig;\n private updateTimer: number | null = null;\n private _visible: boolean = false; // User-toggled visibility (D key)\n private _getProgress: (() => Record<string, any>) | null = null;\n\n constructor(config: DownloadOverlayConfig) {\n this.config = {\n updateInterval: 1000,\n autoHide: true,\n ...config\n };\n\n if (this.config.enabled) {\n this.createOverlay();\n // Start hidden — only shown when downloads are active or user presses D\n this.overlay!.style.display = 'none';\n }\n }\n\n private createOverlay() {\n this.overlay = document.createElement('div');\n this.overlay.id = 'download-overlay';\n // Style like top status messages - always visible, clean design\n this.overlay.style.cssText = `\n position: fixed;\n top: 1.5vh;\n left: 1.5vw;\n background: rgba(0, 0, 0, 0.88);\n color: #fff;\n font-family: system-ui, -apple-system, sans-serif;\n font-size: 1.4vw;\n padding: 1vh 1.2vw;\n border-radius: 0.4vw;\n border: 1px solid rgba(255, 255, 255, 0.25);\n z-index: 999999;\n max-width: 35vw;\n box-shadow: 0 0.3vh 1.2vw rgba(0, 0, 0, 0.5);\n `;\n\n document.body.appendChild(this.overlay);\n }\n\n /**\n * Set the progress callback. Called by PwaPlayer after DownloadManager is created.\n */\n public setProgressCallback(fn: () => Record<string, any>) {\n this._getProgress = fn;\n }\n\n private updateOverlay() {\n if (!this.overlay) return;\n\n const progress = this._getProgress ? this._getProgress() : {};\n const html = this.renderStatus(progress);\n const hasDownloads = !!html;\n\n if (hasDownloads) {\n // Active downloads — show overlay (auto or user-toggled)\n this.overlay.innerHTML = html;\n this.overlay.style.display = 'block';\n } else if (this._visible) {\n // User toggled on (D key) but no downloads — show idle status\n this.overlay.innerHTML = '<div style=\"color: #6c6; font-size: 1.4vw;\">✓ All downloads complete</div>';\n this.overlay.style.display = 'block';\n } else {\n // No downloads and not user-toggled — stop polling, hide\n this.stopUpdating();\n this.overlay.style.display = 'none';\n }\n }\n\n private renderStatus(progress: any): string {\n const downloads = progress || {};\n\n if (Object.keys(downloads).length === 0) {\n if (this.config.autoHide) {\n return ''; // Hide when no downloads\n }\n return `<div style=\"color: #6c6;\">✓ No downloads</div>`;\n }\n\n const numDownloads = Object.keys(downloads).length;\n let html = `<div style=\"font-weight: 600; margin-bottom: 0.8vh; font-size: 1.4vw;\">Downloads: ${numDownloads} active</div>`;\n\n for (const [url, progress] of Object.entries(downloads)) {\n const filename = this.extractFilename(url);\n const percent = Math.round((progress as any).percent || 0);\n const downloaded = this.formatBytes((progress as any).downloaded || 0);\n const total = this.formatBytes((progress as any).total || 0);\n\n html += `\n <div style=\"margin-bottom: 0.6vh; padding-bottom: 0.6vh; border-bottom: 1px solid rgba(255,255,255,0.1);\">\n <div style=\"font-size: 1.2vw; margin-bottom: 0.2vh;\">${filename}</div>\n <div style=\"background: rgba(255,255,255,0.1); height: 0.4vh; border-radius: 0.2vw; overflow: hidden;\">\n <div style=\"width: ${percent}%; height: 100%; background: #4a9eff; transition: width 0.3s;\"></div>\n </div>\n <div style=\"color: #999; font-size: 1.1vw; margin-top: 0.2vh;\">\n ${percent}% · ${downloaded} / ${total}\n </div>\n </div>\n `;\n }\n\n return html;\n }\n\n private extractFilename(key: string): string {\n // Key is now \"type/id\" (e.g. \"media/5\", \"layout/12\") — no URL parsing needed\n return key || 'unknown';\n }\n\n private formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n const kb = bytes / 1024;\n if (kb < 1024) return `${kb.toFixed(1)} KB`;\n const mb = kb / 1024;\n if (mb < 1024) return `${mb.toFixed(1)} MB`;\n return `${(mb / 1024).toFixed(1)} GB`;\n }\n\n /**\n * Toggle overlay visibility (D key).\n * When toggled on, starts polling. When toggled off, hides immediately.\n */\n public toggle() {\n if (!this.overlay) return;\n this._visible = !this._visible;\n if (this._visible) {\n this.overlay.style.display = 'block';\n this.updateOverlay(); // Immediate update\n this.startUpdating();\n } else {\n this.overlay.style.display = 'none';\n this.stopUpdating();\n }\n }\n\n /**\n * Start polling for download progress.\n * Safe to call multiple times — won't create duplicate timers.\n * Does NOT set _visible — the overlay auto-shows when downloads are active\n * and auto-hides when they finish. Use toggle() for user-controlled visibility.\n */\n public startUpdating() {\n if (this.updateTimer) return; // Already polling\n this.updateTimer = window.setInterval(() => {\n this.updateOverlay();\n }, this.config.updateInterval);\n this.updateOverlay(); // Immediate first update\n }\n\n /**\n * Stop polling. Called automatically when no downloads are active.\n */\n private stopUpdating() {\n if (this.updateTimer) {\n clearInterval(this.updateTimer);\n this.updateTimer = null;\n }\n }\n\n public destroy() {\n this.stopUpdating();\n if (this.overlay) {\n this.overlay.remove();\n this.overlay = null;\n }\n }\n\n public setEnabled(enabled: boolean) {\n this.config.enabled = enabled;\n\n if (enabled && !this.overlay) {\n this.createOverlay();\n // Polling starts on demand via startUpdating()\n } else if (!enabled && this.overlay) {\n this.destroy();\n }\n }\n}\n\n/**\n * Get default configuration based on environment\n */\nexport function getDefaultOverlayConfig(): DownloadOverlayConfig {\n // Check URL parameter override\n const urlParams = new URLSearchParams(window.location.search);\n const showDownloads = urlParams.get('showDownloads');\n\n if (showDownloads !== null) {\n return { enabled: showDownloads !== '0' && showDownloads !== 'false' };\n }\n\n // Check localStorage preference\n const savedPref = localStorage.getItem('xibo_show_download_overlay');\n if (savedPref !== null) {\n return { enabled: savedPref === 'true' };\n }\n\n // Default: disabled — toggle with D key or ?showDownloads=1\n return { enabled: false };\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Timeline Overlay\n *\n * Toggleable debug overlay showing upcoming schedule timeline.\n * Displays: layout IDs, time ranges, durations, current layout highlight.\n * Positioned bottom-left (download overlay is top-left).\n */\n\nimport { parseLayoutFile } from '@xiboplayer/schedule';\n\ninterface HiddenLayout {\n file: string;\n priority: number;\n}\n\ninterface TimelineEntry {\n layoutFile: string;\n startTime: Date;\n endTime: Date;\n duration: number;\n isDefault: boolean;\n hidden?: HiddenLayout[];\n missingMedia?: string[];\n}\n\nexport class TimelineOverlay {\n private overlay: HTMLElement | null = null;\n private visible: boolean;\n private timeline: TimelineEntry[] = [];\n private currentLayoutId: number | null = null;\n private layoutStartedAt: number | null = null; // wall-clock ms when layout began\n private currentDuration: number | null = null;\n private currentIsDefault: boolean = false;\n private previousLayout: { id: number; duration: number; startedAt: number } | null = null;\n private offline: boolean = false;\n private onLayoutClick: ((layoutId: number) => void) | null = null;\n private refreshTimer: ReturnType<typeof setInterval> | null = null;\n\n constructor(visible = false, onLayoutClick?: (layoutId: number) => void) {\n this.visible = visible;\n this.onLayoutClick = onLayoutClick || null;\n this.createOverlay();\n if (!this.visible) {\n this.overlay!.style.display = 'none';\n }\n // Re-render every 5s to update the remaining-time countdown on the current layout\n this.refreshTimer = setInterval(() => this.render(), 5000);\n }\n\n private createOverlay() {\n this.overlay = document.createElement('div');\n this.overlay.id = 'timeline-overlay';\n this.overlay.style.cssText = `\n position: fixed;\n bottom: 1.5vh;\n left: 1.5vw;\n background: rgba(0, 0, 0, 0.88);\n color: #fff;\n font-family: system-ui, -apple-system, sans-serif;\n font-size: 1.4vw;\n padding: 1vh 1.2vw;\n border-radius: 0.4vw;\n border: 1px solid rgba(255, 255, 255, 0.25);\n z-index: 999999;\n max-width: 35vw;\n box-shadow: 0 0.3vh 1.2vw rgba(0, 0, 0, 0.5);\n pointer-events: auto;\n `;\n // Click-to-skip: delegate click events on layout entries\n this.overlay.addEventListener('click', (e: MouseEvent) => {\n const target = (e.target as HTMLElement).closest('[data-layout-id]') as HTMLElement | null;\n if (!target || !this.onLayoutClick) return;\n const layoutId = parseInt(target.dataset.layoutId!, 10);\n if (isNaN(layoutId) || layoutId === this.currentLayoutId) return;\n this.onLayoutClick(layoutId);\n });\n\n document.body.appendChild(this.overlay);\n }\n\n toggle() {\n this.visible = !this.visible;\n if (this.overlay) {\n this.overlay.style.display = this.visible ? 'block' : 'none';\n }\n // Re-render when becoming visible (render() skips while hidden)\n if (this.visible) {\n this.render();\n }\n // Persist preference\n localStorage.setItem('xibo_show_timeline_overlay', String(this.visible));\n }\n\n /**\n * Update the overlay with new timeline data and/or current layout highlight.\n * Pass timeline=null to keep existing timeline and only update the highlight.\n */\n setOffline(offline: boolean) {\n this.offline = offline;\n this.render();\n }\n\n update(timeline: TimelineEntry[] | null, currentLayoutId: number | null, currentDuration?: number) {\n if (currentLayoutId !== null) {\n // Detect layout change — save previous for history display\n if (currentLayoutId !== this.currentLayoutId) {\n if (this.currentLayoutId !== null && this.currentDuration !== null && this.layoutStartedAt !== null) {\n this.previousLayout = { id: this.currentLayoutId, duration: this.currentDuration, startedAt: this.layoutStartedAt };\n }\n this.currentLayoutId = currentLayoutId;\n this.currentIsDefault = false;\n }\n // Always reset start time — same-layout replays emit layoutStart too\n this.layoutStartedAt = Date.now();\n // Duration is known at layout start — set it directly rather than\n // searching the timeline (which only contains future layouts).\n if (currentDuration !== undefined) {\n this.currentDuration = currentDuration;\n }\n }\n\n if (timeline !== null) {\n this.timeline = timeline;\n }\n\n this.render();\n }\n\n private render() {\n if (!this.overlay || !this.visible) return;\n\n if (this.timeline.length === 0 && !this.previousLayout && !this.currentLayoutId) {\n this.overlay.innerHTML = '<div style=\"color: #999;\">Timeline — no upcoming layouts</div>';\n return;\n }\n\n const now = Date.now();\n const clickable = this.onLayoutClick !== null;\n\n // Build upcoming list: timeline entries minus the first occurrence of the current layout\n let skippedCurrent = false;\n const upcoming: TimelineEntry[] = [];\n for (const entry of this.timeline) {\n const layoutId = parseLayoutFile(entry.layoutFile);\n if (!skippedCurrent && layoutId === this.currentLayoutId) {\n skippedCurrent = true;\n continue;\n }\n upcoming.push(entry);\n }\n\n // Count: previous (if any) + current (if any) + upcoming\n const totalCount = (this.previousLayout ? 1 : 0) + (this.currentLayoutId ? 1 : 0) + upcoming.length;\n const offlineBadge = this.offline ? ' <span style=\"color: #ff4444; font-size: 1.1vw;\">OFFLINE</span>' : '';\n let html = `<div style=\"font-weight: 600; margin-bottom: 0.8vh; font-size: 1.4vw; color: #ccc;\">Timeline (${totalCount} scheduled)${offlineBadge}</div>`;\n\n const maxVisible = 8;\n let rendered = 0;\n\n // 1. Previous layout (dimmed, strikethrough)\n if (this.previousLayout && rendered < maxVisible) {\n const prev = this.previousLayout;\n const durStr = this.formatDuration(prev.duration);\n const durPad = durStr.padStart(7).replace(/ /g, ' ');\n const idCol = `#${prev.id}`.padEnd(6).replace(/ /g, ' ');\n const startDate = new Date(prev.startedAt);\n const endDate = new Date(prev.startedAt + prev.duration * 1000);\n const timeRange = `${this.formatTime(startDate)}–${this.formatTime(endDate)} `;\n const cursor = clickable ? 'cursor: pointer;' : '';\n const hover = clickable ? 'onmouseover=\"this.style.background=\\'rgba(255,255,255,0.1)\\'\" onmouseout=\"this.style.background=\\'none\\'\"' : '';\n html += `<div data-layout-id=\"${prev.id}\" style=\"border-left: 0.25vw solid #555; padding-left: 0.6vw; color: #666; text-decoration: line-through; ${cursor} margin-bottom: 0.3vh; font-family: monospace; font-size: 1.3vw; line-height: 1.5; white-space: nowrap;\" ${hover}>`;\n html += `${timeRange}${idCol}${durPad}`;\n html += '</div>';\n rendered++;\n }\n\n // 2. Current layout (blue highlight, countdown from wall-clock start, with time range)\n if (this.currentLayoutId !== null && rendered < maxVisible) {\n let durStr: string;\n let timeRange = '';\n if (this.currentDuration !== null && this.layoutStartedAt !== null) {\n const elapsed = (now - this.layoutStartedAt) / 1000;\n const remainingSec = Math.max(0, Math.round(this.currentDuration - elapsed));\n durStr = this.formatDuration(remainingSec);\n const startDate = new Date(this.layoutStartedAt);\n const endDate = new Date(this.layoutStartedAt + this.currentDuration * 1000);\n timeRange = `${this.formatTime(startDate)}–${this.formatTime(endDate)} `;\n } else {\n durStr = '---';\n }\n const durPad = durStr.padStart(7).replace(/ /g, ' ');\n const idCol = `#${this.currentLayoutId}`.padEnd(6).replace(/ /g, ' ');\n html += `<div data-layout-id=\"${this.currentLayoutId}\" style=\"border-left: 0.25vw solid #4a9eff; padding-left: 0.6vw; color: #fff; font-weight: 600; margin-bottom: 0.3vh; font-family: monospace; font-size: 1.3vw; line-height: 1.5; white-space: nowrap;\">`;\n html += `${timeRange}${idCol}${durPad}`;\n if (this.currentIsDefault) html += ' <span style=\"color: #888;\">[def]</span>';\n html += '</div>';\n rendered++;\n }\n\n // 3. Upcoming layouts — compute times by chaining from current layout end\n let nextStartMs = (this.layoutStartedAt !== null && this.currentDuration !== null)\n ? this.layoutStartedAt + this.currentDuration * 1000\n : now;\n for (const entry of upcoming) {\n if (rendered >= maxVisible) break;\n const layoutId = parseLayoutFile(entry.layoutFile);\n const hasMissing = entry.missingMedia && entry.missingMedia.length > 0;\n const durStr = this.formatDuration(entry.duration);\n const entryEndMs = nextStartMs + entry.duration * 1000;\n const startStr = this.formatTime(new Date(nextStartMs));\n const endStr = this.formatTime(new Date(entryEndMs));\n\n let borderLeft: string;\n let color: string;\n if (hasMissing) {\n borderLeft = 'border-left: 0.25vw solid #ff4444; padding-left: 0.6vw;';\n color = 'color: #ff6666;';\n } else {\n borderLeft = 'padding-left: 0.85vw;';\n color = 'color: #aaa;';\n }\n const cursor = clickable ? 'cursor: pointer;' : '';\n const hover = clickable ? 'onmouseover=\"this.style.background=\\'rgba(255,255,255,0.1)\\'\" onmouseout=\"this.style.background=\\'none\\'\"' : '';\n\n html += `<div data-layout-id=\"${layoutId}\" style=\"${borderLeft} ${color} ${cursor} margin-bottom: 0.3vh; font-family: monospace; font-size: 1.3vw; line-height: 1.5; white-space: nowrap;\" ${hover}>`;\n const idCol = `#${layoutId}`.padEnd(6).replace(/ /g, ' ');\n const durPad = durStr.padStart(7).replace(/ /g, ' ');\n html += `${startStr}–${endStr} ${idCol}${durPad}`;\n if (entry.isDefault) html += ' <span style=\"color: #888;\">[def]</span>';\n if (hasMissing) {\n const missingList = entry.missingMedia!.join(', ');\n html += ` <span style=\"color: #ff4444; font-size: 1.1vw;\" title=\"Missing: ${missingList}\">⚠ ${entry.missingMedia!.length}</span>`;\n }\n if (entry.hidden && entry.hidden.length > 0) {\n const hiddenIds = entry.hidden.map(h => `#${h.file.replace('.xlf', '')} (p${h.priority})`).join(', ');\n html += ` <span style=\"color: #8899aa; font-size: 1.1vw;\" title=\"Also scheduled: ${hiddenIds}\">+${entry.hidden.length}</span>`;\n }\n html += '</div>';\n nextStartMs = entryEndMs;\n rendered++;\n }\n\n if (totalCount > maxVisible) {\n html += `<div style=\"padding-left: 0.85vw; color: #888; font-size: 1.1vw; margin-top: 0.3vh;\">+${totalCount - maxVisible} more</div>`;\n }\n\n this.overlay.innerHTML = html;\n }\n\n private formatTime(date: Date): string {\n return date.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });\n }\n\n private formatDuration(seconds: number): string {\n const m = Math.floor(seconds / 60);\n const s = Math.round(seconds % 60);\n return m > 0 ? `${m}m ${s.toString().padStart(2, '0')}s` : `${s}s`;\n }\n\n destroy() {\n if (this.refreshTimer) {\n clearInterval(this.refreshTimer);\n this.refreshTimer = null;\n }\n if (this.overlay) {\n this.overlay.remove();\n this.overlay = null;\n }\n }\n}\n\n/**\n * Determine initial visibility from URL param or localStorage.\n */\nexport function isTimelineVisible(): boolean {\n const urlParams = new URLSearchParams(window.location.search);\n const showTimeline = urlParams.get('showTimeline');\n if (showTimeline !== null) {\n return showTimeline !== '0' && showTimeline !== 'false';\n }\n\n const saved = localStorage.getItem('xibo_show_timeline_overlay');\n if (saved !== null) {\n return saved === 'true';\n }\n\n return false;\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Setup Overlay\n *\n * Two-phase overlay that never navigates away from the player:\n * 1. CMS key gate — verifies identity\n * 2. Full setup form — setup.html in a fullscreen iframe\n *\n * Dismissible with Esc or Cancel at both phases. On successful setup\n * the iframe redirects to index.html, which we intercept to reload.\n */\n\nimport { createLogger, config } from '@xiboplayer/utils';\n\nconst log = createLogger('SetupOverlay');\n\nexport class SetupOverlay {\n private backdrop: HTMLElement | null = null;\n private gateCard: HTMLElement | null = null;\n private iframe: HTMLIFrameElement | null = null;\n private cancelBtn: HTMLElement | null = null;\n private visible = false;\n\n show() {\n if (this.visible) return;\n this.visible = true;\n\n if (!this.backdrop) {\n this.create();\n }\n\n // Always start with the gate phase\n this.showGate();\n this.backdrop!.style.display = 'flex';\n log.info('[SetupOverlay] Opened');\n }\n\n hide() {\n if (!this.visible) return;\n this.visible = false;\n\n if (this.backdrop) {\n this.backdrop.style.display = 'none';\n }\n // Clear iframe to stop any polling timers inside setup.html\n if (this.iframe) {\n this.iframe.src = 'about:blank';\n this.iframe.style.display = 'none';\n }\n log.info('[SetupOverlay] Closed');\n }\n\n toggle() {\n if (this.visible) {\n this.hide();\n } else {\n this.show();\n }\n }\n\n isVisible() {\n return this.visible;\n }\n\n /** Show the CMS key gate card, hide the iframe */\n private showGate() {\n if (this.gateCard) this.gateCard.style.display = 'block';\n if (this.iframe) this.iframe.style.display = 'none';\n if (this.cancelBtn) this.cancelBtn.style.display = 'none';\n\n const input = this.gateCard?.querySelector('#gate-key') as HTMLInputElement;\n if (input) {\n input.value = '';\n requestAnimationFrame(() => input.focus());\n }\n const err = this.gateCard?.querySelector('#gate-error') as HTMLElement;\n if (err) err.style.display = 'none';\n }\n\n /** Show the setup iframe, hide the gate card */\n private showSetup() {\n if (this.gateCard) this.gateCard.style.display = 'none';\n if (this.cancelBtn) this.cancelBtn.style.display = 'block';\n if (this.iframe) {\n this.iframe.style.display = 'block';\n this.iframe.src = './setup.html?unlocked=1';\n }\n }\n\n private create() {\n // ── Backdrop ──\n this.backdrop = document.createElement('div');\n this.backdrop.id = 'setup-overlay-backdrop';\n this.backdrop.style.cssText = `\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.85);\n z-index: 1000000;\n display: none;\n align-items: center;\n justify-content: center;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n `;\n\n // ── Cancel button (visible in iframe phase) ──\n this.cancelBtn = document.createElement('button');\n this.cancelBtn.textContent = 'Cancel';\n this.cancelBtn.style.cssText = `\n position: absolute;\n top: 12px;\n right: 16px;\n background: transparent;\n border: 1px solid rgba(255, 255, 255, 0.3);\n color: #aaa;\n font-size: 14px;\n padding: 6px 18px;\n border-radius: 6px;\n cursor: pointer;\n z-index: 1000001;\n display: none;\n transition: background 0.2s, color 0.2s;\n `;\n this.cancelBtn.addEventListener('mouseenter', () => {\n this.cancelBtn!.style.background = 'rgba(255,255,255,0.1)';\n this.cancelBtn!.style.color = '#fff';\n });\n this.cancelBtn.addEventListener('mouseleave', () => {\n this.cancelBtn!.style.background = 'transparent';\n this.cancelBtn!.style.color = '#aaa';\n });\n this.cancelBtn.addEventListener('click', () => this.hide());\n\n // ── Gate card (matches setup.html .container) ──\n this.gateCard = document.createElement('div');\n this.gateCard.style.cssText = `\n background: #2A2A2A;\n border-radius: 16px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);\n padding: 48px;\n max-width: 480px;\n width: 90vw;\n color: #E0E0E0;\n `;\n this.gateCard.innerHTML = `\n <div style=\"text-align: center; margin-bottom: 32px;\">\n <div style=\"font-size: 36px; font-weight: 700; color: #fff; letter-spacing: -0.5px;\">\n <span style=\"color: #0097D8;\">xibo</span> player\n </div>\n <div style=\"font-size: 14px; color: #888; margin-top: 4px;\">PWA Digital Signage</div>\n </div>\n <div style=\"font-size: 18px; font-weight: 600; color: #fff; margin-bottom: 8px; text-align: center;\">\n Reconfigure Display\n </div>\n <div style=\"font-size: 13px; color: #888; margin-bottom: 20px; text-align: center; line-height: 1.5;\">\n Enter the current CMS Key to change settings.\n </div>\n <form id=\"gate-form\">\n <div style=\"margin-bottom: 20px;\">\n <label style=\"display: block; margin-bottom: 6px; color: #AAA; font-size: 13px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;\">\n CMS Key\n </label>\n <input type=\"password\" id=\"gate-key\" placeholder=\"Current CMS key\" required\n style=\"width: 100%; padding: 12px 14px; background: #1D1D1D; border: 2px solid #3A3A3A; border-radius: 8px; font-size: 15px; color: #E0E0E0; transition: border-color 0.2s; box-sizing: border-box;\">\n </div>\n <button type=\"submit\" style=\"width: 100%; padding: 14px; background: #0097D8; color: white; border: none; border-radius: 8px; font-size: 15px; font-weight: 600; cursor: pointer; transition: background 0.2s, transform 0.1s;\">\n Unlock\n </button>\n </form>\n <div id=\"gate-error\" style=\"margin-top: 16px; padding: 12px 14px; background: rgba(244, 67, 54, 0.15); border: 1px solid rgba(244, 67, 54, 0.3); border-radius: 8px; color: #EF9A9A; font-size: 14px; display: none;\"></div>\n <button id=\"gate-cancel\" style=\"width: 100%; padding: 14px; background: transparent; border: 1px solid #3A3A3A; color: #AAA; border-radius: 8px; font-size: 15px; font-weight: 600; cursor: pointer; margin-top: 8px; transition: background 0.2s;\">\n Cancel\n </button>\n `;\n\n // ── Iframe (fullscreen, same look as setup.html) ──\n this.iframe = document.createElement('iframe');\n this.iframe.style.cssText = `\n width: 100%;\n height: 100%;\n border: none;\n background: #1D1D1D;\n display: none;\n `;\n\n // Detect success redirect: setup.html navigates to index.html → reload player\n this.iframe.addEventListener('load', () => {\n try {\n const href = this.iframe!.contentWindow?.location?.href || '';\n if (href.includes('index.html')) {\n this.hide();\n window.location.reload();\n return;\n }\n\n // Esc inside the iframe dismisses the overlay\n const iframeDoc = this.iframe!.contentDocument;\n if (!iframeDoc) return;\n iframeDoc.addEventListener('keydown', (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n e.preventDefault();\n this.hide();\n }\n });\n } catch { /* not loaded yet */ }\n });\n\n this.backdrop.appendChild(this.cancelBtn);\n this.backdrop.appendChild(this.gateCard);\n this.backdrop.appendChild(this.iframe);\n document.body.appendChild(this.backdrop);\n\n // ── Gate event handlers ──\n const form = this.gateCard.querySelector('#gate-form') as HTMLFormElement;\n const input = this.gateCard.querySelector('#gate-key') as HTMLInputElement;\n const errorEl = this.gateCard.querySelector('#gate-error') as HTMLElement;\n const gateCancelBtn = this.gateCard.querySelector('#gate-cancel') as HTMLButtonElement;\n\n input.addEventListener('focus', () => { input.style.borderColor = '#0097D8'; });\n input.addEventListener('blur', () => { input.style.borderColor = '#3A3A3A'; });\n\n form.addEventListener('submit', (e: Event) => {\n e.preventDefault();\n const entered = input.value.trim();\n\n if (entered === config.cmsKey) {\n this.showSetup();\n } else {\n errorEl.textContent = 'Incorrect CMS key';\n errorEl.style.display = 'block';\n input.focus();\n input.select();\n }\n });\n\n gateCancelBtn.addEventListener('click', () => this.hide());\n\n // Esc closes overlay; stopPropagation blocks player shortcuts\n this.backdrop.addEventListener('keydown', (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n e.preventDefault();\n this.hide();\n }\n e.stopPropagation();\n });\n }\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * PWA Player with RendererLite\n *\n * Lightweight PWA player using modular PlayerCore orchestration.\n * Platform layer handles UI, DOM manipulation, and platform-specific features.\n */\n\nimport { RendererLite } from '@xiboplayer/renderer';\nimport { StoreClient, DownloadManager, BARRIER } from '@xiboplayer/cache';\nimport { PlayerCore, CORE_EVENTS as E } from '@xiboplayer/core';\nimport { parseLayoutDuration, parseLayoutFile } from '@xiboplayer/schedule';\nimport { createLogger, registerLogSink, PLAYER_API } from '@xiboplayer/utils';\nimport { DownloadOverlay, getDefaultOverlayConfig } from './download-overlay.js';\nimport { TimelineOverlay, isTimelineVisible } from './timeline-overlay.js';\nimport { SetupOverlay } from './setup-overlay.js';\n\ndeclare const __APP_VERSION__: string;\ndeclare const __BUILD_DATE__: string;\n\nconst log = createLogger('PWA');\n\n// ContentStore key prefix — mirrors PLAYER_API without leading slash\nconst STORE_PREFIX = PLAYER_API.slice(1);\n\n// Dynamic base path — same build serves /player/pwa/, /player/pwa-xmds/, /player/pwa-xlr/\nconst PLAYER_BASE = new URL('./', window.location.href).pathname.replace(/\\/$/, '');\n\n// Import core modules (will be loaded at runtime)\nlet cacheWidgetHtml: any;\nlet scheduleManager: any;\nlet config: any;\nlet RestClient: any;\nlet XmdsClient: any;\nlet ProtocolDetector: any;\nlet XmrWrapper: any;\nlet store: StoreClient;\nlet downloadManager: DownloadManager;\nlet StatsCollector: any;\nlet formatStats: any;\nlet LogReporter: any;\nlet formatLogs: any;\nlet DisplaySettings: any;\nlet SyncManager: any;\nlet computeStagger: any;\n\n// SDK package versions (populated in loadCoreModules)\nconst sdkVersions: Record<string, string> = {};\n\nclass PwaPlayer {\n private renderer!: RendererLite;\n private core!: PlayerCore;\n private xmds!: any;\n private downloadOverlay: DownloadOverlay | null = null;\n private timelineOverlay: TimelineOverlay | null = null;\n private setupOverlay: SetupOverlay | null = null;\n private statsCollector: any = null;\n private logReporter: any = null;\n private displaySettings: any = null;\n private currentScheduleId: number = -1; // Track scheduleId for stats\n private scheduledLayoutIds: Set<number> = new Set(); // Layout IDs from current schedule\n private preparingLayoutId: number | null = null; // Guard against concurrent prepareLayout calls\n private _pendingRetryLayoutId: number | null = null; // Queued retry when check-pending-layout arrives during preparation\n private _screenshotInterval: any = null;\n private _screenshotMethod: 'electron' | 'displayMedia' | 'html2canvas' | null = null;\n private _html2canvasMod: any = null;\n private _screenshotInFlight = false; // Concurrency guard — one capture at a time\n private _wakeLock: any = null; // Screen Wake Lock sentinel\n private syncManager: any = null; // Multi-display sync coordinator\n private _currentLayoutEnableStat: boolean = true; // enableStat from current layout XLF\n private _probeTimer: any = null; // Debounce timer for duration probing\n private _mediaStatusTimer: ReturnType<typeof setTimeout> | null = null; // Debounce timer for media status check\n private _pendingFollowerStats: any[] | null = null; // In-flight stats delegated to lead\n private _pendingFollowerLogs: any[] | null = null; // In-flight logs delegated to lead\n private _iframeObserver: MutationObserver | null = null; // Iframe key-forwarding observer\n private _swIcHandler: any = null; // SW Interactive Control message handler\n private _chunkConfig: any = null; // Device-adaptive chunk configuration\n private _fileIdToSaveAs: Map<string, string> = new Map(); // Numeric file ID → storedAs filename\n private _cachedMediaKeys: Set<string> = new Set(); // saveAs keys confirmed cached (avoids HEAD 404s)\n private protocolDetector: any = null; // CMS protocol auto-detector\n\n async init() {\n log.info('Initializing player with RendererLite + PlayerCore...');\n\n // Load core modules\n await this.loadCoreModules();\n\n // Register Service Worker for offline-first kiosk mode\n if ('serviceWorker' in navigator) {\n try {\n const registration = await navigator.serviceWorker.register(`${PLAYER_BASE}/sw-pwa.js?v=${Date.now()}`, {\n scope: `${PLAYER_BASE}/`,\n type: 'module',\n updateViaCache: 'none'\n });\n log.info('Service Worker registered for offline mode:', registration.scope);\n\n // Request persistent storage (kiosk requirement)\n if (navigator.storage && navigator.storage.persist) {\n const persistent = await navigator.storage.persist();\n if (persistent) {\n log.info('Persistent storage granted - cache won\\'t be evicted');\n } else {\n log.warn('Persistent storage denied - cache may be evicted');\n }\n }\n } catch (error) {\n log.warn('Service Worker registration failed:', error);\n }\n }\n\n // Initialize StoreClient (REST) + DownloadManager (main thread)\n log.info('Initializing cache clients...');\n store = new StoreClient();\n const { calculateChunkConfig } = await import('@xiboplayer/sw');\n this._chunkConfig = calculateChunkConfig(log);\n downloadManager = new DownloadManager({\n concurrency: this._chunkConfig.concurrency,\n chunkSize: this._chunkConfig.chunkSize,\n chunksPerFile: 2,\n });\n log.info('Cache clients ready — StoreClient + DownloadManager');\n\n // Create renderer\n const container = document.getElementById('player-container');\n if (!container) {\n throw new Error('No #player-container found');\n }\n\n this.renderer = new RendererLite(\n {\n cmsUrl: config.cmsUrl,\n hardwareKey: config.hardwareKey\n },\n container,\n {\n // Provide fileId→saveAs map for layout background resolution\n fileIdToSaveAs: this._fileIdToSaveAs,\n\n // Provide widget HTML resolver — check ContentStore via proxy\n getWidgetHtml: async (widget: any) => {\n const widgetPath = `${PLAYER_API}/widgets/${widget.layoutId}/${widget.regionId}/${widget.id}`;\n log.debug(`Looking for widget HTML at: ${widgetPath}`, widget);\n\n try {\n const exists = await store.has(`${STORE_PREFIX}/widgets`, `${widget.layoutId}/${widget.regionId}/${widget.id}`);\n if (exists) {\n log.debug(`Widget HTML found in store, using mirror URL for iframe`);\n return { url: widgetPath, fallback: widget.raw || '' };\n } else {\n log.warn(`No widget HTML found in store: ${widgetPath}`);\n }\n } catch (error) {\n log.error(`Failed to check widget HTML for ${widget.id}:`, error);\n }\n\n // Fallback to widget.raw (XLF template)\n log.warn(`Using widget.raw fallback for ${widget.id}`);\n return widget.raw || '';\n }\n }\n );\n\n // Create PlayerCore (with CMS-namespaced offline cache DB)\n this.core = new PlayerCore({\n config,\n xmds: this.xmds,\n cache: store,\n schedule: scheduleManager,\n renderer: this.renderer,\n xmrWrapper: XmrWrapper,\n statsCollector: this.statsCollector,\n displaySettings: this.displaySettings,\n cmsId: config.activeCmsId,\n });\n\n // Setup platform-specific event handlers\n this.setupCoreEventHandlers();\n this.setupRendererEventHandlers();\n this.setupInteractiveControl();\n this.setupDataConnectorNotify();\n this.setupRemoteControls();\n\n // Setup UI\n this.updateConfigDisplay();\n\n // Online/offline event listeners for seamless offline mode\n window.addEventListener('online', () => {\n log.info('Browser reports online — triggering immediate collection');\n this.updateStatus('Back online, syncing...');\n this.removeOfflineIndicator();\n this.core.collectNow().catch((error: any) => {\n log.error('Failed to collect after coming online:', error);\n });\n });\n window.addEventListener('offline', () => {\n log.warn('Browser reports offline — continuing playback with cached data');\n this.updateStatus('Offline mode — using cached content');\n this.showOfflineIndicator();\n });\n\n // Initialize download progress overlay (configurable debug feature)\n // Respect controls.keyboard.debugOverlays — if disabled, don't restore overlays\n const controls = this.getControls();\n const debugOverlaysEnabled = (controls.keyboard || {}).debugOverlays === true;\n\n const overlayConfig = getDefaultOverlayConfig();\n if (overlayConfig.enabled && debugOverlaysEnabled) {\n this.downloadOverlay = new DownloadOverlay(overlayConfig);\n this.downloadOverlay.setProgressCallback(() => downloadManager.getProgress());\n log.info('Download overlay enabled (hover bottom-right corner)');\n }\n\n // Timeline overlay — created on first T key press (or if previously visible)\n if (isTimelineVisible() && debugOverlaysEnabled) {\n this.timelineOverlay = new TimelineOverlay(true, (layoutId) => this.skipToLayout(layoutId));\n }\n\n // Listen for certificate warnings from Electron main process\n this.setupCertWarnings();\n\n // Listen for XMR connection status changes\n this.setupXmrWarning();\n\n // Request Screen Wake Lock to prevent display sleep\n await this.requestWakeLock();\n\n // Re-acquire wake lock when tab becomes visible again\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'visible') {\n this.requestWakeLock();\n }\n });\n\n // Start collection cycle\n await this.core.collect();\n\n log.info('Player initialized successfully');\n }\n\n /**\n * Request Screen Wake Lock to prevent display from sleeping\n * Re-acquired on visibility change (browser releases it when tab is hidden)\n */\n private async requestWakeLock() {\n if (!('wakeLock' in navigator)) {\n log.debug('Wake Lock API not supported');\n return;\n }\n\n try {\n this._wakeLock = await (navigator as any).wakeLock.request('screen');\n log.info('Screen Wake Lock acquired — display will stay on');\n\n this._wakeLock.addEventListener('release', () => {\n log.debug('Screen Wake Lock released');\n this._wakeLock = null;\n });\n } catch (error: any) {\n log.warn('Wake Lock request failed:', error?.message);\n }\n }\n\n /**\n * Listen for certificate warnings from Electron and show in the top bar.\n * The #overlay bar (defined in index.html) is the status bar with\n * #config-info (left) and #status (right). If it was removed (statusBarOnHover\n * not set), we recreate it. Cert warnings make the bar always visible.\n */\n private setupCertWarnings() {\n const warnedHosts = new Set<string>();\n\n window.addEventListener('cert-warning', ((e: CustomEvent) => {\n const { host, error } = e.detail;\n if (warnedHosts.has(host)) return;\n warnedHosts.add(host);\n\n log.warn(`Invalid SSL certificate accepted for stream: ${host} (${error})`);\n\n // Find or recreate the top bar\n let overlay = document.getElementById('overlay');\n let created = false;\n if (!overlay) {\n overlay = document.createElement('div');\n overlay.id = 'overlay';\n // Recreate child structure: config-info | status\n const info = document.createElement('div');\n info.id = 'config-info';\n overlay.appendChild(info);\n const status = document.createElement('div');\n status.id = 'status';\n overlay.appendChild(status);\n document.body.appendChild(overlay);\n created = true;\n }\n\n // Find or create the cert warning span between #config-info and #status\n let certSpan = document.getElementById('cert-warnings');\n if (!certSpan) {\n certSpan = document.createElement('span');\n certSpan.id = 'cert-warnings';\n certSpan.style.cssText = 'color: #ffaa33; flex: 0 0 auto;';\n const statusEl = document.getElementById('status');\n overlay.insertBefore(certSpan, statusEl);\n }\n\n const hosts = [...warnedHosts].join(', ');\n certSpan.textContent = `\\u26A0 SSL: ${hosts}`;\n\n // Don't force always-visible — let hover-only CSS handle show/hide\n\n // If we recreated the overlay, repopulate config info\n if (created) this.updateConfigDisplay();\n }) as EventListener);\n }\n\n /**\n * Show/hide an XMR disconnected warning in the top bar.\n * Placed before #cert-warnings (or before #status if no cert warnings).\n */\n private setupXmrWarning() {\n this.core.on('xmr-status', ({ connected }: { connected: boolean }) => {\n const overlay = document.getElementById('overlay');\n if (!overlay) return;\n\n let span = document.getElementById('xmr-warning');\n\n if (!connected) {\n if (!span) {\n span = document.createElement('span');\n span.id = 'xmr-warning';\n span.style.cssText = 'color: #ff6666; flex: 0 0 auto;';\n // Insert before cert-warnings or status (whichever comes first)\n const anchor = document.getElementById('cert-warnings') || document.getElementById('status');\n overlay.insertBefore(span, anchor);\n }\n span.textContent = '\\u26A0 XMR disconnected';\n } else {\n span?.remove();\n }\n });\n }\n\n /**\n * Load core modules\n */\n private async loadCoreModules() {\n try {\n const [\n cacheModule, xmdsModule, scheduleModule, configModule,\n xmrModule, statsModule, displaySettingsModule, coreModule,\n rendererModule, syncModule,\n ] = await Promise.all([\n import('@xiboplayer/cache'),\n import('@xiboplayer/xmds'),\n import('@xiboplayer/schedule'),\n import('@xiboplayer/utils'),\n import('@xiboplayer/xmr'),\n import('@xiboplayer/stats'),\n import('@xiboplayer/settings'),\n import('@xiboplayer/core'),\n import('@xiboplayer/renderer'),\n import('@xiboplayer/sync'),\n ]);\n\n cacheWidgetHtml = cacheModule.cacheWidgetHtml;\n SyncManager = syncModule.SyncManager;\n computeStagger = syncModule.computeStagger;\n scheduleManager = scheduleModule.scheduleManager;\n config = configModule.config;\n RestClient = xmdsModule.RestClient;\n XmdsClient = xmdsModule.XmdsClient;\n ProtocolDetector = xmdsModule.ProtocolDetector;\n XmrWrapper = xmrModule.XmrWrapper;\n StatsCollector = statsModule.StatsCollector;\n formatStats = statsModule.formatStats;\n LogReporter = statsModule.LogReporter;\n formatLogs = statsModule.formatLogs;\n DisplaySettings = displaySettingsModule.DisplaySettings;\n\n // Capture SDK package versions\n sdkVersions.core = coreModule.VERSION || '?';\n sdkVersions.cache = cacheModule.VERSION || '?';\n sdkVersions.renderer = rendererModule.VERSION || '?';\n sdkVersions.schedule = scheduleModule.VERSION || '?';\n sdkVersions.xmds = xmdsModule.VERSION || '?';\n sdkVersions.xmr = xmrModule.VERSION || '?';\n sdkVersions.utils = configModule.VERSION || '?';\n sdkVersions.stats = statsModule.VERSION || '?';\n sdkVersions.settings = displaySettingsModule.VERSION || '?';\n\n // Get MAC address from Electron if available (for WOL support)\n if ((window as any).electronAPI?.getSystemInfo) {\n try {\n const sysInfo = await (window as any).electronAPI.getSystemInfo();\n if (sysInfo.macAddress) {\n config.macAddress = sysInfo.macAddress;\n }\n } catch (_) { /* pure PWA — no Electron API */ }\n }\n\n // Transport selection:\n // transport: \"rest\" → forced REST API\n // transport: \"xmds\" → forced SOAP\n // transport: \"auto\" → probe REST → SOAP fallback (default)\n // /player/pwa-xmds/ → forced SOAP (URL-based override)\n // ?transport=xmds → forced SOAP (query param override)\n const cfgTransport = config.transport !== 'auto' ? config.transport : undefined;\n const urlTransport = new URLSearchParams(window.location.search).get('transport');\n const transport = urlTransport\n || (PLAYER_BASE.includes('pwa-xmds') ? 'xmds' : null)\n || cfgTransport\n || 'auto';\n\n // Use ProtocolDetector for auto-detection with re-probe support\n this.protocolDetector = new ProtocolDetector(config.cmsUrl, RestClient, XmdsClient);\n const forceProtocol = (transport === 'auto') ? undefined : transport;\n const { client } = await this.protocolDetector.detect(config, forceProtocol);\n this.xmds = client;\n\n // Initialize stats collector (namespaced by CMS ID)\n const cmsId = config.activeCmsId;\n this.statsCollector = new StatsCollector(cmsId);\n await this.statsCollector.init();\n log.info(`Stats collector initialized${cmsId ? ` (CMS: ${cmsId})` : ''}`);\n\n // Initialize log reporter for CMS log submission (namespaced by CMS ID)\n this.logReporter = new LogReporter(cmsId);\n await this.logReporter.init();\n log.info(`Log reporter initialized${cmsId ? ` (CMS: ${cmsId})` : ''}`);\n\n // Serialize log args to string (shared by log reporter and console forwarder)\n const serializeArgs = (args: any[]) => args.map((a: any) => typeof a === 'string' ? a : JSON.stringify(a)).join(' ');\n\n // Bridge logger output to LogReporter for CMS submission\n registerLogSink(({ level, name, args }: { level: string; name: string; args: any[] }) => {\n if (!this.logReporter) return;\n const message = serializeArgs(args);\n this.logReporter.log(level, `[${name}] ${message}`, 'PLAYER').catch(() => {});\n });\n\n // Forward console logs to proxy stdout (for journald/log analysis).\n // Controlled by debug.consoleLogs in config.json.\n // Optional debug.consoleLogsInterval (seconds) sets the batch flush interval (default 10s).\n const debugConfig = config.debug;\n if (debugConfig?.consoleLogs) {\n const flushIntervalMs = (debugConfig.consoleLogsInterval || 10) * 1000;\n let batch: Array<{ level: string; name: string; message: string; ts: string }> = [];\n let flushTimer: ReturnType<typeof setTimeout> | null = null;\n\n const flushLogs = () => {\n if (batch.length === 0) return;\n const payload = batch;\n batch = [];\n flushTimer = null;\n // Fire-and-forget POST — log forwarding must never block the player\n fetch('/debug/log', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n }).catch(() => {});\n };\n\n registerLogSink(({ level, name, args }: { level: string; name: string; args: any[] }) => {\n const message = serializeArgs(args);\n batch.push({ level, name, message, ts: new Date().toISOString() });\n if (!flushTimer) {\n flushTimer = setTimeout(flushLogs, flushIntervalMs);\n }\n });\n\n log.info(`Console log forwarding to proxy enabled (flush every ${flushIntervalMs / 1000}s)`);\n }\n\n // Initialize display settings manager\n this.displaySettings = new DisplaySettings();\n log.info('Display settings manager initialized');\n\n // Log version and environment information for debugging\n const buildDate = typeof __BUILD_DATE__ !== 'undefined' ? __BUILD_DATE__ : '?';\n const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '?';\n log.info(`v${appVersion} built ${buildDate}`);\n const versionParts = Object.entries(sdkVersions).map(([k, v]) => `${k}=${v}`).join(' ');\n log.info(`SDK: ${versionParts}`);\n const isElectron = !!(window as any).electronAPI;\n const electronVersion = isElectron ? (navigator.userAgent.match(/Electron\\/([\\d.]+)/)?.[1] || '?') : null;\n const chromeVersion = navigator.userAgent.match(/Chrome\\/([\\d.]+)/)?.[1] || '?';\n const platform = isElectron ? `Electron ${electronVersion} / Chrome ${chromeVersion}` : `Chrome ${chromeVersion}`;\n log.info(`Env: PWA v${appVersion} | ${platform} | ${navigator.platform} | ${screen.width}x${screen.height}`);\n\n log.info('Core modules loaded');\n } catch (error) {\n log.error('Failed to load core modules:', error);\n throw error;\n }\n }\n\n /**\n * Setup PlayerCore event handlers (Platform-specific UI updates)\n */\n private setupCoreEventHandlers() {\n // Delegate to focused handler groups\n this.setupSyncEventHandlers();\n this.setupDownloadEventHandlers();\n this.setupCommandEventHandlers();\n\n // Collection events\n this.core.on(E.COLLECTION_START, () => {\n this.updateStatus('Collecting data from CMS...');\n });\n\n this.core.on(E.REGISTER_COMPLETE, (regResult: any) => {\n const displayName = this.displaySettings?.getDisplayName() || regResult.displayName || config.hardwareKey;\n this.updateStatus(`Registered: ${displayName}`);\n\n // Update page title with display name\n if (this.displaySettings) {\n document.title = `Xibo Player - ${this.displaySettings.getDisplayName()}`;\n }\n\n // Set display location from CMS settings\n const lat = parseFloat(regResult?.settings?.latitude);\n const lng = parseFloat(regResult?.settings?.longitude);\n if (lat && lng && !isNaN(lat) && !isNaN(lng)) {\n log.info(`Display location from CMS: ${lat.toFixed(4)}, ${lng.toFixed(4)}`);\n if (scheduleManager?.setLocation) {\n scheduleManager.setLocation(lat, lng);\n }\n } else if (this.core.requestGeoLocation) {\n // No CMS coordinates — try browser Geolocation API as fallback\n log.info('No CMS coordinates, requesting browser geolocation...');\n this.core.requestGeoLocation();\n }\n\n // Multi-display sync: local config fallback when CMS doesn't provide syncConfig\n if (!regResult.syncConfig && config.data?.sync) {\n log.info('[Sync] Using local sync config (CMS did not provide syncConfig)');\n this.core.syncConfig = config.data.sync;\n this.core.emit(E.SYNC_CONFIG, config.data.sync);\n }\n });\n\n // NOTE: Two OFFLINE_MODE listeners are intentional — this one handles UI,\n // setupSyncEventHandlers() registers a second one for sync bootstrap.\n this.core.on(E.OFFLINE_MODE, (isOffline: boolean) => {\n if (isOffline) {\n this.updateStatus('Offline mode — using cached content');\n this.showOfflineIndicator();\n } else {\n this.updateStatus('Back online');\n this.removeOfflineIndicator();\n }\n });\n\n this.core.on(E.SCHEDULE_RECEIVED, (schedule: any) => {\n this.updateStatus('Processing schedule...');\n\n // Extract scheduleId for stats tracking\n // Check layouts or campaigns for scheduleId\n if (schedule.layouts && schedule.layouts.length > 0) {\n this.currentScheduleId = parseInt(schedule.layouts[0].scheduleid) || -1;\n } else if (schedule.campaigns && schedule.campaigns.length > 0) {\n this.currentScheduleId = parseInt(schedule.campaigns[0].scheduleid) || -1;\n }\n\n // Selectively clear preloaded layouts not in the new schedule.\n // Keep warm entries whose layout ID is still scheduled — their DOM is still valid.\n // (The CMS schedule CRC changes every collection due to timestamps, even when\n // the actual layout list hasn't changed. Blindly clearing would destroy preloads.)\n if (this.renderer?.layoutPool) {\n const scheduledIds = new Set<number>();\n if (schedule.layouts) {\n for (const l of schedule.layouts) {\n const id = parseLayoutFile(l.file || l.id || l);\n if (id) scheduledIds.add(id);\n }\n }\n if (schedule.campaigns) {\n for (const c of schedule.campaigns) {\n if (c.layouts) {\n for (const l of c.layouts) {\n const id = parseLayoutFile(l.file || l.id || l);\n if (id) scheduledIds.add(id);\n }\n }\n }\n }\n const cleared = this.renderer.layoutPool.clearWarmNotIn(scheduledIds);\n if (cleared > 0) {\n log.info(`Cleared ${cleared} preloaded layout(s) no longer in schedule`);\n }\n this.scheduledLayoutIds = scheduledIds;\n }\n\n log.debug('Current scheduleId for stats:', this.currentScheduleId);\n });\n\n this.core.on(E.LAYOUT_PREPARE_REQUEST, async (layoutId: number) => {\n await this.prepareLayout(layoutId);\n // Non-sync or no layout playing yet: show immediately.\n // Sync transitions: onLayoutShow handles showing with stagger.\n if (!this.syncManager || this.renderer.getCurrentLayoutId() === null) {\n this.renderer.showLayout(layoutId);\n }\n });\n\n this.core.on(E.LAYOUT_ALREADY_PLAYING, (layoutId: number) => {\n // The schedule says this layout should still be playing. Verify the renderer\n // actually has an active timer — if not, the renderer stalled (e.g. after a\n // GPU crash/recovery or restart) and we need to force a re-show.\n if (!this.renderer.hasActiveLayoutTimer()) {\n log.warn(`Layout ${layoutId} has no active timer — restarting layout`);\n this.renderer.stopCurrentLayout();\n // stopCurrentLayout → layoutEnd → advanceToNextLayout → re-prepares + shows\n }\n });\n\n this.core.on(E.LAYOUT_EXPIRE_CURRENT, () => {\n log.info('Schedule changed — expiring current layout');\n this.renderer.stopCurrentLayout();\n // stopCurrentLayout() emits layoutEnd → the layoutEnd handler\n // calls advanceToNextLayout() which picks the next scheduled layout\n });\n\n this.core.on(E.NO_LAYOUTS_SCHEDULED, () => {\n this.updateStatus('No layouts scheduled');\n });\n\n this.core.on(E.COLLECTION_COMPLETE, () => {\n const layoutId = this.core.getCurrentLayoutId();\n if (layoutId) {\n this.updateStatus(`Playing layout ${layoutId}`);\n } else if (this.preparingLayoutId) {\n this.updateStatus(`Downloading layout ${this.preparingLayoutId}...`);\n }\n\n // Duration probing is handled by the debounced re-probe (3s after last\n // file cached) — avoids 404s from probing before downloads complete.\n });\n\n this.core.on(E.COLLECTION_ERROR, async (error: any) => {\n this.updateStatus(`Collection error: ${error}`, 'error');\n\n // Display not found / not authorized — show setup screen so user can re-register\n const msg = error?.message || String(error);\n if (msg.includes('403') && (msg.includes('Display not found') || msg.includes('not authorized'))) {\n log.warn('Display not registered or not authorized — showing setup screen');\n if (!this.setupOverlay) {\n this.setupOverlay = new SetupOverlay();\n }\n this.setupOverlay.show();\n return;\n }\n\n // Report fault to CMS (triggers dashboard alert)\n this.reportFault('COLLECTION_FAILED', `Collection cycle failed: ${error?.message || error}`);\n });\n\n this.core.on(E.XMR_CONNECTED, (url: string) => {\n log.info('XMR connected:', url);\n });\n\n this.core.on(E.XMR_MISCONFIGURED, (info: { reason: string; url?: string; message: string }) => {\n log.warn(`XMR misconfigured (${info.reason}): ${info.message}`);\n });\n\n // Log level changes from CMS (overlays are controlled by config.controls, not log level)\n this.core.on(E.LOG_LEVEL_CHANGED, () => {\n log.info(`Log level changed`);\n });\n\n // Overlay layout push from XMR\n this.core.on(E.OVERLAY_LAYOUT_REQUEST, async (layoutId: number) => {\n log.info('Overlay layout requested:', layoutId);\n // Re-use existing overlay rendering (schedule-driven overlays already work)\n // Just need to prepare and render the overlay layout\n await this.prepareLayout(layoutId);\n this.renderer.showLayout(layoutId);\n });\n\n // Revert to schedule (undo XMR layout override)\n this.core.on(E.REVERT_TO_SCHEDULE, () => {\n log.info('Reverting to scheduled content');\n this.updateStatus('Reverting to schedule...');\n });\n\n // Display settings events\n if (this.displaySettings) {\n this.displaySettings.on('interval-changed', (newInterval: number) => {\n log.info(`Collection interval changed to ${newInterval}s`);\n });\n\n this.displaySettings.on('settings-applied', (_settings: any, changes: string[]) => {\n if (changes.length > 0) {\n log.info('Settings updated from CMS:', changes.join(', '));\n }\n // Start periodic screenshots once we have settings (only first time)\n if (!this._screenshotInterval) {\n this.startScreenshotInterval();\n }\n });\n }\n\n // Stats submission\n this.core.on(E.SUBMIT_STATS_REQUEST, async () => {\n await this.submitStats();\n });\n\n // Log submission to CMS\n this.core.on(E.SUBMIT_LOGS_REQUEST, async () => {\n await this.submitLogs();\n });\n\n // Screenshot capture (triggered by XMR or periodic interval)\n this.core.on(E.SCREENSHOT_REQUEST, async () => {\n await this.captureAndSubmitScreenshot();\n });\n\n // Handle check-pending-layout events — layout was pending download, now ready\n this.core.on(E.CHECK_PENDING_LAYOUT, async (layoutId: number) => {\n await this.prepareLayout(layoutId);\n this.renderer.showLayout(layoutId);\n });\n\n // Navigate to widget (navWidget action via triggerCode from schedule-level actions)\n this.core.on(E.NAVIGATE_TO_WIDGET, (action: any) => {\n if (action.targetId) {\n this.renderer.navigateToWidget(action.targetId);\n } else {\n log.warn('navigate-to-widget action has no targetId:', action);\n }\n });\n\n // Timeline overlay — visualize upcoming schedule\n this.core.on(E.TIMELINE_UPDATED, (timeline: any[]) => {\n const id = this.core.getCurrentLayoutId();\n const dur = id ? this.core.getLayoutDuration(id) : undefined;\n this.timelineOverlay?.update(timeline, id, dur);\n });\n }\n\n /**\n * Setup multi-display sync event handlers.\n * Handles SYNC_CONFIG and offline sync fallback via OFFLINE_MODE.\n */\n private setupSyncEventHandlers() {\n // Offline sync: if CMS is unreachable but local config has sync settings,\n // start SyncManager so LAN-only displays can still sync with each other.\n this.core.on(E.OFFLINE_MODE, (isOffline: boolean) => {\n if (isOffline && !this.syncManager && config.data?.sync) {\n log.info('[Sync] Offline mode with local sync config — starting sync');\n this.core.syncConfig = config.data.sync;\n this.core.emit(E.SYNC_CONFIG, config.data.sync);\n }\n });\n\n // Multi-display sync: create SyncManager when CMS provides sync config (or local fallback)\n this.core.on(E.SYNC_CONFIG, async (syncConfig: any) => {\n if (this.syncManager) {\n this.syncManager.stop();\n }\n\n // Cross-device sync: build WebSocket relay URL.\n // Always rebuild for followers (mDNS re-discovers lead IP/port each cycle).\n // Lead connects to its own relay (localhost).\n if (syncConfig.syncPublisherPort) {\n if (syncConfig.syncGroupId) {\n syncConfig.syncGroup = String(syncConfig.syncGroupId);\n }\n\n if (syncConfig.isLead) {\n syncConfig.relayUrl = `ws://localhost:${syncConfig.syncPublisherPort}/sync`;\n // Trigger mDNS advertisement so followers can discover us\n fetch('/system/advertise-sync', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ syncGroupId: syncConfig.syncGroupId, port: syncConfig.syncPublisherPort, displayId: config.hardwareKey }),\n }).catch(() => {});\n } else {\n // Try mDNS discovery first, fall back to CMS-provided IP\n let leadHost = syncConfig.syncGroup;\n try {\n const res = await fetch(`/system/discover-lead?syncGroupId=${syncConfig.syncGroupId}`);\n if (res.ok) {\n const { host, port } = await res.json();\n leadHost = host;\n log.info(`mDNS discovered lead at ${host}:${port}`);\n }\n } catch (_) {\n log.warn('mDNS discovery failed, using CMS-provided IP');\n }\n syncConfig.relayUrl = `ws://${leadHost}:${syncConfig.syncPublisherPort}/sync`;\n }\n }\n\n // Persist resolved sync config to config.json so offline restarts\n // can sync over LAN without CMS. Strips runtime-only fields.\n // Merge with existing sync config to preserve local-only fields\n // (topology, choreography, staggerMs, gridCols, gridRows).\n const { syncToken, ...persistable } = syncConfig;\n const merged = { ...(config.data?.sync || {}), ...persistable };\n if ((window as any).electronAPI?.setConfig) {\n (window as any).electronAPI.setConfig({ sync: merged });\n } else {\n fetch('/config', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sync: merged }),\n }).catch(() => {});\n }\n\n // Pass CMS server key as sync token for relay auth (shared by all displays on this CMS)\n if (!syncConfig.syncToken) {\n syncConfig.syncToken = config.cmsKey;\n }\n\n this.syncManager = new SyncManager({\n displayId: config.hardwareKey,\n syncConfig,\n onLayoutChange: async (layoutId: string) => {\n // Wall mode: map lead's layout to this display's position-specific layout\n const layoutMap = syncConfig.layoutMap || config.sync?.layoutMap;\n const mappedId = layoutMap?.[layoutId] ?? layoutId;\n if (mappedId !== layoutId) {\n log.info(`[Sync] Wall mode: lead layout ${layoutId} → local layout ${mappedId}`);\n }\n // Follower: preload layout hidden, show comes from onLayoutShow\n log.info(`[Sync] Preparing layout ${mappedId} (waiting for show signal)`);\n await this.prepareLayout(parseInt(String(mappedId), 10));\n // Report ready to lead (use lead's layoutId so lead can track readiness)\n this.syncManager?.reportReady(layoutId);\n },\n onLayoutShow: (layoutId: string) => {\n // Map lead's layout ID to this display's layout (wall mode)\n const layoutMap = syncConfig.layoutMap || config.sync?.layoutMap;\n const mappedId = layoutMap?.[layoutId] ?? layoutId;\n const numericId = parseInt(String(mappedId), 10);\n\n // Compute choreography stagger delay (0 if no choreography configured)\n const choreo = syncConfig.choreography || 'simultaneous';\n const staggerMs = syncConfig.staggerMs ?? 150;\n\n // Build stagger options: prefer 2D topology, fall back to 1D position\n const staggerOpts: any = { choreography: choreo, staggerMs };\n if (syncConfig.topology) {\n staggerOpts.topology = syncConfig.topology;\n staggerOpts.gridCols = syncConfig.gridCols ?? 1;\n staggerOpts.gridRows = syncConfig.gridRows ?? 1;\n } else {\n staggerOpts.position = syncConfig.position ?? 0;\n staggerOpts.totalDisplays = syncConfig.totalDisplays ?? 1;\n }\n const stagger = computeStagger(staggerOpts);\n\n if (stagger > 0) {\n log.info(`[Sync] Show layout ${numericId} with ${stagger}ms choreography delay (${choreo})`);\n setTimeout(() => this.renderer.showLayout(numericId), stagger);\n } else {\n log.info(`[Sync] Show layout ${numericId}`);\n this.renderer.showLayout(numericId);\n }\n },\n onVideoStart: (layoutId: string, regionId: string) => {\n // Resume paused video in the specified region\n log.info(`[Sync] Video start: layout ${layoutId} region ${regionId}`);\n this.renderer.resumeRegionMedia?.(regionId);\n },\n // Lead: follower delegated stats — submit on their behalf\n onStatsReport: async (followerId: string, statsXml: string, ack: () => void) => {\n log.info(`[Sync] Submitting stats for follower ${followerId}`);\n try {\n const success = await this.xmds.submitStats(statsXml, followerId);\n if (success) ack();\n } catch (err: any) {\n log.warn(`[Sync] Stats submission failed for follower ${followerId}:`, err);\n }\n },\n // Lead: follower delegated logs — submit on their behalf\n onLogsReport: async (followerId: string, logsXml: string, ack: () => void) => {\n log.info(`[Sync] Submitting logs for follower ${followerId}`);\n try {\n const success = await this.xmds.submitLog(logsXml, followerId);\n if (success) ack();\n } catch (err: any) {\n log.warn(`[Sync] Log submission failed for follower ${followerId}:`, err);\n }\n },\n // Follower: lead confirmed our stats were submitted\n onStatsAck: async (_displayId: string) => {\n log.info('[Sync] Lead confirmed stats submission');\n if (this._pendingFollowerStats && this.statsCollector) {\n await this.statsCollector.clearSubmittedStats(this._pendingFollowerStats);\n this._pendingFollowerStats = null;\n }\n },\n // Follower: lead confirmed our logs were submitted\n onLogsAck: async (_displayId: string) => {\n log.info('[Sync] Lead confirmed logs submission');\n if (this._pendingFollowerLogs && this.logReporter) {\n await this.logReporter.clearSubmittedLogs(this._pendingFollowerLogs);\n this._pendingFollowerLogs = null;\n }\n },\n // Relay: group membership changed (auto-detect totalDisplays)\n onGroupUpdate: (totalDisplays: number, topology: Record<string, any>) => {\n log.info(`[Sync] Group update: ${totalDisplays} displays, topology: ${JSON.stringify(topology)}`);\n syncConfig.totalDisplays = totalDisplays;\n },\n });\n this.core.setSyncManager(this.syncManager);\n this.syncManager.start();\n log.info(`[Sync] SyncManager started as ${syncConfig.isLead ? 'LEAD' : 'FOLLOWER'}`);\n this.updateConfigDisplay();\n });\n }\n\n /**\n * Setup download and cache event handlers.\n * Handles FILES_RECEIVED, DOWNLOAD_REQUEST, PURGE_REQUEST, PURGE_ALL_REQUEST.\n */\n private setupDownloadEventHandlers() {\n this.core.on(E.FILES_RECEIVED, (files: any[]) => {\n this.updateStatus(`Downloading ${files.length} files...`);\n });\n\n this.core.on(E.DOWNLOAD_REQUEST, async (groupedFiles: any) => {\n // Download orchestration runs in main thread — no SW messaging\n this.downloadOverlay?.startUpdating();\n try {\n // Push current JWT token to proxy for cache-through CMS requests\n const token = this.xmds?.getToken?.() || null;\n if (token) {\n await fetch('/auth-token', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ token }),\n });\n }\n await this.enqueueDownloads(groupedFiles);\n log.info('Download enqueue complete');\n } catch (error) {\n log.error('Download request failed:', error);\n this.updateStatus('Download failed: ' + error, 'error');\n }\n });\n\n this.core.on(E.PURGE_REQUEST, async (purgeFiles: any[]) => {\n try {\n const result = await store.remove(purgeFiles);\n log.info(`Purge complete: ${result.deleted}/${result.total} files deleted`);\n } catch (error) {\n log.warn('Purge failed:', error);\n }\n });\n\n this.core.on(E.PURGE_ALL_REQUEST, async () => {\n log.info('Purging all cached content...');\n this.updateStatus('Purging cache...');\n try {\n // Delete all files from ContentStore\n const allFiles = await store.list();\n if (allFiles.length > 0) {\n const result = await store.remove(allFiles);\n log.info(`Purged ${result.deleted} files from ContentStore`);\n }\n // Clean up any legacy Cache API caches (pre-ContentStore migration)\n const cacheNames = await caches.keys();\n if (cacheNames.length > 0) {\n await Promise.all(cacheNames.map(name => caches.delete(name)));\n log.info(`Purged ${cacheNames.length} legacy caches`);\n }\n } catch (error) {\n log.error('Cache purge failed:', error);\n }\n });\n }\n\n /**\n * Setup command execution event handlers.\n * Handles EXECUTE_NATIVE_COMMAND, COMMAND_RESULT, SCHEDULED_COMMAND.\n */\n private setupCommandEventHandlers() {\n // Native command execution (#202) — shell commands delegated by PlayerCore\n // Electron: use IPC (in-process, faster). Chromium/other: HTTP to proxy server.\n this.core.on(E.EXECUTE_NATIVE_COMMAND, async (data: any) => {\n let result;\n if ((window as any).electronAPI?.executeShellCommand) {\n result = await (window as any).electronAPI.executeShellCommand({\n commandString: data.commandString,\n });\n } else {\n try {\n const resp = await fetch('/shell-command', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ commandString: data.commandString }),\n });\n result = await resp.json();\n } catch (err: any) {\n result = { success: false, reason: err.message };\n }\n }\n this.core.emit(E.COMMAND_RESULT, { code: data.code, ...result });\n });\n\n // Command execution result\n this.core.on(E.COMMAND_RESULT, (result: any) => {\n log.info('Command result:', result);\n if (!result.success) {\n this.reportFault('COMMAND_FAILED', `Command ${result.code} failed: ${result.reason || 'unknown'}`);\n }\n });\n\n // Scheduled commands (#17) — execute commands whose scheduled time has arrived\n this.core.on(E.SCHEDULED_COMMAND, (command: any) => {\n log.info(`Scheduled command: ${command.code}`);\n this.core.executeCommand(command.code);\n });\n }\n\n\n /**\n * Setup Interactive Control handler (receives messages from SW for widget IC requests)\n * IC library in widget iframes makes XHR to /player/pwa/ic/*, SW forwards here.\n */\n private setupInteractiveControl() {\n this._swIcHandler = (event: any) => {\n if (event.data?.type !== 'INTERACTIVE_CONTROL') return;\n\n const { method, path, search, body } = event.data;\n const port = event.ports?.[0];\n if (!port) return;\n\n const response = this.handleInteractiveControl(method, path, search, body);\n port.postMessage(response);\n };\n navigator.serviceWorker?.addEventListener('message', this._swIcHandler);\n }\n\n /**\n * Notify widget iframes when DataConnector data changes.\n * XIC library listens for postMessage { ctrl: 'rtNotifyData', data: { dataKey } }\n * and calls the widget's registered notifyData callback.\n */\n private setupDataConnectorNotify() {\n const dcManager = this.core.getDataConnectorManager();\n dcManager.on('data-changed', (dataKey: string) => {\n const iframes = document.querySelectorAll<HTMLIFrameElement>('iframe');\n const message = { ctrl: 'rtNotifyData', data: { dataKey } };\n for (const iframe of iframes) {\n try {\n iframe.contentWindow?.postMessage(message, '*');\n } catch { /* cross-origin iframe, ignore */ }\n }\n });\n }\n\n /**\n * Setup keyboard and presenter remote controls.\n * Handles arrow keys, page up/down, space for next/prev/pause,\n * and MediaSession API for multimedia keyboard keys.\n */\n private setupRemoteControls() {\n // Keep focus on main document so keyboard shortcuts work even with widget iframes.\n // Iframes steal focus — this pulls it back after a short delay so interactive\n // widgets still work momentarily but keyboard control returns to the player.\n window.addEventListener('blur', () => {\n // Don't steal focus when setup overlay is open (user is typing in iframe inputs)\n if (this.setupOverlay?.isVisible()) return;\n setTimeout(() => window.focus(), 200);\n });\n\n // Forward keyboard events from widget iframes to the main document.\n // Iframes have their own document, so keydown on the parent never fires\n // when an iframe has focus. We observe new iframes and attach forwarders.\n const attachIframeKeyForwarder = (iframe: HTMLIFrameElement) => {\n const tryAttach = () => {\n try {\n const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;\n if (!iframeDoc) return;\n if ((iframe as any).__keyForwarderAttached) return;\n (iframe as any).__keyForwarderAttached = true;\n iframeDoc.addEventListener('keydown', (e: KeyboardEvent) => {\n // Don't forward keys from setup overlay — user is typing in form inputs\n if (this.setupOverlay?.isVisible()) return;\n // Re-dispatch on the main document so our handler fires\n const clone = new KeyboardEvent('keydown', {\n key: e.key, code: e.code, keyCode: e.keyCode,\n ctrlKey: e.ctrlKey, shiftKey: e.shiftKey, altKey: e.altKey, metaKey: e.metaKey,\n bubbles: true, cancelable: true,\n });\n if (document.dispatchEvent(clone)) return; // not prevented\n e.preventDefault();\n });\n } catch { /* cross-origin iframe, ignore */ }\n };\n iframe.addEventListener('load', tryAttach);\n tryAttach();\n };\n\n // Attach to existing and future iframes\n Array.from(document.querySelectorAll('iframe')).forEach(f => attachIframeKeyForwarder(f as HTMLIFrameElement));\n this._iframeObserver = new MutationObserver((mutations) => {\n for (const m of mutations) {\n for (const node of m.addedNodes) {\n if (node instanceof HTMLIFrameElement) attachIframeKeyForwarder(node);\n if (node instanceof HTMLElement) {\n node.querySelectorAll('iframe').forEach(f => attachIframeKeyForwarder(f as HTMLIFrameElement));\n }\n }\n }\n });\n this._iframeObserver.observe(document.body, { childList: true, subtree: true });\n\n // Read control toggles from config (injected by proxy into localStorage)\n const controls = this.getControls();\n const { keyboard: kb = {} } = controls;\n const debugOverlays = kb.debugOverlays === true;\n const setupKey = kb.setupKey === true;\n const playbackControl = kb.playbackControl === true;\n const videoControls = kb.videoControls === true;\n\n // Keyboard / presenter remote (clicker) controls\n document.addEventListener('keydown', (e: KeyboardEvent) => {\n // Ctrl+Q — quit (Chromium kiosk: calls server /quit; Electron: handled by menu accelerator)\n if (e.key === 'q' && (e.ctrlKey || e.metaKey)) {\n e.preventDefault();\n log.info('[Remote] Quit requested (Ctrl+Q)');\n fetch('/quit', { method: 'POST' }).catch(() => {});\n return;\n }\n\n switch (e.key) {\n case 't':\n case 'T':\n if (!debugOverlays) break;\n if (!this.timelineOverlay) {\n this.timelineOverlay = new TimelineOverlay(true, (layoutId) => this.skipToLayout(layoutId));\n }\n this.timelineOverlay.toggle();\n break;\n case 'd':\n case 'D':\n if (!debugOverlays) break;\n if (!this.downloadOverlay) {\n this.downloadOverlay = new DownloadOverlay({ enabled: true, autoHide: false });\n this.downloadOverlay.setProgressCallback(() => downloadManager.getProgress());\n }\n this.downloadOverlay.toggle();\n break;\n case 'v':\n case 'V': {\n if (!videoControls) break;\n // Collect videos from parent + all same-origin iframes (widget regions)\n const allVideos: HTMLVideoElement[] = [...document.querySelectorAll<HTMLVideoElement>('video')];\n document.querySelectorAll<HTMLIFrameElement>('iframe').forEach(iframe => {\n try { allVideos.push(...iframe.contentDocument!.querySelectorAll<HTMLVideoElement>('video')); } catch {}\n });\n const show = allVideos.length > 0 && !allVideos[0].controls;\n allVideos.forEach(v => v.controls = show);\n break;\n }\n // Playback control: next/prev/pause\n case 'ArrowRight':\n case 'PageDown':\n if (!playbackControl) break;\n log.info('[Remote] Next layout (keyboard)');\n this.core.advanceToNextLayout();\n e.preventDefault();\n break;\n case 'ArrowLeft':\n case 'PageUp':\n if (!playbackControl) break;\n log.info('[Remote] Previous layout (keyboard)');\n this.core.advanceToPreviousLayout();\n e.preventDefault();\n break;\n case ' ':\n if (!playbackControl) break;\n log.info('[Remote] Toggle pause (keyboard)');\n if (this.renderer.isPaused()) {\n this.renderer.resume();\n } else {\n this.renderer.pause();\n }\n e.preventDefault();\n break;\n case 'r':\n case 'R':\n if (!playbackControl) break;\n if (this.core.isLayoutOverridden()) {\n log.info('[Remote] Revert to schedule (keyboard)');\n this.core.revertToSchedule();\n }\n break;\n case 's':\n case 'S':\n if (!setupKey) break;\n if (!this.setupOverlay) {\n this.setupOverlay = new SetupOverlay();\n }\n this.setupOverlay.toggle();\n e.preventDefault(); // prevent 's' from being typed into the focused input\n break;\n }\n });\n\n // MediaSession API for multimedia keys (only fires when media is active)\n if (playbackControl && 'mediaSession' in navigator) {\n navigator.mediaSession.setActionHandler('nexttrack', () => {\n log.info('[Remote] Next layout (MediaSession)');\n this.core.advanceToNextLayout();\n });\n navigator.mediaSession.setActionHandler('previoustrack', () => {\n log.info('[Remote] Previous layout (MediaSession)');\n this.core.advanceToPreviousLayout();\n });\n navigator.mediaSession.setActionHandler('pause', () => {\n log.info('[Remote] Pause (MediaSession)');\n this.renderer.pause();\n });\n navigator.mediaSession.setActionHandler('play', () => {\n log.info('[Remote] Resume (MediaSession)');\n this.renderer.resume();\n });\n }\n\n log.info('Remote controls initialized (keyboard + MediaSession)');\n }\n\n /** Read controls config (injected by proxy from config.json into localStorage). */\n private getControls(): Record<string, any> {\n return config.controls;\n }\n\n /**\n * Skip to a specific layout by ID (from timeline click or XMR command).\n * Uses changeLayout() which sets a layout override — press R to revert to schedule.\n */\n private skipToLayout(layoutId: number) {\n log.info(`Skipping to layout ${layoutId} (timeline click)`);\n this.core.changeLayout(layoutId);\n }\n\n private parseBody(body: string | null): any {\n try { return body ? JSON.parse(body) : {}; } catch (_) { return {}; }\n }\n\n /**\n * Handle an Interactive Control request from a widget\n */\n private handleInteractiveControl(method: string, path: string, search: string, body: string | null): any {\n log.debug('IC request:', method, path, search);\n\n switch (path) {\n case '/info':\n return {\n status: 200,\n body: JSON.stringify({\n hardwareKey: config.hardwareKey,\n displayName: config.displayName,\n playerType: 'pwa',\n currentLayoutId: this.core.getCurrentLayoutId()\n })\n };\n\n case '/trigger': {\n const data = this.parseBody(body);\n // Forward to renderer for layout-level actions (widget navigation)\n this.renderer.emit('interactiveTrigger', {\n targetId: data.id,\n triggerCode: data.trigger\n });\n // Forward to core for schedule-level actions (layout navigation)\n if (data.trigger) {\n this.core.handleTrigger(data.trigger);\n }\n return { status: 200, body: 'OK' };\n }\n\n case '/duration/expire': {\n const data = this.parseBody(body);\n log.info('IC: Widget duration expire requested for', data.id);\n this.renderer.emit('widgetExpire', { widgetId: data.id });\n return { status: 200, body: 'OK' };\n }\n\n case '/duration/extend': {\n const data = this.parseBody(body);\n log.info('IC: Widget duration extend by', data.duration, 'for', data.id);\n this.renderer.emit('widgetExtendDuration', {\n widgetId: data.id,\n duration: parseInt(data.duration)\n });\n return { status: 200, body: 'OK' };\n }\n\n case '/duration/set': {\n const data = this.parseBody(body);\n log.info('IC: Widget duration set to', data.duration, 'for', data.id);\n this.renderer.emit('widgetSetDuration', {\n widgetId: data.id,\n duration: parseInt(data.duration)\n });\n return { status: 200, body: 'OK' };\n }\n\n case '/fault': {\n const data = this.parseBody(body);\n this.reportFault(data.code || 'WIDGET_FAULT', data.reason || 'Widget reported fault', {\n layoutId: data.layoutId,\n regionId: data.regionId,\n widgetId: data.widgetId\n });\n return { status: 200, body: 'OK' };\n }\n\n case '/realtime': {\n const params = new URLSearchParams(search);\n const dataKey = params.get('dataKey');\n log.debug('IC: Realtime data request for key:', dataKey);\n\n if (!dataKey) {\n return { status: 400, body: JSON.stringify({ error: 'Missing dataKey parameter' }) };\n }\n\n const dcManager = this.core.getDataConnectorManager();\n const connectorData = dcManager.getData(dataKey);\n\n if (connectorData === null) {\n return { status: 404, body: JSON.stringify({ error: `No data available for key: ${dataKey}` }) };\n }\n\n const responseBody = typeof connectorData === 'string' ? connectorData : JSON.stringify(connectorData);\n return { status: 200, body: responseBody };\n }\n\n case '/criteria': {\n // Return display properties/criteria that widgets can query\n // Used by widgets to adapt content based on display characteristics\n return {\n status: 200,\n body: JSON.stringify({\n displayId: config.displayId,\n hardwareKey: config.hardwareKey,\n displayName: config.displayName,\n width: window.innerWidth,\n height: window.innerHeight,\n latitude: config.latitude || null,\n longitude: config.longitude || null,\n playerType: 'pwa'\n })\n };\n }\n\n default:\n return { status: 404, body: JSON.stringify({ error: 'Unknown IC route' }) };\n }\n }\n\n /**\n * Notify PlayerCore that a file download completed.\n * Called directly from enqueueDownloads() — no SW messaging needed.\n */\n private notifyFileCached(fileId: string, fileType: string) {\n log.debug(`Download complete: ${fileType}/${fileId}`);\n\n if (fileType === 'layout') {\n this.core.notifyMediaReady(parseInt(fileId), fileType);\n } else if (fileType === 'media') {\n // Pass saveAs string for media files (matches pendingLayouts entries)\n const saveAs = this._fileIdToSaveAs.get(fileId) || fileId;\n this._cachedMediaKeys.add(saveAs);\n this.core.notifyMediaReady(saveAs, fileType);\n } else {\n // Dependencies, widgets, datasets — track by storeKey\n this._cachedMediaKeys.add(fileId);\n }\n\n // Debounced duration probe — run after downloads settle\n if (this._probeTimer) clearTimeout(this._probeTimer);\n this._probeTimer = setTimeout(() => {\n this._probeTimer = null;\n this.probeLayoutDurations().catch(() => {});\n }, 3000);\n\n // Debounced media status check — update timeline missing-media annotations\n if (this._mediaStatusTimer) clearTimeout(this._mediaStatusTimer);\n this._mediaStatusTimer = setTimeout(() => {\n this._mediaStatusTimer = null;\n this.checkTimelineMediaStatus().catch(() => {});\n }, 2000);\n }\n\n /**\n * Enqueue files for download — runs in main thread, no SW messaging.\n * Ported from MessageHandler.handleDownloadFiles() with direct callbacks.\n */\n private async enqueueDownloads(data: any) {\n const { extractMediaIdsFromXlf } = await import('@xiboplayer/sw');\n const { layoutOrder, files, layoutDependants } = data;\n // Use DownloadManager facade methods (not direct queue access)\n\n /** Store key = URL path without leading / and query params */\n const storeKeyFrom = (f: any) => (f.path || '').split('?')[0].replace(/^\\/+/, '') || `${f.type || 'media'}/${f.id}`;\n\n // Build fileId→saveAs map from CMS RequiredFiles data\n for (const f of files) {\n if (f.saveAs) {\n this._fileIdToSaveAs.set(String(f.id), f.saveAs);\n }\n }\n // Build lookup maps from flat CMS file list\n const xlfFiles = new Map();\n const resources: any[] = [];\n const mediaFiles = new Map();\n const idToKeys = new Map();\n for (const f of files) {\n if (f.type === 'layout') {\n xlfFiles.set(parseInt(f.id), f);\n } else if (f.type === 'static') {\n resources.push(f);\n } else {\n const key = `${f.type}:${f.id}`;\n mediaFiles.set(key, f);\n const bareId = String(f.id);\n if (!idToKeys.has(bareId)) idToKeys.set(bareId, []);\n idToKeys.get(bareId).push(key);\n }\n }\n\n log.info(`Download: ${layoutOrder.length} layouts, ${mediaFiles.size} media, ${resources.length} resources`);\n\n // ── Step 1: Fetch + parse all XLFs (cache-through handles store/CMS) ──\n const layoutMediaMap = new Map();\n const allXlfIds = [...layoutOrder, ...[...xlfFiles.keys()].filter((id: number) => !layoutOrder.includes(id))];\n const xlfPromises = allXlfIds.map(async (layoutId: number) => {\n const xlfFile = xlfFiles.get(layoutId);\n if (!xlfFile?.path) return;\n\n let xlfText: string | undefined;\n\n // Try store first, then cache-through fetches from CMS on miss\n try {\n const headers: Record<string, string> = {};\n if (xlfFile.cmsDownloadUrl) headers['X-Cms-Download-Url'] = xlfFile.cmsDownloadUrl;\n const resp = await fetch(xlfFile.path, Object.keys(headers).length ? { headers } : undefined);\n if (resp.ok) {\n xlfText = await resp.text();\n log.info(`Fetched XLF ${layoutId} (${xlfText.length} bytes)`);\n // Store XLF in content store so prepareLayout() can find it via store.get()\n await store.put(`${STORE_PREFIX}/layouts`, String(layoutId), new Blob([xlfText], { type: 'text/xml' }));\n this.notifyFileCached(String(layoutId), 'layout');\n }\n } catch (_) {}\n\n if (xlfText) {\n layoutMediaMap.set(layoutId, extractMediaIdsFromXlf(xlfText, log));\n }\n });\n await Promise.allSettled(xlfPromises);\n log.info(`Parsed ${layoutMediaMap.size} XLFs`);\n\n // Helper: enqueue a file, attach completion callback\n const enqueueFile = async (builder: any, file: any): Promise<boolean> => {\n if (!file.path || file.path === 'null' || file.path === 'undefined') return false;\n\n const storeKey = storeKeyFrom(file);\n\n // Check if already stored on disk (200 = cached, 204 = not in store)\n try {\n const headResp = await fetch(`/store/${storeKey}`, { method: 'HEAD' });\n if (headResp.status === 200) return false;\n } catch (_) {}\n\n // Check if already downloading (download manager keys are type/id, not URL paths)\n const dmKey = `${file.type}/${file.id}`;\n if (downloadManager.getTask(dmKey)) return false;\n\n // Check for existing chunks — skip already-downloaded ones\n try {\n const mcResp = await fetch(`/store/missing-chunks/${storeKey}`);\n if (mcResp.ok) {\n const { missing, numChunks } = await mcResp.json();\n if (numChunks > 0 && missing.length < numChunks) {\n const existing = new Set<number>();\n for (let i = 0; i < numChunks; i++) {\n if (!missing.includes(i)) existing.add(i);\n }\n file.skipChunks = existing;\n log.info(`Resuming ${storeKey}: ${existing.size}/${numChunks} chunks cached, ${missing.length} to download`);\n }\n }\n } catch (_) {}\n\n const fileDownload = builder.addFile(file);\n if (fileDownload.state !== 'pending') return false;\n\n // Direct callback — no postMessage needed\n fileDownload.wait().then((blob: any) => {\n const fileSize = parseInt(file.size) || blob.size;\n log.info('Download complete:', storeKey, `(${fileSize} bytes)`);\n\n // Mark chunked files as complete\n if (fileSize > this._chunkConfig.chunkSize) {\n fetch('/store/mark-complete', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ storeKey }),\n }).catch((e: any) => log.warn('mark-complete failed:', storeKey, e.message));\n }\n\n this.notifyFileCached(String(file.id), file.type);\n downloadManager.removeCompleted(dmKey);\n }).catch((err: any) => {\n log.error('Download failed:', file.id, err);\n downloadManager.removeCompleted(dmKey);\n });\n return true;\n };\n\n // ── Step 2: Enqueue resources (parallel HEAD checks) ──\n const resourceBuilder = downloadManager.createTaskBuilder();\n await Promise.all(resources.map(file => enqueueFile(resourceBuilder, file)));\n const resourceTasks = await resourceBuilder.build();\n if (resourceTasks.length > 0) {\n resourceTasks.push(BARRIER);\n downloadManager.enqueueOrderedTasks(resourceTasks);\n }\n\n // ── Step 3: For each layout in play order, merge XLF + dependants ──\n const claimed = new Set();\n const nonScheduledIds = [...layoutMediaMap.keys()].filter((id: number) => !layoutOrder.includes(id));\n const filenameToMediaId = new Map();\n for (const [key, file] of mediaFiles) {\n if (file.saveAs) filenameToMediaId.set(file.saveAs, key);\n }\n\n const depMap = new Map();\n if (layoutDependants) {\n for (const [id, filenames] of Object.entries(layoutDependants)) {\n depMap.set(parseInt(id, 10), filenames);\n }\n }\n\n for (const layoutId of layoutOrder) {\n const xlfMediaIds = layoutMediaMap.get(layoutId);\n if (!xlfMediaIds) continue;\n\n const bareIds = new Set(xlfMediaIds);\n for (const nsId of nonScheduledIds) {\n const nsMediaIds = layoutMediaMap.get(nsId);\n if (nsMediaIds) {\n for (const id of nsMediaIds) bareIds.add(id);\n }\n }\n const deps = depMap.get(layoutId) || [];\n for (const filename of deps) {\n const key = filenameToMediaId.get(filename);\n if (key) bareIds.add(key);\n }\n\n const matched: any[] = [];\n for (const bareId of bareIds) {\n if (mediaFiles.has(bareId) && !claimed.has(bareId)) {\n matched.push(mediaFiles.get(bareId));\n claimed.add(bareId);\n continue;\n }\n const keys = idToKeys.get(String(bareId)) || [];\n for (const key of keys) {\n if (claimed.has(key)) continue;\n matched.push(mediaFiles.get(key));\n claimed.add(key);\n }\n }\n if (matched.length === 0) continue;\n\n log.info(`Layout ${layoutId}: ${matched.length} media`);\n matched.sort((a: any, b: any) => (a.size || 0) - (b.size || 0));\n const builder = downloadManager.createTaskBuilder();\n await Promise.all(matched.map(file => enqueueFile(builder, file)));\n const orderedTasks = await builder.build();\n if (orderedTasks.length > 0) {\n orderedTasks.push(BARRIER);\n downloadManager.enqueueOrderedTasks(orderedTasks);\n }\n }\n\n // Enqueue unclaimed media\n const unclaimed = [...mediaFiles.keys()].filter((id: string) => !claimed.has(id));\n if (unclaimed.length > 0) {\n log.info(`${unclaimed.length} media not in any XLF`);\n const builder = downloadManager.createTaskBuilder();\n await Promise.all(unclaimed.map(id => {\n const file = mediaFiles.get(id);\n return file ? enqueueFile(builder, file) : Promise.resolve(false);\n }));\n const orderedTasks = await builder.build();\n if (orderedTasks.length > 0) {\n downloadManager.enqueueOrderedTasks(orderedTasks);\n }\n }\n\n log.info('Downloads active:', downloadManager.running, ', queued:', downloadManager.queued);\n }\n\n /**\n * Setup renderer event handlers\n */\n private setupRendererEventHandlers() {\n this.renderer.on('layoutStart', (layoutId: number, _layout: any) => {\n log.info('Layout started:', layoutId);\n this.updateStatus(`Playing layout ${layoutId}`);\n\n this.core.setCurrentLayout(layoutId);\n\n // Store layout-level enableStat for use in layoutEnd\n this._currentLayoutEnableStat = _layout?.enableStat !== false;\n\n // Update timeline overlay with current layout's known duration\n const layoutDur = this.core.getLayoutDuration(layoutId) || _layout?.duration;\n this.timelineOverlay?.update(null, layoutId, layoutDur);\n\n // Track stats: start layout (only if enableStat is not disabled)\n if (this.statsCollector && this._currentLayoutEnableStat) {\n this.statsCollector.startLayout(layoutId, this.currentScheduleId).catch((err: any) => {\n log.error('Failed to start layout stat:', err);\n });\n }\n });\n\n this.renderer.on('layoutEnd', (layoutId: number) => {\n log.info('Layout ended:', layoutId);\n\n // Record play at END so maxPlaysPerHour doesn't interrupt the current play.\n // Previously recorded at layoutStart, which caused periodic collections to\n // filter the layout mid-playback (e.g., 200s video cut at 168s).\n scheduleManager?.recordPlay(layoutId.toString());\n\n // Track stats: end layout (only if enableStat was not disabled)\n if (this.statsCollector && this._currentLayoutEnableStat) {\n this.statsCollector.endLayout(layoutId, this.currentScheduleId).catch((err: any) => {\n log.error('Failed to end layout stat:', err);\n });\n }\n\n // If a new layout is already rendering or being prepared (async fetch),\n // skip advance — the transition was already handled by the caller.\n // Stats/play recording above still run for proper tracking.\n if (this.renderer.getCurrentLayoutId() && this.renderer.getCurrentLayoutId() !== layoutId) {\n log.debug(`Layout ${layoutId} ended but ${this.renderer.getCurrentLayoutId()} already playing, skipping advance`);\n return;\n }\n if (this.preparingLayoutId && this.preparingLayoutId !== layoutId) {\n log.debug(`Layout ${layoutId} ended but ${this.preparingLayoutId} being prepared, skipping advance`);\n return;\n }\n\n // Report to CMS\n this.core.notifyLayoutStatus(layoutId);\n\n // Clear current layout to allow replay/advance\n this.core.clearCurrentLayout();\n\n // If a new layout is already pending download, don't advance\n // (avoids redundant XMDS calls and duplicate download requests)\n const pending = this.core.getPendingLayouts();\n if (pending.length > 0) {\n log.info(`Layout ${pending[0]} pending download, skipping advance`);\n return;\n }\n\n // Advance to the next layout in the schedule (round-robin cycling)\n // This avoids a full collect() cycle — just picks the next layout and renders it.\n // Periodic collect() cycles still run on the collection interval to sync with CMS.\n log.info('Layout cycle completed, advancing to next layout...');\n this.core.advanceToNextLayout();\n });\n\n this.renderer.on('widgetStart', (data: any) => {\n const { widgetId, layoutId, mediaId } = data;\n log.debug('Widget started:', data.type, widgetId, 'media:', mediaId);\n\n // Track stats: start widget/media (only if enableStat is not disabled)\n if (this.statsCollector && mediaId && data.enableStat !== false) {\n this.statsCollector.startWidget(mediaId, layoutId, this.currentScheduleId).catch((err: any) => {\n log.error('Failed to start widget stat:', err);\n });\n }\n });\n\n this.renderer.on('widgetEnd', (data: any) => {\n const { widgetId, layoutId, mediaId } = data;\n log.debug('Widget ended:', data.type, widgetId, 'media:', mediaId);\n\n // Track stats: end widget/media (only if enableStat is not disabled)\n if (this.statsCollector && mediaId && data.enableStat !== false) {\n this.statsCollector.endWidget(mediaId, layoutId, this.currentScheduleId).catch((err: any) => {\n log.error('Failed to end widget stat:', err);\n });\n }\n });\n\n // Widget commands (#202) — execute commands embedded in layout widgets\n this.renderer.on('widgetCommand', (data: any) => {\n log.info('Widget command:', data.commandCode);\n const commands = { [data.commandCode]: { commandString: data.commandString } };\n this.core.executeCommand(data.commandCode, commands);\n });\n\n this.renderer.on('error', (error: any) => {\n log.error('Renderer error:', error);\n this.updateStatus(`Error: ${error.type}`, 'error');\n\n // Report fault to CMS (triggers dashboard alert)\n this.reportFault(error.type || 'RENDERER_ERROR', `Renderer error: ${error.message || error.type}`, {\n layoutId: error.layoutId,\n regionId: error.regionId,\n widgetId: error.widgetId\n });\n });\n\n // Handle interactive actions from touch/click and keyboard triggers\n this.renderer.on('action-trigger', (data: any) => {\n const { actionType, triggerCode, layoutCode, targetId, commandCode } = data;\n log.info('Action trigger:', actionType, data);\n\n switch (actionType) {\n case 'navLayout':\n case 'navigateToLayout':\n if (triggerCode) {\n this.core.handleTrigger(triggerCode);\n } else if (layoutCode) {\n this.core.changeLayout(layoutCode);\n }\n break;\n\n case 'navWidget':\n case 'navigateToWidget':\n if (triggerCode) {\n this.core.handleTrigger(triggerCode);\n } else if (targetId) {\n this.renderer.navigateToWidget(targetId);\n }\n break;\n\n case 'previousWidget':\n this.renderer.previousWidget(data.source?.regionId);\n break;\n\n case 'nextWidget':\n this.renderer.nextWidget(data.source?.regionId);\n break;\n\n case 'command':\n if (commandCode) {\n this.core.executeCommand(commandCode);\n }\n break;\n\n default:\n log.warn('Unknown action type:', actionType);\n }\n\n // Record interaction event for proof of play (#19)\n if (this.statsCollector) {\n this.statsCollector.recordEvent('touch', this.core.getCurrentLayoutId(), data.targetId || null, this.currentScheduleId);\n }\n });\n\n // Widget duration webhooks (#16) — fire HTTP POST when widget duration expires\n this.renderer.on('widgetAction', (data: any) => {\n if (data.type === 'durationEnd' && data.url) {\n log.info(`Widget ${data.widgetId} duration ended, calling webhook: ${data.url}`);\n\n // Record webhook event for proof of play (#19)\n if (this.statsCollector) {\n this.statsCollector.recordEvent('webhook', data.layoutId, data.widgetId, this.currentScheduleId);\n }\n\n fetch(data.url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n widgetId: data.widgetId,\n layoutId: data.layoutId,\n regionId: data.regionId,\n event: 'durationEnd',\n timestamp: new Date().toISOString()\n })\n }).catch(err => log.warn('Webhook failed (non-critical):', err));\n }\n });\n\n // Correct timeline duration when video metadata reveals actual duration\n this.renderer.on('layoutDurationUpdated', (layoutId: number, duration: number, final: boolean) => {\n this.core.recordLayoutDuration(String(layoutId), duration, final);\n });\n\n // Handle next layout preload request from renderer\n // Fired at 75% of current layout duration to pre-build the next layout's DOM\n this.renderer.on('request-next-layout-preload', async () => {\n try {\n // Peek at the next layout without advancing the schedule index\n const next = this.core.peekNextLayout();\n if (!next) {\n log.debug('No next layout to preload (single layout schedule or same layout)');\n return;\n }\n\n const nextLayoutId = next.layoutId;\n\n // Skip if already preloaded or preload in-flight\n if (this.renderer.layoutPool.has(nextLayoutId)) {\n log.debug(`Layout ${nextLayoutId} already in preload pool`);\n return;\n }\n if ((this.renderer as any)._preloadingLayoutId === nextLayoutId) {\n log.debug(`Layout ${nextLayoutId} preload already in-flight`);\n return;\n }\n\n log.info(`Preloading next layout ${nextLayoutId}...`);\n\n // Get XLF from cache\n const xlfBlob = await store.get(`${STORE_PREFIX}/layouts`, nextLayoutId);\n if (!xlfBlob) {\n log.debug(`Layout ${nextLayoutId} XLF not cached, skipping preload`);\n return;\n }\n\n const xlfXml = await xlfBlob.text();\n const doc = new DOMParser().parseFromString(xlfXml, 'text/xml');\n\n // Check if all required media is cached\n const { allMedia: requiredMedia } = this.getMediaIds(doc);\n const allMediaCached = await this.checkAllMediaCached(requiredMedia);\n\n if (!allMediaCached) {\n log.debug(`Media not fully cached for layout ${nextLayoutId}, skipping preload`);\n return;\n }\n\n // Fetch widget HTML before preloading (same as prepareLayout)\n await this.fetchWidgetHtml(doc, nextLayoutId);\n\n // Preload the layout into the renderer's pool\n const success = await this.renderer.preloadLayout(xlfXml, nextLayoutId);\n if (success) {\n log.info(`Layout ${nextLayoutId} preloaded successfully`);\n } else {\n log.warn(`Layout ${nextLayoutId} preload failed (will fall back to normal render)`);\n }\n } catch (error) {\n log.warn('Layout preload failed (non-blocking):', error);\n // Non-blocking: preload failure is graceful, normal render path will be used\n }\n });\n\n // Handle video playback errors — re-download only missing chunks\n this.renderer.on('videoError', async ({ storedAs }: any) => {\n if (!storedAs) return;\n const storeKey = `${PLAYER_API.slice(1)}/media/file/${storedAs}`;\n try {\n const resp = await fetch(`/store/missing-chunks/${storeKey}`);\n const { missing } = await resp.json();\n if (missing.length === 0) {\n log.warn(`Video ${storedAs}: corrupt file (all chunks present), deleting for re-download`);\n await fetch('/store/delete', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ files: [{ key: storeKey }] }),\n });\n const layoutId = this.core.getCurrentLayoutId();\n if (layoutId) {\n this.core.setPendingLayout(layoutId, [storedAs]);\n }\n this.core.collectNow().catch((err: any) => {\n log.error(`Failed to trigger re-download for ${storedAs}:`, err.message);\n });\n return;\n }\n log.warn(`Video ${storedAs}: ${missing.length} missing chunks (${missing.join(', ')}), re-downloading`);\n\n // Unmark completion (keeps existing chunks on disk) so HEAD returns 404\n await fetch('/store/unmark-complete', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ storeKey }),\n });\n\n // Trigger collection — enqueueFile will populate skipChunks for existing chunks\n this.core.collectNow().catch((err: any) => {\n log.error(`Failed to trigger re-download for ${storedAs}:`, err.message);\n });\n } catch (err: any) {\n log.error(`Failed to check/re-download ${storedAs}:`, err.message);\n }\n });\n }\n\n /**\n * Prepare and render layout (Platform-specific logic)\n */\n private async prepareLayout(layoutId: number) {\n // Same layout replay — use renderer's built-in replay path which\n // re-emits layoutStart, restarts timer and widget cycling.\n if (this.renderer.getCurrentLayoutId() === layoutId) {\n log.debug(`Layout ${layoutId} replay`);\n this.core.clearPreparingLayout();\n // Renderer's same-layout replay path reuses existing DOM — XLF not re-parsed\n await this.renderer.renderLayout('', layoutId);\n return;\n }\n\n // Guard: prevent concurrent preparations of the same layout.\n // Instead of dropping the event (which caused permanent stalls when the\n // first attempt failed due to a store race), schedule a retry after\n // the current preparation finishes.\n if (this.preparingLayoutId === layoutId) {\n log.debug(`Layout ${layoutId} preparation in progress, will retry after it completes`);\n this._pendingRetryLayoutId = layoutId;\n return;\n }\n\n this.preparingLayoutId = layoutId;\n try {\n // Get XLF from cache\n const xlfBlob = await store.get(`${STORE_PREFIX}/layouts`, layoutId);\n if (!xlfBlob) {\n log.info('Layout not in cache yet, marking as pending:', layoutId);\n // Mark layout as pending so when it downloads, we'll retry\n // Use layoutId as required file (will trigger on layout file cached)\n this.core.setPendingLayout(layoutId, [String(layoutId)]);\n this.updateStatus(`Downloading layout ${layoutId}...`);\n return;\n }\n\n const xlfXml = await xlfBlob.text();\n\n // Parse XLF once — reuse Document for media check and widget HTML fetch\n const xlfDoc = new DOMParser().parseFromString(xlfXml, 'text/xml');\n\n // Check if all required media is cached\n const { allMedia: requiredMedia } = this.getMediaIds(xlfDoc);\n const allMediaCached = await this.checkAllMediaCached(requiredMedia);\n\n if (!allMediaCached) {\n // Reorder download queue: current layout's media first, hold others.\n // All files (including all chunks) must complete before other layouts start.\n downloadManager.prioritizeLayoutFiles(requiredMedia.map(String));\n\n log.info(`Waiting for media to finish downloading for layout ${layoutId}`);\n this.updateStatus(`Preparing layout ${layoutId}...`);\n this.core.setPendingLayout(layoutId, requiredMedia);\n return; // Keep playing current layout until media is ready\n }\n\n // Fetch widget HTML (skip if already preloaded — was fetched during preload)\n if (!this.renderer.hasPreloadedLayout(layoutId)) {\n await this.fetchWidgetHtml(xlfDoc, layoutId);\n }\n\n // Preload layout into pool (hidden). Caller decides when to show.\n await this.renderer.preloadLayout(xlfXml, layoutId);\n\n // Clear pending status — layout prepared successfully, all media cached.\n // Without this, pendingLayouts keeps the layout marked as ⚠ missing\n // until it actually plays (which may be minutes later in the rotation).\n this.core.pendingLayouts.delete(layoutId);\n\n log.info(`Layout ${layoutId} ready`);\n\n } catch (error: any) {\n log.error('Failed to prepare layout:', layoutId, error);\n this.updateStatus(`Failed to load layout ${layoutId}`, 'error');\n\n // Report fault to CMS (triggers dashboard alert)\n this.reportFault('LAYOUT_LOAD_FAILED', `Failed to prepare layout ${layoutId}: ${error?.message || error}`, {\n layoutId\n });\n } finally {\n this.preparingLayoutId = null;\n this.core.clearPreparingLayout();\n\n // If another check-pending-layout arrived while we were preparing,\n // retry after a short delay to let the ContentStore settle.\n // This fixes the race where FILE_CACHED notification arrives before\n // the PUT to ContentStore is visible to HEAD requests.\n const retryId = this._pendingRetryLayoutId;\n this._pendingRetryLayoutId = null;\n if (retryId !== null && retryId !== undefined && this.core.getCurrentLayoutId() !== retryId) {\n log.debug(`Retrying preparation for layout ${retryId} after 500ms`);\n setTimeout(() => this.prepareLayout(retryId), 500);\n }\n }\n }\n\n /**\n * Get all required media saveAs filenames and video-specific ones from layout XLF.\n * Returns saveAs strings (via _fileIdToSaveAs map) for store key matching.\n */\n private getMediaIds(xlfXmlOrDoc: string | Document): { allMedia: string[]; videoMedia: string[] } {\n const doc = typeof xlfXmlOrDoc === 'string'\n ? new DOMParser().parseFromString(xlfXmlOrDoc, 'text/xml')\n : xlfXmlOrDoc;\n const allMedia: string[] = [];\n const videoMedia: string[] = [];\n\n doc.querySelectorAll('media[fileId]').forEach(el => {\n const fileId = el.getAttribute('fileId');\n if (fileId) {\n const saveAs = this._fileIdToSaveAs.get(fileId) || fileId;\n // Skip layout XLF references — stored in layouts/ store, not media/file/\n if (saveAs.endsWith('.xlf')) return;\n allMedia.push(saveAs);\n if (el.getAttribute('type') === 'video') {\n videoMedia.push(saveAs);\n }\n }\n });\n\n // Include background image file ID from layout element\n const bgFileId = doc.querySelector('layout')?.getAttribute('background');\n if (bgFileId) {\n const saveAs = this._fileIdToSaveAs.get(bgFileId) || bgFileId;\n if (!allMedia.includes(saveAs)) {\n allMedia.push(saveAs);\n }\n }\n\n return { allMedia, videoMedia };\n }\n\n /**\n * Check if all required media files are cached and ready.\n * Uses StoreClient.has() → HEAD /store${PLAYER_API}/media/:id to check ContentStore.\n */\n /**\n * Check if all required media files are cached and ready.\n * Uses storedAs filenames for store key matching: /media/file/{saveAs}\n */\n private async checkAllMediaCached(mediaSaveAs: string[]): Promise<boolean> {\n // Check in-memory set first — avoids all HEAD requests for known-cached files\n const unknown = mediaSaveAs.filter(s => !this._cachedMediaKeys.has(s));\n if (unknown.length === 0) return true;\n\n // HEAD-check all unknown files against the content store.\n // Always check the store directly — the download queue may have stale tasks\n // for files that are already cached (race between download completion and\n // task cleanup). The HEAD check is fast (<1ms for local store) and authoritative.\n const toCheck = unknown;\n\n const results = await Promise.all(\n toCheck.map(async (saveAs) => {\n try {\n const cached = await store.has(STORE_PREFIX, `media/file/${saveAs}`);\n if (cached) this._cachedMediaKeys.add(saveAs);\n return cached;\n } catch {\n log.warn(`Unable to verify media ${saveAs}, assuming cached (offline mode)`);\n return true;\n }\n })\n );\n const missing = toCheck.filter((_, i) => !results[i]);\n if (missing.length > 0) {\n log.debug(`Media not yet cached: ${missing.join(', ')}`);\n return false;\n }\n return true;\n }\n\n /**\n * Fetch widget HTML for all widgets in layout (parallel)\n */\n private async fetchWidgetHtml(xlfXmlOrDoc: string | Document, layoutId: number) {\n const doc = typeof xlfXmlOrDoc === 'string'\n ? new DOMParser().parseFromString(xlfXmlOrDoc, 'text/xml')\n : xlfXmlOrDoc;\n\n const fetchPromises: Promise<void>[] = [];\n\n for (const regionEl of doc.querySelectorAll('region')) {\n const regionId = regionEl.getAttribute('id');\n\n for (const mediaEl of regionEl.querySelectorAll('media')) {\n const type = mediaEl.getAttribute('type');\n const widgetId = mediaEl.getAttribute('id');\n const render = mediaEl.getAttribute('render');\n\n // XLF render=\"html\" means CMS provides pre-rendered HTML via getResource.\n // render=\"native\" means player handles the media directly (video, image, audio).\n if (render === 'html') {\n fetchPromises.push(\n (async () => {\n try {\n // Check ContentStore for existing widget HTML\n const storeId = `${layoutId}/${regionId}/${widgetId}`;\n let html: string | null = null;\n\n const existing = await store.get(`${STORE_PREFIX}/widgets`, storeId);\n if (existing) {\n html = await existing.text();\n log.debug(`Found cached widget HTML for ${type} ${widgetId}`);\n }\n\n if (!html) {\n html = await this.xmds.getResource(layoutId, regionId, widgetId);\n log.debug(`Retrieved widget HTML for ${type} ${widgetId} from CMS`);\n }\n // Always process: injects <base> tag, rewrites IC hostAddress.\n // cacheWidgetHtml is idempotent — already-rewritten URLs won't re-match.\n const result = await cacheWidgetHtml(layoutId, regionId, widgetId, html);\n html = result.html;\n\n // Update raw content in XLF\n const rawEl = mediaEl.querySelector('raw');\n if (rawEl) {\n rawEl.textContent = html;\n } else {\n const newRaw = doc.createElement('raw');\n newRaw.textContent = html;\n mediaEl.appendChild(newRaw);\n }\n } catch (error) {\n log.warn(`Failed to get widget HTML for ${type} ${widgetId}:`, error);\n }\n })()\n );\n }\n }\n }\n\n if (fetchPromises.length > 0) {\n log.info(`Fetching ${fetchPromises.length} widget HTML resources in parallel...`);\n await Promise.all(fetchPromises);\n log.debug('All widget HTML fetched');\n }\n }\n\n /**\n * Check media cache status for all scheduled layouts.\n * For each layout: load XLF from cache, extract media IDs, check each with store.has().\n * Feeds results into PlayerCore.setLayoutMediaStatus() for timeline annotation.\n */\n private async checkTimelineMediaStatus() {\n if (this.scheduledLayoutIds.size === 0) return;\n\n for (const layoutId of this.scheduledLayoutIds) {\n const layoutFile = `${layoutId}.xlf`;\n try {\n const xlfBlob = await store.get(`${STORE_PREFIX}/layouts`, layoutId);\n if (!xlfBlob) continue;\n\n const xlfXml = await xlfBlob.text();\n const { allMedia } = this.getMediaIds(xlfXml);\n\n if (allMedia.length === 0) {\n this.core.setLayoutMediaStatus(layoutFile, true);\n continue;\n }\n\n const missing: string[] = [];\n for (const saveAs of allMedia) {\n if (this._cachedMediaKeys.has(saveAs)) continue;\n // If in download queue, it's not cached — skip HEAD\n const storeKey = `${STORE_PREFIX}/media/file/${saveAs}`;\n if (downloadManager.getTask(storeKey)) { missing.push(saveAs); continue; }\n try {\n const cached = await store.has(STORE_PREFIX, `media/file/${saveAs}`);\n if (cached) this._cachedMediaKeys.add(saveAs);\n else missing.push(saveAs);\n } catch {\n // Assume cached on error (offline mode)\n }\n }\n\n this.core.setLayoutMediaStatus(layoutFile, missing.length === 0, missing);\n } catch {\n // Skip layouts we can't load\n }\n }\n\n // Re-emit annotated timeline\n this.core.logUpcomingTimeline();\n }\n\n /**\n * Probe video durations for all scheduled layouts.\n * Uses preload=\"metadata\" — only fetches headers (~50KB), not the full video.\n * Feeds discovered durations into PlayerCore for accurate timeline calculation.\n */\n private async probeLayoutDurations() {\n if (this.scheduledLayoutIds.size === 0) return;\n\n for (const layoutId of this.scheduledLayoutIds) {\n\n try {\n const xlfBlob = await store.get(`${STORE_PREFIX}/layouts`, layoutId);\n if (!xlfBlob) continue;\n\n const xlfXml = await xlfBlob.text();\n const { videoMedia } = this.getMediaIds(xlfXml);\n if (videoMedia.length === 0) continue;\n\n // Parse XLF to find video widgets with duration=0 (use media length)\n const parser = new DOMParser();\n const doc = parser.parseFromString(xlfXml, 'text/xml');\n\n // Probe actual video durations, keyed by fileId\n const videoDurations = new Map<string, number>();\n let dynamicVideoCount = 0;\n for (const mediaEl of doc.querySelectorAll('media[type=\"video\"]')) {\n const useDuration = mediaEl.getAttribute('useDuration');\n if (useDuration === '1') continue; // Has explicit CMS duration, skip\n\n const fileId = mediaEl.getAttribute('fileId');\n if (!fileId) continue;\n dynamicVideoCount++;\n\n const saveAs = this._fileIdToSaveAs.get(fileId) || fileId;\n const exists = await store.has(STORE_PREFIX, `media/file/${saveAs}`);\n if (!exists) continue;\n\n // Probe metadata only — does NOT download the full video\n const duration = await this.probeVideoDuration(`${window.location.origin}${PLAYER_API}/media/file/${saveAs}`);\n if (duration > 0) {\n videoDurations.set(fileId, duration);\n }\n }\n\n if (videoDurations.size === 0) continue;\n\n // Only mark final if ALL dynamic videos were successfully probed\n const allProbed = videoDurations.size >= dynamicVideoCount;\n\n // Phase 2: refine layout duration with probed video lengths\n const { duration: probedDuration } = parseLayoutDuration(xlfXml, videoDurations);\n if (probedDuration > 0) {\n this.core.recordLayoutDuration(String(layoutId), probedDuration, allProbed);\n }\n } catch (err) {\n log.debug(`Duration probe failed for layout ${layoutId}:`, err);\n }\n }\n }\n\n /**\n * Probe a single video's duration using metadata only.\n * Creates a temporary <video preload=\"metadata\"> element, reads duration, destroys it.\n */\n private probeVideoDuration(url: string): Promise<number> {\n return new Promise((resolve) => {\n const video = document.createElement('video');\n video.preload = 'metadata';\n video.muted = true;\n\n const cleanup = () => {\n video.removeAttribute('src');\n video.load(); // Release resources\n };\n\n video.addEventListener('loadedmetadata', () => {\n const dur = video.duration;\n cleanup();\n resolve(dur);\n }, { once: true });\n\n video.addEventListener('error', () => {\n cleanup();\n resolve(0);\n }, { once: true });\n\n // Safety timeout — don't block forever\n setTimeout(() => {\n cleanup();\n resolve(0);\n }, 5000);\n\n video.src = url;\n });\n }\n\n /**\n * Update config display\n */\n private updateConfigDisplay() {\n const configEl = document.getElementById('config-info');\n if (configEl) {\n const version = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '?';\n const buildDate = typeof __BUILD_DATE__ !== 'undefined' ? __BUILD_DATE__.replace('T', ' ').replace(/\\.\\d+Z$/, '') : '';\n const versionStr = buildDate ? `v${version} (${buildDate})` : `v${version}`;\n let text = `${versionStr} | CMS: ${config.cmsUrl} | Display: ${config.displayName || 'Unknown'} | HW: ${config.hardwareKey}`;\n const sc = this.core?.getSyncConfig?.();\n if (sc) {\n const relay = sc.relayUrl ? new URL(sc.relayUrl).host : '';\n text += ` | Sync: ${sc.isLead ? 'LEAD' : `FOLLOWER → ${relay}`} (group ${sc.syncGroupId || sc.syncGroup})`;\n }\n configEl.textContent = text;\n }\n }\n\n /**\n * Generic submission pipeline for stats and logs.\n * Handles in-flight guard, sync delegation, CMS submission, and cleanup.\n */\n private async submitCollectedData(options: {\n name: string;\n pendingFlag: '_pendingFollowerStats' | '_pendingFollowerLogs';\n getItems: () => Promise<any[]>;\n formatFn: (items: any[]) => string;\n delegateFn: (xml: string) => void;\n submitFn: (xml: string) => Promise<any>;\n clearFn: (items: any[]) => Promise<void>;\n }): Promise<void> {\n const { name, pendingFlag, getItems, formatFn, delegateFn, submitFn, clearFn } = options;\n\n // Guard: don't start a new delegation while one is in-flight\n if (this[pendingFlag] !== null) {\n log.debug(`${name} delegation in-flight, skipping`);\n return;\n }\n\n try {\n const items = await getItems();\n\n if (items.length === 0) {\n log.debug(`No ${name} to submit`);\n return;\n }\n\n const xml = formatFn(items);\n\n // Follower with live lead: delegate via BroadcastChannel\n if (this.syncManager && !this.syncManager.isLead && this._syncLeadAlive()) {\n log.info(`[Sync] Delegating ${items.length} ${name} to lead`);\n this[pendingFlag] = items;\n delegateFn(xml);\n return;\n }\n\n // Lead, standalone, or lead-dead follower: submit directly\n if (this.syncManager && !this.syncManager.isLead) {\n log.warn(`[Sync] Lead not alive, submitting ${name} directly`);\n }\n\n log.info(`Submitting ${items.length} ${name} to CMS...`);\n\n const success = await submitFn(xml);\n\n if (success) {\n log.info(`${name} submitted successfully`);\n await clearFn(items);\n } else {\n log.warn(`${name} submission failed (CMS returned false)`);\n }\n } catch (error) {\n log.error(`Failed to submit ${name}:`, error);\n }\n }\n\n /**\n * Submit proof of play stats to CMS\n */\n private async submitStats(): Promise<void> {\n if (!this.statsCollector) {\n log.warn('Stats collector not initialized');\n return;\n }\n\n const aggregationLevel = this.displaySettings?.getSetting('aggregationLevel') || 'Individual';\n\n await this.submitCollectedData({\n name: 'stats',\n pendingFlag: '_pendingFollowerStats',\n getItems: async () => aggregationLevel === 'Aggregate'\n ? this.statsCollector.getAggregatedStatsForSubmission(50)\n : this.statsCollector.getStatsForSubmission(50),\n formatFn: formatStats,\n delegateFn: (xml) => this.syncManager.reportStats(xml),\n submitFn: (xml) => this.xmds.submitStats(xml),\n clearFn: (items) => this.statsCollector.clearSubmittedStats(items),\n });\n }\n\n /**\n * Submit player logs to CMS for remote debugging\n */\n private async submitLogs(): Promise<void> {\n if (!this.logReporter) return;\n\n await this.submitCollectedData({\n name: 'logs',\n pendingFlag: '_pendingFollowerLogs',\n getItems: () => this.logReporter.getLogsForSubmission(),\n formatFn: formatLogs,\n delegateFn: (xml) => this.syncManager.reportLogs(xml),\n submitFn: (xml) => this.xmds.submitLog(xml),\n clearFn: (items) => this.logReporter.clearSubmittedLogs(items),\n });\n }\n\n /**\n * Report a fault to both the log dashboard and the player_faults dashboard.\n * Combines logReporter.reportFault() (log dashboard) with submitFault() (faults dashboard).\n */\n private reportFault(code: string, reason: string, details?: { layoutId?: number; regionId?: string; widgetId?: string }): void {\n this.logReporter?.reportFault(code, reason);\n this.submitFault(code, reason, details);\n }\n\n /**\n * Submit a fault report to CMS for the player_faults dashboard.\n */\n private submitFault(code: string, reason: string, details?: { layoutId?: number; regionId?: string; widgetId?: string }) {\n if (!this.xmds) return;\n\n const fault = JSON.stringify([{\n code,\n reason,\n date: new Date().toISOString().replace('T', ' ').substring(0, 19),\n ...details\n }]);\n\n this.xmds.reportFaults(fault).catch((err: any) => {\n log.debug('reportFaults failed (non-critical):', err);\n });\n }\n\n /**\n * Capture screenshot and submit to CMS.\n *\n * Strategy (best available, tried in order):\n * 0. Electron IPC — webContents.capturePage() via preload bridge.\n * Pixel-perfect, captures video/WebGL/composited layers, zero DOM cost.\n * Only available when running inside the Electron shell.\n * 1. getDisplayMedia() — native pixel capture via screen sharing API.\n * Pixel-perfect, zero DOM manipulation. Chromium kiosk auto-approves\n * via --auto-select-desktop-capture-source flag.\n * 2. Direct canvas drawing — fallback that draws img/video/canvas elements\n * directly. Text-only widgets (clocks, tickers) won't appear.\n *\n * The first successful method is cached for subsequent calls.\n */\n private async captureAndSubmitScreenshot() {\n // Concurrency guard — skip if a capture is already in flight\n if (this._screenshotInFlight) {\n log.debug('Screenshot capture already in progress, skipping');\n return;\n }\n this._screenshotInFlight = true;\n\n try {\n let base64: string;\n\n // Electron path: use native webContents.capturePage() via IPC\n if (this._screenshotMethod === 'electron' ||\n (this._screenshotMethod === null && (window as any).electronAPI?.captureScreenshot)) {\n const electronResult = await (window as any).electronAPI.captureScreenshot();\n if (electronResult) {\n this._screenshotMethod = 'electron';\n base64 = electronResult;\n } else {\n // Electron capture returned null (window not yet painted).\n // Do NOT fall through to getDisplayMedia — it triggers a\n // permission dialog that blocks the whole UI. Skip this\n // cycle; capturePage() will succeed on the next interval.\n log.debug('Electron screenshot not ready yet, will retry next interval');\n return;\n }\n } else if (this._screenshotMethod === 'displayMedia' ||\n (this._screenshotMethod === null && typeof navigator.mediaDevices?.getDisplayMedia === 'function')) {\n // Try getDisplayMedia — pixel-perfect screen capture, zero DOM cost.\n // Chromium kiosk auto-approves via --auto-accept-this-tab-capture.\n try {\n base64 = await this.captureDisplayMedia();\n this._screenshotMethod = 'displayMedia';\n } catch (e: any) {\n log.warn('getDisplayMedia failed, falling back to html2canvas:', e.message || e);\n this._screenshotMethod = null;\n base64 = await this.captureHtml2CanvasIsolated();\n this._screenshotMethod = 'html2canvas';\n }\n } else {\n // Tier 3: html2canvas hybrid (Firefox, other browsers)\n // Direct draw for img/video/canvas + per-iframe html2canvas for HTML widgets\n this._screenshotMethod = 'html2canvas';\n base64 = await this.captureHtml2CanvasIsolated();\n }\n\n const success = await this.xmds.submitScreenShot(base64);\n if (success) {\n log.info(`Screenshot submitted (${this._screenshotMethod})`);\n } else {\n log.warn('Screenshot submission failed');\n }\n } catch (error) {\n log.error('Failed to capture screenshot:', error);\n } finally {\n this._screenshotInFlight = false;\n }\n }\n\n /**\n * Capture screenshot via getDisplayMedia (screen sharing API).\n * Pixel-perfect, captures everything the GPU renders including video,\n * WebGL, composited layers, and all widget content.\n * Chromium kiosk auto-approves via --auto-select-desktop-capture-source.\n */\n private async captureDisplayMedia(): Promise<string> {\n const stream = await navigator.mediaDevices.getDisplayMedia({\n video: true,\n audio: false,\n preferCurrentTab: true,\n } as any);\n\n try {\n const track = stream.getVideoTracks()[0];\n const imageCapture = new (window as any).ImageCapture(track);\n const bitmap = await imageCapture.grabFrame();\n\n const canvas = document.createElement('canvas');\n canvas.width = bitmap.width;\n canvas.height = bitmap.height;\n const ctx = canvas.getContext('2d')!;\n ctx.drawImage(bitmap, 0, 0);\n bitmap.close();\n\n return canvas.toDataURL('image/jpeg', 0.8).split(',')[1];\n } finally {\n stream.getTracks().forEach(t => t.stop());\n }\n }\n\n /**\n * Capture screenshot via html2canvas hybrid approach.\n * Draws images/video/canvas directly, uses per-iframe html2canvas for\n * HTML widgets (clocks, tickers). CSS contain: strict on capture divs\n * prevents ResizeObserver glitches.\n */\n private async captureHtml2CanvasIsolated(): Promise<string> {\n const canvas = document.createElement('canvas');\n canvas.width = window.innerWidth;\n canvas.height = window.innerHeight;\n const ctx = canvas.getContext('2d')!;\n\n ctx.fillStyle = '#000';\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n\n const container = document.getElementById('player-container');\n if (!container) {\n return canvas.toDataURL('image/jpeg', 0.8).split(',')[1];\n }\n\n // Lazy-load html2canvas\n if (!this._html2canvasMod) {\n this._html2canvasMod = (await import('html2canvas')).default;\n }\n\n // Suppress resize during capture\n if (this.renderer) {\n this.renderer._resizeSuppressed = true;\n }\n\n // Protect the player container from external DOM changes.\n // html2canvas appends/removes elements to document.body which can trigger\n // Chromium reflow affecting the player. contain:strict on the player itself\n // makes it immune to any layout changes outside it.\n const prevContain = container.style.contain || '';\n container.style.contain = 'strict';\n\n try {\n // Draw container background\n const containerRect = container.getBoundingClientRect();\n const containerStyle = getComputedStyle(container);\n const bgColor = containerStyle.backgroundColor;\n if (bgColor && bgColor !== 'transparent' && bgColor !== 'rgba(0, 0, 0, 0)') {\n ctx.fillStyle = bgColor;\n ctx.fillRect(containerRect.left, containerRect.top, containerRect.width, containerRect.height);\n }\n const bgImage = containerStyle.backgroundImage;\n if (bgImage && bgImage !== 'none') {\n const urlMatch = bgImage.match(/url\\([\"']?(.*?)[\"']?\\)/);\n if (urlMatch) {\n try {\n const bgImg = new Image();\n bgImg.crossOrigin = 'anonymous';\n await new Promise<void>((resolve) => {\n bgImg.onload = () => resolve();\n bgImg.onerror = () => resolve();\n setTimeout(() => resolve(), 2000);\n bgImg.src = urlMatch[1];\n });\n if (bgImg.naturalWidth) {\n ctx.drawImage(bgImg, containerRect.left, containerRect.top, containerRect.width, containerRect.height);\n }\n } catch (_) { /* skip failed background */ }\n }\n }\n\n // Draw each visible widget element\n const elements = container.querySelectorAll('img, video, iframe, canvas');\n let drawn = 0;\n\n for (const el of elements) {\n const htmlEl = el as HTMLElement;\n if (htmlEl.style.visibility === 'hidden') continue;\n if (htmlEl.style.display === 'none') continue;\n const rect = el.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) continue;\n\n try {\n if (el instanceof HTMLImageElement) {\n if (!el.complete || !el.naturalWidth) continue;\n const fit = getComputedStyle(el).objectFit;\n if (fit === 'contain' && el.naturalWidth && el.naturalHeight) {\n const d = this.containedRect(el.naturalWidth, el.naturalHeight, rect);\n ctx.drawImage(el, d.x, d.y, d.w, d.h);\n } else {\n ctx.drawImage(el, rect.left, rect.top, rect.width, rect.height);\n }\n drawn++;\n } else if (el instanceof HTMLVideoElement) {\n if (el.readyState < 2) continue;\n const fit = getComputedStyle(el).objectFit;\n if (fit === 'contain' && el.videoWidth && el.videoHeight) {\n const d = this.containedRect(el.videoWidth, el.videoHeight, rect);\n ctx.drawImage(el, d.x, d.y, d.w, d.h);\n } else {\n ctx.drawImage(el, rect.left, rect.top, rect.width, rect.height);\n }\n drawn++;\n } else if (el instanceof HTMLCanvasElement) {\n ctx.drawImage(el, rect.left, rect.top, rect.width, rect.height);\n drawn++;\n } else if (el instanceof HTMLIFrameElement) {\n const iDoc = el.contentDocument;\n if (!iDoc?.body) continue;\n\n // Clone iframe DOM into main document for html2canvas rendering.\n // contain: strict prevents layout effects on the live player.\n const captureDiv = document.createElement('div');\n captureDiv.style.cssText = `position:fixed;left:-9999px;top:0;width:${rect.width}px;height:${rect.height}px;overflow:hidden;`;\n\n // Clone stylesheets with absolute URLs\n const linkPromises: Promise<void>[] = [];\n for (const styleEl of iDoc.querySelectorAll('style')) {\n captureDiv.appendChild(styleEl.cloneNode(true));\n }\n for (const linkEl of iDoc.querySelectorAll('link[rel=\"stylesheet\"]')) {\n const newLink = document.createElement('link');\n newLink.rel = 'stylesheet';\n newLink.href = new URL(linkEl.getAttribute('href') || '', iDoc.baseURI).href;\n captureDiv.appendChild(newLink);\n linkPromises.push(new Promise<void>(resolve => {\n newLink.onload = () => resolve();\n newLink.onerror = () => resolve();\n }));\n }\n\n // Clone body content with absolute img URLs\n const clonedBody = iDoc.body.cloneNode(true) as HTMLElement;\n for (const img of clonedBody.querySelectorAll('img[src]')) {\n const src = img.getAttribute('src') || '';\n if (src && !src.startsWith('http') && !src.startsWith('data:') && !src.startsWith('blob:')) {\n img.setAttribute('src', new URL(src, iDoc.baseURI).href);\n }\n }\n captureDiv.appendChild(clonedBody);\n document.body.appendChild(captureDiv);\n\n // Collect natural dimensions from original iframe images\n const origImgs = iDoc.querySelectorAll('img');\n const imgNaturals = new Map<string, { nw: number; nh: number }>();\n origImgs.forEach((img, i) => {\n if (img.naturalWidth && img.naturalHeight) {\n imgNaturals.set(String(i), { nw: img.naturalWidth, nh: img.naturalHeight });\n }\n });\n\n if (linkPromises.length > 0) {\n await Promise.race([\n Promise.all(linkPromises),\n new Promise(r => setTimeout(r, 500)),\n ]);\n }\n\n const iframeCanvas = await this._html2canvasMod(captureDiv, {\n useCORS: true, allowTaint: true, logging: false,\n backgroundColor: null,\n width: rect.width, height: rect.height,\n onclone: (clonedDoc: Document) => {\n // Force visible — CSS animations reset to opacity:0 in cloned DOM\n const s = clonedDoc.createElement('style');\n s.textContent = '*, *::before, *::after { animation: none !important; transition: none !important; opacity: 1 !important; }';\n clonedDoc.head.appendChild(s);\n\n // Fix object-fit: contain sizing for html2canvas\n const clonedImgs = clonedDoc.querySelectorAll('img');\n clonedImgs.forEach((cImg, i) => {\n const style = clonedDoc.defaultView?.getComputedStyle(cImg);\n if (!style || style.objectFit !== 'contain') return;\n const dims = imgNaturals.get(String(i));\n if (!dims) return;\n\n const cW = cImg.clientWidth || parseFloat(style.width) || 0;\n const cH = cImg.clientHeight || parseFloat(style.height) || 0;\n if (!cW || !cH) return;\n\n const srcAspect = dims.nw / dims.nh;\n const dstAspect = cW / cH;\n let drawW: number, drawH: number;\n if (srcAspect > dstAspect) {\n drawW = cW; drawH = cW / srcAspect;\n } else {\n drawH = cH; drawW = cH * srcAspect;\n }\n\n const wrapper = clonedDoc.createElement('div');\n wrapper.style.cssText = `width:${cW}px;height:${cH}px;display:flex;align-items:center;justify-content:center;overflow:hidden;`;\n cImg.style.objectFit = 'fill';\n cImg.style.width = `${drawW}px`;\n cImg.style.height = `${drawH}px`;\n cImg.parentNode?.insertBefore(wrapper, cImg);\n wrapper.appendChild(cImg);\n });\n },\n });\n\n document.body.removeChild(captureDiv);\n ctx.drawImage(iframeCanvas, rect.left, rect.top, rect.width, rect.height);\n\n // Draw videos directly — html2canvas can't render <video> elements.\n // Draw on top of the html2canvas result so video overlays black placeholder.\n const iframeRect = el.getBoundingClientRect();\n for (const vid of iDoc.querySelectorAll('video') as NodeListOf<HTMLVideoElement>) {\n if (vid.readyState < 2) continue;\n const vr = vid.getBoundingClientRect();\n if (vr.width === 0 || vr.height === 0) continue;\n try {\n const fit = iDoc.defaultView?.getComputedStyle(vid)?.objectFit;\n if (fit === 'contain' && vid.videoWidth && vid.videoHeight) {\n const d = this.containedRect(vid.videoWidth, vid.videoHeight,\n new DOMRect(iframeRect.left + vr.left, iframeRect.top + vr.top, vr.width, vr.height));\n ctx.drawImage(vid, d.x, d.y, d.w, d.h);\n } else {\n ctx.drawImage(vid, iframeRect.left + vr.left, iframeRect.top + vr.top, vr.width, vr.height);\n }\n } catch (_) { /* tainted video */ }\n }\n\n // Draw canvas elements directly (PDF pages, charts rendered in iframe)\n for (const c of iDoc.querySelectorAll('canvas') as NodeListOf<HTMLCanvasElement>) {\n const cr = c.getBoundingClientRect();\n if (cr.width === 0 || cr.height === 0) continue;\n try {\n ctx.drawImage(c, iframeRect.left + cr.left, iframeRect.top + cr.top, cr.width, cr.height);\n } catch (_) { /* tainted canvas */ }\n }\n\n drawn++;\n }\n } catch (e: any) {\n log.warn('Screenshot: failed to draw element', el.tagName, e);\n }\n }\n\n log.debug(`Screenshot: composed ${drawn}/${elements.length} elements`);\n return canvas.toDataURL('image/jpeg', 0.8).split(',')[1];\n } finally {\n container.style.contain = prevContain;\n if (this.renderer) {\n this.renderer._resizeSuppressed = false;\n }\n }\n }\n\n /**\n * Calculate the destination rect for object-fit: contain.\n * Returns the centered rect that preserves the source aspect ratio\n * within the bounding rect (letterbox/pillarbox).\n */\n private containedRect(\n srcW: number, srcH: number, rect: DOMRect\n ): { x: number; y: number; w: number; h: number } {\n const srcAspect = srcW / srcH;\n const dstAspect = rect.width / rect.height;\n let w: number, h: number;\n if (srcAspect > dstAspect) {\n // Source is wider — fit to width, letterbox top/bottom\n w = rect.width;\n h = rect.width / srcAspect;\n } else {\n // Source is taller — fit to height, pillarbox left/right\n h = rect.height;\n w = rect.height * srcAspect;\n }\n return {\n x: rect.left + (rect.width - w) / 2,\n y: rect.top + (rect.height - h) / 2,\n w, h,\n };\n }\n\n /**\n * Start periodic screenshot submission\n */\n private startScreenshotInterval() {\n const intervalSecs = this.displaySettings?.getSetting('screenshotInterval') || 0;\n if (!intervalSecs || intervalSecs <= 0) return;\n\n // Pre-load html2canvas for non-Electron/non-Chromium browsers\n if (!this._html2canvasMod && !(window as any).electronAPI) {\n import('html2canvas').then(m => { this._html2canvasMod = m.default; });\n }\n\n const intervalMs = intervalSecs * 1000;\n log.info(`Starting periodic screenshots every ${intervalSecs}s`);\n this._screenshotInterval = setInterval(() => {\n this.captureAndSubmitScreenshot();\n }, intervalMs);\n }\n\n /**\n * Update status message (Platform-specific UI)\n */\n private updateStatus(message: string, type: 'info' | 'error' = 'info') {\n const statusEl = document.getElementById('status');\n if (statusEl) {\n statusEl.textContent = message;\n statusEl.className = `status status-${type}`;\n }\n if (type === 'error') {\n log.error('Status:', message);\n } else {\n log.info('Status:', message);\n }\n }\n\n private showOfflineIndicator() {\n this.timelineOverlay?.setOffline(true);\n }\n\n private removeOfflineIndicator() {\n this.timelineOverlay?.setOffline(false);\n }\n\n /**\n * Check if the sync lead is alive (for follower delegation).\n * Returns true if any peer with role 'lead' has been seen in the last 15s.\n */\n private _syncLeadAlive(): boolean {\n if (!this.syncManager) return false;\n for (const [, peer] of this.syncManager.followers) {\n if (peer.role === 'lead' && Date.now() - peer.lastSeen < 15000) {\n return true;\n }\n }\n return false;\n }\n\n /**\n * Cleanup\n */\n cleanup() {\n this.core.cleanup();\n this.renderer.cleanup();\n\n if (this._screenshotInterval) {\n clearInterval(this._screenshotInterval);\n this._screenshotInterval = null;\n }\n\n if (this._wakeLock) {\n this._wakeLock.release();\n this._wakeLock = null;\n }\n\n if (this.downloadOverlay) {\n this.downloadOverlay.destroy();\n }\n\n if (this.timelineOverlay) {\n this.timelineOverlay.destroy();\n }\n\n // Disconnect iframe observer\n if (this._iframeObserver) {\n this._iframeObserver.disconnect();\n this._iframeObserver = null;\n }\n\n // Remove SW message listeners\n if (navigator.serviceWorker) {\n if (this._swIcHandler) {\n navigator.serviceWorker.removeEventListener('message', this._swIcHandler);\n this._swIcHandler = null;\n }\n }\n\n // Clean up DownloadManager\n downloadManager?.clear();\n\n if (this._probeTimer) {\n clearTimeout(this._probeTimer);\n this._probeTimer = null;\n }\n\n if (this._mediaStatusTimer) {\n clearTimeout(this._mediaStatusTimer);\n this._mediaStatusTimer = null;\n }\n }\n}\n\nfunction startPlayer() {\n const player = new PwaPlayer();\n player.init().catch(error => {\n log.error('Failed to initialize:', error);\n // First boot with bad config — redirect to setup so user can fix it\n log.warn('Redirecting to setup screen...');\n window.location.href = './setup.html';\n });\n window.addEventListener('beforeunload', () => {\n player.cleanup();\n });\n}\n\nif (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', startPlayer);\n} else {\n startPlayer();\n}\n"],"file":"main-CT7JIMxf.js"}
|