react-native-nitro-player 0.0.1 → 0.3.0-alpha.10
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/README.md +282 -2
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +37 -29
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +24 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +408 -16
- package/ios/HybridAudioRoutePicker.swift +47 -46
- package/ios/HybridTrackPlayer.swift +22 -0
- package/ios/core/TrackPlayerCore.swift +538 -48
- package/lib/hooks/callbackManager.d.ts +28 -0
- package/lib/hooks/callbackManager.js +76 -0
- package/lib/hooks/index.d.ts +7 -0
- package/lib/hooks/index.js +3 -0
- package/lib/hooks/useActualQueue.d.ts +48 -0
- package/lib/hooks/useActualQueue.js +98 -0
- package/lib/hooks/useNowPlaying.d.ts +36 -0
- package/lib/hooks/useNowPlaying.js +87 -0
- package/lib/hooks/useOnChangeTrack.d.ts +33 -6
- package/lib/hooks/useOnChangeTrack.js +65 -9
- package/lib/hooks/useOnPlaybackStateChange.d.ts +32 -6
- package/lib/hooks/useOnPlaybackStateChange.js +65 -9
- package/lib/hooks/usePlaylist.d.ts +48 -0
- package/lib/hooks/usePlaylist.js +136 -0
- package/lib/index.d.ts +1 -0
- package/lib/specs/TrackPlayer.nitro.d.ts +6 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +46 -9
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +5 -0
- package/nitrogen/generated/android/c++/JRepeatMode.hpp +62 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +20 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/RepeatMode.kt +22 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +9 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +44 -4
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +5 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +64 -0
- package/nitrogen/generated/ios/swift/RepeatMode.swift +44 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +5 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +12 -3
- package/nitrogen/generated/shared/c++/RepeatMode.hpp +80 -0
- package/package.json +13 -12
- package/src/hooks/callbackManager.ts +96 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/useActualQueue.ts +116 -0
- package/src/hooks/useNowPlaying.ts +97 -0
- package/src/hooks/useOnChangeTrack.ts +77 -13
- package/src/hooks/useOnPlaybackStateChange.ts +83 -13
- package/src/hooks/usePlaylist.ts +161 -0
- package/src/index.ts +1 -1
- package/src/specs/TrackPlayer.nitro.ts +7 -0
|
@@ -1,24 +1,88 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
1
|
+
import { useEffect, useState, useRef } from 'react'
|
|
2
2
|
import { TrackPlayer } from '../index'
|
|
3
|
+
import { callbackManager } from './callbackManager'
|
|
3
4
|
import type { TrackItem, Reason } from '../types/PlayerQueue'
|
|
4
5
|
|
|
6
|
+
export interface TrackChangeResult {
|
|
7
|
+
track: TrackItem | null
|
|
8
|
+
reason: Reason | undefined
|
|
9
|
+
isReady: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
/**
|
|
6
|
-
* Hook to
|
|
7
|
-
*
|
|
13
|
+
* Hook to subscribe to track changes.
|
|
14
|
+
*
|
|
15
|
+
* This hook provides real-time track change updates using native callbacks.
|
|
16
|
+
* Multiple components can use this hook simultaneously without interfering
|
|
17
|
+
* with each other.
|
|
18
|
+
*
|
|
19
|
+
* @returns Object with:
|
|
20
|
+
* - track: Current track or null if no track is playing
|
|
21
|
+
* - reason: Reason for the last track change
|
|
22
|
+
* - isReady: Whether the initial state has been loaded
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* function NowPlaying() {
|
|
27
|
+
* const { track, reason, isReady } = useOnChangeTrack()
|
|
28
|
+
*
|
|
29
|
+
* if (!isReady) return <Text>Loading...</Text>
|
|
30
|
+
* if (!track) return <Text>No track playing</Text>
|
|
31
|
+
*
|
|
32
|
+
* return (
|
|
33
|
+
* <View>
|
|
34
|
+
* <Text>{track.title}</Text>
|
|
35
|
+
* <Text>{track.artist}</Text>
|
|
36
|
+
* </View>
|
|
37
|
+
* )
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
8
40
|
*/
|
|
9
|
-
export function useOnChangeTrack(): {
|
|
10
|
-
track
|
|
11
|
-
reason: Reason | undefined
|
|
12
|
-
} {
|
|
13
|
-
const [track, setTrack] = useState<TrackItem | undefined>(undefined)
|
|
41
|
+
export function useOnChangeTrack(): TrackChangeResult {
|
|
42
|
+
const [track, setTrack] = useState<TrackItem | null>(null)
|
|
14
43
|
const [reason, setReason] = useState<Reason | undefined>(undefined)
|
|
44
|
+
const [isReady, setIsReady] = useState(false)
|
|
45
|
+
const isMounted = useRef(true)
|
|
15
46
|
|
|
47
|
+
// Initialize with current track from the player
|
|
16
48
|
useEffect(() => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
49
|
+
isMounted.current = true
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const playerState = TrackPlayer.getState()
|
|
53
|
+
if (isMounted.current) {
|
|
54
|
+
setTrack(playerState.currentTrack)
|
|
55
|
+
setIsReady(true)
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error('[useOnChangeTrack] Failed to get initial state:', error)
|
|
59
|
+
if (isMounted.current) {
|
|
60
|
+
setTrack(null)
|
|
61
|
+
setIsReady(true)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return () => {
|
|
66
|
+
isMounted.current = false
|
|
67
|
+
}
|
|
68
|
+
}, [])
|
|
69
|
+
|
|
70
|
+
// Subscribe to track changes
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
const handleTrackChange = (newTrack: TrackItem, newReason?: Reason) => {
|
|
73
|
+
if (isMounted.current) {
|
|
74
|
+
setTrack(newTrack)
|
|
75
|
+
setReason(newReason)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const unsubscribe =
|
|
80
|
+
callbackManager.subscribeToTrackChange(handleTrackChange)
|
|
81
|
+
|
|
82
|
+
return () => {
|
|
83
|
+
unsubscribe()
|
|
84
|
+
}
|
|
21
85
|
}, [])
|
|
22
86
|
|
|
23
|
-
return { track, reason }
|
|
87
|
+
return { track, reason, isReady }
|
|
24
88
|
}
|
|
@@ -1,24 +1,94 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
1
|
+
import { useEffect, useState, useRef } from 'react'
|
|
2
2
|
import { TrackPlayer } from '../index'
|
|
3
|
+
import { callbackManager } from './callbackManager'
|
|
3
4
|
import type { TrackPlayerState, Reason } from '../types/PlayerQueue'
|
|
4
5
|
|
|
6
|
+
export interface PlaybackStateResult {
|
|
7
|
+
state: TrackPlayerState
|
|
8
|
+
reason: Reason | undefined
|
|
9
|
+
isReady: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
/**
|
|
6
|
-
* Hook to
|
|
7
|
-
*
|
|
13
|
+
* Hook to subscribe to playback state changes.
|
|
14
|
+
*
|
|
15
|
+
* This hook provides real-time playback state updates using native callbacks.
|
|
16
|
+
* Multiple components can use this hook simultaneously without interfering
|
|
17
|
+
* with each other.
|
|
18
|
+
*
|
|
19
|
+
* @returns Object with:
|
|
20
|
+
* - state: Current playback state ('playing' | 'paused' | 'stopped')
|
|
21
|
+
* - reason: Reason for the last state change
|
|
22
|
+
* - isReady: Whether the initial state has been loaded
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* function PlaybackIndicator() {
|
|
27
|
+
* const { state, reason, isReady } = useOnPlaybackStateChange()
|
|
28
|
+
*
|
|
29
|
+
* if (!isReady) return <Text>Loading...</Text>
|
|
30
|
+
*
|
|
31
|
+
* return (
|
|
32
|
+
* <View>
|
|
33
|
+
* <Text>State: {state}</Text>
|
|
34
|
+
* {reason && <Text>Reason: {reason}</Text>}
|
|
35
|
+
* </View>
|
|
36
|
+
* )
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
8
39
|
*/
|
|
9
|
-
export function useOnPlaybackStateChange(): {
|
|
10
|
-
state
|
|
11
|
-
reason: Reason | undefined
|
|
12
|
-
} {
|
|
13
|
-
const [state, setState] = useState<TrackPlayerState | undefined>(undefined)
|
|
40
|
+
export function useOnPlaybackStateChange(): PlaybackStateResult {
|
|
41
|
+
const [state, setState] = useState<TrackPlayerState>('stopped')
|
|
14
42
|
const [reason, setReason] = useState<Reason | undefined>(undefined)
|
|
43
|
+
const [isReady, setIsReady] = useState(false)
|
|
44
|
+
const isMounted = useRef(true)
|
|
15
45
|
|
|
46
|
+
// Initialize with current state from the player
|
|
16
47
|
useEffect(() => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
48
|
+
isMounted.current = true
|
|
49
|
+
|
|
50
|
+
// Get initial state synchronously
|
|
51
|
+
try {
|
|
52
|
+
const playerState = TrackPlayer.getState()
|
|
53
|
+
if (isMounted.current) {
|
|
54
|
+
setState(playerState.currentState)
|
|
55
|
+
setIsReady(true)
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error(
|
|
59
|
+
'[useOnPlaybackStateChange] Failed to get initial state:',
|
|
60
|
+
error
|
|
61
|
+
)
|
|
62
|
+
if (isMounted.current) {
|
|
63
|
+
setState('stopped')
|
|
64
|
+
setIsReady(true)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return () => {
|
|
69
|
+
isMounted.current = false
|
|
70
|
+
}
|
|
71
|
+
}, [])
|
|
72
|
+
|
|
73
|
+
// Subscribe to playback state changes
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
const handleStateChange = (
|
|
76
|
+
newState: TrackPlayerState,
|
|
77
|
+
newReason?: Reason
|
|
78
|
+
) => {
|
|
79
|
+
if (isMounted.current) {
|
|
80
|
+
setState(newState)
|
|
81
|
+
setReason(newReason)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const unsubscribe =
|
|
86
|
+
callbackManager.subscribeToPlaybackState(handleStateChange)
|
|
87
|
+
|
|
88
|
+
return () => {
|
|
89
|
+
unsubscribe()
|
|
90
|
+
}
|
|
21
91
|
}, [])
|
|
22
92
|
|
|
23
|
-
return { state, reason }
|
|
93
|
+
return { state, reason, isReady }
|
|
24
94
|
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { useEffect, useState, useRef, useCallback } from 'react'
|
|
2
|
+
import { PlayerQueue } from '../index'
|
|
3
|
+
import { callbackManager } from './callbackManager'
|
|
4
|
+
import type { Playlist, TrackItem } from '../types/PlayerQueue'
|
|
5
|
+
|
|
6
|
+
export interface UsePlaylistResult {
|
|
7
|
+
/** The currently loaded playlist */
|
|
8
|
+
currentPlaylist: Playlist | null
|
|
9
|
+
/** ID of the currently loaded playlist */
|
|
10
|
+
currentPlaylistId: string | null
|
|
11
|
+
/** All available playlists */
|
|
12
|
+
allPlaylists: Playlist[]
|
|
13
|
+
/** All tracks from all playlists (flattened) */
|
|
14
|
+
allTracks: TrackItem[]
|
|
15
|
+
/** Whether the playlists are currently loading */
|
|
16
|
+
isLoading: boolean
|
|
17
|
+
/** Manually refresh playlist data */
|
|
18
|
+
refreshPlaylists: () => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Hook to manage playlist state
|
|
23
|
+
*
|
|
24
|
+
* Provides current playlist, all playlists, and all tracks across playlists.
|
|
25
|
+
* Automatically refreshes when:
|
|
26
|
+
* - Component mounts
|
|
27
|
+
* - Track changes (to update currentPlaylistId)
|
|
28
|
+
* - Playlists are modified via PlayerQueue methods
|
|
29
|
+
*
|
|
30
|
+
* Call `refreshPlaylists()` after creating/deleting playlists to update the state.
|
|
31
|
+
*
|
|
32
|
+
* @returns Object containing playlist state and refresh function
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```tsx
|
|
36
|
+
* function MyComponent() {
|
|
37
|
+
* const { currentPlaylist, allTracks, refreshPlaylists } = usePlaylist();
|
|
38
|
+
*
|
|
39
|
+
* const handleCreatePlaylist = () => {
|
|
40
|
+
* PlayerQueue.createPlaylist('New Playlist');
|
|
41
|
+
* refreshPlaylists(); // Refresh to see the new playlist
|
|
42
|
+
* };
|
|
43
|
+
*
|
|
44
|
+
* return (
|
|
45
|
+
* <View>
|
|
46
|
+
* <Text>{currentPlaylist?.name}</Text>
|
|
47
|
+
* <Text>Total tracks: {allTracks.length}</Text>
|
|
48
|
+
* </View>
|
|
49
|
+
* );
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function usePlaylist(): UsePlaylistResult {
|
|
54
|
+
const [currentPlaylist, setCurrentPlaylist] = useState<Playlist | null>(null)
|
|
55
|
+
const [currentPlaylistId, setCurrentPlaylistId] = useState<string | null>(
|
|
56
|
+
null
|
|
57
|
+
)
|
|
58
|
+
const [allPlaylists, setAllPlaylists] = useState<Playlist[]>([])
|
|
59
|
+
const [allTracks, setAllTracks] = useState<TrackItem[]>([])
|
|
60
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
61
|
+
const isMounted = useRef(true)
|
|
62
|
+
const hasSubscribed = useRef(false)
|
|
63
|
+
|
|
64
|
+
const refreshPlaylists = useCallback(() => {
|
|
65
|
+
if (!isMounted.current) return
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
// Get current playlist ID
|
|
69
|
+
const playlistId = PlayerQueue.getCurrentPlaylistId()
|
|
70
|
+
if (!isMounted.current) return
|
|
71
|
+
setCurrentPlaylistId(playlistId)
|
|
72
|
+
|
|
73
|
+
// Get current playlist details
|
|
74
|
+
if (playlistId) {
|
|
75
|
+
const playlist = PlayerQueue.getPlaylist(playlistId)
|
|
76
|
+
if (isMounted.current) {
|
|
77
|
+
setCurrentPlaylist(playlist)
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
if (isMounted.current) {
|
|
81
|
+
setCurrentPlaylist(null)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Get all playlists
|
|
86
|
+
const playlists = PlayerQueue.getAllPlaylists()
|
|
87
|
+
if (!isMounted.current) return
|
|
88
|
+
setAllPlaylists(playlists)
|
|
89
|
+
|
|
90
|
+
// Get all tracks from all playlists (deduplicated by id)
|
|
91
|
+
const trackMap = new Map<string, TrackItem>()
|
|
92
|
+
playlists.forEach((playlist) => {
|
|
93
|
+
playlist.tracks.forEach((track) => {
|
|
94
|
+
if (!trackMap.has(track.id)) {
|
|
95
|
+
trackMap.set(track.id, track)
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
if (isMounted.current) {
|
|
100
|
+
setAllTracks(Array.from(trackMap.values()))
|
|
101
|
+
setIsLoading(false)
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error('[usePlaylist] Error refreshing playlists:', error)
|
|
105
|
+
if (isMounted.current) {
|
|
106
|
+
setIsLoading(false)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}, [])
|
|
110
|
+
|
|
111
|
+
// Initialize and setup mounted ref
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
isMounted.current = true
|
|
114
|
+
|
|
115
|
+
// Initial load
|
|
116
|
+
refreshPlaylists()
|
|
117
|
+
|
|
118
|
+
return () => {
|
|
119
|
+
isMounted.current = false
|
|
120
|
+
}
|
|
121
|
+
}, [refreshPlaylists])
|
|
122
|
+
|
|
123
|
+
// Subscribe to native playlist changes (only once)
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (hasSubscribed.current) return
|
|
126
|
+
hasSubscribed.current = true
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
PlayerQueue.onPlaylistsChanged(() => {
|
|
130
|
+
if (isMounted.current) {
|
|
131
|
+
refreshPlaylists()
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error('[usePlaylist] Error setting up playlist listener:', error)
|
|
136
|
+
}
|
|
137
|
+
}, [refreshPlaylists])
|
|
138
|
+
|
|
139
|
+
// Also refresh when track changes (as it might indicate playlist loaded)
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
const unsubscribe = callbackManager.subscribeToTrackChange(() => {
|
|
142
|
+
// Refresh to update currentPlaylistId when track changes
|
|
143
|
+
if (isMounted.current) {
|
|
144
|
+
refreshPlaylists()
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
return () => {
|
|
149
|
+
unsubscribe()
|
|
150
|
+
}
|
|
151
|
+
}, [refreshPlaylists])
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
currentPlaylist,
|
|
155
|
+
currentPlaylistId,
|
|
156
|
+
allPlaylists,
|
|
157
|
+
allTracks,
|
|
158
|
+
isLoading,
|
|
159
|
+
refreshPlaylists,
|
|
160
|
+
}
|
|
161
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -42,6 +42,6 @@ export * from './hooks'
|
|
|
42
42
|
export * from './types/PlayerQueue'
|
|
43
43
|
export * from './types/AndroidAutoMediaLibrary'
|
|
44
44
|
export type { TAudioDevice } from './specs/AudioDevices.nitro'
|
|
45
|
-
|
|
45
|
+
export type { RepeatMode } from './specs/TrackPlayer.nitro'
|
|
46
46
|
// Export utilities
|
|
47
47
|
export { AndroidAutoMediaLibraryHelper } from './utils/androidAutoMediaLibrary'
|
|
@@ -54,6 +54,8 @@ export interface PlayerQueue
|
|
|
54
54
|
): void
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
export type RepeatMode = 'off' | 'Playlist' | 'track'
|
|
58
|
+
|
|
57
59
|
export interface TrackPlayer
|
|
58
60
|
extends HybridObject<{ android: 'kotlin'; ios: 'swift' }> {
|
|
59
61
|
play(): void
|
|
@@ -62,7 +64,11 @@ export interface TrackPlayer
|
|
|
62
64
|
skipToNext(): void
|
|
63
65
|
skipToPrevious(): void
|
|
64
66
|
seek(position: number): void
|
|
67
|
+
addToUpNext(trackId: string): void
|
|
68
|
+
playNext(trackId: string): void
|
|
69
|
+
getActualQueue(): TrackItem[]
|
|
65
70
|
getState(): PlayerState
|
|
71
|
+
setRepeatMode(mode: RepeatMode): boolean
|
|
66
72
|
configure(config: PlayerConfig): void
|
|
67
73
|
onChangeTrack(callback: (track: TrackItem, reason?: Reason) => void): void
|
|
68
74
|
onPlaybackStateChange(
|
|
@@ -78,4 +84,5 @@ export interface TrackPlayer
|
|
|
78
84
|
): void
|
|
79
85
|
onAndroidAutoConnectionChange(callback: (connected: boolean) => void): void
|
|
80
86
|
isAndroidAutoConnected(): boolean
|
|
87
|
+
setVolume(volume: number): boolean
|
|
81
88
|
}
|