react-native-nitro-player 0.3.0-alpha.9 → 0.4.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) 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 +944 -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 +996 -288
  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/callbackManager.d.ts +18 -0
  31. package/lib/hooks/callbackManager.js +66 -0
  32. package/lib/hooks/downloadCallbackManager.d.ts +36 -0
  33. package/lib/hooks/downloadCallbackManager.js +108 -0
  34. package/lib/hooks/equalizerCallbackManager.d.ts +37 -0
  35. package/lib/hooks/equalizerCallbackManager.js +109 -0
  36. package/lib/hooks/index.d.ts +16 -0
  37. package/lib/hooks/index.js +10 -0
  38. package/lib/hooks/useActualQueue.d.ts +48 -0
  39. package/lib/hooks/useActualQueue.js +98 -0
  40. package/lib/hooks/useDownloadActions.d.ts +26 -0
  41. package/lib/hooks/useDownloadActions.js +117 -0
  42. package/lib/hooks/useDownloadProgress.d.ts +25 -0
  43. package/lib/hooks/useDownloadProgress.js +79 -0
  44. package/lib/hooks/useDownloadStorage.d.ts +19 -0
  45. package/lib/hooks/useDownloadStorage.js +60 -0
  46. package/lib/hooks/useDownloadedTracks.d.ts +25 -0
  47. package/lib/hooks/useDownloadedTracks.js +69 -0
  48. package/lib/hooks/useEqualizer.d.ts +25 -0
  49. package/lib/hooks/useEqualizer.js +124 -0
  50. package/lib/hooks/useEqualizerPresets.d.ts +22 -0
  51. package/lib/hooks/useEqualizerPresets.js +96 -0
  52. package/lib/hooks/useNowPlaying.js +32 -19
  53. package/lib/hooks/useOnChangeTrack.js +15 -12
  54. package/lib/hooks/useOnPlaybackProgressChange.js +2 -2
  55. package/lib/hooks/useOnPlaybackStateChange.js +16 -13
  56. package/lib/hooks/usePlaylist.d.ts +48 -0
  57. package/lib/hooks/usePlaylist.js +136 -0
  58. package/lib/index.d.ts +6 -0
  59. package/lib/index.js +6 -0
  60. package/lib/specs/DownloadManager.nitro.d.ts +152 -0
  61. package/lib/specs/DownloadManager.nitro.js +1 -0
  62. package/lib/specs/Equalizer.nitro.d.ts +43 -0
  63. package/lib/specs/Equalizer.nitro.js +1 -0
  64. package/lib/specs/TrackPlayer.nitro.d.ts +6 -2
  65. package/lib/types/DownloadTypes.d.ts +110 -0
  66. package/lib/types/DownloadTypes.js +1 -0
  67. package/lib/types/EqualizerTypes.d.ts +52 -0
  68. package/lib/types/EqualizerTypes.js +1 -0
  69. package/lib/types/PlayerQueue.d.ts +4 -0
  70. package/nitro.json +8 -0
  71. package/nitrogen/generated/android/NitroPlayer+autolinking.cmake +10 -1
  72. package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +32 -2
  73. package/nitrogen/generated/android/c++/JCurrentPlayingType.hpp +65 -0
  74. package/nitrogen/generated/android/c++/JDownloadConfig.hpp +92 -0
  75. package/nitrogen/generated/android/c++/JDownloadError.hpp +71 -0
  76. package/nitrogen/generated/android/c++/JDownloadErrorReason.hpp +74 -0
  77. package/nitrogen/generated/android/c++/JDownloadProgress.hpp +79 -0
  78. package/nitrogen/generated/android/c++/JDownloadQueueStatus.hpp +81 -0
  79. package/nitrogen/generated/android/c++/JDownloadState.hpp +71 -0
  80. package/nitrogen/generated/android/c++/JDownloadStorageInfo.hpp +73 -0
  81. package/nitrogen/generated/android/c++/JDownloadTask.hpp +108 -0
  82. package/nitrogen/generated/android/c++/JDownloadedPlaylist.hpp +111 -0
  83. package/nitrogen/generated/android/c++/JDownloadedTrack.hpp +92 -0
  84. package/nitrogen/generated/android/c++/JEqualizerBand.hpp +69 -0
  85. package/nitrogen/generated/android/c++/JEqualizerPreset.hpp +78 -0
  86. package/nitrogen/generated/android/c++/JEqualizerState.hpp +91 -0
  87. package/nitrogen/generated/android/c++/JFunc_void_DownloadProgress.hpp +80 -0
  88. package/nitrogen/generated/android/c++/JFunc_void_DownloadedTrack.hpp +89 -0
  89. package/nitrogen/generated/android/c++/JFunc_void_TrackItem_std__optional_Reason_.hpp +2 -0
  90. package/nitrogen/generated/android/c++/JFunc_void_std__optional_std__variant_nitro__NullType__std__string__.hpp +81 -0
  91. package/nitrogen/generated/android/c++/JFunc_void_std__string_Playlist_std__optional_QueueOperation_.hpp +2 -0
  92. package/nitrogen/generated/android/c++/JFunc_void_std__string_std__string_DownloadState_std__optional_DownloadError_.hpp +83 -0
  93. package/nitrogen/generated/android/c++/JFunc_void_std__vector_EqualizerBand_.hpp +97 -0
  94. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Playlist__std__optional_QueueOperation_.hpp +2 -0
  95. package/nitrogen/generated/android/c++/JGainRange.hpp +61 -0
  96. package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.cpp +470 -0
  97. package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.hpp +99 -0
  98. package/nitrogen/generated/android/c++/JHybridEqualizerSpec.cpp +204 -0
  99. package/nitrogen/generated/android/c++/JHybridEqualizerSpec.hpp +82 -0
  100. package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +2 -0
  101. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +117 -15
  102. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +6 -2
  103. package/nitrogen/generated/android/c++/JPlaybackSource.hpp +62 -0
  104. package/nitrogen/generated/android/c++/JPlayerState.hpp +11 -3
  105. package/nitrogen/generated/android/c++/JPlaylist.hpp +2 -0
  106. package/nitrogen/generated/android/c++/JPresetType.hpp +59 -0
  107. package/nitrogen/generated/android/c++/JStorageLocation.hpp +59 -0
  108. package/nitrogen/generated/android/c++/JTrackItem.hpp +9 -3
  109. package/nitrogen/generated/android/c++/JTrackPlayerState.hpp +3 -3
  110. package/nitrogen/generated/android/c++/JVariant_NullType_Double.cpp +26 -0
  111. package/nitrogen/generated/android/c++/JVariant_NullType_Double.hpp +69 -0
  112. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadError.cpp +26 -0
  113. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadError.hpp +74 -0
  114. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadTask.cpp +26 -0
  115. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadTask.hpp +84 -0
  116. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedPlaylist.cpp +26 -0
  117. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedPlaylist.hpp +85 -0
  118. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedTrack.cpp +26 -0
  119. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedTrack.hpp +80 -0
  120. package/nitrogen/generated/android/c++/JVariant_NullType_Playlist.hpp +2 -0
  121. package/nitrogen/generated/android/c++/JVariant_NullType_TrackItem.hpp +2 -0
  122. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/CurrentPlayingType.kt +23 -0
  123. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadConfig.kt +59 -0
  124. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadError.kt +47 -0
  125. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadErrorReason.kt +26 -0
  126. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadProgress.kt +53 -0
  127. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadQueueStatus.kt +56 -0
  128. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadState.kt +25 -0
  129. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadStorageInfo.kt +50 -0
  130. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadTask.kt +65 -0
  131. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadedPlaylist.kt +53 -0
  132. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadedTrack.kt +56 -0
  133. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerBand.kt +47 -0
  134. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerPreset.kt +44 -0
  135. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerState.kt +44 -0
  136. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_DownloadProgress.kt +80 -0
  137. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_DownloadedTrack.kt +80 -0
  138. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__optional_std__variant_nitro__NullType__std__string__.kt +80 -0
  139. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__string_std__string_DownloadState_std__optional_DownloadError_.kt +80 -0
  140. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_EqualizerBand_.kt +80 -0
  141. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/GainRange.kt +41 -0
  142. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridDownloadManagerSpec.kt +210 -0
  143. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridEqualizerSpec.kt +141 -0
  144. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +19 -2
  145. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlaybackSource.kt +22 -0
  146. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerState.kt +6 -3
  147. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PresetType.kt +21 -0
  148. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/StorageLocation.kt +21 -0
  149. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackItem.kt +7 -3
  150. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackPlayerState.kt +2 -2
  151. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_Double.kt +59 -0
  152. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadError.kt +59 -0
  153. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadTask.kt +59 -0
  154. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadedPlaylist.kt +59 -0
  155. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadedTrack.kt +59 -0
  156. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +138 -8
  157. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +1046 -121
  158. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +66 -0
  159. package/nitrogen/generated/ios/NitroPlayerAutolinking.mm +16 -0
  160. package/nitrogen/generated/ios/NitroPlayerAutolinking.swift +30 -0
  161. package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.cpp +11 -0
  162. package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.hpp +386 -0
  163. package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.cpp +11 -0
  164. package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.hpp +223 -0
  165. package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.hpp +1 -0
  166. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +46 -6
  167. package/nitrogen/generated/ios/swift/CurrentPlayingType.swift +48 -0
  168. package/nitrogen/generated/ios/swift/DownloadConfig.swift +270 -0
  169. package/nitrogen/generated/ios/swift/DownloadError.swift +69 -0
  170. package/nitrogen/generated/ios/swift/DownloadErrorReason.swift +60 -0
  171. package/nitrogen/generated/ios/swift/DownloadProgress.swift +91 -0
  172. package/nitrogen/generated/ios/swift/DownloadQueueStatus.swift +102 -0
  173. package/nitrogen/generated/ios/swift/DownloadState.swift +56 -0
  174. package/nitrogen/generated/ios/swift/DownloadStorageInfo.swift +80 -0
  175. package/nitrogen/generated/ios/swift/DownloadTask.swift +315 -0
  176. package/nitrogen/generated/ios/swift/DownloadedPlaylist.swift +103 -0
  177. package/nitrogen/generated/ios/swift/DownloadedTrack.swift +147 -0
  178. package/nitrogen/generated/ios/swift/EqualizerBand.swift +69 -0
  179. package/nitrogen/generated/ios/swift/EqualizerPreset.swift +70 -0
  180. package/nitrogen/generated/ios/swift/EqualizerState.swift +115 -0
  181. package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
  182. package/nitrogen/generated/ios/swift/Func_void_DownloadProgress.swift +47 -0
  183. package/nitrogen/generated/ios/swift/Func_void_DownloadStorageInfo.swift +47 -0
  184. package/nitrogen/generated/ios/swift/Func_void_DownloadedTrack.swift +47 -0
  185. package/nitrogen/generated/ios/swift/Func_void_PlayerState.swift +47 -0
  186. package/nitrogen/generated/ios/swift/Func_void_bool.swift +5 -5
  187. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
  188. package/nitrogen/generated/ios/swift/Func_void_std__optional_std__variant_nitro__NullType__std__string__.swift +66 -0
  189. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +47 -0
  190. package/nitrogen/generated/ios/swift/Func_void_std__string_std__string_DownloadState_std__optional_DownloadError_.swift +47 -0
  191. package/nitrogen/generated/ios/swift/Func_void_std__vector_EqualizerBand_.swift +47 -0
  192. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem_.swift +47 -0
  193. package/nitrogen/generated/ios/swift/Func_void_std__vector_std__string_.swift +47 -0
  194. package/nitrogen/generated/ios/swift/GainRange.swift +47 -0
  195. package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec.swift +90 -0
  196. package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec_cxx.swift +705 -0
  197. package/nitrogen/generated/ios/swift/HybridEqualizerSpec.swift +73 -0
  198. package/nitrogen/generated/ios/swift/HybridEqualizerSpec_cxx.swift +396 -0
  199. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +6 -2
  200. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +105 -8
  201. package/nitrogen/generated/ios/swift/PlaybackSource.swift +44 -0
  202. package/nitrogen/generated/ios/swift/PlayerState.swift +13 -2
  203. package/nitrogen/generated/ios/swift/PresetType.swift +40 -0
  204. package/nitrogen/generated/ios/swift/StorageLocation.swift +40 -0
  205. package/nitrogen/generated/ios/swift/TrackItem.swift +31 -1
  206. package/nitrogen/generated/ios/swift/TrackPlayerState.swift +4 -4
  207. package/nitrogen/generated/ios/swift/Variant_NullType_Double.swift +18 -0
  208. package/nitrogen/generated/ios/swift/Variant_NullType_DownloadError.swift +18 -0
  209. package/nitrogen/generated/ios/swift/Variant_NullType_DownloadTask.swift +18 -0
  210. package/nitrogen/generated/ios/swift/Variant_NullType_DownloadedPlaylist.swift +18 -0
  211. package/nitrogen/generated/ios/swift/Variant_NullType_DownloadedTrack.swift +18 -0
  212. package/nitrogen/generated/shared/c++/CurrentPlayingType.hpp +84 -0
  213. package/nitrogen/generated/shared/c++/DownloadConfig.hpp +108 -0
  214. package/nitrogen/generated/shared/c++/DownloadError.hpp +89 -0
  215. package/nitrogen/generated/shared/c++/DownloadErrorReason.hpp +96 -0
  216. package/nitrogen/generated/shared/c++/DownloadProgress.hpp +97 -0
  217. package/nitrogen/generated/shared/c++/DownloadQueueStatus.hpp +99 -0
  218. package/nitrogen/generated/shared/c++/DownloadState.hpp +92 -0
  219. package/nitrogen/generated/shared/c++/DownloadStorageInfo.hpp +91 -0
  220. package/nitrogen/generated/shared/c++/DownloadTask.hpp +122 -0
  221. package/nitrogen/generated/shared/c++/DownloadedPlaylist.hpp +101 -0
  222. package/nitrogen/generated/shared/c++/DownloadedTrack.hpp +107 -0
  223. package/nitrogen/generated/shared/c++/EqualizerBand.hpp +87 -0
  224. package/nitrogen/generated/shared/c++/EqualizerPreset.hpp +86 -0
  225. package/nitrogen/generated/shared/c++/EqualizerState.hpp +89 -0
  226. package/nitrogen/generated/shared/c++/GainRange.hpp +79 -0
  227. package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.cpp +55 -0
  228. package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.hpp +134 -0
  229. package/nitrogen/generated/shared/c++/HybridEqualizerSpec.cpp +38 -0
  230. package/nitrogen/generated/shared/c++/HybridEqualizerSpec.hpp +95 -0
  231. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +4 -0
  232. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +11 -5
  233. package/nitrogen/generated/shared/c++/PlaybackSource.hpp +80 -0
  234. package/nitrogen/generated/shared/c++/PlayerState.hpp +9 -2
  235. package/nitrogen/generated/shared/c++/PresetType.hpp +76 -0
  236. package/nitrogen/generated/shared/c++/StorageLocation.hpp +76 -0
  237. package/nitrogen/generated/shared/c++/TrackItem.hpp +7 -2
  238. package/nitrogen/generated/shared/c++/TrackPlayerState.hpp +5 -5
  239. package/package.json +1 -1
  240. package/src/hooks/callbackManager.ts +87 -0
  241. package/src/hooks/downloadCallbackManager.ts +149 -0
  242. package/src/hooks/equalizerCallbackManager.ts +138 -0
  243. package/src/hooks/index.ts +23 -0
  244. package/src/hooks/useActualQueue.ts +116 -0
  245. package/src/hooks/useDownloadActions.ts +179 -0
  246. package/src/hooks/useDownloadProgress.ts +126 -0
  247. package/src/hooks/useDownloadStorage.ts +84 -0
  248. package/src/hooks/useDownloadedTracks.ts +138 -0
  249. package/src/hooks/useEqualizer.ts +173 -0
  250. package/src/hooks/useEqualizerPresets.ts +140 -0
  251. package/src/hooks/useNowPlaying.ts +33 -20
  252. package/src/hooks/useOnChangeTrack.ts +15 -11
  253. package/src/hooks/useOnPlaybackProgressChange.ts +2 -2
  254. package/src/hooks/useOnPlaybackStateChange.ts +19 -15
  255. package/src/hooks/usePlaylist.ts +161 -0
  256. package/src/index.ts +12 -0
  257. package/src/specs/DownloadManager.nitro.ts +203 -0
  258. package/src/specs/Equalizer.nitro.ts +69 -0
  259. package/src/specs/TrackPlayer.nitro.ts +6 -2
  260. package/src/types/DownloadTypes.ts +135 -0
  261. package/src/types/EqualizerTypes.ts +72 -0
  262. 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,40 @@ class TrackPlayerCore private constructor(
484
625
 
485
626
  fun skipToPrevious() {
486
627
  handler.post {
487
- if (player.hasPreviousMediaItem()) {
488
- player.seekToPreviousMediaItem()
628
+ val currentPosition = player.currentPosition // milliseconds
629
+
630
+ if (currentPosition > 2000) {
631
+ // More than 2 seconds in, restart current track
632
+ println("🔄 TrackPlayerCore: Past threshold, restarting current track")
633
+ player.seekTo(0)
634
+ } else if (currentTemporaryType != TemporaryType.NONE) {
635
+ // Playing temporary track within threshold — remove from its list, go back to original
636
+ println("🔄 TrackPlayerCore: Removing temp track, going back to original")
637
+ val currentMediaItem = player.currentMediaItem
638
+ if (currentMediaItem != null) {
639
+ val trackId = extractTrackId(currentMediaItem.mediaId)
640
+ when (currentTemporaryType) {
641
+ TemporaryType.PLAY_NEXT -> {
642
+ val idx = playNextStack.indexOfFirst { it.id == trackId }
643
+ if (idx >= 0) playNextStack.removeAt(idx)
644
+ }
645
+ TemporaryType.UP_NEXT -> {
646
+ val idx = upNextQueue.indexOfFirst { it.id == trackId }
647
+ if (idx >= 0) upNextQueue.removeAt(idx)
648
+ }
649
+ else -> {}
650
+ }
651
+ }
652
+ currentTemporaryType = TemporaryType.NONE
653
+ playFromIndexInternal(currentTrackIndex)
654
+ } else if (currentTrackIndex > 0) {
655
+ // Go to previous track in original playlist
656
+ println("🔄 TrackPlayerCore: Going to previous track, currentTrackIndex: $currentTrackIndex -> ${currentTrackIndex - 1}")
657
+ playFromIndexInternal(currentTrackIndex - 1)
658
+ } else {
659
+ // Already at first track, seek to beginning
660
+ println("🔄 TrackPlayerCore: Already at first track, seeking to beginning")
661
+ player.seekTo(0)
489
662
  }
490
663
  }
491
664
  }
@@ -513,6 +686,7 @@ class TrackPlayerCore private constructor(
513
686
  }
514
687
 
515
688
  fun getState(): PlayerState {
689
+ // Called from Promise.async background thread
516
690
  // Check if we're already on the main thread
517
691
  if (android.os.Looper.myLooper() == handler.looper) {
518
692
  return getStateInternal()
@@ -542,13 +716,8 @@ class TrackPlayerCore private constructor(
542
716
 
543
717
  private fun getStateInternal(): PlayerState =
544
718
  if (::player.isInitialized) {
545
- val currentMediaItem = player.currentMediaItem
546
- val track =
547
- if (currentMediaItem != null) {
548
- findTrack(currentMediaItem)
549
- } else {
550
- null
551
- }
719
+ // Use getCurrentTrack() which handles temporary tracks properly
720
+ val track = getCurrentTrack()
552
721
 
553
722
  // Convert nullable TrackItem to Variant_NullType_TrackItem
554
723
  val currentTrack: Variant_NullType_TrackItem? =
@@ -581,6 +750,18 @@ class TrackPlayerCore private constructor(
581
750
  -1.0
582
751
  }
583
752
 
753
+ // Map internal temporary type to CurrentPlayingType
754
+ val currentPlayingTypeValue =
755
+ if (track == null) {
756
+ CurrentPlayingType.NOT_PLAYING
757
+ } else {
758
+ when (currentTemporaryType) {
759
+ TemporaryType.NONE -> CurrentPlayingType.PLAYLIST
760
+ TemporaryType.PLAY_NEXT -> CurrentPlayingType.PLAY_NEXT
761
+ TemporaryType.UP_NEXT -> CurrentPlayingType.UP_NEXT
762
+ }
763
+ }
764
+
584
765
  PlayerState(
585
766
  currentTrack = currentTrack,
586
767
  currentPosition = currentPosition,
@@ -588,6 +769,7 @@ class TrackPlayerCore private constructor(
588
769
  currentState = currentState,
589
770
  currentPlaylistId = currentPlaylistId?.let { Variant_NullType_String.create(it) },
590
771
  currentIndex = currentIndex,
772
+ currentPlayingType = currentPlayingTypeValue,
591
773
  )
592
774
  } else {
593
775
  // Return default state if player is not initialized
@@ -598,6 +780,7 @@ class TrackPlayerCore private constructor(
598
780
  currentState = TrackPlayerState.STOPPED,
599
781
  currentPlaylistId = currentPlaylistId?.let { Variant_NullType_String.create(it) },
600
782
  currentIndex = -1.0,
783
+ currentPlayingType = CurrentPlayingType.NOT_PLAYING,
601
784
  )
602
785
  }
603
786
 
@@ -625,17 +808,381 @@ class TrackPlayerCore private constructor(
625
808
  fun getCurrentTrack(): TrackItem? {
626
809
  if (!::player.isInitialized) return null
627
810
  val currentMediaItem = player.currentMediaItem ?: return null
811
+
812
+ // If playing a temporary track, return that
813
+ if (currentTemporaryType != TemporaryType.NONE) {
814
+ val trackId = extractTrackId(currentMediaItem.mediaId)
815
+
816
+ when (currentTemporaryType) {
817
+ TemporaryType.PLAY_NEXT -> {
818
+ return playNextStack.firstOrNull { it.id == trackId }
819
+ }
820
+
821
+ TemporaryType.UP_NEXT -> {
822
+ return upNextQueue.firstOrNull { it.id == trackId }
823
+ }
824
+
825
+ else -> {}
826
+ }
827
+ }
828
+
829
+ // Otherwise return from original playlist
628
830
  return findTrack(currentMediaItem)
629
831
  }
630
832
 
833
+ private fun extractTrackId(mediaId: String): String =
834
+ if (mediaId.contains(':')) {
835
+ // Format: "playlistId:trackId"
836
+ mediaId.substring(mediaId.indexOf(':') + 1)
837
+ } else {
838
+ mediaId
839
+ }
840
+
841
+ // Public method to play from a specific index (for Android Auto)
631
842
  // Public method to play from a specific index (for Android Auto)
632
843
  fun playFromIndex(index: Int) {
844
+ if (android.os.Looper.myLooper() == handler.looper) {
845
+ playFromIndexInternal(index)
846
+ } else {
847
+ handler.post {
848
+ playFromIndexInternal(index)
849
+ }
850
+ }
851
+ }
852
+
853
+ // MARK: - Skip to Index in Actual Queue
854
+
855
+ fun skipToIndex(index: Int): Boolean {
856
+ // Check if we're already on the main thread
857
+ if (android.os.Looper.myLooper() == handler.looper) {
858
+ return skipToIndexInternal(index)
859
+ }
860
+
861
+ // Use CountDownLatch to wait for the result on the main thread
862
+ val latch = CountDownLatch(1)
863
+ var result = false
864
+
633
865
  handler.post {
634
- if (::player.isInitialized && index >= 0 && index < player.mediaItemCount) {
635
- player.seekToDefaultPosition(index)
636
- player.playWhenReady = true
866
+ try {
867
+ result = skipToIndexInternal(index)
868
+ } finally {
869
+ latch.countDown()
870
+ }
871
+ }
872
+
873
+ try {
874
+ // Wait up to 5 seconds for the result
875
+ latch.await(5, TimeUnit.SECONDS)
876
+ } catch (e: InterruptedException) {
877
+ Thread.currentThread().interrupt()
878
+ }
879
+
880
+ return result
881
+ }
882
+
883
+ private fun skipToIndexInternal(index: Int): Boolean {
884
+ if (!::player.isInitialized) return false
885
+
886
+ // Get actual queue to validate index and determine position
887
+ val actualQueue = getActualQueueInternal()
888
+ val totalQueueSize = actualQueue.size
889
+
890
+ // Validate index
891
+ if (index < 0 || index >= totalQueueSize) return false
892
+
893
+ // Calculate queue section boundaries using effective sizes
894
+ // (reduced by 1 when current track is from that temp list, matching getActualQueueInternal)
895
+ // When temp is playing, the original track at currentTrackIndex is included in "before",
896
+ // so the current playing position shifts by 1
897
+ val currentPos = if (currentTemporaryType != TemporaryType.NONE)
898
+ currentTrackIndex + 1 else currentTrackIndex
899
+ val effectivePlayNextSize = if (currentTemporaryType == TemporaryType.PLAY_NEXT)
900
+ maxOf(0, playNextStack.size - 1) else playNextStack.size
901
+ val effectiveUpNextSize = if (currentTemporaryType == TemporaryType.UP_NEXT)
902
+ maxOf(0, upNextQueue.size - 1) else upNextQueue.size
903
+
904
+ val playNextStart = currentPos + 1
905
+ val playNextEnd = playNextStart + effectivePlayNextSize
906
+ val upNextStart = playNextEnd
907
+ val upNextEnd = upNextStart + effectiveUpNextSize
908
+ val originalRemainingStart = upNextEnd
909
+
910
+ // Case 1: Target is before current - use playFromIndex on original
911
+ if (index < currentPos) {
912
+ playFromIndexInternal(index)
913
+ return true
914
+ }
915
+
916
+ // Case 2: Target is current - seek to beginning
917
+ if (index == currentPos) {
918
+ player.seekTo(0)
919
+ return true
920
+ }
921
+
922
+ // Case 3: Target is in playNext section
923
+ if (index >= playNextStart && index < playNextEnd) {
924
+ val playNextIndex = index - playNextStart
925
+ // Offset by 1 if current is from playNext (index 0 is already playing)
926
+ val actualListIndex = if (currentTemporaryType == TemporaryType.PLAY_NEXT)
927
+ playNextIndex + 1 else playNextIndex
928
+
929
+ // Remove tracks before the target from playNext (they're being skipped)
930
+ if (actualListIndex > 0) {
931
+ repeat(actualListIndex) { playNextStack.removeAt(0) }
637
932
  }
933
+
934
+ // Rebuild queue and advance
935
+ rebuildQueueFromCurrentPosition()
936
+ player.seekToNextMediaItem()
937
+ return true
938
+ }
939
+
940
+ // Case 4: Target is in upNext section
941
+ if (index >= upNextStart && index < upNextEnd) {
942
+ val upNextIndex = index - upNextStart
943
+ // Offset by 1 if current is from upNext (index 0 is already playing)
944
+ val actualListIndex = if (currentTemporaryType == TemporaryType.UP_NEXT)
945
+ upNextIndex + 1 else upNextIndex
946
+
947
+ // Clear all playNext tracks (they're being skipped)
948
+ playNextStack.clear()
949
+
950
+ // Remove tracks before target from upNext
951
+ if (actualListIndex > 0) {
952
+ repeat(actualListIndex) { upNextQueue.removeAt(0) }
953
+ }
954
+
955
+ // Rebuild queue and advance
956
+ rebuildQueueFromCurrentPosition()
957
+ player.seekToNextMediaItem()
958
+ return true
959
+ }
960
+
961
+ // Case 5: Target is in remaining original tracks
962
+ if (index >= originalRemainingStart) {
963
+ val targetTrack = actualQueue[index]
964
+
965
+ // Find this track's index in the original playlist
966
+ val originalIndex = currentTracks.indexOfFirst { it.id == targetTrack.id }
967
+ if (originalIndex == -1) return false
968
+
969
+ // Clear all temporary tracks (they're being skipped)
970
+ playNextStack.clear()
971
+ upNextQueue.clear()
972
+ currentTemporaryType = TemporaryType.NONE
973
+
974
+ rebuildQueueAndPlayFromIndex(originalIndex)
975
+ return true
638
976
  }
977
+
978
+ return false
979
+ }
980
+
981
+ private fun playFromIndexInternal(index: Int) {
982
+ // Clear temporary tracks when jumping to specific index
983
+ playNextStack.clear()
984
+ upNextQueue.clear()
985
+ currentTemporaryType = TemporaryType.NONE
986
+
987
+ rebuildQueueAndPlayFromIndex(index)
988
+ }
989
+
990
+ /**
991
+ * Rebuild the entire ExoPlayer queue from the original playlist starting at the given index
992
+ * This clears all temporary tracks and rebuilds the queue fresh
993
+ */
994
+ private fun rebuildQueueAndPlayFromIndex(index: Int) {
995
+ if (!::player.isInitialized) {
996
+ println(" ❌ Player not initialized")
997
+ return
998
+ }
999
+
1000
+ if (index < 0 || index >= currentTracks.size) {
1001
+ println(" ❌ Invalid index $index for currentTracks size ${currentTracks.size}")
1002
+ return
1003
+ }
1004
+
1005
+ println("\n🔄 TrackPlayerCore: REBUILD QUEUE AND PLAY FROM INDEX $index")
1006
+ println(" currentTracks.size: ${currentTracks.size}")
1007
+ println(" currentTracks IDs: ${currentTracks.map { it.id }}")
1008
+
1009
+ // Build queue from the target index onwards
1010
+ val tracksToPlay = currentTracks.subList(index, currentTracks.size)
1011
+ println(" tracksToPlay (${tracksToPlay.size}): ${tracksToPlay.map { it.id }}")
1012
+
1013
+ val playlistId = currentPlaylistId ?: ""
1014
+ val mediaItems =
1015
+ tracksToPlay.map { track ->
1016
+ val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
1017
+ track.toMediaItem(mediaId)
1018
+ }
1019
+
1020
+ // Update our internal tracking of the position in original playlist
1021
+ currentTrackIndex = index
1022
+ println(" Setting currentTrackIndex to $index")
1023
+
1024
+ // Clear the entire player queue and set new items
1025
+ player.clearMediaItems()
1026
+ player.setMediaItems(mediaItems)
1027
+ player.seekToDefaultPosition(0) // Seek to first item (which is our target track)
1028
+ player.playWhenReady = true
1029
+ player.prepare()
1030
+
1031
+ println(" ✅ Queue rebuilt with ${player.mediaItemCount} items, playing from index 0 (track ${tracksToPlay.firstOrNull()?.id})")
1032
+ }
1033
+
1034
+ // MARK: - Temporary Track Management
1035
+
1036
+ /**
1037
+ * Add a track to the up-next queue (FIFO - first added plays first)
1038
+ * Track will be inserted after currently playing track and any playNext tracks
1039
+ */
1040
+ fun addToUpNext(trackId: String) {
1041
+ handler.post {
1042
+ addToUpNextInternal(trackId)
1043
+ }
1044
+ }
1045
+
1046
+ private fun addToUpNextInternal(trackId: String) {
1047
+ println("📋 TrackPlayerCore: addToUpNext($trackId)")
1048
+
1049
+ // Find the track from current playlist or all playlists
1050
+ val track = findTrackById(trackId)
1051
+ if (track == null) {
1052
+ println("❌ TrackPlayerCore: Track $trackId not found")
1053
+ return
1054
+ }
1055
+
1056
+ // Add to end of upNext queue (FIFO)
1057
+ upNextQueue.add(track)
1058
+ println(" ✅ Added '${track.title}' to upNext queue (position: ${upNextQueue.size})")
1059
+
1060
+ // Rebuild the player queue if actively playing
1061
+ if (::player.isInitialized && player.currentMediaItem != null) {
1062
+ rebuildQueueFromCurrentPosition()
1063
+ }
1064
+ }
1065
+
1066
+ /**
1067
+ * Add a track to play next (LIFO - last added plays first)
1068
+ * Track will be inserted immediately after currently playing track
1069
+ */
1070
+ fun playNext(trackId: String) {
1071
+ handler.post {
1072
+ playNextInternal(trackId)
1073
+ }
1074
+ }
1075
+
1076
+ private fun playNextInternal(trackId: String) {
1077
+ println("⏭️ TrackPlayerCore: playNext($trackId)")
1078
+
1079
+ // Find the track from current playlist or all playlists
1080
+ val track = findTrackById(trackId)
1081
+ if (track == null) {
1082
+ println("❌ TrackPlayerCore: Track $trackId not found")
1083
+ return
1084
+ }
1085
+
1086
+ // Insert at beginning of playNext stack (LIFO)
1087
+ playNextStack.add(0, track)
1088
+ println(" ✅ Added '${track.title}' to playNext stack (position: 1)")
1089
+
1090
+ // Rebuild the player queue if actively playing
1091
+ if (::player.isInitialized && player.currentMediaItem != null) {
1092
+ rebuildQueueFromCurrentPosition()
1093
+ }
1094
+ }
1095
+
1096
+ /**
1097
+ * Rebuild the ExoPlayer queue from current position with temporary tracks
1098
+ * Order: [current] + [playNext stack] + [upNext queue] + [remaining original]
1099
+ */
1100
+ private fun rebuildQueueFromCurrentPosition() {
1101
+ if (!::player.isInitialized) return
1102
+
1103
+ val currentIndex = player.currentMediaItemIndex
1104
+ if (currentIndex < 0) return
1105
+
1106
+ val newQueueTracks = mutableListOf<TrackItem>()
1107
+
1108
+ // Add playNext stack (LIFO - most recently added plays first)
1109
+ // Skip index 0 if current track is from playNext (it's already playing)
1110
+ if (currentTemporaryType == TemporaryType.PLAY_NEXT && playNextStack.size > 1) {
1111
+ newQueueTracks.addAll(playNextStack.subList(1, playNextStack.size))
1112
+ } else if (currentTemporaryType != TemporaryType.PLAY_NEXT) {
1113
+ newQueueTracks.addAll(playNextStack)
1114
+ }
1115
+
1116
+ // Add upNext queue (in order, FIFO)
1117
+ // Skip index 0 if current track is from upNext (it's already playing)
1118
+ if (currentTemporaryType == TemporaryType.UP_NEXT && upNextQueue.size > 1) {
1119
+ newQueueTracks.addAll(upNextQueue.subList(1, upNextQueue.size))
1120
+ } else if (currentTemporaryType != TemporaryType.UP_NEXT) {
1121
+ newQueueTracks.addAll(upNextQueue)
1122
+ }
1123
+
1124
+ // Add remaining original tracks — use currentTrackIndex (original playlist position)
1125
+ if (currentTrackIndex + 1 < currentTracks.size) {
1126
+ val remaining = currentTracks.subList(currentTrackIndex + 1, currentTracks.size)
1127
+ newQueueTracks.addAll(remaining)
1128
+ }
1129
+
1130
+ // Create MediaItems for new tracks
1131
+ val playlistId = currentPlaylistId ?: ""
1132
+ val newMediaItems =
1133
+ newQueueTracks.map { track ->
1134
+ val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
1135
+ track.toMediaItem(mediaId)
1136
+ }
1137
+
1138
+ // Remove all items after current
1139
+ while (player.mediaItemCount > currentIndex + 1) {
1140
+ player.removeMediaItem(currentIndex + 1)
1141
+ }
1142
+
1143
+ // Add new items
1144
+ player.addMediaItems(newMediaItems)
1145
+ }
1146
+
1147
+ /**
1148
+ * Find a track by ID from current playlist or all playlists
1149
+ */
1150
+ private fun findTrackById(trackId: String): TrackItem? {
1151
+ // First check current playlist
1152
+ currentTracks.find { it.id == trackId }?.let { return it }
1153
+
1154
+ // Then check all playlists
1155
+ val allPlaylists = playlistManager.getAllPlaylists()
1156
+ for (playlist in allPlaylists) {
1157
+ playlist.tracks.find { it.id == trackId }?.let { return it }
1158
+ }
1159
+
1160
+ return null
1161
+ }
1162
+
1163
+ /**
1164
+ * Determine what type of track is currently playing
1165
+ */
1166
+ private fun determineCurrentTemporaryType(): TemporaryType {
1167
+ val currentItem = player.currentMediaItem ?: return TemporaryType.NONE
1168
+ val trackId =
1169
+ if (currentItem.mediaId.contains(':')) {
1170
+ currentItem.mediaId.substring(currentItem.mediaId.indexOf(':') + 1)
1171
+ } else {
1172
+ currentItem.mediaId
1173
+ }
1174
+
1175
+ // Check if in playNext stack
1176
+ if (playNextStack.any { it.id == trackId }) {
1177
+ return TemporaryType.PLAY_NEXT
1178
+ }
1179
+
1180
+ // Check if in upNext queue
1181
+ if (upNextQueue.any { it.id == trackId }) {
1182
+ return TemporaryType.UP_NEXT
1183
+ }
1184
+
1185
+ return TemporaryType.NONE
639
1186
  }
640
1187
 
641
1188
  // Clean up resources
@@ -689,4 +1236,188 @@ class TrackPlayerCore private constructor(
689
1236
  println("⚠️ TrackPlayerCore: Cannot set volume - player not initialized")
690
1237
  false
691
1238
  }
1239
+
1240
+ // Add event listeners
1241
+ fun addOnChangeTrackListener(callback: (TrackItem, Reason?) -> Unit) {
1242
+ val box = WeakCallbackBox(WeakReference(this), callback)
1243
+ onChangeTrackListeners.add(box)
1244
+ }
1245
+
1246
+ fun addOnPlaybackStateChangeListener(callback: (TrackPlayerState, Reason?) -> Unit) {
1247
+ val box = WeakCallbackBox(WeakReference(this), callback)
1248
+ onPlaybackStateChangeListeners.add(box)
1249
+ }
1250
+
1251
+ fun addOnSeekListener(callback: (Double, Double) -> Unit) {
1252
+ val box = WeakCallbackBox(WeakReference(this), callback)
1253
+ onSeekListeners.add(box)
1254
+ }
1255
+
1256
+ fun addOnPlaybackProgressChangeListener(callback: (Double, Double, Boolean?) -> Unit) {
1257
+ val box = WeakCallbackBox(WeakReference(this), callback)
1258
+ onPlaybackProgressChangeListeners.add(box)
1259
+ }
1260
+
1261
+ // Notification helpers with auto-cleanup
1262
+ private fun notifyTrackChange(
1263
+ track: TrackItem,
1264
+ reason: Reason?,
1265
+ ) {
1266
+ val liveCallbacks =
1267
+ synchronized(onChangeTrackListeners) {
1268
+ onChangeTrackListeners.removeAll { !it.isAlive }
1269
+ onChangeTrackListeners.filter { it.isAlive }.map { it.callback }
1270
+ }
1271
+
1272
+ handler.post {
1273
+ for (callback in liveCallbacks) {
1274
+ try {
1275
+ callback(track, reason)
1276
+ } catch (e: Exception) {
1277
+ println("⚠️ Error in track change listener: ${e.message}")
1278
+ }
1279
+ }
1280
+ }
1281
+ }
1282
+
1283
+ private fun notifyPlaybackStateChange(
1284
+ state: TrackPlayerState,
1285
+ reason: Reason?,
1286
+ ) {
1287
+ val liveCallbacks =
1288
+ synchronized(onPlaybackStateChangeListeners) {
1289
+ onPlaybackStateChangeListeners.removeAll { !it.isAlive }
1290
+ onPlaybackStateChangeListeners.filter { it.isAlive }.map { it.callback }
1291
+ }
1292
+
1293
+ handler.post {
1294
+ for (callback in liveCallbacks) {
1295
+ try {
1296
+ callback(state, reason)
1297
+ } catch (e: Exception) {
1298
+ println("⚠️ Error in playback state listener: ${e.message}")
1299
+ }
1300
+ }
1301
+ }
1302
+ }
1303
+
1304
+ private fun notifySeek(
1305
+ position: Double,
1306
+ duration: Double,
1307
+ ) {
1308
+ val liveCallbacks =
1309
+ synchronized(onSeekListeners) {
1310
+ onSeekListeners.removeAll { !it.isAlive }
1311
+ onSeekListeners.filter { it.isAlive }.map { it.callback }
1312
+ }
1313
+
1314
+ handler.post {
1315
+ for (callback in liveCallbacks) {
1316
+ try {
1317
+ callback(position, duration)
1318
+ } catch (e: Exception) {
1319
+ println("⚠️ Error in seek listener: ${e.message}")
1320
+ }
1321
+ }
1322
+ }
1323
+ }
1324
+
1325
+ private fun notifyPlaybackProgress(
1326
+ position: Double,
1327
+ duration: Double,
1328
+ isPlaying: Boolean?,
1329
+ ) {
1330
+ val liveCallbacks =
1331
+ synchronized(onPlaybackProgressChangeListeners) {
1332
+ onPlaybackProgressChangeListeners.removeAll { !it.isAlive }
1333
+ onPlaybackProgressChangeListeners.filter { it.isAlive }.map { it.callback }
1334
+ }
1335
+
1336
+ handler.post {
1337
+ for (callback in liveCallbacks) {
1338
+ try {
1339
+ callback(position, duration, isPlaying)
1340
+ } catch (e: Exception) {
1341
+ println("⚠️ Error in playback progress listener: ${e.message}")
1342
+ }
1343
+ }
1344
+ }
1345
+ }
1346
+
1347
+ /**
1348
+ * Get the actual queue with temporary tracks
1349
+ * Returns: [original_before_current] + [current] + [playNext_stack] + [upNext_queue] + [original_after_current]
1350
+ */
1351
+ fun getActualQueue(): List<TrackItem> {
1352
+ // Called from Promise.async background thread
1353
+ // Check if we're already on the main thread
1354
+ if (android.os.Looper.myLooper() == handler.looper) {
1355
+ return getActualQueueInternal()
1356
+ }
1357
+
1358
+ // Use CountDownLatch to wait for the result on the main thread
1359
+ val latch = CountDownLatch(1)
1360
+ var result: List<TrackItem>? = null
1361
+
1362
+ handler.post {
1363
+ try {
1364
+ result = getActualQueueInternal()
1365
+ } finally {
1366
+ latch.countDown()
1367
+ }
1368
+ }
1369
+
1370
+ try {
1371
+ // Wait up to 5 seconds for the result
1372
+ latch.await(5, TimeUnit.SECONDS)
1373
+ } catch (e: InterruptedException) {
1374
+ println("⚠️ TrackPlayerCore: Interrupted while waiting for actual queue")
1375
+ }
1376
+
1377
+ return result ?: emptyList()
1378
+ }
1379
+
1380
+ private fun getActualQueueInternal(): List<TrackItem> {
1381
+ val queue = mutableListOf<TrackItem>()
1382
+
1383
+ if (!::player.isInitialized) return emptyList()
1384
+
1385
+ val currentIndex = currentTrackIndex
1386
+ if (currentIndex < 0) return emptyList()
1387
+
1388
+ // Add tracks before current (original playlist)
1389
+ // When a temp track is playing, include the original track at currentTrackIndex
1390
+ // (it already played before the temp track started)
1391
+ val beforeEnd = if (currentTemporaryType != TemporaryType.NONE)
1392
+ minOf(currentIndex + 1, currentTracks.size) else currentIndex
1393
+ if (beforeEnd > 0) {
1394
+ queue.addAll(currentTracks.subList(0, beforeEnd))
1395
+ }
1396
+
1397
+ // Add current track (temp or original)
1398
+ getCurrentTrack()?.let { queue.add(it) }
1399
+
1400
+ // Add playNext stack (LIFO - most recently added plays first)
1401
+ // Skip index 0 if current track is from playNext (it's already added as current)
1402
+ if (currentTemporaryType == TemporaryType.PLAY_NEXT && playNextStack.size > 1) {
1403
+ queue.addAll(playNextStack.subList(1, playNextStack.size))
1404
+ } else if (currentTemporaryType != TemporaryType.PLAY_NEXT) {
1405
+ queue.addAll(playNextStack)
1406
+ }
1407
+
1408
+ // Add upNext queue (in order, FIFO)
1409
+ // Skip index 0 if current track is from upNext (it's already added as current)
1410
+ if (currentTemporaryType == TemporaryType.UP_NEXT && upNextQueue.size > 1) {
1411
+ queue.addAll(upNextQueue.subList(1, upNextQueue.size))
1412
+ } else if (currentTemporaryType != TemporaryType.UP_NEXT) {
1413
+ queue.addAll(upNextQueue)
1414
+ }
1415
+
1416
+ // Add remaining original tracks
1417
+ if (currentIndex + 1 < currentTracks.size) {
1418
+ queue.addAll(currentTracks.subList(currentIndex + 1, currentTracks.size))
1419
+ }
1420
+
1421
+ return queue
1422
+ }
692
1423
  }