react-native-nitro-player 0.3.0-alpha.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/README.md +444 -4
  2. package/android/build.gradle +4 -1
  3. package/android/src/main/AndroidManifest.xml +16 -1
  4. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +2 -0
  5. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +8 -0
  6. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +225 -0
  7. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +105 -0
  8. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +6 -6
  9. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +37 -12
  10. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +970 -213
  11. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +475 -0
  12. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadFileManager.kt +159 -0
  13. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadManagerCore.kt +489 -0
  14. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadWorker.kt +209 -0
  15. package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +486 -0
  16. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaBrowserService.kt +3 -1
  17. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +14 -6
  18. package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +27 -0
  19. package/ios/HybridDownloadManager.swift +226 -0
  20. package/ios/HybridEqualizer.swift +111 -0
  21. package/ios/HybridTrackPlayer.swift +36 -8
  22. package/ios/core/TrackPlayerCore.swift +998 -276
  23. package/ios/download/DownloadDatabase.swift +493 -0
  24. package/ios/download/DownloadFileManager.swift +241 -0
  25. package/ios/download/DownloadManagerCore.swift +923 -0
  26. package/ios/equalizer/EqualizerCore.swift +685 -0
  27. package/ios/media/MediaSessionManager.swift +40 -28
  28. package/ios/playlist/PlaylistManager.swift +40 -9
  29. package/ios/queue/HybridPlayerQueue.swift +33 -13
  30. package/lib/hooks/downloadCallbackManager.d.ts +36 -0
  31. package/lib/hooks/downloadCallbackManager.js +108 -0
  32. package/lib/hooks/equalizerCallbackManager.d.ts +37 -0
  33. package/lib/hooks/equalizerCallbackManager.js +109 -0
  34. package/lib/hooks/index.d.ts +16 -0
  35. package/lib/hooks/index.js +10 -0
  36. package/lib/hooks/useActualQueue.d.ts +48 -0
  37. package/lib/hooks/useActualQueue.js +98 -0
  38. package/lib/hooks/useDownloadActions.d.ts +26 -0
  39. package/lib/hooks/useDownloadActions.js +117 -0
  40. package/lib/hooks/useDownloadProgress.d.ts +25 -0
  41. package/lib/hooks/useDownloadProgress.js +79 -0
  42. package/lib/hooks/useDownloadStorage.d.ts +19 -0
  43. package/lib/hooks/useDownloadStorage.js +60 -0
  44. package/lib/hooks/useDownloadedTracks.d.ts +25 -0
  45. package/lib/hooks/useDownloadedTracks.js +69 -0
  46. package/lib/hooks/useEqualizer.d.ts +25 -0
  47. package/lib/hooks/useEqualizer.js +124 -0
  48. package/lib/hooks/useEqualizerPresets.d.ts +22 -0
  49. package/lib/hooks/useEqualizerPresets.js +96 -0
  50. package/lib/hooks/useNowPlaying.js +3 -2
  51. package/lib/hooks/useOnChangeTrack.js +15 -12
  52. package/lib/hooks/useOnPlaybackStateChange.js +16 -13
  53. package/lib/hooks/usePlaylist.d.ts +48 -0
  54. package/lib/hooks/usePlaylist.js +136 -0
  55. package/lib/index.d.ts +6 -0
  56. package/lib/index.js +6 -0
  57. package/lib/specs/DownloadManager.nitro.d.ts +152 -0
  58. package/lib/specs/DownloadManager.nitro.js +1 -0
  59. package/lib/specs/Equalizer.nitro.d.ts +43 -0
  60. package/lib/specs/Equalizer.nitro.js +1 -0
  61. package/lib/specs/TrackPlayer.nitro.d.ts +6 -2
  62. package/lib/types/DownloadTypes.d.ts +110 -0
  63. package/lib/types/DownloadTypes.js +1 -0
  64. package/lib/types/EqualizerTypes.d.ts +52 -0
  65. package/lib/types/EqualizerTypes.js +1 -0
  66. package/lib/types/PlayerQueue.d.ts +4 -0
  67. package/nitro.json +8 -0
  68. package/nitrogen/generated/android/NitroPlayer+autolinking.cmake +10 -1
  69. package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +32 -2
  70. package/nitrogen/generated/android/c++/JCurrentPlayingType.hpp +65 -0
  71. package/nitrogen/generated/android/c++/JDownloadConfig.hpp +92 -0
  72. package/nitrogen/generated/android/c++/JDownloadError.hpp +71 -0
  73. package/nitrogen/generated/android/c++/JDownloadErrorReason.hpp +74 -0
  74. package/nitrogen/generated/android/c++/JDownloadProgress.hpp +79 -0
  75. package/nitrogen/generated/android/c++/JDownloadQueueStatus.hpp +81 -0
  76. package/nitrogen/generated/android/c++/JDownloadState.hpp +71 -0
  77. package/nitrogen/generated/android/c++/JDownloadStorageInfo.hpp +73 -0
  78. package/nitrogen/generated/android/c++/JDownloadTask.hpp +108 -0
  79. package/nitrogen/generated/android/c++/JDownloadedPlaylist.hpp +111 -0
  80. package/nitrogen/generated/android/c++/JDownloadedTrack.hpp +92 -0
  81. package/nitrogen/generated/android/c++/JEqualizerBand.hpp +69 -0
  82. package/nitrogen/generated/android/c++/JEqualizerPreset.hpp +78 -0
  83. package/nitrogen/generated/android/c++/JEqualizerState.hpp +91 -0
  84. package/nitrogen/generated/android/c++/JFunc_void_DownloadProgress.hpp +80 -0
  85. package/nitrogen/generated/android/c++/JFunc_void_DownloadedTrack.hpp +89 -0
  86. package/nitrogen/generated/android/c++/JFunc_void_TrackItem_std__optional_Reason_.hpp +2 -0
  87. package/nitrogen/generated/android/c++/JFunc_void_std__optional_std__variant_nitro__NullType__std__string__.hpp +81 -0
  88. package/nitrogen/generated/android/c++/JFunc_void_std__string_Playlist_std__optional_QueueOperation_.hpp +2 -0
  89. package/nitrogen/generated/android/c++/JFunc_void_std__string_std__string_DownloadState_std__optional_DownloadError_.hpp +83 -0
  90. package/nitrogen/generated/android/c++/JFunc_void_std__vector_EqualizerBand_.hpp +97 -0
  91. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Playlist__std__optional_QueueOperation_.hpp +2 -0
  92. package/nitrogen/generated/android/c++/JGainRange.hpp +61 -0
  93. package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.cpp +470 -0
  94. package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.hpp +99 -0
  95. package/nitrogen/generated/android/c++/JHybridEqualizerSpec.cpp +204 -0
  96. package/nitrogen/generated/android/c++/JHybridEqualizerSpec.hpp +82 -0
  97. package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +2 -0
  98. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +117 -15
  99. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +6 -2
  100. package/nitrogen/generated/android/c++/JPlaybackSource.hpp +62 -0
  101. package/nitrogen/generated/android/c++/JPlayerState.hpp +11 -3
  102. package/nitrogen/generated/android/c++/JPlaylist.hpp +2 -0
  103. package/nitrogen/generated/android/c++/JPresetType.hpp +59 -0
  104. package/nitrogen/generated/android/c++/JStorageLocation.hpp +59 -0
  105. package/nitrogen/generated/android/c++/JTrackItem.hpp +9 -3
  106. package/nitrogen/generated/android/c++/JTrackPlayerState.hpp +3 -3
  107. package/nitrogen/generated/android/c++/JVariant_NullType_Double.cpp +26 -0
  108. package/nitrogen/generated/android/c++/JVariant_NullType_Double.hpp +69 -0
  109. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadError.cpp +26 -0
  110. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadError.hpp +74 -0
  111. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadTask.cpp +26 -0
  112. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadTask.hpp +84 -0
  113. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedPlaylist.cpp +26 -0
  114. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedPlaylist.hpp +85 -0
  115. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedTrack.cpp +26 -0
  116. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedTrack.hpp +80 -0
  117. package/nitrogen/generated/android/c++/JVariant_NullType_Playlist.hpp +2 -0
  118. package/nitrogen/generated/android/c++/JVariant_NullType_TrackItem.hpp +2 -0
  119. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/CurrentPlayingType.kt +23 -0
  120. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadConfig.kt +59 -0
  121. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadError.kt +47 -0
  122. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadErrorReason.kt +26 -0
  123. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadProgress.kt +53 -0
  124. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadQueueStatus.kt +56 -0
  125. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadState.kt +25 -0
  126. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadStorageInfo.kt +50 -0
  127. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadTask.kt +65 -0
  128. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadedPlaylist.kt +53 -0
  129. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadedTrack.kt +56 -0
  130. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerBand.kt +47 -0
  131. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerPreset.kt +44 -0
  132. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerState.kt +44 -0
  133. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_DownloadProgress.kt +80 -0
  134. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_DownloadedTrack.kt +80 -0
  135. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__optional_std__variant_nitro__NullType__std__string__.kt +80 -0
  136. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__string_std__string_DownloadState_std__optional_DownloadError_.kt +80 -0
  137. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_EqualizerBand_.kt +80 -0
  138. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/GainRange.kt +41 -0
  139. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridDownloadManagerSpec.kt +210 -0
  140. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridEqualizerSpec.kt +141 -0
  141. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +19 -2
  142. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlaybackSource.kt +22 -0
  143. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerState.kt +6 -3
  144. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PresetType.kt +21 -0
  145. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/StorageLocation.kt +21 -0
  146. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackItem.kt +7 -3
  147. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackPlayerState.kt +2 -2
  148. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_Double.kt +59 -0
  149. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadError.kt +59 -0
  150. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadTask.kt +59 -0
  151. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadedPlaylist.kt +59 -0
  152. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadedTrack.kt +59 -0
  153. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +138 -8
  154. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +1046 -121
  155. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +66 -0
  156. package/nitrogen/generated/ios/NitroPlayerAutolinking.mm +16 -0
  157. package/nitrogen/generated/ios/NitroPlayerAutolinking.swift +30 -0
  158. package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.cpp +11 -0
  159. package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.hpp +386 -0
  160. package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.cpp +11 -0
  161. package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.hpp +223 -0
  162. package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.hpp +1 -0
  163. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +46 -6
  164. package/nitrogen/generated/ios/swift/CurrentPlayingType.swift +48 -0
  165. package/nitrogen/generated/ios/swift/DownloadConfig.swift +270 -0
  166. package/nitrogen/generated/ios/swift/DownloadError.swift +69 -0
  167. package/nitrogen/generated/ios/swift/DownloadErrorReason.swift +60 -0
  168. package/nitrogen/generated/ios/swift/DownloadProgress.swift +91 -0
  169. package/nitrogen/generated/ios/swift/DownloadQueueStatus.swift +102 -0
  170. package/nitrogen/generated/ios/swift/DownloadState.swift +56 -0
  171. package/nitrogen/generated/ios/swift/DownloadStorageInfo.swift +80 -0
  172. package/nitrogen/generated/ios/swift/DownloadTask.swift +315 -0
  173. package/nitrogen/generated/ios/swift/DownloadedPlaylist.swift +103 -0
  174. package/nitrogen/generated/ios/swift/DownloadedTrack.swift +147 -0
  175. package/nitrogen/generated/ios/swift/EqualizerBand.swift +69 -0
  176. package/nitrogen/generated/ios/swift/EqualizerPreset.swift +70 -0
  177. package/nitrogen/generated/ios/swift/EqualizerState.swift +115 -0
  178. package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
  179. package/nitrogen/generated/ios/swift/Func_void_DownloadProgress.swift +47 -0
  180. package/nitrogen/generated/ios/swift/Func_void_DownloadStorageInfo.swift +47 -0
  181. package/nitrogen/generated/ios/swift/Func_void_DownloadedTrack.swift +47 -0
  182. package/nitrogen/generated/ios/swift/Func_void_PlayerState.swift +47 -0
  183. package/nitrogen/generated/ios/swift/Func_void_bool.swift +5 -5
  184. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
  185. package/nitrogen/generated/ios/swift/Func_void_std__optional_std__variant_nitro__NullType__std__string__.swift +66 -0
  186. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +47 -0
  187. package/nitrogen/generated/ios/swift/Func_void_std__string_std__string_DownloadState_std__optional_DownloadError_.swift +47 -0
  188. package/nitrogen/generated/ios/swift/Func_void_std__vector_EqualizerBand_.swift +47 -0
  189. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem_.swift +47 -0
  190. package/nitrogen/generated/ios/swift/Func_void_std__vector_std__string_.swift +47 -0
  191. package/nitrogen/generated/ios/swift/GainRange.swift +47 -0
  192. package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec.swift +90 -0
  193. package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec_cxx.swift +705 -0
  194. package/nitrogen/generated/ios/swift/HybridEqualizerSpec.swift +73 -0
  195. package/nitrogen/generated/ios/swift/HybridEqualizerSpec_cxx.swift +396 -0
  196. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +6 -2
  197. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +105 -8
  198. package/nitrogen/generated/ios/swift/PlaybackSource.swift +44 -0
  199. package/nitrogen/generated/ios/swift/PlayerState.swift +13 -2
  200. package/nitrogen/generated/ios/swift/PresetType.swift +40 -0
  201. package/nitrogen/generated/ios/swift/StorageLocation.swift +40 -0
  202. package/nitrogen/generated/ios/swift/TrackItem.swift +31 -1
  203. package/nitrogen/generated/ios/swift/TrackPlayerState.swift +4 -4
  204. package/nitrogen/generated/ios/swift/Variant_NullType_Double.swift +18 -0
  205. package/nitrogen/generated/ios/swift/Variant_NullType_DownloadError.swift +18 -0
  206. package/nitrogen/generated/ios/swift/Variant_NullType_DownloadTask.swift +18 -0
  207. package/nitrogen/generated/ios/swift/Variant_NullType_DownloadedPlaylist.swift +18 -0
  208. package/nitrogen/generated/ios/swift/Variant_NullType_DownloadedTrack.swift +18 -0
  209. package/nitrogen/generated/shared/c++/CurrentPlayingType.hpp +84 -0
  210. package/nitrogen/generated/shared/c++/DownloadConfig.hpp +108 -0
  211. package/nitrogen/generated/shared/c++/DownloadError.hpp +89 -0
  212. package/nitrogen/generated/shared/c++/DownloadErrorReason.hpp +96 -0
  213. package/nitrogen/generated/shared/c++/DownloadProgress.hpp +97 -0
  214. package/nitrogen/generated/shared/c++/DownloadQueueStatus.hpp +99 -0
  215. package/nitrogen/generated/shared/c++/DownloadState.hpp +92 -0
  216. package/nitrogen/generated/shared/c++/DownloadStorageInfo.hpp +91 -0
  217. package/nitrogen/generated/shared/c++/DownloadTask.hpp +122 -0
  218. package/nitrogen/generated/shared/c++/DownloadedPlaylist.hpp +101 -0
  219. package/nitrogen/generated/shared/c++/DownloadedTrack.hpp +107 -0
  220. package/nitrogen/generated/shared/c++/EqualizerBand.hpp +87 -0
  221. package/nitrogen/generated/shared/c++/EqualizerPreset.hpp +86 -0
  222. package/nitrogen/generated/shared/c++/EqualizerState.hpp +89 -0
  223. package/nitrogen/generated/shared/c++/GainRange.hpp +79 -0
  224. package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.cpp +55 -0
  225. package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.hpp +134 -0
  226. package/nitrogen/generated/shared/c++/HybridEqualizerSpec.cpp +38 -0
  227. package/nitrogen/generated/shared/c++/HybridEqualizerSpec.hpp +95 -0
  228. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +4 -0
  229. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +11 -5
  230. package/nitrogen/generated/shared/c++/PlaybackSource.hpp +80 -0
  231. package/nitrogen/generated/shared/c++/PlayerState.hpp +9 -2
  232. package/nitrogen/generated/shared/c++/PresetType.hpp +76 -0
  233. package/nitrogen/generated/shared/c++/StorageLocation.hpp +76 -0
  234. package/nitrogen/generated/shared/c++/TrackItem.hpp +7 -2
  235. package/nitrogen/generated/shared/c++/TrackPlayerState.hpp +5 -5
  236. package/package.json +1 -1
  237. package/src/hooks/downloadCallbackManager.ts +149 -0
  238. package/src/hooks/equalizerCallbackManager.ts +138 -0
  239. package/src/hooks/index.ts +23 -0
  240. package/src/hooks/useActualQueue.ts +116 -0
  241. package/src/hooks/useDownloadActions.ts +179 -0
  242. package/src/hooks/useDownloadProgress.ts +126 -0
  243. package/src/hooks/useDownloadStorage.ts +84 -0
  244. package/src/hooks/useDownloadedTracks.ts +138 -0
  245. package/src/hooks/useEqualizer.ts +173 -0
  246. package/src/hooks/useEqualizerPresets.ts +140 -0
  247. package/src/hooks/useNowPlaying.ts +3 -2
  248. package/src/hooks/useOnChangeTrack.ts +15 -11
  249. package/src/hooks/useOnPlaybackStateChange.ts +19 -15
  250. package/src/hooks/usePlaylist.ts +161 -0
  251. package/src/index.ts +12 -0
  252. package/src/specs/DownloadManager.nitro.ts +203 -0
  253. package/src/specs/Equalizer.nitro.ts +69 -0
  254. package/src/specs/TrackPlayer.nitro.ts +6 -2
  255. package/src/types/DownloadTypes.ts +135 -0
  256. package/src/types/EqualizerTypes.ts +72 -0
  257. package/src/types/PlayerQueue.ts +9 -0
@@ -12,6 +12,7 @@ import androidx.media3.common.Player
12
12
  import androidx.media3.exoplayer.DefaultLoadControl
13
13
  import androidx.media3.exoplayer.ExoPlayer
14
14
  import com.margelo.nitro.core.NullType
15
+ import com.margelo.nitro.nitroplayer.CurrentPlayingType
15
16
  import com.margelo.nitro.nitroplayer.NitroPlayerPackage
16
17
  import com.margelo.nitro.nitroplayer.PlayerState
17
18
  import com.margelo.nitro.nitroplayer.Reason
@@ -21,12 +22,16 @@ import com.margelo.nitro.nitroplayer.TrackPlayerState
21
22
  import com.margelo.nitro.nitroplayer.Variant_NullType_String
22
23
  import com.margelo.nitro.nitroplayer.Variant_NullType_TrackItem
23
24
  import com.margelo.nitro.nitroplayer.connection.AndroidAutoConnectionDetector
25
+ import com.margelo.nitro.nitroplayer.download.DownloadManagerCore
26
+ import com.margelo.nitro.nitroplayer.equalizer.EqualizerCore
24
27
  import com.margelo.nitro.nitroplayer.media.MediaLibrary
25
28
  import com.margelo.nitro.nitroplayer.media.MediaLibraryManager
26
29
  import com.margelo.nitro.nitroplayer.media.MediaLibraryParser
27
30
  import com.margelo.nitro.nitroplayer.media.MediaSessionManager
28
31
  import com.margelo.nitro.nitroplayer.media.NitroPlayerMediaBrowserService
29
32
  import com.margelo.nitro.nitroplayer.playlist.PlaylistManager
33
+ import java.lang.ref.WeakReference
34
+ import java.util.Collections
30
35
  import java.util.concurrent.CountDownLatch
31
36
  import java.util.concurrent.TimeUnit
32
37
 
@@ -36,6 +41,7 @@ class TrackPlayerCore private constructor(
36
41
  private val handler = android.os.Handler(android.os.Looper.getMainLooper())
37
42
  private lateinit var player: ExoPlayer
38
43
  private val playlistManager = PlaylistManager.getInstance(context)
44
+ private val downloadManager = DownloadManagerCore.getInstance(context)
39
45
  private val mediaLibraryManager = MediaLibraryManager.getInstance(context)
40
46
  private var mediaSessionManager: MediaSessionManager? = null
41
47
  private var currentPlaylistId: String? = null
@@ -43,23 +49,52 @@ class TrackPlayerCore private constructor(
43
49
  private var isAndroidAutoConnected: Boolean = false
44
50
  private var androidAutoConnectionDetector: AndroidAutoConnectionDetector? = null
45
51
  var onAndroidAutoConnectionChange: ((Boolean) -> Unit)? = null
52
+ private var previousMediaItem: MediaItem? = null
53
+
46
54
  private val progressUpdateRunnable =
47
55
  object : Runnable {
48
56
  override fun run() {
49
57
  if (::player.isInitialized && player.playbackState != Player.STATE_IDLE) {
50
58
  val position = player.currentPosition / 1000.0
51
59
  val duration = if (player.duration > 0) player.duration / 1000.0 else 0.0
52
- onPlaybackProgressChange?.invoke(position, duration, if (isManuallySeeked) true else null)
60
+ notifyPlaybackProgress(position, duration, if (isManuallySeeked) true else null)
53
61
  isManuallySeeked = false
54
62
  }
55
63
  handler.postDelayed(this, 250) // Update every 250ms
56
64
  }
57
65
  }
58
66
 
59
- var onChangeTrack: ((TrackItem, Reason?) -> Unit)? = null
60
- var onPlaybackStateChange: ((TrackPlayerState, Reason?) -> Unit)? = null
61
- var onSeek: ((Double, Double) -> Unit)? = null
62
- var onPlaybackProgressChange: ((Double, Double, Boolean?) -> Unit)? = null
67
+ // Weak callback wrapper for auto-cleanup
68
+ private data class WeakCallbackBox<T>(
69
+ private val ownerRef: WeakReference<Any>,
70
+ val callback: T,
71
+ ) {
72
+ val isAlive: Boolean get() = ownerRef.get() != null
73
+ }
74
+
75
+ // Event listeners - support multiple listeners with auto-cleanup
76
+ private val onChangeTrackListeners =
77
+ Collections.synchronizedList(mutableListOf<WeakCallbackBox<(TrackItem, Reason?) -> Unit>>())
78
+ private val onPlaybackStateChangeListeners =
79
+ Collections.synchronizedList(mutableListOf<WeakCallbackBox<(TrackPlayerState, Reason?) -> Unit>>())
80
+ private val onSeekListeners =
81
+ Collections.synchronizedList(mutableListOf<WeakCallbackBox<(Double, Double) -> Unit>>())
82
+ private val onPlaybackProgressChangeListeners =
83
+ Collections.synchronizedList(mutableListOf<WeakCallbackBox<(Double, Double, Boolean?) -> Unit>>())
84
+
85
+ // Temporary tracks for addToUpNext and playNext
86
+ private var playNextStack: MutableList<TrackItem> = mutableListOf() // LIFO - last added plays first
87
+ private var upNextQueue: MutableList<TrackItem> = mutableListOf() // FIFO - first added plays first
88
+ private var currentTemporaryType: TemporaryType = TemporaryType.NONE
89
+ private var currentTracks: List<TrackItem> = emptyList()
90
+ private var currentTrackIndex: Int = -1 // Index in the original playlist (currentTracks)
91
+
92
+ // Enum to track what type of track is currently playing
93
+ private enum class TemporaryType {
94
+ NONE, // Playing from original playlist
95
+ PLAY_NEXT, // Currently in playNextStack
96
+ UP_NEXT, // Currently in upNextQueue
97
+ }
63
98
 
64
99
  companion object {
65
100
  @Volatile
@@ -73,154 +108,238 @@ class TrackPlayerCore private constructor(
73
108
  }
74
109
 
75
110
  init {
76
- handler.post {
77
- // ============================================================
78
- // GAPLESS PLAYBACK CONFIGURATION
79
- // ============================================================
80
- // Configure LoadControl for maximum gapless playback
81
- // Large buffers ensure next track is fully ready before current ends
82
- val loadControl =
83
- DefaultLoadControl
84
- .Builder()
85
- .setBufferDurationsMs(
86
- 30_000, // MIN_BUFFER_MS: 30 seconds minimum buffer
87
- 120_000, // MAX_BUFFER_MS: 2 minutes maximum buffer (enables preloading next tracks)
88
- 2_500, // BUFFER_FOR_PLAYBACK_MS: 2.5s before playback starts
89
- 5_000, // BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS: 5s after rebuffer
90
- ).setBackBuffer(30_000, true) // Keep 30s back buffer for seamless seek-back
91
- .setTargetBufferBytes(C.LENGTH_UNSET) // No size limit - prioritize time
92
- .setPrioritizeTimeOverSizeThresholds(true) // Prioritize time-based buffering
93
- .build()
94
-
95
- // Configure audio attributes for optimal music playback
96
- // This enables gapless audio processing in the audio pipeline
97
- val audioAttributes =
98
- AudioAttributes
99
- .Builder()
100
- .setUsage(C.USAGE_MEDIA)
101
- .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
102
- .build()
103
-
104
- player =
105
- ExoPlayer
106
- .Builder(context)
107
- .setLoadControl(loadControl)
108
- .setAudioAttributes(audioAttributes, true) // handleAudioFocus = true for gapless
109
- .setHandleAudioBecomingNoisy(true) // Pause when headphones disconnected
110
- .setPauseAtEndOfMediaItems(false) // Don't pause between items - key for gapless!
111
- .build()
112
-
113
- println("🎵 TrackPlayerCore: Gapless playback configured - 120s buffer, audio focus handling enabled")
114
- mediaSessionManager =
115
- MediaSessionManager(context, player, playlistManager).apply {
116
- setTrackPlayerCore(this@TrackPlayerCore)
117
- }
111
+ // Run synchronously on main thread to avoid deadlock
112
+ // when awaitInitialization is called from main thread
113
+ val initRunnable =
114
+ Runnable {
115
+ // ============================================================
116
+ // GAPLESS PLAYBACK CONFIGURATION
117
+ // ============================================================
118
+ // Configure LoadControl for maximum gapless playback
119
+ // Large buffers ensure next track is fully ready before current ends
120
+ val loadControl =
121
+ DefaultLoadControl
122
+ .Builder()
123
+ .setBufferDurationsMs(
124
+ 30_000, // MIN_BUFFER_MS: 30 seconds minimum buffer
125
+ 120_000, // MAX_BUFFER_MS: 2 minutes maximum buffer (enables preloading next tracks)
126
+ 2_500, // BUFFER_FOR_PLAYBACK_MS: 2.5s before playback starts
127
+ 5_000, // BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS: 5s after rebuffer
128
+ ).setBackBuffer(30_000, true) // Keep 30s back buffer for seamless seek-back
129
+ .setTargetBufferBytes(C.LENGTH_UNSET) // No size limit - prioritize time
130
+ .setPrioritizeTimeOverSizeThresholds(true) // Prioritize time-based buffering
131
+ .build()
132
+
133
+ // Configure audio attributes for optimal music playback
134
+ // This enables gapless audio processing in the audio pipeline
135
+ val audioAttributes =
136
+ AudioAttributes
137
+ .Builder()
138
+ .setUsage(C.USAGE_MEDIA)
139
+ .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
140
+ .build()
141
+
142
+ player =
143
+ ExoPlayer
144
+ .Builder(context)
145
+ .setLoadControl(loadControl)
146
+ .setAudioAttributes(audioAttributes, true) // handleAudioFocus = true for gapless
147
+ .setHandleAudioBecomingNoisy(true) // Pause when headphones disconnected
148
+ .setPauseAtEndOfMediaItems(false) // Don't pause between items - key for gapless!
149
+ .build()
150
+
151
+ mediaSessionManager =
152
+ MediaSessionManager(context, player, playlistManager).apply {
153
+ setTrackPlayerCore(this@TrackPlayerCore)
154
+ }
118
155
 
119
- // Set references for MediaBrowserService
120
- NitroPlayerMediaBrowserService.trackPlayerCore = this
121
- NitroPlayerMediaBrowserService.mediaSessionManager = mediaSessionManager
156
+ // Set references for MediaBrowserService
157
+ NitroPlayerMediaBrowserService.trackPlayerCore = this
158
+ NitroPlayerMediaBrowserService.mediaSessionManager = mediaSessionManager
122
159
 
123
- // Initialize Android Auto connection detector
124
- androidAutoConnectionDetector =
125
- AndroidAutoConnectionDetector(context).apply {
126
- onConnectionChanged = { connected, connectionType ->
127
- handler.post {
128
- isAndroidAutoConnected = connected
129
- NitroPlayerMediaBrowserService.isAndroidAutoConnected = connected
160
+ // Initialize Android Auto connection detector
161
+ androidAutoConnectionDetector =
162
+ AndroidAutoConnectionDetector(context).apply {
163
+ onConnectionChanged = { connected, connectionType ->
164
+ handler.post {
165
+ isAndroidAutoConnected = connected
166
+ NitroPlayerMediaBrowserService.isAndroidAutoConnected = connected
130
167
 
131
- // Notify JavaScript
132
- onAndroidAutoConnectionChange?.invoke(connected)
168
+ // Notify JavaScript
169
+ onAndroidAutoConnectionChange?.invoke(connected)
133
170
 
134
- println("🚗 Android Auto connection changed: connected=$connected, type=$connectionType")
171
+ println("🚗 Android Auto connection changed: connected=$connected, type=$connectionType")
172
+ }
135
173
  }
174
+ registerCarConnectionReceiver()
136
175
  }
137
- registerCarConnectionReceiver()
138
- }
139
176
 
140
- player.addListener(
141
- object : Player.Listener {
142
- override fun onMediaItemTransition(
143
- mediaItem: MediaItem?,
144
- reason: Int,
145
- ) {
146
- // Handle playlist switching if needed
147
- mediaItem?.mediaId?.let { mediaId ->
148
- if (mediaId.contains(':')) {
149
- val colonIndex = mediaId.indexOf(':')
150
- val playlistId = mediaId.substring(0, colonIndex)
151
- if (playlistId != currentPlaylistId) {
152
- // Track from different playlist - ensure playlist is loaded
153
- val playlist = playlistManager.getPlaylist(playlistId)
154
- if (playlist != null && currentPlaylistId != playlistId) {
155
- // This shouldn't happen if playlists are loaded correctly,
156
- // but handle it as a safety measure
157
- println(
158
- "⚠️ TrackPlayerCore: Detected track from different playlist, updating...",
159
- )
177
+ player.addListener(
178
+ object : Player.Listener {
179
+ override fun onMediaItemTransition(
180
+ mediaItem: MediaItem?,
181
+ reason: Int,
182
+ ) {
183
+ println("\n🔄 onMediaItemTransition called")
184
+ println(
185
+ " reason: ${when (reason) {
186
+ Player.MEDIA_ITEM_TRANSITION_REASON_AUTO -> "AUTO (track ended)"
187
+ Player.MEDIA_ITEM_TRANSITION_REASON_SEEK -> "SEEK"
188
+ Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED -> "PLAYLIST_CHANGED"
189
+ Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT -> "REPEAT"
190
+ else -> "UNKNOWN($reason)"
191
+ }}",
192
+ )
193
+ println(" previousMediaItem: ${previousMediaItem?.mediaId}")
194
+ println(" new mediaItem: ${mediaItem?.mediaId}")
195
+ println(" playNextStack: ${playNextStack.map { it.id }}")
196
+ println(" upNextQueue: ${upNextQueue.map { it.id }}")
197
+
198
+ // Remove finished track from temporary lists
199
+ // Handle AUTO (natural end) and SEEK (skip next) transitions
200
+ if ((
201
+ reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO ||
202
+ reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK
203
+ ) &&
204
+ previousMediaItem != null
205
+ ) {
206
+ previousMediaItem?.mediaId?.let { mediaId ->
207
+ val trackId = extractTrackId(mediaId)
208
+ println("🏁 Track finished/skipped, checking for removal: $trackId")
209
+
210
+ // Find and remove from playNext stack (like iOS does)
211
+ val playNextIndex = playNextStack.indexOfFirst { it.id == trackId }
212
+ if (playNextIndex >= 0) {
213
+ val track = playNextStack.removeAt(playNextIndex)
214
+ println(" ✅ Removed from playNext stack: ${track.title}")
215
+ } else {
216
+ // Find and remove from upNext queue
217
+ val upNextIndex = upNextQueue.indexOfFirst { it.id == trackId }
218
+ if (upNextIndex >= 0) {
219
+ val track = upNextQueue.removeAt(upNextIndex)
220
+ println(" ✅ Removed from upNext queue: ${track.title}")
221
+ } else {
222
+ println(" ℹ️ Was an original playlist track")
223
+ }
224
+ }
225
+ }
226
+ } else {
227
+ println(" ⏭️ Skipping removal (reason=$reason, prev=${previousMediaItem != null})")
228
+ }
229
+
230
+ // Store current item as previous for next transition
231
+ previousMediaItem = mediaItem
232
+
233
+ // Update temporary type for current track
234
+ currentTemporaryType = determineCurrentTemporaryType()
235
+ println(" Updated currentTemporaryType: $currentTemporaryType")
236
+
237
+ // Update currentTrackIndex when we land on an original playlist track
238
+ if (currentTemporaryType == TemporaryType.NONE && mediaItem != null) {
239
+ val trackId = extractTrackId(mediaItem.mediaId)
240
+ val newIndex = currentTracks.indexOfFirst { it.id == trackId }
241
+ if (newIndex >= 0 && newIndex != currentTrackIndex) {
242
+ println(" 📍 Updating currentTrackIndex from $currentTrackIndex to $newIndex")
243
+ currentTrackIndex = newIndex
244
+ }
245
+ }
246
+
247
+ // Handle playlist switching if needed
248
+ mediaItem?.mediaId?.let { mediaId ->
249
+ if (mediaId.contains(':')) {
250
+ val colonIndex = mediaId.indexOf(':')
251
+ val playlistId = mediaId.substring(0, colonIndex)
252
+ if (playlistId != currentPlaylistId) {
253
+ // Track from different playlist - ensure playlist is loaded
254
+ val playlist = playlistManager.getPlaylist(playlistId)
255
+ if (playlist != null && currentPlaylistId != playlistId) {
256
+ // This shouldn't happen if playlists are loaded correctly,
257
+ // but handle it as a safety measure
258
+ println(
259
+ "⚠️ TrackPlayerCore: Detected track from different playlist, updating...",
260
+ )
261
+ }
160
262
  }
161
263
  }
162
264
  }
265
+
266
+ // Use getCurrentTrack() which handles temporary tracks properly
267
+ val track = getCurrentTrack()
268
+ if (track != null) {
269
+ val r =
270
+ when (reason) {
271
+ Player.MEDIA_ITEM_TRANSITION_REASON_AUTO -> Reason.END
272
+ Player.MEDIA_ITEM_TRANSITION_REASON_SEEK -> Reason.USER_ACTION
273
+ Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED -> Reason.USER_ACTION
274
+ else -> null
275
+ }
276
+ notifyTrackChange(track, r)
277
+ mediaSessionManager?.onTrackChanged()
278
+ }
279
+ }
280
+
281
+ override fun onTimelineChanged(
282
+ timeline: androidx.media3.common.Timeline,
283
+ reason: Int,
284
+ ) {
285
+ if (reason == Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) {
286
+ // Playlist changed - update MediaBrowserService
287
+ NitroPlayerMediaBrowserService.getInstance()?.onPlaylistsUpdated()
288
+ }
163
289
  }
164
290
 
165
- val track = findTrack(mediaItem)
166
- if (track != null) {
291
+ override fun onPlayWhenReadyChanged(
292
+ playWhenReady: Boolean,
293
+ reason: Int,
294
+ ) {
167
295
  val r =
168
296
  when (reason) {
169
- Player.MEDIA_ITEM_TRANSITION_REASON_AUTO -> Reason.END
170
- Player.MEDIA_ITEM_TRANSITION_REASON_SEEK -> Reason.USER_ACTION
171
- Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED -> Reason.USER_ACTION
297
+ Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST -> Reason.USER_ACTION
172
298
  else -> null
173
299
  }
174
- onChangeTrack?.invoke(track, r)
175
- mediaSessionManager?.onTrackChanged()
300
+ emitStateChange(r)
176
301
  }
177
- }
178
302
 
179
- override fun onTimelineChanged(
180
- timeline: androidx.media3.common.Timeline,
181
- reason: Int,
182
- ) {
183
- if (reason == Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) {
184
- // Playlist changed - update MediaBrowserService
185
- NitroPlayerMediaBrowserService.getInstance()?.onPlaylistsUpdated()
303
+ override fun onPlaybackStateChanged(playbackState: Int) {
304
+ emitStateChange()
186
305
  }
187
- }
188
-
189
- override fun onPlayWhenReadyChanged(
190
- playWhenReady: Boolean,
191
- reason: Int,
192
- ) {
193
- val r =
194
- when (reason) {
195
- Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST -> Reason.USER_ACTION
196
- else -> null
197
- }
198
- emitStateChange(r)
199
- }
200
306
 
201
- override fun onPlaybackStateChanged(playbackState: Int) {
202
- emitStateChange()
203
- }
307
+ override fun onIsPlayingChanged(isPlaying: Boolean) {
308
+ emitStateChange()
309
+ }
204
310
 
205
- override fun onIsPlayingChanged(isPlaying: Boolean) {
206
- emitStateChange()
207
- }
311
+ override fun onPositionDiscontinuity(
312
+ oldPosition: Player.PositionInfo,
313
+ newPosition: Player.PositionInfo,
314
+ reason: Int,
315
+ ) {
316
+ if (reason == Player.DISCONTINUITY_REASON_SEEK) {
317
+ isManuallySeeked = true
318
+ notifySeek(newPosition.positionMs / 1000.0, player.duration / 1000.0)
319
+ }
320
+ }
208
321
 
209
- override fun onPositionDiscontinuity(
210
- oldPosition: Player.PositionInfo,
211
- newPosition: Player.PositionInfo,
212
- reason: Int,
213
- ) {
214
- if (reason == Player.DISCONTINUITY_REASON_SEEK) {
215
- isManuallySeeked = true
216
- onSeek?.invoke(newPosition.positionMs / 1000.0, player.duration / 1000.0)
322
+ override fun onAudioSessionIdChanged(audioSessionId: Int) {
323
+ if (audioSessionId != 0) {
324
+ try {
325
+ EqualizerCore.getInstance(context).initialize(audioSessionId)
326
+ } catch (e: Exception) {
327
+ // Equalizer initialization failed - non-critical
328
+ }
329
+ }
217
330
  }
218
- }
219
- },
220
- )
331
+ },
332
+ )
221
333
 
222
- // Start progress updates
223
- handler.post(progressUpdateRunnable)
334
+ // Start progress updates
335
+ handler.post(progressUpdateRunnable)
336
+ }
337
+
338
+ // Execute on main thread: if already on main thread, run synchronously to avoid deadlock
339
+ if (android.os.Looper.myLooper() == android.os.Looper.getMainLooper()) {
340
+ initRunnable.run()
341
+ } else {
342
+ handler.post(initRunnable)
224
343
  }
225
344
  }
226
345
 
@@ -230,6 +349,12 @@ class TrackPlayerCore private constructor(
230
349
  */
231
350
  fun loadPlaylist(playlistId: String) {
232
351
  handler.post {
352
+ // Clear temporary tracks when loading new playlist
353
+ playNextStack.clear()
354
+ upNextQueue.clear()
355
+ currentTemporaryType = TemporaryType.NONE
356
+ println(" 🧹 Cleared temporary tracks")
357
+
233
358
  val playlist = playlistManager.getPlaylist(playlistId)
234
359
  if (playlist != null) {
235
360
  currentPlaylistId = playlistId
@@ -311,11 +436,14 @@ class TrackPlayerCore private constructor(
311
436
  }
312
437
 
313
438
  val actualReason = reason ?: if (player.playbackState == Player.STATE_ENDED) Reason.END else null
314
- onPlaybackStateChange?.invoke(state, actualReason)
439
+ notifyPlaybackStateChange(state, actualReason)
315
440
  mediaSessionManager?.onPlaybackStateChanged()
316
441
  }
317
442
 
318
443
  private fun updatePlayerQueue(tracks: List<TrackItem>) {
444
+ // Store the original tracks
445
+ currentTracks = tracks
446
+
319
447
  // Create MediaItems with playlist info in mediaId for Android Auto
320
448
  val mediaItems =
321
449
  tracks.mapIndexed { index, track ->
@@ -347,10 +475,13 @@ class TrackPlayerCore private constructor(
347
475
  }
348
476
  }
349
477
 
478
+ // Use downloadManager.getEffectiveUrl to automatically get local path if downloaded
479
+ val effectiveUrl = downloadManager.getEffectiveUrl(this)
480
+
350
481
  return MediaItem
351
482
  .Builder()
352
483
  .setMediaId(customMediaId ?: id)
353
- .setUri(url)
484
+ .setUri(effectiveUrl)
354
485
  .setMediaMetadata(metadataBuilder.build())
355
486
  .build()
356
487
  }
@@ -383,94 +514,104 @@ class TrackPlayerCore private constructor(
383
514
  songId: String,
384
515
  fromPlaylist: String?,
385
516
  ) {
386
- println("🎵 TrackPlayerCore: playSong() called - songId: $songId, fromPlaylist: $fromPlaylist")
387
-
388
517
  handler.post {
389
- var targetPlaylistId: String? = null
390
- var songIndex: Int = -1
518
+ playSongInternal(songId, fromPlaylist)
519
+ }
520
+ }
391
521
 
392
- // Case 1: If fromPlaylist is provided, use that playlist
393
- if (fromPlaylist != null) {
394
- println("🎵 TrackPlayerCore: Looking for song in specified playlist: $fromPlaylist")
395
- val playlist = playlistManager.getPlaylist(fromPlaylist)
396
- if (playlist != null) {
397
- songIndex = playlist.tracks.indexOfFirst { it.id == songId }
398
- if (songIndex >= 0) {
399
- targetPlaylistId = fromPlaylist
400
- println(" Found song at index $songIndex in playlist $fromPlaylist")
401
- } else {
402
- println("⚠️ Song $songId not found in specified playlist $fromPlaylist")
403
- return@post
404
- }
522
+ private fun playSongInternal(
523
+ songId: String,
524
+ fromPlaylist: String?,
525
+ ) {
526
+ // Clear temporary tracks when directly playing a song
527
+ playNextStack.clear()
528
+ upNextQueue.clear()
529
+ currentTemporaryType = TemporaryType.NONE
530
+ println(" 🧹 Cleared temporary tracks")
531
+
532
+ var targetPlaylistId: String? = null
533
+ var songIndex: Int = -1
534
+
535
+ // Case 1: If fromPlaylist is provided, use that playlist
536
+ if (fromPlaylist != null) {
537
+ println("🎵 TrackPlayerCore: Looking for song in specified playlist: $fromPlaylist")
538
+ val playlist = playlistManager.getPlaylist(fromPlaylist)
539
+ if (playlist != null) {
540
+ songIndex = playlist.tracks.indexOfFirst { it.id == songId }
541
+ if (songIndex >= 0) {
542
+ targetPlaylistId = fromPlaylist
543
+ println("✅ Found song at index $songIndex in playlist $fromPlaylist")
405
544
  } else {
406
- println("⚠️ Playlist $fromPlaylist not found")
407
- return@post
545
+ println("⚠️ Song $songId not found in specified playlist $fromPlaylist")
546
+ return
408
547
  }
548
+ } else {
549
+ println("⚠️ Playlist $fromPlaylist not found")
550
+ return
409
551
  }
410
- // Case 2: If fromPlaylist is not provided, search in current/loaded playlist first
411
- else {
412
- println("🎵 TrackPlayerCore: No playlist specified, checking current playlist")
413
-
414
- // Check if song exists in currently loaded playlist
415
- if (currentPlaylistId != null) {
416
- val currentPlaylist = playlistManager.getPlaylist(currentPlaylistId!!)
417
- if (currentPlaylist != null) {
418
- songIndex = currentPlaylist.tracks.indexOfFirst { it.id == songId }
419
- if (songIndex >= 0) {
420
- targetPlaylistId = currentPlaylistId
421
- println("✅ Found song at index $songIndex in current playlist $currentPlaylistId")
422
- }
552
+ }
553
+ // Case 2: If fromPlaylist is not provided, search in current/loaded playlist first
554
+ else {
555
+ println("🎵 TrackPlayerCore: No playlist specified, checking current playlist")
556
+
557
+ // Check if song exists in currently loaded playlist
558
+ if (currentPlaylistId != null) {
559
+ val currentPlaylist = playlistManager.getPlaylist(currentPlaylistId!!)
560
+ if (currentPlaylist != null) {
561
+ songIndex = currentPlaylist.tracks.indexOfFirst { it.id == songId }
562
+ if (songIndex >= 0) {
563
+ targetPlaylistId = currentPlaylistId
564
+ println("✅ Found song at index $songIndex in current playlist $currentPlaylistId")
423
565
  }
424
566
  }
567
+ }
425
568
 
426
- // If not found in current playlist, search in all playlists
427
- if (songIndex == -1) {
428
- println("🔍 Song not found in current playlist, searching all playlists...")
429
- val allPlaylists = playlistManager.getAllPlaylists()
430
-
431
- for (playlist in allPlaylists) {
432
- songIndex = playlist.tracks.indexOfFirst { it.id == songId }
433
- if (songIndex >= 0) {
434
- targetPlaylistId = playlist.id
435
- println("✅ Found song at index $songIndex in playlist ${playlist.id}")
436
- break
437
- }
438
- }
569
+ // If not found in current playlist, search in all playlists
570
+ if (songIndex == -1) {
571
+ println("🔍 Song not found in current playlist, searching all playlists...")
572
+ val allPlaylists = playlistManager.getAllPlaylists()
439
573
 
440
- // If still not found, just use the first playlist if available
441
- if (songIndex == -1 && allPlaylists.isNotEmpty()) {
442
- targetPlaylistId = allPlaylists[0].id
443
- songIndex = 0
444
- println("⚠️ Song not found in any playlist, using first playlist and starting at index 0")
574
+ for (playlist in allPlaylists) {
575
+ songIndex = playlist.tracks.indexOfFirst { it.id == songId }
576
+ if (songIndex >= 0) {
577
+ targetPlaylistId = playlist.id
578
+ println(" Found song at index $songIndex in playlist ${playlist.id}")
579
+ break
445
580
  }
446
581
  }
447
- }
448
582
 
449
- // Now play the song
450
- if (targetPlaylistId == null || songIndex < 0) {
451
- println("❌ Could not determine playlist or song index")
452
- return@post
583
+ // If still not found, just use the first playlist if available
584
+ if (songIndex == -1 && allPlaylists.isNotEmpty()) {
585
+ targetPlaylistId = allPlaylists[0].id
586
+ songIndex = 0
587
+ println("⚠️ Song not found in any playlist, using first playlist and starting at index 0")
588
+ }
453
589
  }
590
+ }
454
591
 
455
- // Load playlist if it's different from current
456
- if (currentPlaylistId != targetPlaylistId) {
457
- println("🔄 Loading new playlist: $targetPlaylistId")
458
- val playlist = playlistManager.getPlaylist(targetPlaylistId)
459
- if (playlist != null) {
460
- currentPlaylistId = targetPlaylistId
461
- updatePlayerQueue(playlist.tracks)
592
+ // Now play the song
593
+ if (targetPlaylistId == null || songIndex < 0) {
594
+ println(" Could not determine playlist or song index")
595
+ return
596
+ }
462
597
 
463
- // Wait a bit for playlist to load, then play from index
464
- handler.postDelayed({
465
- println("▶️ Playing from index: $songIndex")
466
- playFromIndex(songIndex)
467
- }, 100)
468
- }
469
- } else {
470
- // Playlist already loaded, just play from index
598
+ // Load playlist if it's different from current
599
+ if (currentPlaylistId != targetPlaylistId) {
600
+ println("🔄 Loading new playlist: $targetPlaylistId")
601
+ val playlist = playlistManager.getPlaylist(targetPlaylistId)
602
+ if (playlist != null) {
603
+ currentPlaylistId = targetPlaylistId
604
+ updatePlayerQueue(playlist.tracks)
605
+
606
+ // Wait a bit for playlist to load, then play from index
607
+ // Note: Removed postDelayed to avoid race conditions with subsequent queue operations
471
608
  println("▶️ Playing from index: $songIndex")
472
609
  playFromIndex(songIndex)
473
610
  }
611
+ } else {
612
+ // Playlist already loaded, just play from index
613
+ println("▶️ Playing from index: $songIndex")
614
+ playFromIndex(songIndex)
474
615
  }
475
616
  }
476
617
 
@@ -484,8 +625,18 @@ class TrackPlayerCore private constructor(
484
625
 
485
626
  fun skipToPrevious() {
486
627
  handler.post {
487
- if (player.hasPreviousMediaItem()) {
488
- player.seekToPreviousMediaItem()
628
+ // If playing temporary track, just seek to beginning (temps not navigable backwards)
629
+ if (currentTemporaryType != TemporaryType.NONE) {
630
+ println("🔄 TrackPlayerCore: Playing temporary track - seeking to beginning")
631
+ player.seekTo(0)
632
+ } else if (currentTrackIndex > 0) {
633
+ // Go to previous track in original playlist
634
+ println("🔄 TrackPlayerCore: Going to previous track, currentTrackIndex: $currentTrackIndex -> ${currentTrackIndex - 1}")
635
+ playFromIndexInternal(currentTrackIndex - 1)
636
+ } else {
637
+ // Already at first track, seek to beginning
638
+ println("🔄 TrackPlayerCore: Already at first track, seeking to beginning")
639
+ player.seekTo(0)
489
640
  }
490
641
  }
491
642
  }
@@ -513,6 +664,7 @@ class TrackPlayerCore private constructor(
513
664
  }
514
665
 
515
666
  fun getState(): PlayerState {
667
+ // Called from Promise.async background thread
516
668
  // Check if we're already on the main thread
517
669
  if (android.os.Looper.myLooper() == handler.looper) {
518
670
  return getStateInternal()
@@ -542,13 +694,8 @@ class TrackPlayerCore private constructor(
542
694
 
543
695
  private fun getStateInternal(): PlayerState =
544
696
  if (::player.isInitialized) {
545
- val currentMediaItem = player.currentMediaItem
546
- val track =
547
- if (currentMediaItem != null) {
548
- findTrack(currentMediaItem)
549
- } else {
550
- null
551
- }
697
+ // Use getCurrentTrack() which handles temporary tracks properly
698
+ val track = getCurrentTrack()
552
699
 
553
700
  // Convert nullable TrackItem to Variant_NullType_TrackItem
554
701
  val currentTrack: Variant_NullType_TrackItem? =
@@ -581,6 +728,18 @@ class TrackPlayerCore private constructor(
581
728
  -1.0
582
729
  }
583
730
 
731
+ // Map internal temporary type to CurrentPlayingType
732
+ val currentPlayingTypeValue =
733
+ if (track == null) {
734
+ CurrentPlayingType.NOT_PLAYING
735
+ } else {
736
+ when (currentTemporaryType) {
737
+ TemporaryType.NONE -> CurrentPlayingType.PLAYLIST
738
+ TemporaryType.PLAY_NEXT -> CurrentPlayingType.PLAY_NEXT
739
+ TemporaryType.UP_NEXT -> CurrentPlayingType.UP_NEXT
740
+ }
741
+ }
742
+
584
743
  PlayerState(
585
744
  currentTrack = currentTrack,
586
745
  currentPosition = currentPosition,
@@ -588,6 +747,7 @@ class TrackPlayerCore private constructor(
588
747
  currentState = currentState,
589
748
  currentPlaylistId = currentPlaylistId?.let { Variant_NullType_String.create(it) },
590
749
  currentIndex = currentIndex,
750
+ currentPlayingType = currentPlayingTypeValue,
591
751
  )
592
752
  } else {
593
753
  // Return default state if player is not initialized
@@ -598,6 +758,7 @@ class TrackPlayerCore private constructor(
598
758
  currentState = TrackPlayerState.STOPPED,
599
759
  currentPlaylistId = currentPlaylistId?.let { Variant_NullType_String.create(it) },
600
760
  currentIndex = -1.0,
761
+ currentPlayingType = CurrentPlayingType.NOT_PLAYING,
601
762
  )
602
763
  }
603
764
 
@@ -625,17 +786,418 @@ class TrackPlayerCore private constructor(
625
786
  fun getCurrentTrack(): TrackItem? {
626
787
  if (!::player.isInitialized) return null
627
788
  val currentMediaItem = player.currentMediaItem ?: return null
789
+
790
+ // If playing a temporary track, return that
791
+ if (currentTemporaryType != TemporaryType.NONE) {
792
+ val trackId = extractTrackId(currentMediaItem.mediaId)
793
+
794
+ when (currentTemporaryType) {
795
+ TemporaryType.PLAY_NEXT -> {
796
+ return playNextStack.firstOrNull { it.id == trackId }
797
+ }
798
+
799
+ TemporaryType.UP_NEXT -> {
800
+ return upNextQueue.firstOrNull { it.id == trackId }
801
+ }
802
+
803
+ else -> {}
804
+ }
805
+ }
806
+
807
+ // Otherwise return from original playlist
628
808
  return findTrack(currentMediaItem)
629
809
  }
630
810
 
811
+ private fun extractTrackId(mediaId: String): String =
812
+ if (mediaId.contains(':')) {
813
+ // Format: "playlistId:trackId"
814
+ mediaId.substring(mediaId.indexOf(':') + 1)
815
+ } else {
816
+ mediaId
817
+ }
818
+
819
+ // Public method to play from a specific index (for Android Auto)
631
820
  // Public method to play from a specific index (for Android Auto)
632
821
  fun playFromIndex(index: Int) {
822
+ if (android.os.Looper.myLooper() == handler.looper) {
823
+ playFromIndexInternal(index)
824
+ } else {
825
+ handler.post {
826
+ playFromIndexInternal(index)
827
+ }
828
+ }
829
+ }
830
+
831
+ // MARK: - Skip to Index in Actual Queue
832
+
833
+ fun skipToIndex(index: Int): Boolean {
834
+ // Check if we're already on the main thread
835
+ if (android.os.Looper.myLooper() == handler.looper) {
836
+ return skipToIndexInternal(index)
837
+ }
838
+
839
+ // Use CountDownLatch to wait for the result on the main thread
840
+ val latch = CountDownLatch(1)
841
+ var result = false
842
+
843
+ handler.post {
844
+ try {
845
+ result = skipToIndexInternal(index)
846
+ } finally {
847
+ latch.countDown()
848
+ }
849
+ }
850
+
851
+ try {
852
+ // Wait up to 5 seconds for the result
853
+ latch.await(5, TimeUnit.SECONDS)
854
+ } catch (e: InterruptedException) {
855
+ Thread.currentThread().interrupt()
856
+ }
857
+
858
+ return result
859
+ }
860
+
861
+ private fun skipToIndexInternal(index: Int): Boolean {
862
+ println("\n🎯 TrackPlayerCore: SKIP TO INDEX $index")
863
+
864
+ if (!::player.isInitialized) {
865
+ println(" ❌ Player not initialized")
866
+ return false
867
+ }
868
+
869
+ // Get actual queue to validate index and determine position
870
+ val actualQueue = getActualQueueInternal()
871
+ val totalQueueSize = actualQueue.size
872
+
873
+ // Validate index
874
+ if (index < 0 || index >= totalQueueSize) {
875
+ println(" ❌ Invalid index $index, queue size is $totalQueueSize")
876
+ return false
877
+ }
878
+
879
+ // Calculate queue section boundaries
880
+ // ActualQueue structure: [before_current] + [current] + [playNext] + [upNext] + [remaining_original]
881
+ // Use our internal tracking instead of player.currentMediaItemIndex (which is relative to ExoPlayer's subset queue)
882
+ val currentPos = currentTrackIndex
883
+ val playNextStart = currentPos + 1
884
+ val playNextEnd = playNextStart + playNextStack.size
885
+ val upNextStart = playNextEnd
886
+ val upNextEnd = upNextStart + upNextQueue.size
887
+ val originalRemainingStart = upNextEnd
888
+
889
+ println(" Queue structure:")
890
+ println(" currentPos: $currentPos")
891
+ println(" playNextStart: $playNextStart, playNextEnd: $playNextEnd")
892
+ println(" upNextStart: $upNextStart, upNextEnd: $upNextEnd")
893
+ println(" originalRemainingStart: $originalRemainingStart")
894
+ println(" totalQueueSize: $totalQueueSize")
895
+
896
+ // Case 1: Target is before current - use playFromIndex on original
897
+ if (index < currentPos) {
898
+ println(" 📍 Target is before current, jumping to original playlist index $index")
899
+ playFromIndexInternal(index)
900
+ return true
901
+ }
902
+
903
+ // Case 2: Target is current - seek to beginning
904
+ if (index == currentPos) {
905
+ println(" 📍 Target is current track, seeking to beginning")
906
+ player.seekTo(0)
907
+ return true
908
+ }
909
+
910
+ // Case 3: Target is in playNext section
911
+ if (index >= playNextStart && index < playNextEnd) {
912
+ val playNextIndex = index - playNextStart
913
+ println(" 📍 Target is in playNext section at position $playNextIndex")
914
+
915
+ // Remove tracks before the target from playNext (they're being skipped)
916
+ if (playNextIndex > 0) {
917
+ repeat(playNextIndex) { playNextStack.removeAt(0) }
918
+ println(" Removed $playNextIndex tracks from playNext stack")
919
+ }
920
+
921
+ // Rebuild queue and advance
922
+ rebuildQueueFromCurrentPosition()
923
+ player.seekToNextMediaItem()
924
+ return true
925
+ }
926
+
927
+ // Case 4: Target is in upNext section
928
+ if (index >= upNextStart && index < upNextEnd) {
929
+ val upNextIndex = index - upNextStart
930
+ println(" 📍 Target is in upNext section at position $upNextIndex")
931
+
932
+ // Clear all playNext tracks (they're being skipped)
933
+ playNextStack.clear()
934
+ println(" Cleared all playNext tracks")
935
+
936
+ // Remove tracks before target from upNext
937
+ if (upNextIndex > 0) {
938
+ repeat(upNextIndex) { upNextQueue.removeAt(0) }
939
+ println(" Removed $upNextIndex tracks from upNext queue")
940
+ }
941
+
942
+ // Rebuild queue and advance
943
+ rebuildQueueFromCurrentPosition()
944
+ player.seekToNextMediaItem()
945
+ return true
946
+ }
947
+
948
+ // Case 5: Target is in remaining original tracks
949
+ if (index >= originalRemainingStart) {
950
+ // Get the target track directly from actualQueue
951
+ val targetTrack = actualQueue[index]
952
+
953
+ println(" 📍 Case 5: Target is in remaining original tracks")
954
+ println(" targetTrack.id: ${targetTrack.id}")
955
+ println(" currentTracks.count: ${currentTracks.size}")
956
+ println(" currentTracks IDs: ${currentTracks.map { it.id }}")
957
+
958
+ // Find this track's index in the original playlist
959
+ val originalIndex = currentTracks.indexOfFirst { it.id == targetTrack.id }
960
+ if (originalIndex == -1) {
961
+ println(" ❌ Could not find track ${targetTrack.id} in original playlist")
962
+ println(" Available tracks: ${currentTracks.map { it.id }}")
963
+ return false
964
+ }
965
+
966
+ println(" originalIndex found: $originalIndex")
967
+
968
+ // Clear all temporary tracks (they're being skipped)
969
+ playNextStack.clear()
970
+ upNextQueue.clear()
971
+ currentTemporaryType = TemporaryType.NONE
972
+ println(" Cleared all temporary tracks")
973
+
974
+ // IMPORTANT: Rebuild the ExoPlayer queue without temporary tracks, then seek
975
+ // We need to rebuild from the target index, not just seek
976
+ rebuildQueueAndPlayFromIndex(originalIndex)
977
+ return true
978
+ }
979
+
980
+ println(" ❌ Unexpected case, index $index not handled")
981
+ return false
982
+ }
983
+
984
+ private fun playFromIndexInternal(index: Int) {
985
+ // Clear temporary tracks when jumping to specific index
986
+ playNextStack.clear()
987
+ upNextQueue.clear()
988
+ currentTemporaryType = TemporaryType.NONE
989
+ println(" 🧹 Cleared temporary tracks")
990
+
991
+ rebuildQueueAndPlayFromIndex(index)
992
+ }
993
+
994
+ /**
995
+ * Rebuild the entire ExoPlayer queue from the original playlist starting at the given index
996
+ * This clears all temporary tracks and rebuilds the queue fresh
997
+ */
998
+ private fun rebuildQueueAndPlayFromIndex(index: Int) {
999
+ if (!::player.isInitialized) {
1000
+ println(" ❌ Player not initialized")
1001
+ return
1002
+ }
1003
+
1004
+ if (index < 0 || index >= currentTracks.size) {
1005
+ println(" ❌ Invalid index $index for currentTracks size ${currentTracks.size}")
1006
+ return
1007
+ }
1008
+
1009
+ println("\n🔄 TrackPlayerCore: REBUILD QUEUE AND PLAY FROM INDEX $index")
1010
+ println(" currentTracks.size: ${currentTracks.size}")
1011
+ println(" currentTracks IDs: ${currentTracks.map { it.id }}")
1012
+
1013
+ // Build queue from the target index onwards
1014
+ val tracksToPlay = currentTracks.subList(index, currentTracks.size)
1015
+ println(" tracksToPlay (${tracksToPlay.size}): ${tracksToPlay.map { it.id }}")
1016
+
1017
+ val playlistId = currentPlaylistId ?: ""
1018
+ val mediaItems =
1019
+ tracksToPlay.map { track ->
1020
+ val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
1021
+ track.toMediaItem(mediaId)
1022
+ }
1023
+
1024
+ // Update our internal tracking of the position in original playlist
1025
+ currentTrackIndex = index
1026
+ println(" Setting currentTrackIndex to $index")
1027
+
1028
+ // Clear the entire player queue and set new items
1029
+ player.clearMediaItems()
1030
+ player.setMediaItems(mediaItems)
1031
+ player.seekToDefaultPosition(0) // Seek to first item (which is our target track)
1032
+ player.playWhenReady = true
1033
+ player.prepare()
1034
+
1035
+ println(" ✅ Queue rebuilt with ${player.mediaItemCount} items, playing from index 0 (track ${tracksToPlay.firstOrNull()?.id})")
1036
+ }
1037
+
1038
+ // MARK: - Temporary Track Management
1039
+
1040
+ /**
1041
+ * Add a track to the up-next queue (FIFO - first added plays first)
1042
+ * Track will be inserted after currently playing track and any playNext tracks
1043
+ */
1044
+ fun addToUpNext(trackId: String) {
1045
+ handler.post {
1046
+ addToUpNextInternal(trackId)
1047
+ }
1048
+ }
1049
+
1050
+ private fun addToUpNextInternal(trackId: String) {
1051
+ println("📋 TrackPlayerCore: addToUpNext($trackId)")
1052
+
1053
+ // Find the track from current playlist or all playlists
1054
+ val track = findTrackById(trackId)
1055
+ if (track == null) {
1056
+ println("❌ TrackPlayerCore: Track $trackId not found")
1057
+ return
1058
+ }
1059
+
1060
+ // Add to end of upNext queue (FIFO)
1061
+ upNextQueue.add(track)
1062
+ println(" ✅ Added '${track.title}' to upNext queue (position: ${upNextQueue.size})")
1063
+
1064
+ // Rebuild the player queue if actively playing
1065
+ if (::player.isInitialized && player.currentMediaItem != null) {
1066
+ rebuildQueueFromCurrentPosition()
1067
+ }
1068
+ }
1069
+
1070
+ /**
1071
+ * Add a track to play next (LIFO - last added plays first)
1072
+ * Track will be inserted immediately after currently playing track
1073
+ */
1074
+ fun playNext(trackId: String) {
633
1075
  handler.post {
634
- if (::player.isInitialized && index >= 0 && index < player.mediaItemCount) {
635
- player.seekToDefaultPosition(index)
636
- player.playWhenReady = true
1076
+ playNextInternal(trackId)
1077
+ }
1078
+ }
1079
+
1080
+ private fun playNextInternal(trackId: String) {
1081
+ println("⏭️ TrackPlayerCore: playNext($trackId)")
1082
+
1083
+ // Find the track from current playlist or all playlists
1084
+ val track = findTrackById(trackId)
1085
+ if (track == null) {
1086
+ println("❌ TrackPlayerCore: Track $trackId not found")
1087
+ return
1088
+ }
1089
+
1090
+ // Insert at beginning of playNext stack (LIFO)
1091
+ playNextStack.add(0, track)
1092
+ println(" ✅ Added '${track.title}' to playNext stack (position: 1)")
1093
+
1094
+ // Rebuild the player queue if actively playing
1095
+ if (::player.isInitialized && player.currentMediaItem != null) {
1096
+ rebuildQueueFromCurrentPosition()
1097
+ }
1098
+ }
1099
+
1100
+ /**
1101
+ * Rebuild the ExoPlayer queue from current position with temporary tracks
1102
+ * Order: [current] + [playNext stack] + [upNext queue] + [remaining original]
1103
+ */
1104
+ private fun rebuildQueueFromCurrentPosition() {
1105
+ if (!::player.isInitialized) return
1106
+
1107
+ println("\n🔄 TrackPlayerCore: REBUILDING QUEUE FROM CURRENT POSITION")
1108
+ println(" currentIndex: ${player.currentMediaItemIndex}")
1109
+ println(" currentMediaItem: ${player.currentMediaItem?.mediaId}")
1110
+ println(" playNextStack (${playNextStack.size}): ${playNextStack.map { "${it.id}:${it.title}" }}")
1111
+ println(" upNextQueue (${upNextQueue.size}): ${upNextQueue.map { "${it.id}:${it.title}" }}")
1112
+
1113
+ val currentIndex = player.currentMediaItemIndex
1114
+ if (currentIndex < 0) return
1115
+
1116
+ // Build new queue order:
1117
+ // [playNext stack] + [upNext queue] + [remaining original tracks]
1118
+ val newQueueTracks = mutableListOf<TrackItem>()
1119
+
1120
+ // Add playNext stack (LIFO - most recently added plays first)
1121
+ // Stack is already in correct order since we insert at position 0
1122
+ newQueueTracks.addAll(playNextStack)
1123
+
1124
+ // Add upNext queue (in order, FIFO)
1125
+ newQueueTracks.addAll(upNextQueue)
1126
+
1127
+ // Add remaining original tracks
1128
+ if (currentIndex + 1 < currentTracks.size) {
1129
+ val remaining = currentTracks.subList(currentIndex + 1, currentTracks.size)
1130
+ println(" remaining original (${remaining.size}): ${remaining.map { it.id }}")
1131
+ newQueueTracks.addAll(remaining)
1132
+ }
1133
+
1134
+ println(" New queue total: ${newQueueTracks.size} tracks")
1135
+ println(" Queue order: ${newQueueTracks.map { it.id }}")
1136
+
1137
+ // Create MediaItems for new tracks
1138
+ val playlistId = currentPlaylistId ?: ""
1139
+ val newMediaItems =
1140
+ newQueueTracks.map { track ->
1141
+ val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
1142
+ println(" Creating MediaItem: mediaId=$mediaId, title=${track.title}")
1143
+ track.toMediaItem(mediaId)
1144
+ }
1145
+
1146
+ // Remove all items after current
1147
+ val removedCount = player.mediaItemCount - currentIndex - 1
1148
+ println(" Removing $removedCount items after current")
1149
+ while (player.mediaItemCount > currentIndex + 1) {
1150
+ player.removeMediaItem(currentIndex + 1)
1151
+ }
1152
+
1153
+ // Add new items
1154
+ player.addMediaItems(newMediaItems)
1155
+
1156
+ println(" ✅ Queue rebuilt. Player now has ${player.mediaItemCount} items")
1157
+ for (i in 0 until player.mediaItemCount) {
1158
+ println(" [$i]: ${player.getMediaItemAt(i).mediaId}")
1159
+ }
1160
+ }
1161
+
1162
+ /**
1163
+ * Find a track by ID from current playlist or all playlists
1164
+ */
1165
+ private fun findTrackById(trackId: String): TrackItem? {
1166
+ // First check current playlist
1167
+ currentTracks.find { it.id == trackId }?.let { return it }
1168
+
1169
+ // Then check all playlists
1170
+ val allPlaylists = playlistManager.getAllPlaylists()
1171
+ for (playlist in allPlaylists) {
1172
+ playlist.tracks.find { it.id == trackId }?.let { return it }
1173
+ }
1174
+
1175
+ return null
1176
+ }
1177
+
1178
+ /**
1179
+ * Determine what type of track is currently playing
1180
+ */
1181
+ private fun determineCurrentTemporaryType(): TemporaryType {
1182
+ val currentItem = player.currentMediaItem ?: return TemporaryType.NONE
1183
+ val trackId =
1184
+ if (currentItem.mediaId.contains(':')) {
1185
+ currentItem.mediaId.substring(currentItem.mediaId.indexOf(':') + 1)
1186
+ } else {
1187
+ currentItem.mediaId
637
1188
  }
1189
+
1190
+ // Check if in playNext stack
1191
+ if (playNextStack.any { it.id == trackId }) {
1192
+ return TemporaryType.PLAY_NEXT
638
1193
  }
1194
+
1195
+ // Check if in upNext queue
1196
+ if (upNextQueue.any { it.id == trackId }) {
1197
+ return TemporaryType.UP_NEXT
1198
+ }
1199
+
1200
+ return TemporaryType.NONE
639
1201
  }
640
1202
 
641
1203
  // Clean up resources
@@ -689,4 +1251,199 @@ class TrackPlayerCore private constructor(
689
1251
  println("⚠️ TrackPlayerCore: Cannot set volume - player not initialized")
690
1252
  false
691
1253
  }
1254
+
1255
+ // Add event listeners
1256
+ fun addOnChangeTrackListener(callback: (TrackItem, Reason?) -> Unit) {
1257
+ val box = WeakCallbackBox(WeakReference(this), callback)
1258
+ onChangeTrackListeners.add(box)
1259
+ }
1260
+
1261
+ fun addOnPlaybackStateChangeListener(callback: (TrackPlayerState, Reason?) -> Unit) {
1262
+ val box = WeakCallbackBox(WeakReference(this), callback)
1263
+ onPlaybackStateChangeListeners.add(box)
1264
+ }
1265
+
1266
+ fun addOnSeekListener(callback: (Double, Double) -> Unit) {
1267
+ val box = WeakCallbackBox(WeakReference(this), callback)
1268
+ onSeekListeners.add(box)
1269
+ }
1270
+
1271
+ fun addOnPlaybackProgressChangeListener(callback: (Double, Double, Boolean?) -> Unit) {
1272
+ val box = WeakCallbackBox(WeakReference(this), callback)
1273
+ onPlaybackProgressChangeListeners.add(box)
1274
+ }
1275
+
1276
+ // Notification helpers with auto-cleanup
1277
+ private fun notifyTrackChange(
1278
+ track: TrackItem,
1279
+ reason: Reason?,
1280
+ ) {
1281
+ val liveCallbacks =
1282
+ synchronized(onChangeTrackListeners) {
1283
+ onChangeTrackListeners.removeAll { !it.isAlive }
1284
+ onChangeTrackListeners.filter { it.isAlive }.map { it.callback }
1285
+ }
1286
+
1287
+ handler.post {
1288
+ for (callback in liveCallbacks) {
1289
+ try {
1290
+ callback(track, reason)
1291
+ } catch (e: Exception) {
1292
+ println("⚠️ Error in track change listener: ${e.message}")
1293
+ }
1294
+ }
1295
+ }
1296
+ }
1297
+
1298
+ private fun notifyPlaybackStateChange(
1299
+ state: TrackPlayerState,
1300
+ reason: Reason?,
1301
+ ) {
1302
+ val liveCallbacks =
1303
+ synchronized(onPlaybackStateChangeListeners) {
1304
+ onPlaybackStateChangeListeners.removeAll { !it.isAlive }
1305
+ onPlaybackStateChangeListeners.filter { it.isAlive }.map { it.callback }
1306
+ }
1307
+
1308
+ handler.post {
1309
+ for (callback in liveCallbacks) {
1310
+ try {
1311
+ callback(state, reason)
1312
+ } catch (e: Exception) {
1313
+ println("⚠️ Error in playback state listener: ${e.message}")
1314
+ }
1315
+ }
1316
+ }
1317
+ }
1318
+
1319
+ private fun notifySeek(
1320
+ position: Double,
1321
+ duration: Double,
1322
+ ) {
1323
+ val liveCallbacks =
1324
+ synchronized(onSeekListeners) {
1325
+ onSeekListeners.removeAll { !it.isAlive }
1326
+ onSeekListeners.filter { it.isAlive }.map { it.callback }
1327
+ }
1328
+
1329
+ handler.post {
1330
+ for (callback in liveCallbacks) {
1331
+ try {
1332
+ callback(position, duration)
1333
+ } catch (e: Exception) {
1334
+ println("⚠️ Error in seek listener: ${e.message}")
1335
+ }
1336
+ }
1337
+ }
1338
+ }
1339
+
1340
+ private fun notifyPlaybackProgress(
1341
+ position: Double,
1342
+ duration: Double,
1343
+ isPlaying: Boolean?,
1344
+ ) {
1345
+ val liveCallbacks =
1346
+ synchronized(onPlaybackProgressChangeListeners) {
1347
+ onPlaybackProgressChangeListeners.removeAll { !it.isAlive }
1348
+ onPlaybackProgressChangeListeners.filter { it.isAlive }.map { it.callback }
1349
+ }
1350
+
1351
+ handler.post {
1352
+ for (callback in liveCallbacks) {
1353
+ try {
1354
+ callback(position, duration, isPlaying)
1355
+ } catch (e: Exception) {
1356
+ println("⚠️ Error in playback progress listener: ${e.message}")
1357
+ }
1358
+ }
1359
+ }
1360
+ }
1361
+
1362
+ /**
1363
+ * Get the actual queue with temporary tracks
1364
+ * Returns: [original_before_current] + [current] + [playNext_stack] + [upNext_queue] + [original_after_current]
1365
+ */
1366
+ fun getActualQueue(): List<TrackItem> {
1367
+ // Called from Promise.async background thread
1368
+ // Check if we're already on the main thread
1369
+ if (android.os.Looper.myLooper() == handler.looper) {
1370
+ return getActualQueueInternal()
1371
+ }
1372
+
1373
+ // Use CountDownLatch to wait for the result on the main thread
1374
+ val latch = CountDownLatch(1)
1375
+ var result: List<TrackItem>? = null
1376
+
1377
+ handler.post {
1378
+ try {
1379
+ result = getActualQueueInternal()
1380
+ } finally {
1381
+ latch.countDown()
1382
+ }
1383
+ }
1384
+
1385
+ try {
1386
+ // Wait up to 5 seconds for the result
1387
+ latch.await(5, TimeUnit.SECONDS)
1388
+ } catch (e: InterruptedException) {
1389
+ println("⚠️ TrackPlayerCore: Interrupted while waiting for actual queue")
1390
+ }
1391
+
1392
+ return result ?: emptyList()
1393
+ }
1394
+
1395
+ private fun getActualQueueInternal(): List<TrackItem> {
1396
+ println("\n🔍 TrackPlayerCore: getActualQueueInternal() called")
1397
+ println(" playNextStack size: ${playNextStack.size}, tracks: ${playNextStack.map { it.id }}")
1398
+ println(" upNextQueue size: ${upNextQueue.size}, tracks: ${upNextQueue.map { it.id }}")
1399
+ println(" currentTracks size: ${currentTracks.size}, tracks: ${currentTracks.map { it.id }}")
1400
+ println(" currentTrackIndex: $currentTrackIndex")
1401
+
1402
+ val queue = mutableListOf<TrackItem>()
1403
+
1404
+ if (!::player.isInitialized) {
1405
+ println(" ❌ Player not initialized, returning empty")
1406
+ return emptyList()
1407
+ }
1408
+
1409
+ // Use our internal tracking of position in original playlist
1410
+ val currentIndex = currentTrackIndex
1411
+ println(" Using currentTrackIndex: $currentIndex")
1412
+ if (currentIndex < 0) {
1413
+ println(" ❌ currentIndex < 0, returning empty")
1414
+ return emptyList()
1415
+ }
1416
+
1417
+ // Add tracks before current (original playlist)
1418
+ if (currentIndex > 0 && currentIndex <= currentTracks.size) {
1419
+ val beforeCurrent = currentTracks.subList(0, currentIndex)
1420
+ println(" Adding ${beforeCurrent.size} tracks before current")
1421
+ queue.addAll(beforeCurrent)
1422
+ }
1423
+
1424
+ // Add current track
1425
+ getCurrentTrack()?.let {
1426
+ println(" Adding current track: ${it.id}")
1427
+ queue.add(it)
1428
+ }
1429
+
1430
+ // Add playNext stack (LIFO - most recently added plays first)
1431
+ // Stack is already in correct order since we insert at position 0
1432
+ println(" Adding ${playNextStack.size} playNext tracks")
1433
+ queue.addAll(playNextStack)
1434
+
1435
+ // Add upNext queue (in order, FIFO)
1436
+ println(" Adding ${upNextQueue.size} upNext tracks")
1437
+ queue.addAll(upNextQueue)
1438
+
1439
+ // Add remaining original tracks
1440
+ if (currentIndex + 1 < currentTracks.size) {
1441
+ val remaining = currentTracks.subList(currentIndex + 1, currentTracks.size)
1442
+ println(" Adding ${remaining.size} remaining tracks")
1443
+ queue.addAll(remaining)
1444
+ }
1445
+
1446
+ println(" ✅ Final queue size: ${queue.size}, tracks: ${queue.map { it.id }}")
1447
+ return queue
1448
+ }
692
1449
  }