react-native-nitro-player 0.3.0-alpha.16 → 0.3.0-alpha.18
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.
|
@@ -12,6 +12,7 @@ import org.json.JSONArray
|
|
|
12
12
|
import org.json.JSONObject
|
|
13
13
|
import java.util.UUID
|
|
14
14
|
import java.util.concurrent.CopyOnWriteArrayList
|
|
15
|
+
|
|
15
16
|
import com.margelo.nitro.core.AnyMap
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -384,7 +385,7 @@ class PlaylistManager private constructor(
|
|
|
384
385
|
track.artwork?.let { put("artwork", it) }
|
|
385
386
|
// Serialize extraPayload to JSON for persistence
|
|
386
387
|
track.extraPayload?.let { payload ->
|
|
387
|
-
val extraPayloadMap = payload.
|
|
388
|
+
val extraPayloadMap = payload.toHashMap()
|
|
388
389
|
val extraPayloadJson = JSONObject(extraPayloadMap)
|
|
389
390
|
put("extraPayload", extraPayloadJson)
|
|
390
391
|
}
|
|
@@ -566,11 +566,13 @@ class TrackPlayerCore: NSObject {
|
|
|
566
566
|
private func setupCurrentItemObservers(item: AVPlayerItem) {
|
|
567
567
|
print("📱 TrackPlayerCore: Setting up item observers")
|
|
568
568
|
|
|
569
|
-
// Observe status - recreate boundaries when ready
|
|
569
|
+
// Observe status - recreate boundaries when ready and update now playing info
|
|
570
570
|
let statusObserver = item.observe(\.status, options: [.new]) { [weak self] item, _ in
|
|
571
571
|
if item.status == .readyToPlay {
|
|
572
572
|
print("✅ TrackPlayerCore: Item ready, setting up boundaries")
|
|
573
573
|
self?.setupBoundaryTimeObserver()
|
|
574
|
+
// Update now playing info now that duration is available
|
|
575
|
+
self?.mediaSessionManager?.updateNowPlayingInfo()
|
|
574
576
|
} else if item.status == .failed {
|
|
575
577
|
print("❌ TrackPlayerCore: Item failed")
|
|
576
578
|
self?.notifyPlaybackStateChange(.stopped, .error)
|
|
@@ -1406,6 +1408,10 @@ class TrackPlayerCore: NSObject {
|
|
|
1406
1408
|
self.isManuallySeeked = true
|
|
1407
1409
|
let time = CMTime(seconds: position, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
|
1408
1410
|
player.seek(to: time) { [weak self] completed in
|
|
1411
|
+
// Always update now playing info to restore playback rate after seek
|
|
1412
|
+
// This ensures the scrubber animation resumes correctly
|
|
1413
|
+
self?.mediaSessionManager?.updateNowPlayingInfo()
|
|
1414
|
+
|
|
1409
1415
|
if completed {
|
|
1410
1416
|
let duration = player.currentItem?.duration.seconds ?? 0.0
|
|
1411
1417
|
self?.notifySeek(position, duration)
|
|
@@ -106,36 +106,29 @@ class MediaSessionManager {
|
|
|
106
106
|
return .success
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
commandCenter.seekForwardCommand.
|
|
112
|
-
|
|
113
|
-
let state = core.getState()
|
|
114
|
-
let newPosition = min(state.currentPosition + Constants.seekInterval, state.totalDuration)
|
|
115
|
-
core.seek(position: newPosition)
|
|
116
|
-
return .success
|
|
117
|
-
}
|
|
109
|
+
// Disable continuous seek commands - they replace the interactive scrubber
|
|
110
|
+
// with non-interactive forward/backward buttons on the lock screen
|
|
111
|
+
commandCenter.seekForwardCommand.isEnabled = false
|
|
112
|
+
commandCenter.seekBackwardCommand.isEnabled = false
|
|
118
113
|
|
|
119
|
-
//
|
|
120
|
-
commandCenter.seekBackwardCommand.isEnabled = true
|
|
121
|
-
commandCenter.seekBackwardCommand.addTarget { [weak self] event in
|
|
122
|
-
guard let self = self, let core = self.trackPlayerCore else { return .commandFailed }
|
|
123
|
-
let state = core.getState()
|
|
124
|
-
let newPosition = max(state.currentPosition - Constants.seekInterval, 0.0)
|
|
125
|
-
core.seek(position: newPosition)
|
|
126
|
-
return .success
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Change playback position
|
|
114
|
+
// Change playback position (interactive scrubber)
|
|
130
115
|
commandCenter.changePlaybackPositionCommand.isEnabled = true
|
|
131
116
|
commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in
|
|
132
117
|
guard let self = self,
|
|
133
118
|
let core = self.trackPlayerCore,
|
|
134
|
-
let
|
|
119
|
+
let positionEvent = event as? MPChangePlaybackPositionCommandEvent
|
|
135
120
|
else {
|
|
136
121
|
return .commandFailed
|
|
137
122
|
}
|
|
138
|
-
|
|
123
|
+
// Immediately update elapsed time AND set playback rate to 0 during seek
|
|
124
|
+
// This prevents the scrubber from freezing/desyncing during the async seek operation
|
|
125
|
+
if var info = MPNowPlayingInfoCenter.default().nowPlayingInfo {
|
|
126
|
+
info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = positionEvent.positionTime
|
|
127
|
+
// Set rate to 0 to pause scrubber animation during seek
|
|
128
|
+
info[MPNowPlayingInfoPropertyPlaybackRate] = 0.0
|
|
129
|
+
MPNowPlayingInfoCenter.default().nowPlayingInfo = info
|
|
130
|
+
}
|
|
131
|
+
core.seek(position: positionEvent.positionTime)
|
|
139
132
|
return .success
|
|
140
133
|
}
|
|
141
134
|
}
|
|
@@ -156,20 +149,39 @@ class MediaSessionManager {
|
|
|
156
149
|
|
|
157
150
|
let state = core.getState()
|
|
158
151
|
|
|
159
|
-
|
|
152
|
+
// Use player duration if valid, otherwise fall back to track metadata duration.
|
|
153
|
+
// Duration must always be present for the lock screen scrubber to be interactive.
|
|
154
|
+
let playerDuration = state.totalDuration
|
|
155
|
+
let effectiveDuration: Double
|
|
156
|
+
if playerDuration > 0 && !playerDuration.isNaN && !playerDuration.isInfinite {
|
|
157
|
+
effectiveDuration = playerDuration
|
|
158
|
+
} else {
|
|
159
|
+
effectiveDuration = track.duration
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
var nowPlayingInfo: [String: Any] = [
|
|
160
163
|
MPMediaItemPropertyTitle: track.title,
|
|
161
164
|
MPMediaItemPropertyArtist: track.artist,
|
|
162
165
|
MPMediaItemPropertyAlbumTitle: track.album,
|
|
163
166
|
MPNowPlayingInfoPropertyElapsedPlaybackTime: state.currentPosition,
|
|
164
|
-
MPMediaItemPropertyPlaybackDuration:
|
|
167
|
+
MPMediaItemPropertyPlaybackDuration: effectiveDuration,
|
|
165
168
|
MPNowPlayingInfoPropertyPlaybackRate: state.currentState == .playing ? 1.0 : 0.0,
|
|
166
169
|
]
|
|
167
170
|
|
|
168
|
-
//
|
|
171
|
+
// Add artwork synchronously if cached, otherwise load async
|
|
169
172
|
if let artwork = track.artwork, case .second(let artworkUrl) = artwork {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
+
if let cachedImage = artworkCache[artworkUrl] {
|
|
174
|
+
// Artwork is cached - include it directly to avoid overwrite race condition
|
|
175
|
+
nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(
|
|
176
|
+
boundsSize: CGSize(width: Constants.artworkSize, height: Constants.artworkSize),
|
|
177
|
+
requestHandler: { _ in cachedImage }
|
|
178
|
+
)
|
|
179
|
+
} else {
|
|
180
|
+
// Artwork not cached - load asynchronously and update later
|
|
181
|
+
loadArtwork(url: artworkUrl) { [weak self] image in
|
|
182
|
+
guard let self = self, let image = image else { return }
|
|
183
|
+
// Re-read current nowPlayingInfo to avoid overwriting other updates
|
|
184
|
+
var updatedInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]
|
|
173
185
|
updatedInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(
|
|
174
186
|
boundsSize: CGSize(width: Constants.artworkSize, height: Constants.artworkSize),
|
|
175
187
|
requestHandler: { _ in image }
|