eb-player 2.0.14 → 2.0.17
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 +516 -58
- package/dist/build/ebplayer.bundle.js +265 -102
- package/dist/build/ebplayer.bundle.js.map +1 -1
- package/dist/build/theme-forja.css +409 -4
- package/dist/build/theme-lequipe.css +7 -14
- package/dist/build/theme-modern.css +6 -9
- package/dist/build/theme-radio.css +4 -1
- package/dist/build/theme-snrt.css +4 -1
- package/dist/build/theme-v2.css +50 -22
- package/dist/build/types/core/command-handler.d.ts +1 -0
- package/dist/build/types/core/command-handler.d.ts.map +1 -1
- package/dist/build/types/core/config.d.ts +2 -1
- package/dist/build/types/core/config.d.ts.map +1 -1
- package/dist/build/types/core/event-bus.d.ts +1 -0
- package/dist/build/types/core/event-bus.d.ts.map +1 -1
- package/dist/build/types/core/i18n.d.ts.map +1 -1
- package/dist/build/types/core/lifecycle.d.ts.map +1 -1
- package/dist/build/types/eb-player.d.ts +23 -10
- package/dist/build/types/eb-player.d.ts.map +1 -1
- package/dist/build/types/engines/base-engine.d.ts.map +1 -1
- package/dist/build/types/engines/hls.d.ts.map +1 -1
- package/dist/build/types/skin/brand/forja-playlist-bar.d.ts.map +1 -1
- package/dist/build/types/skin/component-registry.d.ts.map +1 -1
- package/dist/build/types/skin/controls/channel-name.d.ts +16 -0
- package/dist/build/types/skin/controls/channel-name.d.ts.map +1 -0
- package/dist/build/types/skin/controls/play-pause-button.d.ts.map +1 -1
- package/dist/build/types/skin/controls/settings-panel.d.ts.map +1 -1
- package/dist/build/types/skin/controls/time-display.d.ts.map +1 -1
- package/dist/build/types/skin/overlays/loading-spinner.d.ts +1 -1
- package/dist/build/types/skin/overlays/loading-spinner.d.ts.map +1 -1
- package/dist/build/types/skin/skin-root.d.ts.map +1 -1
- package/dist/eb-player.css +516 -58
- package/dist/theme-forja.css +409 -4
- package/dist/theme-lequipe.css +7 -14
- package/dist/theme-modern.css +6 -9
- package/dist/theme-radio.css +4 -1
- package/dist/theme-snrt.css +4 -1
- package/dist/theme-v2.css +50 -22
- package/package.json +1 -1
|
@@ -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.
|
|
7
|
+
var __EB_PLAYER_VERSION__ = "2.0.17";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Finite State Machine for player playback state transitions.
|
|
@@ -287,7 +287,7 @@
|
|
|
287
287
|
{ value: 'radio', label: 'Radio', primaryColor: '#F4A261' },
|
|
288
288
|
{ value: 'snrt', label: 'SNRT', primaryColor: '#006633' },
|
|
289
289
|
{ value: 'modern', label: 'Modern', primaryColor: '#7c3aed' },
|
|
290
|
-
{ value: 'v2', label: 'V2', primaryColor: '#
|
|
290
|
+
{ value: 'v2', label: 'V2', primaryColor: '#3d4097' },
|
|
291
291
|
{ value: 'lequipe', label: "L'Equipe", primaryColor: '#d61e00' },
|
|
292
292
|
];
|
|
293
293
|
/**
|
|
@@ -297,12 +297,12 @@
|
|
|
297
297
|
const V2_LAYOUT = {
|
|
298
298
|
topBar: {
|
|
299
299
|
left: [],
|
|
300
|
-
right: ['
|
|
300
|
+
right: ['pip', 'settings']
|
|
301
301
|
},
|
|
302
302
|
bottomBar: {
|
|
303
|
-
left: ['
|
|
304
|
-
center: ['seekbar'],
|
|
305
|
-
right: [
|
|
303
|
+
left: ['live-sync', 'time', 'channel-name'],
|
|
304
|
+
center: ['play-pause', 'seekbar', 'volume', 'fullscreen'],
|
|
305
|
+
right: []
|
|
306
306
|
},
|
|
307
307
|
middleBar: {
|
|
308
308
|
left: ['rewind'],
|
|
@@ -326,10 +326,29 @@
|
|
|
326
326
|
right: ['forward']
|
|
327
327
|
}
|
|
328
328
|
};
|
|
329
|
+
const FORJA_LAYOUT = {
|
|
330
|
+
topBar: {
|
|
331
|
+
left: [],
|
|
332
|
+
right: []
|
|
333
|
+
},
|
|
334
|
+
bottomBar: {
|
|
335
|
+
// Row 1: seekbar (full width)
|
|
336
|
+
// Row 2: play-pause + volume (left) | live-sync + settings + pip + fullscreen (right)
|
|
337
|
+
left: ['seekbar'],
|
|
338
|
+
center: ['play-pause', 'volume'],
|
|
339
|
+
right: ['live-sync', 'settings', 'pip', 'fullscreen']
|
|
340
|
+
},
|
|
341
|
+
middleBar: {
|
|
342
|
+
left: ['rewind'],
|
|
343
|
+
center: [],
|
|
344
|
+
right: ['forward']
|
|
345
|
+
}
|
|
346
|
+
};
|
|
329
347
|
const THEME_LAYOUTS = {
|
|
330
348
|
v2: V2_LAYOUT,
|
|
331
349
|
lequipe: LEQUIPE_LAYOUT,
|
|
332
350
|
modern: V2_LAYOUT,
|
|
351
|
+
forja: FORJA_LAYOUT,
|
|
333
352
|
};
|
|
334
353
|
/**
|
|
335
354
|
* Returns the effective layout for a given config.
|
|
@@ -355,7 +374,7 @@
|
|
|
355
374
|
// Drivers
|
|
356
375
|
dashjs: 'https://reference.dashif.org/dash.js/v4.7.4/dist/dash.all.min.js',
|
|
357
376
|
hlsjs: 'https://cdn.jsdelivr.net/npm/hls.js@1.6.10/dist/hls.min.js',
|
|
358
|
-
engineSettings: { liveSyncDurationCount:
|
|
377
|
+
engineSettings: { liveSyncDurationCount: 3 },
|
|
359
378
|
chromecast: 'https://cdnjs.cloudflare.com/ajax/libs/castjs/5.3.0/cast.min.js',
|
|
360
379
|
chromecastApp: undefined,
|
|
361
380
|
chromecastMetadata: undefined,
|
|
@@ -396,7 +415,7 @@
|
|
|
396
415
|
forceAutoplay: false,
|
|
397
416
|
forceQuality: false,
|
|
398
417
|
liveButton: false,
|
|
399
|
-
syncLiveMargin:
|
|
418
|
+
syncLiveMargin: 30,
|
|
400
419
|
isLive: undefined,
|
|
401
420
|
noUi: false,
|
|
402
421
|
preroll: false,
|
|
@@ -430,7 +449,7 @@
|
|
|
430
449
|
}
|
|
431
450
|
},
|
|
432
451
|
hasPlaylistPicker: false,
|
|
433
|
-
disableCustomAbr:
|
|
452
|
+
disableCustomAbr: true,
|
|
434
453
|
lang: undefined,
|
|
435
454
|
// EPG
|
|
436
455
|
epgContentId: undefined,
|
|
@@ -438,6 +457,8 @@
|
|
|
438
457
|
epgDefaultLang: 'en',
|
|
439
458
|
showEpgTitlePreview: false,
|
|
440
459
|
showProgressThumb: false,
|
|
460
|
+
// Display
|
|
461
|
+
channelName: undefined,
|
|
441
462
|
// Layout
|
|
442
463
|
layout: undefined
|
|
443
464
|
};
|
|
@@ -545,6 +566,12 @@
|
|
|
545
566
|
ar: 'The stream is unavailable. Please try again later.',
|
|
546
567
|
es: 'La transmisión no está disponible. Por favor, inténtelo más tarde.'
|
|
547
568
|
},
|
|
569
|
+
'error.geoblocked': {
|
|
570
|
+
en: 'This content is not available in your region.',
|
|
571
|
+
fr: 'Ce contenu n\'est pas disponible dans votre région.',
|
|
572
|
+
ar: 'This content is not available in your region.',
|
|
573
|
+
es: 'Este contenido no está disponible en tu región.'
|
|
574
|
+
},
|
|
548
575
|
'cast.failed': {
|
|
549
576
|
en: 'Casting failed. Resuming local playback.',
|
|
550
577
|
fr: 'La diffusion a échoué. Reprise de la lecture locale.',
|
|
@@ -686,6 +713,7 @@
|
|
|
686
713
|
this.wireReloadOrchestration(bus, state, i18n, onReload, signal);
|
|
687
714
|
this.wireCastHandoff(bus, video, state, chromecastManager, i18n, signal);
|
|
688
715
|
this.wireVolumeRouting(state, chromecastManager, signal);
|
|
716
|
+
this.wireGeoblock(bus, state, i18n, signal);
|
|
689
717
|
this.wireAbortCleanup(signal);
|
|
690
718
|
}
|
|
691
719
|
// ---------------------------------------------------------------------------
|
|
@@ -819,6 +847,11 @@
|
|
|
819
847
|
}
|
|
820
848
|
}, { signal });
|
|
821
849
|
}
|
|
850
|
+
wireGeoblock(bus, state, i18n, signal) {
|
|
851
|
+
bus.on('geoblock-detected', () => {
|
|
852
|
+
state.error = i18n.t('error.geoblocked');
|
|
853
|
+
}, { signal });
|
|
854
|
+
}
|
|
822
855
|
showToast(state, message) {
|
|
823
856
|
state.toast = message;
|
|
824
857
|
clearTimeout(this.toastTimer);
|
|
@@ -1117,7 +1150,7 @@
|
|
|
1117
1150
|
}
|
|
1118
1151
|
template() {
|
|
1119
1152
|
const state = this.state;
|
|
1120
|
-
const isPlaying = state.playbackState === 'playing';
|
|
1153
|
+
const isPlaying = state.playbackState === 'playing' || state.playbackState === 'buffering';
|
|
1121
1154
|
const layout = resolveLayout(this.config).middleBar;
|
|
1122
1155
|
const slot = (componentId) => b `<div class="eb-slot-${componentId}"></div>`;
|
|
1123
1156
|
return b `
|
|
@@ -1137,7 +1170,7 @@
|
|
|
1137
1170
|
handleClick() {
|
|
1138
1171
|
const state = this.state;
|
|
1139
1172
|
const bus = this.bus;
|
|
1140
|
-
if (state.playbackState === 'playing') {
|
|
1173
|
+
if (state.playbackState === 'playing' || state.playbackState === 'buffering') {
|
|
1141
1174
|
bus.emit('pause');
|
|
1142
1175
|
}
|
|
1143
1176
|
else {
|
|
@@ -1159,7 +1192,8 @@
|
|
|
1159
1192
|
this.render();
|
|
1160
1193
|
}
|
|
1161
1194
|
template() {
|
|
1162
|
-
const
|
|
1195
|
+
const playbackState = this.state.playbackState;
|
|
1196
|
+
const isPlaying = playbackState === 'playing' || playbackState === 'buffering';
|
|
1163
1197
|
const ariaLabel = isPlaying ? 'Pause' : 'Play';
|
|
1164
1198
|
const iconName = isPlaying ? 'pause' : 'play';
|
|
1165
1199
|
return b `
|
|
@@ -1611,16 +1645,18 @@
|
|
|
1611
1645
|
}
|
|
1612
1646
|
template() {
|
|
1613
1647
|
const { currentTime, duration, isLive, isSyncWithLive } = this.state;
|
|
1648
|
+
if (!this.config.seekbar) {
|
|
1649
|
+
return b `<div class="eb-time-display" hidden></div>`;
|
|
1650
|
+
}
|
|
1614
1651
|
let timeText;
|
|
1615
1652
|
if (isLive) {
|
|
1616
1653
|
if (isSyncWithLive || !Number.isFinite(duration)) {
|
|
1617
|
-
// At the live edge or non-DVR stream: show current wall-clock time
|
|
1618
1654
|
timeText = formatWallClock(Date.now());
|
|
1619
1655
|
}
|
|
1620
1656
|
else {
|
|
1621
|
-
// Behind
|
|
1657
|
+
// Behind live edge: show wall-clock time + negative offset
|
|
1622
1658
|
const offset = duration - currentTime;
|
|
1623
|
-
|
|
1659
|
+
return b `<div class="eb-time-display">${formatWallClock(Date.now())} <span class="eb-time-display__offset">-${formatDuration(offset)}</span></div>`;
|
|
1624
1660
|
}
|
|
1625
1661
|
}
|
|
1626
1662
|
else {
|
|
@@ -1652,7 +1688,7 @@
|
|
|
1652
1688
|
template() {
|
|
1653
1689
|
const { isLive, isSyncWithLive } = this.state;
|
|
1654
1690
|
const configLive = this.config.liveButton === true || this.config.isLive === true;
|
|
1655
|
-
if (!isLive && !configLive) {
|
|
1691
|
+
if (!this.config.seekbar || (!isLive && !configLive)) {
|
|
1656
1692
|
return b `<button class="eb-live-sync" hidden style="display:none">${icon('live')}</button>`;
|
|
1657
1693
|
}
|
|
1658
1694
|
return b `
|
|
@@ -1818,15 +1854,19 @@
|
|
|
1818
1854
|
}
|
|
1819
1855
|
if (mode === 'audio') {
|
|
1820
1856
|
const tracks = this.state.audioTracks;
|
|
1857
|
+
if (tracks.length === 0)
|
|
1858
|
+
return i18n.t('settings.default');
|
|
1821
1859
|
const current = this.state.currentAudioTrack;
|
|
1822
1860
|
const track = tracks[current];
|
|
1823
1861
|
return track?.name || track?.lang || i18n.t('settings.default');
|
|
1824
1862
|
}
|
|
1825
1863
|
if (mode === 'subtitles') {
|
|
1864
|
+
const tracks = this.state.subtitleTracks;
|
|
1865
|
+
if (tracks.length === 0)
|
|
1866
|
+
return i18n.t('settings.off');
|
|
1826
1867
|
const current = this.state.currentSubtitleTrack;
|
|
1827
1868
|
if (current === -1)
|
|
1828
1869
|
return i18n.t('settings.off');
|
|
1829
|
-
const tracks = this.state.subtitleTracks;
|
|
1830
1870
|
const track = tracks[current];
|
|
1831
1871
|
return track?.name || track?.lang || `Track ${current}`;
|
|
1832
1872
|
}
|
|
@@ -1834,13 +1874,9 @@
|
|
|
1834
1874
|
}
|
|
1835
1875
|
renderRootMenu() {
|
|
1836
1876
|
const qualityLevels = this.state.qualityLevels;
|
|
1837
|
-
const audioTracks = this.state.audioTracks;
|
|
1838
|
-
const subtitleTracks = this.state.subtitleTracks;
|
|
1839
1877
|
const i18n = this.i18n;
|
|
1840
1878
|
const showQuality = qualityLevels.length > 0;
|
|
1841
1879
|
const showSpeed = this.config.speed === true;
|
|
1842
|
-
const showAudio = audioTracks.length > 1;
|
|
1843
|
-
const showSubtitles = subtitleTracks.length > 0;
|
|
1844
1880
|
const row = (iconName, label, value, mode) => b `
|
|
1845
1881
|
<li>
|
|
1846
1882
|
<button class="eb-settings-category" @click="${() => this.navigateTo(mode)}">
|
|
@@ -1853,8 +1889,8 @@
|
|
|
1853
1889
|
`;
|
|
1854
1890
|
return b `
|
|
1855
1891
|
<ul class="eb-settings-menu eb-settings-root">
|
|
1856
|
-
${
|
|
1857
|
-
${
|
|
1892
|
+
${row('audio', i18n.t('settings.audio'), this.currentValueLabel('audio'), 'audio')}
|
|
1893
|
+
${row('subtitle', i18n.t('settings.subtitles'), this.currentValueLabel('subtitles'), 'subtitles')}
|
|
1858
1894
|
${showSpeed ? row('speed', i18n.t('settings.speed'), this.currentValueLabel('speed'), 'speed') : ''}
|
|
1859
1895
|
${showQuality ? row('quality', i18n.t('settings.quality'), this.currentValueLabel('quality'), 'quality') : ''}
|
|
1860
1896
|
</ul>
|
|
@@ -1902,17 +1938,25 @@
|
|
|
1902
1938
|
renderAudioMenu() {
|
|
1903
1939
|
const tracks = this.state.audioTracks;
|
|
1904
1940
|
const currentTrack = this.state.currentAudioTrack;
|
|
1905
|
-
const items =
|
|
1941
|
+
const items = tracks.length > 0
|
|
1942
|
+
? getAudioItems(tracks, currentTrack)
|
|
1943
|
+
: [{ label: this.i18n.t('settings.default'), value: 0, selected: true }];
|
|
1906
1944
|
return this.renderSubMenu(this.i18n.t('settings.audio'), items, (item) => {
|
|
1907
|
-
|
|
1945
|
+
if (tracks.length > 0) {
|
|
1946
|
+
this.bus.emit('settings-select-audio', { index: item.value });
|
|
1947
|
+
}
|
|
1908
1948
|
});
|
|
1909
1949
|
}
|
|
1910
1950
|
renderSubtitlesMenu() {
|
|
1911
1951
|
const tracks = this.state.subtitleTracks;
|
|
1912
1952
|
const currentTrack = this.state.currentSubtitleTrack;
|
|
1913
|
-
const items =
|
|
1953
|
+
const items = tracks.length > 0
|
|
1954
|
+
? getSubtitleItems(tracks, currentTrack)
|
|
1955
|
+
: [{ label: this.i18n.t('settings.off'), value: -1, selected: true }];
|
|
1914
1956
|
return this.renderSubMenu(this.i18n.t('settings.subtitles'), items, (item) => {
|
|
1915
|
-
|
|
1957
|
+
if (tracks.length > 0) {
|
|
1958
|
+
this.bus.emit('settings-select-subtitle', { index: item.value });
|
|
1959
|
+
}
|
|
1916
1960
|
});
|
|
1917
1961
|
}
|
|
1918
1962
|
addOutsideClickListener() {
|
|
@@ -2506,6 +2550,27 @@
|
|
|
2506
2550
|
}
|
|
2507
2551
|
}
|
|
2508
2552
|
|
|
2553
|
+
/**
|
|
2554
|
+
* ChannelName displays a static label (e.g. channel or program name)
|
|
2555
|
+
* in the bottom bar, next to the live badge and time display.
|
|
2556
|
+
*
|
|
2557
|
+
* Reads config.channelName and renders nothing if unset.
|
|
2558
|
+
*
|
|
2559
|
+
* Usage:
|
|
2560
|
+
* EBPlayer.start({ src: '...', skin: 'v2', channelName: 'ALOULA' })
|
|
2561
|
+
*/
|
|
2562
|
+
class ChannelName extends BaseComponent {
|
|
2563
|
+
onConnect() {
|
|
2564
|
+
this.render();
|
|
2565
|
+
}
|
|
2566
|
+
template() {
|
|
2567
|
+
const name = this.config.channelName;
|
|
2568
|
+
if (!name)
|
|
2569
|
+
return b ``;
|
|
2570
|
+
return b `<span class="eb-channel-name">${name}</span>`;
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2509
2574
|
/**
|
|
2510
2575
|
* Maps ComponentId strings to factory functions that create component instances.
|
|
2511
2576
|
* Used by SkinRoot.connectChildComponents() to dynamically mount components
|
|
@@ -2524,7 +2589,8 @@
|
|
|
2524
2589
|
'rewind': () => new RewindButton(),
|
|
2525
2590
|
'forward': () => new ForwardButton(),
|
|
2526
2591
|
'share': () => new ShareButton(),
|
|
2527
|
-
'info': () => new InfoButton()
|
|
2592
|
+
'info': () => new InfoButton(),
|
|
2593
|
+
'channel-name': () => new ChannelName()
|
|
2528
2594
|
};
|
|
2529
2595
|
/**
|
|
2530
2596
|
* Returns the CSS selector for a component's slot element.
|
|
@@ -2544,7 +2610,7 @@
|
|
|
2544
2610
|
* - Reconnecting: visible when state.reconnecting === true (shows "Reconnecting..." text)
|
|
2545
2611
|
*
|
|
2546
2612
|
* Hidden when not buffering and not reconnecting.
|
|
2547
|
-
* CSS
|
|
2613
|
+
* Renders a CSS arc ring that spins around the center (wrapping the play/pause button).
|
|
2548
2614
|
*/
|
|
2549
2615
|
class LoadingSpinner extends BaseComponent {
|
|
2550
2616
|
onConnect() {
|
|
@@ -2556,13 +2622,13 @@
|
|
|
2556
2622
|
const isBuffering = this.state.playbackState === 'buffering';
|
|
2557
2623
|
const isReconnecting = this.state.reconnecting;
|
|
2558
2624
|
if (!isBuffering && !isReconnecting) {
|
|
2559
|
-
return b `<div class="eb-loading" hidden aria-hidden="true"
|
|
2625
|
+
return b `<div class="eb-loading" hidden aria-hidden="true"></div>`;
|
|
2560
2626
|
}
|
|
2561
2627
|
const label = isReconnecting
|
|
2562
2628
|
? (this.i18n?.t('loading.reconnecting') ?? 'Reconnecting...')
|
|
2563
2629
|
: 'Loading';
|
|
2564
2630
|
return b `<div class="eb-loading" role="status" aria-label="${label}">
|
|
2565
|
-
|
|
2631
|
+
<div class="eb-loading-arc"></div>
|
|
2566
2632
|
${isReconnecting ? b `<span class="eb-loading-text">${label}</span>` : ''}
|
|
2567
2633
|
</div>`;
|
|
2568
2634
|
}
|
|
@@ -2730,6 +2796,8 @@
|
|
|
2730
2796
|
template() {
|
|
2731
2797
|
const playlist = this.state?.playlist ?? [];
|
|
2732
2798
|
const currentEpisode = this.state?.currentEpisode ?? 0;
|
|
2799
|
+
if (playlist.length === 0)
|
|
2800
|
+
return b ``;
|
|
2733
2801
|
return b `
|
|
2734
2802
|
<div class="eb-forja-playlist-bar">
|
|
2735
2803
|
<button
|
|
@@ -2933,7 +3001,7 @@
|
|
|
2933
3001
|
<video
|
|
2934
3002
|
class="eb-video"
|
|
2935
3003
|
playsinline
|
|
2936
|
-
|
|
3004
|
+
.muted="${config.muted}"
|
|
2937
3005
|
?autoplay="${config.autoplay}"
|
|
2938
3006
|
?controls="${isIOS}"
|
|
2939
3007
|
></video>
|
|
@@ -3063,16 +3131,14 @@
|
|
|
3063
3131
|
}
|
|
3064
3132
|
}
|
|
3065
3133
|
}
|
|
3066
|
-
// Loading spinner —
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
this.childComponents.push(loadingSpinner);
|
|
3075
|
-
}
|
|
3134
|
+
// Loading spinner — mounted directly into .eb-player (not overlay-zone)
|
|
3135
|
+
// so it escapes the overlay-zone stacking context and displays above the middle bar
|
|
3136
|
+
const loadingSlot = document.createElement('div');
|
|
3137
|
+
loadingSlot.className = 'eb-loading-slot';
|
|
3138
|
+
inner.appendChild(loadingSlot);
|
|
3139
|
+
const loadingSpinner = new LoadingSpinner();
|
|
3140
|
+
loadingSpinner.connect(loadingSlot, state, bus, config, i18n);
|
|
3141
|
+
this.childComponents.push(loadingSpinner);
|
|
3076
3142
|
// Interactive overlays — mounted directly into .eb-player (not overlay-zone)
|
|
3077
3143
|
// so they escape the overlay-zone stacking context and display above all controls
|
|
3078
3144
|
const interactiveOverlays = [
|
|
@@ -3103,7 +3169,7 @@
|
|
|
3103
3169
|
<video
|
|
3104
3170
|
class="eb-video"
|
|
3105
3171
|
playsinline
|
|
3106
|
-
|
|
3172
|
+
.muted="${config.muted}"
|
|
3107
3173
|
?autoplay="${config.autoplay}"
|
|
3108
3174
|
></video>
|
|
3109
3175
|
</div>
|
|
@@ -3900,10 +3966,14 @@
|
|
|
3900
3966
|
const skinColors = this.config.skinColors;
|
|
3901
3967
|
if (this.config.primaryColor) {
|
|
3902
3968
|
container.style.setProperty('--eb-color-primary', this.config.primaryColor);
|
|
3969
|
+
container.style.setProperty('--eb-color-progress', this.config.primaryColor);
|
|
3970
|
+
container.style.setProperty('--eb-accent', this.config.primaryColor);
|
|
3903
3971
|
}
|
|
3904
3972
|
// skinColors.general overrides primaryColor
|
|
3905
3973
|
if (skinColors?.general) {
|
|
3906
3974
|
container.style.setProperty('--eb-color-primary', skinColors.general);
|
|
3975
|
+
container.style.setProperty('--eb-color-progress', skinColors.general);
|
|
3976
|
+
container.style.setProperty('--eb-accent', skinColors.general);
|
|
3907
3977
|
}
|
|
3908
3978
|
if (skinColors?.progressBar) {
|
|
3909
3979
|
container.style.setProperty('--eb-color-progress', skinColors.progressBar);
|
|
@@ -4246,7 +4316,7 @@
|
|
|
4246
4316
|
// -------------------------------------------------------------------------
|
|
4247
4317
|
bindVideoEvents(video, state, signal) {
|
|
4248
4318
|
// Playback timing + live sync detection
|
|
4249
|
-
const syncMargin = this.config?.syncLiveMargin ??
|
|
4319
|
+
const syncMargin = this.config?.syncLiveMargin ?? 30;
|
|
4250
4320
|
video.addEventListener('timeupdate', () => {
|
|
4251
4321
|
state.currentTime = Math.round(video.currentTime);
|
|
4252
4322
|
if (state.isLive) {
|
|
@@ -4272,14 +4342,41 @@
|
|
|
4272
4342
|
state.playbackRate = video.playbackRate;
|
|
4273
4343
|
}, { signal });
|
|
4274
4344
|
// FSM transitions: playing, pause, waiting, ended
|
|
4345
|
+
//
|
|
4346
|
+
// Pause guard: browsers fire spurious 'pause' events when currentTime is
|
|
4347
|
+
// set programmatically (rewind/forward/seekbar). We only honour the 'pause'
|
|
4348
|
+
// event when the previous playback state was 'playing' — meaning the user
|
|
4349
|
+
// (or our code) explicitly called video.pause(). During a seek the state
|
|
4350
|
+
// is already 'buffering' by the time the late 'pause' arrives, so it's
|
|
4351
|
+
// harmlessly ignored.
|
|
4352
|
+
let userRequestedPause = false;
|
|
4275
4353
|
video.addEventListener('playing', () => {
|
|
4354
|
+
userRequestedPause = false;
|
|
4276
4355
|
state.playbackState = 'playing';
|
|
4277
4356
|
}, { signal });
|
|
4357
|
+
video.addEventListener('waiting', () => {
|
|
4358
|
+
state.playbackState = 'buffering';
|
|
4359
|
+
}, { signal });
|
|
4278
4360
|
video.addEventListener('pause', () => {
|
|
4361
|
+
if (video.seeking)
|
|
4362
|
+
return;
|
|
4363
|
+
if (state.playbackState === 'buffering') {
|
|
4364
|
+
userRequestedPause = true;
|
|
4365
|
+
return;
|
|
4366
|
+
}
|
|
4279
4367
|
state.playbackState = 'paused';
|
|
4280
4368
|
}, { signal });
|
|
4281
|
-
video.addEventListener('
|
|
4282
|
-
state
|
|
4369
|
+
video.addEventListener('seeked', () => {
|
|
4370
|
+
// Update live sync state immediately after seek so the UI reflects
|
|
4371
|
+
// the new position without waiting for the next timeupdate
|
|
4372
|
+
if (state.isLive) {
|
|
4373
|
+
state.currentTime = Math.round(video.currentTime);
|
|
4374
|
+
state.isSyncWithLive = video.currentTime + syncMargin > video.duration;
|
|
4375
|
+
}
|
|
4376
|
+
if (userRequestedPause && video.paused) {
|
|
4377
|
+
state.playbackState = 'paused';
|
|
4378
|
+
userRequestedPause = false;
|
|
4379
|
+
}
|
|
4283
4380
|
}, { signal });
|
|
4284
4381
|
video.addEventListener('ended', () => {
|
|
4285
4382
|
state.playbackState = 'ended';
|
|
@@ -5233,8 +5330,8 @@
|
|
|
5233
5330
|
seek(time) {
|
|
5234
5331
|
if (this.video === null)
|
|
5235
5332
|
return;
|
|
5236
|
-
// Disable hls.js live sync
|
|
5237
|
-
//
|
|
5333
|
+
// Disable hls.js live sync on first seek so it never auto-seeks
|
|
5334
|
+
// back to the live edge on DVR/timeshift streams.
|
|
5238
5335
|
if (!this.liveSyncDisabled && this.driver !== null && this.state?.isLive) {
|
|
5239
5336
|
const cfg = this.driver.config;
|
|
5240
5337
|
cfg.liveSyncDurationCount = 0;
|
|
@@ -5377,6 +5474,24 @@
|
|
|
5377
5474
|
}
|
|
5378
5475
|
});
|
|
5379
5476
|
}
|
|
5477
|
+
// Geo-block detection (opt-in, ported from v1.x).
|
|
5478
|
+
// Detects 403 on manifest/level load and surfaces it via the bus + a
|
|
5479
|
+
// window CustomEvent for back-compat with v1 host integrations.
|
|
5480
|
+
if (config.useGeoblockingErrorHandle) {
|
|
5481
|
+
const busRef = this.bus;
|
|
5482
|
+
driver.on(Hls.Events.ERROR, (_event, data) => {
|
|
5483
|
+
const error = data;
|
|
5484
|
+
if (error?.type === Hls.ErrorTypes.NETWORK_ERROR
|
|
5485
|
+
&& (error?.details === 'manifestLoadError' || error?.details === 'levelLoadError')
|
|
5486
|
+
&& error?.response?.code === 403) {
|
|
5487
|
+
busRef?.emit('geoblock-detected');
|
|
5488
|
+
window.dispatchEvent(new CustomEvent('geoblock', {
|
|
5489
|
+
detail: { title: 'Geoblock', message: 'Geoblock detected' }
|
|
5490
|
+
}));
|
|
5491
|
+
this.stopWatchdog();
|
|
5492
|
+
}
|
|
5493
|
+
});
|
|
5494
|
+
}
|
|
5380
5495
|
// Bind native video element events to state
|
|
5381
5496
|
this.bindVideoEvents(video, state, signal);
|
|
5382
5497
|
// Attach media and load source
|
|
@@ -6368,21 +6483,37 @@
|
|
|
6368
6483
|
/**
|
|
6369
6484
|
* EBPlayer facade — consumer-facing API layer.
|
|
6370
6485
|
*
|
|
6371
|
-
* Provides the window.EBPlayer interface
|
|
6486
|
+
* Provides the window.EBPlayer interface:
|
|
6372
6487
|
* - window.EBPlayer.start(config) — synchronously returns a PlayerReference
|
|
6373
|
-
* - window.EBPlayer.stop() — detaches
|
|
6374
|
-
* - window.EBPlayer.destroy() — disposes
|
|
6488
|
+
* - window.EBPlayer.stop() — detaches all active engines (skins stay mounted)
|
|
6489
|
+
* - window.EBPlayer.destroy() — disposes all controllers (tears down everything)
|
|
6375
6490
|
*
|
|
6376
|
-
*
|
|
6377
|
-
*
|
|
6491
|
+
* Supports multiple simultaneous player instances on the same page.
|
|
6492
|
+
* Each instance is keyed by its container element:
|
|
6493
|
+
* - Calling start() with the SAME container disposes the old instance first (backward compat).
|
|
6494
|
+
* - Calling start() with a DIFFERENT container creates an independent player.
|
|
6495
|
+
*
|
|
6496
|
+
* Per-instance cleanup is available via PlayerReference.destroy().
|
|
6497
|
+
* Global stop()/destroy() operate on all instances at once.
|
|
6378
6498
|
*/
|
|
6379
6499
|
// Import from barrel — triggers CSS imports (base.css + skin.css) for postcss extraction
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6500
|
+
/** Active player instances keyed by their container element. */
|
|
6501
|
+
const instances = new Map();
|
|
6502
|
+
/**
|
|
6503
|
+
* Tear down a single instance: snapshot handler, engine, then controller.
|
|
6504
|
+
* Does NOT remove the entry from the instances Map (caller must do that).
|
|
6505
|
+
*/
|
|
6506
|
+
function destroyInstance(inst) {
|
|
6507
|
+
if (inst.snapshotDestroy !== null) {
|
|
6508
|
+
inst.snapshotDestroy();
|
|
6509
|
+
inst.snapshotDestroy = null;
|
|
6510
|
+
}
|
|
6511
|
+
if (inst.engine !== null) {
|
|
6512
|
+
inst.engine.detach();
|
|
6513
|
+
inst.engine = null;
|
|
6514
|
+
}
|
|
6515
|
+
inst.controller.dispose();
|
|
6516
|
+
}
|
|
6386
6517
|
// ---------------------------------------------------------------------------
|
|
6387
6518
|
// Container resolution
|
|
6388
6519
|
// ---------------------------------------------------------------------------
|
|
@@ -6425,31 +6556,44 @@
|
|
|
6425
6556
|
// ---------------------------------------------------------------------------
|
|
6426
6557
|
/**
|
|
6427
6558
|
* Start the player with the given config.
|
|
6428
|
-
*
|
|
6559
|
+
*
|
|
6560
|
+
* If a player is already mounted on the same container element, it is disposed
|
|
6561
|
+
* before the new one is created (backward-compatible single-instance behavior).
|
|
6562
|
+
* If the container is different, the new player coexists alongside existing ones
|
|
6563
|
+
* (multi-instance support).
|
|
6429
6564
|
*
|
|
6430
6565
|
* Returns a PlayerReference synchronously — never returns a Promise.
|
|
6431
6566
|
*/
|
|
6432
6567
|
function start(runtimeConfig) {
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6568
|
+
const container = resolveContainer(runtimeConfig.el);
|
|
6569
|
+
// Dispose any existing instance on the SAME container (backward compat).
|
|
6570
|
+
// Instances on OTHER containers are left untouched (multi-instance).
|
|
6571
|
+
const existing = instances.get(container);
|
|
6572
|
+
if (existing !== undefined) {
|
|
6573
|
+
destroyInstance(existing);
|
|
6574
|
+
instances.delete(container);
|
|
6436
6575
|
}
|
|
6437
6576
|
const mergedConfig = mergeConfig(DEFAULT_CONFIG, runtimeConfig);
|
|
6438
6577
|
const controller = new PlayerController(runtimeConfig);
|
|
6439
|
-
const container = resolveContainer(runtimeConfig.el);
|
|
6440
6578
|
controller.mount(container);
|
|
6441
6579
|
// Video element is available after mount() creates the skin DOM
|
|
6442
6580
|
const video = container.querySelector('video');
|
|
6443
|
-
|
|
6444
|
-
|
|
6581
|
+
// Per-instance state — captured by the reference closure, never shared across instances
|
|
6582
|
+
const inst = {
|
|
6583
|
+
controller,
|
|
6584
|
+
engine: null,
|
|
6585
|
+
snapshotDestroy: null,
|
|
6586
|
+
container
|
|
6587
|
+
};
|
|
6588
|
+
instances.set(container, inst);
|
|
6445
6589
|
let lastSrc = '';
|
|
6446
6590
|
const reference = {
|
|
6447
6591
|
open(src) {
|
|
6448
6592
|
lastSrc = src;
|
|
6449
6593
|
// Detach any existing engine before opening a new stream
|
|
6450
|
-
if (
|
|
6451
|
-
|
|
6452
|
-
|
|
6594
|
+
if (inst.engine !== null) {
|
|
6595
|
+
inst.engine.detach();
|
|
6596
|
+
inst.engine = null;
|
|
6453
6597
|
}
|
|
6454
6598
|
const engine = selectEngine(src, mergedConfig);
|
|
6455
6599
|
if (video !== null) {
|
|
@@ -6458,7 +6602,7 @@
|
|
|
6458
6602
|
engine.setBus(controller.bus);
|
|
6459
6603
|
engine.setConfig(mergedConfig);
|
|
6460
6604
|
controller.setEngineSync(engine);
|
|
6461
|
-
|
|
6605
|
+
inst.engine = engine;
|
|
6462
6606
|
// P2P integration: wire EasyBroadcast SDK when config.lib and config.manager are set
|
|
6463
6607
|
if (mergedConfig.lib && mergedConfig.manager && video !== null) {
|
|
6464
6608
|
const isDash = src.includes('.mpd') && mergedConfig.dashjs !== false;
|
|
@@ -6479,9 +6623,9 @@
|
|
|
6479
6623
|
// Snapshot handler: create snapshot engine for seekbar preview thumbnails
|
|
6480
6624
|
if (mergedConfig.showProgressThumb) {
|
|
6481
6625
|
// Clean up any previous snapshot handler
|
|
6482
|
-
if (
|
|
6483
|
-
|
|
6484
|
-
|
|
6626
|
+
if (inst.snapshotDestroy !== null) {
|
|
6627
|
+
inst.snapshotDestroy();
|
|
6628
|
+
inst.snapshotDestroy = null;
|
|
6485
6629
|
}
|
|
6486
6630
|
const isDash = src.includes('.mpd') && mergedConfig.dashjs !== false;
|
|
6487
6631
|
// Wait for engine driver to be ready (CDN script loaded) before initializing snapshot
|
|
@@ -6492,7 +6636,7 @@
|
|
|
6492
6636
|
const handler = new DashSnapshotHandler(src);
|
|
6493
6637
|
handler.init(win.dashjs)
|
|
6494
6638
|
.then((handle) => {
|
|
6495
|
-
|
|
6639
|
+
inst.snapshotDestroy = () => handle.destroy();
|
|
6496
6640
|
controller.bus.emit('snapshot-handler-ready', { take: handle.take, video: handle.video });
|
|
6497
6641
|
})
|
|
6498
6642
|
.catch((error) => {
|
|
@@ -6512,7 +6656,7 @@
|
|
|
6512
6656
|
const handler = new HlsSnapshotHandler({ src, engineSettings: { ...mergedConfig.engineSettings, ...snapshotDrmConfig } }, sharedTokenManager);
|
|
6513
6657
|
handler.init(win.Hls)
|
|
6514
6658
|
.then(() => {
|
|
6515
|
-
|
|
6659
|
+
inst.snapshotDestroy = () => handler.destroy();
|
|
6516
6660
|
const snapshotVideo = handler.getVideo();
|
|
6517
6661
|
if (snapshotVideo !== null) {
|
|
6518
6662
|
controller.bus.emit('snapshot-handler-ready', { take: (time) => handler.take(time), video: snapshotVideo });
|
|
@@ -6547,15 +6691,19 @@
|
|
|
6547
6691
|
}
|
|
6548
6692
|
},
|
|
6549
6693
|
close() {
|
|
6550
|
-
if (
|
|
6551
|
-
|
|
6552
|
-
|
|
6694
|
+
if (inst.snapshotDestroy !== null) {
|
|
6695
|
+
inst.snapshotDestroy();
|
|
6696
|
+
inst.snapshotDestroy = null;
|
|
6553
6697
|
}
|
|
6554
|
-
if (
|
|
6555
|
-
|
|
6556
|
-
|
|
6698
|
+
if (inst.engine !== null) {
|
|
6699
|
+
inst.engine.detach();
|
|
6700
|
+
inst.engine = null;
|
|
6557
6701
|
}
|
|
6558
6702
|
},
|
|
6703
|
+
destroy() {
|
|
6704
|
+
destroyInstance(inst);
|
|
6705
|
+
instances.delete(container);
|
|
6706
|
+
},
|
|
6559
6707
|
get state() {
|
|
6560
6708
|
return controller.state;
|
|
6561
6709
|
}
|
|
@@ -6576,10 +6724,10 @@
|
|
|
6576
6724
|
reference.open(currentSrc);
|
|
6577
6725
|
// Restore selections after manifest loads (~2s pragmatic delay for async manifest parsing)
|
|
6578
6726
|
setTimeout(() => {
|
|
6579
|
-
if (
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6727
|
+
if (inst.engine !== null) {
|
|
6728
|
+
inst.engine.setQuality(saved.quality);
|
|
6729
|
+
inst.engine.setAudioTrack(saved.audioTrack);
|
|
6730
|
+
inst.engine.setSubtitle(saved.subtitleTrack);
|
|
6583
6731
|
}
|
|
6584
6732
|
}, 2000);
|
|
6585
6733
|
}, 500);
|
|
@@ -6595,9 +6743,24 @@
|
|
|
6595
6743
|
else {
|
|
6596
6744
|
let deferredOpen = true;
|
|
6597
6745
|
controller.bus.on('play', () => {
|
|
6598
|
-
if (deferredOpen)
|
|
6599
|
-
|
|
6600
|
-
|
|
6746
|
+
if (!deferredOpen)
|
|
6747
|
+
return;
|
|
6748
|
+
deferredOpen = false;
|
|
6749
|
+
reference.open(mergedConfig.src);
|
|
6750
|
+
// Honor the user's click intent. CommandHandler's earlier bus.on('play')
|
|
6751
|
+
// listener fired before us with no source attached, so its video.play()
|
|
6752
|
+
// was a silent no-op. Wait for driverReady (attachMedia + loadSource
|
|
6753
|
+
// done) then start playback ourselves.
|
|
6754
|
+
const engine = inst.engine;
|
|
6755
|
+
if (engine !== null && video !== null) {
|
|
6756
|
+
engine.driverReady.then(() => {
|
|
6757
|
+
const playResult = video.play();
|
|
6758
|
+
if (playResult && typeof playResult.catch === 'function') {
|
|
6759
|
+
playResult.catch((error) => {
|
|
6760
|
+
console.warn('EBPlayer: deferred play() rejected', error);
|
|
6761
|
+
});
|
|
6762
|
+
}
|
|
6763
|
+
});
|
|
6601
6764
|
}
|
|
6602
6765
|
}, { signal: controller.signal });
|
|
6603
6766
|
}
|
|
@@ -6605,28 +6768,28 @@
|
|
|
6605
6768
|
return reference;
|
|
6606
6769
|
}
|
|
6607
6770
|
/**
|
|
6608
|
-
* Stop
|
|
6609
|
-
* Detaches
|
|
6771
|
+
* Stop all active streams.
|
|
6772
|
+
* Detaches engines without disposing controllers (skins stay mounted).
|
|
6773
|
+
* For single-instance usage this behaves identically to the old global stop().
|
|
6610
6774
|
*/
|
|
6611
6775
|
function stop() {
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6776
|
+
for (const inst of instances.values()) {
|
|
6777
|
+
if (inst.engine !== null) {
|
|
6778
|
+
inst.engine.detach();
|
|
6779
|
+
inst.engine = null;
|
|
6780
|
+
}
|
|
6615
6781
|
}
|
|
6616
6782
|
}
|
|
6617
6783
|
/**
|
|
6618
|
-
* Destroy
|
|
6619
|
-
* Disposes
|
|
6784
|
+
* Destroy all player instances completely.
|
|
6785
|
+
* Disposes every controller (tears down skin, events, and all resources).
|
|
6786
|
+
* For single-instance usage this behaves identically to the old global destroy().
|
|
6620
6787
|
*/
|
|
6621
6788
|
function destroy() {
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
activeEngine = null;
|
|
6625
|
-
}
|
|
6626
|
-
if (activeController !== null) {
|
|
6627
|
-
activeController.dispose();
|
|
6628
|
-
activeController = null;
|
|
6789
|
+
for (const inst of instances.values()) {
|
|
6790
|
+
destroyInstance(inst);
|
|
6629
6791
|
}
|
|
6792
|
+
instances.clear();
|
|
6630
6793
|
}
|
|
6631
6794
|
// ---------------------------------------------------------------------------
|
|
6632
6795
|
// Version
|