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,395 @@
|
|
|
1
|
+
//
|
|
2
|
+
// TrackPlayerListener.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 setupPlayer() {
|
|
14
|
+
// Must be called on playerQueue
|
|
15
|
+
player = AVQueuePlayer()
|
|
16
|
+
|
|
17
|
+
// Start with stall-waiting enabled so the first track buffers before playing.
|
|
18
|
+
// Once the first item is ready (readyToPlay), this is flipped to false for
|
|
19
|
+
// gapless inter-track transitions (see setupCurrentItemObservers).
|
|
20
|
+
player?.automaticallyWaitsToMinimizeStalling = true
|
|
21
|
+
|
|
22
|
+
// Set action at item end to advance for gapless playback
|
|
23
|
+
player?.actionAtItemEnd = .advance
|
|
24
|
+
|
|
25
|
+
// Configure for high-quality audio playback with minimal latency
|
|
26
|
+
if #available(iOS 15.0, *) {
|
|
27
|
+
player?.audiovisualBackgroundPlaybackPolicy = .continuesIfPossible
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
NitroPlayerLogger.log("TrackPlayerCore", "π΅ Gapless playback configured - automaticallyWaitsToMinimizeStalling=true (flipped to false on first readyToPlay)")
|
|
31
|
+
|
|
32
|
+
// Listen for EQ enabled/disabled changes so we can update ALL items in
|
|
33
|
+
// the queue atomically, keeping the audio pipeline configuration uniform.
|
|
34
|
+
// A mismatch (some items with tap, some without) forces AVQueuePlayer to
|
|
35
|
+
// reconfigure the pipeline at transition boundaries β audible gap.
|
|
36
|
+
EqualizerCore.shared.addOnEnabledChangeListener { [weak self] enabled in
|
|
37
|
+
self?.playerQueue.async {
|
|
38
|
+
guard let self, let player = self.player else { return }
|
|
39
|
+
for item in player.items() {
|
|
40
|
+
if enabled {
|
|
41
|
+
EqualizerCore.shared.applyAudioMix(to: item)
|
|
42
|
+
} else {
|
|
43
|
+
item.audioMix = nil
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
NitroPlayerLogger.log("TrackPlayerCore",
|
|
47
|
+
"ποΈ EQ toggled \(enabled ? "ON" : "OFF") β updated \(player.items().count) items for pipeline consistency")
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setupPlayerObservers()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func setupPlayerObservers() {
|
|
55
|
+
guard let player else { return }
|
|
56
|
+
|
|
57
|
+
player.addObserver(self, forKeyPath: "status", options: [.new], context: nil)
|
|
58
|
+
player.addObserver(self, forKeyPath: "rate", options: [.new], context: nil)
|
|
59
|
+
player.addObserver(self, forKeyPath: "timeControlStatus", options: [.new], context: nil)
|
|
60
|
+
player.addObserver(self, forKeyPath: "currentItem", options: [.new], context: nil)
|
|
61
|
+
|
|
62
|
+
NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidPlayToEndTime(_:)),
|
|
63
|
+
name: .AVPlayerItemDidPlayToEndTime, object: nil)
|
|
64
|
+
NotificationCenter.default.addObserver(self, selector: #selector(playerItemFailedToPlayToEndTime(_:)),
|
|
65
|
+
name: .AVPlayerItemFailedToPlayToEndTime, object: nil)
|
|
66
|
+
NotificationCenter.default.addObserver(self, selector: #selector(playerItemNewErrorLogEntry(_:)),
|
|
67
|
+
name: .AVPlayerItemNewErrorLogEntry, object: nil)
|
|
68
|
+
NotificationCenter.default.addObserver(self, selector: #selector(playerItemTimeJumped(_:)),
|
|
69
|
+
name: .AVPlayerItemTimeJumped, object: nil)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func setupBoundaryTimeObserver() {
|
|
73
|
+
if let obs = boundaryTimeObserver, let p = player {
|
|
74
|
+
p.removeTimeObserver(obs)
|
|
75
|
+
boundaryTimeObserver = nil
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
guard let player, let currentItem = player.currentItem,
|
|
79
|
+
currentItem.status == .readyToPlay else { return }
|
|
80
|
+
|
|
81
|
+
let duration = currentItem.duration.seconds
|
|
82
|
+
guard duration > 0 && !duration.isNaN && !duration.isInfinite else { return }
|
|
83
|
+
|
|
84
|
+
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 }
|
|
88
|
+
|
|
89
|
+
NitroPlayerLogger.log("TrackPlayerCore", "β±οΈ Setting up periodic observer (interval: \(interval)s, duration: \(Int(duration))s)")
|
|
90
|
+
|
|
91
|
+
let cmInterval = CMTime(seconds: interval, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
|
92
|
+
// Deliver on playerQueue (not main)
|
|
93
|
+
boundaryTimeObserver = player.addPeriodicTimeObserver(
|
|
94
|
+
forInterval: cmInterval, queue: playerQueue
|
|
95
|
+
) { [weak self] _ in
|
|
96
|
+
self?.handleBoundaryTimeCrossed()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
NitroPlayerLogger.log("TrackPlayerCore", "β±οΈ Periodic time observer setup complete")
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func handleBoundaryTimeCrossed() {
|
|
103
|
+
guard let player, let currentItem = player.currentItem else { return }
|
|
104
|
+
guard player.rate > 0 else { return }
|
|
105
|
+
|
|
106
|
+
let position = currentItem.currentTime().seconds
|
|
107
|
+
let duration = currentItem.duration.seconds
|
|
108
|
+
guard duration > 0 && !duration.isNaN && !duration.isInfinite else { return }
|
|
109
|
+
|
|
110
|
+
NitroPlayerLogger.log("TrackPlayerCore", "β±οΈ Boundary crossed - position: \(Int(position))s / \(Int(duration))s")
|
|
111
|
+
|
|
112
|
+
notifyPlaybackProgress(position, duration, isManuallySeeked ? true : nil)
|
|
113
|
+
isManuallySeeked = false
|
|
114
|
+
|
|
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)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// MARK: - KVO β fires on main or internal thread, dispatch to playerQueue
|
|
125
|
+
override func observeValue(
|
|
126
|
+
forKeyPath keyPath: String?, of object: Any?,
|
|
127
|
+
change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?
|
|
128
|
+
) {
|
|
129
|
+
playerQueue.async { [weak self] in
|
|
130
|
+
guard let self, let player = self.player else { return }
|
|
131
|
+
NitroPlayerLogger.log("TrackPlayerCore", "π KVO - keyPath: \(keyPath ?? "nil")")
|
|
132
|
+
if keyPath == "status" {
|
|
133
|
+
NitroPlayerLogger.log("TrackPlayerCore", "π Player status changed to: \(player.status.rawValue)")
|
|
134
|
+
if player.status == .readyToPlay { self.emitStateChange() }
|
|
135
|
+
else if player.status == .failed {
|
|
136
|
+
NitroPlayerLogger.log("TrackPlayerCore", "β Player failed")
|
|
137
|
+
self.notifyPlaybackStateChange(.stopped, .error)
|
|
138
|
+
}
|
|
139
|
+
} else if keyPath == "rate" {
|
|
140
|
+
NitroPlayerLogger.log("TrackPlayerCore", "π Rate changed to: \(player.rate)")
|
|
141
|
+
self.emitStateChange()
|
|
142
|
+
} else if keyPath == "timeControlStatus" {
|
|
143
|
+
NitroPlayerLogger.log("TrackPlayerCore", "π TimeControlStatus changed to: \(player.timeControlStatus.rawValue)")
|
|
144
|
+
self.emitStateChange()
|
|
145
|
+
} else if keyPath == "currentItem" {
|
|
146
|
+
NitroPlayerLogger.log("TrackPlayerCore", "π Current item changed")
|
|
147
|
+
self.currentItemDidChange()
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// MARK: - Notifications β fire on arbitrary thread, dispatch to playerQueue
|
|
153
|
+
@objc func playerItemDidPlayToEndTime(_ notification: Notification) {
|
|
154
|
+
playerQueue.async { [weak self] in self?.playerItemDidPlayToEndTimeInternal(notification) }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
func playerItemDidPlayToEndTimeInternal(_ notification: Notification) {
|
|
158
|
+
NitroPlayerLogger.log("TrackPlayerCore", "\nπ Track finished playing")
|
|
159
|
+
guard let finishedItem = notification.object as? AVPlayerItem else { return }
|
|
160
|
+
|
|
161
|
+
// 1. TRACK repeat β handle FIRST, before any temp-track removal
|
|
162
|
+
if currentRepeatMode == .track {
|
|
163
|
+
NitroPlayerLogger.log("TrackPlayerCore", "π TRACK repeat β seeking to zero and replaying")
|
|
164
|
+
player?.seek(to: .zero)
|
|
165
|
+
player?.play()
|
|
166
|
+
return // do not remove temp tracks, do not notify track change (same track looping)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 2. Remove finished temp track from its list
|
|
170
|
+
if let trackId = finishedItem.trackId {
|
|
171
|
+
if let index = playNextStack.firstIndex(where: { $0.id == trackId }) {
|
|
172
|
+
let track = playNextStack.remove(at: index)
|
|
173
|
+
NitroPlayerLogger.log("TrackPlayerCore", "π Finished playNext track: \(track.title) - removed from stack")
|
|
174
|
+
notifyTemporaryQueueChange()
|
|
175
|
+
} else if let index = upNextQueue.firstIndex(where: { $0.id == trackId }) {
|
|
176
|
+
let track = upNextQueue.remove(at: index)
|
|
177
|
+
NitroPlayerLogger.log("TrackPlayerCore", "π Finished upNext track: \(track.title) - removed from queue")
|
|
178
|
+
notifyTemporaryQueueChange()
|
|
179
|
+
} else if let track = currentTracks.first(where: { $0.id == trackId }) {
|
|
180
|
+
NitroPlayerLogger.log("TrackPlayerCore", "π Finished original track: \(track.title)")
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 3. Normal advance via actionAtItemEnd = .advance
|
|
185
|
+
if let player = player {
|
|
186
|
+
NitroPlayerLogger.log("TrackPlayerCore", "π Remaining items in queue: \(player.items().count)")
|
|
187
|
+
}
|
|
188
|
+
checkUpcomingTracksForUrls(lookahead: lookaheadCount)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@objc func playerItemFailedToPlayToEndTime(_ notification: Notification) {
|
|
192
|
+
if let error = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? Error {
|
|
193
|
+
NitroPlayerLogger.log("TrackPlayerCore", "β Playback failed - \(error)")
|
|
194
|
+
notifyPlaybackStateChange(.stopped, .error)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@objc func playerItemNewErrorLogEntry(_ notification: Notification) {
|
|
199
|
+
guard let item = notification.object as? AVPlayerItem, let errorLog = item.errorLog() else { return }
|
|
200
|
+
for event in errorLog.events ?? [] {
|
|
201
|
+
NitroPlayerLogger.log("TrackPlayerCore", "β Error log - \(event.errorComment ?? "Unknown error") - Code: \(event.errorStatusCode)")
|
|
202
|
+
}
|
|
203
|
+
if let error = item.error {
|
|
204
|
+
NitroPlayerLogger.log("TrackPlayerCore", "β Item error - \(error.localizedDescription)")
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
@objc func playerItemTimeJumped(_ notification: Notification) {
|
|
209
|
+
playerQueue.async { [weak self] in
|
|
210
|
+
guard let self, let player = self.player, let currentItem = player.currentItem else { return }
|
|
211
|
+
let position = currentItem.currentTime().seconds
|
|
212
|
+
let duration = currentItem.duration.seconds
|
|
213
|
+
NitroPlayerLogger.log("TrackPlayerCore", "π― Time jumped (seek detected) - position: \(Int(position))s")
|
|
214
|
+
self.notifySeek(position, duration)
|
|
215
|
+
self.isManuallySeeked = true
|
|
216
|
+
self.handleBoundaryTimeCrossed()
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
func currentItemDidChange() {
|
|
221
|
+
// Clear old item observers
|
|
222
|
+
currentItemObservers.removeAll()
|
|
223
|
+
|
|
224
|
+
// Reset proactive URL check debounce for the new track
|
|
225
|
+
didRequestUrlsForCurrentItem = false
|
|
226
|
+
|
|
227
|
+
guard let player, let currentItem = player.currentItem else {
|
|
228
|
+
NitroPlayerLogger.log("TrackPlayerCore", "β οΈ Current item changed to nil")
|
|
229
|
+
// Queue exhausted β handle PLAYLIST repeat
|
|
230
|
+
if currentRepeatMode == .playlist && !currentTracks.isEmpty, let player = self.player {
|
|
231
|
+
NitroPlayerLogger.log("TrackPlayerCore", "π PLAYLIST repeat β rebuilding original queue and restarting")
|
|
232
|
+
playNextStack.removeAll()
|
|
233
|
+
upNextQueue.removeAll()
|
|
234
|
+
currentTemporaryType = .none
|
|
235
|
+
|
|
236
|
+
let allItems = currentTracks.compactMap { createGaplessPlayerItem(for: $0, isPreload: false) }
|
|
237
|
+
var lastItem: AVPlayerItem? = nil
|
|
238
|
+
for item in allItems {
|
|
239
|
+
player.insert(item, after: lastItem)
|
|
240
|
+
lastItem = item
|
|
241
|
+
}
|
|
242
|
+
currentTrackIndex = 0
|
|
243
|
+
player.play()
|
|
244
|
+
|
|
245
|
+
if let firstTrack = currentTracks.first {
|
|
246
|
+
notifyTrackChange(firstTrack, .repeat)
|
|
247
|
+
}
|
|
248
|
+
notifyTemporaryQueueChange()
|
|
249
|
+
}
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#if DEBUG
|
|
254
|
+
NitroPlayerLogger.log("TrackPlayerCore", "\n" + String(repeating: "βΆ", count: Constants.separatorLineLength))
|
|
255
|
+
NitroPlayerLogger.log("TrackPlayerCore", "π CURRENT ITEM CHANGED")
|
|
256
|
+
NitroPlayerLogger.log("TrackPlayerCore", String(repeating: "βΆ", count: Constants.separatorLineLength))
|
|
257
|
+
|
|
258
|
+
if let trackId = currentItem.trackId,
|
|
259
|
+
let track = currentTracks.first(where: { $0.id == trackId })
|
|
260
|
+
{
|
|
261
|
+
NitroPlayerLogger.log("TrackPlayerCore", "βΆοΈ NOW PLAYING: \(track.title) - \(track.artist) (ID: \(track.id))")
|
|
262
|
+
} else {
|
|
263
|
+
NitroPlayerLogger.log("TrackPlayerCore", "β οΈ NOW PLAYING: Unknown track (trackId: \(currentItem.trackId ?? "nil"))")
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let remainingItems = player.items()
|
|
267
|
+
NitroPlayerLogger.log("TrackPlayerCore", "\nπ REMAINING ITEMS IN QUEUE: \(remainingItems.count)")
|
|
268
|
+
for (index, item) in remainingItems.enumerated() {
|
|
269
|
+
if let trackId = item.trackId, let track = currentTracks.first(where: { $0.id == trackId }) {
|
|
270
|
+
let marker = item == currentItem ? "βΆοΈ" : " "
|
|
271
|
+
NitroPlayerLogger.log("TrackPlayerCore", "\(marker) [\(index + 1)] \(track.title) - \(track.artist)")
|
|
272
|
+
} else {
|
|
273
|
+
NitroPlayerLogger.log("TrackPlayerCore", " [\(index + 1)] β οΈ Unknown track")
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
NitroPlayerLogger.log("TrackPlayerCore", String(repeating: "βΆ", count: Constants.separatorLineLength) + "\n")
|
|
278
|
+
#endif
|
|
279
|
+
|
|
280
|
+
NitroPlayerLogger.log("TrackPlayerCore", "π± Item status: \(currentItem.status.rawValue)")
|
|
281
|
+
|
|
282
|
+
if let error = currentItem.error {
|
|
283
|
+
NitroPlayerLogger.log("TrackPlayerCore", "β Current item has error - \(error.localizedDescription)")
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Setup KVO observers for current item
|
|
287
|
+
setupCurrentItemObservers(item: currentItem)
|
|
288
|
+
|
|
289
|
+
// Update track index and determine temporary type
|
|
290
|
+
if let trackId = currentItem.trackId {
|
|
291
|
+
NitroPlayerLogger.log("TrackPlayerCore", "π Looking up trackId '\(trackId)' in currentTracks...")
|
|
292
|
+
NitroPlayerLogger.log("TrackPlayerCore", " Current index BEFORE lookup: \(currentTrackIndex)")
|
|
293
|
+
|
|
294
|
+
currentTemporaryType = determineCurrentTemporaryType()
|
|
295
|
+
NitroPlayerLogger.log("TrackPlayerCore", " π― Track type: \(currentTemporaryType)")
|
|
296
|
+
|
|
297
|
+
if currentTemporaryType != .none {
|
|
298
|
+
var tempTrack: TrackItem?
|
|
299
|
+
if currentTemporaryType == .playNext { tempTrack = playNextStack.first(where: { $0.id == trackId }) }
|
|
300
|
+
else if currentTemporaryType == .upNext { tempTrack = upNextQueue.first(where: { $0.id == trackId }) }
|
|
301
|
+
if let track = tempTrack {
|
|
302
|
+
NitroPlayerLogger.log("TrackPlayerCore", " π΅ Temporary track: \(track.title) - \(track.artist)")
|
|
303
|
+
NitroPlayerLogger.log("TrackPlayerCore", " π’ Emitting onChangeTrack for temporary track")
|
|
304
|
+
notifyTrackChange(track, .skip)
|
|
305
|
+
}
|
|
306
|
+
} else if let index = currentTracks.firstIndex(where: { $0.id == trackId }) {
|
|
307
|
+
NitroPlayerLogger.log("TrackPlayerCore", " β
Found track at index: \(index)")
|
|
308
|
+
NitroPlayerLogger.log("TrackPlayerCore", " Setting currentTrackIndex from \(currentTrackIndex) to \(index)")
|
|
309
|
+
|
|
310
|
+
let oldIndex = currentTrackIndex
|
|
311
|
+
currentTrackIndex = index
|
|
312
|
+
|
|
313
|
+
if let track = currentTracks[safe: index] {
|
|
314
|
+
NitroPlayerLogger.log("TrackPlayerCore", " π΅ Track: \(track.title) - \(track.artist)")
|
|
315
|
+
if oldIndex != index {
|
|
316
|
+
NitroPlayerLogger.log("TrackPlayerCore", " π’ Emitting onChangeTrack (index changed from \(oldIndex) to \(index))")
|
|
317
|
+
notifyTrackChange(track, .skip)
|
|
318
|
+
} else {
|
|
319
|
+
NitroPlayerLogger.log("TrackPlayerCore", " βοΈ Skipping onChangeTrack emission (index unchanged)")
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
NitroPlayerLogger.log("TrackPlayerCore", " β οΈ Track ID '\(trackId)' NOT FOUND in currentTracks!")
|
|
324
|
+
#if DEBUG
|
|
325
|
+
NitroPlayerLogger.log("TrackPlayerCore", " Current tracks:")
|
|
326
|
+
for (idx, track) in currentTracks.enumerated() {
|
|
327
|
+
NitroPlayerLogger.log("TrackPlayerCore", " [\(idx)] \(track.id) - \(track.title)")
|
|
328
|
+
}
|
|
329
|
+
#endif
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Setup boundary observers when item is ready
|
|
334
|
+
if currentItem.status == .readyToPlay {
|
|
335
|
+
setupBoundaryTimeObserver()
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Preload upcoming tracks for gapless playback
|
|
339
|
+
preloadUpcomingTracks(from: currentTrackIndex + 1)
|
|
340
|
+
cleanupPreloadedAssets(keepingFrom: currentTrackIndex)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
func setupCurrentItemObservers(item: AVPlayerItem) {
|
|
344
|
+
NitroPlayerLogger.log("TrackPlayerCore", "π± Setting up item observers")
|
|
345
|
+
|
|
346
|
+
let statusObserver = item.observe(\.status, options: [.new]) { [weak self] item, _ in
|
|
347
|
+
self?.playerQueue.async {
|
|
348
|
+
if item.status == .readyToPlay {
|
|
349
|
+
NitroPlayerLogger.log("TrackPlayerCore", "β
Item ready, setting up boundaries")
|
|
350
|
+
self?.setupBoundaryTimeObserver()
|
|
351
|
+
// First item is buffered and ready β disable stall waiting for gapless inter-track transitions
|
|
352
|
+
self?.player?.automaticallyWaitsToMinimizeStalling = false
|
|
353
|
+
// Update now playing info now that duration is available (capture on playerQueue first)
|
|
354
|
+
let state = self?.getStateInternal()
|
|
355
|
+
let queue = self?.getActualQueueInternal() ?? []
|
|
356
|
+
let track = self?.getCurrentTrack()
|
|
357
|
+
DispatchQueue.main.async {
|
|
358
|
+
if let track = track, let state = state {
|
|
359
|
+
self?.mediaSessionManager?.updateFromPlayerQueue(track: track, state: state, queue: queue)
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
} else if item.status == .failed {
|
|
363
|
+
NitroPlayerLogger.log("TrackPlayerCore", "β Item failed")
|
|
364
|
+
self?.notifyPlaybackStateChange(.stopped, .error)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
currentItemObservers.append(statusObserver)
|
|
369
|
+
|
|
370
|
+
let bufferEmptyObserver = item.observe(\.isPlaybackBufferEmpty, options: [.new]) { item, _ in
|
|
371
|
+
if item.isPlaybackBufferEmpty {
|
|
372
|
+
NitroPlayerLogger.log("TrackPlayerCore", "βΈοΈ Buffer empty (buffering)")
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
currentItemObservers.append(bufferEmptyObserver)
|
|
376
|
+
|
|
377
|
+
let bufferKeepUpObserver = item.observe(\.isPlaybackLikelyToKeepUp, options: [.new]) { item, _ in
|
|
378
|
+
if item.isPlaybackLikelyToKeepUp {
|
|
379
|
+
NitroPlayerLogger.log("TrackPlayerCore", "βΆοΈ Buffer likely to keep up")
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
currentItemObservers.append(bufferKeepUpObserver)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
func emitStateChange(reason: Reason? = nil) {
|
|
386
|
+
guard let player else { return }
|
|
387
|
+
let state: TrackPlayerState
|
|
388
|
+
if player.rate == 0 { state = .paused }
|
|
389
|
+
else if player.timeControlStatus == .playing { state = .playing }
|
|
390
|
+
else if player.timeControlStatus == .waitingToPlayAtSpecifiedRate { state = .paused }
|
|
391
|
+
else { state = .stopped }
|
|
392
|
+
NitroPlayerLogger.log("TrackPlayerCore", "π Emitting state change: \(state)")
|
|
393
|
+
notifyPlaybackStateChange(state, reason)
|
|
394
|
+
}
|
|
395
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//
|
|
2
|
+
// TrackPlayerNotify.swift
|
|
3
|
+
// NitroPlayer
|
|
4
|
+
//
|
|
5
|
+
// Created by Ritesh Shukla on 25/03/26.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
import Foundation
|
|
10
|
+
|
|
11
|
+
extension TrackPlayerCore {
|
|
12
|
+
|
|
13
|
+
// Called on playerQueue β invoke listeners directly (Nitro handles JS thread hop)
|
|
14
|
+
func notifyTrackChange(_ track: TrackItem, _ reason: Reason?) {
|
|
15
|
+
onChangeTrackListeners.forEach { $0(track, reason) }
|
|
16
|
+
// Capture state + queue now (on playerQueue), pass pre-computed values to main
|
|
17
|
+
let state = getStateInternal()
|
|
18
|
+
let queue = getActualQueueInternal()
|
|
19
|
+
DispatchQueue.main.async { [weak self] in
|
|
20
|
+
self?.mediaSessionManager?.updateFromPlayerQueue(track: track, state: state, queue: queue)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func notifyPlaybackStateChange(_ state: TrackPlayerState, _ reason: Reason?) {
|
|
25
|
+
onPlaybackStateChangeListeners.forEach { $0(state, reason) }
|
|
26
|
+
let playerState = getStateInternal()
|
|
27
|
+
let queue = getActualQueueInternal()
|
|
28
|
+
let track = getCurrentTrack()
|
|
29
|
+
DispatchQueue.main.async { [weak self] in
|
|
30
|
+
guard let track = track else { return }
|
|
31
|
+
self?.mediaSessionManager?.updateFromPlayerQueue(track: track, state: playerState, queue: queue)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func notifySeek(_ position: Double, _ duration: Double) {
|
|
36
|
+
onSeekListeners.forEach { $0(position, duration) }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func notifyPlaybackProgress(_ position: Double, _ duration: Double, _ isManuallySeeked: Bool?) {
|
|
40
|
+
onProgressListeners.forEach { $0(position, duration, isManuallySeeked) }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func notifyTracksNeedUpdate(tracks: [TrackItem], lookahead: Int) {
|
|
44
|
+
onTracksNeedUpdateListeners.forEach { $0(tracks, lookahead) }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func notifyTemporaryQueueChange() {
|
|
48
|
+
let pn = playNextStack
|
|
49
|
+
let un = upNextQueue
|
|
50
|
+
onTemporaryQueueChangeListeners.forEach { $0(pn, un) }
|
|
51
|
+
}
|
|
52
|
+
}
|