react-native-nitro-player 1.6.0 → 1.7.0-alpha.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 +6 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +9 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +23 -3
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +23 -3
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +34 -17
- package/android/src/main/java/com/margelo/nitro/nitroplayer/connection/AndroidAutoConnectionDetector.kt +22 -22
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +21 -9
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +19 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +33 -19
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +0 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +9 -5
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadManagerCore.kt +96 -56
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadWorker.kt +62 -22
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/TrackItemJson.kt +60 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +9 -9
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/AuthAwareHttpDataSourceFactory.kt +12 -3
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/ExoPlayerBuilder.kt +4 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +99 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/PlaybackService.kt +7 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +85 -70
- package/android/src/main/res/values/strings.xml +7 -0
- package/ios/HybridDownloadManager.swift +13 -3
- package/ios/HybridTrackPlayer.swift +4 -2
- package/ios/core/TrackPlayerCast.swift +32 -1
- package/ios/core/TrackPlayerCore.swift +19 -3
- package/ios/core/TrackPlayerListener.swift +15 -16
- package/ios/core/TrackPlayerNotify.swift +11 -11
- package/ios/core/TrackPlayerPlayback.swift +54 -16
- package/ios/core/TrackPlayerQueueBuild.swift +14 -2
- package/ios/core/TrackPlayerTempQueue.swift +1 -0
- package/ios/core/TrackPlayerUrlLoader.swift +11 -3
- package/ios/download/DownloadDatabase.swift +16 -5
- package/ios/download/DownloadFileManager.swift +12 -15
- package/ios/download/DownloadManagerCore.swift +61 -16
- package/ios/equalizer/EqualizerCore.swift +37 -9
- package/ios/media/MediaSessionManager.swift +89 -4
- package/ios/playlist/PlaylistManager.swift +47 -46
- package/ios/queue/HybridPlayerQueue.swift +28 -7
- package/lib/hooks/callbackManager.d.ts +9 -0
- package/lib/hooks/callbackManager.js +33 -0
- package/lib/hooks/useActualQueue.js +18 -6
- package/lib/hooks/useAndroidAutoConnection.js +2 -4
- package/lib/hooks/useAudioDevices.js +21 -24
- package/lib/hooks/useDownloadActions.js +38 -16
- package/lib/hooks/useDownloadProgress.js +20 -14
- package/lib/hooks/useDownloadStorage.js +1 -1
- package/lib/hooks/useNowPlaying.js +32 -6
- package/lib/hooks/useOnSeek.js +2 -2
- package/lib/hooks/usePlaylist.js +6 -15
- package/lib/specs/TrackPlayer.nitro.d.ts +1 -2
- package/lib/types/PlayerQueue.d.ts +8 -0
- package/nitrogen/generated/android/c++/JPlayerConfig.hpp +9 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerConfig.kt +8 -2
- package/nitrogen/generated/ios/swift/PlayerConfig.swift +37 -1
- package/nitrogen/generated/shared/c++/PlayerConfig.hpp +9 -1
- package/package.json +1 -1
- package/src/hooks/callbackManager.ts +44 -0
- package/src/hooks/useActualQueue.ts +18 -6
- package/src/hooks/useAndroidAutoConnection.ts +6 -6
- package/src/hooks/useAudioDevices.ts +21 -24
- package/src/hooks/useDownloadActions.ts +56 -26
- package/src/hooks/useDownloadProgress.ts +22 -19
- package/src/hooks/useDownloadStorage.ts +4 -3
- package/src/hooks/useNowPlaying.ts +34 -6
- package/src/hooks/useOnSeek.ts +2 -2
- package/src/hooks/usePlaylist.ts +6 -14
- package/src/specs/TrackPlayer.nitro.ts +1 -2
- package/src/types/PlayerQueue.ts +8 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/NitroPlayerNotificationProvider.kt +0 -200
- package/android/src/main/java/com/margelo/nitro/nitroplayer/queue/Queue.kt +0 -94
- package/android/src/main/java/com/margelo/nitro/nitroplayer/queue/QueueManager.kt +0 -143
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useState } from 'react';
|
|
1
|
+
import { useCallback, useRef, useState } from 'react';
|
|
2
2
|
import { DownloadManager } from '../index';
|
|
3
3
|
/**
|
|
4
4
|
* Hook for download actions (download, pause, resume, cancel, delete)
|
|
@@ -7,8 +7,30 @@ export function useDownloadActions() {
|
|
|
7
7
|
const [isDownloading, setIsDownloading] = useState(false);
|
|
8
8
|
const [isDeleting, setIsDeleting] = useState(false);
|
|
9
9
|
const [error, setError] = useState(null);
|
|
10
|
-
|
|
10
|
+
// Pending counts: overlapping calls otherwise clear the flag while a
|
|
11
|
+
// second request is still in flight
|
|
12
|
+
const downloadingCount = useRef(0);
|
|
13
|
+
const deletingCount = useRef(0);
|
|
14
|
+
const beginDownloading = useCallback(() => {
|
|
15
|
+
downloadingCount.current += 1;
|
|
11
16
|
setIsDownloading(true);
|
|
17
|
+
}, []);
|
|
18
|
+
const endDownloading = useCallback(() => {
|
|
19
|
+
downloadingCount.current = Math.max(0, downloadingCount.current - 1);
|
|
20
|
+
if (downloadingCount.current === 0)
|
|
21
|
+
setIsDownloading(false);
|
|
22
|
+
}, []);
|
|
23
|
+
const beginDeleting = useCallback(() => {
|
|
24
|
+
deletingCount.current += 1;
|
|
25
|
+
setIsDeleting(true);
|
|
26
|
+
}, []);
|
|
27
|
+
const endDeleting = useCallback(() => {
|
|
28
|
+
deletingCount.current = Math.max(0, deletingCount.current - 1);
|
|
29
|
+
if (deletingCount.current === 0)
|
|
30
|
+
setIsDeleting(false);
|
|
31
|
+
}, []);
|
|
32
|
+
const downloadTrack = useCallback(async (track, playlistId) => {
|
|
33
|
+
beginDownloading();
|
|
12
34
|
setError(null);
|
|
13
35
|
try {
|
|
14
36
|
const downloadId = await DownloadManager.downloadTrack(track, playlistId);
|
|
@@ -19,11 +41,11 @@ export function useDownloadActions() {
|
|
|
19
41
|
throw e;
|
|
20
42
|
}
|
|
21
43
|
finally {
|
|
22
|
-
|
|
44
|
+
endDownloading();
|
|
23
45
|
}
|
|
24
|
-
}, []);
|
|
46
|
+
}, [beginDownloading, endDownloading]);
|
|
25
47
|
const downloadPlaylist = useCallback(async (playlistId, tracks) => {
|
|
26
|
-
|
|
48
|
+
beginDownloading();
|
|
27
49
|
setError(null);
|
|
28
50
|
try {
|
|
29
51
|
const downloadIds = await DownloadManager.downloadPlaylist(playlistId, tracks);
|
|
@@ -34,9 +56,9 @@ export function useDownloadActions() {
|
|
|
34
56
|
throw e;
|
|
35
57
|
}
|
|
36
58
|
finally {
|
|
37
|
-
|
|
59
|
+
endDownloading();
|
|
38
60
|
}
|
|
39
|
-
}, []);
|
|
61
|
+
}, [beginDownloading, endDownloading]);
|
|
40
62
|
const pauseDownload = useCallback(async (downloadId) => {
|
|
41
63
|
await DownloadManager.pauseDownload(downloadId);
|
|
42
64
|
}, []);
|
|
@@ -59,32 +81,32 @@ export function useDownloadActions() {
|
|
|
59
81
|
await DownloadManager.cancelAllDownloads();
|
|
60
82
|
}, []);
|
|
61
83
|
const deleteTrack = useCallback(async (trackId) => {
|
|
62
|
-
|
|
84
|
+
beginDeleting();
|
|
63
85
|
try {
|
|
64
86
|
await DownloadManager.deleteDownloadedTrack(trackId);
|
|
65
87
|
}
|
|
66
88
|
finally {
|
|
67
|
-
|
|
89
|
+
endDeleting();
|
|
68
90
|
}
|
|
69
|
-
}, []);
|
|
91
|
+
}, [beginDeleting, endDeleting]);
|
|
70
92
|
const deletePlaylist = useCallback(async (playlistId) => {
|
|
71
|
-
|
|
93
|
+
beginDeleting();
|
|
72
94
|
try {
|
|
73
95
|
await DownloadManager.deleteDownloadedPlaylist(playlistId);
|
|
74
96
|
}
|
|
75
97
|
finally {
|
|
76
|
-
|
|
98
|
+
endDeleting();
|
|
77
99
|
}
|
|
78
|
-
}, []);
|
|
100
|
+
}, [beginDeleting, endDeleting]);
|
|
79
101
|
const deleteAll = useCallback(async () => {
|
|
80
|
-
|
|
102
|
+
beginDeleting();
|
|
81
103
|
try {
|
|
82
104
|
await DownloadManager.deleteAllDownloads();
|
|
83
105
|
}
|
|
84
106
|
finally {
|
|
85
|
-
|
|
107
|
+
endDeleting();
|
|
86
108
|
}
|
|
87
|
-
}, []);
|
|
109
|
+
}, [beginDeleting, endDeleting]);
|
|
88
110
|
const configure = useCallback((config) => {
|
|
89
111
|
DownloadManager.configure(config);
|
|
90
112
|
}, []);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useState, useRef, useCallback } from 'react';
|
|
1
|
+
import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
|
|
2
2
|
import { DownloadManager } from '../index';
|
|
3
3
|
import { downloadCallbackManager } from './downloadCallbackManager';
|
|
4
4
|
/**
|
|
@@ -8,22 +8,24 @@ export function useDownloadProgress(options = {}) {
|
|
|
8
8
|
const { trackIds, downloadIds, activeOnly = false } = options;
|
|
9
9
|
const [progressMap, setProgressMap] = useState(new Map());
|
|
10
10
|
const isMounted = useRef(true);
|
|
11
|
+
// Content keys: callers pass inline arrays, so identity-based deps would
|
|
12
|
+
// resubscribe (and re-fetch over the bridge) on every parent render
|
|
13
|
+
const trackIdsKey = JSON.stringify(trackIds ?? []);
|
|
14
|
+
const downloadIdsKey = JSON.stringify(downloadIds ?? []);
|
|
11
15
|
const shouldTrack = useCallback((progress) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
const ids = JSON.parse(trackIdsKey);
|
|
17
|
+
const dlIds = JSON.parse(downloadIdsKey);
|
|
18
|
+
if (ids.length > 0 && !ids.includes(progress.trackId)) {
|
|
15
19
|
return false;
|
|
16
20
|
}
|
|
17
|
-
if (
|
|
18
|
-
downloadIds.length > 0 &&
|
|
19
|
-
!downloadIds.includes(progress.downloadId)) {
|
|
21
|
+
if (dlIds.length > 0 && !dlIds.includes(progress.downloadId)) {
|
|
20
22
|
return false;
|
|
21
23
|
}
|
|
22
24
|
if (activeOnly && progress.state !== 'downloading') {
|
|
23
25
|
return false;
|
|
24
26
|
}
|
|
25
27
|
return true;
|
|
26
|
-
}, [
|
|
28
|
+
}, [trackIdsKey, downloadIdsKey, activeOnly]);
|
|
27
29
|
useEffect(() => {
|
|
28
30
|
isMounted.current = true;
|
|
29
31
|
// Initialize with current active downloads
|
|
@@ -69,12 +71,16 @@ export function useDownloadProgress(options = {}) {
|
|
|
69
71
|
unsubscribe();
|
|
70
72
|
};
|
|
71
73
|
}, [shouldTrack]);
|
|
72
|
-
const progressList =
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
progressList
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
const { progressList, overallProgress, isDownloading } = useMemo(() => {
|
|
75
|
+
const list = Array.from(progressMap.values());
|
|
76
|
+
return {
|
|
77
|
+
progressList: list,
|
|
78
|
+
overallProgress: list.length > 0
|
|
79
|
+
? list.reduce((sum, p) => sum + p.progress, 0) / list.length
|
|
80
|
+
: 0,
|
|
81
|
+
isDownloading: list.some((p) => p.state === 'downloading'),
|
|
82
|
+
};
|
|
83
|
+
}, [progressMap]);
|
|
78
84
|
const getProgress = useCallback((trackId) => progressMap.get(trackId), [progressMap]);
|
|
79
85
|
return {
|
|
80
86
|
progressMap,
|
|
@@ -46,7 +46,7 @@ export function useDownloadStorage() {
|
|
|
46
46
|
const formattedAvailable = storageInfo
|
|
47
47
|
? formatBytes(storageInfo.availableSpace)
|
|
48
48
|
: '0 B';
|
|
49
|
-
const usagePercentage = storageInfo
|
|
49
|
+
const usagePercentage = storageInfo && storageInfo.totalSpace > 0
|
|
50
50
|
? (storageInfo.totalDownloadedSize / storageInfo.totalSpace) * 100
|
|
51
51
|
: 0;
|
|
52
52
|
return {
|
|
@@ -47,19 +47,41 @@ const DEFAULT_STATE = {
|
|
|
47
47
|
export function useNowPlaying() {
|
|
48
48
|
const [state, setState] = useState(DEFAULT_STATE);
|
|
49
49
|
const isMounted = useRef(true);
|
|
50
|
+
// Once pushed events carry position, an async getState() snapshot must not
|
|
51
|
+
// overwrite it — a slow snapshot resolving after newer ticks jumped the
|
|
52
|
+
// progress bar backward on play/pause/track transitions.
|
|
53
|
+
const eventsOwnPosition = useRef(false);
|
|
54
|
+
const fetchScheduled = useRef(false);
|
|
50
55
|
const fetchFullState = useCallback(async () => {
|
|
51
56
|
if (!isMounted.current)
|
|
52
57
|
return;
|
|
53
58
|
try {
|
|
54
59
|
const newState = await TrackPlayer.getState();
|
|
55
60
|
if (isMounted.current) {
|
|
56
|
-
setState(
|
|
61
|
+
setState((prev) => eventsOwnPosition.current
|
|
62
|
+
? {
|
|
63
|
+
...newState,
|
|
64
|
+
currentPosition: prev.currentPosition,
|
|
65
|
+
totalDuration: prev.totalDuration,
|
|
66
|
+
}
|
|
67
|
+
: newState);
|
|
57
68
|
}
|
|
58
69
|
}
|
|
59
70
|
catch (error) {
|
|
60
71
|
console.error('[useNowPlaying] Error updating player state:', error);
|
|
61
72
|
}
|
|
62
73
|
}, []);
|
|
74
|
+
// Coalesces the track-change + playback-state burst that fires on one
|
|
75
|
+
// transition into a single getState() round trip
|
|
76
|
+
const requestFullState = useCallback(() => {
|
|
77
|
+
if (fetchScheduled.current)
|
|
78
|
+
return;
|
|
79
|
+
fetchScheduled.current = true;
|
|
80
|
+
queueMicrotask(() => {
|
|
81
|
+
fetchScheduled.current = false;
|
|
82
|
+
fetchFullState();
|
|
83
|
+
});
|
|
84
|
+
}, [fetchFullState]);
|
|
63
85
|
// Initialize with current state
|
|
64
86
|
useEffect(() => {
|
|
65
87
|
isMounted.current = true;
|
|
@@ -68,18 +90,20 @@ export function useNowPlaying() {
|
|
|
68
90
|
isMounted.current = false;
|
|
69
91
|
};
|
|
70
92
|
}, [fetchFullState]);
|
|
71
|
-
// Subscribe to track changes — full refresh
|
|
93
|
+
// Subscribe to track changes — full refresh; the new track's position comes
|
|
94
|
+
// from the snapshot, so release event ownership until ticks resume
|
|
72
95
|
useEffect(() => {
|
|
73
96
|
return callbackManager.subscribeToTrackChange(() => {
|
|
74
|
-
|
|
97
|
+
eventsOwnPosition.current = false;
|
|
98
|
+
requestFullState();
|
|
75
99
|
});
|
|
76
|
-
}, [
|
|
100
|
+
}, [requestFullState]);
|
|
77
101
|
// Subscribe to playback state changes — full refresh
|
|
78
102
|
useEffect(() => {
|
|
79
103
|
return callbackManager.subscribeToPlaybackState(() => {
|
|
80
|
-
|
|
104
|
+
requestFullState();
|
|
81
105
|
});
|
|
82
|
-
}, [
|
|
106
|
+
}, [requestFullState]);
|
|
83
107
|
// Subscribe to progress changes — lightweight position/duration update.
|
|
84
108
|
// Bail out when neither value changed at whole-second resolution: the native tick
|
|
85
109
|
// is sub-second, and a new state object on every tick re-renders every consumer.
|
|
@@ -87,6 +111,7 @@ export function useNowPlaying() {
|
|
|
87
111
|
return callbackManager.subscribeToPlaybackProgressChange((currentPosition, totalDuration) => {
|
|
88
112
|
if (!isMounted.current)
|
|
89
113
|
return;
|
|
114
|
+
eventsOwnPosition.current = true;
|
|
90
115
|
setState((prev) => {
|
|
91
116
|
if (Math.floor(prev.currentPosition) === Math.floor(currentPosition) &&
|
|
92
117
|
prev.totalDuration === totalDuration) {
|
|
@@ -101,6 +126,7 @@ export function useNowPlaying() {
|
|
|
101
126
|
return callbackManager.subscribeToSeek((currentPosition, totalDuration) => {
|
|
102
127
|
if (!isMounted.current)
|
|
103
128
|
return;
|
|
129
|
+
eventsOwnPosition.current = true;
|
|
104
130
|
setState((prev) => ({ ...prev, currentPosition, totalDuration }));
|
|
105
131
|
});
|
|
106
132
|
}, []);
|
package/lib/hooks/useOnSeek.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { callbackManager } from './callbackManager';
|
|
3
3
|
/**
|
|
4
4
|
* Hook to get the last seek event information
|
|
5
5
|
* @returns Object with last seek position and total duration, or undefined if no seek has occurred
|
|
@@ -8,7 +8,7 @@ export function useOnSeek() {
|
|
|
8
8
|
const [position, setPosition] = useState(undefined);
|
|
9
9
|
const [totalDuration, setTotalDuration] = useState(undefined);
|
|
10
10
|
useEffect(() => {
|
|
11
|
-
|
|
11
|
+
return callbackManager.subscribeToSeek((newPosition, newTotalDuration) => {
|
|
12
12
|
setPosition(newPosition);
|
|
13
13
|
setTotalDuration(newTotalDuration);
|
|
14
14
|
});
|
package/lib/hooks/usePlaylist.js
CHANGED
|
@@ -40,7 +40,6 @@ export function usePlaylist() {
|
|
|
40
40
|
const [allTracks, setAllTracks] = useState([]);
|
|
41
41
|
const [isLoading, setIsLoading] = useState(true);
|
|
42
42
|
const isMounted = useRef(true);
|
|
43
|
-
const hasSubscribed = useRef(false);
|
|
44
43
|
// Tracks the last fetched playlist ID so track-change events can skip
|
|
45
44
|
// a full refresh when the playlist itself hasn't changed.
|
|
46
45
|
const lastPlaylistIdRef = useRef(undefined);
|
|
@@ -119,21 +118,13 @@ export function usePlaylist() {
|
|
|
119
118
|
isMounted.current = false;
|
|
120
119
|
};
|
|
121
120
|
}, [refreshPlaylists]);
|
|
122
|
-
// Subscribe to native playlist changes
|
|
121
|
+
// Subscribe to native playlist changes via the shared manager
|
|
123
122
|
useEffect(() => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (isMounted.current) {
|
|
130
|
-
refreshPlaylists();
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
console.error('[usePlaylist] Error setting up playlist listener:', error);
|
|
136
|
-
}
|
|
123
|
+
return callbackManager.subscribeToPlaylistsChanged(() => {
|
|
124
|
+
if (isMounted.current) {
|
|
125
|
+
refreshPlaylists();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
137
128
|
}, [refreshPlaylists]);
|
|
138
129
|
// Refresh on track change only if the active playlist ID changed.
|
|
139
130
|
useEffect(() => {
|
|
@@ -92,8 +92,7 @@ export interface TrackPlayer extends HybridObject<{
|
|
|
92
92
|
*/
|
|
93
93
|
onTracksNeedUpdate(callback: (tracks: TrackItem[], lookahead: number) => void): void;
|
|
94
94
|
/**
|
|
95
|
-
*
|
|
96
|
-
* @returns Promise resolving to 0-based index, or -1 if no track playing
|
|
95
|
+
* Set playback speed. Rejects non-finite and non-positive values.
|
|
97
96
|
*/
|
|
98
97
|
setPlaybackSpeed(speed: number): Promise<void>;
|
|
99
98
|
/**
|
|
@@ -48,6 +48,14 @@ export interface PlayerConfig {
|
|
|
48
48
|
androidAutoEnabled?: boolean;
|
|
49
49
|
carPlayEnabled?: boolean;
|
|
50
50
|
showInNotification?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Fixed interval, in seconds, for remote skip-forward controls (default: 15)
|
|
53
|
+
*/
|
|
54
|
+
remoteSkipForwardInterval?: number;
|
|
55
|
+
/**
|
|
56
|
+
* Fixed interval, in seconds, for remote skip-backward controls (default: 15)
|
|
57
|
+
*/
|
|
58
|
+
remoteSkipBackwardInterval?: number;
|
|
51
59
|
/**
|
|
52
60
|
* Number of upcoming tracks to preload URLs for (default: 5)
|
|
53
61
|
* Higher values = more proactive loading, but more network requests
|
|
@@ -38,6 +38,10 @@ namespace margelo::nitro::nitroplayer {
|
|
|
38
38
|
jni::local_ref<jni::JBoolean> carPlayEnabled = this->getFieldValue(fieldCarPlayEnabled);
|
|
39
39
|
static const auto fieldShowInNotification = clazz->getField<jni::JBoolean>("showInNotification");
|
|
40
40
|
jni::local_ref<jni::JBoolean> showInNotification = this->getFieldValue(fieldShowInNotification);
|
|
41
|
+
static const auto fieldRemoteSkipForwardInterval = clazz->getField<jni::JDouble>("remoteSkipForwardInterval");
|
|
42
|
+
jni::local_ref<jni::JDouble> remoteSkipForwardInterval = this->getFieldValue(fieldRemoteSkipForwardInterval);
|
|
43
|
+
static const auto fieldRemoteSkipBackwardInterval = clazz->getField<jni::JDouble>("remoteSkipBackwardInterval");
|
|
44
|
+
jni::local_ref<jni::JDouble> remoteSkipBackwardInterval = this->getFieldValue(fieldRemoteSkipBackwardInterval);
|
|
41
45
|
static const auto fieldLookaheadCount = clazz->getField<jni::JDouble>("lookaheadCount");
|
|
42
46
|
jni::local_ref<jni::JDouble> lookaheadCount = this->getFieldValue(fieldLookaheadCount);
|
|
43
47
|
static const auto fieldAndroidNotificationIcon = clazz->getField<jni::JString>("androidNotificationIcon");
|
|
@@ -46,6 +50,8 @@ namespace margelo::nitro::nitroplayer {
|
|
|
46
50
|
androidAutoEnabled != nullptr ? std::make_optional(static_cast<bool>(androidAutoEnabled->value())) : std::nullopt,
|
|
47
51
|
carPlayEnabled != nullptr ? std::make_optional(static_cast<bool>(carPlayEnabled->value())) : std::nullopt,
|
|
48
52
|
showInNotification != nullptr ? std::make_optional(static_cast<bool>(showInNotification->value())) : std::nullopt,
|
|
53
|
+
remoteSkipForwardInterval != nullptr ? std::make_optional(remoteSkipForwardInterval->value()) : std::nullopt,
|
|
54
|
+
remoteSkipBackwardInterval != nullptr ? std::make_optional(remoteSkipBackwardInterval->value()) : std::nullopt,
|
|
49
55
|
lookaheadCount != nullptr ? std::make_optional(lookaheadCount->value()) : std::nullopt,
|
|
50
56
|
androidNotificationIcon != nullptr ? std::make_optional(androidNotificationIcon->toStdString()) : std::nullopt
|
|
51
57
|
);
|
|
@@ -57,7 +63,7 @@ namespace margelo::nitro::nitroplayer {
|
|
|
57
63
|
*/
|
|
58
64
|
[[maybe_unused]]
|
|
59
65
|
static jni::local_ref<JPlayerConfig::javaobject> fromCpp(const PlayerConfig& value) {
|
|
60
|
-
using JSignature = JPlayerConfig(jni::alias_ref<jni::JBoolean>, jni::alias_ref<jni::JBoolean>, jni::alias_ref<jni::JBoolean>, jni::alias_ref<jni::JDouble>, jni::alias_ref<jni::JString>);
|
|
66
|
+
using JSignature = JPlayerConfig(jni::alias_ref<jni::JBoolean>, jni::alias_ref<jni::JBoolean>, jni::alias_ref<jni::JBoolean>, jni::alias_ref<jni::JDouble>, jni::alias_ref<jni::JDouble>, jni::alias_ref<jni::JDouble>, jni::alias_ref<jni::JString>);
|
|
61
67
|
static const auto clazz = javaClassStatic();
|
|
62
68
|
static const auto create = clazz->getStaticMethod<JSignature>("fromCpp");
|
|
63
69
|
return create(
|
|
@@ -65,6 +71,8 @@ namespace margelo::nitro::nitroplayer {
|
|
|
65
71
|
value.androidAutoEnabled.has_value() ? jni::JBoolean::valueOf(value.androidAutoEnabled.value()) : nullptr,
|
|
66
72
|
value.carPlayEnabled.has_value() ? jni::JBoolean::valueOf(value.carPlayEnabled.value()) : nullptr,
|
|
67
73
|
value.showInNotification.has_value() ? jni::JBoolean::valueOf(value.showInNotification.value()) : nullptr,
|
|
74
|
+
value.remoteSkipForwardInterval.has_value() ? jni::JDouble::valueOf(value.remoteSkipForwardInterval.value()) : nullptr,
|
|
75
|
+
value.remoteSkipBackwardInterval.has_value() ? jni::JDouble::valueOf(value.remoteSkipBackwardInterval.value()) : nullptr,
|
|
68
76
|
value.lookaheadCount.has_value() ? jni::JDouble::valueOf(value.lookaheadCount.value()) : nullptr,
|
|
69
77
|
value.androidNotificationIcon.has_value() ? jni::make_jstring(value.androidNotificationIcon.value()) : nullptr
|
|
70
78
|
);
|
|
@@ -28,6 +28,12 @@ data class PlayerConfig(
|
|
|
28
28
|
val showInNotification: Boolean?,
|
|
29
29
|
@DoNotStrip
|
|
30
30
|
@Keep
|
|
31
|
+
val remoteSkipForwardInterval: Double?,
|
|
32
|
+
@DoNotStrip
|
|
33
|
+
@Keep
|
|
34
|
+
val remoteSkipBackwardInterval: Double?,
|
|
35
|
+
@DoNotStrip
|
|
36
|
+
@Keep
|
|
31
37
|
val lookaheadCount: Double?,
|
|
32
38
|
@DoNotStrip
|
|
33
39
|
@Keep
|
|
@@ -43,8 +49,8 @@ data class PlayerConfig(
|
|
|
43
49
|
@Keep
|
|
44
50
|
@Suppress("unused")
|
|
45
51
|
@JvmStatic
|
|
46
|
-
private fun fromCpp(androidAutoEnabled: Boolean?, carPlayEnabled: Boolean?, showInNotification: Boolean?, lookaheadCount: Double?, androidNotificationIcon: String?): PlayerConfig {
|
|
47
|
-
return PlayerConfig(androidAutoEnabled, carPlayEnabled, showInNotification, lookaheadCount, androidNotificationIcon)
|
|
52
|
+
private fun fromCpp(androidAutoEnabled: Boolean?, carPlayEnabled: Boolean?, showInNotification: Boolean?, remoteSkipForwardInterval: Double?, remoteSkipBackwardInterval: Double?, lookaheadCount: Double?, androidNotificationIcon: String?): PlayerConfig {
|
|
53
|
+
return PlayerConfig(androidAutoEnabled, carPlayEnabled, showInNotification, remoteSkipForwardInterval, remoteSkipBackwardInterval, lookaheadCount, androidNotificationIcon)
|
|
48
54
|
}
|
|
49
55
|
}
|
|
50
56
|
}
|
|
@@ -18,7 +18,7 @@ public extension PlayerConfig {
|
|
|
18
18
|
/**
|
|
19
19
|
* Create a new instance of `PlayerConfig`.
|
|
20
20
|
*/
|
|
21
|
-
init(androidAutoEnabled: Bool?, carPlayEnabled: Bool?, showInNotification: Bool?, lookaheadCount: Double?, androidNotificationIcon: String?) {
|
|
21
|
+
init(androidAutoEnabled: Bool?, carPlayEnabled: Bool?, showInNotification: Bool?, remoteSkipForwardInterval: Double?, remoteSkipBackwardInterval: Double?, lookaheadCount: Double?, androidNotificationIcon: String?) {
|
|
22
22
|
self.init({ () -> bridge.std__optional_bool_ in
|
|
23
23
|
if let __unwrappedValue = androidAutoEnabled {
|
|
24
24
|
return bridge.create_std__optional_bool_(__unwrappedValue)
|
|
@@ -37,6 +37,18 @@ public extension PlayerConfig {
|
|
|
37
37
|
} else {
|
|
38
38
|
return .init()
|
|
39
39
|
}
|
|
40
|
+
}(), { () -> bridge.std__optional_double_ in
|
|
41
|
+
if let __unwrappedValue = remoteSkipForwardInterval {
|
|
42
|
+
return bridge.create_std__optional_double_(__unwrappedValue)
|
|
43
|
+
} else {
|
|
44
|
+
return .init()
|
|
45
|
+
}
|
|
46
|
+
}(), { () -> bridge.std__optional_double_ in
|
|
47
|
+
if let __unwrappedValue = remoteSkipBackwardInterval {
|
|
48
|
+
return bridge.create_std__optional_double_(__unwrappedValue)
|
|
49
|
+
} else {
|
|
50
|
+
return .init()
|
|
51
|
+
}
|
|
40
52
|
}(), { () -> bridge.std__optional_double_ in
|
|
41
53
|
if let __unwrappedValue = lookaheadCount {
|
|
42
54
|
return bridge.create_std__optional_double_(__unwrappedValue)
|
|
@@ -88,6 +100,30 @@ public extension PlayerConfig {
|
|
|
88
100
|
}()
|
|
89
101
|
}
|
|
90
102
|
|
|
103
|
+
@inline(__always)
|
|
104
|
+
var remoteSkipForwardInterval: Double? {
|
|
105
|
+
return { () -> Double? in
|
|
106
|
+
if bridge.has_value_std__optional_double_(self.__remoteSkipForwardInterval) {
|
|
107
|
+
let __unwrapped = bridge.get_std__optional_double_(self.__remoteSkipForwardInterval)
|
|
108
|
+
return __unwrapped
|
|
109
|
+
} else {
|
|
110
|
+
return nil
|
|
111
|
+
}
|
|
112
|
+
}()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@inline(__always)
|
|
116
|
+
var remoteSkipBackwardInterval: Double? {
|
|
117
|
+
return { () -> Double? in
|
|
118
|
+
if bridge.has_value_std__optional_double_(self.__remoteSkipBackwardInterval) {
|
|
119
|
+
let __unwrapped = bridge.get_std__optional_double_(self.__remoteSkipBackwardInterval)
|
|
120
|
+
return __unwrapped
|
|
121
|
+
} else {
|
|
122
|
+
return nil
|
|
123
|
+
}
|
|
124
|
+
}()
|
|
125
|
+
}
|
|
126
|
+
|
|
91
127
|
@inline(__always)
|
|
92
128
|
var lookaheadCount: Double? {
|
|
93
129
|
return { () -> Double? in
|
|
@@ -43,12 +43,14 @@ namespace margelo::nitro::nitroplayer {
|
|
|
43
43
|
std::optional<bool> androidAutoEnabled SWIFT_PRIVATE;
|
|
44
44
|
std::optional<bool> carPlayEnabled SWIFT_PRIVATE;
|
|
45
45
|
std::optional<bool> showInNotification SWIFT_PRIVATE;
|
|
46
|
+
std::optional<double> remoteSkipForwardInterval SWIFT_PRIVATE;
|
|
47
|
+
std::optional<double> remoteSkipBackwardInterval SWIFT_PRIVATE;
|
|
46
48
|
std::optional<double> lookaheadCount SWIFT_PRIVATE;
|
|
47
49
|
std::optional<std::string> androidNotificationIcon SWIFT_PRIVATE;
|
|
48
50
|
|
|
49
51
|
public:
|
|
50
52
|
PlayerConfig() = default;
|
|
51
|
-
explicit PlayerConfig(std::optional<bool> androidAutoEnabled, std::optional<bool> carPlayEnabled, std::optional<bool> showInNotification, std::optional<double> lookaheadCount, std::optional<std::string> androidNotificationIcon): androidAutoEnabled(androidAutoEnabled), carPlayEnabled(carPlayEnabled), showInNotification(showInNotification), lookaheadCount(lookaheadCount), androidNotificationIcon(androidNotificationIcon) {}
|
|
53
|
+
explicit PlayerConfig(std::optional<bool> androidAutoEnabled, std::optional<bool> carPlayEnabled, std::optional<bool> showInNotification, std::optional<double> remoteSkipForwardInterval, std::optional<double> remoteSkipBackwardInterval, std::optional<double> lookaheadCount, std::optional<std::string> androidNotificationIcon): androidAutoEnabled(androidAutoEnabled), carPlayEnabled(carPlayEnabled), showInNotification(showInNotification), remoteSkipForwardInterval(remoteSkipForwardInterval), remoteSkipBackwardInterval(remoteSkipBackwardInterval), lookaheadCount(lookaheadCount), androidNotificationIcon(androidNotificationIcon) {}
|
|
52
54
|
|
|
53
55
|
public:
|
|
54
56
|
friend bool operator==(const PlayerConfig& lhs, const PlayerConfig& rhs) = default;
|
|
@@ -67,6 +69,8 @@ namespace margelo::nitro {
|
|
|
67
69
|
JSIConverter<std::optional<bool>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "androidAutoEnabled"))),
|
|
68
70
|
JSIConverter<std::optional<bool>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "carPlayEnabled"))),
|
|
69
71
|
JSIConverter<std::optional<bool>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "showInNotification"))),
|
|
72
|
+
JSIConverter<std::optional<double>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "remoteSkipForwardInterval"))),
|
|
73
|
+
JSIConverter<std::optional<double>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "remoteSkipBackwardInterval"))),
|
|
70
74
|
JSIConverter<std::optional<double>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "lookaheadCount"))),
|
|
71
75
|
JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "androidNotificationIcon")))
|
|
72
76
|
);
|
|
@@ -76,6 +80,8 @@ namespace margelo::nitro {
|
|
|
76
80
|
obj.setProperty(runtime, PropNameIDCache::get(runtime, "androidAutoEnabled"), JSIConverter<std::optional<bool>>::toJSI(runtime, arg.androidAutoEnabled));
|
|
77
81
|
obj.setProperty(runtime, PropNameIDCache::get(runtime, "carPlayEnabled"), JSIConverter<std::optional<bool>>::toJSI(runtime, arg.carPlayEnabled));
|
|
78
82
|
obj.setProperty(runtime, PropNameIDCache::get(runtime, "showInNotification"), JSIConverter<std::optional<bool>>::toJSI(runtime, arg.showInNotification));
|
|
83
|
+
obj.setProperty(runtime, PropNameIDCache::get(runtime, "remoteSkipForwardInterval"), JSIConverter<std::optional<double>>::toJSI(runtime, arg.remoteSkipForwardInterval));
|
|
84
|
+
obj.setProperty(runtime, PropNameIDCache::get(runtime, "remoteSkipBackwardInterval"), JSIConverter<std::optional<double>>::toJSI(runtime, arg.remoteSkipBackwardInterval));
|
|
79
85
|
obj.setProperty(runtime, PropNameIDCache::get(runtime, "lookaheadCount"), JSIConverter<std::optional<double>>::toJSI(runtime, arg.lookaheadCount));
|
|
80
86
|
obj.setProperty(runtime, PropNameIDCache::get(runtime, "androidNotificationIcon"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.androidNotificationIcon));
|
|
81
87
|
return obj;
|
|
@@ -91,6 +97,8 @@ namespace margelo::nitro {
|
|
|
91
97
|
if (!JSIConverter<std::optional<bool>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "androidAutoEnabled")))) return false;
|
|
92
98
|
if (!JSIConverter<std::optional<bool>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "carPlayEnabled")))) return false;
|
|
93
99
|
if (!JSIConverter<std::optional<bool>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "showInNotification")))) return false;
|
|
100
|
+
if (!JSIConverter<std::optional<double>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "remoteSkipForwardInterval")))) return false;
|
|
101
|
+
if (!JSIConverter<std::optional<double>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "remoteSkipBackwardInterval")))) return false;
|
|
94
102
|
if (!JSIConverter<std::optional<double>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "lookaheadCount")))) return false;
|
|
95
103
|
if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "androidNotificationIcon")))) return false;
|
|
96
104
|
return true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-nitro-player",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0-alpha.1",
|
|
4
4
|
"description": "A powerful audio player library for React Native with playlist management, playback controls, and support for Android Auto and CarPlay",
|
|
5
5
|
"main": "lib/index",
|
|
6
6
|
"module": "lib/index",
|
|
@@ -19,6 +19,7 @@ type TemporaryQueueCallback = (
|
|
|
19
19
|
playNextQueue: TrackItem[],
|
|
20
20
|
upNextQueue: TrackItem[]
|
|
21
21
|
) => void
|
|
22
|
+
type AndroidAutoConnectionCallback = (connected: boolean) => void
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Internal subscription manager that allows multiple hooks to subscribe
|
|
@@ -33,7 +34,10 @@ class CallbackSubscriptionManager {
|
|
|
33
34
|
private timedMetadataSubscribers = new Set<TimedMetadataCallback>()
|
|
34
35
|
private temporaryQueueSubscribers = new Set<TemporaryQueueCallback>()
|
|
35
36
|
private playlistsChangedSubscribers = new Set<() => void>()
|
|
37
|
+
private androidAutoConnectionSubscribers =
|
|
38
|
+
new Set<AndroidAutoConnectionCallback>()
|
|
36
39
|
private isPlaylistsChangedRegistered = false
|
|
40
|
+
private isAndroidAutoConnectionRegistered = false
|
|
37
41
|
private isPlaybackStateRegistered = false
|
|
38
42
|
private isTrackChangeRegistered = false
|
|
39
43
|
private isPlaybackProgressRegistered = false
|
|
@@ -272,6 +276,46 @@ class CallbackSubscriptionManager {
|
|
|
272
276
|
}
|
|
273
277
|
}
|
|
274
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Subscribe to Android Auto connection changes
|
|
281
|
+
* @returns Unsubscribe function
|
|
282
|
+
*/
|
|
283
|
+
subscribeToAndroidAutoConnection(
|
|
284
|
+
callback: AndroidAutoConnectionCallback
|
|
285
|
+
): () => void {
|
|
286
|
+
this.androidAutoConnectionSubscribers.add(callback)
|
|
287
|
+
this.ensureAndroidAutoConnectionRegistered()
|
|
288
|
+
|
|
289
|
+
return () => {
|
|
290
|
+
this.androidAutoConnectionSubscribers.delete(callback)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private ensureAndroidAutoConnectionRegistered(): void {
|
|
295
|
+
if (this.isAndroidAutoConnectionRegistered) return
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
TrackPlayer.onAndroidAutoConnectionChange((connected) => {
|
|
299
|
+
this.androidAutoConnectionSubscribers.forEach((subscriber) => {
|
|
300
|
+
try {
|
|
301
|
+
subscriber(connected)
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.error(
|
|
304
|
+
'[CallbackManager] Error in Android Auto connection subscriber:',
|
|
305
|
+
error
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
this.isAndroidAutoConnectionRegistered = true
|
|
311
|
+
} catch (error) {
|
|
312
|
+
console.error(
|
|
313
|
+
'[CallbackManager] Failed to register Android Auto connection callback:',
|
|
314
|
+
error
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
275
319
|
/**
|
|
276
320
|
* Subscribe to in-stream (timed) metadata events
|
|
277
321
|
* @returns Unsubscribe function
|
|
@@ -73,6 +73,18 @@ export function useActualQueue(): UseActualQueueResult {
|
|
|
73
73
|
}
|
|
74
74
|
}, [])
|
|
75
75
|
|
|
76
|
+
// Skips involving temp tracks fire track-change and temporary-queue-change
|
|
77
|
+
// together; the gate collapses the burst into one getActualQueue() fetch
|
|
78
|
+
const fetchScheduled = useRef(false)
|
|
79
|
+
const scheduleUpdate = useCallback(() => {
|
|
80
|
+
if (fetchScheduled.current) return
|
|
81
|
+
fetchScheduled.current = true
|
|
82
|
+
queueMicrotask(() => {
|
|
83
|
+
fetchScheduled.current = false
|
|
84
|
+
updateQueue()
|
|
85
|
+
})
|
|
86
|
+
}, [updateQueue])
|
|
87
|
+
|
|
76
88
|
const refreshQueue = useCallback(() => {
|
|
77
89
|
if (!isMounted.current) return
|
|
78
90
|
setIsLoading(true)
|
|
@@ -97,25 +109,25 @@ export function useActualQueue(): UseActualQueueResult {
|
|
|
97
109
|
// track changes and temporary-queue edits do.
|
|
98
110
|
useEffect(() => {
|
|
99
111
|
return callbackManager.subscribeToTrackChange(() => {
|
|
100
|
-
|
|
112
|
+
scheduleUpdate()
|
|
101
113
|
})
|
|
102
|
-
}, [
|
|
114
|
+
}, [scheduleUpdate])
|
|
103
115
|
|
|
104
116
|
// Update queue when playNext / upNext change (native pushes the new lists).
|
|
105
117
|
useEffect(() => {
|
|
106
118
|
return callbackManager.subscribeToTemporaryQueueChange(() => {
|
|
107
|
-
|
|
119
|
+
scheduleUpdate()
|
|
108
120
|
})
|
|
109
|
-
}, [
|
|
121
|
+
}, [scheduleUpdate])
|
|
110
122
|
|
|
111
123
|
// Update queue when the playlist itself is edited (track added / removed / reordered).
|
|
112
124
|
// Without this, a playlist mutation while paused would leave the queue stale until the
|
|
113
125
|
// next track change — playback-state changes used to paper over it.
|
|
114
126
|
useEffect(() => {
|
|
115
127
|
return callbackManager.subscribeToPlaylistsChanged(() => {
|
|
116
|
-
|
|
128
|
+
scheduleUpdate()
|
|
117
129
|
})
|
|
118
|
-
}, [
|
|
130
|
+
}, [scheduleUpdate])
|
|
119
131
|
|
|
120
132
|
return { queue, refreshQueue, isLoading }
|
|
121
133
|
}
|