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.
- package/README.md +444 -4
- package/android/build.gradle +4 -1
- package/android/src/main/AndroidManifest.xml +16 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +2 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +8 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +225 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +105 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +6 -6
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +37 -12
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +944 -213
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +475 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadFileManager.kt +159 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadManagerCore.kt +489 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadWorker.kt +209 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +486 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaBrowserService.kt +3 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +14 -6
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +27 -0
- package/ios/HybridDownloadManager.swift +226 -0
- package/ios/HybridEqualizer.swift +111 -0
- package/ios/HybridTrackPlayer.swift +36 -8
- package/ios/core/TrackPlayerCore.swift +996 -288
- package/ios/download/DownloadDatabase.swift +493 -0
- package/ios/download/DownloadFileManager.swift +241 -0
- package/ios/download/DownloadManagerCore.swift +923 -0
- package/ios/equalizer/EqualizerCore.swift +685 -0
- package/ios/media/MediaSessionManager.swift +40 -28
- package/ios/playlist/PlaylistManager.swift +40 -9
- package/ios/queue/HybridPlayerQueue.swift +33 -13
- package/lib/hooks/callbackManager.d.ts +18 -0
- package/lib/hooks/callbackManager.js +66 -0
- package/lib/hooks/downloadCallbackManager.d.ts +36 -0
- package/lib/hooks/downloadCallbackManager.js +108 -0
- package/lib/hooks/equalizerCallbackManager.d.ts +37 -0
- package/lib/hooks/equalizerCallbackManager.js +109 -0
- package/lib/hooks/index.d.ts +16 -0
- package/lib/hooks/index.js +10 -0
- package/lib/hooks/useActualQueue.d.ts +48 -0
- package/lib/hooks/useActualQueue.js +98 -0
- package/lib/hooks/useDownloadActions.d.ts +26 -0
- package/lib/hooks/useDownloadActions.js +117 -0
- package/lib/hooks/useDownloadProgress.d.ts +25 -0
- package/lib/hooks/useDownloadProgress.js +79 -0
- package/lib/hooks/useDownloadStorage.d.ts +19 -0
- package/lib/hooks/useDownloadStorage.js +60 -0
- package/lib/hooks/useDownloadedTracks.d.ts +25 -0
- package/lib/hooks/useDownloadedTracks.js +69 -0
- package/lib/hooks/useEqualizer.d.ts +25 -0
- package/lib/hooks/useEqualizer.js +124 -0
- package/lib/hooks/useEqualizerPresets.d.ts +22 -0
- package/lib/hooks/useEqualizerPresets.js +96 -0
- package/lib/hooks/useNowPlaying.js +32 -19
- package/lib/hooks/useOnChangeTrack.js +15 -12
- package/lib/hooks/useOnPlaybackProgressChange.js +2 -2
- package/lib/hooks/useOnPlaybackStateChange.js +16 -13
- package/lib/hooks/usePlaylist.d.ts +48 -0
- package/lib/hooks/usePlaylist.js +136 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +6 -0
- package/lib/specs/DownloadManager.nitro.d.ts +152 -0
- package/lib/specs/DownloadManager.nitro.js +1 -0
- package/lib/specs/Equalizer.nitro.d.ts +43 -0
- package/lib/specs/Equalizer.nitro.js +1 -0
- package/lib/specs/TrackPlayer.nitro.d.ts +6 -2
- package/lib/types/DownloadTypes.d.ts +110 -0
- package/lib/types/DownloadTypes.js +1 -0
- package/lib/types/EqualizerTypes.d.ts +52 -0
- package/lib/types/EqualizerTypes.js +1 -0
- package/lib/types/PlayerQueue.d.ts +4 -0
- package/nitro.json +8 -0
- package/nitrogen/generated/android/NitroPlayer+autolinking.cmake +10 -1
- package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +32 -2
- package/nitrogen/generated/android/c++/JCurrentPlayingType.hpp +65 -0
- package/nitrogen/generated/android/c++/JDownloadConfig.hpp +92 -0
- package/nitrogen/generated/android/c++/JDownloadError.hpp +71 -0
- package/nitrogen/generated/android/c++/JDownloadErrorReason.hpp +74 -0
- package/nitrogen/generated/android/c++/JDownloadProgress.hpp +79 -0
- package/nitrogen/generated/android/c++/JDownloadQueueStatus.hpp +81 -0
- package/nitrogen/generated/android/c++/JDownloadState.hpp +71 -0
- package/nitrogen/generated/android/c++/JDownloadStorageInfo.hpp +73 -0
- package/nitrogen/generated/android/c++/JDownloadTask.hpp +108 -0
- package/nitrogen/generated/android/c++/JDownloadedPlaylist.hpp +111 -0
- package/nitrogen/generated/android/c++/JDownloadedTrack.hpp +92 -0
- package/nitrogen/generated/android/c++/JEqualizerBand.hpp +69 -0
- package/nitrogen/generated/android/c++/JEqualizerPreset.hpp +78 -0
- package/nitrogen/generated/android/c++/JEqualizerState.hpp +91 -0
- package/nitrogen/generated/android/c++/JFunc_void_DownloadProgress.hpp +80 -0
- package/nitrogen/generated/android/c++/JFunc_void_DownloadedTrack.hpp +89 -0
- package/nitrogen/generated/android/c++/JFunc_void_TrackItem_std__optional_Reason_.hpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_std__variant_nitro__NullType__std__string__.hpp +81 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string_Playlist_std__optional_QueueOperation_.hpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string_std__string_DownloadState_std__optional_DownloadError_.hpp +83 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_EqualizerBand_.hpp +97 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Playlist__std__optional_QueueOperation_.hpp +2 -0
- package/nitrogen/generated/android/c++/JGainRange.hpp +61 -0
- package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.cpp +470 -0
- package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.hpp +99 -0
- package/nitrogen/generated/android/c++/JHybridEqualizerSpec.cpp +204 -0
- package/nitrogen/generated/android/c++/JHybridEqualizerSpec.hpp +82 -0
- package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +2 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +117 -15
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +6 -2
- package/nitrogen/generated/android/c++/JPlaybackSource.hpp +62 -0
- package/nitrogen/generated/android/c++/JPlayerState.hpp +11 -3
- package/nitrogen/generated/android/c++/JPlaylist.hpp +2 -0
- package/nitrogen/generated/android/c++/JPresetType.hpp +59 -0
- package/nitrogen/generated/android/c++/JStorageLocation.hpp +59 -0
- package/nitrogen/generated/android/c++/JTrackItem.hpp +9 -3
- package/nitrogen/generated/android/c++/JTrackPlayerState.hpp +3 -3
- package/nitrogen/generated/android/c++/JVariant_NullType_Double.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_Double.hpp +69 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadError.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadError.hpp +74 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadTask.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadTask.hpp +84 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedPlaylist.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedPlaylist.hpp +85 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedTrack.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedTrack.hpp +80 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_Playlist.hpp +2 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_TrackItem.hpp +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/CurrentPlayingType.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadConfig.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadError.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadErrorReason.kt +26 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadProgress.kt +53 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadQueueStatus.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadState.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadStorageInfo.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadTask.kt +65 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadedPlaylist.kt +53 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadedTrack.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerBand.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerPreset.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerState.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_DownloadProgress.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_DownloadedTrack.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__optional_std__variant_nitro__NullType__std__string__.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__string_std__string_DownloadState_std__optional_DownloadError_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_EqualizerBand_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/GainRange.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridDownloadManagerSpec.kt +210 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridEqualizerSpec.kt +141 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +19 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlaybackSource.kt +22 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerState.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PresetType.kt +21 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/StorageLocation.kt +21 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackItem.kt +7 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackPlayerState.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_Double.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadError.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadTask.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadedPlaylist.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadedTrack.kt +59 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +138 -8
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +1046 -121
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +66 -0
- package/nitrogen/generated/ios/NitroPlayerAutolinking.mm +16 -0
- package/nitrogen/generated/ios/NitroPlayerAutolinking.swift +30 -0
- package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.hpp +386 -0
- package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.hpp +223 -0
- package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.hpp +1 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +46 -6
- package/nitrogen/generated/ios/swift/CurrentPlayingType.swift +48 -0
- package/nitrogen/generated/ios/swift/DownloadConfig.swift +270 -0
- package/nitrogen/generated/ios/swift/DownloadError.swift +69 -0
- package/nitrogen/generated/ios/swift/DownloadErrorReason.swift +60 -0
- package/nitrogen/generated/ios/swift/DownloadProgress.swift +91 -0
- package/nitrogen/generated/ios/swift/DownloadQueueStatus.swift +102 -0
- package/nitrogen/generated/ios/swift/DownloadState.swift +56 -0
- package/nitrogen/generated/ios/swift/DownloadStorageInfo.swift +80 -0
- package/nitrogen/generated/ios/swift/DownloadTask.swift +315 -0
- package/nitrogen/generated/ios/swift/DownloadedPlaylist.swift +103 -0
- package/nitrogen/generated/ios/swift/DownloadedTrack.swift +147 -0
- package/nitrogen/generated/ios/swift/EqualizerBand.swift +69 -0
- package/nitrogen/generated/ios/swift/EqualizerPreset.swift +70 -0
- package/nitrogen/generated/ios/swift/EqualizerState.swift +115 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_DownloadProgress.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_DownloadStorageInfo.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_DownloadedTrack.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_PlayerState.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +5 -5
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__optional_std__variant_nitro__NullType__std__string__.swift +66 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string_std__string_DownloadState_std__optional_DownloadError_.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_EqualizerBand_.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem_.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_std__string_.swift +47 -0
- package/nitrogen/generated/ios/swift/GainRange.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec.swift +90 -0
- package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec_cxx.swift +705 -0
- package/nitrogen/generated/ios/swift/HybridEqualizerSpec.swift +73 -0
- package/nitrogen/generated/ios/swift/HybridEqualizerSpec_cxx.swift +396 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +6 -2
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +105 -8
- package/nitrogen/generated/ios/swift/PlaybackSource.swift +44 -0
- package/nitrogen/generated/ios/swift/PlayerState.swift +13 -2
- package/nitrogen/generated/ios/swift/PresetType.swift +40 -0
- package/nitrogen/generated/ios/swift/StorageLocation.swift +40 -0
- package/nitrogen/generated/ios/swift/TrackItem.swift +31 -1
- package/nitrogen/generated/ios/swift/TrackPlayerState.swift +4 -4
- package/nitrogen/generated/ios/swift/Variant_NullType_Double.swift +18 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_DownloadError.swift +18 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_DownloadTask.swift +18 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_DownloadedPlaylist.swift +18 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_DownloadedTrack.swift +18 -0
- package/nitrogen/generated/shared/c++/CurrentPlayingType.hpp +84 -0
- package/nitrogen/generated/shared/c++/DownloadConfig.hpp +108 -0
- package/nitrogen/generated/shared/c++/DownloadError.hpp +89 -0
- package/nitrogen/generated/shared/c++/DownloadErrorReason.hpp +96 -0
- package/nitrogen/generated/shared/c++/DownloadProgress.hpp +97 -0
- package/nitrogen/generated/shared/c++/DownloadQueueStatus.hpp +99 -0
- package/nitrogen/generated/shared/c++/DownloadState.hpp +92 -0
- package/nitrogen/generated/shared/c++/DownloadStorageInfo.hpp +91 -0
- package/nitrogen/generated/shared/c++/DownloadTask.hpp +122 -0
- package/nitrogen/generated/shared/c++/DownloadedPlaylist.hpp +101 -0
- package/nitrogen/generated/shared/c++/DownloadedTrack.hpp +107 -0
- package/nitrogen/generated/shared/c++/EqualizerBand.hpp +87 -0
- package/nitrogen/generated/shared/c++/EqualizerPreset.hpp +86 -0
- package/nitrogen/generated/shared/c++/EqualizerState.hpp +89 -0
- package/nitrogen/generated/shared/c++/GainRange.hpp +79 -0
- package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.cpp +55 -0
- package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.hpp +134 -0
- package/nitrogen/generated/shared/c++/HybridEqualizerSpec.cpp +38 -0
- package/nitrogen/generated/shared/c++/HybridEqualizerSpec.hpp +95 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +4 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +11 -5
- package/nitrogen/generated/shared/c++/PlaybackSource.hpp +80 -0
- package/nitrogen/generated/shared/c++/PlayerState.hpp +9 -2
- package/nitrogen/generated/shared/c++/PresetType.hpp +76 -0
- package/nitrogen/generated/shared/c++/StorageLocation.hpp +76 -0
- package/nitrogen/generated/shared/c++/TrackItem.hpp +7 -2
- package/nitrogen/generated/shared/c++/TrackPlayerState.hpp +5 -5
- package/package.json +1 -1
- package/src/hooks/callbackManager.ts +87 -0
- package/src/hooks/downloadCallbackManager.ts +149 -0
- package/src/hooks/equalizerCallbackManager.ts +138 -0
- package/src/hooks/index.ts +23 -0
- package/src/hooks/useActualQueue.ts +116 -0
- package/src/hooks/useDownloadActions.ts +179 -0
- package/src/hooks/useDownloadProgress.ts +126 -0
- package/src/hooks/useDownloadStorage.ts +84 -0
- package/src/hooks/useDownloadedTracks.ts +138 -0
- package/src/hooks/useEqualizer.ts +173 -0
- package/src/hooks/useEqualizerPresets.ts +140 -0
- package/src/hooks/useNowPlaying.ts +33 -20
- package/src/hooks/useOnChangeTrack.ts +15 -11
- package/src/hooks/useOnPlaybackProgressChange.ts +2 -2
- package/src/hooks/useOnPlaybackStateChange.ts +19 -15
- package/src/hooks/usePlaylist.ts +161 -0
- package/src/index.ts +12 -0
- package/src/specs/DownloadManager.nitro.ts +203 -0
- package/src/specs/Equalizer.nitro.ts +69 -0
- package/src/specs/TrackPlayer.nitro.ts +6 -2
- package/src/types/DownloadTypes.ts +135 -0
- package/src/types/EqualizerTypes.ts +72 -0
- 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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
156
|
+
// Set references for MediaBrowserService
|
|
157
|
+
NitroPlayerMediaBrowserService.trackPlayerCore = this
|
|
158
|
+
NitroPlayerMediaBrowserService.mediaSessionManager = mediaSessionManager
|
|
122
159
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
132
|
-
|
|
168
|
+
// Notify JavaScript
|
|
169
|
+
onAndroidAutoConnectionChange?.invoke(connected)
|
|
133
170
|
|
|
134
|
-
|
|
171
|
+
println("🚗 Android Auto connection changed: connected=$connected, type=$connectionType")
|
|
172
|
+
}
|
|
135
173
|
}
|
|
174
|
+
registerCarConnectionReceiver()
|
|
136
175
|
}
|
|
137
|
-
registerCarConnectionReceiver()
|
|
138
|
-
}
|
|
139
176
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
166
|
-
|
|
291
|
+
override fun onPlayWhenReadyChanged(
|
|
292
|
+
playWhenReady: Boolean,
|
|
293
|
+
reason: Int,
|
|
294
|
+
) {
|
|
167
295
|
val r =
|
|
168
296
|
when (reason) {
|
|
169
|
-
Player.
|
|
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
|
-
|
|
175
|
-
mediaSessionManager?.onTrackChanged()
|
|
300
|
+
emitStateChange(r)
|
|
176
301
|
}
|
|
177
|
-
}
|
|
178
302
|
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
307
|
+
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
|
308
|
+
emitStateChange()
|
|
309
|
+
}
|
|
204
310
|
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
390
|
-
|
|
518
|
+
playSongInternal(songId, fromPlaylist)
|
|
519
|
+
}
|
|
520
|
+
}
|
|
391
521
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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("⚠️
|
|
407
|
-
return
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
println("
|
|
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
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
488
|
-
|
|
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
|
-
|
|
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
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
}
|