apm-react-audio-player 1.2.0 → 2.0.1

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: !0,
18
+ configurable: !0,
19
+ writable: !0
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++) {
@@ -27,15 +36,15 @@ function _iterableToArrayLimit(r, l) {
27
36
  i,
28
37
  u,
29
38
  a = [],
30
- f = true,
31
- o = false;
39
+ f = !0,
40
+ o = !1;
32
41
  try {
33
42
  if (i = (t = t.call(r)).next, 0 === l) {
34
43
  if (Object(t) !== t) return;
35
44
  f = !1;
36
45
  } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);
37
46
  } catch (r) {
38
- o = true, n = r;
47
+ o = !0, n = r;
39
48
  } finally {
40
49
  try {
41
50
  if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
@@ -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), !0).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 || "default");
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
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);
326
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,27 +463,67 @@ 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
- // Skip load() on initial mount calling it pre-buffers live streams so that
354
- // by the time the user clicks play the seekable window has advanced and
355
- // segments are stale. On first play the browser fetches the live edge
356
- // naturally. On subsequent src changes we do call load() to reset the element.
357
- // 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.
358
480
  React.useEffect(function () {
359
- if (audioPlayerRef.current && audioSrc) {
360
- if (hasInitializedRef.current) {
361
- resetDuration === null || resetDuration === void 0 ? void 0 : resetDuration();
481
+ if (!audioPlayerRef.current || !audioSrc) return;
482
+ if (hasInitializedRef.current) {
483
+ resetDuration === null || resetDuration === void 0 ? void 0 : resetDuration();
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
+ autoStartLoad: false,
496
+ liveSyncDurationCount: 3,
497
+ liveMaxLatencyDurationCount: 5,
498
+ enableWorker: !isSafari
499
+ }, isSafari && {
500
+ maxBufferHole: 2,
501
+ maxSeekHole: 2
502
+ }));
503
+ hls.loadSource(hlsSrc);
504
+ hls.attachMedia(audioPlayerRef.current);
505
+ hlsRef.current = hls;
506
+ } else {
507
+ // Non-HLS: call load() to prime the new source.
508
+ // Safari: skip load() when playing — the synchronous play() called in the
509
+ // gesture handler would be aborted (AbortError), losing the user activation token.
510
+ // Chrome/Firefox: always call load(); AudioContext defers play() via pendingPlayRef
511
+ // until after this effect, so load() and play() are sequenced with no concurrent abort.
512
+ if (hasInitializedRef.current && (!isSafari || !isPlayingRef.current)) {
362
513
  try {
363
514
  audioPlayerRef.current.load();
364
515
  } catch (err) {
365
516
  console.warn('Failed to reload audio source:', err);
366
517
  }
367
518
  }
368
- hasInitializedRef.current = true;
369
519
  }
520
+ hasInitializedRef.current = true;
521
+ return function () {
522
+ if (hlsRef.current) {
523
+ hlsRef.current.destroy();
524
+ hlsRef.current = null;
525
+ }
526
+ };
370
527
  }, [JSON.stringify(audioSrc)]);
371
528
 
372
529
  // Set initial volume to 100%
@@ -386,16 +543,13 @@ var ReactAudioPlayerInner = function ReactAudioPlayerInner(props) {
386
543
  preload: "none",
387
544
  onLoadedMetadata: onLoadedMetadata,
388
545
  muted: isMuted
389
- }, Array.isArray(audioSrc) ? audioSrc.map(function (src, index) {
546
+ }, !isHlsManaged && (Array.isArray(audioSrc) ? audioSrc : [audioSrc]).map(function (url, i) {
390
547
  return /*#__PURE__*/React.createElement("source", {
391
- key: index,
392
- src: src,
393
- type: getTypeFromExtension(src)
548
+ key: i,
549
+ src: url,
550
+ type: getTypeFromExtension(url)
394
551
  });
395
- }) : audioSrc ? /*#__PURE__*/React.createElement("source", {
396
- src: audioSrc,
397
- type: getTypeFromExtension(audioSrc)
398
- }) : null, "Your browser does not support the audio element."), /*#__PURE__*/React.createElement("div", {
552
+ })), /*#__PURE__*/React.createElement("div", {
399
553
  className: "player-layout"
400
554
  }, volumeCtrl && /*#__PURE__*/React.createElement("div", {
401
555
  className: "player-controls-secondary-outer"
@@ -492,11 +646,13 @@ var ReactAudioPlayer = function ReactAudioPlayer(props) {
492
646
  // references
493
647
  var audioPlayerRef = (_props$audioPlayerRef = props.audioPlayerRef) !== null && _props$audioPlayerRef !== void 0 ? _props$audioPlayerRef : React.useRef(); // reference our audio component
494
648
  var progressBarRef = (_props$progressBarRef = props.progressBarRef) !== null && _props$progressBarRef !== void 0 ? _props$progressBarRef : React.useRef(); // reference our progress bar
495
-
649
+ var hlsRef = React.useRef(null);
496
650
  var customStyles = props ? props.style : '';
497
651
 
498
652
  // hooks
499
- var _useAudioPlayer = useAudioPlayer(audioPlayerRef, progressBarRef),
653
+ var _useAudioPlayer = useAudioPlayer(audioPlayerRef, progressBarRef, {
654
+ hlsRef: hlsRef
655
+ }),
500
656
  isPlaying = _useAudioPlayer.isPlaying,
501
657
  currentTime = _useAudioPlayer.currentTime,
502
658
  duration = _useAudioPlayer.duration,
@@ -513,6 +669,7 @@ var ReactAudioPlayer = function ReactAudioPlayer(props) {
513
669
  return /*#__PURE__*/React.createElement(ReactAudioPlayerInner, _extends({}, props, {
514
670
  audioPlayerRef: audioPlayerRef,
515
671
  progressBarRef: progressBarRef,
672
+ hlsRef: hlsRef,
516
673
  isPlaying: isPlaying,
517
674
  isMuted: isMuted,
518
675
  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: !0,
16
+ configurable: !0,
17
+ writable: !0
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++) {
@@ -25,15 +34,15 @@ function _iterableToArrayLimit(r, l) {
25
34
  i,
26
35
  u,
27
36
  a = [],
28
- f = true,
29
- o = false;
37
+ f = !0,
38
+ o = !1;
30
39
  try {
31
40
  if (i = (t = t.call(r)).next, 0 === l) {
32
41
  if (Object(t) !== t) return;
33
42
  f = !1;
34
43
  } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);
35
44
  } catch (r) {
36
- o = true, n = r;
45
+ o = !0, n = r;
37
46
  } finally {
38
47
  try {
39
48
  if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
@@ -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), !0).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 || "default");
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
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);
324
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,27 +461,67 @@ 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
- // Skip load() on initial mount calling it pre-buffers live streams so that
352
- // by the time the user clicks play the seekable window has advanced and
353
- // segments are stale. On first play the browser fetches the live edge
354
- // naturally. On subsequent src changes we do call load() to reset the element.
355
- // 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.
356
478
  useEffect(function () {
357
- if (audioPlayerRef.current && audioSrc) {
358
- if (hasInitializedRef.current) {
359
- resetDuration === null || resetDuration === void 0 ? void 0 : resetDuration();
479
+ if (!audioPlayerRef.current || !audioSrc) return;
480
+ if (hasInitializedRef.current) {
481
+ resetDuration === null || resetDuration === void 0 ? void 0 : resetDuration();
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
+ autoStartLoad: false,
494
+ liveSyncDurationCount: 3,
495
+ liveMaxLatencyDurationCount: 5,
496
+ enableWorker: !isSafari
497
+ }, isSafari && {
498
+ maxBufferHole: 2,
499
+ maxSeekHole: 2
500
+ }));
501
+ hls.loadSource(hlsSrc);
502
+ hls.attachMedia(audioPlayerRef.current);
503
+ hlsRef.current = hls;
504
+ } else {
505
+ // Non-HLS: call load() to prime the new source.
506
+ // Safari: skip load() when playing — the synchronous play() called in the
507
+ // gesture handler would be aborted (AbortError), losing the user activation token.
508
+ // Chrome/Firefox: always call load(); AudioContext defers play() via pendingPlayRef
509
+ // until after this effect, so load() and play() are sequenced with no concurrent abort.
510
+ if (hasInitializedRef.current && (!isSafari || !isPlayingRef.current)) {
360
511
  try {
361
512
  audioPlayerRef.current.load();
362
513
  } catch (err) {
363
514
  console.warn('Failed to reload audio source:', err);
364
515
  }
365
516
  }
366
- hasInitializedRef.current = true;
367
517
  }
518
+ hasInitializedRef.current = true;
519
+ return function () {
520
+ if (hlsRef.current) {
521
+ hlsRef.current.destroy();
522
+ hlsRef.current = null;
523
+ }
524
+ };
368
525
  }, [JSON.stringify(audioSrc)]);
369
526
 
370
527
  // Set initial volume to 100%
@@ -384,16 +541,13 @@ var ReactAudioPlayerInner = function ReactAudioPlayerInner(props) {
384
541
  preload: "none",
385
542
  onLoadedMetadata: onLoadedMetadata,
386
543
  muted: isMuted
387
- }, Array.isArray(audioSrc) ? audioSrc.map(function (src, index) {
544
+ }, !isHlsManaged && (Array.isArray(audioSrc) ? audioSrc : [audioSrc]).map(function (url, i) {
388
545
  return /*#__PURE__*/React.createElement("source", {
389
- key: index,
390
- src: src,
391
- type: getTypeFromExtension(src)
546
+ key: i,
547
+ src: url,
548
+ type: getTypeFromExtension(url)
392
549
  });
393
- }) : audioSrc ? /*#__PURE__*/React.createElement("source", {
394
- src: audioSrc,
395
- type: getTypeFromExtension(audioSrc)
396
- }) : null, "Your browser does not support the audio element."), /*#__PURE__*/React.createElement("div", {
550
+ })), /*#__PURE__*/React.createElement("div", {
397
551
  className: "player-layout"
398
552
  }, volumeCtrl && /*#__PURE__*/React.createElement("div", {
399
553
  className: "player-controls-secondary-outer"
@@ -490,11 +644,13 @@ var ReactAudioPlayer = function ReactAudioPlayer(props) {
490
644
  // references
491
645
  var audioPlayerRef = (_props$audioPlayerRef = props.audioPlayerRef) !== null && _props$audioPlayerRef !== void 0 ? _props$audioPlayerRef : useRef(); // reference our audio component
492
646
  var progressBarRef = (_props$progressBarRef = props.progressBarRef) !== null && _props$progressBarRef !== void 0 ? _props$progressBarRef : useRef(); // reference our progress bar
493
-
647
+ var hlsRef = useRef(null);
494
648
  var customStyles = props ? props.style : '';
495
649
 
496
650
  // hooks
497
- var _useAudioPlayer = useAudioPlayer(audioPlayerRef, progressBarRef),
651
+ var _useAudioPlayer = useAudioPlayer(audioPlayerRef, progressBarRef, {
652
+ hlsRef: hlsRef
653
+ }),
498
654
  isPlaying = _useAudioPlayer.isPlaying,
499
655
  currentTime = _useAudioPlayer.currentTime,
500
656
  duration = _useAudioPlayer.duration,
@@ -511,6 +667,7 @@ var ReactAudioPlayer = function ReactAudioPlayer(props) {
511
667
  return /*#__PURE__*/React.createElement(ReactAudioPlayerInner, _extends({}, props, {
512
668
  audioPlayerRef: audioPlayerRef,
513
669
  progressBarRef: progressBarRef,
670
+ hlsRef: hlsRef,
514
671
  isPlaying: isPlaying,
515
672
  isMuted: isMuted,
516
673
  currentTime: currentTime,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apm-react-audio-player",
3
- "version": "1.2.0",
3
+ "version": "2.0.1",
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": [