@xiboplayer/pwa 0.3.2 → 0.3.4
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 +10 -3
- package/package.json +10 -10
- package/dist/assets/cache-proxy-CrlayfSe.js +0 -2
- package/dist/assets/cache-proxy-CrlayfSe.js.map +0 -1
- package/dist/assets/cms-api-Ce7EVg5h.js +0 -2
- package/dist/assets/cms-api-Ce7EVg5h.js.map +0 -1
- package/dist/assets/html2canvas.esm-CBrSDip1.js +0 -23
- package/dist/assets/html2canvas.esm-CBrSDip1.js.map +0 -1
- package/dist/assets/index-BF8qB-pu.js +0 -2
- package/dist/assets/index-BF8qB-pu.js.map +0 -1
- package/dist/assets/index-Baows0WY.js +0 -2
- package/dist/assets/index-Baows0WY.js.map +0 -1
- package/dist/assets/index-Be_IxwIZ.js +0 -2
- package/dist/assets/index-Be_IxwIZ.js.map +0 -1
- package/dist/assets/index-BhHwWvzx.js +0 -2
- package/dist/assets/index-BhHwWvzx.js.map +0 -1
- package/dist/assets/index-C77HSi9N.js +0 -8
- package/dist/assets/index-C77HSi9N.js.map +0 -1
- package/dist/assets/index-ChPoQ8Bt.js +0 -607
- package/dist/assets/index-ChPoQ8Bt.js.map +0 -1
- package/dist/assets/index-DPR3fBRV.js +0 -2
- package/dist/assets/index-DPR3fBRV.js.map +0 -1
- package/dist/assets/index-b1tfCACR.js +0 -2
- package/dist/assets/index-b1tfCACR.js.map +0 -1
- package/dist/assets/index-es8y3c70.js +0 -2
- package/dist/assets/index-es8y3c70.js.map +0 -1
- package/dist/assets/main-BUvkpHsV.js +0 -44
- package/dist/assets/main-BUvkpHsV.js.map +0 -1
- package/dist/assets/modulepreload-polyfill-B5Qt9EMX.js +0 -2
- package/dist/assets/modulepreload-polyfill-B5Qt9EMX.js.map +0 -1
- package/dist/assets/pdf-BnPRJEQ6.js +0 -13
- package/dist/assets/pdf-BnPRJEQ6.js.map +0 -1
- package/dist/assets/setup-B9GCkQRS.js +0 -2
- package/dist/assets/setup-B9GCkQRS.js.map +0 -1
- package/dist/assets/xmds-client-MaDHqpeL.js +0 -16
- package/dist/assets/xmds-client-MaDHqpeL.js.map +0 -1
- package/dist/index.html +0 -130
- package/dist/setup.html +0 -371
- package/dist/sw-pwa.js +0 -2
- package/dist/sw-pwa.js.map +0 -1
- package/dist/sw.test.js +0 -271
|
@@ -1 +0,0 @@
|
|
|
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","rid","idx","tagName","el","playAfterSeek","videoEl","resolve","timer","onPlaying","imgEl","onLoad","readyPromises","widgetElement","animPromise","animation","audioEl","showFn","hideFn","onCycleComplete","playNext","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","elapsed","fn","DAY_NAMES","getMetricValue","metric","now","displayProperties","evaluateCondition","actual","condition","expected","a","evaluateCriteria","criteria","criterion","ScheduleManager","schedule","item","currentDayOfWeek","d","rangeEnd","date","day","from","to","currentTime","fromTime","toTime","time","results","campaign","skipRateLimiting","skipInterrupts","quiet","_log","activeItems","maxPriority","allLayouts","l","normalLayouts","interruptLayouts","lastCheck","maxPlaysPerHour","oneHourAgo","playsInLastHour","timestamp","minGapMs","lastPlayTime","remainingMin","history","cleaned","layoutFile","meta","triggerCode","latitude","longitude","properties","geoLocation","defaultRadius","parts","s","fenceLat","fenceLng","radius","distance","within","lat1","lon1","lat2","lon2","toRad","deg","dLat","dLon","scheduleManager","logger","InterruptScheduler","current","requiredSeconds","resolvedInterruptLayouts","interruptSecondsInHour","index","satisfied","allSatisfied","currentInterrupt","normalSecondsInHour","resolvedNormalLayouts","loop","layouts","targetSeconds","resolved","remainingSeconds","pickCount","normalPick","interruptPick","normalIndex","interruptIndex","totalSecondsAllocated","OverlayScheduler","overlays","activeOverlays","b","priorityA","o","parseLayoutDuration","explicit","dur","arraysEqual","canSimulatedPlay","timeMs","t","lastPlay","seedPlayHistory","realHistory","simulated","timestamps","file","getPlayableLayouts","simPlays","eligible","calculateTimeline","durations","hours","defaultDuration","timeline","hasFullApi","maxEntries","playable","defaultFile","endMs","nextAll","nextPlayable","next","DataConnectorManager","EventEmitter","connectors","connector","dataKey","intervalMs","keys","response","fetchWithRetry","contentType","previousData","OFFLINE_DB_NAME","OFFLINE_DB_VERSION","OFFLINE_STORE","parseLayoutFile","f","openOfflineDb","reject","req","db","PlayerCore","store","settings","requiredFiles","r","key","tx","cachedReg","cachedSchedule","layoutFiles","prefix","regResult","applyCmsLogLevel","checkRf","checkSchedule","allFiles","purgeFiles","files","layoutIds","layoutOrder","defaultLayoutId","_b","_c","xmrUrl","xmrCmsKey","_d","collectIntervalSeconds","newIntervalSeconds","seconds","requiredMediaIds","replayId","fileType","isLayoutFile","isRequiredMedia","commandCode","commands","command","commandString","success","inventoryXml","mediaId","reason","syncManager","parsed","lines","end","prev","DownloadOverlay","__publicField","mc","progressPromise","progress","downloads","percent","downloaded","total","bytes","kb","mb","enabled","getDefaultOverlayConfig","showDownloads","savedPref","TimelineOverlay","visible","offline","currentLayoutId","entries","maxVisible","offlineBadge","isCurrent","startStr","endStr","durStr","marker","m","isTimelineVisible","showTimeline","saved","PLAYER_BASE","cacheManager","RestClient","XmdsClient","XmrWrapper","cacheProxy","StatsCollector","formatStats","LogReporter","formatLogs","DisplaySettings","sdkVersions","PwaPlayer","registration","CacheProxy","streamingUrl","cacheKey","lat","lng","overlayConfig","cacheModule","n","xmdsModule","scheduleModule","configModule","xmrModule","statsModule","displaySettingsModule","coreModule","rendererModule","sysInfo","registerLogSink","level","name","message","buildDate","appVersion","versionParts","k","isElectron","electronVersion","chromeVersion","platform","displayName","isOffline","groupedFiles","scheduledIds","c","cleared","debugNow","cacheNames","newInterval","_settings","changes","method","path","search","body","port","connectorData","_layout","pending","actionType","layoutCode","targetId","nextLayoutId","xlfBlob","requiredMedia","videoMediaIds","allMedia","videoMedia","bgFileId","mediaIds","cache","metadataResponse","metadataText","metadata","sizeMB","lastIdx","lastChunkKey","sizeKB","sizeStr","widgetTypes","fetchPromises","cachedResponse","newRaw","cleanup","configEl","version","versionStr","stats","statsXml","logs","logXml","base64","electronResult","ctx","containerRect","containerStyle","bgColor","bgImage","urlMatch","bgImg","__vite_default__","elements","drawn","htmlEl","rect","iDoc","captureDiv","linkPromises","styleEl","linkEl","newLink","origImgs","imgNaturals","iframeCanvas","clonedDoc","cImg","style","dims","cW","cH","srcAspect","dstAspect","drawW","drawH","srcW","srcH","h","intervalSecs","statusEl","startPlayer","player"],"ignoreList":[0],"sources":["../../../xiboplayer/node_modules/.pnpm/nanoevents@9.1.0/node_modules/nanoevents/index.js","../../../xiboplayer/packages/renderer/src/layout-pool.js","../../../xiboplayer/packages/renderer/src/renderer-lite.js","../../../xiboplayer/packages/schedule/src/criteria.js","../../../xiboplayer/packages/schedule/src/schedule.js","../../../xiboplayer/packages/schedule/src/interrupts.js","../../../xiboplayer/packages/schedule/src/overlays.js","../../../xiboplayer/packages/schedule/src/timeline.js","../../../xiboplayer/packages/core/src/data-connectors.js","../../../xiboplayer/packages/core/src/player-core.js","../../src/download-overlay.ts","../../src/timeline-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._paused = false;\n this._layoutTimerStartedAt = null; // Date.now() when layout timer started\n this._layoutTimerDurationMs = null; // Total layout duration in ms\n this._layoutTimerRemaining = null; // ms remaining when paused\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 this.emit('layoutDurationUpdated', this.currentLayoutId, maxRegionDuration);\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 this._startRegionCycle(\n region, regionId,\n (rid, idx) => this.renderWidget(rid, idx),\n (rid, idx) => this.stopWidget(rid, idx),\n () => {\n this.log.info(`Region ${regionId} completed one full cycle`);\n this.checkLayoutComplete();\n }\n );\n }\n\n /**\n * Create a widget element (extracted for pre-creation)\n * @param {Object} widget - Widget config\n * @param {Object} region - Region state\n * @returns {Promise<HTMLElement>} Widget DOM element\n */\n async createWidgetElement(widget, region) {\n 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 // Restart video or audio on widget show (even if looping)\n const mediaEl = this.findMediaElement(element, 'VIDEO') || this.findMediaElement(element, 'AUDIO');\n if (mediaEl) {\n this._restartMediaElement(mediaEl);\n this.log.info(`${mediaEl.tagName === 'VIDEO' ? 'Video' : 'Audio'} restarted: ${widget.fileId || widget.id}`);\n }\n }\n\n /**\n * Restart a media element from the beginning.\n * Waits for seek to complete before playing — avoids DOMException\n * \"The play() request was interrupted\" when calling play() mid-seek.\n */\n _restartMediaElement(el) {\n el.currentTime = 0;\n const playAfterSeek = () => {\n el.removeEventListener('seeked', playAfterSeek);\n el.play().catch(() => {});\n };\n el.addEventListener('seeked', playAfterSeek);\n // Fallback: if seeked doesn't fire (already at 0), try play directly\n if (el.currentTime === 0 && el.readyState >= 2) {\n el.removeEventListener('seeked', playAfterSeek);\n el.play().catch(() => {});\n }\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._layoutTimerStartedAt = Date.now();\n this._layoutTimerDurationMs = layoutDurationMs;\n this.layoutTimer = setTimeout(() => {\n this.log.info(`Layout ${layoutId} duration expired (${layout.duration}s)`);\n if (this.currentLayoutId) {\n this.layoutEndEmitted = true;\n this.emit('layoutEnd', this.currentLayoutId);\n }\n }, layoutDurationMs);\n }\n\n /**\n * Render a widget in a region (using element reuse)\n * @param {string} regionId - Region ID\n * @param {number} widgetIndex - Widget index in region\n */\n /**\n * Core: show a widget in a region (shared by main layout + overlay)\n * Returns the widget object on success, null on failure.\n */\n async _showWidget(region, widgetIndex) {\n const widget = region.widgets[widgetIndex];\n if (!widget) return null;\n\n let element = region.widgetElements.get(widget.id);\n\n if (!element) {\n this.log.warn(`Widget ${widget.id} not pre-created, creating now`);\n element = await this.createWidgetElement(widget, region);\n 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 this.updateMediaElement(element, widget);\n element.style.visibility = 'visible';\n\n if (widget.transitions.in) {\n Transitions.apply(element, widget.transitions.in, true, region.width, region.height);\n } else {\n element.style.opacity = '1';\n }\n\n return widget;\n }\n\n /**\n * Core: hide a widget in a region (shared by main layout + overlay).\n * Returns { widget, animPromise } synchronously — callers await animPromise if needed.\n * NOT async, so callers that don't need the animation stay on the same microtask.\n */\n _hideWidget(region, widgetIndex) {\n const widget = region.widgets[widgetIndex];\n if (!widget) return { widget: null, animPromise: null };\n\n const widgetElement = region.widgetElements.get(widget.id);\n if (!widgetElement) return { widget: null, animPromise: null };\n\n let animPromise = null;\n if (widget.transitions.out) {\n const animation = Transitions.apply(\n widgetElement, widget.transitions.out, false, region.width, region.height\n );\n if (animation) {\n animPromise = new Promise(resolve => { animation.onfinish = resolve; });\n }\n }\n\n const videoEl = widgetElement.querySelector('video');\n if (videoEl && widget.options.loop !== '1') videoEl.pause();\n\n const audioEl = widgetElement.querySelector('audio');\n if (audioEl && widget.options.loop !== '1') audioEl.pause();\n\n return { widget, animPromise };\n }\n\n /**\n * Core: cycle through widgets in a region (shared by main layout + overlay)\n * @param {Object} region - Region state object\n * @param {string} regionId - Region ID\n * @param {Function} showFn - (regionId, widgetIndex) => show widget\n * @param {Function} hideFn - (regionId, widgetIndex) => hide widget\n * @param {Function} [onCycleComplete] - Called when region completes one full cycle\n */\n _startRegionCycle(region, regionId, showFn, hideFn, onCycleComplete) {\n if (!region || region.widgets.length === 0) return;\n\n if (region.widgets.length === 1) {\n showFn(regionId, 0);\n return;\n }\n\n const playNext = () => {\n const widgetIndex = region.currentIndex;\n const widget = region.widgets[widgetIndex];\n\n showFn(regionId, widgetIndex);\n\n const duration = widget.duration * 1000;\n region.timer = setTimeout(() => {\n hideFn(regionId, widgetIndex);\n\n const nextIndex = (region.currentIndex + 1) % region.widgets.length;\n if (nextIndex === 0 && !region.complete) {\n region.complete = true;\n onCycleComplete?.();\n }\n\n region.currentIndex = nextIndex;\n playNext();\n }, duration);\n };\n\n playNext();\n }\n\n async renderWidget(regionId, widgetIndex) {\n const region = this.regions.get(regionId);\n if (!region) return;\n\n try {\n const widget = await this._showWidget(region, widgetIndex);\n if (widget) {\n this.log.info(`Showing widget ${widget.type} (${widget.id}) in region ${regionId}`);\n this.emit('widgetStart', {\n widgetId: widget.id, regionId, layoutId: this.currentLayoutId,\n mediaId: parseInt(widget.fileId || widget.id) || null,\n type: widget.type, duration: widget.duration\n });\n }\n } catch (error) {\n this.log.error(`Error rendering widget:`, error);\n this.emit('error', { type: 'widgetError', error, widgetId: region.widgets[widgetIndex]?.id, regionId });\n }\n }\n\n /**\n * Stop a widget (with element reuse - don't revoke blob URLs!)\n * @param {string} regionId - Region ID\n * @param {number} widgetIndex - Widget index\n */\n async stopWidget(regionId, widgetIndex) {\n const region = this.regions.get(regionId);\n if (!region) return;\n\n const { widget, animPromise } = this._hideWidget(region, widgetIndex);\n if (animPromise) await animPromise;\n if (widget) {\n this.emit('widgetEnd', {\n widgetId: widget.id, regionId, layoutId: this.currentLayoutId,\n mediaId: parseInt(widget.fileId || widget.id) || null,\n type: widget.type\n });\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 this._startRegionCycle(\n region, regionId,\n (rid, idx) => this.renderOverlayWidget(overlayId, rid, idx),\n (rid, idx) => this.stopOverlayWidget(overlayId, rid, idx),\n () => this.log.info(`Overlay ${overlayId} region ${regionId} completed one full cycle`)\n );\n }\n\n /**\n * Render a widget in an overlay region\n * @param {number} overlayId - Overlay layout ID\n * @param {string} regionId - Region ID\n * @param {number} widgetIndex - Widget index in region\n */\n async renderOverlayWidget(overlayId, regionId, widgetIndex) {\n const overlayState = this.activeOverlays.get(overlayId);\n if (!overlayState) return;\n\n const region = overlayState.regions.get(regionId);\n if (!region) return;\n\n try {\n const widget = await this._showWidget(region, widgetIndex);\n if (widget) {\n this.log.info(`Showing overlay widget ${widget.type} (${widget.id}) in overlay ${overlayId} region ${regionId}`);\n this.emit('overlayWidgetStart', {\n overlayId, widgetId: widget.id, regionId,\n type: widget.type, duration: widget.duration\n });\n }\n } catch (error) {\n this.log.error(`Error rendering overlay widget:`, error);\n this.emit('error', { type: 'overlayWidgetError', error, widgetId: region.widgets[widgetIndex]?.id, regionId, overlayId });\n }\n }\n\n /**\n * Stop an overlay widget\n * @param {number} overlayId - Overlay layout ID\n * @param {string} regionId - Region ID\n * @param {number} widgetIndex - Widget index\n */\n async stopOverlayWidget(overlayId, regionId, widgetIndex) {\n const overlayState = this.activeOverlays.get(overlayId);\n if (!overlayState) return;\n\n const region = overlayState.regions.get(regionId);\n if (!region) return;\n\n const { widget, animPromise } = this._hideWidget(region, widgetIndex);\n if (animPromise) await animPromise;\n if (widget) {\n this.emit('overlayWidgetEnd', {\n overlayId, widgetId: widget.id, regionId, type: widget.type\n });\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 * Pause playback: stop layout timer, pause all media, stop widget cycling.\n * The layout timer's remaining time is saved so resume() can restart it.\n */\n pause() {\n if (this._paused) return;\n this._paused = true;\n\n // Save remaining layout time\n if (this.layoutTimer && this._layoutTimerStartedAt) {\n const elapsed = Date.now() - this._layoutTimerStartedAt;\n this._layoutTimerRemaining = Math.max(0, this._layoutTimerDurationMs - elapsed);\n clearTimeout(this.layoutTimer);\n this.layoutTimer = null;\n }\n\n // Stop all region widget-cycling timers\n for (const [, region] of this.regions) {\n if (region.timer) {\n clearTimeout(region.timer);\n region.timer = null;\n }\n }\n\n // Pause all video/audio elements\n this._forEachMedia(el => el.pause());\n\n this.emit('paused');\n this.log.info('Playback paused');\n }\n\n /**\n * Resume playback: restart layout timer with remaining time, resume media and widget cycling.\n */\n resume() {\n if (!this._paused) return;\n this._paused = false;\n\n // Resume layout timer with remaining time\n if (this._layoutTimerRemaining != null && this._layoutTimerRemaining > 0) {\n this._layoutTimerStartedAt = Date.now();\n this._layoutTimerDurationMs = this._layoutTimerRemaining;\n const layoutId = this.currentLayoutId;\n this.layoutTimer = setTimeout(() => {\n this.log.info(`Layout ${layoutId} duration expired (resumed)`);\n if (this.currentLayoutId) {\n this.layoutEndEmitted = true;\n this.emit('layoutEnd', this.currentLayoutId);\n }\n }, this._layoutTimerRemaining);\n this._layoutTimerRemaining = null;\n }\n\n // Resume all video/audio\n this._forEachMedia(el => el.play().catch(() => {}));\n\n // Restart region widget cycling (re-enters cycle from current widget)\n for (const [regionId] of this.regions) {\n this.startRegion(regionId);\n }\n\n this.emit('resumed');\n this.log.info('Playback resumed');\n }\n\n /**\n * Apply a function to every video/audio element in all regions.\n */\n _forEachMedia(fn) {\n for (const [, region] of this.regions) {\n region.element?.querySelectorAll('video, audio').forEach(fn);\n }\n }\n\n /**\n * Cleanup renderer\n */\n cleanup() {\n this.stopAllOverlays();\n this.stopCurrentLayout();\n\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 * Criteria Evaluator\n *\n * Evaluates schedule criteria against current player state.\n * Criteria are conditions set in the CMS that determine whether\n * a layout/overlay should display on a given player.\n *\n * Supported metrics:\n * - dayOfWeek: Current day name (Monday-Sunday)\n * - dayOfMonth: Day number (1-31)\n * - month: Month number (1-12)\n * - hour: Hour (0-23)\n * - isoDay: ISO day of week (1=Monday, 7=Sunday)\n *\n * Supported conditions:\n * - equals, notEquals\n * - greaterThan, greaterThanOrEquals, lessThan, lessThanOrEquals\n * - contains, notContains, startsWith, endsWith\n * - in (comma-separated list)\n *\n * Display property metrics are resolved via a property map\n * provided at evaluation time.\n */\n\nimport { createLogger } from '@xiboplayer/utils';\n\nconst log = createLogger('schedule:criteria');\n\nconst DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];\n\n/**\n * Get built-in metric value from current date/time\n * @param {string} metric - Metric name\n * @param {Date} now - Current date\n * @param {Object} displayProperties - Display property map from CMS\n * @returns {string|null} Metric value or null if unknown\n */\nfunction getMetricValue(metric, now, displayProperties = {}) {\n switch (metric) {\n case 'dayOfWeek':\n return DAY_NAMES[now.getDay()];\n case 'dayOfMonth':\n return String(now.getDate());\n case 'month':\n return String(now.getMonth() + 1);\n case 'hour':\n return String(now.getHours());\n case 'isoDay':\n return String(now.getDay() === 0 ? 7 : now.getDay());\n default:\n // Check display properties (custom fields set in CMS)\n if (displayProperties[metric] !== undefined) {\n return String(displayProperties[metric]);\n }\n log.debug(`Unknown metric: ${metric}`);\n return null;\n }\n}\n\n/**\n * Evaluate a single condition\n * @param {string} actual - Actual value from player state\n * @param {string} condition - Condition operator\n * @param {string} expected - Expected value from criteria\n * @param {string} type - Value type ('string' or 'number')\n * @returns {boolean}\n */\nfunction evaluateCondition(actual, condition, expected, type) {\n if (actual === null) return false;\n\n // Number comparison\n if (type === 'number') {\n const a = parseFloat(actual);\n const e = parseFloat(expected);\n if (isNaN(a) || isNaN(e)) return false;\n\n switch (condition) {\n case 'equals': return a === e;\n case 'notEquals': return a !== e;\n case 'greaterThan': return a > e;\n case 'greaterThanOrEquals': return a >= e;\n case 'lessThan': return a < e;\n case 'lessThanOrEquals': return a <= e;\n default: return false;\n }\n }\n\n // String comparison (case-insensitive)\n const a = actual.toLowerCase();\n const e = expected.toLowerCase();\n\n switch (condition) {\n case 'equals': return a === e;\n case 'notEquals': return a !== e;\n case 'contains': return a.includes(e);\n case 'notContains': return !a.includes(e);\n case 'startsWith': return a.startsWith(e);\n case 'endsWith': return a.endsWith(e);\n case 'in': return e.split(',').map(s => s.trim().toLowerCase()).includes(a);\n case 'greaterThan': return a > e;\n case 'lessThan': return a < e;\n default:\n log.debug(`Unknown condition: ${condition}`);\n return false;\n }\n}\n\n/**\n * Evaluate all criteria for a schedule item.\n * All criteria must match (AND logic) for the item to display.\n *\n * @param {Array<{metric: string, condition: string, type: string, value: string}>} criteria\n * @param {Object} options\n * @param {Date} [options.now] - Current date (defaults to new Date())\n * @param {Object} [options.displayProperties] - Display property map from CMS\n * @returns {boolean} True if all criteria match (or no criteria)\n */\nexport function evaluateCriteria(criteria, options = {}) {\n if (!criteria || criteria.length === 0) return true;\n\n const now = options.now || new Date();\n const displayProperties = options.displayProperties || {};\n\n for (const criterion of criteria) {\n const actual = getMetricValue(criterion.metric, now, displayProperties);\n const matches = evaluateCondition(actual, criterion.condition, criterion.value, criterion.type);\n\n if (!matches) {\n log.debug(`Criteria failed: ${criterion.metric} ${criterion.condition} \"${criterion.value}\" (actual: \"${actual}\")`);\n return false;\n }\n }\n\n return true;\n}\n","/**\n * Schedule manager - determines which layouts to show\n */\n\nimport { createLogger } from '@xiboplayer/utils';\nimport { evaluateCriteria } from './criteria.js';\n\nconst log = createLogger('Schedule');\n\nexport class ScheduleManager {\n constructor(options = {}) {\n this.schedule = null;\n this.playHistory = new Map(); // Track plays per layout: layoutId -> [timestamps]\n this.interruptScheduler = options.interruptScheduler || null; // Optional interrupt scheduler\n this.displayProperties = options.displayProperties || {}; // CMS display custom properties\n this.playerLocation = null; // { latitude, longitude } from Geolocation API\n this._layoutMetadata = new Map(); // layoutFile → { syncEvent, shareOfVoice, ... }\n }\n\n /**\n * Update schedule from XMDS\n */\n setSchedule(schedule) {\n this.schedule = schedule;\n }\n\n /**\n * Get data connectors from current schedule\n * @returns {Array} Data connector configurations, or empty array\n */\n getDataConnectors() {\n return this.schedule?.dataConnectors || [];\n }\n\n /**\n * Check if a schedule item is active based on recurrence rules\n * Supports weekly dayparting (recurring schedules on specific days/times)\n */\n isRecurringScheduleActive(item, now) {\n // If no recurrence, it's not a recurring schedule\n if (!item.recurrenceType) {\n return true; // Not a recurring schedule, use date/time checks instead\n }\n\n // Currently only support Weekly recurrence (dayparting)\n if (item.recurrenceType !== 'Week') {\n return true; // Unsupported recurrence type, fallback to date/time checks\n }\n\n // Check if current day of week matches recurrenceRepeatsOn\n // recurrenceRepeatsOn format: \"1,2,3,4,5\" (1=Monday, 7=Sunday, ISO format)\n if (item.recurrenceRepeatsOn) {\n const currentDayOfWeek = this.getIsoDayOfWeek(now);\n const allowedDays = item.recurrenceRepeatsOn.split(',').map(d => parseInt(d.trim()));\n\n if (!allowedDays.includes(currentDayOfWeek)) {\n return false; // Today is not in the allowed days\n }\n }\n\n // Check recurrence range if specified\n if (item.recurrenceRange) {\n const rangeEnd = new Date(item.recurrenceRange);\n if (now > rangeEnd) {\n return false; // Recurrence has ended\n }\n }\n\n return true;\n }\n\n /**\n * Get ISO day of week (1=Monday, 7=Sunday)\n */\n getIsoDayOfWeek(date) {\n const day = date.getDay(); // 0=Sunday, 6=Saturday\n return day === 0 ? 7 : day; // Convert to ISO (1=Monday, 7=Sunday)\n }\n\n /**\n * Check if current time is within the schedule's time window\n * Handles both date ranges and time-of-day for dayparting\n */\n isTimeActive(item, now) {\n const from = item.fromdt ? new Date(item.fromdt) : null;\n const to = item.todt ? new Date(item.todt) : null;\n\n // For recurring schedules, check time-of-day instead of full datetime\n if (item.recurrenceType === 'Week') {\n // Extract time from fromdt/todt and compare with current time\n if (from && to) {\n const currentTime = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds();\n const fromTime = from.getHours() * 3600 + from.getMinutes() * 60 + from.getSeconds();\n const toTime = to.getHours() * 3600 + to.getMinutes() * 60 + to.getSeconds();\n\n // Handle midnight crossing\n if (fromTime <= toTime) {\n // Normal case: 09:00 - 17:00\n return currentTime >= fromTime && currentTime <= toTime;\n } else {\n // Midnight crossing: 22:00 - 02:00\n return currentTime >= fromTime || currentTime <= toTime;\n }\n }\n return true;\n }\n\n // For non-recurring schedules, use full date/time comparison\n if (from && now < from) return false;\n if (to && now > to) return false;\n return true;\n }\n\n /**\n * Get current layouts to display\n * Returns array of layout files, prioritized\n *\n * Campaign behavior:\n * - Priority applies at campaign level, not individual layout level\n * - All layouts in a campaign share the campaign's priority\n * - Layouts within a campaign are returned in order for cycling\n * - Standalone layouts compete with campaigns at their own priority\n *\n * Dayparting behavior:\n * - Schedules can recur weekly on specific days (recurrenceType='Week')\n * - recurrenceRepeatsOn specifies days: \"1,2,3,4,5\" (Mon-Fri, ISO format)\n * - Time matching uses time-of-day for recurring schedules\n * - Non-recurring schedules use full date/time ranges\n *\n * Interrupt behavior (shareOfVoice):\n * - Layouts with shareOfVoice > 0 are interrupts\n * - They must play for a percentage of each hour\n * - Normal layouts fill remaining time\n * - Interrupts are interleaved with normal layouts\n */\n getCurrentLayouts() {\n return this._getLayoutsAt(new Date());\n }\n\n /**\n * Get layouts active at a specific time.\n * Skips rate limiting and interrupt processing (those depend on real-time state).\n * Used by timeline calculator to predict future playback.\n * @param {Date} time - The time to evaluate\n * @returns {string[]} Layout files active at that time\n */\n getLayoutsAtTime(time) {\n return this._getLayoutsAt(time, { skipRateLimiting: true, skipInterrupts: true, quiet: true });\n }\n\n /**\n * Get ALL time-active layouts with metadata, without priority or rate-limit filtering.\n * Used by calculateTimeline() to simulate real playback with rate limiting and\n * priority fallback (e.g., when high-priority layouts hit maxPlaysPerHour, lower\n * priority layouts fill the gap).\n *\n * @param {Date} time - The time to evaluate\n * @returns {Array<{file: string, priority: number, maxPlaysPerHour: number}>}\n */\n getAllLayoutsAtTime(time) {\n if (!this.schedule) return [];\n\n const now = time;\n const results = [];\n\n // Standalone layouts\n if (this.schedule.layouts) {\n for (const layout of this.schedule.layouts) {\n if (!this.isRecurringScheduleActive(layout, now)) continue;\n if (!this.isTimeActive(layout, now)) continue;\n if (layout.criteria && layout.criteria.length > 0) {\n if (!evaluateCriteria(layout.criteria, { now, displayProperties: this.displayProperties })) continue;\n }\n if (layout.isGeoAware && layout.geoLocation) {\n if (!this.isWithinGeoFence(layout.geoLocation)) continue;\n }\n results.push({\n file: layout.file,\n priority: layout.priority || 0,\n maxPlaysPerHour: layout.maxPlaysPerHour || 0,\n });\n }\n }\n\n // Campaign layouts\n if (this.schedule.campaigns) {\n for (const campaign of this.schedule.campaigns) {\n if (!this.isRecurringScheduleActive(campaign, now)) continue;\n if (!this.isTimeActive(campaign, now)) continue;\n for (const layout of campaign.layouts) {\n results.push({\n file: layout.file,\n priority: campaign.priority || 0,\n maxPlaysPerHour: layout.maxPlaysPerHour || 0,\n });\n }\n }\n }\n\n return results;\n }\n\n /**\n * Internal: evaluate schedule at a given time.\n * @param {Date} now - Time to evaluate\n * @param {Object} [options] - Options\n * @param {boolean} [options.skipRateLimiting] - Skip maxPlaysPerHour checks\n * @param {boolean} [options.skipInterrupts] - Skip interrupt/shareOfVoice processing\n */\n _getLayoutsAt(now, options = {}) {\n if (!this.schedule) {\n return [];\n }\n\n const { skipRateLimiting = false, skipInterrupts = false, quiet = false } = options;\n const _log = quiet ? () => {} : (...args) => log.info(...args);\n const activeItems = []; // Mix of campaign objects and standalone layouts\n\n // Track the highest priority of any time-active layout BEFORE rate-limit\n // filtering. Used by advanceToNextLayout() to detect when only lower-\n // priority layouts remain (all high-priority ones are rate-limited) and\n // replay the current layout instead of downgrading.\n this._maxActivePriority = 0;\n\n // Find all active campaigns\n if (this.schedule.campaigns) {\n for (const campaign of this.schedule.campaigns) {\n // Check recurrence and time window\n if (!this.isRecurringScheduleActive(campaign, now)) {\n continue;\n }\n if (!this.isTimeActive(campaign, now)) {\n continue;\n }\n\n this._maxActivePriority = Math.max(this._maxActivePriority, campaign.priority || 0);\n\n // Campaign is active - add it as a single item with its priority\n activeItems.push({\n type: 'campaign',\n priority: campaign.priority,\n layouts: campaign.layouts, // Keep full layout objects for interrupt processing\n campaignId: campaign.id\n });\n }\n }\n\n // Find all active standalone layouts\n if (this.schedule.layouts) {\n for (const layout of this.schedule.layouts) {\n // Check recurrence and time window\n if (!this.isRecurringScheduleActive(layout, now)) {\n continue;\n }\n if (!this.isTimeActive(layout, now)) {\n continue;\n }\n\n // Check criteria conditions (date/time, display properties)\n if (layout.criteria && layout.criteria.length > 0) {\n if (!evaluateCriteria(layout.criteria, { now, displayProperties: this.displayProperties })) {\n _log('[Schedule] Layout', layout.id, 'filtered by criteria');\n continue;\n }\n }\n\n // Check geo-fencing\n if (layout.isGeoAware && layout.geoLocation) {\n if (!this.isWithinGeoFence(layout.geoLocation)) {\n _log('[Schedule] Layout', layout.id, 'filtered by geofence');\n continue;\n }\n }\n\n // Track priority before rate-limit filtering\n this._maxActivePriority = Math.max(this._maxActivePriority, layout.priority || 0);\n\n // Check max plays per hour (skip for future time queries)\n if (!skipRateLimiting && !this.canPlayLayout(layout.id, layout.maxPlaysPerHour)) {\n _log('[Schedule] Layout', layout.id, 'filtered by maxPlaysPerHour (limit:', layout.maxPlaysPerHour, ')');\n // Continue to check other layouts, but don't add this one\n continue;\n }\n\n activeItems.push({\n type: 'layout',\n priority: layout.priority || 0,\n layouts: [layout], // Keep full layout object for interrupt processing\n layoutId: layout.id\n });\n }\n }\n\n // If no active schedules, return default\n if (activeItems.length === 0) {\n return this.schedule.default ? [this.schedule.default] : [];\n }\n\n // Find maximum priority across all items (campaigns and layouts)\n let maxPriority = Math.max(...activeItems.map(item => item.priority));\n _log('[Schedule] Max priority:', maxPriority, 'from', activeItems.length, 'active items');\n\n // Collect all layouts from items with max priority\n let allLayouts = [];\n for (const item of activeItems) {\n if (item.priority === maxPriority) {\n _log('[Schedule] Including priority', item.priority, 'layouts:', item.layouts.map(l => l.file));\n // Add all layouts from this campaign or standalone layout\n allLayouts.push(...item.layouts);\n } else {\n _log('[Schedule] Skipping priority', item.priority, '< max', maxPriority);\n }\n }\n\n // Build layout metadata map (syncEvent, shareOfVoice, etc.)\n this._layoutMetadata.clear();\n for (const layout of allLayouts) {\n this._layoutMetadata.set(layout.file, {\n syncEvent: layout.syncEvent || false,\n shareOfVoice: layout.shareOfVoice || 0,\n scheduleid: layout.scheduleid,\n priority: layout.priority || 0,\n });\n }\n\n // Process interrupts if interrupt scheduler is available (skip for future time queries)\n if (!skipInterrupts && this.interruptScheduler) {\n const { normalLayouts, interruptLayouts } = this.interruptScheduler.separateLayouts(allLayouts);\n\n if (interruptLayouts.length > 0) {\n _log('[Schedule] Found', interruptLayouts.length, 'interrupt layouts with shareOfVoice');\n const processedLayouts = this.interruptScheduler.processInterrupts(normalLayouts, interruptLayouts);\n // Extract file IDs from processed layouts\n const result = processedLayouts.map(l => l.file);\n _log('[Schedule] Final layouts (with interrupts):', result);\n return result;\n }\n }\n\n // No interrupts, return layout files\n const result = allLayouts.map(l => l.file);\n _log('[Schedule] Final layouts:', result);\n return result;\n }\n\n /**\n * Check if schedule needs update (every minute)\n */\n shouldCheckSchedule(lastCheck) {\n if (!lastCheck) return true;\n const elapsed = Date.now() - lastCheck;\n return elapsed >= 60000; // 1 minute\n }\n\n /**\n * Check if layout can play based on maxPlaysPerHour with even distribution.\n *\n * Instead of allowing bursts (3 plays back-to-back then nothing for 50 min),\n * plays are distributed evenly across the hour:\n * maxPlaysPerHour=3 → minimum 20 min gap between plays\n * maxPlaysPerHour=6 → minimum 10 min gap between plays\n *\n * Two checks:\n * 1. Total plays in sliding 1-hour window < maxPlaysPerHour\n * 2. Time since last play >= (60 / maxPlaysPerHour) minutes\n *\n * @param {string} layoutId - Layout ID to check\n * @param {number} maxPlaysPerHour - Maximum plays allowed per hour (0 = unlimited)\n * @returns {boolean} True if layout can play, false if exceeded limit\n */\n canPlayLayout(layoutId, maxPlaysPerHour) {\n // If maxPlaysPerHour is 0 or undefined, unlimited plays\n if (!maxPlaysPerHour || maxPlaysPerHour === 0) {\n return true;\n }\n\n const now = Date.now();\n const oneHourAgo = now - (60 * 60 * 1000);\n\n // Get play history for this layout\n const history = this.playHistory.get(layoutId) || [];\n\n // Filter to plays within the last hour\n const playsInLastHour = history.filter(timestamp => timestamp > oneHourAgo);\n\n // Check 1: Total plays in last hour must be under limit\n if (playsInLastHour.length >= maxPlaysPerHour) {\n log.info(`Layout ${layoutId} has reached max plays per hour (${playsInLastHour.length}/${maxPlaysPerHour})`);\n return false;\n }\n\n // Check 2: Minimum gap between plays for even distribution\n // e.g., 3/hour → 1 every 20 min, 6/hour → 1 every 10 min\n if (playsInLastHour.length > 0) {\n const minGapMs = (60 * 60 * 1000) / maxPlaysPerHour;\n const lastPlayTime = Math.max(...playsInLastHour);\n const elapsed = now - lastPlayTime;\n\n if (elapsed < minGapMs) {\n const remainingMin = ((minGapMs - elapsed) / 60000).toFixed(1);\n log.info(`Layout ${layoutId} spacing: next play in ${remainingMin} min (${playsInLastHour.length}/${maxPlaysPerHour} plays, ${Math.round(minGapMs/60000)} min gap)`);\n return false;\n }\n }\n\n return true;\n }\n\n /**\n * Record that a layout was played\n * @param {string} layoutId - Layout ID that was played\n */\n recordPlay(layoutId) {\n if (!this.playHistory.has(layoutId)) {\n this.playHistory.set(layoutId, []);\n }\n\n const history = this.playHistory.get(layoutId);\n history.push(Date.now());\n\n // Clean up old entries (older than 1 hour)\n const oneHourAgo = Date.now() - (60 * 60 * 1000);\n const cleaned = history.filter(timestamp => timestamp > oneHourAgo);\n this.playHistory.set(layoutId, cleaned);\n\n log.info(`Recorded play for layout ${layoutId} (${cleaned.length} plays in last hour)`);\n }\n\n /**\n * Get the max priority of any time-active layout (ignoring rate-limit filtering).\n * Returns 0 if no layouts are active or if getCurrentLayouts() hasn't been called.\n * @returns {number}\n */\n getMaxActivePriority() {\n return this._maxActivePriority || 0;\n }\n\n /**\n * Check if a layout file is a sync event (part of multi-display sync group)\n * @param {string} layoutFile - Layout file identifier (e.g., '123')\n * @returns {boolean}\n */\n isSyncEvent(layoutFile) {\n const meta = this._layoutMetadata.get(layoutFile);\n return meta?.syncEvent === true;\n }\n\n /**\n * Get metadata for a layout file (syncEvent, shareOfVoice, etc.)\n * @param {string} layoutFile - Layout file identifier\n * @returns {Object|null} Metadata or null if not found\n */\n getLayoutMetadata(layoutFile) {\n return this._layoutMetadata.get(layoutFile) || null;\n }\n\n /**\n * Check if any current layouts are sync events\n * @returns {boolean}\n */\n hasSyncEvents() {\n for (const meta of this._layoutMetadata.values()) {\n if (meta.syncEvent) return true;\n }\n return false;\n }\n\n /**\n * Get currently active actions (within their time window)\n * @returns {Array} Active action objects\n */\n getActiveActions() {\n if (!this.schedule?.actions) return [];\n\n const now = new Date();\n return this.schedule.actions.filter(action => this.isTimeActive(action, now));\n }\n\n /**\n * Get scheduled commands\n * @returns {Array} Command objects\n */\n getCommands() {\n return this.schedule?.commands || [];\n }\n\n /**\n * Find action by trigger code\n * @param {string} triggerCode - The trigger code to match\n * @returns {Object|null} Matching action or null\n */\n findActionByTrigger(triggerCode) {\n const activeActions = this.getActiveActions();\n return activeActions.find(a => a.triggerCode === triggerCode) || null;\n }\n\n /**\n * Clear play history (useful for testing or reset)\n */\n clearPlayHistory() {\n this.playHistory.clear();\n log.info('Play history cleared');\n }\n\n /**\n * Set player's current GPS location (from Geolocation API or XMR command)\n * @param {number} latitude\n * @param {number} longitude\n */\n setLocation(latitude, longitude) {\n this.playerLocation = { latitude, longitude };\n log.info(`Location set: ${latitude}, ${longitude}`);\n }\n\n /**\n * Set display properties from CMS (custom fields for criteria evaluation)\n * @param {Object} properties - Key-value map of display properties\n */\n setDisplayProperties(properties) {\n this.displayProperties = properties || {};\n }\n\n /**\n * Check if player is within a geo-fence.\n * geoLocation format from CMS: \"lat,lng\" (point + default radius)\n * or \"lat1,lng1;lat2,lng2;...\" (polygon — future)\n *\n * Default radius: 500 meters (Xibo default for point geofences)\n *\n * @param {string} geoLocation - Geo-fence specification from CMS\n * @param {number} [defaultRadius=500] - Default radius in meters for point geofences\n * @returns {boolean} True if within geofence or no location available\n */\n isWithinGeoFence(geoLocation, defaultRadius = 500) {\n if (!this.playerLocation) {\n // No location available — be permissive, show the content\n log.debug('No player location, skipping geofence check');\n return true;\n }\n\n if (!geoLocation) return true;\n\n // Parse \"lat,lng\" format\n const parts = geoLocation.split(',').map(s => parseFloat(s.trim()));\n if (parts.length < 2 || isNaN(parts[0]) || isNaN(parts[1])) {\n log.warn('Invalid geoLocation format:', geoLocation);\n return true; // Invalid format, be permissive\n }\n\n const fenceLat = parts[0];\n const fenceLng = parts[1];\n const radius = parts[2] || defaultRadius; // Optional 3rd param: radius in meters\n\n const distance = this.haversineDistance(\n this.playerLocation.latitude, this.playerLocation.longitude,\n fenceLat, fenceLng\n );\n\n const within = distance <= radius;\n log.info(`Geofence: ${distance.toFixed(0)}m from (${fenceLat},${fenceLng}), radius ${radius}m → ${within ? 'WITHIN' : 'OUTSIDE'}`);\n return within;\n }\n\n /**\n * Haversine formula: calculate distance between two GPS coordinates\n * @param {number} lat1 - Latitude 1 (degrees)\n * @param {number} lon1 - Longitude 1 (degrees)\n * @param {number} lat2 - Latitude 2 (degrees)\n * @param {number} lon2 - Longitude 2 (degrees)\n * @returns {number} Distance in meters\n */\n haversineDistance(lat1, lon1, lat2, lon2) {\n const R = 6371000; // Earth radius in meters\n const toRad = deg => deg * Math.PI / 180;\n\n const dLat = toRad(lat2 - lat1);\n const dLon = toRad(lon2 - lon1);\n\n const a = Math.sin(dLat / 2) ** 2 +\n Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *\n Math.sin(dLon / 2) ** 2;\n\n return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));\n }\n}\n\nexport const scheduleManager = new ScheduleManager();\n","/**\n * Interrupt Layout Scheduler (Share of Voice)\n *\n * Implements the shareOfVoice algorithm from upstream electron-player.\n * Interrupts are layouts that must play for a percentage of each hour.\n *\n * Algorithm:\n * 1. Separate interrupts from normal layouts\n * 2. Calculate how many times each interrupt must play per hour\n * 3. Fill remaining time with normal layouts\n * 4. Interleave interrupts and normal layouts evenly\n *\n * Based on: electron-player/src/main/common/scheduleManager.ts (lines 181-321)\n */\n\nimport { createLogger } from '@xiboplayer/utils';\n\nconst logger = createLogger('schedule:interrupts');\n\n/**\n * Interrupt Scheduler\n * Handles shareOfVoice layouts that must play for a percentage of each hour\n */\nexport class InterruptScheduler {\n constructor() {\n // Track committed duration per interrupt layout\n this.interruptCommittedDurations = new Map(); // layoutId -> seconds\n }\n\n /**\n * Check if a layout is an interrupt (has shareOfVoice > 0)\n * @param {Object} layout - Layout object with shareOfVoice property\n * @returns {boolean} True if layout is an interrupt\n */\n isInterrupt(layout) {\n return !!(layout.shareOfVoice && layout.shareOfVoice > 0);\n }\n\n /**\n * Reset committed duration tracking (call this every hour)\n */\n resetCommittedDurations() {\n this.interruptCommittedDurations.clear();\n logger.debug('Reset interrupt committed durations');\n }\n\n /**\n * Get committed duration for a layout\n * @param {string} layoutId - Layout ID\n * @returns {number} Committed duration in seconds\n */\n getCommittedDuration(layoutId) {\n return this.interruptCommittedDurations.get(layoutId) || 0;\n }\n\n /**\n * Add committed duration for a layout\n * @param {string} layoutId - Layout ID\n * @param {number} duration - Duration to add in seconds\n */\n addCommittedDuration(layoutId, duration) {\n const current = this.getCommittedDuration(layoutId);\n this.interruptCommittedDurations.set(layoutId, current + duration);\n }\n\n /**\n * Check if interrupt layout has satisfied its shareOfVoice requirement\n * @param {Object} layout - Layout with shareOfVoice and duration\n * @returns {boolean} True if satisfied\n */\n isInterruptDurationSatisfied(layout) {\n if (!layout.shareOfVoice) {\n return true; // Not an interrupt\n }\n\n const layoutId = layout.id || layout.file;\n const requiredSeconds = (layout.shareOfVoice / 100) * 3600; // shareOfVoice is percentage\n const committedSeconds = this.getCommittedDuration(layoutId);\n\n return committedSeconds >= requiredSeconds;\n }\n\n /**\n * Calculate how many seconds this interrupt needs to play per hour\n * @param {Object} layout - Layout with shareOfVoice\n * @returns {number} Required seconds per hour\n */\n getRequiredSeconds(layout) {\n if (!layout.shareOfVoice) {\n return 0;\n }\n return (layout.shareOfVoice / 100) * 3600;\n }\n\n /**\n * Process interrupt layouts and combine with normal layouts\n * Implements the shareOfVoice algorithm from upstream\n *\n * @param {Array} normalLayouts - Normal scheduled layouts\n * @param {Array} interruptLayouts - Interrupt layouts with shareOfVoice\n * @returns {Array} Combined layout loop for the hour\n */\n processInterrupts(normalLayouts, interruptLayouts) {\n if (!interruptLayouts || interruptLayouts.length === 0) {\n logger.debug('No interrupt layouts, returning normal layouts');\n return normalLayouts;\n }\n\n if (!normalLayouts || normalLayouts.length === 0) {\n logger.warn('No normal layouts available, interrupts will fill entire hour');\n return this.fillHourWithInterrupts(interruptLayouts);\n }\n\n logger.info(`Processing ${interruptLayouts.length} interrupt layouts with ${normalLayouts.length} normal layouts`);\n\n // Reset committed durations for this calculation\n for (const layout of interruptLayouts) {\n const layoutId = layout.id || layout.file;\n this.interruptCommittedDurations.set(layoutId, 0);\n }\n\n const resolvedInterruptLayouts = [];\n let interruptSecondsInHour = 0;\n let index = 0;\n let satisfied = false;\n\n // Step 1: Build interrupt loop by cycling through interrupts until all are satisfied\n while (!satisfied) {\n // Gone all the way around? Check if all satisfied\n if (index >= interruptLayouts.length) {\n index = 0;\n\n // Check if all interrupts are satisfied\n let allSatisfied = true;\n for (const layout of interruptLayouts) {\n if (!this.isInterruptDurationSatisfied(layout)) {\n allSatisfied = false;\n break;\n }\n }\n\n if (allSatisfied) {\n satisfied = true;\n break;\n }\n }\n\n const currentInterrupt = interruptLayouts[index];\n\n // If this interrupt is not satisfied, add it to the loop\n if (!this.isInterruptDurationSatisfied(currentInterrupt)) {\n const layoutId = currentInterrupt.id || currentInterrupt.file;\n this.addCommittedDuration(layoutId, currentInterrupt.duration);\n interruptSecondsInHour += currentInterrupt.duration;\n resolvedInterruptLayouts.push(currentInterrupt);\n }\n\n index++;\n }\n\n logger.debug(`Resolved ${resolvedInterruptLayouts.length} interrupt plays (${interruptSecondsInHour}s total)`);\n\n // Step 2: If interrupts fill the entire hour, return only interrupts\n if (interruptSecondsInHour >= 3600) {\n logger.info('Interrupts fill entire hour (>= 3600s), no room for normal layouts');\n return resolvedInterruptLayouts;\n }\n\n // Step 3: Fill remaining time with normal layouts\n const normalSecondsInHour = 3600 - interruptSecondsInHour;\n const resolvedNormalLayouts = this.fillTimeWithLayouts(normalLayouts, normalSecondsInHour);\n\n logger.debug(`Resolved ${resolvedNormalLayouts.length} normal plays (${normalSecondsInHour}s target)`);\n\n // Step 4: Interleave interrupts and normal layouts\n const loop = this.interleaveLayouts(resolvedNormalLayouts, resolvedInterruptLayouts);\n\n logger.info(`Final loop: ${loop.length} layouts (${resolvedNormalLayouts.length} normal + ${resolvedInterruptLayouts.length} interrupts)`);\n\n return loop;\n }\n\n /**\n * Fill time with layouts by repeating them until duration is reached\n * @param {Array} layouts - Layouts to use\n * @param {number} targetSeconds - Target duration in seconds\n * @returns {Array} Resolved layout array\n */\n fillTimeWithLayouts(layouts, targetSeconds) {\n const resolved = [];\n let remainingSeconds = targetSeconds;\n let index = 0;\n\n while (remainingSeconds > 0) {\n if (index >= layouts.length) {\n index = 0; // Loop back\n }\n\n const layout = layouts[index];\n resolved.push(layout);\n remainingSeconds -= layout.duration;\n index++;\n }\n\n return resolved;\n }\n\n /**\n * Fill entire hour with interrupt layouts only\n * @param {Array} interruptLayouts - Interrupt layouts\n * @returns {Array} Layout loop\n */\n fillHourWithInterrupts(interruptLayouts) {\n return this.fillTimeWithLayouts(interruptLayouts, 3600);\n }\n\n /**\n * Interleave normal and interrupt layouts evenly\n * Based on upstream algorithm (scheduleManager.ts lines 268-316)\n *\n * @param {Array} normalLayouts - Normal layouts\n * @param {Array} interruptLayouts - Interrupt layouts\n * @returns {Array} Interleaved layout array\n */\n interleaveLayouts(normalLayouts, interruptLayouts) {\n const loop = [];\n const pickCount = Math.max(normalLayouts.length, interruptLayouts.length);\n\n // Calculate pick intervals\n // Normal: ceiling (pick more often from normal)\n // Interrupt: floor (pick less often from interrupts)\n const normalPick = Math.ceil(1.0 * pickCount / normalLayouts.length);\n const interruptPick = Math.floor(1.0 * pickCount / interruptLayouts.length);\n\n logger.debug(`Interleaving: pickCount=${pickCount}, normalPick=${normalPick}, interruptPick=${interruptPick}`);\n\n let normalIndex = 0;\n let interruptIndex = 0;\n let totalSecondsAllocated = 0;\n\n for (let i = 0; i < pickCount; i++) {\n // Pick from normal list\n if (i % normalPick === 0) {\n // Allow wrapping around\n if (normalIndex >= normalLayouts.length) {\n normalIndex = 0;\n }\n loop.push(normalLayouts[normalIndex]);\n totalSecondsAllocated += normalLayouts[normalIndex].duration;\n normalIndex++;\n }\n\n // Pick from interrupt list (only if we haven't picked them all yet)\n if (i % interruptPick === 0 && interruptIndex < interruptLayouts.length) {\n loop.push(interruptLayouts[interruptIndex]);\n totalSecondsAllocated += interruptLayouts[interruptIndex].duration;\n interruptIndex++;\n }\n }\n\n // Fill remaining time with normal layouts (due to ceiling/floor rounding)\n while (totalSecondsAllocated < 3600) {\n if (normalIndex >= normalLayouts.length) {\n normalIndex = 0;\n }\n loop.push(normalLayouts[normalIndex]);\n totalSecondsAllocated += normalLayouts[normalIndex].duration;\n normalIndex++;\n }\n\n logger.debug(`Interleaved ${loop.length} layouts, total duration: ${totalSecondsAllocated}s`);\n\n return loop;\n }\n\n /**\n * Separate layouts into normal and interrupt arrays\n * @param {Array} layouts - All layouts\n * @returns {Object} { normalLayouts, interruptLayouts }\n */\n separateLayouts(layouts) {\n const normalLayouts = [];\n const interruptLayouts = [];\n\n for (const layout of layouts) {\n if (this.isInterrupt(layout)) {\n interruptLayouts.push(layout);\n } else {\n normalLayouts.push(layout);\n }\n }\n\n return { normalLayouts, interruptLayouts };\n }\n}\n\n// Export singleton instance for convenience\nexport const interruptScheduler = new InterruptScheduler();\n","/**\n * Overlay Layout Scheduler\n *\n * Manages overlay layouts that appear on top of main layouts.\n * Based on upstream electron-player implementation.\n *\n * Overlays:\n * - Render on top of main layout (higher z-index)\n * - Have scheduled start/end times\n * - Support priority ordering (multiple overlays)\n * - Support criteria-based display (future)\n * - Support geofencing (future)\n *\n * Reference: upstream_players/electron-player/src/main/xmds/response/schedule/events/overlayLayout.ts\n */\n\nimport { createLogger } from '@xiboplayer/utils';\nimport { evaluateCriteria } from './criteria.js';\n\nconst logger = createLogger('schedule:overlays');\n\n/**\n * Overlay Scheduler\n * Handles overlay layouts that display on top of main layouts\n */\nexport class OverlayScheduler {\n constructor() {\n this.overlays = [];\n this.displayProperties = {};\n this.scheduleManager = null; // Reference to ScheduleManager for geo checks\n logger.debug('OverlayScheduler initialized');\n }\n\n /**\n * Set reference to ScheduleManager for geo-fence checks\n * @param {ScheduleManager} scheduleManager\n */\n setScheduleManager(scheduleManager) {\n this.scheduleManager = scheduleManager;\n }\n\n /**\n * Set display properties for criteria evaluation\n * @param {Object} properties\n */\n setDisplayProperties(properties) {\n this.displayProperties = properties || {};\n }\n\n /**\n * Update overlays from XMDS Schedule response\n * @param {Array} overlays - Overlay objects from XMDS\n */\n setOverlays(overlays) {\n this.overlays = overlays || [];\n logger.info(`Loaded ${this.overlays.length} overlay(s)`);\n }\n\n /**\n * Get currently active overlays\n * @returns {Array} Active overlay objects sorted by priority (highest first)\n */\n getCurrentOverlays() {\n if (!this.overlays || this.overlays.length === 0) {\n return [];\n }\n\n const now = new Date();\n const activeOverlays = [];\n\n for (const overlay of this.overlays) {\n // Check time window\n if (!this.isTimeActive(overlay, now)) {\n logger.debug(`Overlay ${overlay.file} not in time window`);\n continue;\n }\n\n // Check geo-awareness\n if (overlay.isGeoAware && overlay.geoLocation) {\n if (this.scheduleManager && !this.scheduleManager.isWithinGeoFence(overlay.geoLocation)) {\n logger.debug(`Overlay ${overlay.file} filtered by geofence`);\n continue;\n }\n }\n\n // Check criteria conditions\n if (overlay.criteria && overlay.criteria.length > 0) {\n if (!evaluateCriteria(overlay.criteria, { now, displayProperties: this.displayProperties })) {\n logger.debug(`Overlay ${overlay.file} filtered by criteria`);\n continue;\n }\n }\n\n activeOverlays.push(overlay);\n }\n\n // Sort by priority (highest first)\n activeOverlays.sort((a, b) => {\n const priorityA = a.priority || 0;\n const priorityB = b.priority || 0;\n return priorityB - priorityA;\n });\n\n if (activeOverlays.length > 0) {\n logger.info(`Active overlays: ${activeOverlays.length}`);\n }\n\n return activeOverlays;\n }\n\n /**\n * Check if overlay is within its time window\n * @param {Object} overlay - Overlay object\n * @param {Date} now - Current time\n * @returns {boolean}\n */\n isTimeActive(overlay, now) {\n const from = overlay.fromDt ? new Date(overlay.fromDt) : null;\n const to = overlay.toDt ? new Date(overlay.toDt) : null;\n\n // Check time bounds\n if (from && now < from) {\n return false;\n }\n if (to && now > to) {\n return false;\n }\n\n return true;\n }\n\n /**\n * Check if overlay schedule needs update (every minute)\n * @param {number} lastCheck - Last check timestamp\n * @returns {boolean}\n */\n shouldCheckOverlays(lastCheck) {\n if (!lastCheck) return true;\n const elapsed = Date.now() - lastCheck;\n return elapsed >= 60000; // 1 minute\n }\n\n /**\n * Get overlay by file ID\n * @param {number} fileId - Layout file ID\n * @returns {Object|null}\n */\n getOverlayByFile(fileId) {\n return this.overlays.find(o => o.file === fileId) || null;\n }\n\n /**\n * Clear all overlays\n */\n clear() {\n this.overlays = [];\n logger.debug('Cleared all overlays');\n }\n\n /**\n * Process overlay layouts (compatibility method for interrupt scheduler pattern)\n * @param {Array} layouts - Base layouts\n * @param {Array} overlays - Overlay layouts\n * @returns {Array} Layouts (unchanged, overlays are separate)\n */\n processOverlays(layouts, overlays) {\n // Overlays don't modify the main layout loop\n // They are rendered separately on top\n this.setOverlays(overlays);\n return layouts;\n }\n}\n\nexport const overlayScheduler = new OverlayScheduler();\n","/**\n * Offline Schedule Timeline Calculator\n *\n * Calculates deterministic playback timelines by parsing layout XLF durations\n * and simulating round-robin scheduling. Enables the player to answer\n * \"what's the playback plan for the next N hours?\" while offline.\n */\n\n/**\n * Parse layout duration from XLF XML string.\n * Lightweight parser — uses DOMParser, no rendering.\n *\n * Duration resolution order:\n * 1. Explicit <layout duration=\"60\"> attribute\n * 2. Sum of widget <media duration=\"X\"> per region (max across regions)\n * 3. Fallback: 60s\n *\n * @param {string} xlfXml - Raw XLF XML string\n * @returns {number} Duration in seconds\n */\nexport function parseLayoutDuration(xlfXml) {\n const doc = new DOMParser().parseFromString(xlfXml, 'text/xml');\n const layoutEl = doc.querySelector('layout');\n if (!layoutEl) return 60;\n\n // 1. Explicit layout duration attribute\n const explicit = parseInt(layoutEl.getAttribute('duration') || '0', 10);\n if (explicit > 0) return explicit;\n\n // 2. Calculate from widget durations (max region wins — regions play in parallel)\n let maxDuration = 0;\n for (const regionEl of layoutEl.querySelectorAll('region')) {\n let regionDuration = 0;\n for (const mediaEl of regionEl.querySelectorAll('media')) {\n const dur = parseInt(mediaEl.getAttribute('duration') || '0', 10);\n const useDuration = parseInt(mediaEl.getAttribute('useDuration') || '1', 10);\n if (dur > 0 && useDuration !== 0) {\n regionDuration += dur;\n } else {\n // Video with useDuration=0 means \"play to end\" — estimate 60s,\n // corrected later via recordLayoutDuration() when video metadata loads\n regionDuration += 60;\n }\n }\n maxDuration = Math.max(maxDuration, regionDuration);\n }\n\n return maxDuration > 0 ? maxDuration : 60;\n}\n\n/**\n * Compare two arrays of layout files for equality.\n * @param {string[]} a\n * @param {string[]} b\n * @returns {boolean}\n */\nfunction arraysEqual(a, b) {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n\n/**\n * Check if a layout can play at a given time based on simulated play history.\n * Replicates ScheduleManager.canPlayLayout() logic for timeline prediction.\n *\n * Even-distribution rules:\n * 1. Total plays in sliding 1-hour window < maxPlaysPerHour\n * 2. Time since last play >= (60 / maxPlaysPerHour) minutes\n *\n * @param {number[]} history - Simulated play timestamps (ms) for this layout\n * @param {number} maxPlaysPerHour - Max plays per hour (0 = unlimited)\n * @param {number} timeMs - Current simulated time in ms\n * @returns {boolean}\n */\nfunction canSimulatedPlay(history, maxPlaysPerHour, timeMs) {\n if (!maxPlaysPerHour || maxPlaysPerHour === 0) return true;\n\n const oneHourAgo = timeMs - 3600000;\n const playsInLastHour = history.filter(t => t > oneHourAgo);\n\n // Check 1: under hourly limit\n if (playsInLastHour.length >= maxPlaysPerHour) return false;\n\n // Check 2: minimum gap for even distribution\n if (playsInLastHour.length > 0) {\n const minGapMs = 3600000 / maxPlaysPerHour;\n const lastPlay = Math.max(...playsInLastHour);\n if (timeMs - lastPlay < minGapMs) return false;\n }\n\n return true;\n}\n\n/**\n * Seed simulated play history from real play history.\n * Maps layoutId-based history to layoutFile-based history.\n * @param {Map<string, number[]>} realHistory - schedule.playHistory (layoutId → [timestamps])\n * @returns {Map<string, number[]>} layoutFile → [timestamps]\n */\nfunction seedPlayHistory(realHistory) {\n const simulated = new Map();\n if (!realHistory) return simulated;\n\n for (const [layoutId, timestamps] of realHistory) {\n const file = `${layoutId}.xlf`;\n simulated.set(file, [...timestamps]);\n }\n return simulated;\n}\n\n/**\n * From a list of layout metadata, apply simulated rate limiting and priority\n * filtering to determine which layouts can actually play at the given time.\n * Mirrors the real player logic: filter rate-limited layouts first, then\n * pick highest remaining priority.\n *\n * @param {Array<{file: string, priority: number, maxPlaysPerHour: number}>} allLayouts\n * @param {Map<string, number[]>} simPlays - Simulated play history\n * @param {number} timeMs - Current simulated time in ms\n * @returns {string[]} Layout files that can play, highest priority first\n */\nfunction getPlayableLayouts(allLayouts, simPlays, timeMs) {\n // Step 1: Filter out rate-limited layouts\n const eligible = allLayouts.filter(l => {\n if (!l.maxPlaysPerHour || l.maxPlaysPerHour === 0) return true;\n const history = simPlays.get(l.file) || [];\n return canSimulatedPlay(history, l.maxPlaysPerHour, timeMs);\n });\n\n if (eligible.length === 0) return [];\n\n // Step 2: Pick highest priority from remaining layouts\n const maxPriority = Math.max(...eligible.map(l => l.priority));\n return eligible\n .filter(l => l.priority === maxPriority)\n .map(l => l.file);\n}\n\n/**\n * Calculate a deterministic playback timeline by simulating round-robin scheduling\n * with rate limiting (maxPlaysPerHour) and priority fallback. Produces a real\n * schedule prediction that matches actual player behavior.\n *\n * When high-priority layouts hit their maxPlaysPerHour limit, the simulation\n * falls back to lower-priority scheduled layouts before using the CMS default.\n *\n * @param {Object} schedule - ScheduleManager instance (needs getAllLayoutsAtTime(), schedule.default, playHistory)\n * @param {Map<string, number>} durations - Map of layoutFile → duration in seconds\n * @param {Object} [options]\n * @param {Date} [options.from] - Start time (default: now)\n * @param {number} [options.hours] - Hours to simulate (default: 2)\n * @param {number} [options.defaultDuration] - Fallback duration in seconds (default: 60)\n * @returns {Array<{layoutFile: string, startTime: Date, endTime: Date, duration: number, isDefault: boolean}>}\n */\nexport function calculateTimeline(schedule, durations, options = {}) {\n const from = options.from || new Date();\n const hours = options.hours || 2;\n const to = new Date(from.getTime() + hours * 3600000);\n const defaultDuration = options.defaultDuration || 60;\n const timeline = [];\n let currentTime = new Date(from);\n\n // Use getAllLayoutsAtTime if available (new API), fall back to getLayoutsAtTime (old API)\n const hasFullApi = typeof schedule.getAllLayoutsAtTime === 'function';\n\n // Seed simulated play history from real plays\n const simPlays = seedPlayHistory(schedule.playHistory);\n\n const maxEntries = 500;\n\n while (currentTime < to && timeline.length < maxEntries) {\n const timeMs = currentTime.getTime();\n let playable;\n\n if (hasFullApi) {\n // Full simulation: get ALL active layouts, apply rate limiting + priority\n const allLayouts = schedule.getAllLayoutsAtTime(currentTime);\n playable = allLayouts.length > 0\n ? getPlayableLayouts(allLayouts, simPlays, timeMs)\n : [];\n } else {\n // Legacy fallback: no rate limiting simulation\n playable = schedule.getLayoutsAtTime(currentTime);\n }\n\n if (playable.length === 0) {\n // No playable layouts — use CMS default or skip ahead\n const defaultFile = schedule.schedule?.default;\n if (defaultFile) {\n const dur = durations.get(defaultFile) || defaultDuration;\n timeline.push({\n layoutFile: defaultFile,\n startTime: new Date(currentTime),\n endTime: new Date(timeMs + dur * 1000),\n duration: dur,\n isDefault: true,\n });\n currentTime = new Date(timeMs + dur * 1000);\n } else {\n currentTime = new Date(timeMs + 60000);\n }\n continue;\n }\n\n // Round-robin through playable layouts\n for (let i = 0; i < playable.length && currentTime < to && timeline.length < maxEntries; i++) {\n const file = playable[i];\n const dur = durations.get(file) || defaultDuration;\n const endMs = currentTime.getTime() + dur * 1000;\n\n timeline.push({\n layoutFile: file,\n startTime: new Date(currentTime),\n endTime: new Date(endMs),\n duration: dur,\n isDefault: false,\n });\n\n // Record simulated play\n if (hasFullApi) {\n if (!simPlays.has(file)) simPlays.set(file, []);\n simPlays.get(file).push(currentTime.getTime());\n }\n\n currentTime = new Date(endMs);\n\n // Re-evaluate: if playable set changed, re-enter outer loop\n if (hasFullApi) {\n const nextAll = schedule.getAllLayoutsAtTime(currentTime);\n const nextPlayable = nextAll.length > 0\n ? getPlayableLayouts(nextAll, simPlays, currentTime.getTime())\n : [];\n if (!arraysEqual(playable, nextPlayable)) break;\n } else {\n const next = schedule.getLayoutsAtTime(currentTime);\n if (!arraysEqual(playable, next)) break;\n }\n }\n }\n\n return timeline;\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 { calculateTimeline, parseLayoutDuration } from '@xiboplayer/schedule';\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/** Extract layout ID from a schedule filename like \"123.xlf\" */\nfunction parseLayoutFile(f) {\n return parseInt(String(f).replace('.xlf', ''), 10);\n}\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 this._normalCollectInterval = null; // Saved interval to restore after offline retry\n this._offlineRetrySeconds = 0; // Current backoff interval (0 = not retrying)\n\n // CRC32 checksums for skip optimization (avoid redundant XMDS calls)\n this._lastCheckRf = null;\n this._lastCheckSchedule = null;\n\n // 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 // Layout durations for timeline calculation (layoutFile/layoutId → seconds)\n this._layoutDurations = new Map();\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 // Exponential backoff: 30s → 60s → 120s → ... → capped at normal interval\n // Recovers quickly from brief outages but doesn't hammer when truly offline\n if (this.collectionInterval) {\n if (!this._normalCollectInterval) {\n this._normalCollectInterval = this._currentCollectInterval;\n this._offlineRetrySeconds = 30;\n } else {\n // Double the backoff, cap at normal interval\n this._offlineRetrySeconds = Math.min(\n this._offlineRetrySeconds * 2,\n this._normalCollectInterval\n );\n }\n this._setCollectionTimer(this._offlineRetrySeconds);\n log.info(`Offline: retry in ${this._offlineRetrySeconds}s`);\n }\n\n // Load cached settings for collection interval (first run only)\n if (!this.collectionInterval) {\n const cachedReg = this._offlineCache.settings;\n if (cachedReg?.settings) {\n this.setupCollectionInterval(cachedReg.settings);\n this._normalCollectInterval = this._currentCollectInterval;\n this._offlineRetrySeconds = 30;\n this._setCollectionTimer(this._offlineRetrySeconds);\n log.info(`Offline: retry in ${this._offlineRetrySeconds}s`);\n }\n }\n\n // Load cached schedule and apply it\n const cachedSchedule = this._offlineCache.schedule;\n if (cachedSchedule) {\n this.schedule.setSchedule(cachedSchedule);\n this.emit('schedule-received', cachedSchedule);\n }\n\n // Evaluate current schedule\n const layoutFiles = this.schedule.getCurrentLayouts();\n log.info('Offline layouts:', layoutFiles);\n this.emit('layouts-scheduled', layoutFiles);\n\n this._evaluateAndSwitchLayout(layoutFiles, 'Offline');\n\n this.emit('collection-complete');\n }\n\n /**\n * Evaluate the current schedule and switch layouts if needed.\n * Shared by both collect() and collectOffline() after emitting 'layouts-scheduled'.\n * @param {string[]} layoutFiles - Currently scheduled layout filenames\n * @param {string} context - Log context label (e.g. 'Offline' or '')\n */\n async _evaluateAndSwitchLayout(layoutFiles, context) {\n const prefix = context ? `${context}: ` : '';\n\n if (layoutFiles.length > 0) {\n if (this.currentLayoutId) {\n const currentStillScheduled = layoutFiles.some(f =>\n parseLayoutFile(f) === this.currentLayoutId\n );\n if (currentStillScheduled) {\n const idx = layoutFiles.findIndex(f =>\n parseLayoutFile(f) === this.currentLayoutId\n );\n if (idx >= 0) this._currentLayoutIndex = idx;\n log.debug(`Layout ${this.currentLayoutId} still in schedule${context ? ` (${context.toLowerCase()})` : ''}, continuing playback`);\n this.emit('layout-already-playing', this.currentLayoutId);\n } else {\n this._currentLayoutIndex = 0;\n const next = this.getNextLayout();\n if (next) {\n log.info(`${prefix}switching to layout ${next.layoutId}${!context ? ` (from ${this.currentLayoutId})` : ''}`);\n this.emit('layout-prepare-request', next.layoutId);\n }\n }\n } else {\n this._currentLayoutIndex = 0;\n const next = this.getNextLayout();\n if (next) {\n log.info(`${prefix}switching to layout ${next.layoutId}`);\n this.emit('layout-prepare-request', next.layoutId);\n }\n }\n } else {\n log.info(`${context ? `${context}: n` : 'N'}o layouts${context ? ' in cached schedule' : ' scheduled, falling back to default'}`);\n this.emit('no-layouts-scheduled');\n }\n\n // Build layout durations and log upcoming timeline\n await this._buildLayoutDurations();\n this.logUpcomingTimeline();\n }\n\n /**\n * Force an immediate collection (used by platform layer on 'online' event)\n */\n async collectNow() {\n this._lastCheckRf = null;\n this._lastCheckSchedule = null;\n return this.collect();\n }\n\n /**\n * Start collection cycle\n * Pure orchestration - emits events instead of updating UI\n */\n async collect() {\n // Prevent concurrent collections\n if (this.collecting) {\n log.debug('Collection already in progress, skipping');\n return;\n }\n\n this.collecting = true;\n\n try {\n // Ensure offline cache is loaded from IndexedDB before checking\n await this._offlineDbReady;\n\n log.info('Starting collection cycle...');\n this.emit('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 // Restore normal collection interval (was shortened for offline retry)\n if (this._normalCollectInterval) {\n this._setCollectionTimer(this._normalCollectInterval);\n this._normalCollectInterval = null;\n this._offlineRetrySeconds = 0;\n }\n }\n\n // Apply display settings if DisplaySettings manager is available\n if (this.displaySettings && regResult.settings) {\n const result = this.displaySettings.applySettings(regResult.settings);\n if (result.changed.includes('collectInterval')) {\n // 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 this.logUpcomingTimeline();\n }\n\n log.debug('Collection step: download-request + mediaInventory');\n const currentLayouts = this.schedule.getCurrentLayouts();\n\n // Layout IDs in playback order (rotated from current index)\n const layoutIds = currentLayouts.map(f => parseLayoutFile(f));\n const layoutOrder = [];\n for (let i = 0; i < layoutIds.length; i++) {\n const idx = (this._currentLayoutIndex + i) % layoutIds.length;\n layoutOrder.push(layoutIds[idx]);\n }\n\n this._lastRequiredFiles = files;\n this.emit('download-request', { layoutOrder, files });\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 this.logUpcomingTimeline();\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 this._evaluateAndSwitchLayout(layoutFiles, '');\n\n // If no layouts scheduled and we're playing one that was filtered (e.g., maxPlaysPerHour),\n // force switch to default layout if available\n if (layoutFiles.length === 0 && this.currentLayoutId && this.schedule.schedule?.default) {\n const defaultLayoutId = parseLayoutFile(this.schedule.schedule.default);\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 // 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 this._setCollectionTimer(collectIntervalSeconds);\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 this._setCollectionTimer(newIntervalSeconds);\n this.emit('collection-interval-updated', newIntervalSeconds);\n }\n }\n\n /** Internal: (re)create the collection setInterval timer */\n _setCollectionTimer(seconds) {\n if (this.collectionInterval) clearInterval(this.collectionInterval);\n this._currentCollectInterval = seconds;\n log.info(`Collection interval: ${seconds}s`);\n this.collectionInterval = setInterval(() => {\n log.debug('Running scheduled collection cycle...');\n this.collect().catch(error => {\n log.error('Collection error:', error);\n this.emit('collection-error', error);\n });\n }, seconds * 1000);\n }\n\n /**\n * Request layout change (called by XMR or schedule)\n * Pure orchestration - emits events for platform to handle\n */\n async requestLayoutChange(layoutId) {\n log.info(`Layout change requested: ${layoutId}`);\n\n // Clear current layout tracking so it will switch\n this.currentLayoutId = null;\n\n this.emit('layout-change-requested', layoutId);\n }\n\n /**\n * Mark layout as ready and current\n * Called by platform after it successfully renders the layout\n */\n setCurrentLayout(layoutId) {\n this.currentLayoutId = layoutId;\n this.pendingLayouts.delete(layoutId);\n this.emit('layout-current', layoutId);\n // Re-log timeline from current time on each layout change\n this.logUpcomingTimeline();\n }\n\n /**\n * Mark layout as pending (waiting for media)\n * Called by platform when layout needs media downloads\n */\n setPendingLayout(layoutId, requiredMediaIds) {\n this.pendingLayouts.set(layoutId, requiredMediaIds);\n this.emit('layout-pending', layoutId, requiredMediaIds);\n }\n\n /**\n * Clear current layout (for replay)\n * Called by platform when layout ends\n */\n clearCurrentLayout() {\n this.currentLayoutId = null;\n this.emit('layout-cleared');\n }\n\n /**\n * Get the next layout from the 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 = parseLayoutFile(layoutFile);\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 = parseLayoutFile(layoutFile);\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 = parseLayoutFile(layoutFile);\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 * Go back to the previous layout in the schedule (round-robin, wraps around).\n * Called by platform layer in response to manual navigation (keyboard/remote).\n * Skips sync-manager logic — manual navigation is local only.\n */\n advanceToPreviousLayout() {\n if (this._layoutOverride) {\n log.info('Layout override active, not going back');\n return;\n }\n\n const layoutFiles = this.schedule.getCurrentLayouts();\n if (layoutFiles.length === 0) return;\n\n // Decrement index (wrap around)\n const prevIndex = (this._currentLayoutIndex - 1 + layoutFiles.length) % layoutFiles.length;\n\n const layoutFile = layoutFiles[prevIndex];\n const layoutId = parseLayoutFile(layoutFile);\n\n // No-op if it's the same layout (single-layout schedule) — don't restart\n if (layoutId === this.currentLayoutId) {\n log.info('Only one layout in schedule, nothing to go back to');\n return;\n }\n\n this._currentLayoutIndex = prevIndex;\n log.info(`Going back 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 = parseLayoutFile(layoutFile);\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 // ── Timeline (offline schedule prediction) ─────────────────────────\n\n /**\n * Parse all cached layout XLFs to extract durations for timeline calculation.\n * Called after collection completes and layouts are known.\n */\n async _buildLayoutDurations() {\n if (!this.cache?.getFile) return; // Cache doesn't support direct file access\n\n const layoutFiles = this.schedule.getCurrentLayouts();\n const defaultFile = this.schedule.schedule?.default;\n const allFiles = [...new Set([...layoutFiles, ...(defaultFile ? [defaultFile] : [])])];\n\n let parsed = 0;\n for (const file of allFiles) {\n const layoutId = parseLayoutFile(file);\n try {\n const xlfXml = await this.cache.getFile('layout', layoutId);\n if (xlfXml) {\n const duration = parseLayoutDuration(xlfXml);\n this._layoutDurations.set(file, duration);\n this._layoutDurations.set(String(layoutId), duration);\n parsed++;\n }\n } catch (e) {\n log.debug(`Could not parse duration for layout ${layoutId}:`, e.message);\n }\n }\n if (parsed > 0) {\n log.info(`[Timeline] Parsed durations for ${parsed} layouts`);\n }\n }\n\n /**\n * Calculate and log the upcoming playback timeline (next 2 hours).\n * Emits 'timeline-updated' with the full timeline array.\n */\n logUpcomingTimeline() {\n if (this._layoutDurations.size === 0) return;\n if (!this.schedule.getLayoutsAtTime) return; // Schedule doesn't support time queries\n\n const timeline = calculateTimeline(this.schedule, this._layoutDurations);\n if (timeline.length === 0) return;\n\n const lines = timeline.slice(0, 20).map(e => {\n const s = e.startTime.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });\n const end = e.endTime.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });\n return ` ${s}-${end} Layout ${e.layoutFile} (${e.duration}s)${e.isDefault ? ' [default]' : ''}`;\n });\n log.info(`[Timeline] Next ${timeline.length} plays:\\n${lines.join('\\n')}`);\n this.emit('timeline-updated', timeline);\n }\n\n /**\n * Record/correct a layout's actual duration (e.g., from video loadedmetadata).\n * Updates the durations map and re-logs the timeline if it changed.\n * @param {string} file - Layout file or layout ID string\n * @param {number} duration - Actual duration in seconds\n */\n recordLayoutDuration(file, duration) {\n const prev = this._layoutDurations.get(file);\n if (prev === duration) return; // No change\n\n this._layoutDurations.set(file, duration);\n log.debug(`[Timeline] Duration corrected: layout ${file} ${prev || '?'}s → ${duration}s`);\n this.logUpcomingTimeline();\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","/**\n * Download Progress Overlay\n *\n * Shows download status on hover (configurable, debug feature)\n * Displays: active downloads, progress, chunk status, queue info\n */\n\nexport interface DownloadOverlayConfig {\n enabled: boolean;\n updateInterval?: number; // ms between updates\n autoHide?: boolean; // Hide when no downloads\n}\n\nexport class DownloadOverlay {\n private overlay: HTMLElement | null = null;\n private config: DownloadOverlayConfig;\n private updateTimer: number | null = null;\n private _visible: boolean = false; // User-toggled visibility (D key)\n\n constructor(config: DownloadOverlayConfig) {\n this.config = {\n updateInterval: 1000,\n autoHide: true,\n ...config\n };\n\n if (this.config.enabled) {\n this.createOverlay();\n // Start hidden — only shown when downloads are active or user presses D\n this.overlay!.style.display = 'none';\n }\n }\n\n private createOverlay() {\n this.overlay = document.createElement('div');\n this.overlay.id = 'download-overlay';\n // Style like top status messages - always visible, clean design\n this.overlay.style.cssText = `\n position: fixed;\n top: 1.5vh;\n left: 1.5vw;\n background: rgba(0, 0, 0, 0.88);\n color: #fff;\n font-family: system-ui, -apple-system, sans-serif;\n font-size: 1.4vw;\n padding: 1vh 1.2vw;\n border-radius: 0.4vw;\n border: 1px solid rgba(255, 255, 255, 0.25);\n z-index: 999999;\n max-width: 35vw;\n box-shadow: 0 0.3vh 1.2vw rgba(0, 0, 0, 0.5);\n `;\n\n document.body.appendChild(this.overlay);\n }\n\n 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 const hasDownloads = !!html;\n\n if (hasDownloads) {\n this.overlay.innerHTML = html;\n if (this._visible) {\n this.overlay.style.display = 'block';\n }\n } else if (this._visible) {\n // User toggled on but no downloads — show idle status, keep polling\n this.overlay.innerHTML = '<div style=\"color: #6c6; font-size: 1.4vw;\">✓ All downloads complete</div>';\n this.overlay.style.display = 'block';\n } else {\n // Auto-triggered but no downloads left — stop polling, hide\n this.stopUpdating();\n this.overlay.style.display = 'none';\n }\n } else {\n throw new Error('Progress request failed');\n }\n } catch (error) {\n // No SW controller or request failed\n if (this._visible && this.overlay) {\n this.overlay.innerHTML = '<div style=\"color: #999; font-size: 1.4vw;\">⋯ Waiting for service worker</div>';\n } else {\n this.stopUpdating();\n if (this.overlay) {\n this.overlay.style.display = 'none';\n }\n }\n }\n }\n\n private renderStatus(progress: any): string {\n const downloads = progress || {};\n\n if (Object.keys(downloads).length === 0) {\n if (this.config.autoHide) {\n return ''; // Hide when no downloads\n }\n return `<div style=\"color: #6c6;\">✓ No downloads</div>`;\n }\n\n const numDownloads = Object.keys(downloads).length;\n let html = `<div style=\"font-weight: 600; margin-bottom: 0.8vh; font-size: 1.4vw;\">Downloads: ${numDownloads} active</div>`;\n\n for (const [url, progress] of Object.entries(downloads)) {\n const filename = this.extractFilename(url);\n const percent = Math.round((progress as any).percent || 0);\n const downloaded = this.formatBytes((progress as any).downloaded || 0);\n const total = this.formatBytes((progress as any).total || 0);\n\n html += `\n <div style=\"margin-bottom: 0.6vh; padding-bottom: 0.6vh; border-bottom: 1px solid rgba(255,255,255,0.1);\">\n <div style=\"font-size: 1.2vw; margin-bottom: 0.2vh;\">${filename}</div>\n <div style=\"background: rgba(255,255,255,0.1); height: 0.4vh; border-radius: 0.2vw; overflow: hidden;\">\n <div style=\"width: ${percent}%; height: 100%; background: #4a9eff; transition: width 0.3s;\"></div>\n </div>\n <div style=\"color: #999; font-size: 1.1vw; margin-top: 0.2vh;\">\n ${percent}% · ${downloaded} / ${total}\n </div>\n </div>\n `;\n }\n\n return html;\n }\n\n private extractFilename(key: string): string {\n // Key is now \"type/id\" (e.g. \"media/5\", \"layout/12\") — no URL parsing needed\n return key || 'unknown';\n }\n\n private formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n const kb = bytes / 1024;\n if (kb < 1024) return `${kb.toFixed(1)} KB`;\n const mb = kb / 1024;\n if (mb < 1024) return `${mb.toFixed(1)} MB`;\n return `${(mb / 1024).toFixed(1)} GB`;\n }\n\n /**\n * Toggle overlay visibility (D key).\n * When toggled on, starts polling. When toggled off, hides immediately.\n */\n public toggle() {\n if (!this.overlay) return;\n this._visible = !this._visible;\n if (this._visible) {\n this.overlay.style.display = 'block';\n this.updateOverlay(); // Immediate update\n this.startUpdating();\n } else {\n this.overlay.style.display = 'none';\n this.stopUpdating();\n }\n }\n\n /**\n * Start polling SW for download progress.\n * Safe to call multiple times — won't create duplicate timers.\n */\n public startUpdating() {\n this._visible = true;\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 * Timeline Overlay\n *\n * Toggleable debug overlay showing upcoming schedule timeline.\n * Displays: layout IDs, time ranges, durations, current layout highlight.\n * Positioned bottom-left (download overlay is top-left).\n */\n\ninterface TimelineEntry {\n layoutFile: string;\n startTime: Date;\n endTime: Date;\n duration: number;\n isDefault: boolean;\n}\n\nexport class TimelineOverlay {\n private overlay: HTMLElement | null = null;\n private visible: boolean;\n private timeline: TimelineEntry[] = [];\n private currentLayoutId: number | null = null;\n private offline: boolean = false;\n\n constructor(visible = false) {\n this.visible = visible;\n this.createOverlay();\n if (!this.visible) {\n this.overlay!.style.display = 'none';\n }\n }\n\n private createOverlay() {\n this.overlay = document.createElement('div');\n this.overlay.id = 'timeline-overlay';\n this.overlay.style.cssText = `\n position: fixed;\n bottom: 1.5vh;\n left: 1.5vw;\n background: rgba(0, 0, 0, 0.88);\n color: #fff;\n font-family: system-ui, -apple-system, sans-serif;\n font-size: 1.4vw;\n padding: 1vh 1.2vw;\n border-radius: 0.4vw;\n border: 1px solid rgba(255, 255, 255, 0.25);\n z-index: 999999;\n max-width: 35vw;\n box-shadow: 0 0.3vh 1.2vw rgba(0, 0, 0, 0.5);\n pointer-events: auto;\n `;\n document.body.appendChild(this.overlay);\n }\n\n toggle() {\n this.visible = !this.visible;\n if (this.overlay) {\n this.overlay.style.display = this.visible ? 'block' : 'none';\n }\n // Re-render when becoming visible (render() skips while hidden)\n if (this.visible) {\n this.render();\n }\n // Persist preference\n localStorage.setItem('xibo_show_timeline_overlay', String(this.visible));\n }\n\n /**\n * Update the overlay with new timeline data and/or current layout highlight.\n * Pass timeline=null to keep existing timeline and only update the highlight.\n */\n setOffline(offline: boolean) {\n this.offline = offline;\n this.render();\n }\n\n update(timeline: TimelineEntry[] | null, currentLayoutId: number | null) {\n if (timeline !== null) {\n this.timeline = timeline;\n }\n if (currentLayoutId !== null) {\n this.currentLayoutId = currentLayoutId;\n }\n this.render();\n }\n\n private render() {\n if (!this.overlay || !this.visible) return;\n\n const now = new Date();\n\n // Filter: show current (endTime > now) + future entries only\n const entries = this.timeline.filter(e => e.endTime > now);\n\n if (entries.length === 0) {\n this.overlay.innerHTML = '<div style=\"color: #999;\">Timeline — no upcoming layouts</div>';\n return;\n }\n\n const maxVisible = 8;\n const count = entries.length;\n const visible = entries.slice(0, maxVisible);\n const offlineBadge = this.offline ? ' <span style=\"color: #ff4444; font-size: 1.1vw;\">OFFLINE</span>' : '';\n let html = `<div style=\"font-weight: 600; margin-bottom: 0.8vh; font-size: 1.4vw; color: #ccc;\">Timeline (${count} upcoming)${offlineBadge}</div>`;\n\n for (const entry of visible) {\n const layoutId = parseInt(entry.layoutFile.replace('.xlf', ''), 10);\n // Current = the entry whose time window contains now AND matches current layout\n const isCurrent = layoutId === this.currentLayoutId\n && entry.startTime <= now && entry.endTime > now;\n\n const startStr = this.formatTime(entry.startTime);\n const endStr = this.formatTime(entry.endTime);\n const durStr = this.formatDuration(entry.duration);\n const marker = isCurrent ? '▶ ' : ' ';\n\n const borderLeft = isCurrent ? 'border-left: 0.25vw solid #4a9eff; padding-left: 0.6vw;' : 'padding-left: 0.85vw;';\n const color = isCurrent ? 'color: #fff;' : 'color: #ccc;';\n\n html += `<div style=\"${borderLeft} ${color} margin-bottom: 0.3vh; font-family: monospace; font-size: 1.3vw; line-height: 1.5; white-space: nowrap;\">`;\n html += `${marker}${startStr}–${endStr} #${layoutId} ${durStr}`;\n if (entry.isDefault) html += ' <span style=\"color: #888;\">[def]</span>';\n html += '</div>';\n }\n\n if (count > maxVisible) {\n html += `<div style=\"padding-left: 0.85vw; color: #888; font-size: 1.1vw; margin-top: 0.3vh;\">+${count - maxVisible} more</div>`;\n }\n\n this.overlay.innerHTML = html;\n }\n\n private formatTime(date: Date): string {\n return date.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });\n }\n\n private formatDuration(seconds: number): string {\n const m = Math.floor(seconds / 60);\n const s = Math.round(seconds % 60);\n return m > 0 ? `${m}m ${s.toString().padStart(2, '0')}s` : `${s}s`;\n }\n\n destroy() {\n if (this.overlay) {\n this.overlay.remove();\n this.overlay = null;\n }\n }\n}\n\n/**\n * Determine initial visibility from URL param or localStorage.\n */\nexport function isTimelineVisible(): boolean {\n const urlParams = new URLSearchParams(window.location.search);\n const showTimeline = urlParams.get('showTimeline');\n if (showTimeline !== null) {\n return showTimeline !== '0' && showTimeline !== 'false';\n }\n\n const saved = localStorage.getItem('xibo_show_timeline_overlay');\n if (saved !== null) {\n return saved === 'true';\n }\n\n return false;\n}\n","/**\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';\nimport { TimelineOverlay, isTimelineVisible } from './timeline-overlay.js';\n\ndeclare const __APP_VERSION__: string;\ndeclare const __BUILD_DATE__: string;\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\n// SDK package versions (populated in loadCoreModules)\nconst sdkVersions: Record<string, string> = {};\n\nclass PwaPlayer {\n private renderer!: RendererLite;\n private core!: PlayerCore;\n private xmds!: any;\n private downloadOverlay: DownloadOverlay | null = null;\n private timelineOverlay: TimelineOverlay | null = null;\n private statsCollector: any = null;\n private logReporter: any = null;\n private displaySettings: any = null;\n private currentScheduleId: number = -1; // Track scheduleId for stats\n private scheduledLayoutIds: Set<number> = new Set(); // Layout IDs from current schedule\n private preparingLayoutId: number | null = null; // Guard against concurrent 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 private _probeTimer: any = null; // Debounce timer for duration probing\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 this.setupRemoteControls();\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.updateConfigDisplay();\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);\n log.info('Download overlay enabled (hover bottom-right corner)');\n }\n\n // Initialize timeline overlay (toggleable with T key)\n this.timelineOverlay = new TimelineOverlay(isTimelineVisible());\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 // @ts-ignore\n const coreModule = await import('@xiboplayer/core');\n // @ts-ignore\n const rendererModule = await import('@xiboplayer/renderer');\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 // Capture SDK package versions\n sdkVersions.core = coreModule.VERSION || '?';\n sdkVersions.cache = cacheModule.VERSION || '?';\n sdkVersions.renderer = rendererModule.VERSION || '?';\n sdkVersions.schedule = scheduleModule.VERSION || '?';\n sdkVersions.xmds = xmdsModule.VERSION || '?';\n sdkVersions.xmr = xmrModule.VERSION || '?';\n sdkVersions.utils = configModule.VERSION || '?';\n sdkVersions.stats = statsModule.VERSION || '?';\n sdkVersions.settings = displaySettingsModule.VERSION || '?';\n\n // Get MAC address from Electron if available (for WOL support)\n if ((window as any).electronAPI?.getSystemInfo) {\n try {\n const sysInfo = await (window as any).electronAPI.getSystemInfo();\n if (sysInfo.macAddress) {\n config.macAddress = sysInfo.macAddress;\n }\n } catch (_) { /* pure PWA — no Electron API */ }\n }\n\n // Transport 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 version and environment information for debugging\n const buildDate = typeof __BUILD_DATE__ !== 'undefined' ? __BUILD_DATE__ : '?';\n const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '?';\n log.info(`v${appVersion} built ${buildDate}`);\n const versionParts = Object.entries(sdkVersions).map(([k, v]) => `${k}=${v}`).join(' ');\n log.info(`SDK: ${versionParts}`);\n const isElectron = !!(window as any).electronAPI;\n const electronVersion = isElectron ? (navigator.userAgent.match(/Electron\\/([\\d.]+)/)?.[1] || '?') : null;\n const chromeVersion = navigator.userAgent.match(/Chrome\\/([\\d.]+)/)?.[1] || '?';\n const platform = isElectron ? `Electron ${electronVersion} / Chrome ${chromeVersion}` : `Chrome ${chromeVersion}`;\n log.info(`Env: PWA v${appVersion} | ${platform} | ${navigator.platform} | ${screen.width}x${screen.height}`);\n\n log.info('Core modules loaded');\n } catch (error) {\n log.error('Failed to load core modules:', error);\n throw error;\n }\n }\n\n /**\n * Setup PlayerCore event handlers (Platform-specific UI updates)\n */\n private setupCoreEventHandlers() {\n // 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 (groupedFiles: any) => {\n // Platform handles the actual download via CacheProxy\n // Restart overlay polling while downloads are active\n this.downloadOverlay?.startUpdating();\n try {\n // groupedFiles is { layouts: [{ layoutId, mediaFiles }] }\n await cacheProxy.requestDownload(groupedFiles);\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 this.scheduledLayoutIds = scheduledIds;\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('no-layouts-scheduled', () => {\n this.updateStatus('No layouts scheduled');\n });\n\n this.core.on('collection-complete', () => {\n const layoutId = this.core.getCurrentLayoutId();\n if (layoutId) {\n this.updateStatus(`Playing layout ${layoutId}`);\n } else if (this.preparingLayoutId) {\n this.updateStatus(`Downloading layout ${this.preparingLayoutId}...`);\n }\n\n // Probe video durations for accurate timeline (metadata only, not full download)\n this.probeLayoutDurations().catch(err => {\n log.debug('Duration probe failed (non-blocking):', err);\n });\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());\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 // 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 // Timeline overlay — visualize upcoming schedule\n this.core.on('timeline-updated', (timeline: any[]) => {\n this.timelineOverlay?.update(timeline, this.core.getCurrentLayoutId());\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 * Setup keyboard and presenter remote controls.\n * Handles arrow keys, page up/down, space for next/prev/pause,\n * and MediaSession API for multimedia keyboard keys.\n */\n private setupRemoteControls() {\n // Keep focus on main document so keyboard shortcuts work even with widget iframes.\n // Iframes steal focus — this pulls it back after a short delay so interactive\n // widgets still work momentarily but keyboard control returns to the player.\n window.addEventListener('blur', () => {\n setTimeout(() => window.focus(), 200);\n });\n\n // Keyboard / presenter remote (clicker) controls\n document.addEventListener('keydown', (e: KeyboardEvent) => {\n switch (e.key) {\n case 'ArrowRight':\n case 'PageDown':\n case 'MediaTrackNext':\n if (this.core.peekNextLayout()) {\n log.info('[Remote] Next layout');\n this.core.advanceToNextLayout();\n }\n break;\n case 'ArrowLeft':\n case 'PageUp':\n case 'MediaTrackPrevious':\n log.info('[Remote] Previous layout');\n this.core.advanceToPreviousLayout();\n break;\n case ' ':\n e.preventDefault(); // prevent scroll\n if (this.renderer._paused) {\n log.info('[Remote] Resume');\n this.renderer.resume();\n } else {\n log.info('[Remote] Pause');\n this.renderer.pause();\n }\n break;\n case 'MediaPlayPause':\n if (this.renderer._paused) {\n log.info('[Remote] Resume (MediaPlayPause)');\n this.renderer.resume();\n } else {\n log.info('[Remote] Pause (MediaPlayPause)');\n this.renderer.pause();\n }\n break;\n case 't':\n case 'T':\n this.timelineOverlay?.toggle();\n break;\n case 'd':\n case 'D':\n this.downloadOverlay?.toggle();\n break;\n }\n });\n\n // MediaSession API for multimedia keys (only fires when media is active)\n if ('mediaSession' in navigator) {\n navigator.mediaSession.setActionHandler('nexttrack', () => {\n log.info('[Remote] Next layout (MediaSession)');\n this.core.advanceToNextLayout();\n });\n navigator.mediaSession.setActionHandler('previoustrack', () => {\n log.info('[Remote] Previous layout (MediaSession)');\n this.core.advanceToPreviousLayout();\n });\n navigator.mediaSession.setActionHandler('pause', () => {\n log.info('[Remote] Pause (MediaSession)');\n this.renderer.pause();\n });\n navigator.mediaSession.setActionHandler('play', () => {\n log.info('[Remote] Resume (MediaSession)');\n this.renderer.resume();\n });\n }\n\n log.info('Remote controls initialized (keyboard + MediaSession)');\n }\n\n private parseBody(body: string | null): any {\n try { return body ? JSON.parse(body) : {}; } catch (_) { return {}; }\n }\n\n /**\n * Handle an Interactive Control request from a widget\n */\n private handleInteractiveControl(method: string, path: string, search: string, body: string | null): any {\n log.debug('IC request:', method, path, search);\n\n switch (path) {\n case '/info':\n return {\n status: 200,\n body: JSON.stringify({\n hardwareKey: config.hardwareKey,\n displayName: config.displayName,\n playerType: 'pwa',\n currentLayoutId: this.core.getCurrentLayoutId()\n })\n };\n\n case '/trigger': {\n const data = this.parseBody(body);\n // Forward to renderer for layout-level actions (widget navigation)\n this.renderer.emit('interactiveTrigger', {\n targetId: data.id,\n triggerCode: data.trigger\n });\n // Forward to core for schedule-level actions (layout navigation)\n if (data.trigger) {\n this.core.handleTrigger(data.trigger);\n }\n return { status: 200, body: 'OK' };\n }\n\n case '/duration/expire': {\n const data = this.parseBody(body);\n log.info('IC: Widget duration expire requested for', data.id);\n this.renderer.emit('widgetExpire', { widgetId: data.id });\n return { status: 200, body: 'OK' };\n }\n\n case '/duration/extend': {\n const data = this.parseBody(body);\n log.info('IC: Widget duration extend by', data.duration, 'for', data.id);\n this.renderer.emit('widgetExtendDuration', {\n widgetId: data.id,\n duration: parseInt(data.duration)\n });\n return { status: 200, body: 'OK' };\n }\n\n case '/duration/set': {\n const data = this.parseBody(body);\n log.info('IC: Widget duration set to', data.duration, 'for', data.id);\n this.renderer.emit('widgetSetDuration', {\n widgetId: data.id,\n duration: parseInt(data.duration)\n });\n return { status: 200, body: 'OK' };\n }\n\n case '/fault': {\n const data = this.parseBody(body);\n this.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 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 // Debounced duration probe — run after downloads settle\n if (this._probeTimer) clearTimeout(this._probeTimer);\n this._probeTimer = setTimeout(() => {\n this._probeTimer = null;\n this.probeLayoutDurations().catch(() => {});\n }, 3000);\n }\n });\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 // Update timeline overlay highlight\n this.timelineOverlay?.update(null, layoutId);\n\n // Correct timeline duration if renderer discovered actual duration\n // (e.g., video loadedmetadata replaces the 60s estimate)\n if (_layout?.duration) {\n this.core.recordLayoutDuration(String(layoutId), _layout.duration);\n }\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 // Correct timeline duration when video metadata reveals actual duration\n this.renderer.on('layoutDurationUpdated', (layoutId: number, duration: number) => {\n this.core.recordLayoutDuration(String(layoutId), duration);\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 { allMedia: requiredMedia, videoMedia: videoMediaIds } = this.getMediaIds(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 { allMedia: requiredMedia, videoMedia: videoMediaIds } = this.getMediaIds(xlfXml);\n const allMediaCached = await this.checkAllMediaCached(requiredMedia, videoMediaIds);\n\n if (!allMediaCached) {\n // Reorder download queue: current layout's media first, hold others.\n // All files (including all chunks) must complete before other layouts start.\n cacheProxy.prioritizeLayoutFiles(requiredMedia.map(String));\n\n log.info(`Waiting for media to finish downloading for layout ${layoutId}`);\n this.updateStatus(`Preparing layout ${layoutId}...`);\n this.core.setPendingLayout(layoutId, requiredMedia);\n return; // Keep playing current layout until media is ready\n }\n\n // Fetch widget HTML 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 and video-specific IDs from layout XLF.\n * Single parse to avoid double DOMParser overhead on the same XML.\n */\n private getMediaIds(xlfXml: string): { allMedia: number[]; videoMedia: number[] } {\n const parser = new DOMParser();\n const doc = parser.parseFromString(xlfXml, 'text/xml');\n const allMedia: number[] = [];\n const videoMedia: number[] = [];\n\n doc.querySelectorAll('media[fileId]').forEach(el => {\n const fileId = el.getAttribute('fileId');\n if (fileId) {\n const id = parseInt(fileId, 10);\n allMedia.push(id);\n if (el.getAttribute('type') === 'video') {\n videoMedia.push(id);\n }\n }\n });\n\n // Include background image file ID from layout element\n const bgFileId = doc.querySelector('layout')?.getAttribute('background');\n if (bgFileId) {\n const parsed = parseInt(bgFileId, 10);\n if (!isNaN(parsed) && !allMedia.includes(parsed)) {\n allMedia.push(parsed);\n }\n }\n\n return { allMedia, videoMedia };\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 * 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 = `${PLAYER_BASE}/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 * Probe video durations for all scheduled layouts.\n * Uses preload=\"metadata\" — only fetches headers (~50KB), not the full video.\n * Feeds discovered durations into PlayerCore for accurate timeline calculation.\n */\n private async probeLayoutDurations() {\n if (this.scheduledLayoutIds.size === 0) return;\n\n for (const layoutId of this.scheduledLayoutIds) {\n\n try {\n const xlfBlob = await cacheManager.getCachedFile('layout', layoutId);\n if (!xlfBlob) continue;\n\n const xlfXml = await xlfBlob.text();\n const { videoMedia } = this.getMediaIds(xlfXml);\n if (videoMedia.length === 0) continue;\n\n // Parse XLF to find video widgets with duration=0 (use media length)\n const parser = new DOMParser();\n const doc = parser.parseFromString(xlfXml, 'text/xml');\n let maxDuration = 0;\n\n for (const mediaEl of doc.querySelectorAll('media[type=\"video\"]')) {\n const useDuration = mediaEl.getAttribute('useDuration');\n if (useDuration === '1') continue; // Has explicit CMS duration, skip\n\n const fileId = mediaEl.getAttribute('fileId');\n if (!fileId) continue;\n\n const exists = await cacheProxy.hasFile('media', fileId);\n if (!exists) continue;\n\n // Probe metadata only — does NOT download the full video\n const duration = await this.probeVideoDuration(`${PLAYER_BASE}/cache/media/${fileId}`);\n if (duration > 0) {\n maxDuration = Math.max(maxDuration, duration);\n }\n }\n\n if (maxDuration > 0) {\n this.core.recordLayoutDuration(String(layoutId), maxDuration);\n }\n } catch (err) {\n log.debug(`Duration probe failed for layout ${layoutId}:`, err);\n }\n }\n }\n\n /**\n * Probe a single video's duration using metadata only.\n * Creates a temporary <video preload=\"metadata\"> element, reads duration, destroys it.\n */\n private probeVideoDuration(url: string): Promise<number> {\n return new Promise((resolve) => {\n const video = document.createElement('video');\n video.preload = 'metadata';\n video.muted = true;\n\n const cleanup = () => {\n video.removeAttribute('src');\n video.load(); // Release resources\n };\n\n video.addEventListener('loadedmetadata', () => {\n const dur = Math.floor(video.duration);\n cleanup();\n resolve(dur);\n }, { once: true });\n\n video.addEventListener('error', () => {\n cleanup();\n resolve(0);\n }, { once: true });\n\n // Safety timeout — don't block forever\n setTimeout(() => {\n cleanup();\n resolve(0);\n }, 5000);\n\n video.src = url;\n });\n }\n\n /**\n * Update config display\n */\n private updateConfigDisplay() {\n const configEl = document.getElementById('config-info');\n if (configEl) {\n const version = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '?';\n const buildDate = typeof __BUILD_DATE__ !== 'undefined' ? __BUILD_DATE__.replace('T', ' ').replace(/\\.\\d+Z$/, '') : '';\n const versionStr = buildDate ? `v${version} (${buildDate})` : `v${version}`;\n configEl.textContent = `${versionStr} | 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 * Uses html2canvas directly — getDisplayMedia() is not used because:\n * - On Wayland, it always shows an OS-level picker dialog (XDG Desktop Portal)\n * - Chrome's --auto-select-desktop-capture-source flag only works on X11\n * - html2canvas works without permissions and captures the layout faithfully\n */\n private async captureWithBrowserMethods(): Promise<string> {\n this._screenshotMethod = 'html2canvas';\n return this.captureHtml2Canvas();\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 this.timelineOverlay?.setOffline(true);\n }\n\n private removeOfflineIndicator() {\n this.timelineOverlay?.setOffline(false);\n }\n\n /**\n * Cleanup\n */\n cleanup() {\n this.core.cleanup();\n this.renderer.cleanup();\n\n if (this._screenshotInterval) {\n clearInterval(this._screenshotInterval);\n this._screenshotInterval = null;\n }\n\n if (this._wakeLock) {\n this._wakeLock.release();\n this._wakeLock = null;\n }\n\n if (this.downloadOverlay) {\n this.downloadOverlay.destroy();\n }\n\n if (this.timelineOverlay) {\n this.timelineOverlay.destroy();\n }\n }\n}\n\nfunction startPlayer() {\n const player = new PwaPlayer();\n player.init().catch(error => {\n log.error('Failed to initialize:', error);\n });\n window.addEventListener('beforeunload', () => {\n player.cleanup();\n });\n}\n\nif (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', startPlayer);\n} else {\n startPlayer();\n}\n"],"file":"assets/main-BUvkpHsV.js"}
|