@xiboplayer/pwa 0.6.13 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{chunk-config-C9U90T6u.js → chunk-config-CpYnrCnz.js} +2 -2
- package/dist/assets/{chunk-config-C9U90T6u.js.map → chunk-config-CpYnrCnz.js.map} +1 -1
- package/dist/assets/{index-B57dvLVB.js → index-BLmNT21V.js} +4 -4
- package/dist/assets/index-BLmNT21V.js.map +1 -0
- package/dist/assets/index-BQElW3UF.js +2 -0
- package/dist/assets/{index-ClTQ0ldA.js.map → index-BQElW3UF.js.map} +1 -1
- package/dist/assets/index-BoLG4XL8.js +2 -0
- package/dist/assets/{index-CLgKYdBJ.js.map → index-BoLG4XL8.js.map} +1 -1
- package/dist/assets/index-C2bD3Z5z.js +2 -0
- package/dist/assets/{index-D39gYDMZ.js.map → index-C2bD3Z5z.js.map} +1 -1
- package/dist/assets/index-DP81_7Rp.js +13 -0
- package/dist/assets/{index-D9_3Ns4E.js.map → index-DP81_7Rp.js.map} +1 -1
- package/dist/assets/index-DkFelvR9.js +2 -0
- package/dist/assets/{index-B2dyKYhW.js.map → index-DkFelvR9.js.map} +1 -1
- package/dist/assets/index-QNL9DsGE.js +2 -0
- package/dist/assets/{index-GPOkOOlx.js.map → index-QNL9DsGE.js.map} +1 -1
- package/dist/assets/{index-BpwloMHc.js → index-WSwWaMW7.js} +2 -2
- package/dist/assets/{index-BpwloMHc.js.map → index-WSwWaMW7.js.map} +1 -1
- package/dist/assets/index-Z6yYXZYs.js +2 -0
- package/dist/assets/{index-dBd5BTjH.js.map → index-Z6yYXZYs.js.map} +1 -1
- package/dist/assets/index-v7YXl8j9.js +2 -0
- package/dist/assets/{index-B_ADwXGQ.js.map → index-v7YXl8j9.js.map} +1 -1
- package/dist/assets/main-C7-NPVem.js +736 -0
- package/dist/assets/main-C7-NPVem.js.map +1 -0
- package/dist/assets/protocol-detector-B1hfkHz0.js +16 -0
- package/dist/assets/protocol-detector-B1hfkHz0.js.map +1 -0
- package/dist/assets/setup-ppxoqsSk.js +2 -0
- package/dist/assets/{setup-CInkLEZK.js.map → setup-ppxoqsSk.js.map} +1 -1
- package/dist/assets/sync-manager-Cov0Op4L.js +2 -0
- package/dist/assets/sync-manager-Cov0Op4L.js.map +1 -0
- package/dist/assets/widget-html-CsEe4aSF.js +2 -0
- package/dist/assets/widget-html-CsEe4aSF.js.map +1 -0
- package/dist/index.html +3 -3
- package/dist/setup.html +3 -3
- package/dist/sw-pwa.js +1 -1
- package/package.json +14 -14
- package/dist/assets/index-B2dyKYhW.js +0 -2
- package/dist/assets/index-B57dvLVB.js.map +0 -1
- package/dist/assets/index-B_ADwXGQ.js +0 -2
- package/dist/assets/index-CLgKYdBJ.js +0 -2
- package/dist/assets/index-ClTQ0ldA.js +0 -2
- package/dist/assets/index-D39gYDMZ.js +0 -2
- package/dist/assets/index-D9_3Ns4E.js +0 -13
- package/dist/assets/index-GPOkOOlx.js +0 -2
- package/dist/assets/index-dBd5BTjH.js +0 -2
- package/dist/assets/main-GlNtH9kX.js +0 -736
- package/dist/assets/main-GlNtH9kX.js.map +0 -1
- package/dist/assets/protocol-detector-CpRVVofI.js +0 -16
- package/dist/assets/protocol-detector-CpRVVofI.js.map +0 -1
- package/dist/assets/setup-CInkLEZK.js +0 -2
- package/dist/assets/sync-manager-C0UDCBzv.js +0 -2
- package/dist/assets/sync-manager-C0UDCBzv.js.map +0 -1
- package/dist/assets/widget-html-BSjCib6e.js +0 -2
- package/dist/assets/widget-html-BSjCib6e.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"mappings":"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","names":["createNanoEvents","event","args","callbacks","length","cb","_a","i","log","createLogger","DAY_NAMES","WEATHER_METRICS","getMetricValue","metric","now","displayProperties","weatherData","weatherKey","evaluateCondition","actual","condition","expected","type","a","e","evaluateCriteria","criteria","options","criterion","parseLayoutDuration","xlfXml","videoDurations","layoutEl","explicit","maxDuration","isDynamic","regionEl","regionType","isCanvas","regionDuration","mediaEl","dur","useDuration","fileId","probed","widgetDuration","arraysEqual","b","canSimulatedPlay","history","maxPlaysPerHour","timeMs","oneHourAgo","playsInLastHour","t","minGapMs","lastPlay","getPlayableLayouts","allLayouts","simPlays","eligible","l","maxPriority","calculateTimeline","queue","queuePosition","from","hours","to","defaultLayout","durations","timeline","currentTime","pos","maxEntries","entry","endMs","gcd","lcm","lcmArray","values","acc","v","buildScheduleQueue","defaultDuration","cmsDurations","getDuration","file","rateLimited","periodSeconds","intervals","sum","cursorMs","periodMs","playable","nextPlayable","defDur","ScheduleManager","schedule","data","map","globalDeps","addLayout","layout","id","deps","campaign","item","rangeEnd","currentDayOfWeek","d","interval","startDate","diffMs","diffDays","allowedDays","currentDayOfMonth","monthsDiff","date","day","fromTime","toTime","time","results","stepMs","conflicts","current","hidden","winners","winnerKey","w","hiddenKey","h","c","skipRateLimiting","skipInterrupts","quiet","_log","activeItems","normalLayouts","interruptLayouts","result","lastCheck","layoutId","timestamp","lastPlayTime","elapsed","remainingMin","cleaned","layoutFile","meta","layoutSetKey","prevLayoutSet","action","triggerCode","latitude","longitude","properties","geoLocation","defaultRadius","parts","s","fenceLat","fenceLng","radius","distance","within","lat1","lon1","lat2","lon2","toRad","deg","dLat","dLon","scheduleManager","logger","InterruptScheduler","duration","requiredSeconds","resolvedInterruptLayouts","interruptSecondsInHour","index","satisfied","allSatisfied","currentInterrupt","normalSecondsInHour","resolvedNormalLayouts","loop","layouts","targetSeconds","resolved","remainingSeconds","pickCount","normalPick","interruptPick","normalIndex","interruptIndex","totalSecondsAllocated","OverlayScheduler","overlays","activeOverlays","overlay","priorityA","o","LayoutPool","maxSize","existing","regionId","region","url","blobUrl","container","videoCount","hlsCount","el","oldest","oldestTime","count","warmIds","keepIds","evictIds","latest","ids","Transitions","element","keyframes","timing","direction","width","height","isIn","dirMap","offset","regionWidth","regionHeight","transitionConfig","RendererLite","config","resizeTimer","screenWidth","screenHeight","scaleX","scaleY","regionConfig","sf","overlayId","callback","parentEl","actions","actionEl","layoutDurationAttr","regionAndDrawerEls","isDrawer","regionOptionsEl","exitTransType","exitTransDuration","exitTransDirection","loopEl","transType","transDuration","transDirection","child","widget","optionsEl","rawEl","raw","transitions","audioNodes","uriEl","commands","commandsEl","cmdEl","parentWidgetId","displayOrder","cyclePlayback","playCount","isRandom","fromDt","toDt","render","blobUrls","maxRegionDuration","oldDuration","final_","remainingMs","allKeyboardActions","touchActionCount","widgetEl","widgetId","handler","source","keyboardActions","pressedKey","keycode","widgetIndex","regionMap","isMain","rid","idx","targetId","found","targetWidgetId","nextIndex","targetWidget","prevIndex","storedAs","PLAYER_API","regions","saveAs","error","widgets","tagName","stream","playAfterSeek","videoEl","resolve","timer","onPlaying","audioEl","imgEl","onLoad","readyPromises","layoutDurationMs","_b","audioElements","audioNode","audio","playPromise","widgetElement","animPromise","animation","html","durationMatch","newDuration","numItemsMatch","numItems","groups","groupId","groupWidgets","selectedWidget","state","effectivePlayCount","r","showFn","hideFn","onCycleComplete","playNext","cmd","key","stopFn","img","scaleType","fitMap","alignMap","valignMap","hPos","vPos","src","video","vScaleType","vFitMap","onEnded","videoSrc","Hls","__vitePreload","hls","_event","createdForLayoutId","onLoadedMetadata","videoDuration","onLoadedData","onError","errorCode","errorMessage","videoConstraints","deviceId","constraints","onAudioEnded","audioCreatedForLayoutId","onAudioLoadedMetadata","audioDuration","onAudioError","icon","info","filename","iframe","blob","pdfjsModule","basePath","pdfUrl","pdf","totalPages","timePerPage","page1","viewport0","scale","canvas","ctx","indicator","isDebug","currentPage","cycleTimer","activeRenderTask","stopped","cyclePage","page","scaledViewport","cancelPromise","task","uri","div","preloadDelay","retryDelay","wrapper","savedCurrentLayoutId","preloadRegions","preloadBlobUrls","savedLayoutBlobUrls","preloaded","oldLayoutId","alreadyEmittedEnd","prop","allComplete","endedLayoutId","shouldEmit","priority","overlayDiv","overlayRegions","durationMs","overlayState","overlayIds","fn","LayoutTranslator","xmds","doc","bgcolor","top","left","zindex","media","transInEl","transOutEl","transInDurationEl","transOutDurationEl","transInDirectionEl","transOutDirectionEl","retries","lastError","attempt","widgetCacheKey","cacheWidgetHtml","delay","resp","storeError","regionHTML","regionJS","mediaJS","m","mediaId","widgetUrl","transIn","transOut","iframeId","startFn","imageSrc","videoFilename","textUrl","audioSrc","audioId","audioLoop","audioVolume","pdfSrc","pdfContainerId","pdfDuration","MAX_BACKOFF_MS","CIRCUIT_BREAKER_THRESHOLD","DataConnectorManager","EventEmitter","connectors","connector","dataKey","intervalMs","err","keys","response","fetchWithRetry","contentType","previousData","baseMs","backoffMs","discoverLanIp","OFFLINE_DB_BASE","OFFLINE_DB_VERSION","OFFLINE_STORE","parseLayoutFile","f","openOfflineDb","cmsId","dbName","reject","req","db","PlayerCore","ip","CacheAnalyzer","label","store","settings","requiredFiles","finalDurations","durVersion","k","tx","cachedReg","cachedSchedule","layoutFiles","context","prefix","next","regResult","applyCmsLogLevel","rawKey","checkRf","checkSchedule","rfResult","files","purgeItems","currentLayouts","layoutOrder","_c","nextWindow","_e","_d","report","_f","_g","xmrUrl","xmrCmsKey","collectIntervalSeconds","newIntervalSeconds","seconds","requiredMediaIds","defaultFile","nextId","after","afterId","replayId","top2","fileType","isLayoutFile","isRequiredMedia","status","lat","lng","browser","apiKey","google","location","tags","TAG_CONFIG_MAP","tag","pipeIdx","value","configKey","position","res","providers","provider","changeMode","commandCode","command","commandString","success","inventoryXml","complete","reason","was","commandKey","commandDate","weatherJson","syncManager","durationEntries","mediaStatusEntries","pendingEntries","queuePos","fingerprint","pendingMedia","lines","end","missingTag","ready","missing","missingKey","final","xlfKey","prev","DownloadOverlay","__publicField","progress","downloads","percent","downloaded","total","bytes","kb","mb","enabled","getDefaultOverlayConfig","showDownloads","savedPref","TimelineOverlay","visible","onLayoutClick","target","offline","currentLayoutId","currentDuration","clickable","skippedCurrent","upcoming","totalCount","offlineBadge","maxVisible","rendered","durPad","idCol","endDate","timeRange","cursor","hover","durStr","remainingSec","nextStartMs","hasMissing","entryEndMs","startStr","endStr","borderLeft","color","missingList","hiddenIds","isTimelineVisible","showTimeline","saved","SetupOverlay","input","iframeDoc","form","errorEl","gateCancelBtn","STORE_PREFIX","PLAYER_BASE","RestClient","XmdsClient","ProtocolDetector","XmrWrapper","downloadManager","StatsCollector","formatStats","LogReporter","formatLogs","DisplaySettings","SyncManager","computeStagger","sdkVersions","PwaPlayer","registration","StoreClient","calculateChunkConfig","DownloadManager","widgetPath","debugOverlaysEnabled","overlayConfig","warnedHosts","host","created","certSpan","statusEl","hosts","connected","span","anchor","cacheModule","xmdsModule","scheduleModule","configModule","xmrModule","statsModule","displaySettingsModule","coreModule","rendererModule","syncModule","sysInfo","cfgTransport","transport","forceProtocol","client","registerLogSink","level","name","message","debugConfig","flushIntervalMs","batch","flushTimer","flushLogs","payload","buildDate","appVersion","versionParts","isElectron","electronVersion","chromeVersion","platform","displayName","isOffline","syncConfig","syncToken","persistable","merged","layoutMap","mappedId","numericId","choreo","staggerMs","staggerOpts","stagger","followerId","statsXml","ack","logsXml","_displayId","totalDisplays","topology","purgeFiles","groupedFiles","token","scheduledIds","cleared","msg","allFiles","cacheNames","newInterval","_settings","changes","method","path","search","body","port","iframes","attachIframeKeyForwarder","tryAttach","clone","mutations","node","controls","debugOverlays","setupKey","playbackControl","videoControls","allVideos","show","connectorData","extractMediaIdsFromXlf","layoutDependants","storeKeyFrom","xlfFiles","resources","mediaFiles","idToKeys","bareId","layoutMediaMap","xlfPromises","xlfFile","xlfText","headers","enqueueFile","builder","storeKey","mcResp","numChunks","fileDownload","fileSize","resourceBuilder","LayoutTaskBuilder","resourceTasks","BARRIER","claimed","nonScheduledIds","filenameToMediaId","depMap","filenames","xlfMediaIds","bareIds","nsId","nsMediaIds","matched","orderedTasks","unclaimed","_layout","layoutDur","pending","actionType","layoutCode","nextLayoutId","xlfBlob","requiredMedia","retryId","allMedia","videoMedia","bgFileId","mediaSaveAs","fetchPromises","storeId","processed","newRaw","dynamicVideoCount","allProbed","probedDuration","cleanup","configEl","version","text","sc","relay","stats","logs","logXml","code","details","fault","base64","electronResult","track","bitmap","__vite_default__","prevContain","containerRect","containerStyle","bgColor","bgImage","urlMatch","bgImg","elements","drawn","htmlEl","rect","iDoc","captureDiv","linkPromises","styleEl","linkEl","newLink","clonedBody","origImgs","imgNaturals","iframeCanvas","clonedDoc","cImg","style","dims","cW","cH","srcAspect","dstAspect","drawW","drawH","iframeRect","vid","vr","cr","srcW","srcH","intervalSecs","peer","startPlayer","player"],"ignoreList":[0],"sources":["../../../../node_modules/.pnpm/nanoevents@9.1.0/node_modules/nanoevents/index.js","../../../schedule/src/criteria.js","../../../schedule/src/timeline.js","../../../schedule/src/schedule.js","../../../schedule/src/interrupts.js","../../../schedule/src/overlays.js","../../../renderer/src/layout-pool.js","../../../renderer/src/renderer-lite.js","../../../renderer/src/layout.js","../../../core/src/data-connectors.js","../../../core/src/player-core.js","../../src/download-overlay.ts","../../src/timeline-overlay.ts","../../src/setup-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","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\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","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\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 * Single source of truth for XLF-based duration calculation.\n * Supports a 3-phase progressive refinement pipeline:\n * Phase 1 (ESTIMATE): parseLayoutDuration(xlf) — static duration from XLF\n * Phase 2 (PROBE): parseLayoutDuration(xlf, videoDurations) — refined with real video lengths\n * Phase 3 (LIVE UPDATE): renderer's updateLayoutDuration() — corrections from DURATION comments\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 * @param {Map<string, number>|null} [videoDurations=null] - Optional map of fileId → probed duration in seconds\n * @returns {{ duration: number, isDynamic: boolean }} Duration in seconds and whether any widget has useDuration=0\n */\nexport function parseLayoutDuration(xlfXml, videoDurations = null) {\n const doc = new DOMParser().parseFromString(xlfXml, 'text/xml');\n const layoutEl = doc.querySelector('layout');\n if (!layoutEl) return { duration: 60, isDynamic: false };\n\n // 1. Explicit layout duration attribute\n const explicit = parseInt(layoutEl.getAttribute('duration') || '0', 10);\n if (explicit > 0) return { duration: explicit, isDynamic: false };\n\n // 2. Calculate from widget durations (max region wins — regions play in parallel)\n let maxDuration = 0;\n let isDynamic = false;\n for (const regionEl of layoutEl.querySelectorAll('region')) {\n const regionType = regionEl.getAttribute('type');\n if (regionType === 'drawer') continue; // Drawers are action-triggered, not timed\n const isCanvas = regionType === 'canvas';\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 const fileId = mediaEl.getAttribute('fileId') || '';\n const probed = videoDurations?.get(fileId);\n\n let widgetDuration;\n if (probed !== undefined) {\n widgetDuration = probed; // Phase 2: probed video duration\n } else if (dur > 0 && useDuration !== 0) {\n widgetDuration = dur; // Explicit CMS duration\n } else {\n // Video with useDuration=0 means \"play to end\" — estimate 60s,\n // corrected later via recordLayoutDuration() when video metadata loads\n widgetDuration = 60;\n isDynamic = true;\n }\n\n if (isCanvas) {\n // Canvas regions play all widgets simultaneously — duration is max, not sum\n regionDuration = Math.max(regionDuration, widgetDuration);\n } else {\n regionDuration += widgetDuration;\n }\n }\n maxDuration = Math.max(maxDuration, regionDuration);\n }\n\n const duration = maxDuration > 0 ? maxDuration : 60;\n return { duration, isDynamic };\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 * 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 walking the pre-built schedule queue.\n *\n * The queue already has all constraints baked in (maxPlaysPerHour, priorities,\n * dayparting, default layout fills). This function simply cycles through it from\n * the current position, generating time-stamped entries for the overlay.\n *\n * @param {Array<{layoutId: string, duration: number}>} queue - Pre-built schedule queue from buildScheduleQueue()\n * @param {number} queuePosition - Current position in the queue (from schedule._queuePosition)\n * @param {Object} [options]\n * @param {Date} [options.from] - Start time (default: now)\n * @param {number} [options.hours] - Hours to project (default: 2)\n * @param {string} [options.defaultLayout] - Default layout file (to tag isDefault entries)\n * @param {Map<string, number>} [options.durations] - Live durations map (overrides queue entry durations with corrected values)\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(queue, queuePosition, 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 currentLayoutStartedAt = options.currentLayoutStartedAt || null;\n const defaultLayout = options.defaultLayout || null;\n const durations = options.durations || null;\n\n if (!queue || queue.length === 0) return [];\n\n const timeline = [];\n let currentTime = new Date(from);\n // queuePosition has already advanced past the currently-playing layout\n // (via popNextFromQueue), so entries here start from the NEXT layout.\n // The current layout's duration is passed directly to the overlay.\n let pos = queuePosition % queue.length;\n const maxEntries = 500;\n\n while (currentTime < to && timeline.length < maxEntries) {\n const entry = queue[pos];\n // Use live-corrected duration (from video metadata, etc.) if available,\n // otherwise fall back to the queue's baked-in duration\n let dur = (durations && durations.get(entry.layoutId)) || entry.duration;\n\n const endMs = currentTime.getTime() + dur * 1000;\n\n timeline.push({\n layoutFile: entry.layoutId,\n startTime: new Date(currentTime),\n endTime: new Date(endMs),\n duration: dur,\n isDefault: defaultLayout ? entry.layoutId === defaultLayout : false,\n });\n\n currentTime = new Date(endMs);\n pos = (pos + 1) % queue.length;\n }\n\n return timeline;\n}\n\n// ── LCM-based deterministic schedule queue ──────────────────────────────\n\n/**\n * Greatest common divisor (Euclidean algorithm).\n * @param {number} a\n * @param {number} b\n * @returns {number}\n */\nfunction gcd(a, b) {\n a = Math.abs(Math.round(a));\n b = Math.abs(Math.round(b));\n while (b) { [a, b] = [b, a % b]; }\n return a;\n}\n\n/**\n * Least common multiple of two integers.\n * @param {number} a\n * @param {number} b\n * @returns {number}\n */\nfunction lcm(a, b) {\n if (a === 0 || b === 0) return 0;\n return Math.abs(Math.round(a) * Math.round(b)) / gcd(a, b);\n}\n\n/**\n * LCM of an array of integers.\n * @param {number[]} values\n * @returns {number}\n */\nfunction lcmArray(values) {\n return values.reduce((acc, v) => lcm(acc, v), 1);\n}\n\n/**\n * Build a deterministic playback queue by simulating one LCM period.\n *\n * Uses getPlayableLayouts() (the same priority-fallback + rate-limit logic\n * that calculateTimeline uses) to simulate playback for one repeating cycle.\n * This ensures the queue matches the timeline overlay exactly: high-priority\n * rate-limited layouts get their slots, then lower-priority layouts fill gaps.\n *\n * @param {Array<{file: string, priority: number, maxPlaysPerHour: number}>} allLayouts\n * All time-active layouts from schedule.getAllLayoutsAtTime()\n * @param {Map<string, number>} durations\n * Map of layoutFile → duration in seconds\n * @param {Object} [options]\n * @param {string} [options.defaultLayout] - Default layout file (CMS fallback)\n * @param {number} [options.defaultDuration] - Fallback duration (default: 60)\n * @param {Set<string>} [options.dynamicLayouts] - Set of layout files that are dynamic (video, useDuration=0)\n * @returns {{ queue: Array<{layoutId: string, duration: number}>, periodSeconds: number }}\n */\nexport function buildScheduleQueue(allLayouts, durations, options = {}) {\n const {\n defaultLayout = null,\n defaultDuration = 60,\n } = options;\n\n if (allLayouts.length === 0 && !defaultLayout) {\n return { queue: [], periodSeconds: 0 };\n }\n\n // Build CMS duration lookup — use CMS-reported duration as fallback\n // when the durations map (from XLF parsing / video metadata) has no entry.\n const cmsDurations = new Map();\n for (const l of allLayouts) {\n if (l.duration > 0) cmsDurations.set(l.file, l.duration);\n }\n const getDuration = (file) => durations.get(file) || cmsDurations.get(file) || defaultDuration;\n\n // Step 1: Identify rate-limited layouts to calculate LCM period\n const rateLimited = allLayouts.filter(l => l.maxPlaysPerHour > 0);\n\n let periodSeconds;\n if (rateLimited.length > 0) {\n const intervals = rateLimited.map(l => Math.round(3600 / l.maxPlaysPerHour));\n periodSeconds = lcmArray(intervals);\n // Cap at 2 hours to prevent absurd periods\n if (periodSeconds > 7200) periodSeconds = 7200;\n } else {\n // No rate-limited layouts — single round-robin cycle\n const totalDuration = allLayouts.reduce((sum, l) => sum + getDuration(l.file), 0)\n + (defaultLayout && !allLayouts.some(l => l.file === defaultLayout)\n ? getDuration(defaultLayout)\n : 0);\n periodSeconds = totalDuration || defaultDuration;\n }\n\n // Step 2: Simulate playback for one period using getPlayableLayouts()\n const queue = [];\n const simPlays = new Map(); // file → [timestampMs] for rate-limit tracking\n let cursorMs = 0;\n const periodMs = periodSeconds * 1000;\n const maxEntries = 500; // safety cap\n\n while (cursorMs < periodMs && queue.length < maxEntries) {\n // Get playable layouts at current simulated time (priority fallback + rate limits)\n const playable = getPlayableLayouts(allLayouts, simPlays, cursorMs);\n\n if (playable.length === 0) {\n // All layouts exhausted — use default\n if (defaultLayout) {\n const dur = getDuration(defaultLayout);\n queue.push({ layoutId: defaultLayout, duration: dur });\n cursorMs += dur * 1000;\n } else {\n // No default — skip ahead 60s to avoid infinite loop\n cursorMs += 60000;\n }\n continue;\n }\n\n // Play all playable layouts in round-robin order (one each), then re-evaluate\n for (let i = 0; i < playable.length && cursorMs < periodMs && queue.length < maxEntries; i++) {\n const file = playable[i];\n const dur = getDuration(file);\n\n queue.push({ layoutId: file, duration: dur });\n\n // Record simulated play for rate-limit tracking\n if (!simPlays.has(file)) simPlays.set(file, []);\n simPlays.get(file).push(cursorMs);\n\n cursorMs += dur * 1000;\n\n // Re-evaluate after each play: if the playable set changed, break to outer loop\n const nextPlayable = getPlayableLayouts(allLayouts, simPlays, cursorMs);\n if (!arraysEqual(playable, nextPlayable)) break;\n }\n }\n\n // Handle edge case: no layouts and only default\n if (queue.length === 0 && defaultLayout) {\n const defDur = getDuration(defaultLayout);\n queue.push({ layoutId: defaultLayout, duration: defDur });\n }\n\n return { queue, periodSeconds };\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Schedule manager - determines which layouts to show\n */\n\nimport { createLogger } from '@xiboplayer/utils';\nimport { evaluateCriteria } from './criteria.js';\nimport { buildScheduleQueue } from './timeline.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 // Pre-calculated schedule queue (LCM-based deterministic timeline)\n this._scheduleQueue = null; // { queue: [{layoutId, duration}], periodSeconds }\n this._queuePosition = 0; // Current position in the queue\n this._queueLayoutSet = null; // Stringified active layout set (for invalidation)\n }\n\n /**\n * Update schedule from XMDS\n */\n setSchedule(schedule) {\n this.schedule = schedule;\n this._invalidateQueue();\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 * Get dependants map: layoutId → filenames that must be cached before that layout plays.\n * Includes both per-layout and global dependants.\n * Used by download manager to prioritize sub-playlist media alongside its parent layout.\n * @returns {Map<number, string[]>} layoutId → dependant filenames\n */\n getDependantsMap() {\n const map = new Map();\n if (!this.schedule) return map;\n\n const globalDeps = this.schedule.dependants || [];\n\n const addLayout = (layout) => {\n const id = parseInt(String(layout.file || layout.id).replace('.xlf', ''), 10);\n const deps = [...globalDeps, ...(layout.dependants || [])];\n if (deps.length > 0) map.set(id, deps);\n };\n\n if (this.schedule.layouts) {\n for (const layout of this.schedule.layouts) addLayout(layout);\n }\n if (this.schedule.campaigns) {\n for (const campaign of this.schedule.campaigns) {\n for (const layout of campaign.layouts) addLayout(layout);\n }\n }\n\n return map;\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 duration: layout.duration || 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 duration: layout.duration || 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 (or build) the deterministic schedule queue.\n *\n * Uses LCM-based even distribution to pre-calculate a repeating cycle where\n * each rate-limited layout plays at evenly spaced intervals and gaps are\n * filled by unlimited layouts and the CMS default.\n *\n * The queue is cached and only rebuilt when:\n * - The schedule changes (setSchedule)\n * - The active layout set changes (time boundary crossed)\n * - durations are updated\n *\n * @param {Map<string, number>} durations - layoutFile → duration in seconds\n * @param {Object} [options]\n * @param {Set<string>} [options.dynamicLayouts] - Set of layout files with useDuration=0\n * @returns {{ queue: Array<{layoutId: string, duration: number}>, periodSeconds: number }}\n */\n getScheduleQueue(durations, options = {}) {\n const allLayouts = this.getAllLayoutsAtTime(new Date());\n const layoutSetKey = allLayouts.map(l => `${l.file}:${l.priority}:${l.maxPlaysPerHour}`).sort().join('|');\n\n // Return cached queue if the active layout set hasn't changed\n if (this._scheduleQueue && this._queueLayoutSet === layoutSetKey) {\n return this._scheduleQueue;\n }\n\n const result = buildScheduleQueue(allLayouts, durations, {\n defaultLayout: this.schedule?.default || null,\n defaultDuration: 60,\n dynamicLayouts: options.dynamicLayouts || new Set(),\n });\n\n const prevLayoutSet = this._queueLayoutSet;\n this._scheduleQueue = result;\n this._queueLayoutSet = layoutSetKey;\n\n // Position only resets when we get a genuinely new set of layouts.\n // Duration corrections rebuild the queue but don't change position.\n if (prevLayoutSet !== layoutSetKey) {\n this._queuePosition = 0;\n }\n\n if (result.queue.length > 0) {\n log.info(`[Schedule] Built queue: ${result.queue.length} entries, period ${result.periodSeconds}s (pos ${this._queuePosition})`);\n log.info(`[Schedule] Queue: ${result.queue.map(e => `${e.layoutId}(${e.duration}s)`).join(' → ')}`);\n }\n\n return result;\n }\n\n /**\n * Pop the next entry from the schedule queue.\n * Wraps around at the end (the LCM period guarantees the pattern repeats).\n *\n * @param {Map<string, number>} durations - layoutFile → duration in seconds\n * @param {Object} [options]\n * @param {Set<string>} [options.dynamicLayouts] - Dynamic layout set\n * @returns {{ layoutId: string, duration: number } | null}\n */\n popNextFromQueue(durations, options = {}) {\n const { queue } = this.getScheduleQueue(durations, options);\n if (queue.length === 0) return null;\n\n const entry = queue[this._queuePosition % queue.length];\n this._queuePosition = (this._queuePosition + 1) % queue.length;\n return entry;\n }\n\n /**\n * Peek at the next entry in the schedule queue without advancing.\n *\n * @param {Map<string, number>} durations - layoutFile → duration in seconds\n * @param {Object} [options]\n * @returns {{ layoutId: string, duration: number } | null}\n */\n peekNextInQueue(durations, options = {}) {\n const { queue } = this.getScheduleQueue(durations, options);\n if (queue.length === 0) return null;\n return queue[this._queuePosition % queue.length];\n }\n\n /**\n * Peek at the entry after the current one (two positions ahead).\n * Used for preloading.\n *\n * @param {Map<string, number>} durations\n * @param {Object} [options]\n * @returns {{ layoutId: string, duration: number } | null}\n */\n peekAfterNext(durations, options = {}) {\n const { queue } = this.getScheduleQueue(durations, options);\n if (queue.length <= 1) return null;\n return queue[(this._queuePosition + 1) % queue.length];\n }\n\n /**\n * Invalidate the cached queue (called on schedule change, time boundaries, etc.)\n */\n _invalidateQueue() {\n this._scheduleQueue = null;\n // Keep _queueLayoutSet so getScheduleQueue() can detect whether the\n // layout set actually changed. Position only resets on new layouts.\n // Resetting on every setSchedule() caused the player to replay position 0\n // endlessly because collections happen more often than layout cycles.\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","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\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","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\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","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * LayoutPool - Maintains a pool of pre-built layout containers\n * for instant layout transitions.\n *\n * Instead of tearing down and rebuilding the DOM on every layout switch,\n * the pool keeps up to `maxSize` layout containers alive. The current\n * layout is marked 'hot' (visible); pre-loaded layouts are 'warm' (hidden).\n * When transitioning, visibility is swapped instantly - no DOM rebuild.\n *\n * Pool entries:\n * layoutId -> { container, layout, regions, blobUrls, mediaUrlCache, status, lastAccess }\n *\n * Status: 'hot' (currently visible) or 'warm' (preloaded, hidden)\n */\n\nimport { createLogger } from '@xiboplayer/utils';\n\nconst log = createLogger('LayoutPool');\n\nexport class LayoutPool {\n /**\n * @param {number} maxSize - Maximum number of layouts to keep in pool (default: 2)\n */\n constructor(maxSize = 2) {\n /** @type {Map<number, Object>} */\n this.layouts = new Map();\n this.maxSize = maxSize;\n /** @type {number|null} */\n this.hotLayoutId = null;\n }\n\n /**\n * Check if a layout is in the pool\n * @param {number} layoutId\n * @returns {boolean}\n */\n has(layoutId) {\n return this.layouts.has(layoutId);\n }\n\n /**\n * Get a pool entry\n * @param {number} layoutId\n * @returns {Object|undefined}\n */\n get(layoutId) {\n return this.layouts.get(layoutId);\n }\n\n /**\n * Add a layout entry to the pool.\n * If pool is full, evicts the least-recently-used warm entry.\n *\n * @param {number} layoutId\n * @param {Object} entry - Pool entry\n * @param {HTMLElement} entry.container - Layout container DOM element\n * @param {Object} entry.layout - Parsed layout object\n * @param {Map} entry.regions - Region map (regionId => region state)\n * @param {Set<string>} entry.blobUrls - Tracked blob URLs for this layout\n * @param {Map} [entry.mediaUrlCache] - Media URL cache (fileId => url)\n */\n add(layoutId, entry) {\n // If already in pool, update in place\n if (this.layouts.has(layoutId)) {\n const existing = this.layouts.get(layoutId);\n Object.assign(existing, entry);\n existing.lastAccess = Date.now();\n return;\n }\n\n // If pool is full, evict LRU warm entry\n if (this.layouts.size >= this.maxSize) {\n this.evictLRU();\n }\n\n entry.status = 'warm';\n entry.lastAccess = Date.now();\n this.layouts.set(layoutId, entry);\n log.info(`Added layout ${layoutId} to pool (size: ${this.layouts.size}/${this.maxSize})`);\n }\n\n /**\n * Mark a layout as active (visible).\n * The previous hot layout is demoted to warm.\n * @param {number} layoutId\n */\n setHot(layoutId) {\n // Demote previous hot layout to warm\n if (this.hotLayoutId !== null && this.layouts.has(this.hotLayoutId)) {\n this.layouts.get(this.hotLayoutId).status = 'warm';\n }\n\n if (this.layouts.has(layoutId)) {\n const entry = this.layouts.get(layoutId);\n entry.status = 'hot';\n entry.lastAccess = Date.now();\n }\n\n this.hotLayoutId = layoutId;\n }\n\n /**\n * Evict a specific layout from the pool.\n * Releases video/audio resources, revokes blob URLs, and removes the container from the DOM.\n * @param {number} layoutId\n */\n evict(layoutId) {\n const entry = this.layouts.get(layoutId);\n if (!entry) return;\n\n log.info(`Evicting layout ${layoutId} from pool`);\n\n // Stop any active region timers\n if (entry.regions) {\n for (const [regionId, region] of entry.regions) {\n if (region.timer) {\n clearTimeout(region.timer);\n region.timer = null;\n }\n }\n }\n\n // Release all video/audio resources BEFORE removing from DOM.\n // Removing a <video> with an active src leaks decoded frame buffers.\n if (entry.container) {\n LayoutPool.releaseMediaElements(entry.container);\n }\n\n // Revoke blob URLs\n if (entry.blobUrls && entry.blobUrls.size > 0) {\n entry.blobUrls.forEach(url => {\n URL.revokeObjectURL(url);\n });\n log.info(`Revoked ${entry.blobUrls.size} blob URLs for layout ${layoutId}`);\n }\n\n // Revoke media URL cache blob URLs\n if (entry.mediaUrlCache) {\n for (const [fileId, blobUrl] of entry.mediaUrlCache) {\n if (blobUrl && typeof blobUrl === 'string' && blobUrl.startsWith('blob:')) {\n URL.revokeObjectURL(blobUrl);\n }\n }\n }\n\n // Remove container from DOM\n if (entry.container && entry.container.parentNode) {\n entry.container.remove();\n }\n\n this.layouts.delete(layoutId);\n\n // Clear hot reference if this was the hot layout\n if (this.hotLayoutId === layoutId) {\n this.hotLayoutId = null;\n }\n }\n\n /**\n * Release all video and audio elements inside a container.\n * Must be called BEFORE removing the container from the DOM —\n * browsers keep decoded frame buffers alive for detached <video> elements\n * that still have a src.\n *\n * @param {HTMLElement} container\n */\n static releaseMediaElements(container) {\n let videoCount = 0;\n let hlsCount = 0;\n\n container.querySelectorAll('video').forEach(v => {\n // Destroy hls.js instance if attached (stored by renderVideo)\n if (v._hlsInstance) {\n v._hlsInstance.destroy();\n v._hlsInstance = null;\n hlsCount++;\n }\n // Stop MediaStream tracks (webcam/mic)\n if (v._mediaStream) {\n v._mediaStream.getTracks().forEach(t => t.stop());\n v._mediaStream = null;\n v.srcObject = null;\n }\n v.pause();\n v.removeAttribute('src');\n v.load(); // Forces browser to release decoded buffers\n videoCount++;\n });\n\n container.querySelectorAll('audio').forEach(a => {\n a.pause();\n a.removeAttribute('src');\n a.load();\n });\n\n // Destroy PDF documents and release GPU canvas backing stores\n container.querySelectorAll('.pdf-widget').forEach(el => {\n if (el._pdfDestroy) el._pdfDestroy();\n });\n\n if (videoCount > 0) {\n log.info(`Released ${videoCount} video(s)${hlsCount ? ` (${hlsCount} HLS)` : ''}`);\n }\n }\n\n /**\n * Evict the least-recently-used warm entry.\n * Only warm entries are eligible for eviction (never the hot layout).\n */\n evictLRU() {\n let oldest = null;\n let oldestTime = Infinity;\n\n for (const [id, entry] of this.layouts) {\n if (entry.status === 'warm' && entry.lastAccess < oldestTime) {\n oldest = id;\n oldestTime = entry.lastAccess;\n }\n }\n\n if (oldest !== null) {\n this.evict(oldest);\n }\n }\n\n /**\n * Clear all warm (preloaded) entries, keeping the hot layout.\n * @returns {number} Number of entries cleared\n */\n clearWarm() {\n let count = 0;\n const warmIds = [];\n\n for (const [id, entry] of this.layouts) {\n if (entry.status === 'warm') {\n warmIds.push(id);\n }\n }\n\n for (const id of warmIds) {\n this.evict(id);\n count++;\n }\n\n if (count > 0) {\n log.info(`Cleared ${count} warm layout(s) from pool`);\n }\n\n return count;\n }\n\n /**\n * Clear warm entries NOT in the given set of layout IDs.\n * Keeps warm entries that are still scheduled.\n * @param {Set<number>} keepIds - Layout IDs to keep\n * @returns {number} Number of entries cleared\n */\n clearWarmNotIn(keepIds) {\n let count = 0;\n const evictIds = [];\n\n for (const [id, entry] of this.layouts) {\n if (entry.status === 'warm' && !keepIds.has(id)) {\n evictIds.push(id);\n }\n }\n\n for (const id of evictIds) {\n this.evict(id);\n count++;\n }\n\n if (count > 0) {\n log.info(`Cleared ${count} warm layout(s) no longer in schedule`);\n }\n\n return count;\n }\n\n /**\n * Get the most recently added layout ID.\n * @returns {number|undefined}\n */\n getLatest() {\n let latest;\n for (const id of this.layouts.keys()) {\n latest = id;\n }\n return latest;\n }\n\n /**\n * Clear all entries (both hot and warm).\n */\n clear() {\n const ids = Array.from(this.layouts.keys());\n for (const id of ids) {\n this.evict(id);\n }\n this.hotLayoutId = null;\n }\n\n /**\n * Get the number of entries in the pool.\n * @returns {number}\n */\n get size() {\n return this.layouts.size;\n }\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * RendererLite - Lightweight XLF Layout Renderer\n *\n * A standalone, reusable JavaScript library for rendering Xibo Layout Format (XLF) files.\n * Provides layout rendering without dependencies on XLR, suitable for any platform.\n *\n * Features:\n * - Parse XLF XML layout files\n * - Create region DOM elements with positioning\n * - Render widgets (text, image, video, audio, PDF, webpage)\n * - Handle widget duration timers\n * - Apply CSS transitions (fade, fly)\n * - Event emitter for lifecycle hooks\n * - Manage layout lifecycle\n *\n * Usage pattern (similar to xmr-wrapper.js):\n *\n * ```javascript\n * import { RendererLite } from './renderer-lite.js';\n *\n * const container = document.getElementById('player-container');\n * const renderer = new RendererLite({ cmsUrl: '...', hardwareKey: '...' }, container);\n *\n * // Listen to events\n * renderer.on('layoutStart', (layoutId) => console.log('Layout started:', layoutId));\n * renderer.on('layoutEnd', (layoutId) => console.log('Layout ended:', layoutId));\n * renderer.on('widgetStart', (widget) => console.log('Widget started:', widget));\n * renderer.on('widgetEnd', (widget) => console.log('Widget ended:', widget));\n * renderer.on('error', (error) => console.error('Error:', error));\n *\n * // Render a layout\n * await renderer.renderLayout(layoutXml, duration);\n *\n * // Stop current layout\n * renderer.stopCurrentLayout();\n *\n * // Cleanup\n * renderer.cleanup();\n * ```\n */\n\nimport { createNanoEvents } from 'nanoevents';\nimport { createLogger, isDebug, PLAYER_API } from '@xiboplayer/utils';\nimport { parseLayoutDuration } from '@xiboplayer/schedule';\nimport { LayoutPool } from './layout-pool.js';\n\n/**\n * Transition utilities for widget animations\n */\nexport const Transitions = {\n /**\n * Apply fade in transition\n */\n fadeIn(element, duration) {\n const keyframes = [\n { opacity: 0 },\n { opacity: 1 }\n ];\n const timing = {\n duration: duration,\n easing: 'linear',\n fill: 'forwards'\n };\n return element.animate(keyframes, timing);\n },\n\n /**\n * Apply fade out transition\n */\n fadeOut(element, duration) {\n const keyframes = [\n { opacity: 1 },\n { opacity: 0 }\n ];\n const timing = {\n duration: duration,\n easing: 'linear',\n fill: 'forwards'\n };\n return element.animate(keyframes, timing);\n },\n\n /**\n * Get fly keyframes based on compass direction\n */\n getFlyKeyframes(direction, width, height, isIn) {\n const dirMap = {\n 'N': { x: 0, y: isIn ? -height : height },\n 'NE': { x: isIn ? width : -width, y: isIn ? -height : height },\n 'E': { x: isIn ? width : -width, y: 0 },\n 'SE': { x: isIn ? width : -width, y: isIn ? height : -height },\n 'S': { x: 0, y: isIn ? height : -height },\n 'SW': { x: isIn ? -width : width, y: isIn ? height : -height },\n 'W': { x: isIn ? -width : width, y: 0 },\n 'NW': { x: isIn ? -width : width, y: isIn ? -height : height }\n };\n\n const offset = dirMap[direction] || dirMap['N'];\n\n if (isIn) {\n return {\n from: {\n transform: `translate(${offset.x}px, ${offset.y}px)`,\n opacity: 0\n },\n to: {\n transform: 'translate(0, 0)',\n opacity: 1\n }\n };\n } else {\n return {\n from: {\n transform: 'translate(0, 0)',\n opacity: 1\n },\n to: {\n transform: `translate(${offset.x}px, ${offset.y}px)`,\n opacity: 0\n }\n };\n }\n },\n\n /**\n * Apply fly in transition\n */\n flyIn(element, duration, direction, regionWidth, regionHeight) {\n const keyframes = this.getFlyKeyframes(direction, regionWidth, regionHeight, true);\n const timing = {\n duration: duration,\n easing: 'ease-out',\n fill: 'forwards'\n };\n return element.animate([keyframes.from, keyframes.to], timing);\n },\n\n /**\n * Apply fly out transition\n */\n flyOut(element, duration, direction, regionWidth, regionHeight) {\n const keyframes = this.getFlyKeyframes(direction, regionWidth, regionHeight, false);\n const timing = {\n duration: duration,\n easing: 'ease-in',\n fill: 'forwards'\n };\n return element.animate([keyframes.from, keyframes.to], timing);\n },\n\n /**\n * Apply transition based on type\n */\n apply(element, transitionConfig, isIn, regionWidth, regionHeight) {\n if (!transitionConfig || !transitionConfig.type) {\n return null;\n }\n\n const type = transitionConfig.type.toLowerCase();\n const duration = transitionConfig.duration || 1000;\n const direction = transitionConfig.direction || 'N';\n\n switch (type) {\n case 'fade':\n return isIn ? this.fadeIn(element, duration) : this.fadeOut(element, duration);\n case 'fadein':\n return isIn ? this.fadeIn(element, duration) : null;\n case 'fadeout':\n return isIn ? null : this.fadeOut(element, duration);\n case 'fly':\n return isIn\n ? this.flyIn(element, duration, direction, regionWidth, regionHeight)\n : this.flyOut(element, duration, direction, regionWidth, regionHeight);\n case 'flyin':\n return isIn ? this.flyIn(element, duration, direction, regionWidth, regionHeight) : null;\n case 'flyout':\n return isIn ? null : this.flyOut(element, duration, direction, regionWidth, regionHeight);\n default:\n return null;\n }\n }\n};\n\n/**\n * RendererLite - Lightweight XLF renderer\n */\nexport class RendererLite {\n /**\n * @param {Object} config - Player configuration\n * @param {string} config.cmsUrl - CMS base URL\n * @param {string} config.hardwareKey - Display hardware key\n * @param {HTMLElement} container - DOM container for rendering\n * @param {Object} options - Renderer options\n * @param {Map<string,string>} [options.fileIdToSaveAs] - Map from numeric file ID to storedAs filename (for layout backgrounds)\n * @param {Function} options.getWidgetHtml - Function to get widget HTML (layoutId, regionId, widgetId) => html\n */\n constructor(config, container, options = {}) {\n this.config = config;\n this.container = container;\n this.options = options;\n\n // Logger with configurable level\n this.log = createLogger('RendererLite', options.logLevel);\n\n // Event emitter for lifecycle hooks\n this.emitter = createNanoEvents();\n\n // State\n this.currentLayout = null;\n this.currentLayoutId = null;\n this._preloadingLayoutId = null; // Set during preload for blob URL tracking\n this.regions = new Map(); // regionId => { element, widgets, currentIndex, timer }\n this.layoutTimer = null;\n this.layoutEndEmitted = false; // Prevents double layoutEnd on stop after timer\n this._deferredTimerLayoutId = null; // Set when timer is deferred for dynamic layouts\n this._deferredTimerFallback = null; // Safety timeout: starts layout timer if metadata never arrives\n this._paused = false;\n this._layoutTimerStartedAt = null; // Date.now() when layout timer started\n this._layoutTimerDurationMs = null; // Total layout duration in ms\n this.widgetTimers = new Map(); // widgetId => timer\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 // Widget lifecycle tracking — ensures symmetric start/stop\n this._startedWidgets = new Set(); // \"regionId:widgetIndex\" keys\n\n // Layout preload pool (2-layout pool for instant transitions)\n this.layoutPool = new LayoutPool(2);\n this.preloadTimer = null;\n this._preloadRetryTimer = null;\n\n // Setup container styles\n this.setupContainer();\n\n // Interactive Control (XIC) event handlers\n this.emitter.on('interactiveTrigger', (data) => this._handleInteractiveTrigger(data));\n this.emitter.on('widgetExpire', (data) => this._handleWidgetExpire(data));\n this.emitter.on('widgetExtendDuration', (data) => this._handleWidgetExtendDuration(data));\n this.emitter.on('widgetSetDuration', (data) => this._handleWidgetSetDuration(data));\n\n this.log.info('Initialized');\n }\n\n /**\n * Setup container element\n */\n setupContainer() {\n this.container.style.position = 'relative';\n this.container.style.width = '100%';\n this.container.style.height = '100vh'; // Use viewport height, not percentage\n this.container.style.overflow = 'hidden';\n\n // Watch for container resize to rescale layout (debounced to avoid spam)\n this._resizeSuppressed = false;\n if (typeof ResizeObserver !== 'undefined') {\n let resizeTimer = null;\n this.resizeObserver = new ResizeObserver(() => {\n if (this._resizeSuppressed) return;\n if (resizeTimer) clearTimeout(resizeTimer);\n resizeTimer = setTimeout(() => this.rescaleRegions(), 150);\n });\n this.resizeObserver.observe(this.container);\n }\n\n // Create overlay container for overlay layouts (higher z-index than main content)\n this.overlayContainer = document.createElement('div');\n this.overlayContainer.id = 'overlay-container';\n this.overlayContainer.style.position = 'absolute';\n this.overlayContainer.style.top = '0';\n this.overlayContainer.style.left = '0';\n this.overlayContainer.style.width = '100%';\n this.overlayContainer.style.height = '100%';\n this.overlayContainer.style.zIndex = '1000'; // Above main layout (z-index 0-999)\n this.overlayContainer.style.pointerEvents = 'none'; // Don't block clicks on main layout\n this.container.appendChild(this.overlayContainer);\n }\n\n /**\n * Calculate scale factor to fit layout into container\n * Centers the layout and scales regions proportionally.\n * @param {Object} layout - Parsed layout with width/height\n */\n calculateScale(layout) {\n const screenWidth = this.container.clientWidth;\n const screenHeight = this.container.clientHeight;\n\n if (!screenWidth || !screenHeight) return;\n\n const scaleX = screenWidth / layout.width;\n const scaleY = screenHeight / layout.height;\n this.scaleFactor = Math.min(scaleX, scaleY);\n this.offsetX = (screenWidth - layout.width * this.scaleFactor) / 2;\n this.offsetY = (screenHeight - layout.height * this.scaleFactor) / 2;\n\n this.log.info(`Scale: ${this.scaleFactor.toFixed(3)} (${layout.width}x${layout.height} → ${screenWidth}x${screenHeight}, offset ${Math.round(this.offsetX)},${Math.round(this.offsetY)})`);\n }\n\n /**\n * Apply scale to a region element\n * @param {HTMLElement} regionEl - Region DOM element\n * @param {Object} regionConfig - Region config with left, top, width, height\n */\n applyRegionScale(regionEl, regionConfig) {\n const sf = this.scaleFactor;\n regionEl.style.left = `${regionConfig.left * sf + this.offsetX}px`;\n regionEl.style.top = `${regionConfig.top * sf + this.offsetY}px`;\n regionEl.style.width = `${regionConfig.width * sf}px`;\n regionEl.style.height = `${regionConfig.height * sf}px`;\n }\n\n /**\n * Reapply scale to all current regions (e.g., on window resize)\n */\n rescaleRegions() {\n if (!this.currentLayout) return;\n\n this.calculateScale(this.currentLayout);\n\n for (const [regionId, region] of this.regions) {\n this.applyRegionScale(region.element, region.config);\n // Update region dimensions for transition calculations\n region.width = region.config.width * this.scaleFactor;\n region.height = region.config.height * this.scaleFactor;\n }\n\n // Rescale active overlays too\n for (const [overlayId, overlay] of this.activeOverlays) {\n this.calculateScale(overlay.layout);\n for (const [regionId, region] of overlay.regions) {\n this.applyRegionScale(region.element, region.config);\n region.width = region.config.width * this.scaleFactor;\n region.height = region.config.height * this.scaleFactor;\n }\n }\n }\n\n /**\n * Event emitter interface (like XMR wrapper)\n */\n on(event, callback) {\n 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 regionType = regionEl.getAttribute('type') || null;\n const region = {\n id: regionEl.getAttribute('id'),\n width: parseInt(regionEl.getAttribute('width') || '0'),\n height: parseInt(regionEl.getAttribute('height') || '0'),\n top: parseInt(regionEl.getAttribute('top') || '0'),\n left: parseInt(regionEl.getAttribute('left') || '0'),\n zindex: parseInt(regionEl.getAttribute('zindex') || (isDrawer ? '2000' : '0')),\n enableStat: regionEl.getAttribute('enableStat') !== '0',\n actions: this.parseActions(regionEl),\n exitTransition: null,\n transitionType: null, // Region-level default widget transition type\n transitionDuration: null,\n transitionDirection: null,\n loop: true, // Default: cycle widgets. Spec: loop=0 means single media stays visible\n isDrawer,\n isCanvas: regionType === 'canvas', // Canvas regions render all widgets simultaneously\n widgets: []\n };\n\n // Parse region-level options (exit transitions, loop)\n // Use direct children only to avoid matching <options> inside <media>\n const regionOptionsEl = Array.from(regionEl.children).find(el => el.tagName === 'options');\n if (regionOptionsEl) {\n const exitTransType = regionOptionsEl.querySelector('exitTransType');\n if (exitTransType && exitTransType.textContent) {\n const exitTransDuration = regionOptionsEl.querySelector('exitTransDuration');\n const exitTransDirection = regionOptionsEl.querySelector('exitTransDirection');\n region.exitTransition = {\n type: exitTransType.textContent,\n duration: parseInt((exitTransDuration && exitTransDuration.textContent) || '1000'),\n direction: (exitTransDirection && exitTransDirection.textContent) || 'N'\n };\n }\n\n // Region loop option: 0 = single media stays on screen, 1 = cycles (default)\n const loopEl = regionOptionsEl.querySelector('loop');\n if (loopEl) {\n region.loop = loopEl.textContent !== '0';\n }\n\n // Region-level default transition for widgets (applied if widget has no own transition)\n const transType = regionOptionsEl.querySelector('transitionType');\n if (transType && transType.textContent) {\n region.transitionType = transType.textContent;\n const transDuration = regionOptionsEl.querySelector('transitionDuration');\n const transDirection = regionOptionsEl.querySelector('transitionDirection');\n region.transitionDuration = parseInt((transDuration && transDuration.textContent) || '1000');\n region.transitionDirection = (transDirection && transDirection.textContent) || 'N';\n }\n }\n\n // Parse media/widgets (use direct children to avoid nested matches)\n for (const child of regionEl.children) {\n if (child.tagName !== 'media') continue;\n const widget = this.parseWidget(child);\n region.widgets.push(widget);\n }\n\n // Auto-detect canvas from CMS \"global\" widget (CMS bundles canvas sub-widgets\n // into a single type=\"global\" media element in the XLF)\n if (!region.isCanvas && region.widgets.some(w => w.type === 'global')) {\n region.isCanvas = true;\n }\n\n layout.regions.push(region);\n\n if (isDrawer) {\n this.log.info(`Parsed drawer: id=${region.id} with ${region.widgets.length} widgets`);\n }\n\n if (region.isCanvas) {\n this.log.info(`Parsed canvas region: id=${region.id} with ${region.widgets.length} widgets (all render simultaneously)`);\n }\n }\n\n // Calculate layout duration if not specified (duration=0)\n // Uses shared parseLayoutDuration() — single source of truth for XLF-based duration calc\n if (layout.duration === 0) {\n const { duration, isDynamic } = parseLayoutDuration(xlfXml);\n layout.duration = duration;\n layout.isDynamic = isDynamic;\n this.log.info(`Calculated layout duration: ${layout.duration}s (not specified in XLF)${isDynamic ? ' [dynamic — has useDuration=0 video]' : ''}`);\n }\n\n return layout;\n }\n\n /**\n * Parse widget from media element\n * @param {Element} mediaEl - Media XML element\n * @returns {Object} Widget config\n */\n parseWidget(mediaEl) {\n const type = mediaEl.getAttribute('type');\n const duration = parseInt(mediaEl.getAttribute('duration') || '10');\n const useDuration = parseInt(mediaEl.getAttribute('useDuration') || '1');\n const id = mediaEl.getAttribute('id');\n const fileId = mediaEl.getAttribute('fileId'); // Media library file ID\n\n // Parse options\n const options = {};\n const optionsEl = mediaEl.querySelector('options');\n if (optionsEl) {\n for (const child of optionsEl.children) {\n options[child.tagName] = child.textContent;\n }\n }\n\n // Parse raw content\n const rawEl = mediaEl.querySelector('raw');\n const raw = rawEl ? rawEl.textContent : '';\n\n // Parse transitions\n const transitions = {\n in: null,\n out: null\n };\n\n if (options.transIn) {\n transitions.in = {\n type: options.transIn,\n duration: parseInt(options.transInDuration || '1000'),\n direction: options.transInDirection || 'N'\n };\n }\n\n if (options.transOut) {\n transitions.out = {\n type: options.transOut,\n duration: parseInt(options.transOutDuration || '1000'),\n direction: options.transOutDirection || 'N'\n };\n }\n\n // Parse widget-level actions\n const actions = this.parseActions(mediaEl);\n\n // Parse audio overlay nodes (<audio> child elements on the widget)\n // Spec format: <audio><uri volume=\"\" loop=\"\" mediaId=\"\">filename.mp3</uri></audio>\n // Also supports flat format: <audio mediaId=\"\" uri=\"\" volume=\"\" loop=\"\">\n const audioNodes = [];\n for (const child of mediaEl.children) {\n if (child.tagName.toLowerCase() === 'audio') {\n const uriEl = child.querySelector('uri');\n if (uriEl) {\n // Spec format: attributes on <uri>, filename as text content\n audioNodes.push({\n mediaId: uriEl.getAttribute('mediaId') || null,\n uri: uriEl.textContent || '',\n volume: parseInt(uriEl.getAttribute('volume') || '100'),\n loop: uriEl.getAttribute('loop') === '1'\n });\n } else {\n // Flat format fallback: attributes directly on <audio>\n audioNodes.push({\n mediaId: child.getAttribute('mediaId') || null,\n uri: child.getAttribute('uri') || '',\n volume: parseInt(child.getAttribute('volume') || '100'),\n loop: child.getAttribute('loop') === '1'\n });\n }\n }\n }\n\n // Parse commands on media (shell/native commands triggered on widget start)\n // Spec: <commands><command commandCode=\"code\" commandString=\"args\"/></commands>\n const commands = [];\n const commandsEl = Array.from(mediaEl.children).find(el => el.tagName === 'commands');\n if (commandsEl) {\n for (const cmdEl of commandsEl.children) {\n if (cmdEl.tagName === 'command') {\n commands.push({\n commandCode: cmdEl.getAttribute('commandCode') || '',\n commandString: cmdEl.getAttribute('commandString') || ''\n });\n }\n }\n }\n\n // Sub-playlist attributes (widgets grouped by parentWidgetId)\n const parentWidgetId = mediaEl.getAttribute('parentWidgetId') || null;\n const displayOrder = parseInt(mediaEl.getAttribute('displayOrder') || '0');\n const cyclePlayback = mediaEl.getAttribute('cyclePlayback') === '1';\n const playCount = parseInt(mediaEl.getAttribute('playCount') || '0');\n const isRandom = mediaEl.getAttribute('isRandom') === '1';\n\n // Media expiry dates (per-widget time-gating within a layout)\n const fromDt = mediaEl.getAttribute('fromDt') || mediaEl.getAttribute('fromdt') || null;\n const toDt = mediaEl.getAttribute('toDt') || mediaEl.getAttribute('todt') || null;\n\n // Render mode: 'native' (player renders directly) or 'html' (use GetResource)\n const render = mediaEl.getAttribute('render') || null;\n\n return {\n type,\n duration,\n useDuration, // Whether to use specified duration (1) or media length (0)\n id,\n fileId, // Media library file ID for cache lookup\n render, // 'native' or 'html' — null means use type-based dispatch\n fromDt, // Widget valid-from date (Y-m-d H:i:s)\n toDt, // Widget valid-to date (Y-m-d H:i:s)\n enableStat: mediaEl.getAttribute('enableStat') !== '0', // absent or \"1\" = enabled\n webhookUrl: options.webhookUrl || null,\n options,\n raw,\n transitions,\n actions,\n audioNodes, // Audio overlays attached to this widget\n commands, // Shell commands triggered on widget start\n parentWidgetId,\n displayOrder,\n cyclePlayback,\n playCount,\n isRandom\n };\n }\n\n /**\n * Track blob URL for lifecycle management\n * @param {string} blobUrl - Blob URL to track\n */\n trackBlobUrl(blobUrl) {\n const layoutId = this._preloadingLayoutId || this.currentLayoutId || 0;\n\n if (!layoutId) {\n this.log.warn('trackBlobUrl called without currentLayoutId, tracking under key 0');\n }\n\n if (!this.layoutBlobUrls.has(layoutId)) {\n this.layoutBlobUrls.set(layoutId, new Set());\n }\n\n this.layoutBlobUrls.get(layoutId).add(blobUrl);\n }\n\n /**\n * Revoke all blob URLs for a specific layout\n * @param {number} layoutId - Layout ID\n */\n revokeBlobUrlsForLayout(layoutId) {\n const blobUrls = this.layoutBlobUrls.get(layoutId);\n if (blobUrls) {\n blobUrls.forEach(url => {\n URL.revokeObjectURL(url);\n });\n this.layoutBlobUrls.delete(layoutId);\n this.log.info(`Revoked ${blobUrls.size} blob URLs for layout ${layoutId}`);\n }\n }\n\n /**\n * Update layout duration based on actual widget durations\n * Called when video metadata loads and we discover actual duration\n */\n updateLayoutDuration() {\n if (!this.currentLayout) return;\n\n // Calculate maximum region duration\n let maxRegionDuration = 0;\n\n for (const region of this.currentLayout.regions) {\n if (region.isDrawer) continue;\n let regionDuration = 0;\n\n for (const widget of region.widgets) {\n if (widget.duration > 0) {\n regionDuration += widget.duration;\n }\n }\n\n maxRegionDuration = Math.max(maxRegionDuration, regionDuration);\n }\n\n // Update layout duration if recalculated value differs.\n // Both upgrades (video metadata revealing longer duration) and downgrades\n // (DURATION comment correcting an overestimate) are legitimate.\n if (maxRegionDuration > 0 && maxRegionDuration !== this.currentLayout.duration) {\n const oldDuration = this.currentLayout.duration;\n this.currentLayout.duration = maxRegionDuration;\n\n this.log.info(`Layout duration updated: ${oldDuration}s → ${maxRegionDuration}s (based on video metadata)`);\n const final_ = !this._hasUnprobedVideos();\n this.emit('layoutDurationUpdated', this.currentLayoutId, maxRegionDuration, final_);\n\n // Deferred timer: video metadata arrived, start the timer now\n if (this._deferredTimerLayoutId === this.currentLayoutId && !this.layoutTimer) {\n if (this._hasUnprobedVideos()) {\n this.log.info(`Layout duration updated to ${maxRegionDuration}s but still has unprobed videos — keeping timer deferred`);\n } else {\n // Cancel safety fallback — metadata arrived in time\n if (this._deferredTimerFallback) {\n clearTimeout(this._deferredTimerFallback);\n this._deferredTimerFallback = null;\n }\n const elapsed = Date.now() - (this._layoutTimerStartedAt || Date.now());\n const remainingMs = Math.max(1000, maxRegionDuration * 1000 - elapsed);\n this._deferredTimerLayoutId = null;\n this._layoutTimerDurationMs = remainingMs;\n this.layoutTimer = setTimeout(() => {\n this.log.info(`Layout ${this.currentLayoutId} duration expired (${this.currentLayout.duration}s)`);\n if (this.currentLayoutId) {\n this.layoutEndEmitted = true;\n this.emit('layoutEnd', this.currentLayoutId);\n }\n }, remainingMs);\n this.log.info(`All video durations resolved — deferred timer started: ${(remainingMs / 1000).toFixed(1)}s remaining (waited ${(elapsed / 1000).toFixed(1)}s for metadata)`);\n }\n } else if (this.layoutTimer) {\n // Reset layout timer with REMAINING time — not full duration.\n clearTimeout(this.layoutTimer);\n\n const elapsed = Date.now() - (this._layoutTimerStartedAt || Date.now());\n const remainingMs = Math.max(1000, this.currentLayout.duration * 1000 - elapsed);\n this.layoutTimer = setTimeout(() => {\n this.log.info(`Layout ${this.currentLayoutId} duration expired (${this.currentLayout.duration}s)`);\n if (this.currentLayoutId) {\n this.layoutEndEmitted = true;\n this.emit('layoutEnd', this.currentLayoutId);\n }\n }, remainingMs);\n\n this.log.info(`Layout timer adjusted to ${(remainingMs / 1000).toFixed(1)}s remaining (elapsed ${(elapsed / 1000).toFixed(1)}s of ${this.currentLayout.duration}s)`);\n } else {\n this.log.info(`Layout duration updated to ${maxRegionDuration}s (timer not yet started, will use new value)`);\n }\n\n // Reschedule preload timer — the initial preload was based on the old\n // duration estimate (e.g. 45s for 60s default). With the real duration\n // (e.g. 375s), the preload should fire much later so that schedule\n // cooldowns (maxPlaysPerHour) have time to expire.\n this._scheduleNextLayoutPreload(this.currentLayout);\n }\n }\n\n // ── Interactive Actions ──────────────────────────────────────────────\n\n /**\n * Attach interactive action event listeners for a layout.\n * Binds touch/click on region/widget elements and a single document keydown handler.\n */\n attachActionListeners(layout) {\n const allKeyboardActions = [];\n let touchActionCount = 0;\n\n // Layout-level actions (attached to the main container)\n for (const action of (layout.actions || [])) {\n if (action.triggerType === 'touch') {\n this.attachTouchAction(this.container, action, null, null);\n touchActionCount++;\n } else if (action.triggerType?.startsWith('keyboard:')) {\n allKeyboardActions.push(action);\n }\n }\n\n for (const regionConfig of layout.regions) {\n const region = this.regions.get(regionConfig.id);\n if (!region) continue;\n\n // Region-level actions\n for (const action of (regionConfig.actions || [])) {\n if (action.triggerType === 'touch') {\n this.attachTouchAction(region.element, action, regionConfig.id, null);\n touchActionCount++;\n } else if (action.triggerType.startsWith('keyboard:')) {\n allKeyboardActions.push(action);\n }\n }\n\n // Widget-level actions\n for (const widget of regionConfig.widgets) {\n if (!widget.actions || widget.actions.length === 0) continue;\n const widgetEl = region.widgetElements.get(widget.id);\n if (!widgetEl) continue;\n\n for (const action of widget.actions) {\n if (action.triggerType === 'touch') {\n this.attachTouchAction(widgetEl, action, regionConfig.id, widget.id);\n touchActionCount++;\n } else if (action.triggerType.startsWith('keyboard:')) {\n allKeyboardActions.push(action);\n }\n }\n }\n }\n\n this.setupKeyboardListener(allKeyboardActions);\n\n if (touchActionCount > 0 || allKeyboardActions.length > 0) {\n this.log.info(`Actions attached: ${touchActionCount} touch, ${allKeyboardActions.length} keyboard`);\n }\n }\n\n /**\n * Attach a click listener to an element for a touch-triggered action.\n */\n attachTouchAction(element, action, regionId, widgetId) {\n element.style.cursor = 'pointer';\n\n const handler = (event) => {\n event.stopPropagation();\n const source = widgetId ? `widget ${widgetId}` : `region ${regionId}`;\n this.log.info(`Touch action fired on ${source}: ${action.actionType}`);\n\n this.emit('action-trigger', {\n actionType: action.actionType,\n triggerType: 'touch',\n triggerCode: action.triggerCode,\n layoutCode: action.layoutCode,\n targetId: action.targetId,\n commandCode: action.commandCode,\n source: { regionId, widgetId }\n });\n };\n\n element.addEventListener('click', handler);\n if (!element._actionHandlers) element._actionHandlers = [];\n element._actionHandlers.push(handler);\n }\n\n /**\n * Setup document-level keyboard listener for keyboard-triggered actions.\n */\n setupKeyboardListener(keyboardActions) {\n this.removeKeyboardListener();\n this._keyboardActions = keyboardActions;\n if (keyboardActions.length === 0) return;\n\n this._keydownHandler = (event) => {\n const pressedKey = event.key;\n for (const action of this._keyboardActions) {\n const keycode = action.triggerType.substring('keyboard:'.length);\n if (pressedKey === keycode) {\n this.log.info(`Keyboard action (key: ${pressedKey}): ${action.actionType}`);\n this.emit('action-trigger', {\n actionType: action.actionType,\n triggerType: action.triggerType,\n triggerCode: action.triggerCode,\n layoutCode: action.layoutCode,\n targetId: action.targetId,\n commandCode: action.commandCode,\n source: { key: pressedKey }\n });\n break;\n }\n }\n };\n\n document.addEventListener('keydown', this._keydownHandler);\n }\n\n /** Remove the document-level keyboard listener */\n removeKeyboardListener() {\n if (this._keydownHandler) {\n document.removeEventListener('keydown', this._keydownHandler);\n this._keydownHandler = null;\n }\n this._keyboardActions = [];\n }\n\n /** Remove all action listeners (touch + keyboard) */\n removeActionListeners() {\n for (const [, region] of this.regions) {\n this._cleanElementActionHandlers(region.element);\n for (const [, widgetEl] of region.widgetElements) {\n this._cleanElementActionHandlers(widgetEl);\n }\n }\n this.removeKeyboardListener();\n }\n\n _cleanElementActionHandlers(element) {\n if (element._actionHandlers) {\n for (const handler of element._actionHandlers) {\n element.removeEventListener('click', handler);\n }\n delete element._actionHandlers;\n element.style.cursor = '';\n }\n }\n\n // ── Interactive Control (XIC) ─────────────────────────────────────\n\n /**\n * Find a region containing a widget by widget ID.\n * Searches main regions first, then overlay regions.\n * @param {string} widgetId\n * @returns {{ regionId: string, region: Object, widget: Object, widgetIndex: number, regionMap: Map }|null}\n */\n _findRegionByWidgetId(widgetId) {\n // Search main regions\n for (const [regionId, region] of this.regions) {\n const widgetIndex = region.widgets.findIndex(w => w.id === widgetId);\n if (widgetIndex !== -1) {\n return { regionId, region, widget: region.widgets[widgetIndex], widgetIndex, regionMap: this.regions };\n }\n }\n // Search overlay regions\n for (const overlay of this.activeOverlays.values()) {\n if (!overlay.regions) continue;\n for (const [regionId, region] of overlay.regions) {\n const widgetIndex = region.widgets.findIndex(w => w.id === widgetId);\n if (widgetIndex !== -1) {\n return { regionId, region, widget: region.widgets[widgetIndex], widgetIndex, regionMap: overlay.regions };\n }\n }\n }\n return null;\n }\n\n /**\n * Advance a region to its next widget using the standard cycle.\n * @param {string} regionId\n * @param {Map} regionMap - The Map containing this region (main or overlay)\n */\n _advanceRegion(regionId, regionMap) {\n const region = regionMap.get(regionId);\n if (!region) return;\n region.currentIndex = (region.currentIndex + 1) % region.widgets.length;\n const isMain = regionMap === this.regions;\n this._startRegionCycle(\n region, regionId,\n isMain ? (rid, idx) => this.renderWidget(rid, idx) : (rid, idx) => this.renderWidget(rid, idx),\n isMain ? (rid, idx) => this.stopWidget(rid, idx) : (rid, idx) => this.stopWidget(rid, idx),\n isMain ? () => this.checkLayoutComplete() : undefined\n );\n }\n\n /**\n * Handle interactiveTrigger XIC event — navigate to a target widget.\n * @param {{ targetId: string, triggerCode: string }} data\n */\n _handleInteractiveTrigger({ targetId, triggerCode }) {\n this.log.info(`XIC interactiveTrigger: target=${targetId} code=${triggerCode}`);\n const found = this._findRegionByWidgetId(targetId);\n if (found) {\n this.navigateToWidget(targetId);\n } else {\n this.log.warn(`XIC interactiveTrigger: widget ${targetId} not found`);\n }\n }\n\n /**\n * Handle widgetExpire XIC event — immediately expire a widget and advance.\n * @param {{ widgetId: string }} data\n */\n _handleWidgetExpire({ widgetId }) {\n const found = this._findRegionByWidgetId(widgetId);\n if (!found) {\n this.log.warn(`XIC widgetExpire: widget ${widgetId} not found`);\n return;\n }\n const { regionId, region, widgetIndex, regionMap } = found;\n this.log.info(`XIC widgetExpire: widget=${widgetId} region=${regionId}`);\n if (region.timer) {\n clearTimeout(region.timer);\n region.timer = null;\n }\n this.stopWidget(regionId, widgetIndex);\n this._advanceRegion(regionId, regionMap);\n }\n\n /**\n * Handle widgetExtendDuration XIC event — extend the current widget timer.\n * @param {{ widgetId: string, duration: number }} data - duration in seconds (added to remaining)\n */\n _handleWidgetExtendDuration({ widgetId, duration }) {\n const found = this._findRegionByWidgetId(widgetId);\n if (!found) {\n this.log.warn(`XIC widgetExtendDuration: widget ${widgetId} not found`);\n return;\n }\n const { regionId, region } = found;\n this.log.info(`XIC widgetExtendDuration: widget=${widgetId} +${duration}s`);\n if (region.timer) {\n clearTimeout(region.timer);\n region.timer = null;\n }\n // Re-arm timer with the extended duration\n region.timer = setTimeout(() => {\n this.stopWidget(regionId, region.currentIndex);\n this._advanceRegion(regionId, found.regionMap);\n }, duration * 1000);\n }\n\n /**\n * Handle widgetSetDuration XIC event — replace the widget timer with an absolute duration.\n * @param {{ widgetId: string, duration: number }} data - duration in seconds (absolute)\n */\n _handleWidgetSetDuration({ widgetId, duration }) {\n const found = this._findRegionByWidgetId(widgetId);\n if (!found) {\n this.log.warn(`XIC widgetSetDuration: widget ${widgetId} not found`);\n return;\n }\n const { regionId, region } = found;\n this.log.info(`XIC widgetSetDuration: widget=${widgetId} ${duration}s`);\n if (region.timer) {\n clearTimeout(region.timer);\n region.timer = null;\n }\n // Set timer with the absolute duration\n region.timer = setTimeout(() => {\n this.stopWidget(regionId, region.currentIndex);\n this._advanceRegion(regionId, found.regionMap);\n }, duration * 1000);\n }\n\n /**\n * Navigate to a specific widget within a region (for navWidget actions)\n */\n navigateToWidget(targetWidgetId) {\n for (const [regionId, region] of this.regions) {\n const widgetIndex = region.widgets.findIndex(w => w.id === targetWidgetId);\n if (widgetIndex === -1) continue;\n\n this.log.info(`Navigating to widget ${targetWidgetId} in region ${regionId} (index ${widgetIndex})`);\n\n // Show drawer region if hidden (drawers start display:none)\n if (region.isDrawer && region.element.style.display === 'none') {\n region.element.style.display = '';\n this.log.info(`Drawer region ${regionId} revealed`);\n }\n\n if (region.timer) {\n clearTimeout(region.timer);\n region.timer = null;\n }\n\n this.stopWidget(regionId, region.currentIndex);\n region.currentIndex = widgetIndex;\n this.renderWidget(regionId, widgetIndex);\n\n if (region.widgets.length > 1) {\n const widget = region.widgets[widgetIndex];\n const duration = widget.duration * 1000;\n region.timer = setTimeout(() => {\n this.stopWidget(regionId, widgetIndex);\n const nextIndex = (widgetIndex + 1) % region.widgets.length;\n region.currentIndex = nextIndex;\n // For drawers, hide again after last widget; for normal regions, continue cycling\n if (region.isDrawer && nextIndex === 0) {\n region.element.style.display = 'none';\n this.log.info(`Drawer region ${regionId} hidden (cycle complete)`);\n } else if (region.isDrawer) {\n // Continue cycling through remaining drawer widgets (will hide on wrap to 0)\n this.navigateToWidget(region.widgets[nextIndex].id);\n } else {\n this.startRegion(regionId);\n }\n }, duration);\n } else if (region.isDrawer) {\n // Single-widget drawer: hide after widget duration\n const widget = region.widgets[widgetIndex];\n const duration = widget.duration * 1000;\n region.timer = setTimeout(() => {\n this.stopWidget(regionId, widgetIndex);\n region.element.style.display = 'none';\n this.log.info(`Drawer region ${regionId} hidden (single widget done)`);\n }, duration);\n }\n return;\n }\n this.log.warn(`Target widget ${targetWidgetId} not found in any region`);\n }\n\n /**\n * Navigate to the next widget in a region (wraps around)\n * @param {string} [regionId] - Target region. If omitted, uses the first region.\n */\n nextWidget(regionId) {\n const region = regionId ? this.regions.get(regionId) : this.regions.values().next().value;\n if (!region || region.widgets.length <= 1) return;\n\n const nextIndex = (region.currentIndex + 1) % region.widgets.length;\n const targetWidget = region.widgets[nextIndex];\n this.log.info(`nextWidget → index ${nextIndex} (widget ${targetWidget.id})`);\n this.navigateToWidget(targetWidget.id);\n }\n\n /**\n * Navigate to the previous widget in a region (wraps around)\n * @param {string} [regionId] - Target region. If omitted, uses the first region.\n */\n previousWidget(regionId) {\n const region = regionId ? this.regions.get(regionId) : this.regions.values().next().value;\n if (!region || region.widgets.length <= 1) return;\n\n const prevIndex = (region.currentIndex - 1 + region.widgets.length) % region.widgets.length;\n const targetWidget = region.widgets[prevIndex];\n this.log.info(`previousWidget → index ${prevIndex} (widget ${targetWidget.id})`);\n this.navigateToWidget(targetWidget.id);\n }\n\n // ── Layout Helpers ───────────────────────────────────────────────\n\n /**\n * Get media file URL for storedAs filename.\n * @param {string} storedAs - The storedAs filename (e.g. \"42_abc123.jpg\")\n * @returns {string} Full URL for the media file\n */\n _mediaFileUrl(storedAs) {\n return `${window.location.origin}${PLAYER_API}/media/file/${storedAs}`;\n }\n\n /**\n * Position a widget element to fill its region (hidden by default).\n * @param {HTMLElement} element\n */\n _positionWidgetElement(element) {\n Object.assign(element.style, {\n position: 'absolute',\n top: '0',\n left: '0',\n width: '100%',\n height: '100%',\n visibility: 'hidden',\n opacity: '0',\n });\n }\n\n /**\n * Apply a background image with cover styling.\n * @param {HTMLElement} element\n * @param {string} url - Image URL\n */\n _applyBackgroundImage(element, url) {\n Object.assign(element.style, {\n backgroundImage: `url(${url})`,\n backgroundSize: 'cover',\n backgroundPosition: 'center',\n backgroundRepeat: 'no-repeat',\n });\n }\n\n /**\n * Clear all region timers in a region map.\n * @param {Map} regions - Region map (regionId → region)\n */\n _clearRegionTimers(regions) {\n for (const [, region] of regions) {\n if (region.timer) {\n clearTimeout(region.timer);\n region.timer = null;\n }\n }\n }\n\n // ── Layout Rendering ──────────────────────────────────────────────\n\n /**\n * Render a layout\n * @param {string} xlfXml - XLF XML content\n * @param {number} layoutId - Layout ID\n * @returns {Promise<void>}\n */\n async renderLayout(xlfXml, layoutId) {\n try {\n this.log.info(`Rendering layout ${layoutId}`);\n\n // Check if we're replaying the same layout\n const isSameLayout = this.currentLayoutId === layoutId;\n\n if (isSameLayout) {\n // OPTIMIZATION: Reuse existing elements for same layout (Arexibo pattern)\n this.log.info(`Replaying layout ${layoutId} - reusing elements (no recreation!)`);\n\n // Stop all region timers and widgets, then reset to first widget\n this._clearRegionTimers(this.regions);\n this._stopAllRegionWidgets(this.regions, (rid, idx) => this.stopWidget(rid, idx));\n for (const [, region] of this.regions) {\n region.currentIndex = 0;\n region.complete = false;\n }\n\n // Clear layout timer\n if (this.layoutTimer) {\n clearTimeout(this.layoutTimer);\n this.layoutTimer = null;\n }\n\n this.layoutEndEmitted = false;\n this._deferredTimerLayoutId = null;\n if (this._deferredTimerFallback) {\n clearTimeout(this._deferredTimerFallback);\n this._deferredTimerFallback = null;\n }\n\n // DON'T call stopCurrentLayout() - keep elements alive!\n // DON'T recreate regions/elements - already exist!\n\n // Emit layout start event\n this.emit('layoutStart', layoutId, this.currentLayout);\n\n // Restart all regions from widget 0 (except drawers)\n for (const [regionId, region] of this.regions) {\n if (region.isDrawer) continue;\n this.startRegion(regionId);\n }\n\n // Wait for all initial widgets to be ready then start layout timer\n this.startLayoutTimerWhenReady(layoutId, this.currentLayout);\n\n this.log.info(`Layout ${layoutId} restarted (reused elements)`);\n\n // Schedule next layout preload for same-layout replay\n this._scheduleNextLayoutPreload(this.currentLayout);\n\n return; // EARLY RETURN - skip recreation below\n }\n\n // Check if this layout was preloaded in the pool\n if (this.layoutPool.has(layoutId)) {\n this.log.info(`Layout ${layoutId} found in preload pool - instant swap!`);\n await this._swapToPreloadedLayout(layoutId);\n return; // EARLY RETURN - preloaded layout swapped in\n }\n\n // Different layout - full teardown and rebuild\n this.log.info(`Switching to new layout ${layoutId}`);\n this.stopCurrentLayout();\n\n // Parse XLF\n const layout = this.parseXlf(xlfXml);\n this.currentLayout = layout;\n this.currentLayoutId = layoutId;\n\n // Calculate scale factor to fit layout into screen\n this.calculateScale(layout);\n\n // Set container background\n this.container.style.backgroundColor = layout.bgcolor;\n this.container.style.backgroundImage = ''; // Reset previous\n\n // Apply background image if specified in XLF\n // With storedAs refactor, background may be a filename (e.g. \"43.png\") or a numeric fileId\n if (layout.background) {\n const saveAs = this.options.fileIdToSaveAs?.get(String(layout.background)) || layout.background;\n this._applyBackgroundImage(this.container, this._mediaFileUrl(saveAs));\n this.log.info(`Background image set: ${layout.background} → ${saveAs}`);\n }\n\n // Create regions\n for (const regionConfig of layout.regions) {\n await this.createRegion(regionConfig);\n }\n\n // PRE-CREATE: Build all widget elements upfront (Arexibo pattern)\n this.log.info('Pre-creating widget elements for instant transitions...');\n for (const [regionId, region] of this.regions) {\n for (let i = 0; i < region.widgets.length; i++) {\n const widget = region.widgets[i];\n widget.layoutId = this.currentLayoutId;\n widget.regionId = regionId;\n\n try {\n const element = await this.createWidgetElement(widget, region);\n this._positionWidgetElement(element);\n region.element.appendChild(element);\n region.widgetElements.set(widget.id, element);\n } catch (error) {\n this.log.error(`Failed to pre-create widget ${widget.id}:`, error);\n }\n }\n }\n this.log.info('All widget elements pre-created');\n\n // Attach interactive action listeners (touch/click and keyboard)\n this.attachActionListeners(layout);\n\n // Emit layout start event\n this.emit('layoutStart', layoutId, layout);\n\n // Report calculated duration so the schedule queue/timeline uses it\n // instead of the 60s default. For layouts with unprobed videos, this\n // is an estimate that will be corrected by updateLayoutDuration().\n if (layout.duration > 0) {\n const final_ = !this._hasUnprobedVideos();\n this.emit('layoutDurationUpdated', layoutId, layout.duration, final_);\n }\n\n // Start all regions (except drawers — they're action-triggered)\n for (const [regionId, region] of this.regions) {\n if (region.isDrawer) continue;\n this.startRegion(regionId);\n }\n\n // Wait for all initial widgets to be ready (videos playing, images loaded)\n // THEN start the layout timer — ensures videos play to their last frame\n this.startLayoutTimerWhenReady(layoutId, layout);\n\n // Schedule preloading of the next layout at 75% of current duration\n this._scheduleNextLayoutPreload(layout);\n\n this.log.info(`Layout ${layoutId} started`);\n\n } catch (error) {\n this.log.error('Error rendering layout:', error);\n this.emit('error', { type: 'layoutError', error, layoutId });\n throw error;\n }\n }\n\n /**\n * 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 isCanvas: regionConfig.isCanvas || false, // Canvas regions render all widgets simultaneously\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 onLoad = () => {\n imgEl.removeEventListener('load', onLoad);\n clearTimeout(timer);\n resolve();\n };\n const timer = setTimeout(() => {\n imgEl.removeEventListener('load', onLoad);\n this.log.warn(`Image ready timeout for widget ${widget.id}`);\n resolve();\n }, READY_TIMEOUT);\n imgEl.addEventListener('load', onLoad);\n });\n }\n\n // Text, embedded, clock, etc. — ready immediately\n return Promise.resolve();\n }\n\n /**\n * Start the layout timer only after all initial widgets are ready.\n * This ensures that the layout duration counts from when content is\n * actually visible, so videos play their full duration to the last frame.\n * @param {number|string} layoutId - Layout ID\n * @param {Object} layout - Layout config with .duration\n */\n async startLayoutTimerWhenReady(layoutId, layout) {\n if (!layout || layout.duration <= 0) return;\n\n // Collect readiness promises for each region's first (current) widget\n const readyPromises = [];\n for (const [regionId, region] of this.regions) {\n if (region.widgets.length === 0) continue;\n const widget = region.widgets[region.currentIndex || 0];\n const element = region.widgetElements.get(widget.id);\n if (element) {\n readyPromises.push(this.waitForWidgetReady(element, widget));\n }\n }\n\n if (readyPromises.length > 0) {\n this.log.info(`Waiting for ${readyPromises.length} widget(s) to be ready before starting layout timer...`);\n await Promise.all(readyPromises);\n this.log.info(`All widgets ready — starting layout timer`);\n }\n\n // Guard: layout may have changed while we were waiting\n if (this.currentLayoutId !== layoutId) {\n this.log.warn(`Layout changed while waiting for widgets — skipping timer for ${layoutId}`);\n return;\n }\n\n // Dynamic layouts (useDuration=0 videos): defer timer until video metadata\n // provides real durations. Safety timeout ensures corrupt/missing videos\n // don't freeze the display forever.\n if (layout.isDynamic && this._hasUnprobedVideos()) {\n this._deferredTimerLayoutId = layoutId;\n this._layoutTimerStartedAt = Date.now();\n this.log.info(`Layout ${layoutId} has unprobed videos — deferring timer until metadata loads`);\n\n // Safety: if metadata never arrives (corrupt file, codec error), start\n // the timer with the estimated duration after 30s so the display keeps cycling.\n this._deferredTimerFallback = setTimeout(() => {\n this._deferredTimerFallback = null;\n if (this._deferredTimerLayoutId === layoutId && !this.layoutTimer) {\n this.log.warn(`Layout ${layoutId}: metadata timeout after 30s — starting timer with ${layout.duration}s estimate`);\n this._deferredTimerLayoutId = null;\n this._startLayoutTimer(layoutId, layout);\n }\n }, 30000);\n\n return;\n }\n\n this._startLayoutTimer(layoutId, layout);\n }\n\n /**\n * Check if any video widget in current layout still has duration=0 (metadata not loaded).\n */\n _hasUnprobedVideos() {\n for (const [, region] of this.regions) {\n for (const widget of region.widgets) {\n if (widget.useDuration === 0 && widget.duration === 0) return true;\n }\n }\n return false;\n }\n\n /**\n * Actually start the layout timer. Called directly or after deferred timer resolves.\n */\n _startLayoutTimer(layoutId, layout) {\n this._deferredTimerLayoutId = null;\n if (this._deferredTimerFallback) {\n clearTimeout(this._deferredTimerFallback);\n this._deferredTimerFallback = null;\n }\n const layoutDurationMs = layout.duration * 1000;\n this.log.info(`Layout ${layoutId} will end after ${layout.duration}s`);\n\n this._layoutTimerStartedAt = Date.now();\n this._layoutTimerDurationMs = layoutDurationMs;\n this.layoutTimer = setTimeout(() => {\n this.log.info(`Layout ${layoutId} duration expired (${layout.duration}s)`);\n if (this.currentLayoutId) {\n this.layoutEndEmitted = true;\n this.emit('layoutEnd', this.currentLayoutId);\n }\n }, layoutDurationMs);\n }\n\n /**\n * Render a widget in a region (using element reuse)\n * @param {string} regionId - Region ID\n * @param {number} widgetIndex - Widget index in region\n */\n /**\n * Core: show a widget in a region (shared by main layout + overlay)\n * Returns the widget object on success, null on failure.\n */\n async _showWidget(region, widgetIndex) {\n const widget = region.widgets[widgetIndex];\n if (!widget) return null;\n\n let element = region.widgetElements.get(widget.id);\n\n if (!element) {\n this.log.warn(`Widget ${widget.id} not pre-created, creating now`);\n element = await this.createWidgetElement(widget, region);\n element.style.position = 'absolute';\n element.style.top = '0';\n element.style.left = '0';\n element.style.width = '100%';\n element.style.height = '100%';\n region.widgetElements.set(widget.id, element);\n region.element.appendChild(element);\n }\n\n // Hide all other widgets in region (skip for canvas — all widgets stay visible)\n // Cancel fill:forwards animations first — they override inline styles\n if (!region.isCanvas) {\n for (const [widgetId, widgetEl] of region.widgetElements) {\n if (widgetId !== widget.id) {\n widgetEl.getAnimations?.().forEach(a => a.cancel());\n widgetEl.style.visibility = 'hidden';\n widgetEl.style.opacity = '0';\n }\n }\n }\n\n this.updateMediaElement(element, widget);\n element.getAnimations?.().forEach(a => a.cancel());\n element.style.visibility = 'visible';\n\n if (widget.transitions.in) {\n Transitions.apply(element, widget.transitions.in, true, region.width, region.height);\n } else {\n element.style.opacity = '1';\n }\n\n // Resume PDF page cycling if this widget was previously paused\n if (element._pdfResume) {\n element._pdfResume();\n }\n\n // Start audio overlays attached to this widget\n this._startAudioOverlays(widget);\n\n return widget;\n }\n\n /**\n * Start audio overlay elements for a widget.\n * Audio overlays are <audio> child nodes in the XLF that play alongside\n * the visual widget (e.g. background music for an image slideshow).\n * @param {Object} widget - Widget config with audioNodes array\n */\n _startAudioOverlays(widget) {\n if (!widget.audioNodes || widget.audioNodes.length === 0) return;\n\n // Stop any existing audio overlays for this widget first\n this._stopAudioOverlays(widget.id);\n\n const audioElements = [];\n for (const audioNode of widget.audioNodes) {\n if (!audioNode.uri) continue;\n\n const audio = document.createElement('audio');\n audio.autoplay = true;\n audio.loop = audioNode.loop;\n audio.volume = Math.max(0, Math.min(1, audioNode.volume / 100));\n\n // Direct URL from storedAs filename\n audio.src = audioNode.uri ? this._mediaFileUrl(audioNode.uri) : '';\n\n // Append to DOM to prevent garbage collection in some browsers\n audio.style.display = 'none';\n this.container.appendChild(audio);\n\n // Handle autoplay restrictions gracefully (play() may return undefined in some envs)\n const playPromise = audio.play();\n if (playPromise && playPromise.catch) playPromise.catch(() => {});\n\n audioElements.push(audio);\n this.log.info(`Audio overlay started for widget ${widget.id}: ${audioNode.uri} (loop=${audioNode.loop}, vol=${audioNode.volume})`);\n }\n\n if (audioElements.length > 0) {\n this.audioOverlays.set(widget.id, audioElements);\n }\n }\n\n /**\n * Stop and clean up audio overlay elements for a widget.\n * @param {string} widgetId - Widget ID\n */\n _stopAudioOverlays(widgetId) {\n const audioElements = this.audioOverlays.get(widgetId);\n if (!audioElements) return;\n\n for (const audio of audioElements) {\n audio.pause();\n audio.removeAttribute('src');\n audio.load(); // Release resources\n if (audio.parentNode) audio.parentNode.removeChild(audio); // Remove from DOM\n }\n\n this.audioOverlays.delete(widgetId);\n this.log.info(`Audio overlays stopped for widget ${widgetId}`);\n }\n\n /**\n * Core: hide a widget in a region (shared by main layout + overlay).\n * Returns { widget, animPromise } synchronously — callers await animPromise if needed.\n * NOT async, so callers that don't need the animation stay on the same microtask.\n */\n _hideWidget(region, widgetIndex) {\n const widget = region.widgets[widgetIndex];\n if (!widget) return { widget: null, animPromise: null };\n\n const widgetElement = region.widgetElements.get(widget.id);\n if (!widgetElement) return { widget: null, animPromise: null };\n\n let animPromise = null;\n if (widget.transitions.out) {\n const animation = Transitions.apply(\n widgetElement, widget.transitions.out, false, region.width, region.height\n );\n if (animation) {\n animPromise = new Promise(resolve => { animation.onfinish = resolve; });\n }\n }\n\n const videoEl = widgetElement.querySelector('video');\n if (videoEl && 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 // Destroy HLS.js instance to free worker + buffers\n if (videoEl?._hlsInstance) {\n videoEl._hlsInstance.destroy();\n videoEl._hlsInstance = null;\n }\n\n // Remove event listeners to prevent accumulation across widget cycles\n if (videoEl?._eventCleanup) {\n for (const [event, handler] of videoEl._eventCleanup) {\n videoEl.removeEventListener(event, handler);\n }\n videoEl._eventCleanup = null;\n }\n\n const audioEl = widgetElement.querySelector('audio');\n if (audioEl && widget.options.loop !== '1') audioEl.pause();\n\n // Remove audio event listeners\n if (audioEl?._eventCleanup) {\n for (const [event, handler] of audioEl._eventCleanup) {\n audioEl.removeEventListener(event, handler);\n }\n audioEl._eventCleanup = null;\n }\n\n // Stop audio overlays attached to this widget\n this._stopAudioOverlays(widget.id);\n\n // Stop PDF page cycling timers\n if (widgetElement._pdfCleanup) {\n widgetElement._pdfCleanup();\n }\n\n return { widget, animPromise };\n }\n\n /**\n * Check if a widget is within its valid time window (fromDt/toDt).\n * Widgets without dates are always active.\n * @param {Object} widget - Widget config with optional fromDt/toDt\n * @returns {boolean}\n */\n _isWidgetActive(widget) {\n const now = new Date();\n if (widget.fromDt) {\n const from = new Date(widget.fromDt);\n if (now < from) return false;\n }\n if (widget.toDt) {\n const to = new Date(widget.toDt);\n if (now > to) return false;\n }\n return true;\n }\n\n /**\n * Parse NUMITEMS and DURATION HTML comments from GetResource responses.\n * CMS embeds these in widget HTML to override duration for dynamic content\n * (e.g. DataSet tickers, RSS feeds). Format: <!-- NUMITEMS=5 --> <!-- DURATION=30 -->\n * DURATION takes precedence; otherwise NUMITEMS × widget.duration is used.\n * @param {string} html - Widget HTML content\n * @param {Object} widget - Widget config (duration may be updated)\n */\n _parseDurationComments(html, widget) {\n const oldDuration = widget.duration;\n\n const durationMatch = html.match(/<!--\\s*DURATION=(\\d+)\\s*-->/);\n if (durationMatch) {\n const newDuration = parseInt(durationMatch[1], 10);\n if (newDuration > 0) {\n this.log.info(`Widget ${widget.id}: DURATION comment overrides duration ${widget.duration}→${newDuration}s`);\n widget.duration = newDuration;\n if (widget.duration !== oldDuration) this.updateLayoutDuration();\n return;\n }\n }\n\n const numItemsMatch = html.match(/<!--\\s*NUMITEMS=(\\d+)\\s*-->/);\n if (numItemsMatch) {\n const numItems = parseInt(numItemsMatch[1], 10);\n if (numItems > 0 && widget.duration > 0) {\n const newDuration = numItems * widget.duration;\n this.log.info(`Widget ${widget.id}: NUMITEMS=${numItems} × ${widget.duration}s = ${newDuration}s`);\n widget.duration = newDuration;\n }\n }\n\n if (widget.duration !== oldDuration) this.updateLayoutDuration();\n }\n\n /**\n * Apply sub-playlist cycle playback filtering.\n * Groups widgets by parentWidgetId, then selects one widget per group for this cycle.\n * Non-grouped widgets pass through unchanged.\n *\n * @param {Array} widgets - All widgets in the region\n * @returns {Array} Filtered widgets for this playback cycle\n */\n _applyCyclePlayback(widgets) {\n // Track cycle indices per group for deterministic round-robin\n if (!this._subPlaylistCycleIndex) {\n this._subPlaylistCycleIndex = new Map();\n }\n\n // Group widgets by parentWidgetId\n const groups = new Map(); // parentWidgetId → [widgets]\n const result = [];\n\n for (const widget of widgets) {\n if (widget.parentWidgetId && widget.cyclePlayback) {\n if (!groups.has(widget.parentWidgetId)) {\n groups.set(widget.parentWidgetId, []);\n }\n groups.get(widget.parentWidgetId).push(widget);\n } else {\n // Non-grouped widget: add a placeholder to preserve order\n result.push({ type: 'direct', widget });\n }\n }\n\n // For each group, select one widget for this cycle\n for (const [groupId, groupWidgets] of groups) {\n // Sort by displayOrder\n groupWidgets.sort((a, b) => a.displayOrder - b.displayOrder);\n\n let selectedWidget;\n if (groupWidgets.some(w => w.isRandom)) {\n // Random selection\n const idx = Math.floor(Math.random() * groupWidgets.length);\n selectedWidget = groupWidgets[idx];\n } else {\n // Round-robin based on cycle index, respecting playCount\n const state = this._subPlaylistCycleIndex.get(groupId) || { widgetIdx: 0, playsDone: 0 };\n selectedWidget = groupWidgets[state.widgetIdx % groupWidgets.length];\n const effectivePlayCount = selectedWidget.playCount || 1;\n\n state.playsDone++;\n if (state.playsDone >= effectivePlayCount) {\n state.widgetIdx++;\n state.playsDone = 0;\n }\n this._subPlaylistCycleIndex.set(groupId, state);\n }\n\n this.log.info(`Sub-playlist cycle: group ${groupId} selected widget ${selectedWidget.id} (${groupWidgets.length} in group)`);\n result.push({ type: 'direct', widget: selectedWidget });\n }\n\n return result.map(r => r.widget);\n }\n\n /**\n * Core: cycle through widgets in a region (shared by main layout + overlay)\n * @param {Object} region - Region state object\n * @param {string} regionId - Region ID\n * @param {Function} showFn - (regionId, widgetIndex) => show widget\n * @param {Function} hideFn - (regionId, widgetIndex) => hide widget\n * @param {Function} [onCycleComplete] - Called when region completes one full cycle\n */\n _startRegionCycle(region, regionId, showFn, hideFn, onCycleComplete) {\n if (!region || region.widgets.length === 0) return;\n\n // Canvas regions: render ALL widgets simultaneously (stacked), no cycling.\n // Duration = max widget duration; region completes when the longest widget expires.\n if (region.isCanvas) {\n this._startCanvasRegion(region, regionId, showFn, onCycleComplete);\n return;\n }\n\n // Non-looping region with a single widget: show it and stay (spec: loop=0)\n if (region.widgets.length === 1) {\n showFn(regionId, 0);\n return;\n }\n\n const playNext = () => {\n const widgetIndex = region.currentIndex;\n const widget = region.widgets[widgetIndex];\n\n showFn(regionId, widgetIndex);\n\n const duration = widget.duration * 1000;\n this.log.info(`Region ${regionId} widget ${widget.id} (${widget.type}) playing for ${widget.duration}s (useDuration=${widget.useDuration}, index ${widgetIndex}/${region.widgets.length})`);\n region.timer = setTimeout(() => {\n this._handleWidgetCycleEnd(widget, region, regionId, widgetIndex, showFn, hideFn, onCycleComplete, playNext);\n }, duration);\n };\n\n playNext();\n }\n\n /**\n * Start a canvas region — render all widgets simultaneously (stacked).\n * Canvas regions show every widget at once rather than cycling through them.\n * The region duration is the maximum widget duration.\n * @param {Object} region - Region state\n * @param {string} regionId - Region ID\n * @param {Function} showFn - Show widget function (regionId, widgetIndex)\n * @param {Function} onCycleComplete - Callback when region completes\n */\n _startCanvasRegion(region, regionId, showFn, onCycleComplete) {\n // Show all widgets at once\n for (let i = 0; i < region.widgets.length; i++) {\n showFn(regionId, i);\n }\n\n // Mark region as complete after max widget duration\n const maxDuration = Math.max(...region.widgets.map(w => w.duration)) * 1000;\n if (maxDuration > 0) {\n region.timer = setTimeout(() => {\n if (!region.complete) {\n region.complete = true;\n onCycleComplete?.();\n }\n }, maxDuration);\n } else {\n // No duration — immediately complete\n region.complete = true;\n onCycleComplete?.();\n }\n }\n\n /**\n * Handle widget cycle end — shared logic for timer-based and event-based cycling\n */\n _handleWidgetCycleEnd(widget, region, regionId, widgetIndex, showFn, hideFn, onCycleComplete, playNext) {\n // Emit widgetAction if widget has a webhook URL configured\n if (widget.webhookUrl) {\n this.emit('widgetAction', {\n type: 'durationEnd',\n widgetId: widget.id,\n layoutId: this.currentLayoutId,\n regionId,\n url: widget.webhookUrl\n });\n }\n\n hideFn(regionId, widgetIndex);\n\n const nextIndex = (region.currentIndex + 1) % region.widgets.length;\n if (nextIndex === 0 && !region.complete) {\n region.complete = true;\n onCycleComplete?.();\n }\n\n // Non-looping single-widget region (loop=0): don't replay.\n // Multi-widget regions (playlists) always cycle regardless of loop setting —\n // in Xibo, loop=0 only means \"don't repeat a single media item.\"\n if (nextIndex === 0 && region.config?.loop === false && region.widgets.length === 1) {\n showFn(regionId, 0);\n return;\n }\n\n // Don't start next widget if layout has already ended (race with layout timer)\n if (this.layoutEndEmitted) return;\n\n region.currentIndex = nextIndex;\n playNext();\n }\n\n async renderWidget(regionId, widgetIndex) {\n const region = this.regions.get(regionId);\n if (!region) return;\n\n try {\n const widget = await this._showWidget(region, widgetIndex);\n if (widget) {\n this.log.info(`Showing widget ${widget.type} (${widget.id}) in region ${regionId}`);\n this._startedWidgets.add(`${regionId}:${widgetIndex}`);\n this.emit('widgetStart', {\n widgetId: widget.id, regionId, layoutId: this.currentLayoutId,\n mediaId: parseInt(widget.fileId || widget.id) || null,\n type: widget.type, duration: widget.duration,\n enableStat: widget.enableStat\n });\n\n // Execute commands attached to this widget (shell/native commands)\n if (widget.commands && widget.commands.length > 0) {\n for (const cmd of widget.commands) {\n this.emit('widgetCommand', {\n commandCode: cmd.commandCode,\n commandString: cmd.commandString,\n widgetId: widget.id,\n regionId,\n layoutId: this.currentLayoutId\n });\n }\n }\n }\n } catch (error) {\n this.log.error(`Error rendering widget:`, error);\n this.emit('error', { type: 'widgetError', error, widgetId: region.widgets[widgetIndex]?.id, regionId });\n }\n }\n\n /**\n * Stop a widget (with element reuse - don't revoke blob URLs!)\n * @param {string} regionId - Region ID\n * @param {number} widgetIndex - Widget index\n */\n async stopWidget(regionId, widgetIndex) {\n const key = `${regionId}:${widgetIndex}`;\n if (!this._startedWidgets.delete(key)) return; // idempotent: already stopped\n\n const region = this.regions.get(regionId);\n if (!region) return;\n\n const { widget, animPromise } = this._hideWidget(region, widgetIndex);\n // Emit widgetEnd immediately — don't wait for exit animation.\n // If we await animPromise first, a pool eviction can remove the DOM element,\n // causing the animation's onfinish to never fire and widgetEnd to be lost.\n if (widget) {\n this.emit('widgetEnd', {\n widgetId: widget.id, regionId, layoutId: this.currentLayoutId,\n mediaId: parseInt(widget.fileId || widget.id) || null,\n type: widget.type,\n enableStat: widget.enableStat\n });\n }\n if (animPromise) await animPromise;\n }\n\n /**\n * Stop all started widgets across regions (symmetric counterpart to startRegion)\n * Canvas regions start ALL widgets; non-canvas regions have one active widget.\n * @param {Map} regions - Region map\n * @param {Function} stopFn - (regionId, widgetIndex) => void\n */\n _stopAllRegionWidgets(regions, stopFn) {\n for (const [regionId, region] of regions) {\n if (region.isCanvas) {\n for (let i = 0; i < region.widgets.length; i++) {\n stopFn(regionId, i);\n }\n } else if (region.widgets.length > 0) {\n stopFn(regionId, region.currentIndex);\n }\n }\n }\n\n /**\n * Render image widget\n */\n async renderImage(widget, region) {\n const img = document.createElement('img');\n img.className = 'renderer-lite-widget';\n img.style.width = '100%';\n img.style.height = '100%';\n // Scale type mapping (CMS image.xml):\n // center (default) → contain: scale proportionally to fit region, centered\n // stretch → fill: ignore aspect ratio, fill entire region\n // fit → cover: scale proportionally to fill region, crop excess\n const scaleType = widget.options.scaleType;\n const fitMap = { stretch: 'fill', center: 'contain', fit: 'cover' };\n img.style.objectFit = fitMap[scaleType] || 'contain';\n\n // Alignment: map alignId/valignId to CSS object-position\n // XLF tags are <alignId> and <valignId> (from CMS image.xml property ids)\n const alignMap = { left: 'left', center: 'center', right: 'right' };\n const valignMap = { top: 'top', middle: 'center', bottom: 'bottom' };\n const hPos = alignMap[widget.options.alignId] || 'center';\n const vPos = valignMap[widget.options.valignId] || 'center';\n img.style.objectPosition = `${hPos} ${vPos}`;\n\n img.style.opacity = '0';\n\n // Direct URL from storedAs filename — store key = widget reference = serve URL\n const src = widget.options.uri\n ? this._mediaFileUrl(widget.options.uri)\n : '';\n\n img.src = src;\n return img;\n }\n\n /**\n * Render video widget\n */\n async renderVideo(widget, region) {\n const video = document.createElement('video');\n video.className = 'renderer-lite-widget';\n video.style.width = '100%';\n video.style.height = '100%';\n const vScaleType = widget.options.scaleType;\n const vFitMap = { stretch: 'fill', center: 'none', fit: 'contain' };\n video.style.objectFit = vFitMap[vScaleType] || 'contain';\n video.style.opacity = '1'; // Immediately visible\n video.autoplay = true;\n video.preload = 'auto'; // Eagerly buffer - chunks are pre-warmed in SW BlobCache\n video.muted = widget.options.mute === '1';\n video.loop = false; // Don't use native loop - we handle it manually to avoid black frames\n video.controls = false; // Hidden by default — toggle with V key in PWA\n video.playsInline = true; // Prevent fullscreen on mobile\n\n // Direct URL from storedAs filename\n const storedAs = widget.options.uri || '';\n const fileId = widget.fileId || widget.id;\n\n // Handle video end - pause on last frame instead of showing black\n // Widget cycling will restart the video via updateMediaElement()\n const onEnded = () => {\n if (widget.options.loop === '1') {\n video.currentTime = 0;\n this.log.info(`Video ${storedAs} ended - reset to start, waiting for widget cycle to replay`);\n } else {\n this.log.info(`Video ${storedAs} ended - paused on last frame`);\n }\n };\n video.addEventListener('ended', onEnded);\n let videoSrc = storedAs ? this._mediaFileUrl(storedAs) : '';\n\n // HLS/DASH streaming support\n const isHlsStream = videoSrc.includes('.m3u8');\n if (isHlsStream) {\n // Try native HLS first (Safari, iOS, some Android)\n if (video.canPlayType('application/vnd.apple.mpegurl')) {\n this.log.info(`HLS stream (native): ${fileId}`);\n video.src = videoSrc;\n } else {\n // Dynamic import hls.js for Chrome/Firefox (code-split, not in main bundle)\n try {\n const { default: Hls } = await import('hls.js');\n if (Hls.isSupported()) {\n const hls = new Hls({ enableWorker: true, lowLatencyMode: true });\n hls.loadSource(videoSrc);\n hls.attachMedia(video);\n video._hlsInstance = hls; // Store for cleanup on eviction\n hls.on(Hls.Events.ERROR, (_event, data) => {\n if (data.fatal) {\n this.log.error(`HLS fatal error: ${data.type}`, data.details);\n hls.destroy();\n video._hlsInstance = null;\n }\n });\n this.log.info(`HLS stream (hls.js): ${fileId}`);\n } else {\n this.log.warn(`HLS not supported on this browser for ${fileId}`);\n video.src = videoSrc; // Fallback — may not work\n }\n } catch (e) {\n this.log.warn(`hls.js not available, falling back to native: ${e.message}`);\n video.src = videoSrc;\n }\n }\n } else {\n video.src = videoSrc;\n }\n\n // Detect video duration for dynamic layout timing (when useDuration=0)\n // Capture the layout ID at creation time — if the layout changes before\n // loadedmetadata fires (e.g. video was preloaded for next layout), we must\n // NOT update the current layout's duration with a different layout's video.\n const createdForLayoutId = this.currentLayoutId;\n const onLoadedMetadata = () => {\n const videoDuration = video.duration;\n this.log.info(`Video ${storedAs} duration detected: ${videoDuration}s`);\n\n if (widget.duration === 0 || widget.useDuration === 0) {\n widget.duration = videoDuration;\n this.log.info(`Updated widget ${widget.id} duration to ${videoDuration}s (useDuration=0)`);\n\n if (this.currentLayoutId === createdForLayoutId) {\n this.updateLayoutDuration();\n } else {\n this.log.info(`Video ${storedAs} duration set but layout timer not updated (preloaded for layout ${createdForLayoutId}, current is ${this.currentLayoutId})`);\n }\n }\n };\n video.addEventListener('loadedmetadata', onLoadedMetadata);\n\n const onLoadedData = () => {\n this.log.info('Video loaded and ready:', storedAs);\n };\n video.addEventListener('loadeddata', onLoadedData);\n\n const onError = () => {\n const error = video.error;\n const errorCode = error?.code;\n const errorMessage = error?.message || 'Unknown error';\n this.log.warn(`Video error: ${storedAs}, code: ${errorCode}, time: ${video.currentTime.toFixed(1)}s, message: ${errorMessage}`);\n\n // Set fallback duration so the deferred timer can proceed.\n // Without this, a corrupt video leaves widget.duration=0 forever,\n // _hasUnprobedVideos() stays true, and the deferred timer never unblocks.\n if (widget.useDuration === 0 && widget.duration === 0) {\n widget.duration = 60;\n this.log.info(`Set fallback duration 60s for errored widget ${widget.id}`);\n if (this.currentLayoutId === createdForLayoutId) {\n this.updateLayoutDuration();\n }\n }\n\n this.emit('videoError', { storedAs, fileId, errorCode, errorMessage, currentTime: video.currentTime });\n };\n video.addEventListener('error', onError);\n\n const onPlaying = () => {\n this.log.info('Video playing:', storedAs);\n };\n video.addEventListener('playing', onPlaying);\n\n // Store listener references for cleanup in _hideWidget()\n video._eventCleanup = [\n ['ended', onEnded],\n ['loadedmetadata', onLoadedMetadata],\n ['loadeddata', onLoadedData],\n ['error', onError],\n ['playing', onPlaying],\n ];\n\n this.log.info('Video element created:', storedAs, video.src);\n\n return video;\n }\n\n /**\n * Render videoin (webcam/microphone) widget.\n * Uses getUserMedia() to capture live video from camera hardware.\n * @param {Object} widget - Widget config with options (sourceId, showFullScreen, mirror, mute, captureAudio)\n * @param {Object} region - Region dimensions (width, height)\n * @returns {HTMLVideoElement}\n */\n async renderVideoIn(widget, region) {\n const video = document.createElement('video');\n video.className = 'renderer-lite-widget';\n video.style.width = '100%';\n video.style.height = '100%';\n video.style.objectFit = widget.options.showFullScreen === '1' ? 'cover' : 'contain';\n video.autoplay = true;\n video.playsInline = true;\n video.controls = false;\n video.muted = widget.options.mute !== '0'; // Muted by default to prevent audio feedback\n\n // Mirror mode (front-facing camera)\n if (widget.options.mirror === '1') {\n video.style.transform = 'scaleX(-1)';\n }\n\n // Build getUserMedia constraints\n const videoConstraints = {\n width: { ideal: region.width },\n height: { ideal: region.height },\n };\n const deviceId = widget.options.sourceId || widget.options.deviceId;\n if (deviceId) {\n videoConstraints.deviceId = { exact: deviceId };\n } else {\n videoConstraints.facingMode = widget.options.facingMode || 'environment';\n }\n\n const constraints = {\n video: videoConstraints,\n audio: widget.options.captureAudio === '1',\n };\n\n // Store constraints for re-acquisition after layout transitions\n video._mediaConstraints = constraints;\n\n try {\n const stream = await navigator.mediaDevices.getUserMedia(constraints);\n video.srcObject = stream;\n video._mediaStream = stream;\n this.log.info(`Webcam stream acquired for widget ${widget.id} (tracks: ${stream.getTracks().length})`);\n } catch (e) {\n this.log.warn(`getUserMedia failed for widget ${widget.id}: ${e.message}`);\n return this._renderUnsupportedPlaceholder(\n { ...widget, type: 'Camera unavailable' },\n region\n );\n }\n\n return video;\n }\n\n /**\n * Render audio widget\n */\n async renderAudio(widget, region) {\n const container = document.createElement('div');\n container.className = 'renderer-lite-widget audio-widget';\n container.style.width = '100%';\n container.style.height = '100%';\n container.style.display = 'flex';\n container.style.flexDirection = 'column';\n container.style.alignItems = 'center';\n container.style.justifyContent = 'center';\n container.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';\n container.style.opacity = '0';\n\n // Audio element\n const audio = document.createElement('audio');\n audio.autoplay = true;\n audio.loop = widget.options.loop === '1';\n audio.volume = parseFloat(widget.options.volume || '100') / 100;\n\n // Direct URL from storedAs filename\n const storedAs = widget.options.uri || '';\n const fileId = widget.fileId || widget.id;\n audio.src = storedAs ? this._mediaFileUrl(storedAs) : '';\n\n // Handle audio end - similar to video ended handling\n const onAudioEnded = () => {\n if (widget.options.loop === '1') {\n audio.currentTime = 0;\n this.log.info(`Audio ${storedAs} ended - reset to start, waiting for widget cycle to replay`);\n } else {\n this.log.info(`Audio ${storedAs} ended - playback complete`);\n }\n };\n audio.addEventListener('ended', onAudioEnded);\n\n // Detect audio duration for dynamic layout timing (when useDuration=0)\n const audioCreatedForLayoutId = this.currentLayoutId;\n const onAudioLoadedMetadata = () => {\n const audioDuration = Math.floor(audio.duration);\n this.log.info(`Audio ${storedAs} duration detected: ${audioDuration}s`);\n\n if (widget.duration === 0 || widget.useDuration === 0) {\n widget.duration = audioDuration;\n this.log.info(`Updated widget ${widget.id} duration to ${audioDuration}s (useDuration=0)`);\n\n if (this.currentLayoutId === audioCreatedForLayoutId) {\n this.updateLayoutDuration();\n } else {\n this.log.info(`Audio ${storedAs} duration set but layout timer not updated (preloaded for layout ${audioCreatedForLayoutId}, current is ${this.currentLayoutId})`);\n }\n }\n };\n audio.addEventListener('loadedmetadata', onAudioLoadedMetadata);\n\n // Handle audio errors\n const onAudioError = () => {\n const error = audio.error;\n this.log.warn(`Audio error (non-fatal): ${storedAs}, code: ${error?.code}, message: ${error?.message || 'Unknown'}`);\n };\n audio.addEventListener('error', onAudioError);\n\n // Store listener references for cleanup in _hideWidget()\n audio._eventCleanup = [\n ['ended', onAudioEnded],\n ['loadedmetadata', onAudioLoadedMetadata],\n ['error', onAudioError],\n ];\n\n // Visual feedback\n const icon = document.createElement('div');\n icon.innerHTML = '♪';\n icon.style.fontSize = '120px';\n icon.style.color = 'white';\n icon.style.marginBottom = '20px';\n\n const info = document.createElement('div');\n info.style.color = 'white';\n info.style.fontSize = '24px';\n info.textContent = 'Playing Audio';\n\n const filename = document.createElement('div');\n filename.style.color = 'rgba(255,255,255,0.7)';\n filename.style.fontSize = '16px';\n filename.style.marginTop = '10px';\n filename.textContent = widget.options.uri;\n\n container.appendChild(audio);\n container.appendChild(icon);\n container.appendChild(info);\n container.appendChild(filename);\n\n return container;\n }\n\n /**\n * Render text/ticker widget\n */\n async renderTextWidget(widget, region) {\n const iframe = document.createElement('iframe');\n iframe.className = 'renderer-lite-widget';\n iframe.style.width = '100%';\n iframe.style.height = '100%';\n iframe.style.border = 'none';\n iframe.style.opacity = '0';\n\n // Get widget HTML (may return { url } for cache-path loading or string for blob)\n let html = widget.raw;\n if (this.options.getWidgetHtml) {\n const result = await this.options.getWidgetHtml(widget);\n if (result && typeof result === 'object' && result.url) {\n // Use cache URL — SW serves HTML and intercepts sub-resources\n iframe.src = result.url;\n\n // Parse NUMITEMS/DURATION from fallback HTML (cache path)\n if (result.fallback) {\n this._parseDurationComments(result.fallback, widget);\n }\n\n return iframe;\n }\n html = result;\n }\n\n if (html) {\n // Parse NUMITEMS/DURATION HTML comments for dynamic widget duration\n 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 — single reusable canvas, page-by-page cycling.\n *\n * Memory strategy:\n * - One canvas is created and reused for all pages (no DOM churn)\n * - Each page is rendered sequentially (avoids concurrent render errors)\n * - page.cleanup() releases PDF.js internal page buffers after each render\n * - pdf.destroy() releases the entire document on widget teardown\n * - Active renderTask is cancelled on cleanup to prevent stale renders\n */\n async renderPdf(widget, region) {\n const container = document.createElement('div');\n container.className = 'renderer-lite-widget pdf-widget';\n container.style.width = '100%';\n container.style.height = '100%';\n container.style.backgroundColor = 'transparent';\n container.style.opacity = '0';\n container.style.position = 'relative';\n\n // Load PDF.js if available\n if (typeof window.pdfjsLib === 'undefined') {\n try {\n const pdfjsModule = await import('pdfjs-dist');\n window.pdfjsLib = pdfjsModule;\n // Derive worker path from current page location (works for /player/pwa/ and /player/)\n const basePath = window.location.pathname.replace(/\\/[^/]*$/, '/');\n window.pdfjsLib.GlobalWorkerOptions.workerSrc = `${window.location.origin}${basePath}pdf.worker.min.mjs`;\n } catch (error) {\n this.log.error('PDF.js not available:', error);\n container.innerHTML = '<div style=\"color:white;padding:20px;text-align:center;\">PDF viewer unavailable</div>';\n container.style.opacity = '1';\n return container;\n }\n }\n\n // Direct URL from storedAs filename\n let pdfUrl = widget.options.uri\n ? this._mediaFileUrl(widget.options.uri)\n : '';\n\n // Render PDF with multi-page cycling\n try {\n const loadingTask = window.pdfjsLib.getDocument(pdfUrl);\n const pdf = await loadingTask.promise;\n const totalPages = pdf.numPages;\n const duration = widget.duration || 60;\n const timePerPage = (duration * 1000) / totalPages;\n this.log.info(`[pdf] PDF loaded: ${totalPages} pages, ${duration}s duration, ${(timePerPage / 1000).toFixed(1)}s/page`);\n\n // Measure page size from first page to set up the single reusable canvas\n const page1 = await pdf.getPage(1);\n const viewport0 = page1.getViewport({ scale: 1 });\n const scale = Math.min(region.width / viewport0.width, region.height / viewport0.height);\n page1.cleanup();\n\n const canvas = document.createElement('canvas');\n canvas.className = 'pdf-page';\n canvas.width = Math.floor(viewport0.width * scale);\n canvas.height = Math.floor(viewport0.height * scale);\n canvas.style.cssText = 'display:block;margin:auto;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);';\n const ctx = canvas.getContext('2d');\n container.appendChild(canvas);\n\n // Page indicator (bottom-right, v1-style pill) — debug only\n const indicator = document.createElement('div');\n indicator.style.cssText = 'position:absolute;bottom:10px;right:10px;background:rgba(0,0,0,0.7);color:white;padding:8px 12px;border-radius:4px;font:14px system-ui;z-index:1;';\n if (!isDebug()) indicator.style.display = 'none';\n container.appendChild(indicator);\n\n let currentPage = 1;\n let cycleTimer = null;\n let activeRenderTask = null;\n let stopped = false;\n\n // Render one page at a time on the single canvas. Sequential scheduling\n // (setTimeout after render completes) avoids the \"Cannot use the same\n // canvas during multiple render() operations\" error from PDF.js.\n const cyclePage = async () => {\n if (stopped) return;\n indicator.textContent = `Page ${currentPage} / ${totalPages}`;\n\n const page = await pdf.getPage(currentPage);\n const scaledViewport = page.getViewport({ scale });\n\n // Clear and render on the reusable canvas\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n activeRenderTask = page.render({ canvasContext: ctx, viewport: scaledViewport });\n try {\n await activeRenderTask.promise;\n } catch (e) {\n // RenderingCancelledException is expected when stopped during render\n if (stopped) return;\n throw e;\n }\n activeRenderTask = null;\n page.cleanup(); // Release PDF.js internal page buffers\n\n // Schedule next page (only after current render completes)\n if (totalPages > 1 && !stopped) {\n cycleTimer = setTimeout(() => {\n currentPage = currentPage >= totalPages ? 1 : currentPage + 1;\n cyclePage();\n }, timePerPage);\n }\n };\n\n await cyclePage();\n\n // Pause: stop page cycling (called by _hideWidget during region cycling / replay)\n // Returns a promise that resolves when the active render is fully cancelled.\n let cancelPromise = null;\n container._pdfCleanup = () => {\n stopped = true;\n if (cycleTimer) clearTimeout(cycleTimer);\n cycleTimer = null;\n if (activeRenderTask) {\n const task = activeRenderTask;\n activeRenderTask = null;\n task.cancel();\n cancelPromise = task.promise.catch(() => {}); // wait for cancellation to propagate\n }\n };\n\n // Resume: restart page cycling from page 1 (called by _showWidget on reuse)\n // Always cleanup first — the PDF may still be rendering from preload\n // (pre-create starts cyclePage immediately, but the widget isn't \"shown\"\n // until the layout swap, so _pdfCleanup was never called).\n container._pdfResume = async () => {\n container._pdfCleanup(); // stop any in-flight render\n if (cancelPromise) { await cancelPromise; cancelPromise = null; }\n stopped = false;\n currentPage = 1;\n cyclePage();\n };\n\n // Destroy: release GPU + PDF resources (called on element removal / eviction)\n container._pdfDestroy = () => {\n container._pdfCleanup();\n canvas.width = 0;\n canvas.height = 0;\n pdf.destroy();\n };\n\n } catch (error) {\n this.log.error('PDF render failed:', error);\n container.innerHTML = '<div style=\"color:white;padding:20px;text-align:center;\">Failed to load PDF</div>';\n }\n\n container.style.opacity = '1';\n return container;\n }\n\n /**\n * Render webpage widget\n */\n async renderWebpage(widget, region) {\n // modeId=1 (or absent) = Open Natively (direct URL), modeId=0 = Manual/GetResource\n const modeId = parseInt(widget.options.modeId || '1');\n if (modeId === 0) {\n // GetResource mode: treat like a generic widget (fetch HTML from CMS)\n return await this.renderGenericWidget(widget, region);\n }\n\n const iframe = document.createElement('iframe');\n iframe.className = 'renderer-lite-widget';\n iframe.style.width = '100%';\n iframe.style.height = '100%';\n iframe.style.border = 'none';\n iframe.style.opacity = '0';\n // CMS may percent-encode the URI in XLF (e.g. https%3A%2F%2F → https://)\n const uri = decodeURIComponent(widget.options.uri || '');\n iframe.src = uri;\n\n return iframe;\n }\n\n /**\n * Render generic widget (clock, calendar, weather, etc.)\n */\n async renderGenericWidget(widget, region) {\n const iframe = document.createElement('iframe');\n iframe.className = 'renderer-lite-widget';\n iframe.style.width = '100%';\n iframe.style.height = '100%';\n iframe.style.border = 'none';\n iframe.style.opacity = '0';\n\n // Get widget HTML (may return { url } for cache-path loading or string for blob)\n let html = widget.raw;\n if (this.options.getWidgetHtml) {\n const result = await this.options.getWidgetHtml(widget);\n if (result && typeof result === 'object' && result.url) {\n // Use cache URL — SW serves HTML and intercepts sub-resources\n iframe.src = result.url;\n\n // Parse NUMITEMS/DURATION from fallback HTML (cache path)\n if (result.fallback) {\n this._parseDurationComments(result.fallback, widget);\n }\n\n return iframe;\n }\n html = result;\n }\n\n if (html) {\n // Parse NUMITEMS/DURATION HTML comments for dynamic widget duration\n // Format: <!-- NUMITEMS=5 --> and <!-- DURATION=30 -->\n this._parseDurationComments(html, widget);\n\n const blob = new Blob([html], { type: 'text/html' });\n const blobUrl = URL.createObjectURL(blob);\n iframe.src = blobUrl;\n\n // Track blob URL for lifecycle management\n this.trackBlobUrl(blobUrl);\n } else {\n this.log.warn(`No HTML for widget ${widget.id}`);\n iframe.srcdoc = '<div style=\"padding:20px;\">Widget content unavailable</div>';\n }\n\n return iframe;\n }\n\n /**\n * Render a placeholder for unsupported widget types (powerpoint, flash)\n */\n _renderUnsupportedPlaceholder(widget, region) {\n const div = document.createElement('div');\n div.className = 'renderer-lite-widget';\n div.style.width = '100%';\n div.style.height = '100%';\n div.style.display = 'flex';\n div.style.alignItems = 'center';\n div.style.justifyContent = 'center';\n div.style.backgroundColor = '#111';\n div.style.color = '#666';\n div.style.fontSize = '14px';\n div.textContent = `Unsupported: ${widget.type}`;\n return div;\n }\n\n // ── Layout Preload Pool ─────────────────────────────────────────────\n\n /**\n * Schedule preloading of the next layout at 75% of current layout duration.\n * Emits 'request-next-layout-preload' so the platform layer can peek at the\n * schedule and call preloadLayout() with the next layout's XLF.\n * @param {Object} layout - Current layout object with .duration\n */\n _scheduleNextLayoutPreload(layout) {\n if (this.preloadTimer) {\n clearTimeout(this.preloadTimer);\n this.preloadTimer = null;\n }\n if (this._preloadRetryTimer) {\n clearTimeout(this._preloadRetryTimer);\n this._preloadRetryTimer = null;\n }\n\n const duration = layout.duration || 60; // seconds\n const preloadDelay = duration * 1000 * 0.75; // 75% through\n const retryDelay = duration * 1000 * 0.90; // 90% retry\n\n this.log.info(`Scheduling next layout preload in ${(preloadDelay / 1000).toFixed(1)}s (75% of ${duration}s)`);\n\n this.preloadTimer = setTimeout(() => {\n this.preloadTimer = null;\n this.emit('request-next-layout-preload');\n }, preloadDelay);\n\n // Retry at 90% if the 75% attempt couldn't find a layout (e.g. cooldowns\n // hadn't expired yet). The platform handler is idempotent — if a layout\n // is already in the pool it skips, so this is safe even if 75% succeeded.\n this._preloadRetryTimer = setTimeout(() => {\n this._preloadRetryTimer = null;\n this.emit('request-next-layout-preload');\n }, retryDelay);\n }\n\n /**\n * Preload a layout into the pool as a warm (hidden) entry.\n * Creates the full DOM hierarchy (regions + widgets) in a hidden container,\n * pre-fetches media, but does NOT start widget cycling or layout timer.\n *\n * This is called by the platform layer in response to 'request-next-layout-preload'.\n *\n * @param {string} xlfXml - XLF XML content for the layout\n * @param {number} layoutId - Layout ID\n * @returns {Promise<boolean>} true if preload succeeded, false on failure\n */\n hasPreloadedLayout(layoutId) {\n return this.layoutPool.has(layoutId);\n }\n\n async preloadLayout(xlfXml, layoutId) {\n // Don't preload if already in pool\n if (this.layoutPool.has(layoutId)) {\n this.log.info(`Layout ${layoutId} already in preload pool, skipping`);\n return true;\n }\n\n // Don't preload the currently playing layout\n if (this.currentLayoutId === layoutId) {\n this.log.info(`Layout ${layoutId} is current, skipping preload`);\n return true;\n }\n\n try {\n this.log.info(`Preloading layout ${layoutId} into pool...`);\n\n // Parse XLF\n const layout = this.parseXlf(xlfXml);\n\n // Calculate scale factor\n this.calculateScale(layout);\n\n // Create a hidden wrapper container for the preloaded layout\n const wrapper = document.createElement('div');\n wrapper.id = `preload_layout_${layoutId}`;\n wrapper.className = 'renderer-lite-preload-wrapper';\n wrapper.style.position = 'absolute';\n wrapper.style.top = '0';\n wrapper.style.left = '0';\n wrapper.style.width = '100%';\n wrapper.style.height = '100%';\n wrapper.style.visibility = 'hidden';\n wrapper.style.zIndex = '-1'; // Behind everything\n\n // Set background\n wrapper.style.backgroundColor = layout.bgcolor;\n\n // Apply background image if specified\n // With storedAs refactor, background may be a filename or a numeric fileId\n if (layout.background) {\n const saveAs = this.options.fileIdToSaveAs?.get(String(layout.background)) || layout.background;\n this._applyBackgroundImage(wrapper, this._mediaFileUrl(saveAs));\n }\n\n const savedCurrentLayoutId = this.currentLayoutId;\n\n // Create regions in the hidden wrapper\n const preloadRegions = new Map();\n 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 // Set _preloadingLayoutId so trackBlobUrl routes to the correct layout\n // without corrupting currentLayoutId (which other code reads during awaits)\n this._preloadingLayoutId = layoutId;\n\n // Pre-create all widget elements\n for (const [regionId, region] of preloadRegions) {\n for (let i = 0; i < region.widgets.length; i++) {\n const widget = region.widgets[i];\n widget.layoutId = layoutId;\n widget.regionId = regionId;\n\n try {\n const element = await this.createWidgetElement(widget, region);\n this._positionWidgetElement(element);\n region.element.appendChild(element);\n region.widgetElements.set(widget.id, element);\n } catch (error) {\n this.log.error(`Preload: Failed to create widget ${widget.id}:`, error);\n }\n }\n }\n\n // Restore state\n this.currentLayoutId = savedCurrentLayoutId;\n\n // Pause all videos in preloaded layout (autoplay starts them even when hidden)\n wrapper.querySelectorAll('video').forEach(v => v.pause());\n\n // Collect any blob URLs tracked during preload\n const trackedBlobUrls = this.layoutBlobUrls.get(layoutId) || new Set();\n trackedBlobUrls.forEach(url => preloadBlobUrls.add(url));\n\n // Restore original layoutBlobUrls\n this.layoutBlobUrls = savedLayoutBlobUrls;\n\n // Add wrapper to main container (hidden)\n this.container.appendChild(wrapper);\n\n // Add to pool as warm\n this.layoutPool.add(layoutId, {\n container: wrapper,\n layout,\n regions: preloadRegions,\n blobUrls: preloadBlobUrls,\n });\n\n this.log.info(`Layout ${layoutId} preloaded into pool (${preloadRegions.size} regions)`);\n return true;\n\n } catch (error) {\n this.log.error(`Preload failed for layout ${layoutId}:`, error);\n return false;\n }\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 const alreadyEmittedEnd = this.layoutEndEmitted;\n\n this.layoutEndEmitted = false;\n // Keep currentLayout/currentLayoutId until widgets are stopped,\n // so widgetEnd events carry the correct layoutId (not null).\n\n if (oldLayoutId && this.layoutPool.has(oldLayoutId)) {\n // Stop all widgets before evicting (symmetric widgetEnd events)\n this._clearRegionTimers(this.regions);\n this._stopAllRegionWidgets(this.regions, (rid, idx) => this.stopWidget(rid, idx));\n // Old layout was preloaded — evict from pool (safe: removes its wrapper div)\n this.layoutPool.evict(oldLayoutId);\n } else {\n // Old layout was rendered normally — manual cleanup.\n // Region elements live directly in this.container (not a wrapper),\n // so we must remove them individually.\n this._clearRegionTimers(this.regions);\n this._stopAllRegionWidgets(this.regions, (rid, idx) => this.stopWidget(rid, idx));\n for (const [, region] of this.regions) {\n // Release video/audio resources before removing from DOM\n LayoutPool.releaseMediaElements(region.element);\n // Apply region exit transition if configured, then remove\n if (region.config?.exitTransition) {\n const animation = Transitions.apply(\n region.element, region.config.exitTransition, false,\n region.width, region.height\n );\n if (animation) {\n const el = region.element;\n animation.onfinish = () => el.remove();\n } else {\n region.element.remove();\n }\n } else {\n region.element.remove();\n }\n }\n // Revoke blob URLs\n if (oldLayoutId) {\n this.revokeBlobUrlsForLayout(oldLayoutId);\n }\n }\n\n // Now safe to clear old layout state — widgets have been stopped with correct layoutId\n this.currentLayout = null;\n this.currentLayoutId = null;\n this.regions.clear();\n\n // ── Activate preloaded layout ──\n preloaded.container.style.visibility = 'visible';\n preloaded.container.style.zIndex = '0';\n\n // Update renderer state to the preloaded layout\n this.layoutPool.setHot(layoutId);\n this.currentLayout = preloaded.layout;\n this.currentLayoutId = layoutId;\n this.regions = preloaded.regions;\n\n // Emit layoutEnd for old layout AFTER setting new currentLayoutId —\n // the listener guard in main.ts sees the new layout already playing\n // and skips advance, while stats/tracking still run.\n // Skip if the layout timer already emitted layoutEnd (avoids double stats).\n if (oldLayoutId && !alreadyEmittedEnd) {\n this.emit('layoutEnd', oldLayoutId);\n }\n\n // Update container background to match preloaded layout\n this.container.style.backgroundColor = preloaded.layout.bgcolor;\n if (preloaded.container.style.backgroundImage) {\n // Copy background styles from preloaded wrapper to main container\n for (const prop of ['backgroundImage', 'backgroundSize', 'backgroundPosition', 'backgroundRepeat']) {\n this.container.style[prop] = preloaded.container.style[prop];\n }\n } else {\n this.container.style.backgroundImage = '';\n }\n\n // Recalculate scale for the preloaded layout\n this.calculateScale(preloaded.layout);\n\n // Attach interactive action listeners\n this.attachActionListeners(preloaded.layout);\n\n // Emit layout start event\n this.emit('layoutStart', layoutId, preloaded.layout);\n\n // Reset all regions and start widget cycling\n for (const [regionId, region] of this.regions) {\n region.currentIndex = 0;\n region.complete = false;\n this.startRegion(regionId);\n }\n\n // Recalculate layout duration from widget durations.\n // During preload, video loadedmetadata updated widget.duration but\n // updateLayoutDuration() updated this.currentLayout (the old layout),\n // so preloaded.layout.duration may still be the XLF default (e.g. 60s).\n this.updateLayoutDuration();\n\n // Wait for widgets to be ready then start layout timer\n this.startLayoutTimerWhenReady(layoutId, preloaded.layout);\n\n // Schedule next preload (unless updateLayoutDuration already did it)\n if (!this.preloadTimer) {\n this._scheduleNextLayoutPreload(preloaded.layout);\n }\n\n this.log.info(`Swapped to preloaded layout ${layoutId} (instant transition)`);\n }\n\n /**\n * Show a preloaded layout (swap from pool to visible).\n * If no layoutId, shows the most recently preloaded layout.\n * No-ops if the layout is not in the pool.\n * @param {number} [layoutId]\n */\n showLayout(layoutId) {\n if (layoutId === undefined) {\n layoutId = this.layoutPool.getLatest();\n if (layoutId === undefined) {\n this.log.warn('showLayout: no preloaded layout to show');\n return;\n }\n }\n // Same layout already showing — skip swap (self-swap would evict then fail).\n // Same-layout replay is handled by renderLayout's replay path instead.\n if (this.currentLayoutId === layoutId) {\n this.log.info(`showLayout: layout ${layoutId} already showing`);\n return;\n }\n if (!this.layoutPool.has(layoutId)) {\n this.log.warn(`showLayout: layout ${layoutId} not in preload pool`);\n return;\n }\n this._swapToPreloadedLayout(layoutId);\n }\n\n /**\n * Check if 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 const endedLayoutId = this.currentLayoutId;\n const shouldEmit = endedLayoutId && !this.layoutEndEmitted;\n\n this.layoutEndEmitted = false;\n this._deferredTimerLayoutId = null;\n if (this._deferredTimerFallback) {\n clearTimeout(this._deferredTimerFallback);\n this._deferredTimerFallback = null;\n }\n this.currentLayout = null;\n this.currentLayoutId = null;\n\n // Clear timers\n if (this.layoutTimer) {\n clearTimeout(this.layoutTimer);\n this.layoutTimer = null;\n }\n if (this.preloadTimer) {\n clearTimeout(this.preloadTimer);\n this.preloadTimer = null;\n }\n if (this._preloadRetryTimer) {\n clearTimeout(this._preloadRetryTimer);\n this._preloadRetryTimer = null;\n }\n\n // Remove interactive action listeners before teardown\n this.removeActionListeners();\n\n // If layout was preloaded (has its own wrapper div in pool), evict safely.\n // Normally-rendered layouts are NOT in the pool, so we do manual cleanup.\n if (endedLayoutId && this.layoutPool.has(endedLayoutId)) {\n this.layoutPool.evict(endedLayoutId);\n } else {\n // Normally-rendered layout - manual cleanup (regions are in this.container)\n\n // Revoke all blob URLs for this layout (tracked lifecycle management)\n if (endedLayoutId) {\n this.revokeBlobUrlsForLayout(endedLayoutId);\n }\n\n // Stop all regions — use helper to stop ALL started widgets (canvas fix)\n this._clearRegionTimers(this.regions);\n this._stopAllRegionWidgets(this.regions, (rid, idx) => this.stopWidget(rid, idx));\n for (const [, region] of this.regions) {\n // Release video/audio resources before removing from DOM\n LayoutPool.releaseMediaElements(region.element);\n\n // Apply region exit transition if configured, then remove\n if (region.config?.exitTransition) {\n const animation = Transitions.apply(\n region.element, region.config.exitTransition, false,\n region.width, region.height\n );\n if (animation) {\n const el = region.element;\n animation.onfinish = () => el.remove();\n } else {\n region.element.remove();\n }\n } else {\n region.element.remove();\n }\n }\n\n }\n\n this.regions.clear();\n\n // Emit LAST — re-entrant renderLayout() sees currentLayout=null,\n // so stopCurrentLayout() returns early. No cascade.\n if (shouldEmit) {\n this.emit('layoutEnd', endedLayoutId);\n }\n }\n\n /**\n * Render an overlay layout on top of the main layout\n * @param {string} xlfXml - XLF XML content for overlay\n * @param {number} layoutId - Overlay layout ID\n * @param {number} priority - Overlay priority (higher = on top)\n * @returns {Promise<void>}\n */\n async renderOverlay(xlfXml, layoutId, priority = 0) {\n try {\n this.log.info(`Rendering overlay ${layoutId} (priority ${priority})`);\n\n // Check if this overlay is already active\n if (this.activeOverlays.has(layoutId)) {\n this.log.warn(`Overlay ${layoutId} already active, skipping`);\n return;\n }\n\n // Parse XLF\n const layout = this.parseXlf(xlfXml);\n\n // Create overlay container\n const overlayDiv = document.createElement('div');\n overlayDiv.id = `overlay_${layoutId}`;\n overlayDiv.className = 'renderer-lite-overlay';\n overlayDiv.style.position = 'absolute';\n overlayDiv.style.top = '0';\n overlayDiv.style.left = '0';\n overlayDiv.style.width = '100%';\n overlayDiv.style.height = '100%';\n overlayDiv.style.zIndex = String(1000 + priority); // Higher priority = higher z-index\n overlayDiv.style.pointerEvents = 'auto'; // Enable clicks on overlay\n overlayDiv.style.backgroundColor = layout.bgcolor;\n\n // Calculate scale for overlay layout\n this.calculateScale(layout);\n\n // Create regions for overlay\n const overlayRegions = new Map();\n 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 isCanvas: regionConfig.isCanvas || 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 this._positionWidgetElement(element);\n region.element.appendChild(element);\n region.widgetElements.set(widget.id, element);\n } catch (error) {\n this.log.error(`Failed to pre-create overlay widget ${widget.id}:`, error);\n }\n }\n }\n\n // Add overlay to container\n this.overlayContainer.appendChild(overlayDiv);\n\n // Store overlay state\n this.activeOverlays.set(layoutId, {\n container: overlayDiv,\n layout: layout,\n regions: overlayRegions,\n timer: null,\n priority: priority\n });\n\n // Emit overlay start event\n this.emit('overlayStart', layoutId, layout);\n\n // Start all overlay regions\n for (const [regionId, region] of overlayRegions) {\n this.startOverlayRegion(layoutId, regionId);\n }\n\n // Set overlay timer based on duration\n if (layout.duration > 0) {\n const durationMs = layout.duration * 1000;\n const overlayState = this.activeOverlays.get(layoutId);\n if (overlayState) {\n overlayState.timer = setTimeout(() => {\n this.log.info(`Overlay ${layoutId} duration expired (${layout.duration}s)`);\n this.emit('overlayEnd', layoutId);\n }, durationMs);\n }\n }\n\n this.log.info(`Overlay ${layoutId} started`);\n\n } catch (error) {\n this.log.error('Error rendering overlay:', error);\n this.emit('error', { type: 'overlayError', error, layoutId });\n throw error;\n }\n }\n\n /**\n * Start playing an overlay region's widgets\n * @param {number} overlayId - Overlay layout ID\n * @param {string} regionId - Region ID\n */\n startOverlayRegion(overlayId, regionId) {\n const overlayState = this.activeOverlays.get(overlayId);\n if (!overlayState) return;\n\n const region = overlayState.regions.get(regionId);\n this._startRegionCycle(\n region, regionId,\n (rid, idx) => this.renderOverlayWidget(overlayId, rid, idx),\n (rid, idx) => this.stopOverlayWidget(overlayId, rid, idx),\n () => this.log.info(`Overlay ${overlayId} region ${regionId} completed one full cycle`)\n );\n }\n\n /**\n * Render a widget in an overlay region\n * @param {number} overlayId - Overlay layout ID\n * @param {string} regionId - Region ID\n * @param {number} widgetIndex - Widget index in region\n */\n async renderOverlayWidget(overlayId, regionId, widgetIndex) {\n const overlayState = this.activeOverlays.get(overlayId);\n if (!overlayState) return;\n\n const region = overlayState.regions.get(regionId);\n if (!region) return;\n\n try {\n const widget = await this._showWidget(region, widgetIndex);\n if (widget) {\n this.log.info(`Showing overlay widget ${widget.type} (${widget.id}) in overlay ${overlayId} region ${regionId}`);\n this._startedWidgets.add(`overlay:${overlayId}:${regionId}:${widgetIndex}`);\n this.emit('overlayWidgetStart', {\n overlayId, widgetId: widget.id, regionId,\n type: widget.type, duration: widget.duration\n });\n }\n } catch (error) {\n this.log.error(`Error rendering overlay widget:`, error);\n this.emit('error', { type: 'overlayWidgetError', error, widgetId: region.widgets[widgetIndex]?.id, regionId, overlayId });\n }\n }\n\n /**\n * Stop an overlay widget\n * @param {number} overlayId - Overlay layout ID\n * @param {string} regionId - Region ID\n * @param {number} widgetIndex - Widget index\n */\n async stopOverlayWidget(overlayId, regionId, widgetIndex) {\n const key = `overlay:${overlayId}:${regionId}:${widgetIndex}`;\n if (!this._startedWidgets.delete(key)) return; // idempotent\n\n const overlayState = this.activeOverlays.get(overlayId);\n if (!overlayState) return;\n\n const region = overlayState.regions.get(regionId);\n if (!region) return;\n\n const { widget, animPromise } = this._hideWidget(region, widgetIndex);\n // Emit immediately — don't wait for exit animation (same fix as stopWidget)\n if (widget) {\n this.emit('overlayWidgetEnd', {\n overlayId, widgetId: widget.id, regionId, type: widget.type\n });\n }\n if (animPromise) await animPromise;\n }\n\n /**\n * Stop and remove an overlay layout\n * @param {number} layoutId - Overlay layout ID\n */\n stopOverlay(layoutId) {\n const overlayState = this.activeOverlays.get(layoutId);\n if (!overlayState) {\n this.log.warn(`Overlay ${layoutId} not active`);\n return;\n }\n\n this.log.info(`Stopping overlay ${layoutId}`);\n\n // Clear overlay timer\n if (overlayState.timer) {\n clearTimeout(overlayState.timer);\n overlayState.timer = null;\n }\n\n // Stop all overlay regions\n for (const [, region] of overlayState.regions) {\n if (region.timer) { clearTimeout(region.timer); region.timer = null; }\n }\n this._stopAllRegionWidgets(overlayState.regions,\n (rid, idx) => this.stopOverlayWidget(layoutId, rid, idx));\n\n // Remove overlay container from DOM\n if (overlayState.container) {\n overlayState.container.remove();\n }\n\n // Revoke blob URLs for this overlay\n this.revokeBlobUrlsForLayout(layoutId);\n\n // Remove from active overlays\n this.activeOverlays.delete(layoutId);\n\n // Emit overlay end event\n this.emit('overlayEnd', layoutId);\n\n this.log.info(`Overlay ${layoutId} stopped`);\n }\n\n /**\n * Stop all active overlays\n */\n stopAllOverlays() {\n const overlayIds = Array.from(this.activeOverlays.keys());\n for (const overlayId of overlayIds) {\n this.stopOverlay(overlayId);\n }\n this.log.info('All overlays stopped');\n }\n\n /**\n * Get active overlay IDs\n * @returns {Array<number>}\n */\n getActiveOverlays() {\n return Array.from(this.activeOverlays.keys());\n }\n\n /**\n * Pause playback: pause all media, stop widget cycling.\n * The layout timer keeps running — schedule is authoritative.\n */\n pause() {\n if (this._paused) return;\n this._paused = true;\n\n // Stop all region widget-cycling timers\n for (const [, region] of this.regions) {\n if (region.timer) {\n clearTimeout(region.timer);\n region.timer = null;\n }\n }\n\n // Pause all video/audio elements\n this._forEachMedia(el => el.pause());\n\n this.emit('paused');\n this.log.info('Playback paused (layout timer continues)');\n }\n\n /**\n * Check if playback is currently paused.\n */\n isPaused() {\n return this._paused;\n }\n\n /**\n * Resume playback: resume media and widget cycling.\n * Layout timer was never paused — no need to restore it.\n */\n resume() {\n if (!this._paused) return;\n this._paused = false;\n\n // Resume all video/audio\n this._forEachMedia(el => el.play().catch(() => {}));\n\n // Restart region widget cycling (re-enters cycle from current widget)\n for (const [regionId] of this.regions) {\n this.startRegion(regionId);\n }\n\n this.emit('resumed');\n this.log.info('Playback resumed');\n }\n\n /**\n * Apply a function to every video/audio element in all regions.\n */\n _forEachMedia(fn) {\n for (const [, region] of this.regions) {\n region.element?.querySelectorAll('video, audio').forEach(fn);\n }\n }\n\n /**\n * Cleanup renderer\n */\n cleanup() {\n this.stopAllOverlays();\n this.stopCurrentLayout();\n this._startedWidgets.clear();\n\n // Clean up any remaining audio overlays\n for (const widgetId of this.audioOverlays.keys()) {\n this._stopAudioOverlays(widgetId);\n }\n\n // Clear the layout preload pool\n this.layoutPool.clear();\n\n if (this.preloadTimer) {\n clearTimeout(this.preloadTimer);\n this.preloadTimer = null;\n }\n if (this._preloadRetryTimer) {\n clearTimeout(this._preloadRetryTimer);\n this._preloadRetryTimer = null;\n }\n\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n this.resizeObserver = null;\n }\n\n this.container.innerHTML = '';\n this.log.info('Cleaned up');\n }\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Layout translator - XLF to HTML\n * Based on arexibo layout.rs\n */\n\nimport { cacheWidgetHtml } from '@xiboplayer/cache';\nimport { createLogger, isDebug, PLAYER_API } from '@xiboplayer/utils';\n\nconst log = createLogger('Layout');\n\nexport class LayoutTranslator {\n constructor(xmds) {\n this.xmds = xmds;\n }\n\n /**\n * Translate XLF XML to playable HTML\n */\n async translateXLF(layoutId, xlfXml) {\n const parser = new DOMParser();\n const doc = parser.parseFromString(xlfXml, 'text/xml');\n\n const layoutEl = doc.querySelector('layout');\n if (!layoutEl) {\n throw new Error('Invalid XLF: no <layout> element');\n }\n\n const width = parseInt(layoutEl.getAttribute('width') || '1920');\n const height = parseInt(layoutEl.getAttribute('height') || '1080');\n const bgcolor = layoutEl.getAttribute('bgcolor') || '#000000';\n\n const regions = [];\n for (const regionEl of doc.querySelectorAll('region')) {\n regions.push(await this.translateRegion(layoutId, regionEl));\n }\n\n return this.generateHTML(width, height, bgcolor, regions);\n }\n\n /**\n * Translate a single region\n */\n async translateRegion(layoutId, regionEl) {\n const id = regionEl.getAttribute('id');\n const width = parseInt(regionEl.getAttribute('width'));\n const height = parseInt(regionEl.getAttribute('height'));\n const top = parseInt(regionEl.getAttribute('top'));\n const left = parseInt(regionEl.getAttribute('left'));\n const zindex = parseInt(regionEl.getAttribute('zindex') || '0');\n\n const media = [];\n for (const mediaEl of regionEl.querySelectorAll('media')) {\n media.push(await this.translateMedia(layoutId, id, mediaEl));\n }\n\n return {\n id,\n width,\n height,\n top,\n left,\n zindex,\n media\n };\n }\n\n /**\n * Translate a single media item\n */\n async translateMedia(layoutId, regionId, mediaEl) {\n const type = mediaEl.getAttribute('type');\n const duration = parseInt(mediaEl.getAttribute('duration') || '10');\n const id = mediaEl.getAttribute('id');\n\n const optionsEl = mediaEl.querySelector('options');\n const rawEl = mediaEl.querySelector('raw');\n\n const options = {};\n if (optionsEl) {\n for (const child of optionsEl.children) {\n options[child.tagName] = child.textContent;\n }\n }\n\n // Parse transition information\n const transitions = {\n in: null,\n out: null\n };\n\n const transInEl = mediaEl.querySelector('options > transIn');\n const transOutEl = mediaEl.querySelector('options > transOut');\n const transInDurationEl = mediaEl.querySelector('options > transInDuration');\n const transOutDurationEl = mediaEl.querySelector('options > transOutDuration');\n const transInDirectionEl = mediaEl.querySelector('options > transInDirection');\n const transOutDirectionEl = mediaEl.querySelector('options > transOutDirection');\n\n if (transInEl && transInEl.textContent) {\n transitions.in = {\n type: transInEl.textContent,\n duration: parseInt(transInDurationEl?.textContent || '1000'),\n direction: transInDirectionEl?.textContent || 'N'\n };\n }\n\n if (transOutEl && transOutEl.textContent) {\n transitions.out = {\n type: transOutEl.textContent,\n duration: parseInt(transOutDurationEl?.textContent || '1000'),\n direction: transOutDirectionEl?.textContent || 'N'\n };\n }\n\n // All videos use cache URL pattern\n // Large videos download in background, small videos are already cached\n // Service Worker handles both cases appropriately\n\n let raw = rawEl ? rawEl.textContent : '';\n\n // For widgets (clock, calendar, etc.), fetch rendered HTML from CMS\n const widgetTypes = ['clock', 'clock-digital', 'clock-analogue', 'calendar', 'weather',\n 'currencies', 'stocks', 'twitter', 'global', 'embedded', 'text', 'ticker'];\n if (widgetTypes.some(w => type.includes(w))) {\n // Try to get widget HTML with retry logic for kiosk reliability\n 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 from ContentStore via proxy\n try {\n const resp = await fetch(`/store${PLAYER_API}/widgets/${layoutId}/${regionId}/${id}`);\n if (resp.ok) {\n raw = await resp.text();\n options.widgetCacheKey = `${PLAYER_API}/widgets/${layoutId}/${regionId}/${id}`;\n log.info(`Using stored widget HTML (${raw.length} chars) - CMS update pending`);\n } else {\n log.error(`No stored version available for widget ${id}`);\n raw = `<div style=\"display:flex;align-items:center;justify-content:center;height:100%;color:#999;font-size:18px;\">Content updating...</div>`;\n }\n } catch (storeError) {\n log.error('Store fallback failed:', storeError);\n raw = `<div style=\"display:flex;align-items:center;justify-content:center;height:100%;color:#999;font-size:18px;\">Content updating...</div>`;\n }\n }\n }\n\n return {\n type,\n duration,\n id,\n options,\n raw,\n transitions\n };\n }\n\n /**\n * Generate HTML from parsed layout\n */\n generateHTML(width, height, bgcolor, regions) {\n const regionHTML = regions.map(r => this.generateRegionHTML(r)).join('\\n');\n const regionJS = regions.map(r => this.generateRegionJS(r)).join(',\\n');\n\n return `<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=${width}, height=${height}\">\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n html, body { width: 100%; height: 100%; overflow: hidden; }\n body { background-color: ${bgcolor}; }\n .region {\n position: absolute;\n overflow: hidden;\n }\n .media {\n width: 100%;\n height: 100%;\n object-fit: contain;\n }\n iframe {\n border: none;\n width: 100%;\n height: 100%;\n }\n </style>\n</head>\n<body>\n${regionHTML}\n<script>\n// Transition utilities\nwindow.Transitions = {\n fadeIn(element, duration) {\n const keyframes = [\n { opacity: 0 },\n { opacity: 1 }\n ];\n const timing = {\n duration: duration,\n easing: 'linear',\n fill: 'forwards'\n };\n return element.animate(keyframes, timing);\n },\n\n fadeOut(element, duration) {\n const keyframes = [\n { opacity: 1 },\n { opacity: 0 }\n ];\n const timing = {\n duration: duration,\n easing: 'linear',\n fill: 'forwards'\n };\n return element.animate(keyframes, timing);\n },\n\n getFlyKeyframes(direction, width, height, isIn) {\n const dirMap = {\n 'N': { x: 0, y: isIn ? -height : height },\n 'NE': { x: isIn ? width : -width, y: isIn ? -height : height },\n 'E': { x: isIn ? width : -width, y: 0 },\n 'SE': { x: isIn ? width : -width, y: isIn ? height : -height },\n 'S': { x: 0, y: isIn ? height : -height },\n 'SW': { x: isIn ? -width : width, y: isIn ? height : -height },\n 'W': { x: isIn ? -width : width, y: 0 },\n 'NW': { x: isIn ? -width : width, y: isIn ? -height : height }\n };\n\n const offset = dirMap[direction] || dirMap['N'];\n\n if (isIn) {\n return [\n { transform: \\`translate(\\${offset.x}px, \\${offset.y}px)\\`, opacity: 0 },\n { transform: 'translate(0, 0)', opacity: 1 }\n ];\n } else {\n return [\n { transform: 'translate(0, 0)', opacity: 1 },\n { transform: \\`translate(\\${offset.x}px, \\${offset.y}px)\\`, opacity: 0 }\n ];\n }\n },\n\n flyIn(element, duration, direction, regionWidth, regionHeight) {\n const keyframes = this.getFlyKeyframes(direction, regionWidth, regionHeight, true);\n const timing = {\n duration: duration,\n easing: 'ease-out',\n fill: 'forwards'\n };\n return element.animate(keyframes, timing);\n },\n\n flyOut(element, duration, direction, regionWidth, regionHeight) {\n const keyframes = this.getFlyKeyframes(direction, regionWidth, regionHeight, false);\n const timing = {\n duration: duration,\n easing: 'ease-in',\n fill: 'forwards'\n };\n return element.animate(keyframes, timing);\n },\n\n apply(element, transitionConfig, isIn, regionWidth, regionHeight) {\n if (!transitionConfig || !transitionConfig.type) {\n return null;\n }\n\n const type = transitionConfig.type.toLowerCase();\n const duration = transitionConfig.duration || 1000;\n const direction = transitionConfig.direction || 'N';\n\n switch (type) {\n case 'fade':\n return isIn ? this.fadeIn(element, duration) : this.fadeOut(element, duration);\n case 'fadein':\n return isIn ? this.fadeIn(element, duration) : null;\n case 'fadeout':\n return isIn ? null : this.fadeOut(element, duration);\n case 'fly':\n return isIn\n ? this.flyIn(element, duration, direction, regionWidth, regionHeight)\n : this.flyOut(element, duration, direction, regionWidth, regionHeight);\n case 'flyin':\n return isIn ? this.flyIn(element, duration, direction, regionWidth, regionHeight) : null;\n case 'flyout':\n return isIn ? null : this.flyOut(element, duration, direction, regionWidth, regionHeight);\n default:\n return null;\n }\n }\n};\n\nconst regions = {\n${regionJS}\n};\n\n// Auto-start all regions\nObject.keys(regions).forEach(id => {\n playRegion(id);\n});\n\n// Track active timers per region so layout teardown can cancel them\nconst regionTimers = {};\n\nfunction playRegion(id) {\n const region = regions[id];\n if (!region || region.media.length === 0) return;\n\n regionTimers[id] = [];\n\n // If only one media item, just show it and don't cycle (arexibo behavior)\n if (region.media.length === 1) {\n const media = region.media[0];\n if (media.start) media.start();\n return; // Don't schedule stop/restart\n }\n\n // Multiple media items - cycle normally\n let currentIndex = 0;\n\n function playNext() {\n const media = region.media[currentIndex];\n if (media.start) media.start();\n\n const duration = media.duration || 10;\n const timerId = setTimeout(() => {\n if (media.stop) media.stop();\n currentIndex = (currentIndex + 1) % region.media.length;\n playNext();\n }, duration * 1000);\n regionTimers[id].push(timerId);\n }\n\n playNext();\n}\n\n// Cleanup function — called before layout teardown\nwindow._stopAllRegions = function() {\n Object.values(regionTimers).forEach(timers => timers.forEach(t => clearTimeout(t)));\n};\n</script>\n</body>\n</html>`;\n }\n\n /**\n * Generate HTML for a region container\n */\n generateRegionHTML(region) {\n return ` <div id=\"region_${region.id}\" class=\"region\" style=\"\n left: ${region.left}px;\n top: ${region.top}px;\n width: ${region.width}px;\n height: ${region.height}px;\n z-index: ${region.zindex};\n \"></div>`;\n }\n\n /**\n * Generate JavaScript for region media control\n */\n generateRegionJS(region) {\n const mediaJS = region.media.map(m => this.generateMediaJS(m, region.id)).join(',\\n ');\n\n return ` '${region.id}': {\n media: [\n${mediaJS}\n ]\n }`;\n }\n\n /**\n * Generate iframe widget JS for text/ticker and generic widget types.\n * Returns { startFn, stopFn } strings for the media item.\n */\n _generateIframeWidgetJS(regionId, mediaId, widgetUrl, transIn, transOut) {\n const iframeId = `widget_${regionId}_${mediaId}`;\n const startFn = `() => {\n const region = document.getElementById('region_${regionId}');\n let iframe = document.getElementById('${iframeId}');\n if (!iframe) {\n iframe = document.createElement('iframe');\n iframe.id = '${iframeId}';\n iframe.src = '${widgetUrl}';\n iframe.style.width = '100%';\n iframe.style.height = '100%';\n iframe.style.border = 'none';\n iframe.scrolling = 'no';\n iframe.style.opacity = '0';\n region.innerHTML = '';\n region.appendChild(iframe);\n\n // Apply transition after iframe loads\n iframe.onload = () => {\n const transIn = ${transIn};\n if (transIn && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n window.Transitions.apply(iframe, transIn, true, regionRect.width, regionRect.height);\n } else {\n iframe.style.opacity = '1';\n }\n };\n } else {\n iframe.style.display = 'block';\n iframe.style.opacity = '1';\n }\n }`;\n const stopFn = `() => {\n const region = document.getElementById('region_${regionId}');\n const iframe = document.getElementById('${iframeId}');\n if (iframe) {\n const transOut = ${transOut};\n if (transOut && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n const animation = window.Transitions.apply(iframe, transOut, false, regionRect.width, regionRect.height);\n if (animation) {\n animation.onfinish = () => {\n iframe.style.display = 'none';\n };\n return;\n }\n }\n iframe.style.display = 'none';\n }\n }`;\n return { startFn, stopFn };\n }\n\n /**\n * Generate JavaScript for a single media item\n */\n generateMediaJS(media, regionId) {\n const duration = media.duration || 10;\n const transIn = media.transitions?.in ? JSON.stringify(media.transitions.in) : 'null';\n const transOut = media.transitions?.out ? JSON.stringify(media.transitions.out) : 'null';\n let startFn = 'null';\n let stopFn = 'null';\n\n switch (media.type) {\n case 'image':\n // Use absolute URL within service worker scope\n const imageSrc = `${window.location.origin}${PLAYER_API}/media/${media.options.uri}`;\n startFn = `() => {\n const region = document.getElementById('region_${regionId}');\n const img = document.createElement('img');\n img.className = 'media';\n img.src = '${imageSrc}';\n img.style.opacity = '0';\n region.innerHTML = '';\n region.appendChild(img);\n\n // Apply transition\n const transIn = ${transIn};\n if (transIn && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n window.Transitions.apply(img, transIn, true, regionRect.width, regionRect.height);\n } else {\n img.style.opacity = '1';\n }\n }`;\n break;\n\n case 'video':\n // All videos use cache URL pattern\n // Background-downloaded videos will auto-reload when cache completes\n const videoSrc = `${window.location.origin}${PLAYER_API}/media/${media.options.uri}`;\n const videoFilename = media.options.uri;\n\n startFn = `() => {\n const region = document.getElementById('region_${regionId}');\n const video = document.createElement('video');\n video.className = 'media';\n video.src = '${videoSrc}';\n video.dataset.filename = '${videoFilename}';\n video.autoplay = true;\n video.muted = ${media.options.mute === '1' ? 'true' : 'false'};\n video.loop = false;\n video.style.width = '100%';\n video.style.height = '100%';\n video.style.objectFit = 'contain';\n video.style.opacity = '0';\n\n // Retry loading if cache completes while video is playing\n const retryOnCache = (event) => {\n if (event.detail.filename === '${videoFilename}' && video.error) {\n console.log('[Video] Cache complete, reloading:', '${videoFilename}');\n video.load();\n video.play();\n }\n };\n video._retryOnCache = retryOnCache;\n window.addEventListener('media-cached', retryOnCache);\n\n region.innerHTML = '';\n region.appendChild(video);\n\n // Apply transition\n const transIn = ${transIn};\n if (transIn && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n window.Transitions.apply(video, transIn, true, regionRect.width, regionRect.height);\n } else {\n video.style.opacity = '1';\n }\n\n console.log('[Video] Playing:', '${media.options.uri}');\n }`;\n stopFn = `() => {\n const region = document.getElementById('region_${regionId}');\n const video = document.querySelector('#region_${regionId} video');\n if (video) {\n // Remove global media-cached listener to prevent leak\n if (video._retryOnCache) {\n window.removeEventListener('media-cached', video._retryOnCache);\n video._retryOnCache = null;\n }\n const transOut = ${transOut};\n if (transOut && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n const animation = window.Transitions.apply(video, transOut, false, regionRect.width, regionRect.height);\n if (animation) {\n animation.onfinish = () => {\n video.pause();\n video.remove();\n };\n return;\n }\n }\n video.pause();\n video.remove();\n }\n }`;\n break;\n\n case 'text':\n case 'ticker':\n // Text/ticker widgets use the same iframe pattern as default widgets.\n // If no widgetCacheKey, fall through to the default case which handles unsupported types.\n if (media.options.widgetCacheKey) {\n const textUrl = `${window.location.origin}${media.options.widgetCacheKey}`;\n const iframe = this._generateIframeWidgetJS(regionId, media.id, textUrl, transIn, transOut);\n startFn = iframe.startFn;\n stopFn = iframe.stopFn;\n break;\n }\n // Fall through to default (handles missing widgetCacheKey as unsupported)\n\n case 'audio':\n const audioSrc = `${window.location.origin}${PLAYER_API}/media/${media.options.uri}`;\n const audioId = `audio_${regionId}_${media.id}`;\n const audioLoop = media.options.loop === '1';\n const audioVolume = (parseInt(media.options.volume || '100') / 100).toFixed(2);\n\n startFn = `() => {\n const region = document.getElementById('region_${regionId}');\n\n // Create audio element\n const audio = document.createElement('audio');\n audio.id = '${audioId}';\n audio.className = 'media';\n audio.src = '${audioSrc}';\n audio.autoplay = true;\n audio.loop = ${audioLoop};\n audio.volume = ${audioVolume};\n\n // Create visual feedback container\n const visualContainer = document.createElement('div');\n visualContainer.className = 'audio-visual';\n visualContainer.style.cssText = \\`\n width: 100%;\n height: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n opacity: 0;\n \\`;\n\n // Audio icon\n const icon = document.createElement('div');\n icon.innerHTML = '♪';\n icon.style.cssText = \\`\n font-size: 120px;\n color: white;\n margin-bottom: 20px;\n animation: pulse 2s ease-in-out infinite;\n \\`;\n\n // Audio info\n const info = document.createElement('div');\n info.style.cssText = \\`\n color: white;\n font-size: 24px;\n text-align: center;\n padding: 0 20px;\n \\`;\n info.textContent = 'Playing Audio';\n\n // Filename\n const filename = document.createElement('div');\n filename.style.cssText = \\`\n color: rgba(255,255,255,0.7);\n font-size: 16px;\n margin-top: 10px;\n \\`;\n filename.textContent = '${media.options.uri}';\n\n visualContainer.appendChild(icon);\n visualContainer.appendChild(info);\n visualContainer.appendChild(filename);\n\n region.innerHTML = '';\n region.appendChild(audio);\n region.appendChild(visualContainer);\n\n // Add pulse animation\n const style = document.createElement('style');\n style.textContent = \\`\n @keyframes pulse {\n 0%, 100% { transform: scale(1); opacity: 1; }\n 50% { transform: scale(1.1); opacity: 0.8; }\n }\n \\`;\n document.head.appendChild(style);\n\n // Apply transition\n const transIn = ${transIn};\n if (transIn && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n window.Transitions.apply(visualContainer, transIn, true, regionRect.width, regionRect.height);\n } else {\n visualContainer.style.opacity = '1';\n }\n\n console.log('[Audio] Playing:', '${audioSrc}', 'Volume:', ${audioVolume}, 'Loop:', ${audioLoop});\n }`;\n\n stopFn = `() => {\n const audio = document.getElementById('${audioId}');\n if (audio) {\n audio.pause();\n audio.remove();\n }\n const region = document.getElementById('region_${regionId}');\n if (region) {\n const visualContainer = region.querySelector('.audio-visual');\n if (visualContainer) {\n const transOut = ${transOut};\n if (transOut && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n const animation = window.Transitions.apply(visualContainer, transOut, false, regionRect.width, regionRect.height);\n if (animation) {\n animation.onfinish = () => visualContainer.remove();\n return;\n }\n }\n visualContainer.remove();\n }\n }\n }`;\n break;\n\n case 'pdf':\n const pdfSrc = `${window.location.origin}${PLAYER_API}/media/${media.options.uri}`;\n const pdfContainerId = `pdf_${regionId}_${media.id}`;\n const pdfDuration = duration; // Total duration for entire PDF\n\n startFn = `async () => {\n const container = document.createElement('div');\n container.className = 'media pdf-container';\n container.id = '${pdfContainerId}';\n container.style.width = '100%';\n container.style.height = '100%';\n container.style.overflow = 'hidden';\n container.style.backgroundColor = '#525659';\n container.style.opacity = '0';\n container.style.position = 'relative';\n\n const region = document.getElementById('region_${regionId}');\n region.innerHTML = '';\n region.appendChild(container);\n\n // Load PDF.js if not already loaded\n if (typeof pdfjsLib === 'undefined') {\n try {\n const pdfjsModule = await import('pdfjs-dist');\n window.pdfjsLib = pdfjsModule;\n pdfjsLib.GlobalWorkerOptions.workerSrc = '${window.location.origin}/player/pdf.worker.min.mjs';\n } catch (error) {\n console.error('[PDF] Failed to load PDF.js:', error);\n container.innerHTML = '<div style=\"color:white;padding:20px;text-align:center;\">PDF viewer unavailable</div>';\n return;\n }\n }\n\n // Render PDF with multi-page support\n try {\n const loadingTask = pdfjsLib.getDocument('${pdfSrc}');\n const pdf = await loadingTask.promise;\n const totalPages = pdf.numPages;\n\n // Calculate time per page (distribute total duration across all pages)\n const timePerPage = (${pdfDuration} * 1000) / totalPages; // milliseconds per page\n\n console.log(\\`[PDF] Loading: \\${totalPages} pages, \\${timePerPage}ms per page\\`);\n\n const containerWidth = container.offsetWidth || ${width};\n const containerHeight = container.offsetHeight || ${height};\n\n // Create page indicator\n const pageIndicator = document.createElement('div');\n pageIndicator.className = 'pdf-page-indicator';\n pageIndicator.style.cssText = \\`\n position: absolute;\n bottom: 10px;\n right: 10px;\n background: rgba(0,0,0,0.7);\n color: white;\n padding: 8px 12px;\n border-radius: 4px;\n font-size: 14px;\n z-index: 10;\n display: ${isDebug() ? 'block' : 'none'};\n \\`;\n container.appendChild(pageIndicator);\n\n let currentPage = 1;\n let pageTimers = [];\n\n // Function to render a single page\n async function renderPage(pageNum) {\n const page = await pdf.getPage(pageNum);\n const viewport = page.getViewport({ scale: 1 });\n\n // Calculate scale to fit page within container\n const scaleX = containerWidth / viewport.width;\n const scaleY = containerHeight / viewport.height;\n const scale = Math.min(scaleX, scaleY);\n\n const scaledViewport = page.getViewport({ scale });\n\n const canvas = document.createElement('canvas');\n canvas.className = 'pdf-page';\n const context = canvas.getContext('2d');\n canvas.width = scaledViewport.width;\n canvas.height = scaledViewport.height;\n\n // Center canvas in container\n canvas.style.cssText = \\`\n display: block;\n margin: auto;\n margin-top: \\${Math.max(0, (containerHeight - scaledViewport.height) / 2)}px;\n position: absolute;\n top: 0;\n left: 50%;\n transform: translateX(-50%);\n opacity: 0;\n transition: opacity 0.5s ease-in-out;\n \\`;\n\n container.appendChild(canvas);\n\n await page.render({\n canvasContext: context,\n viewport: scaledViewport\n }).promise;\n\n // Fade in new page\n setTimeout(() => canvas.style.opacity = '1', 50);\n\n return canvas;\n }\n\n // Function to cycle through pages\n async function cyclePage() {\n // Update page indicator\n pageIndicator.textContent = \\`Page \\${currentPage} / \\${totalPages}\\`;\n\n // Remove old pages\n const oldPages = container.querySelectorAll('.pdf-page');\n oldPages.forEach(oldPage => {\n if (oldPage !== container.lastChild) {\n oldPage.style.opacity = '0';\n setTimeout(() => oldPage.remove(), 500);\n }\n });\n\n // Render current page\n await renderPage(currentPage);\n\n console.log(\\`[PDF] Showing page \\${currentPage}/\\${totalPages}\\`);\n\n // Schedule next page\n if (totalPages > 1) {\n const timer = setTimeout(() => {\n currentPage = currentPage >= totalPages ? 1 : currentPage + 1;\n cyclePage();\n }, timePerPage);\n pageTimers.push(timer);\n }\n }\n\n // Store live timer array on element for cleanup (not JSON — stays current)\n container._pageTimers = pageTimers;\n\n // Start cycling\n await cyclePage();\n\n // Apply transition to container\n const transIn = ${transIn};\n if (transIn && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n window.Transitions.apply(container, transIn, true, regionRect.width, regionRect.height);\n } else {\n container.style.opacity = '1';\n }\n\n } catch (error) {\n console.error('[PDF] Render failed:', error);\n container.innerHTML = '<div style=\"color:white;padding:20px;text-align:center;\">Failed to load PDF</div>';\n container.style.opacity = '1';\n }\n }`;\n\n stopFn = `() => {\n const region = document.getElementById('region_${regionId}');\n const container = document.getElementById('${pdfContainerId}');\n if (container) {\n // Clear page cycling timers (live array, always current)\n if (container._pageTimers) {\n container._pageTimers.forEach(t => clearTimeout(t));\n container._pageTimers.length = 0;\n }\n\n const transOut = ${transOut};\n if (transOut && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n const animation = window.Transitions.apply(container, transOut, false, regionRect.width, regionRect.height);\n if (animation) {\n animation.onfinish = () => {\n container.remove();\n };\n return;\n }\n }\n container.remove();\n }\n }`;\n break;\n\n case 'webpage':\n const url = decodeURIComponent(media.options.uri || '');\n startFn = `() => {\n const region = document.getElementById('region_${regionId}');\n const iframe = document.createElement('iframe');\n iframe.src = '${url}';\n iframe.style.opacity = '0';\n region.innerHTML = '';\n region.appendChild(iframe);\n\n // Apply transition after iframe loads\n iframe.onload = () => {\n const transIn = ${transIn};\n if (transIn && window.Transitions) {\n const regionRect = region.getBoundingClientRect();\n window.Transitions.apply(iframe, transIn, true, regionRect.width, regionRect.height);\n } else {\n iframe.style.opacity = '1';\n }\n };\n }`;\n break;\n\n default:\n // Widgets (clock, calendar, weather, etc.) - use cache URL pattern in /player/ scope for SW\n // Keep widget iframes alive across duration cycles (arexibo behavior)\n if (media.options.widgetCacheKey) {\n const widgetUrl = `${window.location.origin}${media.options.widgetCacheKey}`;\n const iframe = this._generateIframeWidgetJS(regionId, media.id, widgetUrl, transIn, transOut);\n startFn = iframe.startFn;\n stopFn = iframe.stopFn;\n } else {\n log.warn(`Unsupported media type: ${media.type}`);\n startFn = `() => console.log('Unsupported media type: ${media.type}')`;\n }\n }\n\n return ` {\n start: ${startFn},\n stop: ${stopFn},\n duration: ${duration}\n }`;\n }\n}\n\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * DataConnectorManager - Manages real-time data connectors from CMS\n *\n * Data connectors allow widgets to receive real-time data from CMS-configured\n * data sources. The CMS sends data connector configuration via the schedule XML,\n * and this manager periodically polls the data source URLs, stores the data,\n * and emits events so the IC /realtime route can serve it to widgets.\n *\n * Usage:\n * const manager = new DataConnectorManager();\n * manager.setConnectors(schedule.dataConnectors);\n * manager.startPolling();\n *\n * // Get data for a widget\n * const data = manager.getData('weather_data');\n *\n * // Listen for updates\n * manager.on('data-updated', (dataKey, data) => { ... });\n */\n\nimport { EventEmitter, createLogger, fetchWithRetry } from '@xiboplayer/utils';\n\nconst log = createLogger('DataConnector');\n\nconst MAX_BACKOFF_MS = 300000; // 5 minutes\nconst CIRCUIT_BREAKER_THRESHOLD = 3;\n\nexport class DataConnectorManager extends EventEmitter {\n constructor() {\n super();\n\n // dataKey -> { config, data, timer, lastFetch, failures }\n this.connectors = new Map();\n }\n\n /**\n * Set active connectors from schedule\n * Stops any existing polling and reconfigures with new connector list.\n * @param {Array} connectors - Array of connector config objects from schedule XML\n * Each: { id, dataConnectorId, dataKey, url, updateInterval }\n */\n setConnectors(connectors) {\n // Stop existing polling before reconfiguring\n this.stopPolling();\n\n // Clear previous connectors\n this.connectors.clear();\n\n if (!connectors || connectors.length === 0) {\n log.debug('No data connectors configured');\n return;\n }\n\n for (const connector of connectors) {\n if (!connector.dataKey || !connector.url) {\n log.warn('Skipping data connector with missing dataKey or url:', connector);\n continue;\n }\n\n this.connectors.set(connector.dataKey, {\n config: connector,\n data: null,\n timer: null,\n lastFetch: null,\n failures: 0\n });\n\n log.info(`Registered data connector: ${connector.dataKey} (interval: ${connector.updateInterval}s)`);\n }\n\n log.info(`${this.connectors.size} data connector(s) configured`);\n }\n\n /**\n * Start polling for all active connectors\n * Performs an initial fetch immediately, then sets up periodic polling.\n */\n startPolling() {\n for (const [dataKey, entry] of this.connectors.entries()) {\n const { config } = entry;\n const intervalMs = (config.updateInterval || 300) * 1000;\n\n // Fetch immediately on start\n this.fetchData(entry).catch(err => {\n log.error(`Initial fetch failed for ${dataKey}:`, err);\n });\n\n // Set up periodic polling\n entry.timer = setInterval(() => {\n this.fetchData(entry).catch(err => {\n log.error(`Polling fetch failed for ${dataKey}:`, err);\n });\n }, intervalMs);\n\n log.debug(`Started polling for ${dataKey} every ${config.updateInterval}s`);\n }\n }\n\n /**\n * Stop all polling timers\n */\n stopPolling() {\n for (const [dataKey, entry] of this.connectors.entries()) {\n if (entry.timer) {\n clearInterval(entry.timer);\n entry.timer = null;\n log.debug(`Stopped polling for ${dataKey}`);\n }\n }\n }\n\n /**\n * Get current data for a dataKey\n * @param {string} dataKey - The data key to look up\n * @returns {Object|null} The stored data, or null if not available\n */\n getData(dataKey) {\n const entry = this.connectors.get(dataKey);\n if (!entry) {\n log.debug(`No data connector found for key: ${dataKey}`);\n return null;\n }\n return entry.data;\n }\n\n /**\n * Get all data keys that have data available\n * @returns {string[]} Array of data keys with data\n */\n getAvailableKeys() {\n const keys = [];\n for (const [dataKey, entry] of this.connectors.entries()) {\n if (entry.data !== null) {\n keys.push(dataKey);\n }\n }\n return keys;\n }\n\n /**\n * Internal: fetch data from CMS data source\n * @param {Object} entry - Connector entry from this.connectors\n */\n async fetchData(entry) {\n const { config } = entry;\n const { dataKey, url } = config;\n\n log.debug(`Fetching data for ${dataKey}: ${url}`);\n\n try {\n const response = await fetchWithRetry(url, {\n method: 'GET',\n headers: {\n 'Accept': 'application/json'\n }\n }, { maxRetries: 2, baseDelayMs: 2000 });\n\n if (!response.ok) {\n log.warn(`Data connector ${dataKey} returned ${response.status}: ${response.statusText}`);\n return;\n }\n\n const contentType = response.headers.get('Content-Type') || '';\n let data;\n\n if (contentType.includes('application/json')) {\n data = await response.json();\n } else {\n // Store as raw text if not JSON\n data = await response.text();\n }\n\n const previousData = entry.data;\n entry.data = data;\n entry.lastFetch = Date.now();\n entry.failures = 0; // Reset on success\n\n log.debug(`Data updated for ${dataKey} (fetched at ${new Date(entry.lastFetch).toISOString()})`);\n\n // Restore normal polling interval if it was backed off\n this._ensureNormalPolling(entry);\n\n // Emit event for listeners (IC route, platform layer)\n this.emit('data-updated', dataKey, data);\n\n // Emit a specific event if data actually changed\n if (JSON.stringify(previousData) !== JSON.stringify(data)) {\n this.emit('data-changed', dataKey, data);\n }\n\n } catch (error) {\n entry.failures = (entry.failures || 0) + 1;\n log.error(`Failed to fetch data for ${dataKey} (${entry.failures}x):`, error);\n this.emit('fetch-error', dataKey, error);\n\n // Circuit breaker: slow down polling after repeated failures\n if (entry.failures >= CIRCUIT_BREAKER_THRESHOLD && entry.timer) {\n const baseMs = (config.updateInterval || 300) * 1000;\n const backoffMs = Math.min(baseMs * Math.pow(2, entry.failures - CIRCUIT_BREAKER_THRESHOLD + 1), MAX_BACKOFF_MS);\n clearInterval(entry.timer);\n entry.timer = setTimeout(() => {\n this.fetchData(entry).catch(() => {});\n // Re-arm with backoff interval\n entry.timer = setInterval(() => {\n this.fetchData(entry).catch(() => {});\n }, backoffMs);\n }, backoffMs);\n log.warn(`Circuit breaker: ${dataKey} backing off to ${Math.round(backoffMs / 1000)}s`);\n }\n }\n }\n\n /**\n * Restore normal polling interval after circuit breaker backoff.\n * @private\n */\n _ensureNormalPolling(entry) {\n if (entry.failures === 0 && entry.timer) {\n const baseMs = (entry.config.updateInterval || 300) * 1000;\n // Clear any backed-off timer and restore the normal interval\n clearInterval(entry.timer);\n clearTimeout(entry.timer);\n entry.timer = setInterval(() => {\n this.fetchData(entry).catch(() => {});\n }, baseMs);\n }\n }\n\n /**\n * Force refresh all connectors — re-fetch immediately and restart polling.\n * Called by XMR dataUpdate command.\n */\n refreshAll() {\n if (this.connectors.size === 0) return;\n\n log.info(`Refreshing all ${this.connectors.size} data connector(s)`);\n this.stopPolling();\n this.startPolling();\n }\n\n /**\n * Cleanup - stop all polling and remove listeners\n */\n cleanup() {\n this.stopPolling();\n this.connectors.clear();\n this.removeAllListeners();\n log.debug('DataConnectorManager cleaned up');\n }\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * 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 } from '@xiboplayer/schedule';\nimport { CacheAnalyzer } from '@xiboplayer/cache';\nimport { DataConnectorManager } from './data-connectors.js';\n\nconst log = createLogger('PlayerCore');\n\n/**\n * Discover a local/LAN IP address.\n * Electron: os.networkInterfaces() via preload (reliable, skips VPN/Docker).\n * Browser: not supported (WebRTC returns mDNS or wrong interface).\n */\nasync function discoverLanIp() {\n if (typeof window !== 'undefined' && window.electronAPI?.getLanIpAddress) {\n try { return await window.electronAPI.getLanIpAddress(); } catch (_) {}\n }\n return '';\n}\n\n// IndexedDB database/store for offline cache\nconst OFFLINE_DB_BASE = 'xibo-offline-cache';\nconst OFFLINE_DB_VERSION = 1;\nconst OFFLINE_STORE = 'cache';\n\n/** 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(cmsId) {\n const dbName = cmsId ? `${OFFLINE_DB_BASE}-${cmsId}` : OFFLINE_DB_BASE;\n return new Promise((resolve, reject) => {\n const req = indexedDB.open(dbName, 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 // CMS ID for namespaced IndexedDB databases\n this._cmsId = options.cmsId || null;\n\n // Data connectors manager (real-time data for widgets)\n this.dataConnectorManager = new DataConnectorManager();\n\n // Discover LAN IP early (async, non-blocking)\n discoverLanIp().then((ip) => {\n this._lanIpAddress = ip;\n log.info('LAN IP:', ip || '(not discovered)');\n });\n\n // State\n this.xmr = null;\n this.currentLayoutId = null;\n this.collecting = false;\n this.collectionInterval = null;\n this.pendingLayouts = new Map(); // layoutId -> required media IDs\n this._layoutMediaStatus = new Map(); // layoutFile → { ready: boolean, missing: string[] }\n this.offlineMode = false; // Track whether we're currently in offline mode\n this._normalCollectInterval = null; // Saved interval to restore after offline retry\n this._offlineRetrySeconds = 0; // Current backoff interval (0 = not retrying)\n\n // CRC32 checksums for skip optimization (avoid redundant XMDS calls)\n this._lastCheckRf = null;\n this._lastCheckSchedule = null;\n\n // Timeline recalculation guard — skip when inputs haven't changed\n this._lastTimelineFingerprint = null;\n this._lastTimeline = null;\n\n // Layout override state (for changeLayout/overlayLayout via XMR → revertToSchedule)\n this._layoutOverride = null; // { layoutId, type: 'change'|'overlay' }\n this._lastRequiredFiles = []; // Track files for MediaInventory\n\n // Scheduled commands tracking (avoid re-executing same command)\n this._executedCommands = new Set();\n\n // Display commands from RegisterDisplay (used by XMR commandAction)\n this.displayCommands = null;\n\n // Fault reporting agent (independent timer, faster than collection cycle)\n this._faultReportingInterval = null;\n this._faultReportingSeconds = 60; // Default: check for faults every 60s\n\n // Unsafe layout blacklist: layoutId → { failures: number, blacklisted: boolean, reason: string }\n this._layoutBlacklist = new 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 // Dynamic layout tracking (useDuration=0 videos — must play to natural end)\n this._dynamicLayouts = new Set();\n\n // Multi-display sync configuration (from RegisterDisplay syncGroup settings)\n this.syncConfig = null;\n this.syncManager = null; // Optional: set via setSyncManager() after RegisterDisplay\n\n // Layout durations for timeline calculation (layoutFile/layoutId → seconds)\n this._layoutDurations = new Map();\n this._finalDurations = new Set(); // layoutFiles whose duration is definitive (all videos probed)\n\n // Guard: layout currently being prepared (async prepareAndRenderLayout in flight)\n this._preparingLayoutId = null;\n\n // Cache analyzer for stale media detection and storage health\n this.cacheAnalyzer = this.cache ? new CacheAnalyzer(this.cache) : null;\n\n // In-memory offline cache (populated from IndexedDB on first load)\n this._offlineCache = { schedule: null, settings: null, requiredFiles: null };\n this._offlineDbReady = this._initOfflineCache();\n }\n\n /** Schedule queue options — avoids repeating this object in 8 call sites */\n get _queueOptions() {\n return { dynamicLayouts: this._dynamicLayouts };\n }\n\n /**\n * Schedule an auto-revert timer for layout/overlay overrides.\n * @param {number} id - Layout ID\n * @param {number} duration - Duration in seconds (0 = no timer)\n * @param {string} label - Description for logging\n */\n _scheduleAutoRevert(id, duration, label) {\n if (duration > 0) {\n setTimeout(() => {\n if (this._layoutOverride?.layoutId === id) {\n log.info(`${label} duration expired (${duration}s), reverting to schedule`);\n this.revertToSchedule();\n }\n }, duration * 1000);\n }\n }\n\n // ── Offline Cache (IndexedDB) ──────────────────────────────────────\n\n /** Load offline cache from IndexedDB into memory on startup */\n async _initOfflineCache() {\n try {\n const db = await openOfflineDb(this._cmsId);\n const tx = db.transaction(OFFLINE_STORE, 'readonly');\n const store = tx.objectStore(OFFLINE_STORE);\n\n const [schedule, settings, requiredFiles, durations, finalDurations, durVersion] = await Promise.all([\n new Promise(r => { const req = store.get('schedule'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n new Promise(r => { const req = store.get('settings'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n new Promise(r => { const req = store.get('requiredFiles'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n new Promise(r => { const req = store.get('durations'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n new Promise(r => { const req = store.get('finalDurations'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n new Promise(r => { const req = store.get('durationsVersion'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n ]);\n\n if (Array.isArray(durations) && durations.length > 0) {\n for (const [k, v] of durations) this._layoutDurations.set(k, v);\n log.info(`[Timeline] Restored ${durations.length} cached durations from IDB`);\n }\n // v2: clear stale final durations from before the fix.\n // Final durations are only valid when set by video metadata / probeLayoutDurations,\n // not by XLF estimates. Old IDB data has 60s defaults marked as final.\n if (durVersion >= 2 && Array.isArray(finalDurations) && finalDurations.length > 0) {\n for (const k of finalDurations) this._finalDurations.add(k);\n log.info(`[Timeline] Restored ${finalDurations.length} final duration keys from IDB`);\n } else if (Array.isArray(finalDurations) && finalDurations.length > 0) {\n log.info(`[Timeline] Discarded ${finalDurations.length} stale final duration keys (pre-v2)`);\n }\n\n this._offlineCache = { schedule, settings, requiredFiles };\n 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(this._cmsId);\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 _evaluateAndSwitchLayout(layoutFiles, context) {\n const prefix = context ? `${context}: ` : '';\n\n // Use the queue (not raw layoutFiles) for play/expire decisions.\n // The queue has all constraints baked in (maxPlaysPerHour, priorities, dayparting).\n // The player is a dumb consumer — it only expires when the queue rebuilds\n // with a different layout set (new CMS schedule, daypart boundary crossed).\n const { queue } = this.schedule.getScheduleQueue(this._layoutDurations, this._queueOptions);\n\n if (queue.length > 0) {\n if (this.currentLayoutId) {\n const stillInQueue = queue.some(e => parseLayoutFile(e.layoutId) === this.currentLayoutId);\n\n if (!stillInQueue) {\n // Schedule changed and current layout is no longer in the queue — expire immediately.\n // Clear currentLayoutId and emit expire event so the renderer can teardown.\n // The renderer's layoutEnd → advanceToNextLayout flow handles the switch.\n log.info(`Layout ${this.currentLayoutId} no longer in queue — expiring`);\n this.currentLayoutId = null;\n this.emit('layout-expire-current');\n } else {\n // Layout is still in queue — don't interrupt, just rebuild queue in background.\n // The playing layout ends when its timer fires (layoutEnd event),\n // at which point advanceToNextLayout() pops from the already-updated queue.\n log.info(`Layout ${this.currentLayoutId} playing — queue updated in background, playback continues`);\n this.emit('layout-already-playing', this.currentLayoutId);\n }\n } else if (!this._preparingLayoutId) {\n // No layout playing or being prepared — start one from the queue.\n // Guard with _preparingLayoutId to prevent a second _evaluateAndSwitchLayout\n // call (e.g. offline-restore then online-collect) from popping another layout\n // before the async prepareAndRenderLayout completes.\n const next = this.getNextLayout();\n if (next) {\n this._preparingLayoutId = next.layoutId;\n log.info(`${prefix}switching to layout ${next.layoutId}`);\n this.emit('layout-prepare-request', next.layoutId);\n }\n } else {\n log.info(`${prefix}layout ${this._preparingLayoutId} already being prepared, skipping`);\n }\n } else {\n log.info(`${context ? `${context}: n` : 'N'}o layouts${context ? ' in cached schedule' : ' scheduled, falling back to default'}`);\n this.emit('no-layouts-scheduled');\n }\n\n this.logUpcomingTimeline();\n }\n\n /**\n * Force an immediate collection (used by platform layer on 'online' event)\n */\n async collectNow() {\n this._lastCheckRf = null;\n this._lastCheckSchedule = null;\n return this.collect();\n }\n\n /**\n * Start collection cycle\n * Pure orchestration - emits events instead of updating UI\n */\n async collect() {\n // Prevent concurrent collections\n if (this.collecting) {\n log.debug('Collection already in progress, skipping');\n return;\n }\n\n this.collecting = true;\n\n try {\n // Ensure offline cache is loaded from IndexedDB before checking\n await this._offlineDbReady;\n\n log.info('Starting collection cycle...');\n this.emit('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 — only emit if CMS config changed\n // (compare raw CMS response, not the mutated config with relayUrl/syncGroupId added by PWA)\n if (regResult.syncConfig) {\n const rawKey = JSON.stringify(regResult.syncConfig);\n if (rawKey !== this._lastRawSyncConfig) {\n this._lastRawSyncConfig = rawKey;\n this.syncConfig = regResult.syncConfig;\n log.info('Sync group:', regResult.syncConfig.isLead ? 'LEAD' : `follower → ${regResult.syncConfig.syncGroup}`,\n `(switchDelay: ${regResult.syncConfig.syncSwitchDelay}ms, videoPauseDelay: ${regResult.syncConfig.syncVideoPauseDelay}ms)`);\n this.emit('sync-config', regResult.syncConfig);\n }\n }\n\n // Extract config from display tags (key|value convention)\n this._applyTagConfig(regResult.tags);\n\n // Store display commands for XMR commandAction resolution\n if (regResult.commands && regResult.commands.length > 0) {\n this.displayCommands = {};\n for (const cmd of regResult.commands) {\n this.displayCommands[cmd.commandCode] = cmd;\n }\n log.debug('Display commands:', Object.keys(this.displayCommands).join(', '));\n }\n\n this.emit('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 (from the pre-calculated queue)\n const { queue } = this.schedule.getScheduleQueue(\n this._layoutDurations,\n this._queueOptions\n );\n const layoutOrder = [...new Set(queue.map(e => parseLayoutFile(e.layoutId)))];\n\n this._lastRequiredFiles = files;\n\n // Download window enforcement (#81) — skip downloads outside configured window\n if (this.displaySettings?.isInDownloadWindow && !this.displaySettings.isInDownloadWindow()) {\n const nextWindow = this.displaySettings.getNextDownloadWindow?.();\n log.info(`Outside download window, skipping downloads${nextWindow ? ` (next: ${nextWindow.toLocaleTimeString()})` : ''}`);\n } else {\n this.emit('download-request', { layoutOrder, files, layoutDependants: Object.fromEntries(this.schedule.getDependantsMap()) });\n }\n\n // Non-blocking cache analysis (stale media detection)\n if (this.cacheAnalyzer) {\n this.cacheAnalyzer.analyze(files).then(report => {\n this.emit('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 } 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 // 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 // Recalculate timeline after every collection cycle completes,\n // even if schedule CRC was unchanged — durations or time may have shifted.\n this.logUpcomingTimeline();\n\n this.emit('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 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._preparingLayoutId = null;\n this._lastLayoutChangeTime = new Date().toISOString();\n this._statusCode = 1; // Running\n this.pendingLayouts.delete(layoutId);\n // Layout proved playable — clear media status (no longer missing)\n this._layoutMediaStatus.delete(`${layoutId}.xlf`);\n this.emit('layout-current', layoutId);\n // Force timeline recalc on layout change (fingerprint reset)\n this._lastTimelineFingerprint = null;\n this.logUpcomingTimeline();\n }\n\n /**\n * Mark layout as pending (waiting for media)\n * Called by platform when layout needs media downloads\n */\n setPendingLayout(layoutId, requiredMediaIds) {\n this.pendingLayouts.set(layoutId, requiredMediaIds);\n this.emit('layout-pending', layoutId, requiredMediaIds);\n }\n\n /**\n * Clear current layout (for replay)\n * Called by platform when layout ends\n */\n clearCurrentLayout() {\n this.currentLayoutId = null;\n this.emit('layout-cleared');\n }\n\n /**\n * Get the next layout from the pre-calculated schedule queue.\n * Pops the next entry, skipping blacklisted layouts.\n * Returns { layoutId, layoutFile } or null.\n */\n getNextLayout() {\n const entry = this.schedule.popNextFromQueue(\n this._layoutDurations,\n this._queueOptions\n );\n\n if (!entry) {\n // No queue entries — try default\n const defaultFile = this.schedule.schedule?.default;\n if (defaultFile) {\n const layoutId = parseLayoutFile(defaultFile);\n return { layoutId, layoutFile: defaultFile };\n }\n return null;\n }\n\n const layoutId = parseLayoutFile(entry.layoutId);\n\n if (this.isLayoutBlacklisted(layoutId)) {\n // Try next entries (up to queue length) to find a non-blacklisted one\n const { queue } = this.schedule.getScheduleQueue(\n this._layoutDurations,\n this._queueOptions\n );\n for (let i = 0; i < queue.length - 1; i++) {\n const next = this.schedule.popNextFromQueue(\n this._layoutDurations,\n this._queueOptions\n );\n if (next) {\n const nextId = parseLayoutFile(next.layoutId);\n if (!this.isLayoutBlacklisted(nextId)) {\n return { layoutId: nextId, layoutFile: next.layoutId };\n }\n }\n }\n // All blacklisted — return this one anyway to avoid blank screen\n log.warn('All queued layouts are blacklisted, using current entry as fallback');\n }\n\n return { layoutId, layoutFile: entry.layoutId };\n }\n\n /**\n * Peek at the next layout in the schedule queue without advancing.\n * Used by the preload system to know which layout to pre-build.\n * Returns { layoutId, layoutFile } or null if no next layout or same as current.\n */\n peekNextLayout() {\n const entry = this.schedule.peekNextInQueue(\n this._layoutDurations,\n this._queueOptions\n );\n\n if (!entry) return null;\n\n const layoutId = parseLayoutFile(entry.layoutId);\n\n // Don't preload if it's the same as current\n if (layoutId === this.currentLayoutId) {\n // Try the one after that\n const after = this.schedule.peekAfterNext(\n this._layoutDurations,\n this._queueOptions\n );\n if (!after) return null;\n const afterId = parseLayoutFile(after.layoutId);\n if (afterId === this.currentLayoutId || this.isLayoutBlacklisted(afterId)) return null;\n return { layoutId: afterId, layoutFile: after.layoutId };\n }\n\n if (this.isLayoutBlacklisted(layoutId)) return null;\n\n return { layoutId, layoutFile: entry.layoutId };\n }\n\n /**\n * Advance to the next layout in the pre-calculated schedule queue.\n * Called by platform layer when a layout finishes (layoutEnd event).\n * Pops the next entry from the queue and emits layout-prepare-request.\n */\n advanceToNextLayout() {\n // Don't cycle if we're in a layout override (XMR changeLayout/overlayLayout)\n if (this._layoutOverride) {\n log.info('Layout override active, not advancing schedule');\n return;\n }\n\n const next = this.getNextLayout();\n\n // ── Never-stop guarantee ────────────────────────────────────────\n if (!next) {\n if (this.currentLayoutId) {\n log.info(`No layouts in queue, replaying ${this.currentLayoutId} to avoid blank screen`);\n const replayId = this.currentLayoutId;\n this.currentLayoutId = null;\n this._preparingLayoutId = replayId;\n this.emit('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 const { layoutId, layoutFile } = next;\n const dur = this._layoutDurations.get(layoutFile) || '?';\n\n // Debug: log incoming layout vs timeline overlay top entries\n if (this._lastTimeline && this._lastTimeline.length > 0) {\n const top2 = this._lastTimeline.slice(0, 2).map(e => {\n const t = e.startTime.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });\n return `${e.layoutFile}(${e.duration}s@${t})`;\n });\n log.debug(`[Timeline] Layout transition: entering ${layoutFile} (${dur}s), overlay top: [${top2.join(', ')}]`);\n\n // Warn if the entering layout doesn't match the first timeline entry\n if (this._lastTimeline[0].layoutFile !== layoutFile) {\n log.warn(`[Timeline] Mismatch: entering ${layoutFile} but overlay expects ${this._lastTimeline[0].layoutFile}`);\n }\n } else {\n log.debug(`[Timeline] Layout transition: entering ${layoutFile} (${dur}s), no timeline data`);\n }\n\n // Multi-display sync: if this is a sync event and we have a SyncManager,\n // delegate layout transitions to the sync protocol\n if (this.syncManager && this.schedule.isSyncEvent(layoutFile)) {\n if (this.isSyncLead()) {\n log.info(`[Sync] Lead requesting coordinated layout change: ${layoutId}`);\n // Lead must render the layout itself (not just coordinate followers).\n // Emit layout-prepare-request so the renderer builds it, while\n // requestLayoutChange coordinates the show timing with followers.\n this._preparingLayoutId = layoutId;\n this.emit('layout-prepare-request', layoutId);\n this.syncManager.requestLayoutChange(layoutId).catch(err => {\n log.error('[Sync] Layout change failed:', err);\n });\n return;\n } else if (this.syncManager.transport?.connected) {\n log.info(`[Sync] Follower waiting for lead signal (not advancing independently)`);\n return;\n } else {\n log.warn(`[Sync] Follower: lead unreachable, advancing independently`);\n }\n }\n\n if (layoutId === this.currentLayoutId) {\n log.info(`Next layout ${layoutId} is same as current, triggering replay`);\n this.currentLayoutId = null;\n }\n\n const { queue } = this.schedule.getScheduleQueue(\n this._layoutDurations,\n this._queueOptions\n );\n const pos = this.schedule._queuePosition;\n log.info(`Advancing to layout ${layoutId} (queue pos ${pos}/${queue.length})`);\n\n // Set _preparingLayoutId BEFORE emitting to prevent collect() cycles\n // from seeing both currentLayoutId=null and _preparingLayoutId=null\n // and popping another layout from the queue (double-pop race).\n this._preparingLayoutId = layoutId;\n this.emit('layout-prepare-request', layoutId);\n }\n\n /**\n * Go back to the previous layout in the schedule queue (wraps around).\n * Called by platform layer in response to manual navigation (keyboard/remote).\n * Skips sync-manager logic — manual navigation is local only.\n */\n advanceToPreviousLayout() {\n if (this._layoutOverride) {\n log.info('Layout override active, not going back');\n return;\n }\n\n const { queue } = this.schedule.getScheduleQueue(\n this._layoutDurations,\n this._queueOptions\n );\n if (queue.length <= 1) {\n log.info('Single or empty queue, nothing to go back to');\n return;\n }\n\n // Go back 2 positions (current was already popped, so -2 from current pos)\n this.schedule._queuePosition =\n (this.schedule._queuePosition - 2 + queue.length) % queue.length;\n const entry = queue[this.schedule._queuePosition];\n this.schedule._queuePosition = (this.schedule._queuePosition + 1) % queue.length;\n\n const layoutId = parseLayoutFile(entry.layoutId);\n\n if (layoutId === this.currentLayoutId) {\n log.info('Previous layout is same as current, nothing to go back to');\n return;\n }\n\n log.info(`Going back to layout ${layoutId}`);\n this.emit('layout-prepare-request', layoutId);\n }\n\n /**\n * Notify that a file is ready (called by platform for both layout and media files)\n * Checks if any pending layouts can now be rendered\n */\n notifyMediaReady(fileId, fileType = 'media') {\n log.debug(`File ${fileId} ready (${fileType})`);\n\n // Check if any pending layouts are now complete\n for (const [layoutId, requiredFiles] of this.pendingLayouts.entries()) {\n // Check if this file is needed by this layout\n // For layout files: match layout ID with file ID (layout 78 needs layout/78)\n // For media files: check if fileId is in requiredFiles array\n const isLayoutFile = fileType === 'layout' && layoutId === parseInt(fileId);\n const isRequiredMedia = fileType === 'media' && requiredFiles.includes(fileId);\n\n if (isLayoutFile || isRequiredMedia) {\n log.debug(`${fileType} ${fileId} was needed by pending layout ${layoutId}, checking if ready...`);\n this.emit('check-pending-layout', layoutId, requiredFiles);\n }\n }\n }\n\n /**\n * Notify layout status to CMS\n */\n async notifyLayoutStatus(layoutId) {\n try {\n const status = {\n currentLayoutId: layoutId,\n deviceName: this.config?.displayName || '',\n displayName: this.config?.displayName || '',\n lastCommandSuccess: this._lastCommandSuccess ?? true,\n code: this._statusCode,\n lastLayoutChangeTime: this._lastLayoutChangeTime || new Date().toISOString(),\n };\n\n // Add geo-location if available\n if (this.config?.latitude) status.latitude = this.config.latitude;\n if (this.config?.longitude) status.longitude = this.config.longitude;\n\n // Report LAN IP so CMS can tell sync followers where the lead is\n if (this._lanIpAddress) status.lanIpAddress = this._lanIpAddress;\n\n await this.xmds.notifyStatus(status);\n this.emit('status-notified', layoutId);\n } catch (error) {\n log.warn('Failed to notify status:', error);\n this.emit('status-notify-failed', layoutId, error);\n }\n }\n\n /**\n * Report geo location (called by XMR when CMS pushes coordinates)\n * Updates schedule location for geo-fencing and triggers schedule re-evaluation.\n * @param {Object} data - { latitude, longitude }\n */\n reportGeoLocation(data) {\n const lat = parseFloat(data?.latitude);\n const lng = parseFloat(data?.longitude);\n\n if (isNaN(lat) || isNaN(lng)) {\n log.warn('reportGeoLocation: invalid coordinates', data);\n return;\n }\n\n log.info(`Geo location from CMS: ${lat.toFixed(4)}, ${lng.toFixed(4)}`);\n\n if (this.schedule?.setLocation) {\n this.schedule.setLocation(lat, lng);\n }\n\n this.emit('location-updated', { latitude: lat, longitude: lng, source: 'cms' });\n this.checkSchedule();\n }\n\n /**\n * Request geo location using a fallback chain:\n * 1. Browser Geolocation API (GPS / OS-level)\n * 2. Google Geolocation API (if GOOGLE_GEO_API_KEY is configured)\n * 3. IP-based geolocation (free, no key required)\n * @returns {Promise<{latitude: number, longitude: number}|null>}\n */\n async requestGeoLocation() {\n // Return cached location if still fresh (re-resolve every 30 minutes)\n const GEO_CACHE_MS = 30 * 60 * 1000;\n if (this._geoCache && (Date.now() - this._geoCache.ts) < GEO_CACHE_MS) {\n return this._geoCache.location;\n }\n\n // Try browser geolocation (works with GPS or Google API key baked into Chromium).\n // Skip if it already failed — Electron without a Google API key will never succeed.\n if (!this._browserGeoFailed) {\n const browser = await this._tryBrowserGeolocation();\n if (browser) {\n return this._cacheGeo(this._applyLocation(browser.latitude, browser.longitude, 'browser'));\n }\n this._browserGeoFailed = true;\n }\n\n // Try Google Geolocation API if key is configured\n const apiKey = this.config?.googleGeoApiKey;\n if (apiKey) {\n const google = await this._tryGoogleGeolocation(apiKey);\n if (google) {\n return this._cacheGeo(this._applyLocation(google.latitude, google.longitude, 'google-api'));\n }\n }\n\n // Fall back to IP-based geolocation (free, no key)\n const ip = await this._tryIpGeolocation();\n if (ip) {\n return this._cacheGeo(this._applyLocation(ip.latitude, ip.longitude, 'ip-geolocation'));\n }\n\n log.warn('All geolocation methods failed');\n return null;\n }\n\n /** Cache a resolved geolocation result. @private */\n _cacheGeo(location) {\n this._geoCache = { location, ts: Date.now() };\n return location;\n }\n\n /**\n * 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 this._scheduleAutoRevert(id, duration, 'Layout override');\n }\n\n /**\n * Push an overlay layout on top of current content (called by XMR wrapper)\n * @param {number|string} layoutId - Layout to overlay\n */\n async overlayLayout(layoutId, options) {\n log.info('Overlay layout requested via XMR:', layoutId);\n const id = parseInt(layoutId, 10);\n const duration = options?.duration || 0;\n this._layoutOverride = { layoutId: id, type: 'overlay', duration };\n this.emit('overlay-layout-request', id);\n this._scheduleAutoRevert(id, duration, 'Overlay');\n }\n\n /**\n * Revert to scheduled content after changeLayout/overlayLayout override\n */\n async revertToSchedule() {\n log.info('Reverting to scheduled content');\n this._layoutOverride = null;\n this.currentLayoutId = null;\n this.emit('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 signal: AbortSignal.timeout(10000),\n });\n const success = response.ok;\n this._lastCommandSuccess = success;\n log.info(`HTTP command ${commandCode} result: ${response.status}`);\n this.emit('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 this._statusCode = 3; // Error — layout failed to render\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 // Duration flow: renderer is the single source of truth.\n // 1. Renderer calculates duration from widgets → emits layoutDurationUpdated\n // 2. recordLayoutDuration stores it (with final flag) → persisted to IDB\n // 3. On restart, IDB restores correct durations → queue uses them immediately\n // No XLF parsing needed in core — the renderer already does this.\n\n /**\n * Calculate and log the upcoming playback timeline (next 2 hours).\n * Emits 'timeline-updated' with the full timeline array.\n */\n logUpcomingTimeline() {\n if (!this.schedule.getLayoutsAtTime) return; // Schedule doesn't support time queries\n\n // Fingerprint inputs: schedule CRC + sorted durations + current layout + media status.\n // When unchanged, re-emit the cached timeline — avoids time drift from\n // re-simulating with a new Date.now() anchor on every collection cycle.\n const durationEntries = [...this._layoutDurations.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([k, v]) => `${k}:${v}`)\n .join('|');\n const mediaStatusEntries = [...this._layoutMediaStatus.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([k, v]) => `${k}:${v.ready}:${v.missingKey}`)\n .join('|');\n const pendingEntries = [...this.pendingLayouts.keys()].sort().join(',');\n const queuePos = this.schedule._queuePosition || 0;\n const fingerprint = `${this._lastCheckSchedule}|${durationEntries}|${this.currentLayoutId}|${queuePos}|${mediaStatusEntries}|${pendingEntries}`;\n\n if (fingerprint === this._lastTimelineFingerprint && this._lastTimeline) {\n this.emit('timeline-updated', this._lastTimeline);\n return;\n }\n\n const { queue } = this.schedule.getScheduleQueue(this._layoutDurations, this._queueOptions);\n const timeline = calculateTimeline(queue, this.schedule._queuePosition, {\n currentLayoutStartedAt: this._lastLayoutChangeTime ? new Date(this._lastLayoutChangeTime) : null,\n defaultLayout: this.schedule.schedule?.default || null,\n durations: this._layoutDurations,\n });\n if (timeline.length === 0) return;\n\n // Annotate entries with missingMedia from pendingLayouts (high authority)\n // and _layoutMediaStatus (proactive check, lower authority)\n for (const entry of timeline) {\n const layoutId = parseInt(entry.layoutFile.replace('.xlf', ''), 10);\n const pendingMedia = this.pendingLayouts.get(layoutId);\n if (pendingMedia && pendingMedia.length > 0) {\n // pendingLayouts takes priority — definitively missing\n entry.missingMedia = pendingMedia.map(String);\n } else {\n const status = this._layoutMediaStatus.get(entry.layoutFile);\n if (status && !status.ready && status.missing.length > 0) {\n entry.missingMedia = status.missing.map(String);\n }\n }\n }\n\n this._lastTimelineFingerprint = fingerprint;\n this._lastTimeline = timeline;\n\n const lines = timeline.slice(0, 20).map(e => {\n const s = e.startTime.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });\n const end = e.endTime.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });\n const missingTag = e.missingMedia ? ` [MISSING: ${e.missingMedia.length} files]` : '';\n return ` ${s}-${end} Layout ${e.layoutFile} (${e.duration}s)${e.isDefault ? ' [default]' : ''}${missingTag}`;\n });\n\n // Log warnings for layouts with missing media\n for (const entry of timeline) {\n if (entry.missingMedia) {\n log.warn(`[Timeline] Layout ${entry.layoutFile}: ${entry.missingMedia.length} files missing`);\n }\n }\n\n log.info(`[Timeline] Next ${timeline.length} plays:\\n${lines.join('\\n')}`);\n this.emit('timeline-updated', timeline);\n }\n\n /**\n * Set media readiness status for a layout (proactive async check from platform layer).\n * No-ops if value is unchanged to avoid fingerprint churn.\n * @param {string} layoutFile - Layout file (e.g. '100.xlf')\n * @param {boolean} ready - Whether all media is cached\n * @param {string[]} [missing] - Array of missing media IDs/filenames\n */\n setLayoutMediaStatus(layoutFile, ready, missing = []) {\n const existing = this._layoutMediaStatus.get(layoutFile);\n const missingKey = missing.slice().sort().join(',');\n if (existing && existing.ready === ready && existing.missingKey === missingKey) return;\n\n this._layoutMediaStatus.set(layoutFile, { ready, missing, missingKey });\n // Invalidate fingerprint to force timeline recalculation\n this._lastTimelineFingerprint = null;\n }\n\n /**\n * Record/correct a layout's actual duration (e.g., from video loadedmetadata).\n * Updates the durations map and re-logs the timeline if it changed.\n * @param {string} file - Layout file or layout ID string\n * @param {number} duration - Actual duration in seconds\n * @param {boolean} [final=false] - True when all videos in the layout have been probed\n */\n recordLayoutDuration(file, duration, final = false) {\n // Normalize: store under both \"492\" and \"492.xlf\" forms so that\n // calculateTimeline (which looks up \"492.xlf\") and other callers\n // (which use \"492\") always find the corrected value.\n const id = String(file).replace('.xlf', '');\n const xlfKey = id + '.xlf';\n\n // Definitive duration — never overwrite once set\n if (this._finalDurations.has(id)) return;\n\n const prev = this._layoutDurations.get(file);\n if (prev === duration && !final) return; // No change\n\n this._layoutDurations.set(id, duration);\n this._layoutDurations.set(xlfKey, duration);\n\n if (final) {\n this._finalDurations.add(id);\n this._finalDurations.add(xlfKey);\n }\n\n log.debug(`[Timeline] Duration corrected: layout ${file} ${prev || '?'}s → ${duration}s${final ? ' (final)' : ''}`);\n\n // Invalidate the cached schedule queue so the next getScheduleQueue() call\n // rebuilds with corrected durations (affects queue log and period calculation).\n this.schedule._scheduleQueue = null;\n\n // Debounce timeline recalculation — multiple video loadedmetadata events\n // can fire within milliseconds; collapse them into one recalculation.\n if (this._timelineRecalcTimer) clearTimeout(this._timelineRecalcTimer);\n this._timelineRecalcTimer = setTimeout(() => {\n this._timelineRecalcTimer = null;\n this.logUpcomingTimeline();\n this._offlineSave('durations', [...this._layoutDurations.entries()]);\n this._offlineSave('finalDurations', [...this._finalDurations]);\n this._offlineSave('durationsVersion', 2);\n }, 500);\n }\n\n /**\n * Cleanup\n */\n cleanup() {\n if (this.collectionInterval) {\n clearInterval(this.collectionInterval);\n this.collectionInterval = null;\n }\n\n if (this._faultReportingInterval) {\n clearInterval(this._faultReportingInterval);\n this._faultReportingInterval = null;\n }\n\n if (this._timelineRecalcTimer) {\n clearTimeout(this._timelineRecalcTimer);\n this._timelineRecalcTimer = null;\n }\n\n if (this.xmr) {\n this.xmr.stop();\n this.xmr = null;\n }\n\n // Stop multi-display sync\n if (this.syncManager) {\n this.syncManager.stop();\n this.syncManager = null;\n }\n\n // Stop data connector polling\n this.dataConnectorManager.cleanup();\n\n // Emit cleanup-complete before removing listeners\n this.emit('cleanup-complete');\n this.removeAllListeners();\n }\n\n /**\n * Get current layout ID\n */\n getCurrentLayoutId() {\n return this.currentLayoutId;\n }\n\n /**\n * Get known duration for a layout (from video metadata or XLF parse).\n * @param {number|string} layoutId\n * @returns {number|undefined}\n */\n getLayoutDuration(layoutId) {\n const id = String(layoutId);\n return this._layoutDurations.get(`${id}.xlf`) || this._layoutDurations.get(id);\n }\n\n /**\n * Check if collecting\n */\n isCollecting() {\n return this.collecting;\n }\n\n /**\n * Get pending layouts\n */\n getPendingLayouts() {\n return Array.from(this.pendingLayouts.keys());\n }\n\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Download Progress Overlay\n *\n * Shows download status on hover (configurable, debug feature)\n * Displays: active downloads, progress, chunk status, queue info\n */\n\nexport interface DownloadOverlayConfig {\n enabled: boolean;\n updateInterval?: number; // ms between updates\n autoHide?: boolean; // Hide when no downloads\n}\n\nexport class DownloadOverlay {\n private overlay: HTMLElement | null = null;\n private config: DownloadOverlayConfig;\n private updateTimer: number | null = null;\n private _visible: boolean = false; // User-toggled visibility (D key)\n private _getProgress: (() => Record<string, any>) | null = null;\n\n constructor(config: DownloadOverlayConfig) {\n this.config = {\n updateInterval: 1000,\n autoHide: true,\n ...config\n };\n\n if (this.config.enabled) {\n this.createOverlay();\n // Start hidden — only shown when downloads are active or user presses D\n this.overlay!.style.display = 'none';\n }\n }\n\n private createOverlay() {\n this.overlay = document.createElement('div');\n this.overlay.id = 'download-overlay';\n // Style like top status messages - always visible, clean design\n this.overlay.style.cssText = `\n position: fixed;\n top: 1.5vh;\n left: 1.5vw;\n background: rgba(0, 0, 0, 0.88);\n color: #fff;\n font-family: system-ui, -apple-system, sans-serif;\n font-size: 1.4vw;\n padding: 1vh 1.2vw;\n border-radius: 0.4vw;\n border: 1px solid rgba(255, 255, 255, 0.25);\n z-index: 999999;\n max-width: 35vw;\n box-shadow: 0 0.3vh 1.2vw rgba(0, 0, 0, 0.5);\n `;\n\n document.body.appendChild(this.overlay);\n }\n\n /**\n * Set the progress callback. Called by PwaPlayer after DownloadManager is created.\n */\n public setProgressCallback(fn: () => Record<string, any>) {\n this._getProgress = fn;\n }\n\n private updateOverlay() {\n if (!this.overlay) return;\n\n const progress = this._getProgress ? this._getProgress() : {};\n const html = this.renderStatus(progress);\n const hasDownloads = !!html;\n\n if (hasDownloads) {\n // Active downloads — show overlay (auto or user-toggled)\n this.overlay.innerHTML = html;\n this.overlay.style.display = 'block';\n } else if (this._visible) {\n // User toggled on (D key) but no downloads — show idle status\n this.overlay.innerHTML = '<div style=\"color: #6c6; font-size: 1.4vw;\">✓ All downloads complete</div>';\n this.overlay.style.display = 'block';\n } else {\n // No downloads and not user-toggled — stop polling, hide\n this.stopUpdating();\n this.overlay.style.display = 'none';\n }\n }\n\n private renderStatus(progress: any): string {\n const downloads = progress || {};\n\n if (Object.keys(downloads).length === 0) {\n if (this.config.autoHide) {\n return ''; // Hide when no downloads\n }\n return `<div style=\"color: #6c6;\">✓ No downloads</div>`;\n }\n\n const numDownloads = Object.keys(downloads).length;\n let html = `<div style=\"font-weight: 600; margin-bottom: 0.8vh; font-size: 1.4vw;\">Downloads: ${numDownloads} active</div>`;\n\n for (const [url, progress] of Object.entries(downloads)) {\n const filename = this.extractFilename(url);\n const percent = Math.round((progress as any).percent || 0);\n const downloaded = this.formatBytes((progress as any).downloaded || 0);\n const total = this.formatBytes((progress as any).total || 0);\n\n html += `\n <div style=\"margin-bottom: 0.6vh; padding-bottom: 0.6vh; border-bottom: 1px solid rgba(255,255,255,0.1);\">\n <div style=\"font-size: 1.2vw; margin-bottom: 0.2vh;\">${filename}</div>\n <div style=\"background: rgba(255,255,255,0.1); height: 0.4vh; border-radius: 0.2vw; overflow: hidden;\">\n <div style=\"width: ${percent}%; height: 100%; background: #4a9eff; transition: width 0.3s;\"></div>\n </div>\n <div style=\"color: #999; font-size: 1.1vw; margin-top: 0.2vh;\">\n ${percent}% · ${downloaded} / ${total}\n </div>\n </div>\n `;\n }\n\n return html;\n }\n\n private extractFilename(key: string): string {\n // Key is now \"type/id\" (e.g. \"media/5\", \"layout/12\") — no URL parsing needed\n return key || 'unknown';\n }\n\n private formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n const kb = bytes / 1024;\n if (kb < 1024) return `${kb.toFixed(1)} KB`;\n const mb = kb / 1024;\n if (mb < 1024) return `${mb.toFixed(1)} MB`;\n return `${(mb / 1024).toFixed(1)} GB`;\n }\n\n /**\n * Toggle overlay visibility (D key).\n * When toggled on, starts polling. When toggled off, hides immediately.\n */\n public toggle() {\n if (!this.overlay) return;\n this._visible = !this._visible;\n if (this._visible) {\n this.overlay.style.display = 'block';\n this.updateOverlay(); // Immediate update\n this.startUpdating();\n } else {\n this.overlay.style.display = 'none';\n this.stopUpdating();\n }\n }\n\n /**\n * Start polling for download progress.\n * Safe to call multiple times — won't create duplicate timers.\n * Does NOT set _visible — the overlay auto-shows when downloads are active\n * and auto-hides when they finish. Use toggle() for user-controlled visibility.\n */\n public startUpdating() {\n if (this.updateTimer) return; // Already polling\n this.updateTimer = window.setInterval(() => {\n this.updateOverlay();\n }, this.config.updateInterval);\n this.updateOverlay(); // Immediate first update\n }\n\n /**\n * Stop polling. Called automatically when no downloads are active.\n */\n private stopUpdating() {\n if (this.updateTimer) {\n clearInterval(this.updateTimer);\n this.updateTimer = null;\n }\n }\n\n public destroy() {\n this.stopUpdating();\n if (this.overlay) {\n this.overlay.remove();\n this.overlay = null;\n }\n }\n\n public setEnabled(enabled: boolean) {\n this.config.enabled = enabled;\n\n if (enabled && !this.overlay) {\n this.createOverlay();\n // Polling starts on demand via startUpdating()\n } else if (!enabled && this.overlay) {\n this.destroy();\n }\n }\n}\n\n/**\n * Get default configuration based on environment\n */\nexport function getDefaultOverlayConfig(): DownloadOverlayConfig {\n // Check URL parameter override\n const urlParams = new URLSearchParams(window.location.search);\n const showDownloads = urlParams.get('showDownloads');\n\n if (showDownloads !== null) {\n return { enabled: showDownloads !== '0' && showDownloads !== 'false' };\n }\n\n // Check localStorage preference\n const savedPref = localStorage.getItem('xibo_show_download_overlay');\n if (savedPref !== null) {\n return { enabled: savedPref === 'true' };\n }\n\n // Default: disabled — toggle with D key or ?showDownloads=1\n return { enabled: false };\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Timeline Overlay\n *\n * Toggleable debug overlay showing upcoming schedule timeline.\n * Displays: layout IDs, time ranges, durations, current layout highlight.\n * Positioned bottom-left (download overlay is top-left).\n */\n\ninterface HiddenLayout {\n file: string;\n priority: number;\n}\n\ninterface TimelineEntry {\n layoutFile: string;\n startTime: Date;\n endTime: Date;\n duration: number;\n isDefault: boolean;\n hidden?: HiddenLayout[];\n missingMedia?: string[];\n}\n\nexport class TimelineOverlay {\n private overlay: HTMLElement | null = null;\n private visible: boolean;\n private timeline: TimelineEntry[] = [];\n private currentLayoutId: number | null = null;\n private layoutStartedAt: number | null = null; // wall-clock ms when layout began\n private currentDuration: number | null = null;\n private currentIsDefault: boolean = false;\n private previousLayout: { id: number; duration: number; startedAt: number } | null = null;\n private offline: boolean = false;\n private onLayoutClick: ((layoutId: number) => void) | null = null;\n private refreshTimer: ReturnType<typeof setInterval> | null = null;\n\n constructor(visible = false, onLayoutClick?: (layoutId: number) => void) {\n this.visible = visible;\n this.onLayoutClick = onLayoutClick || null;\n this.createOverlay();\n if (!this.visible) {\n this.overlay!.style.display = 'none';\n }\n // Re-render every 5s to update the remaining-time countdown on the current layout\n this.refreshTimer = setInterval(() => this.render(), 5000);\n }\n\n private createOverlay() {\n this.overlay = document.createElement('div');\n this.overlay.id = 'timeline-overlay';\n this.overlay.style.cssText = `\n position: fixed;\n bottom: 1.5vh;\n left: 1.5vw;\n background: rgba(0, 0, 0, 0.88);\n color: #fff;\n font-family: system-ui, -apple-system, sans-serif;\n font-size: 1.4vw;\n padding: 1vh 1.2vw;\n border-radius: 0.4vw;\n border: 1px solid rgba(255, 255, 255, 0.25);\n z-index: 999999;\n max-width: 35vw;\n box-shadow: 0 0.3vh 1.2vw rgba(0, 0, 0, 0.5);\n pointer-events: auto;\n `;\n // Click-to-skip: delegate click events on layout entries\n this.overlay.addEventListener('click', (e: MouseEvent) => {\n const target = (e.target as HTMLElement).closest('[data-layout-id]') as HTMLElement | null;\n if (!target || !this.onLayoutClick) return;\n const layoutId = parseInt(target.dataset.layoutId!, 10);\n if (isNaN(layoutId) || layoutId === this.currentLayoutId) return;\n this.onLayoutClick(layoutId);\n });\n\n document.body.appendChild(this.overlay);\n }\n\n toggle() {\n this.visible = !this.visible;\n if (this.overlay) {\n this.overlay.style.display = this.visible ? 'block' : 'none';\n }\n // Re-render when becoming visible (render() skips while hidden)\n if (this.visible) {\n this.render();\n }\n // Persist preference\n localStorage.setItem('xibo_show_timeline_overlay', String(this.visible));\n }\n\n /**\n * Update the overlay with new timeline data and/or current layout highlight.\n * Pass timeline=null to keep existing timeline and only update the highlight.\n */\n setOffline(offline: boolean) {\n this.offline = offline;\n this.render();\n }\n\n update(timeline: TimelineEntry[] | null, currentLayoutId: number | null, currentDuration?: number) {\n if (currentLayoutId !== null) {\n // Detect layout change — save previous for history display\n if (currentLayoutId !== this.currentLayoutId) {\n if (this.currentLayoutId !== null && this.currentDuration !== null && this.layoutStartedAt !== null) {\n this.previousLayout = { id: this.currentLayoutId, duration: this.currentDuration, startedAt: this.layoutStartedAt };\n }\n this.currentLayoutId = currentLayoutId;\n this.currentIsDefault = false;\n }\n // Always reset start time — same-layout replays emit layoutStart too\n this.layoutStartedAt = Date.now();\n // Duration is known at layout start — set it directly rather than\n // searching the timeline (which only contains future layouts).\n if (currentDuration !== undefined) {\n this.currentDuration = currentDuration;\n }\n }\n\n if (timeline !== null) {\n this.timeline = timeline;\n }\n\n this.render();\n }\n\n private render() {\n if (!this.overlay || !this.visible) return;\n\n if (this.timeline.length === 0 && !this.previousLayout && !this.currentLayoutId) {\n this.overlay.innerHTML = '<div style=\"color: #999;\">Timeline — no upcoming layouts</div>';\n return;\n }\n\n const now = Date.now();\n const clickable = this.onLayoutClick !== null;\n\n // Build upcoming list: timeline entries minus the first occurrence of the current layout\n let skippedCurrent = false;\n const upcoming: TimelineEntry[] = [];\n for (const entry of this.timeline) {\n const layoutId = parseInt(entry.layoutFile.replace('.xlf', ''), 10);\n if (!skippedCurrent && layoutId === this.currentLayoutId) {\n skippedCurrent = true;\n continue;\n }\n upcoming.push(entry);\n }\n\n // Count: previous (if any) + current (if any) + upcoming\n const totalCount = (this.previousLayout ? 1 : 0) + (this.currentLayoutId ? 1 : 0) + upcoming.length;\n const offlineBadge = this.offline ? ' <span style=\"color: #ff4444; font-size: 1.1vw;\">OFFLINE</span>' : '';\n let html = `<div style=\"font-weight: 600; margin-bottom: 0.8vh; font-size: 1.4vw; color: #ccc;\">Timeline (${totalCount} scheduled)${offlineBadge}</div>`;\n\n const maxVisible = 8;\n let rendered = 0;\n\n // 1. Previous layout (dimmed, strikethrough)\n if (this.previousLayout && rendered < maxVisible) {\n const prev = this.previousLayout;\n const durStr = this.formatDuration(prev.duration);\n const durPad = durStr.padStart(7).replace(/ /g, ' ');\n const idCol = `#${prev.id}`.padEnd(6).replace(/ /g, ' ');\n const startDate = new Date(prev.startedAt);\n const endDate = new Date(prev.startedAt + prev.duration * 1000);\n const timeRange = `${this.formatTime(startDate)}–${this.formatTime(endDate)} `;\n const cursor = clickable ? 'cursor: pointer;' : '';\n const hover = clickable ? 'onmouseover=\"this.style.background=\\'rgba(255,255,255,0.1)\\'\" onmouseout=\"this.style.background=\\'none\\'\"' : '';\n html += `<div data-layout-id=\"${prev.id}\" style=\"border-left: 0.25vw solid #555; padding-left: 0.6vw; color: #666; text-decoration: line-through; ${cursor} margin-bottom: 0.3vh; font-family: monospace; font-size: 1.3vw; line-height: 1.5; white-space: nowrap;\" ${hover}>`;\n html += `${timeRange}${idCol}${durPad}`;\n html += '</div>';\n rendered++;\n }\n\n // 2. Current layout (blue highlight, countdown from wall-clock start, with time range)\n if (this.currentLayoutId !== null && rendered < maxVisible) {\n let durStr: string;\n let timeRange = '';\n if (this.currentDuration !== null && this.layoutStartedAt !== null) {\n const elapsed = (now - this.layoutStartedAt) / 1000;\n const remainingSec = Math.max(0, Math.round(this.currentDuration - elapsed));\n durStr = this.formatDuration(remainingSec);\n const startDate = new Date(this.layoutStartedAt);\n const endDate = new Date(this.layoutStartedAt + this.currentDuration * 1000);\n timeRange = `${this.formatTime(startDate)}–${this.formatTime(endDate)} `;\n } else {\n durStr = '---';\n }\n const durPad = durStr.padStart(7).replace(/ /g, ' ');\n const idCol = `#${this.currentLayoutId}`.padEnd(6).replace(/ /g, ' ');\n html += `<div data-layout-id=\"${this.currentLayoutId}\" style=\"border-left: 0.25vw solid #4a9eff; padding-left: 0.6vw; color: #fff; font-weight: 600; margin-bottom: 0.3vh; font-family: monospace; font-size: 1.3vw; line-height: 1.5; white-space: nowrap;\">`;\n html += `${timeRange}${idCol}${durPad}`;\n if (this.currentIsDefault) html += ' <span style=\"color: #888;\">[def]</span>';\n html += '</div>';\n rendered++;\n }\n\n // 3. Upcoming layouts — compute times by chaining from current layout end\n let nextStartMs = (this.layoutStartedAt !== null && this.currentDuration !== null)\n ? this.layoutStartedAt + this.currentDuration * 1000\n : now;\n for (const entry of upcoming) {\n if (rendered >= maxVisible) break;\n const layoutId = parseInt(entry.layoutFile.replace('.xlf', ''), 10);\n const hasMissing = entry.missingMedia && entry.missingMedia.length > 0;\n const durStr = this.formatDuration(entry.duration);\n const entryEndMs = nextStartMs + entry.duration * 1000;\n const startStr = this.formatTime(new Date(nextStartMs));\n const endStr = this.formatTime(new Date(entryEndMs));\n\n let borderLeft: string;\n let color: string;\n if (hasMissing) {\n borderLeft = 'border-left: 0.25vw solid #ff4444; padding-left: 0.6vw;';\n color = 'color: #ff6666;';\n } else {\n borderLeft = 'padding-left: 0.85vw;';\n color = 'color: #aaa;';\n }\n const cursor = clickable ? 'cursor: pointer;' : '';\n const hover = clickable ? 'onmouseover=\"this.style.background=\\'rgba(255,255,255,0.1)\\'\" onmouseout=\"this.style.background=\\'none\\'\"' : '';\n\n html += `<div data-layout-id=\"${layoutId}\" style=\"${borderLeft} ${color} ${cursor} margin-bottom: 0.3vh; font-family: monospace; font-size: 1.3vw; line-height: 1.5; white-space: nowrap;\" ${hover}>`;\n const idCol = `#${layoutId}`.padEnd(6).replace(/ /g, ' ');\n const durPad = durStr.padStart(7).replace(/ /g, ' ');\n html += `${startStr}–${endStr} ${idCol}${durPad}`;\n if (entry.isDefault) html += ' <span style=\"color: #888;\">[def]</span>';\n if (hasMissing) {\n const missingList = entry.missingMedia!.join(', ');\n html += ` <span style=\"color: #ff4444; font-size: 1.1vw;\" title=\"Missing: ${missingList}\">⚠ ${entry.missingMedia!.length}</span>`;\n }\n if (entry.hidden && entry.hidden.length > 0) {\n const hiddenIds = entry.hidden.map(h => `#${h.file.replace('.xlf', '')} (p${h.priority})`).join(', ');\n html += ` <span style=\"color: #8899aa; font-size: 1.1vw;\" title=\"Also scheduled: ${hiddenIds}\">+${entry.hidden.length}</span>`;\n }\n html += '</div>';\n nextStartMs = entryEndMs;\n rendered++;\n }\n\n if (totalCount > maxVisible) {\n html += `<div style=\"padding-left: 0.85vw; color: #888; font-size: 1.1vw; margin-top: 0.3vh;\">+${totalCount - maxVisible} more</div>`;\n }\n\n this.overlay.innerHTML = html;\n }\n\n private formatTime(date: Date): string {\n return date.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });\n }\n\n private formatDuration(seconds: number): string {\n const m = Math.floor(seconds / 60);\n const s = Math.round(seconds % 60);\n return m > 0 ? `${m}m ${s.toString().padStart(2, '0')}s` : `${s}s`;\n }\n\n destroy() {\n if (this.refreshTimer) {\n clearInterval(this.refreshTimer);\n this.refreshTimer = null;\n }\n if (this.overlay) {\n this.overlay.remove();\n this.overlay = null;\n }\n }\n}\n\n/**\n * Determine initial visibility from URL param or localStorage.\n */\nexport function isTimelineVisible(): boolean {\n const urlParams = new URLSearchParams(window.location.search);\n const showTimeline = urlParams.get('showTimeline');\n if (showTimeline !== null) {\n return showTimeline !== '0' && showTimeline !== 'false';\n }\n\n const saved = localStorage.getItem('xibo_show_timeline_overlay');\n if (saved !== null) {\n return saved === 'true';\n }\n\n return false;\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Setup Overlay\n *\n * Two-phase overlay that never navigates away from the player:\n * 1. CMS key gate — verifies identity\n * 2. Full setup form — setup.html in a fullscreen iframe\n *\n * Dismissible with Esc or Cancel at both phases. On successful setup\n * the iframe redirects to index.html, which we intercept to reload.\n */\n\n// @ts-ignore - JavaScript module\nimport { createLogger, config } from '@xiboplayer/utils';\n\nconst log = createLogger('SetupOverlay');\n\nexport class SetupOverlay {\n private backdrop: HTMLElement | null = null;\n private gateCard: HTMLElement | null = null;\n private iframe: HTMLIFrameElement | null = null;\n private cancelBtn: HTMLElement | null = null;\n private visible = false;\n\n show() {\n if (this.visible) return;\n this.visible = true;\n\n if (!this.backdrop) {\n this.create();\n }\n\n // Always start with the gate phase\n this.showGate();\n this.backdrop!.style.display = 'flex';\n log.info('[SetupOverlay] Opened');\n }\n\n hide() {\n if (!this.visible) return;\n this.visible = false;\n\n if (this.backdrop) {\n this.backdrop.style.display = 'none';\n }\n // Clear iframe to stop any polling timers inside setup.html\n if (this.iframe) {\n this.iframe.src = 'about:blank';\n this.iframe.style.display = 'none';\n }\n log.info('[SetupOverlay] Closed');\n }\n\n toggle() {\n if (this.visible) {\n this.hide();\n } else {\n this.show();\n }\n }\n\n isVisible() {\n return this.visible;\n }\n\n /** Show the CMS key gate card, hide the iframe */\n private showGate() {\n if (this.gateCard) this.gateCard.style.display = 'block';\n if (this.iframe) this.iframe.style.display = 'none';\n if (this.cancelBtn) this.cancelBtn.style.display = 'none';\n\n const input = this.gateCard?.querySelector('#gate-key') as HTMLInputElement;\n if (input) {\n input.value = '';\n requestAnimationFrame(() => input.focus());\n }\n const err = this.gateCard?.querySelector('#gate-error') as HTMLElement;\n if (err) err.style.display = 'none';\n }\n\n /** Show the setup iframe, hide the gate card */\n private showSetup() {\n if (this.gateCard) this.gateCard.style.display = 'none';\n if (this.cancelBtn) this.cancelBtn.style.display = 'block';\n if (this.iframe) {\n this.iframe.style.display = 'block';\n this.iframe.src = './setup.html?unlocked=1';\n }\n }\n\n private create() {\n // ── Backdrop ──\n this.backdrop = document.createElement('div');\n this.backdrop.id = 'setup-overlay-backdrop';\n this.backdrop.style.cssText = `\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.85);\n z-index: 1000000;\n display: none;\n align-items: center;\n justify-content: center;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n `;\n\n // ── Cancel button (visible in iframe phase) ──\n this.cancelBtn = document.createElement('button');\n this.cancelBtn.textContent = 'Cancel';\n this.cancelBtn.style.cssText = `\n position: absolute;\n top: 12px;\n right: 16px;\n background: transparent;\n border: 1px solid rgba(255, 255, 255, 0.3);\n color: #aaa;\n font-size: 14px;\n padding: 6px 18px;\n border-radius: 6px;\n cursor: pointer;\n z-index: 1000001;\n display: none;\n transition: background 0.2s, color 0.2s;\n `;\n this.cancelBtn.addEventListener('mouseenter', () => {\n this.cancelBtn!.style.background = 'rgba(255,255,255,0.1)';\n this.cancelBtn!.style.color = '#fff';\n });\n this.cancelBtn.addEventListener('mouseleave', () => {\n this.cancelBtn!.style.background = 'transparent';\n this.cancelBtn!.style.color = '#aaa';\n });\n this.cancelBtn.addEventListener('click', () => this.hide());\n\n // ── Gate card (matches setup.html .container) ──\n this.gateCard = document.createElement('div');\n this.gateCard.style.cssText = `\n background: #2A2A2A;\n border-radius: 16px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);\n padding: 48px;\n max-width: 480px;\n width: 90vw;\n color: #E0E0E0;\n `;\n this.gateCard.innerHTML = `\n <div style=\"text-align: center; margin-bottom: 32px;\">\n <div style=\"font-size: 36px; font-weight: 700; color: #fff; letter-spacing: -0.5px;\">\n <span style=\"color: #0097D8;\">xibo</span> player\n </div>\n <div style=\"font-size: 14px; color: #888; margin-top: 4px;\">PWA Digital Signage</div>\n </div>\n <div style=\"font-size: 18px; font-weight: 600; color: #fff; margin-bottom: 8px; text-align: center;\">\n Reconfigure Display\n </div>\n <div style=\"font-size: 13px; color: #888; margin-bottom: 20px; text-align: center; line-height: 1.5;\">\n Enter the current CMS Key to change settings.\n </div>\n <form id=\"gate-form\">\n <div style=\"margin-bottom: 20px;\">\n <label style=\"display: block; margin-bottom: 6px; color: #AAA; font-size: 13px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;\">\n CMS Key\n </label>\n <input type=\"password\" id=\"gate-key\" placeholder=\"Current CMS key\" required\n style=\"width: 100%; padding: 12px 14px; background: #1D1D1D; border: 2px solid #3A3A3A; border-radius: 8px; font-size: 15px; color: #E0E0E0; transition: border-color 0.2s; box-sizing: border-box;\">\n </div>\n <button type=\"submit\" style=\"width: 100%; padding: 14px; background: #0097D8; color: white; border: none; border-radius: 8px; font-size: 15px; font-weight: 600; cursor: pointer; transition: background 0.2s, transform 0.1s;\">\n Unlock\n </button>\n </form>\n <div id=\"gate-error\" style=\"margin-top: 16px; padding: 12px 14px; background: rgba(244, 67, 54, 0.15); border: 1px solid rgba(244, 67, 54, 0.3); border-radius: 8px; color: #EF9A9A; font-size: 14px; display: none;\"></div>\n <button id=\"gate-cancel\" style=\"width: 100%; padding: 14px; background: transparent; border: 1px solid #3A3A3A; color: #AAA; border-radius: 8px; font-size: 15px; font-weight: 600; cursor: pointer; margin-top: 8px; transition: background 0.2s;\">\n Cancel\n </button>\n `;\n\n // ── Iframe (fullscreen, same look as setup.html) ──\n this.iframe = document.createElement('iframe');\n this.iframe.style.cssText = `\n width: 100%;\n height: 100%;\n border: none;\n background: #1D1D1D;\n display: none;\n `;\n\n // Detect success redirect: setup.html navigates to index.html → reload player\n this.iframe.addEventListener('load', () => {\n try {\n const href = this.iframe!.contentWindow?.location?.href || '';\n if (href.includes('index.html')) {\n this.hide();\n window.location.reload();\n return;\n }\n\n // Esc inside the iframe dismisses the overlay\n const iframeDoc = this.iframe!.contentDocument;\n if (!iframeDoc) return;\n iframeDoc.addEventListener('keydown', (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n e.preventDefault();\n this.hide();\n }\n });\n } catch { /* not loaded yet */ }\n });\n\n this.backdrop.appendChild(this.cancelBtn);\n this.backdrop.appendChild(this.gateCard);\n this.backdrop.appendChild(this.iframe);\n document.body.appendChild(this.backdrop);\n\n // ── Gate event handlers ──\n const form = this.gateCard.querySelector('#gate-form') as HTMLFormElement;\n const input = this.gateCard.querySelector('#gate-key') as HTMLInputElement;\n const errorEl = this.gateCard.querySelector('#gate-error') as HTMLElement;\n const gateCancelBtn = this.gateCard.querySelector('#gate-cancel') as HTMLButtonElement;\n\n input.addEventListener('focus', () => { input.style.borderColor = '#0097D8'; });\n input.addEventListener('blur', () => { input.style.borderColor = '#3A3A3A'; });\n\n form.addEventListener('submit', (e: Event) => {\n e.preventDefault();\n const entered = input.value.trim();\n\n if (entered === config.cmsKey) {\n this.showSetup();\n } else {\n errorEl.textContent = 'Incorrect CMS key';\n errorEl.style.display = 'block';\n input.focus();\n input.select();\n }\n });\n\n gateCancelBtn.addEventListener('click', () => this.hide());\n\n // Esc closes overlay; stopPropagation blocks player shortcuts\n this.backdrop.addEventListener('keydown', (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n e.preventDefault();\n this.hide();\n }\n e.stopPropagation();\n });\n }\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * PWA Player with RendererLite\n *\n * Lightweight PWA player using modular PlayerCore orchestration.\n * Platform layer handles UI, DOM manipulation, and platform-specific features.\n */\n\nimport { RendererLite } from '@xiboplayer/renderer';\nimport { StoreClient, DownloadManager, LayoutTaskBuilder, BARRIER } from '@xiboplayer/cache';\nimport { PlayerCore } from '@xiboplayer/core';\nimport { parseLayoutDuration } from '@xiboplayer/schedule';\nimport { createLogger, registerLogSink, PLAYER_API } from '@xiboplayer/utils';\nimport { DownloadOverlay, getDefaultOverlayConfig } from './download-overlay.js';\nimport { TimelineOverlay, isTimelineVisible } from './timeline-overlay.js';\nimport { SetupOverlay } from './setup-overlay.js';\n\ndeclare const __APP_VERSION__: string;\ndeclare const __BUILD_DATE__: string;\n\nconst log = createLogger('PWA');\n\n// ContentStore key prefix — mirrors PLAYER_API without leading slash\nconst STORE_PREFIX = PLAYER_API.slice(1);\n\n// Dynamic base path — same build serves /player/pwa/, /player/pwa-xmds/, /player/pwa-xlr/\nconst PLAYER_BASE = new URL('./', window.location.href).pathname.replace(/\\/$/, '');\n\n// Import core modules (will be loaded at runtime)\nlet cacheWidgetHtml: any;\nlet scheduleManager: any;\nlet config: any;\nlet RestClient: any;\nlet XmdsClient: any;\nlet ProtocolDetector: any;\nlet XmrWrapper: any;\nlet store: StoreClient;\nlet downloadManager: DownloadManager;\nlet StatsCollector: any;\nlet formatStats: any;\nlet LogReporter: any;\nlet formatLogs: any;\nlet DisplaySettings: any;\nlet SyncManager: any;\nlet computeStagger: any;\n\n// SDK package versions (populated in loadCoreModules)\nconst sdkVersions: Record<string, string> = {};\n\nclass PwaPlayer {\n private renderer!: RendererLite;\n private core!: PlayerCore;\n private xmds!: any;\n private downloadOverlay: DownloadOverlay | null = null;\n private timelineOverlay: TimelineOverlay | null = null;\n private setupOverlay: SetupOverlay | null = null;\n private statsCollector: any = null;\n private logReporter: any = null;\n private displaySettings: any = null;\n private currentScheduleId: number = -1; // Track scheduleId for stats\n private scheduledLayoutIds: Set<number> = new Set(); // Layout IDs from current schedule\n private preparingLayoutId: number | null = null; // Guard against concurrent prepareLayout calls\n private _pendingRetryLayoutId: number | null = null; // Queued retry when check-pending-layout arrives during preparation\n private _screenshotInterval: any = null;\n private _screenshotMethod: 'electron' | 'displayMedia' | 'html2canvas' | null = null;\n private _html2canvasMod: any = null;\n private _screenshotInFlight = false; // Concurrency guard — one capture at a time\n private _wakeLock: any = null; // Screen Wake Lock sentinel\n private syncManager: any = null; // Multi-display sync coordinator\n private _currentLayoutEnableStat: boolean = true; // enableStat from current layout XLF\n private _probeTimer: any = null; // Debounce timer for duration probing\n private _mediaStatusTimer: ReturnType<typeof setTimeout> | null = null; // Debounce timer for media status check\n private _pendingFollowerStats: any[] | null = null; // In-flight stats delegated to lead\n private _pendingFollowerLogs: any[] | null = null; // In-flight logs delegated to lead\n private _iframeObserver: MutationObserver | null = null; // Iframe key-forwarding observer\n private _swIcHandler: any = null; // SW Interactive Control message handler\n private _chunkConfig: any = null; // Device-adaptive chunk configuration\n private _fileIdToSaveAs: Map<string, string> = new Map(); // Numeric file ID → storedAs filename\n private protocolDetector: any = null; // CMS protocol auto-detector\n\n async init() {\n log.info('Initializing player with RendererLite + PlayerCore...');\n\n // Load core modules\n await this.loadCoreModules();\n\n // Register Service Worker for offline-first kiosk mode\n if ('serviceWorker' in navigator) {\n try {\n const registration = await navigator.serviceWorker.register(`${PLAYER_BASE}/sw-pwa.js?v=${Date.now()}`, {\n scope: `${PLAYER_BASE}/`,\n type: 'module',\n updateViaCache: 'none'\n });\n log.info('Service Worker registered for offline mode:', registration.scope);\n\n // Request persistent storage (kiosk requirement)\n if (navigator.storage && navigator.storage.persist) {\n const persistent = await navigator.storage.persist();\n if (persistent) {\n log.info('Persistent storage granted - cache won\\'t be evicted');\n } else {\n log.warn('Persistent storage denied - cache may be evicted');\n }\n }\n } catch (error) {\n log.warn('Service Worker registration failed:', error);\n }\n }\n\n // Initialize StoreClient (REST) + DownloadManager (main thread)\n log.info('Initializing cache clients...');\n store = new StoreClient();\n const { calculateChunkConfig } = await import('@xiboplayer/sw');\n this._chunkConfig = calculateChunkConfig(log);\n downloadManager = new DownloadManager({\n concurrency: this._chunkConfig.concurrency,\n chunkSize: this._chunkConfig.chunkSize,\n chunksPerFile: 2,\n });\n log.info('Cache clients ready — StoreClient + DownloadManager');\n\n // Create renderer\n const container = document.getElementById('player-container');\n if (!container) {\n throw new Error('No #player-container found');\n }\n\n this.renderer = new RendererLite(\n {\n cmsUrl: config.cmsUrl,\n hardwareKey: config.hardwareKey\n },\n container,\n {\n // Provide fileId→saveAs map for layout background resolution\n fileIdToSaveAs: this._fileIdToSaveAs,\n\n // Provide widget HTML resolver — check ContentStore via proxy\n getWidgetHtml: async (widget: any) => {\n const widgetPath = `${PLAYER_API}/widgets/${widget.layoutId}/${widget.regionId}/${widget.id}`;\n log.debug(`Looking for widget HTML at: ${widgetPath}`, widget);\n\n try {\n const exists = await store.has(`${STORE_PREFIX}/widgets`, `${widget.layoutId}/${widget.regionId}/${widget.id}`);\n if (exists) {\n log.debug(`Widget HTML found in store, using mirror URL for iframe`);\n return { url: widgetPath, fallback: widget.raw || '' };\n } else {\n log.warn(`No widget HTML found in store: ${widgetPath}`);\n }\n } catch (error) {\n log.error(`Failed to check widget HTML for ${widget.id}:`, error);\n }\n\n // Fallback to widget.raw (XLF template)\n log.warn(`Using widget.raw fallback for ${widget.id}`);\n return widget.raw || '';\n }\n }\n );\n\n // Create PlayerCore (with CMS-namespaced offline cache DB)\n this.core = new PlayerCore({\n config,\n xmds: this.xmds,\n cache: store,\n schedule: scheduleManager,\n renderer: this.renderer,\n xmrWrapper: XmrWrapper,\n statsCollector: this.statsCollector,\n displaySettings: this.displaySettings,\n cmsId: config.activeCmsId,\n });\n\n // Setup platform-specific event handlers\n this.setupCoreEventHandlers();\n this.setupRendererEventHandlers();\n this.setupInteractiveControl();\n this.setupDataConnectorNotify();\n this.setupRemoteControls();\n\n // 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 // Respect controls.keyboard.debugOverlays — if disabled, don't restore overlays\n const controls = this.getControls();\n const debugOverlaysEnabled = (controls.keyboard || {}).debugOverlays === true;\n\n const overlayConfig = getDefaultOverlayConfig();\n if (overlayConfig.enabled && debugOverlaysEnabled) {\n this.downloadOverlay = new DownloadOverlay(overlayConfig);\n this.downloadOverlay.setProgressCallback(() => downloadManager.getProgress());\n log.info('Download overlay enabled (hover bottom-right corner)');\n }\n\n // Timeline overlay — created on first T key press (or if previously visible)\n if (isTimelineVisible() && debugOverlaysEnabled) {\n this.timelineOverlay = new TimelineOverlay(true, (layoutId) => this.skipToLayout(layoutId));\n }\n\n // Listen for certificate warnings from Electron main process\n this.setupCertWarnings();\n\n // Listen for XMR connection status changes\n this.setupXmrWarning();\n\n // Request Screen Wake Lock to prevent display sleep\n await this.requestWakeLock();\n\n // Re-acquire wake lock when tab becomes visible again\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'visible') {\n this.requestWakeLock();\n }\n });\n\n // Start collection cycle\n await this.core.collect();\n\n log.info('Player initialized successfully');\n }\n\n /**\n * Request Screen Wake Lock to prevent display from sleeping\n * Re-acquired on visibility change (browser releases it when tab is hidden)\n */\n private async requestWakeLock() {\n if (!('wakeLock' in navigator)) {\n log.debug('Wake Lock API not supported');\n return;\n }\n\n try {\n this._wakeLock = await (navigator as any).wakeLock.request('screen');\n log.info('Screen Wake Lock acquired — display will stay on');\n\n this._wakeLock.addEventListener('release', () => {\n log.debug('Screen Wake Lock released');\n this._wakeLock = null;\n });\n } catch (error: any) {\n log.warn('Wake Lock request failed:', error?.message);\n }\n }\n\n /**\n * Listen for certificate warnings from Electron and show in the top bar.\n * The #overlay bar (defined in index.html) is the status bar with\n * #config-info (left) and #status (right). If it was removed (statusBarOnHover\n * not set), we recreate it. Cert warnings make the bar always visible.\n */\n private setupCertWarnings() {\n const warnedHosts = new Set<string>();\n\n window.addEventListener('cert-warning', ((e: CustomEvent) => {\n const { host, error } = e.detail;\n if (warnedHosts.has(host)) return;\n warnedHosts.add(host);\n\n log.warn(`Invalid SSL certificate accepted for stream: ${host} (${error})`);\n\n // Find or recreate the top bar\n let overlay = document.getElementById('overlay');\n let created = false;\n if (!overlay) {\n overlay = document.createElement('div');\n overlay.id = 'overlay';\n // Recreate child structure: config-info | status\n const info = document.createElement('div');\n info.id = 'config-info';\n overlay.appendChild(info);\n const status = document.createElement('div');\n status.id = 'status';\n overlay.appendChild(status);\n document.body.appendChild(overlay);\n created = true;\n }\n\n // Find or create the cert warning span between #config-info and #status\n let certSpan = document.getElementById('cert-warnings');\n if (!certSpan) {\n certSpan = document.createElement('span');\n certSpan.id = 'cert-warnings';\n certSpan.style.cssText = 'color: #ffaa33; flex: 0 0 auto;';\n const statusEl = document.getElementById('status');\n overlay.insertBefore(certSpan, statusEl);\n }\n\n const hosts = [...warnedHosts].join(', ');\n certSpan.textContent = `\\u26A0 SSL: ${hosts}`;\n\n // Don't force always-visible — let hover-only CSS handle show/hide\n\n // If we recreated the overlay, repopulate config info\n if (created) this.updateConfigDisplay();\n }) as EventListener);\n }\n\n /**\n * Show/hide an XMR disconnected warning in the top bar.\n * Placed before #cert-warnings (or before #status if no cert warnings).\n */\n private setupXmrWarning() {\n this.core.on('xmr-status', ({ connected }: { connected: boolean }) => {\n const overlay = document.getElementById('overlay');\n if (!overlay) return;\n\n let span = document.getElementById('xmr-warning');\n\n if (!connected) {\n if (!span) {\n span = document.createElement('span');\n span.id = 'xmr-warning';\n span.style.cssText = 'color: #ff6666; flex: 0 0 auto;';\n // Insert before cert-warnings or status (whichever comes first)\n const anchor = document.getElementById('cert-warnings') || document.getElementById('status');\n overlay.insertBefore(span, anchor);\n }\n span.textContent = '\\u26A0 XMR disconnected';\n } else {\n span?.remove();\n }\n });\n }\n\n /**\n * Load core modules\n */\n private async loadCoreModules() {\n try {\n const [\n cacheModule, xmdsModule, scheduleModule, configModule,\n xmrModule, statsModule, displaySettingsModule, coreModule,\n rendererModule, syncModule,\n ] = await Promise.all([\n import('@xiboplayer/cache'),\n import('@xiboplayer/xmds'),\n import('@xiboplayer/schedule'),\n import('@xiboplayer/utils'),\n import('@xiboplayer/xmr'),\n import('@xiboplayer/stats'),\n import('@xiboplayer/settings'),\n import('@xiboplayer/core'),\n import('@xiboplayer/renderer'),\n import('@xiboplayer/sync'),\n ]);\n\n cacheWidgetHtml = cacheModule.cacheWidgetHtml;\n SyncManager = syncModule.SyncManager;\n computeStagger = syncModule.computeStagger;\n scheduleManager = scheduleModule.scheduleManager;\n config = configModule.config;\n RestClient = xmdsModule.RestClient;\n XmdsClient = xmdsModule.XmdsClient;\n ProtocolDetector = xmdsModule.ProtocolDetector;\n XmrWrapper = xmrModule.XmrWrapper;\n StatsCollector = statsModule.StatsCollector;\n formatStats = statsModule.formatStats;\n LogReporter = statsModule.LogReporter;\n formatLogs = statsModule.formatLogs;\n DisplaySettings = displaySettingsModule.DisplaySettings;\n\n // Capture SDK package versions\n sdkVersions.core = coreModule.VERSION || '?';\n sdkVersions.cache = cacheModule.VERSION || '?';\n sdkVersions.renderer = rendererModule.VERSION || '?';\n sdkVersions.schedule = scheduleModule.VERSION || '?';\n sdkVersions.xmds = xmdsModule.VERSION || '?';\n sdkVersions.xmr = xmrModule.VERSION || '?';\n sdkVersions.utils = configModule.VERSION || '?';\n sdkVersions.stats = statsModule.VERSION || '?';\n sdkVersions.settings = displaySettingsModule.VERSION || '?';\n\n // Get MAC address from Electron if available (for WOL support)\n if ((window as any).electronAPI?.getSystemInfo) {\n try {\n const sysInfo = await (window as any).electronAPI.getSystemInfo();\n if (sysInfo.macAddress) {\n config.macAddress = sysInfo.macAddress;\n }\n } catch (_) { /* pure PWA — no Electron API */ }\n }\n\n // Transport selection:\n // transport: \"rest\" → forced REST API\n // transport: \"xmds\" → forced SOAP\n // transport: \"auto\" → probe REST → SOAP fallback (default)\n // /player/pwa-xmds/ → forced SOAP (URL-based override)\n // ?transport=xmds → forced SOAP (query param override)\n const cfgTransport = config.transport !== 'auto' ? config.transport : undefined;\n const urlTransport = new URLSearchParams(window.location.search).get('transport');\n const transport = urlTransport\n || (PLAYER_BASE.includes('pwa-xmds') ? 'xmds' : null)\n || cfgTransport\n || 'auto';\n\n // Use ProtocolDetector for auto-detection with re-probe support\n this.protocolDetector = new ProtocolDetector(config.cmsUrl, RestClient, XmdsClient);\n const forceProtocol = (transport === 'auto') ? undefined : transport;\n const { client } = await this.protocolDetector.detect(config, forceProtocol);\n this.xmds = client;\n\n // Initialize stats collector (namespaced by CMS ID)\n const cmsId = config.activeCmsId;\n this.statsCollector = new StatsCollector(cmsId);\n await this.statsCollector.init();\n log.info(`Stats collector initialized${cmsId ? ` (CMS: ${cmsId})` : ''}`);\n\n // Initialize log reporter for CMS log submission (namespaced by CMS ID)\n this.logReporter = new LogReporter(cmsId);\n await this.logReporter.init();\n log.info(`Log reporter initialized${cmsId ? ` (CMS: ${cmsId})` : ''}`);\n\n // 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 // Forward console logs to proxy stdout (for journald/log analysis).\n // Controlled by debug.consoleLogs in config.json.\n // Optional debug.consoleLogsInterval (seconds) sets the batch flush interval (default 10s).\n const debugConfig = config.debug;\n if (debugConfig?.consoleLogs) {\n const flushIntervalMs = (debugConfig.consoleLogsInterval || 10) * 1000;\n let batch: Array<{ level: string; name: string; message: string; ts: string }> = [];\n let flushTimer: ReturnType<typeof setTimeout> | null = null;\n\n const flushLogs = () => {\n if (batch.length === 0) return;\n const payload = batch;\n batch = [];\n flushTimer = null;\n // Fire-and-forget POST — log forwarding must never block the player\n fetch('/debug/log', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n }).catch(() => {});\n };\n\n registerLogSink(({ level, name, args }: { level: string; name: string; args: any[] }) => {\n const message = args.map((a: any) => typeof a === 'string' ? a : JSON.stringify(a)).join(' ');\n batch.push({ level, name, message, ts: new Date().toISOString() });\n if (!flushTimer) {\n flushTimer = setTimeout(flushLogs, flushIntervalMs);\n }\n });\n\n log.info(`Console log forwarding to proxy enabled (flush every ${flushIntervalMs / 1000}s)`);\n }\n\n // Initialize display settings manager\n this.displaySettings = new DisplaySettings();\n log.info('Display settings manager initialized');\n\n // Log version and environment information for debugging\n const buildDate = typeof __BUILD_DATE__ !== 'undefined' ? __BUILD_DATE__ : '?';\n const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '?';\n log.info(`v${appVersion} built ${buildDate}`);\n const versionParts = Object.entries(sdkVersions).map(([k, v]) => `${k}=${v}`).join(' ');\n log.info(`SDK: ${versionParts}`);\n const isElectron = !!(window as any).electronAPI;\n const electronVersion = isElectron ? (navigator.userAgent.match(/Electron\\/([\\d.]+)/)?.[1] || '?') : null;\n const chromeVersion = navigator.userAgent.match(/Chrome\\/([\\d.]+)/)?.[1] || '?';\n const platform = isElectron ? `Electron ${electronVersion} / Chrome ${chromeVersion}` : `Chrome ${chromeVersion}`;\n log.info(`Env: PWA v${appVersion} | ${platform} | ${navigator.platform} | ${screen.width}x${screen.height}`);\n\n log.info('Core modules loaded');\n } catch (error) {\n log.error('Failed to load core modules:', error);\n throw error;\n }\n }\n\n /**\n * Setup PlayerCore event handlers (Platform-specific UI updates)\n */\n private setupCoreEventHandlers() {\n // 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: local config fallback when CMS doesn't provide syncConfig\n this.core.on('register-complete', (regResult: any) => {\n if (!regResult.syncConfig && config.data?.sync) {\n log.info('[Sync] Using local sync config (CMS did not provide syncConfig)');\n this.core.syncConfig = config.data.sync;\n this.core.emit('sync-config', config.data.sync);\n }\n });\n\n // Offline sync: if CMS is unreachable but local config has sync settings,\n // start SyncManager so LAN-only displays can still sync with each other.\n this.core.on('offline-mode', (isOffline: boolean) => {\n if (isOffline && !this.syncManager && config.data?.sync) {\n log.info('[Sync] Offline mode with local sync config — starting sync');\n this.core.syncConfig = config.data.sync;\n this.core.emit('sync-config', config.data.sync);\n }\n });\n\n // Multi-display sync: create SyncManager when CMS provides sync config (or local fallback)\n this.core.on('sync-config', (syncConfig: any) => {\n if (this.syncManager) {\n this.syncManager.stop();\n }\n\n // Cross-device sync: build WebSocket relay URL if not explicitly set.\n // Lead connects to its own relay (localhost), followers connect to lead's IP.\n // Use syncGroupId as relay group name — CMS gives different syncGroup values\n // to lead (\"lead\") vs followers (lead's IP), but syncGroupId is the same for all.\n if (!syncConfig.relayUrl && syncConfig.syncPublisherPort) {\n const host = syncConfig.isLead ? 'localhost' : syncConfig.syncGroup;\n syncConfig.relayUrl = `ws://${host}:${syncConfig.syncPublisherPort}/sync`;\n if (syncConfig.syncGroupId) {\n syncConfig.syncGroup = String(syncConfig.syncGroupId);\n }\n }\n\n // Persist resolved sync config to config.json so offline restarts\n // can sync over LAN without CMS. Strips runtime-only fields.\n // Merge with existing sync config to preserve local-only fields\n // (topology, choreography, staggerMs, gridCols, gridRows).\n const { syncToken, ...persistable } = syncConfig;\n const merged = { ...(config.data?.sync || {}), ...persistable };\n if ((window as any).electronAPI?.setConfig) {\n (window as any).electronAPI.setConfig({ sync: merged });\n } else {\n fetch('/config', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sync: merged }),\n }).catch(() => {});\n }\n\n // Pass CMS server key as sync token for relay auth (shared by all displays on this CMS)\n if (!syncConfig.syncToken) {\n syncConfig.syncToken = config.cmsKey;\n }\n\n // Persist resolved sync config to config.json so offline restarts\n // can sync over LAN without CMS. Strips runtime-only fields.\n if ((window as any).electronAPI?.setConfig) {\n const { syncToken, ...persistable } = syncConfig;\n (window as any).electronAPI.setConfig({ sync: persistable });\n }\n\n // Pass CMS server key as sync token for relay auth (shared by all displays on this CMS)\n if (!syncConfig.syncToken) {\n syncConfig.syncToken = config.cmsKey;\n }\n\n this.syncManager = new SyncManager({\n displayId: config.hardwareKey,\n syncConfig,\n onLayoutChange: async (layoutId: string) => {\n // Wall mode: map lead's layout to this display's position-specific layout\n const layoutMap = syncConfig.layoutMap || config.sync?.layoutMap;\n const mappedId = layoutMap?.[layoutId] ?? layoutId;\n if (mappedId !== layoutId) {\n log.info(`[Sync] Wall mode: lead layout ${layoutId} → local layout ${mappedId}`);\n }\n // Follower: preload layout hidden, show comes from onLayoutShow\n log.info(`[Sync] Preparing layout ${mappedId} (waiting for show signal)`);\n await this.prepareLayout(parseInt(String(mappedId), 10));\n // Report ready to lead (use lead's layoutId so lead can track readiness)\n this.syncManager?.reportReady(layoutId);\n },\n onLayoutShow: (layoutId: string) => {\n // Map lead's layout ID to this display's layout (wall mode)\n const layoutMap = syncConfig.layoutMap || config.sync?.layoutMap;\n const mappedId = layoutMap?.[layoutId] ?? layoutId;\n const numericId = parseInt(String(mappedId), 10);\n\n // Compute choreography stagger delay (0 if no choreography configured)\n const choreo = syncConfig.choreography || 'simultaneous';\n const staggerMs = syncConfig.staggerMs ?? 150;\n\n // Build stagger options: prefer 2D topology, fall back to 1D position\n const staggerOpts: any = { choreography: choreo, staggerMs };\n if (syncConfig.topology) {\n staggerOpts.topology = syncConfig.topology;\n staggerOpts.gridCols = syncConfig.gridCols ?? 1;\n staggerOpts.gridRows = syncConfig.gridRows ?? 1;\n } else {\n staggerOpts.position = syncConfig.position ?? 0;\n staggerOpts.totalDisplays = syncConfig.totalDisplays ?? 1;\n }\n const stagger = computeStagger(staggerOpts);\n\n if (stagger > 0) {\n log.info(`[Sync] Show layout ${numericId} with ${stagger}ms choreography delay (${choreo})`);\n setTimeout(() => this.renderer.showLayout(numericId), stagger);\n } else {\n log.info(`[Sync] Show layout ${numericId}`);\n this.renderer.showLayout(numericId);\n }\n },\n onVideoStart: (layoutId: string, regionId: string) => {\n // Resume paused video in the specified region\n log.info(`[Sync] Video start: layout ${layoutId} region ${regionId}`);\n this.renderer.resumeRegionMedia?.(regionId);\n },\n // Lead: follower delegated stats — submit on their behalf\n onStatsReport: async (followerId: string, statsXml: string, ack: () => void) => {\n log.info(`[Sync] Submitting stats for follower ${followerId}`);\n try {\n const success = await this.xmds.submitStats(statsXml, followerId);\n if (success) ack();\n } catch (err: any) {\n log.warn(`[Sync] Stats submission failed for follower ${followerId}:`, err);\n }\n },\n // Lead: follower delegated logs — submit on their behalf\n onLogsReport: async (followerId: string, logsXml: string, ack: () => void) => {\n log.info(`[Sync] Submitting logs for follower ${followerId}`);\n try {\n const success = await this.xmds.submitLog(logsXml, followerId);\n if (success) ack();\n } catch (err: any) {\n log.warn(`[Sync] Log submission failed for follower ${followerId}:`, err);\n }\n },\n // Follower: lead confirmed our stats were submitted\n onStatsAck: async (_displayId: string) => {\n log.info('[Sync] Lead confirmed stats submission');\n if (this._pendingFollowerStats && this.statsCollector) {\n await this.statsCollector.clearSubmittedStats(this._pendingFollowerStats);\n this._pendingFollowerStats = null;\n }\n },\n // Follower: lead confirmed our logs were submitted\n onLogsAck: async (_displayId: string) => {\n log.info('[Sync] Lead confirmed logs submission');\n if (this._pendingFollowerLogs && this.logReporter) {\n await this.logReporter.clearSubmittedLogs(this._pendingFollowerLogs);\n this._pendingFollowerLogs = null;\n }\n },\n // Relay: group membership changed (auto-detect totalDisplays)\n onGroupUpdate: (totalDisplays: number, topology: Record<string, any>) => {\n log.info(`[Sync] Group update: ${totalDisplays} displays, topology: ${JSON.stringify(topology)}`);\n syncConfig.totalDisplays = totalDisplays;\n },\n });\n this.core.setSyncManager(this.syncManager);\n this.syncManager.start();\n log.info(`[Sync] SyncManager started as ${syncConfig.isLead ? 'LEAD' : 'FOLLOWER'}`);\n this.updateConfigDisplay();\n });\n\n 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 store.remove(purgeFiles);\n log.info(`Purge complete: ${result.deleted}/${result.total} files deleted`);\n } catch (error) {\n log.warn('Purge failed:', error);\n }\n });\n\n this.core.on('download-request', async (groupedFiles: any) => {\n // Download orchestration runs in main thread — no SW messaging\n this.downloadOverlay?.startUpdating();\n try {\n // Push current JWT token to proxy for cache-through CMS requests\n const token = this.xmds?._token || null;\n if (token) {\n await fetch('/auth-token', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ token }),\n });\n }\n await this.enqueueDownloads(groupedFiles);\n log.info('Download enqueue complete');\n } catch (error) {\n log.error('Download request failed:', error);\n this.updateStatus('Download failed: ' + error, 'error');\n }\n });\n\n this.core.on('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.prepareLayout(layoutId);\n // Non-sync or no layout playing yet: show immediately.\n // Sync transitions: onLayoutShow handles showing with stagger.\n if (!this.syncManager || this.renderer.currentLayoutId === null) {\n this.renderer.showLayout(layoutId);\n }\n });\n\n this.core.on('layout-expire-current', () => {\n log.info('Schedule changed — expiring current layout');\n this.renderer.stopCurrentLayout();\n // stopCurrentLayout() emits layoutEnd → the layoutEnd handler\n // calls advanceToNextLayout() which picks the next scheduled layout\n });\n\n this.core.on('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 // Duration probing is handled by the debounced re-probe (3s after last\n // file cached) — avoids 404s from probing before downloads complete.\n });\n\n this.core.on('collection-error', async (error: any) => {\n this.updateStatus(`Collection error: ${error}`, 'error');\n\n // Display not found / not authorized — show setup screen so user can re-register\n const msg = error?.message || String(error);\n if (msg.includes('403') && (msg.includes('Display not found') || msg.includes('not authorized'))) {\n log.warn('Display not registered or not authorized — showing setup screen');\n if (!this.setupOverlay) {\n this.setupOverlay = new SetupOverlay();\n }\n this.setupOverlay.show();\n return;\n }\n\n // Report fault to CMS (triggers dashboard alert)\n this.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 });\n\n // Log level changes from CMS (overlays are controlled by config.controls, not log level)\n this.core.on('log-level-changed', () => {\n log.info(`Log level changed`);\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.prepareLayout(layoutId);\n this.renderer.showLayout(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 files from ContentStore\n const allFiles = await store.list();\n if (allFiles.length > 0) {\n const result = await store.remove(allFiles);\n log.info(`Purged ${result.deleted} files from ContentStore`);\n }\n // Clean up any legacy Cache API caches (pre-ContentStore migration)\n const cacheNames = await caches.keys();\n if (cacheNames.length > 0) {\n await Promise.all(cacheNames.map(name => caches.delete(name)));\n log.info(`Purged ${cacheNames.length} legacy caches`);\n }\n } catch (error) {\n log.error('Cache purge failed:', error);\n }\n });\n\n // 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 // Native command execution (#202) — shell commands delegated by PlayerCore\n // Electron: use IPC (in-process, faster). Chromium/other: HTTP to proxy server.\n this.core.on('execute-native-command', async (data: any) => {\n let result;\n if ((window as any).electronAPI?.executeShellCommand) {\n result = await (window as any).electronAPI.executeShellCommand({\n commandString: data.commandString,\n });\n } else {\n try {\n const resp = await fetch('/shell-command', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ commandString: data.commandString }),\n });\n result = await resp.json();\n } catch (err: any) {\n result = { success: false, reason: err.message };\n }\n }\n this.core.emit('command-result', { code: data.code, ...result });\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 — layout was pending download, now ready\n this.core.on('check-pending-layout', async (layoutId: number) => {\n await this.prepareLayout(layoutId);\n this.renderer.showLayout(layoutId);\n });\n\n // Navigate to widget (navWidget action via triggerCode from schedule-level actions)\n this.core.on('navigate-to-widget', (action: any) => {\n if (action.targetId) {\n this.renderer.navigateToWidget(action.targetId);\n } else {\n log.warn('navigate-to-widget action has no targetId:', action);\n }\n });\n\n // Timeline overlay — visualize upcoming schedule\n this.core.on('timeline-updated', (timeline: any[]) => {\n const id = this.core.getCurrentLayoutId();\n const dur = id ? this.core.getLayoutDuration(id) : undefined;\n this.timelineOverlay?.update(timeline, id, dur);\n });\n }\n\n\n /**\n * Setup Interactive Control handler (receives messages from SW for widget IC requests)\n * IC library in widget iframes makes XHR to /player/pwa/ic/*, SW forwards here.\n */\n private setupInteractiveControl() {\n this._swIcHandler = (event: any) => {\n if (event.data?.type !== 'INTERACTIVE_CONTROL') return;\n\n const { method, path, search, body } = event.data;\n const port = event.ports?.[0];\n if (!port) return;\n\n const response = this.handleInteractiveControl(method, path, search, body);\n port.postMessage(response);\n };\n navigator.serviceWorker?.addEventListener('message', this._swIcHandler);\n }\n\n /**\n * Notify widget iframes when DataConnector data changes.\n * XIC library listens for postMessage { ctrl: 'rtNotifyData', data: { dataKey } }\n * and calls the widget's registered notifyData callback.\n */\n private setupDataConnectorNotify() {\n const dcManager = this.core.getDataConnectorManager();\n dcManager.on('data-changed', (dataKey: string) => {\n const iframes = document.querySelectorAll<HTMLIFrameElement>('iframe');\n const message = { ctrl: 'rtNotifyData', data: { dataKey } };\n for (const iframe of iframes) {\n try {\n iframe.contentWindow?.postMessage(message, '*');\n } catch { /* cross-origin iframe, ignore */ }\n }\n });\n }\n\n /**\n * Setup keyboard and presenter remote controls.\n * Handles arrow keys, page up/down, space for next/prev/pause,\n * and MediaSession API for multimedia keyboard keys.\n */\n private setupRemoteControls() {\n // Keep focus on main document so keyboard shortcuts work even with widget iframes.\n // Iframes steal focus — this pulls it back after a short delay so interactive\n // widgets still work momentarily but keyboard control returns to the player.\n window.addEventListener('blur', () => {\n // Don't steal focus when setup overlay is open (user is typing in iframe inputs)\n if (this.setupOverlay?.isVisible()) return;\n setTimeout(() => window.focus(), 200);\n });\n\n // Forward keyboard events from widget iframes to the main document.\n // Iframes have their own document, so keydown on the parent never fires\n // when an iframe has focus. We observe new iframes and attach forwarders.\n const attachIframeKeyForwarder = (iframe: HTMLIFrameElement) => {\n const tryAttach = () => {\n try {\n const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;\n if (!iframeDoc) return;\n if ((iframe as any).__keyForwarderAttached) return;\n (iframe as any).__keyForwarderAttached = true;\n iframeDoc.addEventListener('keydown', (e: KeyboardEvent) => {\n // Don't forward keys from setup overlay — user is typing in form inputs\n if (this.setupOverlay?.isVisible()) return;\n // Re-dispatch on the main document so our handler fires\n const clone = new KeyboardEvent('keydown', {\n key: e.key, code: e.code, keyCode: e.keyCode,\n ctrlKey: e.ctrlKey, shiftKey: e.shiftKey, altKey: e.altKey, metaKey: e.metaKey,\n bubbles: true, cancelable: true,\n });\n if (document.dispatchEvent(clone)) return; // not prevented\n e.preventDefault();\n });\n } catch { /* cross-origin iframe, ignore */ }\n };\n iframe.addEventListener('load', tryAttach);\n tryAttach();\n };\n\n // Attach to existing and future iframes\n Array.from(document.querySelectorAll('iframe')).forEach(f => attachIframeKeyForwarder(f as HTMLIFrameElement));\n this._iframeObserver = new MutationObserver((mutations) => {\n for (const m of mutations) {\n for (const node of m.addedNodes) {\n if (node instanceof HTMLIFrameElement) attachIframeKeyForwarder(node);\n if (node instanceof HTMLElement) {\n node.querySelectorAll('iframe').forEach(f => attachIframeKeyForwarder(f as HTMLIFrameElement));\n }\n }\n }\n });\n this._iframeObserver.observe(document.body, { childList: true, subtree: true });\n\n // Read control toggles from config (injected by proxy into localStorage)\n const controls = this.getControls();\n const { keyboard: kb = {} } = controls;\n const debugOverlays = kb.debugOverlays === true;\n const setupKey = kb.setupKey === true;\n const playbackControl = kb.playbackControl === true;\n const videoControls = kb.videoControls === true;\n\n // Keyboard / presenter remote (clicker) controls\n document.addEventListener('keydown', (e: KeyboardEvent) => {\n // Ctrl+Q — quit (Chromium kiosk: calls server /quit; Electron: handled by menu accelerator)\n if (e.key === 'q' && (e.ctrlKey || e.metaKey)) {\n e.preventDefault();\n log.info('[Remote] Quit requested (Ctrl+Q)');\n fetch('/quit', { method: 'POST' }).catch(() => {});\n return;\n }\n\n switch (e.key) {\n case 't':\n case 'T':\n if (!debugOverlays) break;\n if (!this.timelineOverlay) {\n this.timelineOverlay = new TimelineOverlay(true, (layoutId) => this.skipToLayout(layoutId));\n }\n this.timelineOverlay.toggle();\n break;\n case 'd':\n case 'D':\n if (!debugOverlays) break;\n if (!this.downloadOverlay) {\n this.downloadOverlay = new DownloadOverlay({ enabled: true, autoHide: false });\n this.downloadOverlay.setProgressCallback(() => downloadManager.getProgress());\n }\n this.downloadOverlay.toggle();\n break;\n case 'v':\n case 'V': {\n if (!videoControls) break;\n // Collect videos from parent + all same-origin iframes (widget regions)\n const allVideos: HTMLVideoElement[] = [...document.querySelectorAll<HTMLVideoElement>('video')];\n document.querySelectorAll<HTMLIFrameElement>('iframe').forEach(iframe => {\n try { allVideos.push(...iframe.contentDocument!.querySelectorAll<HTMLVideoElement>('video')); } catch {}\n });\n const show = allVideos.length > 0 && !allVideos[0].controls;\n allVideos.forEach(v => v.controls = show);\n break;\n }\n // Playback control: next/prev/pause\n case 'ArrowRight':\n case 'PageDown':\n if (!playbackControl) break;\n log.info('[Remote] Next layout (keyboard)');\n this.core.advanceToNextLayout();\n e.preventDefault();\n break;\n case 'ArrowLeft':\n case 'PageUp':\n if (!playbackControl) break;\n log.info('[Remote] Previous layout (keyboard)');\n this.core.advanceToPreviousLayout();\n e.preventDefault();\n break;\n case ' ':\n if (!playbackControl) break;\n log.info('[Remote] Toggle pause (keyboard)');\n if (this.renderer.isPaused()) {\n this.renderer.resume();\n } else {\n this.renderer.pause();\n }\n e.preventDefault();\n break;\n case 'r':\n case 'R':\n if (!playbackControl) break;\n if (this.core.isLayoutOverridden()) {\n log.info('[Remote] Revert to schedule (keyboard)');\n this.core.revertToSchedule();\n }\n break;\n case 's':\n case 'S':\n if (!setupKey) break;\n if (!this.setupOverlay) {\n this.setupOverlay = new SetupOverlay();\n }\n this.setupOverlay.toggle();\n e.preventDefault(); // prevent 's' from being typed into the focused input\n break;\n }\n });\n\n // MediaSession API for multimedia keys (only fires when media is active)\n if (playbackControl && 'mediaSession' in navigator) {\n navigator.mediaSession.setActionHandler('nexttrack', () => {\n log.info('[Remote] Next layout (MediaSession)');\n this.core.advanceToNextLayout();\n });\n navigator.mediaSession.setActionHandler('previoustrack', () => {\n log.info('[Remote] Previous layout (MediaSession)');\n this.core.advanceToPreviousLayout();\n });\n navigator.mediaSession.setActionHandler('pause', () => {\n log.info('[Remote] Pause (MediaSession)');\n this.renderer.pause();\n });\n navigator.mediaSession.setActionHandler('play', () => {\n log.info('[Remote] Resume (MediaSession)');\n this.renderer.resume();\n });\n }\n\n log.info('Remote controls initialized (keyboard + MediaSession)');\n }\n\n /** Read controls config (injected by proxy from config.json into localStorage). */\n private getControls(): Record<string, any> {\n return config.controls;\n }\n\n /**\n * Skip to a specific layout by ID (from timeline click or XMR command).\n * Uses changeLayout() which sets a layout override — press R to revert to schedule.\n */\n private skipToLayout(layoutId: number) {\n log.info(`Skipping to layout ${layoutId} (timeline click)`);\n this.core.changeLayout(layoutId);\n }\n\n private parseBody(body: string | null): any {\n try { return body ? JSON.parse(body) : {}; } catch (_) { return {}; }\n }\n\n /**\n * Handle an Interactive Control request from a widget\n */\n private handleInteractiveControl(method: string, path: string, search: string, body: string | null): any {\n log.debug('IC request:', method, path, search);\n\n switch (path) {\n case '/info':\n return {\n status: 200,\n body: JSON.stringify({\n hardwareKey: config.hardwareKey,\n displayName: config.displayName,\n playerType: 'pwa',\n currentLayoutId: this.core.getCurrentLayoutId()\n })\n };\n\n case '/trigger': {\n const data = this.parseBody(body);\n // Forward to renderer for layout-level actions (widget navigation)\n this.renderer.emit('interactiveTrigger', {\n targetId: data.id,\n triggerCode: data.trigger\n });\n // Forward to core for schedule-level actions (layout navigation)\n if (data.trigger) {\n this.core.handleTrigger(data.trigger);\n }\n return { status: 200, body: 'OK' };\n }\n\n case '/duration/expire': {\n const data = this.parseBody(body);\n log.info('IC: Widget duration expire requested for', data.id);\n this.renderer.emit('widgetExpire', { widgetId: data.id });\n return { status: 200, body: 'OK' };\n }\n\n case '/duration/extend': {\n const data = this.parseBody(body);\n log.info('IC: Widget duration extend by', data.duration, 'for', data.id);\n this.renderer.emit('widgetExtendDuration', {\n widgetId: data.id,\n duration: parseInt(data.duration)\n });\n return { status: 200, body: 'OK' };\n }\n\n case '/duration/set': {\n const data = this.parseBody(body);\n log.info('IC: Widget duration set to', data.duration, 'for', data.id);\n this.renderer.emit('widgetSetDuration', {\n widgetId: data.id,\n duration: parseInt(data.duration)\n });\n return { status: 200, body: 'OK' };\n }\n\n case '/fault': {\n const data = this.parseBody(body);\n this.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 * Notify PlayerCore that a file download completed.\n * Called directly from enqueueDownloads() — no SW messaging needed.\n */\n private notifyFileCached(fileId: string, fileType: string) {\n log.debug(`Download complete: ${fileType}/${fileId}`);\n\n if (fileType === 'layout') {\n this.core.notifyMediaReady(parseInt(fileId), fileType);\n } else if (fileType === 'media') {\n // Pass saveAs string for media files (matches pendingLayouts entries)\n const saveAs = this._fileIdToSaveAs.get(fileId) || fileId;\n this.core.notifyMediaReady(saveAs, 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 // Debounced media status check — update timeline missing-media annotations\n if (this._mediaStatusTimer) clearTimeout(this._mediaStatusTimer);\n this._mediaStatusTimer = setTimeout(() => {\n this._mediaStatusTimer = null;\n this.checkTimelineMediaStatus().catch(() => {});\n }, 2000);\n }\n\n /**\n * Enqueue files for download — runs in main thread, no SW messaging.\n * Ported from MessageHandler.handleDownloadFiles() with direct callbacks.\n */\n private async enqueueDownloads(data: any) {\n const { extractMediaIdsFromXlf } = await import('@xiboplayer/sw');\n const { layoutOrder, files, layoutDependants } = data;\n const queue = downloadManager.queue;\n\n /** Store key = URL path without leading / and query params */\n const storeKeyFrom = (f: any) => (f.path || '').split('?')[0].replace(/^\\/+/, '') || `${f.type || 'media'}/${f.id}`;\n\n // Build fileId→saveAs map from CMS RequiredFiles data\n for (const f of files) {\n if (f.saveAs) {\n this._fileIdToSaveAs.set(String(f.id), f.saveAs);\n }\n }\n // Build lookup maps from flat CMS file list\n const xlfFiles = new Map();\n const resources: any[] = [];\n const mediaFiles = new Map();\n const idToKeys = new Map();\n for (const f of files) {\n if (f.type === 'layout') {\n xlfFiles.set(parseInt(f.id), f);\n } else if (f.type === 'static') {\n resources.push(f);\n } else {\n const key = `${f.type}:${f.id}`;\n mediaFiles.set(key, f);\n const bareId = String(f.id);\n if (!idToKeys.has(bareId)) idToKeys.set(bareId, []);\n idToKeys.get(bareId).push(key);\n }\n }\n\n log.info(`Download: ${layoutOrder.length} layouts, ${mediaFiles.size} media, ${resources.length} resources`);\n\n // ── Step 1: Fetch + parse all XLFs (cache-through handles store/CMS) ──\n const layoutMediaMap = new Map();\n const allXlfIds = [...layoutOrder, ...[...xlfFiles.keys()].filter((id: number) => !layoutOrder.includes(id))];\n const xlfPromises = allXlfIds.map(async (layoutId: number) => {\n const xlfFile = xlfFiles.get(layoutId);\n if (!xlfFile?.path) return;\n\n let xlfText: string | undefined;\n\n // Try store first, then cache-through fetches from CMS on miss\n try {\n const headers: Record<string, string> = {};\n if (xlfFile.cmsDownloadUrl) headers['X-Cms-Download-Url'] = xlfFile.cmsDownloadUrl;\n const resp = await fetch(xlfFile.path, Object.keys(headers).length ? { headers } : undefined);\n if (resp.ok) {\n xlfText = await resp.text();\n log.info(`Fetched XLF ${layoutId} (${xlfText.length} bytes)`);\n this.notifyFileCached(String(layoutId), 'layout');\n }\n } catch (_) {}\n\n if (xlfText) {\n layoutMediaMap.set(layoutId, extractMediaIdsFromXlf(xlfText, log));\n }\n });\n await Promise.allSettled(xlfPromises);\n log.info(`Parsed ${layoutMediaMap.size} XLFs`);\n\n // Helper: enqueue a file, attach completion callback\n const enqueueFile = async (builder: any, file: any): Promise<boolean> => {\n if (!file.path || file.path === 'null' || file.path === 'undefined') return false;\n\n const storeKey = storeKeyFrom(file);\n\n // Check if already stored on disk\n try {\n const headResp = await fetch(`/store/${storeKey}`, { method: 'HEAD' });\n if (headResp.ok) return false;\n } catch (_) {}\n\n // Check if already downloading\n if (downloadManager.getTask(storeKey)) return false;\n\n // Check for existing chunks — skip already-downloaded ones\n try {\n const mcResp = await fetch(`/store/missing-chunks/${storeKey}`);\n if (mcResp.ok) {\n const { missing, numChunks } = await mcResp.json();\n if (numChunks > 0 && missing.length < numChunks) {\n const existing = new Set<number>();\n for (let i = 0; i < numChunks; i++) {\n if (!missing.includes(i)) existing.add(i);\n }\n file.skipChunks = existing;\n log.info(`Resuming ${storeKey}: ${existing.size}/${numChunks} chunks cached, ${missing.length} to download`);\n }\n }\n } catch (_) {}\n\n const fileDownload = builder.addFile(file);\n if (fileDownload.state !== 'pending') return false;\n\n // Direct callback — no postMessage needed\n fileDownload.wait().then((blob: any) => {\n const fileSize = parseInt(file.size) || blob.size;\n log.info('Download complete:', storeKey, `(${fileSize} bytes)`);\n\n // Mark chunked files as complete\n if (fileSize > this._chunkConfig.chunkSize) {\n fetch('/store/mark-complete', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ storeKey }),\n }).catch((e: any) => log.warn('mark-complete failed:', storeKey, e.message));\n }\n\n this.notifyFileCached(String(file.id), file.type);\n queue.removeCompleted(storeKey);\n }).catch((err: any) => {\n log.error('Download failed:', file.id, err);\n queue.removeCompleted(storeKeyFrom(file));\n });\n return true;\n };\n\n // ── Step 2: Enqueue resources ──\n const resourceBuilder = new LayoutTaskBuilder(queue);\n for (const file of resources) {\n await enqueueFile(resourceBuilder, file);\n }\n const resourceTasks = await resourceBuilder.build();\n if (resourceTasks.length > 0) {\n resourceTasks.push(BARRIER);\n queue.enqueueOrderedTasks(resourceTasks);\n }\n\n // ── Step 3: For each layout in play order, merge XLF + dependants ──\n const claimed = new Set();\n const nonScheduledIds = [...layoutMediaMap.keys()].filter((id: number) => !layoutOrder.includes(id));\n const filenameToMediaId = new Map();\n for (const [key, file] of mediaFiles) {\n if (file.saveAs) filenameToMediaId.set(file.saveAs, key);\n }\n\n const depMap = new Map();\n if (layoutDependants) {\n for (const [id, filenames] of Object.entries(layoutDependants)) {\n depMap.set(parseInt(id, 10), filenames);\n }\n }\n\n for (const layoutId of layoutOrder) {\n const xlfMediaIds = layoutMediaMap.get(layoutId);\n if (!xlfMediaIds) continue;\n\n const bareIds = new Set(xlfMediaIds);\n for (const nsId of nonScheduledIds) {\n const nsMediaIds = layoutMediaMap.get(nsId);\n if (nsMediaIds) {\n for (const id of nsMediaIds) bareIds.add(id);\n }\n }\n const deps = depMap.get(layoutId) || [];\n for (const filename of deps) {\n const key = filenameToMediaId.get(filename);\n if (key) bareIds.add(key);\n }\n\n const matched: any[] = [];\n for (const bareId of bareIds) {\n if (mediaFiles.has(bareId) && !claimed.has(bareId)) {\n matched.push(mediaFiles.get(bareId));\n claimed.add(bareId);\n continue;\n }\n const keys = idToKeys.get(String(bareId)) || [];\n for (const key of keys) {\n if (claimed.has(key)) continue;\n matched.push(mediaFiles.get(key));\n claimed.add(key);\n }\n }\n if (matched.length === 0) continue;\n\n log.info(`Layout ${layoutId}: ${matched.length} media`);\n matched.sort((a: any, b: any) => (a.size || 0) - (b.size || 0));\n const builder = new LayoutTaskBuilder(queue);\n for (const file of matched) {\n await enqueueFile(builder, file);\n }\n const orderedTasks = await builder.build();\n if (orderedTasks.length > 0) {\n orderedTasks.push(BARRIER);\n queue.enqueueOrderedTasks(orderedTasks);\n }\n }\n\n // Enqueue unclaimed media\n const unclaimed = [...mediaFiles.keys()].filter((id: string) => !claimed.has(id));\n if (unclaimed.length > 0) {\n log.info(`${unclaimed.length} media not in any XLF`);\n const builder = new LayoutTaskBuilder(queue);\n for (const id of unclaimed) {\n const file = mediaFiles.get(id);\n if (file) await enqueueFile(builder, file);\n }\n const orderedTasks = await builder.build();\n if (orderedTasks.length > 0) {\n queue.enqueueOrderedTasks(orderedTasks);\n }\n }\n\n log.info('Downloads active:', queue.running, ', queued:', queue.queue.length);\n }\n\n /**\n * Setup renderer event handlers\n */\n private setupRendererEventHandlers() {\n this.renderer.on('layoutStart', (layoutId: number, _layout: any) => {\n log.info('Layout started:', layoutId);\n this.updateStatus(`Playing layout ${layoutId}`);\n\n this.core.setCurrentLayout(layoutId);\n\n // Store layout-level enableStat for use in layoutEnd\n this._currentLayoutEnableStat = _layout?.enableStat !== false;\n\n // Update timeline overlay with current layout's known duration\n const layoutDur = this.core.getLayoutDuration(layoutId) || _layout?.duration;\n this.timelineOverlay?.update(null, layoutId, layoutDur);\n\n // Track stats: start layout (only if enableStat is not disabled)\n if (this.statsCollector && this._currentLayoutEnableStat) {\n this.statsCollector.startLayout(layoutId, this.currentScheduleId).catch((err: any) => {\n log.error('Failed to start layout stat:', err);\n });\n }\n });\n\n this.renderer.on('layoutEnd', (layoutId: number) => {\n log.info('Layout ended:', layoutId);\n\n // Record play at END so maxPlaysPerHour doesn't interrupt the current play.\n // Previously recorded at layoutStart, which caused periodic collections to\n // filter the layout mid-playback (e.g., 200s video cut at 168s).\n scheduleManager?.recordPlay(layoutId.toString());\n\n // Track stats: end layout (only if enableStat was not disabled)\n if (this.statsCollector && this._currentLayoutEnableStat) {\n this.statsCollector.endLayout(layoutId, this.currentScheduleId).catch((err: any) => {\n log.error('Failed to end layout stat:', err);\n });\n }\n\n // If a new layout is already rendering or being prepared (async fetch),\n // skip advance — the transition was already handled by the caller.\n // Stats/play recording above still run for proper tracking.\n if (this.renderer.currentLayoutId && this.renderer.currentLayoutId !== layoutId) {\n log.debug(`Layout ${layoutId} ended but ${this.renderer.currentLayoutId} already playing, skipping advance`);\n return;\n }\n if (this.preparingLayoutId && this.preparingLayoutId !== layoutId) {\n log.debug(`Layout ${layoutId} ended but ${this.preparingLayoutId} being prepared, skipping advance`);\n return;\n }\n\n // Report to CMS\n this.core.notifyLayoutStatus(layoutId);\n\n // Clear current layout to allow replay/advance\n this.core.clearCurrentLayout();\n\n // If a new layout is already pending download, don't advance\n // (avoids redundant XMDS calls and duplicate download requests)\n const pending = this.core.getPendingLayouts();\n if (pending.length > 0) {\n log.info(`Layout ${pending[0]} pending download, skipping advance`);\n return;\n }\n\n // Advance to the next layout in the schedule (round-robin cycling)\n // This avoids a full collect() cycle — just picks the next layout and renders it.\n // Periodic collect() cycles still run on the collection interval to sync with CMS.\n log.info('Layout cycle completed, advancing to next layout...');\n this.core.advanceToNextLayout();\n });\n\n this.renderer.on('widgetStart', (data: any) => {\n const { widgetId, layoutId, mediaId } = data;\n log.debug('Widget started:', data.type, widgetId, 'media:', mediaId);\n\n // Track stats: start widget/media (only if enableStat is not disabled)\n if (this.statsCollector && mediaId && data.enableStat !== false) {\n this.statsCollector.startWidget(mediaId, layoutId, this.currentScheduleId).catch((err: any) => {\n log.error('Failed to start widget stat:', err);\n });\n }\n });\n\n this.renderer.on('widgetEnd', (data: any) => {\n const { widgetId, layoutId, mediaId } = data;\n log.debug('Widget ended:', data.type, widgetId, 'media:', mediaId);\n\n // Track stats: end widget/media (only if enableStat is not disabled)\n if (this.statsCollector && mediaId && data.enableStat !== false) {\n this.statsCollector.endWidget(mediaId, layoutId, this.currentScheduleId).catch((err: any) => {\n log.error('Failed to end widget stat:', err);\n });\n }\n });\n\n // Widget commands (#202) — execute commands embedded in layout widgets\n this.renderer.on('widgetCommand', (data: any) => {\n log.info('Widget command:', data.commandCode);\n const commands = { [data.commandCode]: { commandString: data.commandString } };\n this.core.executeCommand(data.commandCode, commands);\n });\n\n this.renderer.on('error', (error: any) => {\n log.error('Renderer error:', error);\n this.updateStatus(`Error: ${error.type}`, 'error');\n\n // Report fault to CMS (triggers dashboard alert)\n this.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, final: boolean) => {\n this.core.recordLayoutDuration(String(layoutId), duration, final);\n });\n\n // Handle next layout preload request from renderer\n // Fired at 75% of current layout duration to pre-build the next layout's DOM\n this.renderer.on('request-next-layout-preload', async () => {\n try {\n // Peek at the next layout without advancing the schedule index\n const next = this.core.peekNextLayout();\n if (!next) {\n log.debug('No next layout to preload (single layout schedule or same layout)');\n return;\n }\n\n const nextLayoutId = next.layoutId;\n\n // Skip if already preloaded\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 store.get(`${STORE_PREFIX}/layouts`, nextLayoutId);\n if (!xlfBlob) {\n log.debug(`Layout ${nextLayoutId} XLF not cached, skipping preload`);\n return;\n }\n\n const xlfXml = await xlfBlob.text();\n\n // Check if all required media is cached\n const { allMedia: requiredMedia } = this.getMediaIds(xlfXml);\n const allMediaCached = await this.checkAllMediaCached(requiredMedia);\n\n if (!allMediaCached) {\n log.debug(`Media not fully cached for layout ${nextLayoutId}, skipping preload`);\n return;\n }\n\n // Fetch widget HTML before preloading (same as prepareLayout)\n await this.fetchWidgetHtml(xlfXml, nextLayoutId);\n\n // Preload the layout into the renderer's pool\n const success = await this.renderer.preloadLayout(xlfXml, nextLayoutId);\n if (success) {\n log.info(`Layout ${nextLayoutId} preloaded successfully`);\n } else {\n log.warn(`Layout ${nextLayoutId} preload failed (will fall back to normal render)`);\n }\n } catch (error) {\n log.warn('Layout preload failed (non-blocking):', error);\n // Non-blocking: preload failure is graceful, normal render path will be used\n }\n });\n\n // Handle video playback errors — re-download only missing chunks\n this.renderer.on('videoError', async ({ storedAs }: any) => {\n if (!storedAs) return;\n const storeKey = `${PLAYER_API.slice(1)}/media/file/${storedAs}`;\n try {\n const resp = await fetch(`/store/missing-chunks/${storeKey}`);\n const { missing } = await resp.json();\n if (missing.length === 0) {\n log.warn(`Video ${storedAs}: corrupt file (all chunks present), deleting for re-download`);\n await fetch('/store/delete', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ files: [{ key: storeKey }] }),\n });\n const layoutId = this.core.getCurrentLayoutId();\n if (layoutId) {\n this.core.setPendingLayout(layoutId, [storedAs]);\n }\n this.core.collectNow().catch((err: any) => {\n log.error(`Failed to trigger re-download for ${storedAs}:`, err.message);\n });\n return;\n }\n log.warn(`Video ${storedAs}: ${missing.length} missing chunks (${missing.join(', ')}), re-downloading`);\n\n // Unmark completion (keeps existing chunks on disk) so HEAD returns 404\n await fetch('/store/unmark-complete', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ storeKey }),\n });\n\n // Trigger collection — enqueueFile will populate skipChunks for existing chunks\n this.core.collectNow().catch((err: any) => {\n log.error(`Failed to trigger re-download for ${storedAs}:`, err.message);\n });\n } catch (err: any) {\n log.error(`Failed to check/re-download ${storedAs}:`, err.message);\n }\n });\n }\n\n /**\n * Prepare and render layout (Platform-specific logic)\n */\n private async prepareLayout(layoutId: number) {\n // Same layout replay — use renderer's built-in replay path which\n // re-emits layoutStart, restarts timer and widget cycling.\n if (this.renderer.currentLayoutId === layoutId) {\n log.debug(`Layout ${layoutId} replay`);\n this.core._preparingLayoutId = null;\n const xlfBlob = await store.get(`${STORE_PREFIX}/layouts`, layoutId);\n if (xlfBlob) {\n await this.renderer.renderLayout(await xlfBlob.text(), layoutId);\n }\n return;\n }\n\n // Guard: prevent concurrent preparations of the same layout.\n // Instead of dropping the event (which caused permanent stalls when the\n // first attempt failed due to a store race), schedule a retry after\n // the current preparation finishes.\n if (this.preparingLayoutId === layoutId) {\n log.debug(`Layout ${layoutId} preparation in progress, will retry after it completes`);\n this._pendingRetryLayoutId = layoutId;\n return;\n }\n\n this.preparingLayoutId = layoutId;\n try {\n // Get XLF from cache\n const xlfBlob = await store.get(`${STORE_PREFIX}/layouts`, layoutId);\n if (!xlfBlob) {\n log.info('Layout not in cache yet, marking as pending:', layoutId);\n // Mark layout as pending so when it downloads, we'll retry\n // Use layoutId as required file (will trigger on layout file cached)\n this.core.setPendingLayout(layoutId, [String(layoutId)]);\n this.updateStatus(`Downloading layout ${layoutId}...`);\n return;\n }\n\n const xlfXml = await xlfBlob.text();\n\n // Check if all required media is cached\n const { allMedia: requiredMedia } = this.getMediaIds(xlfXml);\n const allMediaCached = await this.checkAllMediaCached(requiredMedia);\n\n if (!allMediaCached) {\n // Reorder download queue: current layout's media first, hold others.\n // All files (including all chunks) must complete before other layouts start.\n downloadManager.prioritizeLayoutFiles(requiredMedia.map(String));\n\n log.info(`Waiting for media to finish downloading for layout ${layoutId}`);\n this.updateStatus(`Preparing layout ${layoutId}...`);\n this.core.setPendingLayout(layoutId, requiredMedia);\n return; // Keep playing current layout until media is ready\n }\n\n // Fetch widget HTML (skip if already preloaded — was fetched during preload)\n if (!this.renderer.hasPreloadedLayout(layoutId)) {\n await this.fetchWidgetHtml(xlfXml, layoutId);\n }\n\n // Preload layout into pool (hidden). Caller decides when to show.\n await this.renderer.preloadLayout(xlfXml, layoutId);\n log.info(`Layout ${layoutId} ready`);\n\n } catch (error: any) {\n log.error('Failed to prepare layout:', layoutId, error);\n this.updateStatus(`Failed to load layout ${layoutId}`, 'error');\n\n // Report fault to CMS (triggers dashboard alert)\n this.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 this.core._preparingLayoutId = null;\n\n // If another check-pending-layout arrived while we were preparing,\n // retry after a short delay to let the ContentStore settle.\n // This fixes the race where FILE_CACHED notification arrives before\n // the PUT to ContentStore is visible to HEAD requests.\n const retryId = this._pendingRetryLayoutId;\n this._pendingRetryLayoutId = null;\n if (retryId !== null && retryId !== undefined && this.core.getCurrentLayoutId() !== retryId) {\n log.debug(`Retrying preparation for layout ${retryId} after 500ms`);\n setTimeout(() => this.prepareLayout(retryId), 500);\n }\n }\n }\n\n /**\n * Get all required media file IDs and video-specific IDs from layout XLF.\n * Single parse to avoid double DOMParser overhead on the same XML.\n */\n /**\n * Get all required media saveAs filenames and video-specific ones from layout XLF.\n * Returns saveAs strings (via _fileIdToSaveAs map) for store key matching.\n */\n private getMediaIds(xlfXml: string): { allMedia: string[]; videoMedia: string[] } {\n const parser = new DOMParser();\n const doc = parser.parseFromString(xlfXml, 'text/xml');\n const allMedia: string[] = [];\n const videoMedia: string[] = [];\n\n doc.querySelectorAll('media[fileId]').forEach(el => {\n const fileId = el.getAttribute('fileId');\n if (fileId) {\n const saveAs = this._fileIdToSaveAs.get(fileId) || fileId;\n // Skip layout XLF references — stored in layouts/ store, not media/file/\n if (saveAs.endsWith('.xlf')) return;\n allMedia.push(saveAs);\n if (el.getAttribute('type') === 'video') {\n videoMedia.push(saveAs);\n }\n }\n });\n\n // Include background image file ID from layout element\n const bgFileId = doc.querySelector('layout')?.getAttribute('background');\n if (bgFileId) {\n const saveAs = this._fileIdToSaveAs.get(bgFileId) || bgFileId;\n if (!allMedia.includes(saveAs)) {\n allMedia.push(saveAs);\n }\n }\n\n return { allMedia, videoMedia };\n }\n\n /**\n * Check if all required media files are cached and ready.\n * Uses StoreClient.has() → HEAD /store${PLAYER_API}/media/:id to check ContentStore.\n */\n /**\n * Check if all required media files are cached and ready.\n * Uses storedAs filenames for store key matching: /media/file/{saveAs}\n */\n private async checkAllMediaCached(mediaSaveAs: string[]): Promise<boolean> {\n for (const saveAs of mediaSaveAs) {\n try {\n const cached = await store.has(STORE_PREFIX, `media/file/${saveAs}`);\n if (!cached) {\n log.debug(`Media ${saveAs} not yet cached`);\n return false;\n }\n log.debug(`Media ${saveAs} cached`);\n } catch (error) {\n log.warn(`Unable to verify media ${saveAs}, 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 fetchPromises.push(\n (async () => {\n try {\n // Check ContentStore for existing widget HTML\n const storeId = `${layoutId}/${regionId}/${widgetId}`;\n let html: string | null = null;\n\n const existing = await store.get(`${STORE_PREFIX}/widgets`, storeId);\n if (existing) {\n html = await existing.text();\n log.debug(`Found cached widget HTML for ${type} ${widgetId}`);\n }\n\n if (!html) {\n html = await this.xmds.getResource(layoutId, regionId, widgetId);\n log.debug(`Retrieved widget HTML for ${type} ${widgetId} from CMS`);\n }\n // Always process: injects <base> tag, rewrites IC hostAddress.\n // cacheWidgetHtml is idempotent — already-rewritten URLs won't re-match.\n await cacheWidgetHtml(layoutId, regionId, widgetId, html);\n // Read back the processed version from ContentStore\n const processed = await store.get(`${STORE_PREFIX}/widgets`, storeId);\n if (processed) html = await processed.text();\n\n // Update raw content in XLF\n const rawEl = mediaEl.querySelector('raw');\n if (rawEl) {\n rawEl.textContent = html;\n } else {\n const newRaw = doc.createElement('raw');\n newRaw.textContent = html;\n mediaEl.appendChild(newRaw);\n }\n } catch (error) {\n log.warn(`Failed to get widget HTML for ${type} ${widgetId}:`, error);\n }\n })()\n );\n }\n }\n }\n\n if (fetchPromises.length > 0) {\n log.info(`Fetching ${fetchPromises.length} widget HTML resources in parallel...`);\n await Promise.all(fetchPromises);\n log.debug('All widget HTML fetched');\n }\n }\n\n /**\n * Check media cache status for all scheduled layouts.\n * For each layout: load XLF from cache, extract media IDs, check each with store.has().\n * Feeds results into PlayerCore.setLayoutMediaStatus() for timeline annotation.\n */\n private async checkTimelineMediaStatus() {\n if (this.scheduledLayoutIds.size === 0) return;\n\n for (const layoutId of this.scheduledLayoutIds) {\n const layoutFile = `${layoutId}.xlf`;\n try {\n const xlfBlob = await store.get(`${STORE_PREFIX}/layouts`, layoutId);\n if (!xlfBlob) continue;\n\n const xlfXml = await xlfBlob.text();\n const { allMedia } = this.getMediaIds(xlfXml);\n\n if (allMedia.length === 0) {\n this.core.setLayoutMediaStatus(layoutFile, true);\n continue;\n }\n\n const missing: string[] = [];\n for (const saveAs of allMedia) {\n try {\n const cached = await store.has(STORE_PREFIX, `media/file/${saveAs}`);\n if (!cached) missing.push(saveAs);\n } catch {\n // Assume cached on error (offline mode)\n }\n }\n\n this.core.setLayoutMediaStatus(layoutFile, missing.length === 0, missing);\n } catch {\n // Skip layouts we can't load\n }\n }\n\n // Re-emit annotated timeline\n this.core.logUpcomingTimeline();\n }\n\n /**\n * Probe video durations for all scheduled layouts.\n * Uses preload=\"metadata\" — only fetches headers (~50KB), not the full video.\n * Feeds discovered durations into PlayerCore for accurate timeline calculation.\n */\n private async probeLayoutDurations() {\n if (this.scheduledLayoutIds.size === 0) return;\n\n for (const layoutId of this.scheduledLayoutIds) {\n\n try {\n const xlfBlob = await store.get(`${STORE_PREFIX}/layouts`, layoutId);\n if (!xlfBlob) continue;\n\n const xlfXml = await xlfBlob.text();\n const { videoMedia } = this.getMediaIds(xlfXml);\n if (videoMedia.length === 0) continue;\n\n // Parse XLF to find video widgets with duration=0 (use media length)\n const parser = new DOMParser();\n const doc = parser.parseFromString(xlfXml, 'text/xml');\n\n // Probe actual video durations, keyed by fileId\n const videoDurations = new Map<string, number>();\n let dynamicVideoCount = 0;\n for (const mediaEl of doc.querySelectorAll('media[type=\"video\"]')) {\n const useDuration = mediaEl.getAttribute('useDuration');\n if (useDuration === '1') continue; // Has explicit CMS duration, skip\n\n const fileId = mediaEl.getAttribute('fileId');\n if (!fileId) continue;\n dynamicVideoCount++;\n\n const saveAs = this._fileIdToSaveAs.get(fileId) || fileId;\n const exists = await store.has(STORE_PREFIX, `media/file/${saveAs}`);\n if (!exists) continue;\n\n // Probe metadata only — does NOT download the full video\n const duration = await this.probeVideoDuration(`${window.location.origin}${PLAYER_API}/media/file/${saveAs}`);\n if (duration > 0) {\n videoDurations.set(fileId, duration);\n }\n }\n\n if (videoDurations.size === 0) continue;\n\n // Only mark final if ALL dynamic videos were successfully probed\n const allProbed = videoDurations.size >= dynamicVideoCount;\n\n // Phase 2: refine layout duration with probed video lengths\n const { duration: probedDuration } = parseLayoutDuration(xlfXml, videoDurations);\n if (probedDuration > 0) {\n this.core.recordLayoutDuration(String(layoutId), probedDuration, allProbed);\n }\n } catch (err) {\n log.debug(`Duration probe failed for layout ${layoutId}:`, err);\n }\n }\n }\n\n /**\n * Probe a single video's duration using metadata only.\n * Creates a temporary <video preload=\"metadata\"> element, reads duration, destroys it.\n */\n private probeVideoDuration(url: string): Promise<number> {\n return new Promise((resolve) => {\n const video = document.createElement('video');\n video.preload = 'metadata';\n video.muted = true;\n\n const cleanup = () => {\n video.removeAttribute('src');\n video.load(); // Release resources\n };\n\n video.addEventListener('loadedmetadata', () => {\n const dur = video.duration;\n cleanup();\n resolve(dur);\n }, { once: true });\n\n video.addEventListener('error', () => {\n cleanup();\n resolve(0);\n }, { once: true });\n\n // Safety timeout — don't block forever\n setTimeout(() => {\n cleanup();\n resolve(0);\n }, 5000);\n\n video.src = url;\n });\n }\n\n /**\n * Update config display\n */\n private updateConfigDisplay() {\n const configEl = document.getElementById('config-info');\n if (configEl) {\n const version = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '?';\n const buildDate = typeof __BUILD_DATE__ !== 'undefined' ? __BUILD_DATE__.replace('T', ' ').replace(/\\.\\d+Z$/, '') : '';\n const versionStr = buildDate ? `v${version} (${buildDate})` : `v${version}`;\n let text = `${versionStr} | CMS: ${config.cmsUrl} | Display: ${config.displayName || 'Unknown'} | HW: ${config.hardwareKey}`;\n const sc = this.core?.getSyncConfig?.();\n if (sc) {\n const relay = sc.relayUrl ? new URL(sc.relayUrl).host : '';\n text += ` | Sync: ${sc.isLead ? 'LEAD' : `FOLLOWER → ${relay}`} (group ${sc.syncGroupId || sc.syncGroup})`;\n }\n configEl.textContent = text;\n }\n }\n\n /**\n * 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 via screen sharing API.\n * Pixel-perfect, zero DOM manipulation. Chromium kiosk auto-approves\n * via --auto-select-desktop-capture-source flag.\n * 2. Direct canvas drawing — fallback that draws img/video/canvas elements\n * directly. Text-only widgets (clocks, tickers) won't appear.\n *\n * The first successful method is cached for subsequent calls.\n */\n private async captureAndSubmitScreenshot() {\n // Concurrency guard — skip if a capture is already in flight\n if (this._screenshotInFlight) {\n log.debug('Screenshot capture already in progress, skipping');\n return;\n }\n this._screenshotInFlight = true;\n\n try {\n let base64: string;\n\n // Electron path: use native webContents.capturePage() via IPC\n if (this._screenshotMethod === 'electron' ||\n (this._screenshotMethod === null && (window as any).electronAPI?.captureScreenshot)) {\n const electronResult = await (window as any).electronAPI.captureScreenshot();\n if (electronResult) {\n this._screenshotMethod = 'electron';\n base64 = electronResult;\n } else {\n // Electron capture returned null (window not yet painted).\n // Do NOT fall through to getDisplayMedia — it triggers a\n // permission dialog that blocks the whole UI. Skip this\n // cycle; capturePage() will succeed on the next interval.\n log.debug('Electron screenshot not ready yet, will retry next interval');\n return;\n }\n } else if (this._screenshotMethod === 'displayMedia' ||\n (this._screenshotMethod === null && typeof navigator.mediaDevices?.getDisplayMedia === 'function')) {\n // Try getDisplayMedia — pixel-perfect screen capture, zero DOM cost.\n // Chromium kiosk auto-approves via --auto-accept-this-tab-capture.\n try {\n base64 = await this.captureDisplayMedia();\n this._screenshotMethod = 'displayMedia';\n } catch (e: any) {\n log.warn('getDisplayMedia failed, falling back to html2canvas:', e.message || e);\n this._screenshotMethod = null;\n base64 = await this.captureHtml2CanvasIsolated();\n this._screenshotMethod = 'html2canvas';\n }\n } else {\n // Tier 3: html2canvas hybrid (Firefox, other browsers)\n // Direct draw for img/video/canvas + per-iframe html2canvas for HTML widgets\n this._screenshotMethod = 'html2canvas';\n base64 = await this.captureHtml2CanvasIsolated();\n }\n\n const success = await this.xmds.submitScreenShot(base64);\n if (success) {\n log.info(`Screenshot submitted (${this._screenshotMethod})`);\n } else {\n log.warn('Screenshot submission failed');\n }\n } catch (error) {\n log.error('Failed to capture screenshot:', error);\n } finally {\n this._screenshotInFlight = false;\n }\n }\n\n /**\n * Capture screenshot via getDisplayMedia (screen sharing API).\n * Pixel-perfect, captures everything the GPU renders including video,\n * WebGL, composited layers, and all widget content.\n * Chromium kiosk auto-approves via --auto-select-desktop-capture-source.\n */\n private async captureDisplayMedia(): Promise<string> {\n const stream = await navigator.mediaDevices.getDisplayMedia({\n video: true,\n audio: false,\n preferCurrentTab: true,\n } as any);\n\n try {\n const track = stream.getVideoTracks()[0];\n // @ts-ignore — ImageCapture is available in Chromium\n const imageCapture = new (window as any).ImageCapture(track);\n const bitmap = await imageCapture.grabFrame();\n\n const canvas = document.createElement('canvas');\n canvas.width = bitmap.width;\n canvas.height = bitmap.height;\n const ctx = canvas.getContext('2d')!;\n ctx.drawImage(bitmap, 0, 0);\n bitmap.close();\n\n return canvas.toDataURL('image/jpeg', 0.8).split(',')[1];\n } finally {\n stream.getTracks().forEach(t => t.stop());\n }\n }\n\n /**\n * Capture screenshot via html2canvas hybrid approach.\n * Draws images/video/canvas directly, uses per-iframe html2canvas for\n * HTML widgets (clocks, tickers). CSS contain: strict on capture divs\n * prevents ResizeObserver glitches.\n */\n private async captureHtml2CanvasIsolated(): Promise<string> {\n const canvas = document.createElement('canvas');\n canvas.width = window.innerWidth;\n canvas.height = window.innerHeight;\n const ctx = canvas.getContext('2d')!;\n\n ctx.fillStyle = '#000';\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n\n const container = document.getElementById('player-container');\n if (!container) {\n return canvas.toDataURL('image/jpeg', 0.8).split(',')[1];\n }\n\n // Lazy-load html2canvas\n if (!this._html2canvasMod) {\n this._html2canvasMod = (await import('html2canvas')).default;\n }\n\n // Suppress resize during capture\n if (this.renderer) {\n this.renderer._resizeSuppressed = true;\n }\n\n // Protect the player container from external DOM changes.\n // html2canvas appends/removes elements to document.body which can trigger\n // Chromium reflow affecting the player. contain:strict on the player itself\n // makes it immune to any layout changes outside it.\n const prevContain = container.style.contain || '';\n container.style.contain = 'strict';\n\n try {\n // Draw container background\n const containerRect = container.getBoundingClientRect();\n const containerStyle = getComputedStyle(container);\n const bgColor = containerStyle.backgroundColor;\n if (bgColor && bgColor !== 'transparent' && bgColor !== 'rgba(0, 0, 0, 0)') {\n ctx.fillStyle = bgColor;\n ctx.fillRect(containerRect.left, containerRect.top, containerRect.width, containerRect.height);\n }\n const bgImage = containerStyle.backgroundImage;\n if (bgImage && bgImage !== 'none') {\n const urlMatch = bgImage.match(/url\\([\"']?(.*?)[\"']?\\)/);\n if (urlMatch) {\n try {\n const bgImg = new Image();\n bgImg.crossOrigin = 'anonymous';\n await new Promise<void>((resolve) => {\n bgImg.onload = () => resolve();\n bgImg.onerror = () => resolve();\n setTimeout(() => resolve(), 2000);\n bgImg.src = urlMatch[1];\n });\n if (bgImg.naturalWidth) {\n ctx.drawImage(bgImg, containerRect.left, containerRect.top, containerRect.width, containerRect.height);\n }\n } catch (_) { /* skip failed background */ }\n }\n }\n\n // Draw each visible widget element\n const elements = container.querySelectorAll('img, video, iframe, canvas');\n let drawn = 0;\n\n for (const el of elements) {\n const htmlEl = el as HTMLElement;\n if (htmlEl.style.visibility === 'hidden') continue;\n if (htmlEl.style.display === 'none') continue;\n const rect = el.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) continue;\n\n try {\n if (el instanceof HTMLImageElement) {\n if (!el.complete || !el.naturalWidth) continue;\n const fit = getComputedStyle(el).objectFit;\n if (fit === 'contain' && el.naturalWidth && el.naturalHeight) {\n const d = this.containedRect(el.naturalWidth, el.naturalHeight, rect);\n ctx.drawImage(el, d.x, d.y, d.w, d.h);\n } else {\n ctx.drawImage(el, rect.left, rect.top, rect.width, rect.height);\n }\n drawn++;\n } else if (el instanceof HTMLVideoElement) {\n if (el.readyState < 2) continue;\n const fit = getComputedStyle(el).objectFit;\n if (fit === 'contain' && el.videoWidth && el.videoHeight) {\n const d = this.containedRect(el.videoWidth, el.videoHeight, rect);\n ctx.drawImage(el, d.x, d.y, d.w, d.h);\n } else {\n ctx.drawImage(el, rect.left, rect.top, rect.width, rect.height);\n }\n drawn++;\n } else if (el instanceof HTMLCanvasElement) {\n ctx.drawImage(el, rect.left, rect.top, rect.width, rect.height);\n drawn++;\n } else if (el instanceof HTMLIFrameElement) {\n const iDoc = el.contentDocument;\n if (!iDoc?.body) continue;\n\n // Clone iframe DOM into main document for html2canvas rendering.\n // contain: strict prevents layout effects on the live player.\n const captureDiv = document.createElement('div');\n captureDiv.style.cssText = `position:fixed;left:-9999px;top:0;width:${rect.width}px;height:${rect.height}px;overflow:hidden;`;\n\n // Clone stylesheets with absolute URLs\n const linkPromises: Promise<void>[] = [];\n for (const styleEl of iDoc.querySelectorAll('style')) {\n captureDiv.appendChild(styleEl.cloneNode(true));\n }\n for (const linkEl of iDoc.querySelectorAll('link[rel=\"stylesheet\"]')) {\n const newLink = document.createElement('link');\n newLink.rel = 'stylesheet';\n newLink.href = new URL(linkEl.getAttribute('href') || '', iDoc.baseURI).href;\n captureDiv.appendChild(newLink);\n linkPromises.push(new Promise<void>(resolve => {\n newLink.onload = () => resolve();\n newLink.onerror = () => resolve();\n }));\n }\n\n // Clone body content with absolute img URLs\n const clonedBody = iDoc.body.cloneNode(true) as HTMLElement;\n for (const img of clonedBody.querySelectorAll('img[src]')) {\n const src = img.getAttribute('src') || '';\n if (src && !src.startsWith('http') && !src.startsWith('data:') && !src.startsWith('blob:')) {\n img.setAttribute('src', new URL(src, iDoc.baseURI).href);\n }\n }\n captureDiv.appendChild(clonedBody);\n document.body.appendChild(captureDiv);\n\n // Collect natural dimensions from original iframe images\n const origImgs = iDoc.querySelectorAll('img');\n const imgNaturals = new Map<string, { nw: number; nh: number }>();\n origImgs.forEach((img, i) => {\n if (img.naturalWidth && img.naturalHeight) {\n imgNaturals.set(String(i), { nw: img.naturalWidth, nh: img.naturalHeight });\n }\n });\n\n if (linkPromises.length > 0) {\n await Promise.race([\n Promise.all(linkPromises),\n new Promise(r => setTimeout(r, 500)),\n ]);\n }\n\n const iframeCanvas = await this._html2canvasMod(captureDiv, {\n useCORS: true, allowTaint: true, logging: false,\n backgroundColor: null,\n width: rect.width, height: rect.height,\n onclone: (clonedDoc: Document) => {\n // Force visible — CSS animations reset to opacity:0 in cloned DOM\n const s = clonedDoc.createElement('style');\n s.textContent = '*, *::before, *::after { animation: none !important; transition: none !important; opacity: 1 !important; }';\n clonedDoc.head.appendChild(s);\n\n // Fix object-fit: contain sizing for html2canvas\n const clonedImgs = clonedDoc.querySelectorAll('img');\n clonedImgs.forEach((cImg, i) => {\n const style = clonedDoc.defaultView?.getComputedStyle(cImg);\n if (!style || style.objectFit !== 'contain') return;\n const dims = imgNaturals.get(String(i));\n if (!dims) return;\n\n const cW = cImg.clientWidth || parseFloat(style.width) || 0;\n const cH = cImg.clientHeight || parseFloat(style.height) || 0;\n if (!cW || !cH) return;\n\n const srcAspect = dims.nw / dims.nh;\n const dstAspect = cW / cH;\n let drawW: number, drawH: number;\n if (srcAspect > dstAspect) {\n drawW = cW; drawH = cW / srcAspect;\n } else {\n drawH = cH; drawW = cH * srcAspect;\n }\n\n const wrapper = clonedDoc.createElement('div');\n wrapper.style.cssText = `width:${cW}px;height:${cH}px;display:flex;align-items:center;justify-content:center;overflow:hidden;`;\n cImg.style.objectFit = 'fill';\n cImg.style.width = `${drawW}px`;\n cImg.style.height = `${drawH}px`;\n cImg.parentNode?.insertBefore(wrapper, cImg);\n wrapper.appendChild(cImg);\n });\n },\n });\n\n document.body.removeChild(captureDiv);\n ctx.drawImage(iframeCanvas, rect.left, rect.top, rect.width, rect.height);\n\n // Draw videos directly — html2canvas can't render <video> elements.\n // Draw on top of the html2canvas result so video overlays black placeholder.\n const iframeRect = el.getBoundingClientRect();\n for (const vid of iDoc.querySelectorAll('video') as NodeListOf<HTMLVideoElement>) {\n if (vid.readyState < 2) continue;\n const vr = vid.getBoundingClientRect();\n if (vr.width === 0 || vr.height === 0) continue;\n try {\n const fit = iDoc.defaultView?.getComputedStyle(vid)?.objectFit;\n if (fit === 'contain' && vid.videoWidth && vid.videoHeight) {\n const d = this.containedRect(vid.videoWidth, vid.videoHeight,\n new DOMRect(iframeRect.left + vr.left, iframeRect.top + vr.top, vr.width, vr.height));\n ctx.drawImage(vid, d.x, d.y, d.w, d.h);\n } else {\n ctx.drawImage(vid, iframeRect.left + vr.left, iframeRect.top + vr.top, vr.width, vr.height);\n }\n } catch (_) { /* tainted video */ }\n }\n\n // Draw canvas elements directly (PDF pages, charts rendered in iframe)\n for (const c of iDoc.querySelectorAll('canvas') as NodeListOf<HTMLCanvasElement>) {\n const cr = c.getBoundingClientRect();\n if (cr.width === 0 || cr.height === 0) continue;\n try {\n ctx.drawImage(c, iframeRect.left + cr.left, iframeRect.top + cr.top, cr.width, cr.height);\n } catch (_) { /* tainted canvas */ }\n }\n\n drawn++;\n }\n } catch (e: any) {\n log.warn('Screenshot: failed to draw element', el.tagName, e);\n }\n }\n\n log.debug(`Screenshot: composed ${drawn}/${elements.length} elements`);\n return canvas.toDataURL('image/jpeg', 0.8).split(',')[1];\n } finally {\n container.style.contain = prevContain;\n if (this.renderer) {\n this.renderer._resizeSuppressed = false;\n }\n }\n }\n\n /**\n * Calculate the destination rect for object-fit: contain.\n * Returns the centered rect that preserves the source aspect ratio\n * within the bounding rect (letterbox/pillarbox).\n */\n private containedRect(\n srcW: number, srcH: number, rect: DOMRect\n ): { x: number; y: number; w: number; h: number } {\n const srcAspect = srcW / srcH;\n const dstAspect = rect.width / rect.height;\n let w: number, h: number;\n if (srcAspect > dstAspect) {\n // Source is wider — fit to width, letterbox top/bottom\n w = rect.width;\n h = rect.width / srcAspect;\n } else {\n // Source is taller — fit to height, pillarbox left/right\n h = rect.height;\n w = rect.height * srcAspect;\n }\n return {\n x: rect.left + (rect.width - w) / 2,\n y: rect.top + (rect.height - h) / 2,\n w, h,\n };\n }\n\n /**\n * Start periodic screenshot submission\n */\n private startScreenshotInterval() {\n const intervalSecs = this.displaySettings?.getSetting('screenshotInterval') || 0;\n if (!intervalSecs || intervalSecs <= 0) return;\n\n // Pre-load html2canvas for non-Electron/non-Chromium browsers\n if (!this._html2canvasMod && !(window as any).electronAPI) {\n import('html2canvas').then(m => { this._html2canvasMod = m.default; });\n }\n\n const intervalMs = intervalSecs * 1000;\n log.info(`Starting periodic screenshots every ${intervalSecs}s`);\n this._screenshotInterval = setInterval(() => {\n this.captureAndSubmitScreenshot();\n }, intervalMs);\n }\n\n /**\n * Update status message (Platform-specific UI)\n */\n private updateStatus(message: string, type: 'info' | 'error' = 'info') {\n const statusEl = document.getElementById('status');\n if (statusEl) {\n statusEl.textContent = message;\n statusEl.className = `status status-${type}`;\n }\n if (type === 'error') {\n log.error('Status:', message);\n } else {\n log.info('Status:', message);\n }\n }\n\n private showOfflineIndicator() {\n this.timelineOverlay?.setOffline(true);\n }\n\n private removeOfflineIndicator() {\n this.timelineOverlay?.setOffline(false);\n }\n\n /**\n * Check if the sync lead is alive (for follower delegation).\n * Returns true if any peer with role 'lead' has been seen in the last 15s.\n */\n private _syncLeadAlive(): boolean {\n if (!this.syncManager) return false;\n for (const [, peer] of this.syncManager.followers) {\n if (peer.role === 'lead' && Date.now() - peer.lastSeen < 15000) {\n return true;\n }\n }\n return false;\n }\n\n /**\n * Cleanup\n */\n cleanup() {\n this.core.cleanup();\n this.renderer.cleanup();\n\n if (this._screenshotInterval) {\n clearInterval(this._screenshotInterval);\n this._screenshotInterval = null;\n }\n\n if (this._wakeLock) {\n this._wakeLock.release();\n this._wakeLock = null;\n }\n\n if (this.downloadOverlay) {\n this.downloadOverlay.destroy();\n }\n\n if (this.timelineOverlay) {\n this.timelineOverlay.destroy();\n }\n\n // Disconnect iframe observer\n if (this._iframeObserver) {\n this._iframeObserver.disconnect();\n this._iframeObserver = null;\n }\n\n // Remove SW message listeners\n if (navigator.serviceWorker) {\n if (this._swIcHandler) {\n navigator.serviceWorker.removeEventListener('message', this._swIcHandler);\n this._swIcHandler = null;\n }\n }\n\n // Clean up DownloadManager\n downloadManager?.clear();\n\n if (this._probeTimer) {\n clearTimeout(this._probeTimer);\n this._probeTimer = null;\n }\n\n if (this._mediaStatusTimer) {\n clearTimeout(this._mediaStatusTimer);\n this._mediaStatusTimer = null;\n }\n }\n}\n\nfunction startPlayer() {\n const player = new PwaPlayer();\n player.init().catch(error => {\n log.error('Failed to initialize:', error);\n // First boot with bad config — redirect to setup so user can fix it\n log.warn('Redirecting to setup screen...');\n window.location.href = './setup.html';\n });\n window.addEventListener('beforeunload', () => {\n player.cleanup();\n });\n}\n\nif (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', startPlayer);\n} else {\n startPlayer();\n}\n"],"file":"assets/main-C7-NPVem.js"}
|