@xiboplayer/pwa 0.7.17 → 0.7.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/assets/{html2canvas-Cjd84h1M.js → html2canvas-BAfZNSwU.js} +2 -2
  2. package/dist/assets/{html2canvas-Cjd84h1M.js.map → html2canvas-BAfZNSwU.js.map} +1 -1
  3. package/dist/assets/main-BG2cZsg-.js +108 -0
  4. package/dist/assets/main-BG2cZsg-.js.map +1 -0
  5. package/dist/assets/main-CkREX2X6.js +3 -0
  6. package/dist/assets/main-CkREX2X6.js.map +1 -0
  7. package/dist/assets/{pdf-CEqPPzMl.js → pdf-Bxz9Nzto.js} +3 -3
  8. package/dist/assets/pdf-Bxz9Nzto.js.map +1 -0
  9. package/dist/assets/preload-helper-Chd9yIcd.js +1 -0
  10. package/dist/assets/{setup-CxMnBRfM.js → setup-COzyFqMn.js} +2 -2
  11. package/dist/assets/{setup-CxMnBRfM.js.map → setup-COzyFqMn.js.map} +1 -1
  12. package/dist/assets/{src-BaaaVytN.js → src-BA9Y85MN.js} +2 -2
  13. package/dist/assets/{src-BaaaVytN.js.map → src-BA9Y85MN.js.map} +1 -1
  14. package/dist/assets/src-BF_sMbmn.js +629 -0
  15. package/dist/assets/src-BF_sMbmn.js.map +1 -0
  16. package/dist/assets/{src-DDl_ym1Y.js → src-BV-4JdnK.js} +2 -2
  17. package/dist/assets/{src-DDl_ym1Y.js.map → src-BV-4JdnK.js.map} +1 -1
  18. package/dist/assets/{src-DWLlXvs6.js → src-BYVnjdc0.js} +2 -2
  19. package/dist/assets/{src-DWLlXvs6.js.map → src-BYVnjdc0.js.map} +1 -1
  20. package/dist/assets/{src-Cvry9zYs.js → src-BdgQ2CiL.js} +2 -2
  21. package/dist/assets/{src-Cvry9zYs.js.map → src-BdgQ2CiL.js.map} +1 -1
  22. package/dist/assets/{src-CNTwwhBq.js → src-C3Sg89t9.js} +2 -2
  23. package/dist/assets/{src-CNTwwhBq.js.map → src-C3Sg89t9.js.map} +1 -1
  24. package/dist/assets/src-CfCGHb7u.js +16 -0
  25. package/dist/assets/src-CfCGHb7u.js.map +1 -0
  26. package/dist/assets/{src-CdA_SXE_.js → src-DnPIj2iO.js} +2 -2
  27. package/dist/assets/{src-CdA_SXE_.js.map → src-DnPIj2iO.js.map} +1 -1
  28. package/dist/assets/{src-DYcXC04Y.js → src-IJdgG16K.js} +2 -2
  29. package/dist/assets/{src-DYcXC04Y.js.map → src-IJdgG16K.js.map} +1 -1
  30. package/dist/assets/{src-DFEuLLEl.js → src-M1enQEwh.js} +2 -2
  31. package/dist/assets/{src-DFEuLLEl.js.map → src-M1enQEwh.js.map} +1 -1
  32. package/dist/assets/{sync-manager-lMIYGCuU.js → sync-manager-C68deMxf.js} +2 -2
  33. package/dist/assets/{sync-manager-lMIYGCuU.js.map → sync-manager-C68deMxf.js.map} +1 -1
  34. package/dist/index.html +2 -1
  35. package/dist/setup.html +3 -3
  36. package/dist/sw-pwa.js +2 -2
  37. package/dist/sw-pwa.js.map +1 -1
  38. package/package.json +15 -15
  39. package/dist/assets/main-CnQTxL1o.js +0 -108
  40. package/dist/assets/main-CnQTxL1o.js.map +0 -1
  41. package/dist/assets/main-DZZRd32h.js +0 -3
  42. package/dist/assets/main-DZZRd32h.js.map +0 -1
  43. package/dist/assets/pdf-CEqPPzMl.js.map +0 -1
  44. package/dist/assets/src-DHhjVCet.js +0 -629
  45. package/dist/assets/src-DHhjVCet.js.map +0 -1
  46. package/dist/assets/src-re534rLt.js +0 -16
  47. package/dist/assets/src-re534rLt.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"mappings":"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","names":[],"ignoreList":[],"sources":["../../src/download-overlay.ts","../../src/timeline-overlay.ts","../../src/setup-overlay.ts","../../src/main.ts"],"sourcesContent":["// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Download Progress Overlay\n *\n * Shows download status on hover (configurable, debug feature)\n * Displays: active downloads, progress, chunk status, queue info\n */\n\nexport interface DownloadOverlayConfig {\n enabled: boolean;\n updateInterval?: number; // ms between updates\n autoHide?: boolean; // Hide when no downloads\n}\n\nexport class DownloadOverlay {\n private overlay: HTMLElement | null = null;\n private config: DownloadOverlayConfig;\n private updateTimer: number | null = null;\n private _visible: boolean = false; // User-toggled visibility (D key)\n private _getProgress: (() => Record<string, any>) | null = null;\n\n constructor(config: DownloadOverlayConfig) {\n this.config = {\n updateInterval: 1000,\n autoHide: true,\n ...config\n };\n\n if (this.config.enabled) {\n this.createOverlay();\n // Start hidden — only shown when downloads are active or user presses D\n this.overlay!.style.display = 'none';\n }\n }\n\n private createOverlay() {\n this.overlay = document.createElement('div');\n this.overlay.id = 'download-overlay';\n // Style like top status messages - always visible, clean design\n this.overlay.style.cssText = `\n position: fixed;\n top: 1.5vh;\n left: 1.5vw;\n background: rgba(0, 0, 0, 0.88);\n color: #fff;\n font-family: system-ui, -apple-system, sans-serif;\n font-size: 1.4vw;\n padding: 1vh 1.2vw;\n border-radius: 0.4vw;\n border: 1px solid rgba(255, 255, 255, 0.25);\n z-index: 999999;\n max-width: 35vw;\n box-shadow: 0 0.3vh 1.2vw rgba(0, 0, 0, 0.5);\n `;\n\n document.body.appendChild(this.overlay);\n }\n\n /**\n * Set the progress callback. Called by PwaPlayer after DownloadManager is created.\n */\n public setProgressCallback(fn: () => Record<string, any>) {\n this._getProgress = fn;\n }\n\n private updateOverlay() {\n if (!this.overlay) return;\n\n const progress = this._getProgress ? this._getProgress() : {};\n const html = this.renderStatus(progress);\n const hasDownloads = !!html;\n\n if (hasDownloads) {\n // Active downloads — show overlay (auto or user-toggled)\n this.overlay.innerHTML = html;\n this.overlay.style.display = 'block';\n } else if (this._visible) {\n // User toggled on (D key) but no downloads — show idle status\n this.overlay.innerHTML = '<div style=\"color: #6c6; font-size: 1.4vw;\">✓ All downloads complete</div>';\n this.overlay.style.display = 'block';\n } else {\n // No downloads and not user-toggled — stop polling, hide\n this.stopUpdating();\n this.overlay.style.display = 'none';\n }\n }\n\n private renderStatus(progress: any): string {\n const downloads = progress || {};\n\n if (Object.keys(downloads).length === 0) {\n if (this.config.autoHide) {\n return ''; // Hide when no downloads\n }\n return `<div style=\"color: #6c6;\">✓ No downloads</div>`;\n }\n\n const numDownloads = Object.keys(downloads).length;\n let html = `<div style=\"font-weight: 600; margin-bottom: 0.8vh; font-size: 1.4vw;\">Downloads: ${numDownloads} active</div>`;\n\n for (const [url, progress] of Object.entries(downloads)) {\n const filename = this.extractFilename(url);\n const percent = Math.round((progress as any).percent || 0);\n const downloaded = this.formatBytes((progress as any).downloaded || 0);\n const total = this.formatBytes((progress as any).total || 0);\n\n html += `\n <div style=\"margin-bottom: 0.6vh; padding-bottom: 0.6vh; border-bottom: 1px solid rgba(255,255,255,0.1);\">\n <div style=\"font-size: 1.2vw; margin-bottom: 0.2vh;\">${filename}</div>\n <div style=\"background: rgba(255,255,255,0.1); height: 0.4vh; border-radius: 0.2vw; overflow: hidden;\">\n <div style=\"width: ${percent}%; height: 100%; background: #4a9eff; transition: width 0.3s;\"></div>\n </div>\n <div style=\"color: #999; font-size: 1.1vw; margin-top: 0.2vh;\">\n ${percent}% · ${downloaded} / ${total}\n </div>\n </div>\n `;\n }\n\n return html;\n }\n\n private extractFilename(key: string): string {\n // Key is now \"type/id\" (e.g. \"media/5\", \"layout/12\") — no URL parsing needed\n return key || 'unknown';\n }\n\n private formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n const kb = bytes / 1024;\n if (kb < 1024) return `${kb.toFixed(1)} KB`;\n const mb = kb / 1024;\n if (mb < 1024) return `${mb.toFixed(1)} MB`;\n return `${(mb / 1024).toFixed(1)} GB`;\n }\n\n /**\n * Toggle overlay visibility (D key).\n * When toggled on, starts polling. When toggled off, hides immediately.\n */\n public toggle() {\n if (!this.overlay) return;\n this._visible = !this._visible;\n if (this._visible) {\n this.overlay.style.display = 'block';\n this.updateOverlay(); // Immediate update\n this.startUpdating();\n } else {\n this.overlay.style.display = 'none';\n this.stopUpdating();\n }\n }\n\n /**\n * Start polling for download progress.\n * Safe to call multiple times — won't create duplicate timers.\n * Does NOT set _visible — the overlay auto-shows when downloads are active\n * and auto-hides when they finish. Use toggle() for user-controlled visibility.\n */\n public startUpdating() {\n if (this.updateTimer) return; // Already polling\n this.updateTimer = window.setInterval(() => {\n this.updateOverlay();\n }, this.config.updateInterval);\n this.updateOverlay(); // Immediate first update\n }\n\n /**\n * Stop polling. Called automatically when no downloads are active.\n */\n private stopUpdating() {\n if (this.updateTimer) {\n clearInterval(this.updateTimer);\n this.updateTimer = null;\n }\n }\n\n public destroy() {\n this.stopUpdating();\n if (this.overlay) {\n this.overlay.remove();\n this.overlay = null;\n }\n }\n\n public setEnabled(enabled: boolean) {\n this.config.enabled = enabled;\n\n if (enabled && !this.overlay) {\n this.createOverlay();\n // Polling starts on demand via startUpdating()\n } else if (!enabled && this.overlay) {\n this.destroy();\n }\n }\n}\n\n/**\n * Get default configuration based on environment\n */\nexport function getDefaultOverlayConfig(): DownloadOverlayConfig {\n // Check URL parameter override\n const urlParams = new URLSearchParams(window.location.search);\n const showDownloads = urlParams.get('showDownloads');\n\n if (showDownloads !== null) {\n return { enabled: showDownloads !== '0' && showDownloads !== 'false' };\n }\n\n // Check localStorage preference\n const savedPref = localStorage.getItem('xibo_show_download_overlay');\n if (savedPref !== null) {\n return { enabled: savedPref === 'true' };\n }\n\n // Default: disabled — toggle with D key or ?showDownloads=1\n return { enabled: false };\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Timeline Overlay\n *\n * Toggleable debug overlay showing upcoming schedule timeline.\n * Displays: layout IDs, time ranges, durations, current layout highlight.\n * Positioned bottom-left (download overlay is top-left).\n */\n\nimport { parseLayoutFile } from '@xiboplayer/schedule';\n\ninterface HiddenLayout {\n file: string;\n priority: number;\n}\n\ninterface TimelineEntry {\n layoutFile: string;\n startTime: Date;\n endTime: Date;\n duration: number;\n isDefault: boolean;\n hidden?: HiddenLayout[];\n missingMedia?: string[];\n}\n\nexport class TimelineOverlay {\n private overlay: HTMLElement | null = null;\n private visible: boolean;\n private timeline: TimelineEntry[] = [];\n private currentLayoutId: number | null = null;\n private layoutStartedAt: number | null = null; // wall-clock ms when layout began\n private currentDuration: number | null = null;\n private currentIsDefault: boolean = false;\n private previousLayout: { id: number; duration: number; startedAt: number } | null = null;\n private offline: boolean = false;\n private onLayoutClick: ((layoutId: number) => void) | null = null;\n private refreshTimer: ReturnType<typeof setInterval> | null = null;\n\n constructor(visible = false, onLayoutClick?: (layoutId: number) => void) {\n this.visible = visible;\n this.onLayoutClick = onLayoutClick || null;\n this.createOverlay();\n if (!this.visible) {\n this.overlay!.style.display = 'none';\n }\n // Re-render every 5s to update the remaining-time countdown on the current layout\n this.refreshTimer = setInterval(() => this.render(), 5000);\n }\n\n private createOverlay() {\n this.overlay = document.createElement('div');\n this.overlay.id = 'timeline-overlay';\n this.overlay.style.cssText = `\n position: fixed;\n bottom: 1.5vh;\n left: 1.5vw;\n background: rgba(0, 0, 0, 0.88);\n color: #fff;\n font-family: system-ui, -apple-system, sans-serif;\n font-size: 1.4vw;\n padding: 1vh 1.2vw;\n border-radius: 0.4vw;\n border: 1px solid rgba(255, 255, 255, 0.25);\n z-index: 999999;\n max-width: 35vw;\n box-shadow: 0 0.3vh 1.2vw rgba(0, 0, 0, 0.5);\n pointer-events: auto;\n `;\n // Click-to-skip: delegate click events on layout entries\n this.overlay.addEventListener('click', (e: MouseEvent) => {\n const target = (e.target as HTMLElement).closest('[data-layout-id]') as HTMLElement | null;\n if (!target || !this.onLayoutClick) return;\n const layoutId = parseInt(target.dataset.layoutId!, 10);\n if (isNaN(layoutId) || layoutId === this.currentLayoutId) return;\n this.onLayoutClick(layoutId);\n });\n\n document.body.appendChild(this.overlay);\n }\n\n toggle() {\n this.visible = !this.visible;\n if (this.overlay) {\n this.overlay.style.display = this.visible ? 'block' : 'none';\n }\n // Re-render when becoming visible (render() skips while hidden)\n if (this.visible) {\n this.render();\n }\n // Persist preference\n localStorage.setItem('xibo_show_timeline_overlay', String(this.visible));\n }\n\n /**\n * Update the overlay with new timeline data and/or current layout highlight.\n * Pass timeline=null to keep existing timeline and only update the highlight.\n */\n setOffline(offline: boolean) {\n this.offline = offline;\n this.render();\n }\n\n update(timeline: TimelineEntry[] | null, currentLayoutId: number | null, currentDuration?: number) {\n if (currentLayoutId !== null) {\n // Detect layout change — save previous for history display\n if (currentLayoutId !== this.currentLayoutId) {\n if (this.currentLayoutId !== null && this.currentDuration !== null && this.layoutStartedAt !== null) {\n this.previousLayout = { id: this.currentLayoutId, duration: this.currentDuration, startedAt: this.layoutStartedAt };\n }\n this.currentLayoutId = currentLayoutId;\n this.currentIsDefault = false;\n }\n // Always reset start time — same-layout replays emit layoutStart too\n this.layoutStartedAt = Date.now();\n // Duration is known at layout start — set it directly rather than\n // searching the timeline (which only contains future layouts).\n if (currentDuration !== undefined) {\n this.currentDuration = currentDuration;\n }\n }\n\n if (timeline !== null) {\n this.timeline = timeline;\n }\n\n this.render();\n }\n\n private render() {\n if (!this.overlay || !this.visible) return;\n\n if (this.timeline.length === 0 && !this.previousLayout && !this.currentLayoutId) {\n this.overlay.innerHTML = '<div style=\"color: #999;\">Timeline — no upcoming layouts</div>';\n return;\n }\n\n const now = Date.now();\n const clickable = this.onLayoutClick !== null;\n\n // Build upcoming list: timeline entries minus the first occurrence of the current layout\n let skippedCurrent = false;\n const upcoming: TimelineEntry[] = [];\n for (const entry of this.timeline) {\n const layoutId = parseLayoutFile(entry.layoutFile);\n if (!skippedCurrent && layoutId === this.currentLayoutId) {\n skippedCurrent = true;\n continue;\n }\n upcoming.push(entry);\n }\n\n // Count: previous (if any) + current (if any) + upcoming\n const totalCount = (this.previousLayout ? 1 : 0) + (this.currentLayoutId ? 1 : 0) + upcoming.length;\n const offlineBadge = this.offline ? ' <span style=\"color: #ff4444; font-size: 1.1vw;\">OFFLINE</span>' : '';\n let html = `<div style=\"font-weight: 600; margin-bottom: 0.8vh; font-size: 1.4vw; color: #ccc;\">Timeline (${totalCount} scheduled)${offlineBadge}</div>`;\n\n const maxVisible = 8;\n let rendered = 0;\n\n // 1. Previous layout (dimmed, strikethrough)\n if (this.previousLayout && rendered < maxVisible) {\n const prev = this.previousLayout;\n const durStr = this.formatDuration(prev.duration);\n const durPad = durStr.padStart(7).replace(/ /g, '&nbsp;');\n const idCol = `#${prev.id}`.padEnd(6).replace(/ /g, '&nbsp;');\n const startDate = new Date(prev.startedAt);\n const endDate = new Date(prev.startedAt + prev.duration * 1000);\n const timeRange = `${this.formatTime(startDate)}–${this.formatTime(endDate)} `;\n const cursor = clickable ? 'cursor: pointer;' : '';\n const hover = clickable ? 'onmouseover=\"this.style.background=\\'rgba(255,255,255,0.1)\\'\" onmouseout=\"this.style.background=\\'none\\'\"' : '';\n html += `<div data-layout-id=\"${prev.id}\" style=\"border-left: 0.25vw solid #555; padding-left: 0.6vw; color: #666; text-decoration: line-through; ${cursor} margin-bottom: 0.3vh; font-family: monospace; font-size: 1.3vw; line-height: 1.5; white-space: nowrap;\" ${hover}>`;\n html += `${timeRange}${idCol}${durPad}`;\n html += '</div>';\n rendered++;\n }\n\n // 2. Current layout (blue highlight, countdown from wall-clock start, with time range)\n if (this.currentLayoutId !== null && rendered < maxVisible) {\n let durStr: string;\n let timeRange = '';\n if (this.currentDuration !== null && this.layoutStartedAt !== null) {\n const elapsed = (now - this.layoutStartedAt) / 1000;\n const remainingSec = Math.max(0, Math.round(this.currentDuration - elapsed));\n durStr = this.formatDuration(remainingSec);\n const startDate = new Date(this.layoutStartedAt);\n const endDate = new Date(this.layoutStartedAt + this.currentDuration * 1000);\n timeRange = `${this.formatTime(startDate)}–${this.formatTime(endDate)} `;\n } else {\n durStr = '---';\n }\n const durPad = durStr.padStart(7).replace(/ /g, '&nbsp;');\n const idCol = `#${this.currentLayoutId}`.padEnd(6).replace(/ /g, '&nbsp;');\n html += `<div data-layout-id=\"${this.currentLayoutId}\" style=\"border-left: 0.25vw solid #4a9eff; padding-left: 0.6vw; color: #fff; font-weight: 600; margin-bottom: 0.3vh; font-family: monospace; font-size: 1.3vw; line-height: 1.5; white-space: nowrap;\">`;\n html += `${timeRange}${idCol}${durPad}`;\n if (this.currentIsDefault) html += ' <span style=\"color: #888;\">[def]</span>';\n html += '</div>';\n rendered++;\n }\n\n // 3. Upcoming layouts — compute times by chaining from current layout end\n let nextStartMs = (this.layoutStartedAt !== null && this.currentDuration !== null)\n ? this.layoutStartedAt + this.currentDuration * 1000\n : now;\n for (const entry of upcoming) {\n if (rendered >= maxVisible) break;\n const layoutId = parseLayoutFile(entry.layoutFile);\n const hasMissing = entry.missingMedia && entry.missingMedia.length > 0;\n const durStr = this.formatDuration(entry.duration);\n const entryEndMs = nextStartMs + entry.duration * 1000;\n const startStr = this.formatTime(new Date(nextStartMs));\n const endStr = this.formatTime(new Date(entryEndMs));\n\n let borderLeft: string;\n let color: string;\n if (hasMissing) {\n borderLeft = 'border-left: 0.25vw solid #ff4444; padding-left: 0.6vw;';\n color = 'color: #ff6666;';\n } else {\n borderLeft = 'padding-left: 0.85vw;';\n color = 'color: #aaa;';\n }\n const cursor = clickable ? 'cursor: pointer;' : '';\n const hover = clickable ? 'onmouseover=\"this.style.background=\\'rgba(255,255,255,0.1)\\'\" onmouseout=\"this.style.background=\\'none\\'\"' : '';\n\n html += `<div data-layout-id=\"${layoutId}\" style=\"${borderLeft} ${color} ${cursor} margin-bottom: 0.3vh; font-family: monospace; font-size: 1.3vw; line-height: 1.5; white-space: nowrap;\" ${hover}>`;\n const idCol = `#${layoutId}`.padEnd(6).replace(/ /g, '&nbsp;');\n const durPad = durStr.padStart(7).replace(/ /g, '&nbsp;');\n html += `${startStr}–${endStr} ${idCol}${durPad}`;\n if (entry.isDefault) html += ' <span style=\"color: #888;\">[def]</span>';\n if (hasMissing) {\n const missingList = entry.missingMedia!.join(', ');\n html += ` <span style=\"color: #ff4444; font-size: 1.1vw;\" title=\"Missing: ${missingList}\">⚠ ${entry.missingMedia!.length}</span>`;\n }\n if (entry.hidden && entry.hidden.length > 0) {\n const hiddenIds = entry.hidden.map(h => `#${h.file.replace('.xlf', '')} (p${h.priority})`).join(', ');\n html += ` <span style=\"color: #8899aa; font-size: 1.1vw;\" title=\"Also scheduled: ${hiddenIds}\">+${entry.hidden.length}</span>`;\n }\n html += '</div>';\n nextStartMs = entryEndMs;\n rendered++;\n }\n\n if (totalCount > maxVisible) {\n html += `<div style=\"padding-left: 0.85vw; color: #888; font-size: 1.1vw; margin-top: 0.3vh;\">+${totalCount - maxVisible} more</div>`;\n }\n\n this.overlay.innerHTML = html;\n }\n\n private formatTime(date: Date): string {\n return date.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });\n }\n\n private formatDuration(seconds: number): string {\n const m = Math.floor(seconds / 60);\n const s = Math.round(seconds % 60);\n return m > 0 ? `${m}m ${s.toString().padStart(2, '0')}s` : `${s}s`;\n }\n\n destroy() {\n if (this.refreshTimer) {\n clearInterval(this.refreshTimer);\n this.refreshTimer = null;\n }\n if (this.overlay) {\n this.overlay.remove();\n this.overlay = null;\n }\n }\n}\n\n/**\n * Determine initial visibility from URL param or localStorage.\n */\nexport function isTimelineVisible(): boolean {\n const urlParams = new URLSearchParams(window.location.search);\n const showTimeline = urlParams.get('showTimeline');\n if (showTimeline !== null) {\n return showTimeline !== '0' && showTimeline !== 'false';\n }\n\n const saved = localStorage.getItem('xibo_show_timeline_overlay');\n if (saved !== null) {\n return saved === 'true';\n }\n\n return false;\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * Setup Overlay\n *\n * Two-phase overlay that never navigates away from the player:\n * 1. CMS key gate — verifies identity\n * 2. Full setup form — setup.html in a fullscreen iframe\n *\n * Dismissible with Esc or Cancel at both phases. On successful setup\n * the iframe redirects to index.html, which we intercept to reload.\n */\n\nimport { createLogger, config } from '@xiboplayer/utils';\n\nconst log = createLogger('SetupOverlay');\n\nexport class SetupOverlay {\n private backdrop: HTMLElement | null = null;\n private gateCard: HTMLElement | null = null;\n private iframe: HTMLIFrameElement | null = null;\n private cancelBtn: HTMLElement | null = null;\n private visible = false;\n\n show() {\n if (this.visible) return;\n this.visible = true;\n\n if (!this.backdrop) {\n this.create();\n }\n\n // Always start with the gate phase\n this.showGate();\n this.backdrop!.style.display = 'flex';\n log.info('[SetupOverlay] Opened');\n }\n\n hide() {\n if (!this.visible) return;\n this.visible = false;\n\n if (this.backdrop) {\n this.backdrop.style.display = 'none';\n }\n // Clear iframe to stop any polling timers inside setup.html\n if (this.iframe) {\n this.iframe.src = 'about:blank';\n this.iframe.style.display = 'none';\n }\n log.info('[SetupOverlay] Closed');\n }\n\n toggle() {\n if (this.visible) {\n this.hide();\n } else {\n this.show();\n }\n }\n\n isVisible() {\n return this.visible;\n }\n\n /** Show the CMS key gate card, hide the iframe */\n private showGate() {\n if (this.gateCard) this.gateCard.style.display = 'block';\n if (this.iframe) this.iframe.style.display = 'none';\n if (this.cancelBtn) this.cancelBtn.style.display = 'none';\n\n const input = this.gateCard?.querySelector('#gate-key') as HTMLInputElement;\n if (input) {\n input.value = '';\n requestAnimationFrame(() => input.focus());\n }\n const err = this.gateCard?.querySelector('#gate-error') as HTMLElement;\n if (err) err.style.display = 'none';\n }\n\n /** Show the setup iframe, hide the gate card */\n private showSetup() {\n if (this.gateCard) this.gateCard.style.display = 'none';\n if (this.cancelBtn) this.cancelBtn.style.display = 'block';\n if (this.iframe) {\n this.iframe.style.display = 'block';\n this.iframe.src = './setup.html?unlocked=1';\n }\n }\n\n private create() {\n // ── Backdrop ──\n this.backdrop = document.createElement('div');\n this.backdrop.id = 'setup-overlay-backdrop';\n this.backdrop.style.cssText = `\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.85);\n z-index: 1000000;\n display: none;\n align-items: center;\n justify-content: center;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n `;\n\n // ── Cancel button (visible in iframe phase) ──\n this.cancelBtn = document.createElement('button');\n this.cancelBtn.textContent = 'Cancel';\n this.cancelBtn.style.cssText = `\n position: absolute;\n top: 12px;\n right: 16px;\n background: transparent;\n border: 1px solid rgba(255, 255, 255, 0.3);\n color: #aaa;\n font-size: 14px;\n padding: 6px 18px;\n border-radius: 6px;\n cursor: pointer;\n z-index: 1000001;\n display: none;\n transition: background 0.2s, color 0.2s;\n `;\n this.cancelBtn.addEventListener('mouseenter', () => {\n this.cancelBtn!.style.background = 'rgba(255,255,255,0.1)';\n this.cancelBtn!.style.color = '#fff';\n });\n this.cancelBtn.addEventListener('mouseleave', () => {\n this.cancelBtn!.style.background = 'transparent';\n this.cancelBtn!.style.color = '#aaa';\n });\n this.cancelBtn.addEventListener('click', () => this.hide());\n\n // ── Gate card (matches setup.html .container) ──\n this.gateCard = document.createElement('div');\n this.gateCard.style.cssText = `\n background: #2A2A2A;\n border-radius: 16px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);\n padding: 48px;\n max-width: 480px;\n width: 90vw;\n color: #E0E0E0;\n `;\n this.gateCard.innerHTML = `\n <div style=\"text-align: center; margin-bottom: 32px;\">\n <div style=\"font-size: 36px; font-weight: 700; color: #fff; letter-spacing: -0.5px;\">\n <span style=\"color: #0097D8;\">xibo</span> player\n </div>\n <div style=\"font-size: 14px; color: #888; margin-top: 4px;\">PWA Digital Signage</div>\n </div>\n <div style=\"font-size: 18px; font-weight: 600; color: #fff; margin-bottom: 8px; text-align: center;\">\n Reconfigure Display\n </div>\n <div style=\"font-size: 13px; color: #888; margin-bottom: 20px; text-align: center; line-height: 1.5;\">\n Enter the current CMS Key to change settings.\n </div>\n <form id=\"gate-form\">\n <div style=\"margin-bottom: 20px;\">\n <label style=\"display: block; margin-bottom: 6px; color: #AAA; font-size: 13px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;\">\n CMS Key\n </label>\n <input type=\"password\" id=\"gate-key\" placeholder=\"Current CMS key\" required\n style=\"width: 100%; padding: 12px 14px; background: #1D1D1D; border: 2px solid #3A3A3A; border-radius: 8px; font-size: 15px; color: #E0E0E0; transition: border-color 0.2s; box-sizing: border-box;\">\n </div>\n <button type=\"submit\" style=\"width: 100%; padding: 14px; background: #0097D8; color: white; border: none; border-radius: 8px; font-size: 15px; font-weight: 600; cursor: pointer; transition: background 0.2s, transform 0.1s;\">\n Unlock\n </button>\n </form>\n <div id=\"gate-error\" style=\"margin-top: 16px; padding: 12px 14px; background: rgba(244, 67, 54, 0.15); border: 1px solid rgba(244, 67, 54, 0.3); border-radius: 8px; color: #EF9A9A; font-size: 14px; display: none;\"></div>\n <button id=\"gate-cancel\" style=\"width: 100%; padding: 14px; background: transparent; border: 1px solid #3A3A3A; color: #AAA; border-radius: 8px; font-size: 15px; font-weight: 600; cursor: pointer; margin-top: 8px; transition: background 0.2s;\">\n Cancel\n </button>\n `;\n\n // ── Iframe (fullscreen, same look as setup.html) ──\n this.iframe = document.createElement('iframe');\n this.iframe.style.cssText = `\n width: 100%;\n height: 100%;\n border: none;\n background: #1D1D1D;\n display: none;\n `;\n\n // Detect success redirect: setup.html navigates to index.html → reload player\n this.iframe.addEventListener('load', () => {\n try {\n const href = this.iframe!.contentWindow?.location?.href || '';\n if (href.includes('index.html')) {\n this.hide();\n window.location.reload();\n return;\n }\n\n // Esc inside the iframe dismisses the overlay\n const iframeDoc = this.iframe!.contentDocument;\n if (!iframeDoc) return;\n iframeDoc.addEventListener('keydown', (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n e.preventDefault();\n this.hide();\n }\n });\n } catch { /* not loaded yet */ }\n });\n\n this.backdrop.appendChild(this.cancelBtn);\n this.backdrop.appendChild(this.gateCard);\n this.backdrop.appendChild(this.iframe);\n document.body.appendChild(this.backdrop);\n\n // ── Gate event handlers ──\n const form = this.gateCard.querySelector('#gate-form') as HTMLFormElement;\n const input = this.gateCard.querySelector('#gate-key') as HTMLInputElement;\n const errorEl = this.gateCard.querySelector('#gate-error') as HTMLElement;\n const gateCancelBtn = this.gateCard.querySelector('#gate-cancel') as HTMLButtonElement;\n\n input.addEventListener('focus', () => { input.style.borderColor = '#0097D8'; });\n input.addEventListener('blur', () => { input.style.borderColor = '#3A3A3A'; });\n\n form.addEventListener('submit', (e: Event) => {\n e.preventDefault();\n const entered = input.value.trim();\n\n if (entered === config.cmsKey) {\n this.showSetup();\n } else {\n errorEl.textContent = 'Incorrect CMS key';\n errorEl.style.display = 'block';\n input.focus();\n input.select();\n }\n });\n\n gateCancelBtn.addEventListener('click', () => this.hide());\n\n // Esc closes overlay; stopPropagation blocks player shortcuts\n this.backdrop.addEventListener('keydown', (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n e.preventDefault();\n this.hide();\n }\n e.stopPropagation();\n });\n }\n}\n","// SPDX-License-Identifier: AGPL-3.0-or-later\n// Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>\n/**\n * PWA Player with RendererLite\n *\n * Lightweight PWA player using modular PlayerCore orchestration.\n * Platform layer handles UI, DOM manipulation, and platform-specific features.\n */\n\nimport { RendererLite } from '@xiboplayer/renderer';\nimport { StoreClient, DownloadManager, BARRIER } from '@xiboplayer/cache';\nimport { PlayerCore, CORE_EVENTS as E } from '@xiboplayer/core';\nimport { parseLayoutDuration, parseLayoutFile } from '@xiboplayer/schedule';\nimport { createLogger, registerLogSink, PLAYER_API } from '@xiboplayer/utils';\nimport { DownloadOverlay, getDefaultOverlayConfig } from './download-overlay.js';\nimport { TimelineOverlay, isTimelineVisible } from './timeline-overlay.js';\nimport { SetupOverlay } from './setup-overlay.js';\n\ndeclare const __APP_VERSION__: string;\ndeclare const __BUILD_DATE__: string;\n\nconst log = createLogger('PWA');\n\n// ContentStore key prefix — mirrors PLAYER_API without leading slash\nconst STORE_PREFIX = PLAYER_API.slice(1);\n\n// Dynamic base path — same build serves /player/pwa/, /player/pwa-xmds/, /player/pwa-xlr/\nconst PLAYER_BASE = new URL('./', window.location.href).pathname.replace(/\\/$/, '');\n\n// Import core modules (will be loaded at runtime)\nlet cacheWidgetHtml: any;\nlet scheduleManager: any;\nlet config: any;\nlet RestClient: any;\nlet XmdsClient: any;\nlet ProtocolDetector: any;\nlet XmrWrapper: any;\nlet store: StoreClient;\nlet downloadManager: DownloadManager;\nlet StatsCollector: any;\nlet formatStats: any;\nlet LogReporter: any;\nlet formatLogs: any;\nlet DisplaySettings: any;\nlet SyncManager: any;\nlet computeStagger: any;\n\n// SDK package versions (populated in loadCoreModules)\nconst sdkVersions: Record<string, string> = {};\n\nclass PwaPlayer {\n private renderer!: RendererLite;\n private core!: PlayerCore;\n private xmds!: any;\n private downloadOverlay: DownloadOverlay | null = null;\n private timelineOverlay: TimelineOverlay | null = null;\n private setupOverlay: SetupOverlay | null = null;\n private statsCollector: any = null;\n private logReporter: any = null;\n private displaySettings: any = null;\n private currentScheduleId: number = -1; // Track scheduleId for stats\n private scheduledLayoutIds: Set<number> = new Set(); // Layout IDs from current schedule\n private preparingLayoutId: number | null = null; // Guard against concurrent prepareLayout calls\n private _pendingRetryLayoutId: number | null = null; // Queued retry when check-pending-layout arrives during preparation\n private _screenshotInterval: any = null;\n private _screenshotMethod: 'electron' | 'displayMedia' | 'html2canvas' | null = null;\n private _html2canvasMod: any = null;\n private _screenshotInFlight = false; // Concurrency guard — one capture at a time\n private _wakeLock: any = null; // Screen Wake Lock sentinel\n private syncManager: any = null; // Multi-display sync coordinator\n private _currentLayoutEnableStat: boolean = true; // enableStat from current layout XLF\n private _probeTimer: any = null; // Debounce timer for duration probing\n private _mediaStatusTimer: ReturnType<typeof setTimeout> | null = null; // Debounce timer for media status check\n private _pendingFollowerStats: any[] | null = null; // In-flight stats delegated to lead\n private _pendingFollowerLogs: any[] | null = null; // In-flight logs delegated to lead\n private _iframeObserver: MutationObserver | null = null; // Iframe key-forwarding observer\n private _swIcHandler: any = null; // SW Interactive Control message handler\n private _chunkConfig: any = null; // Device-adaptive chunk configuration\n private _fileIdToSaveAs: Map<string, string> = new Map(); // Numeric file ID → storedAs filename\n private _cachedMediaKeys: Set<string> = new Set(); // saveAs keys confirmed cached (avoids HEAD 404s)\n private protocolDetector: any = null; // CMS protocol auto-detector\n\n async init() {\n log.info('Initializing player with RendererLite + PlayerCore...');\n\n // Load core modules\n await this.loadCoreModules();\n\n // Register Service Worker for offline-first kiosk mode\n if ('serviceWorker' in navigator) {\n try {\n const registration = await navigator.serviceWorker.register(`${PLAYER_BASE}/sw-pwa.js?v=${Date.now()}`, {\n scope: `${PLAYER_BASE}/`,\n type: 'module',\n updateViaCache: 'none'\n });\n log.info('Service Worker registered for offline mode:', registration.scope);\n\n // Request persistent storage (kiosk requirement)\n if (navigator.storage && navigator.storage.persist) {\n const persistent = await navigator.storage.persist();\n if (persistent) {\n log.info('Persistent storage granted - cache won\\'t be evicted');\n } else {\n log.warn('Persistent storage denied - cache may be evicted');\n }\n }\n } catch (error) {\n log.warn('Service Worker registration failed:', error);\n }\n }\n\n // Initialize StoreClient (REST) + DownloadManager (main thread)\n log.info('Initializing cache clients...');\n store = new StoreClient();\n const { calculateChunkConfig } = await import('@xiboplayer/sw');\n this._chunkConfig = calculateChunkConfig(log);\n downloadManager = new DownloadManager({\n concurrency: this._chunkConfig.concurrency,\n chunkSize: this._chunkConfig.chunkSize,\n chunksPerFile: 2,\n });\n log.info('Cache clients ready — StoreClient + DownloadManager');\n\n // Create renderer\n const container = document.getElementById('player-container');\n if (!container) {\n throw new Error('No #player-container found');\n }\n\n this.renderer = new RendererLite(\n {\n cmsUrl: config.cmsUrl,\n hardwareKey: config.hardwareKey\n },\n container,\n {\n // Provide fileId→saveAs map for layout background resolution\n fileIdToSaveAs: this._fileIdToSaveAs,\n\n // Provide widget HTML resolver — check ContentStore via proxy\n getWidgetHtml: async (widget: any) => {\n const widgetPath = `${PLAYER_API}/widgets/${widget.layoutId}/${widget.regionId}/${widget.id}`;\n log.debug(`Looking for widget HTML at: ${widgetPath}`, widget);\n\n try {\n const exists = await store.has(`${STORE_PREFIX}/widgets`, `${widget.layoutId}/${widget.regionId}/${widget.id}`);\n if (exists) {\n log.debug(`Widget HTML found in store, using mirror URL for iframe`);\n return { url: widgetPath, fallback: widget.raw || '' };\n } else {\n log.warn(`No widget HTML found in store: ${widgetPath}`);\n }\n } catch (error) {\n log.error(`Failed to check widget HTML for ${widget.id}:`, error);\n }\n\n // Fallback to widget.raw (XLF template)\n log.warn(`Using widget.raw fallback for ${widget.id}`);\n return widget.raw || '';\n }\n }\n );\n\n // Create PlayerCore (with CMS-namespaced offline cache DB)\n this.core = new PlayerCore({\n config,\n xmds: this.xmds,\n cache: store,\n schedule: scheduleManager,\n renderer: this.renderer,\n xmrWrapper: XmrWrapper,\n statsCollector: this.statsCollector,\n displaySettings: this.displaySettings,\n cmsId: config.activeCmsId,\n });\n\n // Setup platform-specific event handlers\n this.setupCoreEventHandlers();\n this.setupRendererEventHandlers();\n this.setupInteractiveControl();\n this.setupDataConnectorNotify();\n this.setupRemoteControls();\n\n // Setup UI\n this.updateConfigDisplay();\n\n // Online/offline event listeners for seamless offline mode\n window.addEventListener('online', () => {\n log.info('Browser reports online — triggering immediate collection');\n this.updateStatus('Back online, syncing...');\n this.removeOfflineIndicator();\n this.core.collectNow().catch((error: any) => {\n log.error('Failed to collect after coming online:', error);\n });\n });\n window.addEventListener('offline', () => {\n log.warn('Browser reports offline — continuing playback with cached data');\n this.updateStatus('Offline mode — using cached content');\n this.showOfflineIndicator();\n });\n\n // Initialize download progress overlay (configurable debug feature)\n // Respect controls.keyboard.debugOverlays — if disabled, don't restore overlays\n const controls = this.getControls();\n const debugOverlaysEnabled = (controls.keyboard || {}).debugOverlays === true;\n\n const overlayConfig = getDefaultOverlayConfig();\n if (overlayConfig.enabled && debugOverlaysEnabled) {\n this.downloadOverlay = new DownloadOverlay(overlayConfig);\n this.downloadOverlay.setProgressCallback(() => downloadManager.getProgress());\n log.info('Download overlay enabled (hover bottom-right corner)');\n }\n\n // Timeline overlay — created on first T key press (or if previously visible)\n if (isTimelineVisible() && debugOverlaysEnabled) {\n this.timelineOverlay = new TimelineOverlay(true, (layoutId) => this.skipToLayout(layoutId));\n }\n\n // Listen for certificate warnings from Electron main process\n this.setupCertWarnings();\n\n // Listen for XMR connection status changes\n this.setupXmrWarning();\n\n // Request Screen Wake Lock to prevent display sleep\n await this.requestWakeLock();\n\n // Re-acquire wake lock when tab becomes visible again\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'visible') {\n this.requestWakeLock();\n }\n });\n\n // Start collection cycle\n await this.core.collect();\n\n log.info('Player initialized successfully');\n }\n\n /**\n * Request Screen Wake Lock to prevent display from sleeping\n * Re-acquired on visibility change (browser releases it when tab is hidden)\n */\n private async requestWakeLock() {\n if (!('wakeLock' in navigator)) {\n log.debug('Wake Lock API not supported');\n return;\n }\n\n try {\n this._wakeLock = await (navigator as any).wakeLock.request('screen');\n log.info('Screen Wake Lock acquired — display will stay on');\n\n this._wakeLock.addEventListener('release', () => {\n log.debug('Screen Wake Lock released');\n this._wakeLock = null;\n });\n } catch (error: any) {\n log.warn('Wake Lock request failed:', error?.message);\n }\n }\n\n /**\n * Listen for certificate warnings from Electron and show in the top bar.\n * The #overlay bar (defined in index.html) is the status bar with\n * #config-info (left) and #status (right). If it was removed (statusBarOnHover\n * not set), we recreate it. Cert warnings make the bar always visible.\n */\n private setupCertWarnings() {\n const warnedHosts = new Set<string>();\n\n window.addEventListener('cert-warning', ((e: CustomEvent) => {\n const { host, error } = e.detail;\n if (warnedHosts.has(host)) return;\n warnedHosts.add(host);\n\n log.warn(`Invalid SSL certificate accepted for stream: ${host} (${error})`);\n\n // Find or recreate the top bar\n let overlay = document.getElementById('overlay');\n let created = false;\n if (!overlay) {\n overlay = document.createElement('div');\n overlay.id = 'overlay';\n // Recreate child structure: config-info | status\n const info = document.createElement('div');\n info.id = 'config-info';\n overlay.appendChild(info);\n const status = document.createElement('div');\n status.id = 'status';\n overlay.appendChild(status);\n document.body.appendChild(overlay);\n created = true;\n }\n\n // Find or create the cert warning span between #config-info and #status\n let certSpan = document.getElementById('cert-warnings');\n if (!certSpan) {\n certSpan = document.createElement('span');\n certSpan.id = 'cert-warnings';\n certSpan.style.cssText = 'color: #ffaa33; flex: 0 0 auto;';\n const statusEl = document.getElementById('status');\n overlay.insertBefore(certSpan, statusEl);\n }\n\n const hosts = [...warnedHosts].join(', ');\n certSpan.textContent = `\\u26A0 SSL: ${hosts}`;\n\n // Don't force always-visible — let hover-only CSS handle show/hide\n\n // If we recreated the overlay, repopulate config info\n if (created) this.updateConfigDisplay();\n }) as EventListener);\n }\n\n /**\n * Show/hide an XMR disconnected warning in the top bar.\n * Placed before #cert-warnings (or before #status if no cert warnings).\n */\n private setupXmrWarning() {\n this.core.on('xmr-status', ({ connected }: { connected: boolean }) => {\n const overlay = document.getElementById('overlay');\n if (!overlay) return;\n\n let span = document.getElementById('xmr-warning');\n\n if (!connected) {\n if (!span) {\n span = document.createElement('span');\n span.id = 'xmr-warning';\n span.style.cssText = 'color: #ff6666; flex: 0 0 auto;';\n // Insert before cert-warnings or status (whichever comes first)\n const anchor = document.getElementById('cert-warnings') || document.getElementById('status');\n overlay.insertBefore(span, anchor);\n }\n span.textContent = '\\u26A0 XMR disconnected';\n } else {\n span?.remove();\n }\n });\n }\n\n /**\n * Load core modules\n */\n private async loadCoreModules() {\n try {\n const [\n cacheModule, xmdsModule, scheduleModule, configModule,\n xmrModule, statsModule, displaySettingsModule, coreModule,\n rendererModule, syncModule,\n ] = await Promise.all([\n import('@xiboplayer/cache'),\n import('@xiboplayer/xmds'),\n import('@xiboplayer/schedule'),\n import('@xiboplayer/utils'),\n import('@xiboplayer/xmr'),\n import('@xiboplayer/stats'),\n import('@xiboplayer/settings'),\n import('@xiboplayer/core'),\n import('@xiboplayer/renderer'),\n import('@xiboplayer/sync'),\n ]);\n\n cacheWidgetHtml = cacheModule.cacheWidgetHtml;\n SyncManager = syncModule.SyncManager;\n computeStagger = syncModule.computeStagger;\n scheduleManager = scheduleModule.scheduleManager;\n config = configModule.config;\n RestClient = xmdsModule.RestClient;\n XmdsClient = xmdsModule.XmdsClient;\n ProtocolDetector = xmdsModule.ProtocolDetector;\n XmrWrapper = xmrModule.XmrWrapper;\n StatsCollector = statsModule.StatsCollector;\n formatStats = statsModule.formatStats;\n LogReporter = statsModule.LogReporter;\n formatLogs = statsModule.formatLogs;\n DisplaySettings = displaySettingsModule.DisplaySettings;\n\n // Capture SDK package versions\n sdkVersions.core = coreModule.VERSION || '?';\n sdkVersions.cache = cacheModule.VERSION || '?';\n sdkVersions.renderer = rendererModule.VERSION || '?';\n sdkVersions.schedule = scheduleModule.VERSION || '?';\n sdkVersions.xmds = xmdsModule.VERSION || '?';\n sdkVersions.xmr = xmrModule.VERSION || '?';\n sdkVersions.utils = configModule.VERSION || '?';\n sdkVersions.stats = statsModule.VERSION || '?';\n sdkVersions.settings = displaySettingsModule.VERSION || '?';\n\n // Get MAC address from Electron if available (for WOL support)\n if ((window as any).electronAPI?.getSystemInfo) {\n try {\n const sysInfo = await (window as any).electronAPI.getSystemInfo();\n if (sysInfo.macAddress) {\n config.macAddress = sysInfo.macAddress;\n }\n } catch (_) { /* pure PWA — no Electron API */ }\n }\n\n // Transport selection:\n // transport: \"rest\" → forced REST API\n // transport: \"xmds\" → forced SOAP\n // transport: \"auto\" → probe REST → SOAP fallback (default)\n // /player/pwa-xmds/ → forced SOAP (URL-based override)\n // ?transport=xmds → forced SOAP (query param override)\n const cfgTransport = config.transport !== 'auto' ? config.transport : undefined;\n const urlTransport = new URLSearchParams(window.location.search).get('transport');\n const transport = urlTransport\n || (PLAYER_BASE.includes('pwa-xmds') ? 'xmds' : null)\n || cfgTransport\n || 'auto';\n\n // Use ProtocolDetector for auto-detection with re-probe support.\n // The detector's first probe uses a generous timeout (10s default)\n // to tolerate cold CMS start-ups — an initial 5-6s TLS+PHP-FPM\n // warm-up used to trip the old 3s timeout and permanently lock\n // the player into XMDS fallback for the rest of the session.\n this.protocolDetector = new ProtocolDetector(config.cmsUrl, RestClient, XmdsClient);\n const forceProtocol = (transport === 'auto') ? undefined : transport;\n const { client, protocol } = await this.protocolDetector.detect(config, forceProtocol);\n this.xmds = client;\n\n // If we fell back to XMDS, start a background auto-reprobe loop.\n // When REST recovers (e.g. CMS restart, network fix), the detector\n // swaps the live client pointer here and we're transparently back\n // on REST without needing to restart the player.\n // Skipped for explicitly forced protocols — if the operator asked\n // for XMDS, stay on XMDS.\n if (protocol === 'xmds' && forceProtocol === undefined) {\n this.protocolDetector.startAutoReprobe(config, (newClient: any) => {\n this.xmds = newClient;\n log.info('[Protocol] Promoted from XMDS back to REST — live client swapped');\n });\n }\n\n // Initialize stats collector (namespaced by CMS ID)\n const cmsId = config.activeCmsId;\n this.statsCollector = new StatsCollector(cmsId);\n await this.statsCollector.init();\n log.info(`Stats collector initialized${cmsId ? ` (CMS: ${cmsId})` : ''}`);\n\n // Initialize log reporter for CMS log submission (namespaced by CMS ID)\n this.logReporter = new LogReporter(cmsId);\n await this.logReporter.init();\n log.info(`Log reporter initialized${cmsId ? ` (CMS: ${cmsId})` : ''}`);\n\n // Serialize log args to string (shared by log reporter and console forwarder)\n const serializeArgs = (args: any[]) => args.map((a: any) => typeof a === 'string' ? a : JSON.stringify(a)).join(' ');\n\n // Bridge logger output to LogReporter for CMS submission\n registerLogSink(({ level, name, args }: { level: string; name: string; args: any[] }) => {\n if (!this.logReporter) return;\n const message = serializeArgs(args);\n this.logReporter.log(level, `[${name}] ${message}`, 'PLAYER').catch(() => {});\n });\n\n // Forward console logs to proxy stdout (for journald/log analysis).\n // Controlled by debug.consoleLogs in config.json.\n // Optional debug.consoleLogsInterval (seconds) sets the batch flush interval (default 10s).\n const debugConfig = config.debug;\n if (debugConfig?.consoleLogs) {\n const flushIntervalMs = (debugConfig.consoleLogsInterval || 10) * 1000;\n let batch: Array<{ level: string; name: string; message: string; ts: string }> = [];\n let flushTimer: ReturnType<typeof setTimeout> | null = null;\n\n const flushLogs = () => {\n if (batch.length === 0) return;\n const payload = batch;\n batch = [];\n flushTimer = null;\n // Fire-and-forget POST — log forwarding must never block the player\n fetch('/debug/log', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n }).catch(() => {});\n };\n\n registerLogSink(({ level, name, args }: { level: string; name: string; args: any[] }) => {\n const message = serializeArgs(args);\n batch.push({ level, name, message, ts: new Date().toISOString() });\n if (!flushTimer) {\n flushTimer = setTimeout(flushLogs, flushIntervalMs);\n }\n });\n\n log.info(`Console log forwarding to proxy enabled (flush every ${flushIntervalMs / 1000}s)`);\n }\n\n // Initialize display settings manager\n this.displaySettings = new DisplaySettings();\n log.info('Display settings manager initialized');\n\n // Log version and environment information for debugging\n const buildDate = typeof __BUILD_DATE__ !== 'undefined' ? __BUILD_DATE__ : '?';\n const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '?';\n log.info(`v${appVersion} built ${buildDate}`);\n const versionParts = Object.entries(sdkVersions).map(([k, v]) => `${k}=${v}`).join(' ');\n log.info(`SDK: ${versionParts}`);\n const isElectron = !!(window as any).electronAPI;\n const electronVersion = isElectron ? (navigator.userAgent.match(/Electron\\/([\\d.]+)/)?.[1] || '?') : null;\n const chromeVersion = navigator.userAgent.match(/Chrome\\/([\\d.]+)/)?.[1] || '?';\n const platform = isElectron ? `Electron ${electronVersion} / Chrome ${chromeVersion}` : `Chrome ${chromeVersion}`;\n log.info(`Env: PWA v${appVersion} | ${platform} | ${navigator.platform} | ${screen.width}x${screen.height}`);\n\n log.info('Core modules loaded');\n } catch (error) {\n log.error('Failed to load core modules:', error);\n throw error;\n }\n }\n\n /**\n * Setup PlayerCore event handlers (Platform-specific UI updates)\n */\n private setupCoreEventHandlers() {\n // Delegate to focused handler groups\n this.setupSyncEventHandlers();\n this.setupDownloadEventHandlers();\n this.setupCommandEventHandlers();\n\n // Collection events\n this.core.on(E.COLLECTION_START, () => {\n this.updateStatus('Collecting data from CMS...');\n });\n\n this.core.on(E.REGISTER_COMPLETE, (regResult: any) => {\n const displayName = this.displaySettings?.getDisplayName() || regResult.displayName || config.hardwareKey;\n this.updateStatus(`Registered: ${displayName}`);\n\n // Update page title with display name\n if (this.displaySettings) {\n document.title = `xiboplayer - ${this.displaySettings.getDisplayName()}`;\n }\n\n // Set display location from CMS settings\n const lat = parseFloat(regResult?.settings?.latitude);\n const lng = parseFloat(regResult?.settings?.longitude);\n if (lat && lng && !isNaN(lat) && !isNaN(lng)) {\n log.info(`Display location from CMS: ${lat.toFixed(4)}, ${lng.toFixed(4)}`);\n if (scheduleManager?.setLocation) {\n scheduleManager.setLocation(lat, lng);\n }\n } else if (this.core.requestGeoLocation) {\n // No CMS coordinates — try browser Geolocation API as fallback\n log.info('No CMS coordinates, requesting browser geolocation...');\n this.core.requestGeoLocation();\n }\n\n // Multi-display sync: local config fallback when CMS doesn't provide syncConfig\n if (!regResult.syncConfig && config.data?.sync) {\n log.info('[Sync] Using local sync config (CMS did not provide syncConfig)');\n this.core.syncConfig = config.data.sync;\n this.core.emit(E.SYNC_CONFIG, config.data.sync);\n }\n });\n\n // NOTE: Two OFFLINE_MODE listeners are intentional — this one handles UI,\n // setupSyncEventHandlers() registers a second one for sync bootstrap.\n this.core.on(E.OFFLINE_MODE, (isOffline: boolean) => {\n if (isOffline) {\n this.updateStatus('Offline mode — using cached content');\n this.showOfflineIndicator();\n } else {\n this.updateStatus('Back online');\n this.removeOfflineIndicator();\n }\n });\n\n this.core.on(E.SCHEDULE_RECEIVED, (schedule: any) => {\n this.updateStatus('Processing schedule...');\n\n // Extract scheduleId for stats tracking\n // Check layouts or campaigns for scheduleId\n if (schedule.layouts && schedule.layouts.length > 0) {\n this.currentScheduleId = parseInt(schedule.layouts[0].scheduleid) || -1;\n } else if (schedule.campaigns && schedule.campaigns.length > 0) {\n this.currentScheduleId = parseInt(schedule.campaigns[0].scheduleid) || -1;\n }\n\n // Selectively clear preloaded layouts not in the new schedule.\n // Keep warm entries whose layout ID is still scheduled — their DOM is still valid.\n // (The CMS schedule CRC changes every collection due to timestamps, even when\n // the actual layout list hasn't changed. Blindly clearing would destroy preloads.)\n if (this.renderer?.layoutPool) {\n const scheduledIds = new Set<number>();\n if (schedule.layouts) {\n for (const l of schedule.layouts) {\n const id = parseLayoutFile(l.file || l.id || l);\n if (id) scheduledIds.add(id);\n }\n }\n if (schedule.campaigns) {\n for (const c of schedule.campaigns) {\n if (c.layouts) {\n for (const l of c.layouts) {\n const id = parseLayoutFile(l.file || l.id || l);\n if (id) scheduledIds.add(id);\n }\n }\n }\n }\n const cleared = this.renderer.layoutPool.clearWarmNotIn(scheduledIds);\n if (cleared > 0) {\n log.info(`Cleared ${cleared} preloaded layout(s) no longer in schedule`);\n }\n this.scheduledLayoutIds = scheduledIds;\n }\n\n log.debug('Current scheduleId for stats:', this.currentScheduleId);\n });\n\n this.core.on(E.LAYOUT_PREPARE_REQUEST, async (layoutId: number) => {\n await this.prepareLayout(layoutId);\n // Non-sync or no layout playing yet: show immediately.\n // Sync transitions: onLayoutShow handles showing with stagger.\n if (!this.syncManager || this.renderer.getCurrentLayoutId() === null) {\n this.renderer.showLayout(layoutId);\n }\n });\n\n this.core.on(E.LAYOUT_ALREADY_PLAYING, (layoutId: number) => {\n // The schedule says this layout should still be playing. Verify the renderer\n // actually has an active timer — if not, the renderer stalled (e.g. after a\n // GPU crash/recovery or restart) and we need to force a re-show.\n if (!this.renderer.hasActiveLayoutTimer()) {\n log.warn(`Layout ${layoutId} has no active timer — restarting layout`);\n this.renderer.stopCurrentLayout();\n // stopCurrentLayout → layoutEnd → advanceToNextLayout → re-prepares + shows\n }\n });\n\n this.core.on(E.LAYOUT_EXPIRE_CURRENT, () => {\n log.info('Schedule changed — expiring current layout');\n this.renderer.stopCurrentLayout();\n // stopCurrentLayout() emits layoutEnd → the layoutEnd handler\n // calls advanceToNextLayout() which picks the next scheduled layout\n });\n\n this.core.on(E.NO_LAYOUTS_SCHEDULED, () => {\n this.updateStatus('No layouts scheduled');\n });\n\n this.core.on(E.COLLECTION_COMPLETE, () => {\n const layoutId = this.core.getCurrentLayoutId();\n if (layoutId) {\n this.updateStatus(`Playing layout ${layoutId}`);\n } else if (this.preparingLayoutId) {\n this.updateStatus(`Downloading layout ${this.preparingLayoutId}...`);\n }\n\n // Duration probing is handled by the debounced re-probe (3s after last\n // file cached) — avoids 404s from probing before downloads complete.\n });\n\n this.core.on(E.COLLECTION_ERROR, async (error: any) => {\n this.updateStatus(`Collection error: ${error}`, 'error');\n\n // Display not found / not authorized — show setup screen so user can re-register\n const msg = error?.message || String(error);\n if (msg.includes('403') && (msg.includes('Display not found') || msg.includes('not authorized'))) {\n log.warn('Display not registered or not authorized — showing setup screen');\n if (!this.setupOverlay) {\n this.setupOverlay = new SetupOverlay();\n }\n this.setupOverlay.show();\n return;\n }\n\n // Report fault to CMS (triggers dashboard alert)\n this.reportFault('COLLECTION_FAILED', `Collection cycle failed: ${error?.message || error}`);\n });\n\n this.core.on(E.XMR_CONNECTED, (url: string) => {\n log.info('XMR connected:', url);\n });\n\n this.core.on(E.XMR_MISCONFIGURED, (info: { reason: string; url?: string; message: string }) => {\n log.warn(`XMR misconfigured (${info.reason}): ${info.message}`);\n });\n\n // Log level changes from CMS (overlays are controlled by config.controls, not log level)\n this.core.on(E.LOG_LEVEL_CHANGED, () => {\n log.info(`Log level changed`);\n });\n\n // Overlay layout push from XMR\n this.core.on(E.OVERLAY_LAYOUT_REQUEST, async (layoutId: number) => {\n log.info('Overlay layout requested:', layoutId);\n // Re-use existing overlay rendering (schedule-driven overlays already work)\n // Just need to prepare and render the overlay layout\n await this.prepareLayout(layoutId);\n this.renderer.showLayout(layoutId);\n });\n\n // Revert to schedule (undo XMR layout override)\n this.core.on(E.REVERT_TO_SCHEDULE, () => {\n log.info('Reverting to scheduled content');\n this.updateStatus('Reverting to schedule...');\n });\n\n // Display settings events\n if (this.displaySettings) {\n this.displaySettings.on('interval-changed', (newInterval: number) => {\n log.info(`Collection interval changed to ${newInterval}s`);\n });\n\n this.displaySettings.on('settings-applied', (_settings: any, changes: string[]) => {\n if (changes.length > 0) {\n log.info('Settings updated from CMS:', changes.join(', '));\n }\n // Start periodic screenshots once we have settings (only first time)\n if (!this._screenshotInterval) {\n this.startScreenshotInterval();\n }\n });\n }\n\n // Stats submission\n this.core.on(E.SUBMIT_STATS_REQUEST, async () => {\n await this.submitStats();\n });\n\n // Log submission to CMS\n this.core.on(E.SUBMIT_LOGS_REQUEST, async () => {\n await this.submitLogs();\n });\n\n // Screenshot capture (triggered by XMR or periodic interval)\n this.core.on(E.SCREENSHOT_REQUEST, async () => {\n await this.captureAndSubmitScreenshot();\n });\n\n // Handle check-pending-layout events — layout was pending download, now ready\n this.core.on(E.CHECK_PENDING_LAYOUT, async (layoutId: number) => {\n await this.prepareLayout(layoutId);\n this.renderer.showLayout(layoutId);\n });\n\n // Navigate to widget (navWidget action via triggerCode from schedule-level actions)\n this.core.on(E.NAVIGATE_TO_WIDGET, (action: any) => {\n if (action.targetId) {\n this.renderer.navigateToWidget(action.targetId);\n } else {\n log.warn('navigate-to-widget action has no targetId:', action);\n }\n });\n\n // Timeline overlay — visualize upcoming schedule\n this.core.on(E.TIMELINE_UPDATED, (timeline: any[]) => {\n const id = this.core.getCurrentLayoutId();\n const dur = id ? this.core.getLayoutDuration(id) : undefined;\n this.timelineOverlay?.update(timeline, id, dur);\n });\n }\n\n /**\n * Setup multi-display sync event handlers.\n * Handles SYNC_CONFIG and offline sync fallback via OFFLINE_MODE.\n */\n private setupSyncEventHandlers() {\n // Offline sync: if CMS is unreachable but local config has sync settings,\n // start SyncManager so LAN-only displays can still sync with each other.\n this.core.on(E.OFFLINE_MODE, (isOffline: boolean) => {\n if (isOffline && !this.syncManager && config.data?.sync) {\n log.info('[Sync] Offline mode with local sync config — starting sync');\n this.core.syncConfig = config.data.sync;\n this.core.emit(E.SYNC_CONFIG, config.data.sync);\n }\n });\n\n // Multi-display sync: create SyncManager when CMS provides sync config (or local fallback)\n this.core.on(E.SYNC_CONFIG, async (syncConfig: any) => {\n if (this.syncManager) {\n this.syncManager.stop();\n }\n\n // Cross-device sync: build WebSocket relay URL.\n // Always rebuild for followers (mDNS re-discovers lead IP/port each cycle).\n // Lead connects to its own relay (localhost).\n if (syncConfig.syncPublisherPort) {\n if (syncConfig.syncGroupId) {\n syncConfig.syncGroup = String(syncConfig.syncGroupId);\n }\n\n if (syncConfig.isLead) {\n syncConfig.relayUrl = `ws://localhost:${syncConfig.syncPublisherPort}/sync`;\n // Trigger mDNS advertisement so followers can discover us\n fetch('/system/advertise-sync', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ syncGroupId: syncConfig.syncGroupId, port: syncConfig.syncPublisherPort, displayId: config.hardwareKey }),\n }).catch(() => {});\n } else {\n // Try mDNS discovery first, fall back to CMS-provided IP\n let leadHost = syncConfig.syncGroup;\n try {\n const res = await fetch(`/system/discover-lead?syncGroupId=${syncConfig.syncGroupId}`);\n if (res.ok) {\n const { host, port } = await res.json();\n leadHost = host;\n log.info(`mDNS discovered lead at ${host}:${port}`);\n }\n } catch (_) {\n log.warn('mDNS discovery failed, using CMS-provided IP');\n }\n syncConfig.relayUrl = `ws://${leadHost}:${syncConfig.syncPublisherPort}/sync`;\n }\n }\n\n // Persist resolved sync config to config.json so offline restarts\n // can sync over LAN without CMS. Strips runtime-only fields.\n // Merge with existing sync config to preserve local-only fields\n // (topology, choreography, staggerMs, gridCols, gridRows).\n const { syncToken, ...persistable } = syncConfig;\n const merged = { ...(config.data?.sync || {}), ...persistable };\n if ((window as any).electronAPI?.setConfig) {\n (window as any).electronAPI.setConfig({ sync: merged });\n } else {\n fetch('/config', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ sync: merged }),\n }).catch(() => {});\n }\n\n // Pass CMS server key as sync token for relay auth (shared by all displays on this CMS)\n if (!syncConfig.syncToken) {\n syncConfig.syncToken = config.cmsKey;\n }\n\n this.syncManager = new SyncManager({\n displayId: config.hardwareKey,\n syncConfig,\n onLayoutChange: async (layoutId: string) => {\n // Wall mode: map lead's layout to this display's position-specific layout\n const layoutMap = syncConfig.layoutMap || config.sync?.layoutMap;\n const mappedId = layoutMap?.[layoutId] ?? layoutId;\n if (mappedId !== layoutId) {\n log.info(`[Sync] Wall mode: lead layout ${layoutId} → local layout ${mappedId}`);\n }\n // Follower: preload layout hidden, show comes from onLayoutShow\n log.info(`[Sync] Preparing layout ${mappedId} (waiting for show signal)`);\n await this.prepareLayout(parseInt(String(mappedId), 10));\n // Report ready to lead (use lead's layoutId so lead can track readiness)\n this.syncManager?.reportReady(layoutId);\n },\n onLayoutShow: (layoutId: string) => {\n // Map lead's layout ID to this display's layout (wall mode)\n const layoutMap = syncConfig.layoutMap || config.sync?.layoutMap;\n const mappedId = layoutMap?.[layoutId] ?? layoutId;\n const numericId = parseInt(String(mappedId), 10);\n\n // Compute choreography stagger delay (0 if no choreography configured)\n const choreo = syncConfig.choreography || 'simultaneous';\n const staggerMs = syncConfig.staggerMs ?? 150;\n\n // Build stagger options: prefer 2D topology, fall back to 1D position\n const staggerOpts: any = { choreography: choreo, staggerMs };\n if (syncConfig.topology) {\n staggerOpts.topology = syncConfig.topology;\n staggerOpts.gridCols = syncConfig.gridCols ?? 1;\n staggerOpts.gridRows = syncConfig.gridRows ?? 1;\n } else {\n staggerOpts.position = syncConfig.position ?? 0;\n staggerOpts.totalDisplays = syncConfig.totalDisplays ?? 1;\n }\n const stagger = computeStagger(staggerOpts);\n\n if (stagger > 0) {\n log.info(`[Sync] Show layout ${numericId} with ${stagger}ms choreography delay (${choreo})`);\n setTimeout(() => this.renderer.showLayout(numericId), stagger);\n } else {\n log.info(`[Sync] Show layout ${numericId}`);\n this.renderer.showLayout(numericId);\n }\n },\n onVideoStart: (layoutId: string, regionId: string) => {\n // Resume paused video in the specified region\n log.info(`[Sync] Video start: layout ${layoutId} region ${regionId}`);\n this.renderer.resumeRegionMedia?.(regionId);\n },\n // Lead: follower delegated stats — submit on their behalf\n onStatsReport: async (followerId: string, statsXml: string, ack: () => void) => {\n log.info(`[Sync] Submitting stats for follower ${followerId}`);\n try {\n const success = await this.xmds.submitStats(statsXml, followerId);\n if (success) ack();\n } catch (err: any) {\n log.warn(`[Sync] Stats submission failed for follower ${followerId}:`, err);\n }\n },\n // Lead: follower delegated logs — submit on their behalf\n onLogsReport: async (followerId: string, logsXml: string, ack: () => void) => {\n log.info(`[Sync] Submitting logs for follower ${followerId}`);\n try {\n const success = await this.xmds.submitLog(logsXml, followerId);\n if (success) ack();\n } catch (err: any) {\n log.warn(`[Sync] Log submission failed for follower ${followerId}:`, err);\n }\n },\n // Follower: lead confirmed our stats were submitted\n onStatsAck: async (_displayId: string) => {\n log.info('[Sync] Lead confirmed stats submission');\n if (this._pendingFollowerStats && this.statsCollector) {\n await this.statsCollector.clearSubmittedStats(this._pendingFollowerStats);\n this._pendingFollowerStats = null;\n }\n },\n // Follower: lead confirmed our logs were submitted\n onLogsAck: async (_displayId: string) => {\n log.info('[Sync] Lead confirmed logs submission');\n if (this._pendingFollowerLogs && this.logReporter) {\n await this.logReporter.clearSubmittedLogs(this._pendingFollowerLogs);\n this._pendingFollowerLogs = null;\n }\n },\n // Relay: group membership changed (auto-detect totalDisplays)\n onGroupUpdate: (totalDisplays: number, topology: Record<string, any>) => {\n log.info(`[Sync] Group update: ${totalDisplays} displays, topology: ${JSON.stringify(topology)}`);\n syncConfig.totalDisplays = totalDisplays;\n },\n });\n this.core.setSyncManager(this.syncManager);\n this.syncManager.start();\n log.info(`[Sync] SyncManager started as ${syncConfig.isLead ? 'LEAD' : 'FOLLOWER'}`);\n this.updateConfigDisplay();\n });\n }\n\n /**\n * Setup download and cache event handlers.\n * Handles FILES_RECEIVED, DOWNLOAD_REQUEST, PURGE_REQUEST, PURGE_ALL_REQUEST.\n */\n private setupDownloadEventHandlers() {\n this.core.on(E.FILES_RECEIVED, (files: any[]) => {\n this.updateStatus(`Downloading ${files.length} files...`);\n });\n\n this.core.on(E.DOWNLOAD_REQUEST, async (groupedFiles: any) => {\n // Download orchestration runs in main thread — no SW messaging\n this.downloadOverlay?.startUpdating();\n try {\n // Push current JWT token to proxy for cache-through CMS requests\n const token = this.xmds?.getToken?.() || null;\n if (token) {\n await fetch('/auth-token', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ token }),\n });\n }\n await this.enqueueDownloads(groupedFiles);\n log.info('Download enqueue complete');\n } catch (error) {\n log.error('Download request failed:', error);\n this.updateStatus('Download failed: ' + error, 'error');\n }\n });\n\n this.core.on(E.PURGE_REQUEST, async (purgeFiles: any[]) => {\n try {\n const result = await store.remove(purgeFiles);\n log.info(`Purge complete: ${result.deleted}/${result.total} files deleted`);\n } catch (error) {\n log.warn('Purge failed:', error);\n }\n });\n\n this.core.on(E.PURGE_ALL_REQUEST, async () => {\n log.info('Purging all cached content...');\n this.updateStatus('Purging cache...');\n try {\n // Delete all files from ContentStore\n const allFiles = await store.list();\n if (allFiles.length > 0) {\n const result = await store.remove(allFiles);\n log.info(`Purged ${result.deleted} files from ContentStore`);\n }\n // Clean up any legacy Cache API caches (pre-ContentStore migration)\n const cacheNames = await caches.keys();\n if (cacheNames.length > 0) {\n await Promise.all(cacheNames.map(name => caches.delete(name)));\n log.info(`Purged ${cacheNames.length} legacy caches`);\n }\n } catch (error) {\n log.error('Cache purge failed:', error);\n }\n });\n }\n\n /**\n * Setup command execution event handlers.\n * Handles EXECUTE_NATIVE_COMMAND, COMMAND_RESULT, SCHEDULED_COMMAND.\n */\n private setupCommandEventHandlers() {\n // Native command execution (#202) — shell commands delegated by PlayerCore\n // Electron: use IPC (in-process, faster). Chromium/other: HTTP to proxy server.\n this.core.on(E.EXECUTE_NATIVE_COMMAND, async (data: any) => {\n let result;\n if ((window as any).electronAPI?.executeShellCommand) {\n result = await (window as any).electronAPI.executeShellCommand({\n commandString: data.commandString,\n });\n } else {\n try {\n const resp = await fetch('/shell-command', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ commandString: data.commandString }),\n });\n result = await resp.json();\n } catch (err: any) {\n result = { success: false, reason: err.message };\n }\n }\n this.core.emit(E.COMMAND_RESULT, { code: data.code, ...result });\n });\n\n // Command execution result\n this.core.on(E.COMMAND_RESULT, (result: any) => {\n log.info('Command result:', result);\n if (!result.success) {\n this.reportFault('COMMAND_FAILED', `Command ${result.code} failed: ${result.reason || 'unknown'}`);\n }\n });\n\n // Scheduled commands (#17) — execute commands whose scheduled time has arrived\n this.core.on(E.SCHEDULED_COMMAND, (command: any) => {\n log.info(`Scheduled command: ${command.code}`);\n this.core.executeCommand(command.code);\n });\n }\n\n\n /**\n * Setup Interactive Control handler (receives messages from SW for widget IC requests)\n * IC library in widget iframes makes XHR to /player/pwa/ic/*, SW forwards here.\n */\n private setupInteractiveControl() {\n this._swIcHandler = (event: any) => {\n if (event.data?.type !== 'INTERACTIVE_CONTROL') return;\n\n const { method, path, search, body } = event.data;\n const port = event.ports?.[0];\n if (!port) return;\n\n const response = this.handleInteractiveControl(method, path, search, body);\n port.postMessage(response);\n };\n navigator.serviceWorker?.addEventListener('message', this._swIcHandler);\n }\n\n /**\n * Notify widget iframes when DataConnector data changes.\n * XIC library listens for postMessage { ctrl: 'rtNotifyData', data: { dataKey } }\n * and calls the widget's registered notifyData callback.\n */\n private setupDataConnectorNotify() {\n const dcManager = this.core.getDataConnectorManager();\n dcManager.on('data-changed', (dataKey: string) => {\n const iframes = document.querySelectorAll<HTMLIFrameElement>('iframe');\n const message = { ctrl: 'rtNotifyData', data: { dataKey } };\n for (const iframe of iframes) {\n try {\n iframe.contentWindow?.postMessage(message, '*');\n } catch { /* cross-origin iframe, ignore */ }\n }\n });\n }\n\n /**\n * Setup keyboard and presenter remote controls.\n * Handles arrow keys, page up/down, space for next/prev/pause,\n * and MediaSession API for multimedia keyboard keys.\n */\n private setupRemoteControls() {\n // Keep focus on main document so keyboard shortcuts work even with widget iframes.\n // Iframes steal focus — this pulls it back after a short delay so interactive\n // widgets still work momentarily but keyboard control returns to the player.\n window.addEventListener('blur', () => {\n // Don't steal focus when setup overlay is open (user is typing in iframe inputs)\n if (this.setupOverlay?.isVisible()) return;\n setTimeout(() => window.focus(), 200);\n });\n\n // Forward keyboard events from widget iframes to the main document.\n // Iframes have their own document, so keydown on the parent never fires\n // when an iframe has focus. We observe new iframes and attach forwarders.\n const attachIframeKeyForwarder = (iframe: HTMLIFrameElement) => {\n const tryAttach = () => {\n try {\n const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;\n if (!iframeDoc) return;\n if ((iframe as any).__keyForwarderAttached) return;\n (iframe as any).__keyForwarderAttached = true;\n iframeDoc.addEventListener('keydown', (e: KeyboardEvent) => {\n // Don't forward keys from setup overlay — user is typing in form inputs\n if (this.setupOverlay?.isVisible()) return;\n // Re-dispatch on the main document so our handler fires\n const clone = new KeyboardEvent('keydown', {\n key: e.key, code: e.code, keyCode: e.keyCode,\n ctrlKey: e.ctrlKey, shiftKey: e.shiftKey, altKey: e.altKey, metaKey: e.metaKey,\n bubbles: true, cancelable: true,\n });\n if (document.dispatchEvent(clone)) return; // not prevented\n e.preventDefault();\n });\n } catch { /* cross-origin iframe, ignore */ }\n };\n iframe.addEventListener('load', tryAttach);\n tryAttach();\n };\n\n // Attach to existing and future iframes\n Array.from(document.querySelectorAll('iframe')).forEach(f => attachIframeKeyForwarder(f as HTMLIFrameElement));\n this._iframeObserver = new MutationObserver((mutations) => {\n for (const m of mutations) {\n for (const node of m.addedNodes) {\n if (node instanceof HTMLIFrameElement) attachIframeKeyForwarder(node);\n if (node instanceof HTMLElement) {\n node.querySelectorAll('iframe').forEach(f => attachIframeKeyForwarder(f as HTMLIFrameElement));\n }\n }\n }\n });\n this._iframeObserver.observe(document.body, { childList: true, subtree: true });\n\n // Read control toggles from config (injected by proxy into localStorage)\n const controls = this.getControls();\n const { keyboard: kb = {} } = controls;\n const debugOverlays = kb.debugOverlays === true;\n const setupKey = kb.setupKey === true;\n const playbackControl = kb.playbackControl === true;\n const videoControls = kb.videoControls === true;\n\n // Keyboard / presenter remote (clicker) controls\n document.addEventListener('keydown', (e: KeyboardEvent) => {\n // Ctrl+Q — quit (Chromium kiosk: calls server /quit; Electron: handled by menu accelerator)\n if (e.key === 'q' && (e.ctrlKey || e.metaKey)) {\n e.preventDefault();\n log.info('[Remote] Quit requested (Ctrl+Q)');\n fetch('/quit', { method: 'POST' }).catch(() => {});\n return;\n }\n\n switch (e.key) {\n case 't':\n case 'T':\n if (!debugOverlays) break;\n if (!this.timelineOverlay) {\n this.timelineOverlay = new TimelineOverlay(true, (layoutId) => this.skipToLayout(layoutId));\n }\n this.timelineOverlay.toggle();\n break;\n case 'd':\n case 'D':\n if (!debugOverlays) break;\n if (!this.downloadOverlay) {\n this.downloadOverlay = new DownloadOverlay({ enabled: true, autoHide: false });\n this.downloadOverlay.setProgressCallback(() => downloadManager.getProgress());\n }\n this.downloadOverlay.toggle();\n break;\n case 'v':\n case 'V': {\n if (!videoControls) break;\n // Collect videos from parent + all same-origin iframes (widget regions)\n const allVideos: HTMLVideoElement[] = [...document.querySelectorAll<HTMLVideoElement>('video')];\n document.querySelectorAll<HTMLIFrameElement>('iframe').forEach(iframe => {\n try { allVideos.push(...iframe.contentDocument!.querySelectorAll<HTMLVideoElement>('video')); } catch {}\n });\n const show = allVideos.length > 0 && !allVideos[0].controls;\n allVideos.forEach(v => v.controls = show);\n break;\n }\n // Playback control: next/prev/pause\n case 'ArrowRight':\n case 'PageDown':\n if (!playbackControl) break;\n log.info('[Remote] Next layout (keyboard)');\n this.core.advanceToNextLayout();\n e.preventDefault();\n break;\n case 'ArrowLeft':\n case 'PageUp':\n if (!playbackControl) break;\n log.info('[Remote] Previous layout (keyboard)');\n this.core.advanceToPreviousLayout();\n e.preventDefault();\n break;\n case ' ':\n if (!playbackControl) break;\n log.info('[Remote] Toggle pause (keyboard)');\n if (this.renderer.isPaused()) {\n this.renderer.resume();\n } else {\n this.renderer.pause();\n }\n e.preventDefault();\n break;\n case 'r':\n case 'R':\n if (!playbackControl) break;\n if (this.core.isLayoutOverridden()) {\n log.info('[Remote] Revert to schedule (keyboard)');\n this.core.revertToSchedule();\n }\n break;\n case 's':\n case 'S':\n if (!setupKey) break;\n if (!this.setupOverlay) {\n this.setupOverlay = new SetupOverlay();\n }\n this.setupOverlay.toggle();\n e.preventDefault(); // prevent 's' from being typed into the focused input\n break;\n }\n });\n\n // MediaSession API for multimedia keys (only fires when media is active)\n if (playbackControl && 'mediaSession' in navigator) {\n navigator.mediaSession.setActionHandler('nexttrack', () => {\n log.info('[Remote] Next layout (MediaSession)');\n this.core.advanceToNextLayout();\n });\n navigator.mediaSession.setActionHandler('previoustrack', () => {\n log.info('[Remote] Previous layout (MediaSession)');\n this.core.advanceToPreviousLayout();\n });\n navigator.mediaSession.setActionHandler('pause', () => {\n log.info('[Remote] Pause (MediaSession)');\n this.renderer.pause();\n });\n navigator.mediaSession.setActionHandler('play', () => {\n log.info('[Remote] Resume (MediaSession)');\n this.renderer.resume();\n });\n }\n\n log.info('Remote controls initialized (keyboard + MediaSession)');\n }\n\n /** Read controls config (injected by proxy from config.json into localStorage). */\n private getControls(): Record<string, any> {\n return config.controls;\n }\n\n /**\n * Skip to a specific layout by ID (from timeline click or XMR command).\n * Uses changeLayout() which sets a layout override — press R to revert to schedule.\n */\n private skipToLayout(layoutId: number) {\n log.info(`Skipping to layout ${layoutId} (timeline click)`);\n this.core.changeLayout(layoutId);\n }\n\n private parseBody(body: string | null): any {\n try { return body ? JSON.parse(body) : {}; } catch (_) { return {}; }\n }\n\n /**\n * Handle an Interactive Control request from a widget\n */\n private handleInteractiveControl(method: string, path: string, search: string, body: string | null): any {\n log.debug('IC request:', method, path, search);\n\n switch (path) {\n case '/info':\n return {\n status: 200,\n body: JSON.stringify({\n hardwareKey: config.hardwareKey,\n displayName: config.displayName,\n playerType: 'pwa',\n currentLayoutId: this.core.getCurrentLayoutId()\n })\n };\n\n case '/trigger': {\n const data = this.parseBody(body);\n // Forward to renderer for layout-level actions (widget navigation)\n this.renderer.emit('interactiveTrigger', {\n targetId: data.id,\n triggerCode: data.trigger\n });\n // Forward to core for schedule-level actions (layout navigation)\n if (data.trigger) {\n this.core.handleTrigger(data.trigger);\n }\n return { status: 200, body: 'OK' };\n }\n\n case '/duration/expire': {\n const data = this.parseBody(body);\n log.info('IC: Widget duration expire requested for', data.id);\n this.renderer.emit('widgetExpire', { widgetId: data.id });\n return { status: 200, body: 'OK' };\n }\n\n case '/duration/extend': {\n const data = this.parseBody(body);\n log.info('IC: Widget duration extend by', data.duration, 'for', data.id);\n this.renderer.emit('widgetExtendDuration', {\n widgetId: data.id,\n duration: parseInt(data.duration)\n });\n return { status: 200, body: 'OK' };\n }\n\n case '/duration/set': {\n const data = this.parseBody(body);\n log.info('IC: Widget duration set to', data.duration, 'for', data.id);\n this.renderer.emit('widgetSetDuration', {\n widgetId: data.id,\n duration: parseInt(data.duration)\n });\n return { status: 200, body: 'OK' };\n }\n\n case '/fault': {\n const data = this.parseBody(body);\n this.reportFault(data.code || 'WIDGET_FAULT', data.reason || 'Widget reported fault', {\n layoutId: data.layoutId,\n regionId: data.regionId,\n widgetId: data.widgetId\n });\n return { status: 200, body: 'OK' };\n }\n\n case '/realtime': {\n const params = new URLSearchParams(search);\n const dataKey = params.get('dataKey');\n log.debug('IC: Realtime data request for key:', dataKey);\n\n if (!dataKey) {\n return { status: 400, body: JSON.stringify({ error: 'Missing dataKey parameter' }) };\n }\n\n const dcManager = this.core.getDataConnectorManager();\n const connectorData = dcManager.getData(dataKey);\n\n if (connectorData === null) {\n return { status: 404, body: JSON.stringify({ error: `No data available for key: ${dataKey}` }) };\n }\n\n const responseBody = typeof connectorData === 'string' ? connectorData : JSON.stringify(connectorData);\n return { status: 200, body: responseBody };\n }\n\n case '/criteria': {\n // Return display properties/criteria that widgets can query\n // Used by widgets to adapt content based on display characteristics\n return {\n status: 200,\n body: JSON.stringify({\n displayId: config.displayId,\n hardwareKey: config.hardwareKey,\n displayName: config.displayName,\n width: window.innerWidth,\n height: window.innerHeight,\n latitude: config.latitude || null,\n longitude: config.longitude || null,\n playerType: 'pwa'\n })\n };\n }\n\n default:\n return { status: 404, body: JSON.stringify({ error: 'Unknown IC route' }) };\n }\n }\n\n /**\n * Notify PlayerCore that a file download completed.\n * Called directly from enqueueDownloads() — no SW messaging needed.\n */\n private notifyFileCached(fileId: string, fileType: string) {\n log.debug(`Download complete: ${fileType}/${fileId}`);\n\n if (fileType === 'layout') {\n this.core.notifyMediaReady(parseInt(fileId), fileType);\n } else if (fileType === 'media') {\n // Pass saveAs string for media files (matches pendingLayouts entries)\n const saveAs = this._fileIdToSaveAs.get(fileId) || fileId;\n this._cachedMediaKeys.add(saveAs);\n this.core.notifyMediaReady(saveAs, fileType);\n } else {\n // Dependencies, widgets, datasets — track by storeKey\n this._cachedMediaKeys.add(fileId);\n }\n\n // Debounced duration probe — run after downloads settle\n if (this._probeTimer) clearTimeout(this._probeTimer);\n this._probeTimer = setTimeout(() => {\n this._probeTimer = null;\n this.probeLayoutDurations().catch(() => {});\n }, 3000);\n\n // Debounced media status check — update timeline missing-media annotations\n if (this._mediaStatusTimer) clearTimeout(this._mediaStatusTimer);\n this._mediaStatusTimer = setTimeout(() => {\n this._mediaStatusTimer = null;\n this.checkTimelineMediaStatus().catch(() => {});\n }, 2000);\n }\n\n /**\n * Enqueue files for download — runs in main thread, no SW messaging.\n * Ported from MessageHandler.handleDownloadFiles() with direct callbacks.\n */\n private async enqueueDownloads(data: any) {\n const { extractMediaIdsFromXlf } = await import('@xiboplayer/sw');\n const { layoutOrder, files, layoutDependants } = data;\n // Use DownloadManager facade methods (not direct queue access)\n\n /** Store key = URL path without leading / and query params */\n const storeKeyFrom = (f: any) => (f.path || '').split('?')[0].replace(/^\\/+/, '') || `${f.type || 'media'}/${f.id}`;\n\n // Build fileId→saveAs map from CMS RequiredFiles data\n for (const f of files) {\n if (f.saveAs) {\n this._fileIdToSaveAs.set(String(f.id), f.saveAs);\n }\n }\n // Build lookup maps from flat CMS file list\n const xlfFiles = new Map();\n const resources: any[] = [];\n const mediaFiles = new Map();\n const idToKeys = new Map();\n for (const f of files) {\n if (f.type === 'layout') {\n xlfFiles.set(parseInt(f.id), f);\n } else if (f.type === 'static') {\n resources.push(f);\n } else {\n const key = `${f.type}:${f.id}`;\n mediaFiles.set(key, f);\n const bareId = String(f.id);\n if (!idToKeys.has(bareId)) idToKeys.set(bareId, []);\n idToKeys.get(bareId).push(key);\n }\n }\n\n log.info(`Download: ${layoutOrder.length} layouts, ${mediaFiles.size} media, ${resources.length} resources`);\n\n // ── Step 1: Fetch + parse all XLFs (cache-through handles store/CMS) ──\n const layoutMediaMap = new Map();\n const allXlfIds = [...layoutOrder, ...[...xlfFiles.keys()].filter((id: number) => !layoutOrder.includes(id))];\n const xlfPromises = allXlfIds.map(async (layoutId: number) => {\n const xlfFile = xlfFiles.get(layoutId);\n if (!xlfFile?.path) return;\n\n let xlfText: string | undefined;\n\n // Try store first, then cache-through fetches from CMS on miss\n try {\n const headers: Record<string, string> = {};\n if (xlfFile.cmsDownloadUrl) headers['X-Cms-Download-Url'] = xlfFile.cmsDownloadUrl;\n const resp = await fetch(xlfFile.path, Object.keys(headers).length ? { headers } : undefined);\n if (resp.ok) {\n xlfText = await resp.text();\n log.info(`Fetched XLF ${layoutId} (${xlfText.length} bytes)`);\n // Store XLF in content store so prepareLayout() can find it via store.get()\n await store.put(`${STORE_PREFIX}/layouts`, String(layoutId), new Blob([xlfText], { type: 'text/xml' }));\n this.notifyFileCached(String(layoutId), 'layout');\n }\n } catch (_) {}\n\n if (xlfText) {\n layoutMediaMap.set(layoutId, extractMediaIdsFromXlf(xlfText, log));\n }\n });\n await Promise.allSettled(xlfPromises);\n log.info(`Parsed ${layoutMediaMap.size} XLFs`);\n\n // Helper: enqueue a file, attach completion callback\n const enqueueFile = async (builder: any, file: any): Promise<boolean> => {\n if (!file.path || file.path === 'null' || file.path === 'undefined') return false;\n\n const storeKey = storeKeyFrom(file);\n\n // Check if already stored on disk (200 = cached, 204 = not in store)\n try {\n const headResp = await fetch(`/store/${storeKey}`, { method: 'HEAD' });\n if (headResp.status === 200) return false;\n } catch (_) {}\n\n // Check if already downloading (download manager keys are type/id, not URL paths)\n const dmKey = `${file.type}/${file.id}`;\n if (downloadManager.getTask(dmKey)) return false;\n\n // Check for existing chunks — skip already-downloaded ones\n try {\n const mcResp = await fetch(`/store/missing-chunks/${storeKey}`);\n if (mcResp.ok) {\n const { missing, numChunks } = await mcResp.json();\n if (numChunks > 0 && missing.length < numChunks) {\n const existing = new Set<number>();\n for (let i = 0; i < numChunks; i++) {\n if (!missing.includes(i)) existing.add(i);\n }\n file.skipChunks = existing;\n log.info(`Resuming ${storeKey}: ${existing.size}/${numChunks} chunks cached, ${missing.length} to download`);\n }\n }\n } catch (_) {}\n\n const fileDownload = builder.addFile(file);\n if (fileDownload.state !== 'pending') return false;\n\n // Direct callback — no postMessage needed\n fileDownload.wait().then((blob: any) => {\n const fileSize = parseInt(file.size) || blob.size;\n log.info('Download complete:', storeKey, `(${fileSize} bytes)`);\n\n // Mark chunked files as complete\n if (fileSize > this._chunkConfig.chunkSize) {\n fetch('/store/mark-complete', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ storeKey }),\n }).catch((e: any) => log.warn('mark-complete failed:', storeKey, e.message));\n }\n\n this.notifyFileCached(String(file.id), file.type);\n downloadManager.removeCompleted(dmKey);\n }).catch((err: any) => {\n log.error('Download failed:', file.id, err);\n downloadManager.removeCompleted(dmKey);\n });\n return true;\n };\n\n // ── Step 2: Enqueue resources (parallel HEAD checks) ──\n const resourceBuilder = downloadManager.createTaskBuilder();\n await Promise.all(resources.map(file => enqueueFile(resourceBuilder, file)));\n const resourceTasks = await resourceBuilder.build();\n if (resourceTasks.length > 0) {\n resourceTasks.push(BARRIER);\n downloadManager.enqueueOrderedTasks(resourceTasks);\n }\n\n // ── Step 3: For each layout in play order, merge XLF + dependants ──\n const claimed = new Set();\n const nonScheduledIds = [...layoutMediaMap.keys()].filter((id: number) => !layoutOrder.includes(id));\n const filenameToMediaId = new Map();\n for (const [key, file] of mediaFiles) {\n if (file.saveAs) filenameToMediaId.set(file.saveAs, key);\n }\n\n const depMap = new Map();\n if (layoutDependants) {\n for (const [id, filenames] of Object.entries(layoutDependants)) {\n depMap.set(parseInt(id, 10), filenames);\n }\n }\n\n for (const layoutId of layoutOrder) {\n const xlfMediaIds = layoutMediaMap.get(layoutId);\n if (!xlfMediaIds) continue;\n\n const bareIds = new Set(xlfMediaIds);\n for (const nsId of nonScheduledIds) {\n const nsMediaIds = layoutMediaMap.get(nsId);\n if (nsMediaIds) {\n for (const id of nsMediaIds) bareIds.add(id);\n }\n }\n const deps = depMap.get(layoutId) || [];\n for (const filename of deps) {\n const key = filenameToMediaId.get(filename);\n if (key) bareIds.add(key);\n }\n\n const matched: any[] = [];\n for (const bareId of bareIds) {\n if (mediaFiles.has(bareId) && !claimed.has(bareId)) {\n matched.push(mediaFiles.get(bareId));\n claimed.add(bareId);\n continue;\n }\n const keys = idToKeys.get(String(bareId)) || [];\n for (const key of keys) {\n if (claimed.has(key)) continue;\n matched.push(mediaFiles.get(key));\n claimed.add(key);\n }\n }\n if (matched.length === 0) continue;\n\n log.info(`Layout ${layoutId}: ${matched.length} media`);\n matched.sort((a: any, b: any) => (a.size || 0) - (b.size || 0));\n const builder = downloadManager.createTaskBuilder();\n await Promise.all(matched.map(file => enqueueFile(builder, file)));\n const orderedTasks = await builder.build();\n if (orderedTasks.length > 0) {\n orderedTasks.push(BARRIER);\n downloadManager.enqueueOrderedTasks(orderedTasks);\n }\n }\n\n // Enqueue unclaimed media\n const unclaimed = [...mediaFiles.keys()].filter((id: string) => !claimed.has(id));\n if (unclaimed.length > 0) {\n log.info(`${unclaimed.length} media not in any XLF`);\n const builder = downloadManager.createTaskBuilder();\n await Promise.all(unclaimed.map(id => {\n const file = mediaFiles.get(id);\n return file ? enqueueFile(builder, file) : Promise.resolve(false);\n }));\n const orderedTasks = await builder.build();\n if (orderedTasks.length > 0) {\n downloadManager.enqueueOrderedTasks(orderedTasks);\n }\n }\n\n log.info('Downloads active:', downloadManager.running, ', queued:', downloadManager.queued);\n }\n\n /**\n * Setup renderer event handlers\n */\n private setupRendererEventHandlers() {\n this.renderer.on('layoutStart', (layoutId: number, _layout: any) => {\n log.info('Layout started:', layoutId);\n this.updateStatus(`Playing layout ${layoutId}`);\n\n this.core.setCurrentLayout(layoutId);\n\n // Store layout-level enableStat for use in layoutEnd\n this._currentLayoutEnableStat = _layout?.enableStat !== false;\n\n // Update timeline overlay with current layout's known duration\n const layoutDur = this.core.getLayoutDuration(layoutId) || _layout?.duration;\n this.timelineOverlay?.update(null, layoutId, layoutDur);\n\n // Track stats: start layout (only if enableStat is not disabled)\n if (this.statsCollector && this._currentLayoutEnableStat) {\n this.statsCollector.startLayout(layoutId, this.currentScheduleId).catch((err: any) => {\n log.error('Failed to start layout stat:', err);\n });\n }\n });\n\n this.renderer.on('layoutEnd', (layoutId: number) => {\n log.info('Layout ended:', layoutId);\n\n // Record play at END so maxPlaysPerHour doesn't interrupt the current play.\n // Previously recorded at layoutStart, which caused periodic collections to\n // filter the layout mid-playback (e.g., 200s video cut at 168s).\n scheduleManager?.recordPlay(layoutId.toString());\n\n // Track stats: end layout (only if enableStat was not disabled)\n if (this.statsCollector && this._currentLayoutEnableStat) {\n this.statsCollector.endLayout(layoutId, this.currentScheduleId).catch((err: any) => {\n log.error('Failed to end layout stat:', err);\n });\n }\n\n // If a new layout is already rendering or being prepared (async fetch),\n // skip advance — the transition was already handled by the caller.\n // Stats/play recording above still run for proper tracking.\n if (this.renderer.getCurrentLayoutId() && this.renderer.getCurrentLayoutId() !== layoutId) {\n log.debug(`Layout ${layoutId} ended but ${this.renderer.getCurrentLayoutId()} already playing, skipping advance`);\n return;\n }\n if (this.preparingLayoutId && this.preparingLayoutId !== layoutId) {\n log.debug(`Layout ${layoutId} ended but ${this.preparingLayoutId} being prepared, skipping advance`);\n return;\n }\n\n // Report to CMS\n this.core.notifyLayoutStatus(layoutId);\n\n // Clear current layout to allow replay/advance\n this.core.clearCurrentLayout();\n\n // If a new layout is already pending download, don't advance\n // (avoids redundant XMDS calls and duplicate download requests)\n const pending = this.core.getPendingLayouts();\n if (pending.length > 0) {\n log.info(`Layout ${pending[0]} pending download, skipping advance`);\n return;\n }\n\n // Advance to the next layout in the schedule (round-robin cycling)\n // This avoids a full collect() cycle — just picks the next layout and renders it.\n // Periodic collect() cycles still run on the collection interval to sync with CMS.\n log.info('Layout cycle completed, advancing to next layout...');\n this.core.advanceToNextLayout();\n });\n\n this.renderer.on('widgetStart', (data: any) => {\n const { widgetId, layoutId, mediaId } = data;\n log.debug('Widget started:', data.type, widgetId, 'media:', mediaId);\n\n // Track stats: start widget/media (only if enableStat is not disabled)\n if (this.statsCollector && mediaId && data.enableStat !== false) {\n this.statsCollector.startWidget(mediaId, layoutId, this.currentScheduleId).catch((err: any) => {\n log.error('Failed to start widget stat:', err);\n });\n }\n });\n\n this.renderer.on('widgetEnd', (data: any) => {\n const { widgetId, layoutId, mediaId } = data;\n log.debug('Widget ended:', data.type, widgetId, 'media:', mediaId);\n\n // Track stats: end widget/media (only if enableStat is not disabled)\n if (this.statsCollector && mediaId && data.enableStat !== false) {\n this.statsCollector.endWidget(mediaId, layoutId, this.currentScheduleId).catch((err: any) => {\n log.error('Failed to end widget stat:', err);\n });\n }\n });\n\n // Widget commands (#202) — execute commands embedded in layout widgets\n this.renderer.on('widgetCommand', (data: any) => {\n log.info('Widget command:', data.commandCode);\n const commands = { [data.commandCode]: { commandString: data.commandString } };\n this.core.executeCommand(data.commandCode, commands);\n });\n\n this.renderer.on('error', (error: any) => {\n log.error('Renderer error:', error);\n this.updateStatus(`Error: ${error.type}`, 'error');\n\n // Report fault to CMS (triggers dashboard alert)\n this.reportFault(error.type || 'RENDERER_ERROR', `Renderer error: ${error.message || error.type}`, {\n layoutId: error.layoutId,\n regionId: error.regionId,\n widgetId: error.widgetId\n });\n });\n\n // Handle interactive actions from touch/click and keyboard triggers\n this.renderer.on('action-trigger', (data: any) => {\n const { actionType, triggerCode, layoutCode, targetId, commandCode } = data;\n log.info('Action trigger:', actionType, data);\n\n switch (actionType) {\n case 'navLayout':\n case 'navigateToLayout':\n if (triggerCode) {\n this.core.handleTrigger(triggerCode);\n } else if (layoutCode) {\n this.core.changeLayout(layoutCode);\n }\n break;\n\n case 'navWidget':\n case 'navigateToWidget':\n if (triggerCode) {\n this.core.handleTrigger(triggerCode);\n } else if (targetId) {\n this.renderer.navigateToWidget(targetId);\n }\n break;\n\n case 'previousWidget':\n this.renderer.previousWidget(data.source?.regionId);\n break;\n\n case 'nextWidget':\n this.renderer.nextWidget(data.source?.regionId);\n break;\n\n case 'command':\n if (commandCode) {\n this.core.executeCommand(commandCode);\n }\n break;\n\n default:\n log.warn('Unknown action type:', actionType);\n }\n\n // Record interaction event for proof of play (#19)\n if (this.statsCollector) {\n this.statsCollector.recordEvent('touch', this.core.getCurrentLayoutId(), data.targetId || null, this.currentScheduleId);\n }\n });\n\n // Widget duration webhooks (#16) — fire HTTP POST when widget duration expires\n this.renderer.on('widgetAction', (data: any) => {\n if (data.type === 'durationEnd' && data.url) {\n log.info(`Widget ${data.widgetId} duration ended, calling webhook: ${data.url}`);\n\n // Record webhook event for proof of play (#19)\n if (this.statsCollector) {\n this.statsCollector.recordEvent('webhook', data.layoutId, data.widgetId, this.currentScheduleId);\n }\n\n fetch(data.url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n widgetId: data.widgetId,\n layoutId: data.layoutId,\n regionId: data.regionId,\n event: 'durationEnd',\n timestamp: new Date().toISOString()\n })\n }).catch(err => log.warn('Webhook failed (non-critical):', err));\n }\n });\n\n // Correct timeline duration when video metadata reveals actual duration\n this.renderer.on('layoutDurationUpdated', (layoutId: number, duration: number, final: boolean) => {\n this.core.recordLayoutDuration(String(layoutId), duration, final);\n });\n\n // Handle next layout preload request from renderer\n // Fired at 75% of current layout duration to pre-build the next layout's DOM\n this.renderer.on('request-next-layout-preload', async () => {\n try {\n // Peek at the next layout without advancing the schedule index\n const next = this.core.peekNextLayout();\n if (!next) {\n log.debug('No next layout to preload (single layout schedule or same layout)');\n return;\n }\n\n const nextLayoutId = next.layoutId;\n\n // Skip if already preloaded or preload in-flight\n if (this.renderer.layoutPool.has(nextLayoutId)) {\n log.debug(`Layout ${nextLayoutId} already in preload pool`);\n return;\n }\n if ((this.renderer as any)._preloadingLayoutId === nextLayoutId) {\n log.debug(`Layout ${nextLayoutId} preload already in-flight`);\n return;\n }\n\n log.info(`Preloading next layout ${nextLayoutId}...`);\n\n // Get XLF from cache\n const xlfBlob = await store.get(`${STORE_PREFIX}/layouts`, nextLayoutId);\n if (!xlfBlob) {\n log.debug(`Layout ${nextLayoutId} XLF not cached, skipping preload`);\n return;\n }\n\n const xlfXml = await xlfBlob.text();\n const doc = new DOMParser().parseFromString(xlfXml, 'text/xml');\n\n // Check if all required media is cached\n const { allMedia: requiredMedia } = this.getMediaIds(doc);\n const allMediaCached = await this.checkAllMediaCached(requiredMedia);\n\n if (!allMediaCached) {\n log.debug(`Media not fully cached for layout ${nextLayoutId}, skipping preload`);\n return;\n }\n\n // Fetch widget HTML before preloading (same as prepareLayout)\n await this.fetchWidgetHtml(doc, nextLayoutId);\n\n // Preload the layout into the renderer's pool\n const success = await this.renderer.preloadLayout(xlfXml, nextLayoutId);\n if (success) {\n log.info(`Layout ${nextLayoutId} preloaded successfully`);\n } else {\n log.warn(`Layout ${nextLayoutId} preload failed (will fall back to normal render)`);\n }\n } catch (error) {\n log.warn('Layout preload failed (non-blocking):', error);\n // Non-blocking: preload failure is graceful, normal render path will be used\n }\n });\n\n // Handle video playback errors — re-download only missing chunks\n this.renderer.on('videoError', async ({ storedAs }: any) => {\n if (!storedAs) return;\n const storeKey = `${PLAYER_API.slice(1)}/media/file/${storedAs}`;\n try {\n const resp = await fetch(`/store/missing-chunks/${storeKey}`);\n const { missing } = await resp.json();\n if (missing.length === 0) {\n log.warn(`Video ${storedAs}: corrupt file (all chunks present), deleting for re-download`);\n await fetch('/store/delete', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ files: [{ key: storeKey }] }),\n });\n const layoutId = this.core.getCurrentLayoutId();\n if (layoutId) {\n this.core.setPendingLayout(layoutId, [storedAs]);\n }\n this.core.collectNow().catch((err: any) => {\n log.error(`Failed to trigger re-download for ${storedAs}:`, err.message);\n });\n return;\n }\n log.warn(`Video ${storedAs}: ${missing.length} missing chunks (${missing.join(', ')}), re-downloading`);\n\n // Unmark completion (keeps existing chunks on disk) so HEAD returns 404\n await fetch('/store/unmark-complete', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ storeKey }),\n });\n\n // Trigger collection — enqueueFile will populate skipChunks for existing chunks\n this.core.collectNow().catch((err: any) => {\n log.error(`Failed to trigger re-download for ${storedAs}:`, err.message);\n });\n } catch (err: any) {\n log.error(`Failed to check/re-download ${storedAs}:`, err.message);\n }\n });\n }\n\n /**\n * Prepare and render layout (Platform-specific logic)\n */\n private async prepareLayout(layoutId: number) {\n // Same layout replay — use renderer's built-in replay path which\n // re-emits layoutStart, restarts timer and widget cycling.\n if (this.renderer.getCurrentLayoutId() === layoutId) {\n log.debug(`Layout ${layoutId} replay`);\n this.core.clearPreparingLayout();\n // Renderer's same-layout replay path reuses existing DOM — XLF not re-parsed\n await this.renderer.renderLayout('', layoutId);\n return;\n }\n\n // Guard: prevent concurrent preparations of the same layout.\n // Instead of dropping the event (which caused permanent stalls when the\n // first attempt failed due to a store race), schedule a retry after\n // the current preparation finishes.\n if (this.preparingLayoutId === layoutId) {\n log.debug(`Layout ${layoutId} preparation in progress, will retry after it completes`);\n this._pendingRetryLayoutId = layoutId;\n return;\n }\n\n this.preparingLayoutId = layoutId;\n try {\n // Get XLF from cache\n const xlfBlob = await store.get(`${STORE_PREFIX}/layouts`, layoutId);\n if (!xlfBlob) {\n log.info('Layout not in cache yet, marking as pending:', layoutId);\n // Mark layout as pending so when it downloads, we'll retry\n // Use layoutId as required file (will trigger on layout file cached)\n this.core.setPendingLayout(layoutId, [String(layoutId)]);\n this.updateStatus(`Downloading layout ${layoutId}...`);\n return;\n }\n\n const xlfXml = await xlfBlob.text();\n\n // Parse XLF once — reuse Document for media check and widget HTML fetch\n const xlfDoc = new DOMParser().parseFromString(xlfXml, 'text/xml');\n\n // Check if all required media is cached\n const { allMedia: requiredMedia } = this.getMediaIds(xlfDoc);\n const allMediaCached = await this.checkAllMediaCached(requiredMedia);\n\n if (!allMediaCached) {\n // Reorder download queue: current layout's media first, hold others.\n // All files (including all chunks) must complete before other layouts start.\n downloadManager.prioritizeLayoutFiles(requiredMedia.map(String));\n\n log.info(`Waiting for media to finish downloading for layout ${layoutId}`);\n this.updateStatus(`Preparing layout ${layoutId}...`);\n this.core.setPendingLayout(layoutId, requiredMedia);\n return; // Keep playing current layout until media is ready\n }\n\n // Fetch widget HTML (skip if already preloaded — was fetched during preload)\n if (!this.renderer.hasPreloadedLayout(layoutId)) {\n await this.fetchWidgetHtml(xlfDoc, layoutId);\n }\n\n // Preload layout into pool (hidden). Caller decides when to show.\n await this.renderer.preloadLayout(xlfXml, layoutId);\n\n // Clear pending status — layout prepared successfully, all media cached.\n // Without this, pendingLayouts keeps the layout marked as ⚠ missing\n // until it actually plays (which may be minutes later in the rotation).\n this.core.pendingLayouts.delete(layoutId);\n\n log.info(`Layout ${layoutId} ready`);\n\n } catch (error: any) {\n log.error('Failed to prepare layout:', layoutId, error);\n this.updateStatus(`Failed to load layout ${layoutId}`, 'error');\n\n // Report fault to CMS (triggers dashboard alert)\n this.reportFault('LAYOUT_LOAD_FAILED', `Failed to prepare layout ${layoutId}: ${error?.message || error}`, {\n layoutId\n });\n } finally {\n this.preparingLayoutId = null;\n this.core.clearPreparingLayout();\n\n // If another check-pending-layout arrived while we were preparing,\n // retry after a short delay to let the ContentStore settle.\n // This fixes the race where FILE_CACHED notification arrives before\n // the PUT to ContentStore is visible to HEAD requests.\n const retryId = this._pendingRetryLayoutId;\n this._pendingRetryLayoutId = null;\n if (retryId !== null && retryId !== undefined && this.core.getCurrentLayoutId() !== retryId) {\n log.debug(`Retrying preparation for layout ${retryId} after 500ms`);\n setTimeout(() => this.prepareLayout(retryId), 500);\n }\n }\n }\n\n /**\n * Get all required media saveAs filenames and video-specific ones from layout XLF.\n * Returns saveAs strings (via _fileIdToSaveAs map) for store key matching.\n */\n private getMediaIds(xlfXmlOrDoc: string | Document): { allMedia: string[]; videoMedia: string[] } {\n const doc = typeof xlfXmlOrDoc === 'string'\n ? new DOMParser().parseFromString(xlfXmlOrDoc, 'text/xml')\n : xlfXmlOrDoc;\n const allMedia: string[] = [];\n const videoMedia: string[] = [];\n\n doc.querySelectorAll('media[fileId]').forEach(el => {\n const fileId = el.getAttribute('fileId');\n if (fileId) {\n const saveAs = this._fileIdToSaveAs.get(fileId) || fileId;\n // Skip layout XLF references — stored in layouts/ store, not media/file/\n if (saveAs.endsWith('.xlf')) return;\n allMedia.push(saveAs);\n if (el.getAttribute('type') === 'video') {\n videoMedia.push(saveAs);\n }\n }\n });\n\n // Include background image file ID from layout element\n const bgFileId = doc.querySelector('layout')?.getAttribute('background');\n if (bgFileId) {\n const saveAs = this._fileIdToSaveAs.get(bgFileId) || bgFileId;\n if (!allMedia.includes(saveAs)) {\n allMedia.push(saveAs);\n }\n }\n\n return { allMedia, videoMedia };\n }\n\n /**\n * Check if all required media files are cached and ready.\n * Uses StoreClient.has() → HEAD /store${PLAYER_API}/media/:id to check ContentStore.\n */\n /**\n * Check if all required media files are cached and ready.\n * Uses storedAs filenames for store key matching: /media/file/{saveAs}\n */\n private async checkAllMediaCached(mediaSaveAs: string[]): Promise<boolean> {\n // Check in-memory set first — avoids all HEAD requests for known-cached files\n const unknown = mediaSaveAs.filter(s => !this._cachedMediaKeys.has(s));\n if (unknown.length === 0) return true;\n\n // HEAD-check all unknown files against the content store.\n // Always check the store directly — the download queue may have stale tasks\n // for files that are already cached (race between download completion and\n // task cleanup). The HEAD check is fast (<1ms for local store) and authoritative.\n const toCheck = unknown;\n\n const results = await Promise.all(\n toCheck.map(async (saveAs) => {\n try {\n const cached = await store.has(STORE_PREFIX, `media/file/${saveAs}`);\n if (cached) this._cachedMediaKeys.add(saveAs);\n return cached;\n } catch {\n log.warn(`Unable to verify media ${saveAs}, assuming cached (offline mode)`);\n return true;\n }\n })\n );\n const missing = toCheck.filter((_, i) => !results[i]);\n if (missing.length > 0) {\n log.debug(`Media not yet cached: ${missing.join(', ')}`);\n return false;\n }\n return true;\n }\n\n /**\n * Fetch widget HTML for all widgets in layout (parallel)\n */\n private async fetchWidgetHtml(xlfXmlOrDoc: string | Document, layoutId: number) {\n const doc = typeof xlfXmlOrDoc === 'string'\n ? new DOMParser().parseFromString(xlfXmlOrDoc, 'text/xml')\n : xlfXmlOrDoc;\n\n const fetchPromises: Promise<void>[] = [];\n\n for (const regionEl of doc.querySelectorAll('region')) {\n const regionId = regionEl.getAttribute('id');\n\n for (const mediaEl of regionEl.querySelectorAll('media')) {\n const type = mediaEl.getAttribute('type');\n const widgetId = mediaEl.getAttribute('id');\n const render = mediaEl.getAttribute('render');\n\n // XLF render=\"html\" means CMS provides pre-rendered HTML via getResource.\n // render=\"native\" means player handles the media directly (video, image, audio).\n if (render === 'html') {\n fetchPromises.push(\n (async () => {\n try {\n // Check ContentStore for existing widget HTML\n const storeId = `${layoutId}/${regionId}/${widgetId}`;\n let html: string | null = null;\n\n const existing = await store.get(`${STORE_PREFIX}/widgets`, storeId);\n if (existing) {\n html = await existing.text();\n log.debug(`Found cached widget HTML for ${type} ${widgetId}`);\n }\n\n if (!html) {\n html = await this.xmds.getResource(layoutId, regionId, widgetId);\n log.debug(`Retrieved widget HTML for ${type} ${widgetId} from CMS`);\n }\n // Always process: injects <base> tag, rewrites IC hostAddress.\n // cacheWidgetHtml is idempotent — already-rewritten URLs won't re-match.\n const result = await cacheWidgetHtml(layoutId, regionId, widgetId, html);\n html = result.html;\n\n // Update raw content in XLF\n const rawEl = mediaEl.querySelector('raw');\n if (rawEl) {\n rawEl.textContent = html;\n } else {\n const newRaw = doc.createElement('raw');\n newRaw.textContent = html;\n mediaEl.appendChild(newRaw);\n }\n } catch (error) {\n log.warn(`Failed to get widget HTML for ${type} ${widgetId}:`, error);\n }\n })()\n );\n }\n }\n }\n\n if (fetchPromises.length > 0) {\n log.info(`Fetching ${fetchPromises.length} widget HTML resources in parallel...`);\n await Promise.all(fetchPromises);\n log.debug('All widget HTML fetched');\n }\n }\n\n /**\n * Check media cache status for all scheduled layouts.\n * For each layout: load XLF from cache, extract media IDs, check each with store.has().\n * Feeds results into PlayerCore.setLayoutMediaStatus() for timeline annotation.\n */\n private async checkTimelineMediaStatus() {\n if (this.scheduledLayoutIds.size === 0) return;\n\n for (const layoutId of this.scheduledLayoutIds) {\n const layoutFile = `${layoutId}.xlf`;\n try {\n const xlfBlob = await store.get(`${STORE_PREFIX}/layouts`, layoutId);\n if (!xlfBlob) continue;\n\n const xlfXml = await xlfBlob.text();\n const { allMedia } = this.getMediaIds(xlfXml);\n\n if (allMedia.length === 0) {\n this.core.setLayoutMediaStatus(layoutFile, true);\n continue;\n }\n\n const missing: string[] = [];\n for (const saveAs of allMedia) {\n if (this._cachedMediaKeys.has(saveAs)) continue;\n // If in download queue, it's not cached — skip HEAD\n const storeKey = `${STORE_PREFIX}/media/file/${saveAs}`;\n if (downloadManager.getTask(storeKey)) { missing.push(saveAs); continue; }\n try {\n const cached = await store.has(STORE_PREFIX, `media/file/${saveAs}`);\n if (cached) this._cachedMediaKeys.add(saveAs);\n else missing.push(saveAs);\n } catch {\n // Assume cached on error (offline mode)\n }\n }\n\n this.core.setLayoutMediaStatus(layoutFile, missing.length === 0, missing);\n } catch {\n // Skip layouts we can't load\n }\n }\n\n // Re-emit annotated timeline\n this.core.logUpcomingTimeline();\n }\n\n /**\n * Probe video durations for all scheduled layouts.\n * Uses preload=\"metadata\" — only fetches headers (~50KB), not the full video.\n * Feeds discovered durations into PlayerCore for accurate timeline calculation.\n */\n private async probeLayoutDurations() {\n if (this.scheduledLayoutIds.size === 0) return;\n\n for (const layoutId of this.scheduledLayoutIds) {\n\n try {\n const xlfBlob = await store.get(`${STORE_PREFIX}/layouts`, layoutId);\n if (!xlfBlob) continue;\n\n const xlfXml = await xlfBlob.text();\n const { videoMedia } = this.getMediaIds(xlfXml);\n if (videoMedia.length === 0) continue;\n\n // Parse XLF to find video widgets with duration=0 (use media length)\n const parser = new DOMParser();\n const doc = parser.parseFromString(xlfXml, 'text/xml');\n\n // Probe actual video durations, keyed by fileId\n const videoDurations = new Map<string, number>();\n let dynamicVideoCount = 0;\n for (const mediaEl of doc.querySelectorAll('media[type=\"video\"]')) {\n const useDuration = mediaEl.getAttribute('useDuration');\n if (useDuration === '1') continue; // Has explicit CMS duration, skip\n\n const fileId = mediaEl.getAttribute('fileId');\n if (!fileId) continue;\n dynamicVideoCount++;\n\n const saveAs = this._fileIdToSaveAs.get(fileId) || fileId;\n const exists = await store.has(STORE_PREFIX, `media/file/${saveAs}`);\n if (!exists) continue;\n\n // Probe metadata only — does NOT download the full video\n const duration = await this.probeVideoDuration(`${window.location.origin}${PLAYER_API}/media/file/${saveAs}`);\n if (duration > 0) {\n videoDurations.set(fileId, duration);\n }\n }\n\n if (videoDurations.size === 0) continue;\n\n // Only mark final if ALL dynamic videos were successfully probed\n const allProbed = videoDurations.size >= dynamicVideoCount;\n\n // Phase 2: refine layout duration with probed video lengths\n const { duration: probedDuration } = parseLayoutDuration(xlfXml, videoDurations);\n if (probedDuration > 0) {\n this.core.recordLayoutDuration(String(layoutId), probedDuration, allProbed);\n }\n } catch (err) {\n log.debug(`Duration probe failed for layout ${layoutId}:`, err);\n }\n }\n }\n\n /**\n * Probe a single video's duration using metadata only.\n * Creates a temporary <video preload=\"metadata\"> element, reads duration, destroys it.\n */\n private probeVideoDuration(url: string): Promise<number> {\n return new Promise((resolve) => {\n const video = document.createElement('video');\n video.preload = 'metadata';\n video.muted = true;\n\n const cleanup = () => {\n video.removeAttribute('src');\n video.load(); // Release resources\n };\n\n video.addEventListener('loadedmetadata', () => {\n const dur = video.duration;\n cleanup();\n resolve(dur);\n }, { once: true });\n\n video.addEventListener('error', () => {\n cleanup();\n resolve(0);\n }, { once: true });\n\n // Safety timeout — don't block forever\n setTimeout(() => {\n cleanup();\n resolve(0);\n }, 5000);\n\n video.src = url;\n });\n }\n\n /**\n * Update config display\n */\n private updateConfigDisplay() {\n const configEl = document.getElementById('config-info');\n if (configEl) {\n const version = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '?';\n const buildDate = typeof __BUILD_DATE__ !== 'undefined' ? __BUILD_DATE__.replace('T', ' ').replace(/\\.\\d+Z$/, '') : '';\n const versionStr = buildDate ? `v${version} (${buildDate})` : `v${version}`;\n let text = `${versionStr} | CMS: ${config.cmsUrl} | Display: ${config.displayName || 'Unknown'} | HW: ${config.hardwareKey}`;\n const sc = this.core?.getSyncConfig?.();\n if (sc) {\n const relay = sc.relayUrl ? new URL(sc.relayUrl).host : '';\n text += ` | Sync: ${sc.isLead ? 'LEAD' : `FOLLOWER → ${relay}`} (group ${sc.syncGroupId || sc.syncGroup})`;\n }\n configEl.textContent = text;\n }\n }\n\n /**\n * Generic submission pipeline for stats and logs.\n * Handles in-flight guard, sync delegation, CMS submission, and cleanup.\n */\n private async submitCollectedData(options: {\n name: string;\n pendingFlag: '_pendingFollowerStats' | '_pendingFollowerLogs';\n getItems: () => Promise<any[]>;\n formatFn: (items: any[]) => string;\n delegateFn: (xml: string) => void;\n submitFn: (xml: string) => Promise<any>;\n clearFn: (items: any[]) => Promise<void>;\n }): Promise<void> {\n const { name, pendingFlag, getItems, formatFn, delegateFn, submitFn, clearFn } = options;\n\n // Guard: don't start a new delegation while one is in-flight\n if (this[pendingFlag] !== null) {\n log.debug(`${name} delegation in-flight, skipping`);\n return;\n }\n\n try {\n const items = await getItems();\n\n if (items.length === 0) {\n log.debug(`No ${name} to submit`);\n return;\n }\n\n const xml = formatFn(items);\n\n // Follower with live lead: delegate via BroadcastChannel\n if (this.syncManager && !this.syncManager.isLead && this._syncLeadAlive()) {\n log.info(`[Sync] Delegating ${items.length} ${name} to lead`);\n this[pendingFlag] = items;\n delegateFn(xml);\n return;\n }\n\n // Lead, standalone, or lead-dead follower: submit directly\n if (this.syncManager && !this.syncManager.isLead) {\n log.warn(`[Sync] Lead not alive, submitting ${name} directly`);\n }\n\n log.info(`Submitting ${items.length} ${name} to CMS...`);\n\n const success = await submitFn(xml);\n\n if (success) {\n log.info(`${name} submitted successfully`);\n await clearFn(items);\n } else {\n log.warn(`${name} submission failed (CMS returned false)`);\n }\n } catch (error) {\n log.error(`Failed to submit ${name}:`, error);\n }\n }\n\n /**\n * Submit proof of play stats to CMS\n */\n private async submitStats(): Promise<void> {\n if (!this.statsCollector) {\n log.warn('Stats collector not initialized');\n return;\n }\n\n const aggregationLevel = this.displaySettings?.getSetting('aggregationLevel') || 'Individual';\n\n await this.submitCollectedData({\n name: 'stats',\n pendingFlag: '_pendingFollowerStats',\n getItems: async () => aggregationLevel === 'Aggregate'\n ? this.statsCollector.getAggregatedStatsForSubmission(50)\n : this.statsCollector.getStatsForSubmission(50),\n formatFn: formatStats,\n delegateFn: (xml) => this.syncManager.reportStats(xml),\n submitFn: (xml) => this.xmds.submitStats(xml),\n clearFn: (items) => this.statsCollector.clearSubmittedStats(items),\n });\n }\n\n /**\n * Submit player logs to CMS for remote debugging\n */\n private async submitLogs(): Promise<void> {\n if (!this.logReporter) return;\n\n await this.submitCollectedData({\n name: 'logs',\n pendingFlag: '_pendingFollowerLogs',\n getItems: () => this.logReporter.getLogsForSubmission(),\n formatFn: formatLogs,\n delegateFn: (xml) => this.syncManager.reportLogs(xml),\n submitFn: (xml) => this.xmds.submitLog(xml),\n clearFn: (items) => this.logReporter.clearSubmittedLogs(items),\n });\n }\n\n /**\n * Report a fault to both the log dashboard and the player_faults dashboard.\n * Combines logReporter.reportFault() (log dashboard) with submitFault() (faults dashboard).\n */\n private reportFault(code: string, reason: string, details?: { layoutId?: number; regionId?: string; widgetId?: string }): void {\n this.logReporter?.reportFault(code, reason);\n this.submitFault(code, reason, details);\n }\n\n /**\n * Submit a fault report to CMS for the player_faults dashboard.\n */\n private submitFault(code: string, reason: string, details?: { layoutId?: number; regionId?: string; widgetId?: string }) {\n if (!this.xmds) return;\n\n const fault = JSON.stringify([{\n code,\n reason,\n date: new Date().toISOString().replace('T', ' ').substring(0, 19),\n ...details\n }]);\n\n this.xmds.reportFaults(fault).catch((err: any) => {\n log.debug('reportFaults failed (non-critical):', err);\n });\n }\n\n /**\n * Capture screenshot and submit to CMS.\n *\n * Strategy (best available, tried in order):\n * 0. Electron IPC — webContents.capturePage() via preload bridge.\n * Pixel-perfect, captures video/WebGL/composited layers, zero DOM cost.\n * Only available when running inside the Electron shell.\n * 1. getDisplayMedia() — native pixel capture via screen sharing API.\n * Pixel-perfect, zero DOM manipulation. Chromium kiosk auto-approves\n * via --auto-select-desktop-capture-source flag.\n * 2. Direct canvas drawing — fallback that draws img/video/canvas elements\n * directly. Text-only widgets (clocks, tickers) won't appear.\n *\n * The first successful method is cached for subsequent calls.\n */\n private async captureAndSubmitScreenshot() {\n // Concurrency guard — skip if a capture is already in flight\n if (this._screenshotInFlight) {\n log.debug('Screenshot capture already in progress, skipping');\n return;\n }\n this._screenshotInFlight = true;\n\n try {\n let base64: string;\n\n // Electron path: use native webContents.capturePage() via IPC\n if (this._screenshotMethod === 'electron' ||\n (this._screenshotMethod === null && (window as any).electronAPI?.captureScreenshot)) {\n const electronResult = await (window as any).electronAPI.captureScreenshot();\n if (electronResult) {\n this._screenshotMethod = 'electron';\n base64 = electronResult;\n } else {\n // Electron capture returned null (window not yet painted).\n // Do NOT fall through to getDisplayMedia — it triggers a\n // permission dialog that blocks the whole UI. Skip this\n // cycle; capturePage() will succeed on the next interval.\n log.debug('Electron screenshot not ready yet, will retry next interval');\n return;\n }\n } else if (this._screenshotMethod === 'displayMedia' ||\n (this._screenshotMethod === null && typeof navigator.mediaDevices?.getDisplayMedia === 'function')) {\n // Try getDisplayMedia — pixel-perfect screen capture, zero DOM cost.\n // Chromium kiosk auto-approves via --auto-accept-this-tab-capture.\n try {\n base64 = await this.captureDisplayMedia();\n this._screenshotMethod = 'displayMedia';\n } catch (e: any) {\n log.warn('getDisplayMedia failed, falling back to html2canvas:', e.message || e);\n this._screenshotMethod = null;\n base64 = await this.captureHtml2CanvasIsolated();\n this._screenshotMethod = 'html2canvas';\n }\n } else {\n // Tier 3: html2canvas hybrid (Firefox, other browsers)\n // Direct draw for img/video/canvas + per-iframe html2canvas for HTML widgets\n this._screenshotMethod = 'html2canvas';\n base64 = await this.captureHtml2CanvasIsolated();\n }\n\n const success = await this.xmds.submitScreenShot(base64);\n if (success) {\n log.info(`Screenshot submitted (${this._screenshotMethod})`);\n } else {\n log.warn('Screenshot submission failed');\n }\n } catch (error) {\n log.error('Failed to capture screenshot:', error);\n } finally {\n this._screenshotInFlight = false;\n }\n }\n\n /**\n * Capture screenshot via getDisplayMedia (screen sharing API).\n * Pixel-perfect, captures everything the GPU renders including video,\n * WebGL, composited layers, and all widget content.\n * Chromium kiosk auto-approves via --auto-select-desktop-capture-source.\n */\n private async captureDisplayMedia(): Promise<string> {\n const stream = await navigator.mediaDevices.getDisplayMedia({\n video: true,\n audio: false,\n preferCurrentTab: true,\n } as any);\n\n try {\n const track = stream.getVideoTracks()[0];\n const imageCapture = new (window as any).ImageCapture(track);\n const bitmap = await imageCapture.grabFrame();\n\n const canvas = document.createElement('canvas');\n canvas.width = bitmap.width;\n canvas.height = bitmap.height;\n const ctx = canvas.getContext('2d')!;\n ctx.drawImage(bitmap, 0, 0);\n bitmap.close();\n\n return canvas.toDataURL('image/jpeg', 0.8).split(',')[1];\n } finally {\n stream.getTracks().forEach(t => t.stop());\n }\n }\n\n /**\n * Capture screenshot via html2canvas hybrid approach.\n * Draws images/video/canvas directly, uses per-iframe html2canvas for\n * HTML widgets (clocks, tickers). CSS contain: strict on capture divs\n * prevents ResizeObserver glitches.\n */\n private async captureHtml2CanvasIsolated(): Promise<string> {\n const canvas = document.createElement('canvas');\n canvas.width = window.innerWidth;\n canvas.height = window.innerHeight;\n const ctx = canvas.getContext('2d')!;\n\n ctx.fillStyle = '#000';\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n\n const container = document.getElementById('player-container');\n if (!container) {\n return canvas.toDataURL('image/jpeg', 0.8).split(',')[1];\n }\n\n // Lazy-load html2canvas\n if (!this._html2canvasMod) {\n this._html2canvasMod = (await import('html2canvas')).default;\n }\n\n // Suppress resize during capture\n if (this.renderer) {\n this.renderer._resizeSuppressed = true;\n }\n\n // Protect the player container from external DOM changes.\n // html2canvas appends/removes elements to document.body which can trigger\n // Chromium reflow affecting the player. contain:strict on the player itself\n // makes it immune to any layout changes outside it.\n const prevContain = container.style.contain || '';\n container.style.contain = 'strict';\n\n try {\n // Draw container background\n const containerRect = container.getBoundingClientRect();\n const containerStyle = getComputedStyle(container);\n const bgColor = containerStyle.backgroundColor;\n if (bgColor && bgColor !== 'transparent' && bgColor !== 'rgba(0, 0, 0, 0)') {\n ctx.fillStyle = bgColor;\n ctx.fillRect(containerRect.left, containerRect.top, containerRect.width, containerRect.height);\n }\n const bgImage = containerStyle.backgroundImage;\n if (bgImage && bgImage !== 'none') {\n const urlMatch = bgImage.match(/url\\([\"']?(.*?)[\"']?\\)/);\n if (urlMatch) {\n try {\n const bgImg = new Image();\n bgImg.crossOrigin = 'anonymous';\n await new Promise<void>((resolve) => {\n bgImg.onload = () => resolve();\n bgImg.onerror = () => resolve();\n setTimeout(() => resolve(), 2000);\n bgImg.src = urlMatch[1];\n });\n if (bgImg.naturalWidth) {\n ctx.drawImage(bgImg, containerRect.left, containerRect.top, containerRect.width, containerRect.height);\n }\n } catch (_) { /* skip failed background */ }\n }\n }\n\n // Draw each visible widget element\n const elements = container.querySelectorAll('img, video, iframe, canvas');\n let drawn = 0;\n\n for (const el of elements) {\n const htmlEl = el as HTMLElement;\n if (htmlEl.style.visibility === 'hidden') continue;\n if (htmlEl.style.display === 'none') continue;\n const rect = el.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) continue;\n\n try {\n if (el instanceof HTMLImageElement) {\n if (!el.complete || !el.naturalWidth) continue;\n const fit = getComputedStyle(el).objectFit;\n if (fit === 'contain' && el.naturalWidth && el.naturalHeight) {\n const d = this.containedRect(el.naturalWidth, el.naturalHeight, rect);\n ctx.drawImage(el, d.x, d.y, d.w, d.h);\n } else {\n ctx.drawImage(el, rect.left, rect.top, rect.width, rect.height);\n }\n drawn++;\n } else if (el instanceof HTMLVideoElement) {\n if (el.readyState < 2) continue;\n const fit = getComputedStyle(el).objectFit;\n if (fit === 'contain' && el.videoWidth && el.videoHeight) {\n const d = this.containedRect(el.videoWidth, el.videoHeight, rect);\n ctx.drawImage(el, d.x, d.y, d.w, d.h);\n } else {\n ctx.drawImage(el, rect.left, rect.top, rect.width, rect.height);\n }\n drawn++;\n } else if (el instanceof HTMLCanvasElement) {\n ctx.drawImage(el, rect.left, rect.top, rect.width, rect.height);\n drawn++;\n } else if (el instanceof HTMLIFrameElement) {\n const iDoc = el.contentDocument;\n if (!iDoc?.body) continue;\n\n // Clone iframe DOM into main document for html2canvas rendering.\n // contain: strict prevents layout effects on the live player.\n const captureDiv = document.createElement('div');\n captureDiv.style.cssText = `position:fixed;left:-9999px;top:0;width:${rect.width}px;height:${rect.height}px;overflow:hidden;`;\n\n // Clone stylesheets with absolute URLs\n const linkPromises: Promise<void>[] = [];\n for (const styleEl of iDoc.querySelectorAll('style')) {\n captureDiv.appendChild(styleEl.cloneNode(true));\n }\n for (const linkEl of iDoc.querySelectorAll('link[rel=\"stylesheet\"]')) {\n const newLink = document.createElement('link');\n newLink.rel = 'stylesheet';\n newLink.href = new URL(linkEl.getAttribute('href') || '', iDoc.baseURI).href;\n captureDiv.appendChild(newLink);\n linkPromises.push(new Promise<void>(resolve => {\n newLink.onload = () => resolve();\n newLink.onerror = () => resolve();\n }));\n }\n\n // Clone body content with absolute img URLs\n const clonedBody = iDoc.body.cloneNode(true) as HTMLElement;\n for (const img of clonedBody.querySelectorAll('img[src]')) {\n const src = img.getAttribute('src') || '';\n if (src && !src.startsWith('http') && !src.startsWith('data:') && !src.startsWith('blob:')) {\n img.setAttribute('src', new URL(src, iDoc.baseURI).href);\n }\n }\n captureDiv.appendChild(clonedBody);\n document.body.appendChild(captureDiv);\n\n // Collect natural dimensions from original iframe images\n const origImgs = iDoc.querySelectorAll('img');\n const imgNaturals = new Map<string, { nw: number; nh: number }>();\n origImgs.forEach((img, i) => {\n if (img.naturalWidth && img.naturalHeight) {\n imgNaturals.set(String(i), { nw: img.naturalWidth, nh: img.naturalHeight });\n }\n });\n\n if (linkPromises.length > 0) {\n await Promise.race([\n Promise.all(linkPromises),\n new Promise(r => setTimeout(r, 500)),\n ]);\n }\n\n const iframeCanvas = await this._html2canvasMod(captureDiv, {\n useCORS: true, allowTaint: true, logging: false,\n backgroundColor: null,\n width: rect.width, height: rect.height,\n onclone: (clonedDoc: Document) => {\n // Force visible — CSS animations reset to opacity:0 in cloned DOM\n const s = clonedDoc.createElement('style');\n s.textContent = '*, *::before, *::after { animation: none !important; transition: none !important; opacity: 1 !important; }';\n clonedDoc.head.appendChild(s);\n\n // Fix object-fit: contain sizing for html2canvas\n const clonedImgs = clonedDoc.querySelectorAll('img');\n clonedImgs.forEach((cImg, i) => {\n const style = clonedDoc.defaultView?.getComputedStyle(cImg);\n if (!style || style.objectFit !== 'contain') return;\n const dims = imgNaturals.get(String(i));\n if (!dims) return;\n\n const cW = cImg.clientWidth || parseFloat(style.width) || 0;\n const cH = cImg.clientHeight || parseFloat(style.height) || 0;\n if (!cW || !cH) return;\n\n const srcAspect = dims.nw / dims.nh;\n const dstAspect = cW / cH;\n let drawW: number, drawH: number;\n if (srcAspect > dstAspect) {\n drawW = cW; drawH = cW / srcAspect;\n } else {\n drawH = cH; drawW = cH * srcAspect;\n }\n\n const wrapper = clonedDoc.createElement('div');\n wrapper.style.cssText = `width:${cW}px;height:${cH}px;display:flex;align-items:center;justify-content:center;overflow:hidden;`;\n cImg.style.objectFit = 'fill';\n cImg.style.width = `${drawW}px`;\n cImg.style.height = `${drawH}px`;\n cImg.parentNode?.insertBefore(wrapper, cImg);\n wrapper.appendChild(cImg);\n });\n },\n });\n\n document.body.removeChild(captureDiv);\n ctx.drawImage(iframeCanvas, rect.left, rect.top, rect.width, rect.height);\n\n // Draw videos directly — html2canvas can't render <video> elements.\n // Draw on top of the html2canvas result so video overlays black placeholder.\n const iframeRect = el.getBoundingClientRect();\n for (const vid of iDoc.querySelectorAll('video') as NodeListOf<HTMLVideoElement>) {\n if (vid.readyState < 2) continue;\n const vr = vid.getBoundingClientRect();\n if (vr.width === 0 || vr.height === 0) continue;\n try {\n const fit = iDoc.defaultView?.getComputedStyle(vid)?.objectFit;\n if (fit === 'contain' && vid.videoWidth && vid.videoHeight) {\n const d = this.containedRect(vid.videoWidth, vid.videoHeight,\n new DOMRect(iframeRect.left + vr.left, iframeRect.top + vr.top, vr.width, vr.height));\n ctx.drawImage(vid, d.x, d.y, d.w, d.h);\n } else {\n ctx.drawImage(vid, iframeRect.left + vr.left, iframeRect.top + vr.top, vr.width, vr.height);\n }\n } catch (_) { /* tainted video */ }\n }\n\n // Draw canvas elements directly (PDF pages, charts rendered in iframe)\n for (const c of iDoc.querySelectorAll('canvas') as NodeListOf<HTMLCanvasElement>) {\n const cr = c.getBoundingClientRect();\n if (cr.width === 0 || cr.height === 0) continue;\n try {\n ctx.drawImage(c, iframeRect.left + cr.left, iframeRect.top + cr.top, cr.width, cr.height);\n } catch (_) { /* tainted canvas */ }\n }\n\n drawn++;\n }\n } catch (e: any) {\n log.warn('Screenshot: failed to draw element', el.tagName, e);\n }\n }\n\n log.debug(`Screenshot: composed ${drawn}/${elements.length} elements`);\n return canvas.toDataURL('image/jpeg', 0.8).split(',')[1];\n } finally {\n container.style.contain = prevContain;\n if (this.renderer) {\n this.renderer._resizeSuppressed = false;\n }\n }\n }\n\n /**\n * Calculate the destination rect for object-fit: contain.\n * Returns the centered rect that preserves the source aspect ratio\n * within the bounding rect (letterbox/pillarbox).\n */\n private containedRect(\n srcW: number, srcH: number, rect: DOMRect\n ): { x: number; y: number; w: number; h: number } {\n const srcAspect = srcW / srcH;\n const dstAspect = rect.width / rect.height;\n let w: number, h: number;\n if (srcAspect > dstAspect) {\n // Source is wider — fit to width, letterbox top/bottom\n w = rect.width;\n h = rect.width / srcAspect;\n } else {\n // Source is taller — fit to height, pillarbox left/right\n h = rect.height;\n w = rect.height * srcAspect;\n }\n return {\n x: rect.left + (rect.width - w) / 2,\n y: rect.top + (rect.height - h) / 2,\n w, h,\n };\n }\n\n /**\n * Start periodic screenshot submission\n */\n private startScreenshotInterval() {\n const intervalSecs = this.displaySettings?.getSetting('screenshotInterval') || 0;\n if (!intervalSecs || intervalSecs <= 0) return;\n\n // Pre-load html2canvas for non-Electron/non-Chromium browsers\n if (!this._html2canvasMod && !(window as any).electronAPI) {\n import('html2canvas').then(m => { this._html2canvasMod = m.default; });\n }\n\n const intervalMs = intervalSecs * 1000;\n log.info(`Starting periodic screenshots every ${intervalSecs}s`);\n this._screenshotInterval = setInterval(() => {\n this.captureAndSubmitScreenshot();\n }, intervalMs);\n }\n\n /**\n * Update status message (Platform-specific UI)\n */\n private updateStatus(message: string, type: 'info' | 'error' = 'info') {\n const statusEl = document.getElementById('status');\n if (statusEl) {\n statusEl.textContent = message;\n statusEl.className = `status status-${type}`;\n }\n if (type === 'error') {\n log.error('Status:', message);\n } else {\n log.info('Status:', message);\n }\n }\n\n private showOfflineIndicator() {\n this.timelineOverlay?.setOffline(true);\n }\n\n private removeOfflineIndicator() {\n this.timelineOverlay?.setOffline(false);\n }\n\n /**\n * Check if the sync lead is alive (for follower delegation).\n * Returns true if any peer with role 'lead' has been seen in the last 15s.\n */\n private _syncLeadAlive(): boolean {\n if (!this.syncManager) return false;\n for (const [, peer] of this.syncManager.followers) {\n if (peer.role === 'lead' && Date.now() - peer.lastSeen < 15000) {\n return true;\n }\n }\n return false;\n }\n\n /**\n * Cleanup\n */\n cleanup() {\n this.core?.cleanup();\n this.renderer?.cleanup();\n\n if (this._screenshotInterval) {\n clearInterval(this._screenshotInterval);\n this._screenshotInterval = null;\n }\n\n if (this._wakeLock) {\n this._wakeLock.release();\n this._wakeLock = null;\n }\n\n if (this.downloadOverlay) {\n this.downloadOverlay.destroy();\n }\n\n if (this.timelineOverlay) {\n this.timelineOverlay.destroy();\n }\n\n // Disconnect iframe observer\n if (this._iframeObserver) {\n this._iframeObserver.disconnect();\n this._iframeObserver = null;\n }\n\n // Remove SW message listeners\n if (navigator.serviceWorker) {\n if (this._swIcHandler) {\n navigator.serviceWorker.removeEventListener('message', this._swIcHandler);\n this._swIcHandler = null;\n }\n }\n\n // Clean up DownloadManager\n downloadManager?.clear();\n\n if (this._probeTimer) {\n clearTimeout(this._probeTimer);\n this._probeTimer = null;\n }\n\n if (this._mediaStatusTimer) {\n clearTimeout(this._mediaStatusTimer);\n this._mediaStatusTimer = null;\n }\n }\n}\n\nfunction startPlayer() {\n const player = new PwaPlayer();\n player.init().catch(error => {\n log.error('Failed to initialize:', error);\n });\n window.addEventListener('beforeunload', () => {\n player.cleanup();\n });\n}\n\nif (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', startPlayer);\n} else {\n startPlayer();\n}\n"],"file":"main-BG2cZsg-.js"}
@@ -0,0 +1,3 @@
1
+ const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./main-BG2cZsg-.js","./chunk-7ZXdHUL4.js","./preload-helper-Chd9yIcd.js","./src-BYVnjdc0.js","./src-C3Sg89t9.js","./src-M1enQEwh.js","./src-BdgQ2CiL.js","./src-BF_sMbmn.js"])))=>i.map(i=>d[i]);
2
+ import"./modulepreload-polyfill-Cf3xff8G.js";import{t as e}from"./preload-helper-Chd9yIcd.js";window.__XIBO_REDIRECTING||e(()=>import(`./main-BG2cZsg-.js`),__vite__mapDeps([0,1,2,3,4,5,6,7]),import.meta.url);
3
+ //# sourceMappingURL=main-CkREX2X6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"mappings":";8FACS,OAAO,oBACV,aAAO","names":[],"ignoreList":[],"sources":["../../index.html?html-proxy&index=1.js"],"sourcesContent":["\n if (!window.__XIBO_REDIRECTING) {\n import('/src/main.ts');\n }\n "],"file":"main-CkREX2X6.js"}