react-native-nitro-player 0.0.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 (191) hide show
  1. package/NitroPlayer.podspec +31 -0
  2. package/README.md +610 -0
  3. package/android/CMakeLists.txt +29 -0
  4. package/android/build.gradle +147 -0
  5. package/android/fix-prefab.gradle +51 -0
  6. package/android/gradle.properties +5 -0
  7. package/android/src/main/AndroidManifest.xml +2 -0
  8. package/android/src/main/cpp/cpp-adapter.cpp +7 -0
  9. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +29 -0
  10. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +116 -0
  11. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +167 -0
  12. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +93 -0
  13. package/android/src/main/java/com/margelo/nitro/nitroplayer/NitroPlayerPackage.kt +21 -0
  14. package/android/src/main/java/com/margelo/nitro/nitroplayer/connection/AndroidAutoConnectionDetector.kt +171 -0
  15. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +639 -0
  16. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaBrowserService.kt +352 -0
  17. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaLibrary.kt +58 -0
  18. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaLibraryManager.kt +77 -0
  19. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaLibraryParser.kt +73 -0
  20. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +506 -0
  21. package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/Playlist.kt +21 -0
  22. package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +454 -0
  23. package/android/src/main/java/com/margelo/nitro/nitroplayer/queue/Queue.kt +94 -0
  24. package/android/src/main/java/com/margelo/nitro/nitroplayer/queue/QueueManager.kt +143 -0
  25. package/ios/HybridAudioRoutePicker.swift +53 -0
  26. package/ios/HybridTrackPlayer.swift +100 -0
  27. package/ios/core/TrackPlayerCore.swift +1040 -0
  28. package/ios/media/MediaSessionManager.swift +230 -0
  29. package/ios/playlist/PlaylistManager.swift +446 -0
  30. package/ios/playlist/PlaylistModel.swift +49 -0
  31. package/ios/queue/HybridPlayerQueue.swift +95 -0
  32. package/ios/queue/Queue.swift +126 -0
  33. package/ios/queue/QueueManager.swift +157 -0
  34. package/lib/hooks/index.d.ts +6 -0
  35. package/lib/hooks/index.js +6 -0
  36. package/lib/hooks/useAndroidAutoConnection.d.ts +13 -0
  37. package/lib/hooks/useAndroidAutoConnection.js +26 -0
  38. package/lib/hooks/useAudioDevices.d.ts +26 -0
  39. package/lib/hooks/useAudioDevices.js +55 -0
  40. package/lib/hooks/useOnChangeTrack.d.ts +9 -0
  41. package/lib/hooks/useOnChangeTrack.js +17 -0
  42. package/lib/hooks/useOnPlaybackProgressChange.d.ts +9 -0
  43. package/lib/hooks/useOnPlaybackProgressChange.js +19 -0
  44. package/lib/hooks/useOnPlaybackStateChange.d.ts +9 -0
  45. package/lib/hooks/useOnPlaybackStateChange.js +17 -0
  46. package/lib/hooks/useOnSeek.d.ts +8 -0
  47. package/lib/hooks/useOnSeek.js +17 -0
  48. package/lib/index.d.ts +14 -0
  49. package/lib/index.js +24 -0
  50. package/lib/specs/AndroidAutoMediaLibrary.nitro.d.ts +21 -0
  51. package/lib/specs/AndroidAutoMediaLibrary.nitro.js +1 -0
  52. package/lib/specs/AudioDevices.nitro.d.ts +24 -0
  53. package/lib/specs/AudioDevices.nitro.js +1 -0
  54. package/lib/specs/AudioRoutePicker.nitro.d.ts +10 -0
  55. package/lib/specs/AudioRoutePicker.nitro.js +1 -0
  56. package/lib/specs/TrackPlayer.nitro.d.ts +39 -0
  57. package/lib/specs/TrackPlayer.nitro.js +1 -0
  58. package/lib/types/AndroidAutoMediaLibrary.d.ts +44 -0
  59. package/lib/types/AndroidAutoMediaLibrary.js +1 -0
  60. package/lib/types/PlayerQueue.d.ts +32 -0
  61. package/lib/types/PlayerQueue.js +1 -0
  62. package/lib/utils/androidAutoMediaLibrary.d.ts +47 -0
  63. package/lib/utils/androidAutoMediaLibrary.js +62 -0
  64. package/nitro.json +31 -0
  65. package/nitrogen/generated/.gitattributes +1 -0
  66. package/nitrogen/generated/android/NitroPlayer+autolinking.cmake +91 -0
  67. package/nitrogen/generated/android/NitroPlayer+autolinking.gradle +27 -0
  68. package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +88 -0
  69. package/nitrogen/generated/android/NitroPlayerOnLoad.hpp +25 -0
  70. package/nitrogen/generated/android/c++/JFunc_void_TrackItem_std__optional_Reason_.hpp +85 -0
  71. package/nitrogen/generated/android/c++/JFunc_void_TrackPlayerState_std__optional_Reason_.hpp +80 -0
  72. package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +75 -0
  73. package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +75 -0
  74. package/nitrogen/generated/android/c++/JFunc_void_double_double_std__optional_bool_.hpp +76 -0
  75. package/nitrogen/generated/android/c++/JFunc_void_std__string_Playlist_std__optional_QueueOperation_.hpp +88 -0
  76. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Playlist__std__optional_QueueOperation_.hpp +106 -0
  77. package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.cpp +55 -0
  78. package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.hpp +66 -0
  79. package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.cpp +70 -0
  80. package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.hpp +66 -0
  81. package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +143 -0
  82. package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.hpp +77 -0
  83. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +137 -0
  84. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +78 -0
  85. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +65 -0
  86. package/nitrogen/generated/android/c++/JPlayerState.hpp +87 -0
  87. package/nitrogen/generated/android/c++/JPlaylist.hpp +99 -0
  88. package/nitrogen/generated/android/c++/JQueueOperation.hpp +65 -0
  89. package/nitrogen/generated/android/c++/JReason.hpp +65 -0
  90. package/nitrogen/generated/android/c++/JTAudioDevice.hpp +69 -0
  91. package/nitrogen/generated/android/c++/JTrackItem.hpp +86 -0
  92. package/nitrogen/generated/android/c++/JTrackPlayerState.hpp +62 -0
  93. package/nitrogen/generated/android/c++/JVariant_NullType_Playlist.cpp +26 -0
  94. package/nitrogen/generated/android/c++/JVariant_NullType_Playlist.hpp +77 -0
  95. package/nitrogen/generated/android/c++/JVariant_NullType_String.cpp +26 -0
  96. package/nitrogen/generated/android/c++/JVariant_NullType_String.hpp +70 -0
  97. package/nitrogen/generated/android/c++/JVariant_NullType_TrackItem.cpp +26 -0
  98. package/nitrogen/generated/android/c++/JVariant_NullType_TrackItem.hpp +74 -0
  99. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_TrackItem_std__optional_Reason_.kt +80 -0
  100. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_TrackPlayerState_std__optional_Reason_.kt +80 -0
  101. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_bool.kt +80 -0
  102. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_double_double.kt +80 -0
  103. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_double_double_std__optional_bool_.kt +80 -0
  104. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__string_Playlist_std__optional_QueueOperation_.kt +80 -0
  105. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_Playlist__std__optional_QueueOperation_.kt +80 -0
  106. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrarySpec.kt +61 -0
  107. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAudioDevicesSpec.kt +61 -0
  108. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridPlayerQueueSpec.kt +116 -0
  109. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +134 -0
  110. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/NitroPlayerOnLoad.kt +35 -0
  111. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerConfig.kt +44 -0
  112. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerState.kt +53 -0
  113. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Playlist.kt +50 -0
  114. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/QueueOperation.kt +23 -0
  115. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Reason.kt +23 -0
  116. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TAudioDevice.kt +47 -0
  117. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackItem.kt +56 -0
  118. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackPlayerState.kt +22 -0
  119. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_Playlist.kt +59 -0
  120. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_String.kt +59 -0
  121. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_TrackItem.kt +59 -0
  122. package/nitrogen/generated/ios/NitroPlayer+autolinking.rb +60 -0
  123. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +123 -0
  124. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +531 -0
  125. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +80 -0
  126. package/nitrogen/generated/ios/NitroPlayerAutolinking.mm +49 -0
  127. package/nitrogen/generated/ios/NitroPlayerAutolinking.swift +55 -0
  128. package/nitrogen/generated/ios/c++/HybridAudioRoutePickerSpecSwift.cpp +11 -0
  129. package/nitrogen/generated/ios/c++/HybridAudioRoutePickerSpecSwift.hpp +74 -0
  130. package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.cpp +11 -0
  131. package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.hpp +167 -0
  132. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.cpp +11 -0
  133. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +174 -0
  134. package/nitrogen/generated/ios/swift/Func_void_TrackItem_std__optional_Reason_.swift +47 -0
  135. package/nitrogen/generated/ios/swift/Func_void_TrackPlayerState_std__optional_Reason_.swift +47 -0
  136. package/nitrogen/generated/ios/swift/Func_void_bool.swift +47 -0
  137. package/nitrogen/generated/ios/swift/Func_void_double_double.swift +47 -0
  138. package/nitrogen/generated/ios/swift/Func_void_double_double_std__optional_bool_.swift +54 -0
  139. package/nitrogen/generated/ios/swift/Func_void_std__string_Playlist_std__optional_QueueOperation_.swift +47 -0
  140. package/nitrogen/generated/ios/swift/Func_void_std__vector_Playlist__std__optional_QueueOperation_.swift +47 -0
  141. package/nitrogen/generated/ios/swift/HybridAudioRoutePickerSpec.swift +56 -0
  142. package/nitrogen/generated/ios/swift/HybridAudioRoutePickerSpec_cxx.swift +130 -0
  143. package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec.swift +68 -0
  144. package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec_cxx.swift +349 -0
  145. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +69 -0
  146. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +325 -0
  147. package/nitrogen/generated/ios/swift/PlayerConfig.swift +115 -0
  148. package/nitrogen/generated/ios/swift/PlayerState.swift +181 -0
  149. package/nitrogen/generated/ios/swift/Playlist.swift +182 -0
  150. package/nitrogen/generated/ios/swift/QueueOperation.swift +48 -0
  151. package/nitrogen/generated/ios/swift/Reason.swift +48 -0
  152. package/nitrogen/generated/ios/swift/TrackItem.swift +147 -0
  153. package/nitrogen/generated/ios/swift/TrackPlayerState.swift +44 -0
  154. package/nitrogen/generated/ios/swift/Variant_NullType_Playlist.swift +18 -0
  155. package/nitrogen/generated/ios/swift/Variant_NullType_String.swift +18 -0
  156. package/nitrogen/generated/ios/swift/Variant_NullType_TrackItem.swift +18 -0
  157. package/nitrogen/generated/shared/c++/HybridAndroidAutoMediaLibrarySpec.cpp +22 -0
  158. package/nitrogen/generated/shared/c++/HybridAndroidAutoMediaLibrarySpec.hpp +63 -0
  159. package/nitrogen/generated/shared/c++/HybridAudioDevicesSpec.cpp +22 -0
  160. package/nitrogen/generated/shared/c++/HybridAudioDevicesSpec.hpp +65 -0
  161. package/nitrogen/generated/shared/c++/HybridAudioRoutePickerSpec.cpp +21 -0
  162. package/nitrogen/generated/shared/c++/HybridAudioRoutePickerSpec.hpp +62 -0
  163. package/nitrogen/generated/shared/c++/HybridPlayerQueueSpec.cpp +33 -0
  164. package/nitrogen/generated/shared/c++/HybridPlayerQueueSpec.hpp +87 -0
  165. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +34 -0
  166. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +91 -0
  167. package/nitrogen/generated/shared/c++/PlayerConfig.hpp +83 -0
  168. package/nitrogen/generated/shared/c++/PlayerState.hpp +103 -0
  169. package/nitrogen/generated/shared/c++/Playlist.hpp +97 -0
  170. package/nitrogen/generated/shared/c++/QueueOperation.hpp +84 -0
  171. package/nitrogen/generated/shared/c++/Reason.hpp +84 -0
  172. package/nitrogen/generated/shared/c++/TAudioDevice.hpp +87 -0
  173. package/nitrogen/generated/shared/c++/TrackItem.hpp +102 -0
  174. package/nitrogen/generated/shared/c++/TrackPlayerState.hpp +80 -0
  175. package/package.json +172 -0
  176. package/react-native.config.js +16 -0
  177. package/src/hooks/index.ts +6 -0
  178. package/src/hooks/useAndroidAutoConnection.ts +30 -0
  179. package/src/hooks/useAudioDevices.ts +64 -0
  180. package/src/hooks/useOnChangeTrack.ts +24 -0
  181. package/src/hooks/useOnPlaybackProgressChange.ts +30 -0
  182. package/src/hooks/useOnPlaybackStateChange.ts +24 -0
  183. package/src/hooks/useOnSeek.ts +25 -0
  184. package/src/index.ts +47 -0
  185. package/src/specs/AndroidAutoMediaLibrary.nitro.ts +22 -0
  186. package/src/specs/AudioDevices.nitro.ts +25 -0
  187. package/src/specs/AudioRoutePicker.nitro.ts +9 -0
  188. package/src/specs/TrackPlayer.nitro.ts +81 -0
  189. package/src/types/AndroidAutoMediaLibrary.ts +58 -0
  190. package/src/types/PlayerQueue.ts +38 -0
  191. package/src/utils/androidAutoMediaLibrary.ts +66 -0
@@ -0,0 +1,230 @@
1
+ //
2
+ // MediaSessionManager.swift
3
+ // NitroPlayer
4
+ //
5
+ // Created by Ritesh Shukla on 10/12/25.
6
+ //
7
+
8
+ import AVFoundation
9
+ import Foundation
10
+ import MediaPlayer
11
+ import NitroModules
12
+ import UIKit
13
+
14
+ class MediaSessionManager {
15
+ // MARK: - Constants
16
+
17
+ private enum Constants {
18
+ // Seek intervals (in seconds)
19
+ static let seekInterval: Double = 10.0
20
+
21
+ // Artwork size
22
+ static let artworkSize: CGFloat = 500.0
23
+ }
24
+
25
+ // MARK: - Properties
26
+
27
+ private var trackPlayerCore: TrackPlayerCore?
28
+ private var artworkCache: [String: UIImage] = [:]
29
+
30
+ private var androidAutoEnabled: Bool = false
31
+ private var carPlayEnabled: Bool = false
32
+ private var showInNotification: Bool = true
33
+
34
+ init() {
35
+ setupRemoteCommandCenter()
36
+ }
37
+
38
+ func setTrackPlayerCore(_ core: TrackPlayerCore) {
39
+ trackPlayerCore = core
40
+ }
41
+
42
+ func configure(
43
+ androidAutoEnabled: Bool?,
44
+ carPlayEnabled: Bool?,
45
+ showInNotification: Bool?
46
+ ) {
47
+ if let androidAutoEnabled = androidAutoEnabled {
48
+ self.androidAutoEnabled = androidAutoEnabled
49
+ }
50
+ if let carPlayEnabled = carPlayEnabled {
51
+ self.carPlayEnabled = carPlayEnabled
52
+ // CarPlay is handled by the app's CarPlaySceneDelegate
53
+ // We just maintain the flag here for reference
54
+ }
55
+ if let showInNotification = showInNotification {
56
+ self.showInNotification = showInNotification
57
+ if showInNotification {
58
+ updateNowPlayingInfo()
59
+ } else {
60
+ clearNowPlayingInfo()
61
+ }
62
+ }
63
+ }
64
+
65
+ private func setupRemoteCommandCenter() {
66
+ let commandCenter = MPRemoteCommandCenter.shared()
67
+
68
+ // Play command
69
+ commandCenter.playCommand.isEnabled = true
70
+ commandCenter.playCommand.addTarget { [weak self] _ in
71
+ self?.trackPlayerCore?.play()
72
+ return .success
73
+ }
74
+
75
+ // Pause command
76
+ commandCenter.pauseCommand.isEnabled = true
77
+ commandCenter.pauseCommand.addTarget { [weak self] _ in
78
+ self?.trackPlayerCore?.pause()
79
+ return .success
80
+ }
81
+
82
+ // Toggle play/pause
83
+ commandCenter.togglePlayPauseCommand.isEnabled = true
84
+ commandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in
85
+ guard let self = self, let core = self.trackPlayerCore else { return .commandFailed }
86
+ let state = core.getState()
87
+ if state.currentState == .playing {
88
+ core.pause()
89
+ } else {
90
+ core.play()
91
+ }
92
+ return .success
93
+ }
94
+
95
+ // Next track command
96
+ commandCenter.nextTrackCommand.isEnabled = true
97
+ commandCenter.nextTrackCommand.addTarget { [weak self] _ in
98
+ self?.trackPlayerCore?.skipToNext()
99
+ return .success
100
+ }
101
+
102
+ // Previous track command
103
+ commandCenter.previousTrackCommand.isEnabled = true
104
+ commandCenter.previousTrackCommand.addTarget { [weak self] _ in
105
+ self?.trackPlayerCore?.skipToPrevious()
106
+ return .success
107
+ }
108
+
109
+ // Seek forward
110
+ commandCenter.seekForwardCommand.isEnabled = true
111
+ commandCenter.seekForwardCommand.addTarget { [weak self] event in
112
+ guard let self = self, let core = self.trackPlayerCore else { return .commandFailed }
113
+ let state = core.getState()
114
+ let newPosition = min(state.currentPosition + Constants.seekInterval, state.totalDuration)
115
+ core.seek(position: newPosition)
116
+ return .success
117
+ }
118
+
119
+ // Seek backward
120
+ commandCenter.seekBackwardCommand.isEnabled = true
121
+ commandCenter.seekBackwardCommand.addTarget { [weak self] event in
122
+ guard let self = self, let core = self.trackPlayerCore else { return .commandFailed }
123
+ let state = core.getState()
124
+ let newPosition = max(state.currentPosition - Constants.seekInterval, 0.0)
125
+ core.seek(position: newPosition)
126
+ return .success
127
+ }
128
+
129
+ // Change playback position
130
+ commandCenter.changePlaybackPositionCommand.isEnabled = true
131
+ commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in
132
+ guard let self = self,
133
+ let core = self.trackPlayerCore,
134
+ let event = event as? MPChangePlaybackPositionCommandEvent
135
+ else {
136
+ return .commandFailed
137
+ }
138
+ core.seek(position: event.positionTime)
139
+ return .success
140
+ }
141
+ }
142
+
143
+ private func getCurrentTrack() -> TrackItem? {
144
+ return trackPlayerCore?.getCurrentTrack()
145
+ }
146
+
147
+ func updateNowPlayingInfo() {
148
+ guard showInNotification else { return }
149
+
150
+ guard let track = getCurrentTrack(),
151
+ let core = trackPlayerCore
152
+ else {
153
+ clearNowPlayingInfo()
154
+ return
155
+ }
156
+
157
+ let state = core.getState()
158
+
159
+ let nowPlayingInfo: [String: Any] = [
160
+ MPMediaItemPropertyTitle: track.title,
161
+ MPMediaItemPropertyArtist: track.artist,
162
+ MPMediaItemPropertyAlbumTitle: track.album,
163
+ MPNowPlayingInfoPropertyElapsedPlaybackTime: state.currentPosition,
164
+ MPMediaItemPropertyPlaybackDuration: state.totalDuration,
165
+ MPNowPlayingInfoPropertyPlaybackRate: state.currentState == .playing ? 1.0 : 0.0,
166
+ ]
167
+
168
+ // Load artwork asynchronously
169
+ if let artwork = track.artwork, case .second(let artworkUrl) = artwork {
170
+ loadArtwork(url: artworkUrl) { [weak self] image in
171
+ if let image = image {
172
+ var updatedInfo = nowPlayingInfo
173
+ updatedInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(
174
+ boundsSize: CGSize(width: Constants.artworkSize, height: Constants.artworkSize),
175
+ requestHandler: { _ in image }
176
+ )
177
+ MPNowPlayingInfoCenter.default().nowPlayingInfo = updatedInfo
178
+ }
179
+ }
180
+ }
181
+
182
+ MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
183
+ }
184
+
185
+ private func clearNowPlayingInfo() {
186
+ MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
187
+ }
188
+
189
+ private func loadArtwork(url: String, completion: @escaping (UIImage?) -> Void) {
190
+ // Check cache first
191
+ if let cached = artworkCache[url] {
192
+ completion(cached)
193
+ return
194
+ }
195
+
196
+ guard let imageUrl = URL(string: url) else {
197
+ completion(nil)
198
+ return
199
+ }
200
+
201
+ // Load image asynchronously
202
+ URLSession.shared.dataTask(with: imageUrl) { [weak self] data, _, _ in
203
+ guard let data = data,
204
+ let image = UIImage(data: data)
205
+ else {
206
+ completion(nil)
207
+ return
208
+ }
209
+
210
+ // Cache the image
211
+ self?.artworkCache[url] = image
212
+ DispatchQueue.main.async {
213
+ completion(image)
214
+ }
215
+ }.resume()
216
+ }
217
+
218
+ func onTrackChanged() {
219
+ updateNowPlayingInfo()
220
+ }
221
+
222
+ func onPlaybackStateChanged() {
223
+ updateNowPlayingInfo()
224
+ }
225
+
226
+ func release() {
227
+ clearNowPlayingInfo()
228
+ artworkCache.removeAll()
229
+ }
230
+ }
@@ -0,0 +1,446 @@
1
+ //
2
+ // PlaylistManager.swift
3
+ // NitroPlayer
4
+ //
5
+ // Created by Ritesh Shukla on 10/12/25.
6
+ //
7
+
8
+ import Foundation
9
+ import NitroModules
10
+
11
+ /// Manages multiple playlists using AVPlayer's native playlist functionality
12
+ class PlaylistManager {
13
+ private var playlists: [String: PlaylistModel] = [:]
14
+ private var listeners: [(String, ([PlaylistModel], QueueOperation?) -> Void)] = []
15
+ private var playlistListeners: [String: [(String, (PlaylistModel, QueueOperation?) -> Void)]] =
16
+ [:]
17
+ private var currentPlaylistId: String?
18
+ private let queue = DispatchQueue(label: "com.margelo.nitro.nitroplayer.playlist")
19
+
20
+ static let shared = PlaylistManager()
21
+
22
+ private init() {
23
+ loadPlaylistsFromUserDefaults()
24
+ }
25
+
26
+ /**
27
+ * Create a new playlist
28
+ */
29
+ func createPlaylist(name: String, description: String? = nil, artwork: String? = nil) -> String {
30
+ let id = UUID().uuidString
31
+ let playlist = PlaylistModel(id: id, name: name, description: description, artwork: artwork)
32
+
33
+ queue.sync {
34
+ playlists[id] = playlist
35
+ }
36
+
37
+ savePlaylistsToUserDefaults()
38
+ notifyPlaylistsChanged(.add)
39
+
40
+ return id
41
+ }
42
+
43
+ /**
44
+ * Delete a playlist
45
+ */
46
+ func deletePlaylist(playlistId: String) -> Bool {
47
+ let removed = queue.sync {
48
+ return playlists.removeValue(forKey: playlistId) != nil
49
+ }
50
+
51
+ if removed {
52
+ if currentPlaylistId == playlistId {
53
+ currentPlaylistId = nil
54
+ }
55
+ playlistListeners.removeValue(forKey: playlistId)
56
+ savePlaylistsToUserDefaults()
57
+ notifyPlaylistsChanged(.remove)
58
+ return true
59
+ }
60
+
61
+ return false
62
+ }
63
+
64
+ /**
65
+ * Update playlist metadata
66
+ */
67
+ func updatePlaylist(
68
+ playlistId: String, name: String? = nil, description: String? = nil, artwork: String? = nil
69
+ ) -> Bool {
70
+ guard let playlist = queue.sync(execute: { playlists[playlistId] }) else {
71
+ return false
72
+ }
73
+
74
+ queue.sync {
75
+ playlists[playlistId] = PlaylistModel(
76
+ id: playlist.id,
77
+ name: name ?? playlist.name,
78
+ description: description ?? playlist.description,
79
+ artwork: artwork ?? playlist.artwork,
80
+ tracks: playlist.tracks
81
+ )
82
+ }
83
+
84
+ savePlaylistsToUserDefaults()
85
+ notifyPlaylistChanged(playlistId, .update)
86
+ notifyPlaylistsChanged(.update)
87
+
88
+ return true
89
+ }
90
+
91
+ /**
92
+ * Get a playlist by ID
93
+ */
94
+ func getPlaylist(playlistId: String) -> PlaylistModel? {
95
+ return queue.sync {
96
+ return playlists[playlistId]
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Get all playlists
102
+ */
103
+ func getAllPlaylists() -> [PlaylistModel] {
104
+ return queue.sync {
105
+ return Array(playlists.values)
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Add a track to a playlist
111
+ */
112
+ func addTrackToPlaylist(playlistId: String, track: TrackItem, index: Int? = nil) -> Bool {
113
+ guard let playlist = queue.sync(execute: { playlists[playlistId] }) else {
114
+ return false
115
+ }
116
+
117
+ queue.sync {
118
+ var tracks = playlist.tracks
119
+ if let index = index, index >= 0 && index <= tracks.count {
120
+ tracks.insert(track, at: index)
121
+ } else {
122
+ tracks.append(track)
123
+ }
124
+ playlists[playlistId] = PlaylistModel(
125
+ id: playlist.id,
126
+ name: playlist.name,
127
+ description: playlist.description,
128
+ artwork: playlist.artwork,
129
+ tracks: tracks
130
+ )
131
+ }
132
+
133
+ savePlaylistsToUserDefaults()
134
+ notifyPlaylistChanged(playlistId, .add)
135
+
136
+ // Update TrackPlayerCore if this is the current playlist
137
+ if currentPlaylistId == playlistId {
138
+ TrackPlayerCore.shared.updatePlaylist(playlistId: playlistId)
139
+ }
140
+
141
+ return true
142
+ }
143
+
144
+ /**
145
+ * Add multiple tracks to a playlist at once
146
+ */
147
+ func addTracksToPlaylist(playlistId: String, tracks: [TrackItem], index: Int? = nil) -> Bool {
148
+ guard let playlist = queue.sync(execute: { playlists[playlistId] }) else {
149
+ return false
150
+ }
151
+
152
+ queue.sync {
153
+ var currentTracks = playlist.tracks
154
+ if let index = index, index >= 0 && index <= currentTracks.count {
155
+ currentTracks.insert(contentsOf: tracks, at: index)
156
+ } else {
157
+ currentTracks.append(contentsOf: tracks)
158
+ }
159
+ playlists[playlistId] = PlaylistModel(
160
+ id: playlist.id,
161
+ name: playlist.name,
162
+ description: playlist.description,
163
+ artwork: playlist.artwork,
164
+ tracks: currentTracks
165
+ )
166
+ }
167
+
168
+ savePlaylistsToUserDefaults()
169
+ notifyPlaylistChanged(playlistId, .add)
170
+
171
+ // Update TrackPlayerCore if this is the current playlist
172
+ if currentPlaylistId == playlistId {
173
+ TrackPlayerCore.shared.updatePlaylist(playlistId: playlistId)
174
+ }
175
+
176
+ return true
177
+ }
178
+
179
+ /**
180
+ * Remove a track from a playlist
181
+ */
182
+ func removeTrackFromPlaylist(playlistId: String, trackId: String) -> Bool {
183
+ guard let playlist = queue.sync(execute: { playlists[playlistId] }) else {
184
+ return false
185
+ }
186
+
187
+ let removed = queue.sync {
188
+ var tracks = playlist.tracks
189
+ let initialCount = tracks.count
190
+ tracks.removeAll { $0.id == trackId }
191
+ let wasRemoved = tracks.count < initialCount
192
+
193
+ if wasRemoved {
194
+ playlists[playlistId] = PlaylistModel(
195
+ id: playlist.id,
196
+ name: playlist.name,
197
+ description: playlist.description,
198
+ artwork: playlist.artwork,
199
+ tracks: tracks
200
+ )
201
+ }
202
+
203
+ return wasRemoved
204
+ }
205
+
206
+ if removed {
207
+ savePlaylistsToUserDefaults()
208
+ notifyPlaylistChanged(playlistId, .remove)
209
+
210
+ // Update TrackPlayerCore if this is the current playlist
211
+ if currentPlaylistId == playlistId {
212
+ TrackPlayerCore.shared.updatePlaylist(playlistId: playlistId)
213
+ }
214
+ }
215
+
216
+ return removed
217
+ }
218
+
219
+ /**
220
+ * Reorder a track in a playlist
221
+ */
222
+ func reorderTrackInPlaylist(playlistId: String, trackId: String, newIndex: Int) -> Bool {
223
+ guard let playlist = queue.sync(execute: { playlists[playlistId] }) else {
224
+ return false
225
+ }
226
+
227
+ let tracks = playlist.tracks
228
+ guard let oldIndex = tracks.firstIndex(where: { $0.id == trackId }),
229
+ newIndex >= 0 && newIndex < tracks.count
230
+ else {
231
+ return false
232
+ }
233
+
234
+ queue.sync {
235
+ var reorderedTracks = tracks
236
+ let track = reorderedTracks.remove(at: oldIndex)
237
+ reorderedTracks.insert(track, at: newIndex)
238
+
239
+ playlists[playlistId] = PlaylistModel(
240
+ id: playlist.id,
241
+ name: playlist.name,
242
+ description: playlist.description,
243
+ artwork: playlist.artwork,
244
+ tracks: reorderedTracks
245
+ )
246
+ }
247
+
248
+ savePlaylistsToUserDefaults()
249
+ notifyPlaylistChanged(playlistId, .update)
250
+
251
+ // Update TrackPlayerCore if this is the current playlist
252
+ if currentPlaylistId == playlistId {
253
+ TrackPlayerCore.shared.updatePlaylist(playlistId: playlistId)
254
+ }
255
+
256
+ return true
257
+ }
258
+
259
+ /**
260
+ * Load a playlist for playback (sets it as current)
261
+ */
262
+ func loadPlaylist(playlistId: String) -> Bool {
263
+ guard let playlist = queue.sync(execute: { playlists[playlistId] }) else {
264
+ return false
265
+ }
266
+
267
+ currentPlaylistId = playlistId
268
+
269
+ // Update TrackPlayerCore
270
+ TrackPlayerCore.shared.loadPlaylist(playlistId: playlistId)
271
+
272
+ return true
273
+ }
274
+
275
+ /**
276
+ * Get the current playlist ID
277
+ */
278
+ func getCurrentPlaylistId() -> String? {
279
+ return currentPlaylistId
280
+ }
281
+
282
+ /**
283
+ * Get the current playlist
284
+ */
285
+ func getCurrentPlaylist() -> PlaylistModel? {
286
+ return currentPlaylistId.flatMap { id in queue.sync { playlists[id] } }
287
+ }
288
+
289
+ /**
290
+ * Add a listener for playlist changes
291
+ */
292
+ func addPlaylistsChangeListener(listener: @escaping ([PlaylistModel], QueueOperation?) -> Void)
293
+ -> () -> Void
294
+ {
295
+ let listenerId = UUID().uuidString
296
+ queue.sync {
297
+ listeners.append((listenerId, listener))
298
+ }
299
+
300
+ return {
301
+ self.queue.sync {
302
+ self.listeners.removeAll { $0.0 == listenerId }
303
+ }
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Add a listener for a specific playlist changes
309
+ */
310
+ func addPlaylistChangeListener(
311
+ playlistId: String, listener: @escaping (PlaylistModel, QueueOperation?) -> Void
312
+ ) -> () -> Void {
313
+ let listenerId = UUID().uuidString
314
+ queue.sync {
315
+ if playlistListeners[playlistId] == nil {
316
+ playlistListeners[playlistId] = []
317
+ }
318
+ playlistListeners[playlistId]?.append((listenerId, listener))
319
+ }
320
+
321
+ return {
322
+ self.queue.sync {
323
+ self.playlistListeners[playlistId]?.removeAll { $0.0 == listenerId }
324
+ }
325
+ }
326
+ }
327
+
328
+ private func notifyPlaylistsChanged(_ operation: QueueOperation?) {
329
+ let allPlaylists = queue.sync {
330
+ return Array(playlists.values)
331
+ }
332
+ listeners.forEach { $0.1(allPlaylists, operation) }
333
+ }
334
+
335
+ private func notifyPlaylistChanged(_ playlistId: String, _ operation: QueueOperation?) {
336
+ guard let playlist = queue.sync(execute: { playlists[playlistId] }) else {
337
+ return
338
+ }
339
+
340
+ playlistListeners[playlistId]?.forEach { $0.1(playlist, operation) }
341
+ }
342
+
343
+ private func savePlaylistsToUserDefaults() {
344
+ // Save playlists to UserDefaults for persistence
345
+ // Implementation similar to Android SharedPreferences
346
+ do {
347
+ let playlistsArray = queue.sync {
348
+ return Array(playlists.values)
349
+ }
350
+ let playlistsData = playlistsArray.map { playlist -> [String: Any] in
351
+ return [
352
+ "id": playlist.id,
353
+ "name": playlist.name,
354
+ "description": playlist.description ?? "",
355
+ "artwork": playlist.artwork ?? "",
356
+ "tracks": playlist.tracks.map { track -> [String: Any] in
357
+ var trackDict: [String: Any] = [
358
+ "id": track.id,
359
+ "title": track.title,
360
+ "artist": track.artist,
361
+ "album": track.album,
362
+ "duration": track.duration,
363
+ "url": track.url,
364
+ ]
365
+ // Handle artwork - unwrap Variant_NullType_String
366
+ if let artwork = track.artwork, case .second(let artworkUrl) = artwork {
367
+ trackDict["artwork"] = artworkUrl
368
+ } else {
369
+ trackDict["artwork"] = ""
370
+ }
371
+ return trackDict
372
+ },
373
+ ]
374
+ }
375
+ let data = try JSONSerialization.data(withJSONObject: playlistsData, options: [])
376
+ UserDefaults.standard.set(data, forKey: "NitroPlayerPlaylists")
377
+ UserDefaults.standard.set(currentPlaylistId, forKey: "NitroPlayerCurrentPlaylistId")
378
+ } catch {
379
+ print("❌ PlaylistManager: Error saving playlists - \(error)")
380
+ }
381
+ }
382
+
383
+ private func loadPlaylistsFromUserDefaults() {
384
+ guard let data = UserDefaults.standard.data(forKey: "NitroPlayerPlaylists") else {
385
+ return
386
+ }
387
+
388
+ do {
389
+ let decoder = JSONDecoder()
390
+ let playlistsDict = try JSONSerialization.jsonObject(with: data) as? [[String: Any]] ?? []
391
+
392
+ queue.sync {
393
+ playlists.removeAll()
394
+ for playlistDict in playlistsDict {
395
+ guard let id = playlistDict["id"] as? String,
396
+ let name = playlistDict["name"] as? String
397
+ else {
398
+ continue
399
+ }
400
+
401
+ let description = playlistDict["description"] as? String
402
+ let artwork = playlistDict["artwork"] as? String
403
+ let tracksArray = playlistDict["tracks"] as? [[String: Any]] ?? []
404
+
405
+ let tracks = tracksArray.compactMap { trackDict -> TrackItem? in
406
+ guard let id = trackDict["id"] as? String,
407
+ let title = trackDict["title"] as? String,
408
+ let artist = trackDict["artist"] as? String,
409
+ let album = trackDict["album"] as? String,
410
+ let duration = trackDict["duration"] as? Double,
411
+ let url = trackDict["url"] as? String
412
+ else {
413
+ return nil
414
+ }
415
+
416
+ let artworkString = trackDict["artwork"] as? String
417
+ let artwork = artworkString.flatMap {
418
+ !$0.isEmpty ? Variant_NullType_String.second($0) : nil
419
+ }
420
+ return TrackItem(
421
+ id: id,
422
+ title: title,
423
+ artist: artist,
424
+ album: album,
425
+ duration: duration,
426
+ url: url,
427
+ artwork: artwork
428
+ )
429
+ }
430
+
431
+ playlists[id] = PlaylistModel(
432
+ id: id,
433
+ name: name,
434
+ description: description,
435
+ artwork: artwork,
436
+ tracks: tracks
437
+ )
438
+ }
439
+ }
440
+
441
+ currentPlaylistId = UserDefaults.standard.string(forKey: "NitroPlayerCurrentPlaylistId")
442
+ } catch {
443
+ print("❌ PlaylistManager: Error loading playlists - \(error)")
444
+ }
445
+ }
446
+ }
@@ -0,0 +1,49 @@
1
+ //
2
+ // PlaylistModel.swift
3
+ // NitroPlayer
4
+ //
5
+ // Created by Ritesh Shukla on 10/12/25.
6
+ //
7
+
8
+ import Foundation
9
+ import NitroModules
10
+
11
+ /// Represents a playlist containing multiple tracks
12
+ /// Uses AVPlayer's native playlist functionality
13
+ class PlaylistModel {
14
+ let id: String
15
+ let name: String
16
+ let description: String?
17
+ let artwork: String?
18
+ var tracks: [TrackItem]
19
+
20
+ init(
21
+ id: String, name: String, description: String? = nil, artwork: String? = nil,
22
+ tracks: [TrackItem] = []
23
+ ) {
24
+ self.id = id
25
+ self.name = name
26
+ self.description = description
27
+ self.artwork = artwork
28
+ self.tracks = tracks
29
+ }
30
+
31
+ func getTrackCount() -> Int {
32
+ return tracks.count
33
+ }
34
+
35
+ func isEmpty() -> Bool {
36
+ return tracks.isEmpty
37
+ }
38
+
39
+ // Convert to generated Playlist type
40
+ func toGeneratedPlaylist() -> Playlist {
41
+ return Playlist(
42
+ id: self.id,
43
+ name: self.name,
44
+ description: self.description.map { Variant_NullType_String.second($0) },
45
+ artwork: self.artwork.map { Variant_NullType_String.second($0) },
46
+ tracks: self.tracks
47
+ )
48
+ }
49
+ }