react-native-nitro-player 0.3.0-alpha.7 → 0.3.0-alpha.9
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 +20 -0
- package/lib/hooks/callbackManager.d.ts +28 -0
- package/lib/hooks/callbackManager.js +76 -0
- package/lib/hooks/index.d.ts +3 -0
- package/lib/hooks/index.js +1 -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/package.json +1 -1
- package/src/hooks/callbackManager.ts +96 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useNowPlaying.ts +97 -0
- package/src/hooks/useOnChangeTrack.ts +77 -13
- package/src/hooks/useOnPlaybackStateChange.ts +83 -13
package/README.md
CHANGED
|
@@ -171,6 +171,21 @@ Automatically polls for audio device changes every 2 seconds.
|
|
|
171
171
|
|
|
172
172
|
- `devices: TAudioDevice[]` - Array of available audio devices
|
|
173
173
|
|
|
174
|
+
### `useNowPlaying()`
|
|
175
|
+
|
|
176
|
+
Returns the complete current player state (same as `TrackPlayer.getState()`). This hook provides all player information in a single object and automatically updates when the player state changes.
|
|
177
|
+
|
|
178
|
+
**Returns:**
|
|
179
|
+
|
|
180
|
+
- `PlayerState` object containing:
|
|
181
|
+
- `currentTrack: TrackItem | null` - The current track being played, or `null` if no track is playing
|
|
182
|
+
- `totalDuration: number` - Total duration of the current track in seconds
|
|
183
|
+
- `currentState: TrackPlayerState` - Current playback state (`'playing'`, `'paused'`, or `'stopped'`)
|
|
184
|
+
- `currentPlaylistId: string | null` - ID of the currently loaded playlist, or `null` if no playlist is loaded
|
|
185
|
+
- `currentIndex: number` - Index of the current track in the playlist (-1 if no track is playing)
|
|
186
|
+
|
|
187
|
+
**Note:** This hook is equivalent to calling `TrackPlayer.getState()` but provides reactive updates. It listens to track changes and playback state changes to update automatically. Also dont rely on progress from this hook
|
|
188
|
+
|
|
174
189
|
## Audio Device APIs
|
|
175
190
|
|
|
176
191
|
### `AudioDevices` (Android only)
|
|
@@ -347,6 +362,9 @@ function PlayerComponent() {
|
|
|
347
362
|
// Check Android Auto connection
|
|
348
363
|
const { isConnected } = useAndroidAutoConnection()
|
|
349
364
|
|
|
365
|
+
// Get complete player state (alternative to individual hooks)
|
|
366
|
+
const nowPlaying = useNowPlaying()
|
|
367
|
+
|
|
350
368
|
return (
|
|
351
369
|
<View>
|
|
352
370
|
{track && (
|
|
@@ -354,6 +372,8 @@ function PlayerComponent() {
|
|
|
354
372
|
)}
|
|
355
373
|
<Text>State: {state}</Text>
|
|
356
374
|
<Text>Progress: {position} / {totalDuration}</Text>
|
|
375
|
+
{/* Or use useNowPlaying for all state at once */}
|
|
376
|
+
<Text>Now Playing State: {nowPlaying.currentState}</Text>
|
|
357
377
|
</View>
|
|
358
378
|
)
|
|
359
379
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { TrackItem, TrackPlayerState, Reason } from '../types/PlayerQueue';
|
|
2
|
+
type PlaybackStateCallback = (state: TrackPlayerState, reason?: Reason) => void;
|
|
3
|
+
type TrackChangeCallback = (track: TrackItem, reason?: Reason) => void;
|
|
4
|
+
/**
|
|
5
|
+
* Internal subscription manager that allows multiple hooks to subscribe
|
|
6
|
+
* to a single native callback. This solves the problem where registering
|
|
7
|
+
* a new callback overwrites the previous one.
|
|
8
|
+
*/
|
|
9
|
+
declare class CallbackSubscriptionManager {
|
|
10
|
+
private playbackStateSubscribers;
|
|
11
|
+
private trackChangeSubscribers;
|
|
12
|
+
private isPlaybackStateRegistered;
|
|
13
|
+
private isTrackChangeRegistered;
|
|
14
|
+
/**
|
|
15
|
+
* Subscribe to playback state changes
|
|
16
|
+
* @returns Unsubscribe function
|
|
17
|
+
*/
|
|
18
|
+
subscribeToPlaybackState(callback: PlaybackStateCallback): () => void;
|
|
19
|
+
/**
|
|
20
|
+
* Subscribe to track changes
|
|
21
|
+
* @returns Unsubscribe function
|
|
22
|
+
*/
|
|
23
|
+
subscribeToTrackChange(callback: TrackChangeCallback): () => void;
|
|
24
|
+
private ensurePlaybackStateRegistered;
|
|
25
|
+
private ensureTrackChangeRegistered;
|
|
26
|
+
}
|
|
27
|
+
export declare const callbackManager: CallbackSubscriptionManager;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { TrackPlayer } from '../index';
|
|
2
|
+
/**
|
|
3
|
+
* Internal subscription manager that allows multiple hooks to subscribe
|
|
4
|
+
* to a single native callback. This solves the problem where registering
|
|
5
|
+
* a new callback overwrites the previous one.
|
|
6
|
+
*/
|
|
7
|
+
class CallbackSubscriptionManager {
|
|
8
|
+
playbackStateSubscribers = new Set();
|
|
9
|
+
trackChangeSubscribers = new Set();
|
|
10
|
+
isPlaybackStateRegistered = false;
|
|
11
|
+
isTrackChangeRegistered = false;
|
|
12
|
+
/**
|
|
13
|
+
* Subscribe to playback state changes
|
|
14
|
+
* @returns Unsubscribe function
|
|
15
|
+
*/
|
|
16
|
+
subscribeToPlaybackState(callback) {
|
|
17
|
+
this.playbackStateSubscribers.add(callback);
|
|
18
|
+
this.ensurePlaybackStateRegistered();
|
|
19
|
+
return () => {
|
|
20
|
+
this.playbackStateSubscribers.delete(callback);
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Subscribe to track changes
|
|
25
|
+
* @returns Unsubscribe function
|
|
26
|
+
*/
|
|
27
|
+
subscribeToTrackChange(callback) {
|
|
28
|
+
this.trackChangeSubscribers.add(callback);
|
|
29
|
+
this.ensureTrackChangeRegistered();
|
|
30
|
+
return () => {
|
|
31
|
+
this.trackChangeSubscribers.delete(callback);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
ensurePlaybackStateRegistered() {
|
|
35
|
+
if (this.isPlaybackStateRegistered)
|
|
36
|
+
return;
|
|
37
|
+
try {
|
|
38
|
+
TrackPlayer.onPlaybackStateChange((state, reason) => {
|
|
39
|
+
this.playbackStateSubscribers.forEach((subscriber) => {
|
|
40
|
+
try {
|
|
41
|
+
subscriber(state, reason);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
console.error('[CallbackManager] Error in playback state subscriber:', error);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
this.isPlaybackStateRegistered = true;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
console.error('[CallbackManager] Failed to register playback state callback:', error);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
ensureTrackChangeRegistered() {
|
|
55
|
+
if (this.isTrackChangeRegistered)
|
|
56
|
+
return;
|
|
57
|
+
try {
|
|
58
|
+
TrackPlayer.onChangeTrack((track, reason) => {
|
|
59
|
+
this.trackChangeSubscribers.forEach((subscriber) => {
|
|
60
|
+
try {
|
|
61
|
+
subscriber(track, reason);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.error('[CallbackManager] Error in track change subscriber:', error);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
this.isTrackChangeRegistered = true;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
console.error('[CallbackManager] Failed to register track change callback:', error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Export singleton instance
|
|
76
|
+
export const callbackManager = new CallbackSubscriptionManager();
|
package/lib/hooks/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export { useOnChangeTrack } from './useOnChangeTrack';
|
|
2
|
+
export type { TrackChangeResult } from './useOnChangeTrack';
|
|
2
3
|
export { useOnPlaybackStateChange } from './useOnPlaybackStateChange';
|
|
4
|
+
export type { PlaybackStateResult } from './useOnPlaybackStateChange';
|
|
3
5
|
export { useOnSeek } from './useOnSeek';
|
|
4
6
|
export { useOnPlaybackProgressChange } from './useOnPlaybackProgressChange';
|
|
5
7
|
export { useAndroidAutoConnection } from './useAndroidAutoConnection';
|
|
6
8
|
export { useAudioDevices } from './useAudioDevices';
|
|
9
|
+
export { useNowPlaying } from './useNowPlaying';
|
package/lib/hooks/index.js
CHANGED
|
@@ -4,3 +4,4 @@ export { useOnSeek } from './useOnSeek';
|
|
|
4
4
|
export { useOnPlaybackProgressChange } from './useOnPlaybackProgressChange';
|
|
5
5
|
export { useAndroidAutoConnection } from './useAndroidAutoConnection';
|
|
6
6
|
export { useAudioDevices } from './useAudioDevices';
|
|
7
|
+
export { useNowPlaying } from './useNowPlaying';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { PlayerState } from '../types/PlayerQueue';
|
|
2
|
+
/**
|
|
3
|
+
* Hook to get the current player state (same as TrackPlayer.getState())
|
|
4
|
+
*
|
|
5
|
+
* This hook provides all player state information including:
|
|
6
|
+
* - Current track
|
|
7
|
+
* - Current position and duration
|
|
8
|
+
* - Playback state (playing, paused, stopped)
|
|
9
|
+
* - Current playlist ID
|
|
10
|
+
* - Current track index
|
|
11
|
+
*
|
|
12
|
+
* The hook uses native callbacks for immediate updates when state changes.
|
|
13
|
+
* Multiple components can use this hook simultaneously.
|
|
14
|
+
*
|
|
15
|
+
* @returns PlayerState object with all current player information
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* function PlayerComponent() {
|
|
20
|
+
* const state = useNowPlaying()
|
|
21
|
+
*
|
|
22
|
+
* return (
|
|
23
|
+
* <View>
|
|
24
|
+
* {state.currentTrack && (
|
|
25
|
+
* <Text>Now Playing: {state.currentTrack.title}</Text>
|
|
26
|
+
* )}
|
|
27
|
+
* <Text>Position: {state.currentPosition} / {state.totalDuration}</Text>
|
|
28
|
+
* <Text>State: {state.currentState}</Text>
|
|
29
|
+
* <Text>Playlist: {state.currentPlaylistId || 'None'}</Text>
|
|
30
|
+
* <Text>Index: {state.currentIndex}</Text>
|
|
31
|
+
* </View>
|
|
32
|
+
* )
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare function useNowPlaying(): PlayerState;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { useEffect, useState, useRef, useCallback } from 'react';
|
|
2
|
+
import { TrackPlayer } from '../index';
|
|
3
|
+
import { callbackManager } from './callbackManager';
|
|
4
|
+
const DEFAULT_STATE = {
|
|
5
|
+
currentTrack: null,
|
|
6
|
+
currentPosition: 0,
|
|
7
|
+
totalDuration: 0,
|
|
8
|
+
currentState: 'stopped',
|
|
9
|
+
currentPlaylistId: null,
|
|
10
|
+
currentIndex: -1,
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Hook to get the current player state (same as TrackPlayer.getState())
|
|
14
|
+
*
|
|
15
|
+
* This hook provides all player state information including:
|
|
16
|
+
* - Current track
|
|
17
|
+
* - Current position and duration
|
|
18
|
+
* - Playback state (playing, paused, stopped)
|
|
19
|
+
* - Current playlist ID
|
|
20
|
+
* - Current track index
|
|
21
|
+
*
|
|
22
|
+
* The hook uses native callbacks for immediate updates when state changes.
|
|
23
|
+
* Multiple components can use this hook simultaneously.
|
|
24
|
+
*
|
|
25
|
+
* @returns PlayerState object with all current player information
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```tsx
|
|
29
|
+
* function PlayerComponent() {
|
|
30
|
+
* const state = useNowPlaying()
|
|
31
|
+
*
|
|
32
|
+
* return (
|
|
33
|
+
* <View>
|
|
34
|
+
* {state.currentTrack && (
|
|
35
|
+
* <Text>Now Playing: {state.currentTrack.title}</Text>
|
|
36
|
+
* )}
|
|
37
|
+
* <Text>Position: {state.currentPosition} / {state.totalDuration}</Text>
|
|
38
|
+
* <Text>State: {state.currentState}</Text>
|
|
39
|
+
* <Text>Playlist: {state.currentPlaylistId || 'None'}</Text>
|
|
40
|
+
* <Text>Index: {state.currentIndex}</Text>
|
|
41
|
+
* </View>
|
|
42
|
+
* )
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function useNowPlaying() {
|
|
47
|
+
const [state, setState] = useState(DEFAULT_STATE);
|
|
48
|
+
const isMounted = useRef(true);
|
|
49
|
+
const updateState = useCallback(() => {
|
|
50
|
+
if (!isMounted.current)
|
|
51
|
+
return;
|
|
52
|
+
try {
|
|
53
|
+
const newState = TrackPlayer.getState();
|
|
54
|
+
setState(newState);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.error('[useNowPlaying] Error updating player state:', error);
|
|
58
|
+
}
|
|
59
|
+
}, []);
|
|
60
|
+
// Initialize with current state
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
isMounted.current = true;
|
|
63
|
+
updateState();
|
|
64
|
+
return () => {
|
|
65
|
+
isMounted.current = false;
|
|
66
|
+
};
|
|
67
|
+
}, [updateState]);
|
|
68
|
+
// Subscribe to track changes
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
const unsubscribe = callbackManager.subscribeToTrackChange(() => {
|
|
71
|
+
updateState();
|
|
72
|
+
});
|
|
73
|
+
return () => {
|
|
74
|
+
unsubscribe();
|
|
75
|
+
};
|
|
76
|
+
}, [updateState]);
|
|
77
|
+
// Subscribe to playback state changes
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const unsubscribe = callbackManager.subscribeToPlaybackState(() => {
|
|
80
|
+
updateState();
|
|
81
|
+
});
|
|
82
|
+
return () => {
|
|
83
|
+
unsubscribe();
|
|
84
|
+
};
|
|
85
|
+
}, [updateState]);
|
|
86
|
+
return state;
|
|
87
|
+
}
|
|
@@ -1,9 +1,36 @@
|
|
|
1
1
|
import type { TrackItem, Reason } from '../types/PlayerQueue';
|
|
2
|
+
export interface TrackChangeResult {
|
|
3
|
+
track: TrackItem | null;
|
|
4
|
+
reason: Reason | undefined;
|
|
5
|
+
isReady: boolean;
|
|
6
|
+
}
|
|
2
7
|
/**
|
|
3
|
-
* Hook to
|
|
4
|
-
*
|
|
8
|
+
* Hook to subscribe to track changes.
|
|
9
|
+
*
|
|
10
|
+
* This hook provides real-time track change updates using native callbacks.
|
|
11
|
+
* Multiple components can use this hook simultaneously without interfering
|
|
12
|
+
* with each other.
|
|
13
|
+
*
|
|
14
|
+
* @returns Object with:
|
|
15
|
+
* - track: Current track or null if no track is playing
|
|
16
|
+
* - reason: Reason for the last track change
|
|
17
|
+
* - isReady: Whether the initial state has been loaded
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* function NowPlaying() {
|
|
22
|
+
* const { track, reason, isReady } = useOnChangeTrack()
|
|
23
|
+
*
|
|
24
|
+
* if (!isReady) return <Text>Loading...</Text>
|
|
25
|
+
* if (!track) return <Text>No track playing</Text>
|
|
26
|
+
*
|
|
27
|
+
* return (
|
|
28
|
+
* <View>
|
|
29
|
+
* <Text>{track.title}</Text>
|
|
30
|
+
* <Text>{track.artist}</Text>
|
|
31
|
+
* </View>
|
|
32
|
+
* )
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
5
35
|
*/
|
|
6
|
-
export declare function useOnChangeTrack():
|
|
7
|
-
track: TrackItem | undefined;
|
|
8
|
-
reason: Reason | undefined;
|
|
9
|
-
};
|
|
36
|
+
export declare function useOnChangeTrack(): TrackChangeResult;
|
|
@@ -1,17 +1,73 @@
|
|
|
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
|
/**
|
|
4
|
-
* Hook to
|
|
5
|
-
*
|
|
5
|
+
* Hook to subscribe to track changes.
|
|
6
|
+
*
|
|
7
|
+
* This hook provides real-time track change updates using native callbacks.
|
|
8
|
+
* Multiple components can use this hook simultaneously without interfering
|
|
9
|
+
* with each other.
|
|
10
|
+
*
|
|
11
|
+
* @returns Object with:
|
|
12
|
+
* - track: Current track or null if no track is playing
|
|
13
|
+
* - reason: Reason for the last track change
|
|
14
|
+
* - isReady: Whether the initial state has been loaded
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* function NowPlaying() {
|
|
19
|
+
* const { track, reason, isReady } = useOnChangeTrack()
|
|
20
|
+
*
|
|
21
|
+
* if (!isReady) return <Text>Loading...</Text>
|
|
22
|
+
* if (!track) return <Text>No track playing</Text>
|
|
23
|
+
*
|
|
24
|
+
* return (
|
|
25
|
+
* <View>
|
|
26
|
+
* <Text>{track.title}</Text>
|
|
27
|
+
* <Text>{track.artist}</Text>
|
|
28
|
+
* </View>
|
|
29
|
+
* )
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
6
32
|
*/
|
|
7
33
|
export function useOnChangeTrack() {
|
|
8
|
-
const [track, setTrack] = useState(
|
|
34
|
+
const [track, setTrack] = useState(null);
|
|
9
35
|
const [reason, setReason] = useState(undefined);
|
|
36
|
+
const [isReady, setIsReady] = useState(false);
|
|
37
|
+
const isMounted = useRef(true);
|
|
38
|
+
// Initialize with current track from the player
|
|
10
39
|
useEffect(() => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
40
|
+
isMounted.current = true;
|
|
41
|
+
try {
|
|
42
|
+
const playerState = TrackPlayer.getState();
|
|
43
|
+
if (isMounted.current) {
|
|
44
|
+
setTrack(playerState.currentTrack);
|
|
45
|
+
setIsReady(true);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.error('[useOnChangeTrack] Failed to get initial state:', error);
|
|
50
|
+
if (isMounted.current) {
|
|
51
|
+
setTrack(null);
|
|
52
|
+
setIsReady(true);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return () => {
|
|
56
|
+
isMounted.current = false;
|
|
57
|
+
};
|
|
15
58
|
}, []);
|
|
16
|
-
|
|
59
|
+
// Subscribe to track changes
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const handleTrackChange = (newTrack, newReason) => {
|
|
62
|
+
if (isMounted.current) {
|
|
63
|
+
setTrack(newTrack);
|
|
64
|
+
setReason(newReason);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const unsubscribe = callbackManager.subscribeToTrackChange(handleTrackChange);
|
|
68
|
+
return () => {
|
|
69
|
+
unsubscribe();
|
|
70
|
+
};
|
|
71
|
+
}, []);
|
|
72
|
+
return { track, reason, isReady };
|
|
17
73
|
}
|
|
@@ -1,9 +1,35 @@
|
|
|
1
1
|
import type { TrackPlayerState, Reason } from '../types/PlayerQueue';
|
|
2
|
+
export interface PlaybackStateResult {
|
|
3
|
+
state: TrackPlayerState;
|
|
4
|
+
reason: Reason | undefined;
|
|
5
|
+
isReady: boolean;
|
|
6
|
+
}
|
|
2
7
|
/**
|
|
3
|
-
* Hook to
|
|
4
|
-
*
|
|
8
|
+
* Hook to subscribe to playback state changes.
|
|
9
|
+
*
|
|
10
|
+
* This hook provides real-time playback state updates using native callbacks.
|
|
11
|
+
* Multiple components can use this hook simultaneously without interfering
|
|
12
|
+
* with each other.
|
|
13
|
+
*
|
|
14
|
+
* @returns Object with:
|
|
15
|
+
* - state: Current playback state ('playing' | 'paused' | 'stopped')
|
|
16
|
+
* - reason: Reason for the last state change
|
|
17
|
+
* - isReady: Whether the initial state has been loaded
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* function PlaybackIndicator() {
|
|
22
|
+
* const { state, reason, isReady } = useOnPlaybackStateChange()
|
|
23
|
+
*
|
|
24
|
+
* if (!isReady) return <Text>Loading...</Text>
|
|
25
|
+
*
|
|
26
|
+
* return (
|
|
27
|
+
* <View>
|
|
28
|
+
* <Text>State: {state}</Text>
|
|
29
|
+
* {reason && <Text>Reason: {reason}</Text>}
|
|
30
|
+
* </View>
|
|
31
|
+
* )
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
5
34
|
*/
|
|
6
|
-
export declare function useOnPlaybackStateChange():
|
|
7
|
-
state: TrackPlayerState | undefined;
|
|
8
|
-
reason: Reason | undefined;
|
|
9
|
-
};
|
|
35
|
+
export declare function useOnPlaybackStateChange(): PlaybackStateResult;
|
|
@@ -1,17 +1,73 @@
|
|
|
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
|
/**
|
|
4
|
-
* Hook to
|
|
5
|
-
*
|
|
5
|
+
* Hook to subscribe to playback state changes.
|
|
6
|
+
*
|
|
7
|
+
* This hook provides real-time playback state updates using native callbacks.
|
|
8
|
+
* Multiple components can use this hook simultaneously without interfering
|
|
9
|
+
* with each other.
|
|
10
|
+
*
|
|
11
|
+
* @returns Object with:
|
|
12
|
+
* - state: Current playback state ('playing' | 'paused' | 'stopped')
|
|
13
|
+
* - reason: Reason for the last state change
|
|
14
|
+
* - isReady: Whether the initial state has been loaded
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* function PlaybackIndicator() {
|
|
19
|
+
* const { state, reason, isReady } = useOnPlaybackStateChange()
|
|
20
|
+
*
|
|
21
|
+
* if (!isReady) return <Text>Loading...</Text>
|
|
22
|
+
*
|
|
23
|
+
* return (
|
|
24
|
+
* <View>
|
|
25
|
+
* <Text>State: {state}</Text>
|
|
26
|
+
* {reason && <Text>Reason: {reason}</Text>}
|
|
27
|
+
* </View>
|
|
28
|
+
* )
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
6
31
|
*/
|
|
7
32
|
export function useOnPlaybackStateChange() {
|
|
8
|
-
const [state, setState] = useState(
|
|
33
|
+
const [state, setState] = useState('stopped');
|
|
9
34
|
const [reason, setReason] = useState(undefined);
|
|
35
|
+
const [isReady, setIsReady] = useState(false);
|
|
36
|
+
const isMounted = useRef(true);
|
|
37
|
+
// Initialize with current state from the player
|
|
10
38
|
useEffect(() => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
39
|
+
isMounted.current = true;
|
|
40
|
+
// Get initial state synchronously
|
|
41
|
+
try {
|
|
42
|
+
const playerState = TrackPlayer.getState();
|
|
43
|
+
if (isMounted.current) {
|
|
44
|
+
setState(playerState.currentState);
|
|
45
|
+
setIsReady(true);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.error('[useOnPlaybackStateChange] Failed to get initial state:', error);
|
|
50
|
+
if (isMounted.current) {
|
|
51
|
+
setState('stopped');
|
|
52
|
+
setIsReady(true);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return () => {
|
|
56
|
+
isMounted.current = false;
|
|
57
|
+
};
|
|
15
58
|
}, []);
|
|
16
|
-
|
|
59
|
+
// Subscribe to playback state changes
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const handleStateChange = (newState, newReason) => {
|
|
62
|
+
if (isMounted.current) {
|
|
63
|
+
setState(newState);
|
|
64
|
+
setReason(newReason);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const unsubscribe = callbackManager.subscribeToPlaybackState(handleStateChange);
|
|
68
|
+
return () => {
|
|
69
|
+
unsubscribe();
|
|
70
|
+
};
|
|
71
|
+
}, []);
|
|
72
|
+
return { state, reason, isReady };
|
|
17
73
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { TrackPlayer } from '../index'
|
|
2
|
+
import type { TrackItem, TrackPlayerState, Reason } from '../types/PlayerQueue'
|
|
3
|
+
|
|
4
|
+
type PlaybackStateCallback = (state: TrackPlayerState, reason?: Reason) => void
|
|
5
|
+
type TrackChangeCallback = (track: TrackItem, reason?: Reason) => void
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Internal subscription manager that allows multiple hooks to subscribe
|
|
9
|
+
* to a single native callback. This solves the problem where registering
|
|
10
|
+
* a new callback overwrites the previous one.
|
|
11
|
+
*/
|
|
12
|
+
class CallbackSubscriptionManager {
|
|
13
|
+
private playbackStateSubscribers = new Set<PlaybackStateCallback>()
|
|
14
|
+
private trackChangeSubscribers = new Set<TrackChangeCallback>()
|
|
15
|
+
private isPlaybackStateRegistered = false
|
|
16
|
+
private isTrackChangeRegistered = false
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Subscribe to playback state changes
|
|
20
|
+
* @returns Unsubscribe function
|
|
21
|
+
*/
|
|
22
|
+
subscribeToPlaybackState(callback: PlaybackStateCallback): () => void {
|
|
23
|
+
this.playbackStateSubscribers.add(callback)
|
|
24
|
+
this.ensurePlaybackStateRegistered()
|
|
25
|
+
|
|
26
|
+
return () => {
|
|
27
|
+
this.playbackStateSubscribers.delete(callback)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Subscribe to track changes
|
|
33
|
+
* @returns Unsubscribe function
|
|
34
|
+
*/
|
|
35
|
+
subscribeToTrackChange(callback: TrackChangeCallback): () => void {
|
|
36
|
+
this.trackChangeSubscribers.add(callback)
|
|
37
|
+
this.ensureTrackChangeRegistered()
|
|
38
|
+
|
|
39
|
+
return () => {
|
|
40
|
+
this.trackChangeSubscribers.delete(callback)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private ensurePlaybackStateRegistered(): void {
|
|
45
|
+
if (this.isPlaybackStateRegistered) return
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
TrackPlayer.onPlaybackStateChange((state, reason) => {
|
|
49
|
+
this.playbackStateSubscribers.forEach((subscriber) => {
|
|
50
|
+
try {
|
|
51
|
+
subscriber(state, reason)
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error(
|
|
54
|
+
'[CallbackManager] Error in playback state subscriber:',
|
|
55
|
+
error
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
this.isPlaybackStateRegistered = true
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error(
|
|
63
|
+
'[CallbackManager] Failed to register playback state callback:',
|
|
64
|
+
error
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private ensureTrackChangeRegistered(): void {
|
|
70
|
+
if (this.isTrackChangeRegistered) return
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
TrackPlayer.onChangeTrack((track, reason) => {
|
|
74
|
+
this.trackChangeSubscribers.forEach((subscriber) => {
|
|
75
|
+
try {
|
|
76
|
+
subscriber(track, reason)
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error(
|
|
79
|
+
'[CallbackManager] Error in track change subscriber:',
|
|
80
|
+
error
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
this.isTrackChangeRegistered = true
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error(
|
|
88
|
+
'[CallbackManager] Failed to register track change callback:',
|
|
89
|
+
error
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Export singleton instance
|
|
96
|
+
export const callbackManager = new CallbackSubscriptionManager()
|
package/src/hooks/index.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export { useOnChangeTrack } from './useOnChangeTrack'
|
|
2
|
+
export type { TrackChangeResult } from './useOnChangeTrack'
|
|
2
3
|
export { useOnPlaybackStateChange } from './useOnPlaybackStateChange'
|
|
4
|
+
export type { PlaybackStateResult } from './useOnPlaybackStateChange'
|
|
3
5
|
export { useOnSeek } from './useOnSeek'
|
|
4
6
|
export { useOnPlaybackProgressChange } from './useOnPlaybackProgressChange'
|
|
5
7
|
export { useAndroidAutoConnection } from './useAndroidAutoConnection'
|
|
6
8
|
export { useAudioDevices } from './useAudioDevices'
|
|
9
|
+
export { useNowPlaying } from './useNowPlaying'
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useEffect, useState, useRef, useCallback } from 'react'
|
|
2
|
+
import { TrackPlayer } from '../index'
|
|
3
|
+
import { callbackManager } from './callbackManager'
|
|
4
|
+
import type { PlayerState } from '../types/PlayerQueue'
|
|
5
|
+
|
|
6
|
+
const DEFAULT_STATE: PlayerState = {
|
|
7
|
+
currentTrack: null,
|
|
8
|
+
currentPosition: 0,
|
|
9
|
+
totalDuration: 0,
|
|
10
|
+
currentState: 'stopped',
|
|
11
|
+
currentPlaylistId: null,
|
|
12
|
+
currentIndex: -1,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Hook to get the current player state (same as TrackPlayer.getState())
|
|
17
|
+
*
|
|
18
|
+
* This hook provides all player state information including:
|
|
19
|
+
* - Current track
|
|
20
|
+
* - Current position and duration
|
|
21
|
+
* - Playback state (playing, paused, stopped)
|
|
22
|
+
* - Current playlist ID
|
|
23
|
+
* - Current track index
|
|
24
|
+
*
|
|
25
|
+
* The hook uses native callbacks for immediate updates when state changes.
|
|
26
|
+
* Multiple components can use this hook simultaneously.
|
|
27
|
+
*
|
|
28
|
+
* @returns PlayerState object with all current player information
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```tsx
|
|
32
|
+
* function PlayerComponent() {
|
|
33
|
+
* const state = useNowPlaying()
|
|
34
|
+
*
|
|
35
|
+
* return (
|
|
36
|
+
* <View>
|
|
37
|
+
* {state.currentTrack && (
|
|
38
|
+
* <Text>Now Playing: {state.currentTrack.title}</Text>
|
|
39
|
+
* )}
|
|
40
|
+
* <Text>Position: {state.currentPosition} / {state.totalDuration}</Text>
|
|
41
|
+
* <Text>State: {state.currentState}</Text>
|
|
42
|
+
* <Text>Playlist: {state.currentPlaylistId || 'None'}</Text>
|
|
43
|
+
* <Text>Index: {state.currentIndex}</Text>
|
|
44
|
+
* </View>
|
|
45
|
+
* )
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function useNowPlaying(): PlayerState {
|
|
50
|
+
const [state, setState] = useState<PlayerState>(DEFAULT_STATE)
|
|
51
|
+
const isMounted = useRef(true)
|
|
52
|
+
|
|
53
|
+
const updateState = useCallback(() => {
|
|
54
|
+
if (!isMounted.current) return
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const newState = TrackPlayer.getState()
|
|
58
|
+
setState(newState)
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('[useNowPlaying] Error updating player state:', error)
|
|
61
|
+
}
|
|
62
|
+
}, [])
|
|
63
|
+
|
|
64
|
+
// Initialize with current state
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
isMounted.current = true
|
|
67
|
+
updateState()
|
|
68
|
+
|
|
69
|
+
return () => {
|
|
70
|
+
isMounted.current = false
|
|
71
|
+
}
|
|
72
|
+
}, [updateState])
|
|
73
|
+
|
|
74
|
+
// Subscribe to track changes
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
const unsubscribe = callbackManager.subscribeToTrackChange(() => {
|
|
77
|
+
updateState()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
return () => {
|
|
81
|
+
unsubscribe()
|
|
82
|
+
}
|
|
83
|
+
}, [updateState])
|
|
84
|
+
|
|
85
|
+
// Subscribe to playback state changes
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
const unsubscribe = callbackManager.subscribeToPlaybackState(() => {
|
|
88
|
+
updateState()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
return () => {
|
|
92
|
+
unsubscribe()
|
|
93
|
+
}
|
|
94
|
+
}, [updateState])
|
|
95
|
+
|
|
96
|
+
return state
|
|
97
|
+
}
|
|
@@ -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
|
}
|