react-native-nitro-player 0.7.1-alpha.2 → 1.0.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.
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ExoPlayerCore.kt +8 -48
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +78 -18
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +15 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +25 -7
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadManagerCore.kt +2 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadWorker.kt +94 -23
- 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 +10 -301
- 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 +72 -11
- package/ios/core/TrackPlayerListener.swift +20 -15
- package/ios/core/TrackPlayerPlayback.swift +10 -2
- package/ios/core/TrackPlayerQueue.swift +2 -1
- package/ios/core/TrackPlayerQueueBuild.swift +16 -2
- package/package.json +1 -1
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
|
+
}
|
|
@@ -1,28 +1,86 @@
|
|
|
1
1
|
package com.margelo.nitro.nitroplayer.media
|
|
2
2
|
|
|
3
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
|
|
4
10
|
import androidx.media3.session.MediaSession
|
|
5
11
|
import androidx.media3.session.MediaSessionService
|
|
6
12
|
import com.margelo.nitro.nitroplayer.core.NitroPlayerLogger
|
|
13
|
+
import com.margelo.nitro.nitroplayer.core.TrackPlayerCore
|
|
14
|
+
import com.margelo.nitro.nitroplayer.playlist.PlaylistManager
|
|
7
15
|
|
|
8
16
|
/**
|
|
9
|
-
* Foreground service that
|
|
17
|
+
* Foreground service that **owns** the ExoPlayer and MediaSession.
|
|
10
18
|
*
|
|
11
|
-
* Media3's
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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.
|
|
16
26
|
*/
|
|
27
|
+
@UnstableApi
|
|
17
28
|
class NitroPlayerPlaybackService : MediaSessionService() {
|
|
18
29
|
|
|
19
30
|
companion object {
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
22
48
|
}
|
|
23
49
|
|
|
24
|
-
|
|
25
|
-
|
|
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)
|
|
26
84
|
}
|
|
27
85
|
|
|
28
86
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
|
@@ -34,7 +92,10 @@ class NitroPlayerPlaybackService : MediaSessionService() {
|
|
|
34
92
|
}
|
|
35
93
|
|
|
36
94
|
override fun onDestroy() {
|
|
37
|
-
NitroPlayerLogger.log("PlaybackService") { "
|
|
95
|
+
NitroPlayerLogger.log("PlaybackService") { "onDestroy" }
|
|
96
|
+
mediaSession?.release()
|
|
97
|
+
mediaSession = null
|
|
98
|
+
player.release()
|
|
38
99
|
super.onDestroy()
|
|
39
100
|
}
|
|
40
101
|
}
|
|
@@ -79,14 +79,16 @@ extension TrackPlayerCore {
|
|
|
79
79
|
currentItem.status == .readyToPlay else { return }
|
|
80
80
|
|
|
81
81
|
let duration = currentItem.duration.seconds
|
|
82
|
-
guard duration > 0 && !duration.isNaN && !duration.isInfinite else { return }
|
|
83
|
-
|
|
84
82
|
let interval: Double
|
|
85
|
-
if duration >
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
if duration > 0 && !duration.isNaN && !duration.isInfinite {
|
|
84
|
+
if duration > Constants.twoHoursInSeconds { interval = Constants.boundaryIntervalLong }
|
|
85
|
+
else if duration > Constants.oneHourInSeconds { interval = Constants.boundaryIntervalMedium }
|
|
86
|
+
else { interval = Constants.boundaryIntervalDefault }
|
|
87
|
+
} else {
|
|
88
|
+
interval = Constants.boundaryIntervalDefault
|
|
89
|
+
}
|
|
88
90
|
|
|
89
|
-
NitroPlayerLogger.log("TrackPlayerCore", "⏱️ Setting up periodic observer (interval: \(interval)s, duration: \(
|
|
91
|
+
NitroPlayerLogger.log("TrackPlayerCore", "⏱️ Setting up periodic observer (interval: \(interval)s, duration: \(duration)s)")
|
|
90
92
|
|
|
91
93
|
let cmInterval = CMTime(seconds: interval, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
|
92
94
|
// Deliver on playerQueue (not main)
|
|
@@ -104,20 +106,23 @@ extension TrackPlayerCore {
|
|
|
104
106
|
guard player.rate > 0 else { return }
|
|
105
107
|
|
|
106
108
|
let position = currentItem.currentTime().seconds
|
|
107
|
-
let
|
|
108
|
-
|
|
109
|
+
let rawDuration = currentItem.duration.seconds
|
|
110
|
+
let duration = (rawDuration > 0 && !rawDuration.isNaN && !rawDuration.isInfinite) ? rawDuration : 0.0
|
|
109
111
|
|
|
110
|
-
NitroPlayerLogger.log("TrackPlayerCore", "⏱️ Boundary crossed - position: \(Int(position))s / \(
|
|
112
|
+
NitroPlayerLogger.log("TrackPlayerCore", "⏱️ Boundary crossed - position: \(Int(position))s / duration: \(duration)s")
|
|
111
113
|
|
|
112
114
|
notifyPlaybackProgress(position, duration, isManuallySeeked ? true : nil)
|
|
113
115
|
isManuallySeeked = false
|
|
114
116
|
|
|
115
|
-
|
|
116
|
-
if
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
// Only do remaining-time preload when duration is known
|
|
118
|
+
if duration > 0 {
|
|
119
|
+
let remaining = duration - position
|
|
120
|
+
if remaining > 0 && remaining <= Constants.preferredForwardBufferDuration && !didRequestUrlsForCurrentItem {
|
|
121
|
+
didRequestUrlsForCurrentItem = true
|
|
122
|
+
NitroPlayerLogger.log("TrackPlayerCore",
|
|
123
|
+
"⏳ \(Int(remaining))s remaining — proactively checking upcoming URLs")
|
|
124
|
+
checkUpcomingTracksForUrls(lookahead: lookaheadCount)
|
|
125
|
+
}
|
|
121
126
|
}
|
|
122
127
|
}
|
|
123
128
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import AVFoundation
|
|
9
9
|
import Foundation
|
|
10
|
+
import MediaPlayer
|
|
10
11
|
|
|
11
12
|
extension TrackPlayerCore {
|
|
12
13
|
|
|
@@ -115,8 +116,15 @@ extension TrackPlayerCore {
|
|
|
115
116
|
self.isManuallySeeked = true
|
|
116
117
|
let time = CMTime(seconds: position, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
|
117
118
|
player.seek(to: time) { [weak self] completed in
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
// HackFix I dont know how to fix this, but it works.
|
|
120
|
+
let rate = Double(player.rate)
|
|
121
|
+
DispatchQueue.main.async {
|
|
122
|
+
if var info = MPNowPlayingInfoCenter.default().nowPlayingInfo {
|
|
123
|
+
info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = position
|
|
124
|
+
info[MPNowPlayingInfoPropertyPlaybackRate] = rate
|
|
125
|
+
MPNowPlayingInfoCenter.default().nowPlayingInfo = info
|
|
126
|
+
}
|
|
127
|
+
}
|
|
120
128
|
if completed {
|
|
121
129
|
let duration = player.currentItem?.duration.seconds ?? 0.0
|
|
122
130
|
self?.notifySeek(position, duration)
|
|
@@ -43,7 +43,8 @@ extension TrackPlayerCore {
|
|
|
43
43
|
}
|
|
44
44
|
let currentTrack = getCurrentTrack()
|
|
45
45
|
let currentPosition = player.currentTime().seconds
|
|
46
|
-
let
|
|
46
|
+
let rawDuration = player.currentItem?.duration.seconds ?? 0.0
|
|
47
|
+
let totalDuration = (rawDuration > 0 && !rawDuration.isNaN && !rawDuration.isInfinite) ? rawDuration : 0.0
|
|
47
48
|
|
|
48
49
|
let state: TrackPlayerState
|
|
49
50
|
if player.rate == 0 { state = .paused }
|
|
@@ -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.
|
|
3
|
+
"version": "1.0.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",
|