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.
Files changed (110) hide show
  1. package/README.md +47 -46
  2. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +9 -13
  3. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +45 -90
  4. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +48 -182
  5. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +21 -77
  6. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +55 -104
  7. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +113 -123
  8. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ExoPlayerCore.kt +82 -0
  9. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ListenerRegistry.kt +48 -0
  10. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerAndroidAuto.kt +62 -0
  11. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +153 -1887
  12. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +122 -0
  13. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerNotify.kt +44 -0
  14. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +162 -0
  15. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueue.kt +179 -0
  16. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +170 -0
  17. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +28 -0
  18. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerTempQueue.kt +121 -0
  19. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +98 -0
  20. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +27 -18
  21. package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +150 -135
  22. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +13 -30
  23. package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +102 -162
  24. package/ios/HybridDownloadManager.swift +32 -26
  25. package/ios/HybridEqualizer.swift +48 -35
  26. package/ios/HybridTrackPlayer.swift +127 -102
  27. package/ios/core/ListenerRegistry.swift +60 -0
  28. package/ios/core/TrackPlayerCore.swift +130 -2356
  29. package/ios/core/TrackPlayerListener.swift +395 -0
  30. package/ios/core/TrackPlayerNotify.swift +52 -0
  31. package/ios/core/TrackPlayerPlayback.swift +274 -0
  32. package/ios/core/TrackPlayerQueue.swift +221 -0
  33. package/ios/core/TrackPlayerQueueBuild.swift +493 -0
  34. package/ios/core/TrackPlayerTempQueue.swift +167 -0
  35. package/ios/core/TrackPlayerUrlLoader.swift +169 -0
  36. package/ios/equalizer/EqualizerCore.swift +63 -123
  37. package/ios/media/MediaSessionManager.swift +32 -49
  38. package/ios/playlist/PlaylistManager.swift +2 -9
  39. package/ios/queue/HybridPlayerQueue.swift +69 -66
  40. package/lib/hooks/useDownloadedTracks.js +16 -13
  41. package/lib/hooks/useEqualizer.d.ts +4 -4
  42. package/lib/hooks/useEqualizer.js +22 -17
  43. package/lib/hooks/useEqualizerPresets.d.ts +3 -3
  44. package/lib/hooks/useEqualizerPresets.js +12 -18
  45. package/lib/specs/AndroidAutoMediaLibrary.nitro.d.ts +2 -2
  46. package/lib/specs/AudioDevices.nitro.d.ts +2 -2
  47. package/lib/specs/DownloadManager.nitro.d.ts +10 -10
  48. package/lib/specs/Equalizer.nitro.d.ts +10 -10
  49. package/lib/specs/TrackPlayer.nitro.d.ts +38 -16
  50. package/lib/types/EqualizerTypes.d.ts +3 -3
  51. package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +2 -0
  52. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__std__vector_TrackItem_.hpp +122 -0
  53. package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.cpp +31 -6
  54. package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.hpp +2 -2
  55. package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.cpp +16 -3
  56. package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.hpp +1 -1
  57. package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.cpp +154 -44
  58. package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.hpp +10 -10
  59. package/nitrogen/generated/android/c++/JHybridEqualizerSpec.cpp +130 -34
  60. package/nitrogen/generated/android/c++/JHybridEqualizerSpec.hpp +9 -9
  61. package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +115 -24
  62. package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.hpp +8 -8
  63. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +243 -24
  64. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +16 -8
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_TrackItem__std__vector_TrackItem_.kt +80 -0
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrarySpec.kt +3 -2
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAudioDevicesSpec.kt +2 -1
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridDownloadManagerSpec.kt +10 -10
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridEqualizerSpec.kt +10 -9
  70. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridPlayerQueueSpec.kt +9 -8
  71. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +45 -8
  72. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +74 -18
  73. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +380 -151
  74. package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.hpp +10 -10
  75. package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.hpp +12 -9
  76. package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.hpp +23 -8
  77. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +82 -8
  78. package/nitrogen/generated/ios/swift/Func_void_EqualizerState.swift +46 -0
  79. package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__DownloadedPlaylist_.swift +58 -0
  80. package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__DownloadedTrack_.swift +58 -0
  81. package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__std__string_.swift +58 -0
  82. package/nitrogen/generated/ios/swift/Func_void_std__vector_DownloadedPlaylist_.swift +46 -0
  83. package/nitrogen/generated/ios/swift/Func_void_std__vector_DownloadedTrack_.swift +46 -0
  84. package/nitrogen/generated/ios/swift/Func_void_std__vector_EqualizerBand_.swift +5 -5
  85. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__std__vector_TrackItem_.swift +46 -0
  86. package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec.swift +10 -10
  87. package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec_cxx.swift +141 -71
  88. package/nitrogen/generated/ios/swift/HybridEqualizerSpec.swift +9 -9
  89. package/nitrogen/generated/ios/swift/HybridEqualizerSpec_cxx.swift +105 -41
  90. package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec.swift +8 -8
  91. package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec_cxx.swift +95 -32
  92. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +16 -8
  93. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +267 -32
  94. package/nitrogen/generated/shared/c++/HybridAndroidAutoMediaLibrarySpec.hpp +3 -2
  95. package/nitrogen/generated/shared/c++/HybridAudioDevicesSpec.hpp +2 -1
  96. package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.hpp +10 -10
  97. package/nitrogen/generated/shared/c++/HybridEqualizerSpec.hpp +10 -9
  98. package/nitrogen/generated/shared/c++/HybridPlayerQueueSpec.hpp +9 -8
  99. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +8 -0
  100. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +16 -8
  101. package/package.json +5 -5
  102. package/src/hooks/useDownloadedTracks.ts +17 -13
  103. package/src/hooks/useEqualizer.ts +26 -21
  104. package/src/hooks/useEqualizerPresets.ts +15 -21
  105. package/src/specs/AndroidAutoMediaLibrary.nitro.ts +2 -2
  106. package/src/specs/AudioDevices.nitro.ts +2 -2
  107. package/src/specs/DownloadManager.nitro.ts +10 -10
  108. package/src/specs/Equalizer.nitro.ts +10 -10
  109. package/src/specs/TrackPlayer.nitro.ts +52 -16
  110. 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
+ }