eb-player 2.0.4 → 2.0.7

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.
@@ -2049,7 +2049,12 @@
2049
2049
  user-select: none;
2050
2050
  }
2051
2051
 
2052
- /* Video element fills the container */
2052
+ /* Video element fills the container.
2053
+ filter: brightness(1) is visually identical to no filter but forces Chrome to
2054
+ composite the video through the GPU filter pipeline instead of using a hardware
2055
+ overlay plane. Without this, DRM-protected video (Widevine) renders on a separate
2056
+ hardware overlay that sits above ALL HTML layers — making seekbar snapshot preview
2057
+ and other overlay elements invisible behind the main video. */
2053
2058
  .eb-player video.eb-video {
2054
2059
  position: absolute;
2055
2060
  top: 0;
@@ -2059,6 +2064,7 @@
2059
2064
  display: block;
2060
2065
  object-fit: contain;
2061
2066
  z-index: 1;
2067
+ filter: brightness(1);
2062
2068
  }
2063
2069
 
2064
2070
  /* ============================================================
@@ -2362,23 +2368,29 @@
2362
2368
  padding: 4px 0;
2363
2369
  }
2364
2370
 
2365
- .eb-settings-submenu .eb-settings-menu {
2371
+ .eb-settings-submenu {
2366
2372
  max-height: 200px;
2367
2373
  overflow-y: auto;
2368
2374
  }
2369
2375
 
2370
- .eb-settings-submenu .eb-settings-menu::-webkit-scrollbar {
2376
+ .eb-settings-submenu::-webkit-scrollbar {
2371
2377
  width: 4px;
2372
2378
  }
2373
2379
 
2374
- .eb-settings-submenu .eb-settings-menu::-webkit-scrollbar-thumb {
2380
+ .eb-settings-submenu::-webkit-scrollbar-thumb {
2375
2381
  background: rgba(255, 255, 255, 0.3);
2376
2382
  border-radius: 2px;
2377
2383
  }
2378
2384
 
2385
+ .eb-settings-header {
2386
+ display: flex;
2387
+ align-items: center;
2388
+ gap: 4px;
2389
+ padding: 5px 12px;
2390
+ }
2391
+
2379
2392
  .eb-settings-category,
2380
- .eb-settings-item,
2381
- .eb-settings-back {
2393
+ .eb-settings-item {
2382
2394
  display: flex;
2383
2395
  align-items: center;
2384
2396
  justify-content: space-between;
@@ -2392,9 +2404,18 @@
2392
2404
  text-align: left;
2393
2405
  }
2394
2406
 
2407
+ .eb-settings-back {
2408
+ display: flex;
2409
+ align-items: center;
2410
+ border: none;
2411
+ background: none;
2412
+ color: #fff;
2413
+ cursor: pointer;
2414
+ padding: 0;
2415
+ }
2416
+
2395
2417
  .eb-settings-category:hover,
2396
- .eb-settings-item:hover,
2397
- .eb-settings-back:hover {
2418
+ .eb-settings-item:hover {
2398
2419
  background: rgba(255, 255, 255, 0.1);
2399
2420
  }
2400
2421
 
@@ -2477,13 +2498,27 @@
2477
2498
  white-space: nowrap;
2478
2499
  pointer-events: none;
2479
2500
  text-align: center;
2501
+ z-index: 50;
2502
+ overflow: hidden;
2480
2503
  }
2481
2504
 
2482
2505
  .eb-seekbar-preview {
2506
+ position: static !important;
2483
2507
  width: 120px;
2484
- height: auto;
2485
- display: block;
2486
- margin-bottom: 4px;
2508
+ height: 68px;
2509
+ min-width: 0 !important;
2510
+ min-height: 0 !important;
2511
+ max-width: none !important;
2512
+ max-height: none !important;
2513
+ display: block !important;
2514
+ object-fit: contain;
2515
+ background: #000;
2516
+ margin: 0 !important;
2517
+ padding: 0 !important;
2518
+ border: none !important;
2519
+ inset: auto !important;
2520
+ transform: none !important;
2521
+ z-index: auto !important;
2487
2522
  }
2488
2523
 
2489
2524
  .eb-chapter-marker {
@@ -2843,7 +2878,7 @@
2843
2878
  Responsive: ensure container fills parent
2844
2879
  ============================================================ */
2845
2880
  .eb-player,
2846
- .eb-player video {
2881
+ .eb-player video.eb-video {
2847
2882
  max-width: 100%;
2848
2883
  max-height: 100%;
2849
2884
  }
@@ -4,7 +4,7 @@
4
4
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.EBPlayer = {}));
5
5
  })(this, (function (exports) { 'use strict';
6
6
 
7
- var __EB_PLAYER_VERSION__ = "2.0.4";
7
+ var __EB_PLAYER_VERSION__ = "2.0.7";
8
8
 
9
9
  /**
10
10
  * Finite State Machine for player playback state transitions.
@@ -1312,6 +1312,8 @@
1312
1312
  this.tooltipX = 0;
1313
1313
  this.previewVideoEl = null;
1314
1314
  this.snapshotTake = null;
1315
+ this.trackEl = null;
1316
+ this.boundDocPointerMove = null;
1315
1317
  }
1316
1318
  onConnect() {
1317
1319
  // Subscribe to snapshot handler readiness (emitted by eb-player.ts)
@@ -1332,6 +1334,10 @@
1332
1334
  this.state.on('epgPrograms', () => this.scheduleRender(), { signal: this.signal });
1333
1335
  this.render();
1334
1336
  }
1337
+ disconnect() {
1338
+ this.stopDocumentTracking();
1339
+ super.disconnect();
1340
+ }
1335
1341
  // ---- rAF batching ----
1336
1342
  scheduleRender() {
1337
1343
  if (this.rafPending)
@@ -1354,11 +1360,47 @@
1354
1360
  const clamped = Math.min(1, Math.max(0, percent));
1355
1361
  return clamped * this.state.duration;
1356
1362
  }
1363
+ // ---- Document-level pointer tracking ----
1364
+ // pointerleave/mouseleave on the track are unreliable when the tooltip (a DOM child)
1365
+ // is absolutely positioned above the track — some browsers consider the pointer still
1366
+ // "inside" the track's subtree. Instead, we track pointer position at the document level
1367
+ // and hide the tooltip when the pointer moves outside the track's bounding rect.
1368
+ startDocumentTracking() {
1369
+ if (this.boundDocPointerMove)
1370
+ return;
1371
+ this.boundDocPointerMove = (event) => this.onDocPointerMove(event);
1372
+ document.addEventListener('pointermove', this.boundDocPointerMove);
1373
+ document.addEventListener('mouseleave', this.boundDocPointerMove);
1374
+ }
1375
+ stopDocumentTracking() {
1376
+ if (!this.boundDocPointerMove)
1377
+ return;
1378
+ document.removeEventListener('pointermove', this.boundDocPointerMove);
1379
+ document.removeEventListener('mouseleave', this.boundDocPointerMove);
1380
+ this.boundDocPointerMove = null;
1381
+ }
1382
+ onDocPointerMove(event) {
1383
+ if (this.isDragging)
1384
+ return;
1385
+ if (!this.trackEl)
1386
+ return;
1387
+ const rect = this.trackEl.getBoundingClientRect();
1388
+ const inBounds = event.clientX >= rect.left
1389
+ && event.clientX <= rect.right
1390
+ && event.clientY >= rect.top
1391
+ && event.clientY <= rect.bottom;
1392
+ if (!inBounds) {
1393
+ this.tooltipVisible = false;
1394
+ this.stopDocumentTracking();
1395
+ this.render();
1396
+ }
1397
+ }
1357
1398
  // ---- Drag handlers ----
1358
1399
  handlePointerDown(event) {
1359
1400
  if (this.state.adPlaying)
1360
1401
  return;
1361
1402
  const trackEl = event.currentTarget;
1403
+ this.trackEl = trackEl;
1362
1404
  // setPointerCapture ensures events continue firing even if pointer leaves the element
1363
1405
  if (typeof trackEl.setPointerCapture === 'function') {
1364
1406
  trackEl.setPointerCapture(event.pointerId);
@@ -1389,20 +1431,16 @@
1389
1431
  const seekTime = this.eventToTime(event, trackEl);
1390
1432
  this.isDragging = false;
1391
1433
  this.bus.emit('seek', { time: seekTime });
1392
- // Hide tooltip if pointer is outside the track bounds after drag ends
1393
- const rect = trackEl.getBoundingClientRect();
1394
- if (event.clientX < rect.left || event.clientX > rect.right) {
1395
- this.tooltipVisible = false;
1396
- }
1397
- this.render();
1398
- }
1399
- handlePointerLeave() {
1434
+ // Always hide tooltip on pointer up it reappears naturally via pointermove
1435
+ // if the pointer is still over the track.
1400
1436
  this.tooltipVisible = false;
1437
+ this.stopDocumentTracking();
1401
1438
  this.render();
1402
1439
  }
1403
1440
  // ---- Tooltip ----
1404
1441
  updateTooltip(event) {
1405
1442
  const trackEl = event.currentTarget;
1443
+ this.trackEl = trackEl;
1406
1444
  const rect = trackEl.getBoundingClientRect();
1407
1445
  // Compute hover time (use LTR calculation for tooltip position regardless of RTL)
1408
1446
  const rawPercent = (event.clientX - rect.left) / rect.width;
@@ -1413,6 +1451,8 @@
1413
1451
  // Position tooltip at pointer X relative to track, clamped to track edges
1414
1452
  this.tooltipX = Math.min(rect.width, Math.max(0, event.clientX - rect.left));
1415
1453
  this.tooltipVisible = true;
1454
+ // Start document-level tracking to detect when pointer leaves the track
1455
+ this.startDocumentTracking();
1416
1456
  // Request snapshot frame for seekbar preview thumbnail
1417
1457
  if (this.snapshotTake !== null) {
1418
1458
  this.snapshotTake(this.tooltipTime);
@@ -1505,11 +1545,10 @@
1505
1545
  const tooltip = b `
1506
1546
  <div
1507
1547
  class="eb-seekbar-tooltip"
1508
- style="left: ${this.tooltipX}px"
1509
- ?hidden="${!this.tooltipVisible}"
1548
+ style="left: ${this.tooltipX}px; visibility: ${this.tooltipVisible ? 'visible' : 'hidden'}"
1510
1549
  >
1511
- ${tooltipTimeText}
1512
1550
  ${this.previewVideoEl !== null ? this.previewVideoEl : b ``}
1551
+ ${tooltipTimeText}
1513
1552
  </div>
1514
1553
  `;
1515
1554
  return b `
@@ -1522,7 +1561,6 @@
1522
1561
  @pointerdown="${(event) => this.handlePointerDown(event)}"
1523
1562
  @pointermove="${(event) => this.handlePointerMove(event)}"
1524
1563
  @pointerup="${(event) => this.handlePointerUp(event)}"
1525
- @pointerleave="${() => this.handlePointerLeave()}"
1526
1564
  >
1527
1565
  <div class="eb-seekbar-buffered" style="width: ${bufferedPercent.toFixed(2)}%"></div>
1528
1566
  <div class="eb-seekbar-progress" style="width: ${progressPercent.toFixed(2)}%">
@@ -2220,13 +2258,24 @@
2220
2258
  *
2221
2259
  * - Calls screenfull.toggle() with the closest .eb-player ancestor on click
2222
2260
  * - Subscribes to screenfull 'change' event to update state.isFullscreen
2223
- * - Hidden when screenfull.isEnabled is false (e.g. iOS Safari)
2261
+ * - Falls back to video-element fullscreen on mobile (iOS Safari webkitEnterFullscreen,
2262
+ * Android Chrome video.requestFullscreen) when the Fullscreen API is not available
2263
+ * for arbitrary elements
2224
2264
  * - Re-renders when state.isFullscreen changes
2225
2265
  */
2226
2266
  class FullscreenButton extends BaseComponent {
2227
2267
  constructor() {
2228
2268
  super(...arguments);
2229
2269
  this.changeHandler = null;
2270
+ this.videoEl = null;
2271
+ this.videoFullscreenBeginHandler = null;
2272
+ this.videoFullscreenEndHandler = null;
2273
+ this.useVideoFallback = false;
2274
+ this.handleDocFullscreenChange = () => {
2275
+ const isFullscreen = document.fullscreenElement === this.videoEl;
2276
+ this.state.isFullscreen = isFullscreen;
2277
+ this.render();
2278
+ };
2230
2279
  }
2231
2280
  onConnect() {
2232
2281
  this.state.on('isFullscreen', () => this.render(), { signal: this.signal });
@@ -2236,25 +2285,90 @@
2236
2285
  this.render();
2237
2286
  };
2238
2287
  screenfull$1.on('change', this.changeHandler);
2239
- // Unsubscribe when signal aborts (on disconnect)
2240
2288
  this.signal.addEventListener('abort', () => {
2241
2289
  screenfull$1.off('change', this.changeHandler);
2242
2290
  this.changeHandler = null;
2243
2291
  });
2244
2292
  }
2293
+ else {
2294
+ // Fullscreen API not available for elements — try video-element fallback (mobile)
2295
+ this.initVideoFallback();
2296
+ }
2245
2297
  this.render();
2246
2298
  }
2299
+ /**
2300
+ * On mobile browsers the Fullscreen API may not work on arbitrary elements,
2301
+ * but the <video> element itself supports fullscreen via webkitEnterFullscreen
2302
+ * (iOS Safari) or video.requestFullscreen() (Android Chrome).
2303
+ */
2304
+ initVideoFallback() {
2305
+ const playerRoot = this.el?.closest('.eb-player');
2306
+ if (!playerRoot)
2307
+ return;
2308
+ const video = playerRoot.querySelector('video.eb-video');
2309
+ if (!video)
2310
+ return;
2311
+ // iOS Safari: webkitSupportsFullscreen + webkitEnterFullscreen
2312
+ const hasWebkitFullscreen = typeof video.webkitEnterFullscreen === 'function';
2313
+ // Android Chrome: standard requestFullscreen on the video element
2314
+ const hasRequestFullscreen = typeof video.requestFullscreen === 'function';
2315
+ if (!hasWebkitFullscreen && !hasRequestFullscreen)
2316
+ return;
2317
+ this.videoEl = video;
2318
+ this.useVideoFallback = true;
2319
+ // Track fullscreen state changes on the video element
2320
+ this.videoFullscreenBeginHandler = () => {
2321
+ this.state.isFullscreen = true;
2322
+ this.render();
2323
+ };
2324
+ this.videoFullscreenEndHandler = () => {
2325
+ this.state.isFullscreen = false;
2326
+ this.render();
2327
+ };
2328
+ // iOS Safari fires these events on the video element
2329
+ video.addEventListener('webkitbeginfullscreen', this.videoFullscreenBeginHandler);
2330
+ video.addEventListener('webkitendfullscreen', this.videoFullscreenEndHandler);
2331
+ // Android Chrome fires standard fullscreenchange on the document
2332
+ document.addEventListener('fullscreenchange', this.handleDocFullscreenChange);
2333
+ this.signal.addEventListener('abort', () => {
2334
+ video.removeEventListener('webkitbeginfullscreen', this.videoFullscreenBeginHandler);
2335
+ video.removeEventListener('webkitendfullscreen', this.videoFullscreenEndHandler);
2336
+ document.removeEventListener('fullscreenchange', this.handleDocFullscreenChange);
2337
+ this.videoFullscreenBeginHandler = null;
2338
+ this.videoFullscreenEndHandler = null;
2339
+ this.videoEl = null;
2340
+ });
2341
+ }
2247
2342
  handleClick() {
2248
- if (!screenfull$1.isEnabled)
2343
+ if (screenfull$1.isEnabled) {
2344
+ const container = (this.el.closest('.eb-player') ?? this.el);
2345
+ screenfull$1.toggle(container);
2249
2346
  return;
2250
- // Use the closest .eb-player ancestor as the fullscreen target,
2251
- // falling back to this component's own element.
2252
- // this.el is always non-null when this handler is called from a mounted button.
2253
- const container = (this.el.closest('.eb-player') ?? this.el);
2254
- screenfull$1.toggle(container);
2347
+ }
2348
+ if (this.useVideoFallback && this.videoEl) {
2349
+ if (this.state.isFullscreen) {
2350
+ // Exit fullscreen
2351
+ if (typeof this.videoEl.webkitExitFullscreen === 'function') {
2352
+ this.videoEl.webkitExitFullscreen();
2353
+ }
2354
+ else if (document.exitFullscreen) {
2355
+ document.exitFullscreen();
2356
+ }
2357
+ }
2358
+ else {
2359
+ // Enter fullscreen
2360
+ if (typeof this.videoEl.webkitEnterFullscreen === 'function') {
2361
+ this.videoEl.webkitEnterFullscreen();
2362
+ }
2363
+ else if (typeof this.videoEl.requestFullscreen === 'function') {
2364
+ this.videoEl.requestFullscreen();
2365
+ }
2366
+ }
2367
+ }
2255
2368
  }
2256
2369
  template() {
2257
- if (!screenfull$1.isEnabled) {
2370
+ const canFullscreen = screenfull$1.isEnabled || this.useVideoFallback;
2371
+ if (!canFullscreen) {
2258
2372
  return b `<button class="eb-fullscreen" hidden aria-hidden="true">${icon('fullscreen')}</button>`;
2259
2373
  }
2260
2374
  const isFullscreen = this.state.isFullscreen;
@@ -4998,6 +5112,14 @@
4998
5112
  getDriver() {
4999
5113
  return this.driver;
5000
5114
  }
5115
+ /**
5116
+ * Returns the CDN token manager used by this engine.
5117
+ * Allows the snapshot handler to share the same manager (avoids duplicate token requests).
5118
+ * Returns null when no token URL is configured or before init().
5119
+ */
5120
+ getTokenManager() {
5121
+ return this.tokenManager;
5122
+ }
5001
5123
  // -------------------------------------------------------------------------
5002
5124
  // BaseEngine hooks
5003
5125
  // -------------------------------------------------------------------------
@@ -5880,7 +6002,7 @@
5880
6002
  init(HlsConstructor) {
5881
6003
  // Create an off-screen video element for the snapshot player
5882
6004
  const offscreenVideo = document.createElement('video');
5883
- offscreenVideo.preload = 'none';
6005
+ offscreenVideo.preload = 'metadata';
5884
6006
  this.offscreenVideo = offscreenVideo;
5885
6007
  // Capture tokenManager via closure (Pitfall 6)
5886
6008
  const tokenManager = this.tokenManager;
@@ -5911,11 +6033,17 @@
5911
6033
  }
5912
6034
  PLoader = SnapshotPLoader;
5913
6035
  }
6036
+ // Strip player-specific keys that are NOT hls.js config options — they contain
6037
+ // non-serializable functions that cause DataCloneError when hls.js posts config to its worker.
6038
+ const rawSettings = { ...(this.config.engineSettings ?? {}) };
6039
+ delete rawSettings['extraParamsCallback'];
6040
+ delete rawSettings['onCDNTokenError'];
5914
6041
  const driverConfig = {
5915
6042
  startLevel: 0,
5916
6043
  enableWebVTT: false,
6044
+ enableWorker: false,
5917
6045
  maxBufferLength: 1,
5918
- ...(this.config.engineSettings ?? {}),
6046
+ ...rawSettings,
5919
6047
  ...(PLoader ? { pLoader: PLoader } : {})
5920
6048
  };
5921
6049
  const driver = new HlsConstructor(driverConfig);
@@ -6246,7 +6374,13 @@
6246
6374
  else {
6247
6375
  const win = window;
6248
6376
  if (win.Hls) {
6249
- const handler = new HlsSnapshotHandler({ src, engineSettings: mergedConfig.engineSettings }, null);
6377
+ // Share the main engine's token manager with the snapshot handler
6378
+ // to avoid duplicate token requests (one CDNTokenManager per player instance)
6379
+ const sharedTokenManager = engine.getTokenManager();
6380
+ // Build DRM config (emeEnabled, drmSystems, licenseXhrSetup) for the snapshot hls.js instance
6381
+ const snapshotDrmConfigurator = new DrmConfigurator(sharedTokenManager);
6382
+ const snapshotDrmConfig = snapshotDrmConfigurator.buildHlsConfig(mergedConfig.engineSettings);
6383
+ const handler = new HlsSnapshotHandler({ src, engineSettings: { ...mergedConfig.engineSettings, ...snapshotDrmConfig } }, sharedTokenManager);
6250
6384
  handler.init(win.Hls)
6251
6385
  .then(() => {
6252
6386
  activeSnapshotDestroy = () => handler.destroy();
@@ -6304,9 +6438,22 @@
6304
6438
  }, 100);
6305
6439
  });
6306
6440
  // Auto-open the stream if src is provided in config (matches legacy player behaviour
6307
- // where consumers call start({ src: '...' }) and expect playback to begin immediately)
6441
+ // where consumers call start({ src: '...' }) and expect playback to begin immediately).
6442
+ // When autoplay is false, defer open() until the user requests play — this avoids
6443
+ // fetching CDN tokens and loading manifests before playback is actually needed.
6308
6444
  if (mergedConfig.src) {
6309
- reference.open(mergedConfig.src);
6445
+ if (mergedConfig.autoplay) {
6446
+ reference.open(mergedConfig.src);
6447
+ }
6448
+ else {
6449
+ let deferredOpen = true;
6450
+ controller.bus.on('play', () => {
6451
+ if (deferredOpen) {
6452
+ deferredOpen = false;
6453
+ reference.open(mergedConfig.src);
6454
+ }
6455
+ }, { signal: controller.signal });
6456
+ }
6310
6457
  }
6311
6458
  return reference;
6312
6459
  }