react-native-nitro-player 0.7.1-alpha.1 → 0.7.1-alpha.3
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/AndroidManifest.xml +15 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +5 -6
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +68 -49
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +67 -21
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +27 -5
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +88 -49
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +40 -10
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ExoPlayerCore.kt +46 -45
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ListenerRegistry.kt +4 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerAndroidAuto.kt +38 -32
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +146 -81
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +24 -13
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerNotify.kt +16 -4
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +101 -72
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueue.kt +42 -22
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +55 -24
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +27 -8
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerTempQueue.kt +73 -62
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +51 -48
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadManagerCore.kt +14 -3
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadWorker.kt +94 -23
- package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +43 -34
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/ExoPlayerBuilder.kt +49 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionCallbackFactory.kt +167 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +11 -450
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/NitroPlayerNotificationProvider.kt +200 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/PlaybackService.kt +101 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +87 -85
- package/ios/core/TrackPlayerQueueBuild.swift +16 -2
- package/package.json +1 -1
- package/src/hooks/useEqualizer.ts +15 -12
- package/src/specs/AndroidAutoMediaLibrary.nitro.ts +3 -2
- package/src/specs/DownloadManager.nitro.ts +4 -2
- package/src/specs/Equalizer.nitro.ts +4 -2
- package/src/specs/TrackPlayer.nitro.ts +18 -6
|
@@ -12,20 +12,24 @@ import com.margelo.nitro.nitroplayer.media.NitroPlayerMediaBrowserService
|
|
|
12
12
|
/**
|
|
13
13
|
* ExoPlayer event listener — translates low-level ExoPlayer callbacks into
|
|
14
14
|
* TrackPlayerCore state mutations and JS-facing listener notifications.
|
|
15
|
-
* All callbacks fire on the
|
|
15
|
+
* All callbacks fire on the main looper (ExoPlayer uses the default application looper).
|
|
16
16
|
*/
|
|
17
17
|
internal class TrackPlayerEventListener(
|
|
18
18
|
private val core: TrackPlayerCore,
|
|
19
19
|
) : Player.Listener {
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
override fun onMediaItemTransition(
|
|
21
|
+
mediaItem: MediaItem?,
|
|
22
|
+
reason: Int,
|
|
23
|
+
) {
|
|
22
24
|
with(core) {
|
|
23
25
|
// TRACK repeat: REPEAT_MODE_ONE fires this every loop — not a real track change
|
|
24
26
|
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT) return
|
|
25
27
|
|
|
26
28
|
// Remove the track that just finished/was skipped from temp lists
|
|
27
|
-
if ((
|
|
28
|
-
|
|
29
|
+
if ((
|
|
30
|
+
reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO ||
|
|
31
|
+
reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK
|
|
32
|
+
) &&
|
|
29
33
|
previousMediaItem != null
|
|
30
34
|
) {
|
|
31
35
|
previousMediaItem?.mediaId?.let { mediaId ->
|
|
@@ -54,12 +58,13 @@ internal class TrackPlayerEventListener(
|
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
val track = getCurrentTrack() ?: return
|
|
57
|
-
val r =
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
val r =
|
|
62
|
+
when (reason) {
|
|
63
|
+
Player.MEDIA_ITEM_TRANSITION_REASON_AUTO -> Reason.END
|
|
64
|
+
Player.MEDIA_ITEM_TRANSITION_REASON_SEEK -> Reason.USER_ACTION
|
|
65
|
+
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED -> Reason.USER_ACTION
|
|
66
|
+
else -> null
|
|
67
|
+
}
|
|
63
68
|
notifyTrackChange(track, r)
|
|
64
69
|
mediaSessionManager?.onTrackChanged(track)
|
|
65
70
|
checkUpcomingTracksForUrls(lookaheadCount)
|
|
@@ -67,13 +72,19 @@ internal class TrackPlayerEventListener(
|
|
|
67
72
|
}
|
|
68
73
|
}
|
|
69
74
|
|
|
70
|
-
override fun onTimelineChanged(
|
|
75
|
+
override fun onTimelineChanged(
|
|
76
|
+
timeline: androidx.media3.common.Timeline,
|
|
77
|
+
reason: Int,
|
|
78
|
+
) {
|
|
71
79
|
if (reason == Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) {
|
|
72
80
|
NitroPlayerMediaBrowserService.getInstance()?.onPlaylistsUpdated()
|
|
73
81
|
}
|
|
74
82
|
}
|
|
75
83
|
|
|
76
|
-
override fun onPlayWhenReadyChanged(
|
|
84
|
+
override fun onPlayWhenReadyChanged(
|
|
85
|
+
playWhenReady: Boolean,
|
|
86
|
+
reason: Int,
|
|
87
|
+
) {
|
|
77
88
|
val r = if (reason == Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST) Reason.USER_ACTION else null
|
|
78
89
|
core.emitStateChange(r)
|
|
79
90
|
}
|
|
@@ -9,15 +9,24 @@ import com.margelo.nitro.nitroplayer.TrackPlayerState
|
|
|
9
9
|
* All methods must be called from the player thread (already serialised).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
internal fun TrackPlayerCore.notifyTrackChange(
|
|
12
|
+
internal fun TrackPlayerCore.notifyTrackChange(
|
|
13
|
+
track: TrackItem,
|
|
14
|
+
reason: Reason?,
|
|
15
|
+
) {
|
|
13
16
|
onChangeTrackListeners.forEach { it(track, reason) }
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
internal fun TrackPlayerCore.notifyPlaybackStateChange(
|
|
19
|
+
internal fun TrackPlayerCore.notifyPlaybackStateChange(
|
|
20
|
+
state: TrackPlayerState,
|
|
21
|
+
reason: Reason?,
|
|
22
|
+
) {
|
|
17
23
|
onPlaybackStateChangeListeners.forEach { it(state, reason) }
|
|
18
24
|
}
|
|
19
25
|
|
|
20
|
-
internal fun TrackPlayerCore.notifySeek(
|
|
26
|
+
internal fun TrackPlayerCore.notifySeek(
|
|
27
|
+
position: Double,
|
|
28
|
+
duration: Double,
|
|
29
|
+
) {
|
|
21
30
|
onSeekListeners.forEach { it(position, duration) }
|
|
22
31
|
}
|
|
23
32
|
|
|
@@ -29,7 +38,10 @@ internal fun TrackPlayerCore.notifyPlaybackProgress(
|
|
|
29
38
|
onProgressListeners.forEach { it(position, duration, isManuallySeeked) }
|
|
30
39
|
}
|
|
31
40
|
|
|
32
|
-
internal fun TrackPlayerCore.notifyTracksNeedUpdate(
|
|
41
|
+
internal fun TrackPlayerCore.notifyTracksNeedUpdate(
|
|
42
|
+
tracks: List<TrackItem>,
|
|
43
|
+
lookahead: Int,
|
|
44
|
+
) {
|
|
33
45
|
onTracksNeedUpdateListeners.forEach { it(tracks, lookahead) }
|
|
34
46
|
}
|
|
35
47
|
|
|
@@ -16,80 +16,99 @@ import com.margelo.nitro.nitroplayer.media.NitroPlayerMediaBrowserService
|
|
|
16
16
|
|
|
17
17
|
suspend fun TrackPlayerCore.play() = withPlayerContext { exo.play() }
|
|
18
18
|
|
|
19
|
-
|
|
20
19
|
suspend fun TrackPlayerCore.pause() = withPlayerContext { exo.pause() }
|
|
21
20
|
|
|
22
|
-
suspend fun TrackPlayerCore.seek(position: Double) =
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
suspend fun TrackPlayerCore.seek(position: Double) =
|
|
22
|
+
withPlayerContext {
|
|
23
|
+
isManuallySeeked = true
|
|
24
|
+
exo.seekTo((position * 1000).toLong())
|
|
25
|
+
}
|
|
26
26
|
|
|
27
|
-
suspend fun TrackPlayerCore.skipToNext() =
|
|
28
|
-
|
|
29
|
-
exo.
|
|
30
|
-
|
|
27
|
+
suspend fun TrackPlayerCore.skipToNext() =
|
|
28
|
+
withPlayerContext {
|
|
29
|
+
if (exo.hasNextMediaItem()) {
|
|
30
|
+
exo.seekToNext()
|
|
31
|
+
checkUpcomingTracksForUrls(lookaheadCount)
|
|
32
|
+
}
|
|
31
33
|
}
|
|
32
|
-
}
|
|
33
34
|
|
|
34
|
-
suspend fun TrackPlayerCore.skipToPrevious() =
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
35
|
+
suspend fun TrackPlayerCore.skipToPrevious() =
|
|
36
|
+
withPlayerContext {
|
|
37
|
+
val currentPosition = exo.currentPosition
|
|
38
|
+
when {
|
|
39
|
+
currentPosition > 2000 -> {
|
|
40
|
+
exo.seekTo(0)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
currentTemporaryType != TrackPlayerCore.TemporaryType.NONE -> {
|
|
44
|
+
val trackId = exo.currentMediaItem?.mediaId?.let { extractTrackId(it) }
|
|
45
|
+
if (trackId != null) {
|
|
46
|
+
when (currentTemporaryType) {
|
|
47
|
+
TrackPlayerCore.TemporaryType.PLAY_NEXT -> {
|
|
48
|
+
val idx = playNextStack.indexOfFirst { it.id == trackId }
|
|
49
|
+
if (idx >= 0) playNextStack.removeAt(idx)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
TrackPlayerCore.TemporaryType.UP_NEXT -> {
|
|
53
|
+
val idx = upNextQueue.indexOfFirst { it.id == trackId }
|
|
54
|
+
if (idx >= 0) upNextQueue.removeAt(idx)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
else -> {}
|
|
50
58
|
}
|
|
51
|
-
else -> {}
|
|
52
59
|
}
|
|
60
|
+
currentTemporaryType = TrackPlayerCore.TemporaryType.NONE
|
|
61
|
+
playFromIndexInternal(currentTrackIndex)
|
|
53
62
|
}
|
|
54
|
-
currentTemporaryType = TrackPlayerCore.TemporaryType.NONE
|
|
55
|
-
playFromIndexInternal(currentTrackIndex)
|
|
56
|
-
}
|
|
57
63
|
|
|
58
|
-
|
|
64
|
+
currentTrackIndex > 0 -> {
|
|
65
|
+
playFromIndexInternal(currentTrackIndex - 1)
|
|
66
|
+
}
|
|
59
67
|
|
|
60
|
-
|
|
68
|
+
else -> {
|
|
69
|
+
exo.seekTo(0)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
checkUpcomingTracksForUrls(lookaheadCount)
|
|
61
73
|
}
|
|
62
|
-
checkUpcomingTracksForUrls(lookaheadCount)
|
|
63
|
-
}
|
|
64
74
|
|
|
65
|
-
suspend fun TrackPlayerCore.setRepeatMode(mode: RepeatMode) =
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
suspend fun TrackPlayerCore.setRepeatMode(mode: RepeatMode) =
|
|
76
|
+
withPlayerContext {
|
|
77
|
+
currentRepeatMode = mode
|
|
78
|
+
exo.setRepeatMode(
|
|
79
|
+
when (mode) {
|
|
80
|
+
RepeatMode.TRACK -> Player.REPEAT_MODE_ONE
|
|
81
|
+
else -> Player.REPEAT_MODE_OFF
|
|
82
|
+
},
|
|
83
|
+
)
|
|
84
|
+
}
|
|
74
85
|
|
|
75
86
|
fun TrackPlayerCore.getRepeatMode(): RepeatMode = currentRepeatMode
|
|
76
87
|
|
|
77
|
-
suspend fun TrackPlayerCore.setVolume(volume: Double) =
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
88
|
+
suspend fun TrackPlayerCore.setVolume(volume: Double) =
|
|
89
|
+
withPlayerContext {
|
|
90
|
+
val clamped = volume.coerceIn(0.0, 100.0)
|
|
91
|
+
exo.setVolume((clamped / 100.0).toFloat())
|
|
92
|
+
}
|
|
81
93
|
|
|
82
|
-
suspend fun TrackPlayerCore.configure(config: PlayerConfig) =
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
suspend fun TrackPlayerCore.configure(config: PlayerConfig) =
|
|
95
|
+
withPlayerContext {
|
|
96
|
+
config.androidAutoEnabled?.let { NitroPlayerMediaBrowserService.isAndroidAutoEnabled = it }
|
|
97
|
+
config.lookaheadCount?.let { lookaheadCount = it.toInt() }
|
|
98
|
+
mediaSessionManager?.configure(config.androidAutoEnabled, config.carPlayEnabled, config.showInNotification)
|
|
99
|
+
}
|
|
87
100
|
|
|
88
|
-
suspend fun TrackPlayerCore.playSong(
|
|
101
|
+
suspend fun TrackPlayerCore.playSong(
|
|
102
|
+
songId: String,
|
|
103
|
+
fromPlaylist: String?,
|
|
104
|
+
) = withPlayerContext {
|
|
89
105
|
playSongInternal(songId, fromPlaylist)
|
|
90
106
|
}
|
|
91
107
|
|
|
92
|
-
internal fun TrackPlayerCore.playSongInternal(
|
|
108
|
+
internal fun TrackPlayerCore.playSongInternal(
|
|
109
|
+
songId: String,
|
|
110
|
+
fromPlaylist: String?,
|
|
111
|
+
) {
|
|
93
112
|
playNextStack.clear()
|
|
94
113
|
upNextQueue.clear()
|
|
95
114
|
currentTemporaryType = TrackPlayerCore.TemporaryType.NONE
|
|
@@ -102,7 +121,9 @@ internal fun TrackPlayerCore.playSongInternal(songId: String, fromPlaylist: Stri
|
|
|
102
121
|
if (playlist != null) {
|
|
103
122
|
songIndex = playlist.tracks.indexOfFirst { it.id == songId }
|
|
104
123
|
if (songIndex >= 0) targetPlaylistId = fromPlaylist else return
|
|
105
|
-
} else
|
|
124
|
+
} else {
|
|
125
|
+
return
|
|
126
|
+
}
|
|
106
127
|
} else {
|
|
107
128
|
if (currentPlaylistId != null) {
|
|
108
129
|
val cp = playlistManager.getPlaylist(currentPlaylistId!!)
|
|
@@ -114,12 +135,18 @@ internal fun TrackPlayerCore.playSongInternal(songId: String, fromPlaylist: Stri
|
|
|
114
135
|
if (songIndex == -1) {
|
|
115
136
|
for (playlist in playlistManager.getAllPlaylists()) {
|
|
116
137
|
songIndex = playlist.tracks.indexOfFirst { it.id == songId }
|
|
117
|
-
if (songIndex >= 0) {
|
|
138
|
+
if (songIndex >= 0) {
|
|
139
|
+
targetPlaylistId = playlist.id
|
|
140
|
+
break
|
|
141
|
+
}
|
|
118
142
|
}
|
|
119
143
|
}
|
|
120
144
|
if (songIndex == -1) {
|
|
121
145
|
val all = playlistManager.getAllPlaylists()
|
|
122
|
-
if (all.isNotEmpty()) {
|
|
146
|
+
if (all.isNotEmpty()) {
|
|
147
|
+
targetPlaylistId = all[0].id
|
|
148
|
+
songIndex = 0
|
|
149
|
+
}
|
|
123
150
|
}
|
|
124
151
|
}
|
|
125
152
|
|
|
@@ -137,26 +164,28 @@ internal fun TrackPlayerCore.playSongInternal(songId: String, fromPlaylist: Stri
|
|
|
137
164
|
|
|
138
165
|
internal fun TrackPlayerCore.emitStateChange(reason: Reason? = null) {
|
|
139
166
|
if (!isExoInitialized) return
|
|
140
|
-
val state =
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
167
|
+
val state =
|
|
168
|
+
when (exo.playbackState) {
|
|
169
|
+
Player.STATE_IDLE -> TrackPlayerState.STOPPED
|
|
170
|
+
Player.STATE_BUFFERING -> if (exo.playWhenReady) TrackPlayerState.PLAYING else TrackPlayerState.PAUSED
|
|
171
|
+
Player.STATE_READY -> if (exo.isPlaying) TrackPlayerState.PLAYING else TrackPlayerState.PAUSED
|
|
172
|
+
Player.STATE_ENDED -> TrackPlayerState.STOPPED
|
|
173
|
+
else -> TrackPlayerState.STOPPED
|
|
174
|
+
}
|
|
147
175
|
val actualReason = reason ?: if (exo.playbackState == Player.STATE_ENDED) Reason.END else null
|
|
148
176
|
notifyPlaybackStateChange(state, actualReason)
|
|
149
177
|
mediaSessionManager?.onPlaybackStateChanged(state == TrackPlayerState.PLAYING)
|
|
150
178
|
}
|
|
151
179
|
|
|
152
|
-
|
|
153
180
|
// ── Playback speed ────────────────────────────────────────────────────────
|
|
154
181
|
|
|
155
|
-
suspend fun TrackPlayerCore.setPlayBackSpeed(speed: Double) =
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
182
|
+
suspend fun TrackPlayerCore.setPlayBackSpeed(speed: Double) =
|
|
183
|
+
withPlayerContext {
|
|
184
|
+
if (speed <= 0.0) throw IllegalArgumentException("Speed must be greater than 0")
|
|
185
|
+
if (isExoInitialized) exo.setPlaybackSpeed(speed.toFloat())
|
|
186
|
+
}
|
|
159
187
|
|
|
160
|
-
suspend fun TrackPlayerCore.getPlayBackSpeed(): Double =
|
|
161
|
-
|
|
162
|
-
|
|
188
|
+
suspend fun TrackPlayerCore.getPlayBackSpeed(): Double =
|
|
189
|
+
withPlayerContext {
|
|
190
|
+
if (isExoInitialized) exo.getPlaybackSpeed().toDouble() else 1.0
|
|
191
|
+
}
|
|
@@ -35,18 +35,24 @@ internal fun TrackPlayerCore.getStateInternal(): PlayerState {
|
|
|
35
35
|
val currentTrack: Variant_NullType_TrackItem? = track?.let { Variant_NullType_TrackItem.create(it) }
|
|
36
36
|
val position = exo.currentPosition / 1000.0
|
|
37
37
|
val duration = if (exo.duration > 0) exo.duration / 1000.0 else 0.0
|
|
38
|
-
val state =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
38
|
+
val state =
|
|
39
|
+
when (exo.playbackState) {
|
|
40
|
+
Player.STATE_IDLE -> TrackPlayerState.STOPPED
|
|
41
|
+
Player.STATE_BUFFERING -> if (exo.playWhenReady) TrackPlayerState.PLAYING else TrackPlayerState.PAUSED
|
|
42
|
+
Player.STATE_READY -> if (exo.isPlaying) TrackPlayerState.PLAYING else TrackPlayerState.PAUSED
|
|
43
|
+
Player.STATE_ENDED -> TrackPlayerState.STOPPED
|
|
44
|
+
else -> TrackPlayerState.STOPPED
|
|
45
|
+
}
|
|
46
|
+
val playingType =
|
|
47
|
+
if (track == null) {
|
|
48
|
+
CurrentPlayingType.NOT_PLAYING
|
|
49
|
+
} else {
|
|
50
|
+
when (currentTemporaryType) {
|
|
51
|
+
TrackPlayerCore.TemporaryType.NONE -> CurrentPlayingType.PLAYLIST
|
|
52
|
+
TrackPlayerCore.TemporaryType.PLAY_NEXT -> CurrentPlayingType.PLAY_NEXT
|
|
53
|
+
TrackPlayerCore.TemporaryType.UP_NEXT -> CurrentPlayingType.UP_NEXT
|
|
54
|
+
}
|
|
55
|
+
}
|
|
50
56
|
return PlayerState(
|
|
51
57
|
currentTrack = currentTrack,
|
|
52
58
|
currentPosition = position,
|
|
@@ -70,11 +76,12 @@ internal fun TrackPlayerCore.getActualQueueInternal(): List<TrackItem> {
|
|
|
70
76
|
val queue = ArrayList<TrackItem>(currentTracks.size + playNextStack.size + upNextQueue.size)
|
|
71
77
|
|
|
72
78
|
// Tracks before current (include currentTrackIndex when a temp track is playing)
|
|
73
|
-
val beforeEnd =
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
val beforeEnd =
|
|
80
|
+
if (currentTemporaryType != TrackPlayerCore.TemporaryType.NONE) {
|
|
81
|
+
minOf(currentIndex + 1, currentTracks.size)
|
|
82
|
+
} else {
|
|
83
|
+
currentIndex
|
|
84
|
+
}
|
|
78
85
|
if (beforeEnd > 0) queue.addAll(currentTracks.subList(0, beforeEnd))
|
|
79
86
|
|
|
80
87
|
// Current track
|
|
@@ -86,7 +93,10 @@ internal fun TrackPlayerCore.getActualQueueInternal(): List<TrackItem> {
|
|
|
86
93
|
if (currentTemporaryType == TrackPlayerCore.TemporaryType.PLAY_NEXT && currentId != null) {
|
|
87
94
|
var skipped = false
|
|
88
95
|
for (track in playNextStack) {
|
|
89
|
-
if (!skipped && track.id == currentId) {
|
|
96
|
+
if (!skipped && track.id == currentId) {
|
|
97
|
+
skipped = true
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
90
100
|
queue.add(track)
|
|
91
101
|
}
|
|
92
102
|
} else if (currentTemporaryType != TrackPlayerCore.TemporaryType.PLAY_NEXT) {
|
|
@@ -97,7 +107,10 @@ internal fun TrackPlayerCore.getActualQueueInternal(): List<TrackItem> {
|
|
|
97
107
|
if (currentTemporaryType == TrackPlayerCore.TemporaryType.UP_NEXT && currentId != null) {
|
|
98
108
|
var skipped = false
|
|
99
109
|
for (track in upNextQueue) {
|
|
100
|
-
if (!skipped && track.id == currentId) {
|
|
110
|
+
if (!skipped && track.id == currentId) {
|
|
111
|
+
skipped = true
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
101
114
|
queue.add(track)
|
|
102
115
|
}
|
|
103
116
|
} else if (currentTemporaryType != TrackPlayerCore.TemporaryType.UP_NEXT) {
|
|
@@ -130,8 +143,14 @@ private fun TrackPlayerCore.skipToIndexInternal(index: Int): Boolean {
|
|
|
130
143
|
val upNextEnd = upNextStart + effectiveUpNextSize
|
|
131
144
|
val originalRemainingStart = upNextEnd
|
|
132
145
|
|
|
133
|
-
if (index < currentPos) {
|
|
134
|
-
|
|
146
|
+
if (index < currentPos) {
|
|
147
|
+
playFromIndexInternal(index)
|
|
148
|
+
return true
|
|
149
|
+
}
|
|
150
|
+
if (index == currentPos) {
|
|
151
|
+
exo.seekTo(0)
|
|
152
|
+
return true
|
|
153
|
+
}
|
|
135
154
|
|
|
136
155
|
if (index in playNextStart until playNextEnd) {
|
|
137
156
|
val targetTrack = actualQueue[index]
|
|
@@ -158,7 +177,8 @@ private fun TrackPlayerCore.skipToIndexInternal(index: Int): Boolean {
|
|
|
158
177
|
val targetTrack = actualQueue[index]
|
|
159
178
|
val originalIndex = currentTracks.indexOfFirst { it.id == targetTrack.id }
|
|
160
179
|
if (originalIndex == -1) return false
|
|
161
|
-
playNextStack.clear()
|
|
180
|
+
playNextStack.clear()
|
|
181
|
+
upNextQueue.clear()
|
|
162
182
|
currentTemporaryType = TrackPlayerCore.TemporaryType.NONE
|
|
163
183
|
rebuildQueueAndPlayFromIndex(originalIndex)
|
|
164
184
|
checkUpcomingTracksForUrls(lookaheadCount)
|
|
@@ -22,10 +22,11 @@ internal fun TrackPlayerCore.rebuildQueueAndPlayFromIndex(index: Int) {
|
|
|
22
22
|
if (index < 0 || index >= currentTracks.size) return
|
|
23
23
|
|
|
24
24
|
val playlistId = currentPlaylistId ?: ""
|
|
25
|
-
val mediaItems =
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
val mediaItems =
|
|
26
|
+
currentTracks.subList(index, currentTracks.size).map { track ->
|
|
27
|
+
val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
|
|
28
|
+
makeMediaItem(track, mediaId)
|
|
29
|
+
}
|
|
29
30
|
|
|
30
31
|
currentTrackIndex = index
|
|
31
32
|
exo.clearMediaItems()
|
|
@@ -44,12 +45,26 @@ internal fun TrackPlayerCore.rebuildQueueFromCurrentPosition() {
|
|
|
44
45
|
|
|
45
46
|
// If current track was removed from the playlist, jump to best substitute
|
|
46
47
|
val currentTrackId = exo.currentMediaItem?.mediaId?.let { extractTrackId(it) }
|
|
47
|
-
|
|
48
|
+
|
|
49
|
+
if (
|
|
50
|
+
currentTrackId != null &&
|
|
51
|
+
currentTracks.none { it.id == currentTrackId } &&
|
|
52
|
+
currentTemporaryType == TrackPlayerCore.TemporaryType.NONE
|
|
53
|
+
) {
|
|
48
54
|
if (currentTracks.isEmpty()) return
|
|
49
55
|
playFromIndexInternal(minOf(currentTrackIndex, currentTracks.size - 1))
|
|
50
56
|
return
|
|
51
57
|
}
|
|
52
58
|
|
|
59
|
+
// Keep the logical playlist pointer in sync after playlist mutations.
|
|
60
|
+
// Without this, getActualQueue/getState can report a stale index until the next track transition.
|
|
61
|
+
if (currentTemporaryType == TrackPlayerCore.TemporaryType.NONE && currentTrackId != null) {
|
|
62
|
+
val resolvedIndex = currentTracks.indexOfFirst { it.id == currentTrackId }
|
|
63
|
+
if (resolvedIndex >= 0) {
|
|
64
|
+
currentTrackIndex = resolvedIndex
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
53
68
|
val newQueueTracks = ArrayList<TrackItem>(playNextStack.size + upNextQueue.size + currentTracks.size)
|
|
54
69
|
val currentId = exo.currentMediaItem?.mediaId?.let { extractTrackId(it) }
|
|
55
70
|
|
|
@@ -57,7 +72,10 @@ internal fun TrackPlayerCore.rebuildQueueFromCurrentPosition() {
|
|
|
57
72
|
if (currentTemporaryType == TrackPlayerCore.TemporaryType.PLAY_NEXT && currentId != null) {
|
|
58
73
|
var skipped = false
|
|
59
74
|
for (track in playNextStack) {
|
|
60
|
-
if (!skipped && track.id == currentId) {
|
|
75
|
+
if (!skipped && track.id == currentId) {
|
|
76
|
+
skipped = true
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
61
79
|
newQueueTracks.add(track)
|
|
62
80
|
}
|
|
63
81
|
} else if (currentTemporaryType != TrackPlayerCore.TemporaryType.PLAY_NEXT) {
|
|
@@ -68,7 +86,10 @@ internal fun TrackPlayerCore.rebuildQueueFromCurrentPosition() {
|
|
|
68
86
|
if (currentTemporaryType == TrackPlayerCore.TemporaryType.UP_NEXT && currentId != null) {
|
|
69
87
|
var skipped = false
|
|
70
88
|
for (track in upNextQueue) {
|
|
71
|
-
if (!skipped && track.id == currentId) {
|
|
89
|
+
if (!skipped && track.id == currentId) {
|
|
90
|
+
skipped = true
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
72
93
|
newQueueTracks.add(track)
|
|
73
94
|
}
|
|
74
95
|
} else if (currentTemporaryType != TrackPlayerCore.TemporaryType.UP_NEXT) {
|
|
@@ -81,10 +102,11 @@ internal fun TrackPlayerCore.rebuildQueueFromCurrentPosition() {
|
|
|
81
102
|
}
|
|
82
103
|
|
|
83
104
|
val playlistId = currentPlaylistId ?: ""
|
|
84
|
-
val newMediaItems =
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
105
|
+
val newMediaItems =
|
|
106
|
+
newQueueTracks.map { track ->
|
|
107
|
+
val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
|
|
108
|
+
makeMediaItem(track, mediaId)
|
|
109
|
+
}
|
|
88
110
|
|
|
89
111
|
if (exo.mediaItemCount > currentIndex + 1) {
|
|
90
112
|
exo.removeMediaItems(currentIndex + 1, exo.mediaItemCount)
|
|
@@ -97,10 +119,11 @@ internal fun TrackPlayerCore.rebuildQueueFromCurrentPosition() {
|
|
|
97
119
|
internal fun TrackPlayerCore.updatePlayerQueue(tracks: List<TrackItem>) {
|
|
98
120
|
currentTracks = tracks
|
|
99
121
|
val playlistId = currentPlaylistId ?: ""
|
|
100
|
-
val mediaItems =
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
122
|
+
val mediaItems =
|
|
123
|
+
tracks.map { track ->
|
|
124
|
+
val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
|
|
125
|
+
makeMediaItem(track, mediaId)
|
|
126
|
+
}
|
|
104
127
|
exo.setMediaItems(mediaItems, false)
|
|
105
128
|
if (exo.playbackState == Player.STATE_IDLE && mediaItems.isNotEmpty()) {
|
|
106
129
|
exo.prepare()
|
|
@@ -109,19 +132,28 @@ internal fun TrackPlayerCore.updatePlayerQueue(tracks: List<TrackItem>) {
|
|
|
109
132
|
|
|
110
133
|
// ── MediaItem construction (member extension to access downloadManager) ────
|
|
111
134
|
|
|
112
|
-
internal fun TrackPlayerCore.makeMediaItem(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
135
|
+
internal fun TrackPlayerCore.makeMediaItem(
|
|
136
|
+
track: TrackItem,
|
|
137
|
+
customMediaId: String? = null,
|
|
138
|
+
): MediaItem {
|
|
139
|
+
val metaBuilder =
|
|
140
|
+
MediaMetadata
|
|
141
|
+
.Builder()
|
|
142
|
+
.setTitle(track.title)
|
|
143
|
+
.setArtist(track.artist)
|
|
144
|
+
.setAlbumTitle(track.album)
|
|
117
145
|
|
|
118
146
|
track.artwork?.asSecondOrNull()?.let { artworkUrl ->
|
|
119
|
-
try {
|
|
147
|
+
try {
|
|
148
|
+
metaBuilder.setArtworkUri(Uri.parse(artworkUrl))
|
|
149
|
+
} catch (_: Exception) {
|
|
150
|
+
}
|
|
120
151
|
}
|
|
121
152
|
|
|
122
153
|
val effectiveUrl = downloadManager.getEffectiveUrl(track)
|
|
123
154
|
|
|
124
|
-
return MediaItem
|
|
155
|
+
return MediaItem
|
|
156
|
+
.Builder()
|
|
125
157
|
.setMediaId(customMediaId ?: track.id)
|
|
126
158
|
.setUri(effectiveUrl)
|
|
127
159
|
.setMediaMetadata(metaBuilder.build())
|
|
@@ -166,5 +198,4 @@ internal fun TrackPlayerCore.determineCurrentTemporaryType(): TrackPlayerCore.Te
|
|
|
166
198
|
return TrackPlayerCore.TemporaryType.NONE
|
|
167
199
|
}
|
|
168
200
|
|
|
169
|
-
internal fun TrackPlayerCore.extractTrackId(mediaId: String): String =
|
|
170
|
-
if (mediaId.contains(':')) mediaId.substring(mediaId.indexOf(':') + 1) else mediaId
|
|
201
|
+
internal fun TrackPlayerCore.extractTrackId(mediaId: String): String = if (mediaId.contains(':')) mediaId.substring(mediaId.indexOf(':') + 1) else mediaId
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
package com.margelo.nitro.nitroplayer.core
|
|
2
2
|
|
|
3
|
+
import com.margelo.nitro.nitroplayer.equalizer.EqualizerCore
|
|
3
4
|
import com.margelo.nitro.nitroplayer.media.MediaSessionManager
|
|
4
5
|
import com.margelo.nitro.nitroplayer.media.NitroPlayerMediaBrowserService
|
|
6
|
+
import com.margelo.nitro.nitroplayer.media.NitroPlayerPlaybackService
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
|
-
* Initialises
|
|
8
|
-
* Called once from TrackPlayerCore.
|
|
9
|
+
* Initialises ExoPlayerCore wrapper and MediaSessionManager from the service binder.
|
|
10
|
+
* Called once from TrackPlayerCore's ServiceConnection.onServiceConnected via playerHandler.post.
|
|
9
11
|
*/
|
|
10
|
-
internal fun TrackPlayerCore.
|
|
11
|
-
|
|
12
|
+
internal fun TrackPlayerCore.initFromService(binder: NitroPlayerPlaybackService.LocalBinder) {
|
|
13
|
+
// Wrap the service-owned ExoPlayer
|
|
14
|
+
exo = ExoPlayerCore(binder.exoPlayer)
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
// Wrap the service-owned MediaSession (no longer creates its own)
|
|
17
|
+
mediaSessionManager =
|
|
18
|
+
MediaSessionManager(context, binder.session, playlistManager).apply {
|
|
19
|
+
setTrackPlayerCore(this@initFromService)
|
|
20
|
+
}
|
|
16
21
|
|
|
17
22
|
// Give MediaBrowserService access to this core and media session
|
|
18
23
|
NitroPlayerMediaBrowserService.trackPlayerCore = this
|
|
@@ -23,6 +28,20 @@ internal fun TrackPlayerCore.initExoAndMedia() {
|
|
|
23
28
|
playerListener = listener
|
|
24
29
|
exo.addListener(listener)
|
|
25
30
|
|
|
26
|
-
//
|
|
31
|
+
// The audio session ID is assigned during ExoPlayer construction (in
|
|
32
|
+
// PlaybackService.onCreate), before our listener is attached.
|
|
33
|
+
// onAudioSessionIdChanged only fires on *changes*, so we'd miss the
|
|
34
|
+
// initial value. Manually feed it to the equalizer now.
|
|
35
|
+
val sessionId = binder.exoPlayer.audioSessionId
|
|
36
|
+
if (sessionId != 0) {
|
|
37
|
+
try {
|
|
38
|
+
EqualizerCore.getInstance(context).initialize(sessionId)
|
|
39
|
+
} catch (_: Exception) { }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Start progress ticks on the main looper
|
|
27
43
|
playerHandler.postDelayed(progressUpdateRunnable, 250)
|
|
44
|
+
|
|
45
|
+
// Signal that the player is ready — unblocks all withPlayerContext callers
|
|
46
|
+
completeServiceReady()
|
|
28
47
|
}
|