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.
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +6 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +1 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaBrowserService.kt +26 -2
- package/ios/core/TrackPlayerListener.swift +20 -15
- package/ios/core/TrackPlayerPlayback.swift +10 -2
- package/ios/core/TrackPlayerQueue.swift +2 -1
- package/ios/core/TrackPlayerTempQueue.swift +2 -0
- package/ios/playlist/PlaylistManager.swift +1 -0
- package/package.json +1 -1
|
@@ -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
|
}
|
|
@@ -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
|
-
|
|
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(
|
|
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 >
|
|
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 }
|
|
@@ -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.
|
|
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",
|