@xiboplayer/pwa 0.7.19 → 0.7.21

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.
Files changed (35) hide show
  1. package/dist/assets/{main-BmtbYkOU.js → main-oacre7st.js} +6 -6
  2. package/dist/assets/main-oacre7st.js.map +1 -0
  3. package/dist/assets/main-vwJkNw4Y.js +3 -0
  4. package/dist/assets/{main-ByurY8zd.js.map → main-vwJkNw4Y.js.map} +1 -1
  5. package/dist/assets/{setup-TzYPvQkI.js → setup-B4gZX38p.js} +2 -2
  6. package/dist/assets/{setup-TzYPvQkI.js.map → setup-B4gZX38p.js.map} +1 -1
  7. package/dist/assets/{src-S093VuQs.js → src-B_BNICay.js} +2 -2
  8. package/dist/assets/{src-S093VuQs.js.map → src-B_BNICay.js.map} +1 -1
  9. package/dist/assets/{src-BbJ9IAbR.js → src-Bjt9ooXK.js} +2 -2
  10. package/dist/assets/{src-BbJ9IAbR.js.map → src-Bjt9ooXK.js.map} +1 -1
  11. package/dist/assets/{src-CRWze-JF.js → src-BtVLiVYZ.js} +2 -2
  12. package/dist/assets/{src-CRWze-JF.js.map → src-BtVLiVYZ.js.map} +1 -1
  13. package/dist/assets/{src-CSIdq4vk.js → src-CKpVxGpH.js} +2 -2
  14. package/dist/assets/{src-CSIdq4vk.js.map → src-CKpVxGpH.js.map} +1 -1
  15. package/dist/assets/{src-CR4vHRyW.js → src-CROvYSP8.js} +2 -2
  16. package/dist/assets/{src-CR4vHRyW.js.map → src-CROvYSP8.js.map} +1 -1
  17. package/dist/assets/{src-Jz8Hu7ah.js → src-C_Lx4lXp.js} +2 -2
  18. package/dist/assets/{src-Jz8Hu7ah.js.map → src-C_Lx4lXp.js.map} +1 -1
  19. package/dist/assets/{src-DLAmD0IH.js → src-Cx3tXAAu.js} +2 -2
  20. package/dist/assets/{src-DLAmD0IH.js.map → src-Cx3tXAAu.js.map} +1 -1
  21. package/dist/assets/{src-DAFtXxFV.js → src-DAB0dqGG.js} +2 -2
  22. package/dist/assets/{src-DAFtXxFV.js.map → src-DAB0dqGG.js.map} +1 -1
  23. package/dist/assets/{src-BhNSI3YP.js → src-WDu491CE.js} +2 -2
  24. package/dist/assets/{src-BhNSI3YP.js.map → src-WDu491CE.js.map} +1 -1
  25. package/dist/assets/{src-cynIeb-W.js → src-cUopH0nN.js} +2 -2
  26. package/dist/assets/{src-cynIeb-W.js.map → src-cUopH0nN.js.map} +1 -1
  27. package/dist/assets/{sync-manager-CJQQp9_5.js → sync-manager-8Z-qwkod.js} +2 -2
  28. package/dist/assets/{sync-manager-CJQQp9_5.js.map → sync-manager-8Z-qwkod.js.map} +1 -1
  29. package/dist/index.html +1 -1
  30. package/dist/setup.html +5 -5
  31. package/dist/sw-pwa.js +2 -2
  32. package/dist/sw-pwa.js.map +1 -1
  33. package/package.json +13 -13
  34. package/dist/assets/main-BmtbYkOU.js.map +0 -1
  35. package/dist/assets/main-ByurY8zd.js +0 -3
@@ -1 +1 @@
1
- {"version":3,"file":"src-DLAmD0IH.js","names":["log","log","E","pkg"],"sources":["../../../core/package.json","../../../core/src/data-connectors.js","../../../core/src/layout-blacklist.js","../../../core/src/events.js","../../../core/src/player-core.js","../../../core/src/index.js"],"sourcesContent":["{\n \"name\": \"@xiboplayer/core\",\n \"version\": \"0.7.19\",\n \"description\": \"xiboplayer core orchestration and lifecycle management\",\n \"type\": \"module\",\n \"main\": \"./src/index.js\",\n \"types\": \"./src/index.d.ts\",\n \"exports\": {\n \".\": \"./src/index.js\",\n \"./player-core\": \"./src/player-core.js\"\n },\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"preview\": \"vite preview\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"test:ui\": \"vitest --ui\",\n \"test:coverage\": \"vitest run --coverage\"\n },\n \"dependencies\": {\n \"@xiboplayer/utils\": \"workspace:*\"\n },\n \"peerDependencies\": {\n \"@xiboplayer/cache\": \"workspace:*\",\n \"@xiboplayer/renderer\": \"workspace:*\",\n \"@xiboplayer/schedule\": \"workspace:*\",\n \"@xiboplayer/xmds\": \"workspace:*\"\n },\n \"devDependencies\": {\n \"@vitest/coverage-v8\": \"^4.1.3\",\n \"@vitest/ui\": \"^4.1.4\",\n \"jsdom\": \"^29.0.2\",\n \"vite\": \"^8.0.8\",\n \"vitest\": \"^4.1.2\"\n },\n \"keywords\": [\n \"xibo\",\n \"digital-signage\",\n \"player\",\n \"core\",\n \"orchestration\"\n ],\n \"author\": \"Pau Aliagas <linuxnow@gmail.com>\",\n \"license\": \"AGPL-3.0-or-later\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/xibo-players/xiboplayer.git\",\n \"directory\": \"packages/core\"\n },\n \"homepage\": \"https://xiboplayer.org\"\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 * 2 ** (entry.failures - CIRCUIT_BREAKER_THRESHOLD + 1), MAX_BACKOFF_MS);\n clearInterval(entry.timer);\n entry.timer = setTimeout(() => {\n this.fetchData(entry).catch(() => {});\n // Re-arm with backoff interval\n entry.timer = setInterval(() => {\n this.fetchData(entry).catch(() => {});\n }, backoffMs);\n }, backoffMs);\n log.warn(`Circuit breaker: ${dataKey} backing off to ${Math.round(backoffMs / 1000)}s`);\n }\n }\n }\n\n /**\n * Restore normal polling interval after circuit breaker backoff.\n * @private\n */\n _ensureNormalPolling(entry) {\n if (entry.failures === 0 && entry.timer) {\n const baseMs = (entry.config.updateInterval || 300) * 1000;\n // Clear any backed-off timer and restore the normal interval\n clearInterval(entry.timer);\n clearTimeout(entry.timer);\n entry.timer = setInterval(() => {\n this.fetchData(entry).catch(() => {});\n }, baseMs);\n }\n }\n\n /**\n * Force refresh all connectors — re-fetch immediately and restart polling.\n * Called by XMR dataUpdate command.\n */\n refreshAll() {\n if (this.connectors.size === 0) return;\n\n log.info(`Refreshing all ${this.connectors.size} data connector(s)`);\n this.stopPolling();\n this.startPolling();\n }\n\n /**\n * Cleanup - stop all polling and remove listeners\n */\n cleanup() {\n this.stopPolling();\n this.connectors.clear();\n this.removeAllListeners();\n log.debug('DataConnectorManager cleaned up');\n }\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Layout blacklist — tracks consecutive rendering failures and\n * blacklists layouts that fail repeatedly to prevent crash loops.\n */\n\nimport { createLogger } from '@xiboplayer/utils';\n\nconst log = createLogger('Blacklist');\n\nexport class LayoutBlacklist {\n /**\n * @param {number} [threshold=3] - Consecutive failures before blacklisting\n */\n constructor(threshold = 3) {\n this._entries = new Map();\n this._threshold = threshold;\n }\n\n /**\n * Record a layout rendering failure.\n * @param {number} layoutId\n * @param {string} reason\n * @returns {{ blacklisted: boolean, failures: number }} Current state after recording\n */\n recordFailure(layoutId, reason) {\n const id = Number(layoutId);\n const entry = this._entries.get(id) || { failures: 0, blacklisted: false, reason: '' };\n entry.failures++;\n entry.reason = reason;\n\n if (!entry.blacklisted && entry.failures >= this._threshold) {\n entry.blacklisted = true;\n log.warn(`Layout ${id} blacklisted after ${entry.failures} consecutive failures: ${reason}`);\n } else if (!entry.blacklisted) {\n log.info(`Layout ${id} failure ${entry.failures}/${this._threshold}: ${reason}`);\n }\n\n this._entries.set(id, entry);\n return { blacklisted: entry.blacklisted, failures: entry.failures };\n }\n\n /**\n * Record a successful layout render. Resets failure counter.\n * @param {number} layoutId\n * @returns {boolean} true if the layout was previously blacklisted (now restored)\n */\n recordSuccess(layoutId) {\n const id = Number(layoutId);\n if (!this._entries.has(id)) return false;\n\n const was = this._entries.get(id);\n this._entries.delete(id);\n\n if (was.blacklisted) {\n log.info(`Layout ${id} removed from blacklist (rendered successfully)`);\n return true;\n }\n return false;\n }\n\n /**\n * Check if a layout is currently blacklisted.\n * @param {number} layoutId\n * @returns {boolean}\n */\n isBlacklisted(layoutId) {\n const entry = this._entries.get(Number(layoutId));\n return entry?.blacklisted === true;\n }\n\n /**\n * Get all currently blacklisted layout IDs.\n * @returns {number[]}\n */\n getBlacklistedIds() {\n const result = [];\n for (const [id, entry] of this._entries) {\n if (entry.blacklisted) result.push(id);\n }\n return result;\n }\n\n /**\n * Reset the blacklist. Called when RequiredFiles changes.\n * @returns {number} Number of entries cleared\n */\n reset() {\n const count = this._entries.size;\n if (count > 0) {\n log.info(`Blacklist reset (${count} entries cleared)`);\n this._entries.clear();\n }\n return count;\n }\n\n get size() {\n return this._entries.size;\n }\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Core event name constants — shared between PlayerCore and platform layers.\n * Using constants prevents silent typo bugs at the core/platform boundary.\n */\n\nexport const CORE_EVENTS = Object.freeze({\n // Collection lifecycle\n COLLECTION_START: 'collection-start',\n COLLECTION_COMPLETE: 'collection-complete',\n COLLECTION_ERROR: 'collection-error',\n\n // Registration\n REGISTER_COMPLETE: 'register-complete',\n\n // Schedule\n SCHEDULE_RECEIVED: 'schedule-received',\n LAYOUTS_SCHEDULED: 'layouts-scheduled',\n NO_LAYOUTS_SCHEDULED: 'no-layouts-scheduled',\n TIMELINE_UPDATED: 'timeline-updated',\n\n // Layout lifecycle\n LAYOUT_PREPARE_REQUEST: 'layout-prepare-request',\n LAYOUT_EXPIRE_CURRENT: 'layout-expire-current',\n LAYOUT_ALREADY_PLAYING: 'layout-already-playing',\n CHECK_PENDING_LAYOUT: 'check-pending-layout',\n\n // Downloads\n FILES_RECEIVED: 'files-received',\n DOWNLOAD_REQUEST: 'download-request',\n\n // Overlay\n OVERLAY_LAYOUT_REQUEST: 'overlay-layout-request',\n REVERT_TO_SCHEDULE: 'revert-to-schedule',\n\n // Sync\n SYNC_CONFIG: 'sync-config',\n\n // XMR\n XMR_CONNECTED: 'xmr-connected',\n XMR_RECONNECTED: 'xmr-reconnected',\n XMR_MISCONFIGURED: 'xmr-misconfigured',\n\n // Navigation\n NAVIGATE_TO_WIDGET: 'navigate-to-widget',\n\n // Commands\n EXECUTE_NATIVE_COMMAND: 'execute-native-command',\n SCHEDULED_COMMAND: 'scheduled-command',\n COMMAND_RESULT: 'command-result',\n\n // Screenshots\n SCREENSHOT_REQUEST: 'screenshot-request',\n\n // Stats/Logs/Faults\n SUBMIT_STATS_REQUEST: 'submit-stats-request',\n SUBMIT_LOGS_REQUEST: 'submit-logs-request',\n SUBMIT_FAULTS_REQUEST: 'submit-faults-request',\n\n // Cache\n CACHE_ANALYSIS: 'cache-analysis',\n\n // Collection\n COLLECTION_INTERVAL_SET: 'collection-interval-set',\n COLLECTION_INTERVAL_UPDATED: 'collection-interval-updated',\n\n // Settings\n LOG_LEVEL_CHANGED: 'log-level-changed',\n OFFLINE_MODE: 'offline-mode',\n\n // Purge\n PURGE_REQUEST: 'purge-request',\n PURGE_ALL_REQUEST: 'purge-all-request',\n});\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * PlayerCore - Platform-independent orchestration module\n *\n * Pure orchestration logic without platform-specific concerns (UI, DOM, storage).\n * Can be reused across PWA, Electron, mobile platforms.\n *\n * Architecture:\n * ┌─────────────────────────────────────────────────────┐\n * │ PlayerCore (Pure Orchestration) │\n * │ - Collection cycle coordination │\n * │ - Schedule checking │\n * │ - Layout transition logic │\n * │ - Event emission (not DOM manipulation) │\n * │ - XMDS communication │\n * │ - XMR integration │\n * └─────────────────────────────────────────────────────┘\n * ↓\n * ┌─────────────────────────────────────────────────────┐\n * │ Platform Layer (PWA/Electron/Mobile) │\n * │ - UI updates (status display, progress bars) │\n * │ - DOM manipulation │\n * │ - Platform-specific storage │\n * │ - Blob URL management │\n * │ - Event listeners for PlayerCore events │\n * └─────────────────────────────────────────────────────┘\n *\n * Usage:\n * const core = new PlayerCore({\n * config,\n * xmds,\n * cache,\n * schedule,\n * renderer,\n * xmrWrapper\n * });\n *\n * // Listen to events\n * core.on('collection-start', () => { ... });\n * core.on('layout-ready', (layoutId) => { ... });\n *\n * // Start collection\n * await core.collect();\n */\n\nimport { EventEmitter, createLogger, applyCmsLogLevel, openIDB } from '@xiboplayer/utils';\nimport { calculateTimeline, parseLayoutFile } from '@xiboplayer/schedule';\nimport { CacheAnalyzer } from '@xiboplayer/cache';\nimport { DataConnectorManager } from './data-connectors.js';\nimport { LayoutBlacklist } from './layout-blacklist.js';\nimport { CORE_EVENTS as E } from './events.js';\n\nconst log = createLogger('PlayerCore');\n\n/**\n * Discover a local/LAN IP address.\n * Electron: os.networkInterfaces() via preload (reliable, skips VPN/Docker).\n * Chromium/browser: proxy endpoint GET /system/lan-ip (Node.js has os.networkInterfaces()).\n */\nasync function discoverLanIp() {\n if (typeof window !== 'undefined' && window.electronAPI?.getLanIpAddress) {\n try { return await window.electronAPI.getLanIpAddress(); } catch (_) {}\n }\n // Fallback: ask the proxy server (works in Chromium kiosk and any browser)\n try {\n const fetcher = globalThis.__nativeFetch || globalThis.fetch;\n const res = await fetcher('/system/lan-ip');\n if (res.ok) {\n const { ip } = await res.json();\n if (ip) return ip;\n }\n } catch (_) {}\n return '';\n}\n\n// IndexedDB database/store for offline cache\nconst OFFLINE_DB_BASE = 'xibo-offline-cache';\nconst OFFLINE_DB_VERSION = 1;\nconst OFFLINE_STORE = 'cache';\n\n\n/** Open the offline cache IndexedDB (creates store on first use) */\nfunction openOfflineDb(cmsId) {\n const dbName = cmsId ? `${OFFLINE_DB_BASE}-${cmsId}` : OFFLINE_DB_BASE;\n return openIDB(dbName, OFFLINE_DB_VERSION, OFFLINE_STORE);\n}\n\nexport class PlayerCore extends EventEmitter {\n constructor(options) {\n super();\n\n // Required dependencies (injected)\n this.config = options.config;\n this.xmds = options.xmds;\n this.cache = options.cache;\n this.schedule = options.schedule;\n this.renderer = options.renderer;\n this.XmrWrapper = options.xmrWrapper;\n this.statsCollector = options.statsCollector; // Optional: proof of play tracking\n this.displaySettings = options.displaySettings; // Optional: CMS display settings manager\n\n // CMS ID for namespaced IndexedDB databases\n this._cmsId = options.cmsId || null;\n\n // Data connectors manager (real-time data for widgets)\n this.dataConnectorManager = new DataConnectorManager();\n\n // Discover LAN IP early (async, non-blocking)\n discoverLanIp().then((ip) => {\n this._lanIpAddress = ip;\n log.info('LAN IP:', ip || '(not discovered)');\n });\n\n // State\n this.xmr = null;\n this.currentLayoutId = null;\n this.collecting = false;\n this.collectionInterval = null;\n this.pendingLayouts = new Map(); // layoutId -> required media IDs\n this._layoutMediaStatus = new Map(); // layoutFile → { ready: boolean, missing: string[] }\n this.offlineMode = false; // Track whether we're currently in offline mode\n this._normalCollectInterval = null; // Saved interval to restore after offline retry\n this._offlineRetrySeconds = 0; // Current backoff interval (0 = not retrying)\n\n // CRC32 checksums for skip optimization (avoid redundant XMDS calls)\n this._lastCheckRf = null;\n this._lastCheckSchedule = null;\n\n // Timeline recalculation guard — skip when inputs haven't changed\n this._lastTimelineFingerprint = null;\n this._lastTimeline = null;\n\n // Layout override state (for changeLayout/overlayLayout via XMR → revertToSchedule)\n this._layoutOverride = null; // { layoutId, type: 'change'|'overlay' }\n this._lastRequiredFiles = []; // Track files for MediaInventory\n\n // Scheduled commands tracking (avoid re-executing same command)\n this._executedCommands = new Set();\n\n // Display commands from RegisterDisplay (used by XMR commandAction)\n this.displayCommands = null;\n\n // Fault reporting agent (independent timer, faster than collection cycle)\n this._faultReportingInterval = null;\n this._faultReportingSeconds = 60; // Default: check for faults every 60s\n\n // Unsafe layout blacklist: layoutId → { failures: number, blacklisted: boolean, reason: string }\n this._layoutBlacklist = new LayoutBlacklist(3);\n\n // Status tracking for NotifyStatus enrichment\n this._lastLayoutChangeTime = null; // ISO timestamp of last layout switch\n this._statusCode = 2; // 1=running, 2=downloading, 3=error\n\n // Dynamic layout tracking (useDuration=0 videos — must play to natural end)\n this._dynamicLayouts = new Set();\n\n // Multi-display sync configuration (from RegisterDisplay syncGroup settings)\n this.syncConfig = null;\n this.syncManager = null; // Optional: set via setSyncManager() after RegisterDisplay\n\n // Layout durations for timeline calculation (layoutFile/layoutId → seconds)\n this._layoutDurations = new Map();\n this._finalDurations = new Set(); // layoutFiles whose duration is definitive (all videos probed)\n\n // Guard: layout currently being prepared (async prepareAndRenderLayout in flight)\n this._preparingLayoutId = null;\n\n // Cache analyzer for stale media detection and storage health\n this.cacheAnalyzer = this.cache ? new CacheAnalyzer(this.cache) : null;\n\n // In-memory offline cache (populated from IndexedDB on first load)\n this._offlineCache = { schedule: null, settings: null, requiredFiles: null };\n this._offlineDbReady = this._initOfflineCache();\n }\n\n /** Schedule queue options — avoids repeating this object in 8 call sites */\n get _queueOptions() {\n return { dynamicLayouts: this._dynamicLayouts };\n }\n\n /**\n * Schedule an auto-revert timer for layout/overlay overrides.\n * @param {number} id - Layout ID\n * @param {number} duration - Duration in seconds (0 = no timer)\n * @param {string} label - Description for logging\n */\n _scheduleAutoRevert(id, duration, label) {\n if (duration > 0) {\n setTimeout(() => {\n if (this._layoutOverride?.layoutId === id) {\n log.info(`${label} duration expired (${duration}s), reverting to schedule`);\n this.revertToSchedule();\n }\n }, duration * 1000);\n }\n }\n\n // ── Offline Cache (IndexedDB) ──────────────────────────────────────\n\n /** Load offline cache from IndexedDB into memory on startup */\n async _initOfflineCache() {\n try {\n const db = await openOfflineDb(this._cmsId);\n const tx = db.transaction(OFFLINE_STORE, 'readonly');\n const store = tx.objectStore(OFFLINE_STORE);\n\n const [schedule, settings, requiredFiles, durations, finalDurations, durVersion] = await Promise.all([\n new Promise(r => { const req = store.get('schedule'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n new Promise(r => { const req = store.get('settings'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n new Promise(r => { const req = store.get('requiredFiles'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n new Promise(r => { const req = store.get('durations'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n new Promise(r => { const req = store.get('finalDurations'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n new Promise(r => { const req = store.get('durationsVersion'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n ]);\n\n if (Array.isArray(durations) && durations.length > 0) {\n for (const [k, v] of durations) this._layoutDurations.set(k, v);\n log.info(`[Timeline] Restored ${durations.length} cached durations from IDB`);\n }\n // v2: clear stale final durations from before the fix.\n // Final durations are only valid when set by video metadata / probeLayoutDurations,\n // not by XLF estimates. Old IDB data has 60s defaults marked as final.\n if (durVersion >= 2 && Array.isArray(finalDurations) && finalDurations.length > 0) {\n for (const k of finalDurations) this._finalDurations.add(k);\n log.info(`[Timeline] Restored ${finalDurations.length} final duration keys from IDB`);\n } else if (Array.isArray(finalDurations) && finalDurations.length > 0) {\n log.info(`[Timeline] Discarded ${finalDurations.length} stale final duration keys (pre-v2)`);\n }\n\n this._offlineCache = { schedule, settings, requiredFiles };\n this._offlineDb = db; // Keep handle open for _offlineSave (avoids reopen per write)\n log.info('Offline cache loaded from IndexedDB',\n schedule ? '(has schedule)' : '(empty)');\n } catch (e) {\n log.warn('Failed to load offline cache from IndexedDB:', e);\n }\n }\n\n /** Save a key to both in-memory cache and IndexedDB (fire-and-forget) */\n async _offlineSave(key, data) {\n this._offlineCache[key] = data;\n try {\n // Reuse persistent handle from _initOfflineCache (avoids 6 open/close per cycle)\n if (!this._offlineDb) {\n this._offlineDb = await openOfflineDb(this._cmsId);\n }\n const tx = this._offlineDb.transaction(OFFLINE_STORE, 'readwrite');\n tx.objectStore(OFFLINE_STORE).put(data, key);\n await new Promise((resolve, reject) => {\n tx.oncomplete = resolve;\n tx.onerror = () => reject(tx.error);\n });\n } catch (e) {\n // Handle closed/invalid DB — reopen on next attempt\n this._offlineDb = null;\n log.warn('Failed to save offline cache:', key, e);\n }\n }\n\n /** Check if we have any cached data to fall back on */\n hasCachedData() {\n return this._offlineCache.schedule !== null;\n }\n\n /** Check if the browser reports being offline */\n isOffline() {\n return typeof navigator !== 'undefined' && navigator.onLine === false;\n }\n\n /** Check if currently in offline mode */\n isInOfflineMode() {\n return this.offlineMode;\n }\n\n /**\n * Run an offline collection cycle using cached data.\n * Evaluates the cached schedule and continues playback.\n */\n collectOffline() {\n log.warn('Offline mode — using cached schedule');\n\n if (!this.offlineMode) {\n this.offlineMode = true;\n this.emit(E.OFFLINE_MODE, true);\n }\n\n // Exponential backoff: 30s → 60s → 120s → ... → capped at normal interval\n // Recovers quickly from brief outages but doesn't hammer when truly offline\n if (this.collectionInterval) {\n if (!this._normalCollectInterval) {\n this._normalCollectInterval = this._currentCollectInterval;\n this._offlineRetrySeconds = 30;\n } else {\n // Double the backoff, cap at normal interval\n this._offlineRetrySeconds = Math.min(\n this._offlineRetrySeconds * 2,\n this._normalCollectInterval\n );\n }\n this._setCollectionTimer(this._offlineRetrySeconds);\n log.info(`Offline: retry in ${this._offlineRetrySeconds}s`);\n }\n\n // Load cached settings for collection interval (first run only)\n if (!this.collectionInterval) {\n const cachedReg = this._offlineCache.settings;\n if (cachedReg?.settings) {\n this.setupCollectionInterval(cachedReg.settings);\n this._normalCollectInterval = this._currentCollectInterval;\n this._offlineRetrySeconds = 30;\n this._setCollectionTimer(this._offlineRetrySeconds);\n log.info(`Offline: retry in ${this._offlineRetrySeconds}s`);\n }\n }\n\n // Load cached schedule and apply it\n const cachedSchedule = this._offlineCache.schedule;\n if (cachedSchedule) {\n this.schedule.setSchedule(cachedSchedule);\n this.emit(E.SCHEDULE_RECEIVED, cachedSchedule);\n }\n\n // Evaluate current schedule\n const layoutFiles = this.schedule.getCurrentLayouts();\n log.info('Offline layouts:', layoutFiles);\n this.emit(E.LAYOUTS_SCHEDULED, layoutFiles);\n\n this._evaluateAndSwitchLayout(layoutFiles, 'Offline');\n\n this.emit(E.COLLECTION_COMPLETE);\n }\n\n /**\n * Evaluate the current schedule and switch layouts if needed.\n * Shared by both collect() and collectOffline() after emitting 'layouts-scheduled'.\n * @param {string[]} layoutFiles - Currently scheduled layout filenames\n * @param {string} context - Log context label (e.g. 'Offline' or '')\n */\n _evaluateAndSwitchLayout(layoutFiles, context) {\n const prefix = context ? `${context}: ` : '';\n\n // Use the queue (not raw layoutFiles) for play/expire decisions.\n // The queue has all constraints baked in (maxPlaysPerHour, priorities, dayparting).\n // The player is a dumb consumer — it only expires when the queue rebuilds\n // with a different layout set (new CMS schedule, daypart boundary crossed).\n const { queue } = this.schedule.getScheduleQueue(this._layoutDurations, this._queueOptions);\n\n if (queue.length > 0) {\n if (this.currentLayoutId) {\n const stillInQueue = queue.some(e => parseLayoutFile(e.layoutId) === this.currentLayoutId);\n\n if (!stillInQueue) {\n // Schedule changed and current layout is no longer in the queue — expire immediately.\n // Clear currentLayoutId and emit expire event so the renderer can teardown.\n // The renderer's layoutEnd → advanceToNextLayout flow handles the switch.\n log.info(`Layout ${this.currentLayoutId} no longer in queue — expiring`);\n this.currentLayoutId = null;\n this.emit(E.LAYOUT_EXPIRE_CURRENT);\n } else {\n // Layout is still in queue — don't interrupt, just rebuild queue in background.\n // The playing layout ends when its timer fires (layoutEnd event),\n // at which point advanceToNextLayout() pops from the already-updated queue.\n log.info(`Layout ${this.currentLayoutId} playing — queue updated in background, playback continues`);\n this.emit(E.LAYOUT_ALREADY_PLAYING, this.currentLayoutId);\n }\n } else if (!this._preparingLayoutId) {\n // No layout playing or being prepared — start one from the queue.\n // Guard with _preparingLayoutId to prevent a second _evaluateAndSwitchLayout\n // call (e.g. offline-restore then online-collect) from popping another layout\n // before the async prepareAndRenderLayout completes.\n const next = this.getNextLayout();\n if (next) {\n this._preparingLayoutId = next.layoutId;\n log.info(`${prefix}switching to layout ${next.layoutId}`);\n this.emit(E.LAYOUT_PREPARE_REQUEST, next.layoutId);\n }\n } else {\n log.info(`${prefix}layout ${this._preparingLayoutId} already being prepared, skipping`);\n }\n } else {\n log.info(`${context ? `${context}: n` : 'N'}o layouts${context ? ' in cached schedule' : ' scheduled, falling back to default'}`);\n this.emit(E.NO_LAYOUTS_SCHEDULED);\n }\n\n this.logUpcomingTimeline();\n }\n\n /**\n * Force an immediate collection (used by platform layer on 'online' event)\n */\n async collectNow() {\n this._lastCheckRf = null;\n this._lastCheckSchedule = null;\n return this.collect();\n }\n\n /**\n * Start collection cycle\n * Pure orchestration - emits events instead of updating UI\n */\n async collect() {\n // Prevent concurrent collections\n if (this.collecting) {\n log.debug('Collection already in progress, skipping');\n return;\n }\n\n this.collecting = true;\n\n try {\n // Ensure offline cache is loaded from IndexedDB before checking\n await this._offlineDbReady;\n\n log.info('Starting collection cycle...');\n this.emit(E.COLLECTION_START);\n\n // Check if browser reports offline\n if (this.isOffline()) {\n if (this.hasCachedData()) {\n this.collecting = false;\n return this.collectOffline();\n }\n throw new Error('Offline with no cached data — cannot start playback');\n }\n\n // Ensure RSA key pair exists before registering\n if (this.config.ensureXmrKeyPair) {\n await this.config.ensureXmrKeyPair();\n }\n\n // Register display\n log.debug('Collection step: registerDisplay');\n const regResult = await this.xmds.registerDisplay();\n log.info(`Display registered: ${regResult.code}${regResult.tags?.length ? `, tags: ${regResult.tags.join(', ')}` : ''}`);\n log.debug('Register result:', JSON.stringify(regResult));\n\n this._processRegistration(regResult);\n\n // Initialize XMR if available\n log.debug('Collection step: initializeXmr');\n await this.initializeXmr(regResult);\n\n // CRC32 skip optimization: only fetch RequiredFiles/Schedule when CMS data changed\n const checkRf = regResult.checkRf || '';\n const checkSchedule = regResult.checkSchedule || '';\n\n // Get required files (skip if CRC unchanged)\n if (!this._lastCheckRf || this._lastCheckRf !== checkRf) {\n // RequiredFiles changed — CMS may have fixed broken layouts\n this.resetBlacklist();\n\n log.debug('Collection step: requiredFiles');\n const rfResult = await this.xmds.requiredFiles();\n // RequiredFiles returns { files, purge } — files to download, items to delete\n const files = rfResult.files || rfResult;\n const purgeItems = rfResult.purge || [];\n log.info('Required files:', files.length, purgeItems.length > 0 ? `(+ ${purgeItems.length} purge)` : '');\n this._lastCheckRf = checkRf;\n this.emit(E.FILES_RECEIVED, files);\n\n // Cache required files for offline use\n this._offlineSave('requiredFiles', rfResult);\n\n if (purgeItems.length > 0) {\n this.emit(E.PURGE_REQUEST, purgeItems);\n }\n\n // Get schedule (skip if CRC unchanged)\n if (!this._lastCheckSchedule || this._lastCheckSchedule !== checkSchedule) {\n log.debug('Collection step: schedule');\n const schedule = await this.xmds.schedule();\n log.info('Schedule received');\n this._lastCheckSchedule = checkSchedule;\n log.debug('Collection step: processing schedule');\n this._applyNewSchedule(schedule);\n this.logUpcomingTimeline();\n }\n\n log.debug('Collection step: download-request + mediaInventory');\n const currentLayouts = this.schedule.getCurrentLayouts();\n\n // Layout IDs in playback order (from the pre-calculated queue)\n const { queue } = this.schedule.getScheduleQueue(\n this._layoutDurations,\n this._queueOptions\n );\n const layoutOrder = [...new Set(queue.map(e => parseLayoutFile(e.layoutId)))];\n\n this._lastRequiredFiles = files;\n\n // Download window enforcement (#81) — skip downloads outside configured window\n if (this.displaySettings?.isInDownloadWindow && !this.displaySettings.isInDownloadWindow()) {\n const nextWindow = this.displaySettings.getNextDownloadWindow?.();\n log.info(`Outside download window, skipping downloads${nextWindow ? ` (next: ${nextWindow.toLocaleTimeString()})` : ''}`);\n } else {\n this.emit(E.DOWNLOAD_REQUEST, { layoutOrder, files, layoutDependants: Object.fromEntries(this.schedule.getDependantsMap()) });\n }\n\n // Non-blocking cache analysis (stale media detection)\n if (this.cacheAnalyzer) {\n this.cacheAnalyzer.analyze(files).then(report => {\n this.emit(E.CACHE_ANALYSIS, report);\n }).catch(err => log.warn('Cache analysis failed:', err));\n }\n\n // Submit media inventory to CMS (reports cached files)\n this.submitMediaInventory(files);\n } else {\n if (checkRf) {\n log.info('RequiredFiles CRC unchanged, skipping download check');\n }\n if (this._lastCheckSchedule !== checkSchedule) {\n const schedule = await this.xmds.schedule();\n log.info('Schedule received (RF unchanged but schedule changed)');\n this._lastCheckSchedule = checkSchedule;\n this._applyNewSchedule(schedule);\n } else if (checkSchedule) {\n log.info('Schedule CRC unchanged, skipping');\n }\n }\n\n // Fetch weather data for schedule criteria evaluation (#15)\n await this._fetchWeatherData();\n\n log.debug('Collection step: evaluateSchedule');\n // Evaluate current schedule\n const layoutFiles = this.schedule.getCurrentLayouts();\n log.info('Current layouts:', layoutFiles);\n this.emit(E.LAYOUTS_SCHEDULED, layoutFiles);\n\n this._evaluateAndSwitchLayout(layoutFiles, '');\n\n // Process scheduled commands (auto-execute commands whose time has arrived)\n this._processScheduledCommands();\n\n // Submit stats if enabled and collector is available\n if (regResult.settings?.statsEnabled === 'On' || regResult.settings?.statsEnabled === '1') {\n if (this.statsCollector) {\n log.info('Stats enabled, submitting proof of play');\n this.emit(E.SUBMIT_STATS_REQUEST);\n } else {\n log.warn('Stats enabled but no StatsCollector provided');\n }\n }\n\n // Submit logs to CMS (always, regardless of stats setting)\n this.emit(E.SUBMIT_LOGS_REQUEST);\n\n // Submit faults immediately (higher priority than logs)\n this.emit(E.SUBMIT_FAULTS_REQUEST);\n\n // Setup collection interval on first run\n if (!this.collectionInterval && regResult.settings) {\n this.setupCollectionInterval(regResult.settings);\n }\n\n // Start fault reporting agent (independent of collection cycle)\n if (!this._faultReportingInterval) {\n this._startFaultReportingAgent();\n }\n\n // Recalculate timeline after every collection cycle completes,\n // even if schedule CRC was unchanged — durations or time may have shifted.\n this.logUpcomingTimeline();\n\n this.emit(E.COLLECTION_COMPLETE);\n\n } catch (error) {\n // Offline fallback: if network failed but we have cached data, use it\n if (this.hasCachedData()) {\n log.warn('Collection failed, falling back to cached data:', error?.message || error);\n this.emit(E.COLLECTION_ERROR, error);\n this.collecting = false;\n return this.collectOffline();\n }\n\n log.error('Collection error:', error);\n this.emit(E.COLLECTION_ERROR, error);\n throw error;\n } finally {\n this.collecting = false;\n }\n }\n\n /**\n * Process registration result — offline exit, settings, sync config, tags, commands.\n */\n _processRegistration(regResult) {\n // Cache settings for offline use\n this._offlineSave('settings', regResult);\n\n // Exit offline mode if we were in it\n if (this.offlineMode) {\n this.offlineMode = false;\n log.info('Back online — resuming normal collection');\n this.emit(E.OFFLINE_MODE, false);\n\n // Restore normal collection interval (was shortened for offline retry)\n if (this._normalCollectInterval) {\n this._setCollectionTimer(this._normalCollectInterval);\n this._normalCollectInterval = null;\n this._offlineRetrySeconds = 0;\n }\n }\n\n // Apply display settings if DisplaySettings manager is available\n if (this.displaySettings && regResult.settings) {\n const result = this.displaySettings.applySettings(regResult.settings);\n if (result.changed.includes('collectInterval')) {\n this.updateCollectionInterval(result.settings.collectInterval);\n }\n\n // Apply CMS logLevel (respects local overrides)\n if (regResult.settings.logLevel) {\n const applied = applyCmsLogLevel(regResult.settings.logLevel);\n if (applied) {\n log.info('Log level updated from CMS:', regResult.settings.logLevel);\n this.emit(E.LOG_LEVEL_CHANGED, regResult.settings.logLevel);\n }\n }\n }\n\n // Pass display properties to schedule for criteria evaluation\n if (this.schedule?.setDisplayProperties && regResult.settings) {\n this.schedule.setDisplayProperties(regResult.settings);\n }\n\n // Store sync config if display is in a sync group — only emit if CMS config changed\n // (compare raw CMS response, not the mutated config with relayUrl/syncGroupId added by PWA)\n if (regResult.syncConfig) {\n const rawKey = JSON.stringify(regResult.syncConfig);\n if (rawKey !== this._lastRawSyncConfig) {\n this._lastRawSyncConfig = rawKey;\n this.syncConfig = regResult.syncConfig;\n log.info('Sync group:', regResult.syncConfig.isLead ? 'LEAD' : `follower → ${regResult.syncConfig.syncGroup}`,\n `(switchDelay: ${regResult.syncConfig.syncSwitchDelay}ms, videoPauseDelay: ${regResult.syncConfig.syncVideoPauseDelay}ms)`);\n this.emit(E.SYNC_CONFIG, regResult.syncConfig);\n }\n }\n\n // Extract config from display tags (key|value convention)\n this._applyTagConfig(regResult.tags);\n\n // Store display commands for XMR commandAction resolution\n if (regResult.commands && regResult.commands.length > 0) {\n this.displayCommands = {};\n for (const cmd of regResult.commands) {\n this.displayCommands[cmd.commandCode] = cmd;\n }\n log.debug('Display commands:', Object.keys(this.displayCommands).join(', '));\n }\n\n this.emit(E.REGISTER_COMPLETE, regResult);\n }\n\n /**\n * Apply a new schedule from CMS — emit event, update schedule manager,\n * reset executed commands, refresh data connectors, and cache offline.\n */\n _applyNewSchedule(schedule) {\n this.emit(E.SCHEDULE_RECEIVED, schedule);\n this.schedule.setSchedule(schedule);\n this._executedCommands.clear();\n this.updateDataConnectors();\n this._offlineSave('schedule', schedule);\n }\n\n /**\n * Initialize XMR WebSocket connection\n */\n async initializeXmr(regResult) {\n const xmrUrl = regResult.settings?.xmrWebSocketAddress || regResult.settings?.xmrNetworkAddress;\n if (!xmrUrl) {\n log.warn('XMR not configured: no xmrWebSocketAddress or xmrNetworkAddress in CMS settings');\n this.emit(E.XMR_MISCONFIGURED, {\n reason: 'missing',\n message: 'XMR address not configured in CMS. Go to CMS Admin → Settings → Configuration → XMR and set the WebSocket address.',\n });\n return;\n }\n\n // Validate URL protocol — PWA players need ws:// or wss://, not tcp://\n if (xmrUrl.startsWith('tcp://')) {\n log.warn(`XMR address uses tcp:// protocol which is not supported by PWA players: ${xmrUrl}`);\n log.warn('Configure XMR_WS_ADDRESS in CMS Admin → Settings → Configuration → XMR (e.g. wss://your-domain/xmr)');\n this.emit(E.XMR_MISCONFIGURED, {\n reason: 'wrong-protocol',\n url: xmrUrl,\n message: `XMR uses tcp:// protocol (not supported by PWA). Set XMR WebSocket Address to wss://your-domain/xmr in CMS Settings.`,\n });\n return;\n }\n\n // Detect placeholder/example URLs\n if (/example\\.(org|com|net)/i.test(xmrUrl)) {\n log.warn(`XMR address contains placeholder domain: ${xmrUrl}`);\n log.warn('Configure the real XMR address in CMS Admin → Settings → Configuration → XMR');\n this.emit(E.XMR_MISCONFIGURED, {\n reason: 'placeholder',\n url: xmrUrl,\n message: `XMR address is still the default placeholder (${xmrUrl}). Update it in CMS Settings.`,\n });\n return;\n }\n\n const xmrCmsKey = regResult.settings?.xmrCmsKey || regResult.settings?.serverKey || this.config.serverKey;\n log.debug('XMR CMS Key:', xmrCmsKey ? 'present' : 'missing');\n\n if (!this.xmr) {\n log.info('Initializing XMR WebSocket:', xmrUrl);\n this.xmr = new this.XmrWrapper(this.config, this);\n await this.xmr.start(xmrUrl, xmrCmsKey);\n this.emit(E.XMR_CONNECTED, xmrUrl);\n } else if (!this.xmr.isConnected()) {\n log.info('XMR disconnected, attempting to reconnect...');\n await this.xmr.start(xmrUrl, xmrCmsKey);\n this.emit(E.XMR_RECONNECTED, xmrUrl);\n } else {\n log.debug('XMR already connected');\n }\n }\n\n /**\n * Setup collection interval\n */\n setupCollectionInterval(settings) {\n // Use DisplaySettings if available, otherwise fallback to raw settings\n const collectIntervalSeconds = this.displaySettings\n ? this.displaySettings.getCollectInterval()\n : parseInt(settings.collectInterval || '300', 10);\n\n this._setCollectionTimer(collectIntervalSeconds);\n this.emit(E.COLLECTION_INTERVAL_SET, collectIntervalSeconds);\n }\n\n /**\n * Update collection interval dynamically\n * Called when CMS changes the collection interval\n */\n updateCollectionInterval(newIntervalSeconds) {\n if (this.collectionInterval) {\n this._setCollectionTimer(newIntervalSeconds);\n this.emit(E.COLLECTION_INTERVAL_UPDATED, newIntervalSeconds);\n }\n }\n\n /**\n * Start the fault reporting agent.\n * Runs on an independent timer (default 60s) to submit faults faster\n * than the normal collection cycle (300s). This ensures the CMS dashboard\n * gets fault alerts with lower latency.\n */\n _startFaultReportingAgent() {\n if (this._faultReportingInterval) clearInterval(this._faultReportingInterval);\n\n log.info(`Fault reporting agent started (interval: ${this._faultReportingSeconds}s)`);\n this._faultReportingInterval = setInterval(() => {\n this.emit(E.SUBMIT_FAULTS_REQUEST);\n }, this._faultReportingSeconds * 1000);\n }\n\n /** Internal: (re)create the collection setInterval timer */\n _setCollectionTimer(seconds) {\n if (this.collectionInterval) clearInterval(this.collectionInterval);\n this._currentCollectInterval = seconds;\n log.info(`Collection interval: ${seconds}s`);\n this.collectionInterval = setInterval(() => {\n log.debug('Running scheduled collection cycle...');\n this.collect().catch(error => {\n log.error('Collection error:', error);\n this.emit(E.COLLECTION_ERROR, error);\n });\n }, seconds * 1000);\n }\n\n /**\n * Request layout change (called by XMR or schedule)\n * Pure orchestration - emits events for platform to handle\n */\n async requestLayoutChange(layoutId) {\n log.info(`Layout change requested: ${layoutId}`);\n\n // Clear current layout tracking so it will switch\n this.currentLayoutId = null;\n\n this.emit('layout-change-requested', layoutId);\n }\n\n /**\n * Mark layout as ready and current\n * Called by platform after it successfully renders the layout\n */\n /**\n * Clear the preparing-layout guard.\n * Called by platform layer when preparation is cancelled or skipped.\n */\n clearPreparingLayout() {\n this._preparingLayoutId = null;\n }\n\n setCurrentLayout(layoutId) {\n this.currentLayoutId = layoutId;\n this._preparingLayoutId = null;\n this._lastLayoutChangeTime = new Date().toISOString();\n this._statusCode = 1; // Running\n this.pendingLayouts.delete(layoutId);\n // Layout proved playable — clear media status (no longer missing)\n this._layoutMediaStatus.delete(`${layoutId}.xlf`);\n this.emit('layout-current', layoutId);\n // Force timeline recalc on layout change (fingerprint reset)\n this._lastTimelineFingerprint = null;\n this.logUpcomingTimeline();\n }\n\n /**\n * Mark layout as pending (waiting for media)\n * Called by platform when layout needs media downloads\n */\n setPendingLayout(layoutId, requiredMediaIds) {\n this.pendingLayouts.set(layoutId, requiredMediaIds);\n this.emit('layout-pending', layoutId, requiredMediaIds);\n }\n\n /**\n * Clear current layout (for replay)\n * Called by platform when layout ends\n */\n clearCurrentLayout() {\n this.currentLayoutId = null;\n this.emit('layout-cleared');\n }\n\n /**\n * Get the next layout from the pre-calculated schedule queue.\n * Pops the next entry, skipping blacklisted layouts.\n * Returns { layoutId, layoutFile } or null.\n */\n getNextLayout() {\n const entry = this.schedule.popNextFromQueue(\n this._layoutDurations,\n this._queueOptions\n );\n\n if (!entry) {\n // No queue entries — try default\n const defaultFile = this.schedule.schedule?.default;\n if (defaultFile) {\n const layoutId = parseLayoutFile(defaultFile);\n return { layoutId, layoutFile: defaultFile };\n }\n return null;\n }\n\n const layoutId = parseLayoutFile(entry.layoutId);\n\n if (this.isLayoutBlacklisted(layoutId)) {\n // Try next entries (up to queue length) to find a non-blacklisted one\n const { queue } = this.schedule.getScheduleQueue(\n this._layoutDurations,\n this._queueOptions\n );\n for (let i = 0; i < queue.length - 1; i++) {\n const next = this.schedule.popNextFromQueue(\n this._layoutDurations,\n this._queueOptions\n );\n if (next) {\n const nextId = parseLayoutFile(next.layoutId);\n if (!this.isLayoutBlacklisted(nextId)) {\n return { layoutId: nextId, layoutFile: next.layoutId };\n }\n }\n }\n // All blacklisted — return this one anyway to avoid blank screen\n log.warn('All queued layouts are blacklisted, using current entry as fallback');\n }\n\n return { layoutId, layoutFile: entry.layoutId };\n }\n\n /**\n * Peek at the next layout in the schedule queue without advancing.\n * Used by the preload system to know which layout to pre-build.\n * Returns { layoutId, layoutFile } or null if no next layout or same as current.\n */\n peekNextLayout() {\n const entry = this.schedule.peekNextInQueue(\n this._layoutDurations,\n this._queueOptions\n );\n\n if (!entry) return null;\n\n const layoutId = parseLayoutFile(entry.layoutId);\n\n // Don't preload if it's the same as current\n if (layoutId === this.currentLayoutId) {\n // Try the one after that\n const after = this.schedule.peekAfterNext(\n this._layoutDurations,\n this._queueOptions\n );\n if (!after) return null;\n const afterId = parseLayoutFile(after.layoutId);\n if (afterId === this.currentLayoutId || this.isLayoutBlacklisted(afterId)) return null;\n return { layoutId: afterId, layoutFile: after.layoutId };\n }\n\n if (this.isLayoutBlacklisted(layoutId)) return null;\n\n return { layoutId, layoutFile: entry.layoutId };\n }\n\n /**\n * Advance to the next layout in the pre-calculated schedule queue.\n * Called by platform layer when a layout finishes (layoutEnd event).\n * Pops the next entry from the queue and emits layout-prepare-request.\n */\n advanceToNextLayout() {\n // Don't cycle if we're in a layout override (XMR changeLayout/overlayLayout)\n if (this._layoutOverride) {\n log.info('Layout override active, not advancing schedule');\n return;\n }\n\n const next = this.getNextLayout();\n\n // ── Never-stop guarantee ────────────────────────────────────────\n if (!next) {\n if (this.currentLayoutId) {\n log.info(`No layouts in queue, replaying ${this.currentLayoutId} to avoid blank screen`);\n const replayId = this.currentLayoutId;\n this.currentLayoutId = null;\n this._preparingLayoutId = replayId;\n this.emit(E.LAYOUT_PREPARE_REQUEST, replayId);\n } else {\n log.info('No layouts scheduled during advance');\n this.emit(E.NO_LAYOUTS_SCHEDULED);\n }\n return;\n }\n\n const { layoutId, layoutFile } = next;\n const dur = this._layoutDurations.get(layoutFile) || '?';\n\n // Debug: log incoming layout vs timeline overlay top entries\n if (this._lastTimeline && this._lastTimeline.length > 0) {\n const top2 = this._lastTimeline.slice(0, 2).map(e => {\n const t = e.startTime.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });\n return `${e.layoutFile}(${e.duration}s@${t})`;\n });\n log.debug(`[Timeline] Layout transition: entering ${layoutFile} (${dur}s), overlay top: [${top2.join(', ')}]`);\n\n // Warn if the entering layout doesn't match the first timeline entry\n if (this._lastTimeline[0].layoutFile !== layoutFile) {\n log.warn(`[Timeline] Mismatch: entering ${layoutFile} but overlay expects ${this._lastTimeline[0].layoutFile}`);\n }\n } else {\n log.debug(`[Timeline] Layout transition: entering ${layoutFile} (${dur}s), no timeline data`);\n }\n\n // Multi-display sync: if this is a sync event and we have a SyncManager,\n // delegate layout transitions to the sync protocol\n if (this.syncManager && this.schedule.isSyncEvent(layoutFile)) {\n if (this.isSyncLead()) {\n log.info(`[Sync] Lead requesting coordinated layout change: ${layoutId}`);\n // Lead must render the layout itself (not just coordinate followers).\n // Emit layout-prepare-request so the renderer builds it, while\n // requestLayoutChange coordinates the show timing with followers.\n this._preparingLayoutId = layoutId;\n this.emit(E.LAYOUT_PREPARE_REQUEST, layoutId);\n this.syncManager.requestLayoutChange(layoutId).catch(err => {\n log.error('[Sync] Layout change failed:', err);\n });\n return;\n } else if (this.syncManager.transport?.connected) {\n log.info(`[Sync] Follower waiting for lead signal (not advancing independently)`);\n return;\n } else {\n log.warn(`[Sync] Follower: lead unreachable, advancing independently`);\n }\n }\n\n if (layoutId === this.currentLayoutId) {\n log.info(`Next layout ${layoutId} is same as current, triggering replay`);\n this.currentLayoutId = null;\n }\n\n const { queue } = this.schedule.getScheduleQueue(\n this._layoutDurations,\n this._queueOptions\n );\n const pos = this.schedule.getQueuePosition();\n log.info(`Advancing to layout ${layoutId} (queue pos ${pos}/${queue.length})`);\n\n // Set _preparingLayoutId BEFORE emitting to prevent collect() cycles\n // from seeing both currentLayoutId=null and _preparingLayoutId=null\n // and popping another layout from the queue (double-pop race).\n this._preparingLayoutId = layoutId;\n this.emit(E.LAYOUT_PREPARE_REQUEST, layoutId);\n }\n\n /**\n * Go back to the previous layout in the schedule queue (wraps around).\n * Called by platform layer in response to manual navigation (keyboard/remote).\n * Skips sync-manager logic — manual navigation is local only.\n */\n advanceToPreviousLayout() {\n if (this._layoutOverride) {\n log.info('Layout override active, not going back');\n return;\n }\n\n const { queue } = this.schedule.getScheduleQueue(\n this._layoutDurations,\n this._queueOptions\n );\n if (queue.length <= 1) {\n log.info('Single or empty queue, nothing to go back to');\n return;\n }\n\n // Go back 2 positions (current was already popped, so -2 from current pos)\n const entry = this.schedule.rewindQueue(2, this._layoutDurations, this._queueOptions);\n if (!entry) return;\n\n const layoutId = parseLayoutFile(entry.layoutId);\n\n if (layoutId === this.currentLayoutId) {\n log.info('Previous layout is same as current, nothing to go back to');\n return;\n }\n\n log.info(`Going back to layout ${layoutId}`);\n this.emit(E.LAYOUT_PREPARE_REQUEST, layoutId);\n }\n\n /**\n * Notify that a file is ready (called by platform for both layout and media files)\n * Checks if any pending layouts can now be rendered\n */\n notifyMediaReady(fileId, fileType = 'media') {\n log.debug(`File ${fileId} ready (${fileType})`);\n\n // Check if any pending layouts are now complete\n for (const [layoutId, requiredFiles] of this.pendingLayouts.entries()) {\n // Check if this file is needed by this layout\n // For layout files: match layout ID with file ID (layout 78 needs layout/78)\n // For media files: check if fileId is in requiredFiles array\n const isLayoutFile = fileType === 'layout' && layoutId === parseInt(fileId);\n const isRequiredMedia = fileType === 'media' && requiredFiles.includes(fileId);\n\n if (isLayoutFile || isRequiredMedia) {\n log.debug(`${fileType} ${fileId} was needed by pending layout ${layoutId}, checking if ready...`);\n this.emit(E.CHECK_PENDING_LAYOUT, layoutId, requiredFiles);\n }\n }\n }\n\n /**\n * Notify layout status to CMS\n */\n async notifyLayoutStatus(layoutId) {\n try {\n const status = {\n currentLayoutId: layoutId,\n deviceName: this.config?.displayName || '',\n displayName: this.config?.displayName || '',\n lastCommandSuccess: this._lastCommandSuccess ?? true,\n code: this._statusCode,\n lastLayoutChangeTime: this._lastLayoutChangeTime || new Date().toISOString(),\n };\n\n // Add geo-location if available\n if (this.config?.latitude) status.latitude = this.config.latitude;\n if (this.config?.longitude) status.longitude = this.config.longitude;\n\n // Report LAN IP so CMS can tell sync followers where the lead is\n if (this._lanIpAddress) status.lanIpAddress = this._lanIpAddress;\n\n await this.xmds.notifyStatus(status);\n this.emit('status-notified', layoutId);\n } catch (error) {\n log.warn('Failed to notify status:', error);\n this.emit('status-notify-failed', layoutId, error);\n }\n }\n\n /**\n * Report geo location (called by XMR when CMS pushes coordinates)\n * Updates schedule location for geo-fencing and triggers schedule re-evaluation.\n * @param {Object} data - { latitude, longitude }\n */\n reportGeoLocation(data) {\n const lat = parseFloat(data?.latitude);\n const lng = parseFloat(data?.longitude);\n\n if (isNaN(lat) || isNaN(lng)) {\n log.warn('reportGeoLocation: invalid coordinates', data);\n return;\n }\n\n log.info(`Geo location from CMS: ${lat.toFixed(4)}, ${lng.toFixed(4)}`);\n\n if (this.schedule?.setLocation) {\n this.schedule.setLocation(lat, lng);\n }\n\n this.emit('location-updated', { latitude: lat, longitude: lng, source: 'cms' });\n this.checkSchedule();\n }\n\n /**\n * Request geo location using a fallback chain:\n * 1. Browser Geolocation API (GPS / OS-level)\n * 2. Google Geolocation API (if GOOGLE_GEO_API_KEY is configured)\n * 3. IP-based geolocation (free, no key required)\n * @returns {Promise<{latitude: number, longitude: number}|null>}\n */\n async requestGeoLocation() {\n // Return cached location if still fresh (re-resolve every 30 minutes)\n const GEO_CACHE_MS = 30 * 60 * 1000;\n if (this._geoCache && (Date.now() - this._geoCache.ts) < GEO_CACHE_MS) {\n return this._geoCache.location;\n }\n\n // Try browser geolocation (works with GPS or Google API key baked into Chromium).\n // Skip if it already failed — Electron without a Google API key will never succeed.\n if (!this._browserGeoFailed) {\n const browser = await this._tryBrowserGeolocation();\n if (browser) {\n return this._cacheGeo(this._applyLocation(browser.latitude, browser.longitude, 'browser'));\n }\n this._browserGeoFailed = true;\n }\n\n // Try Google Geolocation API if key is configured\n const apiKey = this.config?.googleGeoApiKey;\n if (apiKey) {\n const google = await this._tryGoogleGeolocation(apiKey);\n if (google) {\n return this._cacheGeo(this._applyLocation(google.latitude, google.longitude, 'google-api'));\n }\n }\n\n // Fall back to IP-based geolocation (free, no key)\n const ip = await this._tryIpGeolocation();\n if (ip) {\n return this._cacheGeo(this._applyLocation(ip.latitude, ip.longitude, 'ip-geolocation'));\n }\n\n log.warn('All geolocation methods failed');\n return null;\n }\n\n /** Cache a resolved geolocation result. @private */\n _cacheGeo(location) {\n this._geoCache = { location, ts: Date.now() };\n return location;\n }\n\n /**\n * Extract config values from CMS display tags using key|value convention.\n * Tags like \"geoApiKey|AIzaSy...\" are parsed and applied to player config.\n * @param {string[]} tags - Array of tag strings from RegisterDisplay\n * @private\n */\n _applyTagConfig(tags) {\n if (!Array.isArray(tags) || tags.length === 0) return;\n\n const TAG_CONFIG_MAP = {\n 'geoApiKey': 'googleGeoApiKey',\n };\n\n for (const tag of tags) {\n const pipeIdx = tag.indexOf('|');\n if (pipeIdx === -1) continue;\n\n const key = tag.substring(0, pipeIdx);\n const value = tag.substring(pipeIdx + 1);\n const configKey = TAG_CONFIG_MAP[key];\n\n if (configKey && value && this.config) {\n log.info(`Config from CMS tag: ${key} → ${configKey}`);\n this.config[configKey] = value;\n }\n }\n }\n\n _applyLocation(lat, lng, source) {\n log.info(`Geolocation (${source}): ${lat.toFixed(4)}, ${lng.toFixed(4)}`);\n\n if (this.schedule?.setLocation) {\n this.schedule.setLocation(lat, lng);\n }\n\n this.emit('location-updated', { latitude: lat, longitude: lng, source });\n this.checkSchedule();\n\n return { latitude: lat, longitude: lng };\n }\n\n /**\n * Try the browser Geolocation API (navigator.geolocation).\n * @returns {Promise<{latitude: number, longitude: number}|null>}\n * @private\n */\n async _tryBrowserGeolocation() {\n if (typeof navigator === 'undefined' || !navigator.geolocation) return null;\n\n try {\n const position = await new Promise((resolve, reject) => {\n navigator.geolocation.getCurrentPosition(resolve, reject, {\n timeout: 10000,\n maximumAge: 300000, // 5 minutes\n enableHighAccuracy: false\n });\n });\n return { latitude: position.coords.latitude, longitude: position.coords.longitude };\n } catch (error) {\n log.warn('Browser geolocation failed:', error?.message || error);\n return null;\n }\n }\n\n /**\n * Try Google Geolocation API (direct HTTPS POST, bypasses Chromium's built-in service).\n * @param {string} apiKey - Google API key\n * @returns {Promise<{latitude: number, longitude: number}|null>}\n * @private\n */\n async _tryGoogleGeolocation(apiKey) {\n try {\n const res = await fetch(\n `https://www.googleapis.com/geolocation/v1/geolocate?key=${apiKey}`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ considerIp: true }),\n signal: AbortSignal.timeout(5000)\n }\n );\n if (!res.ok) {\n log.warn(`Google Geolocation API returned ${res.status}`);\n return null;\n }\n const data = await res.json();\n if (data.location?.lat != null && data.location?.lng != null) {\n return { latitude: data.location.lat, longitude: data.location.lng };\n }\n return null;\n } catch (error) {\n log.warn('Google Geolocation API failed:', error?.message || error);\n return null;\n }\n }\n\n /**\n * Try IP-based geolocation using free HTTPS providers (no API key needed).\n * Tries ipapi.co first, then freeipapi.com as fallback.\n * @returns {Promise<{latitude: number, longitude: number}|null>}\n * @private\n */\n async _tryIpGeolocation() {\n const providers = [\n {\n url: 'https://ipapi.co/json/',\n parse: (data) => data.latitude != null && data.longitude != null\n ? { latitude: data.latitude, longitude: data.longitude }\n : null\n },\n {\n url: 'https://freeipapi.com/api/json',\n parse: (data) => data.latitude != null && data.longitude != null\n ? { latitude: data.latitude, longitude: data.longitude }\n : null\n }\n ];\n\n for (const provider of providers) {\n try {\n const res = await fetch(provider.url, { signal: AbortSignal.timeout(5000) });\n if (!res.ok) continue;\n const data = await res.json();\n const location = provider.parse(data);\n if (location) return location;\n } catch (error) {\n log.warn(`IP geolocation (${provider.url}) failed:`, error?.message || error);\n }\n }\n return null;\n }\n\n /**\n * Re-evaluate current schedule and switch layouts if needed.\n * Called after location updates or other schedule-affecting changes.\n */\n checkSchedule() {\n const layoutFiles = this.schedule.getCurrentLayouts();\n this.emit(E.LAYOUTS_SCHEDULED, layoutFiles);\n this._evaluateAndSwitchLayout(layoutFiles, '');\n }\n\n /**\n * Capture screenshot (called by XMR wrapper)\n * Emits event for platform layer to handle\n */\n async captureScreenshot() {\n log.info('Screenshot requested');\n this.emit(E.SCREENSHOT_REQUEST);\n }\n\n /**\n * Change to a specific layout (called by XMR wrapper)\n * Tracks override state so revertToSchedule() can undo it.\n */\n async changeLayout(layoutId, options) {\n log.info('Layout change requested via XMR:', layoutId);\n const id = parseInt(layoutId, 10);\n const duration = options?.duration || 0;\n const changeMode = options?.changeMode || 'replace';\n this._layoutOverride = { layoutId: id, type: 'change', duration, changeMode };\n this.currentLayoutId = null; // Force re-render\n this.emit(E.LAYOUT_PREPARE_REQUEST, id);\n this._scheduleAutoRevert(id, duration, 'Layout override');\n }\n\n /**\n * Push an overlay layout on top of current content (called by XMR wrapper)\n * @param {number|string} layoutId - Layout to overlay\n */\n async overlayLayout(layoutId, options) {\n log.info('Overlay layout requested via XMR:', layoutId);\n const id = parseInt(layoutId, 10);\n const duration = options?.duration || 0;\n this._layoutOverride = { layoutId: id, type: 'overlay', duration };\n this.emit(E.OVERLAY_LAYOUT_REQUEST, id);\n this._scheduleAutoRevert(id, duration, 'Overlay');\n }\n\n /**\n * Revert to scheduled content after changeLayout/overlayLayout override\n */\n async revertToSchedule() {\n log.info('Reverting to scheduled content');\n this._layoutOverride = null;\n this.currentLayoutId = null;\n this.emit(E.REVERT_TO_SCHEDULE);\n\n // Re-evaluate schedule to get the right layout\n const layoutFiles = this.schedule.getCurrentLayouts();\n if (layoutFiles.length > 0) {\n const layoutFile = layoutFiles[0];\n const layoutId = parseLayoutFile(layoutFile);\n this.emit(E.LAYOUT_PREPARE_REQUEST, layoutId);\n } else {\n this.emit(E.NO_LAYOUTS_SCHEDULED);\n }\n }\n\n /**\n * Purge all cached content and re-download (called by XMR wrapper)\n */\n async purgeAll() {\n log.info('Purge all cache requested via XMR');\n this._lastCheckRf = null;\n this._lastCheckSchedule = null;\n this.emit(E.PURGE_ALL_REQUEST);\n // Trigger immediate re-collection after purge\n return this.collectNow();\n }\n\n /**\n * Execute a command (HTTP only in browser context)\n * @param {string} commandCode - The command code from CMS\n * @param {Object} commands - Commands map from display settings\n */\n async executeCommand(commandCode, commands) {\n log.info('Execute command requested:', commandCode);\n\n if (!commands || !commands[commandCode]) {\n log.warn('Unknown command code:', commandCode);\n this._lastCommandSuccess = false;\n this.emit(E.COMMAND_RESULT, { code: commandCode, success: false, reason: 'Unknown command' });\n return;\n }\n\n const command = commands[commandCode];\n const commandString = command.commandString || command.value || '';\n\n // Only HTTP commands are possible in a browser\n if (commandString.startsWith('http|')) {\n const parts = commandString.split('|');\n const url = parts[1];\n const contentType = parts[2] || 'application/json';\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': contentType },\n signal: AbortSignal.timeout(10000),\n });\n const success = response.ok;\n this._lastCommandSuccess = success;\n log.info(`HTTP command ${commandCode} result: ${response.status}`);\n this.emit(E.COMMAND_RESULT, { code: commandCode, success, status: response.status });\n } catch (error) {\n this._lastCommandSuccess = false;\n log.error(`HTTP command ${commandCode} failed:`, error);\n this.emit(E.COMMAND_RESULT, { code: commandCode, success: false, reason: error.message });\n }\n } else {\n // Emit event for platform layer (Electron/Chromium) to handle native commands\n // (shell, RS232, Android intent, etc.)\n log.info('Delegating non-HTTP command to platform layer:', commandCode);\n this.emit(E.EXECUTE_NATIVE_COMMAND, { code: commandCode, commandString });\n }\n }\n\n /**\n * Trigger a webhook action (called by XMR wrapper)\n * @param {string} triggerCode - The trigger code to fire\n */\n triggerWebhook(triggerCode) {\n log.info('Webhook trigger from XMR:', triggerCode);\n this.handleTrigger(triggerCode);\n }\n\n /**\n * Force refresh of data connectors (called by XMR wrapper)\n */\n refreshDataConnectors() {\n log.info('Data connector refresh requested via XMR');\n this.dataConnectorManager.refreshAll();\n this.emit('data-connectors-refreshed');\n }\n\n /**\n * Submit media inventory to CMS\n * Reports which files are cached and complete.\n * @param {Array} files - List of files from RequiredFiles\n */\n async submitMediaInventory(files) {\n if (!files || files.length === 0) return;\n\n try {\n // Build inventory XML: <files><file type=\"media\" id=\"1\" complete=\"1\" md5=\"abc\" lastChecked=\"123\"/></files>\n // complete: use file.complete if set by caller (cache layer), default to \"1\"\n const now = Math.floor(Date.now() / 1000);\n const fileEntries = files\n .filter(f => ['media', 'layout', 'resource', 'dependency', 'widget'].includes(f.type))\n .map(f => {\n const complete = f.complete !== undefined ? (f.complete ? '1' : '0') : '1';\n const fileType = f.fileType ? ` fileType=\"${f.fileType}\"` : '';\n return `<file type=\"${f.type}\" id=\"${f.id}\" complete=\"${complete}\" md5=\"${f.md5 || ''}\" lastChecked=\"${now}\"${fileType}/>`;\n })\n .join('');\n const inventoryXml = `<files>${fileEntries}</files>`;\n\n await this.xmds.mediaInventory(inventoryXml);\n log.info(`Media inventory submitted: ${files.length} files`);\n this.emit('media-inventory-submitted', files.length);\n } catch (error) {\n log.warn('MediaInventory submission failed:', error);\n }\n }\n\n /**\n * BlackList a media file (report broken media to CMS)\n * @param {string|number} mediaId - The media ID\n * @param {string} type - File type ('media' or 'layout')\n * @param {string} reason - Reason for blacklisting\n */\n async blackList(mediaId, type, reason) {\n try {\n await this.xmds.blackList(mediaId, type, reason);\n this.emit('media-blacklisted', { mediaId, type, reason });\n } catch (error) {\n log.warn('BlackList failed:', error);\n }\n }\n\n /**\n * Report a layout render failure. After N consecutive failures\n * (default 3), the layout is blacklisted and skipped in schedule\n * evaluation. Blacklisted layouts are reported to CMS via the\n * BlackList XMDS method.\n *\n * @param {number} layoutId - The layout that failed\n * @param {string} reason - Human-readable failure description\n */\n reportLayoutFailure(layoutId, reason) {\n const id = Number(layoutId);\n this._statusCode = 3; // Error — layout failed to render\n\n const { blacklisted, failures } = this._layoutBlacklist.recordFailure(id, reason);\n if (blacklisted && failures === 3) {\n // Newly blacklisted (threshold just reached)\n this.emit('layout-blacklisted', { layoutId: id, reason, failures });\n this.blackList(id, 'layout', reason);\n }\n }\n\n reportLayoutSuccess(layoutId) {\n const wasBlacklisted = this._layoutBlacklist.recordSuccess(Number(layoutId));\n if (wasBlacklisted) {\n this.emit('layout-unblacklisted', { layoutId: Number(layoutId) });\n }\n }\n\n isLayoutBlacklisted(layoutId) {\n return this._layoutBlacklist.isBlacklisted(layoutId);\n }\n\n getBlacklistedLayouts() {\n return this._layoutBlacklist.getBlacklistedIds();\n }\n\n resetBlacklist() {\n if (this._layoutBlacklist.reset() > 0) {\n this.emit('blacklist-reset');\n }\n }\n\n /**\n * Check if currently in a layout override (from XMR changeLayout/overlayLayout)\n */\n isLayoutOverridden() {\n return this._layoutOverride !== null;\n }\n\n /**\n * Handle interactive trigger (from IC or touch events)\n * Looks up matching action in schedule and executes it\n * @param {string} triggerCode - The trigger code from the IC request\n */\n handleTrigger(triggerCode) {\n const action = this.schedule.findActionByTrigger(triggerCode);\n if (!action) {\n log.debug('No scheduled action matches trigger:', triggerCode);\n return;\n }\n\n log.info(`Action triggered: ${action.actionType} (trigger: ${triggerCode})`);\n\n switch (action.actionType) {\n case 'navLayout':\n case 'navigateToLayout':\n if (action.layoutCode) {\n this.changeLayout(action.layoutCode);\n }\n break;\n case 'navWidget':\n case 'navigateToWidget':\n this.emit(E.NAVIGATE_TO_WIDGET, action);\n break;\n case 'command':\n this.emit('execute-command', action.commandCode);\n break;\n default:\n log.warn('Unknown action type:', action.actionType);\n }\n }\n\n /**\n * Update data connectors from current schedule\n * Reconfigures and restarts polling when schedule changes.\n */\n updateDataConnectors() {\n const connectors = this.schedule.getDataConnectors();\n\n if (connectors.length > 0) {\n log.info(`Configuring ${connectors.length} data connector(s)`);\n }\n\n this.dataConnectorManager.setConnectors(connectors);\n\n if (connectors.length > 0) {\n this.dataConnectorManager.startPolling();\n this.emit('data-connectors-started', connectors.length);\n }\n }\n\n /**\n * Process scheduled commands from the CMS schedule.\n * Checks for command events whose scheduled date has arrived and executes them.\n * Each command is only executed once (tracked by code+date key in _executedCommands).\n */\n _processScheduledCommands() {\n if (!this.schedule?.getCommands) return;\n\n const commands = this.schedule.getCommands();\n if (commands.length === 0) return;\n\n const now = new Date();\n\n for (const command of commands) {\n if (!command.code || !command.date) continue;\n\n // Unique key to track execution (same command can be scheduled multiple times)\n const commandKey = `${command.code}|${command.date}`;\n\n // Skip already executed commands\n if (this._executedCommands.has(commandKey)) continue;\n\n // Check if the command's scheduled time has arrived\n const commandDate = new Date(command.date);\n if (isNaN(commandDate.getTime())) {\n log.warn('Scheduled command has invalid date:', command.date);\n continue;\n }\n\n if (now >= commandDate) {\n log.info(`Executing scheduled command: ${command.code} (scheduled: ${command.date})`);\n this._executedCommands.add(commandKey);\n\n // Handle built-in commands directly\n if (command.code === 'collectNow') {\n // Trigger immediate collection on next tick (avoid re-entrance)\n setTimeout(() => this.collectNow().catch(e => log.error('collectNow command failed:', e)), 0);\n } else {\n // Emit event for platform layer to handle (reboot, restart, etc.)\n this.emit(E.SCHEDULED_COMMAND, command);\n }\n }\n }\n }\n\n /**\n * Fetch weather data from CMS and pass to schedule for criteria evaluation.\n * Non-blocking: weather fetch failure doesn't prevent schedule evaluation.\n */\n async _fetchWeatherData() {\n if (!this.xmds?.getWeather || !this.schedule?.setWeatherData) return;\n\n try {\n const weatherJson = await this.xmds.getWeather();\n const weatherData = typeof weatherJson === 'string' ? JSON.parse(weatherJson) : weatherJson;\n this.schedule.setWeatherData(weatherData);\n log.info('Weather data updated:', Object.keys(weatherData).join(', '));\n } catch (e) {\n log.warn('GetWeather failed (non-critical):', e?.message || e);\n }\n }\n\n /**\n * Get the DataConnectorManager instance\n * Used by platform layer to serve data to widgets via IC /realtime\n * @returns {DataConnectorManager}\n */\n getDataConnectorManager() {\n return this.dataConnectorManager;\n }\n\n /**\n * Set the SyncManager instance for multi-display coordination.\n * Called by platform layer after RegisterDisplay returns syncConfig.\n *\n * @param {SyncManager} syncManager - SyncManager instance\n */\n setSyncManager(syncManager) {\n this.syncManager = syncManager;\n log.info('SyncManager attached:', syncManager.isLead ? 'LEAD' : 'FOLLOWER');\n }\n\n /**\n * Check if this display is part of a sync group\n * @returns {boolean}\n */\n isInSyncGroup() {\n return this.syncConfig !== null;\n }\n\n /**\n * Check if this display is the sync group leader\n * @returns {boolean}\n */\n isSyncLead() {\n return this.syncConfig?.isLead === true;\n }\n\n /**\n * Get sync configuration\n * @returns {Object|null} { syncGroup, syncPublisherPort, syncSwitchDelay, syncVideoPauseDelay, isLead }\n */\n getSyncConfig() {\n return this.syncConfig;\n }\n\n // ── Timeline (offline schedule prediction) ─────────────────────────\n\n // Duration flow: renderer is the single source of truth.\n // 1. Renderer calculates duration from widgets → emits layoutDurationUpdated\n // 2. recordLayoutDuration stores it (with final flag) → persisted to IDB\n // 3. On restart, IDB restores correct durations → queue uses them immediately\n // No XLF parsing needed in core — the renderer already does this.\n\n /**\n * Calculate and log the upcoming playback timeline (next 2 hours).\n * Emits 'timeline-updated' with the full timeline array.\n */\n logUpcomingTimeline() {\n if (!this.schedule.getLayoutsAtTime) return; // Schedule doesn't support time queries\n\n // Fingerprint inputs: schedule CRC + sorted durations + current layout + media status.\n // When unchanged, re-emit the cached timeline — avoids time drift from\n // re-simulating with a new Date.now() anchor on every collection cycle.\n const durationEntries = [...this._layoutDurations.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([k, v]) => `${k}:${v}`)\n .join('|');\n const mediaStatusEntries = [...this._layoutMediaStatus.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([k, v]) => `${k}:${v.ready}:${v.missingKey}`)\n .join('|');\n const pendingEntries = [...this.pendingLayouts.keys()].sort().join(',');\n const queuePos = this.schedule.getQueuePosition() || 0;\n const fingerprint = `${this._lastCheckSchedule}|${durationEntries}|${this.currentLayoutId}|${queuePos}|${mediaStatusEntries}|${pendingEntries}`;\n\n if (fingerprint === this._lastTimelineFingerprint && this._lastTimeline) {\n this.emit(E.TIMELINE_UPDATED, this._lastTimeline);\n return;\n }\n\n const { queue } = this.schedule.getScheduleQueue(this._layoutDurations, this._queueOptions);\n const timeline = calculateTimeline(queue, this.schedule.getQueuePosition(), {\n currentLayoutStartedAt: this._lastLayoutChangeTime ? new Date(this._lastLayoutChangeTime) : null,\n defaultLayout: this.schedule.schedule?.default || null,\n durations: this._layoutDurations,\n });\n if (timeline.length === 0) return;\n\n // Annotate entries with missingMedia from pendingLayouts (high authority)\n // and _layoutMediaStatus (proactive check, lower authority)\n for (const entry of timeline) {\n const layoutId = parseInt(entry.layoutFile.replace('.xlf', ''), 10);\n const pendingMedia = this.pendingLayouts.get(layoutId);\n if (pendingMedia && pendingMedia.length > 0) {\n // pendingLayouts takes priority — definitively missing\n entry.missingMedia = pendingMedia.map(String);\n } else {\n const status = this._layoutMediaStatus.get(entry.layoutFile);\n if (status && !status.ready && status.missing.length > 0) {\n entry.missingMedia = status.missing.map(String);\n }\n }\n }\n\n this._lastTimelineFingerprint = fingerprint;\n this._lastTimeline = timeline;\n\n const lines = timeline.slice(0, 20).map(e => {\n const s = e.startTime.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });\n const end = e.endTime.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });\n const missingTag = e.missingMedia ? ` [MISSING: ${e.missingMedia.length} files]` : '';\n return ` ${s}-${end} Layout ${e.layoutFile} (${e.duration}s)${e.isDefault ? ' [default]' : ''}${missingTag}`;\n });\n\n // Log warnings for layouts with missing media\n for (const entry of timeline) {\n if (entry.missingMedia) {\n log.warn(`[Timeline] Layout ${entry.layoutFile}: ${entry.missingMedia.length} files missing`);\n }\n }\n\n log.info(`[Timeline] Next ${timeline.length} plays:\\n${lines.join('\\n')}`);\n this.emit(E.TIMELINE_UPDATED, timeline);\n }\n\n /**\n * Set media readiness status for a layout (proactive async check from platform layer).\n * No-ops if value is unchanged to avoid fingerprint churn.\n * @param {string} layoutFile - Layout file (e.g. '100.xlf')\n * @param {boolean} ready - Whether all media is cached\n * @param {string[]} [missing] - Array of missing media IDs/filenames\n */\n setLayoutMediaStatus(layoutFile, ready, missing = []) {\n const existing = this._layoutMediaStatus.get(layoutFile);\n const missingKey = missing.slice().sort().join(',');\n if (existing && existing.ready === ready && existing.missingKey === missingKey) return;\n\n this._layoutMediaStatus.set(layoutFile, { ready, missing, missingKey });\n // Invalidate fingerprint to force timeline recalculation\n this._lastTimelineFingerprint = null;\n }\n\n /**\n * Record/correct a layout's actual duration (e.g., from video loadedmetadata).\n * Updates the durations map and re-logs the timeline if it changed.\n * @param {string} file - Layout file or layout ID string\n * @param {number} duration - Actual duration in seconds\n * @param {boolean} [final=false] - True when all videos in the layout have been probed\n */\n recordLayoutDuration(file, duration, final = false) {\n // Normalize: store under both \"492\" and \"492.xlf\" forms so that\n // calculateTimeline (which looks up \"492.xlf\") and other callers\n // (which use \"492\") always find the corrected value.\n const id = String(file).replace('.xlf', '');\n const xlfKey = id + '.xlf';\n\n // Definitive duration — never overwrite once set\n if (this._finalDurations.has(id)) return;\n\n const prev = this._layoutDurations.get(file);\n if (prev === duration && !final) return; // No change\n\n this._layoutDurations.set(id, duration);\n this._layoutDurations.set(xlfKey, duration);\n\n if (final) {\n this._finalDurations.add(id);\n this._finalDurations.add(xlfKey);\n }\n\n log.debug(`[Timeline] Duration corrected: layout ${file} ${prev || '?'}s → ${duration}s${final ? ' (final)' : ''}`);\n\n // Invalidate the cached schedule queue so the next getScheduleQueue() call\n // rebuilds with corrected durations (affects queue log and period calculation).\n this.schedule.invalidateQueue();\n\n // Debounce timeline recalculation — multiple video loadedmetadata events\n // can fire within milliseconds; collapse them into one recalculation.\n if (this._timelineRecalcTimer) clearTimeout(this._timelineRecalcTimer);\n this._timelineRecalcTimer = setTimeout(() => {\n this._timelineRecalcTimer = null;\n this.logUpcomingTimeline();\n this._offlineSave('durations', [...this._layoutDurations.entries()]);\n this._offlineSave('finalDurations', [...this._finalDurations]);\n this._offlineSave('durationsVersion', 2);\n }, 500);\n }\n\n /**\n * Cleanup\n */\n cleanup() {\n if (this.collectionInterval) {\n clearInterval(this.collectionInterval);\n this.collectionInterval = null;\n }\n\n if (this._faultReportingInterval) {\n clearInterval(this._faultReportingInterval);\n this._faultReportingInterval = null;\n }\n\n if (this._timelineRecalcTimer) {\n clearTimeout(this._timelineRecalcTimer);\n this._timelineRecalcTimer = null;\n }\n\n if (this.xmr) {\n this.xmr.stop();\n this.xmr = null;\n }\n\n // Stop multi-display sync\n if (this.syncManager) {\n this.syncManager.stop();\n this.syncManager = null;\n }\n\n // Stop data connector polling\n this.dataConnectorManager.cleanup();\n\n // Emit cleanup-complete before removing listeners\n this.emit('cleanup-complete');\n this.removeAllListeners();\n }\n\n /**\n * Get current layout ID\n */\n getCurrentLayoutId() {\n return this.currentLayoutId;\n }\n\n /**\n * Get known duration for a layout (from video metadata or XLF parse).\n * @param {number|string} layoutId\n * @returns {number|undefined}\n */\n getLayoutDuration(layoutId) {\n const id = String(layoutId);\n return this._layoutDurations.get(`${id}.xlf`) || this._layoutDurations.get(id);\n }\n\n /**\n * Check if collecting\n */\n isCollecting() {\n return this.collecting;\n }\n\n /**\n * Get pending layouts\n */\n getPendingLayouts() {\n return Array.from(this.pendingLayouts.keys());\n }\n\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n// @xiboplayer/core - Player core orchestration\nimport pkg from '../package.json' with { type: 'json' };\nexport const VERSION = pkg.version;\nexport { PlayerCore } from './player-core.js';\nexport { DataConnectorManager } from './data-connectors.js';\nexport { CORE_EVENTS } from './events.js';\n"],"mappings":"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"}
1
+ {"version":3,"file":"src-Cx3tXAAu.js","names":["log","log","E","pkg"],"sources":["../../../core/package.json","../../../core/src/data-connectors.js","../../../core/src/layout-blacklist.js","../../../core/src/events.js","../../../core/src/player-core.js","../../../core/src/index.js"],"sourcesContent":["{\n \"name\": \"@xiboplayer/core\",\n \"version\": \"0.7.21\",\n \"description\": \"xiboplayer core orchestration and lifecycle management\",\n \"type\": \"module\",\n \"main\": \"./src/index.js\",\n \"types\": \"./src/index.d.ts\",\n \"exports\": {\n \".\": \"./src/index.js\",\n \"./player-core\": \"./src/player-core.js\"\n },\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"preview\": \"vite preview\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"test:ui\": \"vitest --ui\",\n \"test:coverage\": \"vitest run --coverage\"\n },\n \"dependencies\": {\n \"@xiboplayer/utils\": \"workspace:*\"\n },\n \"peerDependencies\": {\n \"@xiboplayer/cache\": \"workspace:*\",\n \"@xiboplayer/renderer\": \"workspace:*\",\n \"@xiboplayer/schedule\": \"workspace:*\",\n \"@xiboplayer/xmds\": \"workspace:*\"\n },\n \"devDependencies\": {\n \"@vitest/coverage-v8\": \"^4.1.3\",\n \"@vitest/ui\": \"^4.1.4\",\n \"jsdom\": \"^29.0.2\",\n \"vite\": \"^8.0.8\",\n \"vitest\": \"^4.1.2\"\n },\n \"keywords\": [\n \"xibo\",\n \"digital-signage\",\n \"player\",\n \"core\",\n \"orchestration\"\n ],\n \"author\": \"Pau Aliagas <linuxnow@gmail.com>\",\n \"license\": \"AGPL-3.0-or-later\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/xibo-players/xiboplayer.git\",\n \"directory\": \"packages/core\"\n },\n \"homepage\": \"https://xiboplayer.org\"\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 * 2 ** (entry.failures - CIRCUIT_BREAKER_THRESHOLD + 1), MAX_BACKOFF_MS);\n clearInterval(entry.timer);\n entry.timer = setTimeout(() => {\n this.fetchData(entry).catch(() => {});\n // Re-arm with backoff interval\n entry.timer = setInterval(() => {\n this.fetchData(entry).catch(() => {});\n }, backoffMs);\n }, backoffMs);\n log.warn(`Circuit breaker: ${dataKey} backing off to ${Math.round(backoffMs / 1000)}s`);\n }\n }\n }\n\n /**\n * Restore normal polling interval after circuit breaker backoff.\n * @private\n */\n _ensureNormalPolling(entry) {\n if (entry.failures === 0 && entry.timer) {\n const baseMs = (entry.config.updateInterval || 300) * 1000;\n // Clear any backed-off timer and restore the normal interval\n clearInterval(entry.timer);\n clearTimeout(entry.timer);\n entry.timer = setInterval(() => {\n this.fetchData(entry).catch(() => {});\n }, baseMs);\n }\n }\n\n /**\n * Force refresh all connectors — re-fetch immediately and restart polling.\n * Called by XMR dataUpdate command.\n */\n refreshAll() {\n if (this.connectors.size === 0) return;\n\n log.info(`Refreshing all ${this.connectors.size} data connector(s)`);\n this.stopPolling();\n this.startPolling();\n }\n\n /**\n * Cleanup - stop all polling and remove listeners\n */\n cleanup() {\n this.stopPolling();\n this.connectors.clear();\n this.removeAllListeners();\n log.debug('DataConnectorManager cleaned up');\n }\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Layout blacklist — tracks consecutive rendering failures and\n * blacklists layouts that fail repeatedly to prevent crash loops.\n */\n\nimport { createLogger } from '@xiboplayer/utils';\n\nconst log = createLogger('Blacklist');\n\nexport class LayoutBlacklist {\n /**\n * @param {number} [threshold=3] - Consecutive failures before blacklisting\n */\n constructor(threshold = 3) {\n this._entries = new Map();\n this._threshold = threshold;\n }\n\n /**\n * Record a layout rendering failure.\n * @param {number} layoutId\n * @param {string} reason\n * @returns {{ blacklisted: boolean, failures: number }} Current state after recording\n */\n recordFailure(layoutId, reason) {\n const id = Number(layoutId);\n const entry = this._entries.get(id) || { failures: 0, blacklisted: false, reason: '' };\n entry.failures++;\n entry.reason = reason;\n\n if (!entry.blacklisted && entry.failures >= this._threshold) {\n entry.blacklisted = true;\n log.warn(`Layout ${id} blacklisted after ${entry.failures} consecutive failures: ${reason}`);\n } else if (!entry.blacklisted) {\n log.info(`Layout ${id} failure ${entry.failures}/${this._threshold}: ${reason}`);\n }\n\n this._entries.set(id, entry);\n return { blacklisted: entry.blacklisted, failures: entry.failures };\n }\n\n /**\n * Record a successful layout render. Resets failure counter.\n * @param {number} layoutId\n * @returns {boolean} true if the layout was previously blacklisted (now restored)\n */\n recordSuccess(layoutId) {\n const id = Number(layoutId);\n if (!this._entries.has(id)) return false;\n\n const was = this._entries.get(id);\n this._entries.delete(id);\n\n if (was.blacklisted) {\n log.info(`Layout ${id} removed from blacklist (rendered successfully)`);\n return true;\n }\n return false;\n }\n\n /**\n * Check if a layout is currently blacklisted.\n * @param {number} layoutId\n * @returns {boolean}\n */\n isBlacklisted(layoutId) {\n const entry = this._entries.get(Number(layoutId));\n return entry?.blacklisted === true;\n }\n\n /**\n * Get all currently blacklisted layout IDs.\n * @returns {number[]}\n */\n getBlacklistedIds() {\n const result = [];\n for (const [id, entry] of this._entries) {\n if (entry.blacklisted) result.push(id);\n }\n return result;\n }\n\n /**\n * Reset the blacklist. Called when RequiredFiles changes.\n * @returns {number} Number of entries cleared\n */\n reset() {\n const count = this._entries.size;\n if (count > 0) {\n log.info(`Blacklist reset (${count} entries cleared)`);\n this._entries.clear();\n }\n return count;\n }\n\n get size() {\n return this._entries.size;\n }\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Core event name constants — shared between PlayerCore and platform layers.\n * Using constants prevents silent typo bugs at the core/platform boundary.\n */\n\nexport const CORE_EVENTS = Object.freeze({\n // Collection lifecycle\n COLLECTION_START: 'collection-start',\n COLLECTION_COMPLETE: 'collection-complete',\n COLLECTION_ERROR: 'collection-error',\n\n // Registration\n REGISTER_COMPLETE: 'register-complete',\n\n // Schedule\n SCHEDULE_RECEIVED: 'schedule-received',\n LAYOUTS_SCHEDULED: 'layouts-scheduled',\n NO_LAYOUTS_SCHEDULED: 'no-layouts-scheduled',\n TIMELINE_UPDATED: 'timeline-updated',\n\n // Layout lifecycle\n LAYOUT_PREPARE_REQUEST: 'layout-prepare-request',\n LAYOUT_EXPIRE_CURRENT: 'layout-expire-current',\n LAYOUT_ALREADY_PLAYING: 'layout-already-playing',\n CHECK_PENDING_LAYOUT: 'check-pending-layout',\n\n // Downloads\n FILES_RECEIVED: 'files-received',\n DOWNLOAD_REQUEST: 'download-request',\n\n // Overlay\n OVERLAY_LAYOUT_REQUEST: 'overlay-layout-request',\n REVERT_TO_SCHEDULE: 'revert-to-schedule',\n\n // Sync\n SYNC_CONFIG: 'sync-config',\n\n // XMR\n XMR_CONNECTED: 'xmr-connected',\n XMR_RECONNECTED: 'xmr-reconnected',\n XMR_MISCONFIGURED: 'xmr-misconfigured',\n\n // Navigation\n NAVIGATE_TO_WIDGET: 'navigate-to-widget',\n\n // Commands\n EXECUTE_NATIVE_COMMAND: 'execute-native-command',\n SCHEDULED_COMMAND: 'scheduled-command',\n COMMAND_RESULT: 'command-result',\n\n // Screenshots\n SCREENSHOT_REQUEST: 'screenshot-request',\n\n // Stats/Logs/Faults\n SUBMIT_STATS_REQUEST: 'submit-stats-request',\n SUBMIT_LOGS_REQUEST: 'submit-logs-request',\n SUBMIT_FAULTS_REQUEST: 'submit-faults-request',\n\n // Cache\n CACHE_ANALYSIS: 'cache-analysis',\n\n // Collection\n COLLECTION_INTERVAL_SET: 'collection-interval-set',\n COLLECTION_INTERVAL_UPDATED: 'collection-interval-updated',\n\n // Settings\n LOG_LEVEL_CHANGED: 'log-level-changed',\n OFFLINE_MODE: 'offline-mode',\n\n // Purge\n PURGE_REQUEST: 'purge-request',\n PURGE_ALL_REQUEST: 'purge-all-request',\n});\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * PlayerCore - Platform-independent orchestration module\n *\n * Pure orchestration logic without platform-specific concerns (UI, DOM, storage).\n * Can be reused across PWA, Electron, mobile platforms.\n *\n * Architecture:\n * ┌─────────────────────────────────────────────────────┐\n * │ PlayerCore (Pure Orchestration) │\n * │ - Collection cycle coordination │\n * │ - Schedule checking │\n * │ - Layout transition logic │\n * │ - Event emission (not DOM manipulation) │\n * │ - XMDS communication │\n * │ - XMR integration │\n * └─────────────────────────────────────────────────────┘\n * ↓\n * ┌─────────────────────────────────────────────────────┐\n * │ Platform Layer (PWA/Electron/Mobile) │\n * │ - UI updates (status display, progress bars) │\n * │ - DOM manipulation │\n * │ - Platform-specific storage │\n * │ - Blob URL management │\n * │ - Event listeners for PlayerCore events │\n * └─────────────────────────────────────────────────────┘\n *\n * Usage:\n * const core = new PlayerCore({\n * config,\n * xmds,\n * cache,\n * schedule,\n * renderer,\n * xmrWrapper\n * });\n *\n * // Listen to events\n * core.on('collection-start', () => { ... });\n * core.on('layout-ready', (layoutId) => { ... });\n *\n * // Start collection\n * await core.collect();\n */\n\nimport { EventEmitter, createLogger, applyCmsLogLevel, openIDB } from '@xiboplayer/utils';\nimport { calculateTimeline, parseLayoutFile } from '@xiboplayer/schedule';\nimport { CacheAnalyzer } from '@xiboplayer/cache';\nimport { DataConnectorManager } from './data-connectors.js';\nimport { LayoutBlacklist } from './layout-blacklist.js';\nimport { CORE_EVENTS as E } from './events.js';\n\nconst log = createLogger('PlayerCore');\n\n/**\n * Discover a local/LAN IP address.\n * Electron: os.networkInterfaces() via preload (reliable, skips VPN/Docker).\n * Chromium/browser: proxy endpoint GET /system/lan-ip (Node.js has os.networkInterfaces()).\n */\nasync function discoverLanIp() {\n if (typeof window !== 'undefined' && window.electronAPI?.getLanIpAddress) {\n try { return await window.electronAPI.getLanIpAddress(); } catch (_) {}\n }\n // Fallback: ask the proxy server (works in Chromium kiosk and any browser)\n try {\n const fetcher = globalThis.__nativeFetch || globalThis.fetch;\n const res = await fetcher('/system/lan-ip');\n if (res.ok) {\n const { ip } = await res.json();\n if (ip) return ip;\n }\n } catch (_) {}\n return '';\n}\n\n// IndexedDB database/store for offline cache\nconst OFFLINE_DB_BASE = 'xibo-offline-cache';\nconst OFFLINE_DB_VERSION = 1;\nconst OFFLINE_STORE = 'cache';\n\n\n/** Open the offline cache IndexedDB (creates store on first use) */\nfunction openOfflineDb(cmsId) {\n const dbName = cmsId ? `${OFFLINE_DB_BASE}-${cmsId}` : OFFLINE_DB_BASE;\n return openIDB(dbName, OFFLINE_DB_VERSION, OFFLINE_STORE);\n}\n\nexport class PlayerCore extends EventEmitter {\n constructor(options) {\n super();\n\n // Required dependencies (injected)\n this.config = options.config;\n this.xmds = options.xmds;\n this.cache = options.cache;\n this.schedule = options.schedule;\n this.renderer = options.renderer;\n this.XmrWrapper = options.xmrWrapper;\n this.statsCollector = options.statsCollector; // Optional: proof of play tracking\n this.displaySettings = options.displaySettings; // Optional: CMS display settings manager\n\n // CMS ID for namespaced IndexedDB databases\n this._cmsId = options.cmsId || null;\n\n // Data connectors manager (real-time data for widgets)\n this.dataConnectorManager = new DataConnectorManager();\n\n // Discover LAN IP early (async, non-blocking)\n discoverLanIp().then((ip) => {\n this._lanIpAddress = ip;\n log.info('LAN IP:', ip || '(not discovered)');\n });\n\n // State\n this.xmr = null;\n this.currentLayoutId = null;\n this.collecting = false;\n this.collectionInterval = null;\n this.pendingLayouts = new Map(); // layoutId -> required media IDs\n this._layoutMediaStatus = new Map(); // layoutFile → { ready: boolean, missing: string[] }\n this.offlineMode = false; // Track whether we're currently in offline mode\n this._normalCollectInterval = null; // Saved interval to restore after offline retry\n this._offlineRetrySeconds = 0; // Current backoff interval (0 = not retrying)\n\n // CRC32 checksums for skip optimization (avoid redundant XMDS calls)\n this._lastCheckRf = null;\n this._lastCheckSchedule = null;\n\n // Timeline recalculation guard — skip when inputs haven't changed\n this._lastTimelineFingerprint = null;\n this._lastTimeline = null;\n\n // Layout override state (for changeLayout/overlayLayout via XMR → revertToSchedule)\n this._layoutOverride = null; // { layoutId, type: 'change'|'overlay' }\n this._lastRequiredFiles = []; // Track files for MediaInventory\n\n // Scheduled commands tracking (avoid re-executing same command)\n this._executedCommands = new Set();\n\n // Display commands from RegisterDisplay (used by XMR commandAction)\n this.displayCommands = null;\n\n // Fault reporting agent (independent timer, faster than collection cycle)\n this._faultReportingInterval = null;\n this._faultReportingSeconds = 60; // Default: check for faults every 60s\n\n // Unsafe layout blacklist: layoutId → { failures: number, blacklisted: boolean, reason: string }\n this._layoutBlacklist = new LayoutBlacklist(3);\n\n // Status tracking for NotifyStatus enrichment\n this._lastLayoutChangeTime = null; // ISO timestamp of last layout switch\n this._statusCode = 2; // 1=running, 2=downloading, 3=error\n\n // Dynamic layout tracking (useDuration=0 videos — must play to natural end)\n this._dynamicLayouts = new Set();\n\n // Multi-display sync configuration (from RegisterDisplay syncGroup settings)\n this.syncConfig = null;\n this.syncManager = null; // Optional: set via setSyncManager() after RegisterDisplay\n\n // Layout durations for timeline calculation (layoutFile/layoutId → seconds)\n this._layoutDurations = new Map();\n this._finalDurations = new Set(); // layoutFiles whose duration is definitive (all videos probed)\n\n // Guard: layout currently being prepared (async prepareAndRenderLayout in flight)\n this._preparingLayoutId = null;\n\n // Cache analyzer for stale media detection and storage health\n this.cacheAnalyzer = this.cache ? new CacheAnalyzer(this.cache) : null;\n\n // In-memory offline cache (populated from IndexedDB on first load)\n this._offlineCache = { schedule: null, settings: null, requiredFiles: null };\n this._offlineDbReady = this._initOfflineCache();\n }\n\n /** Schedule queue options — avoids repeating this object in 8 call sites */\n get _queueOptions() {\n return { dynamicLayouts: this._dynamicLayouts };\n }\n\n /**\n * Schedule an auto-revert timer for layout/overlay overrides.\n * @param {number} id - Layout ID\n * @param {number} duration - Duration in seconds (0 = no timer)\n * @param {string} label - Description for logging\n */\n _scheduleAutoRevert(id, duration, label) {\n if (duration > 0) {\n setTimeout(() => {\n if (this._layoutOverride?.layoutId === id) {\n log.info(`${label} duration expired (${duration}s), reverting to schedule`);\n this.revertToSchedule();\n }\n }, duration * 1000);\n }\n }\n\n // ── Offline Cache (IndexedDB) ──────────────────────────────────────\n\n /** Load offline cache from IndexedDB into memory on startup */\n async _initOfflineCache() {\n try {\n const db = await openOfflineDb(this._cmsId);\n const tx = db.transaction(OFFLINE_STORE, 'readonly');\n const store = tx.objectStore(OFFLINE_STORE);\n\n const [schedule, settings, requiredFiles, durations, finalDurations, durVersion] = await Promise.all([\n new Promise(r => { const req = store.get('schedule'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n new Promise(r => { const req = store.get('settings'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n new Promise(r => { const req = store.get('requiredFiles'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n new Promise(r => { const req = store.get('durations'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n new Promise(r => { const req = store.get('finalDurations'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n new Promise(r => { const req = store.get('durationsVersion'); req.onsuccess = () => r(req.result ?? null); req.onerror = () => r(null); }),\n ]);\n\n if (Array.isArray(durations) && durations.length > 0) {\n for (const [k, v] of durations) this._layoutDurations.set(k, v);\n log.info(`[Timeline] Restored ${durations.length} cached durations from IDB`);\n }\n // v2: clear stale final durations from before the fix.\n // Final durations are only valid when set by video metadata / probeLayoutDurations,\n // not by XLF estimates. Old IDB data has 60s defaults marked as final.\n if (durVersion >= 2 && Array.isArray(finalDurations) && finalDurations.length > 0) {\n for (const k of finalDurations) this._finalDurations.add(k);\n log.info(`[Timeline] Restored ${finalDurations.length} final duration keys from IDB`);\n } else if (Array.isArray(finalDurations) && finalDurations.length > 0) {\n log.info(`[Timeline] Discarded ${finalDurations.length} stale final duration keys (pre-v2)`);\n }\n\n this._offlineCache = { schedule, settings, requiredFiles };\n this._offlineDb = db; // Keep handle open for _offlineSave (avoids reopen per write)\n log.info('Offline cache loaded from IndexedDB',\n schedule ? '(has schedule)' : '(empty)');\n } catch (e) {\n log.warn('Failed to load offline cache from IndexedDB:', e);\n }\n }\n\n /** Save a key to both in-memory cache and IndexedDB (fire-and-forget) */\n async _offlineSave(key, data) {\n this._offlineCache[key] = data;\n try {\n // Reuse persistent handle from _initOfflineCache (avoids 6 open/close per cycle)\n if (!this._offlineDb) {\n this._offlineDb = await openOfflineDb(this._cmsId);\n }\n const tx = this._offlineDb.transaction(OFFLINE_STORE, 'readwrite');\n tx.objectStore(OFFLINE_STORE).put(data, key);\n await new Promise((resolve, reject) => {\n tx.oncomplete = resolve;\n tx.onerror = () => reject(tx.error);\n });\n } catch (e) {\n // Handle closed/invalid DB — reopen on next attempt\n this._offlineDb = null;\n log.warn('Failed to save offline cache:', key, e);\n }\n }\n\n /** Check if we have any cached data to fall back on */\n hasCachedData() {\n return this._offlineCache.schedule !== null;\n }\n\n /** Check if the browser reports being offline */\n isOffline() {\n return typeof navigator !== 'undefined' && navigator.onLine === false;\n }\n\n /** Check if currently in offline mode */\n isInOfflineMode() {\n return this.offlineMode;\n }\n\n /**\n * Run an offline collection cycle using cached data.\n * Evaluates the cached schedule and continues playback.\n */\n collectOffline() {\n log.warn('Offline mode — using cached schedule');\n\n if (!this.offlineMode) {\n this.offlineMode = true;\n this.emit(E.OFFLINE_MODE, true);\n }\n\n // Exponential backoff: 30s → 60s → 120s → ... → capped at normal interval\n // Recovers quickly from brief outages but doesn't hammer when truly offline\n if (this.collectionInterval) {\n if (!this._normalCollectInterval) {\n this._normalCollectInterval = this._currentCollectInterval;\n this._offlineRetrySeconds = 30;\n } else {\n // Double the backoff, cap at normal interval\n this._offlineRetrySeconds = Math.min(\n this._offlineRetrySeconds * 2,\n this._normalCollectInterval\n );\n }\n this._setCollectionTimer(this._offlineRetrySeconds);\n log.info(`Offline: retry in ${this._offlineRetrySeconds}s`);\n }\n\n // Load cached settings for collection interval (first run only)\n if (!this.collectionInterval) {\n const cachedReg = this._offlineCache.settings;\n if (cachedReg?.settings) {\n this.setupCollectionInterval(cachedReg.settings);\n this._normalCollectInterval = this._currentCollectInterval;\n this._offlineRetrySeconds = 30;\n this._setCollectionTimer(this._offlineRetrySeconds);\n log.info(`Offline: retry in ${this._offlineRetrySeconds}s`);\n }\n }\n\n // Load cached schedule and apply it\n const cachedSchedule = this._offlineCache.schedule;\n if (cachedSchedule) {\n this.schedule.setSchedule(cachedSchedule);\n this.emit(E.SCHEDULE_RECEIVED, cachedSchedule);\n }\n\n // Evaluate current schedule\n const layoutFiles = this.schedule.getCurrentLayouts();\n log.info('Offline layouts:', layoutFiles);\n this.emit(E.LAYOUTS_SCHEDULED, layoutFiles);\n\n this._evaluateAndSwitchLayout(layoutFiles, 'Offline');\n\n this.emit(E.COLLECTION_COMPLETE);\n }\n\n /**\n * Evaluate the current schedule and switch layouts if needed.\n * Shared by both collect() and collectOffline() after emitting 'layouts-scheduled'.\n * @param {string[]} layoutFiles - Currently scheduled layout filenames\n * @param {string} context - Log context label (e.g. 'Offline' or '')\n */\n _evaluateAndSwitchLayout(layoutFiles, context) {\n const prefix = context ? `${context}: ` : '';\n\n // Use the queue (not raw layoutFiles) for play/expire decisions.\n // The queue has all constraints baked in (maxPlaysPerHour, priorities, dayparting).\n // The player is a dumb consumer — it only expires when the queue rebuilds\n // with a different layout set (new CMS schedule, daypart boundary crossed).\n const { queue } = this.schedule.getScheduleQueue(this._layoutDurations, this._queueOptions);\n\n if (queue.length > 0) {\n if (this.currentLayoutId) {\n const stillInQueue = queue.some(e => parseLayoutFile(e.layoutId) === this.currentLayoutId);\n\n if (!stillInQueue) {\n // Schedule changed and current layout is no longer in the queue — expire immediately.\n // Clear currentLayoutId and emit expire event so the renderer can teardown.\n // The renderer's layoutEnd → advanceToNextLayout flow handles the switch.\n log.info(`Layout ${this.currentLayoutId} no longer in queue — expiring`);\n this.currentLayoutId = null;\n this.emit(E.LAYOUT_EXPIRE_CURRENT);\n } else {\n // Layout is still in queue — don't interrupt, just rebuild queue in background.\n // The playing layout ends when its timer fires (layoutEnd event),\n // at which point advanceToNextLayout() pops from the already-updated queue.\n log.info(`Layout ${this.currentLayoutId} playing — queue updated in background, playback continues`);\n this.emit(E.LAYOUT_ALREADY_PLAYING, this.currentLayoutId);\n }\n } else if (!this._preparingLayoutId) {\n // No layout playing or being prepared — start one from the queue.\n // Guard with _preparingLayoutId to prevent a second _evaluateAndSwitchLayout\n // call (e.g. offline-restore then online-collect) from popping another layout\n // before the async prepareAndRenderLayout completes.\n const next = this.getNextLayout();\n if (next) {\n this._preparingLayoutId = next.layoutId;\n log.info(`${prefix}switching to layout ${next.layoutId}`);\n this.emit(E.LAYOUT_PREPARE_REQUEST, next.layoutId);\n }\n } else {\n log.info(`${prefix}layout ${this._preparingLayoutId} already being prepared, skipping`);\n }\n } else {\n log.info(`${context ? `${context}: n` : 'N'}o layouts${context ? ' in cached schedule' : ' scheduled, falling back to default'}`);\n this.emit(E.NO_LAYOUTS_SCHEDULED);\n }\n\n this.logUpcomingTimeline();\n }\n\n /**\n * Force an immediate collection (used by platform layer on 'online' event)\n */\n async collectNow() {\n this._lastCheckRf = null;\n this._lastCheckSchedule = null;\n return this.collect();\n }\n\n /**\n * Start collection cycle\n * Pure orchestration - emits events instead of updating UI\n */\n async collect() {\n // Prevent concurrent collections\n if (this.collecting) {\n log.debug('Collection already in progress, skipping');\n return;\n }\n\n this.collecting = true;\n\n try {\n // Ensure offline cache is loaded from IndexedDB before checking\n await this._offlineDbReady;\n\n log.info('Starting collection cycle...');\n this.emit(E.COLLECTION_START);\n\n // Check if browser reports offline\n if (this.isOffline()) {\n if (this.hasCachedData()) {\n this.collecting = false;\n return this.collectOffline();\n }\n throw new Error('Offline with no cached data — cannot start playback');\n }\n\n // Ensure RSA key pair exists before registering\n if (this.config.ensureXmrKeyPair) {\n await this.config.ensureXmrKeyPair();\n }\n\n // Register display\n log.debug('Collection step: registerDisplay');\n const regResult = await this.xmds.registerDisplay();\n log.info(`Display registered: ${regResult.code}${regResult.tags?.length ? `, tags: ${regResult.tags.join(', ')}` : ''}`);\n log.debug('Register result:', JSON.stringify(regResult));\n\n this._processRegistration(regResult);\n\n // Initialize XMR if available\n log.debug('Collection step: initializeXmr');\n await this.initializeXmr(regResult);\n\n // CRC32 skip optimization: only fetch RequiredFiles/Schedule when CMS data changed\n const checkRf = regResult.checkRf || '';\n const checkSchedule = regResult.checkSchedule || '';\n\n // Get required files (skip if CRC unchanged)\n if (!this._lastCheckRf || this._lastCheckRf !== checkRf) {\n // RequiredFiles changed — CMS may have fixed broken layouts\n this.resetBlacklist();\n\n log.debug('Collection step: requiredFiles');\n const rfResult = await this.xmds.requiredFiles();\n // RequiredFiles returns { files, purge } — files to download, items to delete\n const files = rfResult.files || rfResult;\n const purgeItems = rfResult.purge || [];\n log.info('Required files:', files.length, purgeItems.length > 0 ? `(+ ${purgeItems.length} purge)` : '');\n this._lastCheckRf = checkRf;\n this.emit(E.FILES_RECEIVED, files);\n\n // Cache required files for offline use\n this._offlineSave('requiredFiles', rfResult);\n\n if (purgeItems.length > 0) {\n this.emit(E.PURGE_REQUEST, purgeItems);\n }\n\n // Get schedule (skip if CRC unchanged)\n if (!this._lastCheckSchedule || this._lastCheckSchedule !== checkSchedule) {\n log.debug('Collection step: schedule');\n const schedule = await this.xmds.schedule();\n log.info('Schedule received');\n this._lastCheckSchedule = checkSchedule;\n log.debug('Collection step: processing schedule');\n this._applyNewSchedule(schedule);\n this.logUpcomingTimeline();\n }\n\n log.debug('Collection step: download-request + mediaInventory');\n const currentLayouts = this.schedule.getCurrentLayouts();\n\n // Layout IDs in playback order (from the pre-calculated queue)\n const { queue } = this.schedule.getScheduleQueue(\n this._layoutDurations,\n this._queueOptions\n );\n const layoutOrder = [...new Set(queue.map(e => parseLayoutFile(e.layoutId)))];\n\n this._lastRequiredFiles = files;\n\n // Download window enforcement (#81) — skip downloads outside configured window\n if (this.displaySettings?.isInDownloadWindow && !this.displaySettings.isInDownloadWindow()) {\n const nextWindow = this.displaySettings.getNextDownloadWindow?.();\n log.info(`Outside download window, skipping downloads${nextWindow ? ` (next: ${nextWindow.toLocaleTimeString()})` : ''}`);\n } else {\n this.emit(E.DOWNLOAD_REQUEST, { layoutOrder, files, layoutDependants: Object.fromEntries(this.schedule.getDependantsMap()) });\n }\n\n // Non-blocking cache analysis (stale media detection)\n if (this.cacheAnalyzer) {\n this.cacheAnalyzer.analyze(files).then(report => {\n this.emit(E.CACHE_ANALYSIS, report);\n }).catch(err => log.warn('Cache analysis failed:', err));\n }\n\n // Submit media inventory to CMS (reports cached files)\n this.submitMediaInventory(files);\n } else {\n if (checkRf) {\n log.info('RequiredFiles CRC unchanged, skipping download check');\n }\n if (this._lastCheckSchedule !== checkSchedule) {\n const schedule = await this.xmds.schedule();\n log.info('Schedule received (RF unchanged but schedule changed)');\n this._lastCheckSchedule = checkSchedule;\n this._applyNewSchedule(schedule);\n } else if (checkSchedule) {\n log.info('Schedule CRC unchanged, skipping');\n }\n }\n\n // Fetch weather data for schedule criteria evaluation (#15)\n await this._fetchWeatherData();\n\n log.debug('Collection step: evaluateSchedule');\n // Evaluate current schedule\n const layoutFiles = this.schedule.getCurrentLayouts();\n log.info('Current layouts:', layoutFiles);\n this.emit(E.LAYOUTS_SCHEDULED, layoutFiles);\n\n this._evaluateAndSwitchLayout(layoutFiles, '');\n\n // Process scheduled commands (auto-execute commands whose time has arrived)\n this._processScheduledCommands();\n\n // Submit stats if enabled and collector is available\n if (regResult.settings?.statsEnabled === 'On' || regResult.settings?.statsEnabled === '1') {\n if (this.statsCollector) {\n log.info('Stats enabled, submitting proof of play');\n this.emit(E.SUBMIT_STATS_REQUEST);\n } else {\n log.warn('Stats enabled but no StatsCollector provided');\n }\n }\n\n // Submit logs to CMS (always, regardless of stats setting)\n this.emit(E.SUBMIT_LOGS_REQUEST);\n\n // Submit faults immediately (higher priority than logs)\n this.emit(E.SUBMIT_FAULTS_REQUEST);\n\n // Setup collection interval on first run\n if (!this.collectionInterval && regResult.settings) {\n this.setupCollectionInterval(regResult.settings);\n }\n\n // Start fault reporting agent (independent of collection cycle)\n if (!this._faultReportingInterval) {\n this._startFaultReportingAgent();\n }\n\n // Recalculate timeline after every collection cycle completes,\n // even if schedule CRC was unchanged — durations or time may have shifted.\n this.logUpcomingTimeline();\n\n this.emit(E.COLLECTION_COMPLETE);\n\n } catch (error) {\n // Offline fallback: if network failed but we have cached data, use it\n if (this.hasCachedData()) {\n log.warn('Collection failed, falling back to cached data:', error?.message || error);\n this.emit(E.COLLECTION_ERROR, error);\n this.collecting = false;\n return this.collectOffline();\n }\n\n log.error('Collection error:', error);\n this.emit(E.COLLECTION_ERROR, error);\n throw error;\n } finally {\n this.collecting = false;\n }\n }\n\n /**\n * Process registration result — offline exit, settings, sync config, tags, commands.\n */\n _processRegistration(regResult) {\n // Cache settings for offline use\n this._offlineSave('settings', regResult);\n\n // Exit offline mode if we were in it\n if (this.offlineMode) {\n this.offlineMode = false;\n log.info('Back online — resuming normal collection');\n this.emit(E.OFFLINE_MODE, false);\n\n // Restore normal collection interval (was shortened for offline retry)\n if (this._normalCollectInterval) {\n this._setCollectionTimer(this._normalCollectInterval);\n this._normalCollectInterval = null;\n this._offlineRetrySeconds = 0;\n }\n }\n\n // Apply display settings if DisplaySettings manager is available\n if (this.displaySettings && regResult.settings) {\n const result = this.displaySettings.applySettings(regResult.settings);\n if (result.changed.includes('collectInterval')) {\n this.updateCollectionInterval(result.settings.collectInterval);\n }\n\n // Apply CMS logLevel (respects local overrides)\n if (regResult.settings.logLevel) {\n const applied = applyCmsLogLevel(regResult.settings.logLevel);\n if (applied) {\n log.info('Log level updated from CMS:', regResult.settings.logLevel);\n this.emit(E.LOG_LEVEL_CHANGED, regResult.settings.logLevel);\n }\n }\n }\n\n // Pass display properties to schedule for criteria evaluation\n if (this.schedule?.setDisplayProperties && regResult.settings) {\n this.schedule.setDisplayProperties(regResult.settings);\n }\n\n // Store sync config if display is in a sync group — only emit if CMS config changed\n // (compare raw CMS response, not the mutated config with relayUrl/syncGroupId added by PWA)\n if (regResult.syncConfig) {\n const rawKey = JSON.stringify(regResult.syncConfig);\n if (rawKey !== this._lastRawSyncConfig) {\n this._lastRawSyncConfig = rawKey;\n this.syncConfig = regResult.syncConfig;\n log.info('Sync group:', regResult.syncConfig.isLead ? 'LEAD' : `follower → ${regResult.syncConfig.syncGroup}`,\n `(switchDelay: ${regResult.syncConfig.syncSwitchDelay}ms, videoPauseDelay: ${regResult.syncConfig.syncVideoPauseDelay}ms)`);\n this.emit(E.SYNC_CONFIG, regResult.syncConfig);\n }\n }\n\n // Extract config from display tags (key|value convention)\n this._applyTagConfig(regResult.tags);\n\n // Store display commands for XMR commandAction resolution\n if (regResult.commands && regResult.commands.length > 0) {\n this.displayCommands = {};\n for (const cmd of regResult.commands) {\n this.displayCommands[cmd.commandCode] = cmd;\n }\n log.debug('Display commands:', Object.keys(this.displayCommands).join(', '));\n }\n\n this.emit(E.REGISTER_COMPLETE, regResult);\n }\n\n /**\n * Apply a new schedule from CMS — emit event, update schedule manager,\n * reset executed commands, refresh data connectors, and cache offline.\n */\n _applyNewSchedule(schedule) {\n this.emit(E.SCHEDULE_RECEIVED, schedule);\n this.schedule.setSchedule(schedule);\n this._executedCommands.clear();\n this.updateDataConnectors();\n this._offlineSave('schedule', schedule);\n }\n\n /**\n * Initialize XMR WebSocket connection\n */\n async initializeXmr(regResult) {\n const xmrUrl = regResult.settings?.xmrWebSocketAddress || regResult.settings?.xmrNetworkAddress;\n if (!xmrUrl) {\n log.warn('XMR not configured: no xmrWebSocketAddress or xmrNetworkAddress in CMS settings');\n this.emit(E.XMR_MISCONFIGURED, {\n reason: 'missing',\n message: 'XMR address not configured in CMS. Go to CMS Admin → Settings → Configuration → XMR and set the WebSocket address.',\n });\n return;\n }\n\n // Validate URL protocol — PWA players need ws:// or wss://, not tcp://\n if (xmrUrl.startsWith('tcp://')) {\n log.warn(`XMR address uses tcp:// protocol which is not supported by PWA players: ${xmrUrl}`);\n log.warn('Configure XMR_WS_ADDRESS in CMS Admin → Settings → Configuration → XMR (e.g. wss://your-domain/xmr)');\n this.emit(E.XMR_MISCONFIGURED, {\n reason: 'wrong-protocol',\n url: xmrUrl,\n message: `XMR uses tcp:// protocol (not supported by PWA). Set XMR WebSocket Address to wss://your-domain/xmr in CMS Settings.`,\n });\n return;\n }\n\n // Detect placeholder/example URLs\n if (/example\\.(org|com|net)/i.test(xmrUrl)) {\n log.warn(`XMR address contains placeholder domain: ${xmrUrl}`);\n log.warn('Configure the real XMR address in CMS Admin → Settings → Configuration → XMR');\n this.emit(E.XMR_MISCONFIGURED, {\n reason: 'placeholder',\n url: xmrUrl,\n message: `XMR address is still the default placeholder (${xmrUrl}). Update it in CMS Settings.`,\n });\n return;\n }\n\n const xmrCmsKey = regResult.settings?.xmrCmsKey || regResult.settings?.serverKey || this.config.serverKey;\n log.debug('XMR CMS Key:', xmrCmsKey ? 'present' : 'missing');\n\n if (!this.xmr) {\n log.info('Initializing XMR WebSocket:', xmrUrl);\n this.xmr = new this.XmrWrapper(this.config, this);\n await this.xmr.start(xmrUrl, xmrCmsKey);\n this.emit(E.XMR_CONNECTED, xmrUrl);\n } else if (!this.xmr.isConnected()) {\n log.info('XMR disconnected, attempting to reconnect...');\n await this.xmr.start(xmrUrl, xmrCmsKey);\n this.emit(E.XMR_RECONNECTED, xmrUrl);\n } else {\n log.debug('XMR already connected');\n }\n }\n\n /**\n * Setup collection interval\n */\n setupCollectionInterval(settings) {\n // Use DisplaySettings if available, otherwise fallback to raw settings\n const collectIntervalSeconds = this.displaySettings\n ? this.displaySettings.getCollectInterval()\n : parseInt(settings.collectInterval || '300', 10);\n\n this._setCollectionTimer(collectIntervalSeconds);\n this.emit(E.COLLECTION_INTERVAL_SET, collectIntervalSeconds);\n }\n\n /**\n * Update collection interval dynamically\n * Called when CMS changes the collection interval\n */\n updateCollectionInterval(newIntervalSeconds) {\n if (this.collectionInterval) {\n this._setCollectionTimer(newIntervalSeconds);\n this.emit(E.COLLECTION_INTERVAL_UPDATED, newIntervalSeconds);\n }\n }\n\n /**\n * Start the fault reporting agent.\n * Runs on an independent timer (default 60s) to submit faults faster\n * than the normal collection cycle (300s). This ensures the CMS dashboard\n * gets fault alerts with lower latency.\n */\n _startFaultReportingAgent() {\n if (this._faultReportingInterval) clearInterval(this._faultReportingInterval);\n\n log.info(`Fault reporting agent started (interval: ${this._faultReportingSeconds}s)`);\n this._faultReportingInterval = setInterval(() => {\n this.emit(E.SUBMIT_FAULTS_REQUEST);\n }, this._faultReportingSeconds * 1000);\n }\n\n /** Internal: (re)create the collection setInterval timer */\n _setCollectionTimer(seconds) {\n if (this.collectionInterval) clearInterval(this.collectionInterval);\n this._currentCollectInterval = seconds;\n log.info(`Collection interval: ${seconds}s`);\n this.collectionInterval = setInterval(() => {\n log.debug('Running scheduled collection cycle...');\n this.collect().catch(error => {\n log.error('Collection error:', error);\n this.emit(E.COLLECTION_ERROR, error);\n });\n }, seconds * 1000);\n }\n\n /**\n * Request layout change (called by XMR or schedule)\n * Pure orchestration - emits events for platform to handle\n */\n async requestLayoutChange(layoutId) {\n log.info(`Layout change requested: ${layoutId}`);\n\n // Clear current layout tracking so it will switch\n this.currentLayoutId = null;\n\n this.emit('layout-change-requested', layoutId);\n }\n\n /**\n * Mark layout as ready and current\n * Called by platform after it successfully renders the layout\n */\n /**\n * Clear the preparing-layout guard.\n * Called by platform layer when preparation is cancelled or skipped.\n */\n clearPreparingLayout() {\n this._preparingLayoutId = null;\n }\n\n setCurrentLayout(layoutId) {\n this.currentLayoutId = layoutId;\n this._preparingLayoutId = null;\n this._lastLayoutChangeTime = new Date().toISOString();\n this._statusCode = 1; // Running\n this.pendingLayouts.delete(layoutId);\n // Layout proved playable — clear media status (no longer missing)\n this._layoutMediaStatus.delete(`${layoutId}.xlf`);\n this.emit('layout-current', layoutId);\n // Force timeline recalc on layout change (fingerprint reset)\n this._lastTimelineFingerprint = null;\n this.logUpcomingTimeline();\n }\n\n /**\n * Mark layout as pending (waiting for media)\n * Called by platform when layout needs media downloads\n */\n setPendingLayout(layoutId, requiredMediaIds) {\n this.pendingLayouts.set(layoutId, requiredMediaIds);\n this.emit('layout-pending', layoutId, requiredMediaIds);\n }\n\n /**\n * Clear current layout (for replay)\n * Called by platform when layout ends\n */\n clearCurrentLayout() {\n this.currentLayoutId = null;\n this.emit('layout-cleared');\n }\n\n /**\n * Get the next layout from the pre-calculated schedule queue.\n * Pops the next entry, skipping blacklisted layouts.\n * Returns { layoutId, layoutFile } or null.\n */\n getNextLayout() {\n const entry = this.schedule.popNextFromQueue(\n this._layoutDurations,\n this._queueOptions\n );\n\n if (!entry) {\n // No queue entries — try default\n const defaultFile = this.schedule.schedule?.default;\n if (defaultFile) {\n const layoutId = parseLayoutFile(defaultFile);\n return { layoutId, layoutFile: defaultFile };\n }\n return null;\n }\n\n const layoutId = parseLayoutFile(entry.layoutId);\n\n if (this.isLayoutBlacklisted(layoutId)) {\n // Try next entries (up to queue length) to find a non-blacklisted one\n const { queue } = this.schedule.getScheduleQueue(\n this._layoutDurations,\n this._queueOptions\n );\n for (let i = 0; i < queue.length - 1; i++) {\n const next = this.schedule.popNextFromQueue(\n this._layoutDurations,\n this._queueOptions\n );\n if (next) {\n const nextId = parseLayoutFile(next.layoutId);\n if (!this.isLayoutBlacklisted(nextId)) {\n return { layoutId: nextId, layoutFile: next.layoutId };\n }\n }\n }\n // All blacklisted — return this one anyway to avoid blank screen\n log.warn('All queued layouts are blacklisted, using current entry as fallback');\n }\n\n return { layoutId, layoutFile: entry.layoutId };\n }\n\n /**\n * Peek at the next layout in the schedule queue without advancing.\n * Used by the preload system to know which layout to pre-build.\n * Returns { layoutId, layoutFile } or null if no next layout or same as current.\n */\n peekNextLayout() {\n const entry = this.schedule.peekNextInQueue(\n this._layoutDurations,\n this._queueOptions\n );\n\n if (!entry) return null;\n\n const layoutId = parseLayoutFile(entry.layoutId);\n\n // Don't preload if it's the same as current\n if (layoutId === this.currentLayoutId) {\n // Try the one after that\n const after = this.schedule.peekAfterNext(\n this._layoutDurations,\n this._queueOptions\n );\n if (!after) return null;\n const afterId = parseLayoutFile(after.layoutId);\n if (afterId === this.currentLayoutId || this.isLayoutBlacklisted(afterId)) return null;\n return { layoutId: afterId, layoutFile: after.layoutId };\n }\n\n if (this.isLayoutBlacklisted(layoutId)) return null;\n\n return { layoutId, layoutFile: entry.layoutId };\n }\n\n /**\n * Advance to the next layout in the pre-calculated schedule queue.\n * Called by platform layer when a layout finishes (layoutEnd event).\n * Pops the next entry from the queue and emits layout-prepare-request.\n */\n advanceToNextLayout() {\n // Don't cycle if we're in a layout override (XMR changeLayout/overlayLayout)\n if (this._layoutOverride) {\n log.info('Layout override active, not advancing schedule');\n return;\n }\n\n const next = this.getNextLayout();\n\n // ── Never-stop guarantee ────────────────────────────────────────\n if (!next) {\n if (this.currentLayoutId) {\n log.info(`No layouts in queue, replaying ${this.currentLayoutId} to avoid blank screen`);\n const replayId = this.currentLayoutId;\n this.currentLayoutId = null;\n this._preparingLayoutId = replayId;\n this.emit(E.LAYOUT_PREPARE_REQUEST, replayId);\n } else {\n log.info('No layouts scheduled during advance');\n this.emit(E.NO_LAYOUTS_SCHEDULED);\n }\n return;\n }\n\n const { layoutId, layoutFile } = next;\n const dur = this._layoutDurations.get(layoutFile) || '?';\n\n // Debug: log incoming layout vs timeline overlay top entries\n if (this._lastTimeline && this._lastTimeline.length > 0) {\n const top2 = this._lastTimeline.slice(0, 2).map(e => {\n const t = e.startTime.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });\n return `${e.layoutFile}(${e.duration}s@${t})`;\n });\n log.debug(`[Timeline] Layout transition: entering ${layoutFile} (${dur}s), overlay top: [${top2.join(', ')}]`);\n\n // Warn if the entering layout doesn't match the first timeline entry\n if (this._lastTimeline[0].layoutFile !== layoutFile) {\n log.warn(`[Timeline] Mismatch: entering ${layoutFile} but overlay expects ${this._lastTimeline[0].layoutFile}`);\n }\n } else {\n log.debug(`[Timeline] Layout transition: entering ${layoutFile} (${dur}s), no timeline data`);\n }\n\n // Multi-display sync: if this is a sync event and we have a SyncManager,\n // delegate layout transitions to the sync protocol\n if (this.syncManager && this.schedule.isSyncEvent(layoutFile)) {\n if (this.isSyncLead()) {\n log.info(`[Sync] Lead requesting coordinated layout change: ${layoutId}`);\n // Lead must render the layout itself (not just coordinate followers).\n // Emit layout-prepare-request so the renderer builds it, while\n // requestLayoutChange coordinates the show timing with followers.\n this._preparingLayoutId = layoutId;\n this.emit(E.LAYOUT_PREPARE_REQUEST, layoutId);\n this.syncManager.requestLayoutChange(layoutId).catch(err => {\n log.error('[Sync] Layout change failed:', err);\n });\n return;\n } else if (this.syncManager.transport?.connected) {\n log.info(`[Sync] Follower waiting for lead signal (not advancing independently)`);\n return;\n } else {\n log.warn(`[Sync] Follower: lead unreachable, advancing independently`);\n }\n }\n\n if (layoutId === this.currentLayoutId) {\n log.info(`Next layout ${layoutId} is same as current, triggering replay`);\n this.currentLayoutId = null;\n }\n\n const { queue } = this.schedule.getScheduleQueue(\n this._layoutDurations,\n this._queueOptions\n );\n const pos = this.schedule.getQueuePosition();\n log.info(`Advancing to layout ${layoutId} (queue pos ${pos}/${queue.length})`);\n\n // Set _preparingLayoutId BEFORE emitting to prevent collect() cycles\n // from seeing both currentLayoutId=null and _preparingLayoutId=null\n // and popping another layout from the queue (double-pop race).\n this._preparingLayoutId = layoutId;\n this.emit(E.LAYOUT_PREPARE_REQUEST, layoutId);\n }\n\n /**\n * Go back to the previous layout in the schedule queue (wraps around).\n * Called by platform layer in response to manual navigation (keyboard/remote).\n * Skips sync-manager logic — manual navigation is local only.\n */\n advanceToPreviousLayout() {\n if (this._layoutOverride) {\n log.info('Layout override active, not going back');\n return;\n }\n\n const { queue } = this.schedule.getScheduleQueue(\n this._layoutDurations,\n this._queueOptions\n );\n if (queue.length <= 1) {\n log.info('Single or empty queue, nothing to go back to');\n return;\n }\n\n // Go back 2 positions (current was already popped, so -2 from current pos)\n const entry = this.schedule.rewindQueue(2, this._layoutDurations, this._queueOptions);\n if (!entry) return;\n\n const layoutId = parseLayoutFile(entry.layoutId);\n\n if (layoutId === this.currentLayoutId) {\n log.info('Previous layout is same as current, nothing to go back to');\n return;\n }\n\n log.info(`Going back to layout ${layoutId}`);\n this.emit(E.LAYOUT_PREPARE_REQUEST, layoutId);\n }\n\n /**\n * Notify that a file is ready (called by platform for both layout and media files)\n * Checks if any pending layouts can now be rendered\n */\n notifyMediaReady(fileId, fileType = 'media') {\n log.debug(`File ${fileId} ready (${fileType})`);\n\n // Check if any pending layouts are now complete\n for (const [layoutId, requiredFiles] of this.pendingLayouts.entries()) {\n // Check if this file is needed by this layout\n // For layout files: match layout ID with file ID (layout 78 needs layout/78)\n // For media files: check if fileId is in requiredFiles array\n const isLayoutFile = fileType === 'layout' && layoutId === parseInt(fileId);\n const isRequiredMedia = fileType === 'media' && requiredFiles.includes(fileId);\n\n if (isLayoutFile || isRequiredMedia) {\n log.debug(`${fileType} ${fileId} was needed by pending layout ${layoutId}, checking if ready...`);\n this.emit(E.CHECK_PENDING_LAYOUT, layoutId, requiredFiles);\n }\n }\n }\n\n /**\n * Notify layout status to CMS\n */\n async notifyLayoutStatus(layoutId) {\n try {\n const status = {\n currentLayoutId: layoutId,\n deviceName: this.config?.displayName || '',\n displayName: this.config?.displayName || '',\n lastCommandSuccess: this._lastCommandSuccess ?? true,\n code: this._statusCode,\n lastLayoutChangeTime: this._lastLayoutChangeTime || new Date().toISOString(),\n };\n\n // Add geo-location if available\n if (this.config?.latitude) status.latitude = this.config.latitude;\n if (this.config?.longitude) status.longitude = this.config.longitude;\n\n // Report LAN IP so CMS can tell sync followers where the lead is\n if (this._lanIpAddress) status.lanIpAddress = this._lanIpAddress;\n\n await this.xmds.notifyStatus(status);\n this.emit('status-notified', layoutId);\n } catch (error) {\n log.warn('Failed to notify status:', error);\n this.emit('status-notify-failed', layoutId, error);\n }\n }\n\n /**\n * Report geo location (called by XMR when CMS pushes coordinates)\n * Updates schedule location for geo-fencing and triggers schedule re-evaluation.\n * @param {Object} data - { latitude, longitude }\n */\n reportGeoLocation(data) {\n const lat = parseFloat(data?.latitude);\n const lng = parseFloat(data?.longitude);\n\n if (isNaN(lat) || isNaN(lng)) {\n log.warn('reportGeoLocation: invalid coordinates', data);\n return;\n }\n\n log.info(`Geo location from CMS: ${lat.toFixed(4)}, ${lng.toFixed(4)}`);\n\n if (this.schedule?.setLocation) {\n this.schedule.setLocation(lat, lng);\n }\n\n this.emit('location-updated', { latitude: lat, longitude: lng, source: 'cms' });\n this.checkSchedule();\n }\n\n /**\n * Request geo location using a fallback chain:\n * 1. Browser Geolocation API (GPS / OS-level)\n * 2. Google Geolocation API (if GOOGLE_GEO_API_KEY is configured)\n * 3. IP-based geolocation (free, no key required)\n * @returns {Promise<{latitude: number, longitude: number}|null>}\n */\n async requestGeoLocation() {\n // Return cached location if still fresh (re-resolve every 30 minutes)\n const GEO_CACHE_MS = 30 * 60 * 1000;\n if (this._geoCache && (Date.now() - this._geoCache.ts) < GEO_CACHE_MS) {\n return this._geoCache.location;\n }\n\n // Try browser geolocation (works with GPS or Google API key baked into Chromium).\n // Skip if it already failed — Electron without a Google API key will never succeed.\n if (!this._browserGeoFailed) {\n const browser = await this._tryBrowserGeolocation();\n if (browser) {\n return this._cacheGeo(this._applyLocation(browser.latitude, browser.longitude, 'browser'));\n }\n this._browserGeoFailed = true;\n }\n\n // Try Google Geolocation API if key is configured\n const apiKey = this.config?.googleGeoApiKey;\n if (apiKey) {\n const google = await this._tryGoogleGeolocation(apiKey);\n if (google) {\n return this._cacheGeo(this._applyLocation(google.latitude, google.longitude, 'google-api'));\n }\n }\n\n // Fall back to IP-based geolocation (free, no key)\n const ip = await this._tryIpGeolocation();\n if (ip) {\n return this._cacheGeo(this._applyLocation(ip.latitude, ip.longitude, 'ip-geolocation'));\n }\n\n log.warn('All geolocation methods failed');\n return null;\n }\n\n /** Cache a resolved geolocation result. @private */\n _cacheGeo(location) {\n this._geoCache = { location, ts: Date.now() };\n return location;\n }\n\n /**\n * Extract config values from CMS display tags using key|value convention.\n * Tags like \"geoApiKey|AIzaSy...\" are parsed and applied to player config.\n * @param {string[]} tags - Array of tag strings from RegisterDisplay\n * @private\n */\n _applyTagConfig(tags) {\n if (!Array.isArray(tags) || tags.length === 0) return;\n\n const TAG_CONFIG_MAP = {\n 'geoApiKey': 'googleGeoApiKey',\n };\n\n for (const tag of tags) {\n const pipeIdx = tag.indexOf('|');\n if (pipeIdx === -1) continue;\n\n const key = tag.substring(0, pipeIdx);\n const value = tag.substring(pipeIdx + 1);\n const configKey = TAG_CONFIG_MAP[key];\n\n if (configKey && value && this.config) {\n log.info(`Config from CMS tag: ${key} → ${configKey}`);\n this.config[configKey] = value;\n }\n }\n }\n\n _applyLocation(lat, lng, source) {\n log.info(`Geolocation (${source}): ${lat.toFixed(4)}, ${lng.toFixed(4)}`);\n\n if (this.schedule?.setLocation) {\n this.schedule.setLocation(lat, lng);\n }\n\n this.emit('location-updated', { latitude: lat, longitude: lng, source });\n this.checkSchedule();\n\n return { latitude: lat, longitude: lng };\n }\n\n /**\n * Try the browser Geolocation API (navigator.geolocation).\n * @returns {Promise<{latitude: number, longitude: number}|null>}\n * @private\n */\n async _tryBrowserGeolocation() {\n if (typeof navigator === 'undefined' || !navigator.geolocation) return null;\n\n try {\n const position = await new Promise((resolve, reject) => {\n navigator.geolocation.getCurrentPosition(resolve, reject, {\n timeout: 10000,\n maximumAge: 300000, // 5 minutes\n enableHighAccuracy: false\n });\n });\n return { latitude: position.coords.latitude, longitude: position.coords.longitude };\n } catch (error) {\n log.warn('Browser geolocation failed:', error?.message || error);\n return null;\n }\n }\n\n /**\n * Try Google Geolocation API (direct HTTPS POST, bypasses Chromium's built-in service).\n * @param {string} apiKey - Google API key\n * @returns {Promise<{latitude: number, longitude: number}|null>}\n * @private\n */\n async _tryGoogleGeolocation(apiKey) {\n try {\n const res = await fetch(\n `https://www.googleapis.com/geolocation/v1/geolocate?key=${apiKey}`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ considerIp: true }),\n signal: AbortSignal.timeout(5000)\n }\n );\n if (!res.ok) {\n log.warn(`Google Geolocation API returned ${res.status}`);\n return null;\n }\n const data = await res.json();\n if (data.location?.lat != null && data.location?.lng != null) {\n return { latitude: data.location.lat, longitude: data.location.lng };\n }\n return null;\n } catch (error) {\n log.warn('Google Geolocation API failed:', error?.message || error);\n return null;\n }\n }\n\n /**\n * Try IP-based geolocation using free HTTPS providers (no API key needed).\n * Tries ipapi.co first, then freeipapi.com as fallback.\n * @returns {Promise<{latitude: number, longitude: number}|null>}\n * @private\n */\n async _tryIpGeolocation() {\n const providers = [\n {\n url: 'https://ipapi.co/json/',\n parse: (data) => data.latitude != null && data.longitude != null\n ? { latitude: data.latitude, longitude: data.longitude }\n : null\n },\n {\n url: 'https://freeipapi.com/api/json',\n parse: (data) => data.latitude != null && data.longitude != null\n ? { latitude: data.latitude, longitude: data.longitude }\n : null\n }\n ];\n\n for (const provider of providers) {\n try {\n const res = await fetch(provider.url, { signal: AbortSignal.timeout(5000) });\n if (!res.ok) continue;\n const data = await res.json();\n const location = provider.parse(data);\n if (location) return location;\n } catch (error) {\n log.warn(`IP geolocation (${provider.url}) failed:`, error?.message || error);\n }\n }\n return null;\n }\n\n /**\n * Re-evaluate current schedule and switch layouts if needed.\n * Called after location updates or other schedule-affecting changes.\n */\n checkSchedule() {\n const layoutFiles = this.schedule.getCurrentLayouts();\n this.emit(E.LAYOUTS_SCHEDULED, layoutFiles);\n this._evaluateAndSwitchLayout(layoutFiles, '');\n }\n\n /**\n * Capture screenshot (called by XMR wrapper)\n * Emits event for platform layer to handle\n */\n async captureScreenshot() {\n log.info('Screenshot requested');\n this.emit(E.SCREENSHOT_REQUEST);\n }\n\n /**\n * Change to a specific layout (called by XMR wrapper)\n * Tracks override state so revertToSchedule() can undo it.\n */\n async changeLayout(layoutId, options) {\n log.info('Layout change requested via XMR:', layoutId);\n const id = parseInt(layoutId, 10);\n const duration = options?.duration || 0;\n const changeMode = options?.changeMode || 'replace';\n this._layoutOverride = { layoutId: id, type: 'change', duration, changeMode };\n this.currentLayoutId = null; // Force re-render\n this.emit(E.LAYOUT_PREPARE_REQUEST, id);\n this._scheduleAutoRevert(id, duration, 'Layout override');\n }\n\n /**\n * Push an overlay layout on top of current content (called by XMR wrapper)\n * @param {number|string} layoutId - Layout to overlay\n */\n async overlayLayout(layoutId, options) {\n log.info('Overlay layout requested via XMR:', layoutId);\n const id = parseInt(layoutId, 10);\n const duration = options?.duration || 0;\n this._layoutOverride = { layoutId: id, type: 'overlay', duration };\n this.emit(E.OVERLAY_LAYOUT_REQUEST, id);\n this._scheduleAutoRevert(id, duration, 'Overlay');\n }\n\n /**\n * Revert to scheduled content after changeLayout/overlayLayout override\n */\n async revertToSchedule() {\n log.info('Reverting to scheduled content');\n this._layoutOverride = null;\n this.currentLayoutId = null;\n this.emit(E.REVERT_TO_SCHEDULE);\n\n // Re-evaluate schedule to get the right layout\n const layoutFiles = this.schedule.getCurrentLayouts();\n if (layoutFiles.length > 0) {\n const layoutFile = layoutFiles[0];\n const layoutId = parseLayoutFile(layoutFile);\n this.emit(E.LAYOUT_PREPARE_REQUEST, layoutId);\n } else {\n this.emit(E.NO_LAYOUTS_SCHEDULED);\n }\n }\n\n /**\n * Purge all cached content and re-download (called by XMR wrapper)\n */\n async purgeAll() {\n log.info('Purge all cache requested via XMR');\n this._lastCheckRf = null;\n this._lastCheckSchedule = null;\n this.emit(E.PURGE_ALL_REQUEST);\n // Trigger immediate re-collection after purge\n return this.collectNow();\n }\n\n /**\n * Execute a command (HTTP only in browser context)\n * @param {string} commandCode - The command code from CMS\n * @param {Object} commands - Commands map from display settings\n */\n async executeCommand(commandCode, commands) {\n log.info('Execute command requested:', commandCode);\n\n if (!commands || !commands[commandCode]) {\n log.warn('Unknown command code:', commandCode);\n this._lastCommandSuccess = false;\n this.emit(E.COMMAND_RESULT, { code: commandCode, success: false, reason: 'Unknown command' });\n return;\n }\n\n const command = commands[commandCode];\n const commandString = command.commandString || command.value || '';\n\n // Only HTTP commands are possible in a browser\n if (commandString.startsWith('http|')) {\n const parts = commandString.split('|');\n const url = parts[1];\n const contentType = parts[2] || 'application/json';\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': contentType },\n signal: AbortSignal.timeout(10000),\n });\n const success = response.ok;\n this._lastCommandSuccess = success;\n log.info(`HTTP command ${commandCode} result: ${response.status}`);\n this.emit(E.COMMAND_RESULT, { code: commandCode, success, status: response.status });\n } catch (error) {\n this._lastCommandSuccess = false;\n log.error(`HTTP command ${commandCode} failed:`, error);\n this.emit(E.COMMAND_RESULT, { code: commandCode, success: false, reason: error.message });\n }\n } else {\n // Emit event for platform layer (Electron/Chromium) to handle native commands\n // (shell, RS232, Android intent, etc.)\n log.info('Delegating non-HTTP command to platform layer:', commandCode);\n this.emit(E.EXECUTE_NATIVE_COMMAND, { code: commandCode, commandString });\n }\n }\n\n /**\n * Trigger a webhook action (called by XMR wrapper)\n * @param {string} triggerCode - The trigger code to fire\n */\n triggerWebhook(triggerCode) {\n log.info('Webhook trigger from XMR:', triggerCode);\n this.handleTrigger(triggerCode);\n }\n\n /**\n * Force refresh of data connectors (called by XMR wrapper)\n */\n refreshDataConnectors() {\n log.info('Data connector refresh requested via XMR');\n this.dataConnectorManager.refreshAll();\n this.emit('data-connectors-refreshed');\n }\n\n /**\n * Submit media inventory to CMS\n * Reports which files are cached and complete.\n * @param {Array} files - List of files from RequiredFiles\n */\n async submitMediaInventory(files) {\n if (!files || files.length === 0) return;\n\n try {\n // Build inventory XML: <files><file type=\"media\" id=\"1\" complete=\"1\" md5=\"abc\" lastChecked=\"123\"/></files>\n // complete: use file.complete if set by caller (cache layer), default to \"1\"\n const now = Math.floor(Date.now() / 1000);\n const fileEntries = files\n .filter(f => ['media', 'layout', 'resource', 'dependency', 'widget'].includes(f.type))\n .map(f => {\n const complete = f.complete !== undefined ? (f.complete ? '1' : '0') : '1';\n const fileType = f.fileType ? ` fileType=\"${f.fileType}\"` : '';\n return `<file type=\"${f.type}\" id=\"${f.id}\" complete=\"${complete}\" md5=\"${f.md5 || ''}\" lastChecked=\"${now}\"${fileType}/>`;\n })\n .join('');\n const inventoryXml = `<files>${fileEntries}</files>`;\n\n await this.xmds.mediaInventory(inventoryXml);\n log.info(`Media inventory submitted: ${files.length} files`);\n this.emit('media-inventory-submitted', files.length);\n } catch (error) {\n log.warn('MediaInventory submission failed:', error);\n }\n }\n\n /**\n * BlackList a media file (report broken media to CMS)\n * @param {string|number} mediaId - The media ID\n * @param {string} type - File type ('media' or 'layout')\n * @param {string} reason - Reason for blacklisting\n */\n async blackList(mediaId, type, reason) {\n try {\n await this.xmds.blackList(mediaId, type, reason);\n this.emit('media-blacklisted', { mediaId, type, reason });\n } catch (error) {\n log.warn('BlackList failed:', error);\n }\n }\n\n /**\n * Report a layout render failure. After N consecutive failures\n * (default 3), the layout is blacklisted and skipped in schedule\n * evaluation. Blacklisted layouts are reported to CMS via the\n * BlackList XMDS method.\n *\n * @param {number} layoutId - The layout that failed\n * @param {string} reason - Human-readable failure description\n */\n reportLayoutFailure(layoutId, reason) {\n const id = Number(layoutId);\n this._statusCode = 3; // Error — layout failed to render\n\n const { blacklisted, failures } = this._layoutBlacklist.recordFailure(id, reason);\n if (blacklisted && failures === 3) {\n // Newly blacklisted (threshold just reached)\n this.emit('layout-blacklisted', { layoutId: id, reason, failures });\n this.blackList(id, 'layout', reason);\n }\n }\n\n reportLayoutSuccess(layoutId) {\n const wasBlacklisted = this._layoutBlacklist.recordSuccess(Number(layoutId));\n if (wasBlacklisted) {\n this.emit('layout-unblacklisted', { layoutId: Number(layoutId) });\n }\n }\n\n isLayoutBlacklisted(layoutId) {\n return this._layoutBlacklist.isBlacklisted(layoutId);\n }\n\n getBlacklistedLayouts() {\n return this._layoutBlacklist.getBlacklistedIds();\n }\n\n resetBlacklist() {\n if (this._layoutBlacklist.reset() > 0) {\n this.emit('blacklist-reset');\n }\n }\n\n /**\n * Check if currently in a layout override (from XMR changeLayout/overlayLayout)\n */\n isLayoutOverridden() {\n return this._layoutOverride !== null;\n }\n\n /**\n * Handle interactive trigger (from IC or touch events)\n * Looks up matching action in schedule and executes it\n * @param {string} triggerCode - The trigger code from the IC request\n */\n handleTrigger(triggerCode) {\n const action = this.schedule.findActionByTrigger(triggerCode);\n if (!action) {\n log.debug('No scheduled action matches trigger:', triggerCode);\n return;\n }\n\n log.info(`Action triggered: ${action.actionType} (trigger: ${triggerCode})`);\n\n switch (action.actionType) {\n case 'navLayout':\n case 'navigateToLayout':\n if (action.layoutCode) {\n this.changeLayout(action.layoutCode);\n }\n break;\n case 'navWidget':\n case 'navigateToWidget':\n this.emit(E.NAVIGATE_TO_WIDGET, action);\n break;\n case 'command':\n this.emit('execute-command', action.commandCode);\n break;\n default:\n log.warn('Unknown action type:', action.actionType);\n }\n }\n\n /**\n * Update data connectors from current schedule\n * Reconfigures and restarts polling when schedule changes.\n */\n updateDataConnectors() {\n const connectors = this.schedule.getDataConnectors();\n\n if (connectors.length > 0) {\n log.info(`Configuring ${connectors.length} data connector(s)`);\n }\n\n this.dataConnectorManager.setConnectors(connectors);\n\n if (connectors.length > 0) {\n this.dataConnectorManager.startPolling();\n this.emit('data-connectors-started', connectors.length);\n }\n }\n\n /**\n * Process scheduled commands from the CMS schedule.\n * Checks for command events whose scheduled date has arrived and executes them.\n * Each command is only executed once (tracked by code+date key in _executedCommands).\n */\n _processScheduledCommands() {\n if (!this.schedule?.getCommands) return;\n\n const commands = this.schedule.getCommands();\n if (commands.length === 0) return;\n\n const now = new Date();\n\n for (const command of commands) {\n if (!command.code || !command.date) continue;\n\n // Unique key to track execution (same command can be scheduled multiple times)\n const commandKey = `${command.code}|${command.date}`;\n\n // Skip already executed commands\n if (this._executedCommands.has(commandKey)) continue;\n\n // Check if the command's scheduled time has arrived\n const commandDate = new Date(command.date);\n if (isNaN(commandDate.getTime())) {\n log.warn('Scheduled command has invalid date:', command.date);\n continue;\n }\n\n if (now >= commandDate) {\n log.info(`Executing scheduled command: ${command.code} (scheduled: ${command.date})`);\n this._executedCommands.add(commandKey);\n\n // Handle built-in commands directly\n if (command.code === 'collectNow') {\n // Trigger immediate collection on next tick (avoid re-entrance)\n setTimeout(() => this.collectNow().catch(e => log.error('collectNow command failed:', e)), 0);\n } else {\n // Emit event for platform layer to handle (reboot, restart, etc.)\n this.emit(E.SCHEDULED_COMMAND, command);\n }\n }\n }\n }\n\n /**\n * Fetch weather data from CMS and pass to schedule for criteria evaluation.\n * Non-blocking: weather fetch failure doesn't prevent schedule evaluation.\n */\n async _fetchWeatherData() {\n if (!this.xmds?.getWeather || !this.schedule?.setWeatherData) return;\n\n try {\n const weatherJson = await this.xmds.getWeather();\n const weatherData = typeof weatherJson === 'string' ? JSON.parse(weatherJson) : weatherJson;\n this.schedule.setWeatherData(weatherData);\n log.info('Weather data updated:', Object.keys(weatherData).join(', '));\n } catch (e) {\n log.warn('GetWeather failed (non-critical):', e?.message || e);\n }\n }\n\n /**\n * Get the DataConnectorManager instance\n * Used by platform layer to serve data to widgets via IC /realtime\n * @returns {DataConnectorManager}\n */\n getDataConnectorManager() {\n return this.dataConnectorManager;\n }\n\n /**\n * Set the SyncManager instance for multi-display coordination.\n * Called by platform layer after RegisterDisplay returns syncConfig.\n *\n * @param {SyncManager} syncManager - SyncManager instance\n */\n setSyncManager(syncManager) {\n this.syncManager = syncManager;\n log.info('SyncManager attached:', syncManager.isLead ? 'LEAD' : 'FOLLOWER');\n }\n\n /**\n * Check if this display is part of a sync group\n * @returns {boolean}\n */\n isInSyncGroup() {\n return this.syncConfig !== null;\n }\n\n /**\n * Check if this display is the sync group leader\n * @returns {boolean}\n */\n isSyncLead() {\n return this.syncConfig?.isLead === true;\n }\n\n /**\n * Get sync configuration\n * @returns {Object|null} { syncGroup, syncPublisherPort, syncSwitchDelay, syncVideoPauseDelay, isLead }\n */\n getSyncConfig() {\n return this.syncConfig;\n }\n\n // ── Timeline (offline schedule prediction) ─────────────────────────\n\n // Duration flow: renderer is the single source of truth.\n // 1. Renderer calculates duration from widgets → emits layoutDurationUpdated\n // 2. recordLayoutDuration stores it (with final flag) → persisted to IDB\n // 3. On restart, IDB restores correct durations → queue uses them immediately\n // No XLF parsing needed in core — the renderer already does this.\n\n /**\n * Calculate and log the upcoming playback timeline (next 2 hours).\n * Emits 'timeline-updated' with the full timeline array.\n */\n logUpcomingTimeline() {\n if (!this.schedule.getLayoutsAtTime) return; // Schedule doesn't support time queries\n\n // Fingerprint inputs: schedule CRC + sorted durations + current layout + media status.\n // When unchanged, re-emit the cached timeline — avoids time drift from\n // re-simulating with a new Date.now() anchor on every collection cycle.\n const durationEntries = [...this._layoutDurations.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([k, v]) => `${k}:${v}`)\n .join('|');\n const mediaStatusEntries = [...this._layoutMediaStatus.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([k, v]) => `${k}:${v.ready}:${v.missingKey}`)\n .join('|');\n const pendingEntries = [...this.pendingLayouts.keys()].sort().join(',');\n const queuePos = this.schedule.getQueuePosition() || 0;\n const fingerprint = `${this._lastCheckSchedule}|${durationEntries}|${this.currentLayoutId}|${queuePos}|${mediaStatusEntries}|${pendingEntries}`;\n\n if (fingerprint === this._lastTimelineFingerprint && this._lastTimeline) {\n this.emit(E.TIMELINE_UPDATED, this._lastTimeline);\n return;\n }\n\n const { queue } = this.schedule.getScheduleQueue(this._layoutDurations, this._queueOptions);\n const timeline = calculateTimeline(queue, this.schedule.getQueuePosition(), {\n currentLayoutStartedAt: this._lastLayoutChangeTime ? new Date(this._lastLayoutChangeTime) : null,\n defaultLayout: this.schedule.schedule?.default || null,\n durations: this._layoutDurations,\n });\n if (timeline.length === 0) return;\n\n // Annotate entries with missingMedia from pendingLayouts (high authority)\n // and _layoutMediaStatus (proactive check, lower authority)\n for (const entry of timeline) {\n const layoutId = parseInt(entry.layoutFile.replace('.xlf', ''), 10);\n const pendingMedia = this.pendingLayouts.get(layoutId);\n if (pendingMedia && pendingMedia.length > 0) {\n // pendingLayouts takes priority — definitively missing\n entry.missingMedia = pendingMedia.map(String);\n } else {\n const status = this._layoutMediaStatus.get(entry.layoutFile);\n if (status && !status.ready && status.missing.length > 0) {\n entry.missingMedia = status.missing.map(String);\n }\n }\n }\n\n this._lastTimelineFingerprint = fingerprint;\n this._lastTimeline = timeline;\n\n const lines = timeline.slice(0, 20).map(e => {\n const s = e.startTime.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });\n const end = e.endTime.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });\n const missingTag = e.missingMedia ? ` [MISSING: ${e.missingMedia.length} files]` : '';\n return ` ${s}-${end} Layout ${e.layoutFile} (${e.duration}s)${e.isDefault ? ' [default]' : ''}${missingTag}`;\n });\n\n // Log warnings for layouts with missing media\n for (const entry of timeline) {\n if (entry.missingMedia) {\n log.warn(`[Timeline] Layout ${entry.layoutFile}: ${entry.missingMedia.length} files missing`);\n }\n }\n\n log.info(`[Timeline] Next ${timeline.length} plays:\\n${lines.join('\\n')}`);\n this.emit(E.TIMELINE_UPDATED, timeline);\n }\n\n /**\n * Set media readiness status for a layout (proactive async check from platform layer).\n * No-ops if value is unchanged to avoid fingerprint churn.\n * @param {string} layoutFile - Layout file (e.g. '100.xlf')\n * @param {boolean} ready - Whether all media is cached\n * @param {string[]} [missing] - Array of missing media IDs/filenames\n */\n setLayoutMediaStatus(layoutFile, ready, missing = []) {\n const existing = this._layoutMediaStatus.get(layoutFile);\n const missingKey = missing.slice().sort().join(',');\n if (existing && existing.ready === ready && existing.missingKey === missingKey) return;\n\n this._layoutMediaStatus.set(layoutFile, { ready, missing, missingKey });\n // Invalidate fingerprint to force timeline recalculation\n this._lastTimelineFingerprint = null;\n }\n\n /**\n * Record/correct a layout's actual duration (e.g., from video loadedmetadata).\n * Updates the durations map and re-logs the timeline if it changed.\n * @param {string} file - Layout file or layout ID string\n * @param {number} duration - Actual duration in seconds\n * @param {boolean} [final=false] - True when all videos in the layout have been probed\n */\n recordLayoutDuration(file, duration, final = false) {\n // Normalize: store under both \"492\" and \"492.xlf\" forms so that\n // calculateTimeline (which looks up \"492.xlf\") and other callers\n // (which use \"492\") always find the corrected value.\n const id = String(file).replace('.xlf', '');\n const xlfKey = id + '.xlf';\n\n // Definitive duration — never overwrite once set\n if (this._finalDurations.has(id)) return;\n\n const prev = this._layoutDurations.get(file);\n if (prev === duration && !final) return; // No change\n\n this._layoutDurations.set(id, duration);\n this._layoutDurations.set(xlfKey, duration);\n\n if (final) {\n this._finalDurations.add(id);\n this._finalDurations.add(xlfKey);\n }\n\n log.debug(`[Timeline] Duration corrected: layout ${file} ${prev || '?'}s → ${duration}s${final ? ' (final)' : ''}`);\n\n // Invalidate the cached schedule queue so the next getScheduleQueue() call\n // rebuilds with corrected durations (affects queue log and period calculation).\n this.schedule.invalidateQueue();\n\n // Debounce timeline recalculation — multiple video loadedmetadata events\n // can fire within milliseconds; collapse them into one recalculation.\n if (this._timelineRecalcTimer) clearTimeout(this._timelineRecalcTimer);\n this._timelineRecalcTimer = setTimeout(() => {\n this._timelineRecalcTimer = null;\n this.logUpcomingTimeline();\n this._offlineSave('durations', [...this._layoutDurations.entries()]);\n this._offlineSave('finalDurations', [...this._finalDurations]);\n this._offlineSave('durationsVersion', 2);\n }, 500);\n }\n\n /**\n * Cleanup\n */\n cleanup() {\n if (this.collectionInterval) {\n clearInterval(this.collectionInterval);\n this.collectionInterval = null;\n }\n\n if (this._faultReportingInterval) {\n clearInterval(this._faultReportingInterval);\n this._faultReportingInterval = null;\n }\n\n if (this._timelineRecalcTimer) {\n clearTimeout(this._timelineRecalcTimer);\n this._timelineRecalcTimer = null;\n }\n\n if (this.xmr) {\n this.xmr.stop();\n this.xmr = null;\n }\n\n // Stop multi-display sync\n if (this.syncManager) {\n this.syncManager.stop();\n this.syncManager = null;\n }\n\n // Stop data connector polling\n this.dataConnectorManager.cleanup();\n\n // Emit cleanup-complete before removing listeners\n this.emit('cleanup-complete');\n this.removeAllListeners();\n }\n\n /**\n * Get current layout ID\n */\n getCurrentLayoutId() {\n return this.currentLayoutId;\n }\n\n /**\n * Get known duration for a layout (from video metadata or XLF parse).\n * @param {number|string} layoutId\n * @returns {number|undefined}\n */\n getLayoutDuration(layoutId) {\n const id = String(layoutId);\n return this._layoutDurations.get(`${id}.xlf`) || this._layoutDurations.get(id);\n }\n\n /**\n * Check if collecting\n */\n isCollecting() {\n return this.collecting;\n }\n\n /**\n * Get pending layouts\n */\n getPendingLayouts() {\n return Array.from(this.pendingLayouts.keys());\n }\n\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n// @xiboplayer/core - Player core orchestration\nimport pkg from '../package.json' with { type: 'json' };\nexport const VERSION = pkg.version;\nexport { PlayerCore } from './player-core.js';\nexport { DataConnectorManager } from './data-connectors.js';\nexport { CORE_EVENTS } from './events.js';\n"],"mappings":"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"}