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.
Files changed (36) hide show
  1. package/android/src/main/AndroidManifest.xml +15 -1
  2. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +5 -6
  3. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +68 -49
  4. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +67 -21
  5. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +27 -5
  6. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +88 -49
  7. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +40 -10
  8. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ExoPlayerCore.kt +46 -45
  9. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ListenerRegistry.kt +4 -1
  10. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerAndroidAuto.kt +38 -32
  11. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +146 -81
  12. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +24 -13
  13. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerNotify.kt +16 -4
  14. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +101 -72
  15. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueue.kt +42 -22
  16. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +55 -24
  17. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +27 -8
  18. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerTempQueue.kt +73 -62
  19. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +51 -48
  20. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +3 -3
  21. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadManagerCore.kt +14 -3
  22. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadWorker.kt +94 -23
  23. package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +43 -34
  24. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/ExoPlayerBuilder.kt +49 -0
  25. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionCallbackFactory.kt +167 -0
  26. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +11 -450
  27. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/NitroPlayerNotificationProvider.kt +200 -0
  28. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/PlaybackService.kt +101 -0
  29. package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +87 -85
  30. package/ios/core/TrackPlayerQueueBuild.swift +16 -2
  31. package/package.json +1 -1
  32. package/src/hooks/useEqualizer.ts +15 -12
  33. package/src/specs/AndroidAutoMediaLibrary.nitro.ts +3 -2
  34. package/src/specs/DownloadManager.nitro.ts +4 -2
  35. package/src/specs/Equalizer.nitro.ts +4 -2
  36. 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 player thread (ExoPlayerCore is built with playerThread.looper).
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
- override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
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 ((reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO ||
28
- reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK) &&
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 = 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
- }
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(timeline: androidx.media3.common.Timeline, reason: Int) {
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(playWhenReady: Boolean, reason: Int) {
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(track: TrackItem, reason: Reason?) {
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(state: TrackPlayerState, reason: Reason?) {
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(position: Double, duration: Double) {
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(tracks: List<TrackItem>, lookahead: Int) {
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) = withPlayerContext {
23
- isManuallySeeked = true
24
- exo.seekTo((position * 1000).toLong())
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() = withPlayerContext {
28
- if (exo.hasNextMediaItem()) {
29
- exo.seekToNext()
30
- checkUpcomingTracksForUrls(lookaheadCount)
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() = 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)
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
- currentTrackIndex > 0 -> playFromIndexInternal(currentTrackIndex - 1)
64
+ currentTrackIndex > 0 -> {
65
+ playFromIndexInternal(currentTrackIndex - 1)
66
+ }
59
67
 
60
- else -> exo.seekTo(0)
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) = 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
- }
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) = withPlayerContext {
78
- val clamped = volume.coerceIn(0.0, 100.0)
79
- exo.setVolume((clamped / 100.0).toFloat())
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) = 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
- }
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(songId: String, fromPlaylist: String?) = withPlayerContext {
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(songId: String, fromPlaylist: String?) {
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 return
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) { targetPlaylistId = playlist.id; break }
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()) { targetPlaylistId = all[0].id; songIndex = 0 }
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 = 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
- }
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) = withPlayerContext {
156
- if (speed <= 0.0) throw IllegalArgumentException("Speed must be greater than 0")
157
- if (isExoInitialized) exo.setPlaybackSpeed(speed.toFloat())
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 = withPlayerContext {
161
- if (isExoInitialized) exo.getPlaybackSpeed().toDouble() else 1.0
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 = 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
- }
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 = if (currentTemporaryType != TrackPlayerCore.TemporaryType.NONE) {
74
- minOf(currentIndex + 1, currentTracks.size)
75
- } else {
76
- currentIndex
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) { skipped = true; continue }
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) { skipped = true; continue }
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) { playFromIndexInternal(index); return true }
134
- if (index == currentPos) { exo.seekTo(0); return true }
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(); upNextQueue.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 = currentTracks.subList(index, currentTracks.size).map { track ->
26
- val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
27
- makeMediaItem(track, mediaId)
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
- if (currentTrackId != null && currentTracks.none { it.id == currentTrackId }) {
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) { skipped = true; continue }
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) { skipped = true; continue }
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 = newQueueTracks.map { track ->
85
- val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
86
- makeMediaItem(track, mediaId)
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 = tracks.map { track ->
101
- val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
102
- makeMediaItem(track, mediaId)
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(track: TrackItem, customMediaId: String? = null): MediaItem {
113
- val metaBuilder = MediaMetadata.Builder()
114
- .setTitle(track.title)
115
- .setArtist(track.artist)
116
- .setAlbumTitle(track.album)
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 { metaBuilder.setArtworkUri(Uri.parse(artworkUrl)) } catch (_: Exception) {}
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.Builder()
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 ExoPlayer (via ExoPlayerCore) and MediaSessionManager on the player thread.
8
- * Called once from TrackPlayerCore.init via playerHandler.post.
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.initExoAndMedia() {
11
- exo = ExoPlayerCore(context, playerThread)
12
+ internal fun TrackPlayerCore.initFromService(binder: NitroPlayerPlaybackService.LocalBinder) {
13
+ // Wrap the service-owned ExoPlayer
14
+ exo = ExoPlayerCore(binder.exoPlayer)
12
15
 
13
- mediaSessionManager = MediaSessionManager(context, exo.player, playlistManager).apply {
14
- setTrackPlayerCore(this@initExoAndMedia)
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
- // Start progress ticks on the player thread
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
  }