react-native-nitro-player 1.1.0 → 1.2.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.
@@ -8,6 +8,11 @@
8
8
  <uses-permission android:name="android.permission.WAKE_LOCK" />
9
9
  <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
10
10
 
11
+ <!-- Required so Android Auto / Assistant treat this as a media app. -->
12
+ <uses-feature
13
+ android:name="android.hardware.audio.output"
14
+ android:required="false" />
15
+
11
16
  <application>
12
17
  <!-- PlaybackService keeps music alive when screen is locked / app backgrounded -->
13
18
  <service
@@ -18,6 +23,13 @@
18
23
  <intent-filter>
19
24
  <action android:name="androidx.media3.session.MediaSessionService" />
20
25
  </intent-filter>
26
+ <!-- Legacy voice-search entry point. Modern Android Auto / Assistant
27
+ routes through MediaSession.Callback.onSetMediaItems with a
28
+ searchQuery, but some surfaces still send this action. -->
29
+ <intent-filter>
30
+ <action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
31
+ <category android:name="android.intent.category.DEFAULT" />
32
+ </intent-filter>
21
33
  </service>
22
34
 
23
35
  <!-- WorkManager's SystemForegroundService for download notifications -->
@@ -109,9 +109,12 @@ class HybridPlayerQueue : HybridPlayerQueueSpec() {
109
109
 
110
110
  // ── Playback control ──────────────────────────────────────────────────────
111
111
 
112
- override fun loadPlaylist(playlistId: String): Promise<Unit> =
112
+ override fun loadPlaylist(playlistId: String, index: Double?): Promise<Unit> =
113
113
  Promise.async {
114
- core.loadPlaylist(playlistId)
114
+ val startIndex = index?.toInt()
115
+ val loaded = playlistManager.loadPlaylist(playlistId, startIndex)
116
+ if (!loaded) return@async
117
+ core.loadPlaylist(playlistId, startIndex)
115
118
  }
116
119
 
117
120
  override fun getCurrentPlaylistId(): Variant_NullType_String {
@@ -30,8 +30,7 @@ internal fun TrackPlayerCore.rebuildQueueAndPlayFromIndex(index: Int) {
30
30
 
31
31
  currentTrackIndex = index
32
32
  exo.clearMediaItems()
33
- exo.setMediaItems(mediaItems)
34
- exo.seekToDefaultPosition(0)
33
+ exo.setMediaItems(mediaItems, true)
35
34
  exo.prepare()
36
35
  }
37
36
 
@@ -123,7 +122,7 @@ internal fun TrackPlayerCore.updatePlayerQueue(tracks: List<TrackItem>) {
123
122
  val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
124
123
  makeMediaItem(track, mediaId)
125
124
  }
126
- exo.setMediaItems(mediaItems, false)
125
+ exo.setMediaItems(mediaItems, true)
127
126
  if (exo.playbackState == Player.STATE_IDLE && mediaItems.isNotEmpty()) {
128
127
  exo.prepare()
129
128
  }
@@ -11,14 +11,33 @@ import com.margelo.nitro.nitroplayer.TrackItem
11
11
 
12
12
  // ── Playlist loading ──────────────────────────────────────────────────────
13
13
 
14
- suspend fun TrackPlayerCore.loadPlaylist(playlistId: String) =
14
+ suspend fun TrackPlayerCore.loadPlaylist(
15
+ playlistId: String,
16
+ startIndex: Int? = null,
17
+ ) =
15
18
  withPlayerContext {
16
19
  playNextStack.clear()
17
20
  upNextQueue.clear()
18
21
  currentTemporaryType = TrackPlayerCore.TemporaryType.NONE
19
22
  val playlist = playlistManager.getPlaylist(playlistId) ?: return@withPlayerContext
23
+
24
+ val targetIndex =
25
+ if (startIndex != null) {
26
+ if (startIndex < 0 || startIndex >= playlist.tracks.size) return@withPlayerContext
27
+ startIndex
28
+ } else {
29
+ 0
30
+ }
31
+
20
32
  currentPlaylistId = playlistId
21
- updatePlayerQueue(playlist.tracks)
33
+ if (targetIndex == 0) {
34
+ updatePlayerQueue(playlist.tracks)
35
+ } else {
36
+ // Bypass updatePlayerQueue to avoid emitting a spurious onTrackChange for index 0.
37
+ // Set currentTracks directly so rebuildQueueAndPlayFromIndex can use them.
38
+ currentTracks = playlist.tracks
39
+ rebuildQueueAndPlayFromIndex(targetIndex)
40
+ }
22
41
  checkUpcomingTracksForUrls(lookaheadCount)
23
42
  notifyTemporaryQueueChange()
24
43
  }
@@ -3,6 +3,7 @@
3
3
  package com.margelo.nitro.nitroplayer.media
4
4
 
5
5
  import android.net.Uri
6
+ import android.provider.MediaStore
6
7
  import androidx.media3.common.MediaItem
7
8
  import androidx.media3.common.MediaMetadata
8
9
  import androidx.media3.exoplayer.ExoPlayer
@@ -14,6 +15,7 @@ import com.google.common.util.concurrent.ListenableFuture
14
15
  import com.margelo.nitro.nitroplayer.TrackItem
15
16
  import com.margelo.nitro.nitroplayer.core.NitroPlayerLogger
16
17
  import com.margelo.nitro.nitroplayer.core.loadPlaylist
18
+ import com.margelo.nitro.nitroplayer.playlist.Playlist
17
19
  import com.margelo.nitro.nitroplayer.playlist.PlaylistManager
18
20
  import kotlinx.coroutines.CoroutineScope
19
21
  import kotlinx.coroutines.Dispatchers
@@ -52,6 +54,13 @@ object MediaSessionCallbackFactory {
52
54
  mediaItems: MutableList<MediaItem>,
53
55
  ): ListenableFuture<MutableList<MediaItem>> {
54
56
  NitroPlayerLogger.log("MediaSessionCallback") { "onAddMediaItems called with ${mediaItems.size} items" }
57
+
58
+ // Voice search ("Add X to the queue") lands here too.
59
+ val voiceMatch = resolveVoiceSearch(mediaItems, playlistManager)
60
+ if (voiceMatch != null) {
61
+ return Futures.immediateFuture(voiceMatch.items.toMutableList())
62
+ }
63
+
55
64
  if (mediaItems.isEmpty()) return Futures.immediateFuture(mutableListOf())
56
65
 
57
66
  val updated = mutableListOf<MediaItem>()
@@ -90,6 +99,21 @@ object MediaSessionCallbackFactory {
90
99
  startPositionMs: Long,
91
100
  ): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
92
101
  NitroPlayerLogger.log("MediaSessionCallback") { "onSetMediaItems called with ${mediaItems.size} items, startIndex: $startIndex" }
102
+
103
+ // Voice search ("Hey Google, play X on …") routes here with a
104
+ // MediaItem whose requestMetadata.searchQuery is set. Resolve
105
+ // it against loaded playlists before falling through.
106
+ val voiceMatch = resolveVoiceSearch(mediaItems, playlistManager)
107
+ if (voiceMatch != null) {
108
+ val (playlistId, items, trackIndex) = voiceMatch
109
+ service.trackPlayerCore?.let { core ->
110
+ scope.launch { core.loadPlaylist(playlistId) }
111
+ }
112
+ return Futures.immediateFuture(
113
+ MediaSession.MediaItemsWithStartPosition(items, trackIndex, startPositionMs),
114
+ )
115
+ }
116
+
93
117
  if (mediaItems.isEmpty()) {
94
118
  return Futures.immediateFuture(
95
119
  MediaSession.MediaItemsWithStartPosition(mutableListOf(), 0, 0),
@@ -125,6 +149,17 @@ object MediaSessionCallbackFactory {
125
149
  } catch (e: Exception) {
126
150
  NitroPlayerLogger.log("MediaSessionCallback") { "Error in onSetMediaItems: ${e.message}" }
127
151
  }
152
+
153
+ // If a voice query was issued but found nothing, return an
154
+ // explicit failure so the controller (Assistant) surfaces a
155
+ // spoken error instead of silently no-oping. Without this the
156
+ // Auto QA bot reports "no music played or error message shown".
157
+ if (mediaItems.isNotEmpty() && mediaItems[0].requestMetadata.searchQuery != null) {
158
+ return Futures.immediateFailedFuture(
159
+ UnsupportedOperationException("No matching tracks for voice search"),
160
+ )
161
+ }
162
+
128
163
  return Futures.immediateFuture(
129
164
  MediaSession.MediaItemsWithStartPosition(mediaItems, startIndex, startPositionMs),
130
165
  )
@@ -140,6 +175,149 @@ object MediaSessionCallbackFactory {
140
175
  }
141
176
  }
142
177
 
178
+ private data class VoiceMatch(
179
+ val playlistId: String,
180
+ val items: List<MediaItem>,
181
+ val startIndex: Int,
182
+ )
183
+
184
+ /**
185
+ * Resolves a voice-search MediaItem into a concrete playlist + start index.
186
+ *
187
+ * Triggered when Google Assistant ("Hey Google, play X on …") sends a
188
+ * MediaItem with `requestMetadata.searchQuery`. Honors the
189
+ * `MediaStore.EXTRA_MEDIA_FOCUS` bundle so artist/album/genre/playlist/title
190
+ * queries narrow the search appropriately. Empty queries fall back to the
191
+ * current or first available playlist (Assistant: "Play music").
192
+ */
193
+ @Suppress("DEPRECATION") // MediaStore.Audio.Playlists constants — Assistant still sends them.
194
+ private fun resolveVoiceSearch(
195
+ mediaItems: List<MediaItem>,
196
+ playlistManager: PlaylistManager,
197
+ ): VoiceMatch? {
198
+ val first = mediaItems.firstOrNull() ?: return null
199
+ val request = first.requestMetadata
200
+ val rawQuery = request.searchQuery ?: return null
201
+ val query = rawQuery.trim()
202
+ val extras = request.extras
203
+
204
+ // Empty query → "play music"-style resume. Prefer current playlist,
205
+ // otherwise the first non-empty playlist.
206
+ if (query.isEmpty()) {
207
+ val fallback =
208
+ playlistManager.getCurrentPlaylist()
209
+ ?: playlistManager.getAllPlaylists().firstOrNull { it.tracks.isNotEmpty() }
210
+ ?: return null
211
+ val items =
212
+ fallback.tracks.map { createMediaItem(it, "${fallback.id}:${it.id}") }
213
+ return VoiceMatch(fallback.id, items, 0)
214
+ }
215
+
216
+ val focus = extras?.getString(MediaStore.EXTRA_MEDIA_FOCUS)
217
+ val artist = extras?.getString(MediaStore.EXTRA_MEDIA_ARTIST)
218
+ val album = extras?.getString(MediaStore.EXTRA_MEDIA_ALBUM)
219
+ val title = extras?.getString(MediaStore.EXTRA_MEDIA_TITLE)
220
+ val playlistName = extras?.getString(MediaStore.EXTRA_MEDIA_PLAYLIST)
221
+ val genre = extras?.getString(MediaStore.EXTRA_MEDIA_GENRE)
222
+
223
+ val all = playlistManager.getAllPlaylists()
224
+ if (all.isEmpty()) return null
225
+
226
+ // Playlist-focused query: best match by playlist name.
227
+ if (focus == MediaStore.Audio.Playlists.ENTRY_CONTENT_TYPE && !playlistName.isNullOrBlank()) {
228
+ val matched = bestPlaylistByName(all, playlistName) ?: bestPlaylistByName(all, query)
229
+ if (matched != null && matched.tracks.isNotEmpty()) {
230
+ val items = matched.tracks.map { createMediaItem(it, "${matched.id}:${it.id}") }
231
+ return VoiceMatch(matched.id, items, 0)
232
+ }
233
+ }
234
+
235
+ // Score every track across every playlist; play the best track in the
236
+ // context of its playlist so "next/previous" still work naturally.
237
+ var bestPlaylist: Playlist? = null
238
+ var bestIndex = -1
239
+ var bestScore = 0
240
+
241
+ for (playlist in all) {
242
+ playlist.tracks.forEachIndexed { idx, track ->
243
+ val score = scoreTrack(track, query, focus, title, artist, album, genre)
244
+ if (score > bestScore) {
245
+ bestScore = score
246
+ bestPlaylist = playlist
247
+ bestIndex = idx
248
+ }
249
+ }
250
+ }
251
+
252
+ val match = bestPlaylist
253
+ if (match == null || bestIndex < 0 || bestScore <= 0) return null
254
+
255
+ val items = match.tracks.map { createMediaItem(it, "${match.id}:${it.id}") }
256
+ return VoiceMatch(match.id, items, bestIndex)
257
+ }
258
+
259
+ private fun bestPlaylistByName(
260
+ playlists: List<Playlist>,
261
+ name: String,
262
+ ): Playlist? {
263
+ val q = name.lowercase().trim()
264
+ if (q.isEmpty()) return null
265
+ return playlists
266
+ .map { it to it.name.lowercase() }
267
+ .filter { (_, n) -> n.contains(q) || q.contains(n) }
268
+ .minByOrNull { (_, n) -> kotlin.math.abs(n.length - q.length) }
269
+ ?.first
270
+ }
271
+
272
+ private fun scoreTrack(
273
+ track: TrackItem,
274
+ query: String,
275
+ focus: String?,
276
+ title: String?,
277
+ artist: String?,
278
+ album: String?,
279
+ genre: String?,
280
+ ): Int {
281
+ val q = query.lowercase()
282
+ val titleL = track.title.lowercase()
283
+ val artistL = track.artist.lowercase()
284
+ val albumL = track.album.lowercase()
285
+
286
+ var score = 0
287
+
288
+ when (focus) {
289
+ MediaStore.Audio.Artists.ENTRY_CONTENT_TYPE -> {
290
+ if (!artist.isNullOrBlank() && artistL.contains(artist.lowercase())) score += 80
291
+ if (artistL.contains(q)) score += 40
292
+ }
293
+ MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE -> {
294
+ if (!album.isNullOrBlank() && albumL.contains(album.lowercase())) score += 80
295
+ if (albumL.contains(q)) score += 40
296
+ }
297
+ MediaStore.Audio.Genres.ENTRY_CONTENT_TYPE -> {
298
+ if (!genre.isNullOrBlank()) {
299
+ val g = genre.lowercase()
300
+ if (titleL.contains(g) || albumL.contains(g) || artistL.contains(g)) score += 40
301
+ }
302
+ }
303
+ else -> {
304
+ if (!title.isNullOrBlank() && titleL.contains(title.lowercase())) score += 80
305
+ if (!artist.isNullOrBlank() && artistL.contains(artist.lowercase())) score += 50
306
+ if (!album.isNullOrBlank() && albumL.contains(album.lowercase())) score += 30
307
+ }
308
+ }
309
+
310
+ // Token-overlap fallback against the whole free-text query.
311
+ val tokens = q.split(' ', ',', '-').filter { it.length >= 2 }
312
+ for (t in tokens) {
313
+ if (titleL.contains(t)) score += 10
314
+ if (artistL.contains(t)) score += 6
315
+ if (albumL.contains(t)) score += 3
316
+ }
317
+ if (titleL == q || artistL == q || albumL == q) score += 100
318
+ return score
319
+ }
320
+
143
321
  private fun createMediaItem(
144
322
  track: TrackItem,
145
323
  mediaId: String,
@@ -304,8 +304,9 @@ class PlaylistManager private constructor(
304
304
  /**
305
305
  * Load a playlist for playback (sets it as current)
306
306
  */
307
- fun loadPlaylist(playlistId: String): Boolean {
308
- if (playlists[playlistId] == null) return false
307
+ fun loadPlaylist(playlistId: String, index: Int? = null): Boolean {
308
+ val playlist = playlists[playlistId] ?: return false
309
+ if (index != null && (index < 0 || index >= playlist.tracks.size)) return false
309
310
  currentPlaylistId = playlistId
310
311
  return true
311
312
  }
@@ -55,11 +55,11 @@ extension TrackPlayerCore {
55
55
  NitroPlayerLogger.log("TrackPlayerCore", "🔄 Removing \(existingPlayer.items().count) old items from player")
56
56
  existingPlayer.removeAllItems()
57
57
 
58
- // Lazy-load mode: if any track has no URL AND is not downloaded locally,
59
- // we can't create an AVPlayerItem for it and the queue order would be wrong.
60
- let isLazyLoad = tracks.contains {
58
+ // Lazy-load mode if the first track needs a URL. Tracks further in the queue with
59
+ // empty URLs are dropped safely by compactMap below and resolved via onTracksNeedUpdate.
60
+ let isLazyLoad = tracks.first.map {
61
61
  $0.url.isEmpty && !DownloadManagerCore.shared.isTrackDownloaded(trackId: $0.id)
62
- }
62
+ } ?? false
63
63
  if isLazyLoad {
64
64
  NitroPlayerLogger.log("TrackPlayerCore", "⏳ Lazy-load mode — player cleared, awaiting URL resolution")
65
65
  return
@@ -156,6 +156,12 @@ extension TrackPlayerCore {
156
156
  && !DownloadManagerCore.shared.isTrackDownloaded(trackId: targetTrack.id)
157
157
  if isLazyLoad {
158
158
  NitroPlayerLogger.log("TrackPlayerCore", " ⏳ Lazy-load — deferring AVQueuePlayer setup; emitting track change for index \(index)")
159
+ // Clear old items immediately so the previous track stops while waiting for the URL to resolve
160
+ if let boundaryObserver = self.boundaryTimeObserver {
161
+ player?.removeTimeObserver(boundaryObserver)
162
+ self.boundaryTimeObserver = nil
163
+ }
164
+ player?.removeAllItems()
159
165
  self.currentTracks = fullPlaylist
160
166
  if let track = self.currentTracks[safe: index] {
161
167
  notifyTrackChange(track, .skip)
@@ -8,7 +8,7 @@ import Foundation
8
8
 
9
9
  extension TrackPlayerCore {
10
10
 
11
- func loadPlaylist(playlistId: String) async {
11
+ func loadPlaylist(playlistId: String, startIndex: Int? = nil) async {
12
12
  await withPlayerQueueNoThrow {
13
13
  self.playNextStack.removeAll()
14
14
  self.upNextQueue.removeAll()
@@ -32,8 +32,27 @@ extension TrackPlayerCore {
32
32
  }
33
33
  NitroPlayerLogger.log("TrackPlayerCore", String(repeating: "🎼", count: Constants.playlistSeparatorLength) + "\n")
34
34
 
35
+ let targetIndex: Int
36
+ if let startIndex {
37
+ guard startIndex >= 0 && startIndex < playlist.tracks.count else {
38
+ NitroPlayerLogger.log("TrackPlayerCore", " ❌ Invalid start index: \(startIndex) (track count: \(playlist.tracks.count))")
39
+ return
40
+ }
41
+ targetIndex = startIndex
42
+ } else {
43
+ targetIndex = 0
44
+ }
45
+
35
46
  self.currentPlaylistId = playlistId
36
- self.updatePlayerQueue(tracks: playlist.tracks)
47
+ if targetIndex == 0 {
48
+ self.updatePlayerQueue(tracks: playlist.tracks)
49
+ } else {
50
+ // Bypass updatePlayerQueue to avoid emitting a spurious onTrackChange for index 0.
51
+ // Set currentTracks directly so rebuildQueueFromPlaylistIndex can use them.
52
+ self.currentTracks = playlist.tracks
53
+ self.preloadedAssets.removeAll()
54
+ _ = self.rebuildQueueFromPlaylistIndex(index: targetIndex)
55
+ }
37
56
  self.emitStateChange()
38
57
  self.checkUpcomingTracksForUrls(lookahead: self.lookaheadCount)
39
58
  self.notifyTemporaryQueueChange()
@@ -261,9 +261,13 @@ class PlaylistManager {
261
261
  /**
262
262
  * Load a playlist for playback (sets it as current)
263
263
  */
264
- func loadPlaylist(playlistId: String) -> Bool {
265
- let exists = queue.sync { playlists[playlistId] != nil }
266
- guard exists else {
264
+ func loadPlaylist(playlistId: String, index: Int? = nil) -> Bool {
265
+ let isValid = queue.sync {
266
+ guard let playlist = playlists[playlistId] else { return false }
267
+ guard let index else { return true }
268
+ return index >= 0 && index < playlist.tracks.count
269
+ }
270
+ guard isValid else {
267
271
  return false
268
272
  }
269
273
 
@@ -77,11 +77,13 @@ final class HybridPlayerQueue: HybridPlayerQueueSpec {
77
77
 
78
78
  // MARK: - Playback control
79
79
 
80
- func loadPlaylist(playlistId: String) throws -> Promise<Void> {
80
+ func loadPlaylist(playlistId: String, index: Double?) throws -> Promise<Void> {
81
81
  Promise.async {
82
+ let startIndex = index.map { Int($0) }
82
83
  // Update PlaylistManager.currentPlaylistId so getCurrentPlaylistId() returns correctly
83
- _ = self.playlistManager.loadPlaylist(playlistId: playlistId)
84
- await self.core.loadPlaylist(playlistId: playlistId)
84
+ let loaded = self.playlistManager.loadPlaylist(playlistId: playlistId, index: startIndex)
85
+ guard loaded else { return }
86
+ await self.core.loadPlaylist(playlistId: playlistId, startIndex: startIndex)
85
87
  }
86
88
  }
87
89
 
@@ -13,7 +13,7 @@ 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
- loadPlaylist(playlistId: string): Promise<void>;
16
+ loadPlaylist(playlistId: string, index?: number): Promise<void>;
17
17
  getCurrentPlaylistId(): string | null;
18
18
  onPlaylistsChanged(callback: (playlists: Playlist[], operation?: QueueOperation) => void): void;
19
19
  onPlaylistChanged(callback: (playlistId: string, playlist: Playlist, operation?: QueueOperation) => void): void;
@@ -205,9 +205,9 @@ namespace margelo::nitro::nitroplayer {
205
205
  return __promise;
206
206
  }();
207
207
  }
208
- std::shared_ptr<Promise<void>> JHybridPlayerQueueSpec::loadPlaylist(const std::string& playlistId) {
209
- static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<JPromise::javaobject>(jni::alias_ref<jni::JString> /* playlistId */)>("loadPlaylist");
210
- auto __result = method(_javaPart, jni::make_jstring(playlistId));
208
+ std::shared_ptr<Promise<void>> JHybridPlayerQueueSpec::loadPlaylist(const std::string& playlistId, std::optional<double> index) {
209
+ static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<JPromise::javaobject>(jni::alias_ref<jni::JString> /* playlistId */, jni::alias_ref<jni::JDouble> /* index */)>("loadPlaylist");
210
+ auto __result = method(_javaPart, jni::make_jstring(playlistId), index.has_value() ? jni::JDouble::valueOf(index.value()) : nullptr);
211
211
  return [&]() {
212
212
  auto __promise = Promise<void>::create();
213
213
  __result->cthis()->addOnResolvedListener([=](const jni::alias_ref<jni::JObject>& /* unit */) {
@@ -63,7 +63,7 @@ namespace margelo::nitro::nitroplayer {
63
63
  std::shared_ptr<Promise<void>> addTracksToPlaylist(const std::string& playlistId, const std::vector<TrackItem>& tracks, std::optional<double> index) override;
64
64
  std::shared_ptr<Promise<void>> removeTrackFromPlaylist(const std::string& playlistId, const std::string& trackId) override;
65
65
  std::shared_ptr<Promise<void>> reorderTrackInPlaylist(const std::string& playlistId, const std::string& trackId, double newIndex) override;
66
- std::shared_ptr<Promise<void>> loadPlaylist(const std::string& playlistId) override;
66
+ std::shared_ptr<Promise<void>> loadPlaylist(const std::string& playlistId, std::optional<double> index) override;
67
67
  std::variant<nitro::NullType, std::string> getCurrentPlaylistId() override;
68
68
  void onPlaylistsChanged(const std::function<void(const std::vector<Playlist>& /* playlists */, std::optional<QueueOperation> /* operation */)>& callback) override;
69
69
  void onPlaylistChanged(const std::function<void(const std::string& /* playlistId */, const Playlist& /* playlist */, std::optional<QueueOperation> /* operation */)>& callback) override;
@@ -68,7 +68,7 @@ abstract class HybridPlayerQueueSpec: HybridObject() {
68
68
 
69
69
  @DoNotStrip
70
70
  @Keep
71
- abstract fun loadPlaylist(playlistId: String): Promise<Unit>
71
+ abstract fun loadPlaylist(playlistId: String, index: Double?): Promise<Unit>
72
72
 
73
73
  @DoNotStrip
74
74
  @Keep
@@ -153,8 +153,8 @@ namespace margelo::nitro::nitroplayer {
153
153
  auto __value = std::move(__result.value());
154
154
  return __value;
155
155
  }
156
- inline std::shared_ptr<Promise<void>> loadPlaylist(const std::string& playlistId) override {
157
- auto __result = _swiftPart.loadPlaylist(playlistId);
156
+ inline std::shared_ptr<Promise<void>> loadPlaylist(const std::string& playlistId, std::optional<double> index) override {
157
+ auto __result = _swiftPart.loadPlaylist(playlistId, index);
158
158
  if (__result.hasError()) [[unlikely]] {
159
159
  std::rethrow_exception(__result.error());
160
160
  }
@@ -22,7 +22,7 @@ public protocol HybridPlayerQueueSpec_protocol: HybridObject {
22
22
  func addTracksToPlaylist(playlistId: String, tracks: [TrackItem], index: Double?) throws -> Promise<Void>
23
23
  func removeTrackFromPlaylist(playlistId: String, trackId: String) throws -> Promise<Void>
24
24
  func reorderTrackInPlaylist(playlistId: String, trackId: String, newIndex: Double) throws -> Promise<Void>
25
- func loadPlaylist(playlistId: String) throws -> Promise<Void>
25
+ func loadPlaylist(playlistId: String, index: Double?) throws -> Promise<Void>
26
26
  func getCurrentPlaylistId() throws -> Variant_NullType_String
27
27
  func onPlaylistsChanged(callback: @escaping (_ playlists: [Playlist], _ operation: QueueOperation?) -> Void) throws -> Void
28
28
  func onPlaylistChanged(callback: @escaping (_ playlistId: String, _ playlist: Playlist, _ operation: QueueOperation?) -> Void) throws -> Void
@@ -344,9 +344,16 @@ open class HybridPlayerQueueSpec_cxx {
344
344
  }
345
345
 
346
346
  @inline(__always)
347
- public final func loadPlaylist(playlistId: std.string) -> bridge.Result_std__shared_ptr_Promise_void___ {
347
+ public final func loadPlaylist(playlistId: std.string, index: bridge.std__optional_double_) -> bridge.Result_std__shared_ptr_Promise_void___ {
348
348
  do {
349
- let __result = try self.__implementation.loadPlaylist(playlistId: String(playlistId))
349
+ let __result = try self.__implementation.loadPlaylist(playlistId: String(playlistId), index: { () -> Double? in
350
+ if bridge.has_value_std__optional_double_(index) {
351
+ let __unwrapped = bridge.get_std__optional_double_(index)
352
+ return __unwrapped
353
+ } else {
354
+ return nil
355
+ }
356
+ }())
350
357
  let __resultCpp = { () -> bridge.std__shared_ptr_Promise_void__ in
351
358
  let __promise = bridge.create_std__shared_ptr_Promise_void__()
352
359
  let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_void__(__promise)
@@ -71,7 +71,7 @@ namespace margelo::nitro::nitroplayer {
71
71
  virtual std::shared_ptr<Promise<void>> addTracksToPlaylist(const std::string& playlistId, const std::vector<TrackItem>& tracks, std::optional<double> index) = 0;
72
72
  virtual std::shared_ptr<Promise<void>> removeTrackFromPlaylist(const std::string& playlistId, const std::string& trackId) = 0;
73
73
  virtual std::shared_ptr<Promise<void>> reorderTrackInPlaylist(const std::string& playlistId, const std::string& trackId, double newIndex) = 0;
74
- virtual std::shared_ptr<Promise<void>> loadPlaylist(const std::string& playlistId) = 0;
74
+ virtual std::shared_ptr<Promise<void>> loadPlaylist(const std::string& playlistId, std::optional<double> index) = 0;
75
75
  virtual std::variant<nitro::NullType, std::string> getCurrentPlaylistId() = 0;
76
76
  virtual void onPlaylistsChanged(const std::function<void(const std::vector<Playlist>& /* playlists */, std::optional<QueueOperation> /* operation */)>& callback) = 0;
77
77
  virtual void onPlaylistChanged(const std::function<void(const std::string& /* playlistId */, const Playlist& /* playlist */, std::optional<QueueOperation> /* operation */)>& callback) = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-player",
3
- "version": "1.1.0",
3
+ "version": "1.2.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",
@@ -48,7 +48,7 @@ export interface PlayerQueue extends HybridObject<{
48
48
  ): Promise<void>
49
49
 
50
50
  // Playback control
51
- loadPlaylist(playlistId: string): Promise<void>
51
+ loadPlaylist(playlistId: string, index?: number): Promise<void>
52
52
  getCurrentPlaylistId(): string | null
53
53
 
54
54
  // Events