react-native-nitro-player 1.5.0 → 1.6.0-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 (63) 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 +77 -56
  3. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +89 -9
  4. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +30 -0
  5. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerNotify.kt +5 -0
  6. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +84 -70
  7. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueue.kt +4 -2
  8. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +43 -11
  9. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +6 -2
  10. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerTempQueue.kt +67 -53
  11. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +42 -11
  12. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +26 -4
  13. package/ios/HybridTrackPlayer.swift +58 -29
  14. package/ios/core/TrackPlayerCore.swift +22 -1
  15. package/ios/core/TrackPlayerListener.swift +84 -5
  16. package/ios/core/TrackPlayerNotify.swift +13 -7
  17. package/ios/core/TrackPlayerPlayback.swift +75 -39
  18. package/ios/core/TrackPlayerQueue.swift +28 -4
  19. package/ios/core/TrackPlayerQueueBuild.swift +100 -40
  20. package/ios/core/TrackPlayerRecovery.swift +1 -1
  21. package/ios/core/TrackPlayerTempQueue.swift +129 -122
  22. package/ios/core/TrackPlayerUrlLoader.swift +34 -10
  23. package/ios/download/DownloadDatabase.swift +41 -13
  24. package/ios/media/MediaSessionManager.swift +19 -12
  25. package/ios/queue/HybridPlayerQueue.swift +10 -5
  26. package/lib/hooks/callbackManager.d.ts +27 -1
  27. package/lib/hooks/callbackManager.js +100 -1
  28. package/lib/hooks/index.d.ts +1 -0
  29. package/lib/hooks/index.js +1 -0
  30. package/lib/hooks/useActualQueue.js +18 -12
  31. package/lib/hooks/useNowPlaying.js +10 -2
  32. package/lib/hooks/useTimedMetadata.d.ts +16 -0
  33. package/lib/hooks/useTimedMetadata.js +21 -0
  34. package/lib/specs/TrackPlayer.nitro.d.ts +6 -1
  35. package/lib/types/PlayerQueue.d.ts +7 -0
  36. package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +2 -0
  37. package/nitrogen/generated/android/c++/JFunc_void_TimedMetadata.hpp +79 -0
  38. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +9 -0
  39. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +1 -0
  40. package/nitrogen/generated/android/c++/JTimedMetadata.hpp +70 -0
  41. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_TimedMetadata.kt +80 -0
  42. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +9 -0
  43. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TimedMetadata.kt +47 -0
  44. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +8 -0
  45. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +25 -0
  46. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +3 -0
  47. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +9 -0
  48. package/nitrogen/generated/ios/swift/Func_void_TimedMetadata.swift +46 -0
  49. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +1 -0
  50. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +16 -0
  51. package/nitrogen/generated/ios/swift/TimedMetadata.swift +96 -0
  52. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +1 -0
  53. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +4 -0
  54. package/nitrogen/generated/shared/c++/TimedMetadata.hpp +96 -0
  55. package/package.json +1 -1
  56. package/src/components/CastButton.tsx +14 -2
  57. package/src/hooks/callbackManager.ts +134 -2
  58. package/src/hooks/index.ts +1 -0
  59. package/src/hooks/useActualQueue.ts +18 -13
  60. package/src/hooks/useNowPlaying.ts +12 -2
  61. package/src/hooks/useTimedMetadata.ts +25 -0
  62. package/src/specs/TrackPlayer.nitro.ts +6 -0
  63. package/src/types/PlayerQueue.ts +8 -0
@@ -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,5 +1,10 @@
1
- import { TrackPlayer } from '../index'
2
- import type { TrackItem, TrackPlayerState, Reason } from '../types/PlayerQueue'
1
+ import { PlayerQueue, TrackPlayer } from '../index'
2
+ import type {
3
+ TrackItem,
4
+ TrackPlayerState,
5
+ Reason,
6
+ TimedMetadata,
7
+ } from '../types/PlayerQueue'
3
8
 
4
9
  type PlaybackStateCallback = (state: TrackPlayerState, reason?: Reason) => void
5
10
  type TrackChangeCallback = (track: TrackItem, reason?: Reason) => void
@@ -9,6 +14,11 @@ type PlaybackProgressCallback = (
9
14
  isManuallySeeked?: boolean
10
15
  ) => void
11
16
  type SeekCallback = (position: number, totalDuration: number) => void
17
+ type TimedMetadataCallback = (metadata: TimedMetadata) => void
18
+ type TemporaryQueueCallback = (
19
+ playNextQueue: TrackItem[],
20
+ upNextQueue: TrackItem[]
21
+ ) => void
12
22
 
13
23
  /**
14
24
  * Internal subscription manager that allows multiple hooks to subscribe
@@ -20,10 +30,16 @@ class CallbackSubscriptionManager {
20
30
  private trackChangeSubscribers = new Set<TrackChangeCallback>()
21
31
  private playbackProgressSubscribers = new Set<PlaybackProgressCallback>()
22
32
  private seekSubscribers = new Set<SeekCallback>()
33
+ private timedMetadataSubscribers = new Set<TimedMetadataCallback>()
34
+ private temporaryQueueSubscribers = new Set<TemporaryQueueCallback>()
35
+ private playlistsChangedSubscribers = new Set<() => void>()
36
+ private isPlaylistsChangedRegistered = false
23
37
  private isPlaybackStateRegistered = false
24
38
  private isTrackChangeRegistered = false
25
39
  private isPlaybackProgressRegistered = false
26
40
  private isSeekRegistered = false
41
+ private isTimedMetadataRegistered = false
42
+ private isTemporaryQueueRegistered = false
27
43
 
28
44
  /**
29
45
  * Subscribe to playback state changes
@@ -156,6 +172,84 @@ class CallbackSubscriptionManager {
156
172
  }
157
173
  }
158
174
 
175
+ /**
176
+ * Subscribe to playNext / upNext changes
177
+ * @returns Unsubscribe function
178
+ */
179
+ subscribeToTemporaryQueueChange(
180
+ callback: TemporaryQueueCallback
181
+ ): () => void {
182
+ this.temporaryQueueSubscribers.add(callback)
183
+ this.ensureTemporaryQueueRegistered()
184
+
185
+ return () => {
186
+ this.temporaryQueueSubscribers.delete(callback)
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Subscribe to playlist mutations (tracks added/removed/reordered).
192
+ * @returns Unsubscribe function
193
+ */
194
+ subscribeToPlaylistsChanged(callback: () => void): () => void {
195
+ this.playlistsChangedSubscribers.add(callback)
196
+ this.ensurePlaylistsChangedRegistered()
197
+
198
+ return () => {
199
+ this.playlistsChangedSubscribers.delete(callback)
200
+ }
201
+ }
202
+
203
+ private ensurePlaylistsChangedRegistered(): void {
204
+ if (this.isPlaylistsChangedRegistered) return
205
+
206
+ try {
207
+ PlayerQueue.onPlaylistsChanged(() => {
208
+ this.playlistsChangedSubscribers.forEach((subscriber) => {
209
+ try {
210
+ subscriber()
211
+ } catch (error) {
212
+ console.error(
213
+ '[CallbackManager] Error in playlists changed subscriber:',
214
+ error
215
+ )
216
+ }
217
+ })
218
+ })
219
+ this.isPlaylistsChangedRegistered = true
220
+ } catch (error) {
221
+ console.error(
222
+ '[CallbackManager] Failed to register playlists changed callback:',
223
+ error
224
+ )
225
+ }
226
+ }
227
+
228
+ private ensureTemporaryQueueRegistered(): void {
229
+ if (this.isTemporaryQueueRegistered) return
230
+
231
+ try {
232
+ TrackPlayer.onTemporaryQueueChange((playNextQueue, upNextQueue) => {
233
+ this.temporaryQueueSubscribers.forEach((subscriber) => {
234
+ try {
235
+ subscriber(playNextQueue, upNextQueue)
236
+ } catch (error) {
237
+ console.error(
238
+ '[CallbackManager] Error in temporary queue subscriber:',
239
+ error
240
+ )
241
+ }
242
+ })
243
+ })
244
+ this.isTemporaryQueueRegistered = true
245
+ } catch (error) {
246
+ console.error(
247
+ '[CallbackManager] Failed to register temporary queue callback:',
248
+ error
249
+ )
250
+ }
251
+ }
252
+
159
253
  private ensureSeekRegistered(): void {
160
254
  if (this.isSeekRegistered) return
161
255
 
@@ -177,6 +271,44 @@ class CallbackSubscriptionManager {
177
271
  )
178
272
  }
179
273
  }
274
+
275
+ /**
276
+ * Subscribe to in-stream (timed) metadata events
277
+ * @returns Unsubscribe function
278
+ */
279
+ subscribeToTimedMetadata(callback: TimedMetadataCallback): () => void {
280
+ this.timedMetadataSubscribers.add(callback)
281
+ this.ensureTimedMetadataRegistered()
282
+
283
+ return () => {
284
+ this.timedMetadataSubscribers.delete(callback)
285
+ }
286
+ }
287
+
288
+ private ensureTimedMetadataRegistered(): void {
289
+ if (this.isTimedMetadataRegistered) return
290
+
291
+ try {
292
+ TrackPlayer.onTimedMetadata((metadata) => {
293
+ this.timedMetadataSubscribers.forEach((subscriber) => {
294
+ try {
295
+ subscriber(metadata)
296
+ } catch (error) {
297
+ console.error(
298
+ '[CallbackManager] Error in timed metadata subscriber:',
299
+ error
300
+ )
301
+ }
302
+ })
303
+ })
304
+ this.isTimedMetadataRegistered = true
305
+ } catch (error) {
306
+ console.error(
307
+ '[CallbackManager] Failed to register timed metadata callback:',
308
+ error
309
+ )
310
+ }
311
+ }
180
312
  }
181
313
 
182
314
  // Export singleton instance
@@ -4,6 +4,7 @@ export { useOnPlaybackStateChange } from './useOnPlaybackStateChange'
4
4
  export type { PlaybackStateResult } from './useOnPlaybackStateChange'
5
5
  export { useOnSeek } from './useOnSeek'
6
6
  export { useOnPlaybackProgressChange } from './useOnPlaybackProgressChange'
7
+ export { useTimedMetadata } from './useTimedMetadata'
7
8
  export { useAndroidAutoConnection } from './useAndroidAutoConnection'
8
9
  export { useAudioDevices } from './useAudioDevices'
9
10
  export { useNowPlaying } from './useNowPlaying'
@@ -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
  }, [])
@@ -0,0 +1,25 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { callbackManager } from './callbackManager'
3
+ import type { TimedMetadata } from '../types/PlayerQueue'
4
+
5
+ /**
6
+ * Hook to subscribe to in-stream (timed) metadata — ICY/Shoutcast StreamTitle
7
+ * updates and ID3 frames emitted while a live stream plays.
8
+ *
9
+ * @returns The most recent metadata, or undefined until the first event arrives
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * function NowPlayingFromStream() {
14
+ * const metadata = useTimedMetadata()
15
+ * return <Text>{metadata?.title ?? 'Live'}</Text>
16
+ * }
17
+ * ```
18
+ */
19
+ export function useTimedMetadata(): TimedMetadata | undefined {
20
+ const [metadata, setMetadata] = useState<TimedMetadata | undefined>(undefined)
21
+
22
+ useEffect(() => callbackManager.subscribeToTimedMetadata(setMetadata), [])
23
+
24
+ return metadata
25
+ }
@@ -7,6 +7,7 @@ import type {
7
7
  PlayerState,
8
8
  PlayerConfig,
9
9
  Playlist,
10
+ TimedMetadata,
10
11
  } from '../types/PlayerQueue'
11
12
 
12
13
  export interface PlayerQueue extends HybridObject<{
@@ -96,6 +97,11 @@ export interface TrackPlayer extends HybridObject<{
96
97
  isManuallySeeked?: boolean
97
98
  ) => void
98
99
  ): void
100
+ /**
101
+ * Register callback for in-stream (timed) metadata — ICY/Shoutcast StreamTitle
102
+ * updates and ID3 frames emitted while a live stream plays.
103
+ */
104
+ onTimedMetadata(callback: (metadata: TimedMetadata) => void): void
99
105
  onAndroidAutoConnectionChange(callback: (connected: boolean) => void): void
100
106
  isAndroidAutoConnected(): boolean
101
107
  setVolume(volume: number): Promise<void>
@@ -40,6 +40,14 @@ export interface PlayerState {
40
40
  currentPlayingType: CurrentPlayingType
41
41
  }
42
42
 
43
+ /** In-stream metadata emitted while a stream plays (ICY/Shoutcast StreamTitle, ID3 frames) */
44
+ export interface TimedMetadata {
45
+ title?: string
46
+ artist?: string
47
+ album?: string
48
+ artworkUrl?: string
49
+ }
50
+
43
51
  export interface PlayerConfig {
44
52
  androidAutoEnabled?: boolean
45
53
  carPlayEnabled?: boolean