react-native-nitro-player 1.6.0 → 1.7.0-alpha.0

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 (71) hide show
  1. package/README.md +6 -1
  2. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +9 -1
  3. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +23 -3
  4. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +23 -3
  5. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +34 -17
  6. package/android/src/main/java/com/margelo/nitro/nitroplayer/connection/AndroidAutoConnectionDetector.kt +22 -22
  7. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +21 -9
  8. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +19 -0
  9. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +33 -19
  10. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +0 -1
  11. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +9 -5
  12. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadManagerCore.kt +96 -56
  13. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadWorker.kt +62 -22
  14. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/TrackItemJson.kt +60 -0
  15. package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +9 -9
  16. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/AuthAwareHttpDataSourceFactory.kt +12 -3
  17. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/ExoPlayerBuilder.kt +4 -0
  18. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +99 -0
  19. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/PlaybackService.kt +7 -1
  20. package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +85 -70
  21. package/android/src/main/res/values/strings.xml +7 -0
  22. package/ios/HybridDownloadManager.swift +13 -3
  23. package/ios/HybridTrackPlayer.swift +4 -2
  24. package/ios/core/TrackPlayerCast.swift +32 -1
  25. package/ios/core/TrackPlayerCore.swift +19 -3
  26. package/ios/core/TrackPlayerListener.swift +15 -16
  27. package/ios/core/TrackPlayerNotify.swift +11 -11
  28. package/ios/core/TrackPlayerPlayback.swift +54 -16
  29. package/ios/core/TrackPlayerQueueBuild.swift +14 -2
  30. package/ios/core/TrackPlayerTempQueue.swift +1 -0
  31. package/ios/core/TrackPlayerUrlLoader.swift +11 -3
  32. package/ios/download/DownloadDatabase.swift +16 -5
  33. package/ios/download/DownloadFileManager.swift +12 -15
  34. package/ios/download/DownloadManagerCore.swift +61 -16
  35. package/ios/equalizer/EqualizerCore.swift +37 -9
  36. package/ios/media/MediaSessionManager.swift +89 -4
  37. package/ios/playlist/PlaylistManager.swift +47 -46
  38. package/ios/queue/HybridPlayerQueue.swift +28 -7
  39. package/lib/hooks/callbackManager.d.ts +9 -0
  40. package/lib/hooks/callbackManager.js +33 -0
  41. package/lib/hooks/useActualQueue.js +18 -6
  42. package/lib/hooks/useAndroidAutoConnection.js +2 -4
  43. package/lib/hooks/useAudioDevices.js +21 -24
  44. package/lib/hooks/useDownloadActions.js +33 -11
  45. package/lib/hooks/useDownloadProgress.js +20 -14
  46. package/lib/hooks/useDownloadStorage.js +1 -1
  47. package/lib/hooks/useNowPlaying.js +32 -6
  48. package/lib/hooks/useOnSeek.js +2 -2
  49. package/lib/hooks/usePlaylist.js +6 -15
  50. package/lib/specs/TrackPlayer.nitro.d.ts +1 -2
  51. package/lib/types/PlayerQueue.d.ts +8 -0
  52. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +9 -1
  53. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerConfig.kt +8 -2
  54. package/nitrogen/generated/ios/swift/PlayerConfig.swift +37 -1
  55. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +9 -1
  56. package/package.json +1 -1
  57. package/src/hooks/callbackManager.ts +44 -0
  58. package/src/hooks/useActualQueue.ts +18 -6
  59. package/src/hooks/useAndroidAutoConnection.ts +6 -6
  60. package/src/hooks/useAudioDevices.ts +23 -24
  61. package/src/hooks/useDownloadActions.ts +35 -11
  62. package/src/hooks/useDownloadProgress.ts +22 -19
  63. package/src/hooks/useDownloadStorage.ts +4 -3
  64. package/src/hooks/useNowPlaying.ts +34 -6
  65. package/src/hooks/useOnSeek.ts +2 -2
  66. package/src/hooks/usePlaylist.ts +6 -14
  67. package/src/specs/TrackPlayer.nitro.ts +1 -2
  68. package/src/types/PlayerQueue.ts +8 -0
  69. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/NitroPlayerNotificationProvider.kt +0 -200
  70. package/android/src/main/java/com/margelo/nitro/nitroplayer/queue/Queue.kt +0 -94
  71. 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
- const downloadTrack = useCallback(async (track, playlistId) => {
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
- setIsDownloading(false);
44
+ endDownloading();
23
45
  }
24
46
  }, []);
25
47
  const downloadPlaylist = useCallback(async (playlistId, tracks) => {
26
- setIsDownloading(true);
48
+ beginDownloading();
27
49
  setError(null);
28
50
  try {
29
51
  const downloadIds = await DownloadManager.downloadPlaylist(playlistId, tracks);
@@ -34,7 +56,7 @@ export function useDownloadActions() {
34
56
  throw e;
35
57
  }
36
58
  finally {
37
- setIsDownloading(false);
59
+ endDownloading();
38
60
  }
39
61
  }, []);
40
62
  const pauseDownload = useCallback(async (downloadId) => {
@@ -59,30 +81,30 @@ export function useDownloadActions() {
59
81
  await DownloadManager.cancelAllDownloads();
60
82
  }, []);
61
83
  const deleteTrack = useCallback(async (trackId) => {
62
- setIsDeleting(true);
84
+ beginDeleting();
63
85
  try {
64
86
  await DownloadManager.deleteDownloadedTrack(trackId);
65
87
  }
66
88
  finally {
67
- setIsDeleting(false);
89
+ endDeleting();
68
90
  }
69
91
  }, []);
70
92
  const deletePlaylist = useCallback(async (playlistId) => {
71
- setIsDeleting(true);
93
+ beginDeleting();
72
94
  try {
73
95
  await DownloadManager.deleteDownloadedPlaylist(playlistId);
74
96
  }
75
97
  finally {
76
- setIsDeleting(false);
98
+ endDeleting();
77
99
  }
78
100
  }, []);
79
101
  const deleteAll = useCallback(async () => {
80
- setIsDeleting(true);
102
+ beginDeleting();
81
103
  try {
82
104
  await DownloadManager.deleteAllDownloads();
83
105
  }
84
106
  finally {
85
- setIsDeleting(false);
107
+ endDeleting();
86
108
  }
87
109
  }, []);
88
110
  const configure = useCallback((config) => {
@@ -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
- if (trackIds &&
13
- trackIds.length > 0 &&
14
- !trackIds.includes(progress.trackId)) {
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 (downloadIds &&
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
- }, [trackIds, downloadIds, activeOnly]);
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 = Array.from(progressMap.values());
73
- const overallProgress = progressList.length > 0
74
- ? progressList.reduce((sum, p) => sum + p.progress, 0) /
75
- progressList.length
76
- : 0;
77
- const isDownloading = progressList.some((p) => p.state === 'downloading');
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(newState);
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
- fetchFullState();
97
+ eventsOwnPosition.current = false;
98
+ requestFullState();
75
99
  });
76
- }, [fetchFullState]);
100
+ }, [requestFullState]);
77
101
  // Subscribe to playback state changes — full refresh
78
102
  useEffect(() => {
79
103
  return callbackManager.subscribeToPlaybackState(() => {
80
- fetchFullState();
104
+ requestFullState();
81
105
  });
82
- }, [fetchFullState]);
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
  }, []);
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useState } from 'react';
2
- import { TrackPlayer } from '../index';
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
- TrackPlayer.onSeek((newPosition, newTotalDuration) => {
11
+ return callbackManager.subscribeToSeek((newPosition, newTotalDuration) => {
12
12
  setPosition(newPosition);
13
13
  setTotalDuration(newTotalDuration);
14
14
  });
@@ -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 (only once)
121
+ // Subscribe to native playlist changes via the shared manager
123
122
  useEffect(() => {
124
- if (hasSubscribed.current)
125
- return;
126
- hasSubscribed.current = true;
127
- try {
128
- PlayerQueue.onPlaylistsChanged(() => {
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
- * Get the current track index in the active playlist
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.6.0",
3
+ "version": "1.7.0-alpha.0",
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
- updateQueue()
112
+ scheduleUpdate()
101
113
  })
102
- }, [updateQueue])
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
- updateQueue()
119
+ scheduleUpdate()
108
120
  })
109
- }, [updateQueue])
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
- updateQueue()
128
+ scheduleUpdate()
117
129
  })
118
- }, [updateQueue])
130
+ }, [scheduleUpdate])
119
131
 
120
132
  return { queue, refreshQueue, isLoading }
121
133
  }
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useState } from 'react'
2
2
  import { TrackPlayer } from '../index'
3
+ import { callbackManager } from './callbackManager'
3
4
 
4
5
  /**
5
6
  * Hook to detect Android Auto connection status using the official Android for Cars API
@@ -15,15 +16,14 @@ export function useAndroidAutoConnection() {
15
16
  const [isConnected, setIsConnected] = useState<boolean>(false)
16
17
 
17
18
  useEffect(() => {
18
- // Set initial state
19
19
  const initialState = TrackPlayer.isAndroidAutoConnected()
20
20
  setIsConnected(initialState)
21
21
 
22
- // Listen for connection changes
23
- TrackPlayer.onAndroidAutoConnectionChange((connected: boolean) => {
24
- setIsConnected(connected)
25
- console.log('🚗 Android Auto connection changed:', connected)
26
- })
22
+ return callbackManager.subscribeToAndroidAutoConnection(
23
+ (connected: boolean) => {
24
+ setIsConnected(connected)
25
+ }
26
+ )
27
27
  }, [])
28
28
 
29
29
  return { isConnected }