react-native-nitro-player 1.5.0-alpha.3 → 1.5.1-alpha.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.
Files changed (33) hide show
  1. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +18 -7
  2. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +71 -56
  3. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +82 -9
  4. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +10 -0
  5. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +84 -70
  6. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueue.kt +4 -2
  7. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +43 -11
  8. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +6 -2
  9. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerTempQueue.kt +67 -53
  10. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +42 -11
  11. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +26 -4
  12. package/ios/HybridTrackPlayer.swift +52 -29
  13. package/ios/core/TrackPlayerCore.swift +9 -1
  14. package/ios/core/TrackPlayerListener.swift +20 -5
  15. package/ios/core/TrackPlayerNotify.swift +9 -7
  16. package/ios/core/TrackPlayerPlayback.swift +75 -39
  17. package/ios/core/TrackPlayerQueue.swift +28 -4
  18. package/ios/core/TrackPlayerQueueBuild.swift +100 -40
  19. package/ios/core/TrackPlayerRecovery.swift +1 -1
  20. package/ios/core/TrackPlayerTempQueue.swift +129 -122
  21. package/ios/core/TrackPlayerUrlLoader.swift +34 -10
  22. package/ios/download/DownloadDatabase.swift +41 -13
  23. package/ios/media/MediaSessionManager.swift +19 -12
  24. package/ios/queue/HybridPlayerQueue.swift +10 -5
  25. package/lib/hooks/callbackManager.d.ts +17 -0
  26. package/lib/hooks/callbackManager.js +67 -1
  27. package/lib/hooks/useActualQueue.js +18 -12
  28. package/lib/hooks/useNowPlaying.js +10 -2
  29. package/package.json +1 -1
  30. package/src/components/CastButton.tsx +14 -2
  31. package/src/hooks/callbackManager.ts +87 -1
  32. package/src/hooks/useActualQueue.ts +18 -13
  33. package/src/hooks/useNowPlaying.ts +12 -2
@@ -1,4 +1,4 @@
1
- import { TrackPlayer } from '../index';
1
+ import { PlayerQueue, TrackPlayer } from '../index';
2
2
  /**
3
3
  * Internal subscription manager that allows multiple hooks to subscribe
4
4
  * to a single native callback. This solves the problem where registering
@@ -9,10 +9,14 @@ class CallbackSubscriptionManager {
9
9
  trackChangeSubscribers = new Set();
10
10
  playbackProgressSubscribers = new Set();
11
11
  seekSubscribers = new Set();
12
+ temporaryQueueSubscribers = new Set();
13
+ playlistsChangedSubscribers = new Set();
14
+ isPlaylistsChangedRegistered = false;
12
15
  isPlaybackStateRegistered = false;
13
16
  isTrackChangeRegistered = false;
14
17
  isPlaybackProgressRegistered = false;
15
18
  isSeekRegistered = false;
19
+ isTemporaryQueueRegistered = false;
16
20
  /**
17
21
  * Subscribe to playback state changes
18
22
  * @returns Unsubscribe function
@@ -117,6 +121,68 @@ class CallbackSubscriptionManager {
117
121
  console.error('[CallbackManager] Failed to register playback progress callback:', error);
118
122
  }
119
123
  }
124
+ /**
125
+ * Subscribe to playNext / upNext changes
126
+ * @returns Unsubscribe function
127
+ */
128
+ subscribeToTemporaryQueueChange(callback) {
129
+ this.temporaryQueueSubscribers.add(callback);
130
+ this.ensureTemporaryQueueRegistered();
131
+ return () => {
132
+ this.temporaryQueueSubscribers.delete(callback);
133
+ };
134
+ }
135
+ /**
136
+ * Subscribe to playlist mutations (tracks added/removed/reordered).
137
+ * @returns Unsubscribe function
138
+ */
139
+ subscribeToPlaylistsChanged(callback) {
140
+ this.playlistsChangedSubscribers.add(callback);
141
+ this.ensurePlaylistsChangedRegistered();
142
+ return () => {
143
+ this.playlistsChangedSubscribers.delete(callback);
144
+ };
145
+ }
146
+ ensurePlaylistsChangedRegistered() {
147
+ if (this.isPlaylistsChangedRegistered)
148
+ return;
149
+ try {
150
+ PlayerQueue.onPlaylistsChanged(() => {
151
+ this.playlistsChangedSubscribers.forEach((subscriber) => {
152
+ try {
153
+ subscriber();
154
+ }
155
+ catch (error) {
156
+ console.error('[CallbackManager] Error in playlists changed subscriber:', error);
157
+ }
158
+ });
159
+ });
160
+ this.isPlaylistsChangedRegistered = true;
161
+ }
162
+ catch (error) {
163
+ console.error('[CallbackManager] Failed to register playlists changed callback:', error);
164
+ }
165
+ }
166
+ ensureTemporaryQueueRegistered() {
167
+ if (this.isTemporaryQueueRegistered)
168
+ return;
169
+ try {
170
+ TrackPlayer.onTemporaryQueueChange((playNextQueue, upNextQueue) => {
171
+ this.temporaryQueueSubscribers.forEach((subscriber) => {
172
+ try {
173
+ subscriber(playNextQueue, upNextQueue);
174
+ }
175
+ catch (error) {
176
+ console.error('[CallbackManager] Error in temporary queue subscriber:', error);
177
+ }
178
+ });
179
+ });
180
+ this.isTemporaryQueueRegistered = true;
181
+ }
182
+ catch (error) {
183
+ console.error('[CallbackManager] Failed to register temporary queue callback:', error);
184
+ }
185
+ }
120
186
  ensureSeekRegistered() {
121
187
  if (this.isSeekRegistered)
122
188
  return;
@@ -75,24 +75,30 @@ export function useActualQueue() {
75
75
  isMounted.current = false;
76
76
  };
77
77
  }, [updateQueue]);
78
- // Update queue on track changes (with slight delay to ensure native side has updated)
78
+ // Update queue on track changes.
79
+ //
80
+ // Deliberately NOT subscribed to playback-state changes: those fire several times
81
+ // per second while buffering or seeking, and each one used to pull the entire queue
82
+ // across the bridge. A playback state change never alters queue membership — only
83
+ // track changes and temporary-queue edits do.
79
84
  useEffect(() => {
80
- const unsubscribe = callbackManager.subscribeToTrackChange(() => {
81
- // Small delay to ensure native queue is updated
82
- setTimeout(updateQueue, 50);
85
+ return callbackManager.subscribeToTrackChange(() => {
86
+ updateQueue();
83
87
  });
84
- return () => {
85
- unsubscribe();
86
- };
87
88
  }, [updateQueue]);
88
- // Update queue on playback state changes
89
+ // Update queue when playNext / upNext change (native pushes the new lists).
89
90
  useEffect(() => {
90
- const unsubscribe = callbackManager.subscribeToPlaybackState(() => {
91
+ return callbackManager.subscribeToTemporaryQueueChange(() => {
92
+ updateQueue();
93
+ });
94
+ }, [updateQueue]);
95
+ // Update queue when the playlist itself is edited (track added / removed / reordered).
96
+ // Without this, a playlist mutation while paused would leave the queue stale until the
97
+ // next track change — playback-state changes used to paper over it.
98
+ useEffect(() => {
99
+ return callbackManager.subscribeToPlaylistsChanged(() => {
91
100
  updateQueue();
92
101
  });
93
- return () => {
94
- unsubscribe();
95
- };
96
102
  }, [updateQueue]);
97
103
  return { queue, refreshQueue, isLoading };
98
104
  }
@@ -80,12 +80,20 @@ export function useNowPlaying() {
80
80
  fetchFullState();
81
81
  });
82
82
  }, [fetchFullState]);
83
- // Subscribe to progress changes — lightweight position/duration update
83
+ // Subscribe to progress changes — lightweight position/duration update.
84
+ // Bail out when neither value changed at whole-second resolution: the native tick
85
+ // is sub-second, and a new state object on every tick re-renders every consumer.
84
86
  useEffect(() => {
85
87
  return callbackManager.subscribeToPlaybackProgressChange((currentPosition, totalDuration) => {
86
88
  if (!isMounted.current)
87
89
  return;
88
- setState((prev) => ({ ...prev, currentPosition, totalDuration }));
90
+ setState((prev) => {
91
+ if (Math.floor(prev.currentPosition) === Math.floor(currentPosition) &&
92
+ prev.totalDuration === totalDuration) {
93
+ return prev;
94
+ }
95
+ return { ...prev, currentPosition, totalDuration };
96
+ });
89
97
  });
90
98
  }, []);
91
99
  // Subscribe to seek events — lightweight position/duration update
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-player",
3
- "version": "1.5.0-alpha.3",
3
+ "version": "1.5.1-alpha.0",
4
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",
@@ -176,8 +176,20 @@ function CastGlyph({
176
176
  }}
177
177
  />
178
178
  {/* Two concentric broadcast arcs radiating from the corner dot */}
179
- <RippleArc cx={cx} cy={cy} radius={size * 0.2} stroke={stroke} color={color} />
180
- <RippleArc cx={cx} cy={cy} radius={size * 0.34} stroke={stroke} color={color} />
179
+ <RippleArc
180
+ cx={cx}
181
+ cy={cy}
182
+ radius={size * 0.2}
183
+ stroke={stroke}
184
+ color={color}
185
+ />
186
+ <RippleArc
187
+ cx={cx}
188
+ cy={cy}
189
+ radius={size * 0.34}
190
+ stroke={stroke}
191
+ color={color}
192
+ />
181
193
  {/* Broadcast dot */}
182
194
  <View
183
195
  style={{
@@ -1,4 +1,4 @@
1
- import { TrackPlayer } from '../index'
1
+ import { PlayerQueue, TrackPlayer } from '../index'
2
2
  import type { TrackItem, TrackPlayerState, Reason } from '../types/PlayerQueue'
3
3
 
4
4
  type PlaybackStateCallback = (state: TrackPlayerState, reason?: Reason) => void
@@ -9,6 +9,10 @@ type PlaybackProgressCallback = (
9
9
  isManuallySeeked?: boolean
10
10
  ) => void
11
11
  type SeekCallback = (position: number, totalDuration: number) => void
12
+ type TemporaryQueueCallback = (
13
+ playNextQueue: TrackItem[],
14
+ upNextQueue: TrackItem[]
15
+ ) => void
12
16
 
13
17
  /**
14
18
  * Internal subscription manager that allows multiple hooks to subscribe
@@ -20,10 +24,14 @@ class CallbackSubscriptionManager {
20
24
  private trackChangeSubscribers = new Set<TrackChangeCallback>()
21
25
  private playbackProgressSubscribers = new Set<PlaybackProgressCallback>()
22
26
  private seekSubscribers = new Set<SeekCallback>()
27
+ private temporaryQueueSubscribers = new Set<TemporaryQueueCallback>()
28
+ private playlistsChangedSubscribers = new Set<() => void>()
29
+ private isPlaylistsChangedRegistered = false
23
30
  private isPlaybackStateRegistered = false
24
31
  private isTrackChangeRegistered = false
25
32
  private isPlaybackProgressRegistered = false
26
33
  private isSeekRegistered = false
34
+ private isTemporaryQueueRegistered = false
27
35
 
28
36
  /**
29
37
  * Subscribe to playback state changes
@@ -156,6 +164,84 @@ class CallbackSubscriptionManager {
156
164
  }
157
165
  }
158
166
 
167
+ /**
168
+ * Subscribe to playNext / upNext changes
169
+ * @returns Unsubscribe function
170
+ */
171
+ subscribeToTemporaryQueueChange(
172
+ callback: TemporaryQueueCallback
173
+ ): () => void {
174
+ this.temporaryQueueSubscribers.add(callback)
175
+ this.ensureTemporaryQueueRegistered()
176
+
177
+ return () => {
178
+ this.temporaryQueueSubscribers.delete(callback)
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Subscribe to playlist mutations (tracks added/removed/reordered).
184
+ * @returns Unsubscribe function
185
+ */
186
+ subscribeToPlaylistsChanged(callback: () => void): () => void {
187
+ this.playlistsChangedSubscribers.add(callback)
188
+ this.ensurePlaylistsChangedRegistered()
189
+
190
+ return () => {
191
+ this.playlistsChangedSubscribers.delete(callback)
192
+ }
193
+ }
194
+
195
+ private ensurePlaylistsChangedRegistered(): void {
196
+ if (this.isPlaylistsChangedRegistered) return
197
+
198
+ try {
199
+ PlayerQueue.onPlaylistsChanged(() => {
200
+ this.playlistsChangedSubscribers.forEach((subscriber) => {
201
+ try {
202
+ subscriber()
203
+ } catch (error) {
204
+ console.error(
205
+ '[CallbackManager] Error in playlists changed subscriber:',
206
+ error
207
+ )
208
+ }
209
+ })
210
+ })
211
+ this.isPlaylistsChangedRegistered = true
212
+ } catch (error) {
213
+ console.error(
214
+ '[CallbackManager] Failed to register playlists changed callback:',
215
+ error
216
+ )
217
+ }
218
+ }
219
+
220
+ private ensureTemporaryQueueRegistered(): void {
221
+ if (this.isTemporaryQueueRegistered) return
222
+
223
+ try {
224
+ TrackPlayer.onTemporaryQueueChange((playNextQueue, upNextQueue) => {
225
+ this.temporaryQueueSubscribers.forEach((subscriber) => {
226
+ try {
227
+ subscriber(playNextQueue, upNextQueue)
228
+ } catch (error) {
229
+ console.error(
230
+ '[CallbackManager] Error in temporary queue subscriber:',
231
+ error
232
+ )
233
+ }
234
+ })
235
+ })
236
+ this.isTemporaryQueueRegistered = true
237
+ } catch (error) {
238
+ console.error(
239
+ '[CallbackManager] Failed to register temporary queue callback:',
240
+ error
241
+ )
242
+ }
243
+ }
244
+
159
245
  private ensureSeekRegistered(): void {
160
246
  if (this.isSeekRegistered) return
161
247
 
@@ -89,27 +89,32 @@ export function useActualQueue(): UseActualQueueResult {
89
89
  }
90
90
  }, [updateQueue])
91
91
 
92
- // Update queue on track changes (with slight delay to ensure native side has updated)
92
+ // Update queue on track changes.
93
+ //
94
+ // Deliberately NOT subscribed to playback-state changes: those fire several times
95
+ // per second while buffering or seeking, and each one used to pull the entire queue
96
+ // across the bridge. A playback state change never alters queue membership — only
97
+ // track changes and temporary-queue edits do.
93
98
  useEffect(() => {
94
- const unsubscribe = callbackManager.subscribeToTrackChange(() => {
95
- // Small delay to ensure native queue is updated
96
- setTimeout(updateQueue, 50)
99
+ return callbackManager.subscribeToTrackChange(() => {
100
+ updateQueue()
97
101
  })
98
-
99
- return () => {
100
- unsubscribe()
101
- }
102
102
  }, [updateQueue])
103
103
 
104
- // Update queue on playback state changes
104
+ // Update queue when playNext / upNext change (native pushes the new lists).
105
105
  useEffect(() => {
106
- const unsubscribe = callbackManager.subscribeToPlaybackState(() => {
106
+ return callbackManager.subscribeToTemporaryQueueChange(() => {
107
107
  updateQueue()
108
108
  })
109
+ }, [updateQueue])
109
110
 
110
- return () => {
111
- unsubscribe()
112
- }
111
+ // Update queue when the playlist itself is edited (track added / removed / reordered).
112
+ // Without this, a playlist mutation while paused would leave the queue stale until the
113
+ // next track change — playback-state changes used to paper over it.
114
+ useEffect(() => {
115
+ return callbackManager.subscribeToPlaylistsChanged(() => {
116
+ updateQueue()
117
+ })
113
118
  }, [updateQueue])
114
119
 
115
120
  return { queue, refreshQueue, isLoading }
@@ -88,12 +88,22 @@ export function useNowPlaying(): PlayerState {
88
88
  })
89
89
  }, [fetchFullState])
90
90
 
91
- // Subscribe to progress changes — lightweight position/duration update
91
+ // Subscribe to progress changes — lightweight position/duration update.
92
+ // Bail out when neither value changed at whole-second resolution: the native tick
93
+ // is sub-second, and a new state object on every tick re-renders every consumer.
92
94
  useEffect(() => {
93
95
  return callbackManager.subscribeToPlaybackProgressChange(
94
96
  (currentPosition, totalDuration) => {
95
97
  if (!isMounted.current) return
96
- setState((prev) => ({ ...prev, currentPosition, totalDuration }))
98
+ setState((prev) => {
99
+ if (
100
+ Math.floor(prev.currentPosition) === Math.floor(currentPosition) &&
101
+ prev.totalDuration === totalDuration
102
+ ) {
103
+ return prev
104
+ }
105
+ return { ...prev, currentPosition, totalDuration }
106
+ })
97
107
  }
98
108
  )
99
109
  }, [])