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,506 @@
1
+ package com.margelo.nitro.nitroplayer.media
2
+
3
+ import android.app.Notification
4
+ import android.app.NotificationChannel
5
+ import android.app.NotificationManager
6
+ import android.app.PendingIntent
7
+ import android.content.Context
8
+ import android.content.Intent
9
+ import android.graphics.Bitmap
10
+ import android.graphics.BitmapFactory
11
+ import android.net.Uri
12
+ import android.os.Build
13
+ import androidx.core.app.NotificationCompat
14
+ import androidx.media3.common.MediaItem
15
+ import androidx.media3.common.MediaMetadata
16
+ import androidx.media3.common.Player
17
+ import androidx.media3.exoplayer.ExoPlayer
18
+ import androidx.media3.session.MediaSession
19
+ import androidx.media3.session.MediaSessionService
20
+ import androidx.media3.session.SessionCommand
21
+ import androidx.media3.session.SessionResult
22
+ import com.google.common.util.concurrent.Futures
23
+ import com.google.common.util.concurrent.ListenableFuture
24
+ import com.margelo.nitro.nitroplayer.TrackItem
25
+ import com.margelo.nitro.nitroplayer.core.TrackPlayerCore
26
+ import com.margelo.nitro.nitroplayer.media.NitroPlayerMediaBrowserService
27
+ import com.margelo.nitro.nitroplayer.playlist.PlaylistManager
28
+ import kotlinx.coroutines.CoroutineScope
29
+ import kotlinx.coroutines.Dispatchers
30
+ import kotlinx.coroutines.SupervisorJob
31
+ import kotlinx.coroutines.launch
32
+ import kotlinx.coroutines.withContext
33
+ import java.net.URL
34
+
35
+ class MediaSessionManager(
36
+ private val context: Context,
37
+ private val player: ExoPlayer,
38
+ private val playlistManager: PlaylistManager,
39
+ ) {
40
+ private var trackPlayerCore: TrackPlayerCore? = null
41
+
42
+ fun setTrackPlayerCore(core: TrackPlayerCore) {
43
+ trackPlayerCore = core
44
+ }
45
+
46
+ var mediaSession: MediaSession? = null // Make public so MediaBrowserService can access it
47
+ private set
48
+ private var notificationManager: NotificationManager? = null
49
+ private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
50
+ private val artworkCache = mutableMapOf<String, Bitmap>()
51
+
52
+ private var androidAutoEnabled: Boolean = false
53
+ private var carPlayEnabled: Boolean = false
54
+ private var showInNotification: Boolean = true
55
+
56
+ companion object {
57
+ private const val NOTIFICATION_ID = 1001
58
+ private const val CHANNEL_ID = "nitro_player_channel"
59
+ private const val CHANNEL_NAME = "Music Player"
60
+ const val ACTION_PLAY = "com.margelo.nitro.nitroplayer.PLAY"
61
+ const val ACTION_PAUSE = "com.margelo.nitro.nitroplayer.PAUSE"
62
+ const val ACTION_NEXT = "com.margelo.nitro.nitroplayer.NEXT"
63
+ const val ACTION_PREVIOUS = "com.margelo.nitro.nitroplayer.PREVIOUS"
64
+ }
65
+
66
+ init {
67
+ setupMediaSession()
68
+ createNotificationChannel()
69
+ }
70
+
71
+ fun configure(
72
+ androidAutoEnabled: Boolean?,
73
+ carPlayEnabled: Boolean?,
74
+ showInNotification: Boolean?,
75
+ ) {
76
+ androidAutoEnabled?.let { this.androidAutoEnabled = it }
77
+ carPlayEnabled?.let { this.carPlayEnabled = it }
78
+ showInNotification?.let {
79
+ this.showInNotification = it
80
+ if (it) {
81
+ updateNotification()
82
+ } else {
83
+ hideNotification()
84
+ }
85
+ }
86
+ }
87
+
88
+ private fun setupMediaSession() {
89
+ try {
90
+ mediaSession =
91
+ MediaSession
92
+ .Builder(context, player)
93
+ .setCallback(
94
+ object : MediaSession.Callback {
95
+ override fun onConnect(
96
+ session: MediaSession,
97
+ controller: MediaSession.ControllerInfo,
98
+ ): MediaSession.ConnectionResult {
99
+ // Accept all connections with default commands
100
+ // Media3 automatically handles play, pause, skip, etc. through the player
101
+ return MediaSession.ConnectionResult
102
+ .AcceptedResultBuilder(session)
103
+ .setAvailableSessionCommands(
104
+ MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS,
105
+ ).setAvailablePlayerCommands(
106
+ MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS,
107
+ ).build()
108
+ }
109
+
110
+ override fun onAddMediaItems(
111
+ mediaSession: MediaSession,
112
+ controller: MediaSession.ControllerInfo,
113
+ mediaItems: MutableList<MediaItem>,
114
+ ): ListenableFuture<MutableList<MediaItem>> {
115
+ // This is called when Android Auto requests to play a track
116
+ println("🎵 MediaSessionManager: onAddMediaItems called with ${mediaItems.size} items")
117
+
118
+ if (mediaItems.isEmpty()) {
119
+ return Futures.immediateFuture(mutableListOf())
120
+ }
121
+
122
+ val updatedMediaItems = mutableListOf<MediaItem>()
123
+
124
+ for (requestedMediaItem in mediaItems) {
125
+ // Get the mediaId from requestMetadata or mediaId
126
+ val mediaId =
127
+ requestedMediaItem.requestMetadata.mediaUri?.toString()
128
+ ?: requestedMediaItem.mediaId
129
+
130
+ println("🎵 MediaSessionManager: Processing mediaId: $mediaId")
131
+
132
+ try {
133
+ // Parse mediaId format: "playlistId:trackId"
134
+ if (mediaId.contains(':')) {
135
+ val colonIndex = mediaId.indexOf(':')
136
+ val playlistId = mediaId.substring(0, colonIndex)
137
+ val trackId = mediaId.substring(colonIndex + 1)
138
+
139
+ println("🎵 MediaSessionManager: Parsed playlistId: $playlistId, trackId: $trackId")
140
+
141
+ // Get the playlist and track
142
+ val playlist = playlistManager.getPlaylist(playlistId)
143
+ if (playlist != null) {
144
+ val track = playlist.tracks.find { it.id == trackId }
145
+ if (track != null) {
146
+ // Create a proper MediaItem with all metadata
147
+ val resolvedMediaItem = createMediaItem(track, mediaId)
148
+ updatedMediaItems.add(resolvedMediaItem)
149
+ println("✅ MediaSessionManager: Resolved track: ${track.title}")
150
+ } else {
151
+ println("⚠️ MediaSessionManager: Track $trackId not found in playlist")
152
+ updatedMediaItems.add(requestedMediaItem)
153
+ }
154
+ } else {
155
+ println("⚠️ MediaSessionManager: Playlist $playlistId not found")
156
+ updatedMediaItems.add(requestedMediaItem)
157
+ }
158
+ } else {
159
+ println("⚠️ MediaSessionManager: Invalid mediaId format: $mediaId")
160
+ updatedMediaItems.add(requestedMediaItem)
161
+ }
162
+ } catch (e: Exception) {
163
+ println("❌ MediaSessionManager: Error processing mediaId - ${e.message}")
164
+ e.printStackTrace()
165
+ updatedMediaItems.add(requestedMediaItem)
166
+ }
167
+ }
168
+
169
+ println("🎵 MediaSessionManager: Returning ${updatedMediaItems.size} resolved media items")
170
+ return Futures.immediateFuture(updatedMediaItems)
171
+ }
172
+
173
+ override fun onSetMediaItems(
174
+ mediaSession: MediaSession,
175
+ controller: MediaSession.ControllerInfo,
176
+ mediaItems: MutableList<MediaItem>,
177
+ startIndex: Int,
178
+ startPositionMs: Long,
179
+ ): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
180
+ // This is called when Android Auto wants to set and play media items
181
+ println("🎵 MediaSessionManager: onSetMediaItems called with ${mediaItems.size} items, startIndex: $startIndex")
182
+
183
+ if (mediaItems.isEmpty()) {
184
+ return Futures.immediateFuture(
185
+ MediaSession.MediaItemsWithStartPosition(
186
+ mutableListOf(),
187
+ 0,
188
+ 0,
189
+ ),
190
+ )
191
+ }
192
+
193
+ try {
194
+ // Get the first item's mediaId to determine the playlist
195
+ val firstMediaId = mediaItems[0].mediaId
196
+ println("🎵 MediaSessionManager: First mediaId: $firstMediaId")
197
+
198
+ // Parse mediaId format: "playlistId:trackId"
199
+ if (firstMediaId.contains(':')) {
200
+ val colonIndex = firstMediaId.indexOf(':')
201
+ val playlistId = firstMediaId.substring(0, colonIndex)
202
+ val trackId = firstMediaId.substring(colonIndex + 1)
203
+
204
+ println("🎵 MediaSessionManager: Loading full playlist: $playlistId, starting at track: $trackId")
205
+
206
+ // Get the full playlist
207
+ val playlist = playlistManager.getPlaylist(playlistId)
208
+ if (playlist != null) {
209
+ // Find the track index in the full playlist
210
+ val trackIndex = playlist.tracks.indexOfFirst { it.id == trackId }
211
+
212
+ if (trackIndex >= 0) {
213
+ // Load the entire playlist into TrackPlayerCore
214
+ trackPlayerCore?.loadPlaylist(playlistId)
215
+
216
+ // Create MediaItems for the entire playlist
217
+ val playlistMediaItems =
218
+ playlist.tracks
219
+ .map { track ->
220
+ val trackMediaId = "$playlistId:${track.id}"
221
+ createMediaItem(track, trackMediaId)
222
+ }.toMutableList()
223
+
224
+ println("✅ MediaSessionManager: Loaded ${playlistMediaItems.size} tracks, starting at index $trackIndex")
225
+
226
+ // Return the full playlist with the correct start index
227
+ return Futures.immediateFuture(
228
+ MediaSession.MediaItemsWithStartPosition(
229
+ playlistMediaItems,
230
+ trackIndex,
231
+ startPositionMs,
232
+ ),
233
+ )
234
+ } else {
235
+ println("⚠️ MediaSessionManager: Track not found in playlist")
236
+ }
237
+ } else {
238
+ println("⚠️ MediaSessionManager: Playlist not found")
239
+ }
240
+ }
241
+ } catch (e: Exception) {
242
+ println("❌ MediaSessionManager: Error in onSetMediaItems - ${e.message}")
243
+ e.printStackTrace()
244
+ }
245
+
246
+ // Fallback: use the provided media items
247
+ println("🎵 MediaSessionManager: Using fallback - provided media items")
248
+ return Futures.immediateFuture(
249
+ MediaSession.MediaItemsWithStartPosition(
250
+ mediaItems,
251
+ startIndex,
252
+ startPositionMs,
253
+ ),
254
+ )
255
+ }
256
+
257
+ override fun onCustomCommand(
258
+ session: MediaSession,
259
+ controller: MediaSession.ControllerInfo,
260
+ customCommand: SessionCommand,
261
+ args: android.os.Bundle,
262
+ ): ListenableFuture<SessionResult> {
263
+ // Handle custom commands if needed
264
+ return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
265
+ }
266
+ },
267
+ ).build()
268
+ // MediaSession is active by default in Media3
269
+ updateMediaSessionMetadata()
270
+ } catch (e: Exception) {
271
+ e.printStackTrace()
272
+ }
273
+ }
274
+
275
+ private fun updateMediaSessionMetadata() {
276
+ // MediaSession will automatically use the metadata from player's current MediaItem
277
+ // No need to manually update here as TrackPlayerCore already sets metadata
278
+ }
279
+
280
+ private suspend fun loadArtworkBitmap(artworkUrl: String?): Bitmap? {
281
+ if (artworkUrl.isNullOrEmpty()) return null
282
+
283
+ // Check cache first
284
+ artworkCache[artworkUrl]?.let { return it }
285
+
286
+ return try {
287
+ val bitmap =
288
+ withContext(Dispatchers.IO) {
289
+ val url = URL(artworkUrl)
290
+ BitmapFactory.decodeStream(url.openConnection().getInputStream())
291
+ }
292
+ // Cache the bitmap
293
+ if (bitmap != null) {
294
+ artworkCache[artworkUrl] = bitmap
295
+ }
296
+ bitmap
297
+ } catch (e: Exception) {
298
+ e.printStackTrace()
299
+ null
300
+ }
301
+ }
302
+
303
+ private fun bitmapToByteArray(bitmap: Bitmap): ByteArray {
304
+ val stream = java.io.ByteArrayOutputStream()
305
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
306
+ return stream.toByteArray()
307
+ }
308
+
309
+ private fun createNotificationChannel() {
310
+ notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
311
+
312
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
313
+ val channel =
314
+ NotificationChannel(
315
+ CHANNEL_ID,
316
+ CHANNEL_NAME,
317
+ NotificationManager.IMPORTANCE_LOW,
318
+ ).apply {
319
+ description = "Media playback controls"
320
+ setShowBadge(false)
321
+ lockscreenVisibility = Notification.VISIBILITY_PUBLIC
322
+ }
323
+
324
+ notificationManager?.createNotificationChannel(channel)
325
+ }
326
+ }
327
+
328
+ private fun getCurrentTrack(): TrackItem? {
329
+ val currentMediaItem = player.currentMediaItem ?: return null
330
+ val mediaId = currentMediaItem.mediaId
331
+
332
+ // Parse mediaId format: "playlistId:trackId" or just "trackId"
333
+ val trackId =
334
+ if (mediaId.contains(':')) {
335
+ mediaId.substring(mediaId.indexOf(':') + 1)
336
+ } else {
337
+ mediaId
338
+ }
339
+
340
+ // Find track in current playlist or all playlists
341
+ return trackPlayerCore?.getCurrentPlaylistId()?.let { playlistId ->
342
+ playlistManager.getPlaylist(playlistId)?.tracks?.find { it.id == trackId }
343
+ } ?: playlistManager
344
+ .getAllPlaylists()
345
+ .flatMap { it.tracks }
346
+ .find { it.id == trackId }
347
+ }
348
+
349
+ private fun updateNotification() {
350
+ if (!showInNotification) return
351
+
352
+ val currentTrack = getCurrentTrack()
353
+ val notification = buildNotification(currentTrack)
354
+ notificationManager?.notify(NOTIFICATION_ID, notification)
355
+ }
356
+
357
+ private fun buildNotification(track: TrackItem?): Notification {
358
+ val mediaSession = this.mediaSession ?: return createEmptyNotification()
359
+
360
+ // Launch intent
361
+ val contentIntent =
362
+ PendingIntent.getActivity(
363
+ context,
364
+ 0,
365
+ context.packageManager.getLaunchIntentForPackage(context.packageName),
366
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
367
+ )
368
+
369
+ val builder =
370
+ NotificationCompat
371
+ .Builder(context, CHANNEL_ID)
372
+ .setContentTitle(track?.title ?: "Unknown Title")
373
+ .setContentText(track?.artist ?: "Unknown Artist")
374
+ .setSubText(track?.album ?: "")
375
+ .setSmallIcon(android.R.drawable.ic_media_play)
376
+ .setContentIntent(contentIntent)
377
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
378
+ .setOngoing(player.isPlaying)
379
+ .setShowWhen(false)
380
+ .setPriority(NotificationCompat.PRIORITY_LOW)
381
+ .setCategory(NotificationCompat.CATEGORY_TRANSPORT)
382
+ .setStyle(
383
+ androidx.media.app.NotificationCompat
384
+ .MediaStyle()
385
+ .setMediaSession(mediaSession.sessionCompatToken)
386
+ .setShowActionsInCompactView(0, 1, 2),
387
+ )
388
+
389
+ // Add action buttons
390
+ builder.addAction(
391
+ android.R.drawable.ic_media_previous,
392
+ "Previous",
393
+ createMediaAction(ACTION_PREVIOUS),
394
+ )
395
+
396
+ if (player.isPlaying) {
397
+ builder.addAction(
398
+ android.R.drawable.ic_media_pause,
399
+ "Pause",
400
+ createMediaAction(ACTION_PAUSE),
401
+ )
402
+ } else {
403
+ builder.addAction(
404
+ android.R.drawable.ic_media_play,
405
+ "Play",
406
+ createMediaAction(ACTION_PLAY),
407
+ )
408
+ }
409
+
410
+ builder.addAction(
411
+ android.R.drawable.ic_media_next,
412
+ "Next",
413
+ createMediaAction(ACTION_NEXT),
414
+ )
415
+
416
+ // Load artwork asynchronously and update notification
417
+ track?.artwork?.asSecondOrNull()?.let { artworkUrl ->
418
+ scope.launch {
419
+ val bitmap = loadArtworkBitmap(artworkUrl)
420
+ if (bitmap != null) {
421
+ builder.setLargeIcon(bitmap)
422
+ notificationManager?.notify(NOTIFICATION_ID, builder.build())
423
+ }
424
+ }
425
+ }
426
+
427
+ return builder.build()
428
+ }
429
+
430
+ private fun createEmptyNotification(): Notification =
431
+ NotificationCompat
432
+ .Builder(context, CHANNEL_ID)
433
+ .setContentTitle("Music Player")
434
+ .setSmallIcon(android.R.drawable.ic_media_play)
435
+ .build()
436
+
437
+ private fun createMediaAction(action: String): PendingIntent {
438
+ val intent =
439
+ Intent(action).apply {
440
+ setPackage(context.packageName)
441
+ }
442
+ return PendingIntent.getBroadcast(
443
+ context,
444
+ 0,
445
+ intent,
446
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
447
+ )
448
+ }
449
+
450
+ private fun hideNotification() {
451
+ notificationManager?.cancel(NOTIFICATION_ID)
452
+ }
453
+
454
+ fun onTrackChanged() {
455
+ // Preload artwork for better notification display
456
+ val currentTrack = getCurrentTrack()
457
+ if (currentTrack != null) {
458
+ scope.launch {
459
+ currentTrack.artwork?.asSecondOrNull()?.let { artworkUrl ->
460
+ loadArtworkBitmap(artworkUrl)
461
+ }
462
+ updateNotification()
463
+ }
464
+ } else {
465
+ updateNotification()
466
+ }
467
+ }
468
+
469
+ fun onPlaybackStateChanged() {
470
+ updateNotification()
471
+ }
472
+
473
+ fun release() {
474
+ hideNotification()
475
+ mediaSession?.release()
476
+ mediaSession = null
477
+ artworkCache.clear()
478
+ }
479
+
480
+ private fun createMediaItem(
481
+ track: TrackItem,
482
+ mediaId: String,
483
+ ): MediaItem {
484
+ val metadataBuilder =
485
+ MediaMetadata
486
+ .Builder()
487
+ .setTitle(track.title)
488
+ .setArtist(track.artist)
489
+ .setAlbumTitle(track.album)
490
+
491
+ track.artwork?.asSecondOrNull()?.let { artworkUrl ->
492
+ try {
493
+ metadataBuilder.setArtworkUri(Uri.parse(artworkUrl))
494
+ } catch (e: Exception) {
495
+ println("⚠️ MediaSessionManager: Invalid artwork URI: $artworkUrl")
496
+ }
497
+ }
498
+
499
+ return MediaItem
500
+ .Builder()
501
+ .setMediaId(mediaId)
502
+ .setUri(track.url)
503
+ .setMediaMetadata(metadataBuilder.build())
504
+ .build()
505
+ }
506
+ }
@@ -0,0 +1,21 @@
1
+ package com.margelo.nitro.nitroplayer.playlist
2
+
3
+ import com.margelo.nitro.nitroplayer.TrackItem
4
+
5
+ /**
6
+ * Represents a playlist containing multiple tracks
7
+ * Uses ExoPlayer's native playlist functionality
8
+ */
9
+ data class Playlist(
10
+ val id: String,
11
+ val name: String,
12
+ val description: String? = null,
13
+ val artwork: String? = null,
14
+ val tracks: MutableList<TrackItem> = mutableListOf(),
15
+ ) {
16
+ fun toTrackItemArray(): Array<TrackItem> = tracks.toTypedArray()
17
+
18
+ fun getTrackCount(): Int = tracks.size
19
+
20
+ fun isEmpty(): Boolean = tracks.isEmpty()
21
+ }