@xiboplayer/pwa 0.7.20 → 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.
- package/dist/assets/{main-C8f1ieZL.js → main-oacre7st.js} +4 -4
- package/dist/assets/{main-C8f1ieZL.js.map → main-oacre7st.js.map} +1 -1
- package/dist/assets/main-vwJkNw4Y.js +3 -0
- package/dist/assets/{main-BqJjhM5z.js.map → main-vwJkNw4Y.js.map} +1 -1
- package/dist/assets/{setup-Dpox3R4b.js → setup-B4gZX38p.js} +2 -2
- package/dist/assets/{setup-Dpox3R4b.js.map → setup-B4gZX38p.js.map} +1 -1
- package/dist/assets/{src-CIERor10.js → src-B_BNICay.js} +2 -2
- package/dist/assets/{src-CIERor10.js.map → src-B_BNICay.js.map} +1 -1
- package/dist/assets/{src-GjeUdI7b.js → src-Bjt9ooXK.js} +2 -2
- package/dist/assets/{src-GjeUdI7b.js.map → src-Bjt9ooXK.js.map} +1 -1
- package/dist/assets/{src-Bzheh5E2.js → src-BtVLiVYZ.js} +2 -2
- package/dist/assets/{src-Bzheh5E2.js.map → src-BtVLiVYZ.js.map} +1 -1
- package/dist/assets/{src-Cvcg0wpA.js → src-CKpVxGpH.js} +2 -2
- package/dist/assets/{src-Cvcg0wpA.js.map → src-CKpVxGpH.js.map} +1 -1
- package/dist/assets/{src-DzgDstdA.js → src-CROvYSP8.js} +2 -2
- package/dist/assets/{src-DzgDstdA.js.map → src-CROvYSP8.js.map} +1 -1
- package/dist/assets/{src-e2yErlKg.js → src-C_Lx4lXp.js} +2 -2
- package/dist/assets/{src-e2yErlKg.js.map → src-C_Lx4lXp.js.map} +1 -1
- package/dist/assets/{src-B0l-hkrA.js → src-Cx3tXAAu.js} +2 -2
- package/dist/assets/{src-B0l-hkrA.js.map → src-Cx3tXAAu.js.map} +1 -1
- package/dist/assets/{src-BLwxK97o.js → src-DAB0dqGG.js} +2 -2
- package/dist/assets/{src-BLwxK97o.js.map → src-DAB0dqGG.js.map} +1 -1
- package/dist/assets/{src-DTJp4-F0.js → src-WDu491CE.js} +2 -2
- package/dist/assets/{src-DTJp4-F0.js.map → src-WDu491CE.js.map} +1 -1
- package/dist/assets/{src-D_pit2UZ.js → src-cUopH0nN.js} +2 -2
- package/dist/assets/{src-D_pit2UZ.js.map → src-cUopH0nN.js.map} +1 -1
- package/dist/assets/{sync-manager-B6kqa4bv.js → sync-manager-8Z-qwkod.js} +2 -2
- package/dist/assets/{sync-manager-B6kqa4bv.js.map → sync-manager-8Z-qwkod.js.map} +1 -1
- package/dist/index.html +1 -1
- package/dist/setup.html +3 -3
- package/dist/sw-pwa.js +2 -2
- package/dist/sw-pwa.js.map +1 -1
- package/package.json +13 -13
- package/dist/assets/main-BqJjhM5z.js +0 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"src-e2yErlKg.js","names":["pkg"],"sources":["../../../settings/package.json","../../../settings/src/settings.js","../../../settings/src/index.js"],"sourcesContent":["{\n \"name\": \"@xiboplayer/settings\",\n \"version\": \"0.7.20\",\n \"description\": \"CMS settings management and application for xiboplayer\",\n \"type\": \"module\",\n \"main\": \"./src/index.js\",\n \"types\": \"./src/index.d.ts\",\n \"exports\": {\n \".\": \"./src/index.js\",\n \"./manager\": \"./src/settings.js\"\n },\n \"scripts\": {\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"test:coverage\": \"vitest run --coverage\"\n },\n \"dependencies\": {\n \"@xiboplayer/utils\": \"workspace:*\"\n },\n \"devDependencies\": {\n \"jsdom\": \"^29.0.2\",\n \"vitest\": \"^4.1.2\"\n },\n \"keywords\": [\n \"xibo\",\n \"digital-signage\",\n \"settings\",\n \"configuration\",\n \"display-settings\"\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/settings\"\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 * DisplaySettings - CMS display settings management\n *\n * Parses and applies configuration from RegisterDisplay response.\n * Based on upstream electron-player implementation.\n *\n * Architecture:\n * ┌─────────────────────────────────────────────────────┐\n * │ PlayerCore │\n * │ - Receives RegisterDisplay response │\n * │ - Passes to DisplaySettings.applySettings() │\n * └─────────────────────────────────────────────────────┘\n * ↓\n * ┌─────────────────────────────────────────────────────┐\n * │ DisplaySettings (this module) │\n * │ - Parse all CMS settings │\n * │ - Validate and normalize values │\n * │ - Apply collection interval │\n * │ - Check download windows │\n * │ - Handle screenshot requests │\n * │ - Emit events on changes │\n * └─────────────────────────────────────────────────────┘\n * ↓\n * ┌─────────────────────────────────────────────────────┐\n * │ Platform Layer (PWA/Electron/Mobile) │\n * │ - Listen for setting change events │\n * │ - Update UI with display name │\n * │ - Handle screenshot requests │\n * │ - Respect download windows │\n * └─────────────────────────────────────────────────────┘\n *\n * Usage:\n * const settings = new DisplaySettings();\n * settings.applySettings(regResult.settings);\n *\n * // Get settings\n * const collectInterval = settings.getCollectInterval();\n * const canDownload = settings.isInDownloadWindow();\n *\n * // Listen for changes\n * settings.on('interval-changed', (newInterval) => { ... });\n */\n\nimport { EventEmitter, createLogger } from '@xiboplayer/utils';\n\nconst log = createLogger('DisplaySettings');\n\nexport class DisplaySettings extends EventEmitter {\n constructor() {\n super();\n\n // Current settings (with defaults)\n this.settings = {\n // Collection\n collectInterval: 300, // seconds (5 minutes default)\n\n // Display info\n displayName: 'Unknown Display',\n sizeX: 1920,\n sizeY: 1080,\n\n // Stats\n statsEnabled: false,\n aggregationLevel: 'Individual', // or 'Aggregate'\n\n // Logging\n logLevel: 'error', // 'error', 'audit', 'info', 'debug'\n\n // XMR\n xmrNetworkAddress: null,\n xmrWebSocketAddress: null,\n xmrCmsKey: null,\n\n // Features\n preventSleep: true,\n embeddedServerPort: 9696,\n screenshotInterval: 120, // seconds\n\n // Download windows\n downloadStartWindow: null,\n downloadEndWindow: null,\n\n // License\n licenceCode: null,\n\n // SSP (ad space)\n isSspEnabled: false,\n };\n }\n\n /**\n * Apply settings from RegisterDisplay response\n * @param {Object} settings - Raw settings from CMS\n * @returns {Object} Applied settings with changes\n */\n applySettings(settings) {\n if (!settings) {\n log.warn('No settings provided');\n return { changed: [], settings: this.settings };\n }\n\n const changes = [];\n const oldInterval = this.settings.collectInterval;\n\n // Parse all settings with defaults\n // Handle both lowercase and CamelCase (uppercase first letter)\n this.settings.collectInterval = this.parseCollectInterval(settings.collectInterval || settings.CollectInterval);\n this.settings.displayName = settings.displayName || settings.DisplayName || this.settings.displayName;\n this.settings.sizeX = parseInt(settings.sizeX || settings.SizeX || this.settings.sizeX);\n this.settings.sizeY = parseInt(settings.sizeY || settings.SizeY || this.settings.sizeY);\n\n // Stats\n this.settings.statsEnabled = this.parseBoolean(settings.statsEnabled || settings.StatsEnabled);\n this.settings.aggregationLevel = settings.aggregationLevel || settings.AggregationLevel || this.settings.aggregationLevel;\n\n // Logging\n this.settings.logLevel = settings.logLevel || settings.LogLevel || this.settings.logLevel;\n\n // XMR\n this.settings.xmrNetworkAddress = settings.xmrNetworkAddress || settings.XmrNetworkAddress || this.settings.xmrNetworkAddress;\n this.settings.xmrWebSocketAddress = settings.xmrWebSocketAddress || settings.XmrWebSocketAddress || this.settings.xmrWebSocketAddress;\n this.settings.xmrCmsKey = settings.xmrCmsKey || settings.XmrCmsKey || this.settings.xmrCmsKey;\n\n // Features\n this.settings.preventSleep = this.parseBoolean(settings.preventSleep || settings.PreventSleep, true);\n this.settings.embeddedServerPort = parseInt(settings.embeddedServerPort || settings.EmbeddedServerPort || this.settings.embeddedServerPort);\n this.settings.screenshotInterval = parseInt(settings.screenshotInterval || settings.ScreenshotInterval || this.settings.screenshotInterval);\n\n // Download windows\n this.settings.downloadStartWindow = settings.downloadStartWindow || settings.DownloadStartWindow || this.settings.downloadStartWindow;\n this.settings.downloadEndWindow = settings.downloadEndWindow || settings.DownloadEndWindow || this.settings.downloadEndWindow;\n\n // License\n this.settings.licenceCode = settings.licenceCode || settings.LicenceCode || this.settings.licenceCode;\n\n // SSP\n this.settings.isSspEnabled = this.parseBoolean(settings.isAdspaceEnabled || settings.IsAdspaceEnabled);\n\n // Detect changes\n if (oldInterval !== this.settings.collectInterval) {\n changes.push('collectInterval');\n this.emit('interval-changed', this.settings.collectInterval);\n }\n\n // Emit generic settings-applied event\n this.emit('settings-applied', this.settings, changes);\n\n log.info('Applied settings:', {\n collectInterval: this.settings.collectInterval,\n displayName: this.settings.displayName,\n statsEnabled: this.settings.statsEnabled,\n changes\n });\n\n return { changed: changes, settings: this.settings };\n }\n\n /**\n * Parse collection interval (seconds)\n * @param {*} value - Raw value from CMS\n * @returns {number} Collection interval in seconds\n */\n parseCollectInterval(value) {\n const interval = parseInt(value, 10);\n\n // Validate range (minimum 60s, maximum 86400s = 24h)\n if (isNaN(interval) || interval < 60) {\n return 300; // 5 minutes default\n }\n\n if (interval > 86400) {\n return 86400; // 24 hours max\n }\n\n return interval;\n }\n\n /**\n * Parse boolean setting\n * @param {*} value - Raw value from CMS (string '1' or '0', or boolean)\n * @param {boolean} defaultValue - Default if not set\n * @returns {boolean}\n */\n parseBoolean(value, defaultValue = false) {\n if (value === true || value === false) {\n return value;\n }\n\n if (value === '1' || value === 1) {\n return true;\n }\n\n if (value === '0' || value === 0) {\n return false;\n }\n\n return defaultValue;\n }\n\n /**\n * Get collection interval in seconds\n * @returns {number}\n */\n getCollectInterval() {\n return this.settings.collectInterval;\n }\n\n /**\n * Get display name\n * @returns {string}\n */\n getDisplayName() {\n return this.settings.displayName;\n }\n\n /**\n * Get display size\n * @returns {{ width: number, height: number }}\n */\n getDisplaySize() {\n return {\n width: this.settings.sizeX,\n height: this.settings.sizeY\n };\n }\n\n /**\n * Check if stats are enabled\n * @returns {boolean}\n */\n isStatsEnabled() {\n return this.settings.statsEnabled;\n }\n\n /**\n * Get all settings\n * @returns {Object}\n */\n getAllSettings() {\n return { ...this.settings };\n }\n\n /**\n * Get a specific setting by key\n * @param {string} key - Setting key\n * @param {*} defaultValue - Default value if not set\n * @returns {*}\n */\n getSetting(key, defaultValue = null) {\n return this.settings[key] !== undefined ? this.settings[key] : defaultValue;\n }\n\n /**\n * Check if current time is within download window\n * @returns {boolean}\n */\n isInDownloadWindow() {\n // If no download window configured, always allow\n // CMS sends \":\" when unconfigured, treat as empty\n if (!this.settings.downloadStartWindow || !this.settings.downloadEndWindow ||\n this.settings.downloadStartWindow === ':' || this.settings.downloadEndWindow === ':') {\n return true;\n }\n\n try {\n const now = new Date();\n const currentTime = now.getHours() * 60 + now.getMinutes();\n\n const start = this.parseTimeWindow(this.settings.downloadStartWindow);\n const end = this.parseTimeWindow(this.settings.downloadEndWindow);\n\n // Handle overnight window (e.g., 22:00 - 06:00)\n if (start > end) {\n // Overnight: allow if AFTER start OR BEFORE end\n return currentTime >= start || currentTime < end;\n } else {\n // Same day: allow if AFTER start AND BEFORE end\n return currentTime >= start && currentTime < end;\n }\n } catch (error) {\n log.warn('Failed to parse download window:', error);\n return true; // Allow downloads if parsing fails\n }\n }\n\n /**\n * Parse time window string to minutes since midnight\n * @param {string} timeStr - Time string (e.g., \"14:30\", \"22:00\")\n * @returns {number} Minutes since midnight\n */\n parseTimeWindow(timeStr) {\n if (!timeStr || typeof timeStr !== 'string') {\n throw new Error('Invalid time window format');\n }\n\n const parts = timeStr.split(':');\n if (parts.length !== 2) {\n throw new Error('Invalid time window format (expected HH:MM)');\n }\n\n const hours = parseInt(parts[0], 10);\n const minutes = parseInt(parts[1], 10);\n\n if (isNaN(hours) || isNaN(minutes) || hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {\n throw new Error('Invalid time window values');\n }\n\n return hours * 60 + minutes;\n }\n\n /**\n * Get next download window start time\n * @returns {Date|null} Next window start, or null if always allowed\n */\n getNextDownloadWindow() {\n if (!this.settings.downloadStartWindow || !this.settings.downloadEndWindow ||\n this.settings.downloadStartWindow === ':' || this.settings.downloadEndWindow === ':') {\n return null;\n }\n\n try {\n const now = new Date();\n const currentTime = now.getHours() * 60 + now.getMinutes();\n const start = this.parseTimeWindow(this.settings.downloadStartWindow);\n\n const nextWindow = new Date(now);\n\n if (currentTime < start) {\n // Window is later today\n nextWindow.setHours(Math.floor(start / 60), start % 60, 0, 0);\n } else {\n // Window is tomorrow\n nextWindow.setDate(nextWindow.getDate() + 1);\n nextWindow.setHours(Math.floor(start / 60), start % 60, 0, 0);\n }\n\n return nextWindow;\n } catch (error) {\n log.warn('Failed to calculate next download window:', error);\n return null;\n }\n }\n\n /**\n * Check if screenshot interval has elapsed\n * @param {Date} lastScreenshot - Last screenshot timestamp\n * @returns {boolean}\n */\n shouldTakeScreenshot(lastScreenshot) {\n if (!lastScreenshot) {\n return true;\n }\n\n const elapsed = (Date.now() - lastScreenshot.getTime()) / 1000;\n return elapsed >= this.settings.screenshotInterval;\n }\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n// @xiboplayer/settings - CMS settings management\nimport pkg from '../package.json' with { type: 'json' };\nexport const VERSION = pkg.version;\n\n/**\n * Settings manager for xiboplayer\n * @module @xiboplayer/settings\n */\nexport { DisplaySettings } from './settings.js';\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"src-C_Lx4lXp.js","names":["pkg"],"sources":["../../../settings/package.json","../../../settings/src/settings.js","../../../settings/src/index.js"],"sourcesContent":["{\n \"name\": \"@xiboplayer/settings\",\n \"version\": \"0.7.21\",\n \"description\": \"CMS settings management and application for xiboplayer\",\n \"type\": \"module\",\n \"main\": \"./src/index.js\",\n \"types\": \"./src/index.d.ts\",\n \"exports\": {\n \".\": \"./src/index.js\",\n \"./manager\": \"./src/settings.js\"\n },\n \"scripts\": {\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"test:coverage\": \"vitest run --coverage\"\n },\n \"dependencies\": {\n \"@xiboplayer/utils\": \"workspace:*\"\n },\n \"devDependencies\": {\n \"jsdom\": \"^29.0.2\",\n \"vitest\": \"^4.1.2\"\n },\n \"keywords\": [\n \"xibo\",\n \"digital-signage\",\n \"settings\",\n \"configuration\",\n \"display-settings\"\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/settings\"\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 * DisplaySettings - CMS display settings management\n *\n * Parses and applies configuration from RegisterDisplay response.\n * Based on upstream electron-player implementation.\n *\n * Architecture:\n * ┌─────────────────────────────────────────────────────┐\n * │ PlayerCore │\n * │ - Receives RegisterDisplay response │\n * │ - Passes to DisplaySettings.applySettings() │\n * └─────────────────────────────────────────────────────┘\n * ↓\n * ┌─────────────────────────────────────────────────────┐\n * │ DisplaySettings (this module) │\n * │ - Parse all CMS settings │\n * │ - Validate and normalize values │\n * │ - Apply collection interval │\n * │ - Check download windows │\n * │ - Handle screenshot requests │\n * │ - Emit events on changes │\n * └─────────────────────────────────────────────────────┘\n * ↓\n * ┌─────────────────────────────────────────────────────┐\n * │ Platform Layer (PWA/Electron/Mobile) │\n * │ - Listen for setting change events │\n * │ - Update UI with display name │\n * │ - Handle screenshot requests │\n * │ - Respect download windows │\n * └─────────────────────────────────────────────────────┘\n *\n * Usage:\n * const settings = new DisplaySettings();\n * settings.applySettings(regResult.settings);\n *\n * // Get settings\n * const collectInterval = settings.getCollectInterval();\n * const canDownload = settings.isInDownloadWindow();\n *\n * // Listen for changes\n * settings.on('interval-changed', (newInterval) => { ... });\n */\n\nimport { EventEmitter, createLogger } from '@xiboplayer/utils';\n\nconst log = createLogger('DisplaySettings');\n\nexport class DisplaySettings extends EventEmitter {\n constructor() {\n super();\n\n // Current settings (with defaults)\n this.settings = {\n // Collection\n collectInterval: 300, // seconds (5 minutes default)\n\n // Display info\n displayName: 'Unknown Display',\n sizeX: 1920,\n sizeY: 1080,\n\n // Stats\n statsEnabled: false,\n aggregationLevel: 'Individual', // or 'Aggregate'\n\n // Logging\n logLevel: 'error', // 'error', 'audit', 'info', 'debug'\n\n // XMR\n xmrNetworkAddress: null,\n xmrWebSocketAddress: null,\n xmrCmsKey: null,\n\n // Features\n preventSleep: true,\n embeddedServerPort: 9696,\n screenshotInterval: 120, // seconds\n\n // Download windows\n downloadStartWindow: null,\n downloadEndWindow: null,\n\n // License\n licenceCode: null,\n\n // SSP (ad space)\n isSspEnabled: false,\n };\n }\n\n /**\n * Apply settings from RegisterDisplay response\n * @param {Object} settings - Raw settings from CMS\n * @returns {Object} Applied settings with changes\n */\n applySettings(settings) {\n if (!settings) {\n log.warn('No settings provided');\n return { changed: [], settings: this.settings };\n }\n\n const changes = [];\n const oldInterval = this.settings.collectInterval;\n\n // Parse all settings with defaults\n // Handle both lowercase and CamelCase (uppercase first letter)\n this.settings.collectInterval = this.parseCollectInterval(settings.collectInterval || settings.CollectInterval);\n this.settings.displayName = settings.displayName || settings.DisplayName || this.settings.displayName;\n this.settings.sizeX = parseInt(settings.sizeX || settings.SizeX || this.settings.sizeX);\n this.settings.sizeY = parseInt(settings.sizeY || settings.SizeY || this.settings.sizeY);\n\n // Stats\n this.settings.statsEnabled = this.parseBoolean(settings.statsEnabled || settings.StatsEnabled);\n this.settings.aggregationLevel = settings.aggregationLevel || settings.AggregationLevel || this.settings.aggregationLevel;\n\n // Logging\n this.settings.logLevel = settings.logLevel || settings.LogLevel || this.settings.logLevel;\n\n // XMR\n this.settings.xmrNetworkAddress = settings.xmrNetworkAddress || settings.XmrNetworkAddress || this.settings.xmrNetworkAddress;\n this.settings.xmrWebSocketAddress = settings.xmrWebSocketAddress || settings.XmrWebSocketAddress || this.settings.xmrWebSocketAddress;\n this.settings.xmrCmsKey = settings.xmrCmsKey || settings.XmrCmsKey || this.settings.xmrCmsKey;\n\n // Features\n this.settings.preventSleep = this.parseBoolean(settings.preventSleep || settings.PreventSleep, true);\n this.settings.embeddedServerPort = parseInt(settings.embeddedServerPort || settings.EmbeddedServerPort || this.settings.embeddedServerPort);\n this.settings.screenshotInterval = parseInt(settings.screenshotInterval || settings.ScreenshotInterval || this.settings.screenshotInterval);\n\n // Download windows\n this.settings.downloadStartWindow = settings.downloadStartWindow || settings.DownloadStartWindow || this.settings.downloadStartWindow;\n this.settings.downloadEndWindow = settings.downloadEndWindow || settings.DownloadEndWindow || this.settings.downloadEndWindow;\n\n // License\n this.settings.licenceCode = settings.licenceCode || settings.LicenceCode || this.settings.licenceCode;\n\n // SSP\n this.settings.isSspEnabled = this.parseBoolean(settings.isAdspaceEnabled || settings.IsAdspaceEnabled);\n\n // Detect changes\n if (oldInterval !== this.settings.collectInterval) {\n changes.push('collectInterval');\n this.emit('interval-changed', this.settings.collectInterval);\n }\n\n // Emit generic settings-applied event\n this.emit('settings-applied', this.settings, changes);\n\n log.info('Applied settings:', {\n collectInterval: this.settings.collectInterval,\n displayName: this.settings.displayName,\n statsEnabled: this.settings.statsEnabled,\n changes\n });\n\n return { changed: changes, settings: this.settings };\n }\n\n /**\n * Parse collection interval (seconds)\n * @param {*} value - Raw value from CMS\n * @returns {number} Collection interval in seconds\n */\n parseCollectInterval(value) {\n const interval = parseInt(value, 10);\n\n // Validate range (minimum 60s, maximum 86400s = 24h)\n if (isNaN(interval) || interval < 60) {\n return 300; // 5 minutes default\n }\n\n if (interval > 86400) {\n return 86400; // 24 hours max\n }\n\n return interval;\n }\n\n /**\n * Parse boolean setting\n * @param {*} value - Raw value from CMS (string '1' or '0', or boolean)\n * @param {boolean} defaultValue - Default if not set\n * @returns {boolean}\n */\n parseBoolean(value, defaultValue = false) {\n if (value === true || value === false) {\n return value;\n }\n\n if (value === '1' || value === 1) {\n return true;\n }\n\n if (value === '0' || value === 0) {\n return false;\n }\n\n return defaultValue;\n }\n\n /**\n * Get collection interval in seconds\n * @returns {number}\n */\n getCollectInterval() {\n return this.settings.collectInterval;\n }\n\n /**\n * Get display name\n * @returns {string}\n */\n getDisplayName() {\n return this.settings.displayName;\n }\n\n /**\n * Get display size\n * @returns {{ width: number, height: number }}\n */\n getDisplaySize() {\n return {\n width: this.settings.sizeX,\n height: this.settings.sizeY\n };\n }\n\n /**\n * Check if stats are enabled\n * @returns {boolean}\n */\n isStatsEnabled() {\n return this.settings.statsEnabled;\n }\n\n /**\n * Get all settings\n * @returns {Object}\n */\n getAllSettings() {\n return { ...this.settings };\n }\n\n /**\n * Get a specific setting by key\n * @param {string} key - Setting key\n * @param {*} defaultValue - Default value if not set\n * @returns {*}\n */\n getSetting(key, defaultValue = null) {\n return this.settings[key] !== undefined ? this.settings[key] : defaultValue;\n }\n\n /**\n * Check if current time is within download window\n * @returns {boolean}\n */\n isInDownloadWindow() {\n // If no download window configured, always allow\n // CMS sends \":\" when unconfigured, treat as empty\n if (!this.settings.downloadStartWindow || !this.settings.downloadEndWindow ||\n this.settings.downloadStartWindow === ':' || this.settings.downloadEndWindow === ':') {\n return true;\n }\n\n try {\n const now = new Date();\n const currentTime = now.getHours() * 60 + now.getMinutes();\n\n const start = this.parseTimeWindow(this.settings.downloadStartWindow);\n const end = this.parseTimeWindow(this.settings.downloadEndWindow);\n\n // Handle overnight window (e.g., 22:00 - 06:00)\n if (start > end) {\n // Overnight: allow if AFTER start OR BEFORE end\n return currentTime >= start || currentTime < end;\n } else {\n // Same day: allow if AFTER start AND BEFORE end\n return currentTime >= start && currentTime < end;\n }\n } catch (error) {\n log.warn('Failed to parse download window:', error);\n return true; // Allow downloads if parsing fails\n }\n }\n\n /**\n * Parse time window string to minutes since midnight\n * @param {string} timeStr - Time string (e.g., \"14:30\", \"22:00\")\n * @returns {number} Minutes since midnight\n */\n parseTimeWindow(timeStr) {\n if (!timeStr || typeof timeStr !== 'string') {\n throw new Error('Invalid time window format');\n }\n\n const parts = timeStr.split(':');\n if (parts.length !== 2) {\n throw new Error('Invalid time window format (expected HH:MM)');\n }\n\n const hours = parseInt(parts[0], 10);\n const minutes = parseInt(parts[1], 10);\n\n if (isNaN(hours) || isNaN(minutes) || hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {\n throw new Error('Invalid time window values');\n }\n\n return hours * 60 + minutes;\n }\n\n /**\n * Get next download window start time\n * @returns {Date|null} Next window start, or null if always allowed\n */\n getNextDownloadWindow() {\n if (!this.settings.downloadStartWindow || !this.settings.downloadEndWindow ||\n this.settings.downloadStartWindow === ':' || this.settings.downloadEndWindow === ':') {\n return null;\n }\n\n try {\n const now = new Date();\n const currentTime = now.getHours() * 60 + now.getMinutes();\n const start = this.parseTimeWindow(this.settings.downloadStartWindow);\n\n const nextWindow = new Date(now);\n\n if (currentTime < start) {\n // Window is later today\n nextWindow.setHours(Math.floor(start / 60), start % 60, 0, 0);\n } else {\n // Window is tomorrow\n nextWindow.setDate(nextWindow.getDate() + 1);\n nextWindow.setHours(Math.floor(start / 60), start % 60, 0, 0);\n }\n\n return nextWindow;\n } catch (error) {\n log.warn('Failed to calculate next download window:', error);\n return null;\n }\n }\n\n /**\n * Check if screenshot interval has elapsed\n * @param {Date} lastScreenshot - Last screenshot timestamp\n * @returns {boolean}\n */\n shouldTakeScreenshot(lastScreenshot) {\n if (!lastScreenshot) {\n return true;\n }\n\n const elapsed = (Date.now() - lastScreenshot.getTime()) / 1000;\n return elapsed >= this.settings.screenshotInterval;\n }\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n// @xiboplayer/settings - CMS settings management\nimport pkg from '../package.json' with { type: 'json' };\nexport const VERSION = pkg.version;\n\n/**\n * Settings manager for xiboplayer\n * @module @xiboplayer/settings\n */\nexport { DisplaySettings } from './settings.js';\n"],"mappings":"ywBC+CM,EAAM,EAAa,kBAAkB,CAE9B,EAAb,cAAqC,CAAa,CAChD,aAAc,CACZ,OAAO,CAGP,KAAK,SAAW,CAEd,gBAAiB,IAGjB,YAAa,kBACb,MAAO,KACP,MAAO,KAGP,aAAc,GACd,iBAAkB,aAGlB,SAAU,QAGV,kBAAmB,KACnB,oBAAqB,KACrB,UAAW,KAGX,aAAc,GACd,mBAAoB,KACpB,mBAAoB,IAGpB,oBAAqB,KACrB,kBAAmB,KAGnB,YAAa,KAGb,aAAc,GACf,CAQH,cAAc,EAAU,CACtB,GAAI,CAAC,EAEH,OADA,EAAI,KAAK,uBAAuB,CACzB,CAAE,QAAS,EAAE,CAAE,SAAU,KAAK,SAAU,CAGjD,IAAM,EAAU,EAAE,CACZ,EAAc,KAAK,SAAS,gBAoDlC,MAhDA,MAAK,SAAS,gBAAkB,KAAK,qBAAqB,EAAS,iBAAmB,EAAS,gBAAgB,CAC/G,KAAK,SAAS,YAAc,EAAS,aAAe,EAAS,aAAe,KAAK,SAAS,YAC1F,KAAK,SAAS,MAAQ,SAAS,EAAS,OAAS,EAAS,OAAS,KAAK,SAAS,MAAM,CACvF,KAAK,SAAS,MAAQ,SAAS,EAAS,OAAS,EAAS,OAAS,KAAK,SAAS,MAAM,CAGvF,KAAK,SAAS,aAAe,KAAK,aAAa,EAAS,cAAgB,EAAS,aAAa,CAC9F,KAAK,SAAS,iBAAmB,EAAS,kBAAoB,EAAS,kBAAoB,KAAK,SAAS,iBAGzG,KAAK,SAAS,SAAW,EAAS,UAAY,EAAS,UAAY,KAAK,SAAS,SAGjF,KAAK,SAAS,kBAAoB,EAAS,mBAAqB,EAAS,mBAAqB,KAAK,SAAS,kBAC5G,KAAK,SAAS,oBAAsB,EAAS,qBAAuB,EAAS,qBAAuB,KAAK,SAAS,oBAClH,KAAK,SAAS,UAAY,EAAS,WAAa,EAAS,WAAa,KAAK,SAAS,UAGpF,KAAK,SAAS,aAAe,KAAK,aAAa,EAAS,cAAgB,EAAS,aAAc,GAAK,CACpG,KAAK,SAAS,mBAAqB,SAAS,EAAS,oBAAsB,EAAS,oBAAsB,KAAK,SAAS,mBAAmB,CAC3I,KAAK,SAAS,mBAAqB,SAAS,EAAS,oBAAsB,EAAS,oBAAsB,KAAK,SAAS,mBAAmB,CAG3I,KAAK,SAAS,oBAAsB,EAAS,qBAAuB,EAAS,qBAAuB,KAAK,SAAS,oBAClH,KAAK,SAAS,kBAAoB,EAAS,mBAAqB,EAAS,mBAAqB,KAAK,SAAS,kBAG5G,KAAK,SAAS,YAAc,EAAS,aAAe,EAAS,aAAe,KAAK,SAAS,YAG1F,KAAK,SAAS,aAAe,KAAK,aAAa,EAAS,kBAAoB,EAAS,iBAAiB,CAGlG,IAAgB,KAAK,SAAS,kBAChC,EAAQ,KAAK,kBAAkB,CAC/B,KAAK,KAAK,mBAAoB,KAAK,SAAS,gBAAgB,EAI9D,KAAK,KAAK,mBAAoB,KAAK,SAAU,EAAQ,CAErD,EAAI,KAAK,oBAAqB,CAC5B,gBAAiB,KAAK,SAAS,gBAC/B,YAAa,KAAK,SAAS,YAC3B,aAAc,KAAK,SAAS,aAC5B,UACD,CAAC,CAEK,CAAE,QAAS,EAAS,SAAU,KAAK,SAAU,CAQtD,qBAAqB,EAAO,CAC1B,IAAM,EAAW,SAAS,EAAO,GAAG,CAWpC,OARI,MAAM,EAAS,EAAI,EAAW,GACzB,IAGL,EAAW,MACN,MAGF,EAST,aAAa,EAAO,EAAe,GAAO,CAaxC,OAZI,IAAU,IAAQ,IAAU,GACvB,EAGL,IAAU,KAAO,IAAU,EACtB,GAGL,IAAU,KAAO,IAAU,EACtB,GAGF,EAOT,oBAAqB,CACnB,OAAO,KAAK,SAAS,gBAOvB,gBAAiB,CACf,OAAO,KAAK,SAAS,YAOvB,gBAAiB,CACf,MAAO,CACL,MAAO,KAAK,SAAS,MACrB,OAAQ,KAAK,SAAS,MACvB,CAOH,gBAAiB,CACf,OAAO,KAAK,SAAS,aAOvB,gBAAiB,CACf,MAAO,CAAE,GAAG,KAAK,SAAU,CAS7B,WAAW,EAAK,EAAe,KAAM,CACnC,OAAO,KAAK,SAAS,KAAS,IAAA,GAAiC,EAArB,KAAK,SAAS,GAO1D,oBAAqB,CAGnB,GAAI,CAAC,KAAK,SAAS,qBAAuB,CAAC,KAAK,SAAS,mBACrD,KAAK,SAAS,sBAAwB,KAAO,KAAK,SAAS,oBAAsB,IACnF,MAAO,GAGT,GAAI,CACF,IAAM,EAAM,IAAI,KACV,EAAc,EAAI,UAAU,CAAG,GAAK,EAAI,YAAY,CAEpD,EAAQ,KAAK,gBAAgB,KAAK,SAAS,oBAAoB,CAC/D,EAAM,KAAK,gBAAgB,KAAK,SAAS,kBAAkB,CAQ/D,OALE,EAAQ,EAEH,GAAe,GAAS,EAAc,EAGtC,GAAe,GAAS,EAAc,QAExC,EAAO,CAEd,OADA,EAAI,KAAK,mCAAoC,EAAM,CAC5C,IASX,gBAAgB,EAAS,CACvB,GAAI,CAAC,GAAW,OAAO,GAAY,SACjC,MAAU,MAAM,6BAA6B,CAG/C,IAAM,EAAQ,EAAQ,MAAM,IAAI,CAChC,GAAI,EAAM,SAAW,EACnB,MAAU,MAAM,8CAA8C,CAGhE,IAAM,EAAQ,SAAS,EAAM,GAAI,GAAG,CAC9B,EAAU,SAAS,EAAM,GAAI,GAAG,CAEtC,GAAI,MAAM,EAAM,EAAI,MAAM,EAAQ,EAAI,EAAQ,GAAK,EAAQ,IAAM,EAAU,GAAK,EAAU,GACxF,MAAU,MAAM,6BAA6B,CAG/C,OAAO,EAAQ,GAAK,EAOtB,uBAAwB,CACtB,GAAI,CAAC,KAAK,SAAS,qBAAuB,CAAC,KAAK,SAAS,mBACrD,KAAK,SAAS,sBAAwB,KAAO,KAAK,SAAS,oBAAsB,IACnF,OAAO,KAGT,GAAI,CACF,IAAM,EAAM,IAAI,KACV,EAAc,EAAI,UAAU,CAAG,GAAK,EAAI,YAAY,CACpD,EAAQ,KAAK,gBAAgB,KAAK,SAAS,oBAAoB,CAE/D,EAAa,IAAI,KAAK,EAAI,CAWhC,OATI,EAAc,GAKhB,EAAW,QAAQ,EAAW,SAAS,CAAG,EAAE,CAH5C,EAAW,SAAS,KAAK,MAAM,EAAQ,GAAG,CAAE,EAAQ,GAAI,EAAG,EAAE,CAOxD,QACA,EAAO,CAEd,OADA,EAAI,KAAK,4CAA6C,EAAM,CACrD,MASX,qBAAqB,EAAgB,CAMnC,OALK,GAIY,KAAK,KAAK,CAAG,EAAe,SAAS,EAAI,KACxC,KAAK,SAAS,mBAJvB,KC5VA,EAAUA,EAAI"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{c as e,l as t,o as n,r,u as i}from"./src-Bzheh5E2.js";import{r as a}from"./src-CIERor10.js";import{i as o,n as s}from"./src-DzgDstdA.js";var c={name:`@xiboplayer/core`,version:`0.7.20`,description:`xiboplayer core orchestration and lifecycle management`,type:`module`,main:`./src/index.js`,types:`./src/index.d.ts`,exports:{".":`./src/index.js`,"./player-core":`./src/player-core.js`},scripts:{dev:`vite`,build:`vite build`,preview:`vite preview`,test:`vitest run`,"test:watch":`vitest`,"test:ui":`vitest --ui`,"test:coverage":`vitest run --coverage`},dependencies:{"@xiboplayer/utils":`workspace:*`},peerDependencies:{"@xiboplayer/cache":`workspace:*`,"@xiboplayer/renderer":`workspace:*`,"@xiboplayer/schedule":`workspace:*`,"@xiboplayer/xmds":`workspace:*`},devDependencies:{"@vitest/coverage-v8":`^4.1.3`,"@vitest/ui":`^4.1.4`,jsdom:`^29.0.2`,vite:`^8.0.8`,vitest:`^4.1.2`},keywords:[`xibo`,`digital-signage`,`player`,`core`,`orchestration`],author:`Pau Aliagas <linuxnow@gmail.com>`,license:`AGPL-3.0-or-later`,repository:{type:`git`,url:`git+https://github.com/xibo-players/xiboplayer.git`,directory:`packages/core`},homepage:`https://xiboplayer.org`},l=i(`DataConnector`),u=3e5,d=3,f=class extends e{constructor(){super(),this.connectors=new Map}setConnectors(e){if(this.stopPolling(),this.connectors.clear(),!e||e.length===0){l.debug(`No data connectors configured`);return}for(let t of e){if(!t.dataKey||!t.url){l.warn(`Skipping data connector with missing dataKey or url:`,t);continue}this.connectors.set(t.dataKey,{config:t,data:null,timer:null,lastFetch:null,failures:0}),l.info(`Registered data connector: ${t.dataKey} (interval: ${t.updateInterval}s)`)}l.info(`${this.connectors.size} data connector(s) configured`)}startPolling(){for(let[e,t]of this.connectors.entries()){let{config:n}=t,r=(n.updateInterval||300)*1e3;this.fetchData(t).catch(t=>{l.error(`Initial fetch failed for ${e}:`,t)}),t.timer=setInterval(()=>{this.fetchData(t).catch(t=>{l.error(`Polling fetch failed for ${e}:`,t)})},r),l.debug(`Started polling for ${e} every ${n.updateInterval}s`)}}stopPolling(){for(let[e,t]of this.connectors.entries())t.timer&&(clearInterval(t.timer),t.timer=null,l.debug(`Stopped polling for ${e}`))}getData(e){let t=this.connectors.get(e);return t?t.data:(l.debug(`No data connector found for key: ${e}`),null)}getAvailableKeys(){let e=[];for(let[t,n]of this.connectors.entries())n.data!==null&&e.push(t);return e}async fetchData(e){let{config:t}=e,{dataKey:n,url:i}=t;l.debug(`Fetching data for ${n}: ${i}`);try{let t=await r(i,{method:`GET`,headers:{Accept:`application/json`}},{maxRetries:2,baseDelayMs:2e3});if(!t.ok){l.warn(`Data connector ${n} returned ${t.status}: ${t.statusText}`);return}let a=t.headers.get(`Content-Type`)||``,o;o=a.includes(`application/json`)?await t.json():await t.text();let s=e.data;e.data=o,e.lastFetch=Date.now(),e.failures=0,l.debug(`Data updated for ${n} (fetched at ${new Date(e.lastFetch).toISOString()})`),this._ensureNormalPolling(e),this.emit(`data-updated`,n,o),JSON.stringify(s)!==JSON.stringify(o)&&this.emit(`data-changed`,n,o)}catch(r){if(e.failures=(e.failures||0)+1,l.error(`Failed to fetch data for ${n} (${e.failures}x):`,r),this.emit(`fetch-error`,n,r),e.failures>=d&&e.timer){let r=(t.updateInterval||300)*1e3,i=Math.min(r*2**(e.failures-d+1),u);clearInterval(e.timer),e.timer=setTimeout(()=>{this.fetchData(e).catch(()=>{}),e.timer=setInterval(()=>{this.fetchData(e).catch(()=>{})},i)},i),l.warn(`Circuit breaker: ${n} backing off to ${Math.round(i/1e3)}s`)}}}_ensureNormalPolling(e){if(e.failures===0&&e.timer){let t=(e.config.updateInterval||300)*1e3;clearInterval(e.timer),clearTimeout(e.timer),e.timer=setInterval(()=>{this.fetchData(e).catch(()=>{})},t)}}refreshAll(){this.connectors.size!==0&&(l.info(`Refreshing all ${this.connectors.size} data connector(s)`),this.stopPolling(),this.startPolling())}cleanup(){this.stopPolling(),this.connectors.clear(),this.removeAllListeners(),l.debug(`DataConnectorManager cleaned up`)}},p=i(`Blacklist`),m=class{constructor(e=3){this._entries=new Map,this._threshold=e}recordFailure(e,t){let n=Number(e),r=this._entries.get(n)||{failures:0,blacklisted:!1,reason:``};return r.failures++,r.reason=t,!r.blacklisted&&r.failures>=this._threshold?(r.blacklisted=!0,p.warn(`Layout ${n} blacklisted after ${r.failures} consecutive failures: ${t}`)):r.blacklisted||p.info(`Layout ${n} failure ${r.failures}/${this._threshold}: ${t}`),this._entries.set(n,r),{blacklisted:r.blacklisted,failures:r.failures}}recordSuccess(e){let t=Number(e);if(!this._entries.has(t))return!1;let n=this._entries.get(t);return this._entries.delete(t),n.blacklisted?(p.info(`Layout ${t} removed from blacklist (rendered successfully)`),!0):!1}isBlacklisted(e){return this._entries.get(Number(e))?.blacklisted===!0}getBlacklistedIds(){let e=[];for(let[t,n]of this._entries)n.blacklisted&&e.push(t);return e}reset(){let e=this._entries.size;return e>0&&(p.info(`Blacklist reset (${e} entries cleared)`),this._entries.clear()),e}get size(){return this._entries.size}},h=Object.freeze({COLLECTION_START:`collection-start`,COLLECTION_COMPLETE:`collection-complete`,COLLECTION_ERROR:`collection-error`,REGISTER_COMPLETE:`register-complete`,SCHEDULE_RECEIVED:`schedule-received`,LAYOUTS_SCHEDULED:`layouts-scheduled`,NO_LAYOUTS_SCHEDULED:`no-layouts-scheduled`,TIMELINE_UPDATED:`timeline-updated`,LAYOUT_PREPARE_REQUEST:`layout-prepare-request`,LAYOUT_EXPIRE_CURRENT:`layout-expire-current`,LAYOUT_ALREADY_PLAYING:`layout-already-playing`,CHECK_PENDING_LAYOUT:`check-pending-layout`,FILES_RECEIVED:`files-received`,DOWNLOAD_REQUEST:`download-request`,OVERLAY_LAYOUT_REQUEST:`overlay-layout-request`,REVERT_TO_SCHEDULE:`revert-to-schedule`,SYNC_CONFIG:`sync-config`,XMR_CONNECTED:`xmr-connected`,XMR_RECONNECTED:`xmr-reconnected`,XMR_MISCONFIGURED:`xmr-misconfigured`,NAVIGATE_TO_WIDGET:`navigate-to-widget`,EXECUTE_NATIVE_COMMAND:`execute-native-command`,SCHEDULED_COMMAND:`scheduled-command`,COMMAND_RESULT:`command-result`,SCREENSHOT_REQUEST:`screenshot-request`,SUBMIT_STATS_REQUEST:`submit-stats-request`,SUBMIT_LOGS_REQUEST:`submit-logs-request`,SUBMIT_FAULTS_REQUEST:`submit-faults-request`,CACHE_ANALYSIS:`cache-analysis`,COLLECTION_INTERVAL_SET:`collection-interval-set`,COLLECTION_INTERVAL_UPDATED:`collection-interval-updated`,LOG_LEVEL_CHANGED:`log-level-changed`,OFFLINE_MODE:`offline-mode`,PURGE_REQUEST:`purge-request`,PURGE_ALL_REQUEST:`purge-all-request`}),g=i(`PlayerCore`);async function _(){if(typeof window<`u`&&window.electronAPI?.getLanIpAddress)try{return await window.electronAPI.getLanIpAddress()}catch{}try{let e=await(globalThis.__nativeFetch||globalThis.fetch)(`/system/lan-ip`);if(e.ok){let{ip:t}=await e.json();if(t)return t}}catch{}return``}var v=`xibo-offline-cache`,y=1,b=`cache`;function x(e){return n(e?`${v}-${e}`:v,y,b)}var S=class extends e{constructor(e){super(),this.config=e.config,this.xmds=e.xmds,this.cache=e.cache,this.schedule=e.schedule,this.renderer=e.renderer,this.XmrWrapper=e.xmrWrapper,this.statsCollector=e.statsCollector,this.displaySettings=e.displaySettings,this._cmsId=e.cmsId||null,this.dataConnectorManager=new f,_().then(e=>{this._lanIpAddress=e,g.info(`LAN IP:`,e||`(not discovered)`)}),this.xmr=null,this.currentLayoutId=null,this.collecting=!1,this.collectionInterval=null,this.pendingLayouts=new Map,this._layoutMediaStatus=new Map,this.offlineMode=!1,this._normalCollectInterval=null,this._offlineRetrySeconds=0,this._lastCheckRf=null,this._lastCheckSchedule=null,this._lastTimelineFingerprint=null,this._lastTimeline=null,this._layoutOverride=null,this._lastRequiredFiles=[],this._executedCommands=new Set,this.displayCommands=null,this._faultReportingInterval=null,this._faultReportingSeconds=60,this._layoutBlacklist=new m(3),this._lastLayoutChangeTime=null,this._statusCode=2,this._dynamicLayouts=new Set,this.syncConfig=null,this.syncManager=null,this._layoutDurations=new Map,this._finalDurations=new Set,this._preparingLayoutId=null,this.cacheAnalyzer=this.cache?new a(this.cache):null,this._offlineCache={schedule:null,settings:null,requiredFiles:null},this._offlineDbReady=this._initOfflineCache()}get _queueOptions(){return{dynamicLayouts:this._dynamicLayouts}}_scheduleAutoRevert(e,t,n){t>0&&setTimeout(()=>{this._layoutOverride?.layoutId===e&&(g.info(`${n} duration expired (${t}s), reverting to schedule`),this.revertToSchedule())},t*1e3)}async _initOfflineCache(){try{let e=await x(this._cmsId),t=e.transaction(b,`readonly`).objectStore(b),[n,r,i,a,o,s]=await Promise.all([new Promise(e=>{let n=t.get(`schedule`);n.onsuccess=()=>e(n.result??null),n.onerror=()=>e(null)}),new Promise(e=>{let n=t.get(`settings`);n.onsuccess=()=>e(n.result??null),n.onerror=()=>e(null)}),new Promise(e=>{let n=t.get(`requiredFiles`);n.onsuccess=()=>e(n.result??null),n.onerror=()=>e(null)}),new Promise(e=>{let n=t.get(`durations`);n.onsuccess=()=>e(n.result??null),n.onerror=()=>e(null)}),new Promise(e=>{let n=t.get(`finalDurations`);n.onsuccess=()=>e(n.result??null),n.onerror=()=>e(null)}),new Promise(e=>{let n=t.get(`durationsVersion`);n.onsuccess=()=>e(n.result??null),n.onerror=()=>e(null)})]);if(Array.isArray(a)&&a.length>0){for(let[e,t]of a)this._layoutDurations.set(e,t);g.info(`[Timeline] Restored ${a.length} cached durations from IDB`)}if(s>=2&&Array.isArray(o)&&o.length>0){for(let e of o)this._finalDurations.add(e);g.info(`[Timeline] Restored ${o.length} final duration keys from IDB`)}else Array.isArray(o)&&o.length>0&&g.info(`[Timeline] Discarded ${o.length} stale final duration keys (pre-v2)`);this._offlineCache={schedule:n,settings:r,requiredFiles:i},this._offlineDb=e,g.info(`Offline cache loaded from IndexedDB`,n?`(has schedule)`:`(empty)`)}catch(e){g.warn(`Failed to load offline cache from IndexedDB:`,e)}}async _offlineSave(e,t){this._offlineCache[e]=t;try{this._offlineDb||=await x(this._cmsId);let n=this._offlineDb.transaction(b,`readwrite`);n.objectStore(b).put(t,e),await new Promise((e,t)=>{n.oncomplete=e,n.onerror=()=>t(n.error)})}catch(t){this._offlineDb=null,g.warn(`Failed to save offline cache:`,e,t)}}hasCachedData(){return this._offlineCache.schedule!==null}isOffline(){return typeof navigator<`u`&&navigator.onLine===!1}isInOfflineMode(){return this.offlineMode}collectOffline(){if(g.warn(`Offline mode — using cached schedule`),this.offlineMode||(this.offlineMode=!0,this.emit(h.OFFLINE_MODE,!0)),this.collectionInterval&&(this._normalCollectInterval?this._offlineRetrySeconds=Math.min(this._offlineRetrySeconds*2,this._normalCollectInterval):(this._normalCollectInterval=this._currentCollectInterval,this._offlineRetrySeconds=30),this._setCollectionTimer(this._offlineRetrySeconds),g.info(`Offline: retry in ${this._offlineRetrySeconds}s`)),!this.collectionInterval){let e=this._offlineCache.settings;e?.settings&&(this.setupCollectionInterval(e.settings),this._normalCollectInterval=this._currentCollectInterval,this._offlineRetrySeconds=30,this._setCollectionTimer(this._offlineRetrySeconds),g.info(`Offline: retry in ${this._offlineRetrySeconds}s`))}let e=this._offlineCache.schedule;e&&(this.schedule.setSchedule(e),this.emit(h.SCHEDULE_RECEIVED,e));let t=this.schedule.getCurrentLayouts();g.info(`Offline layouts:`,t),this.emit(h.LAYOUTS_SCHEDULED,t),this._evaluateAndSwitchLayout(t,`Offline`),this.emit(h.COLLECTION_COMPLETE)}_evaluateAndSwitchLayout(e,t){let n=t?`${t}: `:``,{queue:r}=this.schedule.getScheduleQueue(this._layoutDurations,this._queueOptions);if(r.length>0)if(this.currentLayoutId)r.some(e=>o(e.layoutId)===this.currentLayoutId)?(g.info(`Layout ${this.currentLayoutId} playing — queue updated in background, playback continues`),this.emit(h.LAYOUT_ALREADY_PLAYING,this.currentLayoutId)):(g.info(`Layout ${this.currentLayoutId} no longer in queue — expiring`),this.currentLayoutId=null,this.emit(h.LAYOUT_EXPIRE_CURRENT));else if(this._preparingLayoutId)g.info(`${n}layout ${this._preparingLayoutId} already being prepared, skipping`);else{let e=this.getNextLayout();e&&(this._preparingLayoutId=e.layoutId,g.info(`${n}switching to layout ${e.layoutId}`),this.emit(h.LAYOUT_PREPARE_REQUEST,e.layoutId))}else g.info(`${t?`${t}: n`:`N`}o layouts${t?` in cached schedule`:` scheduled, falling back to default`}`),this.emit(h.NO_LAYOUTS_SCHEDULED);this.logUpcomingTimeline()}async collectNow(){return this._lastCheckRf=null,this._lastCheckSchedule=null,this.collect()}async collect(){if(this.collecting){g.debug(`Collection already in progress, skipping`);return}this.collecting=!0;try{if(await this._offlineDbReady,g.info(`Starting collection cycle...`),this.emit(h.COLLECTION_START),this.isOffline()){if(this.hasCachedData())return this.collecting=!1,this.collectOffline();throw Error(`Offline with no cached data — cannot start playback`)}this.config.ensureXmrKeyPair&&await this.config.ensureXmrKeyPair(),g.debug(`Collection step: registerDisplay`);let e=await this.xmds.registerDisplay();g.info(`Display registered: ${e.code}${e.tags?.length?`, tags: ${e.tags.join(`, `)}`:``}`),g.debug(`Register result:`,JSON.stringify(e)),this._processRegistration(e),g.debug(`Collection step: initializeXmr`),await this.initializeXmr(e);let t=e.checkRf||``,n=e.checkSchedule||``;if(!this._lastCheckRf||this._lastCheckRf!==t){this.resetBlacklist(),g.debug(`Collection step: requiredFiles`);let e=await this.xmds.requiredFiles(),r=e.files||e,i=e.purge||[];if(g.info(`Required files:`,r.length,i.length>0?`(+ ${i.length} purge)`:``),this._lastCheckRf=t,this.emit(h.FILES_RECEIVED,r),this._offlineSave(`requiredFiles`,e),i.length>0&&this.emit(h.PURGE_REQUEST,i),!this._lastCheckSchedule||this._lastCheckSchedule!==n){g.debug(`Collection step: schedule`);let e=await this.xmds.schedule();g.info(`Schedule received`),this._lastCheckSchedule=n,g.debug(`Collection step: processing schedule`),this._applyNewSchedule(e),this.logUpcomingTimeline()}g.debug(`Collection step: download-request + mediaInventory`),this.schedule.getCurrentLayouts();let{queue:a}=this.schedule.getScheduleQueue(this._layoutDurations,this._queueOptions),s=[...new Set(a.map(e=>o(e.layoutId)))];if(this._lastRequiredFiles=r,this.displaySettings?.isInDownloadWindow&&!this.displaySettings.isInDownloadWindow()){let e=this.displaySettings.getNextDownloadWindow?.();g.info(`Outside download window, skipping downloads${e?` (next: ${e.toLocaleTimeString()})`:``}`)}else this.emit(h.DOWNLOAD_REQUEST,{layoutOrder:s,files:r,layoutDependants:Object.fromEntries(this.schedule.getDependantsMap())});this.cacheAnalyzer&&this.cacheAnalyzer.analyze(r).then(e=>{this.emit(h.CACHE_ANALYSIS,e)}).catch(e=>g.warn(`Cache analysis failed:`,e)),this.submitMediaInventory(r)}else if(t&&g.info(`RequiredFiles CRC unchanged, skipping download check`),this._lastCheckSchedule!==n){let e=await this.xmds.schedule();g.info(`Schedule received (RF unchanged but schedule changed)`),this._lastCheckSchedule=n,this._applyNewSchedule(e)}else n&&g.info(`Schedule CRC unchanged, skipping`);await this._fetchWeatherData(),g.debug(`Collection step: evaluateSchedule`);let r=this.schedule.getCurrentLayouts();g.info(`Current layouts:`,r),this.emit(h.LAYOUTS_SCHEDULED,r),this._evaluateAndSwitchLayout(r,``),this._processScheduledCommands(),(e.settings?.statsEnabled===`On`||e.settings?.statsEnabled===`1`)&&(this.statsCollector?(g.info(`Stats enabled, submitting proof of play`),this.emit(h.SUBMIT_STATS_REQUEST)):g.warn(`Stats enabled but no StatsCollector provided`)),this.emit(h.SUBMIT_LOGS_REQUEST),this.emit(h.SUBMIT_FAULTS_REQUEST),!this.collectionInterval&&e.settings&&this.setupCollectionInterval(e.settings),this._faultReportingInterval||this._startFaultReportingAgent(),this.logUpcomingTimeline(),this.emit(h.COLLECTION_COMPLETE)}catch(e){if(this.hasCachedData())return g.warn(`Collection failed, falling back to cached data:`,e?.message||e),this.emit(h.COLLECTION_ERROR,e),this.collecting=!1,this.collectOffline();throw g.error(`Collection error:`,e),this.emit(h.COLLECTION_ERROR,e),e}finally{this.collecting=!1}}_processRegistration(e){if(this._offlineSave(`settings`,e),this.offlineMode&&(this.offlineMode=!1,g.info(`Back online — resuming normal collection`),this.emit(h.OFFLINE_MODE,!1),this._normalCollectInterval&&(this._setCollectionTimer(this._normalCollectInterval),this._normalCollectInterval=null,this._offlineRetrySeconds=0)),this.displaySettings&&e.settings){let n=this.displaySettings.applySettings(e.settings);n.changed.includes(`collectInterval`)&&this.updateCollectionInterval(n.settings.collectInterval),e.settings.logLevel&&t(e.settings.logLevel)&&(g.info(`Log level updated from CMS:`,e.settings.logLevel),this.emit(h.LOG_LEVEL_CHANGED,e.settings.logLevel))}if(this.schedule?.setDisplayProperties&&e.settings&&this.schedule.setDisplayProperties(e.settings),e.syncConfig){let t=JSON.stringify(e.syncConfig);t!==this._lastRawSyncConfig&&(this._lastRawSyncConfig=t,this.syncConfig=e.syncConfig,g.info(`Sync group:`,e.syncConfig.isLead?`LEAD`:`follower → ${e.syncConfig.syncGroup}`,`(switchDelay: ${e.syncConfig.syncSwitchDelay}ms, videoPauseDelay: ${e.syncConfig.syncVideoPauseDelay}ms)`),this.emit(h.SYNC_CONFIG,e.syncConfig))}if(this._applyTagConfig(e.tags),e.commands&&e.commands.length>0){this.displayCommands={};for(let t of e.commands)this.displayCommands[t.commandCode]=t;g.debug(`Display commands:`,Object.keys(this.displayCommands).join(`, `))}this.emit(h.REGISTER_COMPLETE,e)}_applyNewSchedule(e){this.emit(h.SCHEDULE_RECEIVED,e),this.schedule.setSchedule(e),this._executedCommands.clear(),this.updateDataConnectors(),this._offlineSave(`schedule`,e)}async initializeXmr(e){let t=e.settings?.xmrWebSocketAddress||e.settings?.xmrNetworkAddress;if(!t){g.warn(`XMR not configured: no xmrWebSocketAddress or xmrNetworkAddress in CMS settings`),this.emit(h.XMR_MISCONFIGURED,{reason:`missing`,message:`XMR address not configured in CMS. Go to CMS Admin → Settings → Configuration → XMR and set the WebSocket address.`});return}if(t.startsWith(`tcp://`)){g.warn(`XMR address uses tcp:// protocol which is not supported by PWA players: ${t}`),g.warn(`Configure XMR_WS_ADDRESS in CMS Admin → Settings → Configuration → XMR (e.g. wss://your-domain/xmr)`),this.emit(h.XMR_MISCONFIGURED,{reason:`wrong-protocol`,url:t,message:`XMR uses tcp:// protocol (not supported by PWA). Set XMR WebSocket Address to wss://your-domain/xmr in CMS Settings.`});return}if(/example\.(org|com|net)/i.test(t)){g.warn(`XMR address contains placeholder domain: ${t}`),g.warn(`Configure the real XMR address in CMS Admin → Settings → Configuration → XMR`),this.emit(h.XMR_MISCONFIGURED,{reason:`placeholder`,url:t,message:`XMR address is still the default placeholder (${t}). Update it in CMS Settings.`});return}let n=e.settings?.xmrCmsKey||e.settings?.serverKey||this.config.serverKey;g.debug(`XMR CMS Key:`,n?`present`:`missing`),this.xmr?this.xmr.isConnected()?g.debug(`XMR already connected`):(g.info(`XMR disconnected, attempting to reconnect...`),await this.xmr.start(t,n),this.emit(h.XMR_RECONNECTED,t)):(g.info(`Initializing XMR WebSocket:`,t),this.xmr=new this.XmrWrapper(this.config,this),await this.xmr.start(t,n),this.emit(h.XMR_CONNECTED,t))}setupCollectionInterval(e){let t=this.displaySettings?this.displaySettings.getCollectInterval():parseInt(e.collectInterval||`300`,10);this._setCollectionTimer(t),this.emit(h.COLLECTION_INTERVAL_SET,t)}updateCollectionInterval(e){this.collectionInterval&&(this._setCollectionTimer(e),this.emit(h.COLLECTION_INTERVAL_UPDATED,e))}_startFaultReportingAgent(){this._faultReportingInterval&&clearInterval(this._faultReportingInterval),g.info(`Fault reporting agent started (interval: ${this._faultReportingSeconds}s)`),this._faultReportingInterval=setInterval(()=>{this.emit(h.SUBMIT_FAULTS_REQUEST)},this._faultReportingSeconds*1e3)}_setCollectionTimer(e){this.collectionInterval&&clearInterval(this.collectionInterval),this._currentCollectInterval=e,g.info(`Collection interval: ${e}s`),this.collectionInterval=setInterval(()=>{g.debug(`Running scheduled collection cycle...`),this.collect().catch(e=>{g.error(`Collection error:`,e),this.emit(h.COLLECTION_ERROR,e)})},e*1e3)}async requestLayoutChange(e){g.info(`Layout change requested: ${e}`),this.currentLayoutId=null,this.emit(`layout-change-requested`,e)}clearPreparingLayout(){this._preparingLayoutId=null}setCurrentLayout(e){this.currentLayoutId=e,this._preparingLayoutId=null,this._lastLayoutChangeTime=new Date().toISOString(),this._statusCode=1,this.pendingLayouts.delete(e),this._layoutMediaStatus.delete(`${e}.xlf`),this.emit(`layout-current`,e),this._lastTimelineFingerprint=null,this.logUpcomingTimeline()}setPendingLayout(e,t){this.pendingLayouts.set(e,t),this.emit(`layout-pending`,e,t)}clearCurrentLayout(){this.currentLayoutId=null,this.emit(`layout-cleared`)}getNextLayout(){let e=this.schedule.popNextFromQueue(this._layoutDurations,this._queueOptions);if(!e){let e=this.schedule.schedule?.default;return e?{layoutId:o(e),layoutFile:e}:null}let t=o(e.layoutId);if(this.isLayoutBlacklisted(t)){let{queue:e}=this.schedule.getScheduleQueue(this._layoutDurations,this._queueOptions);for(let t=0;t<e.length-1;t++){let e=this.schedule.popNextFromQueue(this._layoutDurations,this._queueOptions);if(e){let t=o(e.layoutId);if(!this.isLayoutBlacklisted(t))return{layoutId:t,layoutFile:e.layoutId}}}g.warn(`All queued layouts are blacklisted, using current entry as fallback`)}return{layoutId:t,layoutFile:e.layoutId}}peekNextLayout(){let e=this.schedule.peekNextInQueue(this._layoutDurations,this._queueOptions);if(!e)return null;let t=o(e.layoutId);if(t===this.currentLayoutId){let e=this.schedule.peekAfterNext(this._layoutDurations,this._queueOptions);if(!e)return null;let t=o(e.layoutId);return t===this.currentLayoutId||this.isLayoutBlacklisted(t)?null:{layoutId:t,layoutFile:e.layoutId}}return this.isLayoutBlacklisted(t)?null:{layoutId:t,layoutFile:e.layoutId}}advanceToNextLayout(){if(this._layoutOverride){g.info(`Layout override active, not advancing schedule`);return}let e=this.getNextLayout();if(!e){if(this.currentLayoutId){g.info(`No layouts in queue, replaying ${this.currentLayoutId} to avoid blank screen`);let e=this.currentLayoutId;this.currentLayoutId=null,this._preparingLayoutId=e,this.emit(h.LAYOUT_PREPARE_REQUEST,e)}else g.info(`No layouts scheduled during advance`),this.emit(h.NO_LAYOUTS_SCHEDULED);return}let{layoutId:t,layoutFile:n}=e,r=this._layoutDurations.get(n)||`?`;if(this._lastTimeline&&this._lastTimeline.length>0){let e=this._lastTimeline.slice(0,2).map(e=>{let t=e.startTime.toLocaleTimeString(`en-GB`,{hour:`2-digit`,minute:`2-digit`,second:`2-digit`});return`${e.layoutFile}(${e.duration}s@${t})`});g.debug(`[Timeline] Layout transition: entering ${n} (${r}s), overlay top: [${e.join(`, `)}]`),this._lastTimeline[0].layoutFile!==n&&g.warn(`[Timeline] Mismatch: entering ${n} but overlay expects ${this._lastTimeline[0].layoutFile}`)}else g.debug(`[Timeline] Layout transition: entering ${n} (${r}s), no timeline data`);if(this.syncManager&&this.schedule.isSyncEvent(n))if(this.isSyncLead()){g.info(`[Sync] Lead requesting coordinated layout change: ${t}`),this._preparingLayoutId=t,this.emit(h.LAYOUT_PREPARE_REQUEST,t),this.syncManager.requestLayoutChange(t).catch(e=>{g.error(`[Sync] Layout change failed:`,e)});return}else if(this.syncManager.transport?.connected){g.info(`[Sync] Follower waiting for lead signal (not advancing independently)`);return}else g.warn(`[Sync] Follower: lead unreachable, advancing independently`);t===this.currentLayoutId&&(g.info(`Next layout ${t} is same as current, triggering replay`),this.currentLayoutId=null);let{queue:i}=this.schedule.getScheduleQueue(this._layoutDurations,this._queueOptions),a=this.schedule.getQueuePosition();g.info(`Advancing to layout ${t} (queue pos ${a}/${i.length})`),this._preparingLayoutId=t,this.emit(h.LAYOUT_PREPARE_REQUEST,t)}advanceToPreviousLayout(){if(this._layoutOverride){g.info(`Layout override active, not going back`);return}let{queue:e}=this.schedule.getScheduleQueue(this._layoutDurations,this._queueOptions);if(e.length<=1){g.info(`Single or empty queue, nothing to go back to`);return}let t=this.schedule.rewindQueue(2,this._layoutDurations,this._queueOptions);if(!t)return;let n=o(t.layoutId);if(n===this.currentLayoutId){g.info(`Previous layout is same as current, nothing to go back to`);return}g.info(`Going back to layout ${n}`),this.emit(h.LAYOUT_PREPARE_REQUEST,n)}notifyMediaReady(e,t=`media`){g.debug(`File ${e} ready (${t})`);for(let[n,r]of this.pendingLayouts.entries()){let i=t===`layout`&&n===parseInt(e),a=t===`media`&&r.includes(e);(i||a)&&(g.debug(`${t} ${e} was needed by pending layout ${n}, checking if ready...`),this.emit(h.CHECK_PENDING_LAYOUT,n,r))}}async notifyLayoutStatus(e){try{let t={currentLayoutId:e,deviceName:this.config?.displayName||``,displayName:this.config?.displayName||``,lastCommandSuccess:this._lastCommandSuccess??!0,code:this._statusCode,lastLayoutChangeTime:this._lastLayoutChangeTime||new Date().toISOString()};this.config?.latitude&&(t.latitude=this.config.latitude),this.config?.longitude&&(t.longitude=this.config.longitude),this._lanIpAddress&&(t.lanIpAddress=this._lanIpAddress),await this.xmds.notifyStatus(t),this.emit(`status-notified`,e)}catch(t){g.warn(`Failed to notify status:`,t),this.emit(`status-notify-failed`,e,t)}}reportGeoLocation(e){let t=parseFloat(e?.latitude),n=parseFloat(e?.longitude);if(isNaN(t)||isNaN(n)){g.warn(`reportGeoLocation: invalid coordinates`,e);return}g.info(`Geo location from CMS: ${t.toFixed(4)}, ${n.toFixed(4)}`),this.schedule?.setLocation&&this.schedule.setLocation(t,n),this.emit(`location-updated`,{latitude:t,longitude:n,source:`cms`}),this.checkSchedule()}async requestGeoLocation(){if(this._geoCache&&Date.now()-this._geoCache.ts<1800*1e3)return this._geoCache.location;if(!this._browserGeoFailed){let e=await this._tryBrowserGeolocation();if(e)return this._cacheGeo(this._applyLocation(e.latitude,e.longitude,`browser`));this._browserGeoFailed=!0}let e=this.config?.googleGeoApiKey;if(e){let t=await this._tryGoogleGeolocation(e);if(t)return this._cacheGeo(this._applyLocation(t.latitude,t.longitude,`google-api`))}let t=await this._tryIpGeolocation();return t?this._cacheGeo(this._applyLocation(t.latitude,t.longitude,`ip-geolocation`)):(g.warn(`All geolocation methods failed`),null)}_cacheGeo(e){return this._geoCache={location:e,ts:Date.now()},e}_applyTagConfig(e){if(!Array.isArray(e)||e.length===0)return;let t={geoApiKey:`googleGeoApiKey`};for(let n of e){let e=n.indexOf(`|`);if(e===-1)continue;let r=n.substring(0,e),i=n.substring(e+1),a=t[r];a&&i&&this.config&&(g.info(`Config from CMS tag: ${r} → ${a}`),this.config[a]=i)}}_applyLocation(e,t,n){return g.info(`Geolocation (${n}): ${e.toFixed(4)}, ${t.toFixed(4)}`),this.schedule?.setLocation&&this.schedule.setLocation(e,t),this.emit(`location-updated`,{latitude:e,longitude:t,source:n}),this.checkSchedule(),{latitude:e,longitude:t}}async _tryBrowserGeolocation(){if(typeof navigator>`u`||!navigator.geolocation)return null;try{let e=await new Promise((e,t)=>{navigator.geolocation.getCurrentPosition(e,t,{timeout:1e4,maximumAge:3e5,enableHighAccuracy:!1})});return{latitude:e.coords.latitude,longitude:e.coords.longitude}}catch(e){return g.warn(`Browser geolocation failed:`,e?.message||e),null}}async _tryGoogleGeolocation(e){try{let t=await fetch(`https://www.googleapis.com/geolocation/v1/geolocate?key=${e}`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({considerIp:!0}),signal:AbortSignal.timeout(5e3)});if(!t.ok)return g.warn(`Google Geolocation API returned ${t.status}`),null;let n=await t.json();return n.location?.lat!=null&&n.location?.lng!=null?{latitude:n.location.lat,longitude:n.location.lng}:null}catch(e){return g.warn(`Google Geolocation API failed:`,e?.message||e),null}}async _tryIpGeolocation(){let e=[{url:`https://ipapi.co/json/`,parse:e=>e.latitude!=null&&e.longitude!=null?{latitude:e.latitude,longitude:e.longitude}:null},{url:`https://freeipapi.com/api/json`,parse:e=>e.latitude!=null&&e.longitude!=null?{latitude:e.latitude,longitude:e.longitude}:null}];for(let t of e)try{let e=await fetch(t.url,{signal:AbortSignal.timeout(5e3)});if(!e.ok)continue;let n=await e.json(),r=t.parse(n);if(r)return r}catch(e){g.warn(`IP geolocation (${t.url}) failed:`,e?.message||e)}return null}checkSchedule(){let e=this.schedule.getCurrentLayouts();this.emit(h.LAYOUTS_SCHEDULED,e),this._evaluateAndSwitchLayout(e,``)}async captureScreenshot(){g.info(`Screenshot requested`),this.emit(h.SCREENSHOT_REQUEST)}async changeLayout(e,t){g.info(`Layout change requested via XMR:`,e);let n=parseInt(e,10),r=t?.duration||0;this._layoutOverride={layoutId:n,type:`change`,duration:r,changeMode:t?.changeMode||`replace`},this.currentLayoutId=null,this.emit(h.LAYOUT_PREPARE_REQUEST,n),this._scheduleAutoRevert(n,r,`Layout override`)}async overlayLayout(e,t){g.info(`Overlay layout requested via XMR:`,e);let n=parseInt(e,10),r=t?.duration||0;this._layoutOverride={layoutId:n,type:`overlay`,duration:r},this.emit(h.OVERLAY_LAYOUT_REQUEST,n),this._scheduleAutoRevert(n,r,`Overlay`)}async revertToSchedule(){g.info(`Reverting to scheduled content`),this._layoutOverride=null,this.currentLayoutId=null,this.emit(h.REVERT_TO_SCHEDULE);let e=this.schedule.getCurrentLayouts();if(e.length>0){let t=e[0],n=o(t);this.emit(h.LAYOUT_PREPARE_REQUEST,n)}else this.emit(h.NO_LAYOUTS_SCHEDULED)}async purgeAll(){return g.info(`Purge all cache requested via XMR`),this._lastCheckRf=null,this._lastCheckSchedule=null,this.emit(h.PURGE_ALL_REQUEST),this.collectNow()}async executeCommand(e,t){if(g.info(`Execute command requested:`,e),!t||!t[e]){g.warn(`Unknown command code:`,e),this._lastCommandSuccess=!1,this.emit(h.COMMAND_RESULT,{code:e,success:!1,reason:`Unknown command`});return}let n=t[e],r=n.commandString||n.value||``;if(r.startsWith(`http|`)){let t=r.split(`|`),n=t[1],i=t[2]||`application/json`;try{let t=await fetch(n,{method:`POST`,headers:{"Content-Type":i},signal:AbortSignal.timeout(1e4)}),r=t.ok;this._lastCommandSuccess=r,g.info(`HTTP command ${e} result: ${t.status}`),this.emit(h.COMMAND_RESULT,{code:e,success:r,status:t.status})}catch(t){this._lastCommandSuccess=!1,g.error(`HTTP command ${e} failed:`,t),this.emit(h.COMMAND_RESULT,{code:e,success:!1,reason:t.message})}}else g.info(`Delegating non-HTTP command to platform layer:`,e),this.emit(h.EXECUTE_NATIVE_COMMAND,{code:e,commandString:r})}triggerWebhook(e){g.info(`Webhook trigger from XMR:`,e),this.handleTrigger(e)}refreshDataConnectors(){g.info(`Data connector refresh requested via XMR`),this.dataConnectorManager.refreshAll(),this.emit(`data-connectors-refreshed`)}async submitMediaInventory(e){if(!(!e||e.length===0))try{let t=Math.floor(Date.now()/1e3),n=`<files>${e.filter(e=>[`media`,`layout`,`resource`,`dependency`,`widget`].includes(e.type)).map(e=>{let n=e.complete===void 0||e.complete?`1`:`0`,r=e.fileType?` fileType="${e.fileType}"`:``;return`<file type="${e.type}" id="${e.id}" complete="${n}" md5="${e.md5||``}" lastChecked="${t}"${r}/>`}).join(``)}</files>`;await this.xmds.mediaInventory(n),g.info(`Media inventory submitted: ${e.length} files`),this.emit(`media-inventory-submitted`,e.length)}catch(e){g.warn(`MediaInventory submission failed:`,e)}}async blackList(e,t,n){try{await this.xmds.blackList(e,t,n),this.emit(`media-blacklisted`,{mediaId:e,type:t,reason:n})}catch(e){g.warn(`BlackList failed:`,e)}}reportLayoutFailure(e,t){let n=Number(e);this._statusCode=3;let{blacklisted:r,failures:i}=this._layoutBlacklist.recordFailure(n,t);r&&i===3&&(this.emit(`layout-blacklisted`,{layoutId:n,reason:t,failures:i}),this.blackList(n,`layout`,t))}reportLayoutSuccess(e){this._layoutBlacklist.recordSuccess(Number(e))&&this.emit(`layout-unblacklisted`,{layoutId:Number(e)})}isLayoutBlacklisted(e){return this._layoutBlacklist.isBlacklisted(e)}getBlacklistedLayouts(){return this._layoutBlacklist.getBlacklistedIds()}resetBlacklist(){this._layoutBlacklist.reset()>0&&this.emit(`blacklist-reset`)}isLayoutOverridden(){return this._layoutOverride!==null}handleTrigger(e){let t=this.schedule.findActionByTrigger(e);if(!t){g.debug(`No scheduled action matches trigger:`,e);return}switch(g.info(`Action triggered: ${t.actionType} (trigger: ${e})`),t.actionType){case`navLayout`:case`navigateToLayout`:t.layoutCode&&this.changeLayout(t.layoutCode);break;case`navWidget`:case`navigateToWidget`:this.emit(h.NAVIGATE_TO_WIDGET,t);break;case`command`:this.emit(`execute-command`,t.commandCode);break;default:g.warn(`Unknown action type:`,t.actionType)}}updateDataConnectors(){let e=this.schedule.getDataConnectors();e.length>0&&g.info(`Configuring ${e.length} data connector(s)`),this.dataConnectorManager.setConnectors(e),e.length>0&&(this.dataConnectorManager.startPolling(),this.emit(`data-connectors-started`,e.length))}_processScheduledCommands(){if(!this.schedule?.getCommands)return;let e=this.schedule.getCommands();if(e.length===0)return;let t=new Date;for(let n of e){if(!n.code||!n.date)continue;let e=`${n.code}|${n.date}`;if(this._executedCommands.has(e))continue;let r=new Date(n.date);if(isNaN(r.getTime())){g.warn(`Scheduled command has invalid date:`,n.date);continue}t>=r&&(g.info(`Executing scheduled command: ${n.code} (scheduled: ${n.date})`),this._executedCommands.add(e),n.code===`collectNow`?setTimeout(()=>this.collectNow().catch(e=>g.error(`collectNow command failed:`,e)),0):this.emit(h.SCHEDULED_COMMAND,n))}}async _fetchWeatherData(){if(!(!this.xmds?.getWeather||!this.schedule?.setWeatherData))try{let e=await this.xmds.getWeather(),t=typeof e==`string`?JSON.parse(e):e;this.schedule.setWeatherData(t),g.info(`Weather data updated:`,Object.keys(t).join(`, `))}catch(e){g.warn(`GetWeather failed (non-critical):`,e?.message||e)}}getDataConnectorManager(){return this.dataConnectorManager}setSyncManager(e){this.syncManager=e,g.info(`SyncManager attached:`,e.isLead?`LEAD`:`FOLLOWER`)}isInSyncGroup(){return this.syncConfig!==null}isSyncLead(){return this.syncConfig?.isLead===!0}getSyncConfig(){return this.syncConfig}logUpcomingTimeline(){if(!this.schedule.getLayoutsAtTime)return;let e=[...this._layoutDurations.entries()].sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>`${e}:${t}`).join(`|`),t=[...this._layoutMediaStatus.entries()].sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>`${e}:${t.ready}:${t.missingKey}`).join(`|`),n=[...this.pendingLayouts.keys()].sort().join(`,`),r=this.schedule.getQueuePosition()||0,i=`${this._lastCheckSchedule}|${e}|${this.currentLayoutId}|${r}|${t}|${n}`;if(i===this._lastTimelineFingerprint&&this._lastTimeline){this.emit(h.TIMELINE_UPDATED,this._lastTimeline);return}let{queue:a}=this.schedule.getScheduleQueue(this._layoutDurations,this._queueOptions),o=s(a,this.schedule.getQueuePosition(),{currentLayoutStartedAt:this._lastLayoutChangeTime?new Date(this._lastLayoutChangeTime):null,defaultLayout:this.schedule.schedule?.default||null,durations:this._layoutDurations});if(o.length===0)return;for(let e of o){let t=parseInt(e.layoutFile.replace(`.xlf`,``),10),n=this.pendingLayouts.get(t);if(n&&n.length>0)e.missingMedia=n.map(String);else{let t=this._layoutMediaStatus.get(e.layoutFile);t&&!t.ready&&t.missing.length>0&&(e.missingMedia=t.missing.map(String))}}this._lastTimelineFingerprint=i,this._lastTimeline=o;let c=o.slice(0,20).map(e=>{let t=e.startTime.toLocaleTimeString(`en-GB`,{hour:`2-digit`,minute:`2-digit`,second:`2-digit`}),n=e.endTime.toLocaleTimeString(`en-GB`,{hour:`2-digit`,minute:`2-digit`,second:`2-digit`}),r=e.missingMedia?` [MISSING: ${e.missingMedia.length} files]`:``;return` ${t}-${n} Layout ${e.layoutFile} (${e.duration}s)${e.isDefault?` [default]`:``}${r}`});for(let e of o)e.missingMedia&&g.warn(`[Timeline] Layout ${e.layoutFile}: ${e.missingMedia.length} files missing`);g.info(`[Timeline] Next ${o.length} plays:\n${c.join(`
|
|
1
|
+
import{c as e,l as t,o as n,r,u as i}from"./src-BtVLiVYZ.js";import{r as a}from"./src-B_BNICay.js";import{i as o,n as s}from"./src-CROvYSP8.js";var c={name:`@xiboplayer/core`,version:`0.7.21`,description:`xiboplayer core orchestration and lifecycle management`,type:`module`,main:`./src/index.js`,types:`./src/index.d.ts`,exports:{".":`./src/index.js`,"./player-core":`./src/player-core.js`},scripts:{dev:`vite`,build:`vite build`,preview:`vite preview`,test:`vitest run`,"test:watch":`vitest`,"test:ui":`vitest --ui`,"test:coverage":`vitest run --coverage`},dependencies:{"@xiboplayer/utils":`workspace:*`},peerDependencies:{"@xiboplayer/cache":`workspace:*`,"@xiboplayer/renderer":`workspace:*`,"@xiboplayer/schedule":`workspace:*`,"@xiboplayer/xmds":`workspace:*`},devDependencies:{"@vitest/coverage-v8":`^4.1.3`,"@vitest/ui":`^4.1.4`,jsdom:`^29.0.2`,vite:`^8.0.8`,vitest:`^4.1.2`},keywords:[`xibo`,`digital-signage`,`player`,`core`,`orchestration`],author:`Pau Aliagas <linuxnow@gmail.com>`,license:`AGPL-3.0-or-later`,repository:{type:`git`,url:`git+https://github.com/xibo-players/xiboplayer.git`,directory:`packages/core`},homepage:`https://xiboplayer.org`},l=i(`DataConnector`),u=3e5,d=3,f=class extends e{constructor(){super(),this.connectors=new Map}setConnectors(e){if(this.stopPolling(),this.connectors.clear(),!e||e.length===0){l.debug(`No data connectors configured`);return}for(let t of e){if(!t.dataKey||!t.url){l.warn(`Skipping data connector with missing dataKey or url:`,t);continue}this.connectors.set(t.dataKey,{config:t,data:null,timer:null,lastFetch:null,failures:0}),l.info(`Registered data connector: ${t.dataKey} (interval: ${t.updateInterval}s)`)}l.info(`${this.connectors.size} data connector(s) configured`)}startPolling(){for(let[e,t]of this.connectors.entries()){let{config:n}=t,r=(n.updateInterval||300)*1e3;this.fetchData(t).catch(t=>{l.error(`Initial fetch failed for ${e}:`,t)}),t.timer=setInterval(()=>{this.fetchData(t).catch(t=>{l.error(`Polling fetch failed for ${e}:`,t)})},r),l.debug(`Started polling for ${e} every ${n.updateInterval}s`)}}stopPolling(){for(let[e,t]of this.connectors.entries())t.timer&&(clearInterval(t.timer),t.timer=null,l.debug(`Stopped polling for ${e}`))}getData(e){let t=this.connectors.get(e);return t?t.data:(l.debug(`No data connector found for key: ${e}`),null)}getAvailableKeys(){let e=[];for(let[t,n]of this.connectors.entries())n.data!==null&&e.push(t);return e}async fetchData(e){let{config:t}=e,{dataKey:n,url:i}=t;l.debug(`Fetching data for ${n}: ${i}`);try{let t=await r(i,{method:`GET`,headers:{Accept:`application/json`}},{maxRetries:2,baseDelayMs:2e3});if(!t.ok){l.warn(`Data connector ${n} returned ${t.status}: ${t.statusText}`);return}let a=t.headers.get(`Content-Type`)||``,o;o=a.includes(`application/json`)?await t.json():await t.text();let s=e.data;e.data=o,e.lastFetch=Date.now(),e.failures=0,l.debug(`Data updated for ${n} (fetched at ${new Date(e.lastFetch).toISOString()})`),this._ensureNormalPolling(e),this.emit(`data-updated`,n,o),JSON.stringify(s)!==JSON.stringify(o)&&this.emit(`data-changed`,n,o)}catch(r){if(e.failures=(e.failures||0)+1,l.error(`Failed to fetch data for ${n} (${e.failures}x):`,r),this.emit(`fetch-error`,n,r),e.failures>=d&&e.timer){let r=(t.updateInterval||300)*1e3,i=Math.min(r*2**(e.failures-d+1),u);clearInterval(e.timer),e.timer=setTimeout(()=>{this.fetchData(e).catch(()=>{}),e.timer=setInterval(()=>{this.fetchData(e).catch(()=>{})},i)},i),l.warn(`Circuit breaker: ${n} backing off to ${Math.round(i/1e3)}s`)}}}_ensureNormalPolling(e){if(e.failures===0&&e.timer){let t=(e.config.updateInterval||300)*1e3;clearInterval(e.timer),clearTimeout(e.timer),e.timer=setInterval(()=>{this.fetchData(e).catch(()=>{})},t)}}refreshAll(){this.connectors.size!==0&&(l.info(`Refreshing all ${this.connectors.size} data connector(s)`),this.stopPolling(),this.startPolling())}cleanup(){this.stopPolling(),this.connectors.clear(),this.removeAllListeners(),l.debug(`DataConnectorManager cleaned up`)}},p=i(`Blacklist`),m=class{constructor(e=3){this._entries=new Map,this._threshold=e}recordFailure(e,t){let n=Number(e),r=this._entries.get(n)||{failures:0,blacklisted:!1,reason:``};return r.failures++,r.reason=t,!r.blacklisted&&r.failures>=this._threshold?(r.blacklisted=!0,p.warn(`Layout ${n} blacklisted after ${r.failures} consecutive failures: ${t}`)):r.blacklisted||p.info(`Layout ${n} failure ${r.failures}/${this._threshold}: ${t}`),this._entries.set(n,r),{blacklisted:r.blacklisted,failures:r.failures}}recordSuccess(e){let t=Number(e);if(!this._entries.has(t))return!1;let n=this._entries.get(t);return this._entries.delete(t),n.blacklisted?(p.info(`Layout ${t} removed from blacklist (rendered successfully)`),!0):!1}isBlacklisted(e){return this._entries.get(Number(e))?.blacklisted===!0}getBlacklistedIds(){let e=[];for(let[t,n]of this._entries)n.blacklisted&&e.push(t);return e}reset(){let e=this._entries.size;return e>0&&(p.info(`Blacklist reset (${e} entries cleared)`),this._entries.clear()),e}get size(){return this._entries.size}},h=Object.freeze({COLLECTION_START:`collection-start`,COLLECTION_COMPLETE:`collection-complete`,COLLECTION_ERROR:`collection-error`,REGISTER_COMPLETE:`register-complete`,SCHEDULE_RECEIVED:`schedule-received`,LAYOUTS_SCHEDULED:`layouts-scheduled`,NO_LAYOUTS_SCHEDULED:`no-layouts-scheduled`,TIMELINE_UPDATED:`timeline-updated`,LAYOUT_PREPARE_REQUEST:`layout-prepare-request`,LAYOUT_EXPIRE_CURRENT:`layout-expire-current`,LAYOUT_ALREADY_PLAYING:`layout-already-playing`,CHECK_PENDING_LAYOUT:`check-pending-layout`,FILES_RECEIVED:`files-received`,DOWNLOAD_REQUEST:`download-request`,OVERLAY_LAYOUT_REQUEST:`overlay-layout-request`,REVERT_TO_SCHEDULE:`revert-to-schedule`,SYNC_CONFIG:`sync-config`,XMR_CONNECTED:`xmr-connected`,XMR_RECONNECTED:`xmr-reconnected`,XMR_MISCONFIGURED:`xmr-misconfigured`,NAVIGATE_TO_WIDGET:`navigate-to-widget`,EXECUTE_NATIVE_COMMAND:`execute-native-command`,SCHEDULED_COMMAND:`scheduled-command`,COMMAND_RESULT:`command-result`,SCREENSHOT_REQUEST:`screenshot-request`,SUBMIT_STATS_REQUEST:`submit-stats-request`,SUBMIT_LOGS_REQUEST:`submit-logs-request`,SUBMIT_FAULTS_REQUEST:`submit-faults-request`,CACHE_ANALYSIS:`cache-analysis`,COLLECTION_INTERVAL_SET:`collection-interval-set`,COLLECTION_INTERVAL_UPDATED:`collection-interval-updated`,LOG_LEVEL_CHANGED:`log-level-changed`,OFFLINE_MODE:`offline-mode`,PURGE_REQUEST:`purge-request`,PURGE_ALL_REQUEST:`purge-all-request`}),g=i(`PlayerCore`);async function _(){if(typeof window<`u`&&window.electronAPI?.getLanIpAddress)try{return await window.electronAPI.getLanIpAddress()}catch{}try{let e=await(globalThis.__nativeFetch||globalThis.fetch)(`/system/lan-ip`);if(e.ok){let{ip:t}=await e.json();if(t)return t}}catch{}return``}var v=`xibo-offline-cache`,y=1,b=`cache`;function x(e){return n(e?`${v}-${e}`:v,y,b)}var S=class extends e{constructor(e){super(),this.config=e.config,this.xmds=e.xmds,this.cache=e.cache,this.schedule=e.schedule,this.renderer=e.renderer,this.XmrWrapper=e.xmrWrapper,this.statsCollector=e.statsCollector,this.displaySettings=e.displaySettings,this._cmsId=e.cmsId||null,this.dataConnectorManager=new f,_().then(e=>{this._lanIpAddress=e,g.info(`LAN IP:`,e||`(not discovered)`)}),this.xmr=null,this.currentLayoutId=null,this.collecting=!1,this.collectionInterval=null,this.pendingLayouts=new Map,this._layoutMediaStatus=new Map,this.offlineMode=!1,this._normalCollectInterval=null,this._offlineRetrySeconds=0,this._lastCheckRf=null,this._lastCheckSchedule=null,this._lastTimelineFingerprint=null,this._lastTimeline=null,this._layoutOverride=null,this._lastRequiredFiles=[],this._executedCommands=new Set,this.displayCommands=null,this._faultReportingInterval=null,this._faultReportingSeconds=60,this._layoutBlacklist=new m(3),this._lastLayoutChangeTime=null,this._statusCode=2,this._dynamicLayouts=new Set,this.syncConfig=null,this.syncManager=null,this._layoutDurations=new Map,this._finalDurations=new Set,this._preparingLayoutId=null,this.cacheAnalyzer=this.cache?new a(this.cache):null,this._offlineCache={schedule:null,settings:null,requiredFiles:null},this._offlineDbReady=this._initOfflineCache()}get _queueOptions(){return{dynamicLayouts:this._dynamicLayouts}}_scheduleAutoRevert(e,t,n){t>0&&setTimeout(()=>{this._layoutOverride?.layoutId===e&&(g.info(`${n} duration expired (${t}s), reverting to schedule`),this.revertToSchedule())},t*1e3)}async _initOfflineCache(){try{let e=await x(this._cmsId),t=e.transaction(b,`readonly`).objectStore(b),[n,r,i,a,o,s]=await Promise.all([new Promise(e=>{let n=t.get(`schedule`);n.onsuccess=()=>e(n.result??null),n.onerror=()=>e(null)}),new Promise(e=>{let n=t.get(`settings`);n.onsuccess=()=>e(n.result??null),n.onerror=()=>e(null)}),new Promise(e=>{let n=t.get(`requiredFiles`);n.onsuccess=()=>e(n.result??null),n.onerror=()=>e(null)}),new Promise(e=>{let n=t.get(`durations`);n.onsuccess=()=>e(n.result??null),n.onerror=()=>e(null)}),new Promise(e=>{let n=t.get(`finalDurations`);n.onsuccess=()=>e(n.result??null),n.onerror=()=>e(null)}),new Promise(e=>{let n=t.get(`durationsVersion`);n.onsuccess=()=>e(n.result??null),n.onerror=()=>e(null)})]);if(Array.isArray(a)&&a.length>0){for(let[e,t]of a)this._layoutDurations.set(e,t);g.info(`[Timeline] Restored ${a.length} cached durations from IDB`)}if(s>=2&&Array.isArray(o)&&o.length>0){for(let e of o)this._finalDurations.add(e);g.info(`[Timeline] Restored ${o.length} final duration keys from IDB`)}else Array.isArray(o)&&o.length>0&&g.info(`[Timeline] Discarded ${o.length} stale final duration keys (pre-v2)`);this._offlineCache={schedule:n,settings:r,requiredFiles:i},this._offlineDb=e,g.info(`Offline cache loaded from IndexedDB`,n?`(has schedule)`:`(empty)`)}catch(e){g.warn(`Failed to load offline cache from IndexedDB:`,e)}}async _offlineSave(e,t){this._offlineCache[e]=t;try{this._offlineDb||=await x(this._cmsId);let n=this._offlineDb.transaction(b,`readwrite`);n.objectStore(b).put(t,e),await new Promise((e,t)=>{n.oncomplete=e,n.onerror=()=>t(n.error)})}catch(t){this._offlineDb=null,g.warn(`Failed to save offline cache:`,e,t)}}hasCachedData(){return this._offlineCache.schedule!==null}isOffline(){return typeof navigator<`u`&&navigator.onLine===!1}isInOfflineMode(){return this.offlineMode}collectOffline(){if(g.warn(`Offline mode — using cached schedule`),this.offlineMode||(this.offlineMode=!0,this.emit(h.OFFLINE_MODE,!0)),this.collectionInterval&&(this._normalCollectInterval?this._offlineRetrySeconds=Math.min(this._offlineRetrySeconds*2,this._normalCollectInterval):(this._normalCollectInterval=this._currentCollectInterval,this._offlineRetrySeconds=30),this._setCollectionTimer(this._offlineRetrySeconds),g.info(`Offline: retry in ${this._offlineRetrySeconds}s`)),!this.collectionInterval){let e=this._offlineCache.settings;e?.settings&&(this.setupCollectionInterval(e.settings),this._normalCollectInterval=this._currentCollectInterval,this._offlineRetrySeconds=30,this._setCollectionTimer(this._offlineRetrySeconds),g.info(`Offline: retry in ${this._offlineRetrySeconds}s`))}let e=this._offlineCache.schedule;e&&(this.schedule.setSchedule(e),this.emit(h.SCHEDULE_RECEIVED,e));let t=this.schedule.getCurrentLayouts();g.info(`Offline layouts:`,t),this.emit(h.LAYOUTS_SCHEDULED,t),this._evaluateAndSwitchLayout(t,`Offline`),this.emit(h.COLLECTION_COMPLETE)}_evaluateAndSwitchLayout(e,t){let n=t?`${t}: `:``,{queue:r}=this.schedule.getScheduleQueue(this._layoutDurations,this._queueOptions);if(r.length>0)if(this.currentLayoutId)r.some(e=>o(e.layoutId)===this.currentLayoutId)?(g.info(`Layout ${this.currentLayoutId} playing — queue updated in background, playback continues`),this.emit(h.LAYOUT_ALREADY_PLAYING,this.currentLayoutId)):(g.info(`Layout ${this.currentLayoutId} no longer in queue — expiring`),this.currentLayoutId=null,this.emit(h.LAYOUT_EXPIRE_CURRENT));else if(this._preparingLayoutId)g.info(`${n}layout ${this._preparingLayoutId} already being prepared, skipping`);else{let e=this.getNextLayout();e&&(this._preparingLayoutId=e.layoutId,g.info(`${n}switching to layout ${e.layoutId}`),this.emit(h.LAYOUT_PREPARE_REQUEST,e.layoutId))}else g.info(`${t?`${t}: n`:`N`}o layouts${t?` in cached schedule`:` scheduled, falling back to default`}`),this.emit(h.NO_LAYOUTS_SCHEDULED);this.logUpcomingTimeline()}async collectNow(){return this._lastCheckRf=null,this._lastCheckSchedule=null,this.collect()}async collect(){if(this.collecting){g.debug(`Collection already in progress, skipping`);return}this.collecting=!0;try{if(await this._offlineDbReady,g.info(`Starting collection cycle...`),this.emit(h.COLLECTION_START),this.isOffline()){if(this.hasCachedData())return this.collecting=!1,this.collectOffline();throw Error(`Offline with no cached data — cannot start playback`)}this.config.ensureXmrKeyPair&&await this.config.ensureXmrKeyPair(),g.debug(`Collection step: registerDisplay`);let e=await this.xmds.registerDisplay();g.info(`Display registered: ${e.code}${e.tags?.length?`, tags: ${e.tags.join(`, `)}`:``}`),g.debug(`Register result:`,JSON.stringify(e)),this._processRegistration(e),g.debug(`Collection step: initializeXmr`),await this.initializeXmr(e);let t=e.checkRf||``,n=e.checkSchedule||``;if(!this._lastCheckRf||this._lastCheckRf!==t){this.resetBlacklist(),g.debug(`Collection step: requiredFiles`);let e=await this.xmds.requiredFiles(),r=e.files||e,i=e.purge||[];if(g.info(`Required files:`,r.length,i.length>0?`(+ ${i.length} purge)`:``),this._lastCheckRf=t,this.emit(h.FILES_RECEIVED,r),this._offlineSave(`requiredFiles`,e),i.length>0&&this.emit(h.PURGE_REQUEST,i),!this._lastCheckSchedule||this._lastCheckSchedule!==n){g.debug(`Collection step: schedule`);let e=await this.xmds.schedule();g.info(`Schedule received`),this._lastCheckSchedule=n,g.debug(`Collection step: processing schedule`),this._applyNewSchedule(e),this.logUpcomingTimeline()}g.debug(`Collection step: download-request + mediaInventory`),this.schedule.getCurrentLayouts();let{queue:a}=this.schedule.getScheduleQueue(this._layoutDurations,this._queueOptions),s=[...new Set(a.map(e=>o(e.layoutId)))];if(this._lastRequiredFiles=r,this.displaySettings?.isInDownloadWindow&&!this.displaySettings.isInDownloadWindow()){let e=this.displaySettings.getNextDownloadWindow?.();g.info(`Outside download window, skipping downloads${e?` (next: ${e.toLocaleTimeString()})`:``}`)}else this.emit(h.DOWNLOAD_REQUEST,{layoutOrder:s,files:r,layoutDependants:Object.fromEntries(this.schedule.getDependantsMap())});this.cacheAnalyzer&&this.cacheAnalyzer.analyze(r).then(e=>{this.emit(h.CACHE_ANALYSIS,e)}).catch(e=>g.warn(`Cache analysis failed:`,e)),this.submitMediaInventory(r)}else if(t&&g.info(`RequiredFiles CRC unchanged, skipping download check`),this._lastCheckSchedule!==n){let e=await this.xmds.schedule();g.info(`Schedule received (RF unchanged but schedule changed)`),this._lastCheckSchedule=n,this._applyNewSchedule(e)}else n&&g.info(`Schedule CRC unchanged, skipping`);await this._fetchWeatherData(),g.debug(`Collection step: evaluateSchedule`);let r=this.schedule.getCurrentLayouts();g.info(`Current layouts:`,r),this.emit(h.LAYOUTS_SCHEDULED,r),this._evaluateAndSwitchLayout(r,``),this._processScheduledCommands(),(e.settings?.statsEnabled===`On`||e.settings?.statsEnabled===`1`)&&(this.statsCollector?(g.info(`Stats enabled, submitting proof of play`),this.emit(h.SUBMIT_STATS_REQUEST)):g.warn(`Stats enabled but no StatsCollector provided`)),this.emit(h.SUBMIT_LOGS_REQUEST),this.emit(h.SUBMIT_FAULTS_REQUEST),!this.collectionInterval&&e.settings&&this.setupCollectionInterval(e.settings),this._faultReportingInterval||this._startFaultReportingAgent(),this.logUpcomingTimeline(),this.emit(h.COLLECTION_COMPLETE)}catch(e){if(this.hasCachedData())return g.warn(`Collection failed, falling back to cached data:`,e?.message||e),this.emit(h.COLLECTION_ERROR,e),this.collecting=!1,this.collectOffline();throw g.error(`Collection error:`,e),this.emit(h.COLLECTION_ERROR,e),e}finally{this.collecting=!1}}_processRegistration(e){if(this._offlineSave(`settings`,e),this.offlineMode&&(this.offlineMode=!1,g.info(`Back online — resuming normal collection`),this.emit(h.OFFLINE_MODE,!1),this._normalCollectInterval&&(this._setCollectionTimer(this._normalCollectInterval),this._normalCollectInterval=null,this._offlineRetrySeconds=0)),this.displaySettings&&e.settings){let n=this.displaySettings.applySettings(e.settings);n.changed.includes(`collectInterval`)&&this.updateCollectionInterval(n.settings.collectInterval),e.settings.logLevel&&t(e.settings.logLevel)&&(g.info(`Log level updated from CMS:`,e.settings.logLevel),this.emit(h.LOG_LEVEL_CHANGED,e.settings.logLevel))}if(this.schedule?.setDisplayProperties&&e.settings&&this.schedule.setDisplayProperties(e.settings),e.syncConfig){let t=JSON.stringify(e.syncConfig);t!==this._lastRawSyncConfig&&(this._lastRawSyncConfig=t,this.syncConfig=e.syncConfig,g.info(`Sync group:`,e.syncConfig.isLead?`LEAD`:`follower → ${e.syncConfig.syncGroup}`,`(switchDelay: ${e.syncConfig.syncSwitchDelay}ms, videoPauseDelay: ${e.syncConfig.syncVideoPauseDelay}ms)`),this.emit(h.SYNC_CONFIG,e.syncConfig))}if(this._applyTagConfig(e.tags),e.commands&&e.commands.length>0){this.displayCommands={};for(let t of e.commands)this.displayCommands[t.commandCode]=t;g.debug(`Display commands:`,Object.keys(this.displayCommands).join(`, `))}this.emit(h.REGISTER_COMPLETE,e)}_applyNewSchedule(e){this.emit(h.SCHEDULE_RECEIVED,e),this.schedule.setSchedule(e),this._executedCommands.clear(),this.updateDataConnectors(),this._offlineSave(`schedule`,e)}async initializeXmr(e){let t=e.settings?.xmrWebSocketAddress||e.settings?.xmrNetworkAddress;if(!t){g.warn(`XMR not configured: no xmrWebSocketAddress or xmrNetworkAddress in CMS settings`),this.emit(h.XMR_MISCONFIGURED,{reason:`missing`,message:`XMR address not configured in CMS. Go to CMS Admin → Settings → Configuration → XMR and set the WebSocket address.`});return}if(t.startsWith(`tcp://`)){g.warn(`XMR address uses tcp:// protocol which is not supported by PWA players: ${t}`),g.warn(`Configure XMR_WS_ADDRESS in CMS Admin → Settings → Configuration → XMR (e.g. wss://your-domain/xmr)`),this.emit(h.XMR_MISCONFIGURED,{reason:`wrong-protocol`,url:t,message:`XMR uses tcp:// protocol (not supported by PWA). Set XMR WebSocket Address to wss://your-domain/xmr in CMS Settings.`});return}if(/example\.(org|com|net)/i.test(t)){g.warn(`XMR address contains placeholder domain: ${t}`),g.warn(`Configure the real XMR address in CMS Admin → Settings → Configuration → XMR`),this.emit(h.XMR_MISCONFIGURED,{reason:`placeholder`,url:t,message:`XMR address is still the default placeholder (${t}). Update it in CMS Settings.`});return}let n=e.settings?.xmrCmsKey||e.settings?.serverKey||this.config.serverKey;g.debug(`XMR CMS Key:`,n?`present`:`missing`),this.xmr?this.xmr.isConnected()?g.debug(`XMR already connected`):(g.info(`XMR disconnected, attempting to reconnect...`),await this.xmr.start(t,n),this.emit(h.XMR_RECONNECTED,t)):(g.info(`Initializing XMR WebSocket:`,t),this.xmr=new this.XmrWrapper(this.config,this),await this.xmr.start(t,n),this.emit(h.XMR_CONNECTED,t))}setupCollectionInterval(e){let t=this.displaySettings?this.displaySettings.getCollectInterval():parseInt(e.collectInterval||`300`,10);this._setCollectionTimer(t),this.emit(h.COLLECTION_INTERVAL_SET,t)}updateCollectionInterval(e){this.collectionInterval&&(this._setCollectionTimer(e),this.emit(h.COLLECTION_INTERVAL_UPDATED,e))}_startFaultReportingAgent(){this._faultReportingInterval&&clearInterval(this._faultReportingInterval),g.info(`Fault reporting agent started (interval: ${this._faultReportingSeconds}s)`),this._faultReportingInterval=setInterval(()=>{this.emit(h.SUBMIT_FAULTS_REQUEST)},this._faultReportingSeconds*1e3)}_setCollectionTimer(e){this.collectionInterval&&clearInterval(this.collectionInterval),this._currentCollectInterval=e,g.info(`Collection interval: ${e}s`),this.collectionInterval=setInterval(()=>{g.debug(`Running scheduled collection cycle...`),this.collect().catch(e=>{g.error(`Collection error:`,e),this.emit(h.COLLECTION_ERROR,e)})},e*1e3)}async requestLayoutChange(e){g.info(`Layout change requested: ${e}`),this.currentLayoutId=null,this.emit(`layout-change-requested`,e)}clearPreparingLayout(){this._preparingLayoutId=null}setCurrentLayout(e){this.currentLayoutId=e,this._preparingLayoutId=null,this._lastLayoutChangeTime=new Date().toISOString(),this._statusCode=1,this.pendingLayouts.delete(e),this._layoutMediaStatus.delete(`${e}.xlf`),this.emit(`layout-current`,e),this._lastTimelineFingerprint=null,this.logUpcomingTimeline()}setPendingLayout(e,t){this.pendingLayouts.set(e,t),this.emit(`layout-pending`,e,t)}clearCurrentLayout(){this.currentLayoutId=null,this.emit(`layout-cleared`)}getNextLayout(){let e=this.schedule.popNextFromQueue(this._layoutDurations,this._queueOptions);if(!e){let e=this.schedule.schedule?.default;return e?{layoutId:o(e),layoutFile:e}:null}let t=o(e.layoutId);if(this.isLayoutBlacklisted(t)){let{queue:e}=this.schedule.getScheduleQueue(this._layoutDurations,this._queueOptions);for(let t=0;t<e.length-1;t++){let e=this.schedule.popNextFromQueue(this._layoutDurations,this._queueOptions);if(e){let t=o(e.layoutId);if(!this.isLayoutBlacklisted(t))return{layoutId:t,layoutFile:e.layoutId}}}g.warn(`All queued layouts are blacklisted, using current entry as fallback`)}return{layoutId:t,layoutFile:e.layoutId}}peekNextLayout(){let e=this.schedule.peekNextInQueue(this._layoutDurations,this._queueOptions);if(!e)return null;let t=o(e.layoutId);if(t===this.currentLayoutId){let e=this.schedule.peekAfterNext(this._layoutDurations,this._queueOptions);if(!e)return null;let t=o(e.layoutId);return t===this.currentLayoutId||this.isLayoutBlacklisted(t)?null:{layoutId:t,layoutFile:e.layoutId}}return this.isLayoutBlacklisted(t)?null:{layoutId:t,layoutFile:e.layoutId}}advanceToNextLayout(){if(this._layoutOverride){g.info(`Layout override active, not advancing schedule`);return}let e=this.getNextLayout();if(!e){if(this.currentLayoutId){g.info(`No layouts in queue, replaying ${this.currentLayoutId} to avoid blank screen`);let e=this.currentLayoutId;this.currentLayoutId=null,this._preparingLayoutId=e,this.emit(h.LAYOUT_PREPARE_REQUEST,e)}else g.info(`No layouts scheduled during advance`),this.emit(h.NO_LAYOUTS_SCHEDULED);return}let{layoutId:t,layoutFile:n}=e,r=this._layoutDurations.get(n)||`?`;if(this._lastTimeline&&this._lastTimeline.length>0){let e=this._lastTimeline.slice(0,2).map(e=>{let t=e.startTime.toLocaleTimeString(`en-GB`,{hour:`2-digit`,minute:`2-digit`,second:`2-digit`});return`${e.layoutFile}(${e.duration}s@${t})`});g.debug(`[Timeline] Layout transition: entering ${n} (${r}s), overlay top: [${e.join(`, `)}]`),this._lastTimeline[0].layoutFile!==n&&g.warn(`[Timeline] Mismatch: entering ${n} but overlay expects ${this._lastTimeline[0].layoutFile}`)}else g.debug(`[Timeline] Layout transition: entering ${n} (${r}s), no timeline data`);if(this.syncManager&&this.schedule.isSyncEvent(n))if(this.isSyncLead()){g.info(`[Sync] Lead requesting coordinated layout change: ${t}`),this._preparingLayoutId=t,this.emit(h.LAYOUT_PREPARE_REQUEST,t),this.syncManager.requestLayoutChange(t).catch(e=>{g.error(`[Sync] Layout change failed:`,e)});return}else if(this.syncManager.transport?.connected){g.info(`[Sync] Follower waiting for lead signal (not advancing independently)`);return}else g.warn(`[Sync] Follower: lead unreachable, advancing independently`);t===this.currentLayoutId&&(g.info(`Next layout ${t} is same as current, triggering replay`),this.currentLayoutId=null);let{queue:i}=this.schedule.getScheduleQueue(this._layoutDurations,this._queueOptions),a=this.schedule.getQueuePosition();g.info(`Advancing to layout ${t} (queue pos ${a}/${i.length})`),this._preparingLayoutId=t,this.emit(h.LAYOUT_PREPARE_REQUEST,t)}advanceToPreviousLayout(){if(this._layoutOverride){g.info(`Layout override active, not going back`);return}let{queue:e}=this.schedule.getScheduleQueue(this._layoutDurations,this._queueOptions);if(e.length<=1){g.info(`Single or empty queue, nothing to go back to`);return}let t=this.schedule.rewindQueue(2,this._layoutDurations,this._queueOptions);if(!t)return;let n=o(t.layoutId);if(n===this.currentLayoutId){g.info(`Previous layout is same as current, nothing to go back to`);return}g.info(`Going back to layout ${n}`),this.emit(h.LAYOUT_PREPARE_REQUEST,n)}notifyMediaReady(e,t=`media`){g.debug(`File ${e} ready (${t})`);for(let[n,r]of this.pendingLayouts.entries()){let i=t===`layout`&&n===parseInt(e),a=t===`media`&&r.includes(e);(i||a)&&(g.debug(`${t} ${e} was needed by pending layout ${n}, checking if ready...`),this.emit(h.CHECK_PENDING_LAYOUT,n,r))}}async notifyLayoutStatus(e){try{let t={currentLayoutId:e,deviceName:this.config?.displayName||``,displayName:this.config?.displayName||``,lastCommandSuccess:this._lastCommandSuccess??!0,code:this._statusCode,lastLayoutChangeTime:this._lastLayoutChangeTime||new Date().toISOString()};this.config?.latitude&&(t.latitude=this.config.latitude),this.config?.longitude&&(t.longitude=this.config.longitude),this._lanIpAddress&&(t.lanIpAddress=this._lanIpAddress),await this.xmds.notifyStatus(t),this.emit(`status-notified`,e)}catch(t){g.warn(`Failed to notify status:`,t),this.emit(`status-notify-failed`,e,t)}}reportGeoLocation(e){let t=parseFloat(e?.latitude),n=parseFloat(e?.longitude);if(isNaN(t)||isNaN(n)){g.warn(`reportGeoLocation: invalid coordinates`,e);return}g.info(`Geo location from CMS: ${t.toFixed(4)}, ${n.toFixed(4)}`),this.schedule?.setLocation&&this.schedule.setLocation(t,n),this.emit(`location-updated`,{latitude:t,longitude:n,source:`cms`}),this.checkSchedule()}async requestGeoLocation(){if(this._geoCache&&Date.now()-this._geoCache.ts<1800*1e3)return this._geoCache.location;if(!this._browserGeoFailed){let e=await this._tryBrowserGeolocation();if(e)return this._cacheGeo(this._applyLocation(e.latitude,e.longitude,`browser`));this._browserGeoFailed=!0}let e=this.config?.googleGeoApiKey;if(e){let t=await this._tryGoogleGeolocation(e);if(t)return this._cacheGeo(this._applyLocation(t.latitude,t.longitude,`google-api`))}let t=await this._tryIpGeolocation();return t?this._cacheGeo(this._applyLocation(t.latitude,t.longitude,`ip-geolocation`)):(g.warn(`All geolocation methods failed`),null)}_cacheGeo(e){return this._geoCache={location:e,ts:Date.now()},e}_applyTagConfig(e){if(!Array.isArray(e)||e.length===0)return;let t={geoApiKey:`googleGeoApiKey`};for(let n of e){let e=n.indexOf(`|`);if(e===-1)continue;let r=n.substring(0,e),i=n.substring(e+1),a=t[r];a&&i&&this.config&&(g.info(`Config from CMS tag: ${r} → ${a}`),this.config[a]=i)}}_applyLocation(e,t,n){return g.info(`Geolocation (${n}): ${e.toFixed(4)}, ${t.toFixed(4)}`),this.schedule?.setLocation&&this.schedule.setLocation(e,t),this.emit(`location-updated`,{latitude:e,longitude:t,source:n}),this.checkSchedule(),{latitude:e,longitude:t}}async _tryBrowserGeolocation(){if(typeof navigator>`u`||!navigator.geolocation)return null;try{let e=await new Promise((e,t)=>{navigator.geolocation.getCurrentPosition(e,t,{timeout:1e4,maximumAge:3e5,enableHighAccuracy:!1})});return{latitude:e.coords.latitude,longitude:e.coords.longitude}}catch(e){return g.warn(`Browser geolocation failed:`,e?.message||e),null}}async _tryGoogleGeolocation(e){try{let t=await fetch(`https://www.googleapis.com/geolocation/v1/geolocate?key=${e}`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({considerIp:!0}),signal:AbortSignal.timeout(5e3)});if(!t.ok)return g.warn(`Google Geolocation API returned ${t.status}`),null;let n=await t.json();return n.location?.lat!=null&&n.location?.lng!=null?{latitude:n.location.lat,longitude:n.location.lng}:null}catch(e){return g.warn(`Google Geolocation API failed:`,e?.message||e),null}}async _tryIpGeolocation(){let e=[{url:`https://ipapi.co/json/`,parse:e=>e.latitude!=null&&e.longitude!=null?{latitude:e.latitude,longitude:e.longitude}:null},{url:`https://freeipapi.com/api/json`,parse:e=>e.latitude!=null&&e.longitude!=null?{latitude:e.latitude,longitude:e.longitude}:null}];for(let t of e)try{let e=await fetch(t.url,{signal:AbortSignal.timeout(5e3)});if(!e.ok)continue;let n=await e.json(),r=t.parse(n);if(r)return r}catch(e){g.warn(`IP geolocation (${t.url}) failed:`,e?.message||e)}return null}checkSchedule(){let e=this.schedule.getCurrentLayouts();this.emit(h.LAYOUTS_SCHEDULED,e),this._evaluateAndSwitchLayout(e,``)}async captureScreenshot(){g.info(`Screenshot requested`),this.emit(h.SCREENSHOT_REQUEST)}async changeLayout(e,t){g.info(`Layout change requested via XMR:`,e);let n=parseInt(e,10),r=t?.duration||0;this._layoutOverride={layoutId:n,type:`change`,duration:r,changeMode:t?.changeMode||`replace`},this.currentLayoutId=null,this.emit(h.LAYOUT_PREPARE_REQUEST,n),this._scheduleAutoRevert(n,r,`Layout override`)}async overlayLayout(e,t){g.info(`Overlay layout requested via XMR:`,e);let n=parseInt(e,10),r=t?.duration||0;this._layoutOverride={layoutId:n,type:`overlay`,duration:r},this.emit(h.OVERLAY_LAYOUT_REQUEST,n),this._scheduleAutoRevert(n,r,`Overlay`)}async revertToSchedule(){g.info(`Reverting to scheduled content`),this._layoutOverride=null,this.currentLayoutId=null,this.emit(h.REVERT_TO_SCHEDULE);let e=this.schedule.getCurrentLayouts();if(e.length>0){let t=e[0],n=o(t);this.emit(h.LAYOUT_PREPARE_REQUEST,n)}else this.emit(h.NO_LAYOUTS_SCHEDULED)}async purgeAll(){return g.info(`Purge all cache requested via XMR`),this._lastCheckRf=null,this._lastCheckSchedule=null,this.emit(h.PURGE_ALL_REQUEST),this.collectNow()}async executeCommand(e,t){if(g.info(`Execute command requested:`,e),!t||!t[e]){g.warn(`Unknown command code:`,e),this._lastCommandSuccess=!1,this.emit(h.COMMAND_RESULT,{code:e,success:!1,reason:`Unknown command`});return}let n=t[e],r=n.commandString||n.value||``;if(r.startsWith(`http|`)){let t=r.split(`|`),n=t[1],i=t[2]||`application/json`;try{let t=await fetch(n,{method:`POST`,headers:{"Content-Type":i},signal:AbortSignal.timeout(1e4)}),r=t.ok;this._lastCommandSuccess=r,g.info(`HTTP command ${e} result: ${t.status}`),this.emit(h.COMMAND_RESULT,{code:e,success:r,status:t.status})}catch(t){this._lastCommandSuccess=!1,g.error(`HTTP command ${e} failed:`,t),this.emit(h.COMMAND_RESULT,{code:e,success:!1,reason:t.message})}}else g.info(`Delegating non-HTTP command to platform layer:`,e),this.emit(h.EXECUTE_NATIVE_COMMAND,{code:e,commandString:r})}triggerWebhook(e){g.info(`Webhook trigger from XMR:`,e),this.handleTrigger(e)}refreshDataConnectors(){g.info(`Data connector refresh requested via XMR`),this.dataConnectorManager.refreshAll(),this.emit(`data-connectors-refreshed`)}async submitMediaInventory(e){if(!(!e||e.length===0))try{let t=Math.floor(Date.now()/1e3),n=`<files>${e.filter(e=>[`media`,`layout`,`resource`,`dependency`,`widget`].includes(e.type)).map(e=>{let n=e.complete===void 0||e.complete?`1`:`0`,r=e.fileType?` fileType="${e.fileType}"`:``;return`<file type="${e.type}" id="${e.id}" complete="${n}" md5="${e.md5||``}" lastChecked="${t}"${r}/>`}).join(``)}</files>`;await this.xmds.mediaInventory(n),g.info(`Media inventory submitted: ${e.length} files`),this.emit(`media-inventory-submitted`,e.length)}catch(e){g.warn(`MediaInventory submission failed:`,e)}}async blackList(e,t,n){try{await this.xmds.blackList(e,t,n),this.emit(`media-blacklisted`,{mediaId:e,type:t,reason:n})}catch(e){g.warn(`BlackList failed:`,e)}}reportLayoutFailure(e,t){let n=Number(e);this._statusCode=3;let{blacklisted:r,failures:i}=this._layoutBlacklist.recordFailure(n,t);r&&i===3&&(this.emit(`layout-blacklisted`,{layoutId:n,reason:t,failures:i}),this.blackList(n,`layout`,t))}reportLayoutSuccess(e){this._layoutBlacklist.recordSuccess(Number(e))&&this.emit(`layout-unblacklisted`,{layoutId:Number(e)})}isLayoutBlacklisted(e){return this._layoutBlacklist.isBlacklisted(e)}getBlacklistedLayouts(){return this._layoutBlacklist.getBlacklistedIds()}resetBlacklist(){this._layoutBlacklist.reset()>0&&this.emit(`blacklist-reset`)}isLayoutOverridden(){return this._layoutOverride!==null}handleTrigger(e){let t=this.schedule.findActionByTrigger(e);if(!t){g.debug(`No scheduled action matches trigger:`,e);return}switch(g.info(`Action triggered: ${t.actionType} (trigger: ${e})`),t.actionType){case`navLayout`:case`navigateToLayout`:t.layoutCode&&this.changeLayout(t.layoutCode);break;case`navWidget`:case`navigateToWidget`:this.emit(h.NAVIGATE_TO_WIDGET,t);break;case`command`:this.emit(`execute-command`,t.commandCode);break;default:g.warn(`Unknown action type:`,t.actionType)}}updateDataConnectors(){let e=this.schedule.getDataConnectors();e.length>0&&g.info(`Configuring ${e.length} data connector(s)`),this.dataConnectorManager.setConnectors(e),e.length>0&&(this.dataConnectorManager.startPolling(),this.emit(`data-connectors-started`,e.length))}_processScheduledCommands(){if(!this.schedule?.getCommands)return;let e=this.schedule.getCommands();if(e.length===0)return;let t=new Date;for(let n of e){if(!n.code||!n.date)continue;let e=`${n.code}|${n.date}`;if(this._executedCommands.has(e))continue;let r=new Date(n.date);if(isNaN(r.getTime())){g.warn(`Scheduled command has invalid date:`,n.date);continue}t>=r&&(g.info(`Executing scheduled command: ${n.code} (scheduled: ${n.date})`),this._executedCommands.add(e),n.code===`collectNow`?setTimeout(()=>this.collectNow().catch(e=>g.error(`collectNow command failed:`,e)),0):this.emit(h.SCHEDULED_COMMAND,n))}}async _fetchWeatherData(){if(!(!this.xmds?.getWeather||!this.schedule?.setWeatherData))try{let e=await this.xmds.getWeather(),t=typeof e==`string`?JSON.parse(e):e;this.schedule.setWeatherData(t),g.info(`Weather data updated:`,Object.keys(t).join(`, `))}catch(e){g.warn(`GetWeather failed (non-critical):`,e?.message||e)}}getDataConnectorManager(){return this.dataConnectorManager}setSyncManager(e){this.syncManager=e,g.info(`SyncManager attached:`,e.isLead?`LEAD`:`FOLLOWER`)}isInSyncGroup(){return this.syncConfig!==null}isSyncLead(){return this.syncConfig?.isLead===!0}getSyncConfig(){return this.syncConfig}logUpcomingTimeline(){if(!this.schedule.getLayoutsAtTime)return;let e=[...this._layoutDurations.entries()].sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>`${e}:${t}`).join(`|`),t=[...this._layoutMediaStatus.entries()].sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>`${e}:${t.ready}:${t.missingKey}`).join(`|`),n=[...this.pendingLayouts.keys()].sort().join(`,`),r=this.schedule.getQueuePosition()||0,i=`${this._lastCheckSchedule}|${e}|${this.currentLayoutId}|${r}|${t}|${n}`;if(i===this._lastTimelineFingerprint&&this._lastTimeline){this.emit(h.TIMELINE_UPDATED,this._lastTimeline);return}let{queue:a}=this.schedule.getScheduleQueue(this._layoutDurations,this._queueOptions),o=s(a,this.schedule.getQueuePosition(),{currentLayoutStartedAt:this._lastLayoutChangeTime?new Date(this._lastLayoutChangeTime):null,defaultLayout:this.schedule.schedule?.default||null,durations:this._layoutDurations});if(o.length===0)return;for(let e of o){let t=parseInt(e.layoutFile.replace(`.xlf`,``),10),n=this.pendingLayouts.get(t);if(n&&n.length>0)e.missingMedia=n.map(String);else{let t=this._layoutMediaStatus.get(e.layoutFile);t&&!t.ready&&t.missing.length>0&&(e.missingMedia=t.missing.map(String))}}this._lastTimelineFingerprint=i,this._lastTimeline=o;let c=o.slice(0,20).map(e=>{let t=e.startTime.toLocaleTimeString(`en-GB`,{hour:`2-digit`,minute:`2-digit`,second:`2-digit`}),n=e.endTime.toLocaleTimeString(`en-GB`,{hour:`2-digit`,minute:`2-digit`,second:`2-digit`}),r=e.missingMedia?` [MISSING: ${e.missingMedia.length} files]`:``;return` ${t}-${n} Layout ${e.layoutFile} (${e.duration}s)${e.isDefault?` [default]`:``}${r}`});for(let e of o)e.missingMedia&&g.warn(`[Timeline] Layout ${e.layoutFile}: ${e.missingMedia.length} files missing`);g.info(`[Timeline] Next ${o.length} plays:\n${c.join(`
|
|
2
2
|
`)}`),this.emit(h.TIMELINE_UPDATED,o)}setLayoutMediaStatus(e,t,n=[]){let r=this._layoutMediaStatus.get(e),i=n.slice().sort().join(`,`);r&&r.ready===t&&r.missingKey===i||(this._layoutMediaStatus.set(e,{ready:t,missing:n,missingKey:i}),this._lastTimelineFingerprint=null)}recordLayoutDuration(e,t,n=!1){let r=String(e).replace(`.xlf`,``),i=r+`.xlf`;if(this._finalDurations.has(r))return;let a=this._layoutDurations.get(e);a===t&&!n||(this._layoutDurations.set(r,t),this._layoutDurations.set(i,t),n&&(this._finalDurations.add(r),this._finalDurations.add(i)),g.debug(`[Timeline] Duration corrected: layout ${e} ${a||`?`}s → ${t}s${n?` (final)`:``}`),this.schedule.invalidateQueue(),this._timelineRecalcTimer&&clearTimeout(this._timelineRecalcTimer),this._timelineRecalcTimer=setTimeout(()=>{this._timelineRecalcTimer=null,this.logUpcomingTimeline(),this._offlineSave(`durations`,[...this._layoutDurations.entries()]),this._offlineSave(`finalDurations`,[...this._finalDurations]),this._offlineSave(`durationsVersion`,2)},500))}cleanup(){this.collectionInterval&&=(clearInterval(this.collectionInterval),null),this._faultReportingInterval&&=(clearInterval(this._faultReportingInterval),null),this._timelineRecalcTimer&&=(clearTimeout(this._timelineRecalcTimer),null),this.xmr&&=(this.xmr.stop(),null),this.syncManager&&=(this.syncManager.stop(),null),this.dataConnectorManager.cleanup(),this.emit(`cleanup-complete`),this.removeAllListeners()}getCurrentLayoutId(){return this.currentLayoutId}getLayoutDuration(e){let t=String(e);return this._layoutDurations.get(`${t}.xlf`)||this._layoutDurations.get(t)}isCollecting(){return this.collecting}getPendingLayouts(){return Array.from(this.pendingLayouts.keys())}},C=c.version;export{h as CORE_EVENTS,f as DataConnectorManager,S as PlayerCore,C as VERSION};
|
|
3
|
-
//# sourceMappingURL=src-
|
|
3
|
+
//# sourceMappingURL=src-Cx3tXAAu.js.map
|