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.
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +18 -7
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +77 -56
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +89 -9
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +30 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerNotify.kt +5 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +84 -70
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueue.kt +4 -2
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +43 -11
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +6 -2
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerTempQueue.kt +67 -53
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +42 -11
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +26 -4
- package/ios/HybridTrackPlayer.swift +58 -29
- package/ios/core/TrackPlayerCore.swift +22 -1
- package/ios/core/TrackPlayerListener.swift +84 -5
- package/ios/core/TrackPlayerNotify.swift +13 -7
- package/ios/core/TrackPlayerPlayback.swift +75 -39
- package/ios/core/TrackPlayerQueue.swift +28 -4
- package/ios/core/TrackPlayerQueueBuild.swift +100 -40
- package/ios/core/TrackPlayerRecovery.swift +1 -1
- package/ios/core/TrackPlayerTempQueue.swift +129 -122
- package/ios/core/TrackPlayerUrlLoader.swift +34 -10
- package/ios/download/DownloadDatabase.swift +41 -13
- package/ios/media/MediaSessionManager.swift +19 -12
- package/ios/queue/HybridPlayerQueue.swift +10 -5
- package/lib/hooks/callbackManager.d.ts +27 -1
- package/lib/hooks/callbackManager.js +100 -1
- package/lib/hooks/index.d.ts +1 -0
- package/lib/hooks/index.js +1 -0
- package/lib/hooks/useActualQueue.js +18 -12
- package/lib/hooks/useNowPlaying.js +10 -2
- package/lib/hooks/useTimedMetadata.d.ts +16 -0
- package/lib/hooks/useTimedMetadata.js +21 -0
- package/lib/specs/TrackPlayer.nitro.d.ts +6 -1
- package/lib/types/PlayerQueue.d.ts +7 -0
- package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_TimedMetadata.hpp +79 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +9 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +1 -0
- package/nitrogen/generated/android/c++/JTimedMetadata.hpp +70 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_TimedMetadata.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +9 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TimedMetadata.kt +47 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +8 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +25 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +9 -0
- package/nitrogen/generated/ios/swift/Func_void_TimedMetadata.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +1 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +16 -0
- package/nitrogen/generated/ios/swift/TimedMetadata.swift +96 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +1 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +4 -0
- package/nitrogen/generated/shared/c++/TimedMetadata.hpp +96 -0
- package/package.json +1 -1
- package/src/components/CastButton.tsx +14 -2
- package/src/hooks/callbackManager.ts +134 -2
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useActualQueue.ts +18 -13
- package/src/hooks/useNowPlaying.ts +12 -2
- package/src/hooks/useTimedMetadata.ts +25 -0
- package/src/specs/TrackPlayer.nitro.ts +6 -0
- 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
|
|
180
|
-
|
|
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 {
|
|
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
|
package/src/hooks/index.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
95
|
-
|
|
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
|
|
104
|
+
// Update queue when playNext / upNext change (native pushes the new lists).
|
|
105
105
|
useEffect(() => {
|
|
106
|
-
|
|
106
|
+
return callbackManager.subscribeToTemporaryQueueChange(() => {
|
|
107
107
|
updateQueue()
|
|
108
108
|
})
|
|
109
|
+
}, [updateQueue])
|
|
109
110
|
|
|
110
|
-
|
|
111
|
-
|
|
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) =>
|
|
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>
|
package/src/types/PlayerQueue.ts
CHANGED
|
@@ -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
|