apm-react-audio-player 1.1.3 → 2.0.0

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/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var React = require('react');
4
+ var Hls = require('hls.js');
4
5
 
5
6
  function _arrayLikeToArray(r, a) {
6
7
  (null == a || a > r.length) && (a = r.length);
@@ -10,6 +11,14 @@ function _arrayLikeToArray(r, a) {
10
11
  function _arrayWithHoles(r) {
11
12
  if (Array.isArray(r)) return r;
12
13
  }
14
+ function _defineProperty(e, r, t) {
15
+ return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
16
+ value: t,
17
+ enumerable: true,
18
+ configurable: true,
19
+ writable: true
20
+ }) : e[r] = t, e;
21
+ }
13
22
  function _extends() {
14
23
  return _extends = Object.assign ? Object.assign.bind() : function (n) {
15
24
  for (var e = 1; e < arguments.length; e++) {
@@ -49,9 +58,44 @@ function _iterableToArrayLimit(r, l) {
49
58
  function _nonIterableRest() {
50
59
  throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
51
60
  }
61
+ function ownKeys(e, r) {
62
+ var t = Object.keys(e);
63
+ if (Object.getOwnPropertySymbols) {
64
+ var o = Object.getOwnPropertySymbols(e);
65
+ r && (o = o.filter(function (r) {
66
+ return Object.getOwnPropertyDescriptor(e, r).enumerable;
67
+ })), t.push.apply(t, o);
68
+ }
69
+ return t;
70
+ }
71
+ function _objectSpread2(e) {
72
+ for (var r = 1; r < arguments.length; r++) {
73
+ var t = null != arguments[r] ? arguments[r] : {};
74
+ r % 2 ? ownKeys(Object(t), true).forEach(function (r) {
75
+ _defineProperty(e, r, t[r]);
76
+ }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) {
77
+ Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
78
+ });
79
+ }
80
+ return e;
81
+ }
52
82
  function _slicedToArray(r, e) {
53
83
  return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
54
84
  }
85
+ function _toPrimitive(t, r) {
86
+ if ("object" != typeof t || !t) return t;
87
+ var e = t[Symbol.toPrimitive];
88
+ if (void 0 !== e) {
89
+ var i = e.call(t, r);
90
+ if ("object" != typeof i) return i;
91
+ throw new TypeError("@@toPrimitive must return a primitive value.");
92
+ }
93
+ return ("string" === r ? String : Number)(t);
94
+ }
95
+ function _toPropertyKey(t) {
96
+ var i = _toPrimitive(t, "string");
97
+ return "symbol" == typeof i ? i : i + "";
98
+ }
55
99
  function _unsupportedIterableToArray(r, a) {
56
100
  if (r) {
57
101
  if ("string" == typeof r) return _arrayLikeToArray(r, a);
@@ -60,8 +104,11 @@ function _unsupportedIterableToArray(r, a) {
60
104
  }
61
105
  }
62
106
 
63
- var useAudioPlayer = function useAudioPlayer(audioRef, progressBarRef, volumeCtrl) {
64
- var initialDuration = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : undefined;
107
+ var useAudioPlayer = function useAudioPlayer(audioRef, progressBarRef) {
108
+ var _ref = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {},
109
+ volumeCtrl = _ref.volumeCtrl,
110
+ initialDuration = _ref.initialDuration,
111
+ hlsRef = _ref.hlsRef;
65
112
  var _useState = React.useState(false),
66
113
  _useState2 = _slicedToArray(_useState, 2),
67
114
  isPlaying = _useState2[0],
@@ -78,11 +125,12 @@ var useAudioPlayer = function useAudioPlayer(audioRef, progressBarRef, volumeCtr
78
125
  _useState8 = _slicedToArray(_useState7, 2),
79
126
  isFinishedPlaying = _useState8[0],
80
127
  setIsFinishedPlaying = _useState8[1];
81
- var animationRef = React.useRef(); // reference the animation
128
+ var animationRef = React.useRef();
129
+ var pendingPlayAbortRef = React.useRef(null);
82
130
  var _useState9 = React.useState(false),
83
- _useState10 = _slicedToArray(_useState9, 2),
84
- isMuted = _useState10[0],
85
- setIsMuted = _useState10[1];
131
+ _useState0 = _slicedToArray(_useState9, 2),
132
+ isMuted = _useState0[0],
133
+ setIsMuted = _useState0[1];
86
134
  var isStream = audioRef.current && audioRef.current.duration === Infinity;
87
135
  React.useEffect(function () {
88
136
  if (currentTime === Number(duration)) {
@@ -141,6 +189,10 @@ var useAudioPlayer = function useAudioPlayer(audioRef, progressBarRef, volumeCtr
141
189
  animationRef.current = window.requestAnimationFrame(_whilePlaying);
142
190
  };
143
191
  var pause = function pause() {
192
+ if (pendingPlayAbortRef.current) {
193
+ pendingPlayAbortRef.current();
194
+ pendingPlayAbortRef.current = null;
195
+ }
144
196
  setIsPlaying(false);
145
197
  audioRef.current.pause();
146
198
  window.cancelAnimationFrame(animationRef.current);
@@ -152,26 +204,80 @@ var useAudioPlayer = function useAudioPlayer(audioRef, progressBarRef, volumeCtr
152
204
  // pause()
153
205
  // }
154
206
 
207
+ var _safePlay = function safePlay(audio) {
208
+ var promise = audio.play();
209
+ if (promise !== undefined) {
210
+ promise["catch"](function (err) {
211
+ if (err.name === 'NotAllowedError') {
212
+ setIsPlaying(false);
213
+ } else if (err.name === 'AbortError') {
214
+ // play() was interrupted by a concurrent load() (e.g. ReactAudioPlayerInner
215
+ // calling load() after a source change) — retry once canplay fires.
216
+ // If the audio element was already unlocked by a prior play() call within
217
+ // a user gesture (e.g. the Safari fix in AudioContext), this retry succeeds.
218
+ audio.addEventListener('canplay', function () {
219
+ return _safePlay(audio);
220
+ }, {
221
+ once: true
222
+ });
223
+ }
224
+ });
225
+ }
226
+ };
155
227
  var play = function play() {
156
228
  setIsPlaying(true);
157
229
  setIsFinishedPlaying(false);
230
+ var elDuration = audioRef.current.duration;
231
+ var isLiveOrUnloaded = elDuration === Infinity || isNaN(elDuration);
232
+ if (isLiveOrUnloaded) {
233
+ var _audioRef$current$cur;
234
+ if (hlsRef !== null && hlsRef !== void 0 && hlsRef.current) {
235
+ var audio = audioRef.current;
236
+ var hls = hlsRef.current;
237
+ // If data is already buffered (e.g. resuming after pause), play immediately.
238
+ // Otherwise wait for the first fragment so Safari's audio decoder is warm
239
+ // before output starts, preventing the first syllable from being cut.
240
+ if (audio.buffered.length > 0) {
241
+ _safePlay(audio);
242
+ } else {
243
+ var _onFragBuffered = function onFragBuffered() {
244
+ pendingPlayAbortRef.current = null;
245
+ hls.off('hlsFragBuffered', _onFragBuffered);
246
+ _safePlay(audio);
247
+ };
248
+ pendingPlayAbortRef.current = function () {
249
+ return hls.off('hlsFragBuffered', _onFragBuffered);
250
+ };
251
+ hls.on('hlsFragBuffered', _onFragBuffered);
252
+ }
253
+ } else if (elDuration === Infinity && (_audioRef$current$cur = audioRef.current.currentSrc) !== null && _audioRef$current$cur !== void 0 && _audioRef$current$cur.split('?')[0].endsWith('.m3u8')) {
254
+ // Native live stream (no hls.js, e.g. iOS Safari): force a fresh manifest
255
+ // fetch so we don't play from a stale buffer position across a discontinuity.
256
+ var onCanPlay = function onCanPlay() {
257
+ pendingPlayAbortRef.current = null;
258
+ if (audioRef.current) _safePlay(audioRef.current);
259
+ };
260
+ pendingPlayAbortRef.current = function () {
261
+ var _audioRef$current;
262
+ return (_audioRef$current = audioRef.current) === null || _audioRef$current === void 0 ? void 0 : _audioRef$current.removeEventListener('canplay', onCanPlay);
263
+ };
264
+ audioRef.current.addEventListener('canplay', onCanPlay, {
265
+ once: true
266
+ });
267
+ audioRef.current.load();
268
+ } else {
269
+ // Finite audio not yet loaded (duration is NaN): call play() directly so
270
+ // Safari's user-gesture scope isn't lost waiting for an async canplay event.
271
+ _safePlay(audioRef.current);
272
+ }
273
+ } else {
274
+ _safePlay(audioRef.current);
158
275
 
159
- // For live streams (duration === Infinity), reset the audio element before
160
- // playing. The audioSrc useEffect in ReactAudioPlayerInner calls load() on
161
- // mount, which causes the browser to pre-buffer the HLS stream. By the time
162
- // the user clicks play the manifest's seekable window has moved forward and
163
- // old segments are gone, so the browser auto-seeks to the earliest available
164
- // position (e.g. 20s in), causing ads to start mid-stream. Calling load()
165
- // here reconnects to the current live edge and gets a fresh manifest.
166
- if (duration === Infinity) {
167
- audioRef.current.load();
168
- }
169
- audioRef.current.play();
170
-
171
- // Only start RAF loop for non-live streams with valid duration
172
- var dur = audioRef.current.duration;
173
- if (dur !== Infinity && !isNaN(dur) && isFinite(dur)) {
174
- animationRef.current = window.requestAnimationFrame(_whilePlaying);
276
+ // Only start RAF loop for non-live streams with valid duration
277
+ var dur = audioRef.current.duration;
278
+ if (dur !== Infinity && !isNaN(dur) && isFinite(dur)) {
279
+ animationRef.current = window.requestAnimationFrame(_whilePlaying);
280
+ }
175
281
  }
176
282
  };
177
283
  var toggleMute = function toggleMute() {
@@ -300,6 +406,13 @@ var Pause = function Pause() {
300
406
  }));
301
407
  };
302
408
 
409
+ var getHlsSrc = function getHlsSrc(audioSrc) {
410
+ var _urls$find;
411
+ var urls = Array.isArray(audioSrc) ? audioSrc : [audioSrc];
412
+ return (_urls$find = urls.find(function (url) {
413
+ return url && url.split('?')[0].endsWith('.m3u8');
414
+ })) !== null && _urls$find !== void 0 ? _urls$find : null;
415
+ };
303
416
  var getTypeFromExtension = function getTypeFromExtension(url) {
304
417
  var extension = url.split('.').pop().split('?')[0];
305
418
  switch (extension) {
@@ -319,11 +432,15 @@ var getTypeFromExtension = function getTypeFromExtension(url) {
319
432
  }
320
433
  };
321
434
  var ReactAudioPlayerInner = function ReactAudioPlayerInner(props) {
322
- var _props$audioPlayerRef, _props$progressBarRef;
323
- // references
324
- var audioPlayerRef = (_props$audioPlayerRef = props.audioPlayerRef) !== null && _props$audioPlayerRef !== void 0 ? _props$audioPlayerRef : React.useRef(); // reference our audio component
325
- var progressBarRef = (_props$progressBarRef = props.progressBarRef) !== null && _props$progressBarRef !== void 0 ? _props$progressBarRef : React.useRef(); // reference our progress bar
326
-
435
+ var _props$audioPlayerRef, _props$progressBarRef, _props$hlsRef;
436
+ // Always call hooks unconditionally — use internal refs when props don't provide them
437
+ var internalAudioRef = React.useRef();
438
+ var internalProgressBarRef = React.useRef();
439
+ var internalHlsRef = React.useRef(null);
440
+ var hasInitializedRef = React.useRef(false);
441
+ // Track isPlaying via a ref so the source-change useEffect can read the
442
+ // current value without adding isPlaying to its dependency array.
443
+ var isPlayingRef = React.useRef(false);
327
444
  var customStyles = props ? props.style : '';
328
445
  var title = props.title,
329
446
  audioSrc = props.audioSrc,
@@ -346,20 +463,66 @@ var ReactAudioPlayerInner = function ReactAudioPlayerInner(props) {
346
463
  forwardControl = props.forwardControl,
347
464
  subtitle = props.subtitle,
348
465
  prefix = props.prefix;
466
+ var audioPlayerRef = (_props$audioPlayerRef = props.audioPlayerRef) !== null && _props$audioPlayerRef !== void 0 ? _props$audioPlayerRef : internalAudioRef;
467
+ var progressBarRef = (_props$progressBarRef = props.progressBarRef) !== null && _props$progressBarRef !== void 0 ? _props$progressBarRef : internalProgressBarRef;
468
+ var hlsRef = (_props$hlsRef = props.hlsRef) !== null && _props$hlsRef !== void 0 ? _props$hlsRef : internalHlsRef;
469
+ var hlsSrcForRender = getHlsSrc(audioSrc);
470
+ var isHlsManaged = !!(hlsSrcForRender && Hls.isSupported());
471
+
472
+ // Keep isPlayingRef in sync on every render (runs before effects).
473
+ isPlayingRef.current = isPlaying;
349
474
  var audioDuration = duration && !isNaN(duration) && calculateTime(duration);
350
475
  var formatDuration = duration && !isNaN(duration) && audioDuration && formatCalculateTime(audioDuration);
351
476
 
352
- // Reload audio when audioSrc changes
353
- // Use JSON.stringify to handle array comparisons by value instead of reference
477
+ // Manage audio source changes. For HLS sources hls.js owns the loading cycle;
478
+ // for everything else we fall back to the native load() path.
479
+ // JSON.stringify handles array-valued audioSrc comparisons by value.
354
480
  React.useEffect(function () {
355
- if (audioPlayerRef.current && audioSrc) {
481
+ if (!audioPlayerRef.current || !audioSrc) return;
482
+ if (hasInitializedRef.current) {
356
483
  resetDuration === null || resetDuration === void 0 ? void 0 : resetDuration();
357
- try {
358
- audioPlayerRef.current.load();
359
- } catch (err) {
360
- console.warn('Failed to reload audio source:', err);
484
+ }
485
+
486
+ // Tear down any existing hls.js instance before re-evaluating the source.
487
+ if (hlsRef.current) {
488
+ hlsRef.current.destroy();
489
+ hlsRef.current = null;
490
+ }
491
+ var hlsSrc = getHlsSrc(audioSrc);
492
+ var isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
493
+ if (hlsSrc && Hls.isSupported()) {
494
+ var hls = new Hls(_objectSpread2({
495
+ liveSyncDurationCount: 3,
496
+ liveMaxLatencyDurationCount: 5,
497
+ enableWorker: !isSafari
498
+ }, isSafari && {
499
+ maxBufferHole: 2,
500
+ maxSeekHole: 2
501
+ }));
502
+ hls.loadSource(hlsSrc);
503
+ hls.attachMedia(audioPlayerRef.current);
504
+ hlsRef.current = hls;
505
+ } else {
506
+ // Non-HLS: call load() to prime the new source.
507
+ // Safari: skip load() when playing — the synchronous play() called in the
508
+ // gesture handler would be aborted (AbortError), losing the user activation token.
509
+ // Chrome/Firefox: always call load(); AudioContext defers play() via pendingPlayRef
510
+ // until after this effect, so load() and play() are sequenced with no concurrent abort.
511
+ if (hasInitializedRef.current && (!isSafari || !isPlayingRef.current)) {
512
+ try {
513
+ audioPlayerRef.current.load();
514
+ } catch (err) {
515
+ console.warn('Failed to reload audio source:', err);
516
+ }
361
517
  }
362
518
  }
519
+ hasInitializedRef.current = true;
520
+ return function () {
521
+ if (hlsRef.current) {
522
+ hlsRef.current.destroy();
523
+ hlsRef.current = null;
524
+ }
525
+ };
363
526
  }, [JSON.stringify(audioSrc)]);
364
527
 
365
528
  // Set initial volume to 100%
@@ -379,16 +542,13 @@ var ReactAudioPlayerInner = function ReactAudioPlayerInner(props) {
379
542
  preload: "none",
380
543
  onLoadedMetadata: onLoadedMetadata,
381
544
  muted: isMuted
382
- }, Array.isArray(audioSrc) ? audioSrc.map(function (src, index) {
545
+ }, !isHlsManaged && (Array.isArray(audioSrc) ? audioSrc : [audioSrc]).map(function (url, i) {
383
546
  return /*#__PURE__*/React.createElement("source", {
384
- key: index,
385
- src: src,
386
- type: getTypeFromExtension(src)
547
+ key: i,
548
+ src: url,
549
+ type: getTypeFromExtension(url)
387
550
  });
388
- }) : audioSrc ? /*#__PURE__*/React.createElement("source", {
389
- src: audioSrc,
390
- type: getTypeFromExtension(audioSrc)
391
- }) : null, "Your browser does not support the audio element."), /*#__PURE__*/React.createElement("div", {
551
+ })), /*#__PURE__*/React.createElement("div", {
392
552
  className: "player-layout"
393
553
  }, volumeCtrl && /*#__PURE__*/React.createElement("div", {
394
554
  className: "player-controls-secondary-outer"
@@ -485,11 +645,13 @@ var ReactAudioPlayer = function ReactAudioPlayer(props) {
485
645
  // references
486
646
  var audioPlayerRef = (_props$audioPlayerRef = props.audioPlayerRef) !== null && _props$audioPlayerRef !== void 0 ? _props$audioPlayerRef : React.useRef(); // reference our audio component
487
647
  var progressBarRef = (_props$progressBarRef = props.progressBarRef) !== null && _props$progressBarRef !== void 0 ? _props$progressBarRef : React.useRef(); // reference our progress bar
488
-
648
+ var hlsRef = React.useRef(null);
489
649
  var customStyles = props ? props.style : '';
490
650
 
491
651
  // hooks
492
- var _useAudioPlayer = useAudioPlayer(audioPlayerRef, progressBarRef),
652
+ var _useAudioPlayer = useAudioPlayer(audioPlayerRef, progressBarRef, {
653
+ hlsRef: hlsRef
654
+ }),
493
655
  isPlaying = _useAudioPlayer.isPlaying,
494
656
  currentTime = _useAudioPlayer.currentTime,
495
657
  duration = _useAudioPlayer.duration,
@@ -506,6 +668,7 @@ var ReactAudioPlayer = function ReactAudioPlayer(props) {
506
668
  return /*#__PURE__*/React.createElement(ReactAudioPlayerInner, _extends({}, props, {
507
669
  audioPlayerRef: audioPlayerRef,
508
670
  progressBarRef: progressBarRef,
671
+ hlsRef: hlsRef,
509
672
  isPlaying: isPlaying,
510
673
  isMuted: isMuted,
511
674
  currentTime: currentTime,
@@ -1,4 +1,5 @@
1
1
  import React, { useState, useRef, useEffect } from 'react';
2
+ import Hls from 'hls.js';
2
3
 
3
4
  function _arrayLikeToArray(r, a) {
4
5
  (null == a || a > r.length) && (a = r.length);
@@ -8,6 +9,14 @@ function _arrayLikeToArray(r, a) {
8
9
  function _arrayWithHoles(r) {
9
10
  if (Array.isArray(r)) return r;
10
11
  }
12
+ function _defineProperty(e, r, t) {
13
+ return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
14
+ value: t,
15
+ enumerable: true,
16
+ configurable: true,
17
+ writable: true
18
+ }) : e[r] = t, e;
19
+ }
11
20
  function _extends() {
12
21
  return _extends = Object.assign ? Object.assign.bind() : function (n) {
13
22
  for (var e = 1; e < arguments.length; e++) {
@@ -47,9 +56,44 @@ function _iterableToArrayLimit(r, l) {
47
56
  function _nonIterableRest() {
48
57
  throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
49
58
  }
59
+ function ownKeys(e, r) {
60
+ var t = Object.keys(e);
61
+ if (Object.getOwnPropertySymbols) {
62
+ var o = Object.getOwnPropertySymbols(e);
63
+ r && (o = o.filter(function (r) {
64
+ return Object.getOwnPropertyDescriptor(e, r).enumerable;
65
+ })), t.push.apply(t, o);
66
+ }
67
+ return t;
68
+ }
69
+ function _objectSpread2(e) {
70
+ for (var r = 1; r < arguments.length; r++) {
71
+ var t = null != arguments[r] ? arguments[r] : {};
72
+ r % 2 ? ownKeys(Object(t), true).forEach(function (r) {
73
+ _defineProperty(e, r, t[r]);
74
+ }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) {
75
+ Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
76
+ });
77
+ }
78
+ return e;
79
+ }
50
80
  function _slicedToArray(r, e) {
51
81
  return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
52
82
  }
83
+ function _toPrimitive(t, r) {
84
+ if ("object" != typeof t || !t) return t;
85
+ var e = t[Symbol.toPrimitive];
86
+ if (void 0 !== e) {
87
+ var i = e.call(t, r);
88
+ if ("object" != typeof i) return i;
89
+ throw new TypeError("@@toPrimitive must return a primitive value.");
90
+ }
91
+ return ("string" === r ? String : Number)(t);
92
+ }
93
+ function _toPropertyKey(t) {
94
+ var i = _toPrimitive(t, "string");
95
+ return "symbol" == typeof i ? i : i + "";
96
+ }
53
97
  function _unsupportedIterableToArray(r, a) {
54
98
  if (r) {
55
99
  if ("string" == typeof r) return _arrayLikeToArray(r, a);
@@ -58,8 +102,11 @@ function _unsupportedIterableToArray(r, a) {
58
102
  }
59
103
  }
60
104
 
61
- var useAudioPlayer = function useAudioPlayer(audioRef, progressBarRef, volumeCtrl) {
62
- var initialDuration = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : undefined;
105
+ var useAudioPlayer = function useAudioPlayer(audioRef, progressBarRef) {
106
+ var _ref = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {},
107
+ volumeCtrl = _ref.volumeCtrl,
108
+ initialDuration = _ref.initialDuration,
109
+ hlsRef = _ref.hlsRef;
63
110
  var _useState = useState(false),
64
111
  _useState2 = _slicedToArray(_useState, 2),
65
112
  isPlaying = _useState2[0],
@@ -76,11 +123,12 @@ var useAudioPlayer = function useAudioPlayer(audioRef, progressBarRef, volumeCtr
76
123
  _useState8 = _slicedToArray(_useState7, 2),
77
124
  isFinishedPlaying = _useState8[0],
78
125
  setIsFinishedPlaying = _useState8[1];
79
- var animationRef = useRef(); // reference the animation
126
+ var animationRef = useRef();
127
+ var pendingPlayAbortRef = useRef(null);
80
128
  var _useState9 = useState(false),
81
- _useState10 = _slicedToArray(_useState9, 2),
82
- isMuted = _useState10[0],
83
- setIsMuted = _useState10[1];
129
+ _useState0 = _slicedToArray(_useState9, 2),
130
+ isMuted = _useState0[0],
131
+ setIsMuted = _useState0[1];
84
132
  var isStream = audioRef.current && audioRef.current.duration === Infinity;
85
133
  useEffect(function () {
86
134
  if (currentTime === Number(duration)) {
@@ -139,6 +187,10 @@ var useAudioPlayer = function useAudioPlayer(audioRef, progressBarRef, volumeCtr
139
187
  animationRef.current = window.requestAnimationFrame(_whilePlaying);
140
188
  };
141
189
  var pause = function pause() {
190
+ if (pendingPlayAbortRef.current) {
191
+ pendingPlayAbortRef.current();
192
+ pendingPlayAbortRef.current = null;
193
+ }
142
194
  setIsPlaying(false);
143
195
  audioRef.current.pause();
144
196
  window.cancelAnimationFrame(animationRef.current);
@@ -150,26 +202,80 @@ var useAudioPlayer = function useAudioPlayer(audioRef, progressBarRef, volumeCtr
150
202
  // pause()
151
203
  // }
152
204
 
205
+ var _safePlay = function safePlay(audio) {
206
+ var promise = audio.play();
207
+ if (promise !== undefined) {
208
+ promise["catch"](function (err) {
209
+ if (err.name === 'NotAllowedError') {
210
+ setIsPlaying(false);
211
+ } else if (err.name === 'AbortError') {
212
+ // play() was interrupted by a concurrent load() (e.g. ReactAudioPlayerInner
213
+ // calling load() after a source change) — retry once canplay fires.
214
+ // If the audio element was already unlocked by a prior play() call within
215
+ // a user gesture (e.g. the Safari fix in AudioContext), this retry succeeds.
216
+ audio.addEventListener('canplay', function () {
217
+ return _safePlay(audio);
218
+ }, {
219
+ once: true
220
+ });
221
+ }
222
+ });
223
+ }
224
+ };
153
225
  var play = function play() {
154
226
  setIsPlaying(true);
155
227
  setIsFinishedPlaying(false);
228
+ var elDuration = audioRef.current.duration;
229
+ var isLiveOrUnloaded = elDuration === Infinity || isNaN(elDuration);
230
+ if (isLiveOrUnloaded) {
231
+ var _audioRef$current$cur;
232
+ if (hlsRef !== null && hlsRef !== void 0 && hlsRef.current) {
233
+ var audio = audioRef.current;
234
+ var hls = hlsRef.current;
235
+ // If data is already buffered (e.g. resuming after pause), play immediately.
236
+ // Otherwise wait for the first fragment so Safari's audio decoder is warm
237
+ // before output starts, preventing the first syllable from being cut.
238
+ if (audio.buffered.length > 0) {
239
+ _safePlay(audio);
240
+ } else {
241
+ var _onFragBuffered = function onFragBuffered() {
242
+ pendingPlayAbortRef.current = null;
243
+ hls.off('hlsFragBuffered', _onFragBuffered);
244
+ _safePlay(audio);
245
+ };
246
+ pendingPlayAbortRef.current = function () {
247
+ return hls.off('hlsFragBuffered', _onFragBuffered);
248
+ };
249
+ hls.on('hlsFragBuffered', _onFragBuffered);
250
+ }
251
+ } else if (elDuration === Infinity && (_audioRef$current$cur = audioRef.current.currentSrc) !== null && _audioRef$current$cur !== void 0 && _audioRef$current$cur.split('?')[0].endsWith('.m3u8')) {
252
+ // Native live stream (no hls.js, e.g. iOS Safari): force a fresh manifest
253
+ // fetch so we don't play from a stale buffer position across a discontinuity.
254
+ var onCanPlay = function onCanPlay() {
255
+ pendingPlayAbortRef.current = null;
256
+ if (audioRef.current) _safePlay(audioRef.current);
257
+ };
258
+ pendingPlayAbortRef.current = function () {
259
+ var _audioRef$current;
260
+ return (_audioRef$current = audioRef.current) === null || _audioRef$current === void 0 ? void 0 : _audioRef$current.removeEventListener('canplay', onCanPlay);
261
+ };
262
+ audioRef.current.addEventListener('canplay', onCanPlay, {
263
+ once: true
264
+ });
265
+ audioRef.current.load();
266
+ } else {
267
+ // Finite audio not yet loaded (duration is NaN): call play() directly so
268
+ // Safari's user-gesture scope isn't lost waiting for an async canplay event.
269
+ _safePlay(audioRef.current);
270
+ }
271
+ } else {
272
+ _safePlay(audioRef.current);
156
273
 
157
- // For live streams (duration === Infinity), reset the audio element before
158
- // playing. The audioSrc useEffect in ReactAudioPlayerInner calls load() on
159
- // mount, which causes the browser to pre-buffer the HLS stream. By the time
160
- // the user clicks play the manifest's seekable window has moved forward and
161
- // old segments are gone, so the browser auto-seeks to the earliest available
162
- // position (e.g. 20s in), causing ads to start mid-stream. Calling load()
163
- // here reconnects to the current live edge and gets a fresh manifest.
164
- if (duration === Infinity) {
165
- audioRef.current.load();
166
- }
167
- audioRef.current.play();
168
-
169
- // Only start RAF loop for non-live streams with valid duration
170
- var dur = audioRef.current.duration;
171
- if (dur !== Infinity && !isNaN(dur) && isFinite(dur)) {
172
- animationRef.current = window.requestAnimationFrame(_whilePlaying);
274
+ // Only start RAF loop for non-live streams with valid duration
275
+ var dur = audioRef.current.duration;
276
+ if (dur !== Infinity && !isNaN(dur) && isFinite(dur)) {
277
+ animationRef.current = window.requestAnimationFrame(_whilePlaying);
278
+ }
173
279
  }
174
280
  };
175
281
  var toggleMute = function toggleMute() {
@@ -298,6 +404,13 @@ var Pause = function Pause() {
298
404
  }));
299
405
  };
300
406
 
407
+ var getHlsSrc = function getHlsSrc(audioSrc) {
408
+ var _urls$find;
409
+ var urls = Array.isArray(audioSrc) ? audioSrc : [audioSrc];
410
+ return (_urls$find = urls.find(function (url) {
411
+ return url && url.split('?')[0].endsWith('.m3u8');
412
+ })) !== null && _urls$find !== void 0 ? _urls$find : null;
413
+ };
301
414
  var getTypeFromExtension = function getTypeFromExtension(url) {
302
415
  var extension = url.split('.').pop().split('?')[0];
303
416
  switch (extension) {
@@ -317,11 +430,15 @@ var getTypeFromExtension = function getTypeFromExtension(url) {
317
430
  }
318
431
  };
319
432
  var ReactAudioPlayerInner = function ReactAudioPlayerInner(props) {
320
- var _props$audioPlayerRef, _props$progressBarRef;
321
- // references
322
- var audioPlayerRef = (_props$audioPlayerRef = props.audioPlayerRef) !== null && _props$audioPlayerRef !== void 0 ? _props$audioPlayerRef : useRef(); // reference our audio component
323
- var progressBarRef = (_props$progressBarRef = props.progressBarRef) !== null && _props$progressBarRef !== void 0 ? _props$progressBarRef : useRef(); // reference our progress bar
324
-
433
+ var _props$audioPlayerRef, _props$progressBarRef, _props$hlsRef;
434
+ // Always call hooks unconditionally — use internal refs when props don't provide them
435
+ var internalAudioRef = useRef();
436
+ var internalProgressBarRef = useRef();
437
+ var internalHlsRef = useRef(null);
438
+ var hasInitializedRef = useRef(false);
439
+ // Track isPlaying via a ref so the source-change useEffect can read the
440
+ // current value without adding isPlaying to its dependency array.
441
+ var isPlayingRef = useRef(false);
325
442
  var customStyles = props ? props.style : '';
326
443
  var title = props.title,
327
444
  audioSrc = props.audioSrc,
@@ -344,20 +461,66 @@ var ReactAudioPlayerInner = function ReactAudioPlayerInner(props) {
344
461
  forwardControl = props.forwardControl,
345
462
  subtitle = props.subtitle,
346
463
  prefix = props.prefix;
464
+ var audioPlayerRef = (_props$audioPlayerRef = props.audioPlayerRef) !== null && _props$audioPlayerRef !== void 0 ? _props$audioPlayerRef : internalAudioRef;
465
+ var progressBarRef = (_props$progressBarRef = props.progressBarRef) !== null && _props$progressBarRef !== void 0 ? _props$progressBarRef : internalProgressBarRef;
466
+ var hlsRef = (_props$hlsRef = props.hlsRef) !== null && _props$hlsRef !== void 0 ? _props$hlsRef : internalHlsRef;
467
+ var hlsSrcForRender = getHlsSrc(audioSrc);
468
+ var isHlsManaged = !!(hlsSrcForRender && Hls.isSupported());
469
+
470
+ // Keep isPlayingRef in sync on every render (runs before effects).
471
+ isPlayingRef.current = isPlaying;
347
472
  var audioDuration = duration && !isNaN(duration) && calculateTime(duration);
348
473
  var formatDuration = duration && !isNaN(duration) && audioDuration && formatCalculateTime(audioDuration);
349
474
 
350
- // Reload audio when audioSrc changes
351
- // Use JSON.stringify to handle array comparisons by value instead of reference
475
+ // Manage audio source changes. For HLS sources hls.js owns the loading cycle;
476
+ // for everything else we fall back to the native load() path.
477
+ // JSON.stringify handles array-valued audioSrc comparisons by value.
352
478
  useEffect(function () {
353
- if (audioPlayerRef.current && audioSrc) {
479
+ if (!audioPlayerRef.current || !audioSrc) return;
480
+ if (hasInitializedRef.current) {
354
481
  resetDuration === null || resetDuration === void 0 ? void 0 : resetDuration();
355
- try {
356
- audioPlayerRef.current.load();
357
- } catch (err) {
358
- console.warn('Failed to reload audio source:', err);
482
+ }
483
+
484
+ // Tear down any existing hls.js instance before re-evaluating the source.
485
+ if (hlsRef.current) {
486
+ hlsRef.current.destroy();
487
+ hlsRef.current = null;
488
+ }
489
+ var hlsSrc = getHlsSrc(audioSrc);
490
+ var isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
491
+ if (hlsSrc && Hls.isSupported()) {
492
+ var hls = new Hls(_objectSpread2({
493
+ liveSyncDurationCount: 3,
494
+ liveMaxLatencyDurationCount: 5,
495
+ enableWorker: !isSafari
496
+ }, isSafari && {
497
+ maxBufferHole: 2,
498
+ maxSeekHole: 2
499
+ }));
500
+ hls.loadSource(hlsSrc);
501
+ hls.attachMedia(audioPlayerRef.current);
502
+ hlsRef.current = hls;
503
+ } else {
504
+ // Non-HLS: call load() to prime the new source.
505
+ // Safari: skip load() when playing — the synchronous play() called in the
506
+ // gesture handler would be aborted (AbortError), losing the user activation token.
507
+ // Chrome/Firefox: always call load(); AudioContext defers play() via pendingPlayRef
508
+ // until after this effect, so load() and play() are sequenced with no concurrent abort.
509
+ if (hasInitializedRef.current && (!isSafari || !isPlayingRef.current)) {
510
+ try {
511
+ audioPlayerRef.current.load();
512
+ } catch (err) {
513
+ console.warn('Failed to reload audio source:', err);
514
+ }
359
515
  }
360
516
  }
517
+ hasInitializedRef.current = true;
518
+ return function () {
519
+ if (hlsRef.current) {
520
+ hlsRef.current.destroy();
521
+ hlsRef.current = null;
522
+ }
523
+ };
361
524
  }, [JSON.stringify(audioSrc)]);
362
525
 
363
526
  // Set initial volume to 100%
@@ -377,16 +540,13 @@ var ReactAudioPlayerInner = function ReactAudioPlayerInner(props) {
377
540
  preload: "none",
378
541
  onLoadedMetadata: onLoadedMetadata,
379
542
  muted: isMuted
380
- }, Array.isArray(audioSrc) ? audioSrc.map(function (src, index) {
543
+ }, !isHlsManaged && (Array.isArray(audioSrc) ? audioSrc : [audioSrc]).map(function (url, i) {
381
544
  return /*#__PURE__*/React.createElement("source", {
382
- key: index,
383
- src: src,
384
- type: getTypeFromExtension(src)
545
+ key: i,
546
+ src: url,
547
+ type: getTypeFromExtension(url)
385
548
  });
386
- }) : audioSrc ? /*#__PURE__*/React.createElement("source", {
387
- src: audioSrc,
388
- type: getTypeFromExtension(audioSrc)
389
- }) : null, "Your browser does not support the audio element."), /*#__PURE__*/React.createElement("div", {
549
+ })), /*#__PURE__*/React.createElement("div", {
390
550
  className: "player-layout"
391
551
  }, volumeCtrl && /*#__PURE__*/React.createElement("div", {
392
552
  className: "player-controls-secondary-outer"
@@ -483,11 +643,13 @@ var ReactAudioPlayer = function ReactAudioPlayer(props) {
483
643
  // references
484
644
  var audioPlayerRef = (_props$audioPlayerRef = props.audioPlayerRef) !== null && _props$audioPlayerRef !== void 0 ? _props$audioPlayerRef : useRef(); // reference our audio component
485
645
  var progressBarRef = (_props$progressBarRef = props.progressBarRef) !== null && _props$progressBarRef !== void 0 ? _props$progressBarRef : useRef(); // reference our progress bar
486
-
646
+ var hlsRef = useRef(null);
487
647
  var customStyles = props ? props.style : '';
488
648
 
489
649
  // hooks
490
- var _useAudioPlayer = useAudioPlayer(audioPlayerRef, progressBarRef),
650
+ var _useAudioPlayer = useAudioPlayer(audioPlayerRef, progressBarRef, {
651
+ hlsRef: hlsRef
652
+ }),
491
653
  isPlaying = _useAudioPlayer.isPlaying,
492
654
  currentTime = _useAudioPlayer.currentTime,
493
655
  duration = _useAudioPlayer.duration,
@@ -504,6 +666,7 @@ var ReactAudioPlayer = function ReactAudioPlayer(props) {
504
666
  return /*#__PURE__*/React.createElement(ReactAudioPlayerInner, _extends({}, props, {
505
667
  audioPlayerRef: audioPlayerRef,
506
668
  progressBarRef: progressBarRef,
669
+ hlsRef: hlsRef,
507
670
  isPlaying: isPlaying,
508
671
  isMuted: isMuted,
509
672
  currentTime: currentTime,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apm-react-audio-player",
3
- "version": "1.1.3",
3
+ "version": "2.0.0",
4
4
  "author": "Jason Phan <jphan@mpr.org>",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -31,6 +31,7 @@
31
31
  "react": "^16.0.0"
32
32
  },
33
33
  "dependencies": {
34
+ "hls.js": "^1.6.16",
34
35
  "rollup": "^4.59.0",
35
36
  "rollup-plugin-babel": "^4.3.3"
36
37
  },
@@ -69,12 +70,15 @@
69
70
  "tough-cookie": ">=4.1.3",
70
71
  "ajv": ">=6.14.0",
71
72
  "qs": ">=6.14.1",
72
- "lodash": ">=4.17.21",
73
+ "lodash": ">=4.18.0",
73
74
  "js-yaml": ">=4.1.0",
74
- "@babel/helpers": ">=7.26.0",
75
- "@babel/runtime": ">=7.26.0",
76
- "flatted": ">=3.3.3",
77
- "brace-expansion": ">=2.0.1",
75
+ "@babel/helpers": ">=7.26.10",
76
+ "@babel/runtime": ">=7.26.10",
77
+ "@babel/plugin-transform-modules-systemjs": ">=7.29.0",
78
+ "flatted": ">=3.4.2",
79
+ "brace-expansion": ">=5.0.5",
80
+ "fast-uri": ">=3.1.2",
81
+ "picomatch": ">=2.3.2",
78
82
  "tmp": ">=0.2.3"
79
83
  },
80
84
  "files": [