react-native-mp3-player 1.0.11 → 1.1.1
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 +32 -1
- package/ios/SwiftAudioEx/Sources/SwiftAudioEx/AudioTap.swift +2 -3
- package/lib/src/hooks/index.d.ts +2 -0
- package/lib/src/hooks/index.js +2 -0
- package/lib/src/hooks/useMiniPlayer.d.ts +34 -0
- package/lib/src/hooks/useMiniPlayer.js +66 -0
- package/lib/src/hooks/useSetupPlayer.d.ts +21 -0
- package/lib/src/hooks/useSetupPlayer.js +50 -0
- package/package.json +1 -1
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useMiniPlayer.ts +98 -0
- package/src/hooks/useSetupPlayer.ts +69 -0
package/README.md
CHANGED
|
@@ -82,10 +82,41 @@ TrackPlayer.registerPlaybackService(() => PlaybackService);
|
|
|
82
82
|
- **Playback:** `play()`, `pause()`, `stop()`, `seekTo()`, `seekBy()`, `setVolume()`, `setRate()`, `setRepeatMode()`
|
|
83
83
|
- **State & progress:** **`getPlaybackState()`** (returns `{ state }`; use this, not `getState`), **`getProgress()`** (returns `{ position, duration, buffered }` in seconds), **`getPosition()`** and **`getDuration()`** (convenience wrappers around `getProgress()`), `getVolume()`, `getRate()`
|
|
84
84
|
- **Events:** `addEventListener(event, listener)` – see `Event` enum. Listen for `Event.PlaybackState` so the UI stays in sync when the user taps play/pause.
|
|
85
|
-
- **Hooks:** **`useProgress(updateInterval?, background?)`** (interval in **milliseconds**; e.g. `useProgress(250)` = every 250 ms), `usePlaybackState()`, `useActiveTrack()`, `useIsPlaying()`, `useTrackPlayerEvents()`, etc.
|
|
85
|
+
- **Hooks:** **`useProgress(updateInterval?, background?)`** (interval in **milliseconds**; e.g. `useProgress(250)` = every 250 ms), `usePlaybackState()`, `useActiveTrack()`, `useIsPlaying()`, **`useSetupPlayer()`**, **`useMiniPlayer()`**, `useTrackPlayerEvents()`, etc.
|
|
86
86
|
|
|
87
87
|
**Setup options** (e.g. in `setupPlayer` / `updateOptions`): `iosCategory` (e.g. `'playback'`), `iosCategoryOptions` (e.g. `['allowAirPlay','allowBluetooth','duckOthers']`), `autoHandleInterruptions`, `autoUpdateMetadata`, `waitForBuffer`, `minBuffer` / buffer-related options, `forwardJumpInterval` / `backwardJumpInterval` (seconds, e.g. 15), `progressUpdateEventInterval` (seconds). Types and options are in the package TypeScript definitions.
|
|
88
88
|
|
|
89
|
+
## Global mini player (iOS & Android)
|
|
90
|
+
|
|
91
|
+
The same APIs and hooks work on both iOS and Android, so you can build a single global mini player (e.g. a persistent bar above the tab bar) that works cross-platform.
|
|
92
|
+
|
|
93
|
+
1. **Setup once at app root** with `useSetupPlayer()`. It runs `setupPlayer()` and, on Android, retries if the app was in the background, so you get a single `isPlayerReady` for both platforms.
|
|
94
|
+
2. **In your mini player component**, use `useMiniPlayer()` to get:
|
|
95
|
+
- `hasTrack`, `isPlaying`, `isLoadingAudio`
|
|
96
|
+
- `track`, `trackTitle`, `trackArtist`, `trackArtwork`
|
|
97
|
+
- `togglePlayPause()`, `pause()`, `stop()`
|
|
98
|
+
- `refreshActiveTrack()`, `refreshPlaybackState()` (e.g. after returning from another screen)
|
|
99
|
+
3. **Full-screen and close** are app-level: e.g. navigate to a full-screen player route for `openFullScreen`, and call `pause()` or `stop()` plus your own state/navigation for close.
|
|
100
|
+
|
|
101
|
+
Example:
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
import TrackPlayer, { useSetupPlayer, useMiniPlayer } from 'react-native-mp3-player';
|
|
105
|
+
|
|
106
|
+
// At root (e.g. in a provider):
|
|
107
|
+
const isPlayerReady = useSetupPlayer({ options: {}, serviceFactory: () => PlaybackService });
|
|
108
|
+
|
|
109
|
+
// In your global mini player component (when isPlayerReady):
|
|
110
|
+
const {
|
|
111
|
+
hasTrack, isPlaying, isLoadingAudio,
|
|
112
|
+
trackTitle, trackArtist, trackArtwork,
|
|
113
|
+
togglePlayPause, pause, stop,
|
|
114
|
+
refreshActiveTrack, refreshPlaybackState,
|
|
115
|
+
} = useMiniPlayer();
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Use `getActiveTrack()` and `getPlaybackState()` when you need to sync state after navigation or from a widget; the hooks stay in sync via events.
|
|
119
|
+
|
|
89
120
|
## Example app
|
|
90
121
|
|
|
91
122
|
From the repo root:
|
|
@@ -84,9 +84,8 @@ extension AVPlayerWrapper {
|
|
|
84
84
|
audioTap.process(numberOfFrames: numberFrames, buffer: UnsafeMutableAudioBufferListPointer(bufferListInOut))
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
//
|
|
89
|
-
// Xcode 16+ / 26 SDK: tapOut expects Unmanaged<MTAudioProcessingTap>? (API returns retained CF object).
|
|
87
|
+
// Newer Xcode / SDK versions import MTAudioProcessingTapCreate’s tap out-parameter as
|
|
88
|
+
// UnsafeMutablePointer<Unmanaged<MTAudioProcessingTap>?>; AudioToolbox returns a +1 retained tap.
|
|
90
89
|
var tapRef: Unmanaged<MTAudioProcessingTap>?
|
|
91
90
|
let error = MTAudioProcessingTapCreate(kCFAllocatorDefault, &callbacks, kMTAudioProcessingTapCreationFlag_PreEffects, &tapRef)
|
|
92
91
|
assert(error == noErr)
|
package/lib/src/hooks/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from './useActiveTrack';
|
|
2
2
|
export * from './useIsPlaying';
|
|
3
|
+
export * from './useMiniPlayer';
|
|
3
4
|
export * from './usePlayWhenReady';
|
|
4
5
|
export * from './usePlaybackState';
|
|
5
6
|
export * from './useProgress';
|
|
7
|
+
export * from './useSetupPlayer';
|
|
6
8
|
export * from './useTrackPlayerEvents';
|
package/lib/src/hooks/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from './useActiveTrack';
|
|
2
2
|
export * from './useIsPlaying';
|
|
3
|
+
export * from './useMiniPlayer';
|
|
3
4
|
export * from './usePlayWhenReady';
|
|
4
5
|
export * from './usePlaybackState';
|
|
5
6
|
export * from './useProgress';
|
|
7
|
+
export * from './useSetupPlayer';
|
|
6
8
|
export * from './useTrackPlayerEvents';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Track } from '../interfaces/Track';
|
|
2
|
+
export interface UseMiniPlayerResult {
|
|
3
|
+
/** Whether there is a current track (queue has an active track). */
|
|
4
|
+
hasTrack: boolean;
|
|
5
|
+
/** Whether the player is in a "playing" state (play when ready and not ended/error/none). */
|
|
6
|
+
isPlaying: boolean;
|
|
7
|
+
/** True when state is loading or buffering (e.g. show a spinner). */
|
|
8
|
+
isLoadingAudio: boolean;
|
|
9
|
+
/** Current track or undefined. */
|
|
10
|
+
track: Track | undefined;
|
|
11
|
+
/** Convenience: track?.title ?? ''. */
|
|
12
|
+
trackTitle: string;
|
|
13
|
+
/** Convenience: track?.artist ?? ''. */
|
|
14
|
+
trackArtist: string;
|
|
15
|
+
/** Convenience: track?.artwork (URL string or undefined). */
|
|
16
|
+
trackArtwork: string | undefined;
|
|
17
|
+
/** Toggle between play and pause. Safe to call when loading. */
|
|
18
|
+
togglePlayPause: () => void;
|
|
19
|
+
/** Pause playback. */
|
|
20
|
+
pause: () => void;
|
|
21
|
+
/** Stop and clear current track. */
|
|
22
|
+
stop: () => void;
|
|
23
|
+
/** Re-fetch active track from native and update hook state. Call when you need to sync (e.g. after returning to the app). */
|
|
24
|
+
refreshActiveTrack: () => Promise<void>;
|
|
25
|
+
/** Re-fetch playback state from native. Call when you need to sync. */
|
|
26
|
+
refreshPlaybackState: () => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Aggregates state and actions needed for a global mini player bar (play/pause, title, artist, artwork, close).
|
|
30
|
+
* Works on both iOS and Android; use with useSetupPlayer() at app root so the player is ready.
|
|
31
|
+
*
|
|
32
|
+
* openFullScreen / closeFullScreen are not provided here — implement them in your app (e.g. navigate to a full-screen player route).
|
|
33
|
+
*/
|
|
34
|
+
export declare function useMiniPlayer(): UseMiniPlayerResult;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { play, pause, stop, getActiveTrack, getPlaybackState } from '../trackPlayer';
|
|
3
|
+
import { State } from '../constants';
|
|
4
|
+
import { useActiveTrack } from './useActiveTrack';
|
|
5
|
+
import { usePlaybackState } from './usePlaybackState';
|
|
6
|
+
import { useIsPlaying } from './useIsPlaying';
|
|
7
|
+
/**
|
|
8
|
+
* Aggregates state and actions needed for a global mini player bar (play/pause, title, artist, artwork, close).
|
|
9
|
+
* Works on both iOS and Android; use with useSetupPlayer() at app root so the player is ready.
|
|
10
|
+
*
|
|
11
|
+
* openFullScreen / closeFullScreen are not provided here — implement them in your app (e.g. navigate to a full-screen player route).
|
|
12
|
+
*/
|
|
13
|
+
export function useMiniPlayer() {
|
|
14
|
+
const track = useActiveTrack();
|
|
15
|
+
const playbackState = usePlaybackState();
|
|
16
|
+
const { playing, bufferingDuringPlay } = useIsPlaying();
|
|
17
|
+
const state = playbackState.state;
|
|
18
|
+
const isPlaying = playing ?? false;
|
|
19
|
+
const isLoadingAudio = state === State.Loading || state === State.Buffering || bufferingDuringPlay === true;
|
|
20
|
+
const togglePlayPause = useCallback(async () => {
|
|
21
|
+
if (isLoadingAudio)
|
|
22
|
+
return;
|
|
23
|
+
if (isPlaying) {
|
|
24
|
+
await pause();
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
await play();
|
|
28
|
+
}
|
|
29
|
+
}, [isPlaying, isLoadingAudio]);
|
|
30
|
+
const refreshActiveTrack = useCallback(async () => {
|
|
31
|
+
try {
|
|
32
|
+
const t = await getActiveTrack();
|
|
33
|
+
// Hooks (useActiveTrack) will update via events; this is for one-off sync.
|
|
34
|
+
// We can't set track here; the hook is the source of truth. So we document
|
|
35
|
+
// that refreshActiveTrack is for triggering a re-sync — the app can also
|
|
36
|
+
// rely on Event.PlaybackActiveTrackChanged and Event.PlaybackState.
|
|
37
|
+
void t;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Not set up yet.
|
|
41
|
+
}
|
|
42
|
+
}, []);
|
|
43
|
+
const refreshPlaybackState = useCallback(async () => {
|
|
44
|
+
try {
|
|
45
|
+
await getPlaybackState();
|
|
46
|
+
// Same as above: events drive the hook state; this is for forcing native read.
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Not set up yet.
|
|
50
|
+
}
|
|
51
|
+
}, []);
|
|
52
|
+
return {
|
|
53
|
+
hasTrack: track != null,
|
|
54
|
+
isPlaying,
|
|
55
|
+
isLoadingAudio,
|
|
56
|
+
track,
|
|
57
|
+
trackTitle: track?.title ?? '',
|
|
58
|
+
trackArtist: track?.artist ?? '',
|
|
59
|
+
trackArtwork: track?.artwork,
|
|
60
|
+
togglePlayPause,
|
|
61
|
+
pause: () => pause(),
|
|
62
|
+
stop: () => stop(),
|
|
63
|
+
refreshActiveTrack,
|
|
64
|
+
refreshPlaybackState,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PlayerOptions } from '../interfaces/PlayerOptions';
|
|
2
|
+
import type { ServiceHandler } from '../interfaces/ServiceHandler';
|
|
3
|
+
export interface UseSetupPlayerOptions {
|
|
4
|
+
/** Options passed to setupPlayer(). Omit to use defaults. */
|
|
5
|
+
options?: PlayerOptions;
|
|
6
|
+
/** Whether to set up for background playback. Default false. */
|
|
7
|
+
background?: boolean;
|
|
8
|
+
/** Optional playback service factory. If provided, registerPlaybackService() is called with it. */
|
|
9
|
+
serviceFactory?: () => ServiceHandler;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Sets up the player once and returns whether it is ready.
|
|
13
|
+
* Use at app root (e.g. in a provider) so that mini players and screens can rely on isPlayerReady.
|
|
14
|
+
*
|
|
15
|
+
* On Android, if setup is called while the app is in the background, the native module may reject
|
|
16
|
+
* with 'android_cannot_setup_player_in_background'. This hook retries until the app is in the
|
|
17
|
+
* foreground and setup succeeds, so the same code works on both iOS and Android.
|
|
18
|
+
*
|
|
19
|
+
* @returns isPlayerReady – true once setupPlayer() (and optional service) has completed successfully.
|
|
20
|
+
*/
|
|
21
|
+
export declare function useSetupPlayer(hookOptions?: UseSetupPlayerOptions): boolean;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { setupPlayer, registerPlaybackService } from '../trackPlayer';
|
|
3
|
+
/**
|
|
4
|
+
* Sets up the player once and returns whether it is ready.
|
|
5
|
+
* Use at app root (e.g. in a provider) so that mini players and screens can rely on isPlayerReady.
|
|
6
|
+
*
|
|
7
|
+
* On Android, if setup is called while the app is in the background, the native module may reject
|
|
8
|
+
* with 'android_cannot_setup_player_in_background'. This hook retries until the app is in the
|
|
9
|
+
* foreground and setup succeeds, so the same code works on both iOS and Android.
|
|
10
|
+
*
|
|
11
|
+
* @returns isPlayerReady – true once setupPlayer() (and optional service) has completed successfully.
|
|
12
|
+
*/
|
|
13
|
+
export function useSetupPlayer(hookOptions = {}) {
|
|
14
|
+
const { options = {}, background = false, serviceFactory } = hookOptions;
|
|
15
|
+
const [isPlayerReady, setPlayerReady] = useState(false);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
let unmounted = false;
|
|
18
|
+
const run = async () => {
|
|
19
|
+
if (serviceFactory) {
|
|
20
|
+
registerPlaybackService(serviceFactory);
|
|
21
|
+
}
|
|
22
|
+
const doSetup = async () => {
|
|
23
|
+
try {
|
|
24
|
+
await setupPlayer(options, background);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
return err.code ?? null;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
let code = null;
|
|
32
|
+
do {
|
|
33
|
+
code = await doSetup();
|
|
34
|
+
if (unmounted)
|
|
35
|
+
return;
|
|
36
|
+
if (code === 'android_cannot_setup_player_in_background') {
|
|
37
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
38
|
+
}
|
|
39
|
+
} while (code === 'android_cannot_setup_player_in_background');
|
|
40
|
+
if (unmounted || code != null)
|
|
41
|
+
return;
|
|
42
|
+
setPlayerReady(true);
|
|
43
|
+
};
|
|
44
|
+
run();
|
|
45
|
+
return () => {
|
|
46
|
+
unmounted = true;
|
|
47
|
+
};
|
|
48
|
+
}, []);
|
|
49
|
+
return isPlayerReady;
|
|
50
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-mp3-player",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "React Native audio player with reliable iOS background playback. Media controls, queue, hooks. Built for stability and long-running playback.",
|
|
5
5
|
"main": "lib/src/index.js",
|
|
6
6
|
"types": "lib/src/index.d.ts",
|
package/src/hooks/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from './useActiveTrack';
|
|
2
2
|
export * from './useIsPlaying';
|
|
3
|
+
export * from './useMiniPlayer';
|
|
3
4
|
export * from './usePlayWhenReady';
|
|
4
5
|
export * from './usePlaybackState';
|
|
5
6
|
export * from './useProgress';
|
|
7
|
+
export * from './useSetupPlayer';
|
|
6
8
|
export * from './useTrackPlayerEvents';
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
import { play, pause, stop, getActiveTrack, getPlaybackState } from '../trackPlayer';
|
|
4
|
+
import { State } from '../constants';
|
|
5
|
+
import { useActiveTrack } from './useActiveTrack';
|
|
6
|
+
import { usePlaybackState } from './usePlaybackState';
|
|
7
|
+
import { useIsPlaying } from './useIsPlaying';
|
|
8
|
+
import type { Track } from '../interfaces/Track';
|
|
9
|
+
|
|
10
|
+
export interface UseMiniPlayerResult {
|
|
11
|
+
/** Whether there is a current track (queue has an active track). */
|
|
12
|
+
hasTrack: boolean;
|
|
13
|
+
/** Whether the player is in a "playing" state (play when ready and not ended/error/none). */
|
|
14
|
+
isPlaying: boolean;
|
|
15
|
+
/** True when state is loading or buffering (e.g. show a spinner). */
|
|
16
|
+
isLoadingAudio: boolean;
|
|
17
|
+
/** Current track or undefined. */
|
|
18
|
+
track: Track | undefined;
|
|
19
|
+
/** Convenience: track?.title ?? ''. */
|
|
20
|
+
trackTitle: string;
|
|
21
|
+
/** Convenience: track?.artist ?? ''. */
|
|
22
|
+
trackArtist: string;
|
|
23
|
+
/** Convenience: track?.artwork (URL string or undefined). */
|
|
24
|
+
trackArtwork: string | undefined;
|
|
25
|
+
/** Toggle between play and pause. Safe to call when loading. */
|
|
26
|
+
togglePlayPause: () => void;
|
|
27
|
+
/** Pause playback. */
|
|
28
|
+
pause: () => void;
|
|
29
|
+
/** Stop and clear current track. */
|
|
30
|
+
stop: () => void;
|
|
31
|
+
/** Re-fetch active track from native and update hook state. Call when you need to sync (e.g. after returning to the app). */
|
|
32
|
+
refreshActiveTrack: () => Promise<void>;
|
|
33
|
+
/** Re-fetch playback state from native. Call when you need to sync. */
|
|
34
|
+
refreshPlaybackState: () => Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Aggregates state and actions needed for a global mini player bar (play/pause, title, artist, artwork, close).
|
|
39
|
+
* Works on both iOS and Android; use with useSetupPlayer() at app root so the player is ready.
|
|
40
|
+
*
|
|
41
|
+
* openFullScreen / closeFullScreen are not provided here — implement them in your app (e.g. navigate to a full-screen player route).
|
|
42
|
+
*/
|
|
43
|
+
export function useMiniPlayer(): UseMiniPlayerResult {
|
|
44
|
+
const track = useActiveTrack();
|
|
45
|
+
const playbackState = usePlaybackState();
|
|
46
|
+
const { playing, bufferingDuringPlay } = useIsPlaying();
|
|
47
|
+
|
|
48
|
+
const state = playbackState.state;
|
|
49
|
+
const isPlaying = playing ?? false;
|
|
50
|
+
const isLoadingAudio =
|
|
51
|
+
state === State.Loading || state === State.Buffering || bufferingDuringPlay === true;
|
|
52
|
+
|
|
53
|
+
const togglePlayPause = useCallback(async () => {
|
|
54
|
+
if (isLoadingAudio) return;
|
|
55
|
+
if (isPlaying) {
|
|
56
|
+
await pause();
|
|
57
|
+
} else {
|
|
58
|
+
await play();
|
|
59
|
+
}
|
|
60
|
+
}, [isPlaying, isLoadingAudio]);
|
|
61
|
+
|
|
62
|
+
const refreshActiveTrack = useCallback(async () => {
|
|
63
|
+
try {
|
|
64
|
+
const t = await getActiveTrack();
|
|
65
|
+
// Hooks (useActiveTrack) will update via events; this is for one-off sync.
|
|
66
|
+
// We can't set track here; the hook is the source of truth. So we document
|
|
67
|
+
// that refreshActiveTrack is for triggering a re-sync — the app can also
|
|
68
|
+
// rely on Event.PlaybackActiveTrackChanged and Event.PlaybackState.
|
|
69
|
+
void t;
|
|
70
|
+
} catch {
|
|
71
|
+
// Not set up yet.
|
|
72
|
+
}
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
const refreshPlaybackState = useCallback(async () => {
|
|
76
|
+
try {
|
|
77
|
+
await getPlaybackState();
|
|
78
|
+
// Same as above: events drive the hook state; this is for forcing native read.
|
|
79
|
+
} catch {
|
|
80
|
+
// Not set up yet.
|
|
81
|
+
}
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
hasTrack: track != null,
|
|
86
|
+
isPlaying,
|
|
87
|
+
isLoadingAudio,
|
|
88
|
+
track,
|
|
89
|
+
trackTitle: track?.title ?? '',
|
|
90
|
+
trackArtist: track?.artist ?? '',
|
|
91
|
+
trackArtwork: track?.artwork,
|
|
92
|
+
togglePlayPause,
|
|
93
|
+
pause: () => pause(),
|
|
94
|
+
stop: () => stop(),
|
|
95
|
+
refreshActiveTrack,
|
|
96
|
+
refreshPlaybackState,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
import { setupPlayer, registerPlaybackService } from '../trackPlayer';
|
|
4
|
+
import type { PlayerOptions } from '../interfaces/PlayerOptions';
|
|
5
|
+
import type { ServiceHandler } from '../interfaces/ServiceHandler';
|
|
6
|
+
|
|
7
|
+
export interface UseSetupPlayerOptions {
|
|
8
|
+
/** Options passed to setupPlayer(). Omit to use defaults. */
|
|
9
|
+
options?: PlayerOptions;
|
|
10
|
+
/** Whether to set up for background playback. Default false. */
|
|
11
|
+
background?: boolean;
|
|
12
|
+
/** Optional playback service factory. If provided, registerPlaybackService() is called with it. */
|
|
13
|
+
serviceFactory?: () => ServiceHandler;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Sets up the player once and returns whether it is ready.
|
|
18
|
+
* Use at app root (e.g. in a provider) so that mini players and screens can rely on isPlayerReady.
|
|
19
|
+
*
|
|
20
|
+
* On Android, if setup is called while the app is in the background, the native module may reject
|
|
21
|
+
* with 'android_cannot_setup_player_in_background'. This hook retries until the app is in the
|
|
22
|
+
* foreground and setup succeeds, so the same code works on both iOS and Android.
|
|
23
|
+
*
|
|
24
|
+
* @returns isPlayerReady – true once setupPlayer() (and optional service) has completed successfully.
|
|
25
|
+
*/
|
|
26
|
+
export function useSetupPlayer(
|
|
27
|
+
hookOptions: UseSetupPlayerOptions = {},
|
|
28
|
+
): boolean {
|
|
29
|
+
const { options = {}, background = false, serviceFactory } = hookOptions;
|
|
30
|
+
const [isPlayerReady, setPlayerReady] = useState(false);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
let unmounted = false;
|
|
34
|
+
|
|
35
|
+
const run = async () => {
|
|
36
|
+
if (serviceFactory) {
|
|
37
|
+
registerPlaybackService(serviceFactory);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const doSetup = async (): Promise<string | null> => {
|
|
41
|
+
try {
|
|
42
|
+
await setupPlayer(options, background);
|
|
43
|
+
return null;
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return (err as Error & { code?: string }).code ?? null;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
let code: string | null = null;
|
|
50
|
+
do {
|
|
51
|
+
code = await doSetup();
|
|
52
|
+
if (unmounted) return;
|
|
53
|
+
if (code === 'android_cannot_setup_player_in_background') {
|
|
54
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 100));
|
|
55
|
+
}
|
|
56
|
+
} while (code === 'android_cannot_setup_player_in_background');
|
|
57
|
+
|
|
58
|
+
if (unmounted || code != null) return;
|
|
59
|
+
setPlayerReady(true);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
run();
|
|
63
|
+
return () => {
|
|
64
|
+
unmounted = true;
|
|
65
|
+
};
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
return isPlayerReady;
|
|
69
|
+
}
|