eb-player 2.5.1 → 2.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.5.1";
7
+ var __EB_PLAYER_VERSION__ = "2.5.2";
8
8
 
9
9
  /**
10
10
  * Finite State Machine for player playback state transitions.
@@ -19,22 +19,28 @@
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 | buffering: resume or stop
22
+ * - paused -> playing | loading | idle | buffering | ended: resume, stop, or reach end
23
23
  * - paused -> buffering is needed when resuming a paused (live) stream where
24
24
  * the browser fires 'waiting' before 'playing' (Safari live segment edge);
25
25
  * without it the FSM strands at 'paused' while the video actually plays
26
- * - buffering -> playing | paused | idle | error: buffer recovered, user pause, or stall
26
+ * - paused -> ended is needed because the element always fires 'pause' before
27
+ * 'ended' when a VOD reaches its end
28
+ * - buffering -> playing | paused | idle | error | ended: buffer recovered, user pause, stall, or end
27
29
  * - buffering -> idle is needed for the stall-watchdog recovery path (kick off reload)
28
30
  * - buffering -> paused is needed for live stream support (user pauses during buffer)
31
+ * - buffering -> ended is needed because a tail 'waiting' firing just before the
32
+ * end can leave the FSM at 'buffering' at the moment 'ended' fires
29
33
  * - error -> loading: retry after error
30
- * - ended -> loading | idle: restart playback or reset
34
+ * - ended -> loading | idle: restart playback or reset. Recovery from 'ended'
35
+ * (seek-back, replay) routes explicitly through 'loading' rather than adding
36
+ * more direct exits here.
31
37
  */
32
38
  const TRANSITIONS = {
33
39
  idle: ['loading'],
34
40
  loading: ['playing', 'buffering', 'paused', 'error', 'idle'],
35
41
  playing: ['paused', 'buffering', 'ended', 'error', 'idle'],
36
- paused: ['playing', 'loading', 'idle', 'buffering'],
37
- buffering: ['playing', 'paused', 'idle', 'error'],
42
+ paused: ['playing', 'loading', 'idle', 'buffering', 'ended'],
43
+ buffering: ['playing', 'paused', 'idle', 'error', 'ended'],
38
44
  error: ['loading', 'idle'],
39
45
  ended: ['loading', 'idle']
40
46
  };
@@ -5117,7 +5123,10 @@
5117
5123
  video.addEventListener('pause', () => {
5118
5124
  if (video.seeking)
5119
5125
  return;
5120
- if (state.playbackState === 'buffering') {
5126
+ // Let 'ended' win: when the browser is pausing us because playback
5127
+ // actually reached the end (pause always fires before ended), don't
5128
+ // swallow it into the buffering branch below.
5129
+ if (state.playbackState === 'buffering' && !video.ended) {
5121
5130
  userRequestedPause = true;
5122
5131
  return;
5123
5132
  }
@@ -5130,7 +5139,12 @@
5130
5139
  state.currentTime = Math.round(video.currentTime);
5131
5140
  state.isSyncWithLive = video.currentTime + syncMargin > video.duration;
5132
5141
  }
5133
- if (userRequestedPause && video.paused) {
5142
+ // Restore 'paused' once a seek settles while the video stays paused.
5143
+ // Covers the existing pause-during-buffering case (userRequestedPause)
5144
+ // and seeking back from 'ended' (routed through 'loading' by the
5145
+ // engine's seek()), where no 'waiting'/'playing' event ever fires
5146
+ // because the video never resumes playing on its own.
5147
+ if ((userRequestedPause || state.playbackState === 'loading') && video.paused) {
5134
5148
  state.playbackState = 'paused';
5135
5149
  userRequestedPause = false;
5136
5150
  }
@@ -5979,6 +5993,33 @@
5979
5993
  const MAX_NON_FATAL_RETRIES = 3;
5980
5994
  /** Error types that cannot be recovered by retrying (platform limitations) */
5981
5995
  const UNRECOVERABLE_TYPES = new Set(['keySystemError']);
5996
+ /**
5997
+ * Margin (seconds) used to detect a VOD tail stall near end-of-stream.
5998
+ * Encoder-produced audio/video rendition length mismatches of a few tens to
5999
+ * ~100ms are common (observed: 27-77ms overhang) and can stall playback for
6000
+ * a second or two right at the end — indistinguishable from a genuinely stuck
6001
+ * stream by currentTime alone. 1s is a safe margin above that overhang.
6002
+ */
6003
+ const NEAR_VOD_END_SECONDS = 1;
6004
+ /**
6005
+ * True when the driver is not live and both the playhead and the buffered
6006
+ * range already reach within NEAR_VOD_END_SECONDS of the media duration —
6007
+ * i.e. a tail stall that hls.js's own nudge/EOS handling can resolve on its
6008
+ * own, where a destructive recoverMediaError()/startLoad() would only race it
6009
+ * (observed: reload lands right as 'ended' would have fired naturally).
6010
+ */
6011
+ function isNearVodEndStall(media, engine) {
6012
+ if (media === null || engine?.isLive === true)
6013
+ return false;
6014
+ const { duration, buffered, currentTime } = media;
6015
+ if (!Number.isFinite(duration) || duration <= 0)
6016
+ return false;
6017
+ if (duration - currentTime > NEAR_VOD_END_SECONDS)
6018
+ return false;
6019
+ if (buffered.length === 0)
6020
+ return false;
6021
+ return buffered.end(buffered.length - 1) >= duration - NEAR_VOD_END_SECONDS;
6022
+ }
5982
6023
  function handleHlsRetry(driver, engine) {
5983
6024
  /** Tracks consecutive non-fatal recovery attempts per error detail */
5984
6025
  const nonFatalRetryCounts = new Map();
@@ -6044,6 +6085,10 @@
6044
6085
  const errorTime = driver.media?.currentTime;
6045
6086
  setTimeout(() => {
6046
6087
  if (errorTime === driver.media?.currentTime && !engine?.chromecast_casting) {
6088
+ if (isNearVodEndStall(driver.media, engine)) {
6089
+ console.info('HLS Retry: near VOD end, letting hls.js finish naturally', event, data);
6090
+ return;
6091
+ }
6047
6092
  console.warn('HLS Retry: Stream is stuck, trying to fix', event, data);
6048
6093
  if (data.type === 'mediaError')
6049
6094
  driver.recoverMediaError();
@@ -6467,7 +6512,19 @@
6467
6512
  cfg.liveMaxLatencyDurationCount = Infinity;
6468
6513
  this.liveSyncDisabled = true;
6469
6514
  }
6515
+ // hls.js's StreamController parks itself in ENDED once the video reaches
6516
+ // its end and never resumes loading on its own — setting currentTime alone
6517
+ // leaves the seek stuck forever. Route through 'loading' and kick the
6518
+ // driver explicitly (deferred per Pitfall 3: never call startLoad
6519
+ // synchronously from a video event handler).
6520
+ const wasEnded = this.video.ended;
6470
6521
  this.video.currentTime = time;
6522
+ if (wasEnded && this.driver !== null) {
6523
+ if (this.state)
6524
+ this.state.playbackState = 'loading';
6525
+ const driverRef = this.driver;
6526
+ setTimeout(() => driverRef.startLoad(time), 0);
6527
+ }
6471
6528
  }
6472
6529
  seekToLive() {
6473
6530
  if (this.video === null)
@@ -6633,6 +6690,7 @@
6633
6690
  if (config.retry) {
6634
6691
  handleHlsRetry(driver, {
6635
6692
  get chromecast_casting() { return state.isCasting; },
6693
+ get isLive() { return state.isLive; },
6636
6694
  onUnrecoverableError: (message) => {
6637
6695
  this.stopWatchdog();
6638
6696
  if (this.state) {
@@ -6718,6 +6776,15 @@
6718
6776
  // Defer startLoad to avoid re-entrancy (Pitfall 3)
6719
6777
  setTimeout(() => driverRef.startLoad(-1), 0);
6720
6778
  }
6779
+ else if (this.state?.playbackState === 'ended') {
6780
+ // Pressing Play at end-of-stream: the browser has already seeked to
6781
+ // the earliest position by the time this fires (evidenced live), so
6782
+ // video.currentTime is the correct resume point. Same StreamController
6783
+ // ENDED trap as seek() above — route through 'loading' and kick the driver.
6784
+ this.state.playbackState = 'loading';
6785
+ const resumeTime = this.video?.currentTime ?? 0;
6786
+ setTimeout(() => driverRef.startLoad(resumeTime), 0);
6787
+ }
6721
6788
  }, { signal });
6722
6789
  // Start stall watchdog
6723
6790
  this.startWatchdog();