react-native-nitro-player 0.7.1-alpha.1 → 0.7.1-alpha.3

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 (36) hide show
  1. package/android/src/main/AndroidManifest.xml +15 -1
  2. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +5 -6
  3. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +68 -49
  4. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +67 -21
  5. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +27 -5
  6. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +88 -49
  7. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +40 -10
  8. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ExoPlayerCore.kt +46 -45
  9. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ListenerRegistry.kt +4 -1
  10. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerAndroidAuto.kt +38 -32
  11. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +146 -81
  12. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +24 -13
  13. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerNotify.kt +16 -4
  14. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +101 -72
  15. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueue.kt +42 -22
  16. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +55 -24
  17. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +27 -8
  18. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerTempQueue.kt +73 -62
  19. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +51 -48
  20. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +3 -3
  21. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadManagerCore.kt +14 -3
  22. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadWorker.kt +94 -23
  23. package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +43 -34
  24. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/ExoPlayerBuilder.kt +49 -0
  25. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionCallbackFactory.kt +167 -0
  26. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +11 -450
  27. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/NitroPlayerNotificationProvider.kt +200 -0
  28. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/PlaybackService.kt +101 -0
  29. package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +87 -85
  30. package/ios/core/TrackPlayerQueueBuild.swift +16 -2
  31. package/package.json +1 -1
  32. package/src/hooks/useEqualizer.ts +15 -12
  33. package/src/specs/AndroidAutoMediaLibrary.nitro.ts +3 -2
  34. package/src/specs/DownloadManager.nitro.ts +4 -2
  35. package/src/specs/Equalizer.nitro.ts +4 -2
  36. package/src/specs/TrackPlayer.nitro.ts +18 -6
@@ -0,0 +1,200 @@
1
+ @file:Suppress("ktlint:standard:max-line-length")
2
+
3
+ package com.margelo.nitro.nitroplayer.media
4
+
5
+ import android.app.Notification
6
+ import android.app.NotificationChannel
7
+ import android.app.NotificationManager
8
+ import android.app.PendingIntent
9
+ import android.content.Context
10
+ import android.content.Intent
11
+ import android.graphics.Bitmap
12
+ import android.graphics.BitmapFactory
13
+ import android.os.Build
14
+ import android.os.Bundle
15
+ import android.util.LruCache
16
+ import android.view.KeyEvent
17
+ import androidx.core.app.NotificationCompat
18
+ import androidx.media3.common.util.UnstableApi
19
+ import androidx.media3.session.CommandButton
20
+ import androidx.media3.session.MediaNotification
21
+ import androidx.media3.session.MediaSession
22
+ import com.google.common.collect.ImmutableList
23
+ import com.margelo.nitro.nitroplayer.core.NitroPlayerLogger
24
+ import kotlinx.coroutines.CoroutineScope
25
+ import kotlinx.coroutines.Dispatchers
26
+ import kotlinx.coroutines.SupervisorJob
27
+ import kotlinx.coroutines.launch
28
+ import kotlinx.coroutines.withContext
29
+ import java.net.URL
30
+
31
+ /**
32
+ * Custom notification provider that manually builds a [MediaStyle][androidx.media.app.NotificationCompat.MediaStyle]
33
+ * notification with artwork, title, artist, and transport controls — matching
34
+ * the proven approach from the main branch.
35
+ */
36
+ @UnstableApi
37
+ class NitroPlayerNotificationProvider(
38
+ private val context: Context,
39
+ ) : MediaNotification.Provider {
40
+ private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
41
+ private val notificationManager =
42
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
43
+ private val artworkCache =
44
+ object : LruCache<String, Bitmap>(20) {
45
+ override fun sizeOf(key: String, value: Bitmap): Int = 1
46
+ }
47
+
48
+ companion object {
49
+ const val NOTIFICATION_ID = 1001
50
+ private const val CHANNEL_ID = "nitro_player_channel"
51
+ private const val CHANNEL_NAME = "Music Player"
52
+ }
53
+
54
+ init {
55
+ createNotificationChannel()
56
+ }
57
+
58
+ override fun createNotification(
59
+ mediaSession: MediaSession,
60
+ customLayout: ImmutableList<CommandButton>,
61
+ actionFactory: MediaNotification.ActionFactory,
62
+ onNotificationChangedCallback: MediaNotification.Provider.Callback,
63
+ ): MediaNotification {
64
+ val player = mediaSession.player
65
+ val metadata = player.mediaMetadata
66
+ val isPlaying = player.isPlaying
67
+
68
+ val contentIntent =
69
+ PendingIntent.getActivity(
70
+ context,
71
+ 0,
72
+ context.packageManager.getLaunchIntentForPackage(context.packageName),
73
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
74
+ )
75
+
76
+ val builder =
77
+ NotificationCompat
78
+ .Builder(context, CHANNEL_ID)
79
+ .setContentTitle(metadata.title ?: "Unknown Title")
80
+ .setContentText(metadata.artist ?: "Unknown Artist")
81
+ .setSubText(metadata.albumTitle ?: "")
82
+ .setSmallIcon(android.R.drawable.ic_media_play)
83
+ .setContentIntent(contentIntent)
84
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
85
+ .setOngoing(isPlaying)
86
+ .setShowWhen(false)
87
+ .setPriority(NotificationCompat.PRIORITY_LOW)
88
+ .setCategory(NotificationCompat.CATEGORY_TRANSPORT)
89
+
90
+ // MediaStyle with session token
91
+ try {
92
+ val compatToken =
93
+ android.support.v4.media.session.MediaSessionCompat.Token
94
+ .fromToken(mediaSession.platformToken)
95
+ builder.setStyle(
96
+ androidx.media.app.NotificationCompat
97
+ .MediaStyle()
98
+ .setMediaSession(compatToken)
99
+ .setShowActionsInCompactView(0, 1, 2),
100
+ )
101
+ } catch (e: Exception) {
102
+ NitroPlayerLogger.log("NotificationProvider") { "Failed to set media session token: ${e.message}" }
103
+ }
104
+
105
+ // Transport actions using media button PendingIntents
106
+ builder.addAction(
107
+ android.R.drawable.ic_media_previous,
108
+ "Previous",
109
+ buildMediaButtonIntent(KeyEvent.KEYCODE_MEDIA_PREVIOUS, 0),
110
+ )
111
+
112
+ if (isPlaying) {
113
+ builder.addAction(
114
+ android.R.drawable.ic_media_pause,
115
+ "Pause",
116
+ buildMediaButtonIntent(KeyEvent.KEYCODE_MEDIA_PAUSE, 1),
117
+ )
118
+ } else {
119
+ builder.addAction(
120
+ android.R.drawable.ic_media_play,
121
+ "Play",
122
+ buildMediaButtonIntent(KeyEvent.KEYCODE_MEDIA_PLAY, 1),
123
+ )
124
+ }
125
+
126
+ builder.addAction(
127
+ android.R.drawable.ic_media_next,
128
+ "Next",
129
+ buildMediaButtonIntent(KeyEvent.KEYCODE_MEDIA_NEXT, 2),
130
+ )
131
+
132
+ // Load artwork async and update notification
133
+ metadata.artworkUri?.toString()?.let { artworkUrl ->
134
+ val cached = artworkCache.get(artworkUrl)
135
+ if (cached != null) {
136
+ builder.setLargeIcon(cached)
137
+ } else {
138
+ scope.launch {
139
+ val bitmap = loadArtworkBitmap(artworkUrl)
140
+ if (bitmap != null) {
141
+ builder.setLargeIcon(bitmap)
142
+ onNotificationChangedCallback.onNotificationChanged(
143
+ MediaNotification(NOTIFICATION_ID, builder.build()),
144
+ )
145
+ }
146
+ }
147
+ }
148
+ }
149
+
150
+ return MediaNotification(NOTIFICATION_ID, builder.build())
151
+ }
152
+
153
+ override fun handleCustomCommand(
154
+ session: MediaSession,
155
+ action: String,
156
+ extras: Bundle,
157
+ ): Boolean = false
158
+
159
+ private fun buildMediaButtonIntent(keyCode: Int, requestCode: Int): PendingIntent {
160
+ val intent =
161
+ Intent(Intent.ACTION_MEDIA_BUTTON).apply {
162
+ putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, keyCode))
163
+ setPackage(context.packageName)
164
+ }
165
+ return PendingIntent.getBroadcast(
166
+ context,
167
+ requestCode,
168
+ intent,
169
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
170
+ )
171
+ }
172
+
173
+ private fun createNotificationChannel() {
174
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
175
+ val channel =
176
+ NotificationChannel(
177
+ CHANNEL_ID,
178
+ CHANNEL_NAME,
179
+ NotificationManager.IMPORTANCE_LOW,
180
+ ).apply {
181
+ description = "Media playback controls"
182
+ setShowBadge(false)
183
+ lockscreenVisibility = Notification.VISIBILITY_PUBLIC
184
+ }
185
+ notificationManager.createNotificationChannel(channel)
186
+ }
187
+ }
188
+
189
+ private suspend fun loadArtworkBitmap(artworkUrl: String): Bitmap? {
190
+ artworkCache.get(artworkUrl)?.let { return it }
191
+ return try {
192
+ withContext(Dispatchers.IO) {
193
+ val url = URL(artworkUrl)
194
+ BitmapFactory.decodeStream(url.openConnection().getInputStream())
195
+ }?.also { artworkCache.put(artworkUrl, it) }
196
+ } catch (_: Exception) {
197
+ null
198
+ }
199
+ }
200
+ }
@@ -0,0 +1,101 @@
1
+ package com.margelo.nitro.nitroplayer.media
2
+
3
+ import android.content.Intent
4
+ import android.os.Binder
5
+ import android.os.Handler
6
+ import android.os.IBinder
7
+ import android.os.Looper
8
+ import androidx.media3.common.util.UnstableApi
9
+ import androidx.media3.exoplayer.ExoPlayer
10
+ import androidx.media3.session.MediaSession
11
+ import androidx.media3.session.MediaSessionService
12
+ import com.margelo.nitro.nitroplayer.core.NitroPlayerLogger
13
+ import com.margelo.nitro.nitroplayer.core.TrackPlayerCore
14
+ import com.margelo.nitro.nitroplayer.playlist.PlaylistManager
15
+
16
+ /**
17
+ * Foreground service that **owns** the ExoPlayer and MediaSession.
18
+ *
19
+ * The player runs on the **main looper** so that Media3's
20
+ * [MediaSessionService] can access it directly for automatic notification
21
+ * management (foreground promotion, media-style notification, demotion).
22
+ *
23
+ * ExoPlayer does all heavy work (decoding, buffering, network I/O) on its
24
+ * own internal threads — the application looper only handles lightweight
25
+ * callbacks and control calls.
26
+ */
27
+ @UnstableApi
28
+ class NitroPlayerPlaybackService : MediaSessionService() {
29
+
30
+ companion object {
31
+ const val ACTION_LOCAL_BIND = "com.margelo.nitro.nitroplayer.LOCAL_BIND"
32
+ }
33
+
34
+ // ── Created in onCreate ────────────────────────────────────────────────
35
+ private lateinit var player: ExoPlayer
36
+ private var mediaSession: MediaSession? = null
37
+ private val mainHandler = Handler(Looper.getMainLooper())
38
+
39
+ @Volatile
40
+ var trackPlayerCore: TrackPlayerCore? = null
41
+
42
+ // ── Binder exposed to TrackPlayerCore ──────────────────────────────────
43
+ inner class LocalBinder : Binder() {
44
+ val service: NitroPlayerPlaybackService get() = this@NitroPlayerPlaybackService
45
+ val exoPlayer: ExoPlayer get() = player
46
+ val session: MediaSession get() = mediaSession!!
47
+ val handler: Handler get() = mainHandler
48
+ }
49
+
50
+ private val localBinder = lazy { LocalBinder() }
51
+
52
+ // ── Lifecycle ───────────────────────────────────────────────────────────
53
+
54
+ override fun onCreate() {
55
+ super.onCreate()
56
+ NitroPlayerLogger.log("PlaybackService") { "onCreate" }
57
+
58
+ // Build ExoPlayer on main looper (default)
59
+ player = ExoPlayerBuilder.build(this)
60
+
61
+ // Build MediaSession
62
+ val playlistManager = PlaylistManager.getInstance(this)
63
+ mediaSession = MediaSession
64
+ .Builder(this, player)
65
+ .setCallback(MediaSessionCallbackFactory.create(this, playlistManager))
66
+ .build()
67
+
68
+ // Explicitly register the session with the service so that
69
+ // MediaNotificationManager (created in super.onCreate()) can
70
+ // connect its internal MediaController and post notifications.
71
+ addSession(mediaSession!!)
72
+
73
+ // Media3 automatically handles the notification via DefaultMediaNotificationProvider.
74
+ }
75
+
76
+ override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? =
77
+ mediaSession
78
+
79
+ override fun onBind(intent: Intent?): IBinder? {
80
+ if (intent?.action == ACTION_LOCAL_BIND) {
81
+ return localBinder.value
82
+ }
83
+ return super.onBind(intent)
84
+ }
85
+
86
+ override fun onTaskRemoved(rootIntent: Intent?) {
87
+ val session = mediaSession
88
+ if (session == null || !session.player.playWhenReady) {
89
+ stopSelf()
90
+ }
91
+ super.onTaskRemoved(rootIntent)
92
+ }
93
+
94
+ override fun onDestroy() {
95
+ NitroPlayerLogger.log("PlaybackService") { "onDestroy" }
96
+ mediaSession?.release()
97
+ mediaSession = null
98
+ player.release()
99
+ super.onDestroy()
100
+ }
101
+ }
@@ -7,17 +7,17 @@ import com.margelo.nitro.nitroplayer.TrackItem
7
7
  import com.margelo.nitro.nitroplayer.Variant_NullType_String
8
8
  import com.margelo.nitro.nitroplayer.media.NitroPlayerMediaBrowserService
9
9
  import com.margelo.nitro.nitroplayer.storage.NitroPlayerStorage
10
- import org.json.JSONArray
11
- import org.json.JSONObject
12
- import java.util.UUID
13
- import java.util.concurrent.ConcurrentHashMap
14
- import java.util.concurrent.CopyOnWriteArrayList
15
10
  import kotlinx.coroutines.CoroutineScope
16
11
  import kotlinx.coroutines.Dispatchers
17
12
  import kotlinx.coroutines.Job
18
13
  import kotlinx.coroutines.SupervisorJob
19
14
  import kotlinx.coroutines.delay
20
15
  import kotlinx.coroutines.launch
16
+ import org.json.JSONArray
17
+ import org.json.JSONObject
18
+ import java.util.UUID
19
+ import java.util.concurrent.ConcurrentHashMap
20
+ import java.util.concurrent.CopyOnWriteArrayList
21
21
 
22
22
  /**
23
23
  * Manages multiple playlists using ExoPlayer's native playlist functionality
@@ -36,10 +36,11 @@ class PlaylistManager private constructor(
36
36
 
37
37
  private fun scheduleSave() {
38
38
  saveJob?.cancel()
39
- saveJob = saveScope.launch {
40
- delay(300)
41
- saveToFile()
42
- }
39
+ saveJob =
40
+ saveScope.launch {
41
+ delay(300)
42
+ saveToFile()
43
+ }
43
44
  }
44
45
 
45
46
  companion object {
@@ -357,32 +358,33 @@ class PlaylistManager private constructor(
357
358
  try {
358
359
  val jsonArray = JSONArray()
359
360
  playlists.values.forEach { playlist ->
360
- val jsonObject = JSONObject().apply {
361
- put("id", playlist.id)
362
- put("name", playlist.name)
363
- put("description", playlist.description ?: "")
364
- put("artwork", playlist.artwork ?: "")
365
- val tracksArray = JSONArray()
366
- playlist.tracks.forEach { track ->
367
- tracksArray.put(
368
- JSONObject().apply {
369
- put("id", track.id)
370
- put("title", track.title)
371
- put("artist", track.artist)
372
- put("album", track.album)
373
- put("duration", track.duration)
374
- put("url", track.url)
375
- track.artwork?.let { put("artwork", it) }
376
- track.extraPayload?.let { payload ->
377
- val extraPayloadMap = payload.toHashMap()
378
- val extraPayloadJson = JSONObject(extraPayloadMap)
379
- put("extraPayload", extraPayloadJson)
380
- }
381
- },
382
- )
361
+ val jsonObject =
362
+ JSONObject().apply {
363
+ put("id", playlist.id)
364
+ put("name", playlist.name)
365
+ put("description", playlist.description ?: "")
366
+ put("artwork", playlist.artwork ?: "")
367
+ val tracksArray = JSONArray()
368
+ playlist.tracks.forEach { track ->
369
+ tracksArray.put(
370
+ JSONObject().apply {
371
+ put("id", track.id)
372
+ put("title", track.title)
373
+ put("artist", track.artist)
374
+ put("album", track.album)
375
+ put("duration", track.duration)
376
+ put("url", track.url)
377
+ track.artwork?.let { put("artwork", it) }
378
+ track.extraPayload?.let { payload ->
379
+ val extraPayloadMap = payload.toHashMap()
380
+ val extraPayloadJson = JSONObject(extraPayloadMap)
381
+ put("extraPayload", extraPayloadJson)
382
+ }
383
+ },
384
+ )
385
+ }
386
+ put("tracks", tracksArray)
383
387
  }
384
- put("tracks", tracksArray)
385
- }
386
388
  jsonArray.put(jsonObject)
387
389
  }
388
390
 
@@ -445,59 +447,59 @@ class PlaylistManager private constructor(
445
447
  private fun parseAndLoadPlaylists(jsonArray: JSONArray) {
446
448
  playlists.clear()
447
449
  for (i in 0 until jsonArray.length()) {
448
- val jsonObject = jsonArray.getJSONObject(i)
449
- val tracks = mutableListOf<TrackItem>()
450
- val tracksArray = jsonObject.getJSONArray("tracks")
451
- for (j in 0 until tracksArray.length()) {
452
- val trackObj = tracksArray.getJSONObject(j)
453
- val artworkStr = trackObj.optString("artwork")
454
- val artwork: Variant_NullType_String? =
455
- if (!artworkStr.isNullOrEmpty()) {
456
- Variant_NullType_String.create(artworkStr)
457
- } else {
458
- null
459
- }
460
- val extraPayload: AnyMap? =
461
- if (trackObj.has("extraPayload")) {
462
- val extraPayloadJson = trackObj.getJSONObject("extraPayload")
463
- val map = AnyMap()
464
- val keyIterator = extraPayloadJson.keys()
465
- while (keyIterator.hasNext()) {
466
- val key = keyIterator.next()
467
- when (val value = extraPayloadJson.get(key)) {
468
- is String -> map.setString(key, value)
469
- is Number -> map.setDouble(key, value.toDouble())
470
- is Boolean -> map.setBoolean(key, value)
471
- }
450
+ val jsonObject = jsonArray.getJSONObject(i)
451
+ val tracks = mutableListOf<TrackItem>()
452
+ val tracksArray = jsonObject.getJSONArray("tracks")
453
+ for (j in 0 until tracksArray.length()) {
454
+ val trackObj = tracksArray.getJSONObject(j)
455
+ val artworkStr = trackObj.optString("artwork")
456
+ val artwork: Variant_NullType_String? =
457
+ if (!artworkStr.isNullOrEmpty()) {
458
+ Variant_NullType_String.create(artworkStr)
459
+ } else {
460
+ null
461
+ }
462
+ val extraPayload: AnyMap? =
463
+ if (trackObj.has("extraPayload")) {
464
+ val extraPayloadJson = trackObj.getJSONObject("extraPayload")
465
+ val map = AnyMap()
466
+ val keyIterator = extraPayloadJson.keys()
467
+ while (keyIterator.hasNext()) {
468
+ val key = keyIterator.next()
469
+ when (val value = extraPayloadJson.get(key)) {
470
+ is String -> map.setString(key, value)
471
+ is Number -> map.setDouble(key, value.toDouble())
472
+ is Boolean -> map.setBoolean(key, value)
472
473
  }
473
- map
474
- } else {
475
- null
476
474
  }
477
- tracks.add(
478
- TrackItem(
479
- id = trackObj.getString("id"),
480
- title = trackObj.getString("title"),
481
- artist = trackObj.getString("artist"),
482
- album = trackObj.getString("album"),
483
- duration = trackObj.getDouble("duration"),
484
- url = trackObj.getString("url"),
485
- artwork = artwork,
486
- extraPayload = extraPayload,
487
- ),
488
- )
489
- }
490
- val descriptionStr = jsonObject.optString("description")
491
- val artworkStr = jsonObject.optString("artwork")
492
- val playlist =
493
- Playlist(
494
- id = jsonObject.getString("id"),
495
- name = jsonObject.getString("name"),
496
- description = if (!descriptionStr.isNullOrEmpty()) descriptionStr else null,
497
- artwork = if (!artworkStr.isNullOrEmpty()) artworkStr else null,
498
- tracks = tracks,
499
- )
500
- playlists[playlist.id] = playlist
475
+ map
476
+ } else {
477
+ null
478
+ }
479
+ tracks.add(
480
+ TrackItem(
481
+ id = trackObj.getString("id"),
482
+ title = trackObj.getString("title"),
483
+ artist = trackObj.getString("artist"),
484
+ album = trackObj.getString("album"),
485
+ duration = trackObj.getDouble("duration"),
486
+ url = trackObj.getString("url"),
487
+ artwork = artwork,
488
+ extraPayload = extraPayload,
489
+ ),
490
+ )
491
+ }
492
+ val descriptionStr = jsonObject.optString("description")
493
+ val artworkStr = jsonObject.optString("artwork")
494
+ val playlist =
495
+ Playlist(
496
+ id = jsonObject.getString("id"),
497
+ name = jsonObject.getString("name"),
498
+ description = if (!descriptionStr.isNullOrEmpty()) descriptionStr else null,
499
+ artwork = if (!artworkStr.isNullOrEmpty()) artworkStr else null,
500
+ tracks = tracks,
501
+ )
502
+ playlists[playlist.id] = playlist
501
503
  }
502
504
  }
503
505
  }
@@ -216,13 +216,19 @@ extension TrackPlayerCore {
216
216
  guard let player = self.player else { return }
217
217
 
218
218
  let currentItem = player.currentItem
219
+
220
+ guard let playingTrackId = currentItem?.trackId else {
221
+ NitroPlayerLogger.log("TrackPlayerCore", "❌ No current item or track ID found during queue rebuild")
222
+ return
223
+ }
224
+
219
225
  let playingItems = player.items()
220
226
 
221
227
  // If the currently playing AVPlayerItem is no longer in currentTracks,
222
228
  // delegate to rebuildQueueFromPlaylistIndex so the player immediately
223
229
  // starts what is now at currentTrackIndex in the updated list.
224
- if let playingTrackId = currentItem?.trackId,
225
- !currentTracks.contains(where: { $0.id == playingTrackId }) {
230
+ if !currentTracks.contains(where: { $0.id == playingTrackId }) &&
231
+ currentTemporaryType == .none {
226
232
  let targetIndex = currentTrackIndex < currentTracks.count
227
233
  ? currentTrackIndex : currentTracks.count - 1
228
234
  if targetIndex >= 0 {
@@ -231,6 +237,14 @@ extension TrackPlayerCore {
231
237
  return
232
238
  }
233
239
 
240
+ // Sync currentTrackIndex to the track's actual position after a playlist mutation
241
+ // (e.g. reorder). Without this, the remaining-tracks slice uses the stale index,
242
+ // causing wrong tracks to play after skip/next.
243
+ if currentTemporaryType == .none,
244
+ let newIndex = currentTracks.firstIndex(where: { $0.id == playingTrackId }) {
245
+ currentTrackIndex = newIndex
246
+ }
247
+
234
248
  // Build the desired upcoming track list
235
249
  var newQueueTracks: [TrackItem] = []
236
250
  let currentId = currentItem?.trackId
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-player",
3
- "version": "0.7.1-alpha.1",
3
+ "version": "0.7.1-alpha.3",
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",
@@ -142,18 +142,21 @@ export function useEqualizer(): UseEqualizerResult {
142
142
  []
143
143
  )
144
144
 
145
- const setAllBandGains = useCallback(async (gains: number[]): Promise<boolean> => {
146
- setBands((prevBands) =>
147
- prevBands.map((b, i) => ({ ...b, gainDb: gains[i] ?? b.gainDb }))
148
- )
149
- try {
150
- await Equalizer.setAllBandGains(gains)
151
- return true
152
- } catch (error) {
153
- console.error('[useEqualizer] Error setting all band gains:', error)
154
- return false
155
- }
156
- }, [])
145
+ const setAllBandGains = useCallback(
146
+ async (gains: number[]): Promise<boolean> => {
147
+ setBands((prevBands) =>
148
+ prevBands.map((b, i) => ({ ...b, gainDb: gains[i] ?? b.gainDb }))
149
+ )
150
+ try {
151
+ await Equalizer.setAllBandGains(gains)
152
+ return true
153
+ } catch (error) {
154
+ console.error('[useEqualizer] Error setting all band gains:', error)
155
+ return false
156
+ }
157
+ },
158
+ []
159
+ )
157
160
 
158
161
  const reset = useCallback(async () => {
159
162
  setBands((prevBands) => prevBands.map((b) => ({ ...b, gainDb: 0 })))
@@ -4,8 +4,9 @@ import type { HybridObject } from 'react-native-nitro-modules'
4
4
  * Android Auto Media Library Manager
5
5
  * Android-only HybridObject for managing Android Auto media browser structure
6
6
  */
7
- export interface AndroidAutoMediaLibrary
8
- extends HybridObject<{ android: 'kotlin' }> {
7
+ export interface AndroidAutoMediaLibrary extends HybridObject<{
8
+ android: 'kotlin'
9
+ }> {
9
10
  /**
10
11
  * Set the Android Auto media library structure
11
12
  * This defines what folders and playlists appear in Android Auto
@@ -13,8 +13,10 @@ import type {
13
13
  PlaybackSource,
14
14
  } from '../types/DownloadTypes'
15
15
 
16
- export interface DownloadManager
17
- extends HybridObject<{ android: 'kotlin'; ios: 'swift' }> {
16
+ export interface DownloadManager extends HybridObject<{
17
+ android: 'kotlin'
18
+ ios: 'swift'
19
+ }> {
18
20
  /**
19
21
  * Configure the download manager
20
22
  */
@@ -6,8 +6,10 @@ import type {
6
6
  GainRange,
7
7
  } from '../types/EqualizerTypes'
8
8
 
9
- export interface Equalizer
10
- extends HybridObject<{ android: 'kotlin'; ios: 'swift' }> {
9
+ export interface Equalizer extends HybridObject<{
10
+ android: 'kotlin'
11
+ ios: 'swift'
12
+ }> {
11
13
  // === Enable/Disable ===
12
14
  /** Enable or disable the equalizer */
13
15
  setEnabled(enabled: boolean): Promise<void>