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,639 @@
1
+ @file:Suppress("ktlint:standard:max-line-length", "ktlint:standard:if-else-wrapping")
2
+
3
+ package com.margelo.nitro.nitroplayer.core
4
+
5
+ import android.content.Context
6
+ import android.net.Uri
7
+ import androidx.media3.common.MediaItem
8
+ import androidx.media3.common.MediaMetadata
9
+ import androidx.media3.common.Player
10
+ import androidx.media3.exoplayer.DefaultLoadControl
11
+ import androidx.media3.exoplayer.ExoPlayer
12
+ import com.margelo.nitro.core.NullType
13
+ import com.margelo.nitro.nitroplayer.NitroPlayerPackage
14
+ import com.margelo.nitro.nitroplayer.PlayerState
15
+ import com.margelo.nitro.nitroplayer.Reason
16
+ import com.margelo.nitro.nitroplayer.TrackItem
17
+ import com.margelo.nitro.nitroplayer.TrackPlayerState
18
+ import com.margelo.nitro.nitroplayer.Variant_NullType_String
19
+ import com.margelo.nitro.nitroplayer.Variant_NullType_TrackItem
20
+ import com.margelo.nitro.nitroplayer.connection.AndroidAutoConnectionDetector
21
+ import com.margelo.nitro.nitroplayer.media.MediaLibrary
22
+ import com.margelo.nitro.nitroplayer.media.MediaLibraryManager
23
+ import com.margelo.nitro.nitroplayer.media.MediaLibraryParser
24
+ import com.margelo.nitro.nitroplayer.media.MediaSessionManager
25
+ import com.margelo.nitro.nitroplayer.media.NitroPlayerMediaBrowserService
26
+ import com.margelo.nitro.nitroplayer.playlist.PlaylistManager
27
+ import java.util.concurrent.CountDownLatch
28
+ import java.util.concurrent.TimeUnit
29
+
30
+ class TrackPlayerCore private constructor(
31
+ private val context: Context,
32
+ ) {
33
+ private val handler = android.os.Handler(android.os.Looper.getMainLooper())
34
+ private lateinit var player: ExoPlayer
35
+ private val playlistManager = PlaylistManager.getInstance(context)
36
+ private val mediaLibraryManager = MediaLibraryManager.getInstance(context)
37
+ private var mediaSessionManager: MediaSessionManager? = null
38
+ private var currentPlaylistId: String? = null
39
+ private var isManuallySeeked = false
40
+ private var isAndroidAutoConnected: Boolean = false
41
+ private var androidAutoConnectionDetector: AndroidAutoConnectionDetector? = null
42
+ var onAndroidAutoConnectionChange: ((Boolean) -> Unit)? = null
43
+ private val progressUpdateRunnable =
44
+ object : Runnable {
45
+ override fun run() {
46
+ if (::player.isInitialized && player.playbackState != Player.STATE_IDLE) {
47
+ val position = player.currentPosition / 1000.0
48
+ val duration = if (player.duration > 0) player.duration / 1000.0 else 0.0
49
+ onPlaybackProgressChange?.invoke(position, duration, if (isManuallySeeked) true else null)
50
+ isManuallySeeked = false
51
+ }
52
+ handler.postDelayed(this, 250) // Update every 250ms
53
+ }
54
+ }
55
+
56
+ var onChangeTrack: ((TrackItem, Reason?) -> Unit)? = null
57
+ var onPlaybackStateChange: ((TrackPlayerState, Reason?) -> Unit)? = null
58
+ var onSeek: ((Double, Double) -> Unit)? = null
59
+ var onPlaybackProgressChange: ((Double, Double, Boolean?) -> Unit)? = null
60
+
61
+ companion object {
62
+ @Volatile
63
+ @Suppress("ktlint:standard:property-naming")
64
+ private var INSTANCE: TrackPlayerCore? = null
65
+
66
+ fun getInstance(context: Context): TrackPlayerCore =
67
+ INSTANCE ?: synchronized(this) {
68
+ INSTANCE ?: TrackPlayerCore(context).also { INSTANCE = it }
69
+ }
70
+ }
71
+
72
+ init {
73
+ handler.post {
74
+ // Configure LoadControl for gapless playback
75
+ // This enables pre-buffering of the next track for seamless transitions
76
+ val loadControl =
77
+ DefaultLoadControl
78
+ .Builder()
79
+ .setBufferDurationsMs(
80
+ DefaultLoadControl.DEFAULT_MIN_BUFFER_MS, // Minimum buffer: 1.5s
81
+ DefaultLoadControl.DEFAULT_MAX_BUFFER_MS, // Maximum buffer: 5s
82
+ DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS, // Buffer for playback: 2.5s
83
+ DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, // Buffer after rebuffer: 5s
84
+ ).setBackBuffer(DefaultLoadControl.DEFAULT_BACK_BUFFER_DURATION_MS, true) // Keep back buffer for seamless transitions
85
+ .setPrioritizeTimeOverSizeThresholds(true) // Prioritize time-based buffering
86
+ .build()
87
+
88
+ player =
89
+ ExoPlayer
90
+ .Builder(context)
91
+ .setLoadControl(loadControl)
92
+ .build()
93
+ mediaSessionManager =
94
+ MediaSessionManager(context, player, playlistManager).apply {
95
+ setTrackPlayerCore(this@TrackPlayerCore)
96
+ }
97
+
98
+ // Set references for MediaBrowserService
99
+ NitroPlayerMediaBrowserService.trackPlayerCore = this
100
+ NitroPlayerMediaBrowserService.mediaSessionManager = mediaSessionManager
101
+
102
+ // Initialize Android Auto connection detector
103
+ androidAutoConnectionDetector =
104
+ AndroidAutoConnectionDetector(context).apply {
105
+ onConnectionChanged = { connected, connectionType ->
106
+ handler.post {
107
+ isAndroidAutoConnected = connected
108
+ NitroPlayerMediaBrowserService.isAndroidAutoConnected = connected
109
+
110
+ // Notify JavaScript
111
+ onAndroidAutoConnectionChange?.invoke(connected)
112
+
113
+ println("🚗 Android Auto connection changed: connected=$connected, type=$connectionType")
114
+ }
115
+ }
116
+ registerCarConnectionReceiver()
117
+ }
118
+
119
+ player.addListener(
120
+ object : Player.Listener {
121
+ override fun onMediaItemTransition(
122
+ mediaItem: MediaItem?,
123
+ reason: Int,
124
+ ) {
125
+ // Handle playlist switching if needed
126
+ mediaItem?.mediaId?.let { mediaId ->
127
+ if (mediaId.contains(':')) {
128
+ val colonIndex = mediaId.indexOf(':')
129
+ val playlistId = mediaId.substring(0, colonIndex)
130
+ if (playlistId != currentPlaylistId) {
131
+ // Track from different playlist - ensure playlist is loaded
132
+ val playlist = playlistManager.getPlaylist(playlistId)
133
+ if (playlist != null && currentPlaylistId != playlistId) {
134
+ // This shouldn't happen if playlists are loaded correctly,
135
+ // but handle it as a safety measure
136
+ println(
137
+ "⚠️ TrackPlayerCore: Detected track from different playlist, updating...",
138
+ )
139
+ }
140
+ }
141
+ }
142
+ }
143
+
144
+ val track = findTrack(mediaItem)
145
+ if (track != null) {
146
+ val r =
147
+ when (reason) {
148
+ Player.MEDIA_ITEM_TRANSITION_REASON_AUTO -> Reason.END
149
+ Player.MEDIA_ITEM_TRANSITION_REASON_SEEK -> Reason.USER_ACTION
150
+ Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED -> Reason.USER_ACTION
151
+ else -> null
152
+ }
153
+ onChangeTrack?.invoke(track, r)
154
+ mediaSessionManager?.onTrackChanged()
155
+ }
156
+ }
157
+
158
+ override fun onTimelineChanged(
159
+ timeline: androidx.media3.common.Timeline,
160
+ reason: Int,
161
+ ) {
162
+ if (reason == Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) {
163
+ // Playlist changed - update MediaBrowserService
164
+ NitroPlayerMediaBrowserService.getInstance()?.onPlaylistsUpdated()
165
+ }
166
+ }
167
+
168
+ override fun onPlayWhenReadyChanged(
169
+ playWhenReady: Boolean,
170
+ reason: Int,
171
+ ) {
172
+ val r =
173
+ when (reason) {
174
+ Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST -> Reason.USER_ACTION
175
+ else -> null
176
+ }
177
+ emitStateChange(r)
178
+ }
179
+
180
+ override fun onPlaybackStateChanged(playbackState: Int) {
181
+ emitStateChange()
182
+ }
183
+
184
+ override fun onIsPlayingChanged(isPlaying: Boolean) {
185
+ emitStateChange()
186
+ }
187
+
188
+ override fun onPositionDiscontinuity(
189
+ oldPosition: Player.PositionInfo,
190
+ newPosition: Player.PositionInfo,
191
+ reason: Int,
192
+ ) {
193
+ if (reason == Player.DISCONTINUITY_REASON_SEEK) {
194
+ isManuallySeeked = true
195
+ onSeek?.invoke(newPosition.positionMs / 1000.0, player.duration / 1000.0)
196
+ }
197
+ }
198
+ },
199
+ )
200
+
201
+ // Start progress updates
202
+ handler.post(progressUpdateRunnable)
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Load a playlist for playback using ExoPlayer's native playlist API
208
+ * Based on: https://developer.android.com/media/media3/exoplayer/playlists
209
+ */
210
+ fun loadPlaylist(playlistId: String) {
211
+ handler.post {
212
+ val playlist = playlistManager.getPlaylist(playlistId)
213
+ if (playlist != null) {
214
+ currentPlaylistId = playlistId
215
+ updatePlayerQueue(playlist.tracks)
216
+ }
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Play a specific track from a playlist (for Android Auto)
222
+ * MediaId format: "playlistId:trackId"
223
+ */
224
+ fun playFromPlaylistTrack(mediaId: String) {
225
+ handler.post {
226
+ try {
227
+ // Parse mediaId: "playlistId:trackId"
228
+ val colonIndex = mediaId.indexOf(':')
229
+ if (colonIndex > 0 && colonIndex < mediaId.length - 1) {
230
+ val playlistId = mediaId.substring(0, colonIndex)
231
+ val trackId = mediaId.substring(colonIndex + 1)
232
+
233
+ val playlist = playlistManager.getPlaylist(playlistId)
234
+ if (playlist != null) {
235
+ val trackIndex = playlist.tracks.indexOfFirst { it.id == trackId }
236
+ if (trackIndex >= 0) {
237
+ // Load playlist if not already loaded
238
+ if (currentPlaylistId != playlistId) {
239
+ loadPlaylist(playlistId)
240
+ // Wait a bit for playlist to load, then seek
241
+ handler.postDelayed({
242
+ playFromIndex(trackIndex)
243
+ }, 100)
244
+ } else {
245
+ // Playlist already loaded, just seek to track
246
+ playFromIndex(trackIndex)
247
+ }
248
+ }
249
+ }
250
+ }
251
+ } catch (e: Exception) {
252
+ println("❌ TrackPlayerCore: Error playing from playlist track - ${e.message}")
253
+ e.printStackTrace()
254
+ }
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Update the player queue when playlist changes
260
+ */
261
+ fun updatePlaylist(playlistId: String) {
262
+ handler.post {
263
+ if (currentPlaylistId == playlistId) {
264
+ val playlist = playlistManager.getPlaylist(playlistId)
265
+ if (playlist != null) {
266
+ updatePlayerQueue(playlist.tracks)
267
+ }
268
+ }
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Get current playlist ID
274
+ */
275
+ fun getCurrentPlaylistId(): String? = currentPlaylistId
276
+
277
+ /**
278
+ * Get playlist manager (for access from other classes like Google Cast)
279
+ */
280
+ fun getPlaylistManager(): PlaylistManager = playlistManager
281
+
282
+ private fun emitStateChange(reason: Reason? = null) {
283
+ val state =
284
+ when (player.playbackState) {
285
+ Player.STATE_IDLE -> TrackPlayerState.STOPPED
286
+ Player.STATE_BUFFERING -> if (player.playWhenReady) TrackPlayerState.PLAYING else TrackPlayerState.PAUSED
287
+ Player.STATE_READY -> if (player.isPlaying) TrackPlayerState.PLAYING else TrackPlayerState.PAUSED
288
+ Player.STATE_ENDED -> TrackPlayerState.STOPPED
289
+ else -> TrackPlayerState.STOPPED
290
+ }
291
+
292
+ val actualReason = reason ?: if (player.playbackState == Player.STATE_ENDED) Reason.END else null
293
+ onPlaybackStateChange?.invoke(state, actualReason)
294
+ mediaSessionManager?.onPlaybackStateChanged()
295
+ }
296
+
297
+ private fun updatePlayerQueue(tracks: List<TrackItem>) {
298
+ // Create MediaItems with playlist info in mediaId for Android Auto
299
+ val mediaItems =
300
+ tracks.mapIndexed { index, track ->
301
+ val playlistId = currentPlaylistId ?: ""
302
+ // Format: "playlistId:trackId" so we can identify playlist and track
303
+ val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
304
+ track.toMediaItem(mediaId)
305
+ }
306
+
307
+ player.setMediaItems(mediaItems, false)
308
+ if (player.playbackState == Player.STATE_IDLE && mediaItems.isNotEmpty()) {
309
+ player.prepare()
310
+ }
311
+ }
312
+
313
+ private fun TrackItem.toMediaItem(customMediaId: String? = null): MediaItem {
314
+ val metadataBuilder =
315
+ MediaMetadata
316
+ .Builder()
317
+ .setTitle(title)
318
+ .setArtist(artist)
319
+ .setAlbumTitle(album)
320
+
321
+ artwork?.asSecondOrNull()?.let { artworkUrl ->
322
+ try {
323
+ metadataBuilder.setArtworkUri(Uri.parse(artworkUrl))
324
+ } catch (e: Exception) {
325
+ // Ignore invalid artwork URI
326
+ }
327
+ }
328
+
329
+ return MediaItem
330
+ .Builder()
331
+ .setMediaId(customMediaId ?: id)
332
+ .setUri(url)
333
+ .setMediaMetadata(metadataBuilder.build())
334
+ .build()
335
+ }
336
+
337
+ private fun findTrack(mediaItem: MediaItem?): TrackItem? {
338
+ if (mediaItem == null) return null
339
+
340
+ val mediaId = mediaItem.mediaId
341
+ val trackId =
342
+ if (mediaId.contains(':')) {
343
+ // Format: "playlistId:trackId"
344
+ mediaId.substring(mediaId.indexOf(':') + 1)
345
+ } else {
346
+ mediaId
347
+ }
348
+
349
+ val playlist = currentPlaylistId?.let { playlistManager.getPlaylist(it) }
350
+ return playlist?.tracks?.find { it.id == trackId }
351
+ }
352
+
353
+ fun play() {
354
+ handler.post { player.play() }
355
+ }
356
+
357
+ fun pause() {
358
+ handler.post { player.pause() }
359
+ }
360
+
361
+ fun playSong(
362
+ songId: String,
363
+ fromPlaylist: String?,
364
+ ) {
365
+ println("🎵 TrackPlayerCore: playSong() called - songId: $songId, fromPlaylist: $fromPlaylist")
366
+
367
+ handler.post {
368
+ var targetPlaylistId: String? = null
369
+ var songIndex: Int = -1
370
+
371
+ // Case 1: If fromPlaylist is provided, use that playlist
372
+ if (fromPlaylist != null) {
373
+ println("🎵 TrackPlayerCore: Looking for song in specified playlist: $fromPlaylist")
374
+ val playlist = playlistManager.getPlaylist(fromPlaylist)
375
+ if (playlist != null) {
376
+ songIndex = playlist.tracks.indexOfFirst { it.id == songId }
377
+ if (songIndex >= 0) {
378
+ targetPlaylistId = fromPlaylist
379
+ println("✅ Found song at index $songIndex in playlist $fromPlaylist")
380
+ } else {
381
+ println("⚠️ Song $songId not found in specified playlist $fromPlaylist")
382
+ return@post
383
+ }
384
+ } else {
385
+ println("⚠️ Playlist $fromPlaylist not found")
386
+ return@post
387
+ }
388
+ }
389
+ // Case 2: If fromPlaylist is not provided, search in current/loaded playlist first
390
+ else {
391
+ println("🎵 TrackPlayerCore: No playlist specified, checking current playlist")
392
+
393
+ // Check if song exists in currently loaded playlist
394
+ if (currentPlaylistId != null) {
395
+ val currentPlaylist = playlistManager.getPlaylist(currentPlaylistId!!)
396
+ if (currentPlaylist != null) {
397
+ songIndex = currentPlaylist.tracks.indexOfFirst { it.id == songId }
398
+ if (songIndex >= 0) {
399
+ targetPlaylistId = currentPlaylistId
400
+ println("✅ Found song at index $songIndex in current playlist $currentPlaylistId")
401
+ }
402
+ }
403
+ }
404
+
405
+ // If not found in current playlist, search in all playlists
406
+ if (songIndex == -1) {
407
+ println("🔍 Song not found in current playlist, searching all playlists...")
408
+ val allPlaylists = playlistManager.getAllPlaylists()
409
+
410
+ for (playlist in allPlaylists) {
411
+ songIndex = playlist.tracks.indexOfFirst { it.id == songId }
412
+ if (songIndex >= 0) {
413
+ targetPlaylistId = playlist.id
414
+ println("✅ Found song at index $songIndex in playlist ${playlist.id}")
415
+ break
416
+ }
417
+ }
418
+
419
+ // If still not found, just use the first playlist if available
420
+ if (songIndex == -1 && allPlaylists.isNotEmpty()) {
421
+ targetPlaylistId = allPlaylists[0].id
422
+ songIndex = 0
423
+ println("⚠️ Song not found in any playlist, using first playlist and starting at index 0")
424
+ }
425
+ }
426
+ }
427
+
428
+ // Now play the song
429
+ if (targetPlaylistId == null || songIndex < 0) {
430
+ println("❌ Could not determine playlist or song index")
431
+ return@post
432
+ }
433
+
434
+ // Load playlist if it's different from current
435
+ if (currentPlaylistId != targetPlaylistId) {
436
+ println("🔄 Loading new playlist: $targetPlaylistId")
437
+ val playlist = playlistManager.getPlaylist(targetPlaylistId)
438
+ if (playlist != null) {
439
+ currentPlaylistId = targetPlaylistId
440
+ updatePlayerQueue(playlist.tracks)
441
+
442
+ // Wait a bit for playlist to load, then play from index
443
+ handler.postDelayed({
444
+ println("▶️ Playing from index: $songIndex")
445
+ playFromIndex(songIndex)
446
+ }, 100)
447
+ }
448
+ } else {
449
+ // Playlist already loaded, just play from index
450
+ println("▶️ Playing from index: $songIndex")
451
+ playFromIndex(songIndex)
452
+ }
453
+ }
454
+ }
455
+
456
+ fun skipToNext() {
457
+ handler.post {
458
+ if (player.hasNextMediaItem()) {
459
+ player.seekToNextMediaItem()
460
+ }
461
+ }
462
+ }
463
+
464
+ fun skipToPrevious() {
465
+ handler.post {
466
+ if (player.hasPreviousMediaItem()) {
467
+ player.seekToPreviousMediaItem()
468
+ }
469
+ }
470
+ }
471
+
472
+ fun seek(position: Double) {
473
+ handler.post {
474
+ isManuallySeeked = true
475
+ player.seekTo((position * 1000).toLong())
476
+ }
477
+ }
478
+
479
+ fun getState(): PlayerState {
480
+ // Check if we're already on the main thread
481
+ if (android.os.Looper.myLooper() == handler.looper) {
482
+ return getStateInternal()
483
+ }
484
+
485
+ // Use CountDownLatch to wait for the result on the main thread
486
+ val latch = CountDownLatch(1)
487
+ var result: PlayerState? = null
488
+
489
+ handler.post {
490
+ try {
491
+ result = getStateInternal()
492
+ } finally {
493
+ latch.countDown()
494
+ }
495
+ }
496
+
497
+ try {
498
+ // Wait up to 5 seconds for the result
499
+ latch.await(5, TimeUnit.SECONDS)
500
+ } catch (e: InterruptedException) {
501
+ Thread.currentThread().interrupt()
502
+ }
503
+
504
+ return result ?: getStateInternal()
505
+ }
506
+
507
+ private fun getStateInternal(): PlayerState =
508
+ if (::player.isInitialized) {
509
+ val currentMediaItem = player.currentMediaItem
510
+ val track =
511
+ if (currentMediaItem != null) {
512
+ findTrack(currentMediaItem)
513
+ } else {
514
+ null
515
+ }
516
+
517
+ // Convert nullable TrackItem to Variant_NullType_TrackItem
518
+ val currentTrack: Variant_NullType_TrackItem? =
519
+ if (track != null) {
520
+ Variant_NullType_TrackItem.create(track)
521
+ } else {
522
+ null
523
+ }
524
+
525
+ val currentPosition = player.currentPosition / 1000.0
526
+ val totalDuration = if (player.duration > 0) player.duration / 1000.0 else 0.0
527
+
528
+ val currentState =
529
+ when (player.playbackState) {
530
+ Player.STATE_IDLE -> TrackPlayerState.STOPPED
531
+ Player.STATE_BUFFERING -> if (player.playWhenReady) TrackPlayerState.PLAYING else TrackPlayerState.PAUSED
532
+ Player.STATE_READY -> if (player.isPlaying) TrackPlayerState.PLAYING else TrackPlayerState.PAUSED
533
+ Player.STATE_ENDED -> TrackPlayerState.STOPPED
534
+ else -> TrackPlayerState.STOPPED
535
+ }
536
+
537
+ // Get current playlist
538
+ val currentPlaylist = currentPlaylistId?.let { playlistManager.getPlaylist(it) }
539
+
540
+ // Use ExoPlayer's currentMediaItemIndex
541
+ val currentIndex =
542
+ if (player.currentMediaItemIndex >= 0) {
543
+ player.currentMediaItemIndex.toDouble()
544
+ } else {
545
+ -1.0
546
+ }
547
+
548
+ PlayerState(
549
+ currentTrack = currentTrack,
550
+ currentPosition = currentPosition,
551
+ totalDuration = totalDuration,
552
+ currentState = currentState,
553
+ currentPlaylistId = currentPlaylistId?.let { Variant_NullType_String.create(it) },
554
+ currentIndex = currentIndex,
555
+ )
556
+ } else {
557
+ // Return default state if player is not initialized
558
+ PlayerState(
559
+ currentTrack = null,
560
+ currentPosition = 0.0,
561
+ totalDuration = 0.0,
562
+ currentState = TrackPlayerState.STOPPED,
563
+ currentPlaylistId = currentPlaylistId?.let { Variant_NullType_String.create(it) },
564
+ currentIndex = -1.0,
565
+ )
566
+ }
567
+
568
+ fun configure(
569
+ androidAutoEnabled: Boolean?,
570
+ carPlayEnabled: Boolean?,
571
+ showInNotification: Boolean?,
572
+ ) {
573
+ handler.post {
574
+ androidAutoEnabled?.let {
575
+ NitroPlayerMediaBrowserService.isAndroidAutoEnabled = it
576
+ }
577
+ mediaSessionManager?.configure(
578
+ androidAutoEnabled,
579
+ carPlayEnabled,
580
+ showInNotification,
581
+ )
582
+ }
583
+ }
584
+
585
+ // Public method to get all playlists (for MediaBrowserService and other classes)
586
+ fun getAllPlaylists(): List<com.margelo.nitro.nitroplayer.playlist.Playlist> = playlistManager.getAllPlaylists()
587
+
588
+ // Public method to get current track for MediaBrowserService
589
+ fun getCurrentTrack(): TrackItem? {
590
+ if (!::player.isInitialized) return null
591
+ val currentMediaItem = player.currentMediaItem ?: return null
592
+ return findTrack(currentMediaItem)
593
+ }
594
+
595
+ // Public method to play from a specific index (for Android Auto)
596
+ fun playFromIndex(index: Int) {
597
+ handler.post {
598
+ if (::player.isInitialized && index >= 0 && index < player.mediaItemCount) {
599
+ player.seekToDefaultPosition(index)
600
+ player.playWhenReady = true
601
+ }
602
+ }
603
+ }
604
+
605
+ // Clean up resources
606
+ fun destroy() {
607
+ handler.post {
608
+ androidAutoConnectionDetector?.unregisterCarConnectionReceiver()
609
+ handler.removeCallbacks(progressUpdateRunnable)
610
+ }
611
+ }
612
+
613
+ // Check if Android Auto is connected
614
+ fun isAndroidAutoConnected(): Boolean = isAndroidAutoConnected
615
+
616
+ // Set the Android Auto media library structure from JSON
617
+ fun setAndroidAutoMediaLibrary(libraryJson: String) {
618
+ handler.post {
619
+ try {
620
+ val library = MediaLibraryParser.fromJson(libraryJson)
621
+ mediaLibraryManager.setMediaLibrary(library)
622
+ // Notify Android Auto to refresh
623
+ NitroPlayerMediaBrowserService.getInstance()?.onPlaylistsUpdated()
624
+ println("✅ TrackPlayerCore: Android Auto media library set successfully")
625
+ } catch (e: Exception) {
626
+ println("❌ TrackPlayerCore: Error setting media library - ${e.message}")
627
+ e.printStackTrace()
628
+ }
629
+ }
630
+ }
631
+
632
+ // Clear the Android Auto media library
633
+ fun clearAndroidAutoMediaLibrary() {
634
+ handler.post {
635
+ mediaLibraryManager.clear()
636
+ NitroPlayerMediaBrowserService.getInstance()?.onPlaylistsUpdated()
637
+ }
638
+ }
639
+ }