@xiboplayer/pwa 0.1.0
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/README.md +60 -0
- package/dist/assets/cache-proxy-Cx4Z8XMC.js +2 -0
- package/dist/assets/cache-proxy-Cx4Z8XMC.js.map +1 -0
- package/dist/assets/cms-api-kzy_Sw-u.js +2 -0
- package/dist/assets/cms-api-kzy_Sw-u.js.map +1 -0
- package/dist/assets/html2canvas.esm-CBrSDip1.js +23 -0
- package/dist/assets/html2canvas.esm-CBrSDip1.js.map +1 -0
- package/dist/assets/index-BEhNaWZ4.js +2 -0
- package/dist/assets/index-BEhNaWZ4.js.map +1 -0
- package/dist/assets/index-BPNsrSEv.js +2 -0
- package/dist/assets/index-BPNsrSEv.js.map +1 -0
- package/dist/assets/index-BY2j60YZ.js +2 -0
- package/dist/assets/index-BY2j60YZ.js.map +1 -0
- package/dist/assets/index-CTmjUTVM.js +8 -0
- package/dist/assets/index-CTmjUTVM.js.map +1 -0
- package/dist/assets/index-_q2HbdAU.js +2 -0
- package/dist/assets/index-_q2HbdAU.js.map +1 -0
- package/dist/assets/index-_uzldOpz.js +16 -0
- package/dist/assets/index-_uzldOpz.js.map +1 -0
- package/dist/assets/main-C4ABDfkq.js +27 -0
- package/dist/assets/main-C4ABDfkq.js.map +1 -0
- package/dist/assets/modulepreload-polyfill-B5Qt9EMX.js +2 -0
- package/dist/assets/modulepreload-polyfill-B5Qt9EMX.js.map +1 -0
- package/dist/assets/pdf-BnPRJEQ6.js +13 -0
- package/dist/assets/pdf-BnPRJEQ6.js.map +1 -0
- package/dist/assets/setup-rqZh5qYs.js +2 -0
- package/dist/assets/setup-rqZh5qYs.js.map +1 -0
- package/dist/index.html +130 -0
- package/dist/setup.html +371 -0
- package/dist/sw-pwa.js +2 -0
- package/dist/sw-pwa.js.map +1 -0
- package/dist/sw-utils.js +210 -0
- package/dist/sw.test.js +271 -0
- package/package.json +39 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"mappings":"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","names":["createNanoEvents","event","args","callbacks","length","cb","_a","i","log","createLogger","LayoutPool","maxSize","layoutId","entry","existing","url","fileId","blobUrl","regionId","region","oldest","oldestTime","id","count","warmIds","keepIds","evictIds","ids","Transitions","element","duration","keyframes","timing","direction","width","height","isIn","dirMap","offset","regionWidth","regionHeight","transitionConfig","type","RendererLite","config","container","options","layout","screenWidth","screenHeight","scaleX","scaleY","regionEl","regionConfig","sf","overlayId","overlay","callback","parentEl","actions","actionEl","xlfXml","doc","layoutEl","layoutDurationAttr","mediaEl","widget","maxDuration","regionDuration","useDuration","optionsEl","child","rawEl","raw","transitions","blobUrls","maxRegionDuration","oldDuration","layoutDurationMs","allKeyboardActions","touchActionCount","action","widgetEl","widgetId","handler","source","keyboardActions","pressedKey","keycode","targetWidgetId","widgetIndex","w","nextIndex","targetWidget","prevIndex","bgUrl","err","mediaPromises","error","playNext","tagName","videoEl","playAfterSeek","audioEl","resolve","timer","onPlaying","imgEl","onLoad","readyPromises","widgetElement","animation","img","imageSrc","video","isDebug","videoSrc","Hls","__vitePreload","hls","_event","data","e","videoDuration","errorCode","errorMessage","audio","audioSrc","icon","info","filename","iframe","html","result","self","blob","pdfjsModule","pdfUrl","page","viewport","scale","scaledViewport","canvas","context","preloadDelay","retryDelay","wrapper","preloadMediaUrlCache","savedMediaUrlCache","savedCurrentLayoutId","preloadRegions","preloadBlobUrls","savedLayoutBlobUrls","v","preloaded","oldLayoutId","allComplete","priority","overlayDiv","overlayRegions","durationMs","overlayState","overlayIds","DataConnectorManager","EventEmitter","connectors","connector","dataKey","intervalMs","keys","response","fetchWithRetry","contentType","previousData","OFFLINE_DB_NAME","OFFLINE_DB_VERSION","OFFLINE_STORE","openOfflineDb","reject","req","db","PlayerCore","store","schedule","settings","requiredFiles","r","key","tx","cachedReg","cachedSchedule","layoutFiles","f","idx","next","regResult","applyCmsLogLevel","checkRf","checkSchedule","allFiles","purgeFiles","files","currentLayouts","prioritizedFiles","defaultLayoutId","_b","_c","xmrUrl","xmrCmsKey","_d","collectIntervalSeconds","collectIntervalMs","newIntervalSeconds","requiredMediaIds","layoutFile","replayId","fileType","isLayoutFile","isRequiredMedia","commandCode","commands","command","commandString","parts","success","triggerCode","now","inventoryXml","mediaId","reason","syncManager","currentLayoutIds","tiered","tier","a","b","t","DownloadOverlay","_cacheProxy","__publicField","mc","progressPromise","progress","downloads","percent","downloaded","total","bytes","kb","mb","enabled","getDefaultOverlayConfig","showDownloads","savedPref","PLAYER_BASE","cacheManager","scheduleManager","RestClient","XmdsClient","XmrWrapper","cacheProxy","StatsCollector","formatStats","LogReporter","formatLogs","DisplaySettings","PwaPlayer","registration","CacheProxy","streamingUrl","cacheKey","lat","lng","overlayConfig","cacheModule","xmdsModule","scheduleModule","configModule","n","xmrModule","statsModule","displaySettingsModule","sysInfo","registerLogSink","level","name","message","displayName","isOffline","scheduledIds","l","c","cleared","debugNow","cacheNames","newInterval","_settings","changes","method","path","search","body","port","connectorData","_layout","pending","actionType","layoutCode","targetId","nextLayoutId","xlfBlob","requiredMedia","videoMediaIds","mediaIds","el","bgFileId","parsed","videoIds","cache","metadataResponse","metadataText","metadata","sizeMB","lastIdx","lastChunkKey","sizeKB","sizeStr","filenames","widgetTypes","fetchPromises","cachedResponse","newRaw","configEl","stats","statsXml","logs","logXml","base64","electronResult","nativeResult","stream","track","bitmap","ctx","containerRect","containerStyle","bgColor","bgImage","urlMatch","bgImg","__vite_default__","elements","drawn","htmlEl","rect","d","iDoc","captureDiv","linkPromises","styleEl","linkEl","newLink","origImgs","imgNaturals","iframeCanvas","clonedDoc","s","cImg","style","dims","cW","cH","srcAspect","dstAspect","drawW","drawH","srcW","srcH","h","intervalSecs","m","statusEl","player"],"ignoreList":[0,1,2,3,4],"sources":["../../node_modules/.pnpm/nanoevents@9.1.0/node_modules/nanoevents/index.js","../../node_modules/.pnpm/@xiboplayer+renderer@0.1.0/node_modules/@xiboplayer/renderer/src/layout-pool.js","../../node_modules/.pnpm/@xiboplayer+renderer@0.1.0/node_modules/@xiboplayer/renderer/src/renderer-lite.js","../../node_modules/.pnpm/@xiboplayer+core@0.1.0_@xiboplayer+cache@0.1.0_@xiboplayer+renderer@0.1.0_@xiboplayer+s_9004d73058ef2b88fe9f964b77c026f0/node_modules/@xiboplayer/core/src/data-connectors.js","../../node_modules/.pnpm/@xiboplayer+core@0.1.0_@xiboplayer+cache@0.1.0_@xiboplayer+renderer@0.1.0_@xiboplayer+s_9004d73058ef2b88fe9f964b77c026f0/node_modules/@xiboplayer/core/src/player-core.js","../../src/download-overlay.ts","../../src/main.ts"],"sourcesContent":["export let createNanoEvents = () => ({\n emit(event, ...args) {\n for (\n let callbacks = this.events[event] || [],\n i = 0,\n length = callbacks.length;\n i < length;\n i++\n ) {\n callbacks[i](...args)\n }\n },\n events: {},\n on(event, cb) {\n ;(this.events[event] ||= []).push(cb)\n return () => {\n this.events[event] = this.events[event]?.filter(i => cb !== i)\n }\n }\n})\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 * 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 // 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 // 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 // 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 * 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 * 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","/**\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 { createNanoEvents } from 'nanoevents';\nimport { createLogger, isDebug } from '@xiboplayer/utils';\nimport { LayoutPool } from './layout-pool.js';\n\n/**\n * Transition utilities for widget animations\n */\nconst 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 'fadein':\n return isIn ? this.fadeIn(element, duration) : null;\n case 'fadeout':\n return isIn ? null : this.fadeOut(element, duration);\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 {Function} options.getMediaUrl - Function to get media file URL (mediaId) => url\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 = createNanoEvents();\n\n // State\n this.currentLayout = null;\n this.currentLayoutId = null;\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.widgetTimers = new Map(); // widgetId => timer\n this.mediaUrlCache = new Map(); // fileId => blob URL (for parallel pre-fetching)\n this.layoutBlobUrls = new Map(); // layoutId => Set<blobUrl> (for lifecycle tracking)\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 // 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 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\n if (typeof ResizeObserver !== 'undefined') {\n this.resizeObserver = new ResizeObserver(() => {\n this.rescaleRegions();\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 return 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 actionType: actionEl.getAttribute('actionType') || '',\n triggerType: actionEl.getAttribute('triggerType') || '',\n triggerCode: actionEl.getAttribute('triggerCode') || '',\n layoutCode: actionEl.getAttribute('layoutCode') || '',\n targetId: actionEl.getAttribute('targetId') || '',\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 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('bgcolor') || '#000000',\n background: layoutEl.getAttribute('background') || null, // Background image fileId\n regions: []\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\n for (const regionEl of doc.querySelectorAll('region')) {\n const region = {\n id: regionEl.getAttribute('id'),\n width: parseInt(regionEl.getAttribute('width')),\n height: parseInt(regionEl.getAttribute('height')),\n top: parseInt(regionEl.getAttribute('top')),\n left: parseInt(regionEl.getAttribute('left')),\n zindex: parseInt(regionEl.getAttribute('zindex') || '0'),\n actions: this.parseActions(regionEl),\n widgets: []\n };\n\n // Parse media/widgets\n for (const mediaEl of regionEl.querySelectorAll('media')) {\n const widget = this.parseWidget(mediaEl);\n region.widgets.push(widget);\n }\n\n layout.regions.push(region);\n }\n\n // Calculate layout duration if not specified (duration=0)\n if (layout.duration === 0) {\n let maxDuration = 0;\n\n for (const region of layout.regions) {\n let regionDuration = 0;\n\n // Calculate region duration based on widgets\n for (const widget of region.widgets) {\n if (widget.duration > 0) {\n regionDuration += widget.duration;\n } else {\n // Widget with duration=0 means \"use media length\"\n // Default to 60s here; actual duration is detected dynamically\n // from video.loadedmetadata event and updateLayoutDuration() recalculates\n regionDuration = 60;\n break;\n }\n }\n\n maxDuration = Math.max(maxDuration, regionDuration);\n }\n\n layout.duration = maxDuration > 0 ? maxDuration : 60;\n this.log.info(`Calculated layout duration: ${layout.duration}s (not specified in XLF)`);\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 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 options,\n raw,\n transitions,\n actions\n };\n }\n\n /**\n * Track blob URL for lifecycle management\n * @param {string} blobUrl - Blob URL to track\n */\n trackBlobUrl(blobUrl) {\n if (!this.currentLayoutId) return;\n\n if (!this.layoutBlobUrls.has(this.currentLayoutId)) {\n this.layoutBlobUrls.set(this.currentLayoutId, new Set());\n }\n\n this.layoutBlobUrls.get(this.currentLayoutId).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 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 // If we calculated a different duration, update layout\n if (maxRegionDuration > 0 && maxRegionDuration !== this.currentLayout.duration) {\n const oldDuration = this.currentLayout.duration;\n this.currentLayout.duration = maxRegionDuration;\n\n this.log.info(`Layout duration updated: ${oldDuration}s → ${maxRegionDuration}s (based on video metadata)`);\n\n // Reset layout timer with new duration — but only if a timer is already running.\n // If startLayoutTimerWhenReady() hasn't fired yet (still waiting for widgets),\n // it will pick up the updated duration when it starts the timer.\n if (this.layoutTimer) {\n clearTimeout(this.layoutTimer);\n\n const layoutDurationMs = this.currentLayout.duration * 1000;\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 }, layoutDurationMs);\n\n this.log.info(`Layout timer reset to ${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 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 /**\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 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 this.startRegion(regionId);\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 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\n for (const [regionId, region] of this.regions) {\n if (region.timer) {\n clearTimeout(region.timer);\n region.timer = null;\n }\n // Reset to first widget\n region.currentIndex = 0;\n }\n\n // Clear layout timer\n if (this.layoutTimer) {\n clearTimeout(this.layoutTimer);\n this.layoutTimer = null;\n }\n this.layoutEndEmitted = false;\n\n // DON'T call stopCurrentLayout() - keep elements alive!\n // DON'T clear mediaUrlCache - keep blob URLs 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\n for (const [regionId, region] of this.regions) {\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 if (layout.background && this.options.getMediaUrl) {\n try {\n const bgUrl = await this.options.getMediaUrl(parseInt(layout.background));\n if (bgUrl) {\n this.container.style.backgroundImage = `url(${bgUrl})`;\n this.container.style.backgroundSize = 'cover';\n this.container.style.backgroundPosition = 'center';\n this.container.style.backgroundRepeat = 'no-repeat';\n this.log.info(`Background image set: ${layout.background}`);\n }\n } catch (err) {\n this.log.warn('Failed to load background image:', err);\n }\n }\n\n // PRE-FETCH: Get all media URLs in parallel (huge speedup!)\n if (this.options.getMediaUrl) {\n const mediaPromises = [];\n this.mediaUrlCache.clear(); // Clear previous layout's cache\n\n for (const region of layout.regions) {\n for (const widget of region.widgets) {\n if (widget.fileId) {\n const fileId = parseInt(widget.fileId || widget.id);\n if (!this.mediaUrlCache.has(fileId)) {\n mediaPromises.push(\n this.options.getMediaUrl(fileId)\n .then(url => {\n this.mediaUrlCache.set(fileId, url);\n })\n .catch(err => {\n this.log.warn(`Failed to fetch media ${fileId}:`, err);\n })\n );\n }\n }\n }\n }\n\n if (mediaPromises.length > 0) {\n this.log.info(`Pre-fetching ${mediaPromises.length} media URLs in parallel...`);\n await Promise.all(mediaPromises);\n this.log.info(`All media URLs pre-fetched`);\n }\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 element.style.visibility = 'hidden'; // Hidden by default\n element.style.opacity = '0';\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 // Start all regions\n for (const [regionId, region] of this.regions) {\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 * Create a region element\n * @param {Object} regionConfig - Region configuration\n */\n async createRegion(regionConfig) {\n const regionEl = document.createElement('div');\n regionEl.id = `region_${regionConfig.id}`;\n regionEl.className = 'renderer-lite-region';\n regionEl.style.position = 'absolute';\n regionEl.style.zIndex = regionConfig.zindex;\n regionEl.style.overflow = 'hidden';\n\n // Apply scaled positioning\n this.applyRegionScale(regionEl, regionConfig);\n\n this.container.appendChild(regionEl);\n\n // Store region state (dimensions use scaled values for transitions)\n const sf = this.scaleFactor;\n this.regions.set(regionConfig.id, {\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, // Track if region has played all widgets once\n widgetElements: new Map() // widgetId -> DOM element (for element reuse)\n });\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 if (!region || region.widgets.length === 0) {\n return;\n }\n\n // If only one widget, just render it (no cycling)\n // Don't set completion timer - layout duration controls ending\n // Region completion is NOT tracked for single-widget regions\n // (they display continuously until layout timer expires)\n if (region.widgets.length === 1) {\n this.renderWidget(regionId, 0);\n return;\n }\n\n // Multiple widgets - cycle through them\n const playNext = () => {\n const widgetIndex = region.currentIndex;\n const widget = region.widgets[widgetIndex];\n\n // Render widget\n this.renderWidget(regionId, widgetIndex);\n\n // Schedule next widget\n const duration = widget.duration * 1000;\n region.timer = setTimeout(() => {\n this.stopWidget(regionId, widgetIndex);\n\n // Move to next widget (wraps to 0 if at end)\n const nextIndex = (region.currentIndex + 1) % region.widgets.length;\n\n // Check if completing full cycle (wrapped back to 0)\n if (nextIndex === 0 && !region.complete) {\n region.complete = true;\n this.log.info(`Region ${regionId} completed one full cycle`);\n this.checkLayoutComplete();\n }\n\n region.currentIndex = nextIndex;\n playNext();\n }, duration);\n };\n\n playNext();\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 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 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 // Videos: ALWAYS restart on widget show (even if looping)\n const videoEl = this.findMediaElement(element, 'VIDEO');\n if (videoEl) {\n videoEl.currentTime = 0;\n // Wait for seek to complete before playing — avoids DOMException\n // \"The play() request was interrupted\" when calling play() mid-seek\n const playAfterSeek = () => {\n videoEl.removeEventListener('seeked', playAfterSeek);\n videoEl.play().catch(() => {}); // Silently ignore — autoplay will retry\n };\n videoEl.addEventListener('seeked', playAfterSeek);\n // Fallback: if seeked doesn't fire (already at 0), try play directly\n if (videoEl.currentTime === 0 && videoEl.readyState >= 2) {\n videoEl.removeEventListener('seeked', playAfterSeek);\n videoEl.play().catch(() => {});\n }\n this.log.info(`Video restarted: ${widget.fileId || widget.id}`);\n return;\n }\n\n // Audio: ALWAYS restart on widget show (even if looping)\n const audioEl = this.findMediaElement(element, 'AUDIO');\n if (audioEl) {\n audioEl.currentTime = 0;\n const playAfterSeek = () => {\n audioEl.removeEventListener('seeked', playAfterSeek);\n audioEl.play().catch(() => {});\n };\n audioEl.addEventListener('seeked', playAfterSeek);\n if (audioEl.currentTime === 0 && audioEl.readyState >= 2) {\n audioEl.removeEventListener('seeked', playAfterSeek);\n audioEl.play().catch(() => {});\n }\n this.log.info(`Audio restarted: ${widget.fileId || widget.id}`);\n return;\n }\n\n // Images: Could refresh src if needed (future enhancement)\n // const imgEl = this.findMediaElement(element, 'IMG');\n\n // Iframes: Could reload if needed (future enhancement)\n // const iframeEl = this.findMediaElement(element, 'IFRAME');\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 // 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 timer = setTimeout(() => {\n this.log.warn(`Image ready timeout for widget ${widget.id}`);\n resolve();\n }, READY_TIMEOUT);\n const onLoad = () => {\n imgEl.removeEventListener('load', onLoad);\n clearTimeout(timer);\n resolve();\n };\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 const layoutDurationMs = layout.duration * 1000;\n this.log.info(`Layout ${layoutId} will end after ${layout.duration}s`);\n\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 async renderWidget(regionId, widgetIndex) {\n const region = this.regions.get(regionId);\n if (!region) return;\n\n const widget = region.widgets[widgetIndex];\n if (!widget) return;\n\n try {\n this.log.info(`Showing widget ${widget.type} (${widget.id}) in region ${regionId}`);\n\n // REUSE: Get existing element instead of creating new one\n let element = region.widgetElements.get(widget.id);\n\n if (!element) {\n // Fallback: create if doesn't exist (shouldn't happen with pre-creation)\n this.log.warn(`Widget ${widget.id} not pre-created, creating now`);\n widget.layoutId = this.currentLayoutId;\n widget.regionId = regionId;\n element = await this.createWidgetElement(widget, region);\n region.widgetElements.set(widget.id, element);\n region.element.appendChild(element);\n }\n\n // Hide all other widgets in region\n for (const [widgetId, widgetEl] of region.widgetElements) {\n if (widgetId !== widget.id) {\n widgetEl.style.visibility = 'hidden';\n widgetEl.style.opacity = '0';\n }\n }\n\n // Update media element if needed (restart videos)\n this.updateMediaElement(element, widget);\n\n // Show this widget\n element.style.visibility = 'visible';\n\n // Apply in transition\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 // Emit widget start event\n this.emit('widgetStart', {\n widgetId: widget.id,\n regionId,\n layoutId: this.currentLayoutId,\n mediaId: parseInt(widget.fileId || widget.id) || null,\n type: widget.type,\n duration: widget.duration\n });\n\n } catch (error) {\n this.log.error(`Error rendering widget:`, error);\n this.emit('error', { type: 'widgetError', error, widgetId: widget.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 region = this.regions.get(regionId);\n if (!region) return;\n\n const widget = region.widgets[widgetIndex];\n if (!widget) return;\n\n // Get widget element from reuse cache\n const widgetElement = region.widgetElements.get(widget.id);\n if (!widgetElement) return;\n\n // Apply out transition\n if (widget.transitions.out) {\n const animation = Transitions.apply(\n widgetElement,\n widget.transitions.out,\n false,\n region.width,\n region.height\n );\n\n if (animation) {\n await new Promise(resolve => {\n animation.onfinish = resolve;\n });\n }\n }\n\n // Pause media elements (but DON'T revoke URLs - element will be reused!)\n const videoEl = widgetElement.querySelector('video');\n if (videoEl && widget.options.loop !== '1') {\n videoEl.pause();\n // Keep src intact for next cycle\n }\n\n const audioEl = widgetElement.querySelector('audio');\n if (audioEl && widget.options.loop !== '1') {\n audioEl.pause();\n // Keep src intact for next cycle\n }\n\n // Emit widget end event\n this.emit('widgetEnd', {\n widgetId: widget.id,\n regionId,\n layoutId: this.currentLayoutId,\n mediaId: parseInt(widget.fileId || widget.id) || null,\n type: widget.type\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 img.style.objectFit = 'contain';\n img.style.opacity = '0';\n\n // Get media URL from cache (already pre-fetched!) or fetch on-demand\n const fileId = parseInt(widget.fileId || widget.id);\n let imageSrc = this.mediaUrlCache.get(fileId);\n\n if (!imageSrc && this.options.getMediaUrl) {\n imageSrc = await this.options.getMediaUrl(fileId);\n } else if (!imageSrc) {\n imageSrc = `${window.location.origin}/player/cache/media/${widget.options.uri}`;\n }\n\n img.src = imageSrc;\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 video.style.objectFit = '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 = isDebug(); // Show controls only in debug mode\n video.playsInline = true; // Prevent fullscreen on mobile\n\n // Handle video end - pause on last frame instead of showing black\n // Widget cycling will restart the video via updateMediaElement()\n video.addEventListener('ended', () => {\n if (widget.options.loop === '1') {\n // For looping videos: seek back to start but stay paused on first frame\n // This avoids black frames - shows first frame until widget cycles\n video.currentTime = 0;\n this.log.info(`Video ${fileId} ended - reset to start, waiting for widget cycle to replay`);\n } else {\n // For non-looping videos: stay paused on last frame\n this.log.info(`Video ${fileId} ended - paused on last frame`);\n }\n });\n\n // Get media URL from cache (already pre-fetched!) or fetch on-demand\n const fileId = parseInt(widget.fileId || widget.id);\n let videoSrc = this.mediaUrlCache.get(fileId);\n\n if (!videoSrc && this.options.getMediaUrl) {\n videoSrc = await this.options.getMediaUrl(fileId);\n } else if (!videoSrc) {\n videoSrc = `${window.location.origin}/player/cache/media/${fileId}`;\n }\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 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 }\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 video.addEventListener('loadedmetadata', () => {\n const videoDuration = Math.floor(video.duration);\n this.log.info(`Video ${fileId} duration detected: ${videoDuration}s`);\n\n // If widget has useDuration=0, update widget duration with actual video length\n if (widget.duration === 0 || widget.useDuration === 0) {\n widget.duration = videoDuration;\n this.log.info(`Updated widget ${widget.id} duration to ${videoDuration}s (useDuration=0)`);\n\n // Recalculate layout duration if needed\n this.updateLayoutDuration();\n }\n });\n\n // Debug video loading\n video.addEventListener('loadeddata', () => {\n this.log.info('Video loaded and ready:', fileId);\n });\n\n // Handle video errors\n video.addEventListener('error', (e) => {\n const error = video.error;\n const errorCode = error?.code;\n const errorMessage = error?.message || 'Unknown error';\n\n // Log all video errors for debugging, but never show to users\n // These are often transient codec warnings that don't prevent playback\n this.log.warn(`Video error (non-fatal, logged only): ${fileId}, code: ${errorCode}, time: ${video.currentTime.toFixed(1)}s, message: ${errorMessage}`);\n\n // Do NOT emit error events - video errors are logged but not surfaced to UI\n // Video will either recover (transient decode error) or fail completely (handled elsewhere)\n });\n\n video.addEventListener('playing', () => {\n this.log.info('Video playing:', fileId);\n });\n\n this.log.info('Video element created:', fileId, video.src);\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 // Get media URL from cache (already pre-fetched!) or fetch on-demand\n const fileId = parseInt(widget.fileId || widget.id);\n let audioSrc = this.mediaUrlCache.get(fileId);\n\n if (!audioSrc && this.options.getMediaUrl) {\n audioSrc = await this.options.getMediaUrl(fileId);\n } else if (!audioSrc) {\n audioSrc = `${window.location.origin}/player/cache/media/${fileId}`;\n }\n\n audio.src = audioSrc;\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 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 // On hard reload (Ctrl+Shift+R), iframe navigation bypasses SW → server 404\n // Detect and fall back to blob URL with original CMS signed URLs\n if (result.fallback) {\n const self = this;\n iframe.addEventListener('load', function() {\n try {\n // Our cached widget HTML has a <base> tag; server 404 page doesn't\n if (!iframe.contentDocument?.querySelector('base')) {\n console.warn('[RendererLite] Cache URL failed (hard reload?), using original CMS URLs');\n const blob = new Blob([result.fallback], { type: 'text/html' });\n const blobUrl = URL.createObjectURL(blob);\n self.trackBlobUrl(blobUrl);\n iframe.src = blobUrl;\n }\n } catch (e) { /* cross-origin — should not happen */ }\n }, { once: true });\n }\n\n return iframe;\n }\n html = result;\n }\n\n // Fallback: Create blob URL for iframe\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\n return iframe;\n }\n\n /**\n * Render PDF widget\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 = '#525659';\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 window.pdfjsLib.GlobalWorkerOptions.workerSrc = `${window.location.origin}/player/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 // Get PDF URL from cache (already pre-fetched!) or fetch on-demand\n const fileId = parseInt(widget.fileId || widget.id);\n let pdfUrl = this.mediaUrlCache.get(fileId);\n\n if (!pdfUrl && this.options.getMediaUrl) {\n pdfUrl = await this.options.getMediaUrl(fileId);\n } else if (!pdfUrl) {\n pdfUrl = `${window.location.origin}/player/cache/media/${widget.options.uri}`;\n }\n\n // Render PDF\n try {\n const loadingTask = window.pdfjsLib.getDocument(pdfUrl);\n const pdf = await loadingTask.promise;\n const page = await pdf.getPage(1); // Render first page\n\n const viewport = page.getViewport({ scale: 1 });\n const scale = Math.min(\n region.width / viewport.width,\n region.height / viewport.height\n );\n const scaledViewport = page.getViewport({ scale });\n\n const canvas = document.createElement('canvas');\n canvas.width = scaledViewport.width;\n canvas.height = scaledViewport.height;\n canvas.style.display = 'block';\n canvas.style.margin = 'auto';\n\n const context = canvas.getContext('2d');\n await page.render({ canvasContext: context, viewport: scaledViewport }).promise;\n\n container.appendChild(canvas);\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 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 iframe.src = widget.options.uri;\n\n return iframe;\n }\n\n /**\n * Render generic widget (clock, calendar, weather, etc.)\n */\n async renderGenericWidget(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 // On hard reload (Ctrl+Shift+R), iframe navigation bypasses SW → server 404\n // Detect and fall back to blob URL with original CMS signed URLs\n if (result.fallback) {\n const self = this;\n iframe.addEventListener('load', function() {\n try {\n // Our cached widget HTML has a <base> tag; server 404 page doesn't\n if (!iframe.contentDocument?.querySelector('base')) {\n console.warn('[RendererLite] Cache URL failed (hard reload?), using original CMS URLs');\n const blob = new Blob([result.fallback], { type: 'text/html' });\n const blobUrl = URL.createObjectURL(blob);\n self.trackBlobUrl(blobUrl);\n iframe.src = blobUrl;\n }\n } catch (e) { /* cross-origin — should not happen */ }\n }, { once: true });\n }\n\n return iframe;\n }\n html = result;\n }\n\n if (html) {\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 // ── 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 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 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 if (layout.background && this.options.getMediaUrl) {\n try {\n const bgUrl = await this.options.getMediaUrl(parseInt(layout.background));\n if (bgUrl) {\n wrapper.style.backgroundImage = `url(${bgUrl})`;\n wrapper.style.backgroundSize = 'cover';\n wrapper.style.backgroundPosition = 'center';\n wrapper.style.backgroundRepeat = 'no-repeat';\n }\n } catch (err) {\n this.log.warn('Preload: Failed to load background image:', err);\n }\n }\n\n // Pre-fetch all media URLs in parallel\n const preloadMediaUrlCache = new Map();\n if (this.options.getMediaUrl) {\n const mediaPromises = [];\n\n for (const region of layout.regions) {\n for (const widget of region.widgets) {\n if (widget.fileId) {\n const fileId = parseInt(widget.fileId || widget.id);\n if (!preloadMediaUrlCache.has(fileId)) {\n mediaPromises.push(\n this.options.getMediaUrl(fileId)\n .then(url => {\n preloadMediaUrlCache.set(fileId, url);\n })\n .catch(err => {\n this.log.warn(`Preload: Failed to fetch media ${fileId}:`, err);\n })\n );\n }\n }\n }\n }\n\n if (mediaPromises.length > 0) {\n this.log.info(`Preload: fetching ${mediaPromises.length} media URLs...`);\n await Promise.all(mediaPromises);\n }\n }\n\n // Temporarily swap mediaUrlCache so createWidgetElement uses preload cache\n const savedMediaUrlCache = this.mediaUrlCache;\n const savedCurrentLayoutId = this.currentLayoutId;\n this.mediaUrlCache = preloadMediaUrlCache;\n\n // Create regions in the hidden wrapper\n const preloadRegions = new Map();\n const sf = this.scaleFactor;\n\n for (const regionConfig of layout.regions) {\n const regionEl = document.createElement('div');\n regionEl.id = `preload_region_${layoutId}_${regionConfig.id}`;\n regionEl.className = 'renderer-lite-region';\n regionEl.style.position = 'absolute';\n regionEl.style.zIndex = regionConfig.zindex;\n regionEl.style.overflow = 'hidden';\n\n // Apply scaled positioning\n this.applyRegionScale(regionEl, regionConfig);\n\n wrapper.appendChild(regionEl);\n\n const region = {\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 };\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 // Temporarily set currentLayoutId for trackBlobUrl to work\n this.currentLayoutId = 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 element.style.visibility = 'hidden';\n element.style.opacity = '0';\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.mediaUrlCache = savedMediaUrlCache;\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 mediaUrlCache: preloadMediaUrlCache\n });\n\n this.log.info(`Layout ${layoutId} preloaded into pool (${preloadRegions.size} regions, ${preloadMediaUrlCache.size} media)`);\n return true;\n\n } catch (error) {\n this.log.error(`Preload failed for layout ${layoutId}:`, error);\n return false;\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\n if (this.layoutTimer) {\n clearTimeout(this.layoutTimer);\n this.layoutTimer = null;\n }\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 const oldLayoutId = this.currentLayoutId;\n\n if (oldLayoutId && this.layoutPool.has(oldLayoutId)) {\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 for (const [regionId, region] of this.regions) {\n if (region.timer) {\n clearTimeout(region.timer);\n region.timer = null;\n }\n // Release video resources\n region.element.querySelectorAll('video').forEach(v => {\n v.pause();\n v.removeAttribute('src');\n v.load();\n });\n region.element.remove();\n }\n // Revoke blob URLs\n if (oldLayoutId) {\n this.revokeBlobUrlsForLayout(oldLayoutId);\n }\n for (const [fileId, blobUrl] of this.mediaUrlCache) {\n if (blobUrl && typeof blobUrl === 'string' && blobUrl.startsWith('blob:')) {\n URL.revokeObjectURL(blobUrl);\n }\n }\n }\n\n // Emit layoutEnd for old layout if timer hasn't already\n if (oldLayoutId && !this.layoutEndEmitted) {\n this.emit('layoutEnd', oldLayoutId);\n }\n\n this.regions.clear();\n this.mediaUrlCache.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 this.mediaUrlCache = preloaded.mediaUrlCache || new Map();\n this.layoutEndEmitted = false;\n\n // Update container background to match preloaded layout\n this.container.style.backgroundColor = preloaded.layout.bgcolor;\n if (preloaded.container.style.backgroundImage) {\n this.container.style.backgroundImage = preloaded.container.style.backgroundImage;\n this.container.style.backgroundSize = preloaded.container.style.backgroundSize;\n this.container.style.backgroundPosition = preloaded.container.style.backgroundPosition;\n this.container.style.backgroundRepeat = preloaded.container.style.backgroundRepeat;\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\n this._scheduleNextLayoutPreload(preloaded.layout);\n\n this.log.info(`Swapped to preloaded layout ${layoutId} (instant transition)`);\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 * Stop current layout\n */\n stopCurrentLayout() {\n if (!this.currentLayout) return;\n\n this.log.info(`Stopping layout ${this.currentLayoutId}`);\n\n // Remove interactive action listeners before teardown\n this.removeActionListeners();\n\n // Clear layout timer\n if (this.layoutTimer) {\n clearTimeout(this.layoutTimer);\n this.layoutTimer = null;\n }\n\n // Clear preload timers\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 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 (this.currentLayoutId && this.layoutPool.has(this.currentLayoutId)) {\n this.layoutPool.evict(this.currentLayoutId);\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 (this.currentLayoutId) {\n this.revokeBlobUrlsForLayout(this.currentLayoutId);\n }\n\n // Stop all regions\n for (const [regionId, region] of this.regions) {\n if (region.timer) {\n clearTimeout(region.timer);\n region.timer = null;\n }\n\n // Stop current widget\n if (region.widgets.length > 0) {\n this.stopWidget(regionId, region.currentIndex);\n }\n\n // Remove region element\n region.element.remove();\n }\n\n // Revoke media blob URLs from cache\n for (const [fileId, blobUrl] of this.mediaUrlCache) {\n if (blobUrl && blobUrl.startsWith('blob:')) {\n URL.revokeObjectURL(blobUrl);\n }\n }\n }\n\n // Clear state\n this.regions.clear();\n this.mediaUrlCache.clear();\n\n // Emit layout end event only if timer hasn't already emitted it.\n // Timer-based layoutEnd (natural expiry) is authoritative — stopCurrentLayout\n // is called afterwards during the switch to the next layout, so we skip the\n // duplicate. But if the layout is forcibly stopped mid-playback (e.g., XMR\n // schedule change), the timer hasn't fired yet, so we DO emit here.\n if (this.currentLayoutId && !this.layoutEndEmitted) {\n this.emit('layoutEnd', this.currentLayoutId);\n }\n\n this.layoutEndEmitted = false;\n this.currentLayout = null;\n this.currentLayoutId = null;\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 // Pre-fetch all media URLs for overlay\n if (this.options.getMediaUrl) {\n const mediaPromises = [];\n for (const region of layout.regions) {\n for (const widget of region.widgets) {\n if (widget.fileId) {\n const fileId = parseInt(widget.fileId || widget.id);\n if (!this.mediaUrlCache.has(fileId)) {\n mediaPromises.push(\n this.options.getMediaUrl(fileId)\n .then(url => {\n this.mediaUrlCache.set(fileId, url);\n })\n .catch(err => {\n this.log.warn(`Failed to fetch overlay media ${fileId}:`, err);\n })\n );\n }\n }\n }\n }\n\n if (mediaPromises.length > 0) {\n this.log.info(`Pre-fetching ${mediaPromises.length} overlay media URLs...`);\n await Promise.all(mediaPromises);\n }\n }\n\n // Calculate scale for overlay layout\n this.calculateScale(layout);\n\n // Create regions for overlay\n const overlayRegions = new Map();\n const sf = this.scaleFactor;\n for (const regionConfig of layout.regions) {\n const regionEl = document.createElement('div');\n regionEl.id = `overlay_${layoutId}_region_${regionConfig.id}`;\n regionEl.className = 'renderer-lite-region overlay-region';\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 overlayDiv.appendChild(regionEl);\n\n // Store region state (dimensions use scaled values)\n overlayRegions.set(regionConfig.id, {\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 });\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 element.style.visibility = 'hidden';\n element.style.opacity = '0';\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 if (!region || region.widgets.length === 0) {\n return;\n }\n\n // If only one widget, just render it (no cycling)\n if (region.widgets.length === 1) {\n this.renderOverlayWidget(overlayId, regionId, 0);\n return;\n }\n\n // Multiple widgets - cycle through them\n const playNext = () => {\n const widgetIndex = region.currentIndex;\n const widget = region.widgets[widgetIndex];\n\n // Render widget\n this.renderOverlayWidget(overlayId, regionId, widgetIndex);\n\n // Schedule next widget\n const duration = widget.duration * 1000;\n region.timer = setTimeout(() => {\n this.stopOverlayWidget(overlayId, regionId, widgetIndex);\n\n // Move to next widget (wraps to 0 if at end)\n const nextIndex = (region.currentIndex + 1) % region.widgets.length;\n\n // Check if completing full cycle (wrapped back to 0)\n if (nextIndex === 0 && !region.complete) {\n region.complete = true;\n this.log.info(`Overlay ${overlayId} region ${regionId} completed one full cycle`);\n }\n\n region.currentIndex = nextIndex;\n playNext();\n }, duration);\n };\n\n playNext();\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 const widget = region.widgets[widgetIndex];\n if (!widget) return;\n\n try {\n this.log.info(`Showing overlay widget ${widget.type} (${widget.id}) in overlay ${overlayId} region ${regionId}`);\n\n // Get existing element (pre-created)\n let element = region.widgetElements.get(widget.id);\n\n if (!element) {\n this.log.warn(`Overlay widget ${widget.id} not pre-created, creating now`);\n element = await this.createWidgetElement(widget, region);\n region.widgetElements.set(widget.id, element);\n region.element.appendChild(element);\n }\n\n // Hide all other widgets in region\n for (const [widgetId, widgetEl] of region.widgetElements) {\n if (widgetId !== widget.id) {\n widgetEl.style.visibility = 'hidden';\n widgetEl.style.opacity = '0';\n }\n }\n\n // Update media element if needed (restart videos)\n this.updateMediaElement(element, widget);\n\n // Show this widget\n element.style.visibility = 'visible';\n\n // Apply in transition\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 // Emit widget start event\n this.emit('overlayWidgetStart', {\n overlayId,\n widgetId: widget.id,\n regionId,\n type: widget.type,\n 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: widget.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 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 = region.widgets[widgetIndex];\n if (!widget) return;\n\n const widgetElement = region.widgetElements.get(widget.id);\n if (!widgetElement) return;\n\n // Apply out transition\n if (widget.transitions.out) {\n const animation = Transitions.apply(\n widgetElement,\n widget.transitions.out,\n false,\n region.width,\n region.height\n );\n\n if (animation) {\n await new Promise(resolve => {\n animation.onfinish = resolve;\n });\n }\n }\n\n // Pause media elements\n const videoEl = widgetElement.querySelector('video');\n if (videoEl && widget.options.loop !== '1') {\n videoEl.pause();\n }\n\n const audioEl = widgetElement.querySelector('audio');\n if (audioEl && widget.options.loop !== '1') {\n audioEl.pause();\n }\n\n // Emit widget end event\n this.emit('overlayWidgetEnd', {\n overlayId,\n widgetId: widget.id,\n regionId,\n type: widget.type\n });\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 [regionId, region] of overlayState.regions) {\n if (region.timer) {\n clearTimeout(region.timer);\n region.timer = null;\n }\n\n // Stop current widget\n if (region.widgets.length > 0) {\n this.stopOverlayWidget(layoutId, regionId, region.currentIndex);\n }\n }\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 * Cleanup renderer\n */\n cleanup() {\n this.stopAllOverlays();\n this.stopCurrentLayout();\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","/**\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\nexport class DataConnectorManager extends EventEmitter {\n constructor() {\n super();\n\n // dataKey -> { config, data, timer, lastFetch }\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 });\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\n log.debug(`Data updated for ${dataKey} (fetched at ${new Date(entry.lastFetch).toISOString()})`);\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 log.error(`Failed to fetch data for ${dataKey}:`, error);\n this.emit('fetch-error', dataKey, error);\n }\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","/**\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 } from '@xiboplayer/utils';\nimport { DataConnectorManager } from './data-connectors.js';\n\nconst log = createLogger('PlayerCore');\n\n// IndexedDB database/store for offline cache\nconst OFFLINE_DB_NAME = 'xibo-offline-cache';\nconst OFFLINE_DB_VERSION = 1;\nconst OFFLINE_STORE = 'cache';\n\n/** Open the offline cache IndexedDB (creates store on first use) */\nfunction openOfflineDb() {\n return new Promise((resolve, reject) => {\n const req = indexedDB.open(OFFLINE_DB_NAME, OFFLINE_DB_VERSION);\n req.onupgradeneeded = () => {\n const db = req.result;\n if (!db.objectStoreNames.contains(OFFLINE_STORE)) {\n db.createObjectStore(OFFLINE_STORE);\n }\n };\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\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 // Data connectors manager (real-time data for widgets)\n this.dataConnectorManager = new DataConnectorManager();\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.offlineMode = false; // Track whether we're currently in offline mode\n\n // CRC32 checksums for skip optimization (avoid redundant XMDS calls)\n this._lastCheckRf = null;\n this._lastCheckSchedule = 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 // Schedule cycle state (round-robin through multiple layouts)\n this._currentLayoutIndex = 0;\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 // 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 // ── Offline Cache (IndexedDB) ──────────────────────────────────────\n\n /** Load offline cache from IndexedDB into memory on startup */\n async _initOfflineCache() {\n try {\n const db = await openOfflineDb();\n const tx = db.transaction(OFFLINE_STORE, 'readonly');\n const store = tx.objectStore(OFFLINE_STORE);\n\n const [schedule, settings, requiredFiles] = 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 ]);\n\n this._offlineCache = { schedule, settings, requiredFiles };\n db.close();\n console.log('[PlayerCore] Offline cache loaded from IndexedDB',\n schedule ? '(has schedule)' : '(empty)');\n } catch (e) {\n console.warn('[PlayerCore] 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 const db = await openOfflineDb();\n const tx = db.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 db.close();\n } catch (e) {\n console.warn('[PlayerCore] 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 console.warn('[PlayerCore] Offline mode — using cached schedule');\n\n if (!this.offlineMode) {\n this.offlineMode = true;\n this.emit('offline-mode', true);\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 }\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('schedule-received', cachedSchedule);\n }\n\n // Evaluate current schedule (same logic as online path)\n const layoutFiles = this.schedule.getCurrentLayouts();\n log.info('Offline layouts:', layoutFiles);\n this.emit('layouts-scheduled', layoutFiles);\n\n if (layoutFiles.length > 0) {\n // If a layout is currently playing and still in the schedule, don't interrupt\n if (this.currentLayoutId) {\n const currentStillScheduled = layoutFiles.some(f =>\n parseInt(String(f).replace('.xlf', ''), 10) === this.currentLayoutId\n );\n if (currentStillScheduled) {\n const idx = layoutFiles.findIndex(f =>\n parseInt(String(f).replace('.xlf', ''), 10) === this.currentLayoutId\n );\n if (idx >= 0) this._currentLayoutIndex = idx;\n log.debug(`Layout ${this.currentLayoutId} still in schedule (offline), continuing playback`);\n this.emit('layout-already-playing', this.currentLayoutId);\n } else {\n // Current layout not in schedule — switch\n this._currentLayoutIndex = 0;\n const next = this.getNextLayout();\n if (next) {\n log.info(`Offline: switching to layout ${next.layoutId}`);\n this.emit('layout-prepare-request', next.layoutId);\n }\n }\n } else {\n // No current layout — start the first one\n this._currentLayoutIndex = 0;\n const next = this.getNextLayout();\n if (next) {\n log.info(`Offline: switching to layout ${next.layoutId}`);\n this.emit('layout-prepare-request', next.layoutId);\n }\n }\n } else {\n log.info('Offline: no layouts in cached schedule');\n this.emit('no-layouts-scheduled');\n }\n\n this.emit('collection-complete');\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('collection-start');\n\n // Check if browser reports offline\n if (this.isOffline()) {\n if (this.hasCachedData()) {\n return this.collectOffline();\n }\n throw new Error('Offline with no cached data — cannot start playback');\n }\n\n // Register display\n log.debug('Collection step: registerDisplay');\n const regResult = await this.xmds.registerDisplay();\n log.info('Display registered:', regResult);\n\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 console.log('[PlayerCore] Back online — resuming normal collection');\n this.emit('offline-mode', false);\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 // Collection interval changed - update interval\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('log-level-changed', regResult.settings.logLevel);\n }\n }\n }\n\n // Store sync config if display is in a sync group\n if (regResult.syncConfig) {\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('sync-config', regResult.syncConfig);\n }\n\n this.emit('register-complete', 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 log.debug('Collection step: requiredFiles');\n const allFiles = await this.xmds.requiredFiles();\n // Separate purge entries from download entries\n const purgeFiles = allFiles.filter(f => f.type === 'purge');\n const files = allFiles.filter(f => f.type !== 'purge');\n log.info('Required files:', files.length, purgeFiles.length > 0 ? `(+ ${purgeFiles.length} purge)` : '');\n this._lastCheckRf = checkRf;\n this.emit('files-received', files);\n\n // Cache required files for offline use\n this._offlineSave('requiredFiles', allFiles);\n\n if (purgeFiles.length > 0) {\n this.emit('purge-request', purgeFiles);\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.emit('schedule-received', schedule);\n this.schedule.setSchedule(schedule);\n this.updateDataConnectors();\n this._offlineSave('schedule', schedule);\n }\n\n log.debug('Collection step: download-request + mediaInventory');\n // Prioritize downloads by layout priority (highest first)\n const currentLayouts = this.schedule.getCurrentLayouts();\n const prioritizedFiles = this.prioritizeFilesByLayout(files, currentLayouts);\n this._lastRequiredFiles = files;\n this.emit('download-request', prioritizedFiles);\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.emit('schedule-received', schedule);\n this.schedule.setSchedule(schedule);\n this.updateDataConnectors();\n this._offlineSave('schedule', schedule);\n } else if (checkSchedule) {\n log.info('Schedule CRC unchanged, skipping');\n }\n }\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('layouts-scheduled', layoutFiles);\n\n if (layoutFiles.length > 0) {\n // If a layout is currently playing and it's still in the schedule, don't interrupt it.\n // Let it finish its natural duration — advanceToNextLayout() handles the transition.\n if (this.currentLayoutId) {\n const currentStillScheduled = layoutFiles.some(f =>\n parseInt(String(f).replace('.xlf', ''), 10) === this.currentLayoutId\n );\n if (currentStillScheduled) {\n // Update round-robin index to match current layout's position\n const idx = layoutFiles.findIndex(f =>\n parseInt(String(f).replace('.xlf', ''), 10) === this.currentLayoutId\n );\n if (idx >= 0) this._currentLayoutIndex = idx;\n log.debug(`Layout ${this.currentLayoutId} still in schedule, continuing playback`);\n this.emit('layout-already-playing', this.currentLayoutId);\n } else {\n // Current layout is not in the schedule (unscheduled or filtered) — switch\n this._currentLayoutIndex = 0;\n const next = this.getNextLayout();\n if (next) {\n log.info(`Switching to layout ${next.layoutId} (from ${this.currentLayoutId})`);\n this.emit('layout-prepare-request', next.layoutId);\n }\n }\n } else {\n // No current layout — start the first one\n this._currentLayoutIndex = 0;\n const next = this.getNextLayout();\n if (next) {\n log.info(`Switching to layout ${next.layoutId}`);\n this.emit('layout-prepare-request', next.layoutId);\n }\n }\n } else {\n log.info('No layouts scheduled, falling back to default');\n this.emit('no-layouts-scheduled');\n\n // If we're currently playing a layout but schedule says no layouts (e.g., maxPlaysPerHour filtered it),\n // force switch to default layout if available\n if (this.currentLayoutId && this.schedule.schedule?.default) {\n const defaultLayoutId = parseInt(this.schedule.schedule.default.replace('.xlf', ''), 10);\n log.info(`Current layout filtered by schedule, switching to default layout ${defaultLayoutId}`);\n this.currentLayoutId = null; // Clear to force switch\n this.emit('layout-prepare-request', defaultLayoutId);\n }\n }\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('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('submit-logs-request');\n\n // Setup collection interval on first run\n if (!this.collectionInterval && regResult.settings) {\n this.setupCollectionInterval(regResult.settings);\n }\n\n this.emit('collection-complete');\n\n } catch (error) {\n // Offline fallback: if network failed but we have cached data, use it\n if (this.hasCachedData()) {\n console.warn('[PlayerCore] Collection failed, falling back to cached data:', error?.message || error);\n this.emit('collection-error', error);\n return this.collectOffline();\n }\n\n log.error('Collection error:', error);\n this.emit('collection-error', error);\n throw error;\n } finally {\n this.collecting = false;\n }\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('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('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('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('xmr-connected', xmrUrl);\n } else if (!this.xmr.isConnected()) {\n log.info('XMR disconnected, attempting to reconnect...');\n this.xmr.reconnectAttempts = 0;\n await this.xmr.start(xmrUrl, xmrCmsKey);\n this.emit('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 const collectIntervalMs = collectIntervalSeconds * 1000;\n\n log.info(`Setting up collection interval: ${collectIntervalSeconds}s`);\n\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('collection-error', error);\n });\n }, collectIntervalMs);\n\n this.emit('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 clearInterval(this.collectionInterval);\n log.info(`Updating collection interval: ${newIntervalSeconds}s`);\n\n const collectIntervalMs = newIntervalSeconds * 1000;\n\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('collection-error', error);\n });\n }, collectIntervalMs);\n\n this.emit('collection-interval-updated', newIntervalSeconds);\n }\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 setCurrentLayout(layoutId) {\n this.currentLayoutId = layoutId;\n this.pendingLayouts.delete(layoutId);\n this.emit('layout-current', layoutId);\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 schedule using round-robin cycling.\n * Returns { layoutId, layoutFile } or null if no layouts are scheduled.\n */\n getNextLayout() {\n const layoutFiles = this.schedule.getCurrentLayouts();\n if (layoutFiles.length === 0) {\n return null;\n }\n\n // Wrap index in case schedule shrank\n if (this._currentLayoutIndex >= layoutFiles.length) {\n this._currentLayoutIndex = 0;\n }\n\n const layoutFile = layoutFiles[this._currentLayoutIndex];\n const layoutId = parseInt(layoutFile.replace('.xlf', ''), 10);\n return { layoutId, layoutFile };\n }\n\n /**\n * Peek at the next layout in the schedule without advancing the index.\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 layoutFiles = this.schedule.getCurrentLayouts();\n if (layoutFiles.length <= 1) {\n // Single layout or empty schedule - no different layout to preload\n return null;\n }\n\n const nextIndex = (this._currentLayoutIndex + 1) % layoutFiles.length;\n const layoutFile = layoutFiles[nextIndex];\n const layoutId = parseInt(layoutFile.replace('.xlf', ''), 10);\n\n // Don't return if it's the same as current (no point preloading)\n if (layoutId === this.currentLayoutId) {\n return null;\n }\n\n return { layoutId, layoutFile };\n }\n\n /**\n * Advance to the next layout in the schedule (round-robin).\n * Called by platform layer when a layout finishes (layoutEnd event).\n * Increments the index and emits layout-prepare-request for the next layout,\n * or triggers replay if only one layout is scheduled.\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 layoutFiles = this.schedule.getCurrentLayouts();\n log.info(`Advancing schedule: ${layoutFiles.length} layout(s) available, current index ${this._currentLayoutIndex}`);\n\n // ── Never-stop guarantee ────────────────────────────────────────\n // If no layouts are available at all (every layout is rate-limited\n // or filtered), replay the current layout as a last resort.\n // maxPlaysPerHour is respected in all other cases — this only fires\n // when the alternative would be a blank screen.\n if (layoutFiles.length === 0) {\n if (this.currentLayoutId) {\n log.info(`No layouts available (all rate-limited), replaying ${this.currentLayoutId} to avoid blank screen`);\n const replayId = this.currentLayoutId;\n this.currentLayoutId = null;\n this.emit('layout-prepare-request', replayId);\n } else {\n log.info('No layouts scheduled during advance');\n this.emit('no-layouts-scheduled');\n }\n return;\n }\n\n // Advance index (wraps around)\n this._currentLayoutIndex = (this._currentLayoutIndex + 1) % layoutFiles.length;\n\n const layoutFile = layoutFiles[this._currentLayoutIndex];\n const layoutId = parseInt(layoutFile.replace('.xlf', ''), 10);\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 // Lead: coordinate with followers before showing\n log.info(`[Sync] Lead requesting coordinated layout change: ${layoutId}`);\n this.syncManager.requestLayoutChange(layoutId).catch(err => {\n log.error('[Sync] Layout change failed:', err);\n // Fallback: show layout anyway\n this.emit('layout-prepare-request', layoutId);\n });\n return;\n } else {\n // Follower: don't advance independently — wait for lead's layout-change signal\n log.info(`[Sync] Follower waiting for lead signal (not advancing independently)`);\n return;\n }\n }\n\n if (layoutId === this.currentLayoutId) {\n // Same layout (single layout schedule or wrapped back) — trigger replay\n log.info(`Next layout ${layoutId} is same as current, triggering replay`);\n this.currentLayoutId = null; // Clear to allow re-render\n }\n\n log.info(`Advancing to layout ${layoutId} (index ${this._currentLayoutIndex}/${layoutFiles.length})`);\n this.emit('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(parseInt(fileId));\n\n if (isLayoutFile || isRequiredMedia) {\n log.debug(`${fileType} ${fileId} was needed by pending layout ${layoutId}, checking if ready...`);\n this.emit('check-pending-layout', layoutId, requiredFiles);\n }\n }\n }\n\n /**\n * Notify layout status to CMS\n */\n async notifyLayoutStatus(layoutId) {\n try {\n await this.xmds.notifyStatus({ currentLayoutId: layoutId });\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 * 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('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) {\n log.info('Layout change requested via XMR:', layoutId);\n this._layoutOverride = { layoutId: parseInt(layoutId, 10), type: 'change' };\n this.currentLayoutId = null; // Force re-render\n this.emit('layout-prepare-request', parseInt(layoutId, 10));\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) {\n log.info('Overlay layout requested via XMR:', layoutId);\n this._layoutOverride = { layoutId: parseInt(layoutId, 10), type: 'overlay' };\n this.emit('overlay-layout-request', parseInt(layoutId, 10));\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('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 = parseInt(layoutFile.replace('.xlf', ''), 10);\n this.emit('layout-prepare-request', layoutId);\n } else {\n this.emit('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('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.emit('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 });\n const success = response.ok;\n log.info(`HTTP command ${commandCode} result: ${response.status}`);\n this.emit('command-result', { code: commandCode, success, status: response.status });\n } catch (error) {\n log.error(`HTTP command ${commandCode} failed:`, error);\n this.emit('command-result', { code: commandCode, success: false, reason: error.message });\n }\n } else {\n log.warn('Non-HTTP commands not supported in browser:', commandCode);\n this.emit('command-result', { code: commandCode, success: false, reason: 'Only HTTP commands supported in browser' });\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 const now = Math.floor(Date.now() / 1000);\n const fileEntries = files\n .filter(f => f.type === 'media' || f.type === 'layout')\n .map(f => `<file type=\"${f.type}\" id=\"${f.id}\" complete=\"1\" md5=\"${f.md5 || ''}\" lastChecked=\"${now}\"/>`)\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 * 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('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 * 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 /**\n * Cleanup\n */\n cleanup() {\n if (this.collectionInterval) {\n clearInterval(this.collectionInterval);\n this.collectionInterval = 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 * 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 * Prioritize file downloads for fastest playback start:\n * 1. Layout XLFs for currently scheduled layouts (tiny, needed for parsing)\n * 2. Other layout XLFs (also tiny)\n * 3. Resource files (fonts, bundle.min.js — small, needed by widgets)\n * 4. Media files sorted by ascending size (small files complete faster)\n *\n * This ensures layouts are parseable ASAP so prepareAndRenderLayout() can\n * call prioritizeDownload() for the specific media the current layout needs.\n */\n prioritizeFilesByLayout(files, currentLayouts) {\n const currentLayoutIds = new Set();\n currentLayouts.forEach((layoutFile) => {\n currentLayoutIds.add(parseInt(String(layoutFile).replace('.xlf', ''), 10));\n });\n\n // Assign priority tiers\n const tiered = files.map(f => {\n let tier;\n if (f.type === 'layout') {\n const layoutId = parseInt(f.id);\n tier = currentLayoutIds.has(layoutId) ? 0 : 1; // Current layouts first\n } else if (f.type === 'resource' || f.code === 'fonts.css' ||\n (f.path && (f.path.includes('bundle.min') || f.path.includes('fonts')))) {\n tier = 2; // Resources (fonts, bundle.min.js)\n } else {\n tier = 3; // Media\n }\n return { file: f, tier };\n });\n\n // Sort by tier, then by ascending size within each tier\n tiered.sort((a, b) => {\n if (a.tier !== b.tier) return a.tier - b.tier;\n return (a.file.size || 0) - (b.file.size || 0);\n });\n\n return tiered.map(t => t.file);\n }\n}\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 position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';\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\n constructor(config: DownloadOverlayConfig, _cacheProxy?: any) {\n this.config = {\n position: 'bottom-right',\n updateInterval: 1000,\n autoHide: true,\n ...config\n };\n // cacheProxy not needed - using SW postMessage instead\n\n if (this.config.enabled) {\n this.createOverlay();\n // Don't start polling yet — startUpdating() is called on demand\n // when downloads begin, and stops automatically when idle.\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: 50px;\n left: 10px;\n background: rgba(0, 0, 0, 0.85);\n color: #fff;\n font-family: system-ui, -apple-system, sans-serif;\n font-size: 12px;\n padding: 8px 12px;\n border-radius: 4px;\n border: 1px solid rgba(255, 255, 255, 0.2);\n z-index: 999999;\n max-width: 350px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n `;\n\n document.body.appendChild(this.overlay);\n }\n\n private async updateOverlay() {\n if (!this.overlay) return;\n\n try {\n // Get download progress from Service Worker via postMessage\n if (!navigator.serviceWorker?.controller) {\n throw new Error('No SW controller');\n }\n\n // Request progress from SW\n const mc = new MessageChannel();\n const progressPromise = new Promise((resolve) => {\n mc.port1.onmessage = (event) => resolve(event.data);\n setTimeout(() => resolve({ success: false }), 500); // Timeout\n });\n\n navigator.serviceWorker.controller.postMessage(\n { type: 'GET_DOWNLOAD_PROGRESS' },\n [mc.port2]\n );\n\n const result: any = await progressPromise;\n\n if (result.success) {\n const html = this.renderStatus(result.progress);\n\n if (html) {\n this.overlay.innerHTML = html;\n this.overlay.style.display = 'block';\n } else {\n // No active downloads — stop polling to avoid SW message noise\n this.stopUpdating();\n if (this.config.autoHide) {\n this.overlay.style.display = 'none';\n }\n }\n } else {\n throw new Error('Progress request failed');\n }\n } catch (error) {\n // No SW controller or request failed — stop polling\n this.stopUpdating();\n if (this.config.autoHide && this.overlay) {\n this.overlay.style.display = 'none';\n }\n }\n }\n\n private renderStatus(progress: any): string {\n const downloads = progress || {};\n\n if (!downloads || 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: 6px;\">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: 6px; padding-bottom: 6px; border-bottom: 1px solid rgba(255,255,255,0.1);\">\n <div style=\"font-size: 11px; margin-bottom: 2px;\">${filename}</div>\n <div style=\"background: rgba(255,255,255,0.1); height: 4px; border-radius: 2px; 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: 10px; margin-top: 2px;\">\n ${percent}% · ${downloaded} / ${total}\n </div>\n </div>\n `;\n }\n\n return html;\n }\n\n private extractFilename(url: string): string {\n try {\n const urlObj = new URL(url);\n const filename = urlObj.searchParams.get('file') || url.split('/').pop() || 'unknown';\n return filename.length > 30 ? filename.substring(0, 27) + '...' : filename;\n } catch {\n return 'unknown';\n }\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 * Start polling SW for download progress.\n * Safe to call multiple times — won't create duplicate timers.\n */\n public startUpdating() {\n if (this.updateTimer) return; // Already polling\n this.updateTimer = window.setInterval(() => {\n this.updateOverlay();\n }, this.config.updateInterval);\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: always enable overlay (autoHide hides it when no downloads active)\n return { enabled: true, autoHide: true };\n}\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\n// @ts-ignore - JavaScript module\nimport { RendererLite } from '@xiboplayer/renderer';\n// @ts-ignore - JavaScript module\nimport { CacheProxy } from '@xiboplayer/cache';\n// @ts-ignore - JavaScript module\nimport { PlayerCore } from '@xiboplayer/core';\n// @ts-ignore - JavaScript module\nimport { createLogger, isDebug, registerLogSink } from '@xiboplayer/utils';\nimport { DownloadOverlay, getDefaultOverlayConfig } from './download-overlay.js';\n\nconst log = createLogger('PWA');\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 cacheManager: any;\nlet scheduleManager: any;\nlet config: any;\nlet RestClient: any;\nlet XmdsClient: any;\nlet XmrWrapper: any;\nlet cacheProxy: CacheProxy;\nlet StatsCollector: any;\nlet formatStats: any;\nlet LogReporter: any;\nlet formatLogs: any;\nlet DisplaySettings: any;\n\nclass PwaPlayer {\n private renderer!: RendererLite;\n private core!: PlayerCore;\n private xmds!: any;\n private downloadOverlay: DownloadOverlay | 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 preparingLayoutId: number | null = null; // Guard against concurrent prepareAndRenderLayout calls\n private _screenshotInterval: any = null;\n private _screenshotMethod: 'electron' | 'native' | 'html2canvas' | null = null;\n private _screenshotInFlight = false; // Concurrency guard — one capture at a time\n private _html2canvasMod: any = null; // Pre-loaded module\n private _wakeLock: any = null; // Screen Wake Lock sentinel\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 cache\n log.info('Initializing cache...');\n await cacheManager.init();\n\n // Initialize CacheProxy (Service Worker only - waits for SW to be ready)\n log.info('Initializing CacheProxy...');\n cacheProxy = new CacheProxy();\n await cacheProxy.init(); // Waits for Service Worker to be ready and controlling\n log.info('CacheProxy ready - using Service Worker backend');\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.cmsAddress,\n hardwareKey: config.hardwareKey\n },\n container,\n {\n // Provide media URL resolver - uses streaming via Service Worker\n getMediaUrl: async (fileId: number) => {\n log.debug(`getMediaUrl called for media ${fileId}`);\n\n // Check if file exists in cache (no blob creation - streaming!)\n const exists = await cacheProxy.hasFile('media', String(fileId));\n\n if (!exists) {\n log.warn(`Media ${fileId} not in cache`);\n return '';\n }\n\n // Return direct URL - Service Worker streams via Range requests\n // This eliminates blob creation delay and reduces memory usage!\n const streamingUrl = `${PLAYER_BASE}/cache/media/${fileId}`;\n log.debug(`Using streaming URL for media ${fileId}: ${streamingUrl}`);\n return streamingUrl;\n },\n\n // Provide widget HTML resolver\n getWidgetHtml: async (widget: any) => {\n const cacheKey = `${PLAYER_BASE}/cache/widget/${widget.layoutId}/${widget.regionId}/${widget.id}`;\n log.debug(`Looking for widget HTML at: ${cacheKey}`, widget);\n\n try {\n const cache = await caches.open('xibo-media-v1');\n const response = await cache.match(cacheKey);\n\n if (response) {\n log.debug(`Widget HTML cached at ${cacheKey}, using cache URL for iframe`);\n // Return cache URL + fallback HTML for hard reload recovery\n // On Ctrl+Shift+R, iframe.src navigation bypasses SW → 404\n // Renderer detects this and falls back to widget.raw (original CMS URLs)\n return { url: cacheKey, fallback: widget.raw || '' };\n } else {\n log.warn(`No cached HTML found at ${cacheKey}`);\n }\n } catch (error) {\n log.error(`Failed to get cached 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\n this.core = new PlayerCore({\n config,\n xmds: this.xmds,\n cache: cacheProxy,\n schedule: scheduleManager,\n renderer: this.renderer,\n xmrWrapper: XmrWrapper,\n statsCollector: this.statsCollector,\n displaySettings: this.displaySettings\n });\n\n // Setup platform-specific event handlers\n this.setupCoreEventHandlers();\n this.setupRendererEventHandlers();\n this.setupServiceWorkerEventHandlers();\n this.setupInteractiveControl();\n\n // Set display location from CMS settings when registration completes\n this.core.on('register-complete', (regResult: any) => {\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 }\n });\n\n // Setup UI\n this.setupUI();\n\n // Online/offline event listeners for seamless offline mode\n window.addEventListener('online', () => {\n console.log('[PWA] 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 console.warn('[PWA] 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 const overlayConfig = getDefaultOverlayConfig();\n if (overlayConfig.enabled) {\n this.downloadOverlay = new DownloadOverlay(overlayConfig, cacheProxy);\n log.info('Download overlay enabled (hover bottom-right corner)');\n }\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 * Load core modules\n */\n private async loadCoreModules() {\n try {\n // @ts-ignore - JavaScript modules\n const cacheModule = await import('@xiboplayer/cache');\n // @ts-ignore\n const xmdsModule = await import('@xiboplayer/xmds');\n // @ts-ignore\n const scheduleModule = await import('@xiboplayer/schedule');\n // @ts-ignore\n const configModule = await import('@xiboplayer/utils');\n // @ts-ignore\n const xmrModule = await import('@xiboplayer/xmr');\n // @ts-ignore\n const statsModule = await import('@xiboplayer/stats');\n // @ts-ignore\n const displaySettingsModule = await import('@xiboplayer/settings');\n\n cacheManager = cacheModule.cacheManager;\n scheduleManager = scheduleModule.scheduleManager;\n config = configModule.config;\n RestClient = xmdsModule.RestClient;\n XmdsClient = xmdsModule.XmdsClient;\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 // 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 auto-detection:\n // - /player/pwa-xmds/ or ?transport=xmds → forced SOAP\n // - Otherwise → try REST, fall back to SOAP if unavailable\n const forceXmds = PLAYER_BASE.includes('pwa-xmds')\n || new URLSearchParams(window.location.search).get('transport') === 'xmds';\n\n if (forceXmds) {\n log.info('Using XMDS/SOAP transport (forced)');\n this.xmds = new XmdsClient(config);\n } else {\n // Try REST — registerDisplay() is always the first call anyway.\n // If the CMS lacks /pwa/ REST endpoints, fall back to SOAP.\n this.xmds = new RestClient(config);\n try {\n await this.xmds.registerDisplay();\n log.info('Using REST transport');\n } catch (e: any) {\n log.warn('REST unavailable, falling back to XMDS/SOAP:', e.message);\n this.xmds = new XmdsClient(config);\n }\n }\n\n // Initialize stats collector\n this.statsCollector = new StatsCollector();\n await this.statsCollector.init();\n log.info('Stats collector initialized');\n\n // Initialize log reporter for CMS log submission\n this.logReporter = new LogReporter();\n await this.logReporter.init();\n log.info('Log reporter initialized');\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 = args.map((a: any) => typeof a === 'string' ? a : JSON.stringify(a)).join(' ');\n this.logReporter.log(level, `[${name}] ${message}`, 'PLAYER').catch(() => {});\n });\n\n // Initialize display settings manager\n this.displaySettings = new DisplaySettings();\n log.info('Display settings manager initialized');\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 // Collection events\n this.core.on('collection-start', () => {\n this.updateStatus('Collecting data from CMS...');\n });\n\n this.core.on('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\n this.core.on('files-received', (files: any[]) => {\n this.updateStatus(`Downloading ${files.length} files...`);\n });\n\n this.core.on('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('purge-request', async (purgeFiles: any[]) => {\n try {\n const result = await cacheProxy.deleteFiles(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('download-request', async (files: any[]) => {\n // Platform handles the actual download via CacheProxy\n // Restart overlay polling while downloads are active\n this.downloadOverlay?.startUpdating();\n try {\n await cacheProxy.requestDownload(files);\n log.info('Download request 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('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 = parseInt(String(l.file || l.id || l).replace('.xlf', ''), 10);\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 = parseInt(String(l.file || l.id || l).replace('.xlf', ''), 10);\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 }\n\n log.debug('Current scheduleId for stats:', this.currentScheduleId);\n });\n\n this.core.on('layout-prepare-request', async (layoutId: number) => {\n await this.prepareAndRenderLayout(layoutId);\n });\n\n this.core.on('layout-already-playing', () => {\n // Layout already playing, no action needed\n });\n\n this.core.on('no-layouts-scheduled', () => {\n this.updateStatus('No layouts scheduled');\n });\n\n this.core.on('collection-complete', () => {\n this.updateStatus('Collection complete');\n });\n\n this.core.on('collection-error', (error: any) => {\n this.updateStatus(`Collection error: ${error}`, 'error');\n\n // Report fault to CMS (triggers dashboard alert)\n this.logReporter?.reportFault(\n 'COLLECTION_FAILED',\n `Collection cycle failed: ${error?.message || error}`\n );\n });\n\n this.core.on('xmr-connected', (url: string) => {\n log.info('XMR connected:', url);\n });\n\n this.core.on('xmr-misconfigured', (info: { reason: string; url?: string; message: string }) => {\n log.warn(`XMR misconfigured (${info.reason}): ${info.message}`);\n console.warn(\n `%c[XMR] ${info.message}`,\n 'background: #ff9800; color: #000; padding: 4px 8px; font-weight: bold;'\n );\n });\n\n // React to CMS log level changes — toggle download overlay at runtime\n this.core.on('log-level-changed', () => {\n const debugNow = isDebug();\n log.info(`Log level changed, debug=${debugNow}`);\n\n if (debugNow && !this.downloadOverlay) {\n this.downloadOverlay = new DownloadOverlay(getDefaultOverlayConfig(), cacheProxy);\n log.info('Download overlay enabled (log level → DEBUG)');\n } else if (!debugNow && this.downloadOverlay) {\n this.downloadOverlay.destroy();\n this.downloadOverlay = null;\n log.info('Download overlay disabled (log level above DEBUG)');\n }\n });\n\n // Overlay layout push from XMR\n this.core.on('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.prepareAndRenderLayout(layoutId);\n });\n\n // Revert to schedule (undo XMR layout override)\n this.core.on('revert-to-schedule', () => {\n log.info('Reverting to scheduled content');\n this.updateStatus('Reverting to schedule...');\n });\n\n // Purge all cache\n this.core.on('purge-all-request', async () => {\n log.info('Purging all cached content...');\n this.updateStatus('Purging cache...');\n try {\n // Delete all caches\n const cacheNames = await caches.keys();\n await Promise.all(cacheNames.map(name => caches.delete(name)));\n log.info(`Purged ${cacheNames.length} caches`);\n\n // Re-initialize cache after purge\n await cacheManager.init();\n } catch (error) {\n log.error('Cache purge failed:', error);\n }\n });\n\n // Command execution result\n this.core.on('command-result', (result: any) => {\n log.info('Command result:', result);\n if (!result.success) {\n this.logReporter?.reportFault(\n 'COMMAND_FAILED',\n `Command ${result.code} failed: ${result.reason || 'unknown'}`\n );\n }\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('submit-stats-request', async () => {\n await this.submitStats();\n });\n\n // Log submission to CMS\n this.core.on('submit-logs-request', async () => {\n await this.submitLogs();\n });\n\n // Screenshot capture (triggered by XMR or periodic interval)\n this.core.on('screenshot-request', async () => {\n await this.captureAndSubmitScreenshot();\n });\n\n // Listen for media downloads completing\n window.addEventListener('media-cached', async (event: any) => {\n const mediaId = event.detail?.id;\n log.debug(`Media ${mediaId} download completed`);\n\n // Notify core that media is ready\n this.core.notifyMediaReady(mediaId);\n });\n\n // Handle check-pending-layout events\n // Re-run prepareAndRenderLayout which checks XLF + actual media IDs correctly\n // (avoids the bug where setPendingLayout(id,[id]) treated layoutId as mediaId)\n this.core.on('check-pending-layout', async (layoutId: number) => {\n await this.prepareAndRenderLayout(layoutId);\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 navigator.serviceWorker?.addEventListener('message', (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 }\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 let data: any = {};\n try { data = body ? JSON.parse(body) : {}; } catch (_) {}\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 let data: any = {};\n try { data = body ? JSON.parse(body) : {}; } catch (_) {}\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 let data: any = {};\n try { data = body ? JSON.parse(body) : {}; } catch (_) {}\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 let data: any = {};\n try { data = body ? JSON.parse(body) : {}; } catch (_) {}\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 let data: any = {};\n try { data = body ? JSON.parse(body) : {}; } catch (_) {}\n this.logReporter?.reportFault(\n data.code || 'WIDGET_FAULT',\n data.reason || 'Widget reported fault'\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 // Return data as JSON (stringify if it's an object, pass through if already a string)\n const responseBody = typeof connectorData === 'string' ? connectorData : JSON.stringify(connectorData);\n return { status: 200, body: responseBody };\n }\n\n default:\n return { status: 404, body: JSON.stringify({ error: 'Unknown IC route' }) };\n }\n }\n\n /**\n * Setup Service Worker event handlers (bridges SW messages to PlayerCore)\n */\n private setupServiceWorkerEventHandlers() {\n if (!navigator.serviceWorker) return;\n\n navigator.serviceWorker.addEventListener('message', (event: any) => {\n const { type, fileId, fileType } = event.data;\n\n if (type === 'FILE_CACHED') {\n log.debug(`Service Worker cached ${fileType}/${fileId}`);\n\n // Notify PlayerCore that file is ready\n // Pass fileType so PlayerCore can distinguish layout files from media files\n if (fileType === 'media' || fileType === 'layout') {\n this.core.notifyMediaReady(parseInt(fileId), fileType);\n }\n }\n });\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 this.core.setCurrentLayout(layoutId);\n\n // Track stats: start layout\n if (this.statsCollector) {\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\n if (this.statsCollector) {\n this.statsCollector.endLayout(layoutId, this.currentScheduleId).catch((err: any) => {\n log.error('Failed to end layout stat:', err);\n });\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\n if (this.statsCollector && mediaId) {\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\n if (this.statsCollector && mediaId) {\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 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.logReporter?.reportFault(\n error.type || 'RENDERER_ERROR',\n `Renderer error: ${error.message || error.type} (layout ${error.layoutId || 'unknown'})`\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\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\n if (this.renderer.layoutPool.has(nextLayoutId)) {\n log.debug(`Layout ${nextLayoutId} already in preload pool`);\n return;\n }\n\n log.info(`Preloading next layout ${nextLayoutId}...`);\n\n // Get XLF from cache\n const xlfBlob = await cacheManager.getCachedFile('layout', 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\n // Check if all required media is cached\n const requiredMedia = await this.getRequiredMediaIds(xlfXml);\n const videoMediaIds = this.getVideoMediaIds(xlfXml);\n const allMediaCached = await this.checkAllMediaCached(requiredMedia, videoMediaIds);\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 prepareAndRenderLayout)\n await this.fetchWidgetHtml(xlfXml, nextLayoutId);\n\n // Pre-warm video chunks in SW BlobCache\n if (videoMediaIds.length > 0) {\n await cacheProxy.prewarmVideoChunks(videoMediaIds);\n }\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\n /**\n * Prepare and render layout (Platform-specific logic)\n */\n private async prepareAndRenderLayout(layoutId: number) {\n // Guard: skip if already playing this layout (another event already rendered it)\n if (this.core.getCurrentLayoutId() === layoutId) {\n log.debug(`Layout ${layoutId} already playing, skipping duplicate prepare`);\n return;\n }\n\n // Guard: prevent concurrent preparations of the same layout\n // (e.g., two check-pending-layout events firing close together)\n if (this.preparingLayoutId === layoutId) {\n log.debug(`Layout ${layoutId} preparation already in progress, skipping`);\n return;\n }\n\n this.preparingLayoutId = layoutId;\n try {\n // Get XLF from cache\n const xlfBlob = await cacheManager.getCachedFile('layout', 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, [layoutId]);\n this.updateStatus(`Downloading layout ${layoutId}...`);\n return;\n }\n\n const xlfXml = await xlfBlob.text();\n\n // Check if all required media is cached\n const requiredMedia = await this.getRequiredMediaIds(xlfXml);\n const videoMediaIds = this.getVideoMediaIds(xlfXml);\n const allMediaCached = await this.checkAllMediaCached(requiredMedia, videoMediaIds);\n\n if (!allMediaCached) {\n // Tell SW to prioritize this layout's media over other downloads.\n // This moves pending media to the front of the queue and avoids\n // competing for bandwidth with media needed by other layouts.\n for (const mediaId of requiredMedia) {\n cacheProxy.prioritizeDownload('media', String(mediaId));\n }\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 // Pre-fetch common widget dependencies (bundle.min.js, fonts.css)\n await this.prefetchWidgetDependencies();\n\n // Fetch widget HTML for all widgets in the layout\n await this.fetchWidgetHtml(xlfXml, layoutId);\n\n // Pre-warm video chunks in SW BlobCache (first + last chunks for moov atom)\n if (videoMediaIds.length > 0) {\n log.info(`Pre-warming ${videoMediaIds.length} video file(s) for layout ${layoutId}`);\n await cacheProxy.prewarmVideoChunks(videoMediaIds);\n }\n\n // Render layout\n await this.renderer.renderLayout(xlfXml, layoutId);\n this.updateStatus(`Playing layout ${layoutId}`);\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.logReporter?.reportFault(\n 'LAYOUT_LOAD_FAILED',\n `Failed to prepare layout ${layoutId}: ${error?.message || error}`\n );\n } finally {\n this.preparingLayoutId = null;\n }\n }\n\n /**\n * Get all required media file IDs from layout XLF\n */\n private async getRequiredMediaIds(xlfXml: string): Promise<number[]> {\n const parser = new DOMParser();\n const doc = parser.parseFromString(xlfXml, 'text/xml');\n const mediaIds: number[] = [];\n\n // Find all media elements with fileId\n const mediaElements = doc.querySelectorAll('media[fileId]');\n mediaElements.forEach(el => {\n const fileId = el.getAttribute('fileId');\n if (fileId) {\n mediaIds.push(parseInt(fileId, 10));\n }\n });\n\n // Include background image file ID from layout element\n const layoutEl = doc.querySelector('layout');\n const bgFileId = layoutEl?.getAttribute('background');\n if (bgFileId) {\n const parsed = parseInt(bgFileId, 10);\n if (!isNaN(parsed) && !mediaIds.includes(parsed)) {\n mediaIds.push(parsed);\n }\n }\n\n return mediaIds;\n }\n\n /**\n * Get media file IDs for video widgets in the layout XLF\n */\n private getVideoMediaIds(xlfXml: string): number[] {\n const parser = new DOMParser();\n const doc = parser.parseFromString(xlfXml, 'text/xml');\n const videoIds: number[] = [];\n\n doc.querySelectorAll('media[type=\"video\"]').forEach(el => {\n const fileId = el.getAttribute('fileId');\n if (fileId) {\n videoIds.push(parseInt(fileId, 10));\n }\n });\n\n return videoIds;\n }\n\n /**\n * Check if all required media files are cached and ready\n */\n private async checkAllMediaCached(mediaIds: number[], videoMediaIds: number[] = []): Promise<boolean> {\n for (const mediaId of mediaIds) {\n try {\n // Use CacheProxy API - it delegates to SW's CacheManager.fileExists()\n const exists = await cacheProxy.hasFile('media', String(mediaId));\n\n if (!exists) {\n log.debug(`Media ${mediaId} not yet cached`);\n return false;\n }\n\n // File exists (either whole file or chunks) - now validate it\n // Check for whole-file storage first\n const response = await cacheManager.getCachedResponse('media', mediaId);\n\n if (!response) {\n // Chunked storage - check readiness based on file type\n const cache = await caches.open('xibo-media-v1');\n const metadataResponse = await cache.match(`${PLAYER_BASE}/cache/media/${mediaId}/metadata`);\n\n if (metadataResponse) {\n const metadataText = await metadataResponse.text();\n const metadata = JSON.parse(metadataText);\n const sizeMB = (metadata.totalSize / 1024 / 1024).toFixed(1);\n const isVideo = videoMediaIds.includes(mediaId);\n\n if (isVideo) {\n // Video: early playback — need chunk 0 (ftyp header) + last chunk (moov atom).\n // Download manager prioritizes these two chunks first (out-of-order download).\n // SW's Range handler retries up to 60s per chunk for middle ones still downloading.\n const chunk0 = await cache.match(`${PLAYER_BASE}/cache/media/${mediaId}/chunk-0`);\n if (!chunk0) {\n log.debug(`Media ${mediaId} video: chunk 0 not yet available`);\n return false;\n }\n const lastIdx = metadata.numChunks - 1;\n if (lastIdx > 0) {\n const lastChunk = await cache.match(`${PLAYER_BASE}/cache/media/${mediaId}/chunk-${lastIdx}`);\n if (!lastChunk) {\n log.debug(`Media ${mediaId} video: last chunk (${lastIdx}) not yet available`);\n return false;\n }\n }\n log.info(`Media ${mediaId} video ready for early playback (chunk 0 + ${lastIdx} of ${metadata.numChunks}, ${sizeMB} MB total)`);\n } else {\n // Non-video: require all chunks (last chunk present = all downloaded)\n const lastChunkKey = `${PLAYER_BASE}/cache/media/${mediaId}/chunk-${metadata.numChunks - 1}`;\n const lastChunk = await cache.match(lastChunkKey);\n if (!lastChunk) {\n log.debug(`Media ${mediaId} chunked but still downloading (chunk ${metadata.numChunks - 1} missing)`);\n return false;\n }\n log.debug(`Media ${mediaId} cached as chunks (${metadata.numChunks} x ${(metadata.chunkSize / 1024 / 1024).toFixed(0)} MB = ${sizeMB} MB total)`);\n }\n continue;\n }\n }\n\n // Validate cached file (detect corrupted entries)\n const contentType = response.headers.get('Content-Type') || '';\n const blob = await response.blob();\n\n // Check for bad cache\n if (contentType === 'text/plain' || blob.size < 100) {\n log.warn(`Media ${mediaId} corrupted (${contentType}, ${blob.size} bytes) - will re-download`);\n\n // Delete bad cache entry\n const cache = await caches.open('xibo-media-v1');\n const cacheKey = `${PLAYER_BASE}/cache/media/${mediaId}`;\n await cache.delete(cacheKey);\n\n return false;\n }\n\n // Format size appropriately (KB for small files, MB for large)\n const sizeKB = blob.size / 1024;\n const sizeMB = sizeKB / 1024;\n const sizeStr = sizeMB >= 1 ? `${sizeMB.toFixed(1)} MB` : `${sizeKB.toFixed(1)} KB`;\n log.debug(`Media ${mediaId} cached and valid (${sizeStr})`);\n\n } catch (error) {\n log.warn(`Unable to verify media ${mediaId}, assuming cached (offline mode)`);\n }\n }\n return true;\n }\n\n /**\n * Pre-fetch common widget dependencies (bundle.min.js, fonts.css)\n * These are downloaded by the SW via signed URLs from RequiredFiles.\n * Just check the SW's static cache — don't construct manual URLs.\n */\n private async prefetchWidgetDependencies() {\n const filenames = ['bundle.min.js', 'fonts.css'];\n\n const cache = await caches.open('xibo-static-v1');\n for (const filename of filenames) {\n const cached = await cache.match(`${PLAYER_BASE}/cache/static/${filename}`);\n if (cached) {\n log.debug(`Widget dependency ${filename} already cached by SW`);\n } else {\n log.debug(`Widget dependency ${filename} not yet cached (will be fetched by SW on first use)`);\n }\n }\n }\n\n /**\n * Fetch widget HTML for all widgets in layout (parallel)\n */\n private async fetchWidgetHtml(xlfXml: string, layoutId: number) {\n const parser = new DOMParser();\n const doc = parser.parseFromString(xlfXml, 'text/xml');\n\n const widgetTypes = ['clock', 'calendar', 'weather', 'currencies', 'stocks',\n 'twitter', 'global', 'embedded', 'text', 'ticker'];\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\n if (widgetTypes.some(w => type?.includes(w))) {\n const cacheKey = `/cache/widget/${layoutId}/${regionId}/${widgetId}`;\n\n fetchPromises.push(\n (async () => {\n try {\n const cache = await caches.open('xibo-media-v1');\n const cachedResponse = await cache.match(cacheKey);\n\n let html: string;\n if (cachedResponse) {\n html = await cachedResponse.text();\n log.debug(`Using cached widget HTML for ${type} ${widgetId}`);\n } else {\n html = await this.xmds.getResource(layoutId, regionId, widgetId);\n await cacheManager.cacheWidgetHtml(layoutId, regionId, widgetId, html);\n log.debug(`Retrieved widget HTML for ${type} ${widgetId}`);\n }\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 * Setup UI\n */\n private setupUI() {\n const container = document.getElementById('player-container');\n if (!container) {\n log.warn('No #player-container found');\n }\n\n this.updateConfigDisplay();\n }\n\n /**\n * Update config display\n */\n private updateConfigDisplay() {\n const configEl = document.getElementById('config-info');\n if (configEl) {\n configEl.textContent = `CMS: ${config.cmsAddress} | Display: ${config.displayName || 'Unknown'} | HW: ${config.hardwareKey}`;\n }\n }\n\n /**\n * Submit proof of play stats to CMS\n */\n private async submitStats() {\n if (!this.statsCollector) {\n log.warn('Stats collector not initialized');\n return;\n }\n\n try {\n // Get stats ready for submission (up to 50 at a time)\n // Use aggregation level from CMS settings if available\n const aggregationLevel = this.displaySettings?.getSetting('aggregationLevel') || 'Individual';\n const stats = aggregationLevel === 'Aggregate'\n ? await this.statsCollector.getAggregatedStatsForSubmission(50)\n : await this.statsCollector.getStatsForSubmission(50);\n\n if (stats.length === 0) {\n log.debug('No stats to submit');\n return;\n }\n\n log.info(`Submitting ${stats.length} proof of play stats...`);\n\n // Format stats as XML\n const statsXml = formatStats(stats);\n\n // Submit to CMS via XMDS\n const success = await this.xmds.submitStats(statsXml);\n\n if (success) {\n log.info('Stats submitted successfully');\n // Clear submitted stats from database\n await this.statsCollector.clearSubmittedStats(stats);\n log.debug(`Cleared ${stats.length} submitted stats from database`);\n } else {\n log.warn('Stats submission failed (CMS returned false)');\n }\n } catch (error) {\n log.error('Failed to submit stats:', error);\n }\n }\n\n /**\n * Submit player logs to CMS for remote debugging\n */\n private async submitLogs() {\n if (!this.logReporter) return;\n\n try {\n const logs = await this.logReporter.getLogsForSubmission(100);\n\n if (logs.length === 0) {\n log.debug('No logs to submit');\n return;\n }\n\n log.info(`Submitting ${logs.length} logs to CMS...`);\n\n const logXml = formatLogs(logs);\n const success = await this.xmds.submitLog(logXml);\n\n if (success) {\n log.info('Logs submitted successfully');\n await this.logReporter.clearSubmittedLogs(logs);\n } else {\n log.warn('Log submission failed (CMS returned false)');\n }\n } catch (error) {\n log.error('Failed to submit logs:', error);\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, works on Chrome with\n * --auto-select-desktop-capture-source flag (kiosk). Pixel-perfect,\n * includes video, composited layers, everything the GPU renders.\n * 2. html2canvas — fallback for Firefox or Chrome without the flag.\n * Re-renders the DOM to canvas; needs a video overlay workaround\n * because html2canvas can't read <video> pixels.\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 {\n base64 = await this.captureWithBrowserMethods();\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 using browser-native methods (non-Electron path)\n */\n private async captureWithBrowserMethods(): Promise<string> {\n // Try native capture first (unless we already know it doesn't work)\n if (this._screenshotMethod !== 'html2canvas') {\n const nativeResult = await this.captureNative();\n if (nativeResult) {\n this._screenshotMethod = 'native';\n return nativeResult;\n }\n this._screenshotMethod = 'html2canvas';\n log.info('Native screen capture unavailable, using html2canvas');\n }\n return this.captureHtml2Canvas();\n }\n\n /**\n * Native screen capture via getDisplayMedia().\n * Works on Chrome/Chromium launched with:\n * --auto-select-desktop-capture-source=\"Entire screen\"\n * Returns base64 JPEG or null if unavailable.\n */\n private async captureNative(): Promise<string | null> {\n if (!navigator.mediaDevices?.getDisplayMedia) return null;\n\n let stream: MediaStream | null = null;\n try {\n stream = await navigator.mediaDevices.getDisplayMedia({\n video: { displaySurface: 'browser' } as any,\n audio: false,\n // @ts-ignore — Chrome-specific hint to prefer current tab\n preferCurrentTab: true,\n });\n\n const track = stream.getVideoTracks()[0];\n // Use ImageCapture if available (Chrome), otherwise VideoFrame fallback\n if (typeof ImageCapture !== 'undefined') {\n const capture = new (ImageCapture as any)(track);\n const bitmap = await (capture as any).grabFrame();\n const canvas = document.createElement('canvas');\n canvas.width = bitmap.width;\n canvas.height = bitmap.height;\n canvas.getContext('2d')!.drawImage(bitmap, 0, 0);\n bitmap.close();\n return canvas.toDataURL('image/jpeg', 0.8).split(',')[1];\n }\n\n // Fallback: draw video track to canvas\n const video = document.createElement('video');\n video.srcObject = stream;\n video.muted = true;\n await video.play();\n // Wait one frame for the video to render\n await new Promise(r => requestAnimationFrame(r));\n const canvas = document.createElement('canvas');\n canvas.width = video.videoWidth;\n canvas.height = video.videoHeight;\n canvas.getContext('2d')!.drawImage(video, 0, 0);\n video.pause();\n return canvas.toDataURL('image/jpeg', 0.8).split(',')[1];\n } catch (_) {\n // User denied, no auto-grant, or API unavailable\n return null;\n } finally {\n // Always stop all tracks to release the capture\n stream?.getTracks().forEach(t => t.stop());\n }\n }\n\n /**\n * Capture screenshot by manually composing a canvas from visible elements.\n * - Images/video/canvas: drawn directly via ctx.drawImage() with object-fit emulation\n * - Iframes: content cloned into main document, rendered via html2canvas\n * (html2canvas fails on cross-document elements, so we clone first)\n * - Background: read from #player-container computed style\n */\n private async captureHtml2Canvas(): 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 // Background: black (matches player default)\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 // Draw container background (layout bgcolor + background image)\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 // Background image (blob URL from layout XLF)\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 // Ensure html2canvas is loaded (pre-loaded at init, fallback to lazy load)\n if (!this._html2canvasMod) {\n this._html2canvasMod = (await import('html2canvas')).default;\n }\n\n // Draw each visible widget element onto the canvas\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 // Emulate object-fit: contain — draw at correct aspect ratio within bounding rect\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 // Emulate object-fit: contain — draw at correct aspect ratio within bounding rect\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 // html2canvas fails on cross-document elements (produces transparent canvas).\n // Clone the iframe's styles + content into the main document first,\n // then run html2canvas on the clone in the main document context.\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 (iframe base may differ)\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 // Wait for each stylesheet to load (or fail) instead of arbitrary delay\n linkPromises.push(new Promise<void>(resolve => {\n newLink.onload = () => resolve();\n newLink.onerror = () => resolve();\n }));\n }\n\n // Clone body content\n captureDiv.appendChild(iDoc.body.cloneNode(true));\n document.body.appendChild(captureDiv);\n\n // Collect natural dimensions from ORIGINAL iframe images (before html2canvas clones).\n // html2canvas doesn't support object-fit, so we fix sizing in onclone.\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 // Wait for stylesheets to load (with 500ms safety timeout)\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 — widget 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 — html2canvas stretches images, ignoring object-fit.\n // Replace with explicit sizing + centering so html2canvas draws correct proportions.\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;\n drawH = cW / srcAspect;\n } else {\n drawH = cH;\n drawW = cH * srcAspect;\n }\n\n // Wrap in a flex container to center, remove object-fit\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 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 }\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 module so first capture is instant\n if (!this._html2canvasMod) {\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 if (document.getElementById('offline-indicator')) return;\n const el = document.createElement('div');\n el.id = 'offline-indicator';\n el.textContent = 'OFFLINE';\n el.style.cssText = 'position:fixed;top:8px;right:8px;background:rgba(255,60,60,0.85);color:#fff;padding:4px 12px;border-radius:4px;font-size:12px;font-weight:bold;z-index:99999;pointer-events:none;';\n document.body.appendChild(el);\n }\n\n private removeOfflineIndicator() {\n document.getElementById('offline-indicator')?.remove();\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}\n\n// Initialize player\nif (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', () => {\n const player = new PwaPlayer();\n player.init().catch(error => {\n log.error('Failed to initialize:', error);\n });\n\n window.addEventListener('beforeunload', () => {\n player.cleanup();\n });\n });\n} else {\n const player = new PwaPlayer();\n player.init().catch(error => {\n log.error('Failed to initialize:', error);\n });\n\n window.addEventListener('beforeunload', () => {\n player.cleanup();\n });\n}\n"],"file":"assets/main-C4ABDfkq.js"}
|