react-native-nitro-player 0.7.0 → 0.7.1-alpha.1
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/README.md +47 -46
- 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 +179 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +170 -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 +150 -135
- 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 +221 -0
- package/ios/core/TrackPlayerQueueBuild.swift +493 -0
- package/ios/core/TrackPlayerTempQueue.swift +167 -0
- package/ios/core/TrackPlayerUrlLoader.swift +169 -0
- package/ios/equalizer/EqualizerCore.swift +63 -123
- 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 +22 -17
- 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 +10 -10
- package/lib/specs/TrackPlayer.nitro.d.ts +38 -16
- package/lib/types/EqualizerTypes.d.ts +3 -3
- package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__std__vector_TrackItem_.hpp +122 -0
- package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.cpp +31 -6
- package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.hpp +2 -2
- package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.cpp +16 -3
- package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.hpp +1 -1
- package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.cpp +154 -44
- package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.hpp +10 -10
- package/nitrogen/generated/android/c++/JHybridEqualizerSpec.cpp +130 -34
- package/nitrogen/generated/android/c++/JHybridEqualizerSpec.hpp +9 -9
- package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +115 -24
- package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.hpp +8 -8
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +243 -24
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +16 -8
- 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 +3 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAudioDevicesSpec.kt +2 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridDownloadManagerSpec.kt +10 -10
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridEqualizerSpec.kt +10 -9
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridPlayerQueueSpec.kt +9 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +45 -8
- 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 +5 -5
- package/src/hooks/useDownloadedTracks.ts +17 -13
- package/src/hooks/useEqualizer.ts +26 -21
- 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 +10 -10
- package/src/specs/TrackPlayer.nitro.ts +52 -16
- package/src/types/EqualizerTypes.ts +17 -13
|
@@ -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
|
+
}
|
|
@@ -22,12 +22,12 @@ class EqualizerCore {
|
|
|
22
22
|
private(set) var isEqualizerEnabled: Bool = false
|
|
23
23
|
private var currentPresetName: String?
|
|
24
24
|
|
|
25
|
-
// Standard
|
|
26
|
-
let frequencies: [Float] = [
|
|
27
|
-
private let frequencyLabels = ["
|
|
25
|
+
// Standard 10-band frequencies: 31Hz, 63Hz, 125Hz, 250Hz, 500Hz, 1kHz, 2kHz, 4kHz, 8kHz, 16kHz
|
|
26
|
+
let frequencies: [Float] = [31, 63, 125, 250, 500, 1000, 2000, 4000, 8000, 16000]
|
|
27
|
+
private let frequencyLabels = ["31 Hz", "63 Hz", "125 Hz", "250 Hz", "500 Hz", "1 kHz", "2 kHz", "4 kHz", "8 kHz", "16 kHz"]
|
|
28
28
|
|
|
29
29
|
// Current gains storage - internal so TapContext can access
|
|
30
|
-
private(set) var currentGains: [Double] = [0, 0, 0, 0, 0]
|
|
30
|
+
private(set) var currentGains: [Double] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
|
31
31
|
private var cachedBands: [EqualizerBand]?
|
|
32
32
|
private var cachedCustomPresets: [EqualizerPreset]?
|
|
33
33
|
|
|
@@ -40,46 +40,33 @@ 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
|
|
|
67
50
|
private static let builtInPresets: [String: [Double]] = [
|
|
68
|
-
"Flat":
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
51
|
+
"Flat": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
52
|
+
"Rock": [4.8, 2.88, -3.36, -4.8, -1.92, 2.4, 5.28, 6.72, 6.72, 6.72],
|
|
53
|
+
"Pop": [0.96, 2.88, 4.32, 4.8, 3.36, 0.0, -1.44, -1.44, 0.96, 0.96],
|
|
54
|
+
"Classical": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -4.32, -4.32, -4.32, -5.76],
|
|
55
|
+
"Dance": [5.76, 4.32, 1.44, 0.0, 0.0, -3.36, -4.32, -4.32, 0.0, 0.0],
|
|
56
|
+
"Techno": [4.8, 3.36, 0.0, -3.36, -2.88, 0.0, 4.8, 5.76, 5.76, 5.28],
|
|
57
|
+
"Club": [0.0, 0.0, 4.8, 3.36, 3.36, 3.36, 1.92, 0.0, 0.0, 0.0],
|
|
58
|
+
"Live": [-2.88, 0.0, 2.4, 3.36, 3.36, 3.36, 2.4, 1.44, 1.44, 1.44],
|
|
59
|
+
"Reggae": [0.0, 0.0, 0.0, -3.36, 0.0, 3.84, 3.84, 0.0, 0.0, 0.0],
|
|
60
|
+
"Full Bass": [4.8, 5.76, 5.76, 3.36, 0.96, -2.4, -4.8, -6.24, -6.72, -6.72],
|
|
61
|
+
"Full Treble": [-5.76, -5.76, -5.76, -2.4, 1.44, 6.72, 9.6, 9.6, 9.6, 10.08],
|
|
62
|
+
"Full Bass & Treble": [4.32, 3.36, 0.0, -4.32, -2.88, 0.96, 4.8, 6.72, 7.2, 7.2],
|
|
63
|
+
"Large Hall": [6.24, 6.24, 3.36, 3.36, 0.0, -2.88, -2.88, -2.88, 0.0, 0.0],
|
|
64
|
+
"Party": [4.32, 4.32, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 4.32, 4.32],
|
|
65
|
+
"Ska": [-1.44, -2.88, -2.4, 0.0, 2.4, 3.36, 5.28, 5.76, 6.72, 5.76],
|
|
66
|
+
"Soft": [2.88, 0.96, 0.0, -1.44, 0.0, 2.4, 4.8, 5.76, 6.72, 7.2],
|
|
67
|
+
"Soft Rock": [2.4, 2.4, 1.44, 0.0, -2.4, -3.36, -1.92, 0.0, 1.44, 5.28],
|
|
68
|
+
"Headphones": [2.88, 6.72, 3.36, -1.92, -1.44, 0.96, 2.88, 5.76, 7.68, 8.64],
|
|
69
|
+
"Laptop Speakers": [2.88, 6.72, 3.36, -1.92, -1.44, 0.96, 2.88, 5.76, 7.68, 8.64],
|
|
83
70
|
]
|
|
84
71
|
|
|
85
72
|
// MARK: - Initialization
|
|
@@ -206,7 +193,7 @@ class EqualizerCore {
|
|
|
206
193
|
|
|
207
194
|
func getBands() -> [EqualizerBand] {
|
|
208
195
|
if let cached = cachedBands { return cached }
|
|
209
|
-
let bands = (0..<
|
|
196
|
+
let bands = (0..<10).map { i in
|
|
210
197
|
EqualizerBand(
|
|
211
198
|
index: Double(i),
|
|
212
199
|
centerFrequency: Double(frequencies[i]),
|
|
@@ -219,7 +206,7 @@ class EqualizerCore {
|
|
|
219
206
|
}
|
|
220
207
|
|
|
221
208
|
func setBandGain(bandIndex: Int, gainDb: Double) -> Bool {
|
|
222
|
-
guard bandIndex >= 0 && bandIndex <
|
|
209
|
+
guard bandIndex >= 0 && bandIndex < 10 else { return false }
|
|
223
210
|
|
|
224
211
|
let clampedGain = max(-12.0, min(12.0, gainDb))
|
|
225
212
|
currentGains[bandIndex] = clampedGain
|
|
@@ -237,9 +224,9 @@ class EqualizerCore {
|
|
|
237
224
|
}
|
|
238
225
|
|
|
239
226
|
func setAllBandGains(_ gains: [Double]) -> Bool {
|
|
240
|
-
guard gains.count ==
|
|
227
|
+
guard gains.count == 10 else { return false }
|
|
241
228
|
|
|
242
|
-
for i in 0..<
|
|
229
|
+
for i in 0..<10 {
|
|
243
230
|
currentGains[i] = max(-12.0, min(12.0, gains[i]))
|
|
244
231
|
}
|
|
245
232
|
cachedBands = nil
|
|
@@ -390,7 +377,7 @@ class EqualizerCore {
|
|
|
390
377
|
}
|
|
391
378
|
|
|
392
379
|
func reset() {
|
|
393
|
-
_ = setAllBandGains([0, 0, 0, 0, 0])
|
|
380
|
+
_ = setAllBandGains([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
|
|
394
381
|
currentPresetName = "Flat"
|
|
395
382
|
notifyPresetChange("Flat")
|
|
396
383
|
saveCurrentPreset("Flat")
|
|
@@ -417,10 +404,11 @@ class EqualizerCore {
|
|
|
417
404
|
|
|
418
405
|
if let data = UserDefaults.standard.data(forKey: bandGainsKey),
|
|
419
406
|
let gains = try? JSONDecoder().decode([Double].self, from: data),
|
|
420
|
-
gains.count ==
|
|
407
|
+
gains.count == 10
|
|
421
408
|
{
|
|
422
409
|
currentGains = gains
|
|
423
410
|
}
|
|
411
|
+
// else: migration from 5-band or fresh install — start at flat (currentGains already zeroed)
|
|
424
412
|
|
|
425
413
|
currentPresetName = UserDefaults.standard.string(forKey: currentPresetKey)
|
|
426
414
|
isEqualizerEnabled = enabled
|
|
@@ -428,88 +416,40 @@ class EqualizerCore {
|
|
|
428
416
|
NitroPlayerLogger.log("EqualizerCore", "✅ Restored settings - enabled: \(enabled), gains: \(currentGains)")
|
|
429
417
|
}
|
|
430
418
|
|
|
431
|
-
// MARK: -
|
|
419
|
+
// MARK: - Listener Management (v2 — stable IDs)
|
|
432
420
|
|
|
433
|
-
func addOnEnabledChangeListener(
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
421
|
+
@discardableResult func addOnEnabledChangeListener(_ callback: @escaping (Bool) -> Void) -> Int64 {
|
|
422
|
+
onEnabledChangeListeners.add(callback)
|
|
423
|
+
}
|
|
424
|
+
@discardableResult func removeOnEnabledChangeListener(id: Int64) -> Bool {
|
|
425
|
+
onEnabledChangeListeners.remove(id: id)
|
|
438
426
|
}
|
|
439
427
|
|
|
440
|
-
func addOnBandChangeListener(
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
428
|
+
@discardableResult func addOnBandChangeListener(_ callback: @escaping ([EqualizerBand]) -> Void) -> Int64 {
|
|
429
|
+
onBandChangeListeners.add(callback)
|
|
430
|
+
}
|
|
431
|
+
@discardableResult func removeOnBandChangeListener(id: Int64) -> Bool {
|
|
432
|
+
onBandChangeListeners.remove(id: id)
|
|
445
433
|
}
|
|
446
434
|
|
|
447
|
-
func addOnPresetChangeListener(
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
self?.onPresetChangeListeners.append(box)
|
|
453
|
-
}
|
|
435
|
+
@discardableResult func addOnPresetChangeListener(_ callback: @escaping (Variant_NullType_String?) -> Void) -> Int64 {
|
|
436
|
+
onPresetChangeListeners.add(callback)
|
|
437
|
+
}
|
|
438
|
+
@discardableResult func removeOnPresetChangeListener(id: Int64) -> Bool {
|
|
439
|
+
onPresetChangeListeners.remove(id: id)
|
|
454
440
|
}
|
|
455
441
|
|
|
456
442
|
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
|
-
}
|
|
443
|
+
onEnabledChangeListeners.forEach { $0(enabled) }
|
|
473
444
|
}
|
|
474
445
|
|
|
475
446
|
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
|
-
}
|
|
447
|
+
onBandChangeListeners.forEach { $0(bands) }
|
|
492
448
|
}
|
|
493
449
|
|
|
494
450
|
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
|
-
}
|
|
451
|
+
let variant: Variant_NullType_String? = presetName.map { .second($0) }
|
|
452
|
+
onPresetChangeListeners.forEach { $0(variant) }
|
|
513
453
|
}
|
|
514
454
|
}
|
|
515
455
|
|
|
@@ -521,28 +461,28 @@ private class TapContext {
|
|
|
521
461
|
var sampleRate: Float = 44100.0
|
|
522
462
|
var channelCount: Int = 2
|
|
523
463
|
|
|
524
|
-
// Biquad filter states for
|
|
464
|
+
// Biquad filter states for 10 bands
|
|
525
465
|
// Each band needs 4 delay elements per channel (x[n-1], x[n-2], y[n-1], y[n-2])
|
|
526
466
|
var filterStates: [[Float]] = []
|
|
527
467
|
|
|
528
|
-
// Biquad coefficients for
|
|
468
|
+
// Biquad coefficients for 10 bands
|
|
529
469
|
// Each band: [b0, b1, b2, a1, a2] (normalized, a0 = 1)
|
|
530
470
|
var filterCoeffs: [[Double]] = []
|
|
531
471
|
|
|
532
472
|
init(eqCore: EqualizerCore) {
|
|
533
473
|
self.eqCore = eqCore
|
|
534
|
-
// Initialize
|
|
535
|
-
for _ in 0..<
|
|
474
|
+
// Initialize 10 bands with flat coefficients
|
|
475
|
+
for _ in 0..<10 {
|
|
536
476
|
filterCoeffs.append([1.0, 0.0, 0.0, 0.0, 0.0]) // Flat/bypass
|
|
537
477
|
}
|
|
538
478
|
}
|
|
539
479
|
|
|
540
480
|
func updateCoefficients() {
|
|
541
481
|
guard let eqCore = eqCore else { return }
|
|
542
|
-
let frequencies: [Float] = [
|
|
482
|
+
let frequencies: [Float] = [31, 63, 125, 250, 500, 1000, 2000, 4000, 8000, 16000]
|
|
543
483
|
let gains = eqCore.currentGains
|
|
544
484
|
|
|
545
|
-
for i in 0..<
|
|
485
|
+
for i in 0..<10 {
|
|
546
486
|
filterCoeffs[i] = calculatePeakingEQCoefficients(
|
|
547
487
|
frequency: Double(frequencies[i]),
|
|
548
488
|
gain: gains[i],
|
|
@@ -582,7 +522,7 @@ private class TapContext {
|
|
|
582
522
|
|
|
583
523
|
func resetFilterStates() {
|
|
584
524
|
filterStates = []
|
|
585
|
-
for _ in 0..<
|
|
525
|
+
for _ in 0..<10 {
|
|
586
526
|
// 4 delay elements per channel (2 for input history, 2 for output history)
|
|
587
527
|
filterStates.append(Array(repeating: Float(0.0), count: channelCount * 4))
|
|
588
528
|
}
|
|
@@ -678,8 +618,8 @@ private func tapProcessCallback(
|
|
|
678
618
|
let frameCount = Int(numberFramesOut.pointee)
|
|
679
619
|
let samples = data.assumingMemoryBound(to: Float.self)
|
|
680
620
|
|
|
681
|
-
// Apply all
|
|
682
|
-
for bandIndex in 0..<
|
|
621
|
+
// Apply all 10 EQ bands in series
|
|
622
|
+
for bandIndex in 0..<10 {
|
|
683
623
|
let coeffs: [Double] = context.filterCoeffs[bandIndex]
|
|
684
624
|
|
|
685
625
|
// Skip if essentially flat
|
|
@@ -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
|
|