react-native-nitro-player 0.4.0 → 0.5.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.
@@ -15,10 +15,6 @@ class MediaSessionManager {
15
15
  // MARK: - Constants
16
16
 
17
17
  private enum Constants {
18
- // Seek intervals (in seconds)
19
- static let seekInterval: Double = 10.0
20
-
21
- // Artwork size
22
18
  static let artworkSize: CGFloat = 500.0
23
19
  }
24
20
 
@@ -27,10 +23,11 @@ class MediaSessionManager {
27
23
  private var trackPlayerCore: TrackPlayerCore?
28
24
  private var artworkCache: [String: UIImage] = [:]
29
25
 
30
- private var androidAutoEnabled: Bool = false
31
- private var carPlayEnabled: Bool = false
32
26
  private var showInNotification: Bool = true
33
27
 
28
+ // Tracks the artwork URL currently shown so we can discard stale async loads
29
+ private var lastArtworkUrl: String?
30
+
34
31
  init() {
35
32
  setupRemoteCommandCenter()
36
33
  }
@@ -44,47 +41,165 @@ class MediaSessionManager {
44
41
  carPlayEnabled: Bool?,
45
42
  showInNotification: Bool?
46
43
  ) {
47
- if let androidAutoEnabled = androidAutoEnabled {
48
- self.androidAutoEnabled = androidAutoEnabled
49
- }
50
- if let carPlayEnabled = carPlayEnabled {
51
- self.carPlayEnabled = carPlayEnabled
52
- // CarPlay is handled by the app's CarPlaySceneDelegate
53
- // We just maintain the flag here for reference
54
- }
55
44
  if let showInNotification = showInNotification {
56
45
  self.showInNotification = showInNotification
57
- if showInNotification {
58
- updateNowPlayingInfo()
46
+ }
47
+ refresh()
48
+ }
49
+
50
+ // MARK: - Single refresh entry point
51
+ //
52
+ // All public callbacks route here. Always dispatches to main thread so
53
+ // MPNowPlayingInfoCenter and MPRemoteCommandCenter are only touched from main.
54
+
55
+ func refresh() {
56
+ if Thread.isMainThread {
57
+ refreshInternal()
58
+ } else {
59
+ DispatchQueue.main.async { [weak self] in
60
+ self?.refreshInternal()
61
+ }
62
+ }
63
+ }
64
+
65
+ // Convenience aliases used by TrackPlayerCore call sites
66
+ func updateNowPlayingInfo() { refresh() }
67
+ func onTrackChanged() { refresh() }
68
+ func onPlaybackStateChanged() { refresh() }
69
+ func onQueueChanged() { refresh() }
70
+
71
+ // MARK: - Core internal update (main thread only)
72
+
73
+ private func refreshInternal() {
74
+ guard showInNotification else {
75
+ clearNowPlayingInfo()
76
+ disableAllCommands()
77
+ return
78
+ }
79
+
80
+ guard let core = trackPlayerCore,
81
+ let track = core.getCurrentTrack()
82
+ else {
83
+ clearNowPlayingInfo()
84
+ disableAllCommands()
85
+ return
86
+ }
87
+
88
+ // Fetch snapshot once — both calls are cheap on main thread (no sync overhead)
89
+ let state = core.getState()
90
+ let queue = core.getActualQueue()
91
+
92
+ // Find the actual position of the current track inside the actual queue.
93
+ // state.currentIndex is the original-playlist index which is wrong when a
94
+ // temp (playNext / upNext) track is playing.
95
+ let positionInQueue = queue.firstIndex(where: { $0.id == track.id }) ?? -1
96
+
97
+ updateNowPlayingInfoInternal(track: track, state: state, queue: queue, positionInQueue: positionInQueue)
98
+ updateCommandCenterState(state: state, queue: queue, positionInQueue: positionInQueue)
99
+ }
100
+
101
+ // MARK: - Now Playing Info
102
+
103
+ private func updateNowPlayingInfoInternal(
104
+ track: TrackItem,
105
+ state: PlayerState,
106
+ queue: [TrackItem],
107
+ positionInQueue: Int
108
+ ) {
109
+ let playerDuration = state.totalDuration
110
+ let effectiveDuration: Double
111
+ if playerDuration > 0 && !playerDuration.isNaN && !playerDuration.isInfinite {
112
+ effectiveDuration = playerDuration
113
+ } else if track.duration > 0 {
114
+ effectiveDuration = track.duration
115
+ } else {
116
+ effectiveDuration = 0
117
+ }
118
+
119
+ let currentPosition = state.currentPosition
120
+ let safePosition = currentPosition.isNaN || currentPosition.isInfinite ? 0 : currentPosition
121
+ let isPlaying = state.currentState == .playing
122
+
123
+ var nowPlayingInfo: [String: Any] = [
124
+ MPMediaItemPropertyTitle: track.title,
125
+ MPMediaItemPropertyArtist: track.artist,
126
+ MPMediaItemPropertyAlbumTitle: track.album,
127
+ MPNowPlayingInfoPropertyElapsedPlaybackTime: safePosition,
128
+ MPMediaItemPropertyPlaybackDuration: effectiveDuration,
129
+ MPNowPlayingInfoPropertyPlaybackRate: isPlaying ? 1.0 : 0.0,
130
+ MPNowPlayingInfoPropertyDefaultPlaybackRate: 1.0,
131
+ MPNowPlayingInfoPropertyPlaybackQueueCount: max(1, queue.count),
132
+ MPNowPlayingInfoPropertyPlaybackQueueIndex: max(0, positionInQueue),
133
+ ]
134
+
135
+ // Artwork: use cache synchronously when available, otherwise kick off async load
136
+ if let artwork = track.artwork, case .second(let artworkUrl) = artwork {
137
+ lastArtworkUrl = artworkUrl
138
+ if let cachedImage = artworkCache[artworkUrl] {
139
+ nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(
140
+ boundsSize: CGSize(width: Constants.artworkSize, height: Constants.artworkSize),
141
+ requestHandler: { _ in cachedImage }
142
+ )
59
143
  } else {
60
- clearNowPlayingInfo()
144
+ // Write info first without artwork, then patch it in when loaded
145
+ MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
146
+ loadArtwork(url: artworkUrl) { [weak self] image in
147
+ guard let self = self, let image = image else { return }
148
+ // Discard if track changed while loading
149
+ guard self.lastArtworkUrl == artworkUrl else { return }
150
+ var updated = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]
151
+ updated[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(
152
+ boundsSize: CGSize(width: Constants.artworkSize, height: Constants.artworkSize),
153
+ requestHandler: { _ in image }
154
+ )
155
+ MPNowPlayingInfoCenter.default().nowPlayingInfo = updated
156
+ }
157
+ return
61
158
  }
159
+ } else {
160
+ lastArtworkUrl = nil
62
161
  }
162
+
163
+ MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
63
164
  }
64
165
 
166
+ // MARK: - Command Center State
167
+
65
168
  private func setupRemoteCommandCenter() {
66
169
  let commandCenter = MPRemoteCommandCenter.shared()
67
170
 
68
- // Play command
171
+ // Clear any previously registered targets before adding fresh ones.
172
+ // Prevents duplicate handlers if this were ever called more than once.
173
+ commandCenter.playCommand.removeTarget(nil)
174
+ commandCenter.pauseCommand.removeTarget(nil)
175
+ commandCenter.togglePlayPauseCommand.removeTarget(nil)
176
+ commandCenter.nextTrackCommand.removeTarget(nil)
177
+ commandCenter.previousTrackCommand.removeTarget(nil)
178
+ commandCenter.seekForwardCommand.removeTarget(nil)
179
+ commandCenter.seekBackwardCommand.removeTarget(nil)
180
+ commandCenter.changePlaybackPositionCommand.removeTarget(nil)
181
+
182
+ // Play
69
183
  commandCenter.playCommand.isEnabled = true
70
184
  commandCenter.playCommand.addTarget { [weak self] _ in
71
- self?.trackPlayerCore?.play()
185
+ guard let core = self?.trackPlayerCore else { return .commandFailed }
186
+ core.play()
72
187
  return .success
73
188
  }
74
189
 
75
- // Pause command
190
+ // Pause
76
191
  commandCenter.pauseCommand.isEnabled = true
77
192
  commandCenter.pauseCommand.addTarget { [weak self] _ in
78
- self?.trackPlayerCore?.pause()
193
+ guard let core = self?.trackPlayerCore else { return .commandFailed }
194
+ core.pause()
79
195
  return .success
80
196
  }
81
197
 
82
198
  // Toggle play/pause
83
199
  commandCenter.togglePlayPauseCommand.isEnabled = true
84
200
  commandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in
85
- guard let self = self, let core = self.trackPlayerCore else { return .commandFailed }
86
- let state = core.getState()
87
- if state.currentState == .playing {
201
+ guard let core = self?.trackPlayerCore else { return .commandFailed }
202
+ if core.getState().currentState == .playing {
88
203
  core.pause()
89
204
  } else {
90
205
  core.play()
@@ -92,27 +207,28 @@ class MediaSessionManager {
92
207
  return .success
93
208
  }
94
209
 
95
- // Next track command
96
- commandCenter.nextTrackCommand.isEnabled = true
210
+ // Next track — isEnabled managed dynamically in updateCommandCenterState
211
+ commandCenter.nextTrackCommand.isEnabled = false
97
212
  commandCenter.nextTrackCommand.addTarget { [weak self] _ in
98
- self?.trackPlayerCore?.skipToNext()
213
+ guard let core = self?.trackPlayerCore else { return .commandFailed }
214
+ core.skipToNext()
99
215
  return .success
100
216
  }
101
217
 
102
- // Previous track command
103
- commandCenter.previousTrackCommand.isEnabled = true
218
+ // Previous track — isEnabled managed dynamically in updateCommandCenterState
219
+ commandCenter.previousTrackCommand.isEnabled = false
104
220
  commandCenter.previousTrackCommand.addTarget { [weak self] _ in
105
- self?.trackPlayerCore?.skipToPrevious()
221
+ guard let core = self?.trackPlayerCore else { return .commandFailed }
222
+ core.skipToPrevious()
106
223
  return .success
107
224
  }
108
225
 
109
- // Disable continuous seek commands - they replace the interactive scrubber
110
- // with non-interactive forward/backward buttons on the lock screen
226
+ // Disable skip-forward/backward these replace the scrubber with non-interactive buttons
111
227
  commandCenter.seekForwardCommand.isEnabled = false
112
228
  commandCenter.seekBackwardCommand.isEnabled = false
113
229
 
114
- // Change playback position (interactive scrubber)
115
- commandCenter.changePlaybackPositionCommand.isEnabled = true
230
+ // Scrubber isEnabled managed dynamically based on known duration
231
+ commandCenter.changePlaybackPositionCommand.isEnabled = false
116
232
  commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in
117
233
  guard let self = self,
118
234
  let core = self.trackPlayerCore,
@@ -120,11 +236,10 @@ class MediaSessionManager {
120
236
  else {
121
237
  return .commandFailed
122
238
  }
123
- // Immediately update elapsed time AND set playback rate to 0 during seek
124
- // This prevents the scrubber from freezing/desyncing during the async seek operation
239
+ // Optimistically freeze the scrubber at the tapped position while the async
240
+ // seek is in flight updateNowPlayingInfo in the seek completion restores it.
125
241
  if var info = MPNowPlayingInfoCenter.default().nowPlayingInfo {
126
242
  info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = positionEvent.positionTime
127
- // Set rate to 0 to pause scrubber animation during seek
128
243
  info[MPNowPlayingInfoPropertyPlaybackRate] = 0.0
129
244
  MPNowPlayingInfoCenter.default().nowPlayingInfo = info
130
245
  }
@@ -133,73 +248,43 @@ class MediaSessionManager {
133
248
  }
134
249
  }
135
250
 
136
- private func getCurrentTrack() -> TrackItem? {
137
- return trackPlayerCore?.getCurrentTrack()
138
- }
139
-
140
- func updateNowPlayingInfo() {
141
- guard showInNotification else { return }
142
-
143
- guard let track = getCurrentTrack(),
144
- let core = trackPlayerCore
145
- else {
146
- clearNowPlayingInfo()
147
- return
148
- }
149
-
150
- let state = core.getState()
251
+ private func updateCommandCenterState(
252
+ state: PlayerState,
253
+ queue: [TrackItem],
254
+ positionInQueue: Int
255
+ ) {
256
+ let commandCenter = MPRemoteCommandCenter.shared()
257
+ let hasCurrentTrack = positionInQueue >= 0
258
+ let isNotLast = positionInQueue < queue.count - 1
151
259
 
152
- // Use player duration if valid, otherwise fall back to track metadata duration.
153
- // Duration must always be present for the lock screen scrubber to be interactive.
154
260
  let playerDuration = state.totalDuration
155
- let effectiveDuration: Double
156
- if playerDuration > 0 && !playerDuration.isNaN && !playerDuration.isInfinite {
157
- effectiveDuration = playerDuration
158
- } else {
159
- effectiveDuration = track.duration
160
- }
261
+ let hasDuration = playerDuration > 0 && !playerDuration.isNaN && !playerDuration.isInfinite
161
262
 
162
- var nowPlayingInfo: [String: Any] = [
163
- MPMediaItemPropertyTitle: track.title,
164
- MPMediaItemPropertyArtist: track.artist,
165
- MPMediaItemPropertyAlbumTitle: track.album,
166
- MPNowPlayingInfoPropertyElapsedPlaybackTime: state.currentPosition,
167
- MPMediaItemPropertyPlaybackDuration: effectiveDuration,
168
- MPNowPlayingInfoPropertyPlaybackRate: state.currentState == .playing ? 1.0 : 0.0,
169
- ]
263
+ // Next: only enabled when there is a track after the current one
264
+ commandCenter.nextTrackCommand.isEnabled = hasCurrentTrack && isNotLast
170
265
 
171
- // Add artwork synchronously if cached, otherwise load async
172
- if let artwork = track.artwork, case .second(let artworkUrl) = artwork {
173
- if let cachedImage = artworkCache[artworkUrl] {
174
- // Artwork is cached - include it directly to avoid overwrite race condition
175
- nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(
176
- boundsSize: CGSize(width: Constants.artworkSize, height: Constants.artworkSize),
177
- requestHandler: { _ in cachedImage }
178
- )
179
- } else {
180
- // Artwork not cached - load asynchronously and update later
181
- loadArtwork(url: artworkUrl) { [weak self] image in
182
- guard let self = self, let image = image else { return }
183
- // Re-read current nowPlayingInfo to avoid overwriting other updates
184
- var updatedInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]
185
- updatedInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(
186
- boundsSize: CGSize(width: Constants.artworkSize, height: Constants.artworkSize),
187
- requestHandler: { _ in image }
188
- )
189
- MPNowPlayingInfoCenter.default().nowPlayingInfo = updatedInfo
190
- }
191
- }
192
- }
266
+ // Previous: always enabled when something is playing — either restarts current or goes back
267
+ commandCenter.previousTrackCommand.isEnabled = hasCurrentTrack
193
268
 
194
- MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
269
+ // Scrubber: only enabled when we have a known, finite duration
270
+ commandCenter.changePlaybackPositionCommand.isEnabled = hasCurrentTrack && hasDuration
271
+ }
272
+
273
+ private func disableAllCommands() {
274
+ let commandCenter = MPRemoteCommandCenter.shared()
275
+ commandCenter.nextTrackCommand.isEnabled = false
276
+ commandCenter.previousTrackCommand.isEnabled = false
277
+ commandCenter.changePlaybackPositionCommand.isEnabled = false
195
278
  }
196
279
 
280
+ // MARK: - Helpers
281
+
197
282
  private func clearNowPlayingInfo() {
198
283
  MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
284
+ lastArtworkUrl = nil
199
285
  }
200
286
 
201
287
  private func loadArtwork(url: String, completion: @escaping (UIImage?) -> Void) {
202
- // Check cache first
203
288
  if let cached = artworkCache[url] {
204
289
  completion(cached)
205
290
  return
@@ -210,33 +295,21 @@ class MediaSessionManager {
210
295
  return
211
296
  }
212
297
 
213
- // Load image asynchronously
214
298
  URLSession.shared.dataTask(with: imageUrl) { [weak self] data, _, _ in
215
- guard let data = data,
216
- let image = UIImage(data: data)
217
- else {
218
- completion(nil)
299
+ guard let data = data, let image = UIImage(data: data) else {
300
+ DispatchQueue.main.async { completion(nil) }
219
301
  return
220
302
  }
221
-
222
- // Cache the image
223
- self?.artworkCache[url] = image
224
303
  DispatchQueue.main.async {
304
+ self?.artworkCache[url] = image
225
305
  completion(image)
226
306
  }
227
307
  }.resume()
228
308
  }
229
309
 
230
- func onTrackChanged() {
231
- updateNowPlayingInfo()
232
- }
233
-
234
- func onPlaybackStateChanged() {
235
- updateNowPlayingInfo()
236
- }
237
-
238
310
  func release() {
239
311
  clearNowPlayingInfo()
312
+ disableAllCommands()
240
313
  artworkCache.removeAll()
241
314
  }
242
315
  }
@@ -1,6 +1,8 @@
1
1
  import type { TrackItem, TrackPlayerState, Reason } from '../types/PlayerQueue';
2
2
  type PlaybackStateCallback = (state: TrackPlayerState, reason?: Reason) => void;
3
3
  type TrackChangeCallback = (track: TrackItem, reason?: Reason) => void;
4
+ type PlaybackProgressCallback = (position: number, totalDuration: number, isManuallySeeked?: boolean) => void;
5
+ type SeekCallback = (position: number, totalDuration: number) => void;
4
6
  /**
5
7
  * Internal subscription manager that allows multiple hooks to subscribe
6
8
  * to a single native callback. This solves the problem where registering
@@ -9,8 +11,12 @@ type TrackChangeCallback = (track: TrackItem, reason?: Reason) => void;
9
11
  declare class CallbackSubscriptionManager {
10
12
  private playbackStateSubscribers;
11
13
  private trackChangeSubscribers;
14
+ private playbackProgressSubscribers;
15
+ private seekSubscribers;
12
16
  private isPlaybackStateRegistered;
13
17
  private isTrackChangeRegistered;
18
+ private isPlaybackProgressRegistered;
19
+ private isSeekRegistered;
14
20
  /**
15
21
  * Subscribe to playback state changes
16
22
  * @returns Unsubscribe function
@@ -23,6 +29,18 @@ declare class CallbackSubscriptionManager {
23
29
  subscribeToTrackChange(callback: TrackChangeCallback): () => void;
24
30
  private ensurePlaybackStateRegistered;
25
31
  private ensureTrackChangeRegistered;
32
+ /**
33
+ * Subscribe to playback progress changes
34
+ * @returns Unsubscribe function
35
+ */
36
+ subscribeToPlaybackProgressChange(callback: PlaybackProgressCallback): () => void;
37
+ /**
38
+ * Subscribe to seek events
39
+ * @returns Unsubscribe function
40
+ */
41
+ subscribeToSeek(callback: SeekCallback): () => void;
42
+ private ensurePlaybackProgressRegistered;
43
+ private ensureSeekRegistered;
26
44
  }
27
45
  export declare const callbackManager: CallbackSubscriptionManager;
28
46
  export {};
@@ -7,8 +7,12 @@ import { TrackPlayer } from '../index';
7
7
  class CallbackSubscriptionManager {
8
8
  playbackStateSubscribers = new Set();
9
9
  trackChangeSubscribers = new Set();
10
+ playbackProgressSubscribers = new Set();
11
+ seekSubscribers = new Set();
10
12
  isPlaybackStateRegistered = false;
11
13
  isTrackChangeRegistered = false;
14
+ isPlaybackProgressRegistered = false;
15
+ isSeekRegistered = false;
12
16
  /**
13
17
  * Subscribe to playback state changes
14
18
  * @returns Unsubscribe function
@@ -71,6 +75,68 @@ class CallbackSubscriptionManager {
71
75
  console.error('[CallbackManager] Failed to register track change callback:', error);
72
76
  }
73
77
  }
78
+ /**
79
+ * Subscribe to playback progress changes
80
+ * @returns Unsubscribe function
81
+ */
82
+ subscribeToPlaybackProgressChange(callback) {
83
+ this.playbackProgressSubscribers.add(callback);
84
+ this.ensurePlaybackProgressRegistered();
85
+ return () => {
86
+ this.playbackProgressSubscribers.delete(callback);
87
+ };
88
+ }
89
+ /**
90
+ * Subscribe to seek events
91
+ * @returns Unsubscribe function
92
+ */
93
+ subscribeToSeek(callback) {
94
+ this.seekSubscribers.add(callback);
95
+ this.ensureSeekRegistered();
96
+ return () => {
97
+ this.seekSubscribers.delete(callback);
98
+ };
99
+ }
100
+ ensurePlaybackProgressRegistered() {
101
+ if (this.isPlaybackProgressRegistered)
102
+ return;
103
+ try {
104
+ TrackPlayer.onPlaybackProgressChange((position, totalDuration, isManuallySeeked) => {
105
+ this.playbackProgressSubscribers.forEach((subscriber) => {
106
+ try {
107
+ subscriber(position, totalDuration, isManuallySeeked);
108
+ }
109
+ catch (error) {
110
+ console.error('[CallbackManager] Error in playback progress subscriber:', error);
111
+ }
112
+ });
113
+ });
114
+ this.isPlaybackProgressRegistered = true;
115
+ }
116
+ catch (error) {
117
+ console.error('[CallbackManager] Failed to register playback progress callback:', error);
118
+ }
119
+ }
120
+ ensureSeekRegistered() {
121
+ if (this.isSeekRegistered)
122
+ return;
123
+ try {
124
+ TrackPlayer.onSeek((position, totalDuration) => {
125
+ this.seekSubscribers.forEach((subscriber) => {
126
+ try {
127
+ subscriber(position, totalDuration);
128
+ }
129
+ catch (error) {
130
+ console.error('[CallbackManager] Error in seek subscriber:', error);
131
+ }
132
+ });
133
+ });
134
+ this.isSeekRegistered = true;
135
+ }
136
+ catch (error) {
137
+ console.error('[CallbackManager] Failed to register seek callback:', error);
138
+ }
139
+ }
74
140
  }
75
141
  // Export singleton instance
76
142
  export const callbackManager = new CallbackSubscriptionManager();
@@ -47,12 +47,14 @@ const DEFAULT_STATE = {
47
47
  export function useNowPlaying() {
48
48
  const [state, setState] = useState(DEFAULT_STATE);
49
49
  const isMounted = useRef(true);
50
- const updateState = useCallback(async () => {
50
+ const fetchFullState = useCallback(async () => {
51
51
  if (!isMounted.current)
52
52
  return;
53
53
  try {
54
54
  const newState = await TrackPlayer.getState();
55
- setState(newState);
55
+ if (isMounted.current) {
56
+ setState(newState);
57
+ }
56
58
  }
57
59
  catch (error) {
58
60
  console.error('[useNowPlaying] Error updating player state:', error);
@@ -61,28 +63,38 @@ export function useNowPlaying() {
61
63
  // Initialize with current state
62
64
  useEffect(() => {
63
65
  isMounted.current = true;
64
- updateState();
66
+ fetchFullState();
65
67
  return () => {
66
68
  isMounted.current = false;
67
69
  };
68
- }, [updateState]);
69
- // Subscribe to track changes
70
+ }, [fetchFullState]);
71
+ // Subscribe to track changes — full refresh
70
72
  useEffect(() => {
71
- const unsubscribe = callbackManager.subscribeToTrackChange(() => {
72
- updateState();
73
+ return callbackManager.subscribeToTrackChange(() => {
74
+ fetchFullState();
73
75
  });
74
- return () => {
75
- unsubscribe();
76
- };
77
- }, [updateState]);
78
- // Subscribe to playback state changes
76
+ }, [fetchFullState]);
77
+ // Subscribe to playback state changes — full refresh
79
78
  useEffect(() => {
80
- const unsubscribe = callbackManager.subscribeToPlaybackState(() => {
81
- updateState();
79
+ return callbackManager.subscribeToPlaybackState(() => {
80
+ fetchFullState();
82
81
  });
83
- return () => {
84
- unsubscribe();
85
- };
86
- }, [updateState]);
82
+ }, [fetchFullState]);
83
+ // Subscribe to progress changes — lightweight position/duration update
84
+ useEffect(() => {
85
+ return callbackManager.subscribeToPlaybackProgressChange((currentPosition, totalDuration) => {
86
+ if (!isMounted.current)
87
+ return;
88
+ setState((prev) => ({ ...prev, currentPosition, totalDuration }));
89
+ });
90
+ }, []);
91
+ // Subscribe to seek events — lightweight position/duration update
92
+ useEffect(() => {
93
+ return callbackManager.subscribeToSeek((currentPosition, totalDuration) => {
94
+ if (!isMounted.current)
95
+ return;
96
+ setState((prev) => ({ ...prev, currentPosition, totalDuration }));
97
+ });
98
+ }, []);
87
99
  return state;
88
100
  }
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useState } from 'react';
2
- import { TrackPlayer } from '../index';
2
+ import { callbackManager } from './callbackManager';
3
3
  /**
4
4
  * Hook to get the current playback progress
5
5
  * @returns Object with current position, total duration, and manual seek indicator
@@ -9,7 +9,7 @@ export function useOnPlaybackProgressChange() {
9
9
  const [totalDuration, setTotalDuration] = useState(0);
10
10
  const [isManuallySeeked, setIsManuallySeeked] = useState(undefined);
11
11
  useEffect(() => {
12
- TrackPlayer.onPlaybackProgressChange((newPosition, newTotalDuration, newIsManuallySeeked) => {
12
+ return callbackManager.subscribeToPlaybackProgressChange((newPosition, newTotalDuration, newIsManuallySeeked) => {
13
13
  setPosition(newPosition);
14
14
  setTotalDuration(newTotalDuration);
15
15
  setIsManuallySeeked(newIsManuallySeeked);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-native-nitro-player",
3
- "version": "0.4.0",
4
- "description": "react-native-nitro-player",
3
+ "version": "0.5.0",
4
+ "description": "A powerful audio player library for React Native with playlist management, playback controls, and support for Android Auto and CarPlay",
5
5
  "main": "lib/index",
6
6
  "module": "lib/index",
7
7
  "types": "lib/index.d.ts",