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.
- package/android/src/main/AndroidManifest.xml +15 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +5 -6
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +68 -49
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +67 -21
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +27 -5
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +88 -49
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +40 -10
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ExoPlayerCore.kt +46 -45
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ListenerRegistry.kt +4 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerAndroidAuto.kt +38 -32
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +146 -81
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +24 -13
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerNotify.kt +16 -4
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +101 -72
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueue.kt +42 -22
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +55 -24
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +27 -8
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerTempQueue.kt +73 -62
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +51 -48
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadManagerCore.kt +14 -3
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadWorker.kt +94 -23
- package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +43 -34
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/ExoPlayerBuilder.kt +49 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionCallbackFactory.kt +167 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +11 -450
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/NitroPlayerNotificationProvider.kt +200 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/PlaybackService.kt +101 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +87 -85
- package/ios/core/TrackPlayerQueueBuild.swift +16 -2
- package/package.json +1 -1
- package/src/hooks/useEqualizer.ts +15 -12
- package/src/specs/AndroidAutoMediaLibrary.nitro.ts +3 -2
- package/src/specs/DownloadManager.nitro.ts +4 -2
- package/src/specs/Equalizer.nitro.ts +4 -2
- package/src/specs/TrackPlayer.nitro.ts +18 -6
package/android/src/main/java/com/margelo/nitro/nitroplayer/media/NitroPlayerNotificationProvider.kt
ADDED
|
@@ -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 =
|
|
40
|
-
|
|
41
|
-
|
|
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 =
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
),
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
)
|
|
500
|
-
|
|
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
|
|
225
|
-
|
|
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.
|
|
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(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>
|