eb-player 2.0.15 → 2.0.18
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 +309 -112
- 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/fsm.d.ts +4 -1
- package/dist/build/types/core/fsm.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/core/player-state.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/integrations/p2p-manager.d.ts +9 -0
- package/dist/build/types/integrations/p2p-manager.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 +4 -2
|
@@ -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.18";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Finite State Machine for player playback state transitions.
|
|
@@ -19,7 +19,10 @@
|
|
|
19
19
|
* - idle -> loading: initial load or post-stall reload
|
|
20
20
|
* - loading -> playing | error: stream starts or fails
|
|
21
21
|
* - playing -> paused | buffering | ended | error: normal playback events
|
|
22
|
-
* - paused -> playing | loading | idle: resume or stop
|
|
22
|
+
* - paused -> playing | loading | idle | buffering: resume or stop
|
|
23
|
+
* - paused -> buffering is needed when resuming a paused (live) stream where
|
|
24
|
+
* the browser fires 'waiting' before 'playing' (Safari live segment edge);
|
|
25
|
+
* without it the FSM strands at 'paused' while the video actually plays
|
|
23
26
|
* - buffering -> playing | paused | idle | error: buffer recovered, user pause, or stall
|
|
24
27
|
* - buffering -> idle is needed for the stall-watchdog recovery path (kick off reload)
|
|
25
28
|
* - buffering -> paused is needed for live stream support (user pauses during buffer)
|
|
@@ -30,7 +33,7 @@
|
|
|
30
33
|
idle: ['loading'],
|
|
31
34
|
loading: ['playing', 'buffering', 'paused', 'error', 'idle'],
|
|
32
35
|
playing: ['paused', 'buffering', 'ended', 'error', 'idle'],
|
|
33
|
-
paused: ['playing', 'loading', 'idle'],
|
|
36
|
+
paused: ['playing', 'loading', 'idle', 'buffering'],
|
|
34
37
|
buffering: ['playing', 'paused', 'idle', 'error'],
|
|
35
38
|
error: ['loading', 'idle'],
|
|
36
39
|
ended: ['loading', 'idle']
|
|
@@ -175,7 +178,11 @@
|
|
|
175
178
|
if (previousValue === value) {
|
|
176
179
|
return true;
|
|
177
180
|
}
|
|
178
|
-
|
|
181
|
+
// Assign and notify subscribers.
|
|
182
|
+
// Cast the typed StateMap target to a mutable record so the unknown value
|
|
183
|
+
// can be written through the Proxy set trap.
|
|
184
|
+
const writable = rawTarget;
|
|
185
|
+
writable[String(key)] = value;
|
|
179
186
|
impl.notify(key, value, previousValue);
|
|
180
187
|
return true;
|
|
181
188
|
}
|
|
@@ -287,7 +294,7 @@
|
|
|
287
294
|
{ value: 'radio', label: 'Radio', primaryColor: '#F4A261' },
|
|
288
295
|
{ value: 'snrt', label: 'SNRT', primaryColor: '#006633' },
|
|
289
296
|
{ value: 'modern', label: 'Modern', primaryColor: '#7c3aed' },
|
|
290
|
-
{ value: 'v2', label: 'V2', primaryColor: '#
|
|
297
|
+
{ value: 'v2', label: 'V2', primaryColor: '#3d4097' },
|
|
291
298
|
{ value: 'lequipe', label: "L'Equipe", primaryColor: '#d61e00' },
|
|
292
299
|
];
|
|
293
300
|
/**
|
|
@@ -297,12 +304,12 @@
|
|
|
297
304
|
const V2_LAYOUT = {
|
|
298
305
|
topBar: {
|
|
299
306
|
left: [],
|
|
300
|
-
right: ['
|
|
307
|
+
right: ['pip', 'settings']
|
|
301
308
|
},
|
|
302
309
|
bottomBar: {
|
|
303
|
-
left: ['
|
|
304
|
-
center: ['seekbar'],
|
|
305
|
-
right: [
|
|
310
|
+
left: ['live-sync', 'time', 'channel-name'],
|
|
311
|
+
center: ['play-pause', 'seekbar', 'volume', 'fullscreen'],
|
|
312
|
+
right: []
|
|
306
313
|
},
|
|
307
314
|
middleBar: {
|
|
308
315
|
left: ['rewind'],
|
|
@@ -326,10 +333,29 @@
|
|
|
326
333
|
right: ['forward']
|
|
327
334
|
}
|
|
328
335
|
};
|
|
336
|
+
const FORJA_LAYOUT = {
|
|
337
|
+
topBar: {
|
|
338
|
+
left: [],
|
|
339
|
+
right: []
|
|
340
|
+
},
|
|
341
|
+
bottomBar: {
|
|
342
|
+
// Row 1: seekbar (full width)
|
|
343
|
+
// Row 2: play-pause + volume (left) | live-sync + settings + pip + fullscreen (right)
|
|
344
|
+
left: ['seekbar'],
|
|
345
|
+
center: ['play-pause', 'volume'],
|
|
346
|
+
right: ['live-sync', 'settings', 'pip', 'fullscreen']
|
|
347
|
+
},
|
|
348
|
+
middleBar: {
|
|
349
|
+
left: ['rewind'],
|
|
350
|
+
center: [],
|
|
351
|
+
right: ['forward']
|
|
352
|
+
}
|
|
353
|
+
};
|
|
329
354
|
const THEME_LAYOUTS = {
|
|
330
355
|
v2: V2_LAYOUT,
|
|
331
356
|
lequipe: LEQUIPE_LAYOUT,
|
|
332
357
|
modern: V2_LAYOUT,
|
|
358
|
+
forja: FORJA_LAYOUT,
|
|
333
359
|
};
|
|
334
360
|
/**
|
|
335
361
|
* Returns the effective layout for a given config.
|
|
@@ -355,7 +381,7 @@
|
|
|
355
381
|
// Drivers
|
|
356
382
|
dashjs: 'https://reference.dashif.org/dash.js/v4.7.4/dist/dash.all.min.js',
|
|
357
383
|
hlsjs: 'https://cdn.jsdelivr.net/npm/hls.js@1.6.10/dist/hls.min.js',
|
|
358
|
-
engineSettings: { liveSyncDurationCount:
|
|
384
|
+
engineSettings: { liveSyncDurationCount: 3 },
|
|
359
385
|
chromecast: 'https://cdnjs.cloudflare.com/ajax/libs/castjs/5.3.0/cast.min.js',
|
|
360
386
|
chromecastApp: undefined,
|
|
361
387
|
chromecastMetadata: undefined,
|
|
@@ -396,7 +422,7 @@
|
|
|
396
422
|
forceAutoplay: false,
|
|
397
423
|
forceQuality: false,
|
|
398
424
|
liveButton: false,
|
|
399
|
-
syncLiveMargin:
|
|
425
|
+
syncLiveMargin: 30,
|
|
400
426
|
isLive: undefined,
|
|
401
427
|
noUi: false,
|
|
402
428
|
preroll: false,
|
|
@@ -438,6 +464,8 @@
|
|
|
438
464
|
epgDefaultLang: 'en',
|
|
439
465
|
showEpgTitlePreview: false,
|
|
440
466
|
showProgressThumb: false,
|
|
467
|
+
// Display
|
|
468
|
+
channelName: undefined,
|
|
441
469
|
// Layout
|
|
442
470
|
layout: undefined
|
|
443
471
|
};
|
|
@@ -545,6 +573,12 @@
|
|
|
545
573
|
ar: 'The stream is unavailable. Please try again later.',
|
|
546
574
|
es: 'La transmisión no está disponible. Por favor, inténtelo más tarde.'
|
|
547
575
|
},
|
|
576
|
+
'error.geoblocked': {
|
|
577
|
+
en: 'This content is not available in your region.',
|
|
578
|
+
fr: 'Ce contenu n\'est pas disponible dans votre région.',
|
|
579
|
+
ar: 'This content is not available in your region.',
|
|
580
|
+
es: 'Este contenido no está disponible en tu región.'
|
|
581
|
+
},
|
|
548
582
|
'cast.failed': {
|
|
549
583
|
en: 'Casting failed. Resuming local playback.',
|
|
550
584
|
fr: 'La diffusion a échoué. Reprise de la lecture locale.',
|
|
@@ -686,6 +720,7 @@
|
|
|
686
720
|
this.wireReloadOrchestration(bus, state, i18n, onReload, signal);
|
|
687
721
|
this.wireCastHandoff(bus, video, state, chromecastManager, i18n, signal);
|
|
688
722
|
this.wireVolumeRouting(state, chromecastManager, signal);
|
|
723
|
+
this.wireGeoblock(bus, state, i18n, signal);
|
|
689
724
|
this.wireAbortCleanup(signal);
|
|
690
725
|
}
|
|
691
726
|
// ---------------------------------------------------------------------------
|
|
@@ -819,6 +854,11 @@
|
|
|
819
854
|
}
|
|
820
855
|
}, { signal });
|
|
821
856
|
}
|
|
857
|
+
wireGeoblock(bus, state, i18n, signal) {
|
|
858
|
+
bus.on('geoblock-detected', () => {
|
|
859
|
+
state.error = i18n.t('error.geoblocked');
|
|
860
|
+
}, { signal });
|
|
861
|
+
}
|
|
822
862
|
showToast(state, message) {
|
|
823
863
|
state.toast = message;
|
|
824
864
|
clearTimeout(this.toastTimer);
|
|
@@ -1117,7 +1157,7 @@
|
|
|
1117
1157
|
}
|
|
1118
1158
|
template() {
|
|
1119
1159
|
const state = this.state;
|
|
1120
|
-
const isPlaying = state.playbackState === 'playing';
|
|
1160
|
+
const isPlaying = state.playbackState === 'playing' || state.playbackState === 'buffering';
|
|
1121
1161
|
const layout = resolveLayout(this.config).middleBar;
|
|
1122
1162
|
const slot = (componentId) => b `<div class="eb-slot-${componentId}"></div>`;
|
|
1123
1163
|
return b `
|
|
@@ -1137,7 +1177,7 @@
|
|
|
1137
1177
|
handleClick() {
|
|
1138
1178
|
const state = this.state;
|
|
1139
1179
|
const bus = this.bus;
|
|
1140
|
-
if (state.playbackState === 'playing') {
|
|
1180
|
+
if (state.playbackState === 'playing' || state.playbackState === 'buffering') {
|
|
1141
1181
|
bus.emit('pause');
|
|
1142
1182
|
}
|
|
1143
1183
|
else {
|
|
@@ -1159,7 +1199,8 @@
|
|
|
1159
1199
|
this.render();
|
|
1160
1200
|
}
|
|
1161
1201
|
template() {
|
|
1162
|
-
const
|
|
1202
|
+
const playbackState = this.state.playbackState;
|
|
1203
|
+
const isPlaying = playbackState === 'playing' || playbackState === 'buffering';
|
|
1163
1204
|
const ariaLabel = isPlaying ? 'Pause' : 'Play';
|
|
1164
1205
|
const iconName = isPlaying ? 'pause' : 'play';
|
|
1165
1206
|
return b `
|
|
@@ -1611,16 +1652,18 @@
|
|
|
1611
1652
|
}
|
|
1612
1653
|
template() {
|
|
1613
1654
|
const { currentTime, duration, isLive, isSyncWithLive } = this.state;
|
|
1655
|
+
if (!this.config.seekbar) {
|
|
1656
|
+
return b `<div class="eb-time-display" hidden></div>`;
|
|
1657
|
+
}
|
|
1614
1658
|
let timeText;
|
|
1615
1659
|
if (isLive) {
|
|
1616
1660
|
if (isSyncWithLive || !Number.isFinite(duration)) {
|
|
1617
|
-
// At the live edge or non-DVR stream: show current wall-clock time
|
|
1618
1661
|
timeText = formatWallClock(Date.now());
|
|
1619
1662
|
}
|
|
1620
1663
|
else {
|
|
1621
|
-
// Behind
|
|
1664
|
+
// Behind live edge: show wall-clock time + negative offset
|
|
1622
1665
|
const offset = duration - currentTime;
|
|
1623
|
-
|
|
1666
|
+
return b `<div class="eb-time-display">${formatWallClock(Date.now())} <span class="eb-time-display__offset">-${formatDuration(offset)}</span></div>`;
|
|
1624
1667
|
}
|
|
1625
1668
|
}
|
|
1626
1669
|
else {
|
|
@@ -1652,7 +1695,7 @@
|
|
|
1652
1695
|
template() {
|
|
1653
1696
|
const { isLive, isSyncWithLive } = this.state;
|
|
1654
1697
|
const configLive = this.config.liveButton === true || this.config.isLive === true;
|
|
1655
|
-
if (!isLive && !configLive) {
|
|
1698
|
+
if (!this.config.seekbar || (!isLive && !configLive)) {
|
|
1656
1699
|
return b `<button class="eb-live-sync" hidden style="display:none">${icon('live')}</button>`;
|
|
1657
1700
|
}
|
|
1658
1701
|
return b `
|
|
@@ -1818,15 +1861,19 @@
|
|
|
1818
1861
|
}
|
|
1819
1862
|
if (mode === 'audio') {
|
|
1820
1863
|
const tracks = this.state.audioTracks;
|
|
1864
|
+
if (tracks.length === 0)
|
|
1865
|
+
return i18n.t('settings.default');
|
|
1821
1866
|
const current = this.state.currentAudioTrack;
|
|
1822
1867
|
const track = tracks[current];
|
|
1823
1868
|
return track?.name || track?.lang || i18n.t('settings.default');
|
|
1824
1869
|
}
|
|
1825
1870
|
if (mode === 'subtitles') {
|
|
1871
|
+
const tracks = this.state.subtitleTracks;
|
|
1872
|
+
if (tracks.length === 0)
|
|
1873
|
+
return i18n.t('settings.off');
|
|
1826
1874
|
const current = this.state.currentSubtitleTrack;
|
|
1827
1875
|
if (current === -1)
|
|
1828
1876
|
return i18n.t('settings.off');
|
|
1829
|
-
const tracks = this.state.subtitleTracks;
|
|
1830
1877
|
const track = tracks[current];
|
|
1831
1878
|
return track?.name || track?.lang || `Track ${current}`;
|
|
1832
1879
|
}
|
|
@@ -1834,13 +1881,9 @@
|
|
|
1834
1881
|
}
|
|
1835
1882
|
renderRootMenu() {
|
|
1836
1883
|
const qualityLevels = this.state.qualityLevels;
|
|
1837
|
-
const audioTracks = this.state.audioTracks;
|
|
1838
|
-
const subtitleTracks = this.state.subtitleTracks;
|
|
1839
1884
|
const i18n = this.i18n;
|
|
1840
1885
|
const showQuality = qualityLevels.length > 0;
|
|
1841
1886
|
const showSpeed = this.config.speed === true;
|
|
1842
|
-
const showAudio = audioTracks.length > 1;
|
|
1843
|
-
const showSubtitles = subtitleTracks.length > 0;
|
|
1844
1887
|
const row = (iconName, label, value, mode) => b `
|
|
1845
1888
|
<li>
|
|
1846
1889
|
<button class="eb-settings-category" @click="${() => this.navigateTo(mode)}">
|
|
@@ -1853,8 +1896,8 @@
|
|
|
1853
1896
|
`;
|
|
1854
1897
|
return b `
|
|
1855
1898
|
<ul class="eb-settings-menu eb-settings-root">
|
|
1856
|
-
${
|
|
1857
|
-
${
|
|
1899
|
+
${row('audio', i18n.t('settings.audio'), this.currentValueLabel('audio'), 'audio')}
|
|
1900
|
+
${row('subtitle', i18n.t('settings.subtitles'), this.currentValueLabel('subtitles'), 'subtitles')}
|
|
1858
1901
|
${showSpeed ? row('speed', i18n.t('settings.speed'), this.currentValueLabel('speed'), 'speed') : ''}
|
|
1859
1902
|
${showQuality ? row('quality', i18n.t('settings.quality'), this.currentValueLabel('quality'), 'quality') : ''}
|
|
1860
1903
|
</ul>
|
|
@@ -1902,17 +1945,25 @@
|
|
|
1902
1945
|
renderAudioMenu() {
|
|
1903
1946
|
const tracks = this.state.audioTracks;
|
|
1904
1947
|
const currentTrack = this.state.currentAudioTrack;
|
|
1905
|
-
const items =
|
|
1948
|
+
const items = tracks.length > 0
|
|
1949
|
+
? getAudioItems(tracks, currentTrack)
|
|
1950
|
+
: [{ label: this.i18n.t('settings.default'), value: 0, selected: true }];
|
|
1906
1951
|
return this.renderSubMenu(this.i18n.t('settings.audio'), items, (item) => {
|
|
1907
|
-
|
|
1952
|
+
if (tracks.length > 0) {
|
|
1953
|
+
this.bus.emit('settings-select-audio', { index: item.value });
|
|
1954
|
+
}
|
|
1908
1955
|
});
|
|
1909
1956
|
}
|
|
1910
1957
|
renderSubtitlesMenu() {
|
|
1911
1958
|
const tracks = this.state.subtitleTracks;
|
|
1912
1959
|
const currentTrack = this.state.currentSubtitleTrack;
|
|
1913
|
-
const items =
|
|
1960
|
+
const items = tracks.length > 0
|
|
1961
|
+
? getSubtitleItems(tracks, currentTrack)
|
|
1962
|
+
: [{ label: this.i18n.t('settings.off'), value: -1, selected: true }];
|
|
1914
1963
|
return this.renderSubMenu(this.i18n.t('settings.subtitles'), items, (item) => {
|
|
1915
|
-
|
|
1964
|
+
if (tracks.length > 0) {
|
|
1965
|
+
this.bus.emit('settings-select-subtitle', { index: item.value });
|
|
1966
|
+
}
|
|
1916
1967
|
});
|
|
1917
1968
|
}
|
|
1918
1969
|
addOutsideClickListener() {
|
|
@@ -2506,6 +2557,27 @@
|
|
|
2506
2557
|
}
|
|
2507
2558
|
}
|
|
2508
2559
|
|
|
2560
|
+
/**
|
|
2561
|
+
* ChannelName displays a static label (e.g. channel or program name)
|
|
2562
|
+
* in the bottom bar, next to the live badge and time display.
|
|
2563
|
+
*
|
|
2564
|
+
* Reads config.channelName and renders nothing if unset.
|
|
2565
|
+
*
|
|
2566
|
+
* Usage:
|
|
2567
|
+
* EBPlayer.start({ src: '...', skin: 'v2', channelName: 'ALOULA' })
|
|
2568
|
+
*/
|
|
2569
|
+
class ChannelName extends BaseComponent {
|
|
2570
|
+
onConnect() {
|
|
2571
|
+
this.render();
|
|
2572
|
+
}
|
|
2573
|
+
template() {
|
|
2574
|
+
const name = this.config.channelName;
|
|
2575
|
+
if (!name)
|
|
2576
|
+
return b ``;
|
|
2577
|
+
return b `<span class="eb-channel-name">${name}</span>`;
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2509
2581
|
/**
|
|
2510
2582
|
* Maps ComponentId strings to factory functions that create component instances.
|
|
2511
2583
|
* Used by SkinRoot.connectChildComponents() to dynamically mount components
|
|
@@ -2524,7 +2596,8 @@
|
|
|
2524
2596
|
'rewind': () => new RewindButton(),
|
|
2525
2597
|
'forward': () => new ForwardButton(),
|
|
2526
2598
|
'share': () => new ShareButton(),
|
|
2527
|
-
'info': () => new InfoButton()
|
|
2599
|
+
'info': () => new InfoButton(),
|
|
2600
|
+
'channel-name': () => new ChannelName()
|
|
2528
2601
|
};
|
|
2529
2602
|
/**
|
|
2530
2603
|
* Returns the CSS selector for a component's slot element.
|
|
@@ -2544,7 +2617,7 @@
|
|
|
2544
2617
|
* - Reconnecting: visible when state.reconnecting === true (shows "Reconnecting..." text)
|
|
2545
2618
|
*
|
|
2546
2619
|
* Hidden when not buffering and not reconnecting.
|
|
2547
|
-
* CSS
|
|
2620
|
+
* Renders a CSS arc ring that spins around the center (wrapping the play/pause button).
|
|
2548
2621
|
*/
|
|
2549
2622
|
class LoadingSpinner extends BaseComponent {
|
|
2550
2623
|
onConnect() {
|
|
@@ -2556,13 +2629,13 @@
|
|
|
2556
2629
|
const isBuffering = this.state.playbackState === 'buffering';
|
|
2557
2630
|
const isReconnecting = this.state.reconnecting;
|
|
2558
2631
|
if (!isBuffering && !isReconnecting) {
|
|
2559
|
-
return b `<div class="eb-loading" hidden aria-hidden="true"
|
|
2632
|
+
return b `<div class="eb-loading" hidden aria-hidden="true"></div>`;
|
|
2560
2633
|
}
|
|
2561
2634
|
const label = isReconnecting
|
|
2562
2635
|
? (this.i18n?.t('loading.reconnecting') ?? 'Reconnecting...')
|
|
2563
2636
|
: 'Loading';
|
|
2564
2637
|
return b `<div class="eb-loading" role="status" aria-label="${label}">
|
|
2565
|
-
|
|
2638
|
+
<div class="eb-loading-arc"></div>
|
|
2566
2639
|
${isReconnecting ? b `<span class="eb-loading-text">${label}</span>` : ''}
|
|
2567
2640
|
</div>`;
|
|
2568
2641
|
}
|
|
@@ -2728,8 +2801,10 @@
|
|
|
2728
2801
|
}, { signal: this.signal });
|
|
2729
2802
|
}
|
|
2730
2803
|
template() {
|
|
2731
|
-
const playlist = this.state
|
|
2732
|
-
const currentEpisode = this.state
|
|
2804
|
+
const playlist = this.state.playlist;
|
|
2805
|
+
const currentEpisode = this.state.currentEpisode;
|
|
2806
|
+
if (playlist.length === 0)
|
|
2807
|
+
return b ``;
|
|
2733
2808
|
return b `
|
|
2734
2809
|
<div class="eb-forja-playlist-bar">
|
|
2735
2810
|
<button
|
|
@@ -2793,7 +2868,7 @@
|
|
|
2793
2868
|
}, { signal: this.signal });
|
|
2794
2869
|
}
|
|
2795
2870
|
template() {
|
|
2796
|
-
const programs = this.state
|
|
2871
|
+
const programs = this.state.epgPrograms;
|
|
2797
2872
|
return b `
|
|
2798
2873
|
<div class="eb-snrt-carousel">
|
|
2799
2874
|
${programs.map((program) => b `
|
|
@@ -2933,7 +3008,7 @@
|
|
|
2933
3008
|
<video
|
|
2934
3009
|
class="eb-video"
|
|
2935
3010
|
playsinline
|
|
2936
|
-
|
|
3011
|
+
.muted="${config.muted}"
|
|
2937
3012
|
?autoplay="${config.autoplay}"
|
|
2938
3013
|
?controls="${isIOS}"
|
|
2939
3014
|
></video>
|
|
@@ -3063,16 +3138,14 @@
|
|
|
3063
3138
|
}
|
|
3064
3139
|
}
|
|
3065
3140
|
}
|
|
3066
|
-
// Loading spinner —
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
this.childComponents.push(loadingSpinner);
|
|
3075
|
-
}
|
|
3141
|
+
// Loading spinner — mounted directly into .eb-player (not overlay-zone)
|
|
3142
|
+
// so it escapes the overlay-zone stacking context and displays above the middle bar
|
|
3143
|
+
const loadingSlot = document.createElement('div');
|
|
3144
|
+
loadingSlot.className = 'eb-loading-slot';
|
|
3145
|
+
inner.appendChild(loadingSlot);
|
|
3146
|
+
const loadingSpinner = new LoadingSpinner();
|
|
3147
|
+
loadingSpinner.connect(loadingSlot, state, bus, config, i18n);
|
|
3148
|
+
this.childComponents.push(loadingSpinner);
|
|
3076
3149
|
// Interactive overlays — mounted directly into .eb-player (not overlay-zone)
|
|
3077
3150
|
// so they escape the overlay-zone stacking context and display above all controls
|
|
3078
3151
|
const interactiveOverlays = [
|
|
@@ -3103,7 +3176,7 @@
|
|
|
3103
3176
|
<video
|
|
3104
3177
|
class="eb-video"
|
|
3105
3178
|
playsinline
|
|
3106
|
-
|
|
3179
|
+
.muted="${config.muted}"
|
|
3107
3180
|
?autoplay="${config.autoplay}"
|
|
3108
3181
|
></video>
|
|
3109
3182
|
</div>
|
|
@@ -3900,10 +3973,14 @@
|
|
|
3900
3973
|
const skinColors = this.config.skinColors;
|
|
3901
3974
|
if (this.config.primaryColor) {
|
|
3902
3975
|
container.style.setProperty('--eb-color-primary', this.config.primaryColor);
|
|
3976
|
+
container.style.setProperty('--eb-color-progress', this.config.primaryColor);
|
|
3977
|
+
container.style.setProperty('--eb-accent', this.config.primaryColor);
|
|
3903
3978
|
}
|
|
3904
3979
|
// skinColors.general overrides primaryColor
|
|
3905
3980
|
if (skinColors?.general) {
|
|
3906
3981
|
container.style.setProperty('--eb-color-primary', skinColors.general);
|
|
3982
|
+
container.style.setProperty('--eb-color-progress', skinColors.general);
|
|
3983
|
+
container.style.setProperty('--eb-accent', skinColors.general);
|
|
3907
3984
|
}
|
|
3908
3985
|
if (skinColors?.progressBar) {
|
|
3909
3986
|
container.style.setProperty('--eb-color-progress', skinColors.progressBar);
|
|
@@ -4246,7 +4323,7 @@
|
|
|
4246
4323
|
// -------------------------------------------------------------------------
|
|
4247
4324
|
bindVideoEvents(video, state, signal) {
|
|
4248
4325
|
// Playback timing + live sync detection
|
|
4249
|
-
const syncMargin = this.config?.syncLiveMargin ??
|
|
4326
|
+
const syncMargin = this.config?.syncLiveMargin ?? 30;
|
|
4250
4327
|
video.addEventListener('timeupdate', () => {
|
|
4251
4328
|
state.currentTime = Math.round(video.currentTime);
|
|
4252
4329
|
if (state.isLive) {
|
|
@@ -4272,14 +4349,41 @@
|
|
|
4272
4349
|
state.playbackRate = video.playbackRate;
|
|
4273
4350
|
}, { signal });
|
|
4274
4351
|
// FSM transitions: playing, pause, waiting, ended
|
|
4352
|
+
//
|
|
4353
|
+
// Pause guard: browsers fire spurious 'pause' events when currentTime is
|
|
4354
|
+
// set programmatically (rewind/forward/seekbar). We only honour the 'pause'
|
|
4355
|
+
// event when the previous playback state was 'playing' — meaning the user
|
|
4356
|
+
// (or our code) explicitly called video.pause(). During a seek the state
|
|
4357
|
+
// is already 'buffering' by the time the late 'pause' arrives, so it's
|
|
4358
|
+
// harmlessly ignored.
|
|
4359
|
+
let userRequestedPause = false;
|
|
4275
4360
|
video.addEventListener('playing', () => {
|
|
4361
|
+
userRequestedPause = false;
|
|
4276
4362
|
state.playbackState = 'playing';
|
|
4277
4363
|
}, { signal });
|
|
4364
|
+
video.addEventListener('waiting', () => {
|
|
4365
|
+
state.playbackState = 'buffering';
|
|
4366
|
+
}, { signal });
|
|
4278
4367
|
video.addEventListener('pause', () => {
|
|
4368
|
+
if (video.seeking)
|
|
4369
|
+
return;
|
|
4370
|
+
if (state.playbackState === 'buffering') {
|
|
4371
|
+
userRequestedPause = true;
|
|
4372
|
+
return;
|
|
4373
|
+
}
|
|
4279
4374
|
state.playbackState = 'paused';
|
|
4280
4375
|
}, { signal });
|
|
4281
|
-
video.addEventListener('
|
|
4282
|
-
state
|
|
4376
|
+
video.addEventListener('seeked', () => {
|
|
4377
|
+
// Update live sync state immediately after seek so the UI reflects
|
|
4378
|
+
// the new position without waiting for the next timeupdate
|
|
4379
|
+
if (state.isLive) {
|
|
4380
|
+
state.currentTime = Math.round(video.currentTime);
|
|
4381
|
+
state.isSyncWithLive = video.currentTime + syncMargin > video.duration;
|
|
4382
|
+
}
|
|
4383
|
+
if (userRequestedPause && video.paused) {
|
|
4384
|
+
state.playbackState = 'paused';
|
|
4385
|
+
userRequestedPause = false;
|
|
4386
|
+
}
|
|
4283
4387
|
}, { signal });
|
|
4284
4388
|
video.addEventListener('ended', () => {
|
|
4285
4389
|
state.playbackState = 'ended';
|
|
@@ -5233,8 +5337,8 @@
|
|
|
5233
5337
|
seek(time) {
|
|
5234
5338
|
if (this.video === null)
|
|
5235
5339
|
return;
|
|
5236
|
-
// Disable hls.js live sync
|
|
5237
|
-
//
|
|
5340
|
+
// Disable hls.js live sync on first seek so it never auto-seeks
|
|
5341
|
+
// back to the live edge on DVR/timeshift streams.
|
|
5238
5342
|
if (!this.liveSyncDisabled && this.driver !== null && this.state?.isLive) {
|
|
5239
5343
|
const cfg = this.driver.config;
|
|
5240
5344
|
cfg.liveSyncDurationCount = 0;
|
|
@@ -5374,6 +5478,27 @@
|
|
|
5374
5478
|
if (this.state) {
|
|
5375
5479
|
this.state.error = message;
|
|
5376
5480
|
}
|
|
5481
|
+
// hls.js nulls its media on a fatal keySystemError; signal so the
|
|
5482
|
+
// P2P SDK is torn down before it loops on a null media element.
|
|
5483
|
+
this.bus?.emit('error-fatal', { code: 'DRM', message });
|
|
5484
|
+
}
|
|
5485
|
+
});
|
|
5486
|
+
}
|
|
5487
|
+
// Geo-block detection (opt-in, ported from v1.x).
|
|
5488
|
+
// Detects 403 on manifest/level load and surfaces it via the bus + a
|
|
5489
|
+
// window CustomEvent for back-compat with v1 host integrations.
|
|
5490
|
+
if (config.useGeoblockingErrorHandle) {
|
|
5491
|
+
const busRef = this.bus;
|
|
5492
|
+
driver.on(Hls.Events.ERROR, (_event, data) => {
|
|
5493
|
+
const error = data;
|
|
5494
|
+
if (error?.type === Hls.ErrorTypes.NETWORK_ERROR
|
|
5495
|
+
&& (error?.details === 'manifestLoadError' || error?.details === 'levelLoadError')
|
|
5496
|
+
&& error?.response?.code === 403) {
|
|
5497
|
+
busRef?.emit('geoblock-detected');
|
|
5498
|
+
window.dispatchEvent(new CustomEvent('geoblock', {
|
|
5499
|
+
detail: { title: 'Geoblock', message: 'Geoblock detected' }
|
|
5500
|
+
}));
|
|
5501
|
+
this.stopWatchdog();
|
|
5377
5502
|
}
|
|
5378
5503
|
});
|
|
5379
5504
|
}
|
|
@@ -6070,12 +6195,23 @@
|
|
|
6070
6195
|
this.lib.start();
|
|
6071
6196
|
// Clean up on abort
|
|
6072
6197
|
signal.addEventListener('abort', () => {
|
|
6073
|
-
|
|
6074
|
-
this.lib.stop();
|
|
6075
|
-
}
|
|
6076
|
-
this.lib = null;
|
|
6198
|
+
this.stop();
|
|
6077
6199
|
}, { once: true });
|
|
6078
6200
|
}
|
|
6201
|
+
/**
|
|
6202
|
+
* Stop and detach the P2P SDK. Idempotent.
|
|
6203
|
+
*
|
|
6204
|
+
* Called on AbortSignal teardown, and eagerly on an unrecoverable engine
|
|
6205
|
+
* error: hls.js nulls its media element on a fatal keySystemError, so a
|
|
6206
|
+
* still-running eblib instance would keep polling `media.currentTime` and
|
|
6207
|
+
* throw in a loop until full dispose.
|
|
6208
|
+
*/
|
|
6209
|
+
stop() {
|
|
6210
|
+
if (this.lib !== null && typeof this.lib.stop === 'function') {
|
|
6211
|
+
this.lib.stop();
|
|
6212
|
+
}
|
|
6213
|
+
this.lib = null;
|
|
6214
|
+
}
|
|
6079
6215
|
}
|
|
6080
6216
|
|
|
6081
6217
|
/**
|
|
@@ -6368,21 +6504,38 @@
|
|
|
6368
6504
|
/**
|
|
6369
6505
|
* EBPlayer facade — consumer-facing API layer.
|
|
6370
6506
|
*
|
|
6371
|
-
* Provides the window.EBPlayer interface
|
|
6507
|
+
* Provides the window.EBPlayer interface:
|
|
6372
6508
|
* - window.EBPlayer.start(config) — synchronously returns a PlayerReference
|
|
6373
|
-
* - window.EBPlayer.stop() — detaches
|
|
6374
|
-
* - window.EBPlayer.destroy() — disposes
|
|
6509
|
+
* - window.EBPlayer.stop() — detaches all active engines (skins stay mounted)
|
|
6510
|
+
* - window.EBPlayer.destroy() — disposes all controllers (tears down everything)
|
|
6375
6511
|
*
|
|
6376
|
-
*
|
|
6377
|
-
*
|
|
6512
|
+
* Supports multiple simultaneous player instances on the same page.
|
|
6513
|
+
* Each instance is keyed by its container element:
|
|
6514
|
+
* - Calling start() with the SAME container disposes the old instance first (backward compat).
|
|
6515
|
+
* - Calling start() with a DIFFERENT container creates an independent player.
|
|
6516
|
+
*
|
|
6517
|
+
* Per-instance cleanup is available via PlayerReference.destroy().
|
|
6518
|
+
* Global stop()/destroy() operate on all instances at once.
|
|
6378
6519
|
*/
|
|
6379
6520
|
// Import from barrel — triggers CSS imports (base.css + skin.css) for postcss extraction
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6521
|
+
/** Active player instances keyed by their container element. */
|
|
6522
|
+
const instances = new Map();
|
|
6523
|
+
/**
|
|
6524
|
+
* Tear down a single instance: snapshot handler, engine, then controller.
|
|
6525
|
+
* Does NOT remove the entry from the instances Map (caller must do that).
|
|
6526
|
+
*/
|
|
6527
|
+
function destroyInstance(inst) {
|
|
6528
|
+
if (inst.snapshotDestroy !== null) {
|
|
6529
|
+
inst.snapshotDestroy();
|
|
6530
|
+
inst.snapshotDestroy = null;
|
|
6531
|
+
}
|
|
6532
|
+
if (inst.engine !== null) {
|
|
6533
|
+
inst.engine.detach();
|
|
6534
|
+
inst.engine = null;
|
|
6535
|
+
}
|
|
6536
|
+
inst.p2p = null;
|
|
6537
|
+
inst.controller.dispose();
|
|
6538
|
+
}
|
|
6386
6539
|
// ---------------------------------------------------------------------------
|
|
6387
6540
|
// Container resolution
|
|
6388
6541
|
// ---------------------------------------------------------------------------
|
|
@@ -6425,31 +6578,45 @@
|
|
|
6425
6578
|
// ---------------------------------------------------------------------------
|
|
6426
6579
|
/**
|
|
6427
6580
|
* Start the player with the given config.
|
|
6428
|
-
*
|
|
6581
|
+
*
|
|
6582
|
+
* If a player is already mounted on the same container element, it is disposed
|
|
6583
|
+
* before the new one is created (backward-compatible single-instance behavior).
|
|
6584
|
+
* If the container is different, the new player coexists alongside existing ones
|
|
6585
|
+
* (multi-instance support).
|
|
6429
6586
|
*
|
|
6430
6587
|
* Returns a PlayerReference synchronously — never returns a Promise.
|
|
6431
6588
|
*/
|
|
6432
6589
|
function start(runtimeConfig) {
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6590
|
+
const container = resolveContainer(runtimeConfig.el);
|
|
6591
|
+
// Dispose any existing instance on the SAME container (backward compat).
|
|
6592
|
+
// Instances on OTHER containers are left untouched (multi-instance).
|
|
6593
|
+
const existing = instances.get(container);
|
|
6594
|
+
if (existing !== undefined) {
|
|
6595
|
+
destroyInstance(existing);
|
|
6596
|
+
instances.delete(container);
|
|
6436
6597
|
}
|
|
6437
6598
|
const mergedConfig = mergeConfig(DEFAULT_CONFIG, runtimeConfig);
|
|
6438
6599
|
const controller = new PlayerController(runtimeConfig);
|
|
6439
|
-
const container = resolveContainer(runtimeConfig.el);
|
|
6440
6600
|
controller.mount(container);
|
|
6441
6601
|
// Video element is available after mount() creates the skin DOM
|
|
6442
6602
|
const video = container.querySelector('video');
|
|
6443
|
-
|
|
6444
|
-
|
|
6603
|
+
// Per-instance state — captured by the reference closure, never shared across instances
|
|
6604
|
+
const inst = {
|
|
6605
|
+
controller,
|
|
6606
|
+
engine: null,
|
|
6607
|
+
snapshotDestroy: null,
|
|
6608
|
+
p2p: null,
|
|
6609
|
+
container
|
|
6610
|
+
};
|
|
6611
|
+
instances.set(container, inst);
|
|
6445
6612
|
let lastSrc = '';
|
|
6446
6613
|
const reference = {
|
|
6447
6614
|
open(src) {
|
|
6448
6615
|
lastSrc = src;
|
|
6449
6616
|
// Detach any existing engine before opening a new stream
|
|
6450
|
-
if (
|
|
6451
|
-
|
|
6452
|
-
|
|
6617
|
+
if (inst.engine !== null) {
|
|
6618
|
+
inst.engine.detach();
|
|
6619
|
+
inst.engine = null;
|
|
6453
6620
|
}
|
|
6454
6621
|
const engine = selectEngine(src, mergedConfig);
|
|
6455
6622
|
if (video !== null) {
|
|
@@ -6458,11 +6625,14 @@
|
|
|
6458
6625
|
engine.setBus(controller.bus);
|
|
6459
6626
|
engine.setConfig(mergedConfig);
|
|
6460
6627
|
controller.setEngineSync(engine);
|
|
6461
|
-
|
|
6628
|
+
inst.engine = engine;
|
|
6462
6629
|
// P2P integration: wire EasyBroadcast SDK when config.lib and config.manager are set
|
|
6463
6630
|
if (mergedConfig.lib && mergedConfig.manager && video !== null) {
|
|
6464
6631
|
const isDash = src.includes('.mpd') && mergedConfig.dashjs !== false;
|
|
6632
|
+
// Stop any P2P SDK from a previous open() before attaching a new one
|
|
6633
|
+
inst.p2p?.stop();
|
|
6465
6634
|
const p2pManager = new P2PManager();
|
|
6635
|
+
inst.p2p = p2pManager;
|
|
6466
6636
|
// Wait for the engine driver to be created before integrating P2P
|
|
6467
6637
|
engine.driverReady.then(() => {
|
|
6468
6638
|
return p2pManager.integrate({
|
|
@@ -6479,9 +6649,9 @@
|
|
|
6479
6649
|
// Snapshot handler: create snapshot engine for seekbar preview thumbnails
|
|
6480
6650
|
if (mergedConfig.showProgressThumb) {
|
|
6481
6651
|
// Clean up any previous snapshot handler
|
|
6482
|
-
if (
|
|
6483
|
-
|
|
6484
|
-
|
|
6652
|
+
if (inst.snapshotDestroy !== null) {
|
|
6653
|
+
inst.snapshotDestroy();
|
|
6654
|
+
inst.snapshotDestroy = null;
|
|
6485
6655
|
}
|
|
6486
6656
|
const isDash = src.includes('.mpd') && mergedConfig.dashjs !== false;
|
|
6487
6657
|
// Wait for engine driver to be ready (CDN script loaded) before initializing snapshot
|
|
@@ -6492,7 +6662,7 @@
|
|
|
6492
6662
|
const handler = new DashSnapshotHandler(src);
|
|
6493
6663
|
handler.init(win.dashjs)
|
|
6494
6664
|
.then((handle) => {
|
|
6495
|
-
|
|
6665
|
+
inst.snapshotDestroy = () => handle.destroy();
|
|
6496
6666
|
controller.bus.emit('snapshot-handler-ready', { take: handle.take, video: handle.video });
|
|
6497
6667
|
})
|
|
6498
6668
|
.catch((error) => {
|
|
@@ -6512,7 +6682,7 @@
|
|
|
6512
6682
|
const handler = new HlsSnapshotHandler({ src, engineSettings: { ...mergedConfig.engineSettings, ...snapshotDrmConfig } }, sharedTokenManager);
|
|
6513
6683
|
handler.init(win.Hls)
|
|
6514
6684
|
.then(() => {
|
|
6515
|
-
|
|
6685
|
+
inst.snapshotDestroy = () => handler.destroy();
|
|
6516
6686
|
const snapshotVideo = handler.getVideo();
|
|
6517
6687
|
if (snapshotVideo !== null) {
|
|
6518
6688
|
controller.bus.emit('snapshot-handler-ready', { take: (time) => handler.take(time), video: snapshotVideo });
|
|
@@ -6547,15 +6717,19 @@
|
|
|
6547
6717
|
}
|
|
6548
6718
|
},
|
|
6549
6719
|
close() {
|
|
6550
|
-
if (
|
|
6551
|
-
|
|
6552
|
-
|
|
6720
|
+
if (inst.snapshotDestroy !== null) {
|
|
6721
|
+
inst.snapshotDestroy();
|
|
6722
|
+
inst.snapshotDestroy = null;
|
|
6553
6723
|
}
|
|
6554
|
-
if (
|
|
6555
|
-
|
|
6556
|
-
|
|
6724
|
+
if (inst.engine !== null) {
|
|
6725
|
+
inst.engine.detach();
|
|
6726
|
+
inst.engine = null;
|
|
6557
6727
|
}
|
|
6558
6728
|
},
|
|
6729
|
+
destroy() {
|
|
6730
|
+
destroyInstance(inst);
|
|
6731
|
+
instances.delete(container);
|
|
6732
|
+
},
|
|
6559
6733
|
get state() {
|
|
6560
6734
|
return controller.state;
|
|
6561
6735
|
}
|
|
@@ -6576,14 +6750,21 @@
|
|
|
6576
6750
|
reference.open(currentSrc);
|
|
6577
6751
|
// Restore selections after manifest loads (~2s pragmatic delay for async manifest parsing)
|
|
6578
6752
|
setTimeout(() => {
|
|
6579
|
-
if (
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6753
|
+
if (inst.engine !== null) {
|
|
6754
|
+
inst.engine.setQuality(saved.quality);
|
|
6755
|
+
inst.engine.setAudioTrack(saved.audioTrack);
|
|
6756
|
+
inst.engine.setSubtitle(saved.subtitleTrack);
|
|
6583
6757
|
}
|
|
6584
6758
|
}, 2000);
|
|
6585
6759
|
}, 500);
|
|
6586
6760
|
});
|
|
6761
|
+
// On an unrecoverable engine error (e.g. fatal DRM keySystemError), tear down
|
|
6762
|
+
// the P2P SDK. hls.js nulls its media element, so a still-running eblib would
|
|
6763
|
+
// otherwise loop on `media.currentTime`. The engine has already surfaced the
|
|
6764
|
+
// clean error via state.error and stopped its stall watchdog.
|
|
6765
|
+
controller.bus.on('error-fatal', () => {
|
|
6766
|
+
inst.p2p?.stop();
|
|
6767
|
+
}, { signal: controller.signal });
|
|
6587
6768
|
// Auto-open the stream if src is provided in config (matches legacy player behaviour
|
|
6588
6769
|
// where consumers call start({ src: '...' }) and expect playback to begin immediately).
|
|
6589
6770
|
// When autoplay is false, defer open() until the user requests play — this avoids
|
|
@@ -6595,9 +6776,24 @@
|
|
|
6595
6776
|
else {
|
|
6596
6777
|
let deferredOpen = true;
|
|
6597
6778
|
controller.bus.on('play', () => {
|
|
6598
|
-
if (deferredOpen)
|
|
6599
|
-
|
|
6600
|
-
|
|
6779
|
+
if (!deferredOpen)
|
|
6780
|
+
return;
|
|
6781
|
+
deferredOpen = false;
|
|
6782
|
+
reference.open(mergedConfig.src);
|
|
6783
|
+
// Honor the user's click intent. CommandHandler's earlier bus.on('play')
|
|
6784
|
+
// listener fired before us with no source attached, so its video.play()
|
|
6785
|
+
// was a silent no-op. Wait for driverReady (attachMedia + loadSource
|
|
6786
|
+
// done) then start playback ourselves.
|
|
6787
|
+
const engine = inst.engine;
|
|
6788
|
+
if (engine !== null && video !== null) {
|
|
6789
|
+
engine.driverReady.then(() => {
|
|
6790
|
+
const playResult = video.play();
|
|
6791
|
+
if (playResult && typeof playResult.catch === 'function') {
|
|
6792
|
+
playResult.catch((error) => {
|
|
6793
|
+
console.warn('EBPlayer: deferred play() rejected', error);
|
|
6794
|
+
});
|
|
6795
|
+
}
|
|
6796
|
+
});
|
|
6601
6797
|
}
|
|
6602
6798
|
}, { signal: controller.signal });
|
|
6603
6799
|
}
|
|
@@ -6605,28 +6801,28 @@
|
|
|
6605
6801
|
return reference;
|
|
6606
6802
|
}
|
|
6607
6803
|
/**
|
|
6608
|
-
* Stop
|
|
6609
|
-
* Detaches
|
|
6804
|
+
* Stop all active streams.
|
|
6805
|
+
* Detaches engines without disposing controllers (skins stay mounted).
|
|
6806
|
+
* For single-instance usage this behaves identically to the old global stop().
|
|
6610
6807
|
*/
|
|
6611
6808
|
function stop() {
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6809
|
+
for (const inst of instances.values()) {
|
|
6810
|
+
if (inst.engine !== null) {
|
|
6811
|
+
inst.engine.detach();
|
|
6812
|
+
inst.engine = null;
|
|
6813
|
+
}
|
|
6615
6814
|
}
|
|
6616
6815
|
}
|
|
6617
6816
|
/**
|
|
6618
|
-
* Destroy
|
|
6619
|
-
* Disposes
|
|
6817
|
+
* Destroy all player instances completely.
|
|
6818
|
+
* Disposes every controller (tears down skin, events, and all resources).
|
|
6819
|
+
* For single-instance usage this behaves identically to the old global destroy().
|
|
6620
6820
|
*/
|
|
6621
6821
|
function destroy() {
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
activeEngine = null;
|
|
6625
|
-
}
|
|
6626
|
-
if (activeController !== null) {
|
|
6627
|
-
activeController.dispose();
|
|
6628
|
-
activeController = null;
|
|
6822
|
+
for (const inst of instances.values()) {
|
|
6823
|
+
destroyInstance(inst);
|
|
6629
6824
|
}
|
|
6825
|
+
instances.clear();
|
|
6630
6826
|
}
|
|
6631
6827
|
// ---------------------------------------------------------------------------
|
|
6632
6828
|
// Version
|
|
@@ -6639,7 +6835,8 @@
|
|
|
6639
6835
|
// ---------------------------------------------------------------------------
|
|
6640
6836
|
if (typeof window !== 'undefined') {
|
|
6641
6837
|
console.info(`%cEBPlayer v${VERSION}`, 'color: #1FA9DD; font-weight: bold');
|
|
6642
|
-
|
|
6838
|
+
const win = window;
|
|
6839
|
+
win.EBPlayer = { start, stop, destroy, AVAILABLE_THEMES, THEME_LAYOUTS, version: VERSION };
|
|
6643
6840
|
}
|
|
6644
6841
|
|
|
6645
6842
|
/**
|