react-native-nitro-player 0.7.1-alpha.3 → 1.0.2

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.
@@ -153,6 +153,12 @@ class HybridTrackPlayer : HybridTrackPlayerSpec() {
153
153
  }
154
154
 
155
155
  override fun onAndroidAutoConnectionChange(callback: (connected: Boolean) -> Unit) {
156
+ // Replace any prior listener registered through this HybridObject so JS
157
+ // reloads (Fast Refresh, hot reload) don't accumulate handlers.
158
+ val existing = listenerIds.filter { it.first == "onAndroidAutoConnectionChange" }
159
+ existing.forEach { (_, oldId) -> core.removeOnAndroidAutoConnectionListener(oldId) }
160
+ listenerIds.removeAll { it.first == "onAndroidAutoConnectionChange" }
161
+
156
162
  val id = core.addOnAndroidAutoConnectionListener(callback)
157
163
  listenerIds += "onAndroidAutoConnectionChange" to id
158
164
  }
@@ -120,6 +120,7 @@ class TrackPlayerCore private constructor(
120
120
  } else {
121
121
  updatePlayerQueue(playlist.tracks)
122
122
  }
123
+ checkUpcomingTracksForUrls(lookaheadCount)
123
124
  }
124
125
 
125
126
  // ── Service binding ────────────────────────────────────────────────────
@@ -319,7 +319,29 @@ class NitroPlayerMediaBrowserService : MediaBrowserServiceCompat() {
319
319
  * Fallback: Load playlists when no media library is set
320
320
  */
321
321
  private suspend fun loadFallbackPlaylists(): MutableList<MediaBrowserCompat.MediaItem> {
322
- val playlists = trackPlayerCore?.getAllPlaylists() ?: emptyList()
322
+ // Apps frequently create internal queue playlists named with raw UUIDs
323
+ // (e.g. `PlayerQueue.createPlaylist(uuid.v4())`). Showing those in
324
+ // Android Auto surfaces ugly UUID strings as titles. Filter:
325
+ // - empty playlists (no tracks)
326
+ // - playlists whose name is a bare UUID
327
+ // - duplicate names (keep latest)
328
+ // and prefer surfacing the currently-loaded playlist first so the user
329
+ // always has access to "what's playing now" while the JS side hasn't
330
+ // published a media library yet.
331
+ val rawPlaylists = trackPlayerCore?.getAllPlaylists() ?: emptyList()
332
+ val currentId = trackPlayerCore?.getCurrentPlaylistId()
333
+ val uuidRegex = Regex("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
334
+
335
+ val dedupedByName = linkedMapOf<String, com.margelo.nitro.nitroplayer.playlist.Playlist>()
336
+ rawPlaylists.forEach { p ->
337
+ if (p.tracks.isEmpty()) return@forEach
338
+ // Always keep the currently-playing playlist regardless of name shape.
339
+ val isCurrent = p.id == currentId
340
+ if (!isCurrent && uuidRegex.matches(p.name)) return@forEach
341
+ dedupedByName[p.name] = p
342
+ }
343
+ val playlists =
344
+ dedupedByName.values.sortedByDescending { it.id == currentId }
323
345
  val mediaItems = mutableListOf<MediaBrowserCompat.MediaItem>()
324
346
 
325
347
  playlists.forEach { playlist ->
@@ -331,11 +353,13 @@ class NitroPlayerMediaBrowserService : MediaBrowserServiceCompat() {
331
353
  )
332
354
  }
333
355
 
356
+ val displayTitle =
357
+ if (uuidRegex.matches(playlist.name)) "Now Playing" else playlist.name
334
358
  val description =
335
359
  MediaDescriptionCompat
336
360
  .Builder()
337
361
  .setMediaId("$PLAYLIST_PREFIX${playlist.id}")
338
- .setTitle(playlist.name)
362
+ .setTitle(displayTitle)
339
363
  .setSubtitle(playlist.description ?: "${playlist.tracks.size} tracks")
340
364
  .setIconUri(playlist.artwork?.let { Uri.parse(it) })
341
365
  .setExtras(extras)
@@ -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 > Constants.twoHoursInSeconds { interval = Constants.boundaryIntervalLong }
86
- else if duration > Constants.oneHourInSeconds { interval = Constants.boundaryIntervalMedium }
87
- else { interval = Constants.boundaryIntervalDefault }
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: \(Int(duration))s)")
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 duration = currentItem.duration.seconds
108
- guard duration > 0 && !duration.isNaN && !duration.isInfinite else { return }
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 / \(Int(duration))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
- let remaining = duration - position
116
- if remaining > 0 && remaining <= Constants.preferredForwardBufferDuration && !didRequestUrlsForCurrentItem {
117
- didRequestUrlsForCurrentItem = true
118
- NitroPlayerLogger.log("TrackPlayerCore",
119
- "⏳ \(Int(remaining))s remaining — proactively checking upcoming URLs")
120
- checkUpcomingTracksForUrls(lookahead: lookaheadCount)
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
- // Update now playing info to restore playback rate after seek
119
- DispatchQueue.main.async { self?.mediaSessionManager?.refresh() }
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 totalDuration = player.currentItem?.duration.seconds ?? 0.0
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 }
@@ -53,12 +53,14 @@ extension TrackPlayerCore {
53
53
  // If nothing is playing yet, do a full load
54
54
  guard self.player?.currentItem != nil else {
55
55
  self.updatePlayerQueue(tracks: playlist.tracks)
56
+ self.checkUpcomingTracksForUrls(lookahead: self.lookaheadCount)
56
57
  return
57
58
  }
58
59
 
59
60
  // Update tracks list without interrupting playback
60
61
  self.currentTracks = playlist.tracks
61
62
  self.rebuildAVQueueFromCurrentPosition()
63
+ self.checkUpcomingTracksForUrls(lookahead: self.lookaheadCount)
62
64
  }
63
65
 
64
66
  pendingPlaylistUpdateWorkItem = workItem
@@ -172,6 +172,7 @@ class PlaylistManager {
172
172
  // Update TrackPlayerCore if this is the current playlist
173
173
  if currentPlaylistId == playlistId {
174
174
  TrackPlayerCore.shared.updatePlaylist(playlistId: playlistId)
175
+ TrackPlayerCore.shared.checkUpcomingTracksForUrls(lookahead: TrackPlayerCore.shared.lookaheadCount)
175
176
  }
176
177
 
177
178
  return true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-player",
3
- "version": "0.7.1-alpha.3",
3
+ "version": "1.0.2",
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",