react-native-nitro-player 0.3.0-alpha.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +970 -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 +998 -276
- 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/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 +3 -2
- package/lib/hooks/useOnChangeTrack.js +15 -12
- 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/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 +3 -2
- package/src/hooks/useOnChangeTrack.ts +15 -11
- 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,18 @@ class TrackPlayerCore private constructor(
|
|
|
484
625
|
|
|
485
626
|
fun skipToPrevious() {
|
|
486
627
|
handler.post {
|
|
487
|
-
|
|
488
|
-
|
|
628
|
+
// If playing temporary track, just seek to beginning (temps not navigable backwards)
|
|
629
|
+
if (currentTemporaryType != TemporaryType.NONE) {
|
|
630
|
+
println("🔄 TrackPlayerCore: Playing temporary track - seeking to beginning")
|
|
631
|
+
player.seekTo(0)
|
|
632
|
+
} else if (currentTrackIndex > 0) {
|
|
633
|
+
// Go to previous track in original playlist
|
|
634
|
+
println("🔄 TrackPlayerCore: Going to previous track, currentTrackIndex: $currentTrackIndex -> ${currentTrackIndex - 1}")
|
|
635
|
+
playFromIndexInternal(currentTrackIndex - 1)
|
|
636
|
+
} else {
|
|
637
|
+
// Already at first track, seek to beginning
|
|
638
|
+
println("🔄 TrackPlayerCore: Already at first track, seeking to beginning")
|
|
639
|
+
player.seekTo(0)
|
|
489
640
|
}
|
|
490
641
|
}
|
|
491
642
|
}
|
|
@@ -513,6 +664,7 @@ class TrackPlayerCore private constructor(
|
|
|
513
664
|
}
|
|
514
665
|
|
|
515
666
|
fun getState(): PlayerState {
|
|
667
|
+
// Called from Promise.async background thread
|
|
516
668
|
// Check if we're already on the main thread
|
|
517
669
|
if (android.os.Looper.myLooper() == handler.looper) {
|
|
518
670
|
return getStateInternal()
|
|
@@ -542,13 +694,8 @@ class TrackPlayerCore private constructor(
|
|
|
542
694
|
|
|
543
695
|
private fun getStateInternal(): PlayerState =
|
|
544
696
|
if (::player.isInitialized) {
|
|
545
|
-
|
|
546
|
-
val track =
|
|
547
|
-
if (currentMediaItem != null) {
|
|
548
|
-
findTrack(currentMediaItem)
|
|
549
|
-
} else {
|
|
550
|
-
null
|
|
551
|
-
}
|
|
697
|
+
// Use getCurrentTrack() which handles temporary tracks properly
|
|
698
|
+
val track = getCurrentTrack()
|
|
552
699
|
|
|
553
700
|
// Convert nullable TrackItem to Variant_NullType_TrackItem
|
|
554
701
|
val currentTrack: Variant_NullType_TrackItem? =
|
|
@@ -581,6 +728,18 @@ class TrackPlayerCore private constructor(
|
|
|
581
728
|
-1.0
|
|
582
729
|
}
|
|
583
730
|
|
|
731
|
+
// Map internal temporary type to CurrentPlayingType
|
|
732
|
+
val currentPlayingTypeValue =
|
|
733
|
+
if (track == null) {
|
|
734
|
+
CurrentPlayingType.NOT_PLAYING
|
|
735
|
+
} else {
|
|
736
|
+
when (currentTemporaryType) {
|
|
737
|
+
TemporaryType.NONE -> CurrentPlayingType.PLAYLIST
|
|
738
|
+
TemporaryType.PLAY_NEXT -> CurrentPlayingType.PLAY_NEXT
|
|
739
|
+
TemporaryType.UP_NEXT -> CurrentPlayingType.UP_NEXT
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
584
743
|
PlayerState(
|
|
585
744
|
currentTrack = currentTrack,
|
|
586
745
|
currentPosition = currentPosition,
|
|
@@ -588,6 +747,7 @@ class TrackPlayerCore private constructor(
|
|
|
588
747
|
currentState = currentState,
|
|
589
748
|
currentPlaylistId = currentPlaylistId?.let { Variant_NullType_String.create(it) },
|
|
590
749
|
currentIndex = currentIndex,
|
|
750
|
+
currentPlayingType = currentPlayingTypeValue,
|
|
591
751
|
)
|
|
592
752
|
} else {
|
|
593
753
|
// Return default state if player is not initialized
|
|
@@ -598,6 +758,7 @@ class TrackPlayerCore private constructor(
|
|
|
598
758
|
currentState = TrackPlayerState.STOPPED,
|
|
599
759
|
currentPlaylistId = currentPlaylistId?.let { Variant_NullType_String.create(it) },
|
|
600
760
|
currentIndex = -1.0,
|
|
761
|
+
currentPlayingType = CurrentPlayingType.NOT_PLAYING,
|
|
601
762
|
)
|
|
602
763
|
}
|
|
603
764
|
|
|
@@ -625,17 +786,418 @@ class TrackPlayerCore private constructor(
|
|
|
625
786
|
fun getCurrentTrack(): TrackItem? {
|
|
626
787
|
if (!::player.isInitialized) return null
|
|
627
788
|
val currentMediaItem = player.currentMediaItem ?: return null
|
|
789
|
+
|
|
790
|
+
// If playing a temporary track, return that
|
|
791
|
+
if (currentTemporaryType != TemporaryType.NONE) {
|
|
792
|
+
val trackId = extractTrackId(currentMediaItem.mediaId)
|
|
793
|
+
|
|
794
|
+
when (currentTemporaryType) {
|
|
795
|
+
TemporaryType.PLAY_NEXT -> {
|
|
796
|
+
return playNextStack.firstOrNull { it.id == trackId }
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
TemporaryType.UP_NEXT -> {
|
|
800
|
+
return upNextQueue.firstOrNull { it.id == trackId }
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
else -> {}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Otherwise return from original playlist
|
|
628
808
|
return findTrack(currentMediaItem)
|
|
629
809
|
}
|
|
630
810
|
|
|
811
|
+
private fun extractTrackId(mediaId: String): String =
|
|
812
|
+
if (mediaId.contains(':')) {
|
|
813
|
+
// Format: "playlistId:trackId"
|
|
814
|
+
mediaId.substring(mediaId.indexOf(':') + 1)
|
|
815
|
+
} else {
|
|
816
|
+
mediaId
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Public method to play from a specific index (for Android Auto)
|
|
631
820
|
// Public method to play from a specific index (for Android Auto)
|
|
632
821
|
fun playFromIndex(index: Int) {
|
|
822
|
+
if (android.os.Looper.myLooper() == handler.looper) {
|
|
823
|
+
playFromIndexInternal(index)
|
|
824
|
+
} else {
|
|
825
|
+
handler.post {
|
|
826
|
+
playFromIndexInternal(index)
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// MARK: - Skip to Index in Actual Queue
|
|
832
|
+
|
|
833
|
+
fun skipToIndex(index: Int): Boolean {
|
|
834
|
+
// Check if we're already on the main thread
|
|
835
|
+
if (android.os.Looper.myLooper() == handler.looper) {
|
|
836
|
+
return skipToIndexInternal(index)
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Use CountDownLatch to wait for the result on the main thread
|
|
840
|
+
val latch = CountDownLatch(1)
|
|
841
|
+
var result = false
|
|
842
|
+
|
|
843
|
+
handler.post {
|
|
844
|
+
try {
|
|
845
|
+
result = skipToIndexInternal(index)
|
|
846
|
+
} finally {
|
|
847
|
+
latch.countDown()
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
try {
|
|
852
|
+
// Wait up to 5 seconds for the result
|
|
853
|
+
latch.await(5, TimeUnit.SECONDS)
|
|
854
|
+
} catch (e: InterruptedException) {
|
|
855
|
+
Thread.currentThread().interrupt()
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
return result
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
private fun skipToIndexInternal(index: Int): Boolean {
|
|
862
|
+
println("\n🎯 TrackPlayerCore: SKIP TO INDEX $index")
|
|
863
|
+
|
|
864
|
+
if (!::player.isInitialized) {
|
|
865
|
+
println(" ❌ Player not initialized")
|
|
866
|
+
return false
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Get actual queue to validate index and determine position
|
|
870
|
+
val actualQueue = getActualQueueInternal()
|
|
871
|
+
val totalQueueSize = actualQueue.size
|
|
872
|
+
|
|
873
|
+
// Validate index
|
|
874
|
+
if (index < 0 || index >= totalQueueSize) {
|
|
875
|
+
println(" ❌ Invalid index $index, queue size is $totalQueueSize")
|
|
876
|
+
return false
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Calculate queue section boundaries
|
|
880
|
+
// ActualQueue structure: [before_current] + [current] + [playNext] + [upNext] + [remaining_original]
|
|
881
|
+
// Use our internal tracking instead of player.currentMediaItemIndex (which is relative to ExoPlayer's subset queue)
|
|
882
|
+
val currentPos = currentTrackIndex
|
|
883
|
+
val playNextStart = currentPos + 1
|
|
884
|
+
val playNextEnd = playNextStart + playNextStack.size
|
|
885
|
+
val upNextStart = playNextEnd
|
|
886
|
+
val upNextEnd = upNextStart + upNextQueue.size
|
|
887
|
+
val originalRemainingStart = upNextEnd
|
|
888
|
+
|
|
889
|
+
println(" Queue structure:")
|
|
890
|
+
println(" currentPos: $currentPos")
|
|
891
|
+
println(" playNextStart: $playNextStart, playNextEnd: $playNextEnd")
|
|
892
|
+
println(" upNextStart: $upNextStart, upNextEnd: $upNextEnd")
|
|
893
|
+
println(" originalRemainingStart: $originalRemainingStart")
|
|
894
|
+
println(" totalQueueSize: $totalQueueSize")
|
|
895
|
+
|
|
896
|
+
// Case 1: Target is before current - use playFromIndex on original
|
|
897
|
+
if (index < currentPos) {
|
|
898
|
+
println(" 📍 Target is before current, jumping to original playlist index $index")
|
|
899
|
+
playFromIndexInternal(index)
|
|
900
|
+
return true
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// Case 2: Target is current - seek to beginning
|
|
904
|
+
if (index == currentPos) {
|
|
905
|
+
println(" 📍 Target is current track, seeking to beginning")
|
|
906
|
+
player.seekTo(0)
|
|
907
|
+
return true
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Case 3: Target is in playNext section
|
|
911
|
+
if (index >= playNextStart && index < playNextEnd) {
|
|
912
|
+
val playNextIndex = index - playNextStart
|
|
913
|
+
println(" 📍 Target is in playNext section at position $playNextIndex")
|
|
914
|
+
|
|
915
|
+
// Remove tracks before the target from playNext (they're being skipped)
|
|
916
|
+
if (playNextIndex > 0) {
|
|
917
|
+
repeat(playNextIndex) { playNextStack.removeAt(0) }
|
|
918
|
+
println(" Removed $playNextIndex tracks from playNext stack")
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Rebuild queue and advance
|
|
922
|
+
rebuildQueueFromCurrentPosition()
|
|
923
|
+
player.seekToNextMediaItem()
|
|
924
|
+
return true
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Case 4: Target is in upNext section
|
|
928
|
+
if (index >= upNextStart && index < upNextEnd) {
|
|
929
|
+
val upNextIndex = index - upNextStart
|
|
930
|
+
println(" 📍 Target is in upNext section at position $upNextIndex")
|
|
931
|
+
|
|
932
|
+
// Clear all playNext tracks (they're being skipped)
|
|
933
|
+
playNextStack.clear()
|
|
934
|
+
println(" Cleared all playNext tracks")
|
|
935
|
+
|
|
936
|
+
// Remove tracks before target from upNext
|
|
937
|
+
if (upNextIndex > 0) {
|
|
938
|
+
repeat(upNextIndex) { upNextQueue.removeAt(0) }
|
|
939
|
+
println(" Removed $upNextIndex tracks from upNext queue")
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Rebuild queue and advance
|
|
943
|
+
rebuildQueueFromCurrentPosition()
|
|
944
|
+
player.seekToNextMediaItem()
|
|
945
|
+
return true
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Case 5: Target is in remaining original tracks
|
|
949
|
+
if (index >= originalRemainingStart) {
|
|
950
|
+
// Get the target track directly from actualQueue
|
|
951
|
+
val targetTrack = actualQueue[index]
|
|
952
|
+
|
|
953
|
+
println(" 📍 Case 5: Target is in remaining original tracks")
|
|
954
|
+
println(" targetTrack.id: ${targetTrack.id}")
|
|
955
|
+
println(" currentTracks.count: ${currentTracks.size}")
|
|
956
|
+
println(" currentTracks IDs: ${currentTracks.map { it.id }}")
|
|
957
|
+
|
|
958
|
+
// Find this track's index in the original playlist
|
|
959
|
+
val originalIndex = currentTracks.indexOfFirst { it.id == targetTrack.id }
|
|
960
|
+
if (originalIndex == -1) {
|
|
961
|
+
println(" ❌ Could not find track ${targetTrack.id} in original playlist")
|
|
962
|
+
println(" Available tracks: ${currentTracks.map { it.id }}")
|
|
963
|
+
return false
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
println(" originalIndex found: $originalIndex")
|
|
967
|
+
|
|
968
|
+
// Clear all temporary tracks (they're being skipped)
|
|
969
|
+
playNextStack.clear()
|
|
970
|
+
upNextQueue.clear()
|
|
971
|
+
currentTemporaryType = TemporaryType.NONE
|
|
972
|
+
println(" Cleared all temporary tracks")
|
|
973
|
+
|
|
974
|
+
// IMPORTANT: Rebuild the ExoPlayer queue without temporary tracks, then seek
|
|
975
|
+
// We need to rebuild from the target index, not just seek
|
|
976
|
+
rebuildQueueAndPlayFromIndex(originalIndex)
|
|
977
|
+
return true
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
println(" ❌ Unexpected case, index $index not handled")
|
|
981
|
+
return false
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
private fun playFromIndexInternal(index: Int) {
|
|
985
|
+
// Clear temporary tracks when jumping to specific index
|
|
986
|
+
playNextStack.clear()
|
|
987
|
+
upNextQueue.clear()
|
|
988
|
+
currentTemporaryType = TemporaryType.NONE
|
|
989
|
+
println(" 🧹 Cleared temporary tracks")
|
|
990
|
+
|
|
991
|
+
rebuildQueueAndPlayFromIndex(index)
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Rebuild the entire ExoPlayer queue from the original playlist starting at the given index
|
|
996
|
+
* This clears all temporary tracks and rebuilds the queue fresh
|
|
997
|
+
*/
|
|
998
|
+
private fun rebuildQueueAndPlayFromIndex(index: Int) {
|
|
999
|
+
if (!::player.isInitialized) {
|
|
1000
|
+
println(" ❌ Player not initialized")
|
|
1001
|
+
return
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
if (index < 0 || index >= currentTracks.size) {
|
|
1005
|
+
println(" ❌ Invalid index $index for currentTracks size ${currentTracks.size}")
|
|
1006
|
+
return
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
println("\n🔄 TrackPlayerCore: REBUILD QUEUE AND PLAY FROM INDEX $index")
|
|
1010
|
+
println(" currentTracks.size: ${currentTracks.size}")
|
|
1011
|
+
println(" currentTracks IDs: ${currentTracks.map { it.id }}")
|
|
1012
|
+
|
|
1013
|
+
// Build queue from the target index onwards
|
|
1014
|
+
val tracksToPlay = currentTracks.subList(index, currentTracks.size)
|
|
1015
|
+
println(" tracksToPlay (${tracksToPlay.size}): ${tracksToPlay.map { it.id }}")
|
|
1016
|
+
|
|
1017
|
+
val playlistId = currentPlaylistId ?: ""
|
|
1018
|
+
val mediaItems =
|
|
1019
|
+
tracksToPlay.map { track ->
|
|
1020
|
+
val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
|
|
1021
|
+
track.toMediaItem(mediaId)
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Update our internal tracking of the position in original playlist
|
|
1025
|
+
currentTrackIndex = index
|
|
1026
|
+
println(" Setting currentTrackIndex to $index")
|
|
1027
|
+
|
|
1028
|
+
// Clear the entire player queue and set new items
|
|
1029
|
+
player.clearMediaItems()
|
|
1030
|
+
player.setMediaItems(mediaItems)
|
|
1031
|
+
player.seekToDefaultPosition(0) // Seek to first item (which is our target track)
|
|
1032
|
+
player.playWhenReady = true
|
|
1033
|
+
player.prepare()
|
|
1034
|
+
|
|
1035
|
+
println(" ✅ Queue rebuilt with ${player.mediaItemCount} items, playing from index 0 (track ${tracksToPlay.firstOrNull()?.id})")
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// MARK: - Temporary Track Management
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Add a track to the up-next queue (FIFO - first added plays first)
|
|
1042
|
+
* Track will be inserted after currently playing track and any playNext tracks
|
|
1043
|
+
*/
|
|
1044
|
+
fun addToUpNext(trackId: String) {
|
|
1045
|
+
handler.post {
|
|
1046
|
+
addToUpNextInternal(trackId)
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
private fun addToUpNextInternal(trackId: String) {
|
|
1051
|
+
println("📋 TrackPlayerCore: addToUpNext($trackId)")
|
|
1052
|
+
|
|
1053
|
+
// Find the track from current playlist or all playlists
|
|
1054
|
+
val track = findTrackById(trackId)
|
|
1055
|
+
if (track == null) {
|
|
1056
|
+
println("❌ TrackPlayerCore: Track $trackId not found")
|
|
1057
|
+
return
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Add to end of upNext queue (FIFO)
|
|
1061
|
+
upNextQueue.add(track)
|
|
1062
|
+
println(" ✅ Added '${track.title}' to upNext queue (position: ${upNextQueue.size})")
|
|
1063
|
+
|
|
1064
|
+
// Rebuild the player queue if actively playing
|
|
1065
|
+
if (::player.isInitialized && player.currentMediaItem != null) {
|
|
1066
|
+
rebuildQueueFromCurrentPosition()
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Add a track to play next (LIFO - last added plays first)
|
|
1072
|
+
* Track will be inserted immediately after currently playing track
|
|
1073
|
+
*/
|
|
1074
|
+
fun playNext(trackId: String) {
|
|
633
1075
|
handler.post {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
1076
|
+
playNextInternal(trackId)
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
private fun playNextInternal(trackId: String) {
|
|
1081
|
+
println("⏭️ TrackPlayerCore: playNext($trackId)")
|
|
1082
|
+
|
|
1083
|
+
// Find the track from current playlist or all playlists
|
|
1084
|
+
val track = findTrackById(trackId)
|
|
1085
|
+
if (track == null) {
|
|
1086
|
+
println("❌ TrackPlayerCore: Track $trackId not found")
|
|
1087
|
+
return
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// Insert at beginning of playNext stack (LIFO)
|
|
1091
|
+
playNextStack.add(0, track)
|
|
1092
|
+
println(" ✅ Added '${track.title}' to playNext stack (position: 1)")
|
|
1093
|
+
|
|
1094
|
+
// Rebuild the player queue if actively playing
|
|
1095
|
+
if (::player.isInitialized && player.currentMediaItem != null) {
|
|
1096
|
+
rebuildQueueFromCurrentPosition()
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Rebuild the ExoPlayer queue from current position with temporary tracks
|
|
1102
|
+
* Order: [current] + [playNext stack] + [upNext queue] + [remaining original]
|
|
1103
|
+
*/
|
|
1104
|
+
private fun rebuildQueueFromCurrentPosition() {
|
|
1105
|
+
if (!::player.isInitialized) return
|
|
1106
|
+
|
|
1107
|
+
println("\n🔄 TrackPlayerCore: REBUILDING QUEUE FROM CURRENT POSITION")
|
|
1108
|
+
println(" currentIndex: ${player.currentMediaItemIndex}")
|
|
1109
|
+
println(" currentMediaItem: ${player.currentMediaItem?.mediaId}")
|
|
1110
|
+
println(" playNextStack (${playNextStack.size}): ${playNextStack.map { "${it.id}:${it.title}" }}")
|
|
1111
|
+
println(" upNextQueue (${upNextQueue.size}): ${upNextQueue.map { "${it.id}:${it.title}" }}")
|
|
1112
|
+
|
|
1113
|
+
val currentIndex = player.currentMediaItemIndex
|
|
1114
|
+
if (currentIndex < 0) return
|
|
1115
|
+
|
|
1116
|
+
// Build new queue order:
|
|
1117
|
+
// [playNext stack] + [upNext queue] + [remaining original tracks]
|
|
1118
|
+
val newQueueTracks = mutableListOf<TrackItem>()
|
|
1119
|
+
|
|
1120
|
+
// Add playNext stack (LIFO - most recently added plays first)
|
|
1121
|
+
// Stack is already in correct order since we insert at position 0
|
|
1122
|
+
newQueueTracks.addAll(playNextStack)
|
|
1123
|
+
|
|
1124
|
+
// Add upNext queue (in order, FIFO)
|
|
1125
|
+
newQueueTracks.addAll(upNextQueue)
|
|
1126
|
+
|
|
1127
|
+
// Add remaining original tracks
|
|
1128
|
+
if (currentIndex + 1 < currentTracks.size) {
|
|
1129
|
+
val remaining = currentTracks.subList(currentIndex + 1, currentTracks.size)
|
|
1130
|
+
println(" remaining original (${remaining.size}): ${remaining.map { it.id }}")
|
|
1131
|
+
newQueueTracks.addAll(remaining)
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
println(" New queue total: ${newQueueTracks.size} tracks")
|
|
1135
|
+
println(" Queue order: ${newQueueTracks.map { it.id }}")
|
|
1136
|
+
|
|
1137
|
+
// Create MediaItems for new tracks
|
|
1138
|
+
val playlistId = currentPlaylistId ?: ""
|
|
1139
|
+
val newMediaItems =
|
|
1140
|
+
newQueueTracks.map { track ->
|
|
1141
|
+
val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
|
|
1142
|
+
println(" Creating MediaItem: mediaId=$mediaId, title=${track.title}")
|
|
1143
|
+
track.toMediaItem(mediaId)
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// Remove all items after current
|
|
1147
|
+
val removedCount = player.mediaItemCount - currentIndex - 1
|
|
1148
|
+
println(" Removing $removedCount items after current")
|
|
1149
|
+
while (player.mediaItemCount > currentIndex + 1) {
|
|
1150
|
+
player.removeMediaItem(currentIndex + 1)
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// Add new items
|
|
1154
|
+
player.addMediaItems(newMediaItems)
|
|
1155
|
+
|
|
1156
|
+
println(" ✅ Queue rebuilt. Player now has ${player.mediaItemCount} items")
|
|
1157
|
+
for (i in 0 until player.mediaItemCount) {
|
|
1158
|
+
println(" [$i]: ${player.getMediaItemAt(i).mediaId}")
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/**
|
|
1163
|
+
* Find a track by ID from current playlist or all playlists
|
|
1164
|
+
*/
|
|
1165
|
+
private fun findTrackById(trackId: String): TrackItem? {
|
|
1166
|
+
// First check current playlist
|
|
1167
|
+
currentTracks.find { it.id == trackId }?.let { return it }
|
|
1168
|
+
|
|
1169
|
+
// Then check all playlists
|
|
1170
|
+
val allPlaylists = playlistManager.getAllPlaylists()
|
|
1171
|
+
for (playlist in allPlaylists) {
|
|
1172
|
+
playlist.tracks.find { it.id == trackId }?.let { return it }
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
return null
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/**
|
|
1179
|
+
* Determine what type of track is currently playing
|
|
1180
|
+
*/
|
|
1181
|
+
private fun determineCurrentTemporaryType(): TemporaryType {
|
|
1182
|
+
val currentItem = player.currentMediaItem ?: return TemporaryType.NONE
|
|
1183
|
+
val trackId =
|
|
1184
|
+
if (currentItem.mediaId.contains(':')) {
|
|
1185
|
+
currentItem.mediaId.substring(currentItem.mediaId.indexOf(':') + 1)
|
|
1186
|
+
} else {
|
|
1187
|
+
currentItem.mediaId
|
|
637
1188
|
}
|
|
1189
|
+
|
|
1190
|
+
// Check if in playNext stack
|
|
1191
|
+
if (playNextStack.any { it.id == trackId }) {
|
|
1192
|
+
return TemporaryType.PLAY_NEXT
|
|
638
1193
|
}
|
|
1194
|
+
|
|
1195
|
+
// Check if in upNext queue
|
|
1196
|
+
if (upNextQueue.any { it.id == trackId }) {
|
|
1197
|
+
return TemporaryType.UP_NEXT
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
return TemporaryType.NONE
|
|
639
1201
|
}
|
|
640
1202
|
|
|
641
1203
|
// Clean up resources
|
|
@@ -689,4 +1251,199 @@ class TrackPlayerCore private constructor(
|
|
|
689
1251
|
println("⚠️ TrackPlayerCore: Cannot set volume - player not initialized")
|
|
690
1252
|
false
|
|
691
1253
|
}
|
|
1254
|
+
|
|
1255
|
+
// Add event listeners
|
|
1256
|
+
fun addOnChangeTrackListener(callback: (TrackItem, Reason?) -> Unit) {
|
|
1257
|
+
val box = WeakCallbackBox(WeakReference(this), callback)
|
|
1258
|
+
onChangeTrackListeners.add(box)
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
fun addOnPlaybackStateChangeListener(callback: (TrackPlayerState, Reason?) -> Unit) {
|
|
1262
|
+
val box = WeakCallbackBox(WeakReference(this), callback)
|
|
1263
|
+
onPlaybackStateChangeListeners.add(box)
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
fun addOnSeekListener(callback: (Double, Double) -> Unit) {
|
|
1267
|
+
val box = WeakCallbackBox(WeakReference(this), callback)
|
|
1268
|
+
onSeekListeners.add(box)
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
fun addOnPlaybackProgressChangeListener(callback: (Double, Double, Boolean?) -> Unit) {
|
|
1272
|
+
val box = WeakCallbackBox(WeakReference(this), callback)
|
|
1273
|
+
onPlaybackProgressChangeListeners.add(box)
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// Notification helpers with auto-cleanup
|
|
1277
|
+
private fun notifyTrackChange(
|
|
1278
|
+
track: TrackItem,
|
|
1279
|
+
reason: Reason?,
|
|
1280
|
+
) {
|
|
1281
|
+
val liveCallbacks =
|
|
1282
|
+
synchronized(onChangeTrackListeners) {
|
|
1283
|
+
onChangeTrackListeners.removeAll { !it.isAlive }
|
|
1284
|
+
onChangeTrackListeners.filter { it.isAlive }.map { it.callback }
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
handler.post {
|
|
1288
|
+
for (callback in liveCallbacks) {
|
|
1289
|
+
try {
|
|
1290
|
+
callback(track, reason)
|
|
1291
|
+
} catch (e: Exception) {
|
|
1292
|
+
println("⚠️ Error in track change listener: ${e.message}")
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
private fun notifyPlaybackStateChange(
|
|
1299
|
+
state: TrackPlayerState,
|
|
1300
|
+
reason: Reason?,
|
|
1301
|
+
) {
|
|
1302
|
+
val liveCallbacks =
|
|
1303
|
+
synchronized(onPlaybackStateChangeListeners) {
|
|
1304
|
+
onPlaybackStateChangeListeners.removeAll { !it.isAlive }
|
|
1305
|
+
onPlaybackStateChangeListeners.filter { it.isAlive }.map { it.callback }
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
handler.post {
|
|
1309
|
+
for (callback in liveCallbacks) {
|
|
1310
|
+
try {
|
|
1311
|
+
callback(state, reason)
|
|
1312
|
+
} catch (e: Exception) {
|
|
1313
|
+
println("⚠️ Error in playback state listener: ${e.message}")
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
private fun notifySeek(
|
|
1320
|
+
position: Double,
|
|
1321
|
+
duration: Double,
|
|
1322
|
+
) {
|
|
1323
|
+
val liveCallbacks =
|
|
1324
|
+
synchronized(onSeekListeners) {
|
|
1325
|
+
onSeekListeners.removeAll { !it.isAlive }
|
|
1326
|
+
onSeekListeners.filter { it.isAlive }.map { it.callback }
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
handler.post {
|
|
1330
|
+
for (callback in liveCallbacks) {
|
|
1331
|
+
try {
|
|
1332
|
+
callback(position, duration)
|
|
1333
|
+
} catch (e: Exception) {
|
|
1334
|
+
println("⚠️ Error in seek listener: ${e.message}")
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
private fun notifyPlaybackProgress(
|
|
1341
|
+
position: Double,
|
|
1342
|
+
duration: Double,
|
|
1343
|
+
isPlaying: Boolean?,
|
|
1344
|
+
) {
|
|
1345
|
+
val liveCallbacks =
|
|
1346
|
+
synchronized(onPlaybackProgressChangeListeners) {
|
|
1347
|
+
onPlaybackProgressChangeListeners.removeAll { !it.isAlive }
|
|
1348
|
+
onPlaybackProgressChangeListeners.filter { it.isAlive }.map { it.callback }
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
handler.post {
|
|
1352
|
+
for (callback in liveCallbacks) {
|
|
1353
|
+
try {
|
|
1354
|
+
callback(position, duration, isPlaying)
|
|
1355
|
+
} catch (e: Exception) {
|
|
1356
|
+
println("⚠️ Error in playback progress listener: ${e.message}")
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/**
|
|
1363
|
+
* Get the actual queue with temporary tracks
|
|
1364
|
+
* Returns: [original_before_current] + [current] + [playNext_stack] + [upNext_queue] + [original_after_current]
|
|
1365
|
+
*/
|
|
1366
|
+
fun getActualQueue(): List<TrackItem> {
|
|
1367
|
+
// Called from Promise.async background thread
|
|
1368
|
+
// Check if we're already on the main thread
|
|
1369
|
+
if (android.os.Looper.myLooper() == handler.looper) {
|
|
1370
|
+
return getActualQueueInternal()
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// Use CountDownLatch to wait for the result on the main thread
|
|
1374
|
+
val latch = CountDownLatch(1)
|
|
1375
|
+
var result: List<TrackItem>? = null
|
|
1376
|
+
|
|
1377
|
+
handler.post {
|
|
1378
|
+
try {
|
|
1379
|
+
result = getActualQueueInternal()
|
|
1380
|
+
} finally {
|
|
1381
|
+
latch.countDown()
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
try {
|
|
1386
|
+
// Wait up to 5 seconds for the result
|
|
1387
|
+
latch.await(5, TimeUnit.SECONDS)
|
|
1388
|
+
} catch (e: InterruptedException) {
|
|
1389
|
+
println("⚠️ TrackPlayerCore: Interrupted while waiting for actual queue")
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
return result ?: emptyList()
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
private fun getActualQueueInternal(): List<TrackItem> {
|
|
1396
|
+
println("\n🔍 TrackPlayerCore: getActualQueueInternal() called")
|
|
1397
|
+
println(" playNextStack size: ${playNextStack.size}, tracks: ${playNextStack.map { it.id }}")
|
|
1398
|
+
println(" upNextQueue size: ${upNextQueue.size}, tracks: ${upNextQueue.map { it.id }}")
|
|
1399
|
+
println(" currentTracks size: ${currentTracks.size}, tracks: ${currentTracks.map { it.id }}")
|
|
1400
|
+
println(" currentTrackIndex: $currentTrackIndex")
|
|
1401
|
+
|
|
1402
|
+
val queue = mutableListOf<TrackItem>()
|
|
1403
|
+
|
|
1404
|
+
if (!::player.isInitialized) {
|
|
1405
|
+
println(" ❌ Player not initialized, returning empty")
|
|
1406
|
+
return emptyList()
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// Use our internal tracking of position in original playlist
|
|
1410
|
+
val currentIndex = currentTrackIndex
|
|
1411
|
+
println(" Using currentTrackIndex: $currentIndex")
|
|
1412
|
+
if (currentIndex < 0) {
|
|
1413
|
+
println(" ❌ currentIndex < 0, returning empty")
|
|
1414
|
+
return emptyList()
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// Add tracks before current (original playlist)
|
|
1418
|
+
if (currentIndex > 0 && currentIndex <= currentTracks.size) {
|
|
1419
|
+
val beforeCurrent = currentTracks.subList(0, currentIndex)
|
|
1420
|
+
println(" Adding ${beforeCurrent.size} tracks before current")
|
|
1421
|
+
queue.addAll(beforeCurrent)
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// Add current track
|
|
1425
|
+
getCurrentTrack()?.let {
|
|
1426
|
+
println(" Adding current track: ${it.id}")
|
|
1427
|
+
queue.add(it)
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// Add playNext stack (LIFO - most recently added plays first)
|
|
1431
|
+
// Stack is already in correct order since we insert at position 0
|
|
1432
|
+
println(" Adding ${playNextStack.size} playNext tracks")
|
|
1433
|
+
queue.addAll(playNextStack)
|
|
1434
|
+
|
|
1435
|
+
// Add upNext queue (in order, FIFO)
|
|
1436
|
+
println(" Adding ${upNextQueue.size} upNext tracks")
|
|
1437
|
+
queue.addAll(upNextQueue)
|
|
1438
|
+
|
|
1439
|
+
// Add remaining original tracks
|
|
1440
|
+
if (currentIndex + 1 < currentTracks.size) {
|
|
1441
|
+
val remaining = currentTracks.subList(currentIndex + 1, currentTracks.size)
|
|
1442
|
+
println(" Adding ${remaining.size} remaining tracks")
|
|
1443
|
+
queue.addAll(remaining)
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
println(" ✅ Final queue size: ${queue.size}, tracks: ${queue.map { it.id }}")
|
|
1447
|
+
return queue
|
|
1448
|
+
}
|
|
692
1449
|
}
|