@xiboplayer/pwa 0.4.2 → 0.4.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/package.json +19 -34
- package/dist/assets/cms-api-Cqh1b8je.js +0 -5
- package/dist/assets/cms-api-Cqh1b8je.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-B0OWmZE0.js +0 -2
- package/dist/assets/index-B0OWmZE0.js.map +0 -1
- package/dist/assets/index-BTXprUS4.js +0 -2
- package/dist/assets/index-BTXprUS4.js.map +0 -1
- package/dist/assets/index-BnvZsfuZ.js +0 -13
- package/dist/assets/index-BnvZsfuZ.js.map +0 -1
- package/dist/assets/index-D6Ae3Fn0.js +0 -2
- package/dist/assets/index-D6Ae3Fn0.js.map +0 -1
- package/dist/assets/index-DbujXivg.js +0 -2
- package/dist/assets/index-DbujXivg.js.map +0 -1
- package/dist/assets/index-DpqLcm3c.js +0 -2
- package/dist/assets/index-DpqLcm3c.js.map +0 -1
- package/dist/assets/index-EMv3lAGt.js +0 -2
- package/dist/assets/index-EMv3lAGt.js.map +0 -1
- package/dist/assets/index-ghvCSzf2.js +0 -2
- package/dist/assets/index-ghvCSzf2.js.map +0 -1
- package/dist/assets/index-kE0POfeq.js +0 -2
- package/dist/assets/index-kE0POfeq.js.map +0 -1
- package/dist/assets/main-BoQ7K6cl.js +0 -649
- package/dist/assets/main-BoQ7K6cl.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-DteaE4RV.js +0 -2
- package/dist/assets/setup-DteaE4RV.js.map +0 -1
- package/dist/assets/sync-manager-WxSlmw-C.js +0 -2
- package/dist/assets/sync-manager-WxSlmw-C.js.map +0 -1
- package/dist/assets/widget-html-D8OwlTop.js +0 -2
- package/dist/assets/widget-html-D8OwlTop.js.map +0 -1
- package/dist/assets/xmds-client-BPdCcQeh.js +0 -16
- package/dist/assets/xmds-client-BPdCcQeh.js.map +0 -1
- package/dist/index.html +0 -131
- package/dist/pdf.worker.min.mjs +0 -21
- package/dist/setup.html +0 -372
- 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","layoutEl","layoutDurationAttr","regionAndDrawerEls","isDrawer","regionOptionsEl","el","exitTransType","exitTransDuration","exitTransDirection","loopEl","transType","transDuration","transDirection","child","widget","maxDuration","regionDuration","mediaEl","useDuration","optionsEl","rawEl","raw","transitions","audioNodes","uriEl","commands","commandsEl","cmdEl","parentWidgetId","displayOrder","cyclePlayback","playCount","isRandom","fromDt","toDt","render","blobUrls","maxRegionDuration","oldDuration","layoutDurationMs","allKeyboardActions","touchActionCount","action","widgetEl","widgetId","handler","source","keyboardActions","pressedKey","keycode","targetWidgetId","widgetIndex","w","nextIndex","targetWidget","prevIndex","bgUrl","err","mediaPromises","error","widgets","rid","idx","tagName","stream","e","playAfterSeek","videoEl","resolve","timer","onPlaying","audioEl","imgEl","onLoad","readyPromises","_b","a","audioElements","audioNode","audio","mediaId","audioSrc","playPromise","widgetElement","animPromise","animation","t","now","from","to","html","durationMatch","newDuration","numItemsMatch","numItems","groups","result","groupId","groupWidgets","b","selectedWidget","cycleIdx","r","showFn","hideFn","onCycleComplete","playNext","cmd","img","scaleType","fitMap","alignMap","valignMap","hPos","vPos","imageSrc","video","vScaleType","vFitMap","videoSrc","Hls","__vitePreload","hls","_event","data","videoDuration","errorCode","errorMessage","videoConstraints","deviceId","constraints","audioDuration","icon","info","filename","iframe","self","blob","pdfjsModule","basePath","pdfUrl","page","viewport","scale","scaledViewport","canvas","context","div","preloadDelay","retryDelay","wrapper","preloadMediaUrlCache","savedMediaUrlCache","savedCurrentLayoutId","preloadRegions","preloadBlobUrls","savedLayoutBlobUrls","v","preloaded","oldLayoutId","allComplete","priority","overlayDiv","overlayRegions","durationMs","overlayState","overlayIds","elapsed","fn","LayoutTranslator","xmds","doc","bgcolor","regions","top","left","zindex","media","transInEl","transOutEl","transInDurationEl","transOutDurationEl","transInDirectionEl","transOutDirectionEl","retries","lastError","attempt","widgetCacheKey","cacheWidgetHtml","delay","cachedKey","cached","cacheError","regionHTML","regionJS","mediaJS","m","widgetUrl","transIn","transOut","iframeId","startFn","stopFn","videoFilename","textUrl","audioId","audioLoop","audioVolume","pdfSrc","pdfContainerId","pdfDuration","DAY_NAMES","WEATHER_METRICS","getMetricValue","metric","displayProperties","weatherData","weatherKey","evaluateCondition","actual","condition","expected","evaluateCriteria","criteria","criterion","ScheduleManager","schedule","item","rangeEnd","currentDayOfWeek","d","interval","startDate","diffMs","diffDays","allowedDays","currentDayOfMonth","monthsDiff","date","day","currentTime","fromTime","toTime","time","results","campaign","hours","stepMs","conflicts","current","allLayouts","maxPriority","l","hidden","winners","winnerKey","hiddenKey","h","c","skipRateLimiting","skipInterrupts","quiet","_log","activeItems","normalLayouts","interruptLayouts","lastCheck","maxPlaysPerHour","oneHourAgo","playsInLastHour","timestamp","minGapMs","lastPlayTime","remainingMin","history","cleaned","layoutFile","meta","layouts","defaultLayout","interleaved","triggerCode","latitude","longitude","properties","geoLocation","defaultRadius","parts","s","fenceLat","fenceLng","radius","distance","within","lat1","lon1","lat2","lon2","toRad","deg","dLat","dLon","scheduleManager","logger","InterruptScheduler","requiredSeconds","resolvedInterruptLayouts","interruptSecondsInHour","index","satisfied","allSatisfied","currentInterrupt","normalSecondsInHour","resolvedNormalLayouts","loop","targetSeconds","resolved","remainingSeconds","pickCount","normalPick","interruptPick","normalIndex","interruptIndex","totalSecondsAllocated","OverlayScheduler","overlays","activeOverlays","priorityA","o","parseLayoutDuration","explicit","dur","arraysEqual","canSimulatedPlay","timeMs","lastPlay","seedPlayHistory","realHistory","simulated","timestamps","file","getPlayableLayouts","simPlays","eligible","calculateTimeline","durations","defaultDuration","currentLayoutStartedAt","timeline","isFirstEntry","hasFullApi","maxEntries","playable","hiddenLayouts","defaultFile","elapsedSec","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","CacheAnalyzer","store","settings","requiredFiles","key","tx","cachedReg","cachedSchedule","layoutFiles","prefix","regResult","applyCmsLogLevel","checkRf","checkSchedule","rfResult","files","purgeItems","layoutIds","layoutOrder","_c","nextWindow","_e","_d","report","_f","defaultLayoutId","_g","_h","xmrUrl","xmrCmsKey","collectIntervalSeconds","newIntervalSeconds","seconds","requiredMediaIds","replayId","fileType","isLayoutFile","isRequiredMedia","status","lat","lng","browser","apiKey","google","ip","tags","TAG_CONFIG_MAP","tag","pipeIdx","value","configKey","position","res","providers","provider","location","changeMode","commandCode","command","commandString","success","inventoryXml","complete","reason","was","commandKey","commandDate","weatherJson","syncManager","allFiles","parsed","lines","end","prev","DownloadOverlay","__publicField","mc","progressPromise","progress","downloads","percent","downloaded","total","bytes","kb","mb","enabled","getDefaultOverlayConfig","showDownloads","savedPref","TimelineOverlay","visible","onLayoutClick","target","offline","currentLayoutId","entries","maxVisible","offlineBadge","clickable","currentFound","isCurrent","startStr","endStr","durStr","hiddenIds","isTimelineVisible","showTimeline","saved","PLAYER_BASE","RestClient","XmdsClient","XmrWrapper","cacheProxy","StatsCollector","formatStats","LogReporter","formatLogs","DisplaySettings","SyncManager","sdkVersions","PwaPlayer","registration","CacheProxy","streamingUrl","cacheKey","overlayConfig","cacheModule","xmdsModule","scheduleModule","configModule","xmrModule","statsModule","displaySettingsModule","coreModule","rendererModule","syncModule","sysInfo","registerLogSink","level","name","message","buildDate","appVersion","versionParts","k","isElectron","electronVersion","chromeVersion","platform","displayName","syncConfig","followerId","statsXml","ack","logsXml","_displayId","isOffline","purgeFiles","groupedFiles","scheduledIds","cleared","debugNow","isDebug","cacheNames","newInterval","_settings","changes","method","path","search","body","port","videos","show","connectorData","_layout","pending","actionType","layoutCode","targetId","nextLayoutId","xlfBlob","requiredMedia","videoMediaIds","allMedia","videoMedia","bgFileId","mediaIds","cache","metadataResponse","metadataText","metadata","sizeMB","lastIdx","lastChunkKey","sizeKB","sizeStr","fetchPromises","cachedResponse","newRaw","videoDurations","useDur","probedDur","cleanup","configEl","version","versionStr","stats","logs","logXml","code","details","fault","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","intervalSecs","statusEl","peer","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/renderer/src/layout.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 'fade':\n case 'fadein':\n return isIn ? this.fadeIn(element, duration) : null;\n case 'fadeout':\n return isIn ? null : this.fadeOut(element, duration);\n case 'fly':\n 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 this.audioOverlays = new Map(); // widgetId => [HTMLAudioElement] (audio overlays for widgets)\n\n // Scale state (for fitting layout to screen)\n this.scaleFactor = 1;\n this.offsetX = 0;\n this.offsetY = 0;\n\n // Overlay state\n this.overlayContainer = null;\n this.activeOverlays = new Map(); // layoutId => { container, layout, timer, regions }\n\n // Interactive action state\n this._keydownHandler = null; // Document keydown listener (single, shared)\n this._keyboardActions = []; // Active keyboard actions for current layout\n\n // Sub-playlist cycle state (round-robin per parentWidgetId group)\n this._subPlaylistCycleIndex = new Map();\n\n // 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 id: actionEl.getAttribute('id') || '',\n actionType: actionEl.getAttribute('actionType') || '',\n triggerType: actionEl.getAttribute('triggerType') || '',\n triggerCode: actionEl.getAttribute('triggerCode') || '',\n source: actionEl.getAttribute('source') || '',\n sourceId: actionEl.getAttribute('sourceId') || '',\n target: actionEl.getAttribute('target') || '',\n targetId: actionEl.getAttribute('targetId') || '',\n widgetId: actionEl.getAttribute('widgetId') || '',\n layoutCode: actionEl.getAttribute('layoutCode') || '',\n commandCode: actionEl.getAttribute('commandCode') || ''\n });\n }\n return actions;\n }\n\n /**\n * Parse XLF XML to layout object\n * @param {string} xlfXml - XLF XML content\n * @returns {Object} Parsed layout\n */\n parseXlf(xlfXml) {\n const parser = new DOMParser();\n const doc = parser.parseFromString(xlfXml, 'text/xml');\n\n const layoutEl = doc.querySelector('layout');\n if (!layoutEl) {\n throw new Error('Invalid XLF: no <layout> element');\n }\n\n const layoutDurationAttr = layoutEl.getAttribute('duration');\n const layout = {\n schemaVersion: parseInt(layoutEl.getAttribute('schemaVersion') || '1'),\n width: parseInt(layoutEl.getAttribute('width') || '1920'),\n height: parseInt(layoutEl.getAttribute('height') || '1080'),\n duration: layoutDurationAttr ? parseInt(layoutDurationAttr) : 0, // 0 = calculate from widgets\n bgcolor: layoutEl.getAttribute('backgroundColor') || layoutEl.getAttribute('bgcolor') || '#000000',\n background: layoutEl.getAttribute('background') || null, // Background image fileId\n enableStat: layoutEl.getAttribute('enableStat') !== '0', // absent or \"1\" = enabled\n actions: this.parseActions(layoutEl),\n regions: []\n };\n\n if (layout.schemaVersion > 1) {\n this.log.debug(`XLF schema version: ${layout.schemaVersion}`);\n }\n\n if (layoutDurationAttr) {\n this.log.info(`Layout duration from XLF: ${layout.duration}s`);\n } else {\n this.log.info(`Layout duration NOT in XLF, will calculate from widgets`);\n }\n\n // Parse regions and drawers (drawers are invisible regions for interactive actions)\n const regionAndDrawerEls = layoutEl.querySelectorAll(':scope > region, :scope > drawer');\n for (const regionEl of regionAndDrawerEls) {\n const isDrawer = regionEl.tagName === 'drawer';\n const region = {\n id: regionEl.getAttribute('id'),\n width: parseInt(regionEl.getAttribute('width') || '0'),\n height: parseInt(regionEl.getAttribute('height') || '0'),\n top: parseInt(regionEl.getAttribute('top') || '0'),\n left: parseInt(regionEl.getAttribute('left') || '0'),\n zindex: parseInt(regionEl.getAttribute('zindex') || (isDrawer ? '2000' : '0')),\n enableStat: regionEl.getAttribute('enableStat') !== '0',\n actions: this.parseActions(regionEl),\n exitTransition: null,\n transitionType: null, // Region-level default widget transition type\n transitionDuration: null,\n transitionDirection: null,\n loop: true, // Default: cycle widgets. Spec: loop=0 means single media stays visible\n isDrawer,\n widgets: []\n };\n\n // Parse region-level options (exit transitions, loop)\n // Use direct children only to avoid matching <options> inside <media>\n const regionOptionsEl = Array.from(regionEl.children).find(el => el.tagName === 'options');\n if (regionOptionsEl) {\n const exitTransType = regionOptionsEl.querySelector('exitTransType');\n if (exitTransType && exitTransType.textContent) {\n const exitTransDuration = regionOptionsEl.querySelector('exitTransDuration');\n const exitTransDirection = regionOptionsEl.querySelector('exitTransDirection');\n region.exitTransition = {\n type: exitTransType.textContent,\n duration: parseInt((exitTransDuration && exitTransDuration.textContent) || '1000'),\n direction: (exitTransDirection && exitTransDirection.textContent) || 'N'\n };\n }\n\n // Region loop option: 0 = single media stays on screen, 1 = cycles (default)\n const loopEl = regionOptionsEl.querySelector('loop');\n if (loopEl) {\n region.loop = loopEl.textContent !== '0';\n }\n\n // Region-level default transition for widgets (applied if widget has no own transition)\n const transType = regionOptionsEl.querySelector('transitionType');\n if (transType && transType.textContent) {\n region.transitionType = transType.textContent;\n const transDuration = regionOptionsEl.querySelector('transitionDuration');\n const transDirection = regionOptionsEl.querySelector('transitionDirection');\n region.transitionDuration = parseInt((transDuration && transDuration.textContent) || '1000');\n region.transitionDirection = (transDirection && transDirection.textContent) || 'N';\n }\n }\n\n // Parse media/widgets (use direct children to avoid nested matches)\n for (const child of regionEl.children) {\n if (child.tagName !== 'media') continue;\n const widget = this.parseWidget(child);\n region.widgets.push(widget);\n }\n\n layout.regions.push(region);\n\n if (isDrawer) {\n this.log.info(`Parsed drawer: id=${region.id} with ${region.widgets.length} widgets`);\n }\n }\n\n // Calculate layout duration if not specified (duration=0)\n // Drawers don't contribute to layout duration (they're action-triggered)\n if (layout.duration === 0) {\n let maxDuration = 0;\n\n for (const region of layout.regions) {\n if (region.isDrawer) continue;\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 // Parse audio overlay nodes (<audio> child elements on the widget)\n // Spec format: <audio><uri volume=\"\" loop=\"\" mediaId=\"\">filename.mp3</uri></audio>\n // Also supports flat format: <audio mediaId=\"\" uri=\"\" volume=\"\" loop=\"\">\n const audioNodes = [];\n for (const child of mediaEl.children) {\n if (child.tagName.toLowerCase() === 'audio') {\n const uriEl = child.querySelector('uri');\n if (uriEl) {\n // Spec format: attributes on <uri>, filename as text content\n audioNodes.push({\n mediaId: uriEl.getAttribute('mediaId') || null,\n uri: uriEl.textContent || '',\n volume: parseInt(uriEl.getAttribute('volume') || '100'),\n loop: uriEl.getAttribute('loop') === '1'\n });\n } else {\n // Flat format fallback: attributes directly on <audio>\n audioNodes.push({\n mediaId: child.getAttribute('mediaId') || null,\n uri: child.getAttribute('uri') || '',\n volume: parseInt(child.getAttribute('volume') || '100'),\n loop: child.getAttribute('loop') === '1'\n });\n }\n }\n }\n\n // Parse commands on media (shell/native commands triggered on widget start)\n // Spec: <commands><command commandCode=\"code\" commandString=\"args\"/></commands>\n const commands = [];\n const commandsEl = Array.from(mediaEl.children).find(el => el.tagName === 'commands');\n if (commandsEl) {\n for (const cmdEl of commandsEl.children) {\n if (cmdEl.tagName === 'command') {\n commands.push({\n commandCode: cmdEl.getAttribute('commandCode') || '',\n commandString: cmdEl.getAttribute('commandString') || ''\n });\n }\n }\n }\n\n // Sub-playlist attributes (widgets grouped by parentWidgetId)\n const parentWidgetId = mediaEl.getAttribute('parentWidgetId') || null;\n const displayOrder = parseInt(mediaEl.getAttribute('displayOrder') || '0');\n const cyclePlayback = mediaEl.getAttribute('cyclePlayback') === '1';\n const playCount = parseInt(mediaEl.getAttribute('playCount') || '0');\n const isRandom = mediaEl.getAttribute('isRandom') === '1';\n\n // Media expiry dates (per-widget time-gating within a layout)\n const fromDt = mediaEl.getAttribute('fromDt') || mediaEl.getAttribute('fromdt') || null;\n const toDt = mediaEl.getAttribute('toDt') || mediaEl.getAttribute('todt') || null;\n\n // Render mode: 'native' (player renders directly) or 'html' (use GetResource)\n const render = mediaEl.getAttribute('render') || null;\n\n return {\n type,\n duration,\n useDuration, // Whether to use specified duration (1) or media length (0)\n id,\n fileId, // Media library file ID for cache lookup\n render, // 'native' or 'html' — null means use type-based dispatch\n fromDt, // Widget valid-from date (Y-m-d H:i:s)\n toDt, // Widget valid-to date (Y-m-d H:i:s)\n enableStat: mediaEl.getAttribute('enableStat') !== '0', // absent or \"1\" = enabled\n webhookUrl: options.webhookUrl || null,\n options,\n raw,\n transitions,\n actions,\n audioNodes, // Audio overlays attached to this widget\n commands, // Shell commands triggered on widget start\n parentWidgetId,\n displayOrder,\n cyclePlayback,\n playCount,\n isRandom\n };\n }\n\n /**\n * Track blob URL for lifecycle management\n * @param {string} blobUrl - Blob URL to track\n */\n trackBlobUrl(blobUrl) {\n 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 if (region.isDrawer) continue;\n let regionDuration = 0;\n\n for (const widget of region.widgets) {\n if (widget.duration > 0) {\n regionDuration += widget.duration;\n }\n }\n\n maxRegionDuration = Math.max(maxRegionDuration, regionDuration);\n }\n\n // 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 // Layout-level actions (attached to the main container)\n for (const action of (layout.actions || [])) {\n if (action.triggerType === 'touch') {\n this.attachTouchAction(this.container, action, null, null);\n touchActionCount++;\n } else if (action.triggerType?.startsWith('keyboard:')) {\n allKeyboardActions.push(action);\n }\n }\n\n for (const regionConfig of layout.regions) {\n const region = this.regions.get(regionConfig.id);\n if (!region) continue;\n\n // Region-level actions\n for (const action of (regionConfig.actions || [])) {\n if (action.triggerType === 'touch') {\n this.attachTouchAction(region.element, action, regionConfig.id, null);\n touchActionCount++;\n } else if (action.triggerType.startsWith('keyboard:')) {\n allKeyboardActions.push(action);\n }\n }\n\n // Widget-level actions\n for (const widget of regionConfig.widgets) {\n if (!widget.actions || widget.actions.length === 0) continue;\n const widgetEl = region.widgetElements.get(widget.id);\n if (!widgetEl) continue;\n\n for (const action of widget.actions) {\n if (action.triggerType === 'touch') {\n this.attachTouchAction(widgetEl, action, regionConfig.id, widget.id);\n touchActionCount++;\n } else if (action.triggerType.startsWith('keyboard:')) {\n allKeyboardActions.push(action);\n }\n }\n }\n }\n\n this.setupKeyboardListener(allKeyboardActions);\n\n if (touchActionCount > 0 || allKeyboardActions.length > 0) {\n this.log.info(`Actions attached: ${touchActionCount} touch, ${allKeyboardActions.length} keyboard`);\n }\n }\n\n /**\n * Attach a click listener to an element for a touch-triggered action.\n */\n attachTouchAction(element, action, regionId, widgetId) {\n element.style.cursor = 'pointer';\n\n const handler = (event) => {\n event.stopPropagation();\n const source = widgetId ? `widget ${widgetId}` : `region ${regionId}`;\n this.log.info(`Touch action fired on ${source}: ${action.actionType}`);\n\n this.emit('action-trigger', {\n actionType: action.actionType,\n triggerType: 'touch',\n triggerCode: action.triggerCode,\n layoutCode: action.layoutCode,\n targetId: action.targetId,\n commandCode: action.commandCode,\n source: { regionId, widgetId }\n });\n };\n\n element.addEventListener('click', handler);\n if (!element._actionHandlers) element._actionHandlers = [];\n element._actionHandlers.push(handler);\n }\n\n /**\n * Setup document-level keyboard listener for keyboard-triggered actions.\n */\n setupKeyboardListener(keyboardActions) {\n this.removeKeyboardListener();\n this._keyboardActions = keyboardActions;\n if (keyboardActions.length === 0) return;\n\n this._keydownHandler = (event) => {\n const pressedKey = event.key;\n for (const action of this._keyboardActions) {\n const keycode = action.triggerType.substring('keyboard:'.length);\n if (pressedKey === keycode) {\n this.log.info(`Keyboard action (key: ${pressedKey}): ${action.actionType}`);\n this.emit('action-trigger', {\n actionType: action.actionType,\n triggerType: action.triggerType,\n triggerCode: action.triggerCode,\n layoutCode: action.layoutCode,\n targetId: action.targetId,\n commandCode: action.commandCode,\n source: { key: pressedKey }\n });\n break;\n }\n }\n };\n\n document.addEventListener('keydown', this._keydownHandler);\n }\n\n /** Remove the document-level keyboard listener */\n removeKeyboardListener() {\n if (this._keydownHandler) {\n document.removeEventListener('keydown', this._keydownHandler);\n this._keydownHandler = null;\n }\n this._keyboardActions = [];\n }\n\n /** Remove all action listeners (touch + keyboard) */\n removeActionListeners() {\n for (const [, region] of this.regions) {\n this._cleanElementActionHandlers(region.element);\n for (const [, widgetEl] of region.widgetElements) {\n this._cleanElementActionHandlers(widgetEl);\n }\n }\n this.removeKeyboardListener();\n }\n\n _cleanElementActionHandlers(element) {\n if (element._actionHandlers) {\n for (const handler of element._actionHandlers) {\n element.removeEventListener('click', handler);\n }\n delete element._actionHandlers;\n element.style.cursor = '';\n }\n }\n\n /**\n * Navigate to a specific widget within a region (for navWidget actions)\n */\n navigateToWidget(targetWidgetId) {\n for (const [regionId, region] of this.regions) {\n const widgetIndex = region.widgets.findIndex(w => w.id === targetWidgetId);\n if (widgetIndex === -1) continue;\n\n this.log.info(`Navigating to widget ${targetWidgetId} in region ${regionId} (index ${widgetIndex})`);\n\n // Show drawer region if hidden (drawers start display:none)\n if (region.isDrawer && region.element.style.display === 'none') {\n region.element.style.display = '';\n this.log.info(`Drawer region ${regionId} revealed`);\n }\n\n if (region.timer) {\n clearTimeout(region.timer);\n region.timer = null;\n }\n\n this.stopWidget(regionId, region.currentIndex);\n region.currentIndex = widgetIndex;\n this.renderWidget(regionId, widgetIndex);\n\n if (region.widgets.length > 1) {\n const widget = region.widgets[widgetIndex];\n const duration = widget.duration * 1000;\n region.timer = setTimeout(() => {\n this.stopWidget(regionId, widgetIndex);\n const nextIndex = (widgetIndex + 1) % region.widgets.length;\n region.currentIndex = nextIndex;\n // For drawers, hide again after last widget; for normal regions, continue cycling\n if (region.isDrawer && nextIndex === 0) {\n region.element.style.display = 'none';\n this.log.info(`Drawer region ${regionId} hidden (cycle complete)`);\n } else {\n this.startRegion(regionId);\n }\n }, duration);\n } else if (region.isDrawer) {\n // Single-widget drawer: hide after widget duration\n const widget = region.widgets[widgetIndex];\n const duration = widget.duration * 1000;\n region.timer = setTimeout(() => {\n this.stopWidget(regionId, widgetIndex);\n region.element.style.display = 'none';\n this.log.info(`Drawer region ${regionId} hidden (single widget done)`);\n }, duration);\n }\n return;\n }\n this.log.warn(`Target widget ${targetWidgetId} not found in any region`);\n }\n\n /**\n * Navigate to the next widget in a region (wraps around)\n * @param {string} [regionId] - Target region. If omitted, uses the first region.\n */\n nextWidget(regionId) {\n const region = regionId ? this.regions.get(regionId) : this.regions.values().next().value;\n if (!region || region.widgets.length <= 1) return;\n\n const nextIndex = (region.currentIndex + 1) % region.widgets.length;\n const targetWidget = region.widgets[nextIndex];\n this.log.info(`nextWidget → index ${nextIndex} (widget ${targetWidget.id})`);\n this.navigateToWidget(targetWidget.id);\n }\n\n /**\n * Navigate to the previous widget in a region (wraps around)\n * @param {string} [regionId] - Target region. If omitted, uses the first region.\n */\n previousWidget(regionId) {\n const region = regionId ? this.regions.get(regionId) : this.regions.values().next().value;\n if (!region || region.widgets.length <= 1) return;\n\n const prevIndex = (region.currentIndex - 1 + region.widgets.length) % region.widgets.length;\n const targetWidget = region.widgets[prevIndex];\n this.log.info(`previousWidget → index ${prevIndex} (widget ${targetWidget.id})`);\n this.navigateToWidget(targetWidget.id);\n }\n\n // ── Layout 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 (except drawers)\n for (const [regionId, region] of this.regions) {\n if (region.isDrawer) continue;\n this.startRegion(regionId);\n }\n\n // Wait for all initial widgets to be ready then start layout timer\n this.startLayoutTimerWhenReady(layoutId, this.currentLayout);\n\n this.log.info(`Layout ${layoutId} restarted (reused elements)`);\n\n // Schedule next layout preload for same-layout replay\n this._scheduleNextLayoutPreload(this.currentLayout);\n\n return; // EARLY RETURN - skip recreation below\n }\n\n // Check if this layout was preloaded in the pool\n if (this.layoutPool.has(layoutId)) {\n this.log.info(`Layout ${layoutId} found in preload pool - instant swap!`);\n await this._swapToPreloadedLayout(layoutId);\n return; // EARLY RETURN - preloaded layout swapped in\n }\n\n // Different layout - full teardown and rebuild\n this.log.info(`Switching to new layout ${layoutId}`);\n this.stopCurrentLayout();\n\n // Parse XLF\n const layout = this.parseXlf(xlfXml);\n this.currentLayout = layout;\n this.currentLayoutId = layoutId;\n\n // Calculate scale factor to fit layout into screen\n this.calculateScale(layout);\n\n // Set container background\n this.container.style.backgroundColor = layout.bgcolor;\n this.container.style.backgroundImage = ''; // Reset previous\n\n // Apply background image if specified in XLF\n 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.position = 'absolute';\n element.style.top = '0';\n element.style.left = '0';\n element.style.width = '100%';\n element.style.height = '100%';\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 (except drawers — they're action-triggered)\n for (const [regionId, region] of this.regions) {\n if (region.isDrawer) continue;\n this.startRegion(regionId);\n }\n\n // Wait for all initial widgets to be ready (videos playing, images loaded)\n // THEN start the layout timer — ensures videos play to their last frame\n this.startLayoutTimerWhenReady(layoutId, layout);\n\n // Schedule preloading of the next layout at 75% of current duration\n this._scheduleNextLayoutPreload(layout);\n\n this.log.info(`Layout ${layoutId} started`);\n\n } catch (error) {\n this.log.error('Error rendering layout:', error);\n this.emit('error', { type: 'layoutError', error, layoutId });\n throw error;\n }\n }\n\n /**\n * 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 // Drawer regions start fully hidden — shown only by navWidget actions\n if (regionConfig.isDrawer) {\n regionEl.style.display = 'none';\n }\n\n // Apply scaled positioning\n this.applyRegionScale(regionEl, regionConfig);\n\n this.container.appendChild(regionEl);\n\n // Filter expired widgets (fromDt/toDt time-gating within XLF)\n let widgets = regionConfig.widgets.filter(w => this._isWidgetActive(w));\n\n // For regions with sub-playlist cycle playback, select which widgets play this cycle\n if (widgets.some(w => w.cyclePlayback)) {\n widgets = this._applyCyclePlayback(widgets);\n }\n\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,\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 isDrawer: regionConfig.isDrawer || false,\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 // render=\"html\" forces GetResource iframe regardless of native type,\n // EXCEPT for types we handle natively (PDF: CMS bundle can't work cross-origin)\n if (widget.render === 'html' && widget.type !== 'pdf') {\n return await this.renderGenericWidget(widget, region);\n }\n\n switch (widget.type) {\n case 'image':\n return await this.renderImage(widget, region);\n case 'video':\n return await this.renderVideo(widget, region);\n case 'audio':\n return await this.renderAudio(widget, region);\n case 'text':\n case 'ticker':\n return await this.renderTextWidget(widget, region);\n case 'pdf':\n return await this.renderPdf(widget, region);\n case 'webpage':\n return await this.renderWebpage(widget, region);\n case 'localvideo':\n return await this.renderVideo(widget, region);\n case 'videoin':\n return await this.renderVideoIn(widget, region);\n case 'powerpoint':\n case 'flash':\n // Legacy Windows-only types — show placeholder instead of failing silently\n this.log.warn(`Widget type '${widget.type}' is not supported on web players (widget ${widget.id})`);\n return this._renderUnsupportedPlaceholder(widget, region);\n default:\n // Generic widget (clock, calendar, weather, etc.)\n return await this.renderGenericWidget(widget, region);\n }\n }\n\n /**\n * Helper: Find media element within widget (works for both direct and wrapped elements)\n * @param {HTMLElement} element - Widget element (might BE the media element or contain it)\n * @param {string} tagName - Tag name to find ('VIDEO', 'AUDIO', 'IMG', 'IFRAME')\n * @returns {HTMLElement|null}\n */\n findMediaElement(element, tagName) {\n // Check if element IS the tag, or contains it as a descendant\n return element.tagName === tagName ? element : element.querySelector(tagName.toLowerCase());\n }\n\n /**\n * Update media element for dynamic content (videos/audio need restart)\n * @param {HTMLElement} element - Widget element\n * @param {Object} widget - Widget config\n */\n updateMediaElement(element, widget) {\n // Restart video or audio on widget show (even if looping)\n const mediaEl = this.findMediaElement(element, 'VIDEO') || this.findMediaElement(element, 'AUDIO');\n if (mediaEl) {\n // Re-acquire webcam stream if it was stopped during _hideWidget()\n if (mediaEl.tagName === 'VIDEO' && mediaEl._mediaConstraints && !mediaEl._mediaStream) {\n navigator.mediaDevices.getUserMedia(mediaEl._mediaConstraints).then(stream => {\n mediaEl.srcObject = stream;\n mediaEl._mediaStream = stream;\n this.log.info(`Webcam stream re-acquired for widget ${widget.id}`);\n }).catch(e => {\n this.log.warn('Failed to re-acquire webcam stream:', e.message);\n });\n return; // srcObject auto-plays, no need for _restartMediaElement\n }\n\n this._restartMediaElement(mediaEl);\n this.log.info(`${mediaEl.tagName === 'VIDEO' ? 'Video' : 'Audio'} restarted: ${widget.fileId || widget.id}`);\n }\n }\n\n /**\n * Restart a media element from the beginning.\n * Waits for seek to complete before playing — avoids DOMException\n * \"The play() request was interrupted\" when calling play() mid-seek.\n */\n _restartMediaElement(el) {\n el.currentTime = 0;\n const playAfterSeek = () => {\n el.removeEventListener('seeked', playAfterSeek);\n el.play().catch(() => {});\n };\n el.addEventListener('seeked', playAfterSeek);\n // 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 // Audio widgets: wait for playback to start\n const audioEl = this.findMediaElement(element, 'AUDIO');\n if (audioEl) {\n if (!audioEl.paused && audioEl.readyState >= 3) {\n return Promise.resolve();\n }\n return new Promise((resolve) => {\n const timer = setTimeout(() => {\n this.log.warn(`Audio ready timeout (${READY_TIMEOUT}ms) for widget ${widget.id}`);\n resolve();\n }, READY_TIMEOUT);\n const onPlaying = () => {\n audioEl.removeEventListener('playing', onPlaying);\n clearTimeout(timer);\n this.log.info(`Audio widget ${widget.id} ready (playing)`);\n resolve();\n };\n audioEl.addEventListener('playing', onPlaying);\n });\n }\n\n // Image widgets: wait for image decode\n const imgEl = this.findMediaElement(element, 'IMG');\n if (imgEl) {\n if (imgEl.complete && imgEl.naturalWidth > 0) {\n return Promise.resolve();\n }\n return new Promise((resolve) => {\n const 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 element.style.position = 'absolute';\n element.style.top = '0';\n element.style.left = '0';\n element.style.width = '100%';\n element.style.height = '100%';\n region.widgetElements.set(widget.id, element);\n region.element.appendChild(element);\n }\n\n // Hide all other widgets in region\n // Cancel fill:forwards animations first — they override inline styles\n for (const [widgetId, widgetEl] of region.widgetElements) {\n if (widgetId !== widget.id) {\n widgetEl.getAnimations?.().forEach(a => a.cancel());\n widgetEl.style.visibility = 'hidden';\n widgetEl.style.opacity = '0';\n }\n }\n\n this.updateMediaElement(element, widget);\n element.getAnimations?.().forEach(a => a.cancel());\n element.style.visibility = 'visible';\n\n if (widget.transitions.in) {\n Transitions.apply(element, widget.transitions.in, true, region.width, region.height);\n } else {\n element.style.opacity = '1';\n }\n\n // Start audio overlays attached to this widget\n this._startAudioOverlays(widget);\n\n return widget;\n }\n\n /**\n * Start audio overlay elements for a widget.\n * Audio overlays are <audio> child nodes in the XLF that play alongside\n * the visual widget (e.g. background music for an image slideshow).\n * @param {Object} widget - Widget config with audioNodes array\n */\n _startAudioOverlays(widget) {\n if (!widget.audioNodes || widget.audioNodes.length === 0) return;\n\n // Stop any existing audio overlays for this widget first\n this._stopAudioOverlays(widget.id);\n\n const audioElements = [];\n for (const audioNode of widget.audioNodes) {\n if (!audioNode.uri) continue;\n\n const audio = document.createElement('audio');\n audio.autoplay = true;\n audio.loop = audioNode.loop;\n audio.volume = Math.max(0, Math.min(1, audioNode.volume / 100));\n\n // Resolve audio URI via cache/proxy\n const mediaId = parseInt(audioNode.mediaId);\n let audioSrc = mediaId ? this.mediaUrlCache.get(mediaId) : null;\n\n if (!audioSrc && mediaId && this.options.getMediaUrl) {\n // Async — fire and forget, set src when ready\n this.options.getMediaUrl(mediaId).then(url => {\n audio.src = url;\n }).catch(() => {\n audio.src = `${window.location.origin}/player/cache/media/${audioNode.uri}`;\n });\n } else if (!audioSrc) {\n audio.src = `${window.location.origin}/player/cache/media/${audioNode.uri}`;\n } else {\n audio.src = audioSrc;\n }\n\n // Append to DOM to prevent garbage collection in some browsers\n audio.style.display = 'none';\n this.container.appendChild(audio);\n\n // Handle autoplay restrictions gracefully (play() may return undefined in some envs)\n const playPromise = audio.play();\n if (playPromise && playPromise.catch) playPromise.catch(() => {});\n\n audioElements.push(audio);\n this.log.info(`Audio overlay started for widget ${widget.id}: ${audioNode.uri} (loop=${audioNode.loop}, vol=${audioNode.volume})`);\n }\n\n if (audioElements.length > 0) {\n this.audioOverlays.set(widget.id, audioElements);\n }\n }\n\n /**\n * Stop and clean up audio overlay elements for a widget.\n * @param {string} widgetId - Widget ID\n */\n _stopAudioOverlays(widgetId) {\n const audioElements = this.audioOverlays.get(widgetId);\n if (!audioElements) return;\n\n for (const audio of audioElements) {\n audio.pause();\n audio.removeAttribute('src');\n audio.load(); // Release resources\n if (audio.parentNode) audio.parentNode.removeChild(audio); // Remove from DOM\n }\n\n this.audioOverlays.delete(widgetId);\n this.log.info(`Audio overlays stopped for widget ${widgetId}`);\n }\n\n /**\n * Core: hide a widget in a region (shared by main layout + overlay).\n * Returns { widget, animPromise } synchronously — callers await animPromise if needed.\n * NOT async, so callers that don't need the animation stay on the same microtask.\n */\n _hideWidget(region, widgetIndex) {\n const widget = region.widgets[widgetIndex];\n if (!widget) return { widget: null, animPromise: null };\n\n const widgetElement = region.widgetElements.get(widget.id);\n if (!widgetElement) return { widget: null, animPromise: null };\n\n let animPromise = null;\n if (widget.transitions.out) {\n const animation = Transitions.apply(\n widgetElement, widget.transitions.out, false, region.width, region.height\n );\n if (animation) {\n animPromise = new Promise(resolve => { animation.onfinish = resolve; });\n }\n }\n\n const videoEl = widgetElement.querySelector('video');\n if (videoEl && widget.options.loop !== '1') videoEl.pause();\n\n // Stop MediaStream tracks (webcam/mic) to release the device\n if (videoEl?._mediaStream) {\n videoEl._mediaStream.getTracks().forEach(t => t.stop());\n videoEl._mediaStream = null;\n videoEl.srcObject = null;\n }\n\n const audioEl = widgetElement.querySelector('audio');\n if (audioEl && widget.options.loop !== '1') audioEl.pause();\n\n // Stop audio overlays attached to this widget\n this._stopAudioOverlays(widget.id);\n\n return { widget, animPromise };\n }\n\n /**\n * Check if a widget is within its valid time window (fromDt/toDt).\n * Widgets without dates are always active.\n * @param {Object} widget - Widget config with optional fromDt/toDt\n * @returns {boolean}\n */\n _isWidgetActive(widget) {\n const now = new Date();\n if (widget.fromDt) {\n const from = new Date(widget.fromDt);\n if (now < from) return false;\n }\n if (widget.toDt) {\n const to = new Date(widget.toDt);\n if (now > to) return false;\n }\n return true;\n }\n\n /**\n * Parse NUMITEMS and DURATION HTML comments from GetResource responses.\n * CMS embeds these in widget HTML to override duration for dynamic content\n * (e.g. DataSet tickers, RSS feeds). Format: <!-- NUMITEMS=5 --> <!-- DURATION=30 -->\n * DURATION takes precedence; otherwise NUMITEMS × widget.duration is used.\n * @param {string} html - Widget HTML content\n * @param {Object} widget - Widget config (duration may be updated)\n */\n _parseDurationComments(html, widget) {\n const durationMatch = html.match(/<!--\\s*DURATION=(\\d+)\\s*-->/);\n if (durationMatch) {\n const newDuration = parseInt(durationMatch[1], 10);\n if (newDuration > 0) {\n this.log.info(`Widget ${widget.id}: DURATION comment overrides duration ${widget.duration}→${newDuration}s`);\n widget.duration = newDuration;\n return;\n }\n }\n\n const numItemsMatch = html.match(/<!--\\s*NUMITEMS=(\\d+)\\s*-->/);\n if (numItemsMatch) {\n const numItems = parseInt(numItemsMatch[1], 10);\n if (numItems > 0 && widget.duration > 0) {\n const newDuration = numItems * widget.duration;\n this.log.info(`Widget ${widget.id}: NUMITEMS=${numItems} × ${widget.duration}s = ${newDuration}s`);\n widget.duration = newDuration;\n }\n }\n }\n\n /**\n * Apply sub-playlist cycle playback filtering.\n * Groups widgets by parentWidgetId, then selects one widget per group for this cycle.\n * Non-grouped widgets pass through unchanged.\n *\n * @param {Array} widgets - All widgets in the region\n * @returns {Array} Filtered widgets for this playback cycle\n */\n _applyCyclePlayback(widgets) {\n // Track cycle indices per group for deterministic round-robin\n if (!this._subPlaylistCycleIndex) {\n this._subPlaylistCycleIndex = new Map();\n }\n\n // Group widgets by parentWidgetId\n const groups = new Map(); // parentWidgetId → [widgets]\n const result = [];\n\n for (const widget of widgets) {\n if (widget.parentWidgetId && widget.cyclePlayback) {\n if (!groups.has(widget.parentWidgetId)) {\n groups.set(widget.parentWidgetId, []);\n }\n groups.get(widget.parentWidgetId).push(widget);\n } else {\n // Non-grouped widget: add a placeholder to preserve order\n result.push({ type: 'direct', widget });\n }\n }\n\n // For each group, select one widget for this cycle\n for (const [groupId, groupWidgets] of groups) {\n // Sort by displayOrder\n groupWidgets.sort((a, b) => a.displayOrder - b.displayOrder);\n\n let selectedWidget;\n if (groupWidgets.some(w => w.isRandom)) {\n // Random selection\n const idx = Math.floor(Math.random() * groupWidgets.length);\n selectedWidget = groupWidgets[idx];\n } else {\n // Round-robin based on cycle index\n const cycleIdx = this._subPlaylistCycleIndex.get(groupId) || 0;\n selectedWidget = groupWidgets[cycleIdx % groupWidgets.length];\n this._subPlaylistCycleIndex.set(groupId, cycleIdx + 1);\n }\n\n this.log.info(`Sub-playlist cycle: group ${groupId} selected widget ${selectedWidget.id} (${groupWidgets.length} in group)`);\n result.push({ type: 'direct', widget: selectedWidget });\n }\n\n return result.map(r => r.widget);\n }\n\n /**\n * Core: cycle through widgets in a region (shared by main layout + overlay)\n * @param {Object} region - Region state object\n * @param {string} regionId - Region ID\n * @param {Function} showFn - (regionId, widgetIndex) => show widget\n * @param {Function} hideFn - (regionId, widgetIndex) => hide widget\n * @param {Function} [onCycleComplete] - Called when region completes one full cycle\n */\n _startRegionCycle(region, regionId, showFn, hideFn, onCycleComplete) {\n if (!region || region.widgets.length === 0) return;\n\n // Non-looping region with a single widget: show it and stay (spec: loop=0)\n if (region.widgets.length === 1) {\n showFn(regionId, 0);\n return;\n }\n\n const playNext = () => {\n const widgetIndex = region.currentIndex;\n const widget = region.widgets[widgetIndex];\n\n showFn(regionId, widgetIndex);\n\n const duration = widget.duration * 1000;\n region.timer = setTimeout(() => {\n this._handleWidgetCycleEnd(widget, region, regionId, widgetIndex, showFn, hideFn, onCycleComplete, playNext);\n }, duration);\n };\n\n playNext();\n }\n\n /**\n * Handle widget cycle end — shared logic for timer-based and event-based cycling\n */\n _handleWidgetCycleEnd(widget, region, regionId, widgetIndex, showFn, hideFn, onCycleComplete, playNext) {\n // Emit widgetAction if widget has a webhook URL configured\n if (widget.webhookUrl) {\n this.emit('widgetAction', {\n type: 'durationEnd',\n widgetId: widget.id,\n layoutId: this.currentLayoutId,\n regionId,\n url: widget.webhookUrl\n });\n }\n\n hideFn(regionId, widgetIndex);\n\n const nextIndex = (region.currentIndex + 1) % region.widgets.length;\n if (nextIndex === 0 && !region.complete) {\n region.complete = true;\n onCycleComplete?.();\n }\n\n // Non-looping region (loop=0): stop after one full cycle\n if (nextIndex === 0 && region.config?.loop === false) {\n // Show the last widget again and keep it visible\n showFn(regionId, region.widgets.length - 1);\n return;\n }\n\n region.currentIndex = nextIndex;\n playNext();\n }\n\n async renderWidget(regionId, widgetIndex) {\n const region = this.regions.get(regionId);\n if (!region) return;\n\n try {\n const widget = await this._showWidget(region, widgetIndex);\n if (widget) {\n this.log.info(`Showing widget ${widget.type} (${widget.id}) in region ${regionId}`);\n this.emit('widgetStart', {\n widgetId: widget.id, regionId, layoutId: this.currentLayoutId,\n mediaId: parseInt(widget.fileId || widget.id) || null,\n type: widget.type, duration: widget.duration,\n enableStat: widget.enableStat\n });\n\n // Execute commands attached to this widget (shell/native commands)\n if (widget.commands && widget.commands.length > 0) {\n for (const cmd of widget.commands) {\n this.emit('widgetCommand', {\n commandCode: cmd.commandCode,\n commandString: cmd.commandString,\n widgetId: widget.id,\n regionId,\n layoutId: this.currentLayoutId\n });\n }\n }\n }\n } catch (error) {\n this.log.error(`Error rendering widget:`, error);\n this.emit('error', { type: 'widgetError', error, widgetId: region.widgets[widgetIndex]?.id, regionId });\n }\n }\n\n /**\n * Stop a widget (with element reuse - don't revoke blob URLs!)\n * @param {string} regionId - Region ID\n * @param {number} widgetIndex - Widget index\n */\n async stopWidget(regionId, widgetIndex) {\n const 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 enableStat: widget.enableStat\n });\n }\n }\n\n /**\n * Render image widget\n */\n async renderImage(widget, region) {\n const img = document.createElement('img');\n img.className = 'renderer-lite-widget';\n img.style.width = '100%';\n img.style.height = '100%';\n // Scale type mapping (CMS image.xml):\n // center (default) → contain: scale proportionally to fit region, centered\n // stretch → fill: ignore aspect ratio, fill entire region\n // fit → cover: scale proportionally to fill region, crop excess\n const scaleType = widget.options.scaleType;\n const fitMap = { stretch: 'fill', center: 'contain', fit: 'cover' };\n img.style.objectFit = fitMap[scaleType] || 'contain';\n\n // Alignment: map alignId/valignId to CSS object-position\n // XLF tags are <alignId> and <valignId> (from CMS image.xml property ids)\n const alignMap = { left: 'left', center: 'center', right: 'right' };\n const valignMap = { top: 'top', middle: 'center', bottom: 'bottom' };\n const hPos = alignMap[widget.options.alignId] || 'center';\n const vPos = valignMap[widget.options.valignId] || 'center';\n img.style.objectPosition = `${hPos} ${vPos}`;\n\n img.style.opacity = '0';\n\n // 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 const vScaleType = widget.options.scaleType;\n const vFitMap = { stretch: 'fill', center: 'none', fit: 'contain' };\n video.style.objectFit = vFitMap[vScaleType] || 'contain';\n video.style.opacity = '1'; // Immediately visible\n video.autoplay = true;\n video.preload = 'auto'; // Eagerly buffer - chunks are pre-warmed in SW BlobCache\n video.muted = widget.options.mute === '1';\n video.loop = false; // Don't use native loop - we handle it manually to avoid black frames\n video.controls = false; // Hidden by default — toggle with V key in PWA\n video.playsInline = true; // Prevent fullscreen on mobile\n\n // 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 videoin (webcam/microphone) widget.\n * Uses getUserMedia() to capture live video from camera hardware.\n * @param {Object} widget - Widget config with options (sourceId, showFullScreen, mirror, mute, captureAudio)\n * @param {Object} region - Region dimensions (width, height)\n * @returns {HTMLVideoElement}\n */\n async renderVideoIn(widget, region) {\n const video = document.createElement('video');\n video.className = 'renderer-lite-widget';\n video.style.width = '100%';\n video.style.height = '100%';\n video.style.objectFit = widget.options.showFullScreen === '1' ? 'cover' : 'contain';\n video.autoplay = true;\n video.playsInline = true;\n video.controls = false;\n video.muted = widget.options.mute !== '0'; // Muted by default to prevent audio feedback\n\n // Mirror mode (front-facing camera)\n if (widget.options.mirror === '1') {\n video.style.transform = 'scaleX(-1)';\n }\n\n // Build getUserMedia constraints\n const videoConstraints = {\n width: { ideal: region.width },\n height: { ideal: region.height },\n };\n const deviceId = widget.options.sourceId || widget.options.deviceId;\n if (deviceId) {\n videoConstraints.deviceId = { exact: deviceId };\n } else {\n videoConstraints.facingMode = widget.options.facingMode || 'environment';\n }\n\n const constraints = {\n video: videoConstraints,\n audio: widget.options.captureAudio === '1',\n };\n\n // Store constraints for re-acquisition after layout transitions\n video._mediaConstraints = constraints;\n\n try {\n const stream = await navigator.mediaDevices.getUserMedia(constraints);\n video.srcObject = stream;\n video._mediaStream = stream;\n this.log.info(`Webcam stream acquired for widget ${widget.id} (tracks: ${stream.getTracks().length})`);\n } catch (e) {\n this.log.warn(`getUserMedia failed for widget ${widget.id}: ${e.message}`);\n return this._renderUnsupportedPlaceholder(\n { ...widget, type: 'Camera unavailable' },\n region\n );\n }\n\n return video;\n }\n\n /**\n * Render audio widget\n */\n async renderAudio(widget, region) {\n const container = document.createElement('div');\n container.className = 'renderer-lite-widget audio-widget';\n container.style.width = '100%';\n container.style.height = '100%';\n container.style.display = 'flex';\n container.style.flexDirection = 'column';\n container.style.alignItems = 'center';\n container.style.justifyContent = 'center';\n container.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';\n container.style.opacity = '0';\n\n // Audio element\n const audio = document.createElement('audio');\n audio.autoplay = true;\n audio.loop = widget.options.loop === '1';\n audio.volume = parseFloat(widget.options.volume || '100') / 100;\n\n // 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 // Handle audio end - similar to video ended handling\n audio.addEventListener('ended', () => {\n if (widget.options.loop === '1') {\n audio.currentTime = 0;\n this.log.info(`Audio ${fileId} ended - reset to start, waiting for widget cycle to replay`);\n } else {\n this.log.info(`Audio ${fileId} ended - playback complete`);\n }\n });\n\n // Detect audio duration for dynamic layout timing (when useDuration=0)\n audio.addEventListener('loadedmetadata', () => {\n const audioDuration = Math.floor(audio.duration);\n this.log.info(`Audio ${fileId} duration detected: ${audioDuration}s`);\n\n if (widget.duration === 0 || widget.useDuration === 0) {\n widget.duration = audioDuration;\n this.log.info(`Updated widget ${widget.id} duration to ${audioDuration}s (useDuration=0)`);\n this.updateLayoutDuration();\n }\n });\n\n // Handle audio errors\n audio.addEventListener('error', () => {\n const error = audio.error;\n this.log.warn(`Audio error (non-fatal): ${fileId}, code: ${error?.code}, message: ${error?.message || 'Unknown'}`);\n });\n\n // Visual feedback\n const icon = document.createElement('div');\n icon.innerHTML = '♪';\n icon.style.fontSize = '120px';\n icon.style.color = 'white';\n icon.style.marginBottom = '20px';\n\n const info = document.createElement('div');\n info.style.color = 'white';\n info.style.fontSize = '24px';\n info.textContent = 'Playing Audio';\n\n const filename = document.createElement('div');\n filename.style.color = 'rgba(255,255,255,0.7)';\n filename.style.fontSize = '16px';\n filename.style.marginTop = '10px';\n filename.textContent = widget.options.uri;\n\n container.appendChild(audio);\n container.appendChild(icon);\n container.appendChild(info);\n container.appendChild(filename);\n\n return container;\n }\n\n /**\n * Render text/ticker widget\n */\n async renderTextWidget(widget, region) {\n 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 self.log.warn('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 // Parse NUMITEMS/DURATION from fallback HTML (cache path)\n if (result.fallback) {\n this._parseDurationComments(result.fallback, widget);\n }\n\n return iframe;\n }\n html = result;\n }\n\n if (html) {\n // Parse NUMITEMS/DURATION HTML comments for dynamic widget duration\n this._parseDurationComments(html, widget);\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 // Derive worker path from current page location (works for /player/pwa/ and /player/)\n const basePath = window.location.pathname.replace(/\\/[^/]*$/, '/');\n window.pdfjsLib.GlobalWorkerOptions.workerSrc = `${window.location.origin}${basePath}pdf.worker.min.mjs`;\n } catch (error) {\n this.log.error('PDF.js not available:', error);\n container.innerHTML = '<div style=\"color:white;padding:20px;text-align:center;\">PDF viewer unavailable</div>';\n container.style.opacity = '1';\n return container;\n }\n }\n\n // 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 // modeId=1 (or absent) = Open Natively (direct URL), modeId=0 = Manual/GetResource\n const modeId = parseInt(widget.options.modeId || '1');\n if (modeId === 0) {\n // GetResource mode: treat like a generic widget (fetch HTML from CMS)\n return await this.renderGenericWidget(widget, region);\n }\n\n const iframe = document.createElement('iframe');\n iframe.className = 'renderer-lite-widget';\n iframe.style.width = '100%';\n iframe.style.height = '100%';\n iframe.style.border = 'none';\n iframe.style.opacity = '0';\n 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 self.log.warn('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 // Parse NUMITEMS/DURATION from fallback HTML (cache path)\n if (result.fallback) {\n this._parseDurationComments(result.fallback, widget);\n }\n\n return iframe;\n }\n html = result;\n }\n\n if (html) {\n // Parse NUMITEMS/DURATION HTML comments for dynamic widget duration\n // Format: <!-- NUMITEMS=5 --> and <!-- DURATION=30 -->\n this._parseDurationComments(html, widget);\n\n const blob = new Blob([html], { type: 'text/html' });\n const blobUrl = URL.createObjectURL(blob);\n iframe.src = blobUrl;\n\n // Track blob URL for lifecycle management\n this.trackBlobUrl(blobUrl);\n } else {\n this.log.warn(`No HTML for widget ${widget.id}`);\n iframe.srcdoc = '<div style=\"padding:20px;\">Widget content unavailable</div>';\n }\n\n return iframe;\n }\n\n /**\n * Render a placeholder for unsupported widget types (powerpoint, flash)\n */\n _renderUnsupportedPlaceholder(widget, region) {\n const div = document.createElement('div');\n div.className = 'renderer-lite-widget';\n div.style.width = '100%';\n div.style.height = '100%';\n div.style.display = 'flex';\n div.style.alignItems = 'center';\n div.style.justifyContent = 'center';\n div.style.backgroundColor = '#111';\n div.style.color = '#666';\n div.style.fontSize = '14px';\n div.textContent = `Unsupported: ${widget.type}`;\n return div;\n }\n\n // ── Layout Preload Pool ─────────────────────────────────────────────\n\n /**\n * Schedule preloading of the next layout at 75% of current layout duration.\n * Emits 'request-next-layout-preload' so the platform layer can peek at the\n * schedule and call preloadLayout() with the next layout's XLF.\n * @param {Object} layout - Current layout object with .duration\n */\n _scheduleNextLayoutPreload(layout) {\n if (this.preloadTimer) {\n clearTimeout(this.preloadTimer);\n this.preloadTimer = null;\n }\n if (this._preloadRetryTimer) {\n clearTimeout(this._preloadRetryTimer);\n this._preloadRetryTimer = null;\n }\n\n const duration = layout.duration || 60; // seconds\n const preloadDelay = duration * 1000 * 0.75; // 75% through\n const retryDelay = duration * 1000 * 0.90; // 90% retry\n\n this.log.info(`Scheduling next layout preload in ${(preloadDelay / 1000).toFixed(1)}s (75% of ${duration}s)`);\n\n this.preloadTimer = setTimeout(() => {\n this.preloadTimer = null;\n this.emit('request-next-layout-preload');\n }, preloadDelay);\n\n // Retry at 90% if the 75% attempt couldn't find a layout (e.g. cooldowns\n // hadn't expired yet). The platform handler is idempotent — if a layout\n // is already in the pool it skips, so this is safe even if 75% succeeded.\n this._preloadRetryTimer = setTimeout(() => {\n this._preloadRetryTimer = null;\n this.emit('request-next-layout-preload');\n }, retryDelay);\n }\n\n /**\n * Preload a layout into the pool as a warm (hidden) entry.\n * Creates the full DOM hierarchy (regions + widgets) in a hidden container,\n * pre-fetches media, but does NOT start widget cycling or layout timer.\n *\n * This is called by the platform layer in response to 'request-next-layout-preload'.\n *\n * @param {string} xlfXml - XLF XML content for the layout\n * @param {number} layoutId - Layout ID\n * @returns {Promise<boolean>} true if preload succeeded, false on failure\n */\n 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.position = 'absolute';\n element.style.top = '0';\n element.style.left = '0';\n element.style.width = '100%';\n element.style.height = '100%';\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 // Apply region exit transition if configured, then remove\n if (region.config && region.config.exitTransition) {\n const animation = Transitions.apply(\n region.element, region.config.exitTransition, false,\n region.width, region.height\n );\n if (animation) {\n const el = region.element;\n animation.onfinish = () => el.remove();\n } else {\n region.element.remove();\n }\n } else {\n region.element.remove();\n }\n }\n // Revoke blob URLs\n if (oldLayoutId) {\n this.revokeBlobUrlsForLayout(oldLayoutId);\n }\n 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 // Apply region exit transition if configured, then remove\n if (region.config && region.config.exitTransition) {\n const animation = Transitions.apply(\n region.element, region.config.exitTransition, false,\n region.width, region.height\n );\n if (animation) {\n // Remove element after exit transition completes\n const el = region.element;\n animation.onfinish = () => el.remove();\n } else {\n region.element.remove();\n }\n } else {\n region.element.remove();\n }\n }\n\n // 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 * Check if playback is currently paused.\n */\n isPaused() {\n return this._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 // Clean up any remaining audio overlays\n for (const widgetId of this.audioOverlays.keys()) {\n this._stopAudioOverlays(widgetId);\n }\n\n // Clear the layout preload pool\n this.layoutPool.clear();\n\n if (this.preloadTimer) {\n clearTimeout(this.preloadTimer);\n this.preloadTimer = null;\n }\n if (this._preloadRetryTimer) {\n clearTimeout(this._preloadRetryTimer);\n this._preloadRetryTimer = null;\n }\n\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n this.resizeObserver = null;\n }\n\n this.container.innerHTML = '';\n this.log.info('Cleaned up');\n }\n}\n","/**\n * Layout translator - XLF to HTML\n * Based on arexibo layout.rs\n */\n\nimport { cacheWidgetHtml } from '@xiboplayer/cache';\nimport { createLogger } from '@xiboplayer/utils';\n\nconst log = createLogger('Layout');\n\nexport class LayoutTranslator {\n constructor(xmds) {\n this.xmds = xmds;\n }\n\n /**\n * Translate XLF XML to playable HTML\n */\n async translateXLF(layoutId, xlfXml) {\n const parser = new DOMParser();\n const doc = parser.parseFromString(xlfXml, 'text/xml');\n\n const layoutEl = doc.querySelector('layout');\n if (!layoutEl) {\n throw new Error('Invalid XLF: no <layout> element');\n }\n\n const width = parseInt(layoutEl.getAttribute('width') || '1920');\n const height = parseInt(layoutEl.getAttribute('height') || '1080');\n const bgcolor = layoutEl.getAttribute('bgcolor') || '#000000';\n\n const regions = [];\n for (const regionEl of doc.querySelectorAll('region')) {\n regions.push(await this.translateRegion(layoutId, regionEl));\n }\n\n return this.generateHTML(width, height, bgcolor, regions);\n }\n\n /**\n * Translate a single region\n */\n async translateRegion(layoutId, regionEl) {\n const id = regionEl.getAttribute('id');\n const width = parseInt(regionEl.getAttribute('width'));\n const height = parseInt(regionEl.getAttribute('height'));\n const top = parseInt(regionEl.getAttribute('top'));\n const left = parseInt(regionEl.getAttribute('left'));\n const zindex = parseInt(regionEl.getAttribute('zindex') || '0');\n\n const media = [];\n for (const mediaEl of regionEl.querySelectorAll('media')) {\n media.push(await this.translateMedia(layoutId, id, mediaEl));\n }\n\n return {\n id,\n width,\n height,\n top,\n left,\n zindex,\n media\n };\n }\n\n /**\n * Translate a single media item\n */\n async translateMedia(layoutId, regionId, mediaEl) {\n const type = mediaEl.getAttribute('type');\n const duration = parseInt(mediaEl.getAttribute('duration') || '10');\n const id = mediaEl.getAttribute('id');\n\n const optionsEl = mediaEl.querySelector('options');\n const rawEl = mediaEl.querySelector('raw');\n\n const options = {};\n if (optionsEl) {\n for (const child of optionsEl.children) {\n options[child.tagName] = child.textContent;\n }\n }\n\n // Parse transition information\n const transitions = {\n in: null,\n out: null\n };\n\n const transInEl = mediaEl.querySelector('options > transIn');\n const transOutEl = mediaEl.querySelector('options > transOut');\n const transInDurationEl = mediaEl.querySelector('options > transInDuration');\n const transOutDurationEl = mediaEl.querySelector('options > transOutDuration');\n const transInDirectionEl = mediaEl.querySelector('options > transInDirection');\n const transOutDirectionEl = mediaEl.querySelector('options > transOutDirection');\n\n if (transInEl && transInEl.textContent) {\n transitions.in = {\n type: transInEl.textContent,\n duration: parseInt(transInDurationEl?.textContent || '1000'),\n direction: transInDirectionEl?.textContent || 'N'\n };\n }\n\n if (transOutEl && transOutEl.textContent) {\n transitions.out = {\n type: transOutEl.textContent,\n duration: parseInt(transOutDurationEl?.textContent || '1000'),\n direction: transOutDirectionEl?.textContent || 'N'\n };\n }\n\n // All videos use cache URL pattern\n // Large videos download in background, small videos are already cached\n // Service Worker handles both cases appropriately\n\n let raw = rawEl ? rawEl.textContent : '';\n\n // For widgets (clock, calendar, etc.), fetch rendered HTML from CMS\n const widgetTypes = ['clock', 'clock-digital', 'clock-analogue', 'calendar', 'weather',\n 'currencies', 'stocks', 'twitter', 'global', 'embedded', 'text', 'ticker'];\n if (widgetTypes.some(w => type.includes(w))) {\n // Try to get widget HTML with retry logic for kiosk reliability\n let retries = 3;\n let lastError = null;\n\n for (let attempt = 1; attempt <= retries; attempt++) {\n try {\n log.info(`Fetching resource for ${type} widget (layout=${layoutId}, region=${regionId}, media=${id}) - attempt ${attempt}/${retries}`);\n raw = await this.xmds.getResource(layoutId, regionId, id);\n log.info(`Got resource HTML (${raw.length} chars)`);\n\n // Store widget HTML in cache and save cache key for iframe src generation\n const widgetCacheKey = await cacheWidgetHtml(layoutId, regionId, id, raw);\n options.widgetCacheKey = widgetCacheKey;\n\n // Success - break retry loop\n break;\n\n } catch (error) {\n lastError = error;\n log.warn(`Failed to get resource (attempt ${attempt}/${retries}):`, error.message);\n\n // If not last attempt, wait before retry\n if (attempt < retries) {\n const delay = attempt * 2000; // 2s, 4s backoff\n log.info(`Retrying in ${delay}ms...`);\n await new Promise(resolve => setTimeout(resolve, delay));\n }\n }\n }\n\n // If all retries failed, try to use cached version as fallback\n if (!raw && lastError) {\n log.warn('All retries failed, checking for cached widget HTML...');\n\n // Try to get cached widget HTML directly from Cache API\n try {\n const cachedKey = `/cache/widget/${layoutId}/${regionId}/${id}.html`;\n const cache = await caches.open('xibo-media-v1');\n const cached = await cache.match(new Request(window.location.origin + '/player' + cachedKey));\n\n if (cached) {\n raw = await cached.text();\n options.widgetCacheKey = cachedKey;\n log.info(`Using cached widget HTML (${raw.length} chars) - CMS update pending`);\n } else {\n log.error(`No cached version available for widget ${id}`);\n // Show minimal placeholder that doesn't look like an error\n raw = `<div style=\"display:flex;align-items:center;justify-content:center;height:100%;color:#999;font-size:18px;\">Content updating...</div>`;\n }\n } catch (cacheError) {\n log.error('Cache fallback failed:', cacheError);\n raw = `<div style=\"display:flex;align-items:center;justify-content:center;height:100%;color:#999;font-size:18px;\">Content updating...</div>`;\n }\n }\n }\n\n return {\n type,\n duration,\n id,\n options,\n raw,\n transitions\n };\n }\n\n /**\n * Generate HTML from parsed layout\n */\n generateHTML(width, height, bgcolor, regions) {\n const regionHTML = regions.map(r => this.generateRegionHTML(r)).join('\\n');\n const regionJS = regions.map(r => this.generateRegionJS(r)).join(',\\n');\n\n return `<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=${width}, height=${height}\">\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n html, body { width: 100%; height: 100%; overflow: hidden; }\n body { background-color: ${bgcolor}; }\n .region {\n position: absolute;\n overflow: hidden;\n }\n .media {\n width: 100%;\n height: 100%;\n object-fit: contain;\n }\n iframe {\n border: none;\n width: 100%;\n height: 100%;\n }\n </style>\n</head>\n<body>\n${regionHTML}\n<script>\n// Transition utilities\nwindow.Transitions = {\n fadeIn(element, duration) {\n const keyframes = [\n { opacity: 0 },\n { opacity: 1 }\n ];\n const timing = {\n duration: duration,\n easing: 'linear',\n fill: 'forwards'\n };\n return element.animate(keyframes, timing);\n },\n\n fadeOut(element, duration) {\n const keyframes = [\n { opacity: 1 },\n { opacity: 0 }\n ];\n const timing = {\n duration: duration,\n easing: 'linear',\n fill: 'forwards'\n };\n return element.animate(keyframes, timing);\n },\n\n getFlyKeyframes(direction, width, height, isIn) {\n const dirMap = {\n 'N': { x: 0, y: isIn ? -height : height },\n 'NE': { x: isIn ? width : -width, y: isIn ? -height : height },\n 'E': { x: isIn ? width : -width, y: 0 },\n 'SE': { x: isIn ? width : -width, y: isIn ? height : -height },\n 'S': { x: 0, y: isIn ? height : -height },\n 'SW': { x: isIn ? -width : width, y: isIn ? height : -height },\n 'W': { x: isIn ? -width : width, y: 0 },\n 'NW': { x: isIn ? -width : width, y: isIn ? -height : height }\n };\n\n const offset = dirMap[direction] || dirMap['N'];\n\n if (isIn) {\n return [\n { transform: \\`translate(\\${offset.x}px, \\${offset.y}px)\\`, opacity: 0 },\n { transform: 'translate(0, 0)', opacity: 1 }\n ];\n } else {\n return [\n { transform: 'translate(0, 0)', opacity: 1 },\n { transform: \\`translate(\\${offset.x}px, \\${offset.y}px)\\`, opacity: 0 }\n ];\n }\n },\n\n flyIn(element, duration, direction, regionWidth, regionHeight) {\n const keyframes = this.getFlyKeyframes(direction, regionWidth, regionHeight, true);\n const timing = {\n duration: duration,\n easing: 'ease-out',\n fill: 'forwards'\n };\n return element.animate(keyframes, timing);\n },\n\n flyOut(element, duration, direction, regionWidth, regionHeight) {\n const keyframes = this.getFlyKeyframes(direction, regionWidth, regionHeight, false);\n const timing = {\n duration: duration,\n easing: 'ease-in',\n fill: 'forwards'\n };\n return element.animate(keyframes, timing);\n },\n\n apply(element, transitionConfig, isIn, regionWidth, regionHeight) {\n if (!transitionConfig || !transitionConfig.type) {\n return null;\n }\n\n const type = transitionConfig.type.toLowerCase();\n const duration = transitionConfig.duration || 1000;\n const direction = transitionConfig.direction || 'N';\n\n switch (type) {\n case '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\nconst regions = {\n${regionJS}\n};\n\n// Auto-start all regions\nObject.keys(regions).forEach(id => {\n playRegion(id);\n});\n\nfunction playRegion(id) {\n const region = regions[id];\n if (!region || region.media.length === 0) return;\n\n // If only one media item, just show it and don't cycle (arexibo behavior)\n if (region.media.length === 1) {\n const media = region.media[0];\n if (media.start) media.start();\n return; // Don't schedule stop/restart\n }\n\n // Multiple media items - cycle normally\n let currentIndex = 0;\n\n function playNext() {\n const media = region.media[currentIndex];\n if (media.start) media.start();\n\n const duration = media.duration || 10;\n setTimeout(() => {\n if (media.stop) media.stop();\n currentIndex = (currentIndex + 1) % region.media.length;\n playNext();\n }, duration * 1000);\n }\n\n playNext();\n}\n</script>\n</body>\n</html>`;\n }\n\n /**\n * Generate HTML for a region container\n */\n generateRegionHTML(region) {\n return ` <div id=\"region_${region.id}\" class=\"region\" style=\"\n left: ${region.left}px;\n top: ${region.top}px;\n width: ${region.width}px;\n height: ${region.height}px;\n z-index: ${region.zindex};\n \"></div>`;\n }\n\n /**\n * Generate JavaScript for region media control\n */\n generateRegionJS(region) {\n const mediaJS = region.media.map(m => this.generateMediaJS(m, region.id)).join(',\\n ');\n\n return ` '${region.id}': {\n media: [\n${mediaJS}\n ]\n }`;\n }\n\n /**\n * Generate iframe widget JS for text/ticker and generic widget types.\n * Returns { startFn, stopFn } strings for the media item.\n */\n _generateIframeWidgetJS(regionId, mediaId, widgetUrl, transIn, transOut) {\n const iframeId = `widget_${regionId}_${mediaId}`;\n const startFn = `() => {\n const region = document.getElementById('region_${regionId}');\n let iframe = document.getElementById('${iframeId}');\n if (!iframe) {\n iframe = document.createElement('iframe');\n iframe.id = '${iframeId}';\n iframe.src = '${widgetUrl}';\n iframe.style.width = '100%';\n iframe.style.height = '100%';\n iframe.style.border = 'none';\n iframe.scrolling = 'no';\n iframe.style.opacity = '0';\n region.innerHTML = '';\n region.appendChild(iframe);\n\n // Apply transition after iframe loads\n iframe.onload = () => {\n const transIn = ${transIn};\n if (transIn && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n window.Transitions.apply(iframe, transIn, true, regionRect.width, regionRect.height);\n } else {\n iframe.style.opacity = '1';\n }\n };\n } else {\n iframe.style.display = 'block';\n iframe.style.opacity = '1';\n }\n }`;\n const stopFn = `() => {\n const region = document.getElementById('region_${regionId}');\n const iframe = document.getElementById('${iframeId}');\n if (iframe) {\n const transOut = ${transOut};\n if (transOut && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n const animation = window.Transitions.apply(iframe, transOut, false, regionRect.width, regionRect.height);\n if (animation) {\n animation.onfinish = () => {\n iframe.style.display = 'none';\n };\n return;\n }\n }\n iframe.style.display = 'none';\n }\n }`;\n return { startFn, stopFn };\n }\n\n /**\n * Generate JavaScript for a single media item\n */\n generateMediaJS(media, regionId) {\n const duration = media.duration || 10;\n const transIn = media.transitions?.in ? JSON.stringify(media.transitions.in) : 'null';\n const transOut = media.transitions?.out ? JSON.stringify(media.transitions.out) : 'null';\n let startFn = 'null';\n let stopFn = 'null';\n\n switch (media.type) {\n case 'image':\n // Use absolute URL within service worker scope\n const imageSrc = `${window.location.origin}/player/cache/media/${media.options.uri}`;\n startFn = `() => {\n const region = document.getElementById('region_${regionId}');\n const img = document.createElement('img');\n img.className = 'media';\n img.src = '${imageSrc}';\n img.style.opacity = '0';\n region.innerHTML = '';\n region.appendChild(img);\n\n // Apply transition\n const transIn = ${transIn};\n if (transIn && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n window.Transitions.apply(img, transIn, true, regionRect.width, regionRect.height);\n } else {\n img.style.opacity = '1';\n }\n }`;\n break;\n\n case 'video':\n // All videos use cache URL pattern\n // Background-downloaded videos will auto-reload when cache completes\n const videoSrc = `${window.location.origin}/player/cache/media/${media.options.uri}`;\n const videoFilename = media.options.uri;\n\n startFn = `() => {\n const region = document.getElementById('region_${regionId}');\n const video = document.createElement('video');\n video.className = 'media';\n video.src = '${videoSrc}';\n video.dataset.filename = '${videoFilename}';\n video.autoplay = true;\n video.muted = ${media.options.mute === '1' ? 'true' : 'false'};\n video.loop = false;\n video.style.width = '100%';\n video.style.height = '100%';\n video.style.objectFit = 'contain';\n video.style.opacity = '0';\n\n // Retry loading if cache completes while video is playing\n const retryOnCache = (event) => {\n if (event.detail.filename === '${videoFilename}' && video.error) {\n console.log('[Video] Cache complete, reloading:', '${videoFilename}');\n video.load();\n video.play();\n }\n };\n window.addEventListener('media-cached', retryOnCache);\n video.dataset.cacheListener = 'attached';\n\n region.innerHTML = '';\n region.appendChild(video);\n\n // Apply transition\n const transIn = ${transIn};\n if (transIn && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n window.Transitions.apply(video, transIn, true, regionRect.width, regionRect.height);\n } else {\n video.style.opacity = '1';\n }\n\n console.log('[Video] Playing:', '${media.options.uri}');\n }`;\n stopFn = `() => {\n const region = document.getElementById('region_${regionId}');\n const video = document.querySelector('#region_${regionId} video');\n if (video) {\n const transOut = ${transOut};\n if (transOut && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n const animation = window.Transitions.apply(video, transOut, false, regionRect.width, regionRect.height);\n if (animation) {\n animation.onfinish = () => {\n video.pause();\n video.remove();\n };\n return;\n }\n }\n video.pause();\n video.remove();\n }\n }`;\n break;\n\n case 'text':\n case 'ticker':\n // Text/ticker widgets use the same iframe pattern as default widgets.\n // If no widgetCacheKey, fall through to the default case which handles unsupported types.\n if (media.options.widgetCacheKey) {\n const textUrl = `${window.location.origin}/player${media.options.widgetCacheKey}`;\n const iframe = this._generateIframeWidgetJS(regionId, media.id, textUrl, transIn, transOut);\n startFn = iframe.startFn;\n stopFn = iframe.stopFn;\n break;\n }\n // Fall through to default (handles missing widgetCacheKey as unsupported)\n\n case 'audio':\n const audioSrc = `${window.location.origin}/player/cache/media/${media.options.uri}`;\n const audioId = `audio_${regionId}_${media.id}`;\n const audioLoop = media.options.loop === '1';\n const audioVolume = (parseInt(media.options.volume || '100') / 100).toFixed(2);\n\n startFn = `() => {\n const region = document.getElementById('region_${regionId}');\n\n // Create audio element\n const audio = document.createElement('audio');\n audio.id = '${audioId}';\n audio.className = 'media';\n audio.src = '${audioSrc}';\n audio.autoplay = true;\n audio.loop = ${audioLoop};\n audio.volume = ${audioVolume};\n\n // Create visual feedback container\n const visualContainer = document.createElement('div');\n visualContainer.className = 'audio-visual';\n visualContainer.style.cssText = \\`\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n opacity: 0;\n \\`;\n\n // Audio icon\n const icon = document.createElement('div');\n icon.innerHTML = '♪';\n icon.style.cssText = \\`\n font-size: 120px;\n color: white;\n margin-bottom: 20px;\n animation: pulse 2s ease-in-out infinite;\n \\`;\n\n // Audio info\n const info = document.createElement('div');\n info.style.cssText = \\`\n color: white;\n font-size: 24px;\n text-align: center;\n padding: 0 20px;\n \\`;\n info.textContent = 'Playing Audio';\n\n // Filename\n const filename = document.createElement('div');\n filename.style.cssText = \\`\n color: rgba(255,255,255,0.7);\n font-size: 16px;\n margin-top: 10px;\n \\`;\n filename.textContent = '${media.options.uri}';\n\n visualContainer.appendChild(icon);\n visualContainer.appendChild(info);\n visualContainer.appendChild(filename);\n\n region.innerHTML = '';\n region.appendChild(audio);\n region.appendChild(visualContainer);\n\n // Add pulse animation\n const style = document.createElement('style');\n style.textContent = \\`\n @keyframes pulse {\n 0%, 100% { transform: scale(1); opacity: 1; }\n 50% { transform: scale(1.1); opacity: 0.8; }\n }\n \\`;\n document.head.appendChild(style);\n\n // Apply transition\n const transIn = ${transIn};\n if (transIn && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n window.Transitions.apply(visualContainer, transIn, true, regionRect.width, regionRect.height);\n } else {\n visualContainer.style.opacity = '1';\n }\n\n console.log('[Audio] Playing:', '${audioSrc}', 'Volume:', ${audioVolume}, 'Loop:', ${audioLoop});\n }`;\n\n stopFn = `() => {\n const audio = document.getElementById('${audioId}');\n if (audio) {\n audio.pause();\n audio.remove();\n }\n const region = document.getElementById('region_${regionId}');\n if (region) {\n const visualContainer = region.querySelector('.audio-visual');\n if (visualContainer) {\n const transOut = ${transOut};\n if (transOut && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n const animation = window.Transitions.apply(visualContainer, transOut, false, regionRect.width, regionRect.height);\n if (animation) {\n animation.onfinish = () => visualContainer.remove();\n return;\n }\n }\n visualContainer.remove();\n }\n }\n }`;\n break;\n\n case 'pdf':\n const pdfSrc = `${window.location.origin}/player/cache/media/${media.options.uri}`;\n const pdfContainerId = `pdf_${regionId}_${media.id}`;\n const pdfDuration = duration; // Total duration for entire PDF\n\n startFn = `async () => {\n const container = document.createElement('div');\n container.className = 'media pdf-container';\n container.id = '${pdfContainerId}';\n container.style.width = '100%';\n container.style.height = '100%';\n container.style.overflow = 'hidden';\n container.style.backgroundColor = '#525659';\n container.style.opacity = '0';\n container.style.position = 'relative';\n\n const region = document.getElementById('region_${regionId}');\n region.innerHTML = '';\n region.appendChild(container);\n\n // Load PDF.js if not already loaded\n if (typeof pdfjsLib === 'undefined') {\n try {\n const pdfjsModule = await import('pdfjs-dist');\n window.pdfjsLib = pdfjsModule;\n pdfjsLib.GlobalWorkerOptions.workerSrc = '${window.location.origin}/player/pdf.worker.min.mjs';\n } catch (error) {\n console.error('[PDF] Failed to load PDF.js:', error);\n container.innerHTML = '<div style=\"color:white;padding:20px;text-align:center;\">PDF viewer unavailable</div>';\n return;\n }\n }\n\n // Render PDF with multi-page support\n try {\n const loadingTask = pdfjsLib.getDocument('${pdfSrc}');\n const pdf = await loadingTask.promise;\n const totalPages = pdf.numPages;\n\n // Calculate time per page (distribute total duration across all pages)\n const timePerPage = (${pdfDuration} * 1000) / totalPages; // milliseconds per page\n\n console.log(\\`[PDF] Loading: \\${totalPages} pages, \\${timePerPage}ms per page\\`);\n\n const containerWidth = container.offsetWidth || ${width};\n const containerHeight = container.offsetHeight || ${height};\n\n // Create page indicator\n const pageIndicator = document.createElement('div');\n pageIndicator.className = 'pdf-page-indicator';\n pageIndicator.style.cssText = \\`\n position: absolute;\n bottom: 10px;\n right: 10px;\n background: rgba(0,0,0,0.7);\n color: white;\n padding: 8px 12px;\n border-radius: 4px;\n font-size: 14px;\n z-index: 10;\n \\`;\n container.appendChild(pageIndicator);\n\n let currentPage = 1;\n let pageTimers = [];\n\n // Function to render a single page\n async function renderPage(pageNum) {\n const page = await pdf.getPage(pageNum);\n const viewport = page.getViewport({ scale: 1 });\n\n // Calculate scale to fit page within container\n const scaleX = containerWidth / viewport.width;\n const scaleY = containerHeight / viewport.height;\n const scale = Math.min(scaleX, scaleY);\n\n const scaledViewport = page.getViewport({ scale });\n\n const canvas = document.createElement('canvas');\n canvas.className = 'pdf-page';\n const context = canvas.getContext('2d');\n canvas.width = scaledViewport.width;\n canvas.height = scaledViewport.height;\n\n // Center canvas in container\n canvas.style.cssText = \\`\n display: block;\n margin: auto;\n margin-top: \\${Math.max(0, (containerHeight - scaledViewport.height) / 2)}px;\n position: absolute;\n top: 0;\n left: 50%;\n transform: translateX(-50%);\n opacity: 0;\n transition: opacity 0.5s ease-in-out;\n \\`;\n\n container.appendChild(canvas);\n\n await page.render({\n canvasContext: context,\n viewport: scaledViewport\n }).promise;\n\n // Fade in new page\n setTimeout(() => canvas.style.opacity = '1', 50);\n\n return canvas;\n }\n\n // Function to cycle through pages\n async function cyclePage() {\n // Update page indicator\n pageIndicator.textContent = \\`Page \\${currentPage} / \\${totalPages}\\`;\n\n // Remove old pages\n const oldPages = container.querySelectorAll('.pdf-page');\n oldPages.forEach(oldPage => {\n if (oldPage !== container.lastChild) {\n oldPage.style.opacity = '0';\n setTimeout(() => oldPage.remove(), 500);\n }\n });\n\n // Render current page\n await renderPage(currentPage);\n\n console.log(\\`[PDF] Showing page \\${currentPage}/\\${totalPages}\\`);\n\n // Schedule next page\n if (totalPages > 1) {\n const timer = setTimeout(() => {\n currentPage = currentPage >= totalPages ? 1 : currentPage + 1;\n cyclePage();\n }, timePerPage);\n pageTimers.push(timer);\n }\n }\n\n // Start cycling\n await cyclePage();\n\n // Apply transition to container\n const transIn = ${transIn};\n if (transIn && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n window.Transitions.apply(container, transIn, true, regionRect.width, regionRect.height);\n } else {\n container.style.opacity = '1';\n }\n\n // Store timers for cleanup\n container.dataset.pageTimers = JSON.stringify(pageTimers.map(t => t));\n\n } catch (error) {\n console.error('[PDF] Render failed:', error);\n container.innerHTML = '<div style=\"color:white;padding:20px;text-align:center;\">Failed to load PDF</div>';\n container.style.opacity = '1';\n }\n }`;\n\n stopFn = `() => {\n const region = document.getElementById('region_${regionId}');\n const container = document.getElementById('${pdfContainerId}');\n if (container) {\n // Clear page cycling timers\n const timers = container.dataset.pageTimers;\n if (timers) {\n try {\n JSON.parse(timers).forEach(t => clearTimeout(t));\n } catch (e) {}\n }\n\n const transOut = ${transOut};\n if (transOut && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n const animation = window.Transitions.apply(container, transOut, false, regionRect.width, regionRect.height);\n if (animation) {\n animation.onfinish = () => {\n container.remove();\n };\n return;\n }\n }\n container.remove();\n }\n }`;\n break;\n\n case 'webpage':\n const url = media.options.uri;\n startFn = `() => {\n const region = document.getElementById('region_${regionId}');\n const iframe = document.createElement('iframe');\n iframe.src = '${url}';\n iframe.style.opacity = '0';\n region.innerHTML = '';\n region.appendChild(iframe);\n\n // Apply transition after iframe loads\n iframe.onload = () => {\n const transIn = ${transIn};\n if (transIn && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n window.Transitions.apply(iframe, transIn, true, regionRect.width, regionRect.height);\n } else {\n iframe.style.opacity = '1';\n }\n };\n }`;\n break;\n\n default:\n // Widgets (clock, calendar, weather, etc.) - use cache URL pattern in /player/ scope for SW\n // Keep widget iframes alive across duration cycles (arexibo behavior)\n if (media.options.widgetCacheKey) {\n const widgetUrl = `${window.location.origin}/player${media.options.widgetCacheKey}`;\n const iframe = this._generateIframeWidgetJS(regionId, media.id, widgetUrl, transIn, transOut);\n startFn = iframe.startFn;\n stopFn = iframe.stopFn;\n } else {\n log.warn(`Unsupported media type: ${media.type}`);\n startFn = `() => console.log('Unsupported media type: ${media.type}')`;\n }\n }\n\n return ` {\n start: ${startFn},\n stop: ${stopFn},\n duration: ${duration}\n }`;\n }\n}\n\n","/**\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 * Weather metrics (require weatherData in options):\n * - weatherTemp: Current temperature\n * - weatherHumidity: Current humidity percentage\n * - weatherWindSpeed: Current wind speed\n * - weatherCondition: Current weather condition (e.g. \"Clear\", \"Rain\")\n * - weatherCloudCover: Cloud cover percentage\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 * Weather metric name → weatherData property mapping\n */\nconst WEATHER_METRICS = {\n weatherTemp: 'temperature',\n weatherHumidity: 'humidity',\n weatherWindSpeed: 'windSpeed',\n weatherCondition: 'condition',\n weatherCloudCover: 'cloudCover',\n};\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 * @param {Object} weatherData - Weather data from GetWeather XMDS call\n * @returns {string|null} Metric value or null if unknown\n */\nfunction getMetricValue(metric, now, displayProperties = {}, weatherData = {}) {\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 weather metrics\n if (WEATHER_METRICS[metric]) {\n const weatherKey = WEATHER_METRICS[metric];\n if (weatherData[weatherKey] !== undefined) {\n return String(weatherData[weatherKey]);\n }\n log.debug(`Weather metric \"${metric}\" requested but no weather data available`);\n return null;\n }\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 * @param {Object} [options.weatherData] - Weather data from GetWeather XMDS call\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 const weatherData = options.weatherData || {};\n\n for (const criterion of criteria) {\n const actual = getMetricValue(criterion.metric, now, displayProperties, weatherData);\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.weatherData = {}; // Weather data from GetWeather XMDS call\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 * Update weather data for criteria evaluation\n * @param {Object} data - Parsed weather object { temperature, humidity, windSpeed, condition, cloudCover }\n */\n setWeatherData(data) {\n this.weatherData = data || {};\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 Week, Day, and Month recurrence types.\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 // Check recurrence range first (applies to all types)\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 switch (item.recurrenceType) {\n case 'Week': {\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 if (!allowedDays.includes(currentDayOfWeek)) {\n return false;\n }\n }\n return true;\n }\n\n case 'Day': {\n // Daily recurrence with optional interval (recurrenceDetail)\n // If recurrenceDetail > 1, only active every N days from fromdt\n const interval = item.recurrenceDetail || 1;\n if (interval > 1 && item.fromdt) {\n const startDate = new Date(item.fromdt);\n const diffMs = now.getTime() - startDate.getTime();\n const diffDays = Math.floor(diffMs / 86400000);\n if (diffDays < 0 || diffDays % interval !== 0) {\n return false;\n }\n }\n return true;\n }\n\n case 'Month': {\n // Monthly recurrence — recurrenceRepeatsOn is day-of-month (1-31)\n if (item.recurrenceRepeatsOn) {\n const allowedDays = item.recurrenceRepeatsOn.split(',').map(d => parseInt(d.trim()));\n const currentDayOfMonth = now.getDate();\n if (!allowedDays.includes(currentDayOfMonth)) {\n return false;\n }\n }\n // If recurrenceDetail > 1, only active every N months from fromdt\n const interval = item.recurrenceDetail || 1;\n if (interval > 1 && item.fromdt) {\n const startDate = new Date(item.fromdt);\n const monthsDiff = (now.getFullYear() - startDate.getFullYear()) * 12\n + now.getMonth() - startDate.getMonth();\n if (monthsDiff < 0 || monthsDiff % interval !== 0) {\n return false;\n }\n }\n return true;\n }\n\n default:\n log.debug(`Unsupported recurrence type: ${item.recurrenceType}`);\n return true; // Unknown type, fallback to date/time checks\n }\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' || item.recurrenceType === 'Day' || item.recurrenceType === 'Month') {\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, weatherData: this.weatherData })) 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 * Detect schedule conflicts: time windows where multiple layouts compete\n * and lower-priority ones are hidden.\n *\n * Scans the schedule in 1-minute increments over the given window.\n * At each point, collects all time-active layouts (after criteria/geofence\n * filtering but before priority filtering). If multiple priorities exist,\n * the lower-priority entries are reported as hidden.\n *\n * @param {Object} [options]\n * @param {Date} [options.from] - Start time (default: now)\n * @param {number} [options.hours] - Hours to scan (default: 24)\n * @returns {Array<{startTime: Date, endTime: Date, winner: {file: string, priority: number}, hidden: Array<{file: string, priority: number}>}>}\n */\n detectConflicts(options = {}) {\n const from = options.from || new Date();\n const hours = options.hours || 24;\n const to = new Date(from.getTime() + hours * 3600000);\n const stepMs = 60000; // 1-minute granularity\n const conflicts = [];\n let current = null; // Current conflict window being built\n\n for (let t = from.getTime(); t < to.getTime(); t += stepMs) {\n const time = new Date(t);\n const allLayouts = this.getAllLayoutsAtTime(time);\n\n if (allLayouts.length === 0) {\n // No layouts → close any open conflict\n if (current) { conflicts.push(current); current = null; }\n continue;\n }\n\n const maxPriority = Math.max(...allLayouts.map(l => l.priority));\n const hidden = allLayouts.filter(l => l.priority < maxPriority);\n\n if (hidden.length === 0) {\n // No conflict at this time\n if (current) { conflicts.push(current); current = null; }\n continue;\n }\n\n // Conflict exists — build or extend window\n const winners = allLayouts.filter(l => l.priority === maxPriority);\n const winnerKey = winners.map(w => w.file).sort().join(',');\n const hiddenKey = hidden.map(h => `${h.file}:${h.priority}`).sort().join(',');\n\n if (current && current._winnerKey === winnerKey && current._hiddenKey === hiddenKey) {\n // Same conflict continues — extend window\n current.endTime = new Date(t + stepMs);\n } else {\n // New or changed conflict\n if (current) conflicts.push(current);\n current = {\n startTime: new Date(t),\n endTime: new Date(t + stepMs),\n winner: { file: winners[0].file, priority: maxPriority },\n hidden: hidden.map(h => ({ file: h.file, priority: h.priority })),\n _winnerKey: winnerKey,\n _hiddenKey: hiddenKey,\n };\n }\n }\n\n if (current) conflicts.push(current);\n\n // Clean internal keys\n for (const c of conflicts) {\n delete c._winnerKey;\n delete c._hiddenKey;\n }\n\n return conflicts;\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, weatherData: this.weatherData })) {\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 * Get current layouts with the default layout interleaved between scheduled layouts.\n *\n * Xibo CMS expects the default layout (fallback) to play between each scheduled\n * layout in the rotation. For example, with layouts [A, B, C] and default D,\n * the result is [A, D, B, D, C, D].\n *\n * No interleaving when:\n * - There is no default layout configured\n * - There are no scheduled layouts (only default plays)\n * - There is only one scheduled layout (no gaps to fill)\n *\n * @returns {string[]} Layout files with default interleaved\n */\n getInterleavedLayouts() {\n const layouts = this.getCurrentLayouts();\n const defaultLayout = this.schedule?.default;\n\n // No interleaving needed: no default, no layouts, or single layout\n if (!defaultLayout || layouts.length <= 1) {\n return layouts;\n }\n\n // If the only layout is the default itself, return as-is\n if (layouts.length === 1 && layouts[0] === defaultLayout) {\n return layouts;\n }\n\n // Interleave: A, D, B, D, C, D\n const interleaved = [];\n for (const layout of layouts) {\n interleaved.push(layout);\n interleaved.push(defaultLayout);\n }\n\n log.info('[Schedule] Interleaved', layouts.length, 'layouts with default', defaultLayout);\n return interleaved;\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 || overlay.fromDt) ? new Date(overlay.fromdt || overlay.fromDt) : null;\n const to = (overlay.todt || overlay.toDt) ? new Date(overlay.todt || 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 * @param {Date} [options.currentLayoutStartedAt] - When current layout started (adjusts first entry to remaining time)\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 currentLayoutStartedAt = options.currentLayoutStartedAt || null;\n const timeline = [];\n let currentTime = new Date(from);\n let isFirstEntry = true;\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 let hiddenLayouts = null;\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 // Detect hidden layouts (lower priority, not playing)\n if (allLayouts.length > playable.length) {\n hiddenLayouts = allLayouts\n .filter(l => !playable.includes(l.file))\n .map(l => ({ file: l.file, priority: l.priority }));\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 let dur = durations.get(file) || defaultDuration;\n\n // First entry: use remaining duration if we know when the current layout started\n if (isFirstEntry && currentLayoutStartedAt) {\n const elapsedSec = (from.getTime() - currentLayoutStartedAt.getTime()) / 1000;\n const remaining = Math.max(1, Math.round(dur - elapsedSec));\n dur = remaining;\n isFirstEntry = false;\n }\n\n const endMs = currentTime.getTime() + dur * 1000;\n\n const entry = {\n layoutFile: file,\n startTime: new Date(currentTime),\n endTime: new Date(endMs),\n duration: dur,\n isDefault: false,\n };\n if (hiddenLayouts && hiddenLayouts.length > 0) {\n entry.hidden = hiddenLayouts;\n }\n timeline.push(entry);\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 { CacheAnalyzer } from '@xiboplayer/cache';\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 // Scheduled commands tracking (avoid re-executing same command)\n this._executedCommands = new Set();\n\n // Display commands from RegisterDisplay (used by XMR commandAction)\n this.displayCommands = null;\n\n // Fault reporting agent (independent timer, faster than collection cycle)\n this._faultReportingInterval = null;\n this._faultReportingSeconds = 60; // Default: check for faults every 60s\n\n // Unsafe layout blacklist: layoutId → { failures: number, blacklisted: boolean, reason: string }\n this._layoutBlacklist = new Map();\n this._blacklistThreshold = 3; // Consecutive failures before blacklisting\n\n // Status tracking for NotifyStatus enrichment\n this._lastLayoutChangeTime = null; // ISO timestamp of last layout switch\n this._statusCode = 2; // 1=running, 2=downloading, 3=error\n\n // 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 // Cache analyzer for stale media detection and storage health\n this.cacheAnalyzer = this.cache ? new CacheAnalyzer(this.cache) : null;\n\n // In-memory offline cache (populated from IndexedDB on first load)\n this._offlineCache = { schedule: null, settings: null, requiredFiles: null };\n this._offlineDbReady = this._initOfflineCache();\n }\n\n // ── 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 log.info('Offline cache loaded from IndexedDB',\n schedule ? '(has schedule)' : '(empty)');\n } catch (e) {\n log.warn('Failed to load offline cache from IndexedDB:', e);\n }\n }\n\n /** Save a key to both in-memory cache and IndexedDB (fire-and-forget) */\n async _offlineSave(key, data) {\n this._offlineCache[key] = data;\n try {\n 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 log.warn('Failed to save offline cache:', key, e);\n }\n }\n\n /** Check if we have any cached data to fall back on */\n hasCachedData() {\n return this._offlineCache.schedule !== null;\n }\n\n /** Check if the browser reports being offline */\n isOffline() {\n return typeof navigator !== 'undefined' && navigator.onLine === false;\n }\n\n /** Check if currently in offline mode */\n isInOfflineMode() {\n return this.offlineMode;\n }\n\n /**\n * Run an offline collection cycle using cached data.\n * Evaluates the cached schedule and continues playback.\n */\n collectOffline() {\n log.warn('Offline mode — using cached schedule');\n\n if (!this.offlineMode) {\n this.offlineMode = true;\n this.emit('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 // Ensure RSA key pair exists before registering\n if (this.config.ensureXmrKeyPair) {\n await this.config.ensureXmrKeyPair();\n }\n\n // Register display\n log.debug('Collection step: registerDisplay');\n const regResult = await this.xmds.registerDisplay();\n log.info(`Display registered: ${regResult.code}${regResult.tags?.length ? `, tags: ${regResult.tags.join(', ')}` : ''}`);\n log.debug('Register result:', JSON.stringify(regResult));\n\n // Cache settings for offline use\n this._offlineSave('settings', regResult);\n\n // Exit offline mode if we were in it\n if (this.offlineMode) {\n this.offlineMode = false;\n log.info('Back online — resuming normal collection');\n this.emit('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 // Pass display properties to schedule for criteria evaluation\n if (this.schedule?.setDisplayProperties && regResult.settings) {\n this.schedule.setDisplayProperties(regResult.settings);\n }\n\n // Store sync config if display is in a sync group\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 // Extract config from display tags (key|value convention)\n this._applyTagConfig(regResult.tags);\n\n // Store display commands for XMR commandAction resolution\n if (regResult.commands && regResult.commands.length > 0) {\n this.displayCommands = {};\n for (const cmd of regResult.commands) {\n this.displayCommands[cmd.commandCode] = cmd;\n }\n log.debug('Display commands:', Object.keys(this.displayCommands).join(', '));\n }\n\n this.emit('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 // RequiredFiles changed — CMS may have fixed broken layouts\n this.resetBlacklist();\n\n log.debug('Collection step: requiredFiles');\n const rfResult = await this.xmds.requiredFiles();\n // RequiredFiles returns { files, purge } — files to download, items to delete\n const files = rfResult.files || rfResult;\n const purgeItems = rfResult.purge || [];\n log.info('Required files:', files.length, purgeItems.length > 0 ? `(+ ${purgeItems.length} purge)` : '');\n this._lastCheckRf = checkRf;\n this.emit('files-received', files);\n\n // Cache required files for offline use\n this._offlineSave('requiredFiles', rfResult);\n\n if (purgeItems.length > 0) {\n this.emit('purge-request', purgeItems);\n }\n\n // Get schedule (skip if CRC unchanged)\n if (!this._lastCheckSchedule || this._lastCheckSchedule !== checkSchedule) {\n log.debug('Collection step: schedule');\n const schedule = await this.xmds.schedule();\n log.info('Schedule received');\n this._lastCheckSchedule = checkSchedule;\n log.debug('Collection step: processing schedule');\n this.emit('schedule-received', schedule);\n this.schedule.setSchedule(schedule);\n this._executedCommands.clear();\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\n // Download window enforcement (#81) — skip downloads outside configured window\n if (this.displaySettings?.isInDownloadWindow && !this.displaySettings.isInDownloadWindow()) {\n const nextWindow = this.displaySettings.getNextDownloadWindow?.();\n log.info(`Outside download window, skipping downloads${nextWindow ? ` (next: ${nextWindow.toLocaleTimeString()})` : ''}`);\n } else {\n this.emit('download-request', { layoutOrder, files });\n }\n\n // Non-blocking cache analysis (stale media detection)\n if (this.cacheAnalyzer) {\n this.cacheAnalyzer.analyze(files).then(report => {\n this.emit('cache-analysis', report);\n }).catch(err => log.warn('Cache analysis failed:', err));\n }\n\n // Submit media inventory to CMS (reports cached files)\n this.submitMediaInventory(files);\n } else {\n if (checkRf) {\n log.info('RequiredFiles CRC unchanged, skipping download check');\n }\n if (this._lastCheckSchedule !== checkSchedule) {\n const schedule = await this.xmds.schedule();\n log.info('Schedule received (RF unchanged but schedule changed)');\n this._lastCheckSchedule = checkSchedule;\n this.emit('schedule-received', schedule);\n this.schedule.setSchedule(schedule);\n this._executedCommands.clear();\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 // Fetch weather data for schedule criteria evaluation (#15)\n await this._fetchWeatherData();\n\n log.debug('Collection step: evaluateSchedule');\n // Evaluate current schedule\n const layoutFiles = this.schedule.getCurrentLayouts();\n log.info('Current layouts:', layoutFiles);\n this.emit('layouts-scheduled', layoutFiles);\n\n this._evaluateAndSwitchLayout(layoutFiles, '');\n\n // Process scheduled commands (auto-execute commands whose time has arrived)\n this._processScheduledCommands();\n\n // 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 // Submit faults immediately (higher priority than logs)\n this.emit('submit-faults-request');\n\n // Setup collection interval on first run\n if (!this.collectionInterval && regResult.settings) {\n this.setupCollectionInterval(regResult.settings);\n }\n\n // Start fault reporting agent (independent of collection cycle)\n if (!this._faultReportingInterval) {\n this._startFaultReportingAgent();\n }\n\n 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 log.warn('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 /**\n * Start the fault reporting agent.\n * Runs on an independent timer (default 60s) to submit faults faster\n * than the normal collection cycle (300s). This ensures the CMS dashboard\n * gets fault alerts with lower latency.\n */\n _startFaultReportingAgent() {\n if (this._faultReportingInterval) clearInterval(this._faultReportingInterval);\n\n log.info(`Fault reporting agent started (interval: ${this._faultReportingSeconds}s)`);\n this._faultReportingInterval = setInterval(() => {\n this.emit('submit-faults-request');\n }, this._faultReportingSeconds * 1000);\n }\n\n /** Internal: (re)create the collection setInterval timer */\n _setCollectionTimer(seconds) {\n if (this.collectionInterval) clearInterval(this.collectionInterval);\n this._currentCollectInterval = seconds;\n log.info(`Collection interval: ${seconds}s`);\n this.collectionInterval = setInterval(() => {\n log.debug('Running scheduled collection cycle...');\n this.collect().catch(error => {\n log.error('Collection error:', error);\n this.emit('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._lastLayoutChangeTime = new Date().toISOString();\n this._statusCode = 1; // Running\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 * Skips blacklisted layouts. Returns { layoutId, layoutFile } or null.\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 // Try each layout starting from current index, skip blacklisted\n for (let i = 0; i < layoutFiles.length; i++) {\n const idx = (this._currentLayoutIndex + i) % layoutFiles.length;\n const layoutFile = layoutFiles[idx];\n const layoutId = parseLayoutFile(layoutFile);\n\n if (!this.isLayoutBlacklisted(layoutId)) {\n this._currentLayoutIndex = idx;\n return { layoutId, layoutFile };\n }\n }\n\n // All layouts blacklisted — return first anyway to avoid blank screen\n log.warn('All scheduled layouts are blacklisted, using first layout as fallback');\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.getInterleavedLayouts?.() || 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 // Find next non-blacklisted layout\n for (let i = 1; i < layoutFiles.length; i++) {\n const idx = (this._currentLayoutIndex + i) % layoutFiles.length;\n const layoutFile = layoutFiles[idx];\n const layoutId = parseLayoutFile(layoutFile);\n\n if (layoutId !== this.currentLayoutId && !this.isLayoutBlacklisted(layoutId)) {\n return { layoutId, layoutFile };\n }\n }\n\n return null;\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.getInterleavedLayouts?.() || 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 // Find next non-blacklisted layout (wraps around, tries all)\n let layoutFile, layoutId;\n for (let i = 1; i <= layoutFiles.length; i++) {\n const idx = (this._currentLayoutIndex + i) % layoutFiles.length;\n const file = layoutFiles[idx];\n const id = parseLayoutFile(file);\n\n if (!this.isLayoutBlacklisted(id)) {\n this._currentLayoutIndex = idx;\n layoutFile = file;\n layoutId = id;\n break;\n }\n }\n\n // All layouts blacklisted — fall back to replaying current\n if (!layoutFile) {\n if (this.currentLayoutId) {\n log.warn('All layouts blacklisted, replaying current to avoid blank screen');\n const replayId = this.currentLayoutId;\n this.currentLayoutId = null;\n this.emit('layout-prepare-request', replayId);\n } else {\n this.emit('no-layouts-scheduled');\n }\n return;\n }\n\n // Multi-display sync: if this is a sync event and we have a SyncManager,\n // delegate layout transitions to the sync protocol\n if (this.syncManager && this.schedule.isSyncEvent(layoutFile)) {\n if (this.isSyncLead()) {\n // 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 const status = {\n currentLayoutId: layoutId,\n deviceName: this.config?.displayName || '',\n displayName: this.config?.displayName || '',\n lastCommandSuccess: this._lastCommandSuccess ?? true,\n code: this._statusCode,\n lastLayoutChangeTime: this._lastLayoutChangeTime || new Date().toISOString(),\n };\n\n // Add geo-location if available\n if (this.config?.latitude) status.latitude = this.config.latitude;\n if (this.config?.longitude) status.longitude = this.config.longitude;\n\n await this.xmds.notifyStatus(status);\n this.emit('status-notified', layoutId);\n } catch (error) {\n log.warn('Failed to notify status:', error);\n this.emit('status-notify-failed', layoutId, error);\n }\n }\n\n /**\n * Report geo location (called by XMR when CMS pushes coordinates)\n * Updates schedule location for geo-fencing and triggers schedule re-evaluation.\n * @param {Object} data - { latitude, longitude }\n */\n reportGeoLocation(data) {\n const lat = parseFloat(data?.latitude);\n const lng = parseFloat(data?.longitude);\n\n if (isNaN(lat) || isNaN(lng)) {\n log.warn('reportGeoLocation: invalid coordinates', data);\n return;\n }\n\n log.info(`Geo location from CMS: ${lat.toFixed(4)}, ${lng.toFixed(4)}`);\n\n if (this.schedule?.setLocation) {\n this.schedule.setLocation(lat, lng);\n }\n\n this.emit('location-updated', { latitude: lat, longitude: lng, source: 'cms' });\n this.checkSchedule();\n }\n\n /**\n * Request geo location using a fallback chain:\n * 1. Browser Geolocation API (GPS / OS-level)\n * 2. Google Geolocation API (if GOOGLE_GEO_API_KEY is configured)\n * 3. IP-based geolocation (free, no key required)\n * @returns {Promise<{latitude: number, longitude: number}|null>}\n */\n async requestGeoLocation() {\n // Try browser geolocation first (works with GPS or Google API key baked into Chromium)\n const browser = await this._tryBrowserGeolocation();\n if (browser) return this._applyLocation(browser.latitude, browser.longitude, 'browser');\n\n // Try Google Geolocation API if key is configured\n const apiKey = this.config?.googleGeoApiKey;\n if (apiKey) {\n const google = await this._tryGoogleGeolocation(apiKey);\n if (google) return this._applyLocation(google.latitude, google.longitude, 'google-api');\n }\n\n // Fall back to IP-based geolocation (free, no key)\n const ip = await this._tryIpGeolocation();\n if (ip) return this._applyLocation(ip.latitude, ip.longitude, 'ip-geolocation');\n\n log.warn('All geolocation methods failed');\n return null;\n }\n\n /**\n * Apply a resolved location: update schedule, emit event, trigger re-evaluation.\n * @param {number} lat\n * @param {number} lng\n * @param {string} source - 'browser' | 'google-api' | 'ip-geolocation'\n * @returns {{latitude: number, longitude: number}}\n * @private\n */\n /**\n * Extract config values from CMS display tags using key|value convention.\n * Tags like \"geoApiKey|AIzaSy...\" are parsed and applied to player config.\n * @param {string[]} tags - Array of tag strings from RegisterDisplay\n * @private\n */\n _applyTagConfig(tags) {\n if (!Array.isArray(tags) || tags.length === 0) return;\n\n const TAG_CONFIG_MAP = {\n 'geoApiKey': 'googleGeoApiKey',\n };\n\n for (const tag of tags) {\n const pipeIdx = tag.indexOf('|');\n if (pipeIdx === -1) continue;\n\n const key = tag.substring(0, pipeIdx);\n const value = tag.substring(pipeIdx + 1);\n const configKey = TAG_CONFIG_MAP[key];\n\n if (configKey && value && this.config) {\n log.info(`Config from CMS tag: ${key} → ${configKey}`);\n this.config[configKey] = value;\n }\n }\n }\n\n _applyLocation(lat, lng, source) {\n log.info(`Geolocation (${source}): ${lat.toFixed(4)}, ${lng.toFixed(4)}`);\n\n if (this.schedule?.setLocation) {\n this.schedule.setLocation(lat, lng);\n }\n\n this.emit('location-updated', { latitude: lat, longitude: lng, source });\n this.checkSchedule();\n\n return { latitude: lat, longitude: lng };\n }\n\n /**\n * Try the browser Geolocation API (navigator.geolocation).\n * @returns {Promise<{latitude: number, longitude: number}|null>}\n * @private\n */\n async _tryBrowserGeolocation() {\n if (typeof navigator === 'undefined' || !navigator.geolocation) return null;\n\n try {\n const position = await new Promise((resolve, reject) => {\n navigator.geolocation.getCurrentPosition(resolve, reject, {\n timeout: 10000,\n maximumAge: 300000, // 5 minutes\n enableHighAccuracy: false\n });\n });\n return { latitude: position.coords.latitude, longitude: position.coords.longitude };\n } catch (error) {\n log.warn('Browser geolocation failed:', error?.message || error);\n return null;\n }\n }\n\n /**\n * Try Google Geolocation API (direct HTTPS POST, bypasses Chromium's built-in service).\n * @param {string} apiKey - Google API key\n * @returns {Promise<{latitude: number, longitude: number}|null>}\n * @private\n */\n async _tryGoogleGeolocation(apiKey) {\n try {\n const res = await fetch(\n `https://www.googleapis.com/geolocation/v1/geolocate?key=${apiKey}`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ considerIp: true }),\n signal: AbortSignal.timeout(5000)\n }\n );\n if (!res.ok) {\n log.warn(`Google Geolocation API returned ${res.status}`);\n return null;\n }\n const data = await res.json();\n if (data.location?.lat != null && data.location?.lng != null) {\n return { latitude: data.location.lat, longitude: data.location.lng };\n }\n return null;\n } catch (error) {\n log.warn('Google Geolocation API failed:', error?.message || error);\n return null;\n }\n }\n\n /**\n * Try IP-based geolocation using free HTTPS providers (no API key needed).\n * Tries ipapi.co first, then freeipapi.com as fallback.\n * @returns {Promise<{latitude: number, longitude: number}|null>}\n * @private\n */\n async _tryIpGeolocation() {\n const providers = [\n {\n url: 'https://ipapi.co/json/',\n parse: (data) => data.latitude != null && data.longitude != null\n ? { latitude: data.latitude, longitude: data.longitude }\n : null\n },\n {\n url: 'https://freeipapi.com/api/json',\n parse: (data) => data.latitude != null && data.longitude != null\n ? { latitude: data.latitude, longitude: data.longitude }\n : null\n }\n ];\n\n for (const provider of providers) {\n try {\n const res = await fetch(provider.url, { signal: AbortSignal.timeout(5000) });\n if (!res.ok) continue;\n const data = await res.json();\n const location = provider.parse(data);\n if (location) return location;\n } catch (error) {\n log.warn(`IP geolocation (${provider.url}) failed:`, error?.message || error);\n }\n }\n return null;\n }\n\n /**\n * Re-evaluate current schedule and switch layouts if needed.\n * Called after location updates or other schedule-affecting changes.\n */\n checkSchedule() {\n const layoutFiles = this.schedule.getCurrentLayouts();\n this.emit('layouts-scheduled', layoutFiles);\n this._evaluateAndSwitchLayout(layoutFiles, '');\n }\n\n /**\n * Capture screenshot (called by XMR wrapper)\n * Emits event for platform layer to handle\n */\n async captureScreenshot() {\n log.info('Screenshot requested');\n this.emit('screenshot-request');\n }\n\n /**\n * Change to a specific layout (called by XMR wrapper)\n * Tracks override state so revertToSchedule() can undo it.\n */\n async changeLayout(layoutId, options) {\n log.info('Layout change requested via XMR:', layoutId);\n const id = parseInt(layoutId, 10);\n const duration = options?.duration || 0;\n const changeMode = options?.changeMode || 'replace';\n this._layoutOverride = { layoutId: id, type: 'change', duration, changeMode };\n this.currentLayoutId = null; // Force re-render\n this.emit('layout-prepare-request', id);\n\n // Auto-revert after duration (if specified)\n if (duration > 0) {\n setTimeout(() => {\n if (this._layoutOverride?.layoutId === id) {\n log.info(`Layout override duration expired (${duration}s), reverting to schedule`);\n this.revertToSchedule();\n }\n }, duration * 1000);\n }\n }\n\n /**\n * Push an overlay layout on top of current content (called by XMR wrapper)\n * @param {number|string} layoutId - Layout to overlay\n */\n async overlayLayout(layoutId, options) {\n log.info('Overlay layout requested via XMR:', layoutId);\n const id = parseInt(layoutId, 10);\n const duration = options?.duration || 0;\n this._layoutOverride = { layoutId: id, type: 'overlay', duration };\n this.emit('overlay-layout-request', id);\n\n // Auto-revert after duration (if specified)\n if (duration > 0) {\n setTimeout(() => {\n if (this._layoutOverride?.layoutId === id) {\n log.info(`Overlay duration expired (${duration}s), reverting to schedule`);\n this.revertToSchedule();\n }\n }, duration * 1000);\n }\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._lastCommandSuccess = false;\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 this._lastCommandSuccess = success;\n log.info(`HTTP command ${commandCode} result: ${response.status}`);\n this.emit('command-result', { code: commandCode, success, status: response.status });\n } catch (error) {\n this._lastCommandSuccess = false;\n log.error(`HTTP command ${commandCode} failed:`, error);\n this.emit('command-result', { code: commandCode, success: false, reason: error.message });\n }\n } else {\n // Emit event for platform layer (Electron/Chromium) to handle native commands\n // (shell, RS232, Android intent, etc.)\n log.info('Delegating non-HTTP command to platform layer:', commandCode);\n this.emit('execute-native-command', { code: commandCode, commandString });\n }\n }\n\n /**\n * Trigger a webhook action (called by XMR wrapper)\n * @param {string} triggerCode - The trigger code to fire\n */\n triggerWebhook(triggerCode) {\n log.info('Webhook trigger from XMR:', triggerCode);\n this.handleTrigger(triggerCode);\n }\n\n /**\n * Force refresh of data connectors (called by XMR wrapper)\n */\n refreshDataConnectors() {\n log.info('Data connector refresh requested via XMR');\n this.dataConnectorManager.refreshAll();\n this.emit('data-connectors-refreshed');\n }\n\n /**\n * Submit media inventory to CMS\n * Reports which files are cached and complete.\n * @param {Array} files - List of files from RequiredFiles\n */\n async submitMediaInventory(files) {\n if (!files || files.length === 0) return;\n\n try {\n // Build inventory XML: <files><file type=\"media\" id=\"1\" complete=\"1\" md5=\"abc\" lastChecked=\"123\"/></files>\n // complete: use file.complete if set by caller (cache layer), default to \"1\"\n const now = Math.floor(Date.now() / 1000);\n const fileEntries = files\n .filter(f => ['media', 'layout', 'resource', 'dependency', 'widget'].includes(f.type))\n .map(f => {\n const complete = f.complete !== undefined ? (f.complete ? '1' : '0') : '1';\n const fileType = f.fileType ? ` fileType=\"${f.fileType}\"` : '';\n return `<file type=\"${f.type}\" id=\"${f.id}\" complete=\"${complete}\" md5=\"${f.md5 || ''}\" lastChecked=\"${now}\"${fileType}/>`;\n })\n .join('');\n const inventoryXml = `<files>${fileEntries}</files>`;\n\n await this.xmds.mediaInventory(inventoryXml);\n log.info(`Media inventory submitted: ${files.length} files`);\n this.emit('media-inventory-submitted', files.length);\n } catch (error) {\n log.warn('MediaInventory submission failed:', error);\n }\n }\n\n /**\n * BlackList a media file (report broken media to CMS)\n * @param {string|number} mediaId - The media ID\n * @param {string} type - File type ('media' or 'layout')\n * @param {string} reason - Reason for blacklisting\n */\n async blackList(mediaId, type, reason) {\n try {\n await this.xmds.blackList(mediaId, type, reason);\n this.emit('media-blacklisted', { mediaId, type, reason });\n } catch (error) {\n log.warn('BlackList failed:', error);\n }\n }\n\n /**\n * Report a layout render failure. After N consecutive failures\n * (default 3), the layout is blacklisted and skipped in schedule\n * evaluation. Blacklisted layouts are reported to CMS via the\n * BlackList XMDS method.\n *\n * @param {number} layoutId - The layout that failed\n * @param {string} reason - Human-readable failure description\n */\n reportLayoutFailure(layoutId, reason) {\n const id = Number(layoutId);\n const entry = this._layoutBlacklist.get(id) || { failures: 0, blacklisted: false, reason: '' };\n entry.failures++;\n entry.reason = reason;\n\n if (!entry.blacklisted && entry.failures >= this._blacklistThreshold) {\n entry.blacklisted = true;\n log.warn(`Layout ${id} blacklisted after ${entry.failures} consecutive failures: ${reason}`);\n this.emit('layout-blacklisted', { layoutId: id, reason, failures: entry.failures });\n\n // Report to CMS (non-blocking)\n this.blackList(id, 'layout', reason);\n } else if (!entry.blacklisted) {\n log.info(`Layout ${id} failure ${entry.failures}/${this._blacklistThreshold}: ${reason}`);\n }\n\n this._layoutBlacklist.set(id, entry);\n }\n\n /**\n * Report a successful layout render. Resets the failure counter for\n * this layout, removing it from the blacklist if it was blacklisted.\n *\n * @param {number} layoutId - The layout that rendered successfully\n */\n reportLayoutSuccess(layoutId) {\n const id = Number(layoutId);\n if (this._layoutBlacklist.has(id)) {\n const was = this._layoutBlacklist.get(id);\n this._layoutBlacklist.delete(id);\n if (was.blacklisted) {\n log.info(`Layout ${id} removed from blacklist (rendered successfully)`);\n this.emit('layout-unblacklisted', { layoutId: id });\n }\n }\n }\n\n /**\n * Check if a layout is currently blacklisted.\n * @param {number} layoutId\n * @returns {boolean}\n */\n isLayoutBlacklisted(layoutId) {\n const entry = this._layoutBlacklist.get(Number(layoutId));\n return entry?.blacklisted === true;\n }\n\n /**\n * Get all currently blacklisted layout IDs.\n * @returns {number[]}\n */\n getBlacklistedLayouts() {\n const result = [];\n for (const [id, entry] of this._layoutBlacklist) {\n if (entry.blacklisted) result.push(id);\n }\n return result;\n }\n\n /**\n * Reset the blacklist. Called when RequiredFiles changes (CMS may\n * have fixed broken layouts).\n */\n resetBlacklist() {\n if (this._layoutBlacklist.size > 0) {\n log.info(`Blacklist reset (${this._layoutBlacklist.size} entries cleared)`);\n this._layoutBlacklist.clear();\n this.emit('blacklist-reset');\n }\n }\n\n /**\n * Check if currently in a layout override (from XMR changeLayout/overlayLayout)\n */\n isLayoutOverridden() {\n return this._layoutOverride !== null;\n }\n\n /**\n * Handle interactive trigger (from IC or touch events)\n * Looks up matching action in schedule and executes it\n * @param {string} triggerCode - The trigger code from the IC request\n */\n handleTrigger(triggerCode) {\n const action = this.schedule.findActionByTrigger(triggerCode);\n if (!action) {\n log.debug('No scheduled action matches trigger:', triggerCode);\n return;\n }\n\n log.info(`Action triggered: ${action.actionType} (trigger: ${triggerCode})`);\n\n switch (action.actionType) {\n case 'navLayout':\n case 'navigateToLayout':\n if (action.layoutCode) {\n this.changeLayout(action.layoutCode);\n }\n break;\n case 'navWidget':\n case 'navigateToWidget':\n this.emit('navigate-to-widget', action);\n break;\n case 'command':\n this.emit('execute-command', action.commandCode);\n break;\n default:\n log.warn('Unknown action type:', action.actionType);\n }\n }\n\n /**\n * Update data connectors from current schedule\n * Reconfigures and restarts polling when schedule changes.\n */\n updateDataConnectors() {\n const connectors = this.schedule.getDataConnectors();\n\n if (connectors.length > 0) {\n log.info(`Configuring ${connectors.length} data connector(s)`);\n }\n\n this.dataConnectorManager.setConnectors(connectors);\n\n if (connectors.length > 0) {\n this.dataConnectorManager.startPolling();\n this.emit('data-connectors-started', connectors.length);\n }\n }\n\n /**\n * Process scheduled commands from the CMS schedule.\n * Checks for command events whose scheduled date has arrived and executes them.\n * Each command is only executed once (tracked by code+date key in _executedCommands).\n */\n _processScheduledCommands() {\n if (!this.schedule?.getCommands) return;\n\n const commands = this.schedule.getCommands();\n if (commands.length === 0) return;\n\n const now = new Date();\n\n for (const command of commands) {\n if (!command.code || !command.date) continue;\n\n // Unique key to track execution (same command can be scheduled multiple times)\n const commandKey = `${command.code}|${command.date}`;\n\n // Skip already executed commands\n if (this._executedCommands.has(commandKey)) continue;\n\n // Check if the command's scheduled time has arrived\n const commandDate = new Date(command.date);\n if (isNaN(commandDate.getTime())) {\n log.warn('Scheduled command has invalid date:', command.date);\n continue;\n }\n\n if (now >= commandDate) {\n log.info(`Executing scheduled command: ${command.code} (scheduled: ${command.date})`);\n this._executedCommands.add(commandKey);\n\n // Handle built-in commands directly\n if (command.code === 'collectNow') {\n // Trigger immediate collection on next tick (avoid re-entrance)\n setTimeout(() => this.collectNow().catch(e => log.error('collectNow command failed:', e)), 0);\n } else {\n // Emit event for platform layer to handle (reboot, restart, etc.)\n this.emit('scheduled-command', command);\n }\n }\n }\n }\n\n /**\n * Fetch weather data from CMS and pass to schedule for criteria evaluation.\n * Non-blocking: weather fetch failure doesn't prevent schedule evaluation.\n */\n async _fetchWeatherData() {\n if (!this.xmds?.getWeather || !this.schedule?.setWeatherData) return;\n\n try {\n const weatherJson = await this.xmds.getWeather();\n const weatherData = typeof weatherJson === 'string' ? JSON.parse(weatherJson) : weatherJson;\n this.schedule.setWeatherData(weatherData);\n log.info('Weather data updated:', Object.keys(weatherData).join(', '));\n } catch (e) {\n log.warn('GetWeather failed (non-critical):', e?.message || e);\n }\n }\n\n /**\n * Get the DataConnectorManager instance\n * Used by platform layer to serve data to widgets via IC /realtime\n * @returns {DataConnectorManager}\n */\n getDataConnectorManager() {\n return this.dataConnectorManager;\n }\n\n /**\n * Set the SyncManager instance for multi-display coordination.\n * Called by platform layer after RegisterDisplay returns syncConfig.\n *\n * @param {SyncManager} syncManager - SyncManager instance\n */\n setSyncManager(syncManager) {\n this.syncManager = syncManager;\n log.info('SyncManager attached:', syncManager.isLead ? 'LEAD' : 'FOLLOWER');\n }\n\n /**\n * Check if this display is part of a sync group\n * @returns {boolean}\n */\n isInSyncGroup() {\n return this.syncConfig !== null;\n }\n\n /**\n * Check if this display is the sync group leader\n * @returns {boolean}\n */\n isSyncLead() {\n return this.syncConfig?.isLead === true;\n }\n\n /**\n * Get sync configuration\n * @returns {Object|null} { syncGroup, syncPublisherPort, syncSwitchDelay, syncVideoPauseDelay, isLead }\n */\n getSyncConfig() {\n return this.syncConfig;\n }\n\n // ── Timeline (offline schedule prediction) ─────────────────────────\n\n /**\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 // Only set if no runtime-corrected value exists yet.\n // Runtime corrections (from video metadata / probeLayoutDurations) are\n // more accurate than static XLF parsing which estimates videos at 60s.\n if (!this._layoutDurations.has(file)) {\n this._layoutDurations.set(file, duration);\n }\n if (!this._layoutDurations.has(String(layoutId))) {\n this._layoutDurations.set(String(layoutId), duration);\n }\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 currentLayoutStartedAt: this._lastLayoutChangeTime ? new Date(this._lastLayoutChangeTime) : null,\n });\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._faultReportingInterval) {\n clearInterval(this._faultReportingInterval);\n this._faultReportingInterval = 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: disabled — toggle with D key or ?showDownloads=1\n return { enabled: false };\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 HiddenLayout {\n file: string;\n priority: number;\n}\n\ninterface TimelineEntry {\n layoutFile: string;\n startTime: Date;\n endTime: Date;\n duration: number;\n isDefault: boolean;\n hidden?: HiddenLayout[];\n}\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 private onLayoutClick: ((layoutId: number) => void) | null = null;\n\n constructor(visible = false, onLayoutClick?: (layoutId: number) => void) {\n this.visible = visible;\n this.onLayoutClick = onLayoutClick || null;\n this.createOverlay();\n if (!this.visible) {\n this.overlay!.style.display = 'none';\n }\n }\n\n private createOverlay() {\n this.overlay = document.createElement('div');\n this.overlay.id = 'timeline-overlay';\n this.overlay.style.cssText = `\n position: fixed;\n bottom: 1.5vh;\n left: 1.5vw;\n background: rgba(0, 0, 0, 0.88);\n color: #fff;\n font-family: system-ui, -apple-system, sans-serif;\n font-size: 1.4vw;\n padding: 1vh 1.2vw;\n border-radius: 0.4vw;\n border: 1px solid rgba(255, 255, 255, 0.25);\n z-index: 999999;\n max-width: 35vw;\n box-shadow: 0 0.3vh 1.2vw rgba(0, 0, 0, 0.5);\n pointer-events: auto;\n `;\n // Click-to-skip: delegate click events on layout entries\n this.overlay.addEventListener('click', (e: MouseEvent) => {\n const target = (e.target as HTMLElement).closest('[data-layout-id]') as HTMLElement | null;\n if (!target || !this.onLayoutClick) return;\n const layoutId = parseInt(target.dataset.layoutId!, 10);\n if (isNaN(layoutId) || layoutId === this.currentLayoutId) return;\n this.onLayoutClick(layoutId);\n });\n\n document.body.appendChild(this.overlay);\n }\n\n toggle() {\n this.visible = !this.visible;\n if (this.overlay) {\n this.overlay.style.display = this.visible ? 'block' : 'none';\n }\n // Re-render when becoming visible (render() skips while hidden)\n if (this.visible) {\n this.render();\n }\n // Persist preference\n localStorage.setItem('xibo_show_timeline_overlay', String(this.visible));\n }\n\n /**\n * Update the overlay with new timeline data and/or current layout highlight.\n * Pass timeline=null to keep existing timeline and only update the highlight.\n */\n setOffline(offline: boolean) {\n this.offline = offline;\n this.render();\n }\n\n update(timeline: TimelineEntry[] | null, currentLayoutId: number | null) {\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 const clickable = this.onLayoutClick !== null;\n let currentFound = false;\n\n for (const entry of visible) {\n const layoutId = parseInt(entry.layoutFile.replace('.xlf', ''), 10);\n // Current = first entry matching the currently playing layout\n const isCurrent = !currentFound && layoutId === this.currentLayoutId;\n if (isCurrent) currentFound = true;\n\n const startStr = this.formatTime(entry.startTime);\n const endStr = this.formatTime(entry.endTime);\n const durStr = this.formatDuration(entry.duration);\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; font-weight: 600;' : 'color: #aaa;';\n const cursor = clickable && !isCurrent ? 'cursor: pointer;' : '';\n const hover = clickable && !isCurrent ? 'onmouseover=\"this.style.background=\\'rgba(255,255,255,0.1)\\'\" onmouseout=\"this.style.background=\\'none\\'\"' : '';\n\n html += `<div data-layout-id=\"${layoutId}\" style=\"${borderLeft} ${color} ${cursor} margin-bottom: 0.3vh; font-family: monospace; font-size: 1.3vw; line-height: 1.5; white-space: nowrap;\" ${hover}>`;\n html += `${startStr}–${endStr} #${layoutId} ${durStr}`;\n if (entry.isDefault) html += ' <span style=\"color: #888;\">[def]</span>';\n if (entry.hidden && entry.hidden.length > 0) {\n const hiddenIds = entry.hidden.map(h => `#${h.file.replace('.xlf', '')} (p${h.priority})`).join(', ');\n html += ` <span style=\"color: #8899aa; font-size: 1.1vw;\" title=\"Also scheduled: ${hiddenIds}\">+${entry.hidden.length}</span>`;\n }\n html += '</div>';\n }\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 cacheWidgetHtml: 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;\nlet SyncManager: 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 syncManager: any = null; // Multi-display sync coordinator\n private _currentLayoutEnableStat: boolean = true; // enableStat from current layout XLF\n private _probeTimer: any = null; // Debounce timer for duration probing\n private _pendingFollowerStats: any[] | null = null; // In-flight stats delegated to lead\n private _pendingFollowerLogs: any[] | null = null; // In-flight logs delegated to lead\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 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 } else if (this.core.requestGeoLocation) {\n // No CMS coordinates — try browser Geolocation API as fallback\n log.info('No CMS coordinates, requesting browser geolocation...');\n this.core.requestGeoLocation();\n }\n });\n\n // Setup UI\n this.updateConfigDisplay();\n\n // Online/offline event listeners for seamless offline mode\n window.addEventListener('online', () => {\n log.info('Browser reports online — triggering immediate collection');\n this.updateStatus('Back online, syncing...');\n this.removeOfflineIndicator();\n this.core.collectNow().catch((error: any) => {\n log.error('Failed to collect after coming online:', error);\n });\n });\n window.addEventListener('offline', () => {\n log.warn('Browser reports offline — continuing playback with cached data');\n this.updateStatus('Offline mode — using cached content');\n this.showOfflineIndicator();\n });\n\n // Initialize download progress overlay (configurable debug feature)\n 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 // Timeline overlay — created on first T key press (or if previously visible)\n if (isTimelineVisible()) {\n this.timelineOverlay = new TimelineOverlay(true, (layoutId) => this.skipToLayout(layoutId));\n }\n\n // Request Screen Wake Lock to prevent display sleep\n await this.requestWakeLock();\n\n // Re-acquire wake lock when tab becomes visible again\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'visible') {\n this.requestWakeLock();\n }\n });\n\n // Start collection cycle\n await this.core.collect();\n\n log.info('Player initialized successfully');\n }\n\n /**\n * Request Screen Wake Lock to prevent display from sleeping\n * Re-acquired on visibility change (browser releases it when tab is hidden)\n */\n private async requestWakeLock() {\n if (!('wakeLock' in navigator)) {\n log.debug('Wake Lock API not supported');\n return;\n }\n\n try {\n this._wakeLock = await (navigator as any).wakeLock.request('screen');\n log.info('Screen Wake Lock acquired — display will stay on');\n\n this._wakeLock.addEventListener('release', () => {\n log.debug('Screen Wake Lock released');\n this._wakeLock = null;\n });\n } catch (error: any) {\n log.warn('Wake Lock request failed:', error?.message);\n }\n }\n\n /**\n * Load core modules\n */\n private async loadCoreModules() {\n try {\n // @ts-ignore - JavaScript modules\n const cacheModule = await import('@xiboplayer/cache');\n // @ts-ignore\n const xmdsModule = await import('@xiboplayer/xmds');\n // @ts-ignore\n const scheduleModule = await import('@xiboplayer/schedule');\n // @ts-ignore\n const configModule = await import('@xiboplayer/utils');\n // @ts-ignore\n const xmrModule = await import('@xiboplayer/xmr');\n // @ts-ignore\n const statsModule = await import('@xiboplayer/stats');\n // @ts-ignore\n const displaySettingsModule = await import('@xiboplayer/settings');\n // @ts-ignore\n const coreModule = await import('@xiboplayer/core');\n // @ts-ignore\n const rendererModule = await import('@xiboplayer/renderer');\n // @ts-ignore\n const syncModule = await import('@xiboplayer/sync');\n\n cacheWidgetHtml = cacheModule.cacheWidgetHtml;\n SyncManager = syncModule.SyncManager;\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 // Multi-display sync: create SyncManager when CMS provides sync config\n this.core.on('sync-config', (syncConfig: any) => {\n if (this.syncManager) {\n this.syncManager.stop();\n }\n this.syncManager = new SyncManager({\n displayId: config.hardwareKey,\n syncConfig,\n onLayoutChange: async (layoutId: string) => {\n // Follower: lead requested a layout change — load it but don't show yet\n log.info(`[Sync] Loading layout ${layoutId} (waiting for show signal)`);\n await this.prepareAndRenderLayout(parseInt(layoutId, 10));\n // Report ready to lead\n this.syncManager?.reportReady(layoutId);\n },\n onLayoutShow: (layoutId: string) => {\n // Lead/Follower: show the layout now (already rendered by prepareAndRenderLayout)\n log.info(`[Sync] Show signal for layout ${layoutId}`);\n },\n onVideoStart: (layoutId: string, regionId: string) => {\n // Resume paused video in the specified region\n log.info(`[Sync] Video start: layout ${layoutId} region ${regionId}`);\n this.renderer.resumeRegionMedia?.(regionId);\n },\n // Lead: follower delegated stats — submit on their behalf\n onStatsReport: async (followerId: string, statsXml: string, ack: () => void) => {\n log.info(`[Sync] Submitting stats for follower ${followerId}`);\n try {\n const success = await this.xmds.submitStats(statsXml, followerId);\n if (success) ack();\n } catch (err: any) {\n log.warn(`[Sync] Stats submission failed for follower ${followerId}:`, err);\n }\n },\n // Lead: follower delegated logs — submit on their behalf\n onLogsReport: async (followerId: string, logsXml: string, ack: () => void) => {\n log.info(`[Sync] Submitting logs for follower ${followerId}`);\n try {\n const success = await this.xmds.submitLog(logsXml, followerId);\n if (success) ack();\n } catch (err: any) {\n log.warn(`[Sync] Log submission failed for follower ${followerId}:`, err);\n }\n },\n // Follower: lead confirmed our stats were submitted\n onStatsAck: async (_displayId: string) => {\n log.info('[Sync] Lead confirmed stats submission');\n if (this._pendingFollowerStats && this.statsCollector) {\n await this.statsCollector.clearSubmittedStats(this._pendingFollowerStats);\n this._pendingFollowerStats = null;\n }\n },\n // Follower: lead confirmed our logs were submitted\n onLogsAck: async (_displayId: string) => {\n log.info('[Sync] Lead confirmed logs submission');\n if (this._pendingFollowerLogs && this.logReporter) {\n await this.logReporter.clearSubmittedLogs(this._pendingFollowerLogs);\n this._pendingFollowerLogs = null;\n }\n },\n });\n this.core.setSyncManager(this.syncManager);\n this.syncManager.start();\n log.info(`[Sync] SyncManager started as ${syncConfig.isLead ? 'LEAD' : 'FOLLOWER'}`);\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 this.submitFault('COLLECTION_FAILED', `Collection cycle failed: ${error?.message || error}`);\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 log.warn(`XMR misconfigured: ${info.message}`);\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 } 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 this.submitFault('COMMAND_FAILED', `Command ${result.code} failed: ${result.reason || 'unknown'}`);\n }\n });\n\n // Scheduled commands (#17) — execute commands whose scheduled time has arrived\n this.core.on('scheduled-command', (command: any) => {\n log.info(`Scheduled command: ${command.code}`);\n this.core.executeCommand(command.code);\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 't':\n case 'T':\n if (!this.timelineOverlay) {\n this.timelineOverlay = new TimelineOverlay(true, (layoutId) => this.skipToLayout(layoutId));\n }\n this.timelineOverlay.toggle();\n break;\n case 'd':\n case 'D':\n if (!this.downloadOverlay) {\n this.downloadOverlay = new DownloadOverlay({ enabled: true, autoHide: false });\n }\n this.downloadOverlay.toggle();\n break;\n case 'v':\n case 'V': {\n const videos = document.querySelectorAll('video');\n const show = videos.length > 0 && !videos[0].controls;\n videos.forEach(v => v.controls = show);\n break;\n }\n // Playback control: next/prev/pause\n case 'ArrowRight':\n case 'PageDown':\n log.info('[Remote] Next layout (keyboard)');\n this.core.advanceToNextLayout();\n e.preventDefault();\n break;\n case 'ArrowLeft':\n case 'PageUp':\n log.info('[Remote] Previous layout (keyboard)');\n this.core.advanceToPreviousLayout();\n e.preventDefault();\n break;\n case ' ':\n log.info('[Remote] Toggle pause (keyboard)');\n if (this.renderer.isPaused()) {\n this.renderer.resume();\n } else {\n this.renderer.pause();\n }\n e.preventDefault();\n break;\n case 'r':\n case 'R':\n if (this.core.isLayoutOverridden()) {\n log.info('[Remote] Revert to schedule (keyboard)');\n this.core.revertToSchedule();\n }\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 /**\n * Skip to a specific layout by ID (from timeline click or XMR command).\n * Uses changeLayout() which sets a layout override — press R to revert to schedule.\n */\n private skipToLayout(layoutId: number) {\n log.info(`Skipping to layout ${layoutId} (timeline click)`);\n this.core.changeLayout(layoutId);\n }\n\n private parseBody(body: string | null): any {\n try { return body ? JSON.parse(body) : {}; } catch (_) { return {}; }\n }\n\n /**\n * Handle an Interactive Control request from a widget\n */\n private handleInteractiveControl(method: string, path: string, search: string, body: string | null): any {\n log.debug('IC request:', method, path, search);\n\n switch (path) {\n case '/info':\n return {\n status: 200,\n body: JSON.stringify({\n hardwareKey: config.hardwareKey,\n displayName: config.displayName,\n playerType: 'pwa',\n currentLayoutId: this.core.getCurrentLayoutId()\n })\n };\n\n case '/trigger': {\n const data = this.parseBody(body);\n // Forward to renderer for layout-level actions (widget navigation)\n this.renderer.emit('interactiveTrigger', {\n targetId: data.id,\n triggerCode: data.trigger\n });\n // Forward to core for schedule-level actions (layout navigation)\n if (data.trigger) {\n this.core.handleTrigger(data.trigger);\n }\n return { status: 200, body: 'OK' };\n }\n\n case '/duration/expire': {\n const data = this.parseBody(body);\n log.info('IC: Widget duration expire requested for', data.id);\n this.renderer.emit('widgetExpire', { widgetId: data.id });\n return { status: 200, body: 'OK' };\n }\n\n case '/duration/extend': {\n const data = this.parseBody(body);\n log.info('IC: Widget duration extend by', data.duration, 'for', data.id);\n this.renderer.emit('widgetExtendDuration', {\n widgetId: data.id,\n duration: parseInt(data.duration)\n });\n return { status: 200, body: 'OK' };\n }\n\n case '/duration/set': {\n const data = this.parseBody(body);\n log.info('IC: Widget duration set to', data.duration, 'for', data.id);\n this.renderer.emit('widgetSetDuration', {\n widgetId: data.id,\n duration: parseInt(data.duration)\n });\n return { status: 200, body: 'OK' };\n }\n\n case '/fault': {\n const data = this.parseBody(body);\n this.logReporter?.reportFault(\n data.code || 'WIDGET_FAULT',\n data.reason || 'Widget reported fault'\n );\n this.submitFault(data.code || 'WIDGET_FAULT', data.reason || 'Widget reported fault', {\n layoutId: data.layoutId,\n regionId: data.regionId,\n widgetId: data.widgetId\n });\n return { status: 200, body: 'OK' };\n }\n\n case '/realtime': {\n const params = new URLSearchParams(search);\n const dataKey = params.get('dataKey');\n log.debug('IC: Realtime data request for key:', dataKey);\n\n if (!dataKey) {\n return { status: 400, body: JSON.stringify({ error: 'Missing dataKey parameter' }) };\n }\n\n const dcManager = this.core.getDataConnectorManager();\n const connectorData = dcManager.getData(dataKey);\n\n if (connectorData === null) {\n return { status: 404, body: JSON.stringify({ error: `No data available for key: ${dataKey}` }) };\n }\n\n const responseBody = typeof connectorData === 'string' ? connectorData : JSON.stringify(connectorData);\n return { status: 200, body: responseBody };\n }\n\n case '/criteria': {\n // Return display properties/criteria that widgets can query\n // Used by widgets to adapt content based on display characteristics\n return {\n status: 200,\n body: JSON.stringify({\n displayId: config.displayId,\n hardwareKey: config.hardwareKey,\n displayName: config.displayName,\n width: window.innerWidth,\n height: window.innerHeight,\n latitude: config.latitude || null,\n longitude: config.longitude || null,\n playerType: 'pwa'\n })\n };\n }\n\n default:\n return { status: 404, body: JSON.stringify({ error: 'Unknown IC route' }) };\n }\n }\n\n /**\n * 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 // Store layout-level enableStat for use in layoutEnd\n this._currentLayoutEnableStat = _layout?.enableStat !== false;\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 (only if enableStat is not disabled)\n if (this.statsCollector && this._currentLayoutEnableStat) {\n this.statsCollector.startLayout(layoutId, this.currentScheduleId).catch((err: any) => {\n log.error('Failed to start layout stat:', err);\n });\n }\n });\n\n this.renderer.on('layoutEnd', (layoutId: number) => {\n log.info('Layout ended:', layoutId);\n\n // Record play at END so maxPlaysPerHour doesn't interrupt the current play.\n // Previously recorded at layoutStart, which caused periodic collections to\n // filter the layout mid-playback (e.g., 200s video cut at 168s).\n scheduleManager?.recordPlay(layoutId.toString());\n\n // Track stats: end layout (only if enableStat was not disabled)\n if (this.statsCollector && this._currentLayoutEnableStat) {\n this.statsCollector.endLayout(layoutId, this.currentScheduleId).catch((err: any) => {\n log.error('Failed to end layout stat:', err);\n });\n }\n\n // Report to CMS\n this.core.notifyLayoutStatus(layoutId);\n\n // Clear current layout to allow replay/advance\n this.core.clearCurrentLayout();\n\n // If a new layout is already pending download, don't advance\n // (avoids redundant XMDS calls and duplicate download requests)\n const pending = this.core.getPendingLayouts();\n if (pending.length > 0) {\n log.info(`Layout ${pending[0]} pending download, skipping advance`);\n return;\n }\n\n // Advance to the next layout in the schedule (round-robin cycling)\n // This avoids a full collect() cycle — just picks the next layout and renders it.\n // Periodic collect() cycles still run on the collection interval to sync with CMS.\n log.info('Layout cycle completed, advancing to next layout...');\n this.core.advanceToNextLayout();\n });\n\n this.renderer.on('widgetStart', (data: any) => {\n const { widgetId, layoutId, mediaId } = data;\n log.debug('Widget started:', data.type, widgetId, 'media:', mediaId);\n\n // Track stats: start widget/media (only if enableStat is not disabled)\n if (this.statsCollector && mediaId && data.enableStat !== false) {\n this.statsCollector.startWidget(mediaId, layoutId, this.currentScheduleId).catch((err: any) => {\n log.error('Failed to start widget stat:', err);\n });\n }\n });\n\n this.renderer.on('widgetEnd', (data: any) => {\n const { widgetId, layoutId, mediaId } = data;\n log.debug('Widget ended:', data.type, widgetId, 'media:', mediaId);\n\n // Track stats: end widget/media (only if enableStat is not disabled)\n if (this.statsCollector && mediaId && data.enableStat !== false) {\n this.statsCollector.endWidget(mediaId, layoutId, this.currentScheduleId).catch((err: any) => {\n log.error('Failed to end widget stat:', err);\n });\n }\n });\n\n 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 this.submitFault(error.type || 'RENDERER_ERROR', `Renderer error: ${error.message || error.type}`, {\n layoutId: error.layoutId,\n regionId: error.regionId,\n widgetId: error.widgetId\n });\n });\n\n // Handle interactive actions from touch/click and keyboard triggers\n this.renderer.on('action-trigger', (data: any) => {\n const { actionType, triggerCode, layoutCode, targetId, commandCode } = data;\n log.info('Action trigger:', actionType, data);\n\n switch (actionType) {\n case 'navLayout':\n case 'navigateToLayout':\n if (triggerCode) {\n this.core.handleTrigger(triggerCode);\n } else if (layoutCode) {\n this.core.changeLayout(layoutCode);\n }\n break;\n\n case 'navWidget':\n case 'navigateToWidget':\n if (triggerCode) {\n this.core.handleTrigger(triggerCode);\n } else if (targetId) {\n this.renderer.navigateToWidget(targetId);\n }\n break;\n\n case 'previousWidget':\n this.renderer.previousWidget(data.source?.regionId);\n break;\n\n case 'nextWidget':\n this.renderer.nextWidget(data.source?.regionId);\n break;\n\n case 'command':\n if (commandCode) {\n this.core.executeCommand(commandCode);\n }\n break;\n\n default:\n log.warn('Unknown action type:', actionType);\n }\n\n // Record interaction event for proof of play (#19)\n if (this.statsCollector) {\n this.statsCollector.recordEvent('touch', this.core.getCurrentLayoutId(), data.targetId || null, this.currentScheduleId);\n }\n });\n\n // Widget duration webhooks (#16) — fire HTTP POST when widget duration expires\n this.renderer.on('widgetAction', (data: any) => {\n if (data.type === 'durationEnd' && data.url) {\n log.info(`Widget ${data.widgetId} duration ended, calling webhook: ${data.url}`);\n\n // Record webhook event for proof of play (#19)\n if (this.statsCollector) {\n this.statsCollector.recordEvent('webhook', data.layoutId, data.widgetId, this.currentScheduleId);\n }\n\n fetch(data.url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n widgetId: data.widgetId,\n layoutId: data.layoutId,\n regionId: data.regionId,\n event: 'durationEnd',\n timestamp: new Date().toISOString()\n })\n }).catch(err => log.warn('Webhook failed (non-critical):', err));\n }\n });\n\n // Correct timeline duration when video metadata reveals actual duration\n this.renderer.on('layoutDurationUpdated', (layoutId: number, duration: number) => {\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 cacheProxy.getFile('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 cacheProxy.getFile('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 this.submitFault('LAYOUT_LOAD_FAILED', `Failed to prepare layout ${layoutId}: ${error?.message || error}`, {\n layoutId\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 chunked storage via metadata\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 {\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 whole file (detect corrupted entries)\n const cacheKey = `${PLAYER_BASE}/cache/media/${mediaId}`;\n const response = await cache.match(cacheKey);\n if (!response) continue; // Shouldn't happen — hasFile was true\n\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 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 fetchPromises: Promise<void>[] = [];\n\n for (const regionEl of doc.querySelectorAll('region')) {\n const regionId = regionEl.getAttribute('id');\n\n for (const mediaEl of regionEl.querySelectorAll('media')) {\n const type = mediaEl.getAttribute('type');\n const widgetId = mediaEl.getAttribute('id');\n const render = mediaEl.getAttribute('render');\n\n // XLF render=\"html\" means CMS provides pre-rendered HTML via getResource.\n // render=\"native\" means player handles the media directly (video, image, audio).\n if (render === 'html') {\n 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 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 cacheProxy.getFile('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\n // Probe actual video durations, keyed by fileId\n const videoDurations = new Map<string, number>();\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 videoDurations.set(fileId, duration);\n }\n }\n\n if (videoDurations.size === 0) continue;\n\n // Calculate full layout duration (max region, summing all widgets per region)\n // Same logic as renderer's updateLayoutDuration() — accounts for non-video\n // widgets (PDFs, images, tickers) alongside probed video durations.\n let maxRegionDuration = 0;\n for (const regionEl of doc.querySelectorAll('region')) {\n if (regionEl.getAttribute('type') === 'drawer') continue;\n let regionDuration = 0;\n\n for (const mediaEl of regionEl.querySelectorAll('media')) {\n const dur = parseInt(mediaEl.getAttribute('duration') || '0', 10);\n const useDur = parseInt(mediaEl.getAttribute('useDuration') || '1', 10);\n const fileId = mediaEl.getAttribute('fileId') || '';\n const probedDur = videoDurations.get(fileId);\n\n if (probedDur !== undefined) {\n // Video with probed duration\n regionDuration += probedDur;\n } else if (dur > 0 && useDur !== 0) {\n // Non-video widget with explicit duration\n regionDuration += dur;\n }\n // Looping widgets (useDuration=0, no probe) contribute 0\n }\n\n maxRegionDuration = Math.max(maxRegionDuration, regionDuration);\n }\n\n if (maxRegionDuration > 0) {\n this.core.recordLayoutDuration(String(layoutId), maxRegionDuration);\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 // Guard: don't start a new delegation while one is in-flight\n if (this._pendingFollowerStats !== null) {\n log.debug('Stats delegation in-flight, skipping');\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 // Format stats as XML\n const statsXml = formatStats(stats);\n\n // Follower with live lead: delegate stats via BroadcastChannel\n if (this.syncManager && !this.syncManager.isLead && this._syncLeadAlive()) {\n log.info(`[Sync] Delegating ${stats.length} stats to lead`);\n this._pendingFollowerStats = stats;\n this.syncManager.reportStats(statsXml);\n return;\n }\n\n // Lead, standalone, or lead-dead follower: submit directly\n if (this.syncManager && !this.syncManager.isLead) {\n log.warn('[Sync] Lead not alive, submitting stats directly');\n }\n\n log.info(`Submitting ${stats.length} proof of play 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 // Guard: don't start a new delegation while one is in-flight\n if (this._pendingFollowerLogs !== null) {\n log.debug('Logs delegation in-flight, skipping');\n return;\n }\n\n try {\n const logs = await this.logReporter.getLogsForSubmission();\n\n if (logs.length === 0) {\n log.debug('No logs to submit');\n return;\n }\n\n const logXml = formatLogs(logs);\n\n // Follower with live lead: delegate logs via BroadcastChannel\n if (this.syncManager && !this.syncManager.isLead && this._syncLeadAlive()) {\n log.info(`[Sync] Delegating ${logs.length} logs to lead`);\n this._pendingFollowerLogs = logs;\n this.syncManager.reportLogs(logXml);\n return;\n }\n\n // Lead, standalone, or lead-dead follower: submit directly\n if (this.syncManager && !this.syncManager.isLead) {\n log.warn('[Sync] Lead not alive, submitting logs directly');\n }\n\n log.info(`Submitting ${logs.length} logs to CMS...`);\n\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 * Submit a fault report to CMS for the player_faults dashboard.\n * Runs alongside logReporter.reportFault() which feeds the log dashboard.\n */\n private submitFault(code: string, reason: string, details?: { layoutId?: number; regionId?: string; widgetId?: string }) {\n if (!this.xmds) return;\n\n const fault = JSON.stringify([{\n code,\n reason,\n date: new Date().toISOString().replace('T', ' ').substring(0, 19),\n ...details\n }]);\n\n this.xmds.reportFaults(fault).catch((err: any) => {\n log.debug('reportFaults failed (non-critical):', err);\n });\n }\n\n /**\n * Capture screenshot and submit to CMS.\n *\n * Strategy (best available, tried in order):\n * 0. Electron IPC — webContents.capturePage() via preload bridge.\n * Pixel-perfect, captures video/WebGL/composited layers, zero DOM cost.\n * Only available when running inside the Electron shell.\n * 1. getDisplayMedia() — native pixel capture, 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 * Check if the sync lead is alive (for follower delegation).\n * Returns true if any peer with role 'lead' has been seen in the last 15s.\n */\n private _syncLeadAlive(): boolean {\n if (!this.syncManager) return false;\n for (const [, peer] of this.syncManager.followers) {\n if (peer.role === 'lead' && Date.now() - peer.lastSeen < 15000) {\n return true;\n }\n }\n return false;\n }\n\n /**\n * Cleanup\n */\n cleanup() {\n this.core.cleanup();\n this.renderer.cleanup();\n\n if (this._screenshotInterval) {\n clearInterval(this._screenshotInterval);\n this._screenshotInterval = null;\n }\n\n if (this._wakeLock) {\n this._wakeLock.release();\n this._wakeLock = null;\n }\n\n if (this.downloadOverlay) {\n this.downloadOverlay.destroy();\n }\n\n if (this.timelineOverlay) {\n this.timelineOverlay.destroy();\n }\n }\n}\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-BoQ7K6cl.js"}
|