react-native-nitro-player 1.7.0-alpha.3 → 1.7.0-alpha.5

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 (98) hide show
  1. package/README.md +9 -0
  2. package/android/src/main/AndroidManifest.xml +8 -0
  3. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +51 -0
  4. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +45 -7
  5. package/android/src/main/java/com/margelo/nitro/nitroplayer/connection/AndroidAutoConnectionDetector.kt +2 -2
  6. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ExoPlayerCore.kt +17 -2
  7. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/NitroPlayerLogger.kt +2 -2
  8. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerAndroidAuto.kt +41 -31
  9. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerAudioFocus.kt +65 -0
  10. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCast.kt +1 -1
  11. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +92 -15
  12. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +12 -17
  13. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerNotify.kt +5 -0
  14. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +39 -13
  15. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueue.kt +1 -1
  16. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +25 -4
  17. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerRecovery.kt +97 -0
  18. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +0 -5
  19. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +22 -5
  20. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +3 -0
  21. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/TrackItemJson.kt +6 -0
  22. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/AudioFocusController.kt +87 -0
  23. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/CastSessionController.kt +2 -0
  24. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/ExoPlayerBuilder.kt +3 -0
  25. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaBrowserService.kt +96 -45
  26. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaLibrary.kt +2 -0
  27. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaLibraryManager.kt +147 -0
  28. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaLibraryParser.kt +3 -0
  29. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionCallbackFactory.kt +102 -24
  30. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +51 -3
  31. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/PlaybackService.kt +82 -1
  32. package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +75 -0
  33. package/ios/cast/CastQueueDiff.swift +58 -0
  34. package/ios/cast/CastQueueReconciler.swift +228 -0
  35. package/ios/core/CastSessionManager.swift +215 -31
  36. package/ios/core/TrackPlayerCast.swift +96 -65
  37. package/ios/core/TrackPlayerCore.swift +31 -0
  38. package/ios/core/TrackPlayerListener.swift +19 -3
  39. package/ios/core/TrackPlayerPlayback.swift +12 -10
  40. package/ios/core/TrackPlayerQueue.swift +10 -2
  41. package/ios/core/TrackPlayerQueueBuild.swift +21 -9
  42. package/ios/core/TrackPlayerTempQueue.swift +27 -15
  43. package/ios/core/TrackPlayerUrlLoader.swift +6 -2
  44. package/ios/playlist/PlaylistManager.swift +60 -1
  45. package/ios/queue/HybridPlayerQueue.swift +40 -13
  46. package/lib/hooks/callbackManager.d.ts +2 -0
  47. package/lib/hooks/callbackManager.js +23 -8
  48. package/lib/index.js +5 -1
  49. package/lib/specs/AndroidAutoMediaLibrary.nitro.d.ts +12 -0
  50. package/lib/specs/TrackPlayer.nitro.d.ts +5 -1
  51. package/lib/types/AndroidAutoMediaLibrary.d.ts +11 -0
  52. package/lib/types/PlayerQueue.d.ts +14 -0
  53. package/lib/utils/androidAutoMediaLibrary.d.ts +13 -1
  54. package/lib/utils/androidAutoMediaLibrary.js +23 -0
  55. package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +6 -0
  56. package/nitrogen/generated/android/c++/JAndroidAudioFocusMode.hpp +61 -0
  57. package/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
  58. package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +76 -0
  59. package/nitrogen/generated/android/c++/JFunc_void_std__string_std__string.hpp +76 -0
  60. package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.cpp +29 -0
  61. package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.hpp +6 -0
  62. package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +43 -0
  63. package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.hpp +2 -0
  64. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +4 -0
  65. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +9 -3
  66. package/nitrogen/generated/android/c++/JShufflePlaylistOptions.hpp +57 -0
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/AndroidAudioFocusMode.kt +24 -0
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void.kt +80 -0
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__string.kt +80 -0
  70. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__string_std__string.kt +80 -0
  71. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrarySpec.kt +44 -0
  72. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridPlayerQueueSpec.kt +8 -0
  73. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerConfig.kt +6 -3
  74. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/ShufflePlaylistOptions.kt +38 -0
  75. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +36 -0
  76. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +6 -0
  77. package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.hpp +19 -0
  78. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +3 -0
  79. package/nitrogen/generated/ios/swift/AndroidAudioFocusMode.swift +44 -0
  80. package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec.swift +2 -0
  81. package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec_cxx.swift +38 -0
  82. package/nitrogen/generated/ios/swift/PlayerConfig.swift +12 -1
  83. package/nitrogen/generated/ios/swift/ShufflePlaylistOptions.swift +42 -0
  84. package/nitrogen/generated/shared/c++/AndroidAudioFocusMode.hpp +80 -0
  85. package/nitrogen/generated/shared/c++/HybridAndroidAutoMediaLibrarySpec.cpp +6 -0
  86. package/nitrogen/generated/shared/c++/HybridAndroidAutoMediaLibrarySpec.hpp +7 -0
  87. package/nitrogen/generated/shared/c++/HybridPlayerQueueSpec.cpp +2 -0
  88. package/nitrogen/generated/shared/c++/HybridPlayerQueueSpec.hpp +5 -0
  89. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +9 -3
  90. package/nitrogen/generated/shared/c++/ShufflePlaylistOptions.hpp +83 -0
  91. package/package.json +1 -1
  92. package/src/hooks/callbackManager.ts +25 -10
  93. package/src/index.ts +9 -1
  94. package/src/specs/AndroidAutoMediaLibrary.nitro.ts +18 -0
  95. package/src/specs/TrackPlayer.nitro.ts +11 -0
  96. package/src/types/AndroidAutoMediaLibrary.ts +24 -0
  97. package/src/types/PlayerQueue.ts +17 -0
  98. package/src/utils/androidAutoMediaLibrary.ts +48 -1
@@ -14,6 +14,7 @@ class PlaylistManager {
14
14
  private var listeners: [(String, ([PlaylistModel], QueueOperation?) -> Void)] = []
15
15
  private var playlistListeners: [String: [(String, (PlaylistModel, QueueOperation?) -> Void)]] =
16
16
  [:]
17
+ private var anyPlaylistListeners: [(String, (PlaylistModel, QueueOperation?) -> Void)] = []
17
18
  // Backing store must only be touched inside `queue`; every current use of the
18
19
  // computed property is outside a queue.sync block, so the accessors can sync.
19
20
  private var _currentPlaylistId: String?
@@ -257,6 +258,48 @@ class PlaylistManager {
257
258
  return true
258
259
  }
259
260
 
261
+ /// Removed count; nil when the playlist does not exist. Unknown ids are ignored.
262
+ func removeTracksFromPlaylist(playlistId: String, trackIds: [String]) -> Int? {
263
+ let ids = Set(trackIds)
264
+ let removed: Int? = queue.sync {
265
+ guard let playlist = playlists[playlistId] else { return nil }
266
+ let tracks = playlist.tracks.filter { !ids.contains($0.id) }
267
+ let count = playlist.tracks.count - tracks.count
268
+ if count > 0 {
269
+ playlists[playlistId] = PlaylistModel(
270
+ id: playlist.id, name: playlist.name, description: playlist.description,
271
+ artwork: playlist.artwork, tracks: tracks)
272
+ }
273
+ return count
274
+ }
275
+ if let removed, removed > 0 {
276
+ scheduleSave()
277
+ notifyPlaylistChanged(playlistId, .remove)
278
+ }
279
+ return removed
280
+ }
281
+
282
+ /// Fisher-Yates; `firstTrackId` is pinned at index 0. Nil when the playlist does not exist.
283
+ func shufflePlaylist(playlistId: String, firstTrackId: String?) -> Bool? {
284
+ let changed: Bool? = queue.sync {
285
+ guard let playlist = playlists[playlistId] else { return nil }
286
+ var tracks = playlist.tracks
287
+ let pinned = tracks.firstIndex { $0.id == firstTrackId }.map { tracks.remove(at: $0) }
288
+ tracks.shuffle()
289
+ if let pinned { tracks.insert(pinned, at: 0) }
290
+ guard tracks.map(\.id) != playlist.tracks.map(\.id) else { return false }
291
+ playlists[playlistId] = PlaylistModel(
292
+ id: playlist.id, name: playlist.name, description: playlist.description,
293
+ artwork: playlist.artwork, tracks: tracks)
294
+ return true
295
+ }
296
+ if changed == true {
297
+ scheduleSave()
298
+ notifyPlaylistChanged(playlistId, .update)
299
+ }
300
+ return changed
301
+ }
302
+
260
303
  /**
261
304
  * Load a playlist for playback (sets it as current)
262
305
  */
@@ -343,6 +386,11 @@ class PlaylistManager {
343
386
  return trackIds.compactMap { foundTracks[$0] }
344
387
  }
345
388
 
389
+ /// Mirrors the core's current playlist; playSong bypasses loadPlaylist.
390
+ func setCurrentPlaylistId(_ playlistId: String?) {
391
+ currentPlaylistId = playlistId
392
+ }
393
+
346
394
  /**
347
395
  * Get the current playlist ID
348
396
  */
@@ -403,11 +451,22 @@ class PlaylistManager {
403
451
  currentListeners.forEach { $0.1(allPlaylists, operation) }
404
452
  }
405
453
 
454
+ /// Fires for ANY playlist, including ones created after registration.
455
+ func addAnyPlaylistChangeListener(
456
+ listener: @escaping (PlaylistModel, QueueOperation?) -> Void
457
+ ) -> () -> Void {
458
+ let listenerId = UUID().uuidString
459
+ queue.sync { anyPlaylistListeners.append((listenerId, listener)) }
460
+ return {
461
+ self.queue.sync { self.anyPlaylistListeners.removeAll { $0.0 == listenerId } }
462
+ }
463
+ }
464
+
406
465
  private func notifyPlaylistChanged(_ playlistId: String, _ operation: QueueOperation?) {
407
466
  let result: (PlaylistModel, [(String, (PlaylistModel, QueueOperation?) -> Void)])? = queue.sync
408
467
  {
409
468
  guard let p = playlists[playlistId] else { return nil }
410
- return (p, playlistListeners[playlistId] ?? [])
469
+ return (p, (playlistListeners[playlistId] ?? []) + anyPlaylistListeners)
411
470
  }
412
471
 
413
472
  guard let (playlist, currentListeners) = result else { return }
@@ -73,14 +73,9 @@ final class HybridPlayerQueue: HybridPlayerQueueSpec {
73
73
  }
74
74
  }
75
75
 
76
+ /// Same semantics as the batch form: unknown ids are skipped, only a missing playlist rejects.
76
77
  func removeTrackFromPlaylist(playlistId: String, trackId: String) throws -> Promise<Void> {
77
- Promise.async {
78
- guard self.playlistManager.removeTrackFromPlaylist(playlistId: playlistId, trackId: trackId)
79
- else {
80
- throw RuntimeError.error(withMessage: "Track \(trackId) not found in playlist \(playlistId)")
81
- }
82
- self.core.updatePlaylist(playlistId: playlistId)
83
- }
78
+ try removeTracksFromPlaylist(playlistId: playlistId, trackIds: [trackId])
84
79
  }
85
80
 
86
81
  func reorderTrackInPlaylist(playlistId: String, trackId: String, newIndex: Double) throws -> Promise<Void> {
@@ -94,6 +89,40 @@ final class HybridPlayerQueue: HybridPlayerQueueSpec {
94
89
  }
95
90
  }
96
91
 
92
+ /// On the player queue: the live queue must reflect the removal before the promise resolves.
93
+ func removeTracksFromPlaylist(playlistId: String, trackIds: [String]) throws -> Promise<Void> {
94
+ let promise = Promise<Void>()
95
+ core.playerQueue.async {
96
+ guard let removed = self.playlistManager.removeTracksFromPlaylist(playlistId: playlistId, trackIds: trackIds) else {
97
+ promise.reject(withError: RuntimeError.error(withMessage: "Playlist not found: \(playlistId)"))
98
+ return
99
+ }
100
+ if removed > 0, self.core.currentPlaylistId == playlistId {
101
+ self.core.syncCurrentPlaylistOnQueue(playlistId: playlistId)
102
+ }
103
+ promise.resolve(withResult: ())
104
+ }
105
+ return promise
106
+ }
107
+
108
+ /// On the player queue: the anchor id is player-queue state.
109
+ func shufflePlaylist(playlistId: String, options: ShufflePlaylistOptions?) throws -> Promise<Void> {
110
+ let promise = Promise<Void>()
111
+ core.playerQueue.async {
112
+ let isCurrent = self.core.currentPlaylistId == playlistId
113
+ let pin: String? =
114
+ (isCurrent && options?.keepCurrentTrackFirst != false)
115
+ ? self.core.currentTracks[safe: self.core.currentTrackIndex]?.id : nil
116
+ guard let changed = self.playlistManager.shufflePlaylist(playlistId: playlistId, firstTrackId: pin) else {
117
+ promise.reject(withError: RuntimeError.error(withMessage: "Playlist not found: \(playlistId)"))
118
+ return
119
+ }
120
+ if changed, isCurrent { self.core.syncCurrentPlaylistOnQueue(playlistId: playlistId) }
121
+ promise.resolve(withResult: ())
122
+ }
123
+ return promise
124
+ }
125
+
97
126
  // MARK: - Playback control
98
127
 
99
128
  /// Enqueued synchronously on the player queue so a `loadPlaylist()` followed by
@@ -130,13 +159,11 @@ final class HybridPlayerQueue: HybridPlayerQueueSpec {
130
159
  }
131
160
 
132
161
  func onPlaylistChanged(callback: @escaping (_ playlistId: String, _ playlist: Playlist, _ operation: QueueOperation?) -> Void) throws {
133
- let allPlaylists = playlistManager.getAllPlaylists()
134
- for playlist in allPlaylists {
135
- let remover = playlistManager.addPlaylistChangeListener(playlistId: playlist.id) { updated, operation in
136
- callback(updated.id, updated.toGeneratedPlaylist(), operation)
137
- }
138
- playlistChangeRemovers.append(remover)
162
+ // Manager-level listener: covers playlists created after registration.
163
+ let remover = playlistManager.addAnyPlaylistChangeListener { updated, operation in
164
+ callback(updated.id, updated.toGeneratedPlaylist(), operation)
139
165
  }
166
+ playlistChangeRemovers.append(remover)
140
167
  }
141
168
 
142
169
  // MARK: - Cleanup
@@ -21,6 +21,7 @@ declare class CallbackSubscriptionManager {
21
21
  private playlistsChangedSubscribers;
22
22
  private androidAutoConnectionSubscribers;
23
23
  private isPlaylistsChangedRegistered;
24
+ private playlistsChangedFanOutScheduled;
24
25
  private isAndroidAutoConnectionRegistered;
25
26
  private isPlaybackStateRegistered;
26
27
  private isTrackChangeRegistered;
@@ -61,6 +62,7 @@ declare class CallbackSubscriptionManager {
61
62
  * @returns Unsubscribe function
62
63
  */
63
64
  subscribeToPlaylistsChanged(callback: () => void): () => void;
65
+ private schedulePlaylistsChangedFanOut;
64
66
  private ensurePlaylistsChangedRegistered;
65
67
  private ensureTemporaryQueueRegistered;
66
68
  private ensureSeekRegistered;
@@ -14,6 +14,7 @@ class CallbackSubscriptionManager {
14
14
  playlistsChangedSubscribers = new Set();
15
15
  androidAutoConnectionSubscribers = new Set();
16
16
  isPlaylistsChangedRegistered = false;
17
+ playlistsChangedFanOutScheduled = false;
17
18
  isAndroidAutoConnectionRegistered = false;
18
19
  isPlaybackStateRegistered = false;
19
20
  isTrackChangeRegistered = false;
@@ -147,19 +148,33 @@ class CallbackSubscriptionManager {
147
148
  this.playlistsChangedSubscribers.delete(callback);
148
149
  };
149
150
  }
151
+ // Task-level, not a microtask: native callbacks arrive as separate JS tasks.
152
+ schedulePlaylistsChangedFanOut() {
153
+ if (this.playlistsChangedFanOutScheduled)
154
+ return;
155
+ this.playlistsChangedFanOutScheduled = true;
156
+ setTimeout(() => {
157
+ this.playlistsChangedFanOutScheduled = false;
158
+ this.playlistsChangedSubscribers.forEach((subscriber) => {
159
+ try {
160
+ subscriber();
161
+ }
162
+ catch (error) {
163
+ console.error('[CallbackManager] Error in playlists changed subscriber:', error);
164
+ }
165
+ });
166
+ }, 0);
167
+ }
150
168
  ensurePlaylistsChangedRegistered() {
151
169
  if (this.isPlaylistsChangedRegistered)
152
170
  return;
153
171
  try {
154
172
  PlayerQueue.onPlaylistsChanged(() => {
155
- this.playlistsChangedSubscribers.forEach((subscriber) => {
156
- try {
157
- subscriber();
158
- }
159
- catch (error) {
160
- console.error('[CallbackManager] Error in playlists changed subscriber:', error);
161
- }
162
- });
173
+ this.schedulePlaylistsChangedFanOut();
174
+ });
175
+ // Track-level mutations only fire the per-playlist event.
176
+ PlayerQueue.onPlaylistChanged(() => {
177
+ this.schedulePlaylistsChangedFanOut();
163
178
  });
164
179
  this.isPlaylistsChangedRegistered = true;
165
180
  }
package/lib/index.js CHANGED
@@ -1,6 +1,10 @@
1
1
  // TODO: Export all HybridObjects here for the user
2
2
  import { NitroModules } from 'react-native-nitro-modules';
3
- import { Platform } from 'react-native';
3
+ import { AppRegistry, Platform } from 'react-native';
4
+ // Native runs this while it needs JS without a resumed activity (car, screen off): it keeps JS timers ticking
5
+ if (Platform.OS === 'android') {
6
+ AppRegistry.registerHeadlessTask('NitroPlayerKeepAlive', () => () => new Promise(() => { }));
7
+ }
4
8
  export const PlayerQueue = NitroModules.createHybridObject('PlayerQueue');
5
9
  export const TrackPlayer = NitroModules.createHybridObject('TrackPlayer');
6
10
  // Android-only: Android Auto Media Library
@@ -18,4 +18,16 @@ export interface AndroidAutoMediaLibrary extends HybridObject<{
18
18
  * Falls back to showing all playlists
19
19
  */
20
20
  clearMediaLibrary(): Promise<void>;
21
+ /** Android Auto opened a folder published with `children: []`; answer with {@link resolveRequest}. */
22
+ onLoadChildren(callback: (requestId: string, parentId: string) => void): void;
23
+ /** A car search, or a voice request the loaded playlists cannot match; answer with {@link resolveRequest}. */
24
+ onSearch(callback: (requestId: string, query: string) => void): void;
25
+ /** Answers {@link onLoadChildren} / {@link onSearch} with a JSON array of MediaItem. */
26
+ resolveRequest(requestId: string, itemsJson: string): void;
27
+ /** Replaces the session buttons with a JSON array of SessionButton. */
28
+ setSessionButtons(buttonsJson: string): void;
29
+ /** Called with the button's `action` when a session button is pressed. */
30
+ onSessionButtonPress(callback: (action: string) => void): void;
31
+ /** A controller asked to play with nothing loaded (car auto-play, play on connect); load the last queue now. */
32
+ onPlaybackResumption(callback: () => void): void;
21
33
  }
@@ -1,5 +1,5 @@
1
1
  import type { HybridObject } from 'react-native-nitro-modules';
2
- import type { QueueOperation, Reason, TrackItem, TrackPlayerState, PlayerState, PlayerConfig, Playlist, TimedMetadata } from '../types/PlayerQueue';
2
+ import type { QueueOperation, Reason, TrackItem, TrackPlayerState, PlayerState, PlayerConfig, Playlist, TimedMetadata, ShufflePlaylistOptions } from '../types/PlayerQueue';
3
3
  export interface PlayerQueue extends HybridObject<{
4
4
  android: 'kotlin';
5
5
  ios: 'swift';
@@ -13,6 +13,10 @@ export interface PlayerQueue extends HybridObject<{
13
13
  addTracksToPlaylist(playlistId: string, tracks: TrackItem[], index?: number): Promise<void>;
14
14
  removeTrackFromPlaylist(playlistId: string, trackId: string): Promise<void>;
15
15
  reorderTrackInPlaylist(playlistId: string, trackId: string, newIndex: number): Promise<void>;
16
+ /** One native pass. Unknown ids are skipped; rejects only if the playlist does not exist. */
17
+ removeTracksFromPlaylist(playlistId: string, trackIds: string[]): Promise<void>;
18
+ /** Fisher-Yates shuffle of the stored playlist, persisted. Never interrupts the playing item. */
19
+ shufflePlaylist(playlistId: string, options?: ShufflePlaylistOptions): Promise<void>;
16
20
  loadPlaylist(playlistId: string, index?: number): Promise<void>;
17
21
  getCurrentPlaylistId(): string | null;
18
22
  onPlaylistsChanged(callback: (playlists: Playlist[], operation?: QueueOperation) => void): void;
@@ -28,6 +28,8 @@ export interface MediaItem {
28
28
  children?: MediaItem[];
29
29
  /** Layout type for folder items (overrides library default) */
30
30
  layoutType?: LayoutType;
31
+ /** Group header; contiguous items sharing one are listed under it */
32
+ groupTitle?: string;
31
33
  }
32
34
  /**
33
35
  * Media library structure for Android Auto
@@ -42,3 +44,12 @@ export interface MediaLibrary {
42
44
  /** Optional app icon URL */
43
45
  appIconUrl?: string;
44
46
  }
47
+ export type SessionButtonIcon = 'heart' | 'heart_filled' | 'star' | 'star_filled' | 'thumb_up' | 'thumb_up_filled' | 'plus' | 'check';
48
+ /** App-defined button beside the transport controls in Android Auto and the notification */
49
+ export interface SessionButton {
50
+ /** Passed to the press callback */
51
+ action: string;
52
+ /** Accessibility label */
53
+ title: string;
54
+ icon: SessionButtonIcon;
55
+ }
@@ -18,6 +18,10 @@ export interface Playlist {
18
18
  tracks: TrackItem[];
19
19
  }
20
20
  export type QueueOperation = 'add' | 'remove' | 'clear' | 'update';
21
+ export interface ShufflePlaylistOptions {
22
+ /** Move the playing track to index 0 and shuffle the rest behind it (default: true). */
23
+ keepCurrentTrackFirst?: boolean;
24
+ }
21
25
  export type TrackPlayerState = 'playing' | 'paused' | 'stopped' | 'buffering';
22
26
  export type Reason = 'user_action' | 'skip' | 'end' | 'error' | 'repeat';
23
27
  export interface PlayerState {
@@ -44,6 +48,14 @@ export interface TimedMetadata {
44
48
  /** Artwork URL the stream tagged as artwork, when it sends one explicitly */
45
49
  artworkUrl?: string;
46
50
  }
51
+ /**
52
+ * How Android reacts when another app takes audio focus.
53
+ * - 'pause': pause for any interruption and resume after it (default, today's behaviour)
54
+ * - 'duck': keep playing at a reduced volume through transient interruptions
55
+ * - 'ignore': never react to audio focus at all
56
+ * iOS ignores this option.
57
+ */
58
+ export type AndroidAudioFocusMode = 'pause' | 'duck' | 'ignore';
47
59
  export interface PlayerConfig {
48
60
  androidAutoEnabled?: boolean;
49
61
  carPlayEnabled?: boolean;
@@ -62,4 +74,6 @@ export interface PlayerConfig {
62
74
  */
63
75
  lookaheadCount?: number;
64
76
  androidNotificationIcon?: string;
77
+ /** How Android reacts when another app takes audio focus (default: 'pause'). */
78
+ androidAudioFocus?: AndroidAudioFocusMode;
65
79
  }
@@ -1,4 +1,5 @@
1
- import type { MediaLibrary } from '../types/AndroidAutoMediaLibrary';
1
+ import type { MediaItem, MediaLibrary, SessionButton } from '../types/AndroidAutoMediaLibrary';
2
+ type ItemsProvider = (argument: string) => Promise<MediaItem[] | null | undefined>;
2
3
  /**
3
4
  * Helper utilities for Android Auto Media Library
4
5
  * Android-only functionality
@@ -40,8 +41,19 @@ export declare const AndroidAutoMediaLibraryHelper: {
40
41
  * Falls back to showing all playlists
41
42
  */
42
43
  clear: () => void;
44
+ /** Children of folders published with `children: []`; playable ids are `${playlistId}:${trackId}`, empty after 10s. Register at module scope: the car can start the app without UI. */
45
+ setChildrenLoader: (loader: ItemsProvider) => void;
46
+ /** Answers car searches and unmatched voice requests (first playable result plays); search shows from the next connection. */
47
+ setSearchHandler: (handler: ItemsProvider) => void;
48
+ /** Buttons beside the transport controls in Android Auto and the notification; call again to change their state. */
49
+ setSessionButtons: (buttons: SessionButton[]) => void;
50
+ /** Called with the pressed button's `action`. */
51
+ onSessionButtonPress: (callback: (action: string) => void) => void;
52
+ /** Car auto-play or play-on-connect found nothing loaded: restore your last queue (playback starts once it is loaded). */
53
+ onPlaybackResumption: (callback: () => void) => void;
43
54
  /**
44
55
  * Check if Android Auto Media Library is available (Android only)
45
56
  */
46
57
  isAvailable: () => boolean;
47
58
  };
59
+ export {};
@@ -1,4 +1,7 @@
1
1
  import { AndroidAutoMediaLibrary as AndroidAutoMediaLibraryModule } from '../index';
2
+ const answer = (requestId, provide) => provide()
3
+ .catch(() => null)
4
+ .then((items) => AndroidAutoMediaLibraryModule?.resolveRequest(requestId, JSON.stringify(items ?? [])));
2
5
  /**
3
6
  * Helper utilities for Android Auto Media Library
4
7
  * Android-only functionality
@@ -53,6 +56,26 @@ export const AndroidAutoMediaLibraryHelper = {
53
56
  }
54
57
  AndroidAutoMediaLibraryModule.clearMediaLibrary();
55
58
  },
59
+ /** Children of folders published with `children: []`; playable ids are `${playlistId}:${trackId}`, empty after 10s. Register at module scope: the car can start the app without UI. */
60
+ setChildrenLoader: (loader) => {
61
+ AndroidAutoMediaLibraryModule?.onLoadChildren((requestId, parentId) => answer(requestId, () => loader(parentId)));
62
+ },
63
+ /** Answers car searches and unmatched voice requests (first playable result plays); search shows from the next connection. */
64
+ setSearchHandler: (handler) => {
65
+ AndroidAutoMediaLibraryModule?.onSearch((requestId, query) => answer(requestId, () => handler(query)));
66
+ },
67
+ /** Buttons beside the transport controls in Android Auto and the notification; call again to change their state. */
68
+ setSessionButtons: (buttons) => {
69
+ AndroidAutoMediaLibraryModule?.setSessionButtons(JSON.stringify(buttons));
70
+ },
71
+ /** Called with the pressed button's `action`. */
72
+ onSessionButtonPress: (callback) => {
73
+ AndroidAutoMediaLibraryModule?.onSessionButtonPress(callback);
74
+ },
75
+ /** Car auto-play or play-on-connect found nothing loaded: restore your last queue (playback starts once it is loaded). */
76
+ onPlaybackResumption: (callback) => {
77
+ AndroidAutoMediaLibraryModule?.onPlaybackResumption(callback);
78
+ },
56
79
  /**
57
80
  * Check if Android Auto Media Library is available (Android only)
58
81
  */
@@ -16,6 +16,9 @@
16
16
  #include <NitroModules/HybridObjectRegistry.hpp>
17
17
 
18
18
  #include "JHybridAndroidAutoMediaLibrarySpec.hpp"
19
+ #include "JFunc_void_std__string_std__string.hpp"
20
+ #include "JFunc_void_std__string.hpp"
21
+ #include "JFunc_void.hpp"
19
22
  #include "JHybridAudioDevicesSpec.hpp"
20
23
  #include "JHybridCastSpec.hpp"
21
24
  #include "JFunc_void_CastState_std__optional_std__variant_nitro__NullType__std__string__.hpp"
@@ -112,6 +115,9 @@ void registerAllNatives() {
112
115
 
113
116
  // Register native JNI methods
114
117
  margelo::nitro::nitroplayer::JHybridAndroidAutoMediaLibrarySpec::CxxPart::registerNatives();
118
+ margelo::nitro::nitroplayer::JFunc_void_std__string_std__string_cxx::registerNatives();
119
+ margelo::nitro::nitroplayer::JFunc_void_std__string_cxx::registerNatives();
120
+ margelo::nitro::nitroplayer::JFunc_void_cxx::registerNatives();
115
121
  margelo::nitro::nitroplayer::JHybridAudioDevicesSpec::CxxPart::registerNatives();
116
122
  margelo::nitro::nitroplayer::JHybridCastSpec::CxxPart::registerNatives();
117
123
  margelo::nitro::nitroplayer::JFunc_void_CastState_std__optional_std__variant_nitro__NullType__std__string___cxx::registerNatives();
@@ -0,0 +1,61 @@
1
+ ///
2
+ /// JAndroidAudioFocusMode.hpp
3
+ /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
4
+ /// https://github.com/mrousavy/nitro
5
+ /// Copyright © Marc Rousavy @ Margelo
6
+ ///
7
+
8
+ #pragma once
9
+
10
+ #include <fbjni/fbjni.h>
11
+ #include "AndroidAudioFocusMode.hpp"
12
+
13
+ namespace margelo::nitro::nitroplayer {
14
+
15
+ using namespace facebook;
16
+
17
+ /**
18
+ * The C++ JNI bridge between the C++ enum "AndroidAudioFocusMode" and the the Kotlin enum "AndroidAudioFocusMode".
19
+ */
20
+ struct JAndroidAudioFocusMode final: public jni::JavaClass<JAndroidAudioFocusMode> {
21
+ public:
22
+ static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/nitroplayer/AndroidAudioFocusMode;";
23
+
24
+ public:
25
+ /**
26
+ * Convert this Java/Kotlin-based enum to the C++ enum AndroidAudioFocusMode.
27
+ */
28
+ [[maybe_unused]]
29
+ [[nodiscard]]
30
+ AndroidAudioFocusMode toCpp() const {
31
+ static const auto clazz = javaClassStatic();
32
+ static const auto fieldOrdinal = clazz->getField<int>("value");
33
+ int ordinal = this->getFieldValue(fieldOrdinal);
34
+ return static_cast<AndroidAudioFocusMode>(ordinal);
35
+ }
36
+
37
+ public:
38
+ /**
39
+ * Create a Java/Kotlin-based enum with the given C++ enum's value.
40
+ */
41
+ [[maybe_unused]]
42
+ static jni::alias_ref<JAndroidAudioFocusMode> fromCpp(AndroidAudioFocusMode value) {
43
+ static const auto clazz = javaClassStatic();
44
+ switch (value) {
45
+ case AndroidAudioFocusMode::PAUSE:
46
+ static const auto fieldPAUSE = clazz->getStaticField<JAndroidAudioFocusMode>("PAUSE");
47
+ return clazz->getStaticFieldValue(fieldPAUSE);
48
+ case AndroidAudioFocusMode::DUCK:
49
+ static const auto fieldDUCK = clazz->getStaticField<JAndroidAudioFocusMode>("DUCK");
50
+ return clazz->getStaticFieldValue(fieldDUCK);
51
+ case AndroidAudioFocusMode::IGNORE:
52
+ static const auto fieldIGNORE = clazz->getStaticField<JAndroidAudioFocusMode>("IGNORE");
53
+ return clazz->getStaticFieldValue(fieldIGNORE);
54
+ default:
55
+ std::string stringValue = std::to_string(static_cast<int>(value));
56
+ throw std::invalid_argument("Invalid enum value (" + stringValue + "!");
57
+ }
58
+ }
59
+ };
60
+
61
+ } // namespace margelo::nitro::nitroplayer
@@ -0,0 +1,75 @@
1
+ ///
2
+ /// JFunc_void.hpp
3
+ /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
4
+ /// https://github.com/mrousavy/nitro
5
+ /// Copyright © Marc Rousavy @ Margelo
6
+ ///
7
+
8
+ #pragma once
9
+
10
+ #include <fbjni/fbjni.h>
11
+ #include <functional>
12
+
13
+ #include <functional>
14
+ #include <NitroModules/JNICallable.hpp>
15
+
16
+ namespace margelo::nitro::nitroplayer {
17
+
18
+ using namespace facebook;
19
+
20
+ /**
21
+ * Represents the Java/Kotlin callback `() -> Unit`.
22
+ * This can be passed around between C++ and Java/Kotlin.
23
+ */
24
+ struct JFunc_void: public jni::JavaClass<JFunc_void> {
25
+ public:
26
+ static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/nitroplayer/Func_void;";
27
+
28
+ public:
29
+ /**
30
+ * Invokes the function this `JFunc_void` instance holds through JNI.
31
+ */
32
+ void invoke() const {
33
+ static const auto method = javaClassStatic()->getMethod<void()>("invoke");
34
+ method(self());
35
+ }
36
+ };
37
+
38
+ /**
39
+ * An implementation of Func_void that is backed by a C++ implementation (using `std::function<...>`)
40
+ */
41
+ class JFunc_void_cxx final: public jni::HybridClass<JFunc_void_cxx, JFunc_void> {
42
+ public:
43
+ static jni::local_ref<JFunc_void::javaobject> fromCpp(const std::function<void()>& func) {
44
+ return JFunc_void_cxx::newObjectCxxArgs(func);
45
+ }
46
+
47
+ public:
48
+ /**
49
+ * Invokes the C++ `std::function<...>` this `JFunc_void_cxx` instance holds.
50
+ */
51
+ void invoke_cxx() {
52
+ _func();
53
+ }
54
+
55
+ public:
56
+ [[nodiscard]]
57
+ inline const std::function<void()>& getFunction() const {
58
+ return _func;
59
+ }
60
+
61
+ public:
62
+ static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/nitroplayer/Func_void_cxx;";
63
+ static void registerNatives() {
64
+ registerHybrid({makeNativeMethod("invoke_cxx", JFunc_void_cxx::invoke_cxx)});
65
+ }
66
+
67
+ private:
68
+ explicit JFunc_void_cxx(const std::function<void()>& func): _func(func) { }
69
+
70
+ private:
71
+ friend HybridBase;
72
+ std::function<void()> _func;
73
+ };
74
+
75
+ } // namespace margelo::nitro::nitroplayer
@@ -0,0 +1,76 @@
1
+ ///
2
+ /// JFunc_void_std__string.hpp
3
+ /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
4
+ /// https://github.com/mrousavy/nitro
5
+ /// Copyright © Marc Rousavy @ Margelo
6
+ ///
7
+
8
+ #pragma once
9
+
10
+ #include <fbjni/fbjni.h>
11
+ #include <functional>
12
+
13
+ #include <string>
14
+ #include <functional>
15
+ #include <NitroModules/JNICallable.hpp>
16
+
17
+ namespace margelo::nitro::nitroplayer {
18
+
19
+ using namespace facebook;
20
+
21
+ /**
22
+ * Represents the Java/Kotlin callback `(action: String) -> Unit`.
23
+ * This can be passed around between C++ and Java/Kotlin.
24
+ */
25
+ struct JFunc_void_std__string: public jni::JavaClass<JFunc_void_std__string> {
26
+ public:
27
+ static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/nitroplayer/Func_void_std__string;";
28
+
29
+ public:
30
+ /**
31
+ * Invokes the function this `JFunc_void_std__string` instance holds through JNI.
32
+ */
33
+ void invoke(const std::string& action) const {
34
+ static const auto method = javaClassStatic()->getMethod<void(jni::alias_ref<jni::JString> /* action */)>("invoke");
35
+ method(self(), jni::make_jstring(action));
36
+ }
37
+ };
38
+
39
+ /**
40
+ * An implementation of Func_void_std__string that is backed by a C++ implementation (using `std::function<...>`)
41
+ */
42
+ class JFunc_void_std__string_cxx final: public jni::HybridClass<JFunc_void_std__string_cxx, JFunc_void_std__string> {
43
+ public:
44
+ static jni::local_ref<JFunc_void_std__string::javaobject> fromCpp(const std::function<void(const std::string& /* action */)>& func) {
45
+ return JFunc_void_std__string_cxx::newObjectCxxArgs(func);
46
+ }
47
+
48
+ public:
49
+ /**
50
+ * Invokes the C++ `std::function<...>` this `JFunc_void_std__string_cxx` instance holds.
51
+ */
52
+ void invoke_cxx(jni::alias_ref<jni::JString> action) {
53
+ _func(action->toStdString());
54
+ }
55
+
56
+ public:
57
+ [[nodiscard]]
58
+ inline const std::function<void(const std::string& /* action */)>& getFunction() const {
59
+ return _func;
60
+ }
61
+
62
+ public:
63
+ static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/nitroplayer/Func_void_std__string_cxx;";
64
+ static void registerNatives() {
65
+ registerHybrid({makeNativeMethod("invoke_cxx", JFunc_void_std__string_cxx::invoke_cxx)});
66
+ }
67
+
68
+ private:
69
+ explicit JFunc_void_std__string_cxx(const std::function<void(const std::string& /* action */)>& func): _func(func) { }
70
+
71
+ private:
72
+ friend HybridBase;
73
+ std::function<void(const std::string& /* action */)> _func;
74
+ };
75
+
76
+ } // namespace margelo::nitro::nitroplayer