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.
Files changed (47) hide show
  1. package/README.md +282 -2
  2. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +37 -29
  3. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +24 -0
  4. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +408 -16
  5. package/ios/HybridAudioRoutePicker.swift +47 -46
  6. package/ios/HybridTrackPlayer.swift +22 -0
  7. package/ios/core/TrackPlayerCore.swift +538 -48
  8. package/lib/hooks/callbackManager.d.ts +28 -0
  9. package/lib/hooks/callbackManager.js +76 -0
  10. package/lib/hooks/index.d.ts +7 -0
  11. package/lib/hooks/index.js +3 -0
  12. package/lib/hooks/useActualQueue.d.ts +48 -0
  13. package/lib/hooks/useActualQueue.js +98 -0
  14. package/lib/hooks/useNowPlaying.d.ts +36 -0
  15. package/lib/hooks/useNowPlaying.js +87 -0
  16. package/lib/hooks/useOnChangeTrack.d.ts +33 -6
  17. package/lib/hooks/useOnChangeTrack.js +65 -9
  18. package/lib/hooks/useOnPlaybackStateChange.d.ts +32 -6
  19. package/lib/hooks/useOnPlaybackStateChange.js +65 -9
  20. package/lib/hooks/usePlaylist.d.ts +48 -0
  21. package/lib/hooks/usePlaylist.js +136 -0
  22. package/lib/index.d.ts +1 -0
  23. package/lib/specs/TrackPlayer.nitro.d.ts +6 -0
  24. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +46 -9
  25. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +5 -0
  26. package/nitrogen/generated/android/c++/JRepeatMode.hpp +62 -0
  27. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +20 -0
  28. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/RepeatMode.kt +22 -0
  29. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +9 -0
  30. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +3 -0
  31. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +44 -4
  32. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +5 -0
  33. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +64 -0
  34. package/nitrogen/generated/ios/swift/RepeatMode.swift +44 -0
  35. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +5 -0
  36. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +12 -3
  37. package/nitrogen/generated/shared/c++/RepeatMode.hpp +80 -0
  38. package/package.json +13 -12
  39. package/src/hooks/callbackManager.ts +96 -0
  40. package/src/hooks/index.ts +7 -0
  41. package/src/hooks/useActualQueue.ts +116 -0
  42. package/src/hooks/useNowPlaying.ts +97 -0
  43. package/src/hooks/useOnChangeTrack.ts +77 -13
  44. package/src/hooks/useOnPlaybackStateChange.ts +83 -13
  45. package/src/hooks/usePlaylist.ts +161 -0
  46. package/src/index.ts +1 -1
  47. package/src/specs/TrackPlayer.nitro.ts +7 -0
@@ -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,13 @@
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';
10
+ export { usePlaylist } from './usePlaylist';
11
+ export type { UsePlaylistResult } from './usePlaylist';
12
+ export { useActualQueue } from './useActualQueue';
13
+ export type { UseActualQueueResult } from './useActualQueue';
@@ -4,3 +4,6 @@ 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';
8
+ export { usePlaylist } from './usePlaylist';
9
+ export { useActualQueue } from './useActualQueue';
@@ -0,0 +1,48 @@
1
+ import type { TrackItem } from '../types/PlayerQueue';
2
+ export interface UseActualQueueResult {
3
+ /** The current queue in playback order */
4
+ queue: TrackItem[];
5
+ /** Manually refresh the queue */
6
+ refreshQueue: () => void;
7
+ /** Whether the queue is currently loading */
8
+ isLoading: boolean;
9
+ }
10
+ /**
11
+ * Hook to get the actual playback queue including temporary tracks
12
+ *
13
+ * Returns the complete queue in playback order:
14
+ * [tracks_before_current] + [current] + [playNext_stack] + [upNext_queue] + [remaining_tracks]
15
+ *
16
+ * Auto-updates when:
17
+ * - Track changes
18
+ * - Playback state changes
19
+ *
20
+ * Call `refreshQueue()` after adding tracks via `playNext()` or `addToUpNext()`
21
+ * to immediately see the updated queue.
22
+ *
23
+ * @returns Object containing queue array, refresh function, and loading state
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * function QueueView() {
28
+ * const { queue, refreshQueue, isLoading } = useActualQueue();
29
+ *
30
+ * const handleAddToUpNext = (trackId: string) => {
31
+ * TrackPlayer.addToUpNext(trackId);
32
+ * // Refresh queue after adding track
33
+ * setTimeout(refreshQueue, 100);
34
+ * };
35
+ *
36
+ * return (
37
+ * <ScrollView>
38
+ * {queue.map((track, index) => (
39
+ * <Text key={track.id}>
40
+ * {index + 1}. {track.title}
41
+ * </Text>
42
+ * ))}
43
+ * </ScrollView>
44
+ * );
45
+ * }
46
+ * ```
47
+ */
48
+ export declare function useActualQueue(): UseActualQueueResult;
@@ -0,0 +1,98 @@
1
+ import { useEffect, useState, useRef, useCallback } from 'react';
2
+ import { TrackPlayer } from '../index';
3
+ import { callbackManager } from './callbackManager';
4
+ /**
5
+ * Hook to get the actual playback queue including temporary tracks
6
+ *
7
+ * Returns the complete queue in playback order:
8
+ * [tracks_before_current] + [current] + [playNext_stack] + [upNext_queue] + [remaining_tracks]
9
+ *
10
+ * Auto-updates when:
11
+ * - Track changes
12
+ * - Playback state changes
13
+ *
14
+ * Call `refreshQueue()` after adding tracks via `playNext()` or `addToUpNext()`
15
+ * to immediately see the updated queue.
16
+ *
17
+ * @returns Object containing queue array, refresh function, and loading state
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * function QueueView() {
22
+ * const { queue, refreshQueue, isLoading } = useActualQueue();
23
+ *
24
+ * const handleAddToUpNext = (trackId: string) => {
25
+ * TrackPlayer.addToUpNext(trackId);
26
+ * // Refresh queue after adding track
27
+ * setTimeout(refreshQueue, 100);
28
+ * };
29
+ *
30
+ * return (
31
+ * <ScrollView>
32
+ * {queue.map((track, index) => (
33
+ * <Text key={track.id}>
34
+ * {index + 1}. {track.title}
35
+ * </Text>
36
+ * ))}
37
+ * </ScrollView>
38
+ * );
39
+ * }
40
+ * ```
41
+ */
42
+ export function useActualQueue() {
43
+ const [queue, setQueue] = useState([]);
44
+ const [isLoading, setIsLoading] = useState(true);
45
+ const isMounted = useRef(true);
46
+ const updateQueue = useCallback(() => {
47
+ if (!isMounted.current)
48
+ return;
49
+ try {
50
+ const actualQueue = TrackPlayer.getActualQueue();
51
+ if (isMounted.current) {
52
+ setQueue(actualQueue);
53
+ setIsLoading(false);
54
+ }
55
+ }
56
+ catch (error) {
57
+ console.error('[useActualQueue] Error getting queue:', error);
58
+ if (isMounted.current) {
59
+ setQueue([]);
60
+ setIsLoading(false);
61
+ }
62
+ }
63
+ }, []);
64
+ const refreshQueue = useCallback(() => {
65
+ if (!isMounted.current)
66
+ return;
67
+ setIsLoading(true);
68
+ updateQueue();
69
+ }, [updateQueue]);
70
+ // Initialize queue
71
+ useEffect(() => {
72
+ isMounted.current = true;
73
+ updateQueue();
74
+ return () => {
75
+ isMounted.current = false;
76
+ };
77
+ }, [updateQueue]);
78
+ // Update queue on track changes (with slight delay to ensure native side has updated)
79
+ useEffect(() => {
80
+ const unsubscribe = callbackManager.subscribeToTrackChange(() => {
81
+ // Small delay to ensure native queue is updated
82
+ setTimeout(updateQueue, 50);
83
+ });
84
+ return () => {
85
+ unsubscribe();
86
+ };
87
+ }, [updateQueue]);
88
+ // Update queue on playback state changes
89
+ useEffect(() => {
90
+ const unsubscribe = callbackManager.subscribeToPlaybackState(() => {
91
+ updateQueue();
92
+ });
93
+ return () => {
94
+ unsubscribe();
95
+ };
96
+ }, [updateQueue]);
97
+ return { queue, refreshQueue, isLoading };
98
+ }
@@ -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
  }