react-native-nitro-player 0.5.3 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +5 -3
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +4 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/connection/AndroidAutoConnectionDetector.kt +14 -13
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/NitroPlayerLogger.kt +31 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +142 -95
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +75 -29
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadManagerCore.kt +2 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadWorker.kt +1 -2
- package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +3 -2
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaBrowserService.kt +25 -24
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaLibraryManager.kt +3 -2
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +20 -19
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +119 -85
- package/android/src/main/java/com/margelo/nitro/nitroplayer/storage/NitroPlayerStorage.kt +50 -0
- package/ios/HybridAudioRoutePicker.swift +1 -1
- package/ios/HybridDownloadManager.swift +3 -3
- package/ios/HybridEqualizer.swift +3 -3
- package/ios/HybridTrackPlayer.swift +8 -4
- package/ios/core/NitroPlayerLogger.swift +22 -0
- package/ios/core/TrackPlayerCore.swift +195 -256
- package/ios/download/DownloadDatabase.swift +92 -62
- package/ios/download/DownloadFileManager.swift +17 -17
- package/ios/download/DownloadManagerCore.swift +80 -44
- package/ios/equalizer/EqualizerCore.swift +25 -20
- package/ios/playlist/PlaylistManager.swift +113 -82
- package/ios/queue/QueueManager.swift +1 -1
- package/ios/storage/NitroPlayerStorage.swift +44 -0
- package/lib/specs/TrackPlayer.nitro.d.ts +1 -0
- package/lib/types/PlayerQueue.d.ts +1 -1
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +5 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +1 -0
- package/nitrogen/generated/android/c++/JReason.hpp +3 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +4 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Reason.kt +2 -1
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +12 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +8 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +1 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +12 -0
- package/nitrogen/generated/ios/swift/Reason.swift +4 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +1 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +1 -0
- package/nitrogen/generated/shared/c++/Reason.hpp +4 -0
- package/package.json +1 -1
- package/src/specs/TrackPlayer.nitro.ts +1 -0
- package/src/types/PlayerQueue.ts +1 -1
|
@@ -29,6 +29,9 @@ class EqualizerCore {
|
|
|
29
29
|
// Current gains storage - internal so TapContext can access
|
|
30
30
|
private(set) var currentGains: [Double] = [0, 0, 0, 0, 0]
|
|
31
31
|
|
|
32
|
+
// Dirty flag: set when gains change so TapContext only recalculates when needed
|
|
33
|
+
var gainsDirty: Bool = true
|
|
34
|
+
|
|
32
35
|
// UserDefaults keys
|
|
33
36
|
private let enabledKey = "eq_enabled"
|
|
34
37
|
private let bandGainsKey = "eq_band_gains"
|
|
@@ -81,7 +84,7 @@ class EqualizerCore {
|
|
|
81
84
|
|
|
82
85
|
private init() {
|
|
83
86
|
restoreSettings()
|
|
84
|
-
|
|
87
|
+
NitroPlayerLogger.log("EqualizerCore", "✅ Initialized with MTAudioProcessingTap support")
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
// MARK: - Audio Mix Creation for AVPlayerItem
|
|
@@ -98,19 +101,18 @@ class EqualizerCore {
|
|
|
98
101
|
let status = asset.statusOfValue(forKey: "tracks", error: &error)
|
|
99
102
|
|
|
100
103
|
if status == .failed {
|
|
101
|
-
|
|
102
|
-
"⚠️ EqualizerCore: Failed to load tracks key: \(error?.localizedDescription ?? "unknown")")
|
|
104
|
+
NitroPlayerLogger.log("EqualizerCore", "⚠️ Failed to load tracks key: \(error?.localizedDescription ?? "unknown")")
|
|
103
105
|
return
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
// Proceed only if loaded successfully
|
|
107
109
|
guard status == .loaded else {
|
|
108
|
-
|
|
110
|
+
NitroPlayerLogger.log("EqualizerCore", "⚠️ Tracks not loaded, status: \(status.rawValue)")
|
|
109
111
|
return
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
guard let audioTrack = asset.tracks(withMediaType: .audio).first else {
|
|
113
|
-
|
|
115
|
+
NitroPlayerLogger.log("EqualizerCore", "⚠️ No audio track found in asset")
|
|
114
116
|
return
|
|
115
117
|
}
|
|
116
118
|
|
|
@@ -137,7 +139,7 @@ class EqualizerCore {
|
|
|
137
139
|
)
|
|
138
140
|
|
|
139
141
|
guard createStatus == noErr, let audioTap = tap else {
|
|
140
|
-
|
|
142
|
+
NitroPlayerLogger.log("EqualizerCore", "❌ Failed to create audio processing tap, status: \(createStatus)")
|
|
141
143
|
return
|
|
142
144
|
}
|
|
143
145
|
|
|
@@ -150,7 +152,7 @@ class EqualizerCore {
|
|
|
150
152
|
// Apply to player item on main thread (AVPlayerItem properties should be accessed/modified on main thread or serial queue usually, but audioMix is thread safe - safely done on main to be sure)
|
|
151
153
|
DispatchQueue.main.async {
|
|
152
154
|
playerItem.audioMix = audioMix
|
|
153
|
-
|
|
155
|
+
NitroPlayerLogger.log("EqualizerCore", "✅ Applied audio mix with EQ tap to player item (async)")
|
|
154
156
|
}
|
|
155
157
|
}
|
|
156
158
|
}
|
|
@@ -163,7 +165,7 @@ class EqualizerCore {
|
|
|
163
165
|
notifyEnabledChange(enabled)
|
|
164
166
|
saveEnabled(enabled)
|
|
165
167
|
|
|
166
|
-
|
|
168
|
+
NitroPlayerLogger.log("EqualizerCore", "🎚️ Equalizer \(enabled ? "enabled" : "disabled")")
|
|
167
169
|
return true
|
|
168
170
|
}
|
|
169
171
|
|
|
@@ -187,6 +189,7 @@ class EqualizerCore {
|
|
|
187
189
|
|
|
188
190
|
let clampedGain = max(-12.0, min(12.0, gainDb))
|
|
189
191
|
currentGains[bandIndex] = clampedGain
|
|
192
|
+
gainsDirty = true
|
|
190
193
|
|
|
191
194
|
currentPresetName = nil
|
|
192
195
|
notifyBandChange(getBands())
|
|
@@ -194,7 +197,7 @@ class EqualizerCore {
|
|
|
194
197
|
saveBandGains(currentGains)
|
|
195
198
|
saveCurrentPreset(nil)
|
|
196
199
|
|
|
197
|
-
|
|
200
|
+
NitroPlayerLogger.log("EqualizerCore", "🎚️ Band \(bandIndex) gain set to \(clampedGain) dB")
|
|
198
201
|
return true
|
|
199
202
|
}
|
|
200
203
|
|
|
@@ -204,11 +207,12 @@ class EqualizerCore {
|
|
|
204
207
|
for i in 0..<5 {
|
|
205
208
|
currentGains[i] = max(-12.0, min(12.0, gains[i]))
|
|
206
209
|
}
|
|
210
|
+
gainsDirty = true
|
|
207
211
|
|
|
208
212
|
notifyBandChange(getBands())
|
|
209
213
|
saveBandGains(currentGains)
|
|
210
214
|
|
|
211
|
-
|
|
215
|
+
NitroPlayerLogger.log("EqualizerCore", "🎚️ All band gains updated")
|
|
212
216
|
return true
|
|
213
217
|
}
|
|
214
218
|
|
|
@@ -380,7 +384,7 @@ class EqualizerCore {
|
|
|
380
384
|
currentPresetName = UserDefaults.standard.string(forKey: currentPresetKey)
|
|
381
385
|
isEqualizerEnabled = enabled
|
|
382
386
|
|
|
383
|
-
|
|
387
|
+
NitroPlayerLogger.log("EqualizerCore", "✅ Restored settings - enabled: \(enabled), gains: \(currentGains)")
|
|
384
388
|
}
|
|
385
389
|
|
|
386
390
|
// MARK: - Callback Management
|
|
@@ -505,6 +509,7 @@ private class TapContext {
|
|
|
505
509
|
sampleRate: Double(sampleRate)
|
|
506
510
|
)
|
|
507
511
|
}
|
|
512
|
+
eqCore.gainsDirty = false
|
|
508
513
|
}
|
|
509
514
|
|
|
510
515
|
/// Calculate biquad coefficients for a peaking EQ filter
|
|
@@ -556,13 +561,13 @@ private func tapInitCallback(
|
|
|
556
561
|
let context = TapContext(eqCore: eqCore)
|
|
557
562
|
tapStorageOut.pointee = Unmanaged.passRetained(context).toOpaque()
|
|
558
563
|
|
|
559
|
-
|
|
564
|
+
NitroPlayerLogger.log("EqualizerCore", "🎛️ Tap initialized")
|
|
560
565
|
}
|
|
561
566
|
|
|
562
567
|
private func tapFinalizeCallback(tap: MTAudioProcessingTap) {
|
|
563
568
|
let storage = MTAudioProcessingTapGetStorage(tap)
|
|
564
569
|
Unmanaged<TapContext>.fromOpaque(storage).release()
|
|
565
|
-
|
|
570
|
+
NitroPlayerLogger.log("EqualizerCore", "🎛️ Tap finalized")
|
|
566
571
|
}
|
|
567
572
|
|
|
568
573
|
private func tapPrepareCallback(
|
|
@@ -578,13 +583,11 @@ private func tapPrepareCallback(
|
|
|
578
583
|
context.updateCoefficients()
|
|
579
584
|
context.resetFilterStates()
|
|
580
585
|
|
|
581
|
-
|
|
582
|
-
"🎛️ EqualizerCore: Tap prepared - sampleRate: \(context.sampleRate), channels: \(context.channelCount)"
|
|
583
|
-
)
|
|
586
|
+
NitroPlayerLogger.log("EqualizerCore", "🎛️ Tap prepared - sampleRate: \(context.sampleRate), channels: \(context.channelCount)")
|
|
584
587
|
}
|
|
585
588
|
|
|
586
589
|
private func tapUnprepareCallback(tap: MTAudioProcessingTap) {
|
|
587
|
-
|
|
590
|
+
NitroPlayerLogger.log("EqualizerCore", "🎛️ Tap unprepared")
|
|
588
591
|
}
|
|
589
592
|
|
|
590
593
|
private func tapProcessCallback(
|
|
@@ -610,7 +613,7 @@ private func tapProcessCallback(
|
|
|
610
613
|
)
|
|
611
614
|
|
|
612
615
|
guard status == noErr else {
|
|
613
|
-
|
|
616
|
+
NitroPlayerLogger.log("EqualizerCore", "❌ Failed to get source audio: \(status)")
|
|
614
617
|
return
|
|
615
618
|
}
|
|
616
619
|
|
|
@@ -620,8 +623,10 @@ private func tapProcessCallback(
|
|
|
620
623
|
return
|
|
621
624
|
}
|
|
622
625
|
|
|
623
|
-
// Update coefficients
|
|
624
|
-
context.
|
|
626
|
+
// Update coefficients only when gains have changed
|
|
627
|
+
if context.eqCore?.gainsDirty == true {
|
|
628
|
+
context.updateCoefficients()
|
|
629
|
+
}
|
|
625
630
|
|
|
626
631
|
// Process each buffer (channel)
|
|
627
632
|
let bufferList = UnsafeMutableAudioBufferListPointer(bufferListInOut)
|
|
@@ -16,11 +16,12 @@ class PlaylistManager {
|
|
|
16
16
|
[:]
|
|
17
17
|
private var currentPlaylistId: String?
|
|
18
18
|
private let queue = DispatchQueue(label: "com.margelo.nitro.nitroplayer.playlist")
|
|
19
|
+
private var saveDebounceWorkItem: DispatchWorkItem?
|
|
19
20
|
|
|
20
21
|
static let shared = PlaylistManager()
|
|
21
22
|
|
|
22
23
|
private init() {
|
|
23
|
-
|
|
24
|
+
loadFromFile()
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
/**
|
|
@@ -34,7 +35,7 @@ class PlaylistManager {
|
|
|
34
35
|
playlists[id] = playlist
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
scheduleSave()
|
|
38
39
|
notifyPlaylistsChanged(.add)
|
|
39
40
|
|
|
40
41
|
return id
|
|
@@ -53,7 +54,7 @@ class PlaylistManager {
|
|
|
53
54
|
currentPlaylistId = nil
|
|
54
55
|
}
|
|
55
56
|
playlistListeners.removeValue(forKey: playlistId)
|
|
56
|
-
|
|
57
|
+
scheduleSave()
|
|
57
58
|
notifyPlaylistsChanged(.remove)
|
|
58
59
|
return true
|
|
59
60
|
}
|
|
@@ -81,7 +82,7 @@ class PlaylistManager {
|
|
|
81
82
|
)
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
scheduleSave()
|
|
85
86
|
notifyPlaylistChanged(playlistId, .update)
|
|
86
87
|
notifyPlaylistsChanged(.update)
|
|
87
88
|
|
|
@@ -130,7 +131,7 @@ class PlaylistManager {
|
|
|
130
131
|
)
|
|
131
132
|
}
|
|
132
133
|
|
|
133
|
-
|
|
134
|
+
scheduleSave()
|
|
134
135
|
notifyPlaylistChanged(playlistId, .add)
|
|
135
136
|
|
|
136
137
|
// Update TrackPlayerCore if this is the current playlist
|
|
@@ -165,7 +166,7 @@ class PlaylistManager {
|
|
|
165
166
|
)
|
|
166
167
|
}
|
|
167
168
|
|
|
168
|
-
|
|
169
|
+
scheduleSave()
|
|
169
170
|
notifyPlaylistChanged(playlistId, .add)
|
|
170
171
|
|
|
171
172
|
// Update TrackPlayerCore if this is the current playlist
|
|
@@ -204,7 +205,7 @@ class PlaylistManager {
|
|
|
204
205
|
}
|
|
205
206
|
|
|
206
207
|
if removed {
|
|
207
|
-
|
|
208
|
+
scheduleSave()
|
|
208
209
|
notifyPlaylistChanged(playlistId, .remove)
|
|
209
210
|
|
|
210
211
|
// Update TrackPlayerCore if this is the current playlist
|
|
@@ -245,7 +246,7 @@ class PlaylistManager {
|
|
|
245
246
|
)
|
|
246
247
|
}
|
|
247
248
|
|
|
248
|
-
|
|
249
|
+
scheduleSave()
|
|
249
250
|
notifyPlaylistChanged(playlistId, .update)
|
|
250
251
|
|
|
251
252
|
// Update TrackPlayerCore if this is the current playlist
|
|
@@ -349,13 +350,20 @@ class PlaylistManager {
|
|
|
349
350
|
}
|
|
350
351
|
}
|
|
351
352
|
|
|
352
|
-
private func
|
|
353
|
-
|
|
354
|
-
|
|
353
|
+
private func scheduleSave() {
|
|
354
|
+
saveDebounceWorkItem?.cancel()
|
|
355
|
+
let work = DispatchWorkItem { [weak self] in self?.saveToFile() }
|
|
356
|
+
saveDebounceWorkItem = work
|
|
357
|
+
// Use global background queue — saveToFile calls queue.sync internally,
|
|
358
|
+
// which would deadlock if scheduled on queue itself.
|
|
359
|
+
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 0.3, execute: work)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// MARK: - Persistence
|
|
363
|
+
|
|
364
|
+
private func saveToFile() {
|
|
355
365
|
do {
|
|
356
|
-
let playlistsArray = queue.sync {
|
|
357
|
-
return Array(playlists.values)
|
|
358
|
-
}
|
|
366
|
+
let playlistsArray = queue.sync { Array(playlists.values) }
|
|
359
367
|
let playlistsData = playlistsArray.map { playlist -> [String: Any] in
|
|
360
368
|
return [
|
|
361
369
|
"id": playlist.id,
|
|
@@ -371,13 +379,11 @@ class PlaylistManager {
|
|
|
371
379
|
"duration": track.duration,
|
|
372
380
|
"url": track.url,
|
|
373
381
|
]
|
|
374
|
-
// Handle artwork - unwrap Variant_NullType_String
|
|
375
382
|
if let artwork = track.artwork, case .second(let artworkUrl) = artwork {
|
|
376
383
|
trackDict["artwork"] = artworkUrl
|
|
377
384
|
} else {
|
|
378
385
|
trackDict["artwork"] = ""
|
|
379
386
|
}
|
|
380
|
-
// Serialize extraPayload to dictionary for persistence
|
|
381
387
|
if let extraPayload = track.extraPayload {
|
|
382
388
|
trackDict["extraPayload"] = extraPayload.toDictionary()
|
|
383
389
|
}
|
|
@@ -385,93 +391,118 @@ class PlaylistManager {
|
|
|
385
391
|
},
|
|
386
392
|
]
|
|
387
393
|
}
|
|
388
|
-
let
|
|
389
|
-
|
|
390
|
-
|
|
394
|
+
let wrapper: [String: Any] = [
|
|
395
|
+
"playlists": playlistsData,
|
|
396
|
+
"currentPlaylistId": currentPlaylistId as Any,
|
|
397
|
+
]
|
|
398
|
+
let data = try JSONSerialization.data(withJSONObject: wrapper, options: [])
|
|
399
|
+
try NitroPlayerStorage.write(filename: "playlists.json", data: data)
|
|
391
400
|
} catch {
|
|
392
|
-
|
|
401
|
+
NitroPlayerLogger.log("PlaylistManager", "❌ Error saving playlists - \(error)")
|
|
393
402
|
}
|
|
394
403
|
}
|
|
395
404
|
|
|
396
|
-
private func
|
|
397
|
-
|
|
405
|
+
private func loadFromFile() {
|
|
406
|
+
// 1. Try new JSON file (post-migration)
|
|
407
|
+
if let data = NitroPlayerStorage.read(filename: "playlists.json") {
|
|
408
|
+
do {
|
|
409
|
+
if let wrapper = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
|
410
|
+
let playlistsDict = wrapper["playlists"] as? [[String: Any]] ?? []
|
|
411
|
+
parsePlaylists(from: playlistsDict)
|
|
412
|
+
currentPlaylistId = wrapper["currentPlaylistId"] as? String
|
|
413
|
+
}
|
|
414
|
+
} catch {
|
|
415
|
+
NitroPlayerLogger.log("PlaylistManager", "❌ Error loading playlists - \(error)")
|
|
416
|
+
}
|
|
417
|
+
return
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// 2. Migrate from UserDefaults (one-time, existing installs)
|
|
421
|
+
if let data = UserDefaults.standard.data(forKey: "NitroPlayerPlaylists") {
|
|
422
|
+
do {
|
|
423
|
+
let playlistsDict = try JSONSerialization.jsonObject(with: data) as? [[String: Any]] ?? []
|
|
424
|
+
parsePlaylists(from: playlistsDict)
|
|
425
|
+
currentPlaylistId = UserDefaults.standard.string(forKey: "NitroPlayerCurrentPlaylistId")
|
|
426
|
+
// Remove old keys to free UserDefaults space
|
|
427
|
+
UserDefaults.standard.removeObject(forKey: "NitroPlayerPlaylists")
|
|
428
|
+
UserDefaults.standard.removeObject(forKey: "NitroPlayerCurrentPlaylistId")
|
|
429
|
+
// Persist in new format
|
|
430
|
+
saveToFile()
|
|
431
|
+
} catch {
|
|
432
|
+
NitroPlayerLogger.log("PlaylistManager", "❌ Error migrating playlists - \(error)")
|
|
433
|
+
}
|
|
398
434
|
return
|
|
399
435
|
}
|
|
400
436
|
|
|
401
|
-
|
|
402
|
-
|
|
437
|
+
// 3. Fresh install — nothing to load
|
|
438
|
+
}
|
|
403
439
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
440
|
+
private func parsePlaylists(from playlistsDict: [[String: Any]]) {
|
|
441
|
+
queue.sync {
|
|
442
|
+
playlists.removeAll()
|
|
443
|
+
for playlistDict in playlistsDict {
|
|
444
|
+
guard let id = playlistDict["id"] as? String,
|
|
445
|
+
let name = playlistDict["name"] as? String
|
|
446
|
+
else {
|
|
447
|
+
continue
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
let description = playlistDict["description"] as? String
|
|
451
|
+
let artwork = playlistDict["artwork"] as? String
|
|
452
|
+
let tracksArray = playlistDict["tracks"] as? [[String: Any]] ?? []
|
|
453
|
+
|
|
454
|
+
let tracks = tracksArray.compactMap { trackDict -> TrackItem? in
|
|
455
|
+
guard let id = trackDict["id"] as? String,
|
|
456
|
+
let title = trackDict["title"] as? String,
|
|
457
|
+
let artist = trackDict["artist"] as? String,
|
|
458
|
+
let album = trackDict["album"] as? String,
|
|
459
|
+
let duration = trackDict["duration"] as? Double,
|
|
460
|
+
let url = trackDict["url"] as? String
|
|
409
461
|
else {
|
|
410
|
-
|
|
462
|
+
return nil
|
|
411
463
|
}
|
|
412
464
|
|
|
413
|
-
let
|
|
414
|
-
let artwork =
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
let tracks = tracksArray.compactMap { trackDict -> TrackItem? in
|
|
418
|
-
guard let id = trackDict["id"] as? String,
|
|
419
|
-
let title = trackDict["title"] as? String,
|
|
420
|
-
let artist = trackDict["artist"] as? String,
|
|
421
|
-
let album = trackDict["album"] as? String,
|
|
422
|
-
let duration = trackDict["duration"] as? Double,
|
|
423
|
-
let url = trackDict["url"] as? String
|
|
424
|
-
else {
|
|
425
|
-
return nil
|
|
426
|
-
}
|
|
465
|
+
let artworkString = trackDict["artwork"] as? String
|
|
466
|
+
let artwork = artworkString.flatMap {
|
|
467
|
+
!$0.isEmpty ? Variant_NullType_String.second($0) : nil
|
|
468
|
+
}
|
|
427
469
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
} else if let doubleValue = value as? Double {
|
|
441
|
-
extraPayload?.setDouble(key: key, value: doubleValue)
|
|
442
|
-
} else if let intValue = value as? Int {
|
|
443
|
-
extraPayload?.setDouble(key: key, value: Double(intValue))
|
|
444
|
-
} else if let boolValue = value as? Bool {
|
|
445
|
-
extraPayload?.setBoolean(key: key, value: boolValue)
|
|
446
|
-
}
|
|
470
|
+
var extraPayload: AnyMap? = nil
|
|
471
|
+
if let extraPayloadDict = trackDict["extraPayload"] as? [String: Any] {
|
|
472
|
+
extraPayload = AnyMap()
|
|
473
|
+
for (key, value) in extraPayloadDict {
|
|
474
|
+
if let stringValue = value as? String {
|
|
475
|
+
extraPayload?.setString(key: key, value: stringValue)
|
|
476
|
+
} else if let doubleValue = value as? Double {
|
|
477
|
+
extraPayload?.setDouble(key: key, value: doubleValue)
|
|
478
|
+
} else if let intValue = value as? Int {
|
|
479
|
+
extraPayload?.setDouble(key: key, value: Double(intValue))
|
|
480
|
+
} else if let boolValue = value as? Bool {
|
|
481
|
+
extraPayload?.setBoolean(key: key, value: boolValue)
|
|
447
482
|
}
|
|
448
483
|
}
|
|
449
|
-
|
|
450
|
-
return TrackItem(
|
|
451
|
-
id: id,
|
|
452
|
-
title: title,
|
|
453
|
-
artist: artist,
|
|
454
|
-
album: album,
|
|
455
|
-
duration: duration,
|
|
456
|
-
url: url,
|
|
457
|
-
artwork: artwork,
|
|
458
|
-
extraPayload: extraPayload
|
|
459
|
-
)
|
|
460
484
|
}
|
|
461
485
|
|
|
462
|
-
|
|
486
|
+
return TrackItem(
|
|
463
487
|
id: id,
|
|
464
|
-
|
|
465
|
-
|
|
488
|
+
title: title,
|
|
489
|
+
artist: artist,
|
|
490
|
+
album: album,
|
|
491
|
+
duration: duration,
|
|
492
|
+
url: url,
|
|
466
493
|
artwork: artwork,
|
|
467
|
-
|
|
494
|
+
extraPayload: extraPayload
|
|
468
495
|
)
|
|
469
496
|
}
|
|
470
|
-
}
|
|
471
497
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
498
|
+
playlists[id] = PlaylistModel(
|
|
499
|
+
id: id,
|
|
500
|
+
name: name,
|
|
501
|
+
description: description,
|
|
502
|
+
artwork: artwork,
|
|
503
|
+
tracks: tracks
|
|
504
|
+
)
|
|
505
|
+
}
|
|
475
506
|
}
|
|
476
507
|
}
|
|
477
508
|
}
|
|
@@ -114,7 +114,7 @@ class QueueManager {
|
|
|
114
114
|
wrapper.listener(currentTracks, operation)
|
|
115
115
|
} catch {
|
|
116
116
|
// Log error but don't break other listeners
|
|
117
|
-
|
|
117
|
+
NitroPlayerLogger.log("QueueManager", "Error in queue change listener: \(error)")
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//
|
|
2
|
+
// NitroPlayerStorage.swift
|
|
3
|
+
// NitroPlayer
|
|
4
|
+
//
|
|
5
|
+
// Created by Ritesh Shukla on 19/02/26.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Foundation
|
|
9
|
+
|
|
10
|
+
enum NitroPlayerStorage {
|
|
11
|
+
/// Reads raw data from a file in the NitroPlayer storage directory.
|
|
12
|
+
/// Returns nil if the file does not exist or cannot be read.
|
|
13
|
+
static func read(filename: String) -> Data? {
|
|
14
|
+
let url = storageDirectory().appendingPathComponent(filename)
|
|
15
|
+
return try? Data(contentsOf: url)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// Atomically writes data to a file in the NitroPlayer storage directory.
|
|
19
|
+
/// Writes to `<filename>.tmp` first, then renames to the final name —
|
|
20
|
+
/// leaving the prior file untouched if the write crashes mid-way.
|
|
21
|
+
static func write(filename: String, data: Data) throws {
|
|
22
|
+
let dir = storageDirectory()
|
|
23
|
+
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
24
|
+
let dest = dir.appendingPathComponent(filename)
|
|
25
|
+
let tmp = dir.appendingPathComponent(filename + ".tmp")
|
|
26
|
+
try data.write(to: tmp)
|
|
27
|
+
if FileManager.default.fileExists(atPath: dest.path) {
|
|
28
|
+
_ = try FileManager.default.replaceItemAt(dest, withItemAt: tmp)
|
|
29
|
+
} else {
|
|
30
|
+
try FileManager.default.moveItem(at: tmp, to: dest)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Returns the NitroPlayer subdirectory inside Application Support.
|
|
35
|
+
/// Uses `FileManager` APIs — never hardcodes the UUID-based container path
|
|
36
|
+
/// so this resolves correctly regardless of which device or simulator the
|
|
37
|
+
/// app runs on.
|
|
38
|
+
private static func storageDirectory() -> URL {
|
|
39
|
+
let appSupport = FileManager.default.urls(
|
|
40
|
+
for: .applicationSupportDirectory, in: .userDomainMask
|
|
41
|
+
).first!
|
|
42
|
+
return appSupport.appendingPathComponent("NitroPlayer", isDirectory: true)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -35,6 +35,7 @@ export interface TrackPlayer extends HybridObject<{
|
|
|
35
35
|
getActualQueue(): Promise<TrackItem[]>;
|
|
36
36
|
getState(): Promise<PlayerState>;
|
|
37
37
|
setRepeatMode(mode: RepeatMode): boolean;
|
|
38
|
+
getRepeatMode(): RepeatMode;
|
|
38
39
|
configure(config: PlayerConfig): void;
|
|
39
40
|
onChangeTrack(callback: (track: TrackItem, reason?: Reason) => void): void;
|
|
40
41
|
onPlaybackStateChange(callback: (state: TrackPlayerState, reason?: Reason) => void): void;
|
|
@@ -19,7 +19,7 @@ export interface Playlist {
|
|
|
19
19
|
}
|
|
20
20
|
export type QueueOperation = 'add' | 'remove' | 'clear' | 'update';
|
|
21
21
|
export type TrackPlayerState = 'playing' | 'paused' | 'stopped';
|
|
22
|
-
export type Reason = 'user_action' | 'skip' | 'end' | 'error';
|
|
22
|
+
export type Reason = 'user_action' | 'skip' | 'end' | 'error' | 'repeat';
|
|
23
23
|
export interface PlayerState {
|
|
24
24
|
currentTrack: TrackItem | null;
|
|
25
25
|
currentPosition: number;
|
|
@@ -215,6 +215,11 @@ namespace margelo::nitro::nitroplayer {
|
|
|
215
215
|
auto __result = method(_javaPart, JRepeatMode::fromCpp(mode));
|
|
216
216
|
return static_cast<bool>(__result);
|
|
217
217
|
}
|
|
218
|
+
RepeatMode JHybridTrackPlayerSpec::getRepeatMode() {
|
|
219
|
+
static const auto method = javaClassStatic()->getMethod<jni::local_ref<JRepeatMode>()>("getRepeatMode");
|
|
220
|
+
auto __result = method(_javaPart);
|
|
221
|
+
return __result->toCpp();
|
|
222
|
+
}
|
|
218
223
|
void JHybridTrackPlayerSpec::configure(const PlayerConfig& config) {
|
|
219
224
|
static const auto method = javaClassStatic()->getMethod<void(jni::alias_ref<JPlayerConfig> /* config */)>("configure");
|
|
220
225
|
method(_javaPart, JPlayerConfig::fromCpp(config));
|
|
@@ -66,6 +66,7 @@ namespace margelo::nitro::nitroplayer {
|
|
|
66
66
|
std::shared_ptr<Promise<std::vector<TrackItem>>> getActualQueue() override;
|
|
67
67
|
std::shared_ptr<Promise<PlayerState>> getState() override;
|
|
68
68
|
bool setRepeatMode(RepeatMode mode) override;
|
|
69
|
+
RepeatMode getRepeatMode() override;
|
|
69
70
|
void configure(const PlayerConfig& config) override;
|
|
70
71
|
void onChangeTrack(const std::function<void(const TrackItem& /* track */, std::optional<Reason> /* reason */)>& callback) override;
|
|
71
72
|
void onPlaybackStateChange(const std::function<void(TrackPlayerState /* state */, std::optional<Reason> /* reason */)>& callback) override;
|
|
@@ -45,6 +45,7 @@ namespace margelo::nitro::nitroplayer {
|
|
|
45
45
|
static const auto fieldSKIP = clazz->getStaticField<JReason>("SKIP");
|
|
46
46
|
static const auto fieldEND = clazz->getStaticField<JReason>("END");
|
|
47
47
|
static const auto fieldERROR = clazz->getStaticField<JReason>("ERROR");
|
|
48
|
+
static const auto fieldREPEAT = clazz->getStaticField<JReason>("REPEAT");
|
|
48
49
|
|
|
49
50
|
switch (value) {
|
|
50
51
|
case Reason::USER_ACTION:
|
|
@@ -55,6 +56,8 @@ namespace margelo::nitro::nitroplayer {
|
|
|
55
56
|
return clazz->getStaticFieldValue(fieldEND);
|
|
56
57
|
case Reason::ERROR:
|
|
57
58
|
return clazz->getStaticFieldValue(fieldERROR);
|
|
59
|
+
case Reason::REPEAT:
|
|
60
|
+
return clazz->getStaticFieldValue(fieldREPEAT);
|
|
58
61
|
default:
|
|
59
62
|
std::string stringValue = std::to_string(static_cast<int>(value));
|
|
60
63
|
throw std::invalid_argument("Invalid enum value (" + stringValue + "!");
|
package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt
CHANGED
|
@@ -94,6 +94,10 @@ abstract class HybridTrackPlayerSpec: HybridObject() {
|
|
|
94
94
|
@Keep
|
|
95
95
|
abstract fun setRepeatMode(mode: RepeatMode): Boolean
|
|
96
96
|
|
|
97
|
+
@DoNotStrip
|
|
98
|
+
@Keep
|
|
99
|
+
abstract fun getRepeatMode(): RepeatMode
|
|
100
|
+
|
|
97
101
|
@DoNotStrip
|
|
98
102
|
@Keep
|
|
99
103
|
abstract fun configure(config: PlayerConfig): Unit
|
|
@@ -60,6 +60,8 @@ namespace margelo::nitro::nitroplayer { enum class PresetType; }
|
|
|
60
60
|
namespace margelo::nitro::nitroplayer { enum class QueueOperation; }
|
|
61
61
|
// Forward declaration of `Reason` to properly resolve imports.
|
|
62
62
|
namespace margelo::nitro::nitroplayer { enum class Reason; }
|
|
63
|
+
// Forward declaration of `RepeatMode` to properly resolve imports.
|
|
64
|
+
namespace margelo::nitro::nitroplayer { enum class RepeatMode; }
|
|
63
65
|
// Forward declaration of `StorageLocation` to properly resolve imports.
|
|
64
66
|
namespace margelo::nitro::nitroplayer { enum class StorageLocation; }
|
|
65
67
|
// Forward declaration of `TrackItem` to properly resolve imports.
|
|
@@ -106,6 +108,7 @@ namespace NitroPlayer { class HybridTrackPlayerSpec_cxx; }
|
|
|
106
108
|
#include "PresetType.hpp"
|
|
107
109
|
#include "QueueOperation.hpp"
|
|
108
110
|
#include "Reason.hpp"
|
|
111
|
+
#include "RepeatMode.hpp"
|
|
109
112
|
#include "StorageLocation.hpp"
|
|
110
113
|
#include "TrackItem.hpp"
|
|
111
114
|
#include "TrackPlayerState.hpp"
|
|
@@ -1452,5 +1455,14 @@ namespace margelo::nitro::nitroplayer::bridge::swift {
|
|
|
1452
1455
|
inline Result_std__shared_ptr_Promise_PlayerState___ create_Result_std__shared_ptr_Promise_PlayerState___(const std::exception_ptr& error) noexcept {
|
|
1453
1456
|
return Result<std::shared_ptr<Promise<PlayerState>>>::withError(error);
|
|
1454
1457
|
}
|
|
1458
|
+
|
|
1459
|
+
// pragma MARK: Result<RepeatMode>
|
|
1460
|
+
using Result_RepeatMode_ = Result<RepeatMode>;
|
|
1461
|
+
inline Result_RepeatMode_ create_Result_RepeatMode_(RepeatMode value) noexcept {
|
|
1462
|
+
return Result<RepeatMode>::withValue(std::move(value));
|
|
1463
|
+
}
|
|
1464
|
+
inline Result_RepeatMode_ create_Result_RepeatMode_(const std::exception_ptr& error) noexcept {
|
|
1465
|
+
return Result<RepeatMode>::withError(error);
|
|
1466
|
+
}
|
|
1455
1467
|
|
|
1456
1468
|
} // namespace margelo::nitro::nitroplayer::bridge::swift
|
|
@@ -173,6 +173,14 @@ namespace margelo::nitro::nitroplayer {
|
|
|
173
173
|
auto __value = std::move(__result.value());
|
|
174
174
|
return __value;
|
|
175
175
|
}
|
|
176
|
+
inline RepeatMode getRepeatMode() override {
|
|
177
|
+
auto __result = _swiftPart.getRepeatMode();
|
|
178
|
+
if (__result.hasError()) [[unlikely]] {
|
|
179
|
+
std::rethrow_exception(__result.error());
|
|
180
|
+
}
|
|
181
|
+
auto __value = std::move(__result.value());
|
|
182
|
+
return __value;
|
|
183
|
+
}
|
|
176
184
|
inline void configure(const PlayerConfig& config) override {
|
|
177
185
|
auto __result = _swiftPart.configure(std::forward<decltype(config)>(config));
|
|
178
186
|
if (__result.hasError()) [[unlikely]] {
|
|
@@ -26,6 +26,7 @@ public protocol HybridTrackPlayerSpec_protocol: HybridObject {
|
|
|
26
26
|
func getActualQueue() throws -> Promise<[TrackItem]>
|
|
27
27
|
func getState() throws -> Promise<PlayerState>
|
|
28
28
|
func setRepeatMode(mode: RepeatMode) throws -> Bool
|
|
29
|
+
func getRepeatMode() throws -> RepeatMode
|
|
29
30
|
func configure(config: PlayerConfig) throws -> Void
|
|
30
31
|
func onChangeTrack(callback: @escaping (_ track: TrackItem, _ reason: Reason?) -> Void) throws -> Void
|
|
31
32
|
func onPlaybackStateChange(callback: @escaping (_ state: TrackPlayerState, _ reason: Reason?) -> Void) throws -> Void
|