react-native-nitro-player 0.7.0 → 0.7.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +9 -13
  2. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +45 -90
  3. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +48 -182
  4. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +21 -77
  5. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +55 -104
  6. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +113 -123
  7. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ExoPlayerCore.kt +82 -0
  8. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ListenerRegistry.kt +48 -0
  9. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerAndroidAuto.kt +62 -0
  10. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +153 -1887
  11. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +122 -0
  12. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerNotify.kt +44 -0
  13. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +162 -0
  14. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueue.kt +165 -0
  15. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +161 -0
  16. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +28 -0
  17. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerTempQueue.kt +121 -0
  18. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +98 -0
  19. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +27 -18
  20. package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +11 -58
  21. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +13 -30
  22. package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +102 -162
  23. package/ios/HybridDownloadManager.swift +32 -26
  24. package/ios/HybridEqualizer.swift +48 -35
  25. package/ios/HybridTrackPlayer.swift +127 -102
  26. package/ios/core/ListenerRegistry.swift +60 -0
  27. package/ios/core/TrackPlayerCore.swift +130 -2356
  28. package/ios/core/TrackPlayerListener.swift +395 -0
  29. package/ios/core/TrackPlayerNotify.swift +52 -0
  30. package/ios/core/TrackPlayerPlayback.swift +274 -0
  31. package/ios/core/TrackPlayerQueue.swift +212 -0
  32. package/ios/core/TrackPlayerQueueBuild.swift +482 -0
  33. package/ios/core/TrackPlayerTempQueue.swift +167 -0
  34. package/ios/core/TrackPlayerUrlLoader.swift +169 -0
  35. package/ios/equalizer/EqualizerCore.swift +24 -89
  36. package/ios/media/MediaSessionManager.swift +32 -49
  37. package/ios/playlist/PlaylistManager.swift +2 -9
  38. package/ios/queue/HybridPlayerQueue.swift +69 -66
  39. package/lib/hooks/useDownloadedTracks.js +16 -13
  40. package/lib/hooks/useEqualizer.d.ts +4 -4
  41. package/lib/hooks/useEqualizer.js +12 -12
  42. package/lib/hooks/useEqualizerPresets.d.ts +3 -3
  43. package/lib/hooks/useEqualizerPresets.js +12 -18
  44. package/lib/specs/AndroidAutoMediaLibrary.nitro.d.ts +2 -2
  45. package/lib/specs/AudioDevices.nitro.d.ts +2 -2
  46. package/lib/specs/DownloadManager.nitro.d.ts +10 -10
  47. package/lib/specs/Equalizer.nitro.d.ts +9 -9
  48. package/lib/specs/TrackPlayer.nitro.d.ts +38 -16
  49. package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +2 -0
  50. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__std__vector_TrackItem_.hpp +122 -0
  51. package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.cpp +31 -6
  52. package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.hpp +2 -2
  53. package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.cpp +16 -3
  54. package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.hpp +1 -1
  55. package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.cpp +154 -44
  56. package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.hpp +10 -10
  57. package/nitrogen/generated/android/c++/JHybridEqualizerSpec.cpp +130 -34
  58. package/nitrogen/generated/android/c++/JHybridEqualizerSpec.hpp +9 -9
  59. package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +115 -24
  60. package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.hpp +8 -8
  61. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +243 -24
  62. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +16 -8
  63. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_TrackItem__std__vector_TrackItem_.kt +80 -0
  64. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrarySpec.kt +3 -2
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAudioDevicesSpec.kt +2 -1
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridDownloadManagerSpec.kt +10 -10
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridEqualizerSpec.kt +10 -9
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridPlayerQueueSpec.kt +9 -8
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +45 -8
  70. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +74 -18
  71. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +380 -151
  72. package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.hpp +10 -10
  73. package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.hpp +12 -9
  74. package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.hpp +23 -8
  75. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +82 -8
  76. package/nitrogen/generated/ios/swift/Func_void_EqualizerState.swift +46 -0
  77. package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__DownloadedPlaylist_.swift +58 -0
  78. package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__DownloadedTrack_.swift +58 -0
  79. package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__std__string_.swift +58 -0
  80. package/nitrogen/generated/ios/swift/Func_void_std__vector_DownloadedPlaylist_.swift +46 -0
  81. package/nitrogen/generated/ios/swift/Func_void_std__vector_DownloadedTrack_.swift +46 -0
  82. package/nitrogen/generated/ios/swift/Func_void_std__vector_EqualizerBand_.swift +5 -5
  83. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__std__vector_TrackItem_.swift +46 -0
  84. package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec.swift +10 -10
  85. package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec_cxx.swift +141 -71
  86. package/nitrogen/generated/ios/swift/HybridEqualizerSpec.swift +9 -9
  87. package/nitrogen/generated/ios/swift/HybridEqualizerSpec_cxx.swift +105 -41
  88. package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec.swift +8 -8
  89. package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec_cxx.swift +95 -32
  90. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +16 -8
  91. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +267 -32
  92. package/nitrogen/generated/shared/c++/HybridAndroidAutoMediaLibrarySpec.hpp +3 -2
  93. package/nitrogen/generated/shared/c++/HybridAudioDevicesSpec.hpp +2 -1
  94. package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.hpp +10 -10
  95. package/nitrogen/generated/shared/c++/HybridEqualizerSpec.hpp +10 -9
  96. package/nitrogen/generated/shared/c++/HybridPlayerQueueSpec.hpp +9 -8
  97. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +8 -0
  98. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +16 -8
  99. package/package.json +1 -1
  100. package/src/hooks/useDownloadedTracks.ts +17 -13
  101. package/src/hooks/useEqualizer.ts +16 -16
  102. package/src/hooks/useEqualizerPresets.ts +15 -21
  103. package/src/specs/AndroidAutoMediaLibrary.nitro.ts +2 -2
  104. package/src/specs/AudioDevices.nitro.ts +2 -2
  105. package/src/specs/DownloadManager.nitro.ts +10 -10
  106. package/src/specs/Equalizer.nitro.ts +9 -9
  107. package/src/specs/TrackPlayer.nitro.ts +52 -16
@@ -12,6 +12,7 @@ final class HybridEqualizer: HybridEqualizerSpec {
12
12
  // MARK: - Properties
13
13
 
14
14
  private let core: EqualizerCore
15
+ private var listenerIds: [(String, Int64)] = []
15
16
 
16
17
  // MARK: - Initialization
17
18
 
@@ -22,90 +23,102 @@ final class HybridEqualizer: HybridEqualizerSpec {
22
23
 
23
24
  // MARK: - Enable/Disable
24
25
 
25
- func setEnabled(enabled: Bool) throws -> Bool {
26
- return core.setEnabled(enabled)
26
+ func setEnabled(enabled: Bool) throws -> Promise<Void> {
27
+ Promise.async { _ = self.core.setEnabled(enabled) }
27
28
  }
28
29
 
29
30
  func isEnabled() throws -> Bool {
30
- return core.isEnabled()
31
+ core.isEnabled()
31
32
  }
32
33
 
33
34
  // MARK: - Band Control
34
35
 
35
- func getBands() throws -> [EqualizerBand] {
36
- return core.getBands()
36
+ func getBands() throws -> Promise<[EqualizerBand]> {
37
+ Promise.async { self.core.getBands() }
37
38
  }
38
39
 
39
- func setBandGain(bandIndex: Double, gainDb: Double) throws -> Bool {
40
- return core.setBandGain(bandIndex: Int(bandIndex), gainDb: gainDb)
40
+ func setBandGain(bandIndex: Double, gainDb: Double) throws -> Promise<Void> {
41
+ Promise.async { _ = self.core.setBandGain(bandIndex: Int(bandIndex), gainDb: gainDb) }
41
42
  }
42
43
 
43
- func setAllBandGains(gains: [Double]) throws -> Bool {
44
- return core.setAllBandGains(gains)
44
+ func setAllBandGains(gains: [Double]) throws -> Promise<Void> {
45
+ Promise.async { _ = self.core.setAllBandGains(gains) }
45
46
  }
46
47
 
47
48
  func getBandRange() throws -> GainRange {
48
- return core.getBandRange()
49
+ core.getBandRange()
49
50
  }
50
51
 
51
52
  // MARK: - Presets
52
53
 
53
54
  func getPresets() throws -> [EqualizerPreset] {
54
- return core.getPresets()
55
+ core.getPresets()
55
56
  }
56
57
 
57
58
  func getBuiltInPresets() throws -> [EqualizerPreset] {
58
- return core.getBuiltInPresets()
59
+ core.getBuiltInPresets()
59
60
  }
60
61
 
61
62
  func getCustomPresets() throws -> [EqualizerPreset] {
62
- return core.getCustomPresets()
63
+ core.getCustomPresets()
63
64
  }
64
65
 
65
- func applyPreset(presetName: String) throws -> Bool {
66
- return core.applyPreset(presetName)
66
+ func applyPreset(presetName: String) throws -> Promise<Void> {
67
+ Promise.async { _ = self.core.applyPreset(presetName) }
67
68
  }
68
69
 
69
70
  func getCurrentPresetName() throws -> Variant_NullType_String {
70
71
  if let name = core.getCurrentPresetName() {
71
72
  return .second(name)
72
- } else {
73
- return .first(NullType.null)
74
73
  }
74
+ return .first(NullType.null)
75
75
  }
76
76
 
77
- func saveCustomPreset(name: String) throws -> Bool {
78
- return core.saveCustomPreset(name)
77
+ func saveCustomPreset(name: String) throws -> Promise<Void> {
78
+ Promise.async { _ = self.core.saveCustomPreset(name) }
79
79
  }
80
80
 
81
- func deleteCustomPreset(name: String) throws -> Bool {
82
- return core.deleteCustomPreset(name)
81
+ func deleteCustomPreset(name: String) throws -> Promise<Void> {
82
+ Promise.async { _ = self.core.deleteCustomPreset(name) }
83
83
  }
84
84
 
85
85
  // MARK: - State
86
86
 
87
- func getState() throws -> EqualizerState {
88
- return core.getState()
87
+ func getState() throws -> Promise<EqualizerState> {
88
+ Promise.async { self.core.getState() }
89
89
  }
90
90
 
91
- func reset() throws {
92
- core.reset()
91
+ func reset() throws -> Promise<Void> {
92
+ Promise.async { self.core.reset() }
93
93
  }
94
94
 
95
- // MARK: - Event Callbacks
95
+ // MARK: - Event listeners (v2 — store IDs for cleanup)
96
96
 
97
- func onEnabledChange(callback: @escaping (Bool) -> Void) throws {
98
- NitroPlayerLogger.log("HybridEqualizer", "onEnabledChange callback registered")
99
- core.addOnEnabledChangeListener(owner: self, callback)
97
+ func onEnabledChange(callback: @escaping (_ enabled: Bool) -> Void) throws {
98
+ let id = core.addOnEnabledChangeListener(callback)
99
+ listenerIds.append(("onEnabledChange", id))
100
100
  }
101
101
 
102
- func onBandChange(callback: @escaping ([EqualizerBand]) -> Void) throws {
103
- NitroPlayerLogger.log("HybridEqualizer", "onBandChange callback registered")
104
- core.addOnBandChangeListener(owner: self, callback)
102
+ func onBandChange(callback: @escaping (_ bands: [EqualizerBand]) -> Void) throws {
103
+ let id = core.addOnBandChangeListener(callback)
104
+ listenerIds.append(("onBandChange", id))
105
105
  }
106
106
 
107
- func onPresetChange(callback: @escaping (Variant_NullType_String?) -> Void) throws {
108
- NitroPlayerLogger.log("HybridEqualizer", "onPresetChange callback registered")
109
- core.addOnPresetChangeListener(owner: self, callback)
107
+ func onPresetChange(callback: @escaping (_ presetName: Variant_NullType_String?) -> Void) throws {
108
+ let id = core.addOnPresetChangeListener(callback)
109
+ listenerIds.append(("onPresetChange", id))
110
+ }
111
+
112
+ // MARK: - Cleanup
113
+
114
+ deinit {
115
+ for (type, id) in listenerIds {
116
+ switch type {
117
+ case "onEnabledChange": _ = core.removeOnEnabledChangeListener(id: id)
118
+ case "onBandChange": _ = core.removeOnBandChangeListener(id: id)
119
+ case "onPresetChange": _ = core.removeOnPresetChangeListener(id: id)
120
+ default: break
121
+ }
122
+ }
110
123
  }
111
124
  }
@@ -15,6 +15,9 @@ final class HybridTrackPlayer: HybridTrackPlayerSpec {
15
15
 
16
16
  private let core: TrackPlayerCore
17
17
 
18
+ /// Stable listener IDs for cleanup on deinit
19
+ private var listenerIds: [(String, Int64)] = []
20
+
18
21
  // MARK: - Initialization
19
22
 
20
23
  override init() {
@@ -22,174 +25,196 @@ final class HybridTrackPlayer: HybridTrackPlayerSpec {
22
25
  super.init()
23
26
  }
24
27
 
25
- // MARK: - Playback Control
28
+ // MARK: - Playback Control (async Promise<Void>)
29
+
30
+ func play() throws -> Promise<Void> {
31
+ Promise.async { await self.core.play() }
32
+ }
33
+
34
+ func pause() throws -> Promise<Void> {
35
+ Promise.async { await self.core.pause() }
36
+ }
37
+
38
+ func seek(position: Double) throws -> Promise<Void> {
39
+ Promise.async { await self.core.seek(position: position) }
40
+ }
26
41
 
27
- func play() throws {
28
- core.play()
42
+ func skipToNext() throws -> Promise<Void> {
43
+ Promise.async { await self.core.skipToNext() }
29
44
  }
30
45
 
31
- func pause() throws {
32
- core.pause()
46
+ func skipToPrevious() throws -> Promise<Void> {
47
+ Promise.async { await self.core.skipToPrevious() }
33
48
  }
34
49
 
35
50
  func playSong(songId: String, fromPlaylist: String?) throws -> Promise<Void> {
36
- return Promise.async {
37
- self.core.playSong(songId: songId, fromPlaylist: fromPlaylist)
38
- }
51
+ Promise.async { await self.core.playSong(songId: songId, fromPlaylist: fromPlaylist) }
39
52
  }
40
53
 
41
- func skipToNext() throws {
42
- core.skipToNext()
54
+ func skipToIndex(index: Double) throws -> Promise<Bool> {
55
+ Promise.async { await self.core.skipToIndex(index: Int(index)) }
43
56
  }
44
57
 
45
- func skipToPrevious() throws {
46
- core.skipToPrevious()
58
+ // MARK: - Repeat / Volume / Config
59
+
60
+ func setRepeatMode(mode: RepeatMode) throws -> Promise<Void> {
61
+ Promise.async { await self.core.setRepeatMode(mode: mode) }
47
62
  }
48
63
 
49
- func seek(position: Double) throws {
50
- core.seek(position: position)
64
+ func getRepeatMode() throws -> RepeatMode {
65
+ core.getRepeatMode()
51
66
  }
52
67
 
53
- func addToUpNext(trackId: String) throws -> Promise<Void> {
54
- return Promise.async {
55
- self.core.addToUpNext(trackId: trackId)
56
- }
68
+ func setVolume(volume: Double) throws -> Promise<Void> {
69
+ Promise.async { await self.core.setVolume(volume: volume) }
57
70
  }
58
71
 
59
- func playNext(trackId: String) throws -> Promise<Void> {
60
- return Promise.async {
61
- self.core.playNext(trackId: trackId)
72
+ func configure(config: PlayerConfig) throws -> Promise<Void> {
73
+ Promise.async {
74
+ await self.core.configure(
75
+ androidAutoEnabled: config.androidAutoEnabled,
76
+ carPlayEnabled: config.carPlayEnabled,
77
+ showInNotification: config.showInNotification,
78
+ lookaheadCount: config.lookaheadCount.map { Int($0) }
79
+ )
62
80
  }
63
81
  }
64
82
 
83
+ // MARK: - Queue / State reads
84
+
65
85
  func getActualQueue() throws -> Promise<[TrackItem]> {
66
- return Promise.async {
67
- return self.core.getActualQueue()
68
- }
86
+ Promise.async { await self.core.getActualQueue() }
69
87
  }
70
88
 
71
89
  func getState() throws -> Promise<PlayerState> {
72
- return Promise.async {
73
- return self.core.getState()
74
- }
90
+ Promise.async { await self.core.getState() }
75
91
  }
76
92
 
77
- func setRepeatMode(mode: RepeatMode) throws -> Bool {
78
- return core.setRepeatMode(mode: mode)
93
+ func getCurrentTrackIndex() throws -> Promise<Double> {
94
+ Promise.async { Double(await self.core.getCurrentTrackIndex()) }
79
95
  }
80
96
 
81
- func getRepeatMode() throws -> RepeatMode {
82
- return core.getRepeatMode()
97
+ // MARK: - URL updates / lazy loading
98
+
99
+ func updateTracks(tracks: [TrackItem]) throws -> Promise<Void> {
100
+ Promise.async { await self.core.updateTracks(tracks: tracks) }
101
+ }
102
+
103
+ func getTracksById(trackIds: [String]) throws -> Promise<[TrackItem]> {
104
+ Promise.async { await self.core.getTracksById(trackIds: trackIds) }
83
105
  }
84
106
 
85
- // MARK: - Configuration
107
+ func getTracksNeedingUrls() throws -> Promise<[TrackItem]> {
108
+ Promise.async { await self.core.getTracksNeedingUrls() }
109
+ }
86
110
 
87
- func configure(config: PlayerConfig) throws {
88
- core.configure(
89
- androidAutoEnabled: config.androidAutoEnabled,
90
- carPlayEnabled: config.carPlayEnabled,
91
- showInNotification: config.showInNotification,
92
- lookaheadCount: config.lookaheadCount.map { Int($0) }
93
- )
111
+ func getNextTracks(count: Double) throws -> Promise<[TrackItem]> {
112
+ Promise.async { await self.core.getNextTracks(count: Int(count)) }
94
113
  }
95
114
 
96
- // MARK: - Event Callbacks
115
+ // MARK: - Playback speed
116
+
117
+ func setPlaybackSpeed(speed: Double) throws -> Promise<Void> {
118
+ Promise.async { await self.core.setPlaybackSpeed(speed) }
119
+ }
97
120
 
98
- func onChangeTrack(callback: @escaping (TrackItem, Reason?) -> Void) throws {
99
- NitroPlayerLogger.log("HybridTrackPlayer", "onChangeTrack callback registered")
100
- core.addOnChangeTrackListener(owner: self, callback)
121
+ func getPlaybackSpeed() throws -> Promise<Double> {
122
+ Promise.async { await self.core.getPlaybackSpeed() }
101
123
  }
102
124
 
103
- func onPlaybackStateChange(callback: @escaping (TrackPlayerState, Reason?) -> Void) throws {
104
- NitroPlayerLogger.log("HybridTrackPlayer", "onPlaybackStateChange callback registered")
105
- core.addOnPlaybackStateChangeListener(owner: self, callback)
125
+ // MARK: - Temporary queue v2
126
+
127
+ func addToUpNext(trackId: String) throws -> Promise<Void> {
128
+ Promise.async { try await self.core.addToUpNext(trackId: trackId) }
106
129
  }
107
130
 
108
- func onSeek(callback: @escaping (Double, Double) -> Void) throws {
109
- NitroPlayerLogger.log("HybridTrackPlayer", "onSeek callback registered")
110
- core.addOnSeekListener(owner: self, callback)
131
+ func playNext(trackId: String) throws -> Promise<Void> {
132
+ Promise.async { try await self.core.playNext(trackId: trackId) }
111
133
  }
112
134
 
113
- func onPlaybackProgressChange(callback: @escaping (Double, Double, Bool?) -> Void) throws {
114
- NitroPlayerLogger.log("HybridTrackPlayer", "onPlaybackProgressChange callback registered")
115
- core.addOnPlaybackProgressChangeListener(owner: self, callback)
135
+ func removeFromPlayNext(trackId: String) throws -> Promise<Bool> {
136
+ Promise.async { await self.core.removeFromPlayNext(trackId: trackId) }
116
137
  }
117
138
 
118
- // MARK: - Android Auto (iOS No-op)
139
+ func removeFromUpNext(trackId: String) throws -> Promise<Bool> {
140
+ Promise.async { await self.core.removeFromUpNext(trackId: trackId) }
141
+ }
119
142
 
120
- /// iOS doesn't support Android Auto, so this is a no-op
121
- /// - Parameter callback: Callback that will never be invoked
122
- func onAndroidAutoConnectionChange(callback: @escaping (Bool) -> Void) throws {
123
- // iOS doesn't have Android Auto, so this is a no-op
143
+ func clearPlayNext() throws -> Promise<Void> {
144
+ Promise.async { await self.core.clearPlayNext() }
124
145
  }
125
146
 
126
- /// iOS doesn't support Android Auto, always returns false
127
- /// - Returns: Always returns false on iOS
128
- func isAndroidAutoConnected() throws -> Bool {
129
- return false
147
+ func clearUpNext() throws -> Promise<Void> {
148
+ Promise.async { await self.core.clearUpNext() }
130
149
  }
131
150
 
132
- func skipToIndex(index: Double) throws -> Promise<Bool> {
133
- return Promise.async {
134
- return self.core.skipToIndex(index: Int(index))
135
- }
151
+ func reorderTemporaryTrack(trackId: String, newIndex: Double) throws -> Promise<Bool> {
152
+ Promise.async { await self.core.reorderTemporaryTrack(trackId: trackId, newIndex: Int(newIndex)) }
136
153
  }
137
154
 
138
- // MARK: - Volume Control
155
+ func getPlayNextQueue() throws -> Promise<[TrackItem]> {
156
+ Promise.async { await self.core.getPlayNextQueue() }
157
+ }
139
158
 
140
- func setVolume(volume: Double) throws -> Bool {
141
- return core.setVolume(volume: volume)
159
+ func getUpNextQueue() throws -> Promise<[TrackItem]> {
160
+ Promise.async { await self.core.getUpNextQueue() }
142
161
  }
143
162
 
144
- // MARK: - Lazy URL Loading
163
+ // MARK: - Android Auto (iOS no-op)
145
164
 
146
- func updateTracks(tracks: [TrackItem]) throws -> Promise<Void> {
147
- return Promise.async {
148
- self.core.updateTracks(tracks: tracks)
149
- }
165
+ func onAndroidAutoConnectionChange(callback: @escaping (Bool) -> Void) throws {
166
+ // No-op on iOS
150
167
  }
151
168
 
152
- func getTracksById(trackIds: [String]) throws -> Promise<[TrackItem]> {
153
- return Promise.async {
154
- return self.core.getTracksById(trackIds: trackIds)
155
- }
169
+ func isAndroidAutoConnected() throws -> Bool { false }
170
+
171
+ // MARK: - Event listeners (v2 — store IDs for cleanup)
172
+
173
+ func onChangeTrack(callback: @escaping (_ track: TrackItem, _ reason: Reason?) -> Void) throws {
174
+ let id = core.addOnChangeTrackListener(callback)
175
+ listenerIds.append(("onChangeTrack", id))
156
176
  }
157
177
 
158
- func getTracksNeedingUrls() throws -> Promise<[TrackItem]> {
159
- return Promise.async {
160
- return self.core.getTracksNeedingUrls()
161
- }
178
+ func onPlaybackStateChange(callback: @escaping (_ state: TrackPlayerState, _ reason: Reason?) -> Void) throws {
179
+ let id = core.addOnPlaybackStateChangeListener(callback)
180
+ listenerIds.append(("onPlaybackStateChange", id))
162
181
  }
163
182
 
164
- func getNextTracks(count: Double) throws -> Promise<[TrackItem]> {
165
- return Promise.async {
166
- return self.core.getNextTracks(count: Int(count))
167
- }
183
+ func onSeek(callback: @escaping (_ position: Double, _ totalDuration: Double) -> Void) throws {
184
+ let id = core.addOnSeekListener(callback)
185
+ listenerIds.append(("onSeek", id))
168
186
  }
169
187
 
170
- func getCurrentTrackIndex() throws -> Promise<Double> {
171
- return Promise.async {
172
- return Double(self.core.getCurrentTrackIndex())
173
- }
188
+ func onPlaybackProgressChange(callback: @escaping (_ position: Double, _ totalDuration: Double, _ isManuallySeeked: Bool?) -> Void) throws {
189
+ let id = core.addOnProgressListener(callback)
190
+ listenerIds.append(("onPlaybackProgressChange", id))
174
191
  }
175
192
 
176
- func onTracksNeedUpdate(callback: @escaping ([TrackItem], Double) -> Void) throws {
177
- core.addOnTracksNeedUpdateListener { tracks, lookahead in
193
+ func onTracksNeedUpdate(callback: @escaping (_ tracks: [TrackItem], _ lookahead: Double) -> Void) throws {
194
+ let id = core.addOnTracksNeedUpdateListener { tracks, lookahead in
178
195
  callback(tracks, Double(lookahead))
179
196
  }
197
+ listenerIds.append(("onTracksNeedUpdate", id))
180
198
  }
181
-
182
- func setPlaybackSpeed(speed: Double) throws -> Promise<Void> {
183
- Promise.async{
184
- self.core.setPlaybackSpeed(speed)
185
- }
186
-
199
+
200
+ func onTemporaryQueueChange(callback: @escaping (_ playNextQueue: [TrackItem], _ upNextQueue: [TrackItem]) -> Void) throws {
201
+ let id = core.addOnTemporaryQueueChangeListener(callback)
202
+ listenerIds.append(("onTemporaryQueueChange", id))
187
203
  }
188
-
189
- func getPlaybackSpeed() throws -> Promise<Double> {
190
- return Promise.async{
191
- return self.core.getPlaybackSpeed()
204
+
205
+ // MARK: - Cleanup
206
+
207
+ deinit {
208
+ for (type, id) in listenerIds {
209
+ switch type {
210
+ case "onChangeTrack": _ = core.removeOnChangeTrackListener(id: id)
211
+ case "onPlaybackStateChange": _ = core.removeOnPlaybackStateChangeListener(id: id)
212
+ case "onSeek": _ = core.removeOnSeekListener(id: id)
213
+ case "onPlaybackProgressChange":_ = core.removeOnProgressListener(id: id)
214
+ case "onTracksNeedUpdate": _ = core.removeOnTracksNeedUpdateListener(id: id)
215
+ case "onTemporaryQueueChange": _ = core.removeOnTemporaryQueueChangeListener(id: id)
216
+ default: break
217
+ }
192
218
  }
193
-
194
219
  }
195
220
  }
@@ -0,0 +1,60 @@
1
+ //
2
+ // ListenerRegistry.swift
3
+ // NitroPlayer
4
+ //
5
+ // Created by Ritesh Shukla on 25/03/26.
6
+ //
7
+
8
+ import Foundation
9
+
10
+ final class ListenerRegistry<T> {
11
+ private struct Entry {
12
+ let id: Int64
13
+ let callback: T
14
+ }
15
+
16
+ private let queue = DispatchQueue(label: "com.nitroplayer.registry")
17
+ private var entries: [Entry] = []
18
+ private var nextId: Int64 = 0
19
+
20
+ /// Register a callback and return its stable ID for later removal.
21
+ @discardableResult
22
+ func add(_ callback: T) -> Int64 {
23
+ var id: Int64 = 0
24
+ queue.sync {
25
+ nextId += 1
26
+ id = nextId
27
+ entries.append(Entry(id: id, callback: callback))
28
+ }
29
+ return id
30
+ }
31
+
32
+ /// Remove the callback with the given ID. Returns true if found.
33
+ @discardableResult
34
+ func remove(id: Int64) -> Bool {
35
+ var found = false
36
+ queue.sync {
37
+ if let idx = entries.firstIndex(where: { $0.id == id }) {
38
+ entries.remove(at: idx)
39
+ found = true
40
+ }
41
+ }
42
+ return found
43
+ }
44
+
45
+ /// Remove all registered callbacks.
46
+ func clear() {
47
+ queue.sync { entries.removeAll() }
48
+ }
49
+
50
+ /// Invoke action for every registered callback (snapshot iteration — safe under mutation).
51
+ func forEach(_ action: (T) -> Void) {
52
+ let snap = queue.sync { entries.map(\.callback) }
53
+ snap.forEach(action)
54
+ }
55
+
56
+ /// True when no callbacks are registered.
57
+ var isEmpty: Bool {
58
+ queue.sync { entries.isEmpty }
59
+ }
60
+ }