react-native-nitro-player 0.6.1 → 0.7.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/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +9 -13
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +45 -90
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +48 -182
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +21 -77
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +55 -104
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +113 -123
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ExoPlayerCore.kt +82 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ListenerRegistry.kt +48 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerAndroidAuto.kt +62 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +153 -1887
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +122 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerNotify.kt +44 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +162 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueue.kt +165 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +161 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +28 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerTempQueue.kt +121 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +98 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +27 -18
- package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +11 -58
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +13 -30
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +102 -162
- package/ios/HybridDownloadManager.swift +32 -26
- package/ios/HybridEqualizer.swift +48 -35
- package/ios/HybridTrackPlayer.swift +127 -102
- package/ios/core/ListenerRegistry.swift +60 -0
- package/ios/core/TrackPlayerCore.swift +130 -2356
- package/ios/core/TrackPlayerListener.swift +395 -0
- package/ios/core/TrackPlayerNotify.swift +52 -0
- package/ios/core/TrackPlayerPlayback.swift +274 -0
- package/ios/core/TrackPlayerQueue.swift +212 -0
- package/ios/core/TrackPlayerQueueBuild.swift +482 -0
- package/ios/core/TrackPlayerTempQueue.swift +167 -0
- package/ios/core/TrackPlayerUrlLoader.swift +169 -0
- package/ios/equalizer/EqualizerCore.swift +24 -89
- package/ios/media/MediaSessionManager.swift +32 -49
- package/ios/playlist/PlaylistManager.swift +2 -9
- package/ios/queue/HybridPlayerQueue.swift +69 -66
- package/lib/hooks/useDownloadedTracks.js +16 -13
- package/lib/hooks/useEqualizer.d.ts +4 -4
- package/lib/hooks/useEqualizer.js +12 -12
- package/lib/hooks/useEqualizerPresets.d.ts +3 -3
- package/lib/hooks/useEqualizerPresets.js +12 -18
- package/lib/specs/AndroidAutoMediaLibrary.nitro.d.ts +2 -2
- package/lib/specs/AudioDevices.nitro.d.ts +2 -2
- package/lib/specs/DownloadManager.nitro.d.ts +10 -10
- package/lib/specs/Equalizer.nitro.d.ts +9 -9
- package/lib/specs/TrackPlayer.nitro.d.ts +38 -16
- package/nitro.json +44 -11
- package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +63 -24
- package/nitrogen/generated/android/c++/JCurrentPlayingType.hpp +1 -1
- package/nitrogen/generated/android/c++/JDownloadConfig.hpp +1 -1
- package/nitrogen/generated/android/c++/JDownloadError.hpp +1 -1
- package/nitrogen/generated/android/c++/JDownloadErrorReason.hpp +1 -1
- package/nitrogen/generated/android/c++/JDownloadProgress.hpp +1 -1
- package/nitrogen/generated/android/c++/JDownloadQueueStatus.hpp +1 -1
- package/nitrogen/generated/android/c++/JDownloadState.hpp +1 -1
- package/nitrogen/generated/android/c++/JDownloadStorageInfo.hpp +1 -1
- package/nitrogen/generated/android/c++/JDownloadTask.hpp +1 -1
- package/nitrogen/generated/android/c++/JDownloadedPlaylist.hpp +1 -1
- package/nitrogen/generated/android/c++/JDownloadedTrack.hpp +1 -1
- package/nitrogen/generated/android/c++/JEqualizerBand.hpp +1 -1
- package/nitrogen/generated/android/c++/JEqualizerPreset.hpp +1 -1
- package/nitrogen/generated/android/c++/JEqualizerState.hpp +1 -1
- package/nitrogen/generated/android/c++/JFunc_void_DownloadProgress.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_DownloadedTrack.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_TrackItem_std__optional_Reason_.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_TrackPlayerState_std__optional_Reason_.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_double_double_std__optional_bool_.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_std__variant_nitro__NullType__std__string__.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_std__string_Playlist_std__optional_QueueOperation_.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_std__string_std__string_DownloadState_std__optional_DownloadError_.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_EqualizerBand_.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Playlist__std__optional_QueueOperation_.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double.hpp +2 -2
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__std__vector_TrackItem_.hpp +122 -0
- package/nitrogen/generated/android/c++/JGainRange.hpp +1 -1
- package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.cpp +49 -30
- package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.hpp +21 -24
- package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.cpp +35 -28
- package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.hpp +20 -23
- package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.cpp +197 -93
- package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.hpp +29 -32
- package/nitrogen/generated/android/c++/JHybridEqualizerSpec.cpp +157 -67
- package/nitrogen/generated/android/c++/JHybridEqualizerSpec.hpp +28 -31
- package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +138 -53
- package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.hpp +27 -30
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +282 -69
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +35 -30
- package/nitrogen/generated/android/c++/JPlaybackSource.hpp +1 -1
- package/nitrogen/generated/android/c++/JPlayerConfig.hpp +1 -1
- package/nitrogen/generated/android/c++/JPlayerState.hpp +1 -1
- package/nitrogen/generated/android/c++/JPlaylist.hpp +1 -1
- package/nitrogen/generated/android/c++/JPresetType.hpp +1 -1
- package/nitrogen/generated/android/c++/JQueueOperation.hpp +1 -1
- package/nitrogen/generated/android/c++/JReason.hpp +1 -1
- package/nitrogen/generated/android/c++/JRepeatMode.hpp +1 -1
- package/nitrogen/generated/android/c++/JStorageLocation.hpp +1 -1
- package/nitrogen/generated/android/c++/JTAudioDevice.hpp +1 -1
- package/nitrogen/generated/android/c++/JTrackItem.hpp +1 -1
- package/nitrogen/generated/android/c++/JTrackPlayerState.hpp +1 -1
- package/nitrogen/generated/android/c++/JVariant_NullType_Double.hpp +3 -3
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadError.hpp +3 -3
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadTask.hpp +3 -3
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedPlaylist.hpp +3 -3
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedTrack.hpp +3 -3
- package/nitrogen/generated/android/c++/JVariant_NullType_Playlist.hpp +3 -3
- package/nitrogen/generated/android/c++/JVariant_NullType_String.hpp +3 -3
- package/nitrogen/generated/android/c++/JVariant_NullType_TrackItem.hpp +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_TrackItem__std__vector_TrackItem_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrarySpec.kt +18 -20
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAudioDevicesSpec.kt +17 -19
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridDownloadManagerSpec.kt +25 -28
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridEqualizerSpec.kt +25 -27
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridPlayerQueueSpec.kt +24 -26
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +60 -26
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_Double.kt +0 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadError.kt +0 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadTask.kt +0 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadedPlaylist.kt +0 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadedTrack.kt +0 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_Playlist.kt +0 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_String.kt +0 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_TrackItem.kt +0 -6
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +74 -18
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +380 -151
- package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.hpp +10 -10
- package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.hpp +12 -9
- package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.hpp +23 -8
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +82 -8
- package/nitrogen/generated/ios/swift/Func_void_EqualizerState.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__DownloadedPlaylist_.swift +58 -0
- package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__DownloadedTrack_.swift +58 -0
- package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__std__string_.swift +58 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_DownloadedPlaylist_.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_DownloadedTrack_.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_EqualizerBand_.swift +5 -5
- package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__std__vector_TrackItem_.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec.swift +10 -10
- package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec_cxx.swift +141 -71
- package/nitrogen/generated/ios/swift/HybridEqualizerSpec.swift +9 -9
- package/nitrogen/generated/ios/swift/HybridEqualizerSpec_cxx.swift +105 -41
- package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec.swift +8 -8
- package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec_cxx.swift +95 -32
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +16 -8
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +267 -32
- package/nitrogen/generated/shared/c++/HybridAndroidAutoMediaLibrarySpec.hpp +3 -2
- package/nitrogen/generated/shared/c++/HybridAudioDevicesSpec.hpp +2 -1
- package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.hpp +10 -10
- package/nitrogen/generated/shared/c++/HybridEqualizerSpec.hpp +10 -9
- package/nitrogen/generated/shared/c++/HybridPlayerQueueSpec.hpp +9 -8
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +8 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +16 -8
- package/package.json +3 -3
- package/src/hooks/useDownloadedTracks.ts +17 -13
- package/src/hooks/useEqualizer.ts +16 -16
- package/src/hooks/useEqualizerPresets.ts +15 -21
- package/src/specs/AndroidAutoMediaLibrary.nitro.ts +2 -2
- package/src/specs/AudioDevices.nitro.ts +2 -2
- package/src/specs/DownloadManager.nitro.ts +10 -10
- package/src/specs/Equalizer.nitro.ts +9 -9
- package/src/specs/TrackPlayer.nitro.ts +52 -16
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
@file:Suppress("ktlint:standard:max-line-length")
|
|
2
|
+
|
|
3
|
+
package com.margelo.nitro.nitroplayer.core
|
|
4
|
+
|
|
5
|
+
import androidx.media3.common.MediaItem
|
|
6
|
+
import androidx.media3.common.Player
|
|
7
|
+
import com.margelo.nitro.nitroplayer.Reason
|
|
8
|
+
import com.margelo.nitro.nitroplayer.RepeatMode
|
|
9
|
+
import com.margelo.nitro.nitroplayer.equalizer.EqualizerCore
|
|
10
|
+
import com.margelo.nitro.nitroplayer.media.NitroPlayerMediaBrowserService
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* ExoPlayer event listener — translates low-level ExoPlayer callbacks into
|
|
14
|
+
* TrackPlayerCore state mutations and JS-facing listener notifications.
|
|
15
|
+
* All callbacks fire on the player thread (ExoPlayerCore is built with playerThread.looper).
|
|
16
|
+
*/
|
|
17
|
+
internal class TrackPlayerEventListener(
|
|
18
|
+
private val core: TrackPlayerCore,
|
|
19
|
+
) : Player.Listener {
|
|
20
|
+
|
|
21
|
+
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
|
22
|
+
with(core) {
|
|
23
|
+
// TRACK repeat: REPEAT_MODE_ONE fires this every loop — not a real track change
|
|
24
|
+
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT) return
|
|
25
|
+
|
|
26
|
+
// Remove the track that just finished/was skipped from temp lists
|
|
27
|
+
if ((reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO ||
|
|
28
|
+
reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK) &&
|
|
29
|
+
previousMediaItem != null
|
|
30
|
+
) {
|
|
31
|
+
previousMediaItem?.mediaId?.let { mediaId ->
|
|
32
|
+
val trackId = extractTrackId(mediaId)
|
|
33
|
+
val pnIdx = playNextStack.indexOfFirst { it.id == trackId }
|
|
34
|
+
if (pnIdx >= 0) {
|
|
35
|
+
playNextStack.removeAt(pnIdx)
|
|
36
|
+
} else {
|
|
37
|
+
val unIdx = upNextQueue.indexOfFirst { it.id == trackId }
|
|
38
|
+
if (unIdx >= 0) upNextQueue.removeAt(unIdx)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Track new current item as "previous" for the next transition
|
|
44
|
+
previousMediaItem = mediaItem
|
|
45
|
+
|
|
46
|
+
// Re-determine temporary type for the new current item
|
|
47
|
+
currentTemporaryType = determineCurrentTemporaryType()
|
|
48
|
+
|
|
49
|
+
// Update currentTrackIndex when landing on an original playlist track
|
|
50
|
+
if (currentTemporaryType == TrackPlayerCore.TemporaryType.NONE && mediaItem != null) {
|
|
51
|
+
val trackId = extractTrackId(mediaItem.mediaId)
|
|
52
|
+
val newIdx = currentTracks.indexOfFirst { it.id == trackId }
|
|
53
|
+
if (newIdx >= 0 && newIdx != currentTrackIndex) currentTrackIndex = newIdx
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
val track = getCurrentTrack() ?: return
|
|
57
|
+
val r = when (reason) {
|
|
58
|
+
Player.MEDIA_ITEM_TRANSITION_REASON_AUTO -> Reason.END
|
|
59
|
+
Player.MEDIA_ITEM_TRANSITION_REASON_SEEK -> Reason.USER_ACTION
|
|
60
|
+
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED -> Reason.USER_ACTION
|
|
61
|
+
else -> null
|
|
62
|
+
}
|
|
63
|
+
notifyTrackChange(track, r)
|
|
64
|
+
mediaSessionManager?.onTrackChanged(track)
|
|
65
|
+
checkUpcomingTracksForUrls(lookaheadCount)
|
|
66
|
+
notifyTemporaryQueueChange()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
override fun onTimelineChanged(timeline: androidx.media3.common.Timeline, reason: Int) {
|
|
71
|
+
if (reason == Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) {
|
|
72
|
+
NitroPlayerMediaBrowserService.getInstance()?.onPlaylistsUpdated()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
|
|
77
|
+
val r = if (reason == Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST) Reason.USER_ACTION else null
|
|
78
|
+
core.emitStateChange(r)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
override fun onPlaybackStateChanged(playbackState: Int) {
|
|
82
|
+
with(core) {
|
|
83
|
+
if (playbackState == Player.STATE_ENDED && currentRepeatMode == RepeatMode.PLAYLIST) {
|
|
84
|
+
playNextStack.clear()
|
|
85
|
+
upNextQueue.clear()
|
|
86
|
+
currentTemporaryType = TrackPlayerCore.TemporaryType.NONE
|
|
87
|
+
rebuildQueueAndPlayFromIndex(0)
|
|
88
|
+
val firstTrack = currentTracks.getOrNull(0)
|
|
89
|
+
if (firstTrack != null) notifyTrackChange(firstTrack, Reason.REPEAT)
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
emitStateChange()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
|
97
|
+
core.emitStateChange()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
override fun onPositionDiscontinuity(
|
|
101
|
+
oldPosition: Player.PositionInfo,
|
|
102
|
+
newPosition: Player.PositionInfo,
|
|
103
|
+
reason: Int,
|
|
104
|
+
) {
|
|
105
|
+
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
|
|
106
|
+
core.isManuallySeeked = true
|
|
107
|
+
val pos = core.exo.currentPosition / 1000.0
|
|
108
|
+
val dur = if (core.exo.duration > 0) core.exo.duration / 1000.0 else 0.0
|
|
109
|
+
core.notifySeek(pos, dur)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
override fun onAudioSessionIdChanged(audioSessionId: Int) {
|
|
114
|
+
if (audioSessionId != 0) {
|
|
115
|
+
try {
|
|
116
|
+
EqualizerCore.getInstance(core.context).initialize(audioSessionId)
|
|
117
|
+
} catch (_: Exception) {
|
|
118
|
+
// Non-critical — device may not support equalizer
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
package com.margelo.nitro.nitroplayer.core
|
|
2
|
+
|
|
3
|
+
import com.margelo.nitro.nitroplayer.Reason
|
|
4
|
+
import com.margelo.nitro.nitroplayer.TrackItem
|
|
5
|
+
import com.margelo.nitro.nitroplayer.TrackPlayerState
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Notification helpers — call the registered ListenerRegistry callbacks.
|
|
9
|
+
* All methods must be called from the player thread (already serialised).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
internal fun TrackPlayerCore.notifyTrackChange(track: TrackItem, reason: Reason?) {
|
|
13
|
+
onChangeTrackListeners.forEach { it(track, reason) }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
internal fun TrackPlayerCore.notifyPlaybackStateChange(state: TrackPlayerState, reason: Reason?) {
|
|
17
|
+
onPlaybackStateChangeListeners.forEach { it(state, reason) }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
internal fun TrackPlayerCore.notifySeek(position: Double, duration: Double) {
|
|
21
|
+
onSeekListeners.forEach { it(position, duration) }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
internal fun TrackPlayerCore.notifyPlaybackProgress(
|
|
25
|
+
position: Double,
|
|
26
|
+
duration: Double,
|
|
27
|
+
isManuallySeeked: Boolean?,
|
|
28
|
+
) {
|
|
29
|
+
onProgressListeners.forEach { it(position, duration, isManuallySeeked) }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
internal fun TrackPlayerCore.notifyTracksNeedUpdate(tracks: List<TrackItem>, lookahead: Int) {
|
|
33
|
+
onTracksNeedUpdateListeners.forEach { it(tracks, lookahead) }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
internal fun TrackPlayerCore.notifyTemporaryQueueChange() {
|
|
37
|
+
val pn = playNextStack.toList()
|
|
38
|
+
val un = upNextQueue.toList()
|
|
39
|
+
onTemporaryQueueChangeListeners.forEach { it(pn, un) }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
internal fun TrackPlayerCore.notifyAndroidAutoConnection(connected: Boolean) {
|
|
43
|
+
onAndroidAutoConnectionListeners.forEach { it(connected) }
|
|
44
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
@file:Suppress("ktlint:standard:max-line-length")
|
|
2
|
+
|
|
3
|
+
package com.margelo.nitro.nitroplayer.core
|
|
4
|
+
|
|
5
|
+
import androidx.media3.common.Player
|
|
6
|
+
import com.margelo.nitro.nitroplayer.PlayerConfig
|
|
7
|
+
import com.margelo.nitro.nitroplayer.Reason
|
|
8
|
+
import com.margelo.nitro.nitroplayer.RepeatMode
|
|
9
|
+
import com.margelo.nitro.nitroplayer.TrackPlayerState
|
|
10
|
+
import com.margelo.nitro.nitroplayer.media.NitroPlayerMediaBrowserService
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Playback control — all public functions are suspend and execute on the player thread
|
|
14
|
+
* via withPlayerContext.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
suspend fun TrackPlayerCore.play() = withPlayerContext { exo.play() }
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
suspend fun TrackPlayerCore.pause() = withPlayerContext { exo.pause() }
|
|
21
|
+
|
|
22
|
+
suspend fun TrackPlayerCore.seek(position: Double) = withPlayerContext {
|
|
23
|
+
isManuallySeeked = true
|
|
24
|
+
exo.seekTo((position * 1000).toLong())
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
suspend fun TrackPlayerCore.skipToNext() = withPlayerContext {
|
|
28
|
+
if (exo.hasNextMediaItem()) {
|
|
29
|
+
exo.seekToNext()
|
|
30
|
+
checkUpcomingTracksForUrls(lookaheadCount)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
suspend fun TrackPlayerCore.skipToPrevious() = withPlayerContext {
|
|
35
|
+
val currentPosition = exo.currentPosition
|
|
36
|
+
when {
|
|
37
|
+
currentPosition > 2000 -> exo.seekTo(0)
|
|
38
|
+
|
|
39
|
+
currentTemporaryType != TrackPlayerCore.TemporaryType.NONE -> {
|
|
40
|
+
val trackId = exo.currentMediaItem?.mediaId?.let { extractTrackId(it) }
|
|
41
|
+
if (trackId != null) {
|
|
42
|
+
when (currentTemporaryType) {
|
|
43
|
+
TrackPlayerCore.TemporaryType.PLAY_NEXT -> {
|
|
44
|
+
val idx = playNextStack.indexOfFirst { it.id == trackId }
|
|
45
|
+
if (idx >= 0) playNextStack.removeAt(idx)
|
|
46
|
+
}
|
|
47
|
+
TrackPlayerCore.TemporaryType.UP_NEXT -> {
|
|
48
|
+
val idx = upNextQueue.indexOfFirst { it.id == trackId }
|
|
49
|
+
if (idx >= 0) upNextQueue.removeAt(idx)
|
|
50
|
+
}
|
|
51
|
+
else -> {}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
currentTemporaryType = TrackPlayerCore.TemporaryType.NONE
|
|
55
|
+
playFromIndexInternal(currentTrackIndex)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
currentTrackIndex > 0 -> playFromIndexInternal(currentTrackIndex - 1)
|
|
59
|
+
|
|
60
|
+
else -> exo.seekTo(0)
|
|
61
|
+
}
|
|
62
|
+
checkUpcomingTracksForUrls(lookaheadCount)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
suspend fun TrackPlayerCore.setRepeatMode(mode: RepeatMode) = withPlayerContext {
|
|
66
|
+
currentRepeatMode = mode
|
|
67
|
+
exo.setRepeatMode(
|
|
68
|
+
when (mode) {
|
|
69
|
+
RepeatMode.TRACK -> Player.REPEAT_MODE_ONE
|
|
70
|
+
else -> Player.REPEAT_MODE_OFF
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fun TrackPlayerCore.getRepeatMode(): RepeatMode = currentRepeatMode
|
|
76
|
+
|
|
77
|
+
suspend fun TrackPlayerCore.setVolume(volume: Double) = withPlayerContext {
|
|
78
|
+
val clamped = volume.coerceIn(0.0, 100.0)
|
|
79
|
+
exo.setVolume((clamped / 100.0).toFloat())
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
suspend fun TrackPlayerCore.configure(config: PlayerConfig) = withPlayerContext {
|
|
83
|
+
config.androidAutoEnabled?.let { NitroPlayerMediaBrowserService.isAndroidAutoEnabled = it }
|
|
84
|
+
config.lookaheadCount?.let { lookaheadCount = it.toInt() }
|
|
85
|
+
mediaSessionManager?.configure(config.androidAutoEnabled, config.carPlayEnabled, config.showInNotification)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
suspend fun TrackPlayerCore.playSong(songId: String, fromPlaylist: String?) = withPlayerContext {
|
|
89
|
+
playSongInternal(songId, fromPlaylist)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
internal fun TrackPlayerCore.playSongInternal(songId: String, fromPlaylist: String?) {
|
|
93
|
+
playNextStack.clear()
|
|
94
|
+
upNextQueue.clear()
|
|
95
|
+
currentTemporaryType = TrackPlayerCore.TemporaryType.NONE
|
|
96
|
+
|
|
97
|
+
var targetPlaylistId: String? = null
|
|
98
|
+
var songIndex: Int = -1
|
|
99
|
+
|
|
100
|
+
if (fromPlaylist != null) {
|
|
101
|
+
val playlist = playlistManager.getPlaylist(fromPlaylist)
|
|
102
|
+
if (playlist != null) {
|
|
103
|
+
songIndex = playlist.tracks.indexOfFirst { it.id == songId }
|
|
104
|
+
if (songIndex >= 0) targetPlaylistId = fromPlaylist else return
|
|
105
|
+
} else return
|
|
106
|
+
} else {
|
|
107
|
+
if (currentPlaylistId != null) {
|
|
108
|
+
val cp = playlistManager.getPlaylist(currentPlaylistId!!)
|
|
109
|
+
if (cp != null) {
|
|
110
|
+
songIndex = cp.tracks.indexOfFirst { it.id == songId }
|
|
111
|
+
if (songIndex >= 0) targetPlaylistId = currentPlaylistId
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (songIndex == -1) {
|
|
115
|
+
for (playlist in playlistManager.getAllPlaylists()) {
|
|
116
|
+
songIndex = playlist.tracks.indexOfFirst { it.id == songId }
|
|
117
|
+
if (songIndex >= 0) { targetPlaylistId = playlist.id; break }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (songIndex == -1) {
|
|
121
|
+
val all = playlistManager.getAllPlaylists()
|
|
122
|
+
if (all.isNotEmpty()) { targetPlaylistId = all[0].id; songIndex = 0 }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (targetPlaylistId == null || songIndex < 0) return
|
|
127
|
+
|
|
128
|
+
if (currentPlaylistId != targetPlaylistId) {
|
|
129
|
+
val playlist = playlistManager.getPlaylist(targetPlaylistId) ?: return
|
|
130
|
+
currentPlaylistId = targetPlaylistId
|
|
131
|
+
updatePlayerQueue(playlist.tracks)
|
|
132
|
+
}
|
|
133
|
+
playFromIndexInternal(songIndex)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── State emission (called from player thread) ─────────────────────────────
|
|
137
|
+
|
|
138
|
+
internal fun TrackPlayerCore.emitStateChange(reason: Reason? = null) {
|
|
139
|
+
if (!isExoInitialized) return
|
|
140
|
+
val state = when (exo.playbackState) {
|
|
141
|
+
Player.STATE_IDLE -> TrackPlayerState.STOPPED
|
|
142
|
+
Player.STATE_BUFFERING -> if (exo.playWhenReady) TrackPlayerState.PLAYING else TrackPlayerState.PAUSED
|
|
143
|
+
Player.STATE_READY -> if (exo.isPlaying) TrackPlayerState.PLAYING else TrackPlayerState.PAUSED
|
|
144
|
+
Player.STATE_ENDED -> TrackPlayerState.STOPPED
|
|
145
|
+
else -> TrackPlayerState.STOPPED
|
|
146
|
+
}
|
|
147
|
+
val actualReason = reason ?: if (exo.playbackState == Player.STATE_ENDED) Reason.END else null
|
|
148
|
+
notifyPlaybackStateChange(state, actualReason)
|
|
149
|
+
mediaSessionManager?.onPlaybackStateChanged(state == TrackPlayerState.PLAYING)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
// ── Playback speed ────────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
suspend fun TrackPlayerCore.setPlayBackSpeed(speed: Double) = withPlayerContext {
|
|
156
|
+
if (speed <= 0.0) throw IllegalArgumentException("Speed must be greater than 0")
|
|
157
|
+
if (isExoInitialized) exo.setPlaybackSpeed(speed.toFloat())
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
suspend fun TrackPlayerCore.getPlayBackSpeed(): Double = withPlayerContext {
|
|
161
|
+
if (isExoInitialized) exo.getPlaybackSpeed().toDouble() else 1.0
|
|
162
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
@file:Suppress("ktlint:standard:max-line-length")
|
|
2
|
+
|
|
3
|
+
package com.margelo.nitro.nitroplayer.core
|
|
4
|
+
|
|
5
|
+
import androidx.media3.common.Player
|
|
6
|
+
import com.margelo.nitro.nitroplayer.CurrentPlayingType
|
|
7
|
+
import com.margelo.nitro.nitroplayer.PlayerState
|
|
8
|
+
import com.margelo.nitro.nitroplayer.TrackItem
|
|
9
|
+
import com.margelo.nitro.nitroplayer.TrackPlayerState
|
|
10
|
+
import com.margelo.nitro.nitroplayer.Variant_NullType_String
|
|
11
|
+
import com.margelo.nitro.nitroplayer.Variant_NullType_TrackItem
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* State read + index-navigation — all public functions are suspend and run on the
|
|
15
|
+
* player thread via withPlayerContext.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// ── Player state ──────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
suspend fun TrackPlayerCore.getState(): PlayerState = withPlayerContext { getStateInternal() }
|
|
21
|
+
|
|
22
|
+
internal fun TrackPlayerCore.getStateInternal(): PlayerState {
|
|
23
|
+
if (!isExoInitialized) {
|
|
24
|
+
return PlayerState(
|
|
25
|
+
currentTrack = null,
|
|
26
|
+
currentPosition = 0.0,
|
|
27
|
+
totalDuration = 0.0,
|
|
28
|
+
currentState = TrackPlayerState.STOPPED,
|
|
29
|
+
currentPlaylistId = currentPlaylistId?.let { Variant_NullType_String.create(it) },
|
|
30
|
+
currentIndex = -1.0,
|
|
31
|
+
currentPlayingType = CurrentPlayingType.NOT_PLAYING,
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
val track = getCurrentTrack()
|
|
35
|
+
val currentTrack: Variant_NullType_TrackItem? = track?.let { Variant_NullType_TrackItem.create(it) }
|
|
36
|
+
val position = exo.currentPosition / 1000.0
|
|
37
|
+
val duration = if (exo.duration > 0) exo.duration / 1000.0 else 0.0
|
|
38
|
+
val state = when (exo.playbackState) {
|
|
39
|
+
Player.STATE_IDLE -> TrackPlayerState.STOPPED
|
|
40
|
+
Player.STATE_BUFFERING -> if (exo.playWhenReady) TrackPlayerState.PLAYING else TrackPlayerState.PAUSED
|
|
41
|
+
Player.STATE_READY -> if (exo.isPlaying) TrackPlayerState.PLAYING else TrackPlayerState.PAUSED
|
|
42
|
+
Player.STATE_ENDED -> TrackPlayerState.STOPPED
|
|
43
|
+
else -> TrackPlayerState.STOPPED
|
|
44
|
+
}
|
|
45
|
+
val playingType = if (track == null) CurrentPlayingType.NOT_PLAYING else when (currentTemporaryType) {
|
|
46
|
+
TrackPlayerCore.TemporaryType.NONE -> CurrentPlayingType.PLAYLIST
|
|
47
|
+
TrackPlayerCore.TemporaryType.PLAY_NEXT -> CurrentPlayingType.PLAY_NEXT
|
|
48
|
+
TrackPlayerCore.TemporaryType.UP_NEXT -> CurrentPlayingType.UP_NEXT
|
|
49
|
+
}
|
|
50
|
+
return PlayerState(
|
|
51
|
+
currentTrack = currentTrack,
|
|
52
|
+
currentPosition = position,
|
|
53
|
+
totalDuration = duration,
|
|
54
|
+
currentState = state,
|
|
55
|
+
currentPlaylistId = currentPlaylistId?.let { Variant_NullType_String.create(it) },
|
|
56
|
+
currentIndex = if (exo.currentMediaItemIndex >= 0) exo.currentMediaItemIndex.toDouble() else -1.0,
|
|
57
|
+
currentPlayingType = playingType,
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Actual queue ──────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
suspend fun TrackPlayerCore.getActualQueue(): List<TrackItem> = withPlayerContext { getActualQueueInternal() }
|
|
64
|
+
|
|
65
|
+
internal fun TrackPlayerCore.getActualQueueInternal(): List<TrackItem> {
|
|
66
|
+
if (!isExoInitialized) return emptyList()
|
|
67
|
+
val currentIndex = currentTrackIndex
|
|
68
|
+
if (currentIndex < 0) return emptyList()
|
|
69
|
+
|
|
70
|
+
val queue = ArrayList<TrackItem>(currentTracks.size + playNextStack.size + upNextQueue.size)
|
|
71
|
+
|
|
72
|
+
// Tracks before current (include currentTrackIndex when a temp track is playing)
|
|
73
|
+
val beforeEnd = if (currentTemporaryType != TrackPlayerCore.TemporaryType.NONE) {
|
|
74
|
+
minOf(currentIndex + 1, currentTracks.size)
|
|
75
|
+
} else {
|
|
76
|
+
currentIndex
|
|
77
|
+
}
|
|
78
|
+
if (beforeEnd > 0) queue.addAll(currentTracks.subList(0, beforeEnd))
|
|
79
|
+
|
|
80
|
+
// Current track
|
|
81
|
+
getCurrentTrack()?.let { queue.add(it) }
|
|
82
|
+
|
|
83
|
+
// playNext — skip index 0 if it is the current item
|
|
84
|
+
if (currentTemporaryType == TrackPlayerCore.TemporaryType.PLAY_NEXT && playNextStack.size > 1) {
|
|
85
|
+
queue.addAll(playNextStack.subList(1, playNextStack.size))
|
|
86
|
+
} else if (currentTemporaryType != TrackPlayerCore.TemporaryType.PLAY_NEXT) {
|
|
87
|
+
queue.addAll(playNextStack)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// upNext — skip index 0 if it is the current item
|
|
91
|
+
if (currentTemporaryType == TrackPlayerCore.TemporaryType.UP_NEXT && upNextQueue.size > 1) {
|
|
92
|
+
queue.addAll(upNextQueue.subList(1, upNextQueue.size))
|
|
93
|
+
} else if (currentTemporaryType != TrackPlayerCore.TemporaryType.UP_NEXT) {
|
|
94
|
+
queue.addAll(upNextQueue)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Remaining original tracks
|
|
98
|
+
if (currentIndex + 1 < currentTracks.size) {
|
|
99
|
+
queue.addAll(currentTracks.subList(currentIndex + 1, currentTracks.size))
|
|
100
|
+
}
|
|
101
|
+
return queue
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Index navigation ──────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
suspend fun TrackPlayerCore.skipToIndex(index: Int): Boolean = withPlayerContext { skipToIndexInternal(index) }
|
|
107
|
+
|
|
108
|
+
private fun TrackPlayerCore.skipToIndexInternal(index: Int): Boolean {
|
|
109
|
+
if (!isExoInitialized) return false
|
|
110
|
+
val actualQueue = getActualQueueInternal()
|
|
111
|
+
if (index < 0 || index >= actualQueue.size) return false
|
|
112
|
+
|
|
113
|
+
val currentPos = if (currentTemporaryType != TrackPlayerCore.TemporaryType.NONE) currentTrackIndex + 1 else currentTrackIndex
|
|
114
|
+
val effectivePlayNextSize = if (currentTemporaryType == TrackPlayerCore.TemporaryType.PLAY_NEXT) maxOf(0, playNextStack.size - 1) else playNextStack.size
|
|
115
|
+
val effectiveUpNextSize = if (currentTemporaryType == TrackPlayerCore.TemporaryType.UP_NEXT) maxOf(0, upNextQueue.size - 1) else upNextQueue.size
|
|
116
|
+
|
|
117
|
+
val playNextStart = currentPos + 1
|
|
118
|
+
val playNextEnd = playNextStart + effectivePlayNextSize
|
|
119
|
+
val upNextStart = playNextEnd
|
|
120
|
+
val upNextEnd = upNextStart + effectiveUpNextSize
|
|
121
|
+
val originalRemainingStart = upNextEnd
|
|
122
|
+
|
|
123
|
+
if (index < currentPos) { playFromIndexInternal(index); return true }
|
|
124
|
+
if (index == currentPos) { exo.seekTo(0); return true }
|
|
125
|
+
|
|
126
|
+
if (index in playNextStart until playNextEnd) {
|
|
127
|
+
val listIndex = (index - playNextStart) + if (currentTemporaryType == TrackPlayerCore.TemporaryType.PLAY_NEXT) 1 else 0
|
|
128
|
+
if (listIndex > 0) playNextStack.subList(0, listIndex).clear()
|
|
129
|
+
rebuildQueueFromCurrentPosition()
|
|
130
|
+
exo.seekToNext()
|
|
131
|
+
return true
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (index in upNextStart until upNextEnd) {
|
|
135
|
+
val listIndex = (index - upNextStart) + if (currentTemporaryType == TrackPlayerCore.TemporaryType.UP_NEXT) 1 else 0
|
|
136
|
+
playNextStack.clear()
|
|
137
|
+
if (listIndex > 0) upNextQueue.subList(0, listIndex).clear()
|
|
138
|
+
rebuildQueueFromCurrentPosition()
|
|
139
|
+
exo.seekToNext()
|
|
140
|
+
return true
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (index >= originalRemainingStart) {
|
|
144
|
+
val targetTrack = actualQueue[index]
|
|
145
|
+
val originalIndex = currentTracks.indexOfFirst { it.id == targetTrack.id }
|
|
146
|
+
if (originalIndex == -1) return false
|
|
147
|
+
playNextStack.clear(); upNextQueue.clear()
|
|
148
|
+
currentTemporaryType = TrackPlayerCore.TemporaryType.NONE
|
|
149
|
+
rebuildQueueAndPlayFromIndex(originalIndex)
|
|
150
|
+
checkUpcomingTracksForUrls(lookaheadCount)
|
|
151
|
+
return true
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
checkUpcomingTracksForUrls(lookaheadCount)
|
|
155
|
+
return false
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
suspend fun TrackPlayerCore.playFromIndex(index: Int) = withPlayerContext { playFromIndexInternal(index) }
|
|
159
|
+
|
|
160
|
+
internal fun TrackPlayerCore.playFromIndexInternal(index: Int) {
|
|
161
|
+
playNextStack.clear()
|
|
162
|
+
upNextQueue.clear()
|
|
163
|
+
currentTemporaryType = TrackPlayerCore.TemporaryType.NONE
|
|
164
|
+
rebuildQueueAndPlayFromIndex(index)
|
|
165
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
@file:Suppress("ktlint:standard:max-line-length")
|
|
2
|
+
|
|
3
|
+
package com.margelo.nitro.nitroplayer.core
|
|
4
|
+
|
|
5
|
+
import android.net.Uri
|
|
6
|
+
import androidx.media3.common.MediaItem
|
|
7
|
+
import androidx.media3.common.MediaMetadata
|
|
8
|
+
import androidx.media3.common.Player
|
|
9
|
+
import com.margelo.nitro.nitroplayer.TrackItem
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Queue-building helpers — called exclusively on the player thread.
|
|
13
|
+
* Surgical rebuild (removeMediaItems + addMediaItems) preserves the current item
|
|
14
|
+
* for gapless playback; full rebuild (clearMediaItems + setMediaItems) is used only
|
|
15
|
+
* when jumping to a specific index.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// ── Full rebuild (jump to index) ───────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
internal fun TrackPlayerCore.rebuildQueueAndPlayFromIndex(index: Int) {
|
|
21
|
+
if (!isExoInitialized) return
|
|
22
|
+
if (index < 0 || index >= currentTracks.size) return
|
|
23
|
+
|
|
24
|
+
val playlistId = currentPlaylistId ?: ""
|
|
25
|
+
val mediaItems = currentTracks.subList(index, currentTracks.size).map { track ->
|
|
26
|
+
val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
|
|
27
|
+
makeMediaItem(track, mediaId)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
currentTrackIndex = index
|
|
31
|
+
exo.clearMediaItems()
|
|
32
|
+
exo.setMediaItems(mediaItems)
|
|
33
|
+
exo.seekToDefaultPosition(0)
|
|
34
|
+
exo.playWhenReady = true
|
|
35
|
+
exo.prepare()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── Surgical rebuild (preserve current item) ──────────────────────────────
|
|
39
|
+
|
|
40
|
+
internal fun TrackPlayerCore.rebuildQueueFromCurrentPosition() {
|
|
41
|
+
if (!isExoInitialized) return
|
|
42
|
+
val currentIndex = exo.currentMediaItemIndex
|
|
43
|
+
if (currentIndex < 0) return
|
|
44
|
+
|
|
45
|
+
// If current track was removed from the playlist, jump to best substitute
|
|
46
|
+
val currentTrackId = exo.currentMediaItem?.mediaId?.let { extractTrackId(it) }
|
|
47
|
+
if (currentTrackId != null && currentTracks.none { it.id == currentTrackId }) {
|
|
48
|
+
if (currentTracks.isEmpty()) return
|
|
49
|
+
playFromIndexInternal(minOf(currentTrackIndex, currentTracks.size - 1))
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
val newQueueTracks = ArrayList<TrackItem>(playNextStack.size + upNextQueue.size + currentTracks.size)
|
|
54
|
+
|
|
55
|
+
// playNext stack — skip index 0 if it is the currently playing item
|
|
56
|
+
if (currentTemporaryType == TrackPlayerCore.TemporaryType.PLAY_NEXT && playNextStack.size > 1) {
|
|
57
|
+
newQueueTracks.addAll(playNextStack.subList(1, playNextStack.size))
|
|
58
|
+
} else if (currentTemporaryType != TrackPlayerCore.TemporaryType.PLAY_NEXT) {
|
|
59
|
+
newQueueTracks.addAll(playNextStack)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// upNext queue — skip index 0 if it is the currently playing item
|
|
63
|
+
if (currentTemporaryType == TrackPlayerCore.TemporaryType.UP_NEXT && upNextQueue.size > 1) {
|
|
64
|
+
newQueueTracks.addAll(upNextQueue.subList(1, upNextQueue.size))
|
|
65
|
+
} else if (currentTemporaryType != TrackPlayerCore.TemporaryType.UP_NEXT) {
|
|
66
|
+
newQueueTracks.addAll(upNextQueue)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Remaining original tracks (after currentTrackIndex, not after ExoPlayer's currentIndex)
|
|
70
|
+
if (currentTrackIndex + 1 < currentTracks.size) {
|
|
71
|
+
newQueueTracks.addAll(currentTracks.subList(currentTrackIndex + 1, currentTracks.size))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
val playlistId = currentPlaylistId ?: ""
|
|
75
|
+
val newMediaItems = newQueueTracks.map { track ->
|
|
76
|
+
val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
|
|
77
|
+
makeMediaItem(track, mediaId)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (exo.mediaItemCount > currentIndex + 1) {
|
|
81
|
+
exo.removeMediaItems(currentIndex + 1, exo.mediaItemCount)
|
|
82
|
+
}
|
|
83
|
+
exo.addMediaItems(newMediaItems)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Full queue set (initial load or no active item) ───────────────────────
|
|
87
|
+
|
|
88
|
+
internal fun TrackPlayerCore.updatePlayerQueue(tracks: List<TrackItem>) {
|
|
89
|
+
currentTracks = tracks
|
|
90
|
+
val playlistId = currentPlaylistId ?: ""
|
|
91
|
+
val mediaItems = tracks.map { track ->
|
|
92
|
+
val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
|
|
93
|
+
makeMediaItem(track, mediaId)
|
|
94
|
+
}
|
|
95
|
+
exo.setMediaItems(mediaItems, false)
|
|
96
|
+
if (exo.playbackState == Player.STATE_IDLE && mediaItems.isNotEmpty()) {
|
|
97
|
+
exo.prepare()
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── MediaItem construction (member extension to access downloadManager) ────
|
|
102
|
+
|
|
103
|
+
internal fun TrackPlayerCore.makeMediaItem(track: TrackItem, customMediaId: String? = null): MediaItem {
|
|
104
|
+
val metaBuilder = MediaMetadata.Builder()
|
|
105
|
+
.setTitle(track.title)
|
|
106
|
+
.setArtist(track.artist)
|
|
107
|
+
.setAlbumTitle(track.album)
|
|
108
|
+
|
|
109
|
+
track.artwork?.asSecondOrNull()?.let { artworkUrl ->
|
|
110
|
+
try { metaBuilder.setArtworkUri(Uri.parse(artworkUrl)) } catch (_: Exception) {}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
val effectiveUrl = downloadManager.getEffectiveUrl(track)
|
|
114
|
+
|
|
115
|
+
return MediaItem.Builder()
|
|
116
|
+
.setMediaId(customMediaId ?: track.id)
|
|
117
|
+
.setUri(effectiveUrl)
|
|
118
|
+
.setMediaMetadata(metaBuilder.build())
|
|
119
|
+
.build()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Track lookup helpers ───────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
internal fun TrackPlayerCore.findTrack(mediaItem: MediaItem?): TrackItem? {
|
|
125
|
+
if (mediaItem == null) return null
|
|
126
|
+
val trackId = extractTrackId(mediaItem.mediaId)
|
|
127
|
+
return currentTracks.find { it.id == trackId }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
internal fun TrackPlayerCore.findTrackById(trackId: String): TrackItem? {
|
|
131
|
+
currentTracks.find { it.id == trackId }?.let { return it }
|
|
132
|
+
for (playlist in playlistManager.getAllPlaylists()) {
|
|
133
|
+
playlist.tracks.find { it.id == trackId }?.let { return it }
|
|
134
|
+
}
|
|
135
|
+
return null
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
internal fun TrackPlayerCore.getCurrentTrack(): TrackItem? {
|
|
139
|
+
if (!isExoInitialized) return null
|
|
140
|
+
val currentMediaItem = exo.currentMediaItem ?: return null
|
|
141
|
+
if (currentTemporaryType != TrackPlayerCore.TemporaryType.NONE) {
|
|
142
|
+
val trackId = extractTrackId(currentMediaItem.mediaId)
|
|
143
|
+
return when (currentTemporaryType) {
|
|
144
|
+
TrackPlayerCore.TemporaryType.PLAY_NEXT -> playNextStack.firstOrNull { it.id == trackId }
|
|
145
|
+
TrackPlayerCore.TemporaryType.UP_NEXT -> upNextQueue.firstOrNull { it.id == trackId }
|
|
146
|
+
else -> null
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return findTrack(currentMediaItem)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
internal fun TrackPlayerCore.determineCurrentTemporaryType(): TrackPlayerCore.TemporaryType {
|
|
153
|
+
val currentItem = exo.currentMediaItem ?: return TrackPlayerCore.TemporaryType.NONE
|
|
154
|
+
val trackId = extractTrackId(currentItem.mediaId)
|
|
155
|
+
if (playNextStack.any { it.id == trackId }) return TrackPlayerCore.TemporaryType.PLAY_NEXT
|
|
156
|
+
if (upNextQueue.any { it.id == trackId }) return TrackPlayerCore.TemporaryType.UP_NEXT
|
|
157
|
+
return TrackPlayerCore.TemporaryType.NONE
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
internal fun TrackPlayerCore.extractTrackId(mediaId: String): String =
|
|
161
|
+
if (mediaId.contains(':')) mediaId.substring(mediaId.indexOf(':') + 1) else mediaId
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package com.margelo.nitro.nitroplayer.core
|
|
2
|
+
|
|
3
|
+
import com.margelo.nitro.nitroplayer.media.MediaSessionManager
|
|
4
|
+
import com.margelo.nitro.nitroplayer.media.NitroPlayerMediaBrowserService
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Initialises ExoPlayer (via ExoPlayerCore) and MediaSessionManager on the player thread.
|
|
8
|
+
* Called once from TrackPlayerCore.init via playerHandler.post.
|
|
9
|
+
*/
|
|
10
|
+
internal fun TrackPlayerCore.initExoAndMedia() {
|
|
11
|
+
exo = ExoPlayerCore(context, playerThread)
|
|
12
|
+
|
|
13
|
+
mediaSessionManager = MediaSessionManager(context, exo.player, playlistManager).apply {
|
|
14
|
+
setTrackPlayerCore(this@initExoAndMedia)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Give MediaBrowserService access to this core and media session
|
|
18
|
+
NitroPlayerMediaBrowserService.trackPlayerCore = this
|
|
19
|
+
NitroPlayerMediaBrowserService.mediaSessionManager = mediaSessionManager
|
|
20
|
+
|
|
21
|
+
// Attach player listener
|
|
22
|
+
val listener = TrackPlayerEventListener(this)
|
|
23
|
+
playerListener = listener
|
|
24
|
+
exo.addListener(listener)
|
|
25
|
+
|
|
26
|
+
// Start progress ticks on the player thread
|
|
27
|
+
playerHandler.postDelayed(progressUpdateRunnable, 250)
|
|
28
|
+
}
|