eb-player 2.0.0 → 2.0.2

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 (37) hide show
  1. package/dist/build/eb-player.css +661 -5
  2. package/dist/build/ebplayer.bundle.js +95 -33
  3. package/dist/build/ebplayer.bundle.js.map +1 -1
  4. package/dist/build/theme-forja.css +1 -1
  5. package/dist/build/theme-lequipe.css +655 -0
  6. package/dist/build/theme-modern.css +1 -1
  7. package/dist/build/theme-v2.css +1 -1
  8. package/dist/build/types/core/config.d.ts +14 -2
  9. package/dist/build/types/core/config.d.ts.map +1 -1
  10. package/dist/build/types/core/index.d.ts +1 -1
  11. package/dist/build/types/core/index.d.ts.map +1 -1
  12. package/dist/build/types/core/lifecycle.d.ts.map +1 -1
  13. package/dist/build/types/eb-player.d.ts.map +1 -1
  14. package/dist/build/types/engines/hls.d.ts +1 -0
  15. package/dist/build/types/engines/hls.d.ts.map +1 -1
  16. package/dist/build/types/engines/snapshot/hls.d.ts +6 -2
  17. package/dist/build/types/engines/snapshot/hls.d.ts.map +1 -1
  18. package/dist/build/types/integrations/p2p-manager.d.ts.map +1 -1
  19. package/dist/build/types/skin/controls/seekbar.d.ts.map +1 -1
  20. package/dist/dev/default.js +734 -508
  21. package/dist/dev/default.js.map +1 -1
  22. package/dist/dev/easybroadcast.js +103 -38
  23. package/dist/dev/easybroadcast.js.map +1 -1
  24. package/dist/dev/equipe.js +6683 -0
  25. package/dist/dev/equipe.js.map +1 -0
  26. package/dist/eb-player.css +661 -5
  27. package/dist/players/easybroadcast/easybroadcast.js +397 -0
  28. package/dist/players/easybroadcast/index.html +1 -0
  29. package/dist/players/equipe/equipe.js +397 -0
  30. package/dist/players/equipe/index.html +1 -0
  31. package/dist/players/forja/forja.js +198 -111
  32. package/dist/players/forja/index.html +1 -1
  33. package/dist/theme-forja.css +1 -1
  34. package/dist/theme-lequipe.css +655 -0
  35. package/dist/theme-modern.css +1 -1
  36. package/dist/theme-v2.css +1 -1
  37. package/package.json +8 -73
@@ -28,22 +28,25 @@ var EBPlayerBundle = (function (exports) {
28
28
  }
29
29
  }
30
30
 
31
- var css_248z$5 = "/**\n * eb-player base CSS\n *\n * CSS custom property defaults are provided as fallbacks in var() calls\n * rather than declared on .eb-player. This allows lifecycle.ts to set\n * overrides on the parent container element and have them inherit into\n * .eb-player without being blocked by a competing declaration.\n *\n * Theme overrides use the .eb-player[data-theme=\"...\"] selector pattern (see theme-*.css).\n * Runtime overrides use style.setProperty() via PlayerController.mount() for backward\n * compatibility with the primaryColor / skinColors consumer API.\n *\n * CSS scoping: The .eb-player prefix ensures no style bleed between two co-hosted instances.\n */\n\n.eb-player {\n /* Base styles — all var() calls include hardcoded fallbacks */\n position: relative;\n overflow: hidden;\n font-family: var(--eb-font-family, Arial, sans-serif);\n font-size: var(--eb-font-size-base, 14px);\n color: var(--eb-color-text, #ffffff);\n box-sizing: border-box;\n}\n\n/* Ensure all descendants use border-box sizing */\n.eb-player *,\n.eb-player *::before,\n.eb-player *::after {\n box-sizing: inherit;\n}\n\n/* BEM block scaffold */\n.eb-player__container {\n position: relative;\n width: 100%;\n height: 100%;\n background-color: var(--eb-color-background, rgba(0, 0, 0, 0.8));\n}\n\n.eb-player__video {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: contain;\n}\n";
31
+ var css_248z$6 = "/**\n * eb-player base CSS\n *\n * CSS custom property defaults are provided as fallbacks in var() calls\n * rather than declared on .eb-player. This allows lifecycle.ts to set\n * overrides on the parent container element and have them inherit into\n * .eb-player without being blocked by a competing declaration.\n *\n * Theme overrides use the .eb-player[data-theme=\"...\"] selector pattern (see theme-*.css).\n * Runtime overrides use style.setProperty() via PlayerController.mount() for backward\n * compatibility with the primaryColor / skinColors consumer API.\n *\n * CSS scoping: The .eb-player prefix ensures no style bleed between two co-hosted instances.\n */\n\n.eb-player {\n /* Base styles — all var() calls include hardcoded fallbacks */\n position: relative;\n overflow: hidden;\n font-family: var(--eb-font-family, Arial, sans-serif);\n font-size: var(--eb-font-size-base, 14px);\n color: var(--eb-color-text, #ffffff);\n box-sizing: border-box;\n}\n\n/* Ensure all descendants use border-box sizing */\n.eb-player *,\n.eb-player *::before,\n.eb-player *::after {\n box-sizing: inherit;\n}\n\n/* BEM block scaffold */\n.eb-player__container {\n position: relative;\n width: 100%;\n height: 100%;\n background-color: var(--eb-color-background, rgba(0, 0, 0, 0.8));\n}\n\n.eb-player__video {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: contain;\n}\n";
32
+ styleInject(css_248z$6);
33
+
34
+ var css_248z$5 = "/**\n * Forja theme overrides\n *\n * Applied when the container has [data-theme=\"forja\"].\n * Uses higher specificity than base.css .eb-player selector.\n */\n\n[data-theme=\"forja\"] .eb-player {\n --eb-color-primary: #E63946;\n}\n";
32
35
  styleInject(css_248z$5);
33
36
 
34
- var css_248z$4 = "/**\n * Forja theme overrides\n *\n * Applied when the container has [data-theme=\"forja\"].\n * Uses higher specificity than base.css .eb-player selector.\n */\n\n[data-theme=\"forja\"] .eb-player {\n --eb-color-primary: #E63946;\n}\n";
37
+ var css_248z$4 = "/**\n * Modern theme overrides\n *\n * Applied when the container has [data-theme=\"modern\"].\n * Glassmorphism-inspired design with purple accent, Inter font,\n * rounded corners, backdrop blur, and smooth transitions.\n *\n * Reference: player-reference.html\n */\n\n/* ============================================================\n Google Fonts (Inter 300-600)\n ============================================================ */\n@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap');\n\n/* ============================================================\n Icons: override stroke-based defaults for filled Material icons\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-icon {\n fill: currentColor;\n stroke: none;\n stroke-width: 0;\n}\n\n/* ============================================================\n Root vars & container\n ============================================================ */\n[data-theme=\"modern\"] .eb-player {\n --eb-color-primary: #7c3aed;\n --eb-color-progress: #7c3aed;\n --eb-color-background: rgba(10, 10, 20, 0.85);\n --eb-accent: #1a73e8;\n --eb-font-family: 'Inter', sans-serif;\n font-family: 'Inter', sans-serif;\n border-radius: 14px;\n box-shadow: 0 40px 80px rgba(0,0,0,.8), 0 0 0 1px rgba(255,255,255,.06);\n}\n\n/* ============================================================\n Gradients\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-top-bar {\n background: linear-gradient(to bottom, rgba(0,0,0,.72), transparent);\n height: 110px;\n padding: 14px 16px;\n gap: 6px;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-bottom-bar__gradient {\n height: 150px;\n background: linear-gradient(to top, rgba(0,0,0,.9), transparent);\n}\n\n/* ============================================================\n Top bar: slide-in animation\n ============================================================ */\n[data-theme=\"modern\"] .eb-player.eb-controls-visible .eb-top-bar {\n opacity: 1;\n transform: translateY(0);\n transition: opacity .3s, transform .3s;\n}\n\n[data-theme=\"modern\"] .eb-player.eb-controls-hidden .eb-top-bar {\n opacity: 0;\n transform: translateY(-6px);\n transition: opacity .3s, transform .3s;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-top-bar__actions {\n gap: 6px;\n}\n\n/* ============================================================\n Bottom bar: slide-up animation + spacing\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-bottom-bar__controls-row {\n padding: 0 14px 12px;\n gap: 8px;\n}\n\n[data-theme=\"modern\"] .eb-player.eb-controls-visible .eb-bottom-bar {\n opacity: 1;\n transform: translateY(0);\n transition: opacity .3s, transform .3s;\n}\n\n[data-theme=\"modern\"] .eb-player.eb-controls-hidden .eb-bottom-bar {\n opacity: 0;\n transform: translateY(8px);\n transition: opacity .3s, transform .3s;\n}\n\n/* ============================================================\n Middle bar: glassmorphism circles + wider gap\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-middle-bar {\n gap: 28px;\n}\n\n/* Controls slide with bottom bar */\n[data-theme=\"modern\"] .eb-player.eb-controls-visible .eb-middle-bar {\n opacity: 1;\n transition: opacity .3s;\n}\n\n[data-theme=\"modern\"] .eb-player.eb-controls-hidden .eb-middle-bar {\n opacity: 0;\n transition: opacity .3s;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-middle-bar__play-btn {\n width: 72px;\n height: 72px;\n background: rgba(255,255,255,.18);\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n border: 2px solid rgba(255,255,255,.28);\n transition: background .15s;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-middle-bar__play-btn:hover {\n background: rgba(255,255,255,.28);\n}\n\n[data-theme=\"modern\"] .eb-player .eb-middle-bar__play-btn .eb-icon {\n width: 30px;\n height: 30px;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-middle-bar__seek-btn {\n width: 52px;\n height: 52px;\n border-radius: 50%;\n background: rgba(255,255,255,.14);\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n border: 1.5px solid rgba(255,255,255,.22);\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 1px;\n transition: background .15s;\n padding: 0;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-middle-bar__seek-btn:hover {\n background: rgba(255,255,255,.24);\n}\n\n[data-theme=\"modern\"] .eb-player .eb-seek-circle {\n width: 18px;\n height: 18px;\n /* Reference uses filled skip arrows instead of the stroke circle */\n fill: white;\n stroke: none;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-seek-label {\n font-size: .58em;\n font-weight: 700;\n color: rgba(255,255,255,.9);\n line-height: 1;\n}\n\n/* ============================================================\n Buttons: rounded, softer color, scale on active\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-button,\n[data-theme=\"modern\"] .eb-player .eb-play-pause,\n[data-theme=\"modern\"] .eb-player .eb-fullscreen,\n[data-theme=\"modern\"] .eb-player .eb-pip,\n[data-theme=\"modern\"] .eb-player .eb-cast,\n[data-theme=\"modern\"] .eb-player .eb-volume-mute,\n[data-theme=\"modern\"] .eb-player .eb-live-sync {\n color: rgba(255,255,255,.82);\n border-radius: 8px;\n width: 38px;\n height: 38px;\n transition: background .15s, color .15s, transform .1s;\n flex-shrink: 0;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-button:hover,\n[data-theme=\"modern\"] .eb-player .eb-play-pause:hover,\n[data-theme=\"modern\"] .eb-player .eb-fullscreen:hover,\n[data-theme=\"modern\"] .eb-player .eb-pip:hover,\n[data-theme=\"modern\"] .eb-player .eb-cast:hover,\n[data-theme=\"modern\"] .eb-player .eb-volume-mute:hover,\n[data-theme=\"modern\"] .eb-player .eb-live-sync:hover {\n background: rgba(255,255,255,.12);\n color: #fff;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-button:active,\n[data-theme=\"modern\"] .eb-player .eb-play-pause:active,\n[data-theme=\"modern\"] .eb-player .eb-fullscreen:active,\n[data-theme=\"modern\"] .eb-player .eb-pip:active,\n[data-theme=\"modern\"] .eb-player .eb-cast:active,\n[data-theme=\"modern\"] .eb-player .eb-volume-mute:active,\n[data-theme=\"modern\"] .eb-player .eb-live-sync:active {\n transform: scale(.88);\n}\n\n/* ============================================================\n Seekbar\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-seekbar-track {\n height: 3px;\n background: rgba(255,255,255,.22);\n border-radius: 3px;\n transition: height .15s;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-seekbar:hover .eb-seekbar-track {\n height: 5px;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-seekbar-buffered {\n background: rgba(255,255,255,.28);\n border-radius: 3px;\n transition: height .15s;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-seekbar-progress {\n background: linear-gradient(90deg, #a78bfa, #7c3aed);\n border-radius: 3px;\n transition: height .15s;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-seekbar-thumb {\n width: 14px;\n height: 14px;\n right: -7px;\n background: #fff;\n box-shadow: 0 0 0 3px rgba(124,58,237,.45);\n}\n\n/* ============================================================\n Seekbar tooltip & preview\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-seekbar-tooltip {\n bottom: 24px;\n background: rgba(0,0,0,.82);\n border-radius: 5px;\n font-size: 11px;\n padding: 3px 7px;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-seekbar-preview {\n width: 128px;\n height: 72px;\n background: #111;\n border: 2px solid rgba(255,255,255,.16);\n border-radius: 6px;\n}\n\n/* ============================================================\n Chapter markers\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-chapter-marker {\n width: 2px;\n height: 7px;\n background: rgba(0,0,0,.45);\n border-radius: 1px;\n top: 50%;\n transform: translate(-50%, -50%);\n}\n\n/* ============================================================\n Volume: expandable slider, white fill\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-volume-control {\n flex-direction: row;\n gap: 0;\n margin-right: 0;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-volume-track {\n width: 0;\n height: 3px;\n background: rgba(255,255,255,.22);\n border-radius: 3px;\n overflow: hidden;\n transition: width .25s ease, margin .25s ease;\n margin: 0;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-volume-control:hover .eb-volume-track,\n[data-theme=\"modern\"] .eb-player .eb-volume-control:focus-within .eb-volume-track {\n width: 66px;\n margin: 0 6px 0 2px;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-volume-fill {\n background: #fff;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-volume-thumb {\n width: 11px;\n height: 11px;\n background: #fff;\n box-shadow: 0 0 0 2px rgba(255,255,255,.35);\n transform: translate(-50%, -50%) scale(0);\n transition: transform .15s;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-volume-control:hover .eb-volume-thumb {\n transform: translate(-50%, -50%) scale(1);\n}\n\n/* ============================================================\n Time display\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-time-display {\n font-size: 12px;\n color: rgba(255,255,255,.6);\n letter-spacing: .02em;\n}\n\n/* ============================================================\n Live badge\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-live-sync {\n width: auto;\n height: auto;\n padding: 4px 10px;\n border-radius: 6px;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: .08em;\n text-transform: uppercase;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-live-synced {\n background: rgba(220,38,38,.9);\n color: #fff;\n box-shadow: 0 2px 8px rgba(220,38,38,.4);\n}\n\n/* ============================================================\n Settings panel: glassmorphism + refined layout\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-settings-panel {\n background: rgba(10,10,20,.55);\n backdrop-filter: blur(18px) saturate(160%);\n -webkit-backdrop-filter: blur(18px) saturate(160%);\n border-radius: 12px;\n min-width: 280px;\n box-shadow: 0 16px 48px rgba(0,0,0,.6), 0 0 0 1px rgba(255,255,255,.1);\n}\n\n[data-theme=\"modern\"] .eb-player .eb-settings-menu {\n padding: 0;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-settings-category,\n[data-theme=\"modern\"] .eb-player .eb-settings-item,\n[data-theme=\"modern\"] .eb-player .eb-settings-back {\n padding: 15px 20px;\n font-size: 13px;\n color: rgba(255,255,255,.9);\n border-bottom: 1px solid rgba(255,255,255,.06);\n transition: background .12s;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-settings-category:last-child,\n[data-theme=\"modern\"] .eb-player .eb-settings-item:last-child {\n border-bottom: none;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-settings-category:hover,\n[data-theme=\"modern\"] .eb-player .eb-settings-item:hover,\n[data-theme=\"modern\"] .eb-player .eb-settings-back:hover {\n background: rgba(255,255,255,.05);\n}\n\n[data-theme=\"modern\"] .eb-player .eb-settings-item--selected {\n color: #fff;\n font-weight: 500;\n}\n\n/* Active selection dot */\n[data-theme=\"modern\"] .eb-player .eb-settings-item--selected::after {\n content: '';\n width: 10px;\n height: 10px;\n border-radius: 50%;\n background: #1a73e8;\n box-shadow: 0 0 0 3px rgba(26,115,232,.25);\n flex-shrink: 0;\n}\n\n/* Non-selected items: hollow dot */\n[data-theme=\"modern\"] .eb-player .eb-settings-item:not(.eb-settings-item--selected)::after {\n content: '';\n width: 10px;\n height: 10px;\n border-radius: 50%;\n border: 2px solid rgba(255,255,255,.2);\n flex-shrink: 0;\n}\n\n/* Sub-panel back button */\n[data-theme=\"modern\"] .eb-player .eb-settings-back {\n gap: 14px;\n font-weight: 600;\n}\n\n/* ============================================================\n Loading spinner\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-loading .eb-icon {\n color: rgba(255,255,255,.7);\n}\n\n/* ============================================================\n Error overlay\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-error {\n background: rgba(10,10,20,.85);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n}\n\n[data-theme=\"modern\"] .eb-player .eb-error-retry {\n border-radius: 8px;\n background: rgba(255,255,255,.1);\n border-color: rgba(255,255,255,.15);\n transition: background .2s, color .2s;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-error-retry:hover {\n background: rgba(255,255,255,.18);\n}\n\n/* ============================================================\n Toast\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-toast {\n background: rgba(0,0,0,.72);\n backdrop-filter: blur(10px);\n -webkit-backdrop-filter: blur(10px);\n border-radius: 10px;\n font-size: 13px;\n}\n\n/* ============================================================\n Info & socials overlays\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-info-overlay,\n[data-theme=\"modern\"] .eb-player .eb-socials-overlay {\n background: rgba(10,10,20,.85);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n}\n\n[data-theme=\"modern\"] .eb-player .eb-info-close,\n[data-theme=\"modern\"] .eb-player .eb-socials-close {\n border-radius: 8px;\n background: rgba(255,255,255,.07);\n border-color: rgba(255,255,255,.08);\n transition: background .2s, color .2s;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-info-close:hover,\n[data-theme=\"modern\"] .eb-player .eb-socials-close:hover {\n background: rgba(255,255,255,.13);\n color: #fff;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-socials-link {\n background: rgba(255,255,255,.07);\n border: 1px solid rgba(255,255,255,.08);\n border-radius: 8px;\n transition: background .2s, color .2s;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-socials-link:hover {\n background: rgba(167,139,250,.15);\n border-color: rgba(167,139,250,.35);\n color: #c4b5fd;\n}\n\n/* ============================================================\n Bottom bar: two-row layout (seekbar above, buttons below)\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-bottom-bar__controls-row {\n flex-wrap: wrap;\n}\n\n/* Seekbar fills full width as its own row */\n[data-theme=\"modern\"] .eb-player .eb-bottom-bar__seekbar-zone {\n flex: none;\n width: 100%;\n order: -1;\n margin-bottom: -6px;\n}\n\n/* Time display sits before seekbar visually (after live badge) */\n[data-theme=\"modern\"] .eb-player .eb-slot-time {\n order: -1;\n margin-left: auto;\n}\n\n/* Live sync badge before time */\n[data-theme=\"modern\"] .eb-player .eb-slot-live-sync {\n order: -2;\n}\n\n/* Move settings + pip to right end of control row */\n[data-theme=\"modern\"] .eb-player .eb-slot-settings {\n margin-left: auto;\n}\n\n/* ============================================================\n Poster\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-poster {\n background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);\n}\n";
35
38
  styleInject(css_248z$4);
36
39
 
37
- var css_248z$3 = "/**\n * Modern theme overrides\n *\n * Applied when the container has [data-theme=\"modern\"].\n * Glassmorphism-inspired design with purple accent, Inter font,\n * rounded corners, backdrop blur, and smooth transitions.\n *\n * Reference: player-reference.html\n */\n\n/* ============================================================\n Google Fonts (Inter 300-600)\n ============================================================ */\n@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap');\n\n/* ============================================================\n Icons: override stroke-based defaults for filled Material icons\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-icon {\n fill: currentColor;\n stroke: none;\n stroke-width: 0;\n}\n\n/* ============================================================\n Root vars & container\n ============================================================ */\n[data-theme=\"modern\"] .eb-player {\n --eb-color-primary: #7c3aed;\n --eb-color-progress: #7c3aed;\n --eb-color-background: rgba(10, 10, 20, 0.85);\n --eb-accent: #1a73e8;\n --eb-font-family: 'Inter', sans-serif;\n font-family: 'Inter', sans-serif;\n border-radius: 14px;\n box-shadow: 0 40px 80px rgba(0,0,0,.8), 0 0 0 1px rgba(255,255,255,.06);\n}\n\n/* ============================================================\n Gradients\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-top-bar {\n background: linear-gradient(to bottom, rgba(0,0,0,.72), transparent);\n height: 110px;\n padding: 14px 16px;\n gap: 6px;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-bottom-bar__gradient {\n height: 150px;\n background: linear-gradient(to top, rgba(0,0,0,.9), transparent);\n}\n\n/* ============================================================\n Top bar: slide-in animation\n ============================================================ */\n[data-theme=\"modern\"] .eb-player.eb-controls-visible .eb-top-bar {\n opacity: 1;\n transform: translateY(0);\n transition: opacity .3s, transform .3s;\n}\n\n[data-theme=\"modern\"] .eb-player.eb-controls-hidden .eb-top-bar {\n opacity: 0;\n transform: translateY(-6px);\n transition: opacity .3s, transform .3s;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-top-bar__actions {\n gap: 6px;\n}\n\n/* ============================================================\n Bottom bar: slide-up animation + spacing\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-bottom-bar__controls-row {\n padding: 0 14px 12px;\n gap: 8px;\n}\n\n[data-theme=\"modern\"] .eb-player.eb-controls-visible .eb-bottom-bar {\n opacity: 1;\n transform: translateY(0);\n transition: opacity .3s, transform .3s;\n}\n\n[data-theme=\"modern\"] .eb-player.eb-controls-hidden .eb-bottom-bar {\n opacity: 0;\n transform: translateY(8px);\n transition: opacity .3s, transform .3s;\n}\n\n/* ============================================================\n Middle bar: glassmorphism circles + wider gap\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-middle-bar {\n gap: 28px;\n}\n\n/* Controls slide with bottom bar */\n[data-theme=\"modern\"] .eb-player.eb-controls-visible .eb-middle-bar {\n opacity: 1;\n transition: opacity .3s;\n}\n\n[data-theme=\"modern\"] .eb-player.eb-controls-hidden .eb-middle-bar {\n opacity: 0;\n transition: opacity .3s;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-middle-bar__play-btn {\n width: 72px;\n height: 72px;\n background: rgba(255,255,255,.18);\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n border: 2px solid rgba(255,255,255,.28);\n transition: background .15s;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-middle-bar__play-btn:hover {\n background: rgba(255,255,255,.28);\n}\n\n[data-theme=\"modern\"] .eb-player .eb-middle-bar__play-btn .eb-icon {\n width: 30px;\n height: 30px;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-middle-bar__seek-btn {\n width: 52px;\n height: 52px;\n border-radius: 50%;\n background: rgba(255,255,255,.14);\n backdrop-filter: blur(6px);\n -webkit-backdrop-filter: blur(6px);\n border: 1.5px solid rgba(255,255,255,.22);\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 1px;\n transition: background .15s;\n padding: 0;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-middle-bar__seek-btn:hover {\n background: rgba(255,255,255,.24);\n}\n\n[data-theme=\"modern\"] .eb-player .eb-seek-circle {\n width: 18px;\n height: 18px;\n /* Reference uses filled skip arrows instead of the stroke circle */\n fill: white;\n stroke: none;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-seek-label {\n font-size: .58em;\n font-weight: 700;\n color: rgba(255,255,255,.9);\n line-height: 1;\n}\n\n/* ============================================================\n Buttons: rounded, softer color, scale on active\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-button,\n[data-theme=\"modern\"] .eb-player .eb-play-pause,\n[data-theme=\"modern\"] .eb-player .eb-fullscreen,\n[data-theme=\"modern\"] .eb-player .eb-pip,\n[data-theme=\"modern\"] .eb-player .eb-cast,\n[data-theme=\"modern\"] .eb-player .eb-volume-mute,\n[data-theme=\"modern\"] .eb-player .eb-live-sync {\n color: rgba(255,255,255,.82);\n border-radius: 8px;\n width: 38px;\n height: 38px;\n transition: background .15s, color .15s, transform .1s;\n flex-shrink: 0;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-button:hover,\n[data-theme=\"modern\"] .eb-player .eb-play-pause:hover,\n[data-theme=\"modern\"] .eb-player .eb-fullscreen:hover,\n[data-theme=\"modern\"] .eb-player .eb-pip:hover,\n[data-theme=\"modern\"] .eb-player .eb-cast:hover,\n[data-theme=\"modern\"] .eb-player .eb-volume-mute:hover,\n[data-theme=\"modern\"] .eb-player .eb-live-sync:hover {\n background: rgba(255,255,255,.12);\n color: #fff;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-button:active,\n[data-theme=\"modern\"] .eb-player .eb-play-pause:active,\n[data-theme=\"modern\"] .eb-player .eb-fullscreen:active,\n[data-theme=\"modern\"] .eb-player .eb-pip:active,\n[data-theme=\"modern\"] .eb-player .eb-cast:active,\n[data-theme=\"modern\"] .eb-player .eb-volume-mute:active,\n[data-theme=\"modern\"] .eb-player .eb-live-sync:active {\n transform: scale(.88);\n}\n\n/* ============================================================\n Seekbar\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-seekbar-track {\n height: 3px;\n background: rgba(255,255,255,.22);\n border-radius: 3px;\n transition: height .15s;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-seekbar:hover .eb-seekbar-track {\n height: 5px;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-seekbar-buffered {\n background: rgba(255,255,255,.28);\n border-radius: 3px;\n transition: height .15s;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-seekbar-progress {\n background: linear-gradient(90deg, #a78bfa, #7c3aed);\n border-radius: 3px;\n transition: height .15s;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-seekbar-thumb {\n width: 14px;\n height: 14px;\n right: -7px;\n background: #fff;\n box-shadow: 0 0 0 3px rgba(124,58,237,.45);\n}\n\n/* ============================================================\n Seekbar tooltip & preview\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-seekbar-tooltip {\n bottom: 24px;\n background: rgba(0,0,0,.82);\n border-radius: 5px;\n font-size: 11px;\n padding: 3px 7px;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-seekbar-preview {\n width: 128px;\n height: 72px;\n background: #111;\n border: 2px solid rgba(255,255,255,.16);\n border-radius: 6px;\n}\n\n/* ============================================================\n Chapter markers\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-chapter-marker {\n width: 2px;\n height: 7px;\n background: rgba(0,0,0,.45);\n border-radius: 1px;\n top: 50%;\n transform: translate(-50%, -50%);\n}\n\n/* ============================================================\n Volume: expandable slider, white fill\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-volume-control {\n flex-direction: row;\n gap: 0;\n margin-right: 0;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-volume-track {\n width: 0;\n height: 3px;\n background: rgba(255,255,255,.22);\n border-radius: 3px;\n overflow: hidden;\n transition: width .25s ease, margin .25s ease;\n margin: 0;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-volume-control:hover .eb-volume-track,\n[data-theme=\"modern\"] .eb-player .eb-volume-control:focus-within .eb-volume-track {\n width: 66px;\n margin: 0 6px 0 2px;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-volume-fill {\n background: #fff;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-volume-thumb {\n width: 11px;\n height: 11px;\n background: #fff;\n box-shadow: 0 0 0 2px rgba(255,255,255,.35);\n transform: translate(-50%, -50%) scale(0);\n transition: transform .15s;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-volume-control:hover .eb-volume-thumb {\n transform: translate(-50%, -50%) scale(1);\n}\n\n/* ============================================================\n Time display\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-time-display {\n font-size: 12px;\n color: rgba(255,255,255,.6);\n letter-spacing: .02em;\n}\n\n/* ============================================================\n Live badge\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-live-sync {\n width: auto;\n height: auto;\n padding: 4px 10px;\n border-radius: 6px;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: .08em;\n text-transform: uppercase;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-live-synced {\n background: rgba(220,38,38,.9);\n color: #fff;\n box-shadow: 0 2px 8px rgba(220,38,38,.4);\n}\n\n/* ============================================================\n Settings panel: glassmorphism + refined layout\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-settings-panel {\n background: rgba(10,10,20,.55);\n backdrop-filter: blur(18px) saturate(160%);\n -webkit-backdrop-filter: blur(18px) saturate(160%);\n border-radius: 12px;\n min-width: 280px;\n box-shadow: 0 16px 48px rgba(0,0,0,.6), 0 0 0 1px rgba(255,255,255,.1);\n}\n\n[data-theme=\"modern\"] .eb-player .eb-settings-menu {\n padding: 0;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-settings-category,\n[data-theme=\"modern\"] .eb-player .eb-settings-item,\n[data-theme=\"modern\"] .eb-player .eb-settings-back {\n padding: 15px 20px;\n font-size: 13px;\n color: rgba(255,255,255,.9);\n border-bottom: 1px solid rgba(255,255,255,.06);\n transition: background .12s;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-settings-category:last-child,\n[data-theme=\"modern\"] .eb-player .eb-settings-item:last-child {\n border-bottom: none;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-settings-category:hover,\n[data-theme=\"modern\"] .eb-player .eb-settings-item:hover,\n[data-theme=\"modern\"] .eb-player .eb-settings-back:hover {\n background: rgba(255,255,255,.05);\n}\n\n[data-theme=\"modern\"] .eb-player .eb-settings-item--selected {\n color: #fff;\n font-weight: 500;\n}\n\n/* Active selection dot */\n[data-theme=\"modern\"] .eb-player .eb-settings-item--selected::after {\n content: '';\n width: 10px;\n height: 10px;\n border-radius: 50%;\n background: #1a73e8;\n box-shadow: 0 0 0 3px rgba(26,115,232,.25);\n flex-shrink: 0;\n}\n\n/* Non-selected items: hollow dot */\n[data-theme=\"modern\"] .eb-player .eb-settings-item:not(.eb-settings-item--selected)::after {\n content: '';\n width: 10px;\n height: 10px;\n border-radius: 50%;\n border: 2px solid rgba(255,255,255,.2);\n flex-shrink: 0;\n}\n\n/* Sub-panel back button */\n[data-theme=\"modern\"] .eb-player .eb-settings-back {\n gap: 14px;\n font-weight: 600;\n}\n\n/* ============================================================\n Loading spinner\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-loading .eb-icon {\n color: rgba(255,255,255,.7);\n}\n\n/* ============================================================\n Error overlay\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-error {\n background: rgba(10,10,20,.85);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n}\n\n[data-theme=\"modern\"] .eb-player .eb-error-retry {\n border-radius: 8px;\n background: rgba(255,255,255,.1);\n border-color: rgba(255,255,255,.15);\n transition: background .2s, color .2s;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-error-retry:hover {\n background: rgba(255,255,255,.18);\n}\n\n/* ============================================================\n Toast\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-toast {\n background: rgba(0,0,0,.72);\n backdrop-filter: blur(10px);\n -webkit-backdrop-filter: blur(10px);\n border-radius: 10px;\n font-size: 13px;\n}\n\n/* ============================================================\n Info & socials overlays\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-info-overlay,\n[data-theme=\"modern\"] .eb-player .eb-socials-overlay {\n background: rgba(10,10,20,.85);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n}\n\n[data-theme=\"modern\"] .eb-player .eb-info-close,\n[data-theme=\"modern\"] .eb-player .eb-socials-close {\n border-radius: 8px;\n background: rgba(255,255,255,.07);\n border-color: rgba(255,255,255,.08);\n transition: background .2s, color .2s;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-info-close:hover,\n[data-theme=\"modern\"] .eb-player .eb-socials-close:hover {\n background: rgba(255,255,255,.13);\n color: #fff;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-socials-link {\n background: rgba(255,255,255,.07);\n border: 1px solid rgba(255,255,255,.08);\n border-radius: 8px;\n transition: background .2s, color .2s;\n}\n\n[data-theme=\"modern\"] .eb-player .eb-socials-link:hover {\n background: rgba(167,139,250,.15);\n border-color: rgba(167,139,250,.35);\n color: #c4b5fd;\n}\n\n/* ============================================================\n Bottom bar: two-row layout (seekbar above, buttons below)\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-bottom-bar__controls-row {\n flex-wrap: wrap;\n}\n\n/* Seekbar fills full width as its own row */\n[data-theme=\"modern\"] .eb-player .eb-bottom-bar__seekbar-zone {\n flex: none;\n width: 100%;\n order: -1;\n margin-bottom: -6px;\n}\n\n/* Time display sits before seekbar visually (after live badge) */\n[data-theme=\"modern\"] .eb-player .eb-slot-time {\n order: -1;\n margin-left: auto;\n}\n\n/* Live sync badge before time */\n[data-theme=\"modern\"] .eb-player .eb-slot-live-sync {\n order: -2;\n}\n\n/* Move settings + pip to right end of control row */\n[data-theme=\"modern\"] .eb-player .eb-slot-settings {\n margin-left: auto;\n}\n\n/* ============================================================\n Poster\n ============================================================ */\n[data-theme=\"modern\"] .eb-player .eb-poster {\n background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);\n}\n";
40
+ var css_248z$3 = "/**\n * Radio theme overrides\n *\n * Applied when the container has [data-theme=\"radio\"].\n * Uses higher specificity than base.css .eb-player selector.\n */\n\n[data-theme=\"radio\"] .eb-player {\n --eb-color-primary: #F4A261;\n --eb-color-background: rgba(20, 20, 30, 0.95);\n}\n";
38
41
  styleInject(css_248z$3);
39
42
 
40
- var css_248z$2 = "/**\n * Radio theme overrides\n *\n * Applied when the container has [data-theme=\"radio\"].\n * Uses higher specificity than base.css .eb-player selector.\n */\n\n[data-theme=\"radio\"] .eb-player {\n --eb-color-primary: #F4A261;\n --eb-color-background: rgba(20, 20, 30, 0.95);\n}\n";
43
+ var css_248z$2 = "/**\n * SNRT theme overrides\n *\n * Applied when the container has [data-theme=\"snrt\"].\n * Uses higher specificity than base.css .eb-player selector.\n */\n\n[data-theme=\"snrt\"] .eb-player {\n --eb-color-primary: #006633;\n --eb-color-background: rgba(0, 0, 0, 0.9);\n}\n";
41
44
  styleInject(css_248z$2);
42
45
 
43
- var css_248z$1 = "/**\n * SNRT theme overrides\n *\n * Applied when the container has [data-theme=\"snrt\"].\n * Uses higher specificity than base.css .eb-player selector.\n */\n\n[data-theme=\"snrt\"] .eb-player {\n --eb-color-primary: #006633;\n --eb-color-background: rgba(0, 0, 0, 0.9);\n}\n";
46
+ var css_248z$1 = "/**\n * V2 theme — based on snrtlive.ma (Aloula) player styling\n *\n * Applied when the container has [data-theme=\"v2\"].\n * Dark UI with orange accent (#ff841f), Inter font, rounded container,\n * backdrop-blur panels, expandable volume, two-row bottom bar\n * (seekbar on top, buttons below), gradient overlays, centered\n * frosted-glass transport circles, and refined slide/fade animations.\n */\n\n/* ============================================================\n Google Fonts (Inter 300-700)\n ============================================================ */\n@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');\n\n/* ============================================================\n Root vars & container\n ============================================================ */\n[data-theme=\"v2\"] .eb-player {\n --eb-color-primary: #ff841f;\n --eb-color-progress: #ff841f;\n --eb-color-background: rgba(10, 10, 20, 0.85);\n --eb-accent: #ff841f;\n --eb-color-text: #fff;\n --eb-font-family: 'Inter', -apple-system, sans-serif;\n --eb-font-size-base: 14px;\n --eb-radius-control: 8px;\n --eb-duration-transition: 200ms;\n font-family: 'Inter', -apple-system, sans-serif;\n border-radius: 14px;\n box-shadow: 0 40px 80px rgba(0,0,0,.8), 0 0 0 1px rgba(255,255,255,.06);\n color: #fff;\n}\n\n/* ============================================================\n Icons: bolder stroke weight to match filled-style target\n ============================================================ */\n[data-theme=\"v2\"] .eb-player .eb-icon {\n stroke-width: 2.5;\n}\n\n/* Fullscreen: fill viewport, drop card styling */\n[data-theme=\"v2\"] .eb-player:fullscreen,\n[data-theme=\"v2\"] .eb-player:-webkit-full-screen {\n border-radius: 0;\n box-shadow: none;\n}\n\n/* ============================================================\n Gradients (top & bottom)\n ============================================================ */\n[data-theme=\"v2\"] .eb-player .eb-top-bar {\n background: linear-gradient(to bottom, rgba(0,0,0,.72), transparent);\n height: 110px;\n padding: 14px 16px;\n gap: 6px;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-bottom-bar__gradient {\n height: 150px;\n background: linear-gradient(to top, rgba(0,0,0,.9), transparent);\n}\n\n/* ============================================================\n Top bar: slide-in from above\n ============================================================ */\n[data-theme=\"v2\"] .eb-player.eb-controls-visible .eb-top-bar {\n opacity: 1;\n transform: translateY(0);\n transition: opacity .3s, transform .3s;\n}\n\n[data-theme=\"v2\"] .eb-player.eb-controls-hidden .eb-top-bar {\n opacity: 0;\n transform: translateY(-6px);\n transition: opacity .3s, transform .3s;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-top-bar__actions {\n gap: 6px;\n}\n\n/* Logo */\n[data-theme=\"v2\"] .eb-player .eb-top-bar__logo {\n max-height: 32px;\n max-width: 120px;\n opacity: .85;\n}\n\n/* ============================================================\n Bottom bar: slide-up animation + spacing\n ============================================================ */\n[data-theme=\"v2\"] .eb-player .eb-bottom-bar__controls-row {\n padding: 0 14px 12px;\n gap: 8px;\n}\n\n[data-theme=\"v2\"] .eb-player.eb-controls-visible .eb-bottom-bar {\n opacity: 1;\n transform: translateY(0);\n transition: opacity .3s, transform .3s;\n}\n\n[data-theme=\"v2\"] .eb-player.eb-controls-hidden .eb-bottom-bar {\n opacity: 0;\n transform: translateY(8px);\n transition: opacity .3s, transform .3s;\n}\n\n/* ============================================================\n Bottom bar: single-row layout\n play | live | time | ——seekbar—— | settings | volume | fullscreen\n PiP / Cast are in the top bar (via THEME_LAYOUTS.v2 in config.ts)\n ============================================================ */\n\n/* ============================================================\n Middle bar: glassmorphism transport circles\n ============================================================ */\n[data-theme=\"v2\"] .eb-player .eb-middle-bar {\n gap: 28px;\n}\n\n[data-theme=\"v2\"] .eb-player.eb-controls-visible .eb-middle-bar {\n opacity: 1;\n transition: opacity .3s;\n}\n\n[data-theme=\"v2\"] .eb-player.eb-controls-hidden .eb-middle-bar {\n opacity: 0;\n transition: opacity .3s;\n}\n\n/* Central play/pause — large frosted circle */\n[data-theme=\"v2\"] .eb-player .eb-middle-bar__play-btn {\n width: 80px;\n height: 80px;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(255,255,255,.15);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border: 2px solid rgba(255,255,255,.25);\n color: #fff;\n padding: 0;\n transition: background .15s, transform .25s, opacity .25s;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-middle-bar__play-btn:hover {\n background: rgba(255,255,255,.25);\n}\n\n[data-theme=\"v2\"] .eb-player .eb-middle-bar__play-btn:active {\n transform: scale(.92);\n}\n\n[data-theme=\"v2\"] .eb-player .eb-middle-bar__play-btn .eb-icon {\n width: 32px;\n height: 32px;\n stroke-width: 2.5;\n}\n\n/* Seek buttons — smaller frosted circles, same style as play button */\n[data-theme=\"v2\"] .eb-player .eb-middle-bar__seek-btn {\n width: 56px;\n height: 56px;\n border-radius: 50%;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n background: rgba(255,255,255,.15);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border: 2px solid rgba(255,255,255,.25);\n color: #fff;\n gap: 1px;\n padding: 0;\n transition: background .15s;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-middle-bar__seek-btn:hover {\n background: rgba(255,255,255,.25);\n}\n\n[data-theme=\"v2\"] .eb-player .eb-seek-circle {\n width: 24px;\n height: 24px;\n color: rgba(255,255,255,.85);\n fill: currentColor;\n stroke: none;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-seek-label {\n font-size: .75em;\n font-weight: 700;\n color: rgba(255,255,255,.9);\n line-height: 1;\n}\n\n/* ============================================================\n Buttons: rounded, soft color, scale on press\n ============================================================ */\n[data-theme=\"v2\"] .eb-player .eb-button:not(.eb-middle-bar__seek-btn):not(.eb-middle-bar__play-btn),\n[data-theme=\"v2\"] .eb-player .eb-play-pause,\n[data-theme=\"v2\"] .eb-player .eb-fullscreen,\n[data-theme=\"v2\"] .eb-player .eb-pip,\n[data-theme=\"v2\"] .eb-player .eb-cast,\n[data-theme=\"v2\"] .eb-player .eb-volume-mute,\n[data-theme=\"v2\"] .eb-player .eb-live-sync {\n color: rgba(255,255,255,.82);\n border-radius: 8px;\n width: 38px;\n height: 38px;\n transition: background .15s, color .15s, transform .1s;\n flex-shrink: 0;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-button:not(.eb-middle-bar__seek-btn):not(.eb-middle-bar__play-btn):hover,\n[data-theme=\"v2\"] .eb-player .eb-play-pause:hover,\n[data-theme=\"v2\"] .eb-player .eb-fullscreen:hover,\n[data-theme=\"v2\"] .eb-player .eb-pip:hover,\n[data-theme=\"v2\"] .eb-player .eb-cast:hover,\n[data-theme=\"v2\"] .eb-player .eb-volume-mute:hover,\n[data-theme=\"v2\"] .eb-player .eb-live-sync:hover {\n background: rgba(255,255,255,.12);\n color: #fff;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-button:not(.eb-middle-bar__seek-btn):not(.eb-middle-bar__play-btn):active,\n[data-theme=\"v2\"] .eb-player .eb-play-pause:active,\n[data-theme=\"v2\"] .eb-player .eb-fullscreen:active,\n[data-theme=\"v2\"] .eb-player .eb-pip:active,\n[data-theme=\"v2\"] .eb-player .eb-cast:active,\n[data-theme=\"v2\"] .eb-player .eb-volume-mute:active,\n[data-theme=\"v2\"] .eb-player .eb-live-sync:active {\n transform: scale(.88);\n}\n\n/* Icon size inside buttons (not middle bar transport) */\n[data-theme=\"v2\"] .eb-player .eb-button:not(.eb-middle-bar__seek-btn):not(.eb-middle-bar__play-btn) .eb-icon,\n[data-theme=\"v2\"] .eb-player .eb-play-pause .eb-icon,\n[data-theme=\"v2\"] .eb-player .eb-fullscreen .eb-icon,\n[data-theme=\"v2\"] .eb-player .eb-pip .eb-icon,\n[data-theme=\"v2\"] .eb-player .eb-cast .eb-icon,\n[data-theme=\"v2\"] .eb-player .eb-volume-mute .eb-icon {\n width: 22px;\n height: 22px;\n}\n\n/* Top bar icons slightly larger */\n[data-theme=\"v2\"] .eb-player .eb-top-bar .eb-icon {\n width: 24px;\n height: 24px;\n}\n\n/* Play/pause icons should be filled (solid), not stroke outlines */\n[data-theme=\"v2\"] .eb-player .eb-play-pause .eb-icon,\n[data-theme=\"v2\"] .eb-player .eb-middle-bar__play-btn .eb-icon {\n fill: currentColor;\n stroke: none;\n}\n\n/* Volume icon: fill the speaker body, keep stroke for sound waves */\n[data-theme=\"v2\"] .eb-player .eb-volume-mute .eb-icon path:first-child {\n fill: currentColor;\n}\n\n/* Cast button active state — cyan */\n[data-theme=\"v2\"] .eb-player .eb-cast-active {\n color: #1eb6d4 !important;\n background: rgba(30, 182, 212, .15) !important;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-cast-active .eb-icon {\n filter: drop-shadow(0 0 4px rgba(30, 182, 212, .6));\n}\n\n/* ============================================================\n Seekbar\n ============================================================ */\n[data-theme=\"v2\"] .eb-player .eb-seekbar-track {\n height: 3px;\n background: rgba(255,255,255,.22);\n border-radius: 3px;\n transition: height .15s;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-seekbar:hover .eb-seekbar-track {\n height: 5px;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-seekbar-buffered {\n background: rgba(255,255,255,.28);\n border-radius: 3px;\n transition: height .15s;\n}\n\n/* Orange gradient fill */\n[data-theme=\"v2\"] .eb-player .eb-seekbar-progress {\n background: linear-gradient(90deg, #ff841f, color-mix(in srgb, #ff841f 70%, #fff));\n border-radius: 3px;\n transition: height .15s;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-seekbar-thumb {\n width: 14px;\n height: 14px;\n right: -7px;\n background: #fff;\n box-shadow: 0 0 0 3px rgba(255,132,31,.45);\n}\n\n/* ============================================================\n Seekbar tooltip & preview\n ============================================================ */\n[data-theme=\"v2\"] .eb-player .eb-seekbar-tooltip {\n bottom: 28px;\n background: rgba(0,0,0,.88);\n border: 1px solid rgba(255,255,255,.12);\n border-radius: 7px;\n font-size: 11px;\n font-weight: 500;\n padding: 0;\n overflow: hidden;\n box-shadow: 0 4px 16px rgba(0,0,0,.5);\n display: flex;\n flex-direction: column;\n align-items: center;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-seekbar-preview {\n width: 160px;\n height: 90px;\n background: #111;\n border: none;\n border-radius: 0;\n margin: 0;\n}\n\n/* ============================================================\n Chapter markers\n ============================================================ */\n[data-theme=\"v2\"] .eb-player .eb-chapter-marker {\n width: 2px;\n height: 7px;\n background: rgba(0,0,0,.45);\n border-radius: 1px;\n top: 50%;\n transform: translate(-50%, -50%);\n}\n\n/* ============================================================\n Volume: expandable slider, white fill\n ============================================================ */\n[data-theme=\"v2\"] .eb-player .eb-volume-control {\n flex-direction: row;\n gap: 0;\n margin-right: 0;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-volume-track {\n width: 0;\n height: 3px;\n background: rgba(255,255,255,.22);\n border-radius: 3px;\n overflow: hidden;\n transition: width .25s ease, margin .25s ease;\n margin: 0;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-volume-control:hover .eb-volume-track,\n[data-theme=\"v2\"] .eb-player .eb-volume-control:focus-within .eb-volume-track {\n width: 66px;\n margin: 0 6px 0 2px;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-volume-fill {\n background: #fff;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-volume-thumb {\n width: 11px;\n height: 11px;\n background: #fff;\n box-shadow: 0 0 0 2px rgba(255,255,255,.35);\n transform: translate(-50%, -50%) scale(0);\n transition: transform .15s;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-volume-control:hover .eb-volume-thumb {\n transform: translate(-50%, -50%) scale(1);\n}\n\n/* ============================================================\n Time display\n ============================================================ */\n[data-theme=\"v2\"] .eb-player .eb-time-display {\n font-size: 13px;\n color: rgba(255,255,255,.85);\n font-weight: 500;\n letter-spacing: .02em;\n}\n\n/* ============================================================\n Live badge — text badge with blinking dot (not icon)\n ============================================================ */\n[data-theme=\"v2\"] .eb-player .eb-live-sync {\n width: auto;\n height: auto;\n padding: 4px 10px;\n border-radius: 6px;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: .08em;\n text-transform: uppercase;\n gap: 5px;\n}\n\n/* Hide icon, show dot + label */\n[data-theme=\"v2\"] .eb-player .eb-live-sync__icon {\n display: none;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-live-sync__dot {\n display: block;\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: #fff;\n flex-shrink: 0;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-live-sync__label {\n display: block;\n}\n\n/* Synced state — red background with blinking dot */\n[data-theme=\"v2\"] .eb-player .eb-live-synced {\n background: rgba(220,38,38,.9);\n color: #fff;\n box-shadow: 0 2px 8px rgba(220,38,38,.4);\n}\n\n[data-theme=\"v2\"] .eb-player .eb-live-synced .eb-live-sync__dot {\n animation: eb-v2-blink 1.2s ease infinite;\n}\n\n/* Delayed / not-at-live-edge */\n[data-theme=\"v2\"] .eb-player .eb-live-sync:not(.eb-live-synced) {\n background: rgba(60,60,80,.75);\n color: rgba(255,255,255,.7);\n box-shadow: none;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-live-sync:not(.eb-live-synced) .eb-live-sync__dot {\n background: rgba(255,255,255,.5);\n animation: none;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-live-sync:not(.eb-live-synced):hover {\n background: rgba(220,38,38,.7);\n color: #fff;\n}\n\n@keyframes eb-v2-blink {\n 0%, 100% { opacity: 1; }\n 50% { opacity: .15; }\n}\n\n/* ============================================================\n Settings panel: glassmorphism dropdown\n ============================================================ */\n[data-theme=\"v2\"] .eb-player .eb-settings-panel {\n background: rgba(10,10,20,.55);\n backdrop-filter: blur(18px) saturate(160%);\n -webkit-backdrop-filter: blur(18px) saturate(160%);\n border-radius: 12px;\n min-width: 300px;\n box-shadow: 0 16px 48px rgba(0,0,0,.6), 0 0 0 1px rgba(255,255,255,.1);\n}\n\n[data-theme=\"v2\"] .eb-player .eb-settings-menu {\n padding: 0;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-settings-category,\n[data-theme=\"v2\"] .eb-player .eb-settings-item,\n[data-theme=\"v2\"] .eb-player .eb-settings-back {\n padding: 15px 20px;\n font-size: 13px;\n color: rgba(255,255,255,.9);\n border-bottom: 1px solid rgba(255,255,255,.06);\n transition: background .12s;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-settings-category:last-child,\n[data-theme=\"v2\"] .eb-player .eb-settings-item:last-child {\n border-bottom: none;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-settings-category:hover,\n[data-theme=\"v2\"] .eb-player .eb-settings-item:hover,\n[data-theme=\"v2\"] .eb-player .eb-settings-back:hover {\n background: rgba(255,255,255,.05);\n}\n\n/* Selected item text */\n[data-theme=\"v2\"] .eb-player .eb-settings-item--selected {\n color: #fff;\n font-weight: 500;\n}\n\n/* Active selection dot (filled orange) */\n[data-theme=\"v2\"] .eb-player .eb-settings-item--selected::after {\n content: '';\n width: 10px;\n height: 10px;\n border-radius: 50%;\n background: var(--eb-accent, #ff841f);\n box-shadow: 0 0 0 3px rgba(255,132,31,.25);\n flex-shrink: 0;\n}\n\n/* Non-selected items: hollow dot */\n[data-theme=\"v2\"] .eb-player .eb-settings-item:not(.eb-settings-item--selected)::after {\n content: '';\n width: 10px;\n height: 10px;\n border-radius: 50%;\n border: 2px solid rgba(255,255,255,.2);\n flex-shrink: 0;\n}\n\n/* Back button */\n[data-theme=\"v2\"] .eb-player .eb-settings-back {\n gap: 14px;\n font-weight: 600;\n}\n\n/* ============================================================\n Loading spinner\n ============================================================ */\n[data-theme=\"v2\"] .eb-player .eb-loading .eb-icon {\n width: 52px;\n height: 52px;\n color: rgba(255,255,255,.7);\n filter: drop-shadow(0 1px 6px rgba(0,0,0,.5));\n animation: eb-spin .75s linear infinite;\n}\n\n/* ============================================================\n Error overlay\n ============================================================ */\n[data-theme=\"v2\"] .eb-player .eb-error {\n background: rgba(10,10,20,.85);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n}\n\n[data-theme=\"v2\"] .eb-player .eb-error-message {\n font-weight: 400;\n letter-spacing: .01em;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-error-retry {\n border-radius: 8px;\n background: rgba(255,255,255,.1);\n border-color: rgba(255,255,255,.15);\n transition: background .2s, color .2s;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-error-retry:hover {\n background: rgba(255,255,255,.18);\n}\n\n/* ============================================================\n Toast (keyboard hints, status messages)\n ============================================================ */\n[data-theme=\"v2\"] .eb-player .eb-toast {\n background: rgba(0,0,0,.72);\n backdrop-filter: blur(10px);\n -webkit-backdrop-filter: blur(10px);\n border-radius: 10px;\n font-size: 13px;\n padding: 8px 16px;\n}\n\n/* ============================================================\n Info & socials overlays\n ============================================================ */\n[data-theme=\"v2\"] .eb-player .eb-info-overlay,\n[data-theme=\"v2\"] .eb-player .eb-socials-overlay {\n background: rgba(10,10,20,.85);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n}\n\n[data-theme=\"v2\"] .eb-player .eb-info-close,\n[data-theme=\"v2\"] .eb-player .eb-socials-close {\n border-radius: 8px;\n background: rgba(255,255,255,.07);\n border-color: rgba(255,255,255,.08);\n transition: background .2s, color .2s;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-info-close:hover,\n[data-theme=\"v2\"] .eb-player .eb-socials-close:hover {\n background: rgba(255,255,255,.13);\n color: #fff;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-socials-link {\n background: rgba(255,255,255,.07);\n border: 1px solid rgba(255,255,255,.08);\n border-radius: 8px;\n transition: background .2s, color .2s, border-color .2s;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-socials-link:hover {\n background: rgba(255,132,31,.15);\n border-color: rgba(255,132,31,.35);\n color: #ffb980;\n}\n\n/* ============================================================\n Poster\n ============================================================ */\n[data-theme=\"v2\"] .eb-player .eb-poster {\n background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);\n}\n\n/* ============================================================\n Radio overlay\n ============================================================ */\n[data-theme=\"v2\"] .eb-player .eb-radio-overlay {\n background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);\n}\n\n[data-theme=\"v2\"] .eb-player .eb-radio-bar {\n background: rgba(255,132,31,.8);\n}\n\n/* ============================================================\n Chapter skip button\n ============================================================ */\n[data-theme=\"v2\"] .eb-player .eb-chapter-skip {\n background: rgba(10,10,20,.55);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n border: 1px solid rgba(255,255,255,.15);\n border-radius: 8px;\n font-size: 12px;\n font-weight: 500;\n transition: background .15s;\n}\n\n[data-theme=\"v2\"] .eb-player .eb-chapter-skip:hover {\n background: rgba(255,255,255,.12);\n}\n";
44
47
  styleInject(css_248z$1);
45
48
 
46
- var css_248z = "/* eb-player skin CSS — BEM-prefixed structural styles */\n/* All class names use the eb- prefix to avoid collisions with consumer CSS */\n\n/* ============================================================\n Root container\n ============================================================ */\n.eb-player {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n font-family: sans-serif;\n background: #000;\n color: #fff;\n box-sizing: border-box;\n user-select: none;\n}\n\n/* Video element fills the container */\n.eb-player video.eb-video {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: block;\n object-fit: contain;\n z-index: 1;\n}\n\n/* ============================================================\n Overlay zone (poster, radio, loading, error, socials, info)\n ============================================================ */\n.eb-overlay-zone {\n position: absolute;\n inset: 0;\n pointer-events: none;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 10;\n}\n\n/* Poster image */\n.eb-poster {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n object-fit: contain;\n pointer-events: none;\n background: #000;\n padding: 15%;\n box-sizing: border-box;\n}\n\n/* Ads container (for IMA SDK) */\n.eb-ads-container {\n position: absolute;\n inset: 0;\n z-index: 20;\n pointer-events: none;\n}\n\n/* ============================================================\n Top bar\n ============================================================ */\n.eb-top-bar {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n padding: 8px 12px;\n background: linear-gradient(to bottom, rgba(0,0,0,0.6), transparent);\n z-index: 30;\n pointer-events: auto;\n}\n\n.eb-top-bar__logo {\n height: 28px;\n width: auto;\n display: block;\n}\n\n.eb-top-bar__logo-link {\n display: inline-flex;\n text-decoration: none;\n}\n\n.eb-top-bar__actions {\n display: flex;\n align-items: center;\n gap: 4px;\n margin-left: auto;\n}\n\n/* ============================================================\n Bottom bar\n ============================================================ */\n.eb-bottom-bar {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n display: flex;\n flex-direction: column;\n z-index: 30;\n pointer-events: auto;\n}\n\n.eb-bottom-bar__gradient {\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n height: 100px;\n background: linear-gradient(to top, rgba(0,0,0,0.7), transparent);\n pointer-events: none;\n z-index: -1;\n}\n\n.eb-bottom-bar__controls-row {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 4px;\n padding: 4px 8px 8px;\n}\n\n.eb-bottom-bar__seekbar-zone {\n flex: 1;\n min-width: 0;\n}\n\n/* ============================================================\n Middle bar\n ============================================================ */\n.eb-middle-bar {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n pointer-events: auto;\n z-index: 25;\n display: flex;\n align-items: center;\n gap: 16px;\n}\n\n.eb-middle-bar__play-btn {\n width: 64px;\n height: 64px;\n border-radius: 50%;\n background: rgba(0,0,0,0.5);\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.eb-middle-bar__seek-btn {\n flex-direction: column;\n gap: 2px;\n background: none;\n padding: 4px;\n}\n\n.eb-middle-bar__seek-btn:hover {\n background: rgba(255,255,255,0.1);\n border-radius: 8px;\n}\n\n.eb-seek-circle {\n width: 32px;\n height: 32px;\n display: block;\n pointer-events: none;\n}\n\n.eb-seek-label {\n font-size: 11px;\n font-weight: 600;\n line-height: 1;\n opacity: 0.85;\n}\n\n/* ============================================================\n Extension zones\n ============================================================ */\n.eb-extension-top-extra,\n.eb-extension-bottom-extra,\n.eb-extension-overlay,\n.eb-extension-controls-extra {\n position: absolute;\n pointer-events: none;\n z-index: 35;\n}\n\n.eb-extension-top-extra > *,\n.eb-extension-bottom-extra > *,\n.eb-extension-overlay > *,\n.eb-extension-controls-extra > * {\n pointer-events: auto;\n}\n\n.eb-extension-top-extra {\n top: 0;\n left: 0;\n right: 0;\n}\n\n.eb-extension-bottom-extra {\n bottom: 0;\n left: 0;\n right: 0;\n}\n\n.eb-extension-overlay {\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.eb-extension-controls-extra {\n bottom: 0;\n right: 0;\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 4px 8px 8px;\n}\n\n/* ============================================================\n Time display\n ============================================================ */\n.eb-time-display {\n font-size: 13px;\n line-height: 1;\n white-space: nowrap;\n padding: 0 4px;\n color: #fff;\n font-variant-numeric: tabular-nums;\n}\n\n/* ============================================================\n Volume control\n ============================================================ */\n.eb-volume-control {\n display: flex;\n align-items: center;\n gap: 4px;\n margin-right: 8px;\n}\n\n.eb-volume-track {\n position: relative;\n width: 60px;\n height: 4px;\n background: rgba(255, 255, 255, 0.2);\n border-radius: 2px;\n cursor: pointer;\n}\n\n.eb-volume-fill {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: var(--eb-color-volume, var(--eb-color-primary, #fff));\n border-radius: 2px;\n pointer-events: none;\n}\n\n.eb-volume-thumb {\n position: absolute;\n top: 50%;\n width: 10px;\n height: 10px;\n border-radius: 50%;\n background: var(--eb-color-volume, var(--eb-color-primary, #fff));\n transform: translate(-50%, -50%);\n pointer-events: none;\n}\n\n/* ============================================================\n Settings panel\n ============================================================ */\n.eb-settings-wrapper {\n position: relative;\n}\n\n.eb-settings-panel {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n background: rgba(0, 0, 0, 0.85);\n border-radius: 6px;\n min-width: 180px;\n overflow: hidden;\n font-size: 13px;\n z-index: 40;\n}\n\n.eb-settings-menu {\n list-style: none;\n margin: 0;\n padding: 4px 0;\n}\n\n.eb-settings-category,\n.eb-settings-item,\n.eb-settings-back {\n display: flex;\n align-items: center;\n justify-content: space-between;\n width: 100%;\n padding: 8px 14px;\n border: none;\n background: none;\n color: #fff;\n cursor: pointer;\n font-size: 13px;\n text-align: left;\n}\n\n.eb-settings-category:hover,\n.eb-settings-item:hover,\n.eb-settings-back:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.eb-settings-item--selected {\n color: var(--eb-accent, #e53935);\n}\n\n/* ============================================================\n Seekbar\n ============================================================ */\n.eb-seekbar {\n position: relative;\n width: 100%;\n padding: 8px 0;\n cursor: pointer;\n}\n\n.eb-seekbar-disabled {\n pointer-events: none;\n opacity: 0.4;\n}\n\n.eb-seekbar-track {\n position: relative;\n height: 4px;\n background: rgba(255, 255, 255, 0.2);\n border-radius: 2px;\n}\n\n.eb-seekbar-buffered {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: rgba(255, 255, 255, 0.4);\n border-radius: 2px;\n pointer-events: none;\n}\n\n.eb-seekbar-progress {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: var(--eb-color-progress, var(--eb-color-primary, #e53935));\n border-radius: 2px;\n pointer-events: none;\n}\n\n.eb-seekbar-thumb {\n position: absolute;\n right: -6px;\n top: 50%;\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background: var(--eb-color-progress, var(--eb-color-primary, #e53935));\n transform: translateY(-50%) scale(0);\n transition: transform 0.15s;\n pointer-events: none;\n}\n\n.eb-seekbar:hover .eb-seekbar-thumb {\n transform: translateY(-50%) scale(1);\n}\n\n.eb-seekbar:hover .eb-seekbar-track {\n height: 6px;\n}\n\n.eb-seekbar-tooltip {\n position: absolute;\n bottom: calc(100% + 8px);\n transform: translateX(-50%);\n background: rgba(0, 0, 0, 0.8);\n color: #fff;\n font-size: 12px;\n padding: 4px 8px;\n border-radius: 3px;\n white-space: nowrap;\n pointer-events: none;\n text-align: center;\n}\n\n.eb-seekbar-preview {\n width: 120px;\n height: auto;\n display: block;\n margin-bottom: 4px;\n}\n\n.eb-chapter-marker {\n position: absolute;\n top: 0;\n width: 3px;\n height: 100%;\n background: rgba(255, 255, 255, 0.6);\n transform: translateX(-50%);\n pointer-events: none;\n}\n\n.eb-chapter-marker.eb-chapter-active {\n background: #fff;\n}\n\n.eb-chapter-skip {\n position: absolute;\n right: 0;\n bottom: calc(100% + 4px);\n background: rgba(0, 0, 0, 0.7);\n color: #fff;\n border: 1px solid rgba(255, 255, 255, 0.4);\n border-radius: 4px;\n padding: 6px 16px;\n font-size: 13px;\n cursor: pointer;\n}\n\n.eb-chapter-skip:hover {\n background: rgba(255, 255, 255, 0.2);\n}\n\n.eb-epg-segment {\n position: absolute;\n top: 0;\n height: 100%;\n border-right: 1px solid rgba(255, 255, 255, 0.3);\n pointer-events: none;\n}\n\n.eb-epg-segment.eb-epg-current {\n background: rgba(255, 255, 255, 0.1);\n}\n\n/* ============================================================\n Icons\n ============================================================ */\n.eb-icon {\n width: 20px;\n height: 20px;\n fill: none;\n stroke: currentColor;\n stroke-width: 2;\n stroke-linecap: round;\n stroke-linejoin: round;\n display: block;\n pointer-events: none;\n flex-shrink: 0;\n}\n\n/* ============================================================\n Buttons (base style)\n ============================================================ */\n.eb-button,\n.eb-play-pause,\n.eb-fullscreen,\n.eb-pip,\n.eb-cast,\n.eb-volume-mute,\n.eb-live-sync {\n cursor: pointer;\n background: none;\n border: none;\n padding: 6px;\n color: var(--eb-color-icon, inherit);\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n transition: background 0.15s;\n -webkit-tap-highlight-color: transparent;\n}\n\n.eb-button:hover,\n.eb-play-pause:hover,\n.eb-fullscreen:hover,\n.eb-pip:hover,\n.eb-cast:hover,\n.eb-volume-mute:hover,\n.eb-live-sync:hover {\n background: rgba(255,255,255,0.15);\n}\n\n.eb-button:active,\n.eb-play-pause:active,\n.eb-fullscreen:active,\n.eb-pip:active,\n.eb-cast:active,\n.eb-volume-mute:active,\n.eb-live-sync:active {\n background: rgba(255,255,255,0.25);\n}\n\n/* ============================================================\n Radio overlay\n ============================================================ */\n.eb-radio-overlay {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background: #000;\n z-index: 15;\n}\n\n.eb-radio-bars {\n display: flex;\n align-items: flex-end;\n gap: 4px;\n height: 40px;\n}\n\n.eb-radio-bar {\n display: block;\n width: 6px;\n background: #fff;\n border-radius: 3px;\n animation: eb-radio-bar-bounce 0.8s ease-in-out infinite;\n}\n\n.eb-radio-bar:nth-child(1) { animation-delay: 0s; height: 20px; }\n.eb-radio-bar:nth-child(2) { animation-delay: 0.1s; height: 32px; }\n.eb-radio-bar:nth-child(3) { animation-delay: 0.2s; height: 40px; }\n.eb-radio-bar:nth-child(4) { animation-delay: 0.3s; height: 28px; }\n.eb-radio-bar:nth-child(5) { animation-delay: 0.4s; height: 16px; }\n\n@keyframes eb-radio-bar-bounce {\n 0%, 100% { transform: scaleY(0.4); opacity: 0.7; }\n 50% { transform: scaleY(1); opacity: 1; }\n}\n\n/* Volume mute button — base styles shared via .eb-button group above */\n\n/* ============================================================\n Live sync button\n ============================================================ */\n.eb-live-sync[hidden] {\n display: none;\n}\n\n.eb-live-synced {\n color: var(--eb-accent, #e53935);\n}\n\n/* ============================================================\n Overlay panels (error, info, socials — mounted in .eb-player)\n ============================================================ */\n.eb-error-slot,\n.eb-info-slot,\n.eb-socials-slot {\n position: absolute;\n inset: 0;\n z-index: 50;\n pointer-events: none;\n}\n\n.eb-toast-slot {\n position: absolute;\n bottom: 60px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 50;\n pointer-events: none;\n}\n\n.eb-toast {\n background: rgba(0, 0, 0, 0.8);\n color: #fff;\n padding: 8px 20px;\n border-radius: 4px;\n font-size: 13px;\n white-space: nowrap;\n pointer-events: auto;\n}\n\n.eb-error,\n.eb-info-overlay,\n.eb-socials-overlay {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n background: rgba(0, 0, 0, 0.75);\n color: #fff;\n z-index: 50;\n pointer-events: auto;\n padding: 24px;\n text-align: center;\n}\n\n.eb-error[hidden],\n.eb-info-overlay[hidden],\n.eb-socials-overlay[hidden] {\n display: none;\n}\n\n.eb-error-message {\n font-size: 16px;\n margin: 0 0 16px;\n}\n\n.eb-error-retry {\n background: rgba(255, 255, 255, 0.15);\n color: #fff;\n border: 1px solid rgba(255, 255, 255, 0.3);\n border-radius: 4px;\n padding: 8px 24px;\n font-size: 14px;\n cursor: pointer;\n}\n\n.eb-error-retry:hover {\n background: rgba(255, 255, 255, 0.25);\n}\n\n.eb-info-content {\n font-size: 14px;\n line-height: 1.5;\n max-width: 400px;\n margin-bottom: 16px;\n}\n\n.eb-info-close,\n.eb-socials-close {\n background: rgba(255, 255, 255, 0.15);\n color: #fff;\n border: 1px solid rgba(255, 255, 255, 0.3);\n border-radius: 4px;\n padding: 8px 24px;\n font-size: 14px;\n cursor: pointer;\n}\n\n.eb-info-close:hover,\n.eb-socials-close:hover {\n background: rgba(255, 255, 255, 0.25);\n}\n\n.eb-socials-links {\n display: flex;\n flex-wrap: wrap;\n gap: 12px;\n justify-content: center;\n margin-bottom: 16px;\n}\n\n.eb-socials-link {\n color: #fff;\n text-decoration: none;\n background: rgba(255, 255, 255, 0.1);\n border-radius: 4px;\n padding: 8px 16px;\n font-size: 14px;\n text-transform: capitalize;\n}\n\n.eb-socials-link:hover {\n background: rgba(255, 255, 255, 0.25);\n}\n\n/* ============================================================\n Loading spinner\n ============================================================ */\n.eb-loading {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 8px;\n color: #fff;\n pointer-events: none;\n}\n\n.eb-loading[hidden] {\n display: none;\n}\n\n.eb-loading .eb-icon {\n width: 40px;\n height: 40px;\n animation: eb-spin 1s linear infinite;\n}\n\n.eb-loading-text {\n font-size: 14px;\n}\n\n@keyframes eb-spin {\n from { transform: rotate(0deg); }\n to { transform: rotate(360deg); }\n}\n\n/* ============================================================\n Controls visibility\n ============================================================ */\n.eb-controls-visible .eb-top-bar,\n.eb-controls-visible .eb-bottom-bar,\n.eb-controls-visible .eb-middle-bar {\n opacity: 1;\n transition: opacity 0.3s;\n}\n\n.eb-controls-hidden .eb-top-bar,\n.eb-controls-hidden .eb-bottom-bar,\n.eb-controls-hidden .eb-middle-bar {\n opacity: 0;\n transition: opacity 0.3s;\n pointer-events: none;\n}\n\n/* ============================================================\n iOS native controls fallback\n ============================================================ */\n.eb-ios-native .eb-top-bar,\n.eb-ios-native .eb-bottom-bar,\n.eb-ios-native .eb-middle-bar,\n.eb-ios-native .eb-overlay-zone,\n.eb-ios-native .eb-error-slot,\n.eb-ios-native .eb-info-slot,\n.eb-ios-native .eb-socials-slot,\n.eb-ios-native .eb-toast-slot {\n display: none;\n}\n\n/* ============================================================\n RTL support\n ============================================================ */\n[dir=\"rtl\"] .eb-bottom-bar__controls-row {\n flex-direction: row-reverse;\n}\n\n[dir=\"rtl\"] .eb-top-bar {\n flex-direction: row-reverse;\n}\n\n/* ============================================================\n Responsive: ensure container fills parent\n ============================================================ */\n.eb-player,\n.eb-player video {\n max-width: 100%;\n max-height: 100%;\n}\n";
49
+ var css_248z = "/* eb-player skin CSS — BEM-prefixed structural styles */\n/* All class names use the eb- prefix to avoid collisions with consumer CSS */\n\n/* ============================================================\n Root container\n ============================================================ */\n.eb-player {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n font-family: sans-serif;\n background: #000;\n color: #fff;\n box-sizing: border-box;\n user-select: none;\n}\n\n/* Video element fills the container */\n.eb-player video.eb-video {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: block;\n object-fit: contain;\n z-index: 1;\n}\n\n/* ============================================================\n Overlay zone (poster, radio, loading, error, socials, info)\n ============================================================ */\n.eb-overlay-zone {\n position: absolute;\n inset: 0;\n pointer-events: none;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 10;\n}\n\n/* Poster image */\n.eb-poster {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n object-fit: contain;\n pointer-events: none;\n background: #000;\n padding: 15%;\n box-sizing: border-box;\n}\n\n/* Ads container (for IMA SDK) */\n.eb-ads-container {\n position: absolute;\n inset: 0;\n z-index: 20;\n pointer-events: none;\n}\n\n/* ============================================================\n Top bar\n ============================================================ */\n.eb-top-bar {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n padding: 8px 12px;\n background: linear-gradient(to bottom, rgba(0,0,0,0.6), transparent);\n z-index: 30;\n pointer-events: auto;\n}\n\n.eb-top-bar__logo {\n height: 28px;\n width: auto;\n display: block;\n}\n\n.eb-top-bar__logo-link {\n display: inline-flex;\n text-decoration: none;\n}\n\n.eb-top-bar__actions {\n display: flex;\n align-items: center;\n gap: 4px;\n margin-left: auto;\n}\n\n/* ============================================================\n Bottom bar\n ============================================================ */\n.eb-bottom-bar {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n display: flex;\n flex-direction: column;\n z-index: 30;\n pointer-events: auto;\n}\n\n.eb-bottom-bar__gradient {\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n height: 100px;\n background: linear-gradient(to top, rgba(0,0,0,0.7), transparent);\n pointer-events: none;\n z-index: -1;\n}\n\n.eb-bottom-bar__controls-row {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 4px;\n padding: 4px 8px 8px;\n}\n\n.eb-bottom-bar__seekbar-zone {\n flex: 1;\n min-width: 0;\n}\n\n/* ============================================================\n Middle bar\n ============================================================ */\n.eb-middle-bar {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n pointer-events: auto;\n z-index: 25;\n display: flex;\n align-items: center;\n gap: 16px;\n}\n\n.eb-middle-bar__play-btn {\n width: 64px;\n height: 64px;\n border-radius: 50%;\n background: rgba(0,0,0,0.5);\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.eb-middle-bar__seek-btn {\n flex-direction: column;\n gap: 2px;\n background: none;\n padding: 4px;\n}\n\n.eb-middle-bar__seek-btn:hover {\n background: rgba(255,255,255,0.1);\n border-radius: 8px;\n}\n\n.eb-seek-circle {\n width: 32px;\n height: 32px;\n display: block;\n pointer-events: none;\n fill: currentColor;\n}\n\n.eb-seek-label {\n font-size: 11px;\n font-weight: 600;\n line-height: 1;\n opacity: 0.85;\n}\n\n/* ============================================================\n Extension zones\n ============================================================ */\n.eb-extension-top-extra,\n.eb-extension-bottom-extra,\n.eb-extension-overlay,\n.eb-extension-controls-extra {\n position: absolute;\n pointer-events: none;\n z-index: 35;\n}\n\n.eb-extension-top-extra > *,\n.eb-extension-bottom-extra > *,\n.eb-extension-overlay > *,\n.eb-extension-controls-extra > * {\n pointer-events: auto;\n}\n\n.eb-extension-top-extra {\n top: 0;\n left: 0;\n right: 0;\n}\n\n.eb-extension-bottom-extra {\n bottom: 0;\n left: 0;\n right: 0;\n}\n\n.eb-extension-overlay {\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.eb-extension-controls-extra {\n bottom: 0;\n right: 0;\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 4px 8px 8px;\n}\n\n/* ============================================================\n Time display\n ============================================================ */\n.eb-time-display {\n font-size: 13px;\n line-height: 1;\n white-space: nowrap;\n padding: 0 4px;\n color: #fff;\n font-variant-numeric: tabular-nums;\n}\n\n/* ============================================================\n Volume control\n ============================================================ */\n.eb-volume-control {\n display: flex;\n align-items: center;\n gap: 4px;\n margin-right: 8px;\n}\n\n.eb-volume-track {\n position: relative;\n width: 60px;\n height: 4px;\n background: rgba(255, 255, 255, 0.2);\n border-radius: 2px;\n cursor: pointer;\n}\n\n.eb-volume-fill {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: var(--eb-color-volume, var(--eb-color-primary, #fff));\n border-radius: 2px;\n pointer-events: none;\n}\n\n.eb-volume-thumb {\n position: absolute;\n top: 50%;\n width: 10px;\n height: 10px;\n border-radius: 50%;\n background: var(--eb-color-volume, var(--eb-color-primary, #fff));\n transform: translate(-50%, -50%);\n pointer-events: none;\n}\n\n/* ============================================================\n Settings panel\n ============================================================ */\n.eb-settings-wrapper {\n position: relative;\n}\n\n.eb-settings-panel {\n position: absolute;\n background: rgba(0, 0, 0, 0.85);\n border-radius: 6px;\n min-width: 180px;\n overflow: hidden;\n font-size: 13px;\n z-index: 40;\n}\n\n/* Vertical direction: up (default) or down */\n.eb-settings-panel--up {\n bottom: calc(100% + 8px);\n}\n\n.eb-settings-panel--down {\n top: calc(100% + 8px);\n}\n\n/* Horizontal alignment: right (default) or left */\n.eb-settings-panel--right {\n right: 0;\n}\n\n.eb-settings-panel--left {\n left: 0;\n}\n\n.eb-settings-menu {\n list-style: none;\n margin: 0;\n padding: 4px 0;\n}\n\n.eb-settings-category,\n.eb-settings-item,\n.eb-settings-back {\n display: flex;\n align-items: center;\n justify-content: space-between;\n width: 100%;\n padding: 8px 14px;\n border: none;\n background: none;\n color: #fff;\n cursor: pointer;\n font-size: 13px;\n text-align: left;\n}\n\n.eb-settings-category:hover,\n.eb-settings-item:hover,\n.eb-settings-back:hover {\n background: rgba(255, 255, 255, 0.1);\n}\n\n.eb-settings-item--selected {\n color: var(--eb-accent, #e53935);\n}\n\n/* ============================================================\n Seekbar\n ============================================================ */\n.eb-seekbar {\n position: relative;\n width: 100%;\n padding: 8px 0;\n cursor: pointer;\n}\n\n.eb-seekbar-disabled {\n pointer-events: none;\n opacity: 0.4;\n}\n\n.eb-seekbar-track {\n position: relative;\n height: 4px;\n background: rgba(255, 255, 255, 0.2);\n border-radius: 2px;\n}\n\n.eb-seekbar-buffered {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: rgba(255, 255, 255, 0.4);\n border-radius: 2px;\n pointer-events: none;\n}\n\n.eb-seekbar-progress {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n background: var(--eb-color-progress, var(--eb-color-primary, #e53935));\n border-radius: 2px;\n pointer-events: none;\n}\n\n.eb-seekbar-thumb {\n position: absolute;\n right: -6px;\n top: 50%;\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background: var(--eb-color-progress, var(--eb-color-primary, #e53935));\n transform: translateY(-50%) scale(0);\n transition: transform 0.15s;\n pointer-events: none;\n}\n\n.eb-seekbar:hover .eb-seekbar-thumb {\n transform: translateY(-50%) scale(1);\n}\n\n.eb-seekbar:hover .eb-seekbar-track {\n height: 6px;\n}\n\n.eb-seekbar-tooltip {\n position: absolute;\n bottom: calc(100% + 8px);\n transform: translateX(-50%);\n background: rgba(0, 0, 0, 0.8);\n color: #fff;\n font-size: 12px;\n padding: 4px 8px;\n border-radius: 3px;\n white-space: nowrap;\n pointer-events: none;\n text-align: center;\n}\n\n.eb-seekbar-preview {\n width: 120px;\n height: auto;\n display: block;\n margin-bottom: 4px;\n}\n\n.eb-chapter-marker {\n position: absolute;\n top: 0;\n width: 3px;\n height: 100%;\n background: rgba(255, 255, 255, 0.6);\n transform: translateX(-50%);\n pointer-events: none;\n}\n\n.eb-chapter-marker.eb-chapter-active {\n background: #fff;\n}\n\n.eb-chapter-skip {\n position: absolute;\n right: 0;\n bottom: calc(100% + 4px);\n background: rgba(0, 0, 0, 0.7);\n color: #fff;\n border: 1px solid rgba(255, 255, 255, 0.4);\n border-radius: 4px;\n padding: 6px 16px;\n font-size: 13px;\n cursor: pointer;\n}\n\n.eb-chapter-skip:hover {\n background: rgba(255, 255, 255, 0.2);\n}\n\n.eb-epg-segment {\n position: absolute;\n top: 0;\n height: 100%;\n border-right: 1px solid rgba(255, 255, 255, 0.3);\n pointer-events: none;\n}\n\n.eb-epg-segment.eb-epg-current {\n background: rgba(255, 255, 255, 0.1);\n}\n\n/* ============================================================\n Icons\n ============================================================ */\n.eb-icon {\n width: 20px;\n height: 20px;\n fill: none;\n stroke: currentColor;\n stroke-width: 2;\n stroke-linecap: round;\n stroke-linejoin: round;\n display: block;\n pointer-events: none;\n flex-shrink: 0;\n}\n\n/* ============================================================\n Buttons (base style)\n ============================================================ */\n.eb-button,\n.eb-play-pause,\n.eb-fullscreen,\n.eb-pip,\n.eb-cast,\n.eb-volume-mute,\n.eb-live-sync {\n cursor: pointer;\n background: none;\n border: none;\n padding: 6px;\n color: var(--eb-color-icon, inherit);\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 4px;\n transition: background 0.15s;\n -webkit-tap-highlight-color: transparent;\n}\n\n.eb-button:hover,\n.eb-play-pause:hover,\n.eb-fullscreen:hover,\n.eb-pip:hover,\n.eb-cast:hover,\n.eb-volume-mute:hover,\n.eb-live-sync:hover {\n background: rgba(255,255,255,0.15);\n}\n\n.eb-button:active,\n.eb-play-pause:active,\n.eb-fullscreen:active,\n.eb-pip:active,\n.eb-cast:active,\n.eb-volume-mute:active,\n.eb-live-sync:active {\n background: rgba(255,255,255,0.25);\n}\n\n/* ============================================================\n Radio overlay\n ============================================================ */\n.eb-radio-overlay {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background: #000;\n z-index: 15;\n}\n\n.eb-radio-bars {\n display: flex;\n align-items: flex-end;\n gap: 4px;\n height: 40px;\n}\n\n.eb-radio-bar {\n display: block;\n width: 6px;\n background: #fff;\n border-radius: 3px;\n animation: eb-radio-bar-bounce 0.8s ease-in-out infinite;\n}\n\n.eb-radio-bar:nth-child(1) { animation-delay: 0s; height: 20px; }\n.eb-radio-bar:nth-child(2) { animation-delay: 0.1s; height: 32px; }\n.eb-radio-bar:nth-child(3) { animation-delay: 0.2s; height: 40px; }\n.eb-radio-bar:nth-child(4) { animation-delay: 0.3s; height: 28px; }\n.eb-radio-bar:nth-child(5) { animation-delay: 0.4s; height: 16px; }\n\n@keyframes eb-radio-bar-bounce {\n 0%, 100% { transform: scaleY(0.4); opacity: 0.7; }\n 50% { transform: scaleY(1); opacity: 1; }\n}\n\n/* Volume mute button — base styles shared via .eb-button group above */\n\n/* ============================================================\n Live sync button\n ============================================================ */\n.eb-live-sync[hidden] {\n display: none;\n}\n\n.eb-live-synced {\n color: var(--eb-accent, #e53935);\n}\n\n/* Default theme: show icon only, hide text badge elements */\n.eb-live-sync__dot,\n.eb-live-sync__label {\n display: none;\n}\n\n/* ============================================================\n Overlay panels (error, info, socials — mounted in .eb-player)\n ============================================================ */\n.eb-error-slot,\n.eb-info-slot,\n.eb-socials-slot {\n position: absolute;\n inset: 0;\n z-index: 50;\n pointer-events: none;\n}\n\n.eb-toast-slot {\n position: absolute;\n bottom: 60px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 50;\n pointer-events: none;\n}\n\n.eb-toast {\n background: rgba(0, 0, 0, 0.8);\n color: #fff;\n padding: 8px 20px;\n border-radius: 4px;\n font-size: 13px;\n white-space: nowrap;\n pointer-events: auto;\n}\n\n.eb-error,\n.eb-info-overlay,\n.eb-socials-overlay {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n background: rgba(0, 0, 0, 0.75);\n color: #fff;\n z-index: 50;\n pointer-events: auto;\n padding: 24px;\n text-align: center;\n}\n\n.eb-error[hidden],\n.eb-info-overlay[hidden],\n.eb-socials-overlay[hidden] {\n display: none;\n}\n\n.eb-error-message {\n font-size: 16px;\n margin: 0 0 16px;\n}\n\n.eb-error-retry {\n background: rgba(255, 255, 255, 0.15);\n color: #fff;\n border: 1px solid rgba(255, 255, 255, 0.3);\n border-radius: 4px;\n padding: 8px 24px;\n font-size: 14px;\n cursor: pointer;\n}\n\n.eb-error-retry:hover {\n background: rgba(255, 255, 255, 0.25);\n}\n\n.eb-info-content {\n font-size: 14px;\n line-height: 1.5;\n max-width: 400px;\n margin-bottom: 16px;\n}\n\n.eb-info-close,\n.eb-socials-close {\n background: rgba(255, 255, 255, 0.15);\n color: #fff;\n border: 1px solid rgba(255, 255, 255, 0.3);\n border-radius: 4px;\n padding: 8px 24px;\n font-size: 14px;\n cursor: pointer;\n}\n\n.eb-info-close:hover,\n.eb-socials-close:hover {\n background: rgba(255, 255, 255, 0.25);\n}\n\n.eb-socials-links {\n display: flex;\n flex-wrap: wrap;\n gap: 12px;\n justify-content: center;\n margin-bottom: 16px;\n}\n\n.eb-socials-link {\n color: #fff;\n text-decoration: none;\n background: rgba(255, 255, 255, 0.1);\n border-radius: 4px;\n padding: 8px 16px;\n font-size: 14px;\n text-transform: capitalize;\n}\n\n.eb-socials-link:hover {\n background: rgba(255, 255, 255, 0.25);\n}\n\n/* ============================================================\n Loading spinner\n ============================================================ */\n.eb-loading {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 8px;\n color: #fff;\n pointer-events: none;\n}\n\n.eb-loading[hidden] {\n display: none;\n}\n\n.eb-loading .eb-icon {\n width: 40px;\n height: 40px;\n animation: eb-spin 1s linear infinite;\n}\n\n.eb-loading-text {\n font-size: 14px;\n}\n\n@keyframes eb-spin {\n from { transform: rotate(0deg); }\n to { transform: rotate(360deg); }\n}\n\n/* ============================================================\n Controls visibility\n ============================================================ */\n.eb-controls-visible .eb-top-bar,\n.eb-controls-visible .eb-bottom-bar,\n.eb-controls-visible .eb-middle-bar {\n opacity: 1;\n transition: opacity 0.3s;\n}\n\n.eb-controls-hidden .eb-top-bar,\n.eb-controls-hidden .eb-bottom-bar,\n.eb-controls-hidden .eb-middle-bar {\n opacity: 0;\n transition: opacity 0.3s;\n pointer-events: none;\n}\n\n/* ============================================================\n iOS native controls fallback\n ============================================================ */\n.eb-ios-native .eb-top-bar,\n.eb-ios-native .eb-bottom-bar,\n.eb-ios-native .eb-middle-bar,\n.eb-ios-native .eb-overlay-zone,\n.eb-ios-native .eb-error-slot,\n.eb-ios-native .eb-info-slot,\n.eb-ios-native .eb-socials-slot,\n.eb-ios-native .eb-toast-slot {\n display: none;\n}\n\n/* ============================================================\n RTL support\n ============================================================ */\n[dir=\"rtl\"] .eb-bottom-bar__controls-row {\n flex-direction: row-reverse;\n}\n\n[dir=\"rtl\"] .eb-top-bar {\n flex-direction: row-reverse;\n}\n\n/* ============================================================\n Responsive: ensure container fills parent\n ============================================================ */\n.eb-player,\n.eb-player video {\n max-width: 100%;\n max-height: 100%;\n}\n";
47
50
  styleInject(css_248z);
48
51
 
49
52
  /**
@@ -296,6 +299,57 @@ var EBPlayerBundle = (function (exports) {
296
299
  * 3. Fixes the JSON.parse/stringify bug in generator/mergeConfig.js by
297
300
  * passing function values by reference instead of serializing them
298
301
  */
302
+ /**
303
+ * Default layout matching the hardcoded component placement.
304
+ * Used as fallback when config.layout is not provided.
305
+ */
306
+ const DEFAULT_LAYOUT = {
307
+ topBar: {
308
+ left: [],
309
+ right: ['share', 'info']
310
+ },
311
+ bottomBar: {
312
+ left: ['play-pause', 'volume'],
313
+ center: ['seekbar'],
314
+ right: ['time', 'live-sync', 'settings', 'pip', 'cast', 'fullscreen']
315
+ },
316
+ middleBar: {
317
+ left: ['rewind'],
318
+ center: [],
319
+ right: ['forward']
320
+ }
321
+ };
322
+ /**
323
+ * Theme-specific default layouts.
324
+ * Used when config.layout is not explicitly provided but a theme is set.
325
+ */
326
+ const THEME_LAYOUTS = {
327
+ v2: {
328
+ topBar: {
329
+ left: [],
330
+ right: ['settings', 'pip', 'cast']
331
+ },
332
+ bottomBar: {
333
+ left: ['play-pause', 'live-sync', 'time'],
334
+ center: ['seekbar'],
335
+ right: ['volume', 'fullscreen']
336
+ },
337
+ middleBar: {
338
+ left: ['rewind'],
339
+ center: [],
340
+ right: ['forward']
341
+ }
342
+ }
343
+ };
344
+ /**
345
+ * Returns the effective layout for a given config.
346
+ * Priority: explicit config.layout > theme default > DEFAULT_LAYOUT
347
+ */
348
+ function resolveLayout(config) {
349
+ const themeLayout = config.skin ? THEME_LAYOUTS[config.skin] : undefined;
350
+ const base = themeLayout ?? DEFAULT_LAYOUT;
351
+ return config.layout ?? base;
352
+ }
299
353
  /**
300
354
  * DEFAULT_CONFIG contains all default values matching configs/default.js.
301
355
  */
@@ -393,7 +447,9 @@ var EBPlayerBundle = (function (exports) {
393
447
  epgPolling: null,
394
448
  epgDefaultLang: 'en',
395
449
  showEpgTitlePreview: false,
396
- showProgressThumb: false
450
+ showProgressThumb: false,
451
+ // Layout
452
+ layout: undefined
397
453
  };
398
454
  /**
399
455
  * Returns true if the value is a plain object (prototype is Object.prototype or null).
@@ -611,7 +667,7 @@ var EBPlayerBundle = (function (exports) {
611
667
  this.savedQuality = -1;
612
668
  this.savedAudioTrack = 0;
613
669
  this.savedSubtitleTrack = -1;
614
- this.wireVideoCommands(bus, video, state, chromecastManager, signal);
670
+ this.wireVideoCommands(bus, video, state, getEngine, chromecastManager, signal);
615
671
  this.wireEngineCommands(bus, getEngine, signal);
616
672
  this.wireReloadOrchestration(bus, state, i18n, onReload, signal);
617
673
  this.wireCastHandoff(bus, video, state, chromecastManager, i18n, signal);
@@ -635,7 +691,7 @@ var EBPlayerBundle = (function (exports) {
635
691
  // ---------------------------------------------------------------------------
636
692
  // Private wiring methods
637
693
  // ---------------------------------------------------------------------------
638
- wireVideoCommands(bus, video, state, chromecastManager, signal) {
694
+ wireVideoCommands(bus, video, state, getEngine, chromecastManager, signal) {
639
695
  bus.on('play', () => {
640
696
  if (state.isCasting && chromecastManager !== null) {
641
697
  chromecastManager.getDriver()?.play();
@@ -655,11 +711,23 @@ var EBPlayerBundle = (function (exports) {
655
711
  chromecastManager.getDriver()?.seek(time);
656
712
  return;
657
713
  }
658
- video.currentTime = time;
714
+ const engine = getEngine();
715
+ if (engine !== null) {
716
+ engine.seek(time);
717
+ }
718
+ else {
719
+ video.currentTime = time;
720
+ }
659
721
  }, { signal });
660
722
  bus.on('mute-toggle', () => {
661
723
  video.muted = !video.muted;
662
724
  }, { signal });
725
+ // Volume slider writes to state.volume — apply to video element
726
+ state.on('volume', (newValue) => {
727
+ if (!state.isCasting) {
728
+ video.volume = newValue;
729
+ }
730
+ }, { signal });
663
731
  }
664
732
  wireEngineCommands(bus, getEngine, signal) {
665
733
  bus.on('settings-select-quality', ({ index }) => {
@@ -834,6 +902,75 @@ var EBPlayerBundle = (function (exports) {
834
902
  }
835
903
  }
836
904
 
905
+ /**
906
+ * TopBar renders the top control bar with logo and dynamic action slots.
907
+ *
908
+ * - Logo: renders as <img> wrapped in <a> if logoLink is set (always built-in)
909
+ * - Action slots: driven by layout config (default: share + info on the right)
910
+ *
911
+ * Subscribes to state.controlsVisible for visibility transitions.
912
+ */
913
+ class TopBar extends BaseComponent {
914
+ onConnect() {
915
+ this.render();
916
+ }
917
+ template() {
918
+ const config = this.config;
919
+ const hasLogo = config.logo !== undefined;
920
+ const layout = resolveLayout(config).topBar;
921
+ const slot = (componentId) => b `<div class="eb-slot-${componentId}"></div>`;
922
+ return b `
923
+ <div class="eb-top-bar">
924
+ ${hasLogo
925
+ ? b `
926
+ ${config.logoLink !== undefined
927
+ ? b `<a class="eb-top-bar__logo-link" href="${config.logoLink}" target="_blank" rel="noopener">
928
+ <img class="eb-top-bar__logo" src="${config.logo}" alt="" />
929
+ </a>`
930
+ : b `<img class="eb-top-bar__logo" src="${config.logo}" alt="" />`}
931
+ `
932
+ : ''}
933
+
934
+ <div class="eb-top-bar__actions">
935
+ ${(layout.left ?? []).map(slot)}
936
+ ${(layout.right ?? []).map(slot)}
937
+ </div>
938
+ </div>
939
+ `;
940
+ }
941
+ }
942
+
943
+ /**
944
+ * BottomBar is a structural container for the seekbar and control buttons.
945
+ *
946
+ * Renders a controls row with dynamic slot divs based on the layout config.
947
+ * When no layout config is provided, falls back to DEFAULT_LAYOUT.
948
+ *
949
+ * The slot divs are empty containers — individual control components
950
+ * mount into them via SkinRoot.connectChildComponents().
951
+ */
952
+ class BottomBar extends BaseComponent {
953
+ onConnect() {
954
+ this.render();
955
+ }
956
+ template() {
957
+ const layout = resolveLayout(this.config).bottomBar;
958
+ const slot = (componentId) => componentId === 'seekbar'
959
+ ? b `<div class="eb-bottom-bar__seekbar-zone"></div>`
960
+ : b `<div class="eb-slot-${componentId}"></div>`;
961
+ return b `
962
+ <div class="eb-bottom-bar">
963
+ <div class="eb-bottom-bar__gradient"></div>
964
+ <div class="eb-bottom-bar__controls-row">
965
+ ${(layout.left ?? []).map(slot)}
966
+ ${(layout.center ?? []).map(slot)}
967
+ ${(layout.right ?? []).map(slot)}
968
+ </div>
969
+ </div>
970
+ `;
971
+ }
972
+ }
973
+
837
974
  /**
838
975
  * DEFAULT_ICONS maps icon names to SVG symbol markup.
839
976
  * Each entry is a <symbol id="eb-{name}" viewBox="0 0 24 24">...</symbol> string.
@@ -850,7 +987,7 @@ var EBPlayerBundle = (function (exports) {
850
987
  'volume-mute': '<symbol id="eb-volume-mute" viewBox="0 0 24 24"><path d="M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z"/><line x1="22" x2="16" y1="9" y2="15"/><line x1="16" x2="22" y1="9" y2="15"/></symbol>',
851
988
  'fullscreen': '<symbol id="eb-fullscreen" viewBox="0 0 24 24"><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/></symbol>',
852
989
  'fullscreen-exit': '<symbol id="eb-fullscreen-exit" viewBox="0 0 24 24"><path d="M8 3v3a2 2 0 0 1-2 2H3"/><path d="M21 8h-3a2 2 0 0 1-2-2V3"/><path d="M3 16h3a2 2 0 0 1 2 2v3"/><path d="M16 21v-3a2 2 0 0 1 2-2h3"/></symbol>',
853
- 'settings': '<symbol id="eb-settings" viewBox="0 0 24 24"><line x1="4" x2="4" y1="21" y2="14"/><line x1="4" x2="4" y1="10" y2="3"/><line x1="12" x2="12" y1="21" y2="12"/><line x1="12" x2="12" y1="8" y2="3"/><line x1="20" x2="20" y1="21" y2="16"/><line x1="20" x2="20" y1="12" y2="3"/><line x1="2" x2="6" y1="14" y2="14"/><line x1="10" x2="14" y1="8" y2="8"/><line x1="18" x2="22" y1="16" y2="16"/></symbol>',
990
+ 'settings': '<symbol id="eb-settings" viewBox="0 0 24 24"><path fill="currentColor" stroke="none" d="M19.14 12.94c.04-.3.06-.61.06-.94s-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></symbol>',
854
991
  'pip': '<symbol id="eb-pip" viewBox="0 0 24 24"><path d="M21 9V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v10c0 1.1.9 2 2 2h4"/><rect width="10" height="7" x="12" y="13" rx="2"/></symbol>',
855
992
  'cast': '<symbol id="eb-cast" viewBox="0 0 24 24"><path d="M2 8V6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-6"/><path d="M2 12a9 9 0 0 1 8 8"/><path d="M2 16a5 5 0 0 1 4 4"/><line x1="2" x2="2.01" y1="20" y2="20"/></symbol>',
856
993
  'close': '<symbol id="eb-close" viewBox="0 0 24 24"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></symbol>',
@@ -949,95 +1086,12 @@ var EBPlayerBundle = (function (exports) {
949
1086
  }
950
1087
 
951
1088
  /**
952
- * TopBar renders the top control bar with logo, share, and info buttons.
953
- *
954
- * - Logo: renders as <img> wrapped in <a> if logoLink is set
955
- * - Share button: shown only when config.socials is not false
956
- * - Info button: always shown; opens the about/info overlay
957
- *
958
- * Subscribes to state.controlsVisible for visibility transitions.
959
- */
960
- class TopBar extends BaseComponent {
961
- onConnect() {
962
- this.render();
963
- }
964
- template() {
965
- const config = this.config;
966
- const hasLogo = config.logo !== undefined;
967
- const hasSocials = config.socials !== false;
968
- return b `
969
- <div class="eb-top-bar">
970
- ${hasLogo
971
- ? b `
972
- ${config.logoLink !== undefined
973
- ? b `<a class="eb-top-bar__logo-link" href="${config.logoLink}" target="_blank" rel="noopener">
974
- <img class="eb-top-bar__logo" src="${config.logo}" alt="" />
975
- </a>`
976
- : b `<img class="eb-top-bar__logo" src="${config.logo}" alt="" />`}
977
- `
978
- : ''}
979
-
980
- <div class="eb-top-bar__actions">
981
- ${hasSocials
982
- ? b `<button
983
- class="eb-button eb-top-bar__btn-share"
984
- @click="${() => { this.state.socialsOpen = !this.state.socialsOpen; }}"
985
- aria-label="Share"
986
- >${icon('share')}</button>`
987
- : ''}
988
-
989
- <button
990
- class="eb-button eb-top-bar__btn-info"
991
- @click="${() => { this.state.infoOpen = !this.state.infoOpen; }}"
992
- aria-label="Info"
993
- >${icon('info')}</button>
994
- </div>
995
- </div>
996
- `;
997
- }
998
- }
999
-
1000
- /**
1001
- * BottomBar is a structural container for the seekbar and control buttons.
1089
+ * MiddleBar renders centered playback controls with a built-in play/pause button.
1002
1090
  *
1003
- * Renders a single controls row with:
1004
- * - play/pause, volume buttons (left)
1005
- * - seekbar filling remaining space + time display (center)
1006
- * - live-sync, settings, pip, cast, fullscreen buttons (right)
1007
- *
1008
- * The slot divs are empty containers — individual control components
1009
- * mount into them via SkinRoot.connectChildComponents().
1010
- */
1011
- class BottomBar extends BaseComponent {
1012
- onConnect() {
1013
- this.render();
1014
- }
1015
- template() {
1016
- return b `
1017
- <div class="eb-bottom-bar">
1018
- <div class="eb-bottom-bar__gradient"></div>
1019
- <div class="eb-bottom-bar__controls-row">
1020
- <div class="eb-slot-play-pause"></div>
1021
- <div class="eb-slot-volume"></div>
1022
- <div class="eb-bottom-bar__seekbar-zone"></div>
1023
- <div class="eb-slot-time"></div>
1024
- <div class="eb-slot-live-sync"></div>
1025
- <div class="eb-slot-settings"></div>
1026
- <div class="eb-slot-pip"></div>
1027
- <div class="eb-slot-cast"></div>
1028
- <div class="eb-slot-fullscreen"></div>
1029
- </div>
1030
- </div>
1031
- `;
1032
- }
1033
- }
1034
-
1035
- /**
1036
- * MiddleBar renders centered playback controls: rewind, play/pause, forward.
1091
+ * The central play/pause button is always present (part of the bar's identity).
1092
+ * Surrounding slots (default: rewind left, forward right) are driven by layout config.
1037
1093
  *
1038
- * - Click play/pause emits 'play' or 'pause' via the EventBus
1039
- * - Rewind/forward buttons are mounted into slots by SkinRoot
1040
- * - Subscribes to state.playbackState for icon toggle
1094
+ * Subscribes to state.playbackState for icon toggle.
1041
1095
  */
1042
1096
  class MiddleBar extends BaseComponent {
1043
1097
  onConnect() {
@@ -1050,9 +1104,11 @@ var EBPlayerBundle = (function (exports) {
1050
1104
  template() {
1051
1105
  const state = this.state;
1052
1106
  const isPlaying = state.playbackState === 'playing';
1107
+ const layout = resolveLayout(this.config).middleBar;
1108
+ const slot = (componentId) => b `<div class="eb-slot-${componentId}"></div>`;
1053
1109
  return b `
1054
1110
  <div class="eb-middle-bar">
1055
- <div class="eb-slot-rewind"></div>
1111
+ ${(layout.left ?? []).map(slot)}
1056
1112
  <button
1057
1113
  class="eb-button eb-middle-bar__play-btn"
1058
1114
  @click="${() => this.handleClick()}"
@@ -1060,7 +1116,7 @@ var EBPlayerBundle = (function (exports) {
1060
1116
  >
1061
1117
  ${isPlaying ? icon('pause') : icon('play')}
1062
1118
  </button>
1063
- <div class="eb-slot-forward"></div>
1119
+ ${(layout.right ?? []).map(slot)}
1064
1120
  </div>
1065
1121
  `;
1066
1122
  }
@@ -1218,29 +1274,51 @@ var EBPlayerBundle = (function (exports) {
1218
1274
  }
1219
1275
 
1220
1276
  /**
1221
- * Time display component.
1277
+ * Seekbar component.
1222
1278
  *
1223
- * For VOD: shows "currentTime / duration" (e.g., "1:23 / 5:00")
1224
- * For live + synced: shows current wall-clock time (e.g., "19:25:30")
1225
- * For live + not synced: shows negative offset (e.g., "-0:30")
1279
+ * Renders a full-featured seekbar with:
1280
+ * - Progress and buffered region tracks
1281
+ * - Pointer Events drag with setPointerCapture
1282
+ * - RTL coordinate flip
1283
+ * - Chapter markers with active state and skip button
1284
+ * - EPG program segments
1285
+ * - Tooltip with time on hover, optional snapshot preview
1286
+ * - Disabled state during ad playback
1226
1287
  *
1227
- * Uses requestAnimationFrame batching for currentTime updates
1228
- * to avoid excessive re-renders during playback.
1288
+ * rAF batching prevents excessive re-renders during high-frequency currentTime updates.
1229
1289
  */
1230
- class TimeDisplay extends BaseComponent {
1290
+ class Seekbar extends BaseComponent {
1231
1291
  constructor() {
1232
1292
  super(...arguments);
1293
+ this.isDragging = false;
1294
+ this.dragValue = 0;
1233
1295
  this.rafPending = false;
1296
+ this.tooltipVisible = false;
1297
+ this.tooltipTime = 0;
1298
+ this.tooltipX = 0;
1299
+ this.previewVideoEl = null;
1300
+ this.snapshotTake = null;
1234
1301
  }
1235
1302
  onConnect() {
1236
- // currentTime changes frequently batch via RAF
1303
+ // Subscribe to snapshot handler readiness (emitted by eb-player.ts)
1304
+ this.bus.on('snapshot-handler-ready', (payload) => {
1305
+ this.snapshotTake = payload.take;
1306
+ this.previewVideoEl = payload.video;
1307
+ this.previewVideoEl.className = 'eb-seekbar-preview';
1308
+ this.previewVideoEl.muted = true;
1309
+ }, { signal: this.signal });
1310
+ // currentTime changes at every animation frame — batch via rAF
1237
1311
  this.state.on('currentTime', () => this.scheduleRender(), { signal: this.signal });
1238
1312
  // Other state changes render immediately (infrequent)
1239
1313
  this.state.on('duration', () => this.scheduleRender(), { signal: this.signal });
1240
- this.state.on('isLive', () => this.scheduleRender(), { signal: this.signal });
1241
- this.state.on('isSyncWithLive', () => this.scheduleRender(), { signal: this.signal });
1314
+ this.state.on('bufferedEnd', () => this.scheduleRender(), { signal: this.signal });
1315
+ this.state.on('isRtl', () => this.scheduleRender(), { signal: this.signal });
1316
+ this.state.on('adPlaying', () => this.scheduleRender(), { signal: this.signal });
1317
+ this.state.on('chapters', () => this.scheduleRender(), { signal: this.signal });
1318
+ this.state.on('epgPrograms', () => this.scheduleRender(), { signal: this.signal });
1242
1319
  this.render();
1243
1320
  }
1321
+ // ---- rAF batching ----
1244
1322
  scheduleRender() {
1245
1323
  if (this.rafPending)
1246
1324
  return;
@@ -1250,56 +1328,283 @@ var EBPlayerBundle = (function (exports) {
1250
1328
  this.render();
1251
1329
  });
1252
1330
  }
1253
- template() {
1254
- const { currentTime, duration, isLive, isSyncWithLive } = this.state;
1255
- let timeText;
1256
- if (isLive) {
1257
- if (isSyncWithLive || !Number.isFinite(duration)) {
1258
- // At the live edge or non-DVR stream: show current wall-clock time
1259
- timeText = formatWallClock(Date.now());
1260
- }
1261
- else {
1262
- // Behind the live edge with DVR: show negative offset
1263
- const offset = duration - currentTime;
1264
- timeText = `- ${formatDuration(offset)}`;
1265
- }
1331
+ // ---- Coordinate helpers ----
1332
+ /**
1333
+ * Converts a PointerEvent clientX to a time value, accounting for RTL mode.
1334
+ * Clamps result to [0, duration].
1335
+ */
1336
+ eventToTime(event, trackEl) {
1337
+ const rect = trackEl.getBoundingClientRect();
1338
+ const rawPercent = (event.clientX - rect.left) / rect.width;
1339
+ const percent = this.state.isRtl ? 1 - rawPercent : rawPercent;
1340
+ const clamped = Math.min(1, Math.max(0, percent));
1341
+ return clamped * this.state.duration;
1342
+ }
1343
+ // ---- Drag handlers ----
1344
+ handlePointerDown(event) {
1345
+ if (this.state.adPlaying)
1346
+ return;
1347
+ const trackEl = event.currentTarget;
1348
+ // setPointerCapture ensures events continue firing even if pointer leaves the element
1349
+ if (typeof trackEl.setPointerCapture === 'function') {
1350
+ trackEl.setPointerCapture(event.pointerId);
1266
1351
  }
1267
- else {
1268
- timeText = `${formatDuration(currentTime)} / ${formatDuration(duration)}`;
1352
+ this.isDragging = true;
1353
+ this.dragValue = this.eventToTime(event, trackEl);
1354
+ this.render();
1355
+ }
1356
+ handlePointerMove(event) {
1357
+ if (this.isDragging) {
1358
+ const trackEl = event.currentTarget;
1359
+ this.dragValue = this.eventToTime(event, trackEl);
1360
+ this.scheduleRender();
1269
1361
  }
1270
- return b `<div class="eb-time-display">${timeText}</div>`;
1362
+ // Always update tooltip on pointermove over the track
1363
+ this.updateTooltip(event);
1271
1364
  }
1272
- }
1273
-
1274
- /**
1275
- * Live sync button.
1276
- *
1277
- * Visible only for live streams (state.isLive === true).
1278
- * Has eb-live-synced class when state.isSyncWithLive is true.
1279
- * Click emits 'sync-live' to jump back to the live edge.
1280
- */
1281
- class LiveSyncButton extends BaseComponent {
1282
- onConnect() {
1283
- this.state.on('isLive', () => this.render(), { signal: this.signal });
1284
- this.state.on('isSyncWithLive', () => this.render(), { signal: this.signal });
1365
+ handlePointerUp(event) {
1366
+ if (!this.isDragging)
1367
+ return;
1368
+ const trackEl = event.currentTarget;
1369
+ const seekTime = this.eventToTime(event, trackEl);
1370
+ this.isDragging = false;
1371
+ this.bus.emit('seek', { time: seekTime });
1285
1372
  this.render();
1286
1373
  }
1287
- template() {
1288
- const { isLive, isSyncWithLive } = this.state;
1289
- if (!isLive) {
1290
- return b `<button class="eb-live-sync" hidden style="display:none">${icon('live')}</button>`;
1291
- }
1292
- return b `
1293
- <button
1294
- class="${isSyncWithLive ? 'eb-live-sync eb-live-synced' : 'eb-live-sync'}"
1295
- aria-label="Go to live"
1296
- @click="${() => this.bus.emit('sync-live')}"
1297
- >
1298
- ${icon('live')}
1299
- </button>
1300
- `;
1374
+ handlePointerLeave() {
1375
+ this.tooltipVisible = false;
1376
+ this.render();
1301
1377
  }
1302
- }
1378
+ // ---- Tooltip ----
1379
+ updateTooltip(event) {
1380
+ const trackEl = event.currentTarget;
1381
+ const rect = trackEl.getBoundingClientRect();
1382
+ // Compute hover time (use LTR calculation for tooltip position regardless of RTL)
1383
+ const rawPercent = (event.clientX - rect.left) / rect.width;
1384
+ const clampedPercent = Math.min(1, Math.max(0, rawPercent));
1385
+ this.tooltipTime = this.state.isRtl
1386
+ ? (1 - clampedPercent) * this.state.duration
1387
+ : clampedPercent * this.state.duration;
1388
+ // Position tooltip at pointer X relative to track, clamped to track edges
1389
+ this.tooltipX = Math.min(rect.width, Math.max(0, event.clientX - rect.left));
1390
+ this.tooltipVisible = true;
1391
+ // Request snapshot frame for seekbar preview thumbnail
1392
+ if (this.snapshotTake !== null) {
1393
+ this.snapshotTake(this.tooltipTime);
1394
+ }
1395
+ this.render();
1396
+ }
1397
+ // ---- Chapter helpers ----
1398
+ findActiveChapter(chapters) {
1399
+ const currentTime = this.state.currentTime;
1400
+ for (const chapter of chapters) {
1401
+ if (currentTime >= chapter.startTime && currentTime <= chapter.endTime) {
1402
+ return chapter;
1403
+ }
1404
+ }
1405
+ return null;
1406
+ }
1407
+ findSkippableChapter(chapter) {
1408
+ const skippable = this.config.skippableChapters;
1409
+ for (const skippableEntry of skippable) {
1410
+ if (skippableEntry.chapter === chapter.title) {
1411
+ return skippableEntry;
1412
+ }
1413
+ }
1414
+ return null;
1415
+ }
1416
+ // ---- EPG helpers ----
1417
+ renderEpgSegments(programs) {
1418
+ if (programs.length === 0)
1419
+ return [];
1420
+ // Compute the time window from first program start to last program end
1421
+ const windowStart = programs[0].start;
1422
+ const windowEnd = programs[programs.length - 1].end;
1423
+ const windowDuration = windowEnd - windowStart;
1424
+ if (windowDuration <= 0)
1425
+ return [];
1426
+ const now = Date.now();
1427
+ return programs.map((program) => {
1428
+ const leftPercent = ((program.start - windowStart) / windowDuration) * 100;
1429
+ const widthPercent = ((program.end - program.start) / windowDuration) * 100;
1430
+ const isCurrent = now >= program.start && now < program.end;
1431
+ return b `
1432
+ <div
1433
+ class="eb-epg-segment${isCurrent ? ' eb-epg-current' : ''}"
1434
+ style="left: ${leftPercent.toFixed(2)}%; width: ${widthPercent.toFixed(2)}%"
1435
+ title="${program.title}"
1436
+ ></div>
1437
+ `;
1438
+ });
1439
+ }
1440
+ // ---- Template ----
1441
+ template() {
1442
+ const { currentTime, duration, bufferedEnd, adPlaying, chapters, epgPrograms } = this.state;
1443
+ const isSeekbarHidden = !this.config.seekbar;
1444
+ const isDisabled = adPlaying;
1445
+ // Progress percentage — use dragValue during drag to prevent live-update jitter
1446
+ const progressPercent = duration > 0
1447
+ ? (this.isDragging ? this.dragValue : currentTime) / duration * 100
1448
+ : 0;
1449
+ // Buffered percentage
1450
+ const bufferedPercent = duration > 0 ? bufferedEnd / duration * 100 : 0;
1451
+ // Chapter markers
1452
+ const activeChapter = this.findActiveChapter(chapters);
1453
+ const skippableEntry = activeChapter !== null ? this.findSkippableChapter(activeChapter) : null;
1454
+ const chapterMarkers = chapters.map((chapter) => {
1455
+ const leftPercent = duration > 0 ? (chapter.startTime / duration) * 100 : 0;
1456
+ const isActive = chapter === activeChapter;
1457
+ return b `
1458
+ <div
1459
+ class="eb-chapter-marker${isActive ? ' eb-chapter-active' : ''}"
1460
+ style="left: ${leftPercent.toFixed(2)}%"
1461
+ title="${chapter.title}"
1462
+ ></div>
1463
+ `;
1464
+ });
1465
+ // Skip button (only for active skippable chapter)
1466
+ const skipButton = activeChapter !== null && skippableEntry !== null
1467
+ ? b `
1468
+ <button
1469
+ class="eb-chapter-skip"
1470
+ @click="${() => this.bus.emit('seek', { time: activeChapter.endTime })}"
1471
+ >${skippableEntry.message}</button>
1472
+ `
1473
+ : b ``;
1474
+ // EPG segments
1475
+ const epgSegments = this.renderEpgSegments(epgPrograms);
1476
+ // Tooltip — for live streams, show wall-clock time at hovered position
1477
+ const tooltipTimeText = this.state.isLive && Number.isFinite(duration)
1478
+ ? formatWallClock(Date.now() - (duration - this.tooltipTime) * 1000)
1479
+ : formatDuration(this.tooltipTime);
1480
+ const tooltip = b `
1481
+ <div
1482
+ class="eb-seekbar-tooltip"
1483
+ style="left: ${this.tooltipX}px"
1484
+ ?hidden="${!this.tooltipVisible}"
1485
+ >
1486
+ ${tooltipTimeText}
1487
+ ${this.previewVideoEl !== null ? this.previewVideoEl : b ``}
1488
+ </div>
1489
+ `;
1490
+ return b `
1491
+ <div
1492
+ class="eb-seekbar${isDisabled ? ' eb-seekbar-disabled' : ''}"
1493
+ ?hidden="${isSeekbarHidden}"
1494
+ >
1495
+ <div
1496
+ class="eb-seekbar-track"
1497
+ @pointerdown="${(event) => this.handlePointerDown(event)}"
1498
+ @pointermove="${(event) => this.handlePointerMove(event)}"
1499
+ @pointerup="${(event) => this.handlePointerUp(event)}"
1500
+ @pointerleave="${() => this.handlePointerLeave()}"
1501
+ >
1502
+ <div class="eb-seekbar-buffered" style="width: ${bufferedPercent.toFixed(2)}%"></div>
1503
+ <div class="eb-seekbar-progress" style="width: ${progressPercent.toFixed(2)}%">
1504
+ <div class="eb-seekbar-thumb"></div>
1505
+ </div>
1506
+ ${chapterMarkers}
1507
+ ${epgSegments}
1508
+ ${tooltip}
1509
+ </div>
1510
+ ${skipButton}
1511
+ </div>
1512
+ `;
1513
+ }
1514
+ }
1515
+
1516
+ /**
1517
+ * Time display component.
1518
+ *
1519
+ * For VOD: shows "currentTime / duration" (e.g., "1:23 / 5:00")
1520
+ * For live + synced: shows current wall-clock time (e.g., "19:25:30")
1521
+ * For live + not synced: shows negative offset (e.g., "-0:30")
1522
+ *
1523
+ * Uses requestAnimationFrame batching for currentTime updates
1524
+ * to avoid excessive re-renders during playback.
1525
+ */
1526
+ class TimeDisplay extends BaseComponent {
1527
+ constructor() {
1528
+ super(...arguments);
1529
+ this.rafPending = false;
1530
+ }
1531
+ onConnect() {
1532
+ // currentTime changes frequently — batch via RAF
1533
+ this.state.on('currentTime', () => this.scheduleRender(), { signal: this.signal });
1534
+ // Other state changes render immediately (infrequent)
1535
+ this.state.on('duration', () => this.scheduleRender(), { signal: this.signal });
1536
+ this.state.on('isLive', () => this.scheduleRender(), { signal: this.signal });
1537
+ this.state.on('isSyncWithLive', () => this.scheduleRender(), { signal: this.signal });
1538
+ this.render();
1539
+ }
1540
+ scheduleRender() {
1541
+ if (this.rafPending)
1542
+ return;
1543
+ this.rafPending = true;
1544
+ requestAnimationFrame(() => {
1545
+ this.rafPending = false;
1546
+ this.render();
1547
+ });
1548
+ }
1549
+ template() {
1550
+ const { currentTime, duration, isLive, isSyncWithLive } = this.state;
1551
+ let timeText;
1552
+ if (isLive) {
1553
+ if (isSyncWithLive || !Number.isFinite(duration)) {
1554
+ // At the live edge or non-DVR stream: show current wall-clock time
1555
+ timeText = formatWallClock(Date.now());
1556
+ }
1557
+ else {
1558
+ // Behind the live edge with DVR: show negative offset
1559
+ const offset = duration - currentTime;
1560
+ timeText = `- ${formatDuration(offset)}`;
1561
+ }
1562
+ }
1563
+ else {
1564
+ timeText = `${formatDuration(currentTime)} / ${formatDuration(duration)}`;
1565
+ }
1566
+ return b `<div class="eb-time-display">${timeText}</div>`;
1567
+ }
1568
+ }
1569
+
1570
+ /**
1571
+ * Live sync button.
1572
+ *
1573
+ * Visible when:
1574
+ * - state.isLive === true (auto-detected live stream), OR
1575
+ * - config.liveButton === true (forced by config)
1576
+ *
1577
+ * Has eb-live-synced class when state.isSyncWithLive is true.
1578
+ * Click emits 'sync-live' to jump back to the live edge.
1579
+ *
1580
+ * Renders both an icon (for default/icon themes) and a text label
1581
+ * with blinking dot (for v2/badge themes). CSS controls which is shown.
1582
+ */
1583
+ class LiveSyncButton extends BaseComponent {
1584
+ onConnect() {
1585
+ this.state.on('isLive', () => this.render(), { signal: this.signal });
1586
+ this.state.on('isSyncWithLive', () => this.render(), { signal: this.signal });
1587
+ this.render();
1588
+ }
1589
+ template() {
1590
+ const { isLive, isSyncWithLive } = this.state;
1591
+ const configLive = this.config.liveButton === true || this.config.isLive === true;
1592
+ if (!isLive && !configLive) {
1593
+ return b `<button class="eb-live-sync" hidden style="display:none">${icon('live')}</button>`;
1594
+ }
1595
+ return b `
1596
+ <button
1597
+ class="${isSyncWithLive ? 'eb-live-sync eb-live-synced' : 'eb-live-sync'}"
1598
+ aria-label="Go to live"
1599
+ @click="${() => this.bus.emit('sync-live')}"
1600
+ >
1601
+ <span class="eb-live-sync__icon">${icon('live')}</span>
1602
+ <span class="eb-live-sync__dot"></span>
1603
+ <span class="eb-live-sync__label">LIVE</span>
1604
+ </button>
1605
+ `;
1606
+ }
1607
+ }
1303
1608
 
1304
1609
  const SPEED_VALUES = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
1305
1610
  /**
@@ -1364,11 +1669,20 @@ var EBPlayerBundle = (function (exports) {
1364
1669
  * - Subtitles: only when state.subtitleTracks is non-empty
1365
1670
  *
1366
1671
  * Panel is visible only when state.settingsOpen is true.
1672
+ *
1673
+ * Panel placement is computed from the toggle button's position relative
1674
+ * to the .eb-player container:
1675
+ * - Top half → panel drops downward (eb-settings-panel--down)
1676
+ * - Bottom half → panel opens upward (eb-settings-panel--up) [default]
1677
+ * - Left half → panel aligns left (eb-settings-panel--left)
1678
+ * - Right half → panel aligns right (eb-settings-panel--right) [default]
1367
1679
  */
1368
1680
  class SettingsPanel extends BaseComponent {
1369
1681
  constructor() {
1370
1682
  super(...arguments);
1371
1683
  this.mode = 'root';
1684
+ this.verticalDir = 'up';
1685
+ this.horizontalDir = 'right';
1372
1686
  }
1373
1687
  onConnect() {
1374
1688
  this.state.on('settingsOpen', () => {
@@ -1376,6 +1690,10 @@ var EBPlayerBundle = (function (exports) {
1376
1690
  if (!this.state.settingsOpen) {
1377
1691
  this.mode = 'root';
1378
1692
  }
1693
+ else {
1694
+ // Compute placement when opening
1695
+ this.computePlacement();
1696
+ }
1379
1697
  this.render();
1380
1698
  }, { signal: this.signal });
1381
1699
  this.state.on('qualityLevels', () => this.render(), { signal: this.signal });
@@ -1387,6 +1705,23 @@ var EBPlayerBundle = (function (exports) {
1387
1705
  this.state.on('playbackRate', () => this.render(), { signal: this.signal });
1388
1706
  this.render();
1389
1707
  }
1708
+ /**
1709
+ * Determines which quadrant of the player the toggle button sits in,
1710
+ * so the panel can open in the direction with the most available space.
1711
+ */
1712
+ computePlacement() {
1713
+ const playerEl = this.el?.closest('.eb-player');
1714
+ const buttonEl = this.el?.querySelector('.eb-settings-toggle');
1715
+ if (!playerEl || !buttonEl)
1716
+ return;
1717
+ const playerRect = playerEl.getBoundingClientRect();
1718
+ const buttonRect = buttonEl.getBoundingClientRect();
1719
+ // Button center relative to the player
1720
+ const buttonCenterY = buttonRect.top + buttonRect.height / 2 - playerRect.top;
1721
+ const buttonCenterX = buttonRect.left + buttonRect.width / 2 - playerRect.left;
1722
+ this.verticalDir = buttonCenterY < playerRect.height / 2 ? 'down' : 'up';
1723
+ this.horizontalDir = buttonCenterX < playerRect.width / 2 ? 'left' : 'right';
1724
+ }
1390
1725
  navigateTo(mode) {
1391
1726
  this.mode = mode;
1392
1727
  this.render();
@@ -1514,8 +1849,9 @@ var EBPlayerBundle = (function (exports) {
1514
1849
  else {
1515
1850
  menuContent = this.renderRootMenu();
1516
1851
  }
1852
+ const panelClass = `eb-settings-panel eb-settings-panel--${this.verticalDir} eb-settings-panel--${this.horizontalDir}`;
1517
1853
  panel = b `
1518
- <div class="eb-settings-panel">
1854
+ <div class="${panelClass}">
1519
1855
  ${menuContent}
1520
1856
  </div>
1521
1857
  `;
@@ -1859,327 +2195,152 @@ var EBPlayerBundle = (function (exports) {
1859
2195
  * Rewind button — seeks back by config.seekOffset seconds (default 15).
1860
2196
  *
1861
2197
  * Displayed in the middle bar beside the play/pause button.
1862
- * Renders a circle with an arrow indicating direction, and the
1863
- * seek offset number below the circle.
2198
+ * Renders a circular arrow icon with the seek offset number below.
1864
2199
  * Hidden during ad playback.
1865
2200
  */
1866
2201
  class RewindButton extends BaseComponent {
1867
2202
  onConnect() {
1868
- this.state.on('adPlaying', () => this.render(), { signal: this.signal });
1869
- this.render();
1870
- }
1871
- template() {
1872
- if (this.state.adPlaying) {
1873
- return b ``;
1874
- }
1875
- const offset = this.config.seekOffset || 15;
1876
- const isModern = this.config.skin === 'modern';
1877
- return b `
1878
- <button
1879
- class="eb-button eb-middle-bar__seek-btn"
1880
- aria-label="Rewind ${offset} seconds"
1881
- @click="${() => this.handleClick()}"
1882
- >
1883
- ${isModern
1884
- ? icon('rewind', 'eb-seek-circle')
1885
- : b `<svg class="eb-seek-circle" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
1886
- <path d="M20 4a16 16 0 1 0 1.25.05" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>
1887
- <path d="M20 4l-5 4 5 4" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
1888
- </svg>`}
1889
- <span class="eb-seek-label">${offset}s</span>
1890
- </button>
1891
- `;
1892
- }
1893
- handleClick() {
1894
- const offset = this.config.seekOffset || 15;
1895
- const seekTime = Math.max(this.state.currentTime - offset, 0);
1896
- this.bus.emit('seek', { time: seekTime });
1897
- }
1898
- }
1899
-
1900
- /**
1901
- * Forward button — seeks forward by config.seekOffset seconds (default 15).
1902
- *
1903
- * Displayed in the middle bar beside the play/pause button.
1904
- * Renders a circle with an arrow indicating direction, and the
1905
- * seek offset number below the circle.
1906
- * Hidden during ad playback.
1907
- */
1908
- class ForwardButton extends BaseComponent {
1909
- onConnect() {
1910
- this.state.on('adPlaying', () => this.render(), { signal: this.signal });
1911
- this.render();
1912
- }
1913
- template() {
1914
- if (this.state.adPlaying) {
1915
- return b ``;
1916
- }
1917
- const offset = this.config.seekOffset || 15;
1918
- const isModern = this.config.skin === 'modern';
1919
- return b `
1920
- <button
1921
- class="eb-button eb-middle-bar__seek-btn"
1922
- aria-label="Forward ${offset} seconds"
1923
- @click="${() => this.handleClick()}"
1924
- >
1925
- ${isModern
1926
- ? icon('forward', 'eb-seek-circle')
1927
- : b `<svg class="eb-seek-circle" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
1928
- <path d="M20 4a16 16 0 1 1-1.25.05" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>
1929
- <path d="M20 4l5 4-5 4" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
1930
- </svg>`}
1931
- <span class="eb-seek-label">${offset}s</span>
1932
- </button>
1933
- `;
1934
- }
1935
- handleClick() {
1936
- const offset = this.config.seekOffset || 15;
1937
- const seekTime = Math.min(this.state.currentTime + offset, this.state.duration);
1938
- this.bus.emit('seek', { time: seekTime });
1939
- }
1940
- }
1941
-
1942
- /**
1943
- * Seekbar component.
1944
- *
1945
- * Renders a full-featured seekbar with:
1946
- * - Progress and buffered region tracks
1947
- * - Pointer Events drag with setPointerCapture
1948
- * - RTL coordinate flip
1949
- * - Chapter markers with active state and skip button
1950
- * - EPG program segments
1951
- * - Tooltip with time on hover, optional snapshot preview
1952
- * - Disabled state during ad playback
1953
- *
1954
- * rAF batching prevents excessive re-renders during high-frequency currentTime updates.
1955
- */
1956
- class Seekbar extends BaseComponent {
1957
- constructor() {
1958
- super(...arguments);
1959
- this.isDragging = false;
1960
- this.dragValue = 0;
1961
- this.rafPending = false;
1962
- this.tooltipVisible = false;
1963
- this.tooltipTime = 0;
1964
- this.tooltipX = 0;
1965
- this.previewVideoEl = null;
1966
- this.snapshotTake = null;
1967
- }
1968
- onConnect() {
1969
- // Subscribe to snapshot handler readiness (emitted by eb-player.ts)
1970
- this.bus.on('snapshot-handler-ready', (payload) => {
1971
- this.snapshotTake = payload.take;
1972
- this.previewVideoEl = payload.video;
1973
- this.previewVideoEl.className = 'eb-seekbar-preview';
1974
- this.previewVideoEl.muted = true;
1975
- }, { signal: this.signal });
1976
- // currentTime changes at every animation frame — batch via rAF
1977
- this.state.on('currentTime', () => this.scheduleRender(), { signal: this.signal });
1978
- // Other state changes render immediately (infrequent)
1979
- this.state.on('duration', () => this.scheduleRender(), { signal: this.signal });
1980
- this.state.on('bufferedEnd', () => this.scheduleRender(), { signal: this.signal });
1981
- this.state.on('isRtl', () => this.scheduleRender(), { signal: this.signal });
1982
- this.state.on('adPlaying', () => this.scheduleRender(), { signal: this.signal });
1983
- this.state.on('chapters', () => this.scheduleRender(), { signal: this.signal });
1984
- this.state.on('epgPrograms', () => this.scheduleRender(), { signal: this.signal });
1985
- this.render();
1986
- }
1987
- // ---- rAF batching ----
1988
- scheduleRender() {
1989
- if (this.rafPending)
1990
- return;
1991
- this.rafPending = true;
1992
- requestAnimationFrame(() => {
1993
- this.rafPending = false;
1994
- this.render();
1995
- });
1996
- }
1997
- // ---- Coordinate helpers ----
1998
- /**
1999
- * Converts a PointerEvent clientX to a time value, accounting for RTL mode.
2000
- * Clamps result to [0, duration].
2001
- */
2002
- eventToTime(event, trackEl) {
2003
- const rect = trackEl.getBoundingClientRect();
2004
- const rawPercent = (event.clientX - rect.left) / rect.width;
2005
- const percent = this.state.isRtl ? 1 - rawPercent : rawPercent;
2006
- const clamped = Math.min(1, Math.max(0, percent));
2007
- return clamped * this.state.duration;
2008
- }
2009
- // ---- Drag handlers ----
2010
- handlePointerDown(event) {
2011
- if (this.state.adPlaying)
2012
- return;
2013
- const trackEl = event.currentTarget;
2014
- // setPointerCapture ensures events continue firing even if pointer leaves the element
2015
- if (typeof trackEl.setPointerCapture === 'function') {
2016
- trackEl.setPointerCapture(event.pointerId);
2017
- }
2018
- this.isDragging = true;
2019
- this.dragValue = this.eventToTime(event, trackEl);
2203
+ this.state.on('adPlaying', () => this.render(), { signal: this.signal });
2020
2204
  this.render();
2021
2205
  }
2022
- handlePointerMove(event) {
2023
- if (this.isDragging) {
2024
- const trackEl = event.currentTarget;
2025
- this.dragValue = this.eventToTime(event, trackEl);
2026
- this.scheduleRender();
2206
+ template() {
2207
+ if (this.state.adPlaying) {
2208
+ return b ``;
2027
2209
  }
2028
- // Always update tooltip on pointermove over the track
2029
- this.updateTooltip(event);
2210
+ const offset = this.config.seekOffset || 15;
2211
+ const isModern = this.config.skin === 'modern';
2212
+ return b `
2213
+ <button
2214
+ class="eb-button eb-middle-bar__seek-btn"
2215
+ aria-label="Rewind ${offset} seconds"
2216
+ @click="${() => this.handleClick()}"
2217
+ >
2218
+ ${isModern
2219
+ ? icon('rewind', 'eb-seek-circle')
2220
+ : b `<svg class="eb-seek-circle" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
2221
+ <path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z" fill="currentColor"/>
2222
+ </svg>`}
2223
+ <span class="eb-seek-label">${offset}</span>
2224
+ </button>
2225
+ `;
2030
2226
  }
2031
- handlePointerUp(event) {
2032
- if (!this.isDragging)
2033
- return;
2034
- const trackEl = event.currentTarget;
2035
- const seekTime = this.eventToTime(event, trackEl);
2036
- this.isDragging = false;
2227
+ handleClick() {
2228
+ const offset = this.config.seekOffset || 15;
2229
+ const seekTime = Math.max(this.state.currentTime - offset, 0);
2037
2230
  this.bus.emit('seek', { time: seekTime });
2038
- this.render();
2039
2231
  }
2040
- handlePointerLeave() {
2041
- this.tooltipVisible = false;
2232
+ }
2233
+
2234
+ /**
2235
+ * Forward button — seeks forward by config.seekOffset seconds (default 15).
2236
+ *
2237
+ * Displayed in the middle bar beside the play/pause button.
2238
+ * Renders a circular arrow icon with the seek offset number below.
2239
+ * Hidden during ad playback.
2240
+ */
2241
+ class ForwardButton extends BaseComponent {
2242
+ onConnect() {
2243
+ this.state.on('adPlaying', () => this.render(), { signal: this.signal });
2042
2244
  this.render();
2043
2245
  }
2044
- // ---- Tooltip ----
2045
- updateTooltip(event) {
2046
- const trackEl = event.currentTarget;
2047
- const rect = trackEl.getBoundingClientRect();
2048
- // Compute hover time (use LTR calculation for tooltip position regardless of RTL)
2049
- const rawPercent = (event.clientX - rect.left) / rect.width;
2050
- const clampedPercent = Math.min(1, Math.max(0, rawPercent));
2051
- this.tooltipTime = this.state.isRtl
2052
- ? (1 - clampedPercent) * this.state.duration
2053
- : clampedPercent * this.state.duration;
2054
- // Position tooltip at pointer X relative to track, clamped to track edges
2055
- this.tooltipX = Math.min(rect.width, Math.max(0, event.clientX - rect.left));
2056
- this.tooltipVisible = true;
2057
- // Request snapshot frame for seekbar preview thumbnail
2058
- if (this.snapshotTake !== null) {
2059
- this.snapshotTake(this.tooltipTime);
2246
+ template() {
2247
+ if (this.state.adPlaying) {
2248
+ return b ``;
2060
2249
  }
2061
- // Preview video element is set via 'snapshot-handler-ready' event — no creation needed here
2062
- this.render();
2250
+ const offset = this.config.seekOffset || 15;
2251
+ const isModern = this.config.skin === 'modern';
2252
+ return b `
2253
+ <button
2254
+ class="eb-button eb-middle-bar__seek-btn"
2255
+ aria-label="Forward ${offset} seconds"
2256
+ @click="${() => this.handleClick()}"
2257
+ >
2258
+ ${isModern
2259
+ ? icon('forward', 'eb-seek-circle')
2260
+ : b `<svg class="eb-seek-circle" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
2261
+ <path d="M12 5V1l5 5-5 5V7c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6h2c0 4.42-3.58 8-8 8s-8-3.58-8-8 3.58-8 8-8z" fill="currentColor"/>
2262
+ </svg>`}
2263
+ <span class="eb-seek-label">${offset}</span>
2264
+ </button>
2265
+ `;
2063
2266
  }
2064
- // ---- Chapter helpers ----
2065
- findActiveChapter(chapters) {
2066
- const currentTime = this.state.currentTime;
2067
- for (const chapter of chapters) {
2068
- if (currentTime >= chapter.startTime && currentTime <= chapter.endTime) {
2069
- return chapter;
2070
- }
2071
- }
2072
- return null;
2267
+ handleClick() {
2268
+ const offset = this.config.seekOffset || 15;
2269
+ const seekTime = Math.min(this.state.currentTime + offset, this.state.duration);
2270
+ this.bus.emit('seek', { time: seekTime });
2073
2271
  }
2074
- findSkippableChapter(chapter) {
2075
- const skippable = this.config.skippableChapters;
2076
- for (const skippableEntry of skippable) {
2077
- if (skippableEntry.chapter === chapter.title) {
2078
- return skippableEntry;
2079
- }
2272
+ }
2273
+
2274
+ /**
2275
+ * ShareButton opens the socials overlay.
2276
+ * Renders hidden when config.socials is false.
2277
+ */
2278
+ class ShareButton extends BaseComponent {
2279
+ onConnect() {
2280
+ this.render();
2281
+ }
2282
+ template() {
2283
+ if (this.config.socials === false) {
2284
+ return b `<span hidden></span>`;
2080
2285
  }
2081
- return null;
2286
+ return b `
2287
+ <button
2288
+ class="eb-button eb-top-bar__btn-share"
2289
+ @click="${() => { this.state.socialsOpen = !this.state.socialsOpen; }}"
2290
+ aria-label="Share"
2291
+ >${icon('share')}</button>
2292
+ `;
2082
2293
  }
2083
- // ---- EPG helpers ----
2084
- renderEpgSegments(programs) {
2085
- if (programs.length === 0)
2086
- return [];
2087
- // Compute the time window from first program start to last program end
2088
- const windowStart = programs[0].start;
2089
- const windowEnd = programs[programs.length - 1].end;
2090
- const windowDuration = windowEnd - windowStart;
2091
- if (windowDuration <= 0)
2092
- return [];
2093
- const now = Date.now();
2094
- return programs.map((program) => {
2095
- const leftPercent = ((program.start - windowStart) / windowDuration) * 100;
2096
- const widthPercent = ((program.end - program.start) / windowDuration) * 100;
2097
- const isCurrent = now >= program.start && now < program.end;
2098
- return b `
2099
- <div
2100
- class="eb-epg-segment${isCurrent ? ' eb-epg-current' : ''}"
2101
- style="left: ${leftPercent.toFixed(2)}%; width: ${widthPercent.toFixed(2)}%"
2102
- title="${program.title}"
2103
- ></div>
2104
- `;
2105
- });
2294
+ }
2295
+
2296
+ /**
2297
+ * InfoButton opens the info/about overlay.
2298
+ */
2299
+ class InfoButton extends BaseComponent {
2300
+ onConnect() {
2301
+ this.render();
2106
2302
  }
2107
- // ---- Template ----
2108
2303
  template() {
2109
- const { currentTime, duration, bufferedEnd, adPlaying, chapters, epgPrograms } = this.state;
2110
- const isSeekbarHidden = !this.config.seekbar;
2111
- const isDisabled = adPlaying;
2112
- // Progress percentage — use dragValue during drag to prevent live-update jitter
2113
- const progressPercent = duration > 0
2114
- ? (this.isDragging ? this.dragValue : currentTime) / duration * 100
2115
- : 0;
2116
- // Buffered percentage
2117
- const bufferedPercent = duration > 0 ? bufferedEnd / duration * 100 : 0;
2118
- // Chapter markers
2119
- const activeChapter = this.findActiveChapter(chapters);
2120
- const skippableEntry = activeChapter !== null ? this.findSkippableChapter(activeChapter) : null;
2121
- const chapterMarkers = chapters.map((chapter) => {
2122
- const leftPercent = duration > 0 ? (chapter.startTime / duration) * 100 : 0;
2123
- const isActive = chapter === activeChapter;
2124
- return b `
2125
- <div
2126
- class="eb-chapter-marker${isActive ? ' eb-chapter-active' : ''}"
2127
- style="left: ${leftPercent.toFixed(2)}%"
2128
- title="${chapter.title}"
2129
- ></div>
2130
- `;
2131
- });
2132
- // Skip button (only for active skippable chapter)
2133
- const skipButton = activeChapter !== null && skippableEntry !== null
2134
- ? b `
2135
- <button
2136
- class="eb-chapter-skip"
2137
- @click="${() => this.bus.emit('seek', { time: activeChapter.endTime })}"
2138
- >${skippableEntry.message}</button>
2139
- `
2140
- : b ``;
2141
- // EPG segments
2142
- const epgSegments = this.renderEpgSegments(epgPrograms);
2143
- // Tooltip — for live streams, show wall-clock time at hovered position
2144
- const tooltipTimeText = this.state.isLive && Number.isFinite(duration)
2145
- ? formatWallClock(Date.now() - (duration - this.tooltipTime) * 1000)
2146
- : formatDuration(this.tooltipTime);
2147
- const tooltip = b `
2148
- <div
2149
- class="eb-seekbar-tooltip"
2150
- style="left: ${this.tooltipX}px"
2151
- ?hidden="${!this.tooltipVisible}"
2152
- >
2153
- ${tooltipTimeText}
2154
- ${this.previewVideoEl !== null ? this.previewVideoEl : b ``}
2155
- </div>
2156
- `;
2157
2304
  return b `
2158
- <div
2159
- class="eb-seekbar${isDisabled ? ' eb-seekbar-disabled' : ''}"
2160
- ?hidden="${isSeekbarHidden}"
2161
- >
2162
- <div
2163
- class="eb-seekbar-track"
2164
- @pointerdown="${(event) => this.handlePointerDown(event)}"
2165
- @pointermove="${(event) => this.handlePointerMove(event)}"
2166
- @pointerup="${(event) => this.handlePointerUp(event)}"
2167
- @pointerleave="${() => this.handlePointerLeave()}"
2168
- >
2169
- <div class="eb-seekbar-buffered" style="width: ${bufferedPercent.toFixed(2)}%"></div>
2170
- <div class="eb-seekbar-progress" style="width: ${progressPercent.toFixed(2)}%">
2171
- <div class="eb-seekbar-thumb"></div>
2172
- </div>
2173
- ${chapterMarkers}
2174
- ${epgSegments}
2175
- ${tooltip}
2176
- </div>
2177
- ${skipButton}
2178
- </div>
2305
+ <button
2306
+ class="eb-button eb-top-bar__btn-info"
2307
+ @click="${() => { this.state.infoOpen = !this.state.infoOpen; }}"
2308
+ aria-label="Info"
2309
+ >${icon('info')}</button>
2179
2310
  `;
2180
2311
  }
2181
2312
  }
2182
2313
 
2314
+ /**
2315
+ * Maps ComponentId strings to factory functions that create component instances.
2316
+ * Used by SkinRoot.connectChildComponents() to dynamically mount components
2317
+ * based on the layout config.
2318
+ */
2319
+ const COMPONENT_REGISTRY = {
2320
+ 'play-pause': () => new PlayPauseButton(),
2321
+ 'volume': () => new VolumeControl(),
2322
+ 'seekbar': () => new Seekbar(),
2323
+ 'time': () => new TimeDisplay(),
2324
+ 'live-sync': () => new LiveSyncButton(),
2325
+ 'settings': () => new SettingsPanel(),
2326
+ 'pip': () => new PipButton(),
2327
+ 'cast': () => new CastButton(),
2328
+ 'fullscreen': () => new FullscreenButton(),
2329
+ 'rewind': () => new RewindButton(),
2330
+ 'forward': () => new ForwardButton(),
2331
+ 'share': () => new ShareButton(),
2332
+ 'info': () => new InfoButton()
2333
+ };
2334
+ /**
2335
+ * Returns the CSS selector for a component's slot element.
2336
+ * Seekbar uses a special class for flex:1 behavior.
2337
+ */
2338
+ function slotSelector(componentId) {
2339
+ if (componentId === 'seekbar')
2340
+ return '.eb-bottom-bar__seekbar-zone';
2341
+ return `.eb-slot-${componentId}`;
2342
+ }
2343
+
2183
2344
  /**
2184
2345
  * Buffering/loading spinner overlay.
2185
2346
  *
@@ -2637,6 +2798,8 @@ var EBPlayerBundle = (function (exports) {
2637
2798
  * Connects all child components into their respective DOM zones.
2638
2799
  * Only called when config.noUi is false (UI mode).
2639
2800
  * Child components are tracked in this.childComponents for cleanup on disconnect().
2801
+ *
2802
+ * Component placement is driven by config.layout (falls back to DEFAULT_LAYOUT).
2640
2803
  */
2641
2804
  connectChildComponents() {
2642
2805
  const config = this.config;
@@ -2647,6 +2810,7 @@ var EBPlayerBundle = (function (exports) {
2647
2810
  return;
2648
2811
  const state = this.state;
2649
2812
  const bus = this.bus;
2813
+ const layout = resolveLayout(config);
2650
2814
  const i18n = this.i18n ?? undefined;
2651
2815
  /**
2652
2816
  * Helper to connect a component into a selector-targeted container.
@@ -2660,47 +2824,47 @@ var EBPlayerBundle = (function (exports) {
2660
2824
  this.childComponents.push(component);
2661
2825
  return true;
2662
2826
  };
2827
+ /** Mount all components from a BarLayout into a parent element. */
2828
+ const mountBarComponents = (parentEl, barLayout) => {
2829
+ const allIds = [
2830
+ ...(barLayout.left ?? []),
2831
+ ...(barLayout.center ?? []),
2832
+ ...(barLayout.right ?? [])
2833
+ ];
2834
+ for (const componentId of allIds) {
2835
+ const factory = COMPONENT_REGISTRY[componentId];
2836
+ if (factory === undefined)
2837
+ continue;
2838
+ const selector = slotSelector(componentId);
2839
+ const target = parentEl.querySelector(selector);
2840
+ if (target === null)
2841
+ continue;
2842
+ const component = factory();
2843
+ component.connect(target, state, bus, config, i18n);
2844
+ this.childComponents.push(component);
2845
+ }
2846
+ };
2663
2847
  // Top bar
2664
2848
  mount(new TopBar(), '.eb-top-bar-zone');
2849
+ const topBarEl = inner.querySelector('.eb-top-bar-zone');
2850
+ if (topBarEl !== null && layout.topBar !== undefined) {
2851
+ mountBarComponents(topBarEl, layout.topBar);
2852
+ }
2665
2853
  // Bottom bar scaffold (creates slot divs)
2666
2854
  const bottomBarMounted = mount(new BottomBar(), '.eb-bottom-bar-zone');
2667
- // Controls inside BottomBar slots only after BottomBar is rendered
2668
- if (bottomBarMounted) {
2855
+ if (bottomBarMounted && layout.bottomBar !== undefined) {
2669
2856
  const bottomBarEl = inner.querySelector('.eb-bottom-bar-zone');
2670
2857
  if (bottomBarEl !== null) {
2671
- const mountInBottom = (component, selector) => {
2672
- const target = bottomBarEl.querySelector(selector);
2673
- if (target === null)
2674
- return;
2675
- component.connect(target, state, bus, config, i18n);
2676
- this.childComponents.push(component);
2677
- };
2678
- mountInBottom(new Seekbar(), '.eb-bottom-bar__seekbar-zone');
2679
- mountInBottom(new PlayPauseButton(), '.eb-slot-play-pause');
2680
- mountInBottom(new VolumeControl(), '.eb-slot-volume');
2681
- mountInBottom(new TimeDisplay(), '.eb-slot-time');
2682
- mountInBottom(new LiveSyncButton(), '.eb-slot-live-sync');
2683
- mountInBottom(new SettingsPanel(), '.eb-slot-settings');
2684
- mountInBottom(new PipButton(), '.eb-slot-pip');
2685
- mountInBottom(new CastButton(), '.eb-slot-cast');
2686
- mountInBottom(new FullscreenButton(), '.eb-slot-fullscreen');
2858
+ mountBarComponents(bottomBarEl, layout.bottomBar);
2687
2859
  }
2688
2860
  }
2689
2861
  // Middle bar (conditional on config.middlebar)
2690
2862
  if (config.middlebar) {
2691
2863
  const middleBarMounted = mount(new MiddleBar(), '.eb-middle-bar-zone');
2692
- if (middleBarMounted) {
2864
+ if (middleBarMounted && layout.middleBar !== undefined) {
2693
2865
  const middleBarEl = inner.querySelector('.eb-middle-bar-zone');
2694
2866
  if (middleBarEl !== null) {
2695
- const mountInMiddle = (component, selector) => {
2696
- const target = middleBarEl.querySelector(selector);
2697
- if (target === null)
2698
- return;
2699
- component.connect(target, state, bus, config, i18n);
2700
- this.childComponents.push(component);
2701
- };
2702
- mountInMiddle(new RewindButton(), '.eb-slot-rewind');
2703
- mountInMiddle(new ForwardButton(), '.eb-slot-forward');
2867
+ mountBarComponents(middleBarEl, layout.middleBar);
2704
2868
  }
2705
2869
  }
2706
2870
  }
@@ -3769,9 +3933,10 @@ var EBPlayerBundle = (function (exports) {
3769
3933
  this.stallAttempts = 0;
3770
3934
  }
3771
3935
  tick() {
3772
- // Skip check when paused or casting
3773
- if (this.video.paused || this.isCasting()) {
3936
+ // Skip check when paused, casting, seeking, or buffering after seek
3937
+ if (this.video.paused || this.video.seeking || this.video.readyState < 3 || this.isCasting()) {
3774
3938
  this.lastTime = null;
3939
+ this.stallAttempts = 0;
3775
3940
  return;
3776
3941
  }
3777
3942
  const currentTime = this.video.currentTime;
@@ -3805,13 +3970,16 @@ var EBPlayerBundle = (function (exports) {
3805
3970
  */
3806
3971
  class BaseEngine extends EngineStateSync {
3807
3972
  constructor() {
3808
- super(...arguments);
3973
+ super();
3809
3974
  this.video = null;
3810
3975
  this.state = null;
3811
3976
  this.signal = null;
3812
3977
  this.bus = null;
3813
3978
  this.config = null;
3814
3979
  this.watchdog = null;
3980
+ this.driverReady = new Promise((resolve) => {
3981
+ this.resolveDriverReady = resolve;
3982
+ });
3815
3983
  }
3816
3984
  // -------------------------------------------------------------------------
3817
3985
  // EngineStateSync contract
@@ -3875,9 +4043,13 @@ var EBPlayerBundle = (function (exports) {
3875
4043
  // Video element event binding
3876
4044
  // -------------------------------------------------------------------------
3877
4045
  bindVideoEvents(video, state, signal) {
3878
- // Playback timing
4046
+ // Playback timing + live sync detection
4047
+ const syncMargin = this.config?.syncLiveMargin ?? 5 * 60;
3879
4048
  video.addEventListener('timeupdate', () => {
3880
4049
  state.currentTime = Math.round(video.currentTime);
4050
+ if (state.isLive) {
4051
+ state.isSyncWithLive = video.currentTime + syncMargin > video.duration;
4052
+ }
3881
4053
  }, { signal });
3882
4054
  video.addEventListener('durationchange', () => {
3883
4055
  state.duration = Math.ceil(video.duration);
@@ -3911,6 +4083,15 @@ var EBPlayerBundle = (function (exports) {
3911
4083
  state.playbackState = 'ended';
3912
4084
  }, { signal });
3913
4085
  }
4086
+ /**
4087
+ * Seek to a specific time. Default implementation sets video.currentTime.
4088
+ * HLS engine overrides this for live streams to reset hls.js stream controller.
4089
+ */
4090
+ seek(time) {
4091
+ if (this.video === null)
4092
+ return;
4093
+ this.video.currentTime = time;
4094
+ }
3914
4095
  }
3915
4096
 
3916
4097
  /**
@@ -4616,7 +4797,7 @@ var EBPlayerBundle = (function (exports) {
4616
4797
  if (data.type === 'mediaError')
4617
4798
  driver.recoverMediaError();
4618
4799
  else
4619
- driver.startLoad();
4800
+ driver.startLoad(driver.media?.currentTime ?? -1);
4620
4801
  }, 1000);
4621
4802
  return;
4622
4803
  }
@@ -4640,7 +4821,7 @@ var EBPlayerBundle = (function (exports) {
4640
4821
  if (data.type === 'mediaError')
4641
4822
  driver.recoverMediaError();
4642
4823
  else
4643
- driver.startLoad();
4824
+ driver.startLoad(driver.media?.currentTime ?? -1);
4644
4825
  }
4645
4826
  else {
4646
4827
  console.info('HLS Retry: Stream is playing, no action needed', event, data);
@@ -4723,6 +4904,7 @@ var EBPlayerBundle = (function (exports) {
4723
4904
  this.tokenManager = null;
4724
4905
  // Holds state reference for named driver event handlers
4725
4906
  this.eventState = null;
4907
+ this.liveSyncDisabled = false;
4726
4908
  }
4727
4909
  getDriver() {
4728
4910
  return this.driver;
@@ -4795,14 +4977,26 @@ var EBPlayerBundle = (function (exports) {
4795
4977
  return;
4796
4978
  this.video.playbackRate = rate;
4797
4979
  }
4798
- seekToLive() {
4799
- if (this.driver === null || this.video === null)
4980
+ seek(time) {
4981
+ if (this.video === null)
4800
4982
  return;
4801
- // liveSyncPosition is only available at runtime for live streams (not on the interface)
4802
- const livePos = this.driver['liveSyncPosition'];
4803
- if (livePos !== undefined && Number.isFinite(livePos)) {
4804
- this.video.currentTime = livePos;
4983
+ // Disable hls.js live sync once on first seek, so it never
4984
+ // auto-seeks back to the live edge on DVR/timeshift streams.
4985
+ if (!this.liveSyncDisabled && this.driver !== null && this.state?.isLive) {
4986
+ const cfg = this.driver.config;
4987
+ cfg.liveSyncDurationCount = 0;
4988
+ cfg.liveMaxLatencyDurationCount = Infinity;
4989
+ this.liveSyncDisabled = true;
4805
4990
  }
4991
+ this.video.currentTime = time;
4992
+ }
4993
+ seekToLive() {
4994
+ if (this.video === null)
4995
+ return;
4996
+ const dur = this.video.duration;
4997
+ if (!Number.isFinite(dur))
4998
+ return;
4999
+ this.video.currentTime = dur - 30;
4806
5000
  }
4807
5001
  // -------------------------------------------------------------------------
4808
5002
  // Initialisation
@@ -4828,7 +5022,7 @@ var EBPlayerBundle = (function (exports) {
4828
5022
  token: config.token,
4829
5023
  tokenType: config.tokenType,
4830
5024
  srcInTokenRequest: config.srcInTokenRequest,
4831
- extraParamsCallback: config.extraParamsCallback,
5025
+ extraParamsCallback: (config.engineSettings.extraParamsCallback ?? config.extraParamsCallback),
4832
5026
  onCDNTokenError: config.engineSettings.onCDNTokenError
4833
5027
  });
4834
5028
  // Fetch initial token
@@ -4920,6 +5114,7 @@ var EBPlayerBundle = (function (exports) {
4920
5114
  // Create the driver (NEVER stored in state)
4921
5115
  const driver = new Hls(driverConfig);
4922
5116
  this.driver = driver;
5117
+ this.resolveDriverReady();
4923
5118
  // Pitfall 4: apply discontinuity workaround BEFORE attachMedia/loadSource
4924
5119
  applyDiscontinuityWorkaround(driver, Hls.Events);
4925
5120
  // Wire retry handler
@@ -4987,6 +5182,9 @@ var EBPlayerBundle = (function (exports) {
4987
5182
  if (levelData?.details?.live !== undefined) {
4988
5183
  state.isLive = levelData.details.live;
4989
5184
  }
5185
+ else if (this.config?.isLive === true) {
5186
+ state.isLive = true;
5187
+ }
4990
5188
  }
4991
5189
  _onLevelSwitched(_event, data) {
4992
5190
  const state = this.eventState;
@@ -5379,6 +5577,7 @@ var EBPlayerBundle = (function (exports) {
5379
5577
  throw new Error('DashEngine: dash.js MediaPlayer could not be created');
5380
5578
  }
5381
5579
  this.driver = player;
5580
+ this.resolveDriverReady();
5382
5581
  player.initialize(video, config.src ?? '', config.autoplay ?? false);
5383
5582
  // Apply retry settings if requested
5384
5583
  if (config.retry) {
@@ -5570,6 +5769,10 @@ var EBPlayerBundle = (function (exports) {
5570
5769
  /**
5571
5770
  * Initialize the snapshot Hls instance using the CDN-loaded Hls constructor.
5572
5771
  * Creates a separate Hls instance with minimal buffering for thumbnail generation.
5772
+ *
5773
+ * Returns a promise that resolves once the manifest is loaded and the snapshot
5774
+ * handler is ready to serve seek thumbnails — matching the legacy Vue 2 behaviour
5775
+ * where the handler only resolved after MANIFEST_LOADED.
5573
5776
  */
5574
5777
  init(HlsConstructor) {
5575
5778
  // Create an off-screen video element for the snapshot player
@@ -5620,6 +5823,16 @@ var EBPlayerBundle = (function (exports) {
5620
5823
  if (this.config.src) {
5621
5824
  driver.loadSource(this.config.src);
5622
5825
  }
5826
+ // Wait for the manifest to load before resolving — the snapshot handler is only
5827
+ // useful once hls.js knows about the available levels and can seek to segments.
5828
+ // This matches the legacy Vue 2 code which resolved after MANIFEST_LOADED and
5829
+ // forced loadLevel = 0 to pin the lowest quality for thumbnails.
5830
+ return new Promise((resolve) => {
5831
+ driver.once(HlsConstructor.Events.MANIFEST_LOADED, () => {
5832
+ driver['loadLevel'] = 0;
5833
+ resolve();
5834
+ });
5835
+ });
5623
5836
  }
5624
5837
  /**
5625
5838
  * Seek the snapshot player to the specified time.
@@ -5890,12 +6103,15 @@ var EBPlayerBundle = (function (exports) {
5890
6103
  if (mergedConfig.lib && mergedConfig.manager && video !== null) {
5891
6104
  const isDash = src.includes('.mpd') && mergedConfig.dashjs !== false;
5892
6105
  const p2pManager = new P2PManager();
5893
- p2pManager.integrate({
5894
- video,
5895
- driver: engine.getDriver(),
5896
- type: isDash ? 'dash' : 'hls',
5897
- config: mergedConfig,
5898
- signal: controller.signal
6106
+ // Wait for the engine driver to be created before integrating P2P
6107
+ engine.driverReady.then(() => {
6108
+ return p2pManager.integrate({
6109
+ video,
6110
+ driver: engine.getDriver(),
6111
+ type: isDash ? 'dash' : 'hls',
6112
+ config: mergedConfig,
6113
+ signal: controller.signal
6114
+ });
5899
6115
  }).catch((error) => {
5900
6116
  console.error('EBPlayer: P2PManager integrate failed:', error);
5901
6117
  });
@@ -5908,8 +6124,8 @@ var EBPlayerBundle = (function (exports) {
5908
6124
  activeSnapshotDestroy = null;
5909
6125
  }
5910
6126
  const isDash = src.includes('.mpd') && mergedConfig.dashjs !== false;
5911
- // Defer init until CDN script is loaded (engine's onAttach loads CDN script async)
5912
- setTimeout(() => {
6127
+ // Wait for engine driver to be ready (CDN script loaded) before initializing snapshot
6128
+ engine.driverReady.then(() => {
5913
6129
  if (isDash) {
5914
6130
  const win = window;
5915
6131
  if (win.dashjs) {
@@ -5928,15 +6144,25 @@ var EBPlayerBundle = (function (exports) {
5928
6144
  const win = window;
5929
6145
  if (win.Hls) {
5930
6146
  const handler = new HlsSnapshotHandler({ src, engineSettings: mergedConfig.engineSettings }, null);
5931
- handler.init(win.Hls);
5932
- activeSnapshotDestroy = () => handler.destroy();
5933
- const snapshotVideo = handler.getVideo();
5934
- if (snapshotVideo !== null) {
5935
- controller.bus.emit('snapshot-handler-ready', { take: (time) => handler.take(time), video: snapshotVideo });
5936
- }
6147
+ handler.init(win.Hls)
6148
+ .then(() => {
6149
+ activeSnapshotDestroy = () => handler.destroy();
6150
+ const snapshotVideo = handler.getVideo();
6151
+ if (snapshotVideo !== null) {
6152
+ controller.bus.emit('snapshot-handler-ready', { take: (time) => handler.take(time), video: snapshotVideo });
6153
+ }
6154
+ })
6155
+ .catch((error) => {
6156
+ console.warn('EBPlayer: HlsSnapshotHandler init failed:', error);
6157
+ });
6158
+ }
6159
+ else {
6160
+ console.warn('EBPlayer: window.Hls not available after driverReady — snapshot preview disabled');
5937
6161
  }
5938
6162
  }
5939
- }, 500);
6163
+ }).catch((error) => {
6164
+ console.warn('EBPlayer: Snapshot handler init failed:', error);
6165
+ });
5940
6166
  }
5941
6167
  },
5942
6168
  close() {