eb-player 2.0.2 → 2.0.6
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 +207 -45
- package/dist/build/ebplayer.bundle.js +317 -59
- package/dist/build/ebplayer.bundle.js.map +1 -1
- package/dist/build/theme-lequipe.css +144 -32
- package/dist/build/types/core/config.d.ts.map +1 -1
- package/dist/build/types/core/i18n.d.ts.map +1 -1
- package/dist/build/types/eb-player-standalone.d.ts +14 -3
- package/dist/build/types/eb-player-standalone.d.ts.map +1 -1
- package/dist/build/types/eb-player.d.ts.map +1 -1
- package/dist/build/types/engines/snapshot/hls.d.ts.map +1 -1
- package/dist/build/types/skin/controls/fullscreen-button.d.ts +14 -1
- package/dist/build/types/skin/controls/fullscreen-button.d.ts.map +1 -1
- package/dist/build/types/skin/controls/seekbar.d.ts +6 -1
- package/dist/build/types/skin/controls/seekbar.d.ts.map +1 -1
- package/dist/build/types/skin/controls/settings-panel.d.ts +8 -0
- package/dist/build/types/skin/controls/settings-panel.d.ts.map +1 -1
- package/dist/dev/easybroadcast.js +343 -65
- package/dist/dev/easybroadcast.js.map +1 -1
- package/dist/eb-player.css +207 -45
- package/dist/players/default/default.js +148 -165
- package/dist/players/equipe/EB_lequipe-preprod.js +383 -0
- package/dist/players/equipe/equipe.js +93 -107
- package/dist/players/equipe/index.html +1 -1
- package/dist/theme-lequipe.css +144 -32
- package/package.json +2 -1
|
@@ -4,6 +4,8 @@
|
|
|
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.6";
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* Finite State Machine for player playback state transitions.
|
|
9
11
|
*
|
|
@@ -308,9 +310,25 @@
|
|
|
308
310
|
right: ['forward']
|
|
309
311
|
}
|
|
310
312
|
};
|
|
313
|
+
const LEQUIPE_LAYOUT = {
|
|
314
|
+
topBar: {
|
|
315
|
+
left: [],
|
|
316
|
+
right: ['pip', 'settings']
|
|
317
|
+
},
|
|
318
|
+
bottomBar: {
|
|
319
|
+
left: ['play-pause', 'live-sync', 'time'],
|
|
320
|
+
center: ['seekbar'],
|
|
321
|
+
right: ['volume', 'fullscreen']
|
|
322
|
+
},
|
|
323
|
+
middleBar: {
|
|
324
|
+
left: ['rewind'],
|
|
325
|
+
center: [],
|
|
326
|
+
right: ['forward']
|
|
327
|
+
}
|
|
328
|
+
};
|
|
311
329
|
const THEME_LAYOUTS = {
|
|
312
330
|
v2: V2_LAYOUT,
|
|
313
|
-
lequipe:
|
|
331
|
+
lequipe: LEQUIPE_LAYOUT,
|
|
314
332
|
modern: V2_LAYOUT,
|
|
315
333
|
};
|
|
316
334
|
/**
|
|
@@ -532,6 +550,30 @@
|
|
|
532
550
|
fr: 'La diffusion a échoué. Reprise de la lecture locale.',
|
|
533
551
|
ar: 'Casting failed. Resuming local playback.',
|
|
534
552
|
es: 'Error en la transmisión. Reanudando la reproducción local.'
|
|
553
|
+
},
|
|
554
|
+
'settings.audio': {
|
|
555
|
+
en: 'Audio track', fr: 'Piste audio', ar: 'مسار صوتي', es: 'Pista de audio'
|
|
556
|
+
},
|
|
557
|
+
'settings.subtitles': {
|
|
558
|
+
en: 'Subtitles', fr: 'Sous-titres', ar: 'ترجمات', es: 'Subtítulos'
|
|
559
|
+
},
|
|
560
|
+
'settings.speed': {
|
|
561
|
+
en: 'Playback speed', fr: 'Vitesse de lecture', ar: 'سرعة التشغيل', es: 'Velocidad'
|
|
562
|
+
},
|
|
563
|
+
'settings.quality': {
|
|
564
|
+
en: 'Quality', fr: 'Qualité', ar: 'الجودة', es: 'Calidad'
|
|
565
|
+
},
|
|
566
|
+
'settings.default': {
|
|
567
|
+
en: 'Default', fr: 'Défaut', ar: 'افتراضي', es: 'Predeterminado'
|
|
568
|
+
},
|
|
569
|
+
'settings.off': {
|
|
570
|
+
en: 'Off', fr: 'Désactivés', ar: 'إيقاف', es: 'Desactivado'
|
|
571
|
+
},
|
|
572
|
+
'settings.normal': {
|
|
573
|
+
en: 'Normal', fr: 'Normale', ar: 'عادي', es: 'Normal'
|
|
574
|
+
},
|
|
575
|
+
'settings.auto': {
|
|
576
|
+
en: 'Auto', fr: 'Auto', ar: 'تلقائي', es: 'Auto'
|
|
535
577
|
}
|
|
536
578
|
};
|
|
537
579
|
/**
|
|
@@ -1270,6 +1312,8 @@
|
|
|
1270
1312
|
this.tooltipX = 0;
|
|
1271
1313
|
this.previewVideoEl = null;
|
|
1272
1314
|
this.snapshotTake = null;
|
|
1315
|
+
this.trackEl = null;
|
|
1316
|
+
this.boundDocPointerMove = null;
|
|
1273
1317
|
}
|
|
1274
1318
|
onConnect() {
|
|
1275
1319
|
// Subscribe to snapshot handler readiness (emitted by eb-player.ts)
|
|
@@ -1290,6 +1334,10 @@
|
|
|
1290
1334
|
this.state.on('epgPrograms', () => this.scheduleRender(), { signal: this.signal });
|
|
1291
1335
|
this.render();
|
|
1292
1336
|
}
|
|
1337
|
+
disconnect() {
|
|
1338
|
+
this.stopDocumentTracking();
|
|
1339
|
+
super.disconnect();
|
|
1340
|
+
}
|
|
1293
1341
|
// ---- rAF batching ----
|
|
1294
1342
|
scheduleRender() {
|
|
1295
1343
|
if (this.rafPending)
|
|
@@ -1312,11 +1360,47 @@
|
|
|
1312
1360
|
const clamped = Math.min(1, Math.max(0, percent));
|
|
1313
1361
|
return clamped * this.state.duration;
|
|
1314
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
|
+
}
|
|
1315
1398
|
// ---- Drag handlers ----
|
|
1316
1399
|
handlePointerDown(event) {
|
|
1317
1400
|
if (this.state.adPlaying)
|
|
1318
1401
|
return;
|
|
1319
1402
|
const trackEl = event.currentTarget;
|
|
1403
|
+
this.trackEl = trackEl;
|
|
1320
1404
|
// setPointerCapture ensures events continue firing even if pointer leaves the element
|
|
1321
1405
|
if (typeof trackEl.setPointerCapture === 'function') {
|
|
1322
1406
|
trackEl.setPointerCapture(event.pointerId);
|
|
@@ -1330,8 +1414,14 @@
|
|
|
1330
1414
|
const trackEl = event.currentTarget;
|
|
1331
1415
|
this.dragValue = this.eventToTime(event, trackEl);
|
|
1332
1416
|
this.scheduleRender();
|
|
1417
|
+
// During drag with pointer capture, only update tooltip when pointer is within track bounds
|
|
1418
|
+
const rect = trackEl.getBoundingClientRect();
|
|
1419
|
+
if (event.clientX >= rect.left && event.clientX <= rect.right) {
|
|
1420
|
+
this.updateTooltip(event);
|
|
1421
|
+
}
|
|
1422
|
+
return;
|
|
1333
1423
|
}
|
|
1334
|
-
// Always update tooltip on pointermove over the track
|
|
1424
|
+
// Always update tooltip on pointermove over the track when not dragging
|
|
1335
1425
|
this.updateTooltip(event);
|
|
1336
1426
|
}
|
|
1337
1427
|
handlePointerUp(event) {
|
|
@@ -1341,15 +1431,16 @@
|
|
|
1341
1431
|
const seekTime = this.eventToTime(event, trackEl);
|
|
1342
1432
|
this.isDragging = false;
|
|
1343
1433
|
this.bus.emit('seek', { time: seekTime });
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
handlePointerLeave() {
|
|
1434
|
+
// Always hide tooltip on pointer up — it reappears naturally via pointermove
|
|
1435
|
+
// if the pointer is still over the track.
|
|
1347
1436
|
this.tooltipVisible = false;
|
|
1437
|
+
this.stopDocumentTracking();
|
|
1348
1438
|
this.render();
|
|
1349
1439
|
}
|
|
1350
1440
|
// ---- Tooltip ----
|
|
1351
1441
|
updateTooltip(event) {
|
|
1352
1442
|
const trackEl = event.currentTarget;
|
|
1443
|
+
this.trackEl = trackEl;
|
|
1353
1444
|
const rect = trackEl.getBoundingClientRect();
|
|
1354
1445
|
// Compute hover time (use LTR calculation for tooltip position regardless of RTL)
|
|
1355
1446
|
const rawPercent = (event.clientX - rect.left) / rect.width;
|
|
@@ -1360,6 +1451,8 @@
|
|
|
1360
1451
|
// Position tooltip at pointer X relative to track, clamped to track edges
|
|
1361
1452
|
this.tooltipX = Math.min(rect.width, Math.max(0, event.clientX - rect.left));
|
|
1362
1453
|
this.tooltipVisible = true;
|
|
1454
|
+
// Start document-level tracking to detect when pointer leaves the track
|
|
1455
|
+
this.startDocumentTracking();
|
|
1363
1456
|
// Request snapshot frame for seekbar preview thumbnail
|
|
1364
1457
|
if (this.snapshotTake !== null) {
|
|
1365
1458
|
this.snapshotTake(this.tooltipTime);
|
|
@@ -1452,11 +1545,10 @@
|
|
|
1452
1545
|
const tooltip = b `
|
|
1453
1546
|
<div
|
|
1454
1547
|
class="eb-seekbar-tooltip"
|
|
1455
|
-
style="left: ${this.tooltipX}px"
|
|
1456
|
-
?hidden="${!this.tooltipVisible}"
|
|
1548
|
+
style="left: ${this.tooltipX}px; visibility: ${this.tooltipVisible ? 'visible' : 'hidden'}"
|
|
1457
1549
|
>
|
|
1458
|
-
${tooltipTimeText}
|
|
1459
1550
|
${this.previewVideoEl !== null ? this.previewVideoEl : b ``}
|
|
1551
|
+
${tooltipTimeText}
|
|
1460
1552
|
</div>
|
|
1461
1553
|
`;
|
|
1462
1554
|
return b `
|
|
@@ -1469,7 +1561,6 @@
|
|
|
1469
1561
|
@pointerdown="${(event) => this.handlePointerDown(event)}"
|
|
1470
1562
|
@pointermove="${(event) => this.handlePointerMove(event)}"
|
|
1471
1563
|
@pointerup="${(event) => this.handlePointerUp(event)}"
|
|
1472
|
-
@pointerleave="${() => this.handlePointerLeave()}"
|
|
1473
1564
|
>
|
|
1474
1565
|
<div class="eb-seekbar-buffered" style="width: ${bufferedPercent.toFixed(2)}%"></div>
|
|
1475
1566
|
<div class="eb-seekbar-progress" style="width: ${progressPercent.toFixed(2)}%">
|
|
@@ -1655,16 +1746,20 @@
|
|
|
1655
1746
|
this.mode = 'root';
|
|
1656
1747
|
this.verticalDir = 'up';
|
|
1657
1748
|
this.horizontalDir = 'right';
|
|
1749
|
+
this.outsideClickHandler = null;
|
|
1750
|
+
this.outsideClickTimer = null;
|
|
1658
1751
|
}
|
|
1659
1752
|
onConnect() {
|
|
1660
1753
|
this.state.on('settingsOpen', () => {
|
|
1661
1754
|
// When settings close, reset to root mode
|
|
1662
1755
|
if (!this.state.settingsOpen) {
|
|
1663
1756
|
this.mode = 'root';
|
|
1757
|
+
this.removeOutsideClickListener();
|
|
1664
1758
|
}
|
|
1665
1759
|
else {
|
|
1666
1760
|
// Compute placement when opening
|
|
1667
1761
|
this.computePlacement();
|
|
1762
|
+
this.addOutsideClickListener();
|
|
1668
1763
|
}
|
|
1669
1764
|
this.render();
|
|
1670
1765
|
}, { signal: this.signal });
|
|
@@ -1675,6 +1770,8 @@
|
|
|
1675
1770
|
this.state.on('subtitleTracks', () => this.render(), { signal: this.signal });
|
|
1676
1771
|
this.state.on('currentSubtitleTrack', () => this.render(), { signal: this.signal });
|
|
1677
1772
|
this.state.on('playbackRate', () => this.render(), { signal: this.signal });
|
|
1773
|
+
// Clean up outside-click listener when component disconnects
|
|
1774
|
+
this.signal.addEventListener('abort', () => this.removeOutsideClickListener());
|
|
1678
1775
|
this.render();
|
|
1679
1776
|
}
|
|
1680
1777
|
/**
|
|
@@ -1702,53 +1799,76 @@
|
|
|
1702
1799
|
this.mode = 'root';
|
|
1703
1800
|
this.render();
|
|
1704
1801
|
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Returns the display label for the currently selected value in a category.
|
|
1804
|
+
*/
|
|
1805
|
+
currentValueLabel(mode) {
|
|
1806
|
+
const i18n = this.i18n;
|
|
1807
|
+
if (mode === 'quality') {
|
|
1808
|
+
const levels = this.state.qualityLevels;
|
|
1809
|
+
const current = this.state.currentQuality;
|
|
1810
|
+
if (current === -1)
|
|
1811
|
+
return i18n.t('settings.auto');
|
|
1812
|
+
const level = levels[current];
|
|
1813
|
+
return level?.height ? `${level.height}p` : i18n.t('settings.auto');
|
|
1814
|
+
}
|
|
1815
|
+
if (mode === 'speed') {
|
|
1816
|
+
const rate = this.state.playbackRate;
|
|
1817
|
+
return rate === 1 ? i18n.t('settings.normal') : `${rate}x`;
|
|
1818
|
+
}
|
|
1819
|
+
if (mode === 'audio') {
|
|
1820
|
+
const tracks = this.state.audioTracks;
|
|
1821
|
+
const current = this.state.currentAudioTrack;
|
|
1822
|
+
const track = tracks[current];
|
|
1823
|
+
return track?.name || track?.lang || i18n.t('settings.default');
|
|
1824
|
+
}
|
|
1825
|
+
if (mode === 'subtitles') {
|
|
1826
|
+
const current = this.state.currentSubtitleTrack;
|
|
1827
|
+
if (current === -1)
|
|
1828
|
+
return i18n.t('settings.off');
|
|
1829
|
+
const tracks = this.state.subtitleTracks;
|
|
1830
|
+
const track = tracks[current];
|
|
1831
|
+
return track?.name || track?.lang || `Track ${current}`;
|
|
1832
|
+
}
|
|
1833
|
+
return '';
|
|
1834
|
+
}
|
|
1705
1835
|
renderRootMenu() {
|
|
1706
1836
|
const qualityLevels = this.state.qualityLevels;
|
|
1707
1837
|
const audioTracks = this.state.audioTracks;
|
|
1708
1838
|
const subtitleTracks = this.state.subtitleTracks;
|
|
1839
|
+
const i18n = this.i18n;
|
|
1709
1840
|
const showQuality = qualityLevels.length > 0;
|
|
1710
1841
|
const showSpeed = this.config.speed === true;
|
|
1711
1842
|
const showAudio = audioTracks.length > 1;
|
|
1712
1843
|
const showSubtitles = subtitleTracks.length > 0;
|
|
1844
|
+
const row = (iconName, label, value, mode) => b `
|
|
1845
|
+
<li>
|
|
1846
|
+
<button class="eb-settings-category" @click="${() => this.navigateTo(mode)}">
|
|
1847
|
+
<span class="eb-settings-category__icon">${icon(iconName)}</span>
|
|
1848
|
+
<span class="eb-settings-category__label">${label}</span>
|
|
1849
|
+
<span class="eb-settings-category__value">${value}</span>
|
|
1850
|
+
<span class="eb-settings-category__chevron">${icon('chevron-right')}</span>
|
|
1851
|
+
</button>
|
|
1852
|
+
</li>
|
|
1853
|
+
`;
|
|
1713
1854
|
return b `
|
|
1714
1855
|
<ul class="eb-settings-menu eb-settings-root">
|
|
1715
|
-
${
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
</button>
|
|
1720
|
-
</li>
|
|
1721
|
-
` : ''}
|
|
1722
|
-
${showSpeed ? b `
|
|
1723
|
-
<li>
|
|
1724
|
-
<button class="eb-settings-category" @click="${() => this.navigateTo('speed')}">
|
|
1725
|
-
Speed
|
|
1726
|
-
</button>
|
|
1727
|
-
</li>
|
|
1728
|
-
` : ''}
|
|
1729
|
-
${showAudio ? b `
|
|
1730
|
-
<li>
|
|
1731
|
-
<button class="eb-settings-category" @click="${() => this.navigateTo('audio')}">
|
|
1732
|
-
Audio
|
|
1733
|
-
</button>
|
|
1734
|
-
</li>
|
|
1735
|
-
` : ''}
|
|
1736
|
-
${showSubtitles ? b `
|
|
1737
|
-
<li>
|
|
1738
|
-
<button class="eb-settings-category" @click="${() => this.navigateTo('subtitles')}">
|
|
1739
|
-
Subtitles
|
|
1740
|
-
</button>
|
|
1741
|
-
</li>
|
|
1742
|
-
` : ''}
|
|
1856
|
+
${showAudio ? row('audio', i18n.t('settings.audio'), this.currentValueLabel('audio'), 'audio') : ''}
|
|
1857
|
+
${showSubtitles ? row('subtitle', i18n.t('settings.subtitles'), this.currentValueLabel('subtitles'), 'subtitles') : ''}
|
|
1858
|
+
${showSpeed ? row('speed', i18n.t('settings.speed'), this.currentValueLabel('speed'), 'speed') : ''}
|
|
1859
|
+
${showQuality ? row('quality', i18n.t('settings.quality'), this.currentValueLabel('quality'), 'quality') : ''}
|
|
1743
1860
|
</ul>
|
|
1744
1861
|
`;
|
|
1745
1862
|
}
|
|
1746
1863
|
renderSubMenu(title, items, onSelect) {
|
|
1747
1864
|
return b `
|
|
1748
1865
|
<div class="eb-settings-submenu">
|
|
1749
|
-
<
|
|
1750
|
-
${
|
|
1751
|
-
|
|
1866
|
+
<div class="eb-settings-header">
|
|
1867
|
+
<button class="eb-settings-back" @click="${() => this.navigateBack()}">
|
|
1868
|
+
${icon('chevron-left')}
|
|
1869
|
+
</button>
|
|
1870
|
+
<span class="eb-settings-header__title">${title}</span>
|
|
1871
|
+
</div>
|
|
1752
1872
|
<ul class="eb-settings-menu">
|
|
1753
1873
|
${items.map((item) => b `
|
|
1754
1874
|
<li>
|
|
@@ -1768,14 +1888,14 @@
|
|
|
1768
1888
|
const levels = this.state.qualityLevels;
|
|
1769
1889
|
const currentQuality = this.state.currentQuality;
|
|
1770
1890
|
const items = getQualityItems(levels, currentQuality);
|
|
1771
|
-
return this.renderSubMenu('
|
|
1891
|
+
return this.renderSubMenu(this.i18n.t('settings.quality'), items, (item) => {
|
|
1772
1892
|
this.bus.emit('settings-select-quality', { index: item.value });
|
|
1773
1893
|
});
|
|
1774
1894
|
}
|
|
1775
1895
|
renderSpeedMenu() {
|
|
1776
1896
|
const currentRate = this.state.playbackRate;
|
|
1777
1897
|
const items = getSpeedItems(currentRate);
|
|
1778
|
-
return this.renderSubMenu('
|
|
1898
|
+
return this.renderSubMenu(this.i18n.t('settings.speed'), items, (item) => {
|
|
1779
1899
|
this.bus.emit('settings-select-speed', { rate: item.value });
|
|
1780
1900
|
});
|
|
1781
1901
|
}
|
|
@@ -1783,7 +1903,7 @@
|
|
|
1783
1903
|
const tracks = this.state.audioTracks;
|
|
1784
1904
|
const currentTrack = this.state.currentAudioTrack;
|
|
1785
1905
|
const items = getAudioItems(tracks, currentTrack);
|
|
1786
|
-
return this.renderSubMenu('
|
|
1906
|
+
return this.renderSubMenu(this.i18n.t('settings.audio'), items, (item) => {
|
|
1787
1907
|
this.bus.emit('settings-select-audio', { index: item.value });
|
|
1788
1908
|
});
|
|
1789
1909
|
}
|
|
@@ -1791,10 +1911,37 @@
|
|
|
1791
1911
|
const tracks = this.state.subtitleTracks;
|
|
1792
1912
|
const currentTrack = this.state.currentSubtitleTrack;
|
|
1793
1913
|
const items = getSubtitleItems(tracks, currentTrack);
|
|
1794
|
-
return this.renderSubMenu('
|
|
1914
|
+
return this.renderSubMenu(this.i18n.t('settings.subtitles'), items, (item) => {
|
|
1795
1915
|
this.bus.emit('settings-select-subtitle', { index: item.value });
|
|
1796
1916
|
});
|
|
1797
1917
|
}
|
|
1918
|
+
addOutsideClickListener() {
|
|
1919
|
+
this.removeOutsideClickListener();
|
|
1920
|
+
// Defer to next tick so the opening click doesn't immediately close
|
|
1921
|
+
this.outsideClickTimer = setTimeout(() => {
|
|
1922
|
+
this.outsideClickTimer = null;
|
|
1923
|
+
// Guard: panel may have closed before this timer fires
|
|
1924
|
+
if (!this.state?.settingsOpen)
|
|
1925
|
+
return;
|
|
1926
|
+
this.outsideClickHandler = (event) => {
|
|
1927
|
+
const wrapper = this.el?.querySelector('.eb-settings-wrapper');
|
|
1928
|
+
if (wrapper && !wrapper.contains(event.target)) {
|
|
1929
|
+
this.state.settingsOpen = false;
|
|
1930
|
+
}
|
|
1931
|
+
};
|
|
1932
|
+
document.addEventListener('click', this.outsideClickHandler, { capture: true });
|
|
1933
|
+
}, 0);
|
|
1934
|
+
}
|
|
1935
|
+
removeOutsideClickListener() {
|
|
1936
|
+
if (this.outsideClickTimer !== null) {
|
|
1937
|
+
clearTimeout(this.outsideClickTimer);
|
|
1938
|
+
this.outsideClickTimer = null;
|
|
1939
|
+
}
|
|
1940
|
+
if (this.outsideClickHandler !== null) {
|
|
1941
|
+
document.removeEventListener('click', this.outsideClickHandler, { capture: true });
|
|
1942
|
+
this.outsideClickHandler = null;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1798
1945
|
toggleSettings() {
|
|
1799
1946
|
this.state.settingsOpen = !this.state.settingsOpen;
|
|
1800
1947
|
}
|
|
@@ -2111,13 +2258,24 @@
|
|
|
2111
2258
|
*
|
|
2112
2259
|
* - Calls screenfull.toggle() with the closest .eb-player ancestor on click
|
|
2113
2260
|
* - Subscribes to screenfull 'change' event to update state.isFullscreen
|
|
2114
|
-
* -
|
|
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
|
|
2115
2264
|
* - Re-renders when state.isFullscreen changes
|
|
2116
2265
|
*/
|
|
2117
2266
|
class FullscreenButton extends BaseComponent {
|
|
2118
2267
|
constructor() {
|
|
2119
2268
|
super(...arguments);
|
|
2120
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
|
+
};
|
|
2121
2279
|
}
|
|
2122
2280
|
onConnect() {
|
|
2123
2281
|
this.state.on('isFullscreen', () => this.render(), { signal: this.signal });
|
|
@@ -2127,25 +2285,90 @@
|
|
|
2127
2285
|
this.render();
|
|
2128
2286
|
};
|
|
2129
2287
|
screenfull$1.on('change', this.changeHandler);
|
|
2130
|
-
// Unsubscribe when signal aborts (on disconnect)
|
|
2131
2288
|
this.signal.addEventListener('abort', () => {
|
|
2132
2289
|
screenfull$1.off('change', this.changeHandler);
|
|
2133
2290
|
this.changeHandler = null;
|
|
2134
2291
|
});
|
|
2135
2292
|
}
|
|
2293
|
+
else {
|
|
2294
|
+
// Fullscreen API not available for elements — try video-element fallback (mobile)
|
|
2295
|
+
this.initVideoFallback();
|
|
2296
|
+
}
|
|
2136
2297
|
this.render();
|
|
2137
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
|
+
}
|
|
2138
2342
|
handleClick() {
|
|
2139
|
-
if (
|
|
2343
|
+
if (screenfull$1.isEnabled) {
|
|
2344
|
+
const container = (this.el.closest('.eb-player') ?? this.el);
|
|
2345
|
+
screenfull$1.toggle(container);
|
|
2140
2346
|
return;
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
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
|
+
}
|
|
2146
2368
|
}
|
|
2147
2369
|
template() {
|
|
2148
|
-
|
|
2370
|
+
const canFullscreen = screenfull$1.isEnabled || this.useVideoFallback;
|
|
2371
|
+
if (!canFullscreen) {
|
|
2149
2372
|
return b `<button class="eb-fullscreen" hidden aria-hidden="true">${icon('fullscreen')}</button>`;
|
|
2150
2373
|
}
|
|
2151
2374
|
const isFullscreen = this.state.isFullscreen;
|
|
@@ -5034,7 +5257,7 @@
|
|
|
5034
5257
|
// Build driver config — only spread known engineSettings keys that hls.js recognises,
|
|
5035
5258
|
// not the entire engineSettings bag (which may contain player-specific keys like
|
|
5036
5259
|
// extraParamsCallback that should NOT leak into the hls.js constructor config).
|
|
5037
|
-
const { emeEnabled, drmSystems, ...hlsEngineSettings } = config.engineSettings;
|
|
5260
|
+
const { emeEnabled: _emeEnabled, drmSystems: _drmSystems, ...hlsEngineSettings } = config.engineSettings;
|
|
5038
5261
|
// Remove player-specific keys that are NOT hls.js config options
|
|
5039
5262
|
const hlsSafeSettings = { ...hlsEngineSettings };
|
|
5040
5263
|
delete hlsSafeSettings['extraParamsCallback'];
|
|
@@ -5771,7 +5994,7 @@
|
|
|
5771
5994
|
init(HlsConstructor) {
|
|
5772
5995
|
// Create an off-screen video element for the snapshot player
|
|
5773
5996
|
const offscreenVideo = document.createElement('video');
|
|
5774
|
-
offscreenVideo.preload = '
|
|
5997
|
+
offscreenVideo.preload = 'metadata';
|
|
5775
5998
|
this.offscreenVideo = offscreenVideo;
|
|
5776
5999
|
// Capture tokenManager via closure (Pitfall 6)
|
|
5777
6000
|
const tokenManager = this.tokenManager;
|
|
@@ -5802,11 +6025,17 @@
|
|
|
5802
6025
|
}
|
|
5803
6026
|
PLoader = SnapshotPLoader;
|
|
5804
6027
|
}
|
|
6028
|
+
// Strip player-specific keys that are NOT hls.js config options — they contain
|
|
6029
|
+
// non-serializable functions that cause DataCloneError when hls.js posts config to its worker.
|
|
6030
|
+
const rawSettings = { ...(this.config.engineSettings ?? {}) };
|
|
6031
|
+
delete rawSettings['extraParamsCallback'];
|
|
6032
|
+
delete rawSettings['onCDNTokenError'];
|
|
5805
6033
|
const driverConfig = {
|
|
5806
6034
|
startLevel: 0,
|
|
5807
6035
|
enableWebVTT: false,
|
|
6036
|
+
enableWorker: false,
|
|
5808
6037
|
maxBufferLength: 1,
|
|
5809
|
-
...
|
|
6038
|
+
...rawSettings,
|
|
5810
6039
|
...(PLoader ? { pLoader: PLoader } : {})
|
|
5811
6040
|
};
|
|
5812
6041
|
const driver = new HlsConstructor(driverConfig);
|
|
@@ -6137,8 +6366,30 @@
|
|
|
6137
6366
|
else {
|
|
6138
6367
|
const win = window;
|
|
6139
6368
|
if (win.Hls) {
|
|
6140
|
-
|
|
6141
|
-
|
|
6369
|
+
// Create a dedicated token manager for the snapshot handler (DRM license + manifest tokens)
|
|
6370
|
+
let snapshotTokenManager = null;
|
|
6371
|
+
if (mergedConfig.token) {
|
|
6372
|
+
snapshotTokenManager = new CDNTokenManager({
|
|
6373
|
+
token: mergedConfig.token,
|
|
6374
|
+
tokenType: mergedConfig.tokenType,
|
|
6375
|
+
srcInTokenRequest: mergedConfig.srcInTokenRequest,
|
|
6376
|
+
extraParamsCallback: (mergedConfig.engineSettings.extraParamsCallback ?? mergedConfig.extraParamsCallback),
|
|
6377
|
+
onCDNTokenError: mergedConfig.engineSettings.onCDNTokenError
|
|
6378
|
+
});
|
|
6379
|
+
}
|
|
6380
|
+
// Build DRM config (emeEnabled, drmSystems, licenseXhrSetup) for the snapshot hls.js instance
|
|
6381
|
+
const snapshotDrmConfigurator = new DrmConfigurator(snapshotTokenManager);
|
|
6382
|
+
const snapshotDrmConfig = snapshotDrmConfigurator.buildHlsConfig(mergedConfig.engineSettings);
|
|
6383
|
+
const handler = new HlsSnapshotHandler({ src, engineSettings: { ...mergedConfig.engineSettings, ...snapshotDrmConfig } }, snapshotTokenManager);
|
|
6384
|
+
// Fetch initial token before init (needed for manifest request)
|
|
6385
|
+
const tokenReady = snapshotTokenManager && src
|
|
6386
|
+
? snapshotTokenManager.fetchToken({ src }).catch((error) => {
|
|
6387
|
+
console.warn('EBPlayer: Snapshot token fetch failed:', error);
|
|
6388
|
+
})
|
|
6389
|
+
: Promise.resolve();
|
|
6390
|
+
tokenReady.then(() => {
|
|
6391
|
+
return handler.init(win.Hls);
|
|
6392
|
+
})
|
|
6142
6393
|
.then(() => {
|
|
6143
6394
|
activeSnapshotDestroy = () => handler.destroy();
|
|
6144
6395
|
const snapshotVideo = handler.getVideo();
|
|
@@ -6226,10 +6477,17 @@
|
|
|
6226
6477
|
}
|
|
6227
6478
|
}
|
|
6228
6479
|
// ---------------------------------------------------------------------------
|
|
6480
|
+
// Version
|
|
6481
|
+
// ---------------------------------------------------------------------------
|
|
6482
|
+
// Injected at build time by Rollup's @rollup/plugin-virtual or replaced by bundler.
|
|
6483
|
+
// Falls back to 'dev' when running unbundled (tests, dev server).
|
|
6484
|
+
const VERSION = typeof __EB_PLAYER_VERSION__ !== 'undefined' ? __EB_PLAYER_VERSION__ : 'dev';
|
|
6485
|
+
// ---------------------------------------------------------------------------
|
|
6229
6486
|
// window.EBPlayer assignment
|
|
6230
6487
|
// ---------------------------------------------------------------------------
|
|
6231
6488
|
if (typeof window !== 'undefined') {
|
|
6232
|
-
|
|
6489
|
+
console.info(`%cEBPlayer v${VERSION}`, 'color: #1FA9DD; font-weight: bold');
|
|
6490
|
+
window.EBPlayer = { start, stop, destroy, AVAILABLE_THEMES, THEME_LAYOUTS, version: VERSION };
|
|
6233
6491
|
}
|
|
6234
6492
|
|
|
6235
6493
|
/**
|