react-native-nitro-player 0.4.1-alpha.0 → 0.5.0

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.
@@ -50,6 +50,7 @@ class DownloadFileManager private constructor(
50
50
  fun createDownloadFile(
51
51
  trackId: String,
52
52
  storageLocation: StorageLocation,
53
+ extension: String = "mp3",
53
54
  ): File {
54
55
  val destinationDir =
55
56
  when (storageLocation) {
@@ -58,7 +59,7 @@ class DownloadFileManager private constructor(
58
59
  }
59
60
 
60
61
  // Create unique filename based on trackId
61
- val fileName = "$trackId.mp3"
62
+ val fileName = "$trackId.$extension"
62
63
  return File(destinationDir, fileName)
63
64
  }
64
65
 
@@ -119,15 +120,17 @@ class DownloadFileManager private constructor(
119
120
 
120
121
  fun getLocalPath(trackId: String): String? {
121
122
  // Check private directory first
122
- val privateFile = File(privateDownloadsDir, "$trackId.mp3")
123
- if (privateFile.exists()) {
124
- return privateFile.absolutePath
123
+ privateDownloadsDir.listFiles()?.forEach { file ->
124
+ if (file.nameWithoutExtension == trackId) {
125
+ return file.absolutePath
126
+ }
125
127
  }
126
128
 
127
129
  // Check public directory
128
- val publicFile = File(publicDownloadsDir, "$trackId.mp3")
129
- if (publicFile.exists()) {
130
- return publicFile.absolutePath
130
+ publicDownloadsDir.listFiles()?.forEach { file ->
131
+ if (file.nameWithoutExtension == trackId) {
132
+ return file.absolutePath
133
+ }
131
134
  }
132
135
 
133
136
  return null
@@ -8,6 +8,7 @@ import androidx.core.app.NotificationCompat
8
8
  import androidx.work.CoroutineWorker
9
9
  import androidx.work.ForegroundInfo
10
10
  import androidx.work.WorkerParameters
11
+ import android.webkit.MimeTypeMap
11
12
  import com.margelo.nitro.nitroplayer.*
12
13
  import kotlinx.coroutines.Dispatchers
13
14
  import kotlinx.coroutines.withContext
@@ -122,16 +123,42 @@ class DownloadWorker(
122
123
  if (responseCode != HttpURLConnection.HTTP_OK) {
123
124
  throw Exception("Server returned HTTP $responseCode")
124
125
  }
126
+ // Determine extension
127
+ var extension = MimeTypeMap.getFileExtensionFromUrl(urlString)
128
+
129
+ // 1. Try Content-Disposition
130
+ if (extension.isNullOrEmpty()) {
131
+ val contentDisposition = connection.getHeaderField("Content-Disposition")
132
+ if (contentDisposition != null) {
133
+ val match = Regex("filename=\"?([^\";]+)\"?").find(contentDisposition)
134
+ if (match != null) {
135
+ val filename = match.groupValues[1]
136
+ extension = MimeTypeMap.getFileExtensionFromUrl(filename)
137
+ }
138
+ }
139
+ }
140
+
141
+ // 2. Try Content-Type
142
+ if (extension.isNullOrEmpty()) {
143
+ val contentType = connection.contentType
144
+ if (contentType != null) {
145
+ val mimeType = contentType.split(";")[0].trim()
146
+ extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)
147
+ }
148
+ }
149
+
150
+ val finalExtension = if (extension.isNullOrEmpty()) "mp3" else extension
125
151
 
126
- val totalBytes = connection.contentLengthLong
127
- var bytesDownloaded: Long = 0
128
152
 
129
153
  // Create destination file
130
- val destinationFile = fileManager.createDownloadFile(trackId, storageLocation)
154
+ val destinationFile = fileManager.createDownloadFile(trackId, storageLocation, finalExtension)
131
155
 
132
156
  inputStream = BufferedInputStream(connection.inputStream)
133
157
  outputStream = FileOutputStream(destinationFile)
134
158
 
159
+ val totalBytes = connection.contentLengthLong
160
+ var bytesDownloaded: Long = 0
161
+
135
162
  val buffer = ByteArray(BUFFER_SIZE)
136
163
  var bytesRead: Int
137
164
  var lastProgressUpdate = System.currentTimeMillis()
@@ -1785,6 +1785,7 @@ class TrackPlayerCore: NSObject {
1785
1785
  if self.player?.currentItem != nil {
1786
1786
  self.rebuildAVQueueFromCurrentPosition()
1787
1787
  }
1788
+ mediaSessionManager?.onQueueChanged()
1788
1789
  }
1789
1790
 
1790
1791
  /**
@@ -1814,6 +1815,7 @@ class TrackPlayerCore: NSObject {
1814
1815
  if self.player?.currentItem != nil {
1815
1816
  self.rebuildAVQueueFromCurrentPosition()
1816
1817
  }
1818
+ mediaSessionManager?.onQueueChanged()
1817
1819
  }
1818
1820
 
1819
1821
  /**
@@ -56,20 +56,28 @@ final class DownloadFileManager {
56
56
 
57
57
  func saveDownloadedFile(
58
58
  from temporaryLocation: URL, trackId: String, storageLocation: StorageLocation,
59
- originalURL: String? = nil
59
+ originalURL: String? = nil,
60
+ suggestedFilename: String? = nil
60
61
  ) -> String? {
61
62
  print("🎯 DownloadFileManager: saveDownloadedFile called for trackId=\(trackId)")
62
63
  print(" From: \(temporaryLocation.path)")
63
64
  print(" Original URL: \(originalURL ?? "nil")")
65
+ print(" Suggested Filename: \(suggestedFilename ?? "nil")")
64
66
 
65
67
  let destinationDirectory =
66
68
  storageLocation == .private ? privateDownloadsDirectory : publicDownloadsDirectory
67
69
  print(" Destination directory: \(destinationDirectory.path)")
68
70
 
69
- // Determine file extension from the original URL, not the temp file
70
- // The temp file has .tmp extension which AVPlayer cannot play
71
+ // Determine file extension
71
72
  var fileExtension = "mp3" // Default fallback
72
- if let originalURL = originalURL, let url = URL(string: originalURL) {
73
+
74
+ if let suggestedFilename = suggestedFilename, !suggestedFilename.isEmpty {
75
+ let url = URL(fileURLWithPath: suggestedFilename)
76
+ let pathExtension = url.pathExtension.lowercased()
77
+ if !pathExtension.isEmpty {
78
+ fileExtension = pathExtension
79
+ }
80
+ } else if let originalURL = originalURL, let url = URL(string: originalURL) {
73
81
  let pathExtension = url.pathExtension.lowercased()
74
82
  if !pathExtension.isEmpty {
75
83
  fileExtension = pathExtension
@@ -681,11 +681,16 @@ extension DownloadManagerCore: URLSessionDownloadDelegate {
681
681
  let (storageLocation, originalURL) = queue.sync {
682
682
  (self.config.storageLocation ?? .private, self.trackMetadata[trackId]?.url)
683
683
  }
684
+
685
+ // Get suggested filename from response
686
+ let suggestedFilename = downloadTask.response?.suggestedFilename
687
+
684
688
  let destinationPath = DownloadFileManager.shared.saveDownloadedFile(
685
689
  from: location,
686
690
  trackId: trackId,
687
691
  storageLocation: storageLocation,
688
- originalURL: originalURL
692
+ originalURL: originalURL,
693
+ suggestedFilename: suggestedFilename
689
694
  )
690
695
 
691
696
  // Now handle the rest asynchronously
@@ -15,10 +15,6 @@ class MediaSessionManager {
15
15
  // MARK: - Constants
16
16
 
17
17
  private enum Constants {
18
- // Seek intervals (in seconds)
19
- static let seekInterval: Double = 10.0
20
-
21
- // Artwork size
22
18
  static let artworkSize: CGFloat = 500.0
23
19
  }
24
20
 
@@ -27,10 +23,11 @@ class MediaSessionManager {
27
23
  private var trackPlayerCore: TrackPlayerCore?
28
24
  private var artworkCache: [String: UIImage] = [:]
29
25
 
30
- private var androidAutoEnabled: Bool = false
31
- private var carPlayEnabled: Bool = false
32
26
  private var showInNotification: Bool = true
33
27
 
28
+ // Tracks the artwork URL currently shown so we can discard stale async loads
29
+ private var lastArtworkUrl: String?
30
+
34
31
  init() {
35
32
  setupRemoteCommandCenter()
36
33
  }
@@ -44,47 +41,165 @@ class MediaSessionManager {
44
41
  carPlayEnabled: Bool?,
45
42
  showInNotification: Bool?
46
43
  ) {
47
- if let androidAutoEnabled = androidAutoEnabled {
48
- self.androidAutoEnabled = androidAutoEnabled
49
- }
50
- if let carPlayEnabled = carPlayEnabled {
51
- self.carPlayEnabled = carPlayEnabled
52
- // CarPlay is handled by the app's CarPlaySceneDelegate
53
- // We just maintain the flag here for reference
54
- }
55
44
  if let showInNotification = showInNotification {
56
45
  self.showInNotification = showInNotification
57
- if showInNotification {
58
- updateNowPlayingInfo()
46
+ }
47
+ refresh()
48
+ }
49
+
50
+ // MARK: - Single refresh entry point
51
+ //
52
+ // All public callbacks route here. Always dispatches to main thread so
53
+ // MPNowPlayingInfoCenter and MPRemoteCommandCenter are only touched from main.
54
+
55
+ func refresh() {
56
+ if Thread.isMainThread {
57
+ refreshInternal()
58
+ } else {
59
+ DispatchQueue.main.async { [weak self] in
60
+ self?.refreshInternal()
61
+ }
62
+ }
63
+ }
64
+
65
+ // Convenience aliases used by TrackPlayerCore call sites
66
+ func updateNowPlayingInfo() { refresh() }
67
+ func onTrackChanged() { refresh() }
68
+ func onPlaybackStateChanged() { refresh() }
69
+ func onQueueChanged() { refresh() }
70
+
71
+ // MARK: - Core internal update (main thread only)
72
+
73
+ private func refreshInternal() {
74
+ guard showInNotification else {
75
+ clearNowPlayingInfo()
76
+ disableAllCommands()
77
+ return
78
+ }
79
+
80
+ guard let core = trackPlayerCore,
81
+ let track = core.getCurrentTrack()
82
+ else {
83
+ clearNowPlayingInfo()
84
+ disableAllCommands()
85
+ return
86
+ }
87
+
88
+ // Fetch snapshot once — both calls are cheap on main thread (no sync overhead)
89
+ let state = core.getState()
90
+ let queue = core.getActualQueue()
91
+
92
+ // Find the actual position of the current track inside the actual queue.
93
+ // state.currentIndex is the original-playlist index which is wrong when a
94
+ // temp (playNext / upNext) track is playing.
95
+ let positionInQueue = queue.firstIndex(where: { $0.id == track.id }) ?? -1
96
+
97
+ updateNowPlayingInfoInternal(track: track, state: state, queue: queue, positionInQueue: positionInQueue)
98
+ updateCommandCenterState(state: state, queue: queue, positionInQueue: positionInQueue)
99
+ }
100
+
101
+ // MARK: - Now Playing Info
102
+
103
+ private func updateNowPlayingInfoInternal(
104
+ track: TrackItem,
105
+ state: PlayerState,
106
+ queue: [TrackItem],
107
+ positionInQueue: Int
108
+ ) {
109
+ let playerDuration = state.totalDuration
110
+ let effectiveDuration: Double
111
+ if playerDuration > 0 && !playerDuration.isNaN && !playerDuration.isInfinite {
112
+ effectiveDuration = playerDuration
113
+ } else if track.duration > 0 {
114
+ effectiveDuration = track.duration
115
+ } else {
116
+ effectiveDuration = 0
117
+ }
118
+
119
+ let currentPosition = state.currentPosition
120
+ let safePosition = currentPosition.isNaN || currentPosition.isInfinite ? 0 : currentPosition
121
+ let isPlaying = state.currentState == .playing
122
+
123
+ var nowPlayingInfo: [String: Any] = [
124
+ MPMediaItemPropertyTitle: track.title,
125
+ MPMediaItemPropertyArtist: track.artist,
126
+ MPMediaItemPropertyAlbumTitle: track.album,
127
+ MPNowPlayingInfoPropertyElapsedPlaybackTime: safePosition,
128
+ MPMediaItemPropertyPlaybackDuration: effectiveDuration,
129
+ MPNowPlayingInfoPropertyPlaybackRate: isPlaying ? 1.0 : 0.0,
130
+ MPNowPlayingInfoPropertyDefaultPlaybackRate: 1.0,
131
+ MPNowPlayingInfoPropertyPlaybackQueueCount: max(1, queue.count),
132
+ MPNowPlayingInfoPropertyPlaybackQueueIndex: max(0, positionInQueue),
133
+ ]
134
+
135
+ // Artwork: use cache synchronously when available, otherwise kick off async load
136
+ if let artwork = track.artwork, case .second(let artworkUrl) = artwork {
137
+ lastArtworkUrl = artworkUrl
138
+ if let cachedImage = artworkCache[artworkUrl] {
139
+ nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(
140
+ boundsSize: CGSize(width: Constants.artworkSize, height: Constants.artworkSize),
141
+ requestHandler: { _ in cachedImage }
142
+ )
59
143
  } else {
60
- clearNowPlayingInfo()
144
+ // Write info first without artwork, then patch it in when loaded
145
+ MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
146
+ loadArtwork(url: artworkUrl) { [weak self] image in
147
+ guard let self = self, let image = image else { return }
148
+ // Discard if track changed while loading
149
+ guard self.lastArtworkUrl == artworkUrl else { return }
150
+ var updated = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]
151
+ updated[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(
152
+ boundsSize: CGSize(width: Constants.artworkSize, height: Constants.artworkSize),
153
+ requestHandler: { _ in image }
154
+ )
155
+ MPNowPlayingInfoCenter.default().nowPlayingInfo = updated
156
+ }
157
+ return
61
158
  }
159
+ } else {
160
+ lastArtworkUrl = nil
62
161
  }
162
+
163
+ MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
63
164
  }
64
165
 
166
+ // MARK: - Command Center State
167
+
65
168
  private func setupRemoteCommandCenter() {
66
169
  let commandCenter = MPRemoteCommandCenter.shared()
67
170
 
68
- // Play command
171
+ // Clear any previously registered targets before adding fresh ones.
172
+ // Prevents duplicate handlers if this were ever called more than once.
173
+ commandCenter.playCommand.removeTarget(nil)
174
+ commandCenter.pauseCommand.removeTarget(nil)
175
+ commandCenter.togglePlayPauseCommand.removeTarget(nil)
176
+ commandCenter.nextTrackCommand.removeTarget(nil)
177
+ commandCenter.previousTrackCommand.removeTarget(nil)
178
+ commandCenter.seekForwardCommand.removeTarget(nil)
179
+ commandCenter.seekBackwardCommand.removeTarget(nil)
180
+ commandCenter.changePlaybackPositionCommand.removeTarget(nil)
181
+
182
+ // Play
69
183
  commandCenter.playCommand.isEnabled = true
70
184
  commandCenter.playCommand.addTarget { [weak self] _ in
71
- self?.trackPlayerCore?.play()
185
+ guard let core = self?.trackPlayerCore else { return .commandFailed }
186
+ core.play()
72
187
  return .success
73
188
  }
74
189
 
75
- // Pause command
190
+ // Pause
76
191
  commandCenter.pauseCommand.isEnabled = true
77
192
  commandCenter.pauseCommand.addTarget { [weak self] _ in
78
- self?.trackPlayerCore?.pause()
193
+ guard let core = self?.trackPlayerCore else { return .commandFailed }
194
+ core.pause()
79
195
  return .success
80
196
  }
81
197
 
82
198
  // Toggle play/pause
83
199
  commandCenter.togglePlayPauseCommand.isEnabled = true
84
200
  commandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in
85
- guard let self = self, let core = self.trackPlayerCore else { return .commandFailed }
86
- let state = core.getState()
87
- if state.currentState == .playing {
201
+ guard let core = self?.trackPlayerCore else { return .commandFailed }
202
+ if core.getState().currentState == .playing {
88
203
  core.pause()
89
204
  } else {
90
205
  core.play()
@@ -92,27 +207,28 @@ class MediaSessionManager {
92
207
  return .success
93
208
  }
94
209
 
95
- // Next track command
96
- commandCenter.nextTrackCommand.isEnabled = true
210
+ // Next track — isEnabled managed dynamically in updateCommandCenterState
211
+ commandCenter.nextTrackCommand.isEnabled = false
97
212
  commandCenter.nextTrackCommand.addTarget { [weak self] _ in
98
- self?.trackPlayerCore?.skipToNext()
213
+ guard let core = self?.trackPlayerCore else { return .commandFailed }
214
+ core.skipToNext()
99
215
  return .success
100
216
  }
101
217
 
102
- // Previous track command
103
- commandCenter.previousTrackCommand.isEnabled = true
218
+ // Previous track — isEnabled managed dynamically in updateCommandCenterState
219
+ commandCenter.previousTrackCommand.isEnabled = false
104
220
  commandCenter.previousTrackCommand.addTarget { [weak self] _ in
105
- self?.trackPlayerCore?.skipToPrevious()
221
+ guard let core = self?.trackPlayerCore else { return .commandFailed }
222
+ core.skipToPrevious()
106
223
  return .success
107
224
  }
108
225
 
109
- // Disable continuous seek commands - they replace the interactive scrubber
110
- // with non-interactive forward/backward buttons on the lock screen
226
+ // Disable skip-forward/backward these replace the scrubber with non-interactive buttons
111
227
  commandCenter.seekForwardCommand.isEnabled = false
112
228
  commandCenter.seekBackwardCommand.isEnabled = false
113
229
 
114
- // Change playback position (interactive scrubber)
115
- commandCenter.changePlaybackPositionCommand.isEnabled = true
230
+ // Scrubber isEnabled managed dynamically based on known duration
231
+ commandCenter.changePlaybackPositionCommand.isEnabled = false
116
232
  commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in
117
233
  guard let self = self,
118
234
  let core = self.trackPlayerCore,
@@ -120,11 +236,10 @@ class MediaSessionManager {
120
236
  else {
121
237
  return .commandFailed
122
238
  }
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
239
+ // Optimistically freeze the scrubber at the tapped position while the async
240
+ // seek is in flight updateNowPlayingInfo in the seek completion restores it.
125
241
  if var info = MPNowPlayingInfoCenter.default().nowPlayingInfo {
126
242
  info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = positionEvent.positionTime
127
- // Set rate to 0 to pause scrubber animation during seek
128
243
  info[MPNowPlayingInfoPropertyPlaybackRate] = 0.0
129
244
  MPNowPlayingInfoCenter.default().nowPlayingInfo = info
130
245
  }
@@ -133,73 +248,43 @@ class MediaSessionManager {
133
248
  }
134
249
  }
135
250
 
136
- private func getCurrentTrack() -> TrackItem? {
137
- return trackPlayerCore?.getCurrentTrack()
138
- }
139
-
140
- func updateNowPlayingInfo() {
141
- guard showInNotification else { return }
142
-
143
- guard let track = getCurrentTrack(),
144
- let core = trackPlayerCore
145
- else {
146
- clearNowPlayingInfo()
147
- return
148
- }
149
-
150
- let state = core.getState()
251
+ private func updateCommandCenterState(
252
+ state: PlayerState,
253
+ queue: [TrackItem],
254
+ positionInQueue: Int
255
+ ) {
256
+ let commandCenter = MPRemoteCommandCenter.shared()
257
+ let hasCurrentTrack = positionInQueue >= 0
258
+ let isNotLast = positionInQueue < queue.count - 1
151
259
 
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
260
  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
- }
261
+ let hasDuration = playerDuration > 0 && !playerDuration.isNaN && !playerDuration.isInfinite
161
262
 
162
- var nowPlayingInfo: [String: Any] = [
163
- MPMediaItemPropertyTitle: track.title,
164
- MPMediaItemPropertyArtist: track.artist,
165
- MPMediaItemPropertyAlbumTitle: track.album,
166
- MPNowPlayingInfoPropertyElapsedPlaybackTime: state.currentPosition,
167
- MPMediaItemPropertyPlaybackDuration: effectiveDuration,
168
- MPNowPlayingInfoPropertyPlaybackRate: state.currentState == .playing ? 1.0 : 0.0,
169
- ]
263
+ // Next: only enabled when there is a track after the current one
264
+ commandCenter.nextTrackCommand.isEnabled = hasCurrentTrack && isNotLast
170
265
 
171
- // Add artwork synchronously if cached, otherwise load async
172
- if let artwork = track.artwork, case .second(let artworkUrl) = artwork {
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 ?? [:]
185
- updatedInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(
186
- boundsSize: CGSize(width: Constants.artworkSize, height: Constants.artworkSize),
187
- requestHandler: { _ in image }
188
- )
189
- MPNowPlayingInfoCenter.default().nowPlayingInfo = updatedInfo
190
- }
191
- }
192
- }
266
+ // Previous: always enabled when something is playing — either restarts current or goes back
267
+ commandCenter.previousTrackCommand.isEnabled = hasCurrentTrack
193
268
 
194
- MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
269
+ // Scrubber: only enabled when we have a known, finite duration
270
+ commandCenter.changePlaybackPositionCommand.isEnabled = hasCurrentTrack && hasDuration
271
+ }
272
+
273
+ private func disableAllCommands() {
274
+ let commandCenter = MPRemoteCommandCenter.shared()
275
+ commandCenter.nextTrackCommand.isEnabled = false
276
+ commandCenter.previousTrackCommand.isEnabled = false
277
+ commandCenter.changePlaybackPositionCommand.isEnabled = false
195
278
  }
196
279
 
280
+ // MARK: - Helpers
281
+
197
282
  private func clearNowPlayingInfo() {
198
283
  MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
284
+ lastArtworkUrl = nil
199
285
  }
200
286
 
201
287
  private func loadArtwork(url: String, completion: @escaping (UIImage?) -> Void) {
202
- // Check cache first
203
288
  if let cached = artworkCache[url] {
204
289
  completion(cached)
205
290
  return
@@ -210,33 +295,21 @@ class MediaSessionManager {
210
295
  return
211
296
  }
212
297
 
213
- // Load image asynchronously
214
298
  URLSession.shared.dataTask(with: imageUrl) { [weak self] data, _, _ in
215
- guard let data = data,
216
- let image = UIImage(data: data)
217
- else {
218
- completion(nil)
299
+ guard let data = data, let image = UIImage(data: data) else {
300
+ DispatchQueue.main.async { completion(nil) }
219
301
  return
220
302
  }
221
-
222
- // Cache the image
223
- self?.artworkCache[url] = image
224
303
  DispatchQueue.main.async {
304
+ self?.artworkCache[url] = image
225
305
  completion(image)
226
306
  }
227
307
  }.resume()
228
308
  }
229
309
 
230
- func onTrackChanged() {
231
- updateNowPlayingInfo()
232
- }
233
-
234
- func onPlaybackStateChanged() {
235
- updateNowPlayingInfo()
236
- }
237
-
238
310
  func release() {
239
311
  clearNowPlayingInfo()
312
+ disableAllCommands()
240
313
  artworkCache.removeAll()
241
314
  }
242
315
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-native-nitro-player",
3
- "version": "0.4.1-alpha.0",
4
- "description": "react-native-nitro-player",
3
+ "version": "0.5.0",
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",
7
7
  "types": "lib/index.d.ts",