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.toMap()
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
- // Seek forward
110
- commandCenter.seekForwardCommand.isEnabled = true
111
- commandCenter.seekForwardCommand.addTarget { [weak self] event in
112
- guard let self = self, let core = self.trackPlayerCore else { return .commandFailed }
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
- // Seek backward
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 event = event as? MPChangePlaybackPositionCommandEvent
119
+ let positionEvent = event as? MPChangePlaybackPositionCommandEvent
135
120
  else {
136
121
  return .commandFailed
137
122
  }
138
- core.seek(position: event.positionTime)
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
- let nowPlayingInfo: [String: Any] = [
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: state.totalDuration,
167
+ MPMediaItemPropertyPlaybackDuration: effectiveDuration,
165
168
  MPNowPlayingInfoPropertyPlaybackRate: state.currentState == .playing ? 1.0 : 0.0,
166
169
  ]
167
170
 
168
- // Load artwork asynchronously
171
+ // Add artwork synchronously if cached, otherwise load async
169
172
  if let artwork = track.artwork, case .second(let artworkUrl) = artwork {
170
- loadArtwork(url: artworkUrl) { [weak self] image in
171
- if let image = image {
172
- var updatedInfo = nowPlayingInfo
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 }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-player",
3
- "version": "0.3.0-alpha.16",
3
+ "version": "0.3.0-alpha.18",
4
4
  "description": "react-native-nitro-player",
5
5
  "main": "lib/index",
6
6
  "module": "lib/index",