@xiboplayer/pwa 0.7.18 → 0.7.19
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-BG2cZsg-.js → main-BmtbYkOU.js} +4 -4
- package/dist/assets/{main-BG2cZsg-.js.map → main-BmtbYkOU.js.map} +1 -1
- package/dist/assets/main-ByurY8zd.js +3 -0
- package/dist/assets/{main-CkREX2X6.js.map → main-ByurY8zd.js.map} +1 -1
- package/dist/assets/{setup-COzyFqMn.js → setup-TzYPvQkI.js} +2 -2
- package/dist/assets/{setup-COzyFqMn.js.map → setup-TzYPvQkI.js.map} +1 -1
- package/dist/assets/{src-CfCGHb7u.js → src-BbJ9IAbR.js} +2 -2
- package/dist/assets/{src-CfCGHb7u.js.map → src-BbJ9IAbR.js.map} +1 -1
- package/dist/assets/{src-IJdgG16K.js → src-BhNSI3YP.js} +2 -2
- package/dist/assets/{src-IJdgG16K.js.map → src-BhNSI3YP.js.map} +1 -1
- package/dist/assets/{src-BdgQ2CiL.js → src-CR4vHRyW.js} +2 -2
- package/dist/assets/{src-BdgQ2CiL.js.map → src-CR4vHRyW.js.map} +1 -1
- package/dist/assets/{src-C3Sg89t9.js → src-CRWze-JF.js} +2 -2
- package/dist/assets/{src-C3Sg89t9.js.map → src-CRWze-JF.js.map} +1 -1
- package/dist/assets/{src-BF_sMbmn.js → src-CSIdq4vk.js} +2 -2
- package/dist/assets/{src-BF_sMbmn.js.map → src-CSIdq4vk.js.map} +1 -1
- package/dist/assets/{src-BV-4JdnK.js → src-DAFtXxFV.js} +2 -2
- package/dist/assets/{src-BV-4JdnK.js.map → src-DAFtXxFV.js.map} +1 -1
- package/dist/assets/{src-M1enQEwh.js → src-DLAmD0IH.js} +2 -2
- package/dist/assets/{src-M1enQEwh.js.map → src-DLAmD0IH.js.map} +1 -1
- package/dist/assets/{src-DnPIj2iO.js → src-Jz8Hu7ah.js} +2 -2
- package/dist/assets/{src-DnPIj2iO.js.map → src-Jz8Hu7ah.js.map} +1 -1
- package/dist/assets/{src-BYVnjdc0.js → src-S093VuQs.js} +2 -2
- package/dist/assets/{src-BYVnjdc0.js.map → src-S093VuQs.js.map} +1 -1
- package/dist/assets/{src-BA9Y85MN.js → src-cynIeb-W.js} +2 -2
- package/dist/assets/{src-BA9Y85MN.js.map → src-cynIeb-W.js.map} +1 -1
- package/dist/assets/{sync-manager-C68deMxf.js → sync-manager-CJQQp9_5.js} +2 -2
- package/dist/assets/{sync-manager-C68deMxf.js.map → sync-manager-CJQQp9_5.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-CkREX2X6.js +0 -3
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{u as e}from"./src-
|
|
2
|
-
//# sourceMappingURL=src-
|
|
1
|
+
import{u as e}from"./src-CRWze-JF.js";var t={name:`@xiboplayer/xmr`,version:`0.7.19`,description:`XMR WebSocket client for real-time Xibo CMS commands`,type:`module`,main:`./src/index.js`,types:`./src/index.d.ts`,exports:{".":`./src/index.js`},scripts:{test:`vitest run`,"test:watch":`vitest`,"test:coverage":`vitest run --coverage`},dependencies:{"@xiboplayer/utils":`workspace:*`},devDependencies:{vitest:`^4.1.2`},keywords:[`xibo`,`digital-signage`,`xmr`,`websocket`,`real-time`],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/xmr`},homepage:`https://xiboplayer.org`},n=e(`XmrClient`),r=class{constructor(e){this.channel=e,this.url=null,this.cmsKey=null,this.socket=null,this.isConnected=!1,this.isConnectionWanted=!1,this.lastMessageAt=0,this._interval=null,this._listeners=new Map}on(e,t){return this._listeners.has(e)||this._listeners.set(e,new Set),this._listeners.get(e).add(t),()=>this._listeners.get(e)?.delete(t)}emit(e,...t){let r=this._listeners.get(e);if(r)for(let i of r)try{i(...t)}catch(t){n.error(`Listener error for '${e}':`,t)}}async init(){this._interval||=setInterval(()=>{this.isConnectionWanted&&!this.isActive()&&this.start(this.url||`DISABLED`,this.cmsKey||`n/a`)},6e4)}async start(e,t){if(this.url=e,this.cmsKey=t,this.isConnectionWanted=!0,this.socket){try{this.socket.close()}catch{}this.socket=null,this.isConnected=!1}try{this.socket=new WebSocket(e)}catch{this.emit(`error`,`Failed to connect`);return}this.socket.addEventListener(`open`,()=>{this.socket.send(JSON.stringify({type:`init`,key:this.cmsKey,channel:this.channel})),this.isConnected=!0,this.lastMessageAt=Date.now(),this.emit(`connected`)}),this.socket.addEventListener(`close`,()=>{this.isConnected=!1,this.emit(`disconnected`)}),this.socket.addEventListener(`error`,()=>{this.emit(`error`,`error`)}),this.socket.addEventListener(`message`,e=>{if(this.lastMessageAt=Date.now(),e.data!==`H`)try{let t=JSON.parse(e.data);if(!t.action)return;if(t.createdDt&&t.ttl){let e=Date.parse(t.createdDt);if(!isNaN(e)&&e+parseInt(t.ttl)*1e3<Date.now())return}this.emit(t.action,t)}catch(e){n.error(`Failed to parse message:`,e)}})}async stop(){this.isConnectionWanted=!1,this._interval&&=(clearInterval(this._interval),null),this.socket&&(this.socket.close(),this.socket=null,this.isConnected=!1)}async send(e,t){if(!this.socket||!this.isConnected)throw Error(`Not connected`);this.socket.send(JSON.stringify({action:e,...t}))}isActive(){return this.isConnected&&Date.now()-this.lastMessageAt<900*1e3}},i=e(`XMR`),a=class{constructor(e,t){this.config=e,this.player=t,this.xmr=null,this.connected=!1}async start(e,t){try{return this.xmr||(i.info(`Initializing connection to:`,e),this.xmr=new r(this.config.xmrChannel||`player-${this.config.hardwareKey}`),this.setupEventHandlers(),await this.xmr.init()),await this.xmr.start(e,t),this.connected=!0,i.info(`Connected successfully`),!0}catch(e){return i.warn(`Failed to start:`,e.message),i.info(`Framework will retry automatically every 60s`),!1}}setupEventHandlers(){this.xmr&&(this.xmr.on(`connected`,()=>{i.info(`WebSocket connected`),this.connected=!0,this.player.emit?.(`xmr-status`,{connected:!0})}),this.xmr.on(`disconnected`,()=>{i.warn(`WebSocket disconnected (framework will reconnect)`),this.connected=!1,this.player.emit?.(`xmr-status`,{connected:!1})}),this.xmr.on(`error`,e=>{i.error(`WebSocket error:`,e)}),this.xmr.on(`collectNow`,async()=>{i.info(`Received collectNow command from CMS`);try{await this.player.collect(),i.debug(`collectNow completed successfully`)}catch(e){i.error(`collectNow failed:`,e)}}),this.xmr.on(`screenShot`,async()=>{i.info(`Received screenShot command from CMS`);try{await this.player.captureScreenshot(),i.debug(`screenShot completed successfully`)}catch(e){i.error(`screenShot failed:`,e)}}),this.xmr.on(`licenceCheck`,()=>{i.debug(`Received licenceCheck (no-op for Linux client)`)}),this.xmr.on(`changeLayout`,async e=>{let t=typeof e==`object`&&e.layoutId||e,n=typeof e==`object`&&parseInt(e.duration)||0,r=typeof e==`object`&&e.changeMode||`replace`;i.info(`Received changeLayout command:`,t,n?`duration=${n}s`:``,r===`replace`?``:`mode=${r}`);try{typeof e==`object`&&e.downloadRequired===!0&&(i.info(`changeLayout: downloadRequired — triggering collection first`),await this.player.collect()),await this.player.changeLayout(t,{duration:n,changeMode:r}),i.debug(`changeLayout completed successfully`)}catch(e){i.error(`changeLayout failed:`,e)}}),this.xmr.on(`overlayLayout`,async e=>{let t=typeof e==`object`&&e.layoutId||e,n=typeof e==`object`&&parseInt(e.duration)||0;i.info(`Received overlayLayout command:`,t,n?`duration=${n}s`:``);try{typeof e==`object`&&e.downloadRequired===!0&&(i.info(`overlayLayout: downloadRequired — triggering collection first`),await this.player.collect()),await this.player.overlayLayout(t,{duration:n}),i.debug(`overlayLayout completed successfully`)}catch(e){i.error(`overlayLayout failed:`,e)}}),this.xmr.on(`revertToSchedule`,async()=>{i.info(`Received revertToSchedule command`);try{await this.player.revertToSchedule(),i.debug(`revertToSchedule completed successfully`)}catch(e){i.error(`revertToSchedule failed:`,e)}}),this.xmr.on(`purgeAll`,async()=>{i.info(`Received purgeAll command`);try{await this.player.purgeAll(),i.debug(`purgeAll completed successfully`)}catch(e){i.error(`purgeAll failed:`,e)}}),this.xmr.on(`commandAction`,async e=>{let t=e?.commandCode||e;i.info(`Received commandAction command:`,t);try{let n=this.player.displayCommands||e?.commands;await this.player.executeCommand(t,n),i.debug(`commandAction completed successfully`)}catch(e){i.error(`commandAction failed:`,e)}}),this.xmr.on(`triggerWebhook`,async e=>{i.info(`Received triggerWebhook command:`,e);try{this.player.triggerWebhook(e?.triggerCode||e),i.debug(`triggerWebhook completed successfully`)}catch(e){i.error(`triggerWebhook failed:`,e)}}),this.xmr.on(`dataUpdate`,async()=>{i.info(`Received dataUpdate command`);try{this.player.refreshDataConnectors(),i.debug(`dataUpdate completed successfully`)}catch(e){i.error(`dataUpdate failed:`,e)}}),this.xmr.on(`rekeyAction`,async()=>{i.info(`Received rekeyAction command - rotating RSA key pair`);try{this.config.data.xmrPubKey=``,this.config.data.xmrPrivKey=``,await this.config.ensureXmrKeyPair(),await this.player.collect(),i.info(`RSA key pair rotated successfully`)}catch(e){i.error(`Key rotation failed:`,e)}}),this.xmr.on(`criteriaUpdate`,async e=>{i.info(`Received criteriaUpdate command:`,e);try{await this.player.collect(),i.debug(`criteriaUpdate completed successfully`)}catch(e){i.error(`criteriaUpdate failed:`,e)}}),this.xmr.on(`currentGeoLocation`,async e=>{i.info(`Received currentGeoLocation command:`,e);try{e&&e.latitude!=null&&e.longitude!=null?this.player.reportGeoLocation?(this.player.reportGeoLocation(e),i.debug(`currentGeoLocation: coordinates applied`)):i.warn(`Geo location reporting not implemented in player`):this.player.requestGeoLocation?(await this.player.requestGeoLocation(),i.debug(`currentGeoLocation: browser location requested`)):i.warn(`Geo location request not implemented in player`)}catch(e){i.error(`currentGeoLocation failed:`,e)}}))}async stop(){if(this.xmr)try{await this.xmr.stop(),this.connected=!1,this.xmr=null,i.info(`Stopped`)}catch(e){i.error(`Error stopping:`,e)}}isConnected(){return this.connected}async send(e,t){if(!this.connected||!this.xmr)return i.warn(`Cannot send - not connected`),!1;try{return await this.xmr.send(e,t),!0}catch(e){return i.error(`Error sending:`,e),!1}}},o=t.version;export{o as VERSION,a as XmrWrapper};
|
|
2
|
+
//# sourceMappingURL=src-DAFtXxFV.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"src-BV-4JdnK.js","names":["log","pkg"],"sources":["../../../xmr/package.json","../../../xmr/src/xmr-client.js","../../../xmr/src/xmr-wrapper.js","../../../xmr/src/index.js"],"sourcesContent":["{\n \"name\": \"@xiboplayer/xmr\",\n \"version\": \"0.7.18\",\n \"description\": \"XMR WebSocket client for real-time Xibo CMS commands\",\n \"type\": \"module\",\n \"main\": \"./src/index.js\",\n \"types\": \"./src/index.d.ts\",\n \"exports\": {\n \".\": \"./src/index.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 \"vitest\": \"^4.1.2\"\n },\n \"keywords\": [\n \"xibo\",\n \"digital-signage\",\n \"xmr\",\n \"websocket\",\n \"real-time\"\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/xmr\"\n },\n \"homepage\": \"https://xiboplayer.org\"\n}\n","import { createLogger } from '@xiboplayer/utils';\n\nconst log = createLogger('XmrClient');\n\n/**\n * Native XMR (Xibo Message Relay) WebSocket Client\n *\n * Drop-in replacement for @xibosignage/xibo-communication-framework.\n * Uses a generic action dispatcher — emit(message.action, message) — so\n * every CMS action works automatically without a hardcoded if-else chain.\n *\n * API-compatible with the upstream Xmr class:\n * new XmrClient(channel) → .init() → .start(url, key) → .on(event, cb)\n */\n\nexport class XmrClient {\n /**\n * @param {string} channel - XMR channel identifier (e.g. \"player-HWKEY\")\n */\n constructor(channel) {\n this.channel = channel;\n this.url = null;\n this.cmsKey = null;\n this.socket = null;\n this.isConnected = false;\n this.isConnectionWanted = false;\n this.lastMessageAt = 0;\n this._interval = null;\n this._listeners = new Map(); // event → Set<callback>\n }\n\n /**\n * Register an event listener.\n * @param {string} event\n * @param {Function} callback\n * @returns {Function} Unsubscribe function\n */\n on(event, callback) {\n if (!this._listeners.has(event)) {\n this._listeners.set(event, new Set());\n }\n this._listeners.get(event).add(callback);\n return () => this._listeners.get(event)?.delete(callback);\n }\n\n /**\n * Emit an event to all registered listeners.\n * @param {string} event\n * @param {...*} args\n */\n emit(event, ...args) {\n const listeners = this._listeners.get(event);\n if (!listeners) return;\n for (const cb of listeners) {\n try {\n cb(...args);\n } catch (e) {\n log.error(`Listener error for '${event}':`, e);\n }\n }\n }\n\n /**\n * Initialize the reconnect interval (60s health check).\n * Same cadence as upstream framework.\n */\n async init() {\n if (this._interval) return;\n this._interval = setInterval(() => {\n if (this.isConnectionWanted && !this.isActive()) {\n this.start(this.url || 'DISABLED', this.cmsKey || 'n/a');\n }\n }, 60_000);\n }\n\n /**\n * Connect to XMR WebSocket server.\n * @param {string} url - WebSocket URL (ws:// or wss://)\n * @param {string} cmsKey - CMS authentication key\n */\n async start(url, cmsKey) {\n this.url = url;\n this.cmsKey = cmsKey;\n this.isConnectionWanted = true;\n\n // Close existing socket if any\n if (this.socket) {\n try { this.socket.close(); } catch (_) { /* ignore */ }\n this.socket = null;\n this.isConnected = false;\n }\n\n try {\n this.socket = new WebSocket(url);\n } catch (e) {\n this.emit('error', 'Failed to connect');\n return;\n }\n\n this.socket.addEventListener('open', () => {\n this.socket.send(JSON.stringify({\n type: 'init',\n key: this.cmsKey,\n channel: this.channel,\n }));\n this.isConnected = true;\n this.lastMessageAt = Date.now();\n this.emit('connected');\n });\n\n this.socket.addEventListener('close', () => {\n this.isConnected = false;\n this.emit('disconnected');\n });\n\n this.socket.addEventListener('error', () => {\n this.emit('error', 'error');\n });\n\n this.socket.addEventListener('message', (event) => {\n this.lastMessageAt = Date.now();\n\n // Heartbeat\n if (event.data === 'H') return;\n\n // JSON action message\n try {\n const message = JSON.parse(event.data);\n if (!message.action) return;\n\n // TTL check: createdDt (ISO 8601) + ttl seconds > now\n if (message.createdDt && message.ttl) {\n const created = Date.parse(message.createdDt);\n if (!isNaN(created)) {\n const expiresAt = created + parseInt(message.ttl) * 1000;\n if (expiresAt < Date.now()) return; // expired\n }\n }\n\n // Generic dispatch — every CMS action works automatically\n this.emit(message.action, message);\n } catch (e) {\n log.error('Failed to parse message:', e);\n }\n });\n }\n\n /**\n * Stop the connection and clear the reconnect interval.\n */\n async stop() {\n this.isConnectionWanted = false;\n if (this._interval) {\n clearInterval(this._interval);\n this._interval = null;\n }\n if (this.socket) {\n this.socket.close();\n this.socket = null;\n this.isConnected = false;\n }\n }\n\n /**\n * Send a message to the server via WebSocket.\n * @param {string} action - Action name\n * @param {*} data - Data payload\n */\n async send(action, data) {\n if (!this.socket || !this.isConnected) {\n throw new Error('Not connected');\n }\n this.socket.send(JSON.stringify({ action, ...data }));\n }\n\n /**\n * Check if the connection is active (connected + message within 15min).\n * @returns {boolean}\n */\n isActive() {\n return this.isConnected && (Date.now() - this.lastMessageAt) < 15 * 60 * 1000;\n }\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * XMR (Xibo Message Relay) Wrapper\n *\n * Integrates the native XmrClient (xmr-client.js) to enable real-time\n * push commands from CMS via WebSocket.\n *\n * Connection lifecycle is delegated to XmrClient, which has a\n * built-in 60s health-check interval that reconnects automatically.\n * This wrapper only routes events to player callbacks.\n *\n * Supported commands:\n * - collectNow: Trigger immediate XMDS collection cycle\n * - screenShot/screenshot: Capture and upload screenshot\n * - licenceCheck: No-op for Linux clients (always valid)\n * - changeLayout: Switch to a specific layout immediately\n * - overlayLayout: Push overlay layout on top of current content\n * - revertToSchedule: Return to normal scheduled content\n * - purgeAll: Clear all cached files and re-download\n * - commandAction: Execute a player command (HTTP only in browser)\n * - triggerWebhook: Fire a webhook trigger action\n * - dataUpdate: Force refresh of data connectors\n * - rekey: RSA key pair rotation (for XMR encryption)\n * - criteriaUpdate: Update display criteria and re-collect\n * - currentGeoLocation: Report current geo location to CMS\n */\n\nimport { XmrClient } from './xmr-client.js';\nimport { createLogger } from '@xiboplayer/utils';\n\nconst log = createLogger('XMR');\n\nexport class XmrWrapper {\n /**\n * @param {Object} config - Player configuration\n * @param {Object} player - Player instance for callbacks\n */\n constructor(config, player) {\n this.config = config;\n this.player = player;\n this.xmr = null;\n this.connected = false;\n }\n\n /**\n * Initialize and start XMR connection.\n *\n * Creates a single Xmr instance and lets the framework manage\n * reconnection via its internal 60s health-check timer.\n * Calling start() again on an already-running instance is safe —\n * the framework skips if already connected to the same URL.\n *\n * @param {string} xmrUrl - WebSocket URL (ws:// or wss://)\n * @param {string} cmsKey - CMS authentication key\n * @returns {Promise<boolean>} Success status\n */\n async start(xmrUrl, cmsKey) {\n try {\n // Reuse existing instance — the framework handles reconnection.\n // Only create a new instance on first call or after stop().\n if (!this.xmr) {\n log.info('Initializing connection to:', xmrUrl);\n const channel = this.config.xmrChannel || `player-${this.config.hardwareKey}`;\n this.xmr = new XmrClient(channel);\n this.setupEventHandlers();\n await this.xmr.init();\n }\n\n await this.xmr.start(xmrUrl, cmsKey);\n this.connected = true;\n log.info('Connected successfully');\n\n return true;\n } catch (error) {\n log.warn('Failed to start:', error.message);\n log.info('Framework will retry automatically every 60s');\n\n return false;\n }\n }\n\n /**\n * Setup event handlers for CMS commands\n */\n setupEventHandlers() {\n if (!this.xmr) return;\n\n // Connection events\n this.xmr.on('connected', () => {\n log.info('WebSocket connected');\n this.connected = true;\n this.player.emit?.('xmr-status', { connected: true });\n });\n\n this.xmr.on('disconnected', () => {\n log.warn('WebSocket disconnected (framework will reconnect)');\n this.connected = false;\n this.player.emit?.('xmr-status', { connected: false });\n });\n\n this.xmr.on('error', (error) => {\n log.error('WebSocket error:', error);\n });\n\n // CMS command: Collect Now\n this.xmr.on('collectNow', async () => {\n log.info('Received collectNow command from CMS');\n try {\n await this.player.collect();\n log.debug('collectNow completed successfully');\n } catch (error) {\n log.error('collectNow failed:', error);\n }\n });\n\n // CMS command: Screenshot\n this.xmr.on('screenShot', async () => {\n log.info('Received screenShot command from CMS');\n try {\n await this.player.captureScreenshot();\n log.debug('screenShot completed successfully');\n } catch (error) {\n log.error('screenShot failed:', error);\n }\n });\n\n // CMS command: License Check (no-op for Linux clients)\n this.xmr.on('licenceCheck', () => {\n log.debug('Received licenceCheck (no-op for Linux client)');\n });\n\n // CMS command: Change Layout\n // Payload may be a layoutId string or an object with { layoutId, duration, downloadRequired, changeMode }\n this.xmr.on('changeLayout', async (data) => {\n const layoutId = typeof data === 'object' ? (data.layoutId || data) : data;\n const duration = typeof data === 'object' ? (parseInt(data.duration) || 0) : 0;\n const changeMode = typeof data === 'object' ? (data.changeMode || 'replace') : 'replace';\n log.info('Received changeLayout command:', layoutId, duration ? `duration=${duration}s` : '', changeMode !== 'replace' ? `mode=${changeMode}` : '');\n try {\n if (typeof data === 'object' && data.downloadRequired === true) {\n log.info('changeLayout: downloadRequired — triggering collection first');\n await this.player.collect();\n }\n await this.player.changeLayout(layoutId, { duration, changeMode });\n log.debug('changeLayout completed successfully');\n } catch (error) {\n log.error('changeLayout failed:', error);\n }\n });\n\n // CMS command: Overlay Layout\n // Payload may be a layoutId string or an object with { layoutId, duration, downloadRequired }\n this.xmr.on('overlayLayout', async (data) => {\n const layoutId = typeof data === 'object' ? (data.layoutId || data) : data;\n const duration = typeof data === 'object' ? (parseInt(data.duration) || 0) : 0;\n log.info('Received overlayLayout command:', layoutId, duration ? `duration=${duration}s` : '');\n try {\n if (typeof data === 'object' && data.downloadRequired === true) {\n log.info('overlayLayout: downloadRequired — triggering collection first');\n await this.player.collect();\n }\n await this.player.overlayLayout(layoutId, { duration });\n log.debug('overlayLayout completed successfully');\n } catch (error) {\n log.error('overlayLayout failed:', error);\n }\n });\n\n // CMS command: Revert to Schedule\n this.xmr.on('revertToSchedule', async () => {\n log.info('Received revertToSchedule command');\n try {\n await this.player.revertToSchedule();\n log.debug('revertToSchedule completed successfully');\n } catch (error) {\n log.error('revertToSchedule failed:', error);\n }\n });\n\n // CMS command: Purge All\n this.xmr.on('purgeAll', async () => {\n log.info('Received purgeAll command');\n try {\n await this.player.purgeAll();\n log.debug('purgeAll completed successfully');\n } catch (error) {\n log.error('purgeAll failed:', error);\n }\n });\n\n // CMS command: Execute Command\n // Resolve command from local display settings (from RegisterDisplay), not from XMR payload\n this.xmr.on('commandAction', async (data) => {\n const commandCode = data?.commandCode || data;\n log.info('Received commandAction command:', commandCode);\n try {\n const localCommands = this.player.displayCommands || data?.commands;\n await this.player.executeCommand(commandCode, localCommands);\n log.debug('commandAction completed successfully');\n } catch (error) {\n log.error('commandAction failed:', error);\n }\n });\n\n // CMS command: Trigger Webhook\n this.xmr.on('triggerWebhook', async (data) => {\n log.info('Received triggerWebhook command:', data);\n try {\n this.player.triggerWebhook(data?.triggerCode || data);\n log.debug('triggerWebhook completed successfully');\n } catch (error) {\n log.error('triggerWebhook failed:', error);\n }\n });\n\n // CMS command: Data Update (force refresh data connectors)\n this.xmr.on('dataUpdate', async () => {\n log.info('Received dataUpdate command');\n try {\n this.player.refreshDataConnectors();\n log.debug('dataUpdate completed successfully');\n } catch (error) {\n log.error('dataUpdate failed:', error);\n }\n });\n\n // CMS command: Rekey (RSA key pair rotation) — spec event name is 'rekeyAction'\n this.xmr.on('rekeyAction', async () => {\n log.info('Received rekeyAction command - rotating RSA key pair');\n try {\n this.config.data.xmrPubKey = '';\n this.config.data.xmrPrivKey = '';\n await this.config.ensureXmrKeyPair();\n await this.player.collect();\n log.info('RSA key pair rotated successfully');\n } catch (error) {\n log.error('Key rotation failed:', error);\n }\n });\n\n // CMS command: Criteria Update\n this.xmr.on('criteriaUpdate', async (data) => {\n log.info('Received criteriaUpdate command:', data);\n try {\n await this.player.collect();\n log.debug('criteriaUpdate completed successfully');\n } catch (error) {\n log.error('criteriaUpdate failed:', error);\n }\n });\n\n // CMS command: Current Geo Location\n // Dual-path: if data has coordinates, CMS is telling us our location.\n // If data is empty/no coordinates, CMS is asking us to report our location.\n this.xmr.on('currentGeoLocation', async (data) => {\n log.info('Received currentGeoLocation command:', data);\n try {\n const hasCoordinates = data && data.latitude != null && data.longitude != null;\n\n if (hasCoordinates) {\n if (this.player.reportGeoLocation) {\n this.player.reportGeoLocation(data);\n log.debug('currentGeoLocation: coordinates applied');\n } else {\n log.warn('Geo location reporting not implemented in player');\n }\n } else {\n if (this.player.requestGeoLocation) {\n await this.player.requestGeoLocation();\n log.debug('currentGeoLocation: browser location requested');\n } else {\n log.warn('Geo location request not implemented in player');\n }\n }\n } catch (error) {\n log.error('currentGeoLocation failed:', error);\n }\n });\n }\n\n /**\n * Stop XMR connection and clean up the framework instance.\n * The framework's internal 60s timer is cleared when the instance\n * is discarded, so no reconnection will occur after stop().\n */\n async stop() {\n if (!this.xmr) return;\n\n try {\n await this.xmr.stop();\n this.connected = false;\n this.xmr = null;\n log.info('Stopped');\n } catch (error) {\n log.error('Error stopping:', error);\n }\n }\n\n /**\n * Check if XMR is connected\n * @returns {boolean}\n */\n isConnected() {\n return this.connected;\n }\n\n /**\n * Send a message to CMS (if needed for future features)\n * @param {string} action - Action name\n * @param {Object} data - Data payload\n */\n async send(action, data) {\n if (!this.connected || !this.xmr) {\n log.warn('Cannot send - not connected');\n return false;\n }\n\n try {\n await this.xmr.send(action, data);\n return true;\n } catch (error) {\n log.error('Error sending:', error);\n return false;\n }\n }\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n// @xiboplayer/xmr - XMR WebSocket client\nimport pkg from '../package.json' with { type: 'json' };\nexport const VERSION = pkg.version;\nexport { XmrWrapper } from './xmr-wrapper.js';\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"src-DAFtXxFV.js","names":["log","pkg"],"sources":["../../../xmr/package.json","../../../xmr/src/xmr-client.js","../../../xmr/src/xmr-wrapper.js","../../../xmr/src/index.js"],"sourcesContent":["{\n \"name\": \"@xiboplayer/xmr\",\n \"version\": \"0.7.19\",\n \"description\": \"XMR WebSocket client for real-time Xibo CMS commands\",\n \"type\": \"module\",\n \"main\": \"./src/index.js\",\n \"types\": \"./src/index.d.ts\",\n \"exports\": {\n \".\": \"./src/index.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 \"vitest\": \"^4.1.2\"\n },\n \"keywords\": [\n \"xibo\",\n \"digital-signage\",\n \"xmr\",\n \"websocket\",\n \"real-time\"\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/xmr\"\n },\n \"homepage\": \"https://xiboplayer.org\"\n}\n","import { createLogger } from '@xiboplayer/utils';\n\nconst log = createLogger('XmrClient');\n\n/**\n * Native XMR (Xibo Message Relay) WebSocket Client\n *\n * Drop-in replacement for @xibosignage/xibo-communication-framework.\n * Uses a generic action dispatcher — emit(message.action, message) — so\n * every CMS action works automatically without a hardcoded if-else chain.\n *\n * API-compatible with the upstream Xmr class:\n * new XmrClient(channel) → .init() → .start(url, key) → .on(event, cb)\n */\n\nexport class XmrClient {\n /**\n * @param {string} channel - XMR channel identifier (e.g. \"player-HWKEY\")\n */\n constructor(channel) {\n this.channel = channel;\n this.url = null;\n this.cmsKey = null;\n this.socket = null;\n this.isConnected = false;\n this.isConnectionWanted = false;\n this.lastMessageAt = 0;\n this._interval = null;\n this._listeners = new Map(); // event → Set<callback>\n }\n\n /**\n * Register an event listener.\n * @param {string} event\n * @param {Function} callback\n * @returns {Function} Unsubscribe function\n */\n on(event, callback) {\n if (!this._listeners.has(event)) {\n this._listeners.set(event, new Set());\n }\n this._listeners.get(event).add(callback);\n return () => this._listeners.get(event)?.delete(callback);\n }\n\n /**\n * Emit an event to all registered listeners.\n * @param {string} event\n * @param {...*} args\n */\n emit(event, ...args) {\n const listeners = this._listeners.get(event);\n if (!listeners) return;\n for (const cb of listeners) {\n try {\n cb(...args);\n } catch (e) {\n log.error(`Listener error for '${event}':`, e);\n }\n }\n }\n\n /**\n * Initialize the reconnect interval (60s health check).\n * Same cadence as upstream framework.\n */\n async init() {\n if (this._interval) return;\n this._interval = setInterval(() => {\n if (this.isConnectionWanted && !this.isActive()) {\n this.start(this.url || 'DISABLED', this.cmsKey || 'n/a');\n }\n }, 60_000);\n }\n\n /**\n * Connect to XMR WebSocket server.\n * @param {string} url - WebSocket URL (ws:// or wss://)\n * @param {string} cmsKey - CMS authentication key\n */\n async start(url, cmsKey) {\n this.url = url;\n this.cmsKey = cmsKey;\n this.isConnectionWanted = true;\n\n // Close existing socket if any\n if (this.socket) {\n try { this.socket.close(); } catch (_) { /* ignore */ }\n this.socket = null;\n this.isConnected = false;\n }\n\n try {\n this.socket = new WebSocket(url);\n } catch (e) {\n this.emit('error', 'Failed to connect');\n return;\n }\n\n this.socket.addEventListener('open', () => {\n this.socket.send(JSON.stringify({\n type: 'init',\n key: this.cmsKey,\n channel: this.channel,\n }));\n this.isConnected = true;\n this.lastMessageAt = Date.now();\n this.emit('connected');\n });\n\n this.socket.addEventListener('close', () => {\n this.isConnected = false;\n this.emit('disconnected');\n });\n\n this.socket.addEventListener('error', () => {\n this.emit('error', 'error');\n });\n\n this.socket.addEventListener('message', (event) => {\n this.lastMessageAt = Date.now();\n\n // Heartbeat\n if (event.data === 'H') return;\n\n // JSON action message\n try {\n const message = JSON.parse(event.data);\n if (!message.action) return;\n\n // TTL check: createdDt (ISO 8601) + ttl seconds > now\n if (message.createdDt && message.ttl) {\n const created = Date.parse(message.createdDt);\n if (!isNaN(created)) {\n const expiresAt = created + parseInt(message.ttl) * 1000;\n if (expiresAt < Date.now()) return; // expired\n }\n }\n\n // Generic dispatch — every CMS action works automatically\n this.emit(message.action, message);\n } catch (e) {\n log.error('Failed to parse message:', e);\n }\n });\n }\n\n /**\n * Stop the connection and clear the reconnect interval.\n */\n async stop() {\n this.isConnectionWanted = false;\n if (this._interval) {\n clearInterval(this._interval);\n this._interval = null;\n }\n if (this.socket) {\n this.socket.close();\n this.socket = null;\n this.isConnected = false;\n }\n }\n\n /**\n * Send a message to the server via WebSocket.\n * @param {string} action - Action name\n * @param {*} data - Data payload\n */\n async send(action, data) {\n if (!this.socket || !this.isConnected) {\n throw new Error('Not connected');\n }\n this.socket.send(JSON.stringify({ action, ...data }));\n }\n\n /**\n * Check if the connection is active (connected + message within 15min).\n * @returns {boolean}\n */\n isActive() {\n return this.isConnected && (Date.now() - this.lastMessageAt) < 15 * 60 * 1000;\n }\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * XMR (Xibo Message Relay) Wrapper\n *\n * Integrates the native XmrClient (xmr-client.js) to enable real-time\n * push commands from CMS via WebSocket.\n *\n * Connection lifecycle is delegated to XmrClient, which has a\n * built-in 60s health-check interval that reconnects automatically.\n * This wrapper only routes events to player callbacks.\n *\n * Supported commands:\n * - collectNow: Trigger immediate XMDS collection cycle\n * - screenShot/screenshot: Capture and upload screenshot\n * - licenceCheck: No-op for Linux clients (always valid)\n * - changeLayout: Switch to a specific layout immediately\n * - overlayLayout: Push overlay layout on top of current content\n * - revertToSchedule: Return to normal scheduled content\n * - purgeAll: Clear all cached files and re-download\n * - commandAction: Execute a player command (HTTP only in browser)\n * - triggerWebhook: Fire a webhook trigger action\n * - dataUpdate: Force refresh of data connectors\n * - rekey: RSA key pair rotation (for XMR encryption)\n * - criteriaUpdate: Update display criteria and re-collect\n * - currentGeoLocation: Report current geo location to CMS\n */\n\nimport { XmrClient } from './xmr-client.js';\nimport { createLogger } from '@xiboplayer/utils';\n\nconst log = createLogger('XMR');\n\nexport class XmrWrapper {\n /**\n * @param {Object} config - Player configuration\n * @param {Object} player - Player instance for callbacks\n */\n constructor(config, player) {\n this.config = config;\n this.player = player;\n this.xmr = null;\n this.connected = false;\n }\n\n /**\n * Initialize and start XMR connection.\n *\n * Creates a single Xmr instance and lets the framework manage\n * reconnection via its internal 60s health-check timer.\n * Calling start() again on an already-running instance is safe —\n * the framework skips if already connected to the same URL.\n *\n * @param {string} xmrUrl - WebSocket URL (ws:// or wss://)\n * @param {string} cmsKey - CMS authentication key\n * @returns {Promise<boolean>} Success status\n */\n async start(xmrUrl, cmsKey) {\n try {\n // Reuse existing instance — the framework handles reconnection.\n // Only create a new instance on first call or after stop().\n if (!this.xmr) {\n log.info('Initializing connection to:', xmrUrl);\n const channel = this.config.xmrChannel || `player-${this.config.hardwareKey}`;\n this.xmr = new XmrClient(channel);\n this.setupEventHandlers();\n await this.xmr.init();\n }\n\n await this.xmr.start(xmrUrl, cmsKey);\n this.connected = true;\n log.info('Connected successfully');\n\n return true;\n } catch (error) {\n log.warn('Failed to start:', error.message);\n log.info('Framework will retry automatically every 60s');\n\n return false;\n }\n }\n\n /**\n * Setup event handlers for CMS commands\n */\n setupEventHandlers() {\n if (!this.xmr) return;\n\n // Connection events\n this.xmr.on('connected', () => {\n log.info('WebSocket connected');\n this.connected = true;\n this.player.emit?.('xmr-status', { connected: true });\n });\n\n this.xmr.on('disconnected', () => {\n log.warn('WebSocket disconnected (framework will reconnect)');\n this.connected = false;\n this.player.emit?.('xmr-status', { connected: false });\n });\n\n this.xmr.on('error', (error) => {\n log.error('WebSocket error:', error);\n });\n\n // CMS command: Collect Now\n this.xmr.on('collectNow', async () => {\n log.info('Received collectNow command from CMS');\n try {\n await this.player.collect();\n log.debug('collectNow completed successfully');\n } catch (error) {\n log.error('collectNow failed:', error);\n }\n });\n\n // CMS command: Screenshot\n this.xmr.on('screenShot', async () => {\n log.info('Received screenShot command from CMS');\n try {\n await this.player.captureScreenshot();\n log.debug('screenShot completed successfully');\n } catch (error) {\n log.error('screenShot failed:', error);\n }\n });\n\n // CMS command: License Check (no-op for Linux clients)\n this.xmr.on('licenceCheck', () => {\n log.debug('Received licenceCheck (no-op for Linux client)');\n });\n\n // CMS command: Change Layout\n // Payload may be a layoutId string or an object with { layoutId, duration, downloadRequired, changeMode }\n this.xmr.on('changeLayout', async (data) => {\n const layoutId = typeof data === 'object' ? (data.layoutId || data) : data;\n const duration = typeof data === 'object' ? (parseInt(data.duration) || 0) : 0;\n const changeMode = typeof data === 'object' ? (data.changeMode || 'replace') : 'replace';\n log.info('Received changeLayout command:', layoutId, duration ? `duration=${duration}s` : '', changeMode !== 'replace' ? `mode=${changeMode}` : '');\n try {\n if (typeof data === 'object' && data.downloadRequired === true) {\n log.info('changeLayout: downloadRequired — triggering collection first');\n await this.player.collect();\n }\n await this.player.changeLayout(layoutId, { duration, changeMode });\n log.debug('changeLayout completed successfully');\n } catch (error) {\n log.error('changeLayout failed:', error);\n }\n });\n\n // CMS command: Overlay Layout\n // Payload may be a layoutId string or an object with { layoutId, duration, downloadRequired }\n this.xmr.on('overlayLayout', async (data) => {\n const layoutId = typeof data === 'object' ? (data.layoutId || data) : data;\n const duration = typeof data === 'object' ? (parseInt(data.duration) || 0) : 0;\n log.info('Received overlayLayout command:', layoutId, duration ? `duration=${duration}s` : '');\n try {\n if (typeof data === 'object' && data.downloadRequired === true) {\n log.info('overlayLayout: downloadRequired — triggering collection first');\n await this.player.collect();\n }\n await this.player.overlayLayout(layoutId, { duration });\n log.debug('overlayLayout completed successfully');\n } catch (error) {\n log.error('overlayLayout failed:', error);\n }\n });\n\n // CMS command: Revert to Schedule\n this.xmr.on('revertToSchedule', async () => {\n log.info('Received revertToSchedule command');\n try {\n await this.player.revertToSchedule();\n log.debug('revertToSchedule completed successfully');\n } catch (error) {\n log.error('revertToSchedule failed:', error);\n }\n });\n\n // CMS command: Purge All\n this.xmr.on('purgeAll', async () => {\n log.info('Received purgeAll command');\n try {\n await this.player.purgeAll();\n log.debug('purgeAll completed successfully');\n } catch (error) {\n log.error('purgeAll failed:', error);\n }\n });\n\n // CMS command: Execute Command\n // Resolve command from local display settings (from RegisterDisplay), not from XMR payload\n this.xmr.on('commandAction', async (data) => {\n const commandCode = data?.commandCode || data;\n log.info('Received commandAction command:', commandCode);\n try {\n const localCommands = this.player.displayCommands || data?.commands;\n await this.player.executeCommand(commandCode, localCommands);\n log.debug('commandAction completed successfully');\n } catch (error) {\n log.error('commandAction failed:', error);\n }\n });\n\n // CMS command: Trigger Webhook\n this.xmr.on('triggerWebhook', async (data) => {\n log.info('Received triggerWebhook command:', data);\n try {\n this.player.triggerWebhook(data?.triggerCode || data);\n log.debug('triggerWebhook completed successfully');\n } catch (error) {\n log.error('triggerWebhook failed:', error);\n }\n });\n\n // CMS command: Data Update (force refresh data connectors)\n this.xmr.on('dataUpdate', async () => {\n log.info('Received dataUpdate command');\n try {\n this.player.refreshDataConnectors();\n log.debug('dataUpdate completed successfully');\n } catch (error) {\n log.error('dataUpdate failed:', error);\n }\n });\n\n // CMS command: Rekey (RSA key pair rotation) — spec event name is 'rekeyAction'\n this.xmr.on('rekeyAction', async () => {\n log.info('Received rekeyAction command - rotating RSA key pair');\n try {\n this.config.data.xmrPubKey = '';\n this.config.data.xmrPrivKey = '';\n await this.config.ensureXmrKeyPair();\n await this.player.collect();\n log.info('RSA key pair rotated successfully');\n } catch (error) {\n log.error('Key rotation failed:', error);\n }\n });\n\n // CMS command: Criteria Update\n this.xmr.on('criteriaUpdate', async (data) => {\n log.info('Received criteriaUpdate command:', data);\n try {\n await this.player.collect();\n log.debug('criteriaUpdate completed successfully');\n } catch (error) {\n log.error('criteriaUpdate failed:', error);\n }\n });\n\n // CMS command: Current Geo Location\n // Dual-path: if data has coordinates, CMS is telling us our location.\n // If data is empty/no coordinates, CMS is asking us to report our location.\n this.xmr.on('currentGeoLocation', async (data) => {\n log.info('Received currentGeoLocation command:', data);\n try {\n const hasCoordinates = data && data.latitude != null && data.longitude != null;\n\n if (hasCoordinates) {\n if (this.player.reportGeoLocation) {\n this.player.reportGeoLocation(data);\n log.debug('currentGeoLocation: coordinates applied');\n } else {\n log.warn('Geo location reporting not implemented in player');\n }\n } else {\n if (this.player.requestGeoLocation) {\n await this.player.requestGeoLocation();\n log.debug('currentGeoLocation: browser location requested');\n } else {\n log.warn('Geo location request not implemented in player');\n }\n }\n } catch (error) {\n log.error('currentGeoLocation failed:', error);\n }\n });\n }\n\n /**\n * Stop XMR connection and clean up the framework instance.\n * The framework's internal 60s timer is cleared when the instance\n * is discarded, so no reconnection will occur after stop().\n */\n async stop() {\n if (!this.xmr) return;\n\n try {\n await this.xmr.stop();\n this.connected = false;\n this.xmr = null;\n log.info('Stopped');\n } catch (error) {\n log.error('Error stopping:', error);\n }\n }\n\n /**\n * Check if XMR is connected\n * @returns {boolean}\n */\n isConnected() {\n return this.connected;\n }\n\n /**\n * Send a message to CMS (if needed for future features)\n * @param {string} action - Action name\n * @param {Object} data - Data payload\n */\n async send(action, data) {\n if (!this.connected || !this.xmr) {\n log.warn('Cannot send - not connected');\n return false;\n }\n\n try {\n await this.xmr.send(action, data);\n return true;\n } catch (error) {\n log.error('Error sending:', error);\n return false;\n }\n }\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n// @xiboplayer/xmr - XMR WebSocket client\nimport pkg from '../package.json' with { type: 'json' };\nexport const VERSION = pkg.version;\nexport { XmrWrapper } from './xmr-wrapper.js';\n"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{c as e,l as t,o as n,r,u as i}from"./src-C3Sg89t9.js";import{r as a}from"./src-BYVnjdc0.js";import{i as o,n as s}from"./src-BdgQ2CiL.js";var c={name:`@xiboplayer/core`,version:`0.7.18`,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-CRWze-JF.js";import{r as a}from"./src-S093VuQs.js";import{i as o,n as s}from"./src-CR4vHRyW.js";var c={name:`@xiboplayer/core`,version:`0.7.19`,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-DLAmD0IH.js.map
|