eb-player 2.0.1 → 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.
- package/dist/build/eb-player.css +661 -5
- package/dist/build/ebplayer.bundle.js +62 -23
- package/dist/build/ebplayer.bundle.js.map +1 -1
- package/dist/build/theme-forja.css +1 -1
- package/dist/build/theme-lequipe.css +655 -0
- package/dist/build/theme-modern.css +1 -1
- package/dist/build/theme-v2.css +1 -1
- package/dist/build/types/core/config.d.ts +14 -2
- package/dist/build/types/core/config.d.ts.map +1 -1
- package/dist/build/types/core/index.d.ts +1 -1
- package/dist/build/types/core/index.d.ts.map +1 -1
- package/dist/build/types/core/lifecycle.d.ts.map +1 -1
- package/dist/build/types/engines/hls.d.ts +1 -0
- package/dist/build/types/engines/hls.d.ts.map +1 -1
- package/dist/build/types/engines/snapshot/hls.d.ts +1 -1
- package/dist/build/types/engines/snapshot/hls.d.ts.map +1 -1
- package/dist/build/types/integrations/p2p-manager.d.ts.map +1 -1
- package/dist/dev/default.js +734 -508
- package/dist/dev/default.js.map +1 -1
- package/dist/dev/easybroadcast.js +89 -47
- package/dist/dev/easybroadcast.js.map +1 -1
- package/dist/dev/equipe.js +6683 -0
- package/dist/dev/equipe.js.map +1 -0
- package/dist/eb-player.css +661 -5
- package/dist/players/easybroadcast/easybroadcast.js +397 -0
- package/dist/players/easybroadcast/index.html +1 -0
- package/dist/players/equipe/equipe.js +397 -0
- package/dist/players/equipe/index.html +1 -0
- package/dist/players/forja/forja.js +198 -111
- package/dist/players/forja/index.html +1 -1
- package/dist/theme-forja.css +1 -1
- package/dist/theme-lequipe.css +655 -0
- package/dist/theme-modern.css +1 -1
- package/dist/theme-v2.css +1 -1
- package/package.json +8 -73
package/dist/dev/default.js
CHANGED
|
@@ -28,22 +28,25 @@ var EBPlayerBundle = (function (exports) {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
var css_248z$
|
|
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 *
|
|
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
|
-
|
|
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"><
|
|
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
|
-
*
|
|
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
|
-
*
|
|
1004
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
1277
|
+
* Seekbar component.
|
|
1222
1278
|
*
|
|
1223
|
-
*
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
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
|
-
*
|
|
1228
|
-
* to avoid excessive re-renders during playback.
|
|
1288
|
+
* rAF batching prevents excessive re-renders during high-frequency currentTime updates.
|
|
1229
1289
|
*/
|
|
1230
|
-
class
|
|
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
|
-
//
|
|
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('
|
|
1241
|
-
this.state.on('
|
|
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
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
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
|
-
|
|
1268
|
-
|
|
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
|
-
|
|
1362
|
+
// Always update tooltip on pointermove over the track
|
|
1363
|
+
this.updateTooltip(event);
|
|
1271
1364
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
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
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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="
|
|
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
|
|
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
|
-
|
|
2023
|
-
if (this.
|
|
2024
|
-
|
|
2025
|
-
this.dragValue = this.eventToTime(event, trackEl);
|
|
2026
|
-
this.scheduleRender();
|
|
2206
|
+
template() {
|
|
2207
|
+
if (this.state.adPlaying) {
|
|
2208
|
+
return b ``;
|
|
2027
2209
|
}
|
|
2028
|
-
|
|
2029
|
-
this.
|
|
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
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
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
|
-
|
|
2041
|
-
|
|
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
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
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
|
-
|
|
2062
|
-
this.
|
|
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
|
-
|
|
2065
|
-
|
|
2066
|
-
const
|
|
2067
|
-
|
|
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
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
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
|
|
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
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
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
|
-
<
|
|
2159
|
-
class="eb-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
4799
|
-
if (this.
|
|
4980
|
+
seek(time) {
|
|
4981
|
+
if (this.video === null)
|
|
4800
4982
|
return;
|
|
4801
|
-
//
|
|
4802
|
-
|
|
4803
|
-
if (
|
|
4804
|
-
this.
|
|
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
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
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
|
-
//
|
|
5912
|
-
|
|
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
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
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
|
-
}
|
|
6163
|
+
}).catch((error) => {
|
|
6164
|
+
console.warn('EBPlayer: Snapshot handler init failed:', error);
|
|
6165
|
+
});
|
|
5940
6166
|
}
|
|
5941
6167
|
},
|
|
5942
6168
|
close() {
|