react-native-nitro-player 0.6.1 → 0.7.1-alpha.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.
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +9 -13
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +45 -90
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +48 -182
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +21 -77
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +55 -104
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +113 -123
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ExoPlayerCore.kt +82 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ListenerRegistry.kt +48 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerAndroidAuto.kt +62 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +153 -1887
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +122 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerNotify.kt +44 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +162 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueue.kt +165 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +161 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +28 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerTempQueue.kt +121 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +98 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +27 -18
- package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +11 -58
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +13 -30
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +102 -162
- package/ios/HybridDownloadManager.swift +32 -26
- package/ios/HybridEqualizer.swift +48 -35
- package/ios/HybridTrackPlayer.swift +127 -102
- package/ios/core/ListenerRegistry.swift +60 -0
- package/ios/core/TrackPlayerCore.swift +130 -2356
- package/ios/core/TrackPlayerListener.swift +395 -0
- package/ios/core/TrackPlayerNotify.swift +52 -0
- package/ios/core/TrackPlayerPlayback.swift +274 -0
- package/ios/core/TrackPlayerQueue.swift +212 -0
- package/ios/core/TrackPlayerQueueBuild.swift +482 -0
- package/ios/core/TrackPlayerTempQueue.swift +167 -0
- package/ios/core/TrackPlayerUrlLoader.swift +169 -0
- package/ios/equalizer/EqualizerCore.swift +24 -89
- package/ios/media/MediaSessionManager.swift +32 -49
- package/ios/playlist/PlaylistManager.swift +2 -9
- package/ios/queue/HybridPlayerQueue.swift +69 -66
- package/lib/hooks/useDownloadedTracks.js +16 -13
- package/lib/hooks/useEqualizer.d.ts +4 -4
- package/lib/hooks/useEqualizer.js +12 -12
- package/lib/hooks/useEqualizerPresets.d.ts +3 -3
- package/lib/hooks/useEqualizerPresets.js +12 -18
- package/lib/specs/AndroidAutoMediaLibrary.nitro.d.ts +2 -2
- package/lib/specs/AudioDevices.nitro.d.ts +2 -2
- package/lib/specs/DownloadManager.nitro.d.ts +10 -10
- package/lib/specs/Equalizer.nitro.d.ts +9 -9
- package/lib/specs/TrackPlayer.nitro.d.ts +38 -16
- package/nitro.json +44 -11
- package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +63 -24
- package/nitrogen/generated/android/c++/JCurrentPlayingType.hpp +1 -1
- package/nitrogen/generated/android/c++/JDownloadConfig.hpp +1 -1
- package/nitrogen/generated/android/c++/JDownloadError.hpp +1 -1
- package/nitrogen/generated/android/c++/JDownloadErrorReason.hpp +1 -1
- package/nitrogen/generated/android/c++/JDownloadProgress.hpp +1 -1
- package/nitrogen/generated/android/c++/JDownloadQueueStatus.hpp +1 -1
- package/nitrogen/generated/android/c++/JDownloadState.hpp +1 -1
- package/nitrogen/generated/android/c++/JDownloadStorageInfo.hpp +1 -1
- package/nitrogen/generated/android/c++/JDownloadTask.hpp +1 -1
- package/nitrogen/generated/android/c++/JDownloadedPlaylist.hpp +1 -1
- package/nitrogen/generated/android/c++/JDownloadedTrack.hpp +1 -1
- package/nitrogen/generated/android/c++/JEqualizerBand.hpp +1 -1
- package/nitrogen/generated/android/c++/JEqualizerPreset.hpp +1 -1
- package/nitrogen/generated/android/c++/JEqualizerState.hpp +1 -1
- package/nitrogen/generated/android/c++/JFunc_void_DownloadProgress.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_DownloadedTrack.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_TrackItem_std__optional_Reason_.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_TrackPlayerState_std__optional_Reason_.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_double_double_std__optional_bool_.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_std__variant_nitro__NullType__std__string__.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_std__string_Playlist_std__optional_QueueOperation_.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_std__string_std__string_DownloadState_std__optional_DownloadError_.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_EqualizerBand_.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Playlist__std__optional_QueueOperation_.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__std__vector_TrackItem_.hpp +122 -0
- package/nitrogen/generated/android/c++/JGainRange.hpp +1 -1
- package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.cpp +49 -30
- package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.hpp +21 -24
- package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.cpp +35 -28
- package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.hpp +20 -23
- package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.cpp +197 -93
- package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.hpp +29 -32
- package/nitrogen/generated/android/c++/JHybridEqualizerSpec.cpp +157 -67
- package/nitrogen/generated/android/c++/JHybridEqualizerSpec.hpp +28 -31
- package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +138 -53
- package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.hpp +27 -30
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +282 -69
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +35 -30
- package/nitrogen/generated/android/c++/JPlaybackSource.hpp +1 -1
- package/nitrogen/generated/android/c++/JPlayerConfig.hpp +1 -1
- package/nitrogen/generated/android/c++/JPlayerState.hpp +1 -1
- package/nitrogen/generated/android/c++/JPlaylist.hpp +1 -1
- package/nitrogen/generated/android/c++/JPresetType.hpp +1 -1
- package/nitrogen/generated/android/c++/JQueueOperation.hpp +1 -1
- package/nitrogen/generated/android/c++/JReason.hpp +1 -1
- package/nitrogen/generated/android/c++/JRepeatMode.hpp +1 -1
- package/nitrogen/generated/android/c++/JStorageLocation.hpp +1 -1
- package/nitrogen/generated/android/c++/JTAudioDevice.hpp +1 -1
- package/nitrogen/generated/android/c++/JTrackItem.hpp +1 -1
- package/nitrogen/generated/android/c++/JTrackPlayerState.hpp +1 -1
- package/nitrogen/generated/android/c++/JVariant_NullType_Double.hpp +3 -3
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadError.hpp +3 -3
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadTask.hpp +3 -3
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedPlaylist.hpp +3 -3
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedTrack.hpp +3 -3
- package/nitrogen/generated/android/c++/JVariant_NullType_Playlist.hpp +3 -3
- package/nitrogen/generated/android/c++/JVariant_NullType_String.hpp +3 -3
- package/nitrogen/generated/android/c++/JVariant_NullType_TrackItem.hpp +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_TrackItem__std__vector_TrackItem_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrarySpec.kt +18 -20
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAudioDevicesSpec.kt +17 -19
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridDownloadManagerSpec.kt +25 -28
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridEqualizerSpec.kt +25 -27
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridPlayerQueueSpec.kt +24 -26
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +60 -26
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_Double.kt +0 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadError.kt +0 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadTask.kt +0 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadedPlaylist.kt +0 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadedTrack.kt +0 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_Playlist.kt +0 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_String.kt +0 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_TrackItem.kt +0 -6
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +74 -18
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +380 -151
- package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.hpp +10 -10
- package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.hpp +12 -9
- package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.hpp +23 -8
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +82 -8
- package/nitrogen/generated/ios/swift/Func_void_EqualizerState.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__DownloadedPlaylist_.swift +58 -0
- package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__DownloadedTrack_.swift +58 -0
- package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__std__string_.swift +58 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_DownloadedPlaylist_.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_DownloadedTrack_.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_EqualizerBand_.swift +5 -5
- package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__std__vector_TrackItem_.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec.swift +10 -10
- package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec_cxx.swift +141 -71
- package/nitrogen/generated/ios/swift/HybridEqualizerSpec.swift +9 -9
- package/nitrogen/generated/ios/swift/HybridEqualizerSpec_cxx.swift +105 -41
- package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec.swift +8 -8
- package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec_cxx.swift +95 -32
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +16 -8
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +267 -32
- package/nitrogen/generated/shared/c++/HybridAndroidAutoMediaLibrarySpec.hpp +3 -2
- package/nitrogen/generated/shared/c++/HybridAudioDevicesSpec.hpp +2 -1
- package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.hpp +10 -10
- package/nitrogen/generated/shared/c++/HybridEqualizerSpec.hpp +10 -9
- package/nitrogen/generated/shared/c++/HybridPlayerQueueSpec.hpp +9 -8
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +8 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +16 -8
- package/package.json +3 -3
- package/src/hooks/useDownloadedTracks.ts +17 -13
- package/src/hooks/useEqualizer.ts +16 -16
- package/src/hooks/useEqualizerPresets.ts +15 -21
- package/src/specs/AndroidAutoMediaLibrary.nitro.ts +2 -2
- package/src/specs/AudioDevices.nitro.ts +2 -2
- package/src/specs/DownloadManager.nitro.ts +10 -10
- package/src/specs/Equalizer.nitro.ts +9 -9
- package/src/specs/TrackPlayer.nitro.ts +52 -16
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
//
|
|
2
|
+
// TrackPlayerUrlLoader.swift
|
|
3
|
+
// NitroPlayer
|
|
4
|
+
//
|
|
5
|
+
// Created by Ritesh Shukla on 25/03/26.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import AVFoundation
|
|
9
|
+
import Foundation
|
|
10
|
+
|
|
11
|
+
extension TrackPlayerCore {
|
|
12
|
+
|
|
13
|
+
func updateTracks(tracks: [TrackItem]) async {
|
|
14
|
+
await withPlayerQueueNoThrow { self.updateTracksInternal(tracks: tracks) }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func getTracksById(trackIds: [String]) async -> [TrackItem] {
|
|
18
|
+
await withPlayerQueueNoThrow { self.playlistManager.getTracksById(trackIds: trackIds) }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func getTracksNeedingUrls() async -> [TrackItem] {
|
|
22
|
+
await withPlayerQueueNoThrow { self.getTracksNeedingUrlsInternal() }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func getNextTracks(count: Int) async -> [TrackItem] {
|
|
26
|
+
await withPlayerQueueNoThrow { self.getNextTracksInternal(count: count) }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// MARK: - Internal
|
|
30
|
+
|
|
31
|
+
func updateTracksInternal(tracks: [TrackItem]) {
|
|
32
|
+
NitroPlayerLogger.log("TrackPlayerCore", "🔄 updateTracks: \(tracks.count) updates")
|
|
33
|
+
|
|
34
|
+
let currentTrack = self.getCurrentTrack()
|
|
35
|
+
let currentTrackId = currentTrack?.id
|
|
36
|
+
// A track is only "empty" if it has no remote URL AND is not downloaded.
|
|
37
|
+
let currentTrackIsEmpty = currentTrack.map {
|
|
38
|
+
$0.url.isEmpty && !DownloadManagerCore.shared.isTrackDownloaded(trackId: $0.id)
|
|
39
|
+
} ?? false
|
|
40
|
+
|
|
41
|
+
let safeTracks = tracks.filter { track in
|
|
42
|
+
switch true {
|
|
43
|
+
case track.id == currentTrackId && !currentTrackIsEmpty:
|
|
44
|
+
NitroPlayerLogger.log("TrackPlayerCore",
|
|
45
|
+
"⚠️ Skipping update for currently playing track: \(track.id) (preserves gapless)")
|
|
46
|
+
return false
|
|
47
|
+
case track.id == currentTrackId && currentTrackIsEmpty:
|
|
48
|
+
NitroPlayerLogger.log("TrackPlayerCore",
|
|
49
|
+
"🔄 Updating current track with no URL: \(track.id)")
|
|
50
|
+
return !track.url.isEmpty
|
|
51
|
+
case track.url.isEmpty:
|
|
52
|
+
NitroPlayerLogger.log("TrackPlayerCore", "⚠️ Skipping track with empty URL: \(track.id)")
|
|
53
|
+
return false
|
|
54
|
+
default:
|
|
55
|
+
return true
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
guard !safeTracks.isEmpty else {
|
|
60
|
+
NitroPlayerLogger.log("TrackPlayerCore", "✅ No valid updates to apply")
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Invalidate preloaded assets for tracks with updated data
|
|
65
|
+
let updatedTrackIds = Set(safeTracks.map { $0.id })
|
|
66
|
+
for trackId in updatedTrackIds {
|
|
67
|
+
if self.preloadedAssets[trackId] != nil {
|
|
68
|
+
NitroPlayerLogger.log("TrackPlayerCore", "🗑️ Invalidating preloaded asset for track: \(trackId)")
|
|
69
|
+
self.preloadedAssets.removeValue(forKey: trackId)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Update in PlaylistManager
|
|
74
|
+
let affectedPlaylists = self.playlistManager.updateTracks(tracks: safeTracks)
|
|
75
|
+
|
|
76
|
+
// If the current track had no URL and now has one, replace the current AVPlayerItem
|
|
77
|
+
if let update = currentTrack, currentTrackIsEmpty, !update.url.isEmpty {
|
|
78
|
+
NitroPlayerLogger.log("TrackPlayerCore",
|
|
79
|
+
"🔄 Replacing current AVPlayerItem for track with resolved URL: \(update.id)")
|
|
80
|
+
if let newItem = self.createGaplessPlayerItem(for: update, isPreload: false) {
|
|
81
|
+
self.player?.replaceCurrentItem(with: newItem)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Rebuild queue if current playlist was affected
|
|
86
|
+
if let currentId = self.currentPlaylistId,
|
|
87
|
+
let updateCount = affectedPlaylists[currentId]
|
|
88
|
+
{
|
|
89
|
+
NitroPlayerLogger.log("TrackPlayerCore",
|
|
90
|
+
"🔄 Rebuilding queue - \(updateCount) tracks updated in current playlist")
|
|
91
|
+
|
|
92
|
+
// Sync currentTracks from the freshly-updated PlaylistManager
|
|
93
|
+
if let updatedPlaylist = self.playlistManager.getPlaylist(playlistId: currentId) {
|
|
94
|
+
self.currentTracks = updatedPlaylist.tracks
|
|
95
|
+
NitroPlayerLogger.log("TrackPlayerCore",
|
|
96
|
+
"📥 Synced currentTracks from PlaylistManager (\(self.currentTracks.count) tracks)")
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if self.player?.currentItem == nil, let player = self.player {
|
|
100
|
+
// No AVPlayerItem exists yet — lazy-load mode: URLs were empty when the queue first loaded.
|
|
101
|
+
NitroPlayerLogger.log("TrackPlayerCore",
|
|
102
|
+
"🔄 No current item — full queue rebuild from currentTrackIndex \(self.currentTrackIndex)")
|
|
103
|
+
player.removeAllItems()
|
|
104
|
+
var lastItem: AVPlayerItem? = nil
|
|
105
|
+
for (offset, track) in self.currentTracks[max(0, self.currentTrackIndex)...].enumerated() {
|
|
106
|
+
let isPreload = offset < Constants.gaplessPreloadCount
|
|
107
|
+
if let newItem = self.createGaplessPlayerItem(for: track, isPreload: isPreload) {
|
|
108
|
+
player.insert(newItem, after: lastItem)
|
|
109
|
+
lastItem = newItem
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
player.play()
|
|
113
|
+
self.preloadUpcomingTracks(from: self.currentTrackIndex + 1)
|
|
114
|
+
} else {
|
|
115
|
+
// A current AVPlayerItem already exists — preserve it and only rebuild upcoming items.
|
|
116
|
+
self.rebuildAVQueueFromCurrentPosition(changedTrackIds: updatedTrackIds)
|
|
117
|
+
self.preloadUpcomingTracks(from: self.currentTrackIndex + 1)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
NitroPlayerLogger.log("TrackPlayerCore", "✅ Queue rebuilt, gapless playback preserved")
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
NitroPlayerLogger.log("TrackPlayerCore",
|
|
124
|
+
"✅ Track updates complete - \(affectedPlaylists.count) playlists affected")
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
func getTracksNeedingUrlsInternal() -> [TrackItem] {
|
|
128
|
+
guard let currentId = currentPlaylistId,
|
|
129
|
+
let playlist = playlistManager.getPlaylist(playlistId: currentId)
|
|
130
|
+
else { return [] }
|
|
131
|
+
|
|
132
|
+
// Only return tracks that truly can't play: empty remote URL AND not downloaded locally.
|
|
133
|
+
return playlist.tracks.filter {
|
|
134
|
+
$0.url.isEmpty && !DownloadManagerCore.shared.isTrackDownloaded(trackId: $0.id)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
func getNextTracksInternal(count: Int) -> [TrackItem] {
|
|
139
|
+
let actualQueue = getActualQueueInternal()
|
|
140
|
+
guard !actualQueue.isEmpty else { return [] }
|
|
141
|
+
|
|
142
|
+
guard let currentTrack = getCurrentTrack(),
|
|
143
|
+
let currentIndex = actualQueue.firstIndex(where: { $0.id == currentTrack.id })
|
|
144
|
+
else { return [] }
|
|
145
|
+
|
|
146
|
+
let startIndex = currentIndex + 1
|
|
147
|
+
let endIndex = min(startIndex + count, actualQueue.count)
|
|
148
|
+
return startIndex < actualQueue.count ? Array(actualQueue[startIndex..<endIndex]) : []
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
func checkUpcomingTracksForUrls(lookahead: Int = 5) {
|
|
152
|
+
let upcomingTracks = getNextTracksInternal(count: lookahead)
|
|
153
|
+
|
|
154
|
+
let currentTrack = getCurrentTrack()
|
|
155
|
+
let currentNeedsUrl = currentTrack.map {
|
|
156
|
+
$0.url.isEmpty && !DownloadManagerCore.shared.isTrackDownloaded(trackId: $0.id)
|
|
157
|
+
} ?? false
|
|
158
|
+
let candidateTracks = currentNeedsUrl ? [currentTrack!] + upcomingTracks : upcomingTracks
|
|
159
|
+
|
|
160
|
+
let tracksNeedingUrls = candidateTracks.filter {
|
|
161
|
+
$0.url.isEmpty && !DownloadManagerCore.shared.isTrackDownloaded(trackId: $0.id)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if !tracksNeedingUrls.isEmpty {
|
|
165
|
+
NitroPlayerLogger.log("TrackPlayerCore", "⚠️ \(tracksNeedingUrls.count) upcoming tracks need URLs")
|
|
166
|
+
notifyTracksNeedUpdate(tracks: tracksNeedingUrls, lookahead: lookahead)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -40,27 +40,10 @@ class EqualizerCore {
|
|
|
40
40
|
private let currentPresetKey = "eq_current_preset"
|
|
41
41
|
private let customPresetsKey = "eq_custom_presets"
|
|
42
42
|
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
private
|
|
46
|
-
|
|
47
|
-
let callback: T
|
|
48
|
-
|
|
49
|
-
init(owner: AnyObject, callback: T) {
|
|
50
|
-
self.owner = owner
|
|
51
|
-
self.callback = callback
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
var isAlive: Bool { owner != nil }
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Event callbacks
|
|
58
|
-
private var onEnabledChangeListeners: [WeakCallbackBox<(Bool) -> Void>] = []
|
|
59
|
-
private var onBandChangeListeners: [WeakCallbackBox<([EqualizerBand]) -> Void>] = []
|
|
60
|
-
private var onPresetChangeListeners: [WeakCallbackBox<(Variant_NullType_String?) -> Void>] = []
|
|
61
|
-
|
|
62
|
-
private let listenersQueue = DispatchQueue(
|
|
63
|
-
label: "com.equalizer.listeners", attributes: .concurrent)
|
|
43
|
+
// Event listeners (v2 — ListenerRegistry with stable IDs)
|
|
44
|
+
private let onEnabledChangeListeners = ListenerRegistry<(Bool) -> Void>()
|
|
45
|
+
private let onBandChangeListeners = ListenerRegistry<([EqualizerBand]) -> Void>()
|
|
46
|
+
private let onPresetChangeListeners = ListenerRegistry<(Variant_NullType_String?) -> Void>()
|
|
64
47
|
|
|
65
48
|
// MARK: - Built-in Presets
|
|
66
49
|
|
|
@@ -428,88 +411,40 @@ class EqualizerCore {
|
|
|
428
411
|
NitroPlayerLogger.log("EqualizerCore", "✅ Restored settings - enabled: \(enabled), gains: \(currentGains)")
|
|
429
412
|
}
|
|
430
413
|
|
|
431
|
-
// MARK: -
|
|
414
|
+
// MARK: - Listener Management (v2 — stable IDs)
|
|
432
415
|
|
|
433
|
-
func addOnEnabledChangeListener(
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
416
|
+
@discardableResult func addOnEnabledChangeListener(_ callback: @escaping (Bool) -> Void) -> Int64 {
|
|
417
|
+
onEnabledChangeListeners.add(callback)
|
|
418
|
+
}
|
|
419
|
+
@discardableResult func removeOnEnabledChangeListener(id: Int64) -> Bool {
|
|
420
|
+
onEnabledChangeListeners.remove(id: id)
|
|
438
421
|
}
|
|
439
422
|
|
|
440
|
-
func addOnBandChangeListener(
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
423
|
+
@discardableResult func addOnBandChangeListener(_ callback: @escaping ([EqualizerBand]) -> Void) -> Int64 {
|
|
424
|
+
onBandChangeListeners.add(callback)
|
|
425
|
+
}
|
|
426
|
+
@discardableResult func removeOnBandChangeListener(id: Int64) -> Bool {
|
|
427
|
+
onBandChangeListeners.remove(id: id)
|
|
445
428
|
}
|
|
446
429
|
|
|
447
|
-
func addOnPresetChangeListener(
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
self?.onPresetChangeListeners.append(box)
|
|
453
|
-
}
|
|
430
|
+
@discardableResult func addOnPresetChangeListener(_ callback: @escaping (Variant_NullType_String?) -> Void) -> Int64 {
|
|
431
|
+
onPresetChangeListeners.add(callback)
|
|
432
|
+
}
|
|
433
|
+
@discardableResult func removeOnPresetChangeListener(id: Int64) -> Bool {
|
|
434
|
+
onPresetChangeListeners.remove(id: id)
|
|
454
435
|
}
|
|
455
436
|
|
|
456
437
|
private func notifyEnabledChange(_ enabled: Bool) {
|
|
457
|
-
|
|
458
|
-
guard let self = self else { return }
|
|
459
|
-
self.onEnabledChangeListeners.removeAll { !$0.isAlive }
|
|
460
|
-
|
|
461
|
-
let callbacks = self.onEnabledChangeListeners.compactMap {
|
|
462
|
-
$0.isAlive ? $0.callback : nil
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
if !callbacks.isEmpty {
|
|
466
|
-
DispatchQueue.main.async {
|
|
467
|
-
for callback in callbacks {
|
|
468
|
-
callback(enabled)
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
438
|
+
onEnabledChangeListeners.forEach { $0(enabled) }
|
|
473
439
|
}
|
|
474
440
|
|
|
475
441
|
private func notifyBandChange(_ bands: [EqualizerBand]) {
|
|
476
|
-
|
|
477
|
-
guard let self = self else { return }
|
|
478
|
-
self.onBandChangeListeners.removeAll { !$0.isAlive }
|
|
479
|
-
|
|
480
|
-
let callbacks = self.onBandChangeListeners.compactMap {
|
|
481
|
-
$0.isAlive ? $0.callback : nil
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
if !callbacks.isEmpty {
|
|
485
|
-
DispatchQueue.main.async {
|
|
486
|
-
for callback in callbacks {
|
|
487
|
-
callback(bands)
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
}
|
|
442
|
+
onBandChangeListeners.forEach { $0(bands) }
|
|
492
443
|
}
|
|
493
444
|
|
|
494
445
|
private func notifyPresetChange(_ presetName: String?) {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
self.onPresetChangeListeners.removeAll { !$0.isAlive }
|
|
498
|
-
|
|
499
|
-
let callbacks = self.onPresetChangeListeners.compactMap {
|
|
500
|
-
$0.isAlive ? $0.callback : nil
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
if !callbacks.isEmpty {
|
|
504
|
-
let variant: Variant_NullType_String? = presetName.map { .second($0) }
|
|
505
|
-
|
|
506
|
-
DispatchQueue.main.async {
|
|
507
|
-
for callback in callbacks {
|
|
508
|
-
callback(variant)
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
}
|
|
446
|
+
let variant: Variant_NullType_String? = presetName.map { .second($0) }
|
|
447
|
+
onPresetChangeListeners.forEach { $0(variant) }
|
|
513
448
|
}
|
|
514
449
|
}
|
|
515
450
|
|
|
@@ -20,7 +20,7 @@ class MediaSessionManager {
|
|
|
20
20
|
|
|
21
21
|
// MARK: - Properties
|
|
22
22
|
|
|
23
|
-
private var trackPlayerCore: TrackPlayerCore?
|
|
23
|
+
private weak var trackPlayerCore: TrackPlayerCore?
|
|
24
24
|
private let artworkCache = NSCache<NSString, UIImage>()
|
|
25
25
|
|
|
26
26
|
private var showInNotification: Bool = true
|
|
@@ -28,6 +28,11 @@ class MediaSessionManager {
|
|
|
28
28
|
// Tracks the artwork URL currently shown so we can discard stale async loads
|
|
29
29
|
private var lastArtworkUrl: String?
|
|
30
30
|
|
|
31
|
+
// Cached values received from playerQueue — main-thread-only reads (no sync needed)
|
|
32
|
+
private var cachedTrack: TrackItem?
|
|
33
|
+
private var cachedState: PlayerState?
|
|
34
|
+
private var cachedQueue: [TrackItem] = []
|
|
35
|
+
|
|
31
36
|
init() {
|
|
32
37
|
setupRemoteCommandCenter()
|
|
33
38
|
}
|
|
@@ -47,27 +52,27 @@ class MediaSessionManager {
|
|
|
47
52
|
refresh()
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
// MARK: -
|
|
55
|
+
// MARK: - Entry point from playerQueue (called via DispatchQueue.main.async)
|
|
51
56
|
//
|
|
52
|
-
//
|
|
53
|
-
|
|
57
|
+
// Receives pre-computed values captured on playerQueue — no player access here.
|
|
58
|
+
|
|
59
|
+
func updateFromPlayerQueue(track: TrackItem, state: PlayerState, queue: [TrackItem]) {
|
|
60
|
+
cachedTrack = track
|
|
61
|
+
cachedState = state
|
|
62
|
+
cachedQueue = queue
|
|
63
|
+
refreshInternal()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// MARK: - Refresh using cached values (main thread only)
|
|
54
67
|
|
|
55
68
|
func refresh() {
|
|
56
69
|
if Thread.isMainThread {
|
|
57
70
|
refreshInternal()
|
|
58
71
|
} else {
|
|
59
|
-
DispatchQueue.main.async { [weak self] in
|
|
60
|
-
self?.refreshInternal()
|
|
61
|
-
}
|
|
72
|
+
DispatchQueue.main.async { [weak self] in self?.refreshInternal() }
|
|
62
73
|
}
|
|
63
74
|
}
|
|
64
75
|
|
|
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
76
|
// MARK: - Core internal update (main thread only)
|
|
72
77
|
|
|
73
78
|
private func refreshInternal() {
|
|
@@ -77,21 +82,13 @@ class MediaSessionManager {
|
|
|
77
82
|
return
|
|
78
83
|
}
|
|
79
84
|
|
|
80
|
-
guard let
|
|
81
|
-
let track = core.getCurrentTrack()
|
|
82
|
-
else {
|
|
85
|
+
guard let track = cachedTrack, let state = cachedState else {
|
|
83
86
|
clearNowPlayingInfo()
|
|
84
87
|
disableAllCommands()
|
|
85
88
|
return
|
|
86
89
|
}
|
|
87
90
|
|
|
88
|
-
|
|
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.
|
|
91
|
+
let queue = cachedQueue
|
|
95
92
|
let positionInQueue = queue.firstIndex(where: { $0.id == track.id }) ?? -1
|
|
96
93
|
|
|
97
94
|
updateNowPlayingInfoInternal(track: track, state: state, queue: queue, positionInQueue: positionInQueue)
|
|
@@ -145,7 +142,6 @@ class MediaSessionManager {
|
|
|
145
142
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
|
146
143
|
loadArtwork(url: artworkUrl) { [weak self] image in
|
|
147
144
|
guard let self = self, let image = image else { return }
|
|
148
|
-
// Discard if track changed while loading
|
|
149
145
|
guard self.lastArtworkUrl == artworkUrl else { return }
|
|
150
146
|
var updated = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]
|
|
151
147
|
updated[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(
|
|
@@ -168,8 +164,6 @@ class MediaSessionManager {
|
|
|
168
164
|
private func setupRemoteCommandCenter() {
|
|
169
165
|
let commandCenter = MPRemoteCommandCenter.shared()
|
|
170
166
|
|
|
171
|
-
// Clear any previously registered targets before adding fresh ones.
|
|
172
|
-
// Prevents duplicate handlers if this were ever called more than once.
|
|
173
167
|
commandCenter.playCommand.removeTarget(nil)
|
|
174
168
|
commandCenter.pauseCommand.removeTarget(nil)
|
|
175
169
|
commandCenter.togglePlayPauseCommand.removeTarget(nil)
|
|
@@ -183,7 +177,7 @@ class MediaSessionManager {
|
|
|
183
177
|
commandCenter.playCommand.isEnabled = true
|
|
184
178
|
commandCenter.playCommand.addTarget { [weak self] _ in
|
|
185
179
|
guard let core = self?.trackPlayerCore else { return .commandFailed }
|
|
186
|
-
core.play()
|
|
180
|
+
Task { await core.play() }
|
|
187
181
|
return .success
|
|
188
182
|
}
|
|
189
183
|
|
|
@@ -191,7 +185,7 @@ class MediaSessionManager {
|
|
|
191
185
|
commandCenter.pauseCommand.isEnabled = true
|
|
192
186
|
commandCenter.pauseCommand.addTarget { [weak self] _ in
|
|
193
187
|
guard let core = self?.trackPlayerCore else { return .commandFailed }
|
|
194
|
-
core.pause()
|
|
188
|
+
Task { await core.pause() }
|
|
195
189
|
return .success
|
|
196
190
|
}
|
|
197
191
|
|
|
@@ -199,51 +193,45 @@ class MediaSessionManager {
|
|
|
199
193
|
commandCenter.togglePlayPauseCommand.isEnabled = true
|
|
200
194
|
commandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in
|
|
201
195
|
guard let core = self?.trackPlayerCore else { return .commandFailed }
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
} else {
|
|
205
|
-
core.play()
|
|
206
|
-
}
|
|
196
|
+
let isPlaying = self?.cachedState?.currentState == .playing
|
|
197
|
+
Task { if isPlaying { await core.pause() } else { await core.play() } }
|
|
207
198
|
return .success
|
|
208
199
|
}
|
|
209
200
|
|
|
210
|
-
// Next track
|
|
201
|
+
// Next track
|
|
211
202
|
commandCenter.nextTrackCommand.isEnabled = false
|
|
212
203
|
commandCenter.nextTrackCommand.addTarget { [weak self] _ in
|
|
213
204
|
guard let core = self?.trackPlayerCore else { return .commandFailed }
|
|
214
|
-
core.skipToNext()
|
|
205
|
+
Task { await core.skipToNext() }
|
|
215
206
|
return .success
|
|
216
207
|
}
|
|
217
208
|
|
|
218
|
-
// Previous track
|
|
209
|
+
// Previous track
|
|
219
210
|
commandCenter.previousTrackCommand.isEnabled = false
|
|
220
211
|
commandCenter.previousTrackCommand.addTarget { [weak self] _ in
|
|
221
212
|
guard let core = self?.trackPlayerCore else { return .commandFailed }
|
|
222
|
-
core.skipToPrevious()
|
|
213
|
+
Task { await core.skipToPrevious() }
|
|
223
214
|
return .success
|
|
224
215
|
}
|
|
225
216
|
|
|
226
|
-
// Disable skip-forward/backward — these replace the scrubber with non-interactive buttons
|
|
227
217
|
commandCenter.seekForwardCommand.isEnabled = false
|
|
228
218
|
commandCenter.seekBackwardCommand.isEnabled = false
|
|
229
219
|
|
|
230
|
-
// Scrubber
|
|
220
|
+
// Scrubber
|
|
231
221
|
commandCenter.changePlaybackPositionCommand.isEnabled = false
|
|
232
222
|
commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in
|
|
233
|
-
guard let
|
|
234
|
-
let core = self.trackPlayerCore,
|
|
223
|
+
guard let core = self?.trackPlayerCore,
|
|
235
224
|
let positionEvent = event as? MPChangePlaybackPositionCommandEvent
|
|
236
225
|
else {
|
|
237
226
|
return .commandFailed
|
|
238
227
|
}
|
|
239
|
-
// Optimistically freeze the scrubber at the tapped position
|
|
240
|
-
// seek is in flight — updateNowPlayingInfo in the seek completion restores it.
|
|
228
|
+
// Optimistically freeze the scrubber at the tapped position
|
|
241
229
|
if var info = MPNowPlayingInfoCenter.default().nowPlayingInfo {
|
|
242
230
|
info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = positionEvent.positionTime
|
|
243
231
|
info[MPNowPlayingInfoPropertyPlaybackRate] = 0.0
|
|
244
232
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = info
|
|
245
233
|
}
|
|
246
|
-
core.seek(position: positionEvent.positionTime)
|
|
234
|
+
Task { await core.seek(position: positionEvent.positionTime) }
|
|
247
235
|
return .success
|
|
248
236
|
}
|
|
249
237
|
}
|
|
@@ -260,13 +248,8 @@ class MediaSessionManager {
|
|
|
260
248
|
let playerDuration = state.totalDuration
|
|
261
249
|
let hasDuration = playerDuration > 0 && !playerDuration.isNaN && !playerDuration.isInfinite
|
|
262
250
|
|
|
263
|
-
// Next: only enabled when there is a track after the current one
|
|
264
251
|
commandCenter.nextTrackCommand.isEnabled = hasCurrentTrack && isNotLast
|
|
265
|
-
|
|
266
|
-
// Previous: always enabled when something is playing — either restarts current or goes back
|
|
267
252
|
commandCenter.previousTrackCommand.isEnabled = hasCurrentTrack
|
|
268
|
-
|
|
269
|
-
// Scrubber: only enabled when we have a known, finite duration
|
|
270
253
|
commandCenter.changePlaybackPositionCommand.isEnabled = hasCurrentTrack && hasDuration
|
|
271
254
|
}
|
|
272
255
|
|
|
@@ -268,9 +268,6 @@ class PlaylistManager {
|
|
|
268
268
|
|
|
269
269
|
currentPlaylistId = playlistId
|
|
270
270
|
|
|
271
|
-
// Update TrackPlayerCore
|
|
272
|
-
TrackPlayerCore.shared.loadPlaylist(playlistId: playlistId)
|
|
273
|
-
|
|
274
271
|
return true
|
|
275
272
|
}
|
|
276
273
|
|
|
@@ -399,9 +396,7 @@ class PlaylistManager {
|
|
|
399
396
|
let (allPlaylists, currentListeners) = queue.sync {
|
|
400
397
|
(Array(playlists.values), listeners)
|
|
401
398
|
}
|
|
402
|
-
|
|
403
|
-
currentListeners.forEach { $0.1(allPlaylists, operation) }
|
|
404
|
-
}
|
|
399
|
+
currentListeners.forEach { $0.1(allPlaylists, operation) }
|
|
405
400
|
}
|
|
406
401
|
|
|
407
402
|
private func notifyPlaylistChanged(_ playlistId: String, _ operation: QueueOperation?) {
|
|
@@ -413,9 +408,7 @@ class PlaylistManager {
|
|
|
413
408
|
|
|
414
409
|
guard let (playlist, currentListeners) = result else { return }
|
|
415
410
|
|
|
416
|
-
|
|
417
|
-
currentListeners.forEach { $0.1(playlist, operation) }
|
|
418
|
-
}
|
|
411
|
+
currentListeners.forEach { $0.1(playlist, operation) }
|
|
419
412
|
}
|
|
420
413
|
|
|
421
414
|
private func scheduleSave() {
|