react-native-mp3-player 1.0.5 → 1.0.6

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/PERFORMANCE.md ADDED
@@ -0,0 +1,76 @@
1
+ # Performance and optimization
2
+
3
+ This document describes how to get the best speed and lowest overhead from **react-native-mp3-player**, and what the package does internally to reduce work.
4
+
5
+ ---
6
+
7
+ ## 1. Progress updates (fewer bridge calls)
8
+
9
+ ### Prefer native-driven progress when possible
10
+
11
+ - **Set `progressUpdateEventInterval`** in `updateOptions()` (in **seconds**, e.g. `1` for every second). The native layer will then emit `Event.PlaybackProgressUpdated` at that interval instead of you polling.
12
+ - **Use `useProgress(intervalMs, background, { useNativeEvents: true })`** so the hook subscribes to `PlaybackProgressUpdated` and updates state from events. You still get a slow fallback poll (e.g. every 3 s) for drift. This reduces bridge round-trips compared to polling every second.
13
+
14
+ Example:
15
+
16
+ ```ts
17
+ // In your setup / updateOptions:
18
+ await TrackPlayer.updateOptions({
19
+ capabilities: [...],
20
+ progressUpdateEventInterval: 1, // seconds; native emits every 1 s
21
+ });
22
+
23
+ // In your component:
24
+ const progress = useProgress(1000, true, { useNativeEvents: true });
25
+ ```
26
+
27
+ - **Avoid** using a very small `updateInterval` in `useProgress` (e.g. 250 ms) without `useNativeEvents: true`; that causes 4+ `getProgress()` bridge calls per second. If you need smooth progress, set `progressUpdateEventInterval` to 0.25 and use `useNativeEvents: true` with a fallback poll of ~2–3 s.
28
+
29
+ ---
30
+
31
+ ## 2. iOS: Now Playing and progress tick
32
+
33
+ - When **`progressUpdateEventInterval`** is set and ≤ 1 second, the native progress tick also updates **MPNowPlayingInfoCenter** (elapsed/duration). A separate 1-second timer is not used in that case, so there is only one periodic task instead of two.
34
+ - When `progressUpdateEventInterval` is 0 or > 1 s, a dedicated 1-second timer runs to keep the lock screen / Control Center widget in sync.
35
+
36
+ ---
37
+
38
+ ## 3. Batch and debounce in your app
39
+
40
+ - **Seeking:** If the user drags a slider, debounce or throttle `seekTo()` (e.g. at most one call every 200–300 ms) so you don’t flood the bridge.
41
+ - **Metadata:** Prefer a single `updateNowPlayingMetadata()` or `updateMetadataForTrack()` with all fields instead of multiple small updates.
42
+ - **Queue changes:** Prefer `setQueue()` or a single `add()` with multiple tracks over many small `add()` calls when building a queue.
43
+
44
+ ---
45
+
46
+ ## 4. Buffer and streaming (setup options)
47
+
48
+ - Use **`minBuffer`**, **`maxBuffer`**, **`playBuffer`** (and Android equivalents where supported) to tune buffering. Larger buffers can reduce stalling on slow networks but use more memory; smaller values can reduce memory and startup time for short clips.
49
+ - For **local files**, you typically need minimal buffering; the default or small values are usually enough.
50
+
51
+ ---
52
+
53
+ ## 5. Android
54
+
55
+ - **Progress:** When `progressUpdateEventInterval` is set, the service uses a single coroutine/flow to emit progress at that interval. No change needed on your side; avoid polling with `getProgress()` at a higher rate than the native interval if you can use `Event.PlaybackProgressUpdated` and `useProgress(..., { useNativeEvents: true })` instead.
56
+ - **Service:** Playback runs in a Media3 `MediaLibraryService`; the system manages the process. Avoid doing heavy work on the JS thread while playback is active so the bridge can process events promptly.
57
+
58
+ ---
59
+
60
+ ## 6. General
61
+
62
+ - **Events vs polling:** Prefer `addEventListener(Event.PlaybackState, ...)` and similar for state changes instead of repeatedly calling `getPlaybackState()` in a loop.
63
+ - **Cleanup:** Remove event listeners and cancel any app-side timers when components unmount or when the player is reset, so the bridge and native side aren’t doing work for inactive UI.
64
+ - **Single source of truth:** Use one place (e.g. one context or hook) to drive progress and playback state so you don’t have multiple subscribers or timers doing the same work.
65
+
66
+ ---
67
+
68
+ ## Summary
69
+
70
+ | Goal | Recommendation |
71
+ |--------------------------|-------------------------------------------------------------------------------|
72
+ | Fewer bridge calls | Set `progressUpdateEventInterval` and use `useProgress(..., { useNativeEvents: true })`. |
73
+ | Smoother progress UI | Use native events (e.g. interval 0.25–1 s) + `useNativeEvents: true`; avoid very fast polling. |
74
+ | Less duplicate work (iOS)| Use `progressUpdateEventInterval` ≤ 1 s so one tick drives both progress and Now Playing. |
75
+ | Responsive seek UI | Throttle/debounce `seekTo()` while the user drags the slider. |
76
+ | Lower memory / faster start | Tune buffer options for your use case (streaming vs local). |
package/README.md CHANGED
@@ -100,6 +100,7 @@ See [example/README.md](./example/README.md) for running the example app.
100
100
 
101
101
  ## Documentation
102
102
 
103
+ - [PERFORMANCE.md](./PERFORMANCE.md) – Optimizing speed and reducing bridge calls (progress, events, buffers).
103
104
  - [NOTICE](./NOTICE) – Attribution and license.
104
105
  - [LICENSE](./LICENSE) – Apache-2.0.
105
106
 
@@ -22,7 +22,11 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
22
22
  private var hasInitialized = false
23
23
  private let player = QueuedAudioPlayer()
24
24
  private let audioSessionController = AudioSessionController.shared
25
+ /// Synchronous playback state for getPlaybackState() and events. Set on play()/pause() (and remote) so the next getPlaybackState() returns the new state before the wrapper's async state update. Updated from handleAudioPlayerStateChange so ended/failed/loading etc. are correct.
26
+ private var effectivePlaybackState: AVPlayerWrapperState? = nil
25
27
  private var shouldEmitProgressEvent: Bool = false
28
+ /// When > 0 and <= 1, the progress tick already fires every second; we use it to update Now Playing instead of a separate timer.
29
+ private var progressUpdateEventIntervalSeconds: Double = 0
26
30
  private var shouldResumePlaybackAfterInterruptionEnds: Bool = false
27
31
  private var forwardJumpInterval: NSNumber? = nil;
28
32
  private var backwardJumpInterval: NSNumber? = nil;
@@ -201,10 +205,9 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
201
205
 
202
206
  player.remoteCommandController.handlePauseCommand = { [weak self] _ in
203
207
  guard let self = self else { return MPRemoteCommandHandlerStatus.commandFailed }
208
+ self.effectivePlaybackState = .paused
204
209
  self.player.pause()
205
- if self.player.currentItem != nil && self.player.automaticallyUpdateNowPlayingInfo {
206
- self.player.updateNowPlayingPlaybackValues()
207
- }
210
+ self.updateNowPlayingPlaybackValuesOnMainIfNeeded()
208
211
  self.emit(event: EventType.PlaybackState, body: self.getPlaybackStateBodyKeyValues(state: .paused))
209
212
  self.emit(event: EventType.RemotePause)
210
213
  return MPRemoteCommandHandlerStatus.success
@@ -212,10 +215,9 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
212
215
 
213
216
  player.remoteCommandController.handlePlayCommand = { [weak self] _ in
214
217
  guard let self = self else { return MPRemoteCommandHandlerStatus.commandFailed }
218
+ self.effectivePlaybackState = .playing
215
219
  self.player.play()
216
- if self.player.currentItem != nil && self.player.automaticallyUpdateNowPlayingInfo {
217
- self.player.updateNowPlayingPlaybackValues()
218
- }
220
+ self.updateNowPlayingPlaybackValuesOnMainIfNeeded()
219
221
  self.emit(event: EventType.PlaybackState, body: self.getPlaybackStateBodyKeyValues(state: .playing))
220
222
  self.emit(event: EventType.RemotePlay)
221
223
  return MPRemoteCommandHandlerStatus.success
@@ -254,10 +256,9 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
254
256
 
255
257
  player.remoteCommandController.handleStopCommand = { [weak self] _ in
256
258
  guard let self = self else { return MPRemoteCommandHandlerStatus.commandFailed }
259
+ self.effectivePlaybackState = .stopped
257
260
  self.player.stop()
258
- if self.player.currentItem != nil && self.player.automaticallyUpdateNowPlayingInfo {
259
- self.player.updateNowPlayingPlaybackValues()
260
- }
261
+ self.updateNowPlayingPlaybackValuesOnMainIfNeeded()
261
262
  self.emit(event: EventType.PlaybackState, body: self.getPlaybackStateBodyKeyValues(state: .stopped))
262
263
  self.emit(event: EventType.RemoteStop)
263
264
  return MPRemoteCommandHandlerStatus.success
@@ -265,18 +266,17 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
265
266
 
266
267
  player.remoteCommandController.handleTogglePlayPauseCommand = { [weak self] _ in
267
268
  guard let self = self else { return MPRemoteCommandHandlerStatus.commandFailed }
268
- if self.player.playerState == .paused {
269
+ let currentState = self.effectivePlaybackState ?? self.player.playerState
270
+ if currentState == .paused || currentState == .stopped || currentState == .ended {
271
+ self.effectivePlaybackState = .playing
269
272
  self.player.play()
270
- if self.player.currentItem != nil && self.player.automaticallyUpdateNowPlayingInfo {
271
- self.player.updateNowPlayingPlaybackValues()
272
- }
273
+ self.updateNowPlayingPlaybackValuesOnMainIfNeeded()
273
274
  self.emit(event: EventType.PlaybackState, body: self.getPlaybackStateBodyKeyValues(state: .playing))
274
275
  self.emit(event: EventType.RemotePlay)
275
276
  } else {
277
+ self.effectivePlaybackState = .paused
276
278
  self.player.pause()
277
- if self.player.currentItem != nil && self.player.automaticallyUpdateNowPlayingInfo {
278
- self.player.updateNowPlayingPlaybackValues()
279
- }
279
+ self.updateNowPlayingPlaybackValuesOnMainIfNeeded()
280
280
  self.emit(event: EventType.PlaybackState, body: self.getPlaybackStateBodyKeyValues(state: .paused))
281
281
  self.emit(event: EventType.RemotePause)
282
282
  }
@@ -377,9 +377,9 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
377
377
  )
378
378
  }
379
379
 
380
- configureProgressUpdateEvent(
381
- interval: ((options["progressUpdateEventInterval"] as? NSNumber) ?? 0).doubleValue
382
- )
380
+ let interval = ((options["progressUpdateEventInterval"] as? NSNumber) ?? 0).doubleValue
381
+ progressUpdateEventIntervalSeconds = interval
382
+ configureProgressUpdateEvent(interval: interval)
383
383
 
384
384
  resolve(NSNull())
385
385
  }
@@ -554,6 +554,7 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
554
554
  public func reset(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
555
555
  if (rejectWhenNotInitialized(reject: reject)) { return }
556
556
 
557
+ effectivePlaybackState = nil
557
558
  stopNowPlayingUpdateTimer()
558
559
  player.stop()
559
560
  player.clear()
@@ -563,11 +564,9 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
563
564
  @objc(play:rejecter:)
564
565
  public func play(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
565
566
  if (rejectWhenNotInitialized(reject: reject)) { return }
567
+ effectivePlaybackState = .playing
566
568
  player.play()
567
- // Update Now Playing widget immediately (rate 1, current elapsed) so it never shows "Not Playing".
568
- if player.currentItem != nil && player.automaticallyUpdateNowPlayingInfo {
569
- player.updateNowPlayingPlaybackValues()
570
- }
569
+ updateNowPlayingPlaybackValuesOnMainIfNeeded()
571
570
  resolve(NSNull())
572
571
  emit(event: EventType.PlaybackState, body: getPlaybackStateBodyKeyValues(state: .playing))
573
572
  }
@@ -575,12 +574,9 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
575
574
  @objc(pause:rejecter:)
576
575
  public func pause(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
577
576
  if (rejectWhenNotInitialized(reject: reject)) { return }
578
-
577
+ effectivePlaybackState = .paused
579
578
  player.pause()
580
- // Update Now Playing widget immediately (rate 0, current elapsed) so it shows "Paused" not "Not Playing".
581
- if player.currentItem != nil && player.automaticallyUpdateNowPlayingInfo {
582
- player.updateNowPlayingPlaybackValues()
583
- }
579
+ updateNowPlayingPlaybackValuesOnMainIfNeeded()
584
580
  resolve(NSNull())
585
581
  emit(event: EventType.PlaybackState, body: getPlaybackStateBodyKeyValues(state: .paused))
586
582
  }
@@ -775,7 +771,8 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
775
771
  @objc(getPlaybackState:rejecter:)
776
772
  public func getPlaybackState(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
777
773
  if (rejectWhenNotInitialized(reject: reject)) { return }
778
- resolve(getPlaybackStateBodyKeyValues(state: player.playerState))
774
+ let state = effectivePlaybackState ?? player.playerState
775
+ resolve(getPlaybackStateBodyKeyValues(state: state))
779
776
  }
780
777
 
781
778
  @objc(updateMetadataForTrack:metadata:resolver:rejecter:)
@@ -850,6 +847,7 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
850
847
  // MARK: - QueuedAudioPlayer Event Handlers
851
848
 
852
849
  func handleAudioPlayerStateChange(state: AVPlayerWrapperState) {
850
+ effectivePlaybackState = state
853
851
  emit(event: EventType.PlaybackState, body: getPlaybackStateBodyKeyValues(state: state))
854
852
  if (state == .ended) {
855
853
  emit(event: EventType.PlaybackQueueEnded, body: [
@@ -857,11 +855,14 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
857
855
  "position": player.currentTime,
858
856
  ] as [String : Any])
859
857
  }
860
- // Keep Now Playing widget elapsed/duration in sync: update every second when we have a current item and are ready/playing/paused.
858
+ // Keep Now Playing widget elapsed/duration in sync. When progress events fire every second (interval > 0 and <= 1), use that tick instead of a separate timer.
861
859
  switch state {
862
860
  case .ready, .playing, .paused:
863
861
  if player.currentItem != nil && player.automaticallyUpdateNowPlayingInfo {
864
- scheduleNextNowPlayingUpdate()
862
+ let useProgressTickForNowPlaying = shouldEmitProgressEvent && progressUpdateEventIntervalSeconds > 0 && progressUpdateEventIntervalSeconds <= 1.0
863
+ if !useProgressTickForNowPlaying {
864
+ scheduleNextNowPlayingUpdate()
865
+ }
865
866
  } else {
866
867
  stopNowPlayingUpdateTimer()
867
868
  }
@@ -885,6 +886,12 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
885
886
  nowPlayingUpdateWorkItem?.cancel()
886
887
  nowPlayingUpdateWorkItem = nil
887
888
  }
889
+
890
+ /// Updates MPNowPlayingInfoCenter (rate, elapsed, duration) synchronously so the widget reflects play/pause before we return to JS. Call after play()/pause() from JS or remote.
891
+ private func updateNowPlayingPlaybackValuesOnMainIfNeeded() {
892
+ guard player.currentItem != nil, player.automaticallyUpdateNowPlayingInfo else { return }
893
+ player.updateNowPlayingPlaybackValuesSync()
894
+ }
888
895
 
889
896
  func handleAudioPlayerCommonMetadataReceived(metadata: [AVMetadataItem]) {
890
897
  let commonMetadata = MetadataAdapter.convertToCommonMetadata(metadata: metadata, skipRaw: true)
@@ -974,6 +981,9 @@ public class RNTrackPlayer: NSObject, AudioSessionControllerDelegate {
974
981
  // _after_ a manipulation to the queu causing no currentItem to exist (see reset)
975
982
  // in which case we shouldn't emit anything or we'll get an exception.
976
983
  if !shouldEmitProgressEvent || player.currentItem == nil { return }
984
+ if player.automaticallyUpdateNowPlayingInfo {
985
+ player.updateNowPlayingPlaybackValues()
986
+ }
977
987
  emit(
978
988
  event: EventType.PlaybackProgressUpdated,
979
989
  body: [
@@ -348,6 +348,14 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
348
348
  ])
349
349
  }
350
350
 
351
+ /// Pushes current duration/elapsed/rate to MPNowPlayingInfoCenter synchronously on main so the widget updates before returning (e.g. after play/pause).
352
+ func updateNowPlayingPlaybackValuesSync() {
353
+ let duration = wrapper.duration
354
+ let elapsed = wrapper.currentTime
355
+ let rate = wrapper.playWhenReady ? Double(wrapper.rate) : 0
356
+ nowPlayingInfoController.setPlaybackValuesSync(duration: duration, elapsed: elapsed, rate: rate)
357
+ }
358
+
351
359
  public func clear() {
352
360
  let playbackWasActive = wrapper.playbackActive
353
361
  currentItem = nil
@@ -9,13 +9,14 @@ import Foundation
9
9
  import MediaPlayer
10
10
 
11
11
  public class NowPlayingInfoController: NowPlayingInfoControllerProtocol {
12
+ private let lock = NSLock()
12
13
  private var infoQueue: DispatchQueueType = DispatchQueue(
13
14
  label: "NowPlayingInfoController.infoQueue",
14
15
  attributes: .concurrent
15
16
  )
16
17
 
17
- private(set) var infoCenter: NowPlayingInfoCenter
18
18
  private(set) var info: [String: Any] = [:]
19
+ private(set) var infoCenter: NowPlayingInfoCenter
19
20
 
20
21
  public required init() {
21
22
  infoCenter = MPNowPlayingInfoCenter.default()
@@ -32,41 +33,74 @@ public class NowPlayingInfoController: NowPlayingInfoControllerProtocol {
32
33
  }
33
34
 
34
35
  public func set(keyValues: [NowPlayingInfoKeyValue]) {
35
- infoQueue.async(flags: .barrier) { [weak self] in
36
- guard let self = self else { return }
37
- keyValues.forEach {
38
- (keyValue) in self.info[keyValue.getKey()] = keyValue.getValue()
39
- }
40
- self.update()
36
+ lock.lock()
37
+ keyValues.forEach { keyValue in
38
+ self.info[keyValue.getKey()] = keyValue.getValue()
41
39
  }
40
+ let snapshot = self.info
41
+ lock.unlock()
42
+ pushToCenter(snapshot)
42
43
  }
43
44
 
44
45
  public func setWithoutUpdate(keyValues: [NowPlayingInfoKeyValue]) {
45
- infoQueue.async(flags: .barrier) { [weak self] in
46
- guard let self = self else { return }
47
- keyValues.forEach {
48
- (keyValue) in self.info[keyValue.getKey()] = keyValue.getValue()
49
- }
46
+ lock.lock()
47
+ keyValues.forEach { keyValue in
48
+ self.info[keyValue.getKey()] = keyValue.getValue()
50
49
  }
50
+ lock.unlock()
51
51
  }
52
52
 
53
53
  public func set(keyValue: NowPlayingInfoKeyValue) {
54
- infoQueue.async(flags: .barrier) { [weak self] in
55
- guard let self = self else { return }
56
- self.info[keyValue.getKey()] = keyValue.getValue()
57
- self.update()
54
+ lock.lock()
55
+ self.info[keyValue.getKey()] = keyValue.getValue()
56
+ let snapshot = self.info
57
+ lock.unlock()
58
+ pushToCenter(snapshot)
59
+ }
60
+
61
+ public func setPlaybackValuesSync(duration: TimeInterval, elapsed: TimeInterval, rate: Double) {
62
+ lock.lock()
63
+ self.info[MPMediaItemPropertyPlaybackDuration] = NSNumber(value: duration)
64
+ self.info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = NSNumber(value: elapsed)
65
+ self.info[MPNowPlayingInfoPropertyPlaybackRate] = NSNumber(value: rate)
66
+ let snapshot = self.info
67
+ lock.unlock()
68
+ if Thread.isMainThread {
69
+ infoCenter.nowPlayingInfo = snapshot
70
+ } else {
71
+ DispatchQueue.main.sync { [weak self] in
72
+ self?.infoCenter.nowPlayingInfo = snapshot
73
+ }
58
74
  }
59
75
  }
60
76
 
61
77
  private func update() {
62
- infoCenter.nowPlayingInfo = info
78
+ lock.lock()
79
+ let snapshot = self.info
80
+ lock.unlock()
81
+ pushToCenter(snapshot)
82
+ }
83
+
84
+ private func pushToCenter(_ snapshot: [String: Any]) {
85
+ if Thread.isMainThread {
86
+ infoCenter.nowPlayingInfo = snapshot
87
+ } else {
88
+ DispatchQueue.main.async { [weak self] in
89
+ self?.infoCenter.nowPlayingInfo = snapshot
90
+ }
91
+ }
63
92
  }
64
93
 
65
94
  public func clear() {
66
- infoQueue.async(flags: .barrier) { [weak self] in
67
- guard let self = self else { return }
68
- self.info = [:]
69
- self.infoCenter.nowPlayingInfo = nil
95
+ lock.lock()
96
+ self.info = [:]
97
+ lock.unlock()
98
+ if Thread.isMainThread {
99
+ infoCenter.nowPlayingInfo = nil
100
+ } else {
101
+ DispatchQueue.main.async { [weak self] in
102
+ self?.infoCenter.nowPlayingInfo = nil
103
+ }
70
104
  }
71
105
  }
72
106
 
@@ -23,4 +23,16 @@ public protocol NowPlayingInfoControllerProtocol {
23
23
 
24
24
  func clear()
25
25
 
26
+ /// Optional: push playback values to the system synchronously on main (e.g. so play/pause widget updates before returning). Default merges and calls set() (async).
27
+ func setPlaybackValuesSync(duration: TimeInterval, elapsed: TimeInterval, rate: Double)
28
+ }
29
+
30
+ extension NowPlayingInfoControllerProtocol {
31
+ public func setPlaybackValuesSync(duration: TimeInterval, elapsed: TimeInterval, rate: Double) {
32
+ set(keyValues: [
33
+ MediaItemProperty.duration(duration),
34
+ NowPlayingInfoProperty.elapsedPlaybackTime(elapsed),
35
+ NowPlayingInfoProperty.playbackRate(rate)
36
+ ])
37
+ }
26
38
  }
@@ -1,7 +1,12 @@
1
1
  import type { Progress } from '../interfaces';
2
+ export interface UseProgressOptions {
3
+ /** If true, subscribe to Event.PlaybackProgressUpdated (native-driven) when you set progressUpdateEventInterval in updateOptions. Reduces bridge round-trips vs polling. Default false. */
4
+ useNativeEvents?: boolean;
5
+ }
2
6
  /**
3
- * Poll for track progress for the given interval (in miliseconds)
4
- * @param updateInterval - ms interval
5
- * @param background - if update state in background. default true. may severely affects performance.
7
+ * Track progress (position, duration, buffered) for the current track.
8
+ * @param updateInterval - Polling interval in ms when not using native events. Default 1000.
9
+ * @param background - Update state when app is in background. Default true. Can affect performance.
10
+ * @param options - useNativeEvents: true to prefer Event.PlaybackProgressUpdated (set progressUpdateEventInterval in updateOptions). Fewer bridge calls.
6
11
  */
7
- export declare function useProgress(updateInterval?: number, background?: boolean): Progress;
12
+ export declare function useProgress(updateInterval?: number, background?: boolean, options?: UseProgressOptions): Progress;
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import { AppState } from 'react-native';
3
- import { getProgress } from '../trackPlayer';
3
+ import { getProgress, addEventListener } from '../trackPlayer';
4
4
  import { Event } from '../constants';
5
5
  import { useTrackPlayerEvents } from './useTrackPlayerEvents';
6
6
  const INITIAL_STATE = {
@@ -9,39 +9,63 @@ const INITIAL_STATE = {
9
9
  buffered: 0,
10
10
  };
11
11
  /**
12
- * Poll for track progress for the given interval (in miliseconds)
13
- * @param updateInterval - ms interval
14
- * @param background - if update state in background. default true. may severely affects performance.
12
+ * Track progress (position, duration, buffered) for the current track.
13
+ * @param updateInterval - Polling interval in ms when not using native events. Default 1000.
14
+ * @param background - Update state when app is in background. Default true. Can affect performance.
15
+ * @param options - useNativeEvents: true to prefer Event.PlaybackProgressUpdated (set progressUpdateEventInterval in updateOptions). Fewer bridge calls.
15
16
  */
16
- export function useProgress(updateInterval = 1000, background = true) {
17
+ export function useProgress(updateInterval = 1000, background = true, options = {}) {
17
18
  const [state, setState] = useState(INITIAL_STATE);
18
19
  useTrackPlayerEvents([Event.PlaybackActiveTrackChanged], () => {
19
20
  setState(INITIAL_STATE);
20
21
  });
21
22
  useEffect(() => {
22
23
  let mounted = true;
23
- const update = async () => {
24
+ const updateFromProgress = (next) => {
25
+ if (!mounted)
26
+ return;
27
+ setState((prev) => next.position === prev.position &&
28
+ next.duration === prev.duration &&
29
+ next.buffered === prev.buffered
30
+ ? prev
31
+ : next);
32
+ };
33
+ const updateFromBridge = async () => {
24
34
  try {
25
35
  if (!mounted)
26
36
  return;
27
37
  if (!background && AppState.currentState !== 'active')
28
38
  return;
29
- const { position, duration, buffered } = await getProgress();
30
- setState((state) => position === state.position &&
31
- duration === state.duration &&
32
- buffered === state.buffered
33
- ? state
34
- : { position, duration, buffered });
39
+ const next = await getProgress();
40
+ updateFromProgress(next);
35
41
  }
36
42
  catch {
37
- // these method only throw while you haven't yet setup, ignore failure.
43
+ // only throws when player not set up
38
44
  }
39
45
  };
46
+ let progressSub = null;
47
+ if (options.useNativeEvents) {
48
+ progressSub = addEventListener(Event.PlaybackProgressUpdated, (payload) => {
49
+ updateFromProgress({
50
+ position: payload.position,
51
+ duration: payload.duration,
52
+ buffered: payload.buffered,
53
+ });
54
+ });
55
+ }
40
56
  const poll = async () => {
41
- await update();
42
- if (!mounted)
43
- return;
44
- await new Promise((resolve) => setTimeout(resolve, updateInterval));
57
+ if (options.useNativeEvents) {
58
+ await updateFromBridge();
59
+ if (!mounted)
60
+ return;
61
+ await new Promise((resolve) => setTimeout(resolve, Math.max(updateInterval * 3, 3000)));
62
+ }
63
+ else {
64
+ await updateFromBridge();
65
+ if (!mounted)
66
+ return;
67
+ await new Promise((resolve) => setTimeout(resolve, updateInterval));
68
+ }
45
69
  if (!mounted)
46
70
  return;
47
71
  poll();
@@ -49,7 +73,8 @@ export function useProgress(updateInterval = 1000, background = true) {
49
73
  poll();
50
74
  return () => {
51
75
  mounted = false;
76
+ progressSub?.remove();
52
77
  };
53
- }, [updateInterval]);
78
+ }, [updateInterval, options?.useNativeEvents ?? false]);
54
79
  return state;
55
80
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-mp3-player",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "React Native audio player with reliable iOS background playback. Media controls, queue, hooks. Built for stability and long-running playback.",
5
5
  "main": "lib/src/index.js",
6
6
  "types": "lib/src/index.d.ts",
@@ -20,6 +20,7 @@
20
20
  "files": [
21
21
  "src",
22
22
  "lib/**/*",
23
+ "PERFORMANCE.md",
23
24
  "ios/**/*",
24
25
  "android/src/**/*",
25
26
  "android/build.gradle",
@@ -1,23 +1,33 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import { AppState } from 'react-native';
3
3
 
4
- import { getProgress } from '../trackPlayer';
4
+ import { getProgress, addEventListener } from '../trackPlayer';
5
5
  import { Event } from '../constants';
6
6
  import type { Progress } from '../interfaces';
7
7
  import { useTrackPlayerEvents } from './useTrackPlayerEvents';
8
8
 
9
- const INITIAL_STATE = {
9
+ const INITIAL_STATE: Progress = {
10
10
  position: 0,
11
11
  duration: 0,
12
12
  buffered: 0,
13
13
  };
14
14
 
15
+ export interface UseProgressOptions {
16
+ /** If true, subscribe to Event.PlaybackProgressUpdated (native-driven) when you set progressUpdateEventInterval in updateOptions. Reduces bridge round-trips vs polling. Default false. */
17
+ useNativeEvents?: boolean;
18
+ }
19
+
15
20
  /**
16
- * Poll for track progress for the given interval (in miliseconds)
17
- * @param updateInterval - ms interval
18
- * @param background - if update state in background. default true. may severely affects performance.
21
+ * Track progress (position, duration, buffered) for the current track.
22
+ * @param updateInterval - Polling interval in ms when not using native events. Default 1000.
23
+ * @param background - Update state when app is in background. Default true. Can affect performance.
24
+ * @param options - useNativeEvents: true to prefer Event.PlaybackProgressUpdated (set progressUpdateEventInterval in updateOptions). Fewer bridge calls.
19
25
  */
20
- export function useProgress(updateInterval = 1000, background = true) {
26
+ export function useProgress(
27
+ updateInterval = 1000,
28
+ background = true,
29
+ options: UseProgressOptions = {},
30
+ ) {
21
31
  const [state, setState] = useState<Progress>(INITIAL_STATE);
22
32
 
23
33
  useTrackPlayerEvents([Event.PlaybackActiveTrackChanged], () => {
@@ -27,28 +37,56 @@ export function useProgress(updateInterval = 1000, background = true) {
27
37
  useEffect(() => {
28
38
  let mounted = true;
29
39
 
30
- const update = async () => {
40
+ const updateFromProgress = (next: Progress) => {
41
+ if (!mounted) return;
42
+ setState((prev) =>
43
+ next.position === prev.position &&
44
+ next.duration === prev.duration &&
45
+ next.buffered === prev.buffered
46
+ ? prev
47
+ : next
48
+ );
49
+ };
50
+
51
+ const updateFromBridge = async () => {
31
52
  try {
32
53
  if (!mounted) return;
33
54
  if (!background && AppState.currentState !== 'active') return;
34
-
35
- const { position, duration, buffered } = await getProgress();
36
- setState((state) =>
37
- position === state.position &&
38
- duration === state.duration &&
39
- buffered === state.buffered
40
- ? state
41
- : { position, duration, buffered }
42
- );
55
+ const next = await getProgress();
56
+ updateFromProgress(next);
43
57
  } catch {
44
- // these method only throw while you haven't yet setup, ignore failure.
58
+ // only throws when player not set up
45
59
  }
46
60
  };
47
61
 
62
+ let progressSub: { remove: () => void } | null = null;
63
+ if (options.useNativeEvents) {
64
+ progressSub = addEventListener(
65
+ Event.PlaybackProgressUpdated,
66
+ (payload: { position: number; duration: number; buffered: number }) => {
67
+ updateFromProgress({
68
+ position: payload.position,
69
+ duration: payload.duration,
70
+ buffered: payload.buffered,
71
+ });
72
+ },
73
+ );
74
+ }
75
+
48
76
  const poll = async () => {
49
- await update();
50
- if (!mounted) return;
51
- await new Promise<void>((resolve) => setTimeout(resolve, updateInterval));
77
+ if (options.useNativeEvents) {
78
+ await updateFromBridge();
79
+ if (!mounted) return;
80
+ await new Promise<void>((resolve) =>
81
+ setTimeout(resolve, Math.max(updateInterval * 3, 3000)),
82
+ );
83
+ } else {
84
+ await updateFromBridge();
85
+ if (!mounted) return;
86
+ await new Promise<void>((resolve) =>
87
+ setTimeout(resolve, updateInterval),
88
+ );
89
+ }
52
90
  if (!mounted) return;
53
91
  poll();
54
92
  };
@@ -57,8 +95,9 @@ export function useProgress(updateInterval = 1000, background = true) {
57
95
 
58
96
  return () => {
59
97
  mounted = false;
98
+ progressSub?.remove();
60
99
  };
61
- }, [updateInterval]);
100
+ }, [updateInterval, options?.useNativeEvents ?? false]);
62
101
 
63
102
  return state;
64
103
  }