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 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();
@@ -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';
@@ -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 get the current track and track change reason
4
- * @returns Object with current track and reason, or undefined if no track is playing
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 get the current track and track change reason
5
- * @returns Object with current track and reason, or undefined if no track is playing
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(undefined);
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
- TrackPlayer.onChangeTrack((newTrack, newReason) => {
12
- setTrack(newTrack);
13
- setReason(newReason);
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
- return { track, reason };
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 get the current playback state and reason
4
- * @returns Object with current playback state and reason
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 get the current playback state and reason
5
- * @returns Object with current playback state and reason
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(undefined);
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
- TrackPlayer.onPlaybackStateChange((newState, newReason) => {
12
- setState(newState);
13
- setReason(newReason);
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
- return { state, reason };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-player",
3
- "version": "0.3.0-alpha.7",
3
+ "version": "0.3.0-alpha.9",
4
4
  "description": "react-native-nitro-player",
5
5
  "main": "lib/index",
6
6
  "module": "lib/index",
@@ -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()
@@ -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 get the current track and track change reason
7
- * @returns Object with current track and reason, or undefined if no track is playing
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: TrackItem | undefined
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
- TrackPlayer.onChangeTrack((newTrack, newReason) => {
18
- setTrack(newTrack)
19
- setReason(newReason)
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 get the current playback state and reason
7
- * @returns Object with current playback state and reason
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: TrackPlayerState | undefined
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
- TrackPlayer.onPlaybackStateChange((newState, newReason) => {
18
- setState(newState)
19
- setReason(newReason)
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
  }