react-native-nitro-player 0.7.1-alpha.1 → 0.7.1-alpha.2

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 (31) hide show
  1. package/android/src/main/AndroidManifest.xml +14 -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 +70 -29
  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 +70 -65
  12. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +23 -12
  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 +40 -23
  17. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +4 -3
  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 +12 -3
  22. package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +43 -34
  23. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +30 -178
  24. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/PlaybackService.kt +40 -0
  25. package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +87 -85
  26. package/package.json +1 -1
  27. package/src/hooks/useEqualizer.ts +15 -12
  28. package/src/specs/AndroidAutoMediaLibrary.nitro.ts +3 -2
  29. package/src/specs/DownloadManager.nitro.ts +4 -2
  30. package/src/specs/Equalizer.nitro.ts +4 -2
  31. package/src/specs/TrackPlayer.nitro.ts +18 -6
@@ -46,22 +46,33 @@ class HybridTrackPlayer : HybridTrackPlayerSpec() {
46
46
  private val listenerIds = mutableListOf<Pair<String, Long>>()
47
47
 
48
48
  init {
49
- val context = NitroModules.applicationContext
50
- ?: throw IllegalStateException("React Context is not initialized")
49
+ val context =
50
+ NitroModules.applicationContext
51
+ ?: throw IllegalStateException("React Context is not initialized")
51
52
  core = TrackPlayerCore.getInstance(context)
52
53
  }
53
54
 
54
55
  // ── Playback ─────────────────────────────────────────────────────────────
55
56
 
56
57
  override fun play(): Promise<Unit> = Promise.async { core.play() }
58
+
57
59
  override fun pause(): Promise<Unit> = Promise.async { core.pause() }
60
+
58
61
  override fun seek(position: Double): Promise<Unit> = Promise.async { core.seek(position) }
62
+
59
63
  override fun skipToNext(): Promise<Unit> = Promise.async { core.skipToNext() }
64
+
60
65
  override fun skipToPrevious(): Promise<Unit> = Promise.async { core.skipToPrevious() }
61
- override fun playSong(songId: String, fromPlaylist: String?): Promise<Unit> = Promise.async { core.playSong(songId, fromPlaylist) }
66
+
67
+ override fun playSong(
68
+ songId: String,
69
+ fromPlaylist: String?,
70
+ ): Promise<Unit> = Promise.async { core.playSong(songId, fromPlaylist) }
71
+
62
72
  override fun skipToIndex(index: Double): Promise<Boolean> = Promise.async { core.skipToIndex(index.toInt()) }
63
73
 
64
74
  override fun setRepeatMode(mode: RepeatMode): Promise<Unit> = Promise.async { core.setRepeatMode(mode) }
75
+
65
76
  override fun getRepeatMode(): RepeatMode = core.getRepeatMode()
66
77
 
67
78
  override fun setVolume(volume: Double): Promise<Unit> = Promise.async { core.setVolume(volume) }
@@ -71,10 +82,15 @@ class HybridTrackPlayer : HybridTrackPlayerSpec() {
71
82
  // ── Queue / state reads ───────────────────────────────────────────────────
72
83
 
73
84
  override fun getActualQueue(): Promise<Array<TrackItem>> = Promise.async { core.getActualQueue().toTypedArray() }
85
+
74
86
  override fun getState(): Promise<PlayerState> = Promise.async { core.getState() }
87
+
75
88
  override fun getTracksById(trackIds: Array<String>): Promise<Array<TrackItem>> = Promise.async { core.getTracksById(trackIds.toList()).toTypedArray() }
89
+
76
90
  override fun getTracksNeedingUrls(): Promise<Array<TrackItem>> = Promise.async { core.getTracksNeedingUrls().toTypedArray() }
91
+
77
92
  override fun getNextTracks(count: Double): Promise<Array<TrackItem>> = Promise.async { core.getNextTracks(count.toInt()).toTypedArray() }
93
+
78
94
  override fun getCurrentTrackIndex(): Promise<Double> = Promise.async { core.getCurrentTrackIndex().toDouble() }
79
95
 
80
96
  // ── URL updates ───────────────────────────────────────────────────────────
@@ -84,18 +100,30 @@ class HybridTrackPlayer : HybridTrackPlayerSpec() {
84
100
  // ── Temporary queue ───────────────────────────────────────────────────────
85
101
 
86
102
  override fun addToUpNext(trackId: String): Promise<Unit> = Promise.async { core.addToUpNext(trackId) }
103
+
87
104
  override fun playNext(trackId: String): Promise<Unit> = Promise.async { core.playNext(trackId) }
105
+
88
106
  override fun removeFromPlayNext(trackId: String): Promise<Boolean> = Promise.async { core.removeFromPlayNext(trackId) }
107
+
89
108
  override fun removeFromUpNext(trackId: String): Promise<Boolean> = Promise.async { core.removeFromUpNext(trackId) }
109
+
90
110
  override fun clearPlayNext(): Promise<Unit> = Promise.async { core.clearPlayNext() }
111
+
91
112
  override fun clearUpNext(): Promise<Unit> = Promise.async { core.clearUpNext() }
92
- override fun reorderTemporaryTrack(trackId: String, newIndex: Double): Promise<Boolean> = Promise.async { core.reorderTemporaryTrack(trackId, newIndex.toInt()) }
113
+
114
+ override fun reorderTemporaryTrack(
115
+ trackId: String,
116
+ newIndex: Double,
117
+ ): Promise<Boolean> = Promise.async { core.reorderTemporaryTrack(trackId, newIndex.toInt()) }
118
+
93
119
  override fun getPlayNextQueue(): Promise<Array<TrackItem>> = Promise.async { core.getPlayNextQueue().toTypedArray() }
120
+
94
121
  override fun getUpNextQueue(): Promise<Array<TrackItem>> = Promise.async { core.getUpNextQueue().toTypedArray() }
95
122
 
96
123
  // ── Playback speed ────────────────────────────────────────────────────────
97
124
 
98
125
  override fun setPlaybackSpeed(speed: Double): Promise<Unit> = Promise.async { core.setPlayBackSpeed(speed) }
126
+
99
127
  override fun getPlaybackSpeed(): Promise<Double> = Promise.async { core.getPlayBackSpeed() }
100
128
 
101
129
  // ── Android Auto ──────────────────────────────────────────────────────────
@@ -130,16 +158,18 @@ class HybridTrackPlayer : HybridTrackPlayerSpec() {
130
158
  }
131
159
 
132
160
  override fun onTracksNeedUpdate(callback: (tracks: Array<TrackItem>, lookahead: Double) -> Unit) {
133
- val id = core.addOnTracksNeedUpdateListener { tracks, lookahead ->
134
- callback(tracks.toTypedArray(), lookahead.toDouble())
135
- }
161
+ val id =
162
+ core.addOnTracksNeedUpdateListener { tracks, lookahead ->
163
+ callback(tracks.toTypedArray(), lookahead.toDouble())
164
+ }
136
165
  listenerIds += "onTracksNeedUpdate" to id
137
166
  }
138
167
 
139
168
  override fun onTemporaryQueueChange(callback: (playNextQueue: Array<TrackItem>, upNextQueue: Array<TrackItem>) -> Unit) {
140
- val id = core.addOnTemporaryQueueChangeListener { pn, un ->
141
- callback(pn.toTypedArray(), un.toTypedArray())
142
- }
169
+ val id =
170
+ core.addOnTemporaryQueueChangeListener { pn, un ->
171
+ callback(pn.toTypedArray(), un.toTypedArray())
172
+ }
143
173
  listenerIds += "onTemporaryQueueChange" to id
144
174
  }
145
175
 
@@ -1,7 +1,7 @@
1
1
  package com.margelo.nitro.nitroplayer.core
2
2
 
3
- import android.os.HandlerThread
4
3
  import android.content.Context
4
+ import android.os.HandlerThread
5
5
  import androidx.media3.common.AudioAttributes
6
6
  import androidx.media3.common.C
7
7
  import androidx.media3.common.MediaItem
@@ -9,63 +9,102 @@ import androidx.media3.common.Player
9
9
  import androidx.media3.exoplayer.DefaultLoadControl
10
10
  import androidx.media3.exoplayer.ExoPlayer
11
11
 
12
-
13
- class ExoPlayerCore(context: Context, playerThread: HandlerThread) {
14
-
12
+ class ExoPlayerCore(
13
+ context: Context,
14
+ playerThread: HandlerThread,
15
+ ) {
15
16
  /** The underlying ExoPlayer instance — accessible for MediaSessionManager wiring. */
16
17
  internal val player: ExoPlayer = build(context, playerThread)
17
18
 
18
- private fun build(context: Context, playerThread: HandlerThread): ExoPlayer {
19
- val loadControl = DefaultLoadControl.Builder()
20
- .setBufferDurationsMs(
21
- /* minBufferMs */ 30_000,
22
- /* maxBufferMs */ 120_000,
23
- /* bufferForPlayback */ 2_500,
24
- /* bufferForRebuffer */ 5_000,
25
- )
26
- .setBackBuffer(30_000, /* retainBackBufferFromKeyframe */ true)
27
- .setTargetBufferBytes(C.LENGTH_UNSET)
28
- .setPrioritizeTimeOverSizeThresholds(true)
29
- .build()
19
+ private fun build(
20
+ context: Context,
21
+ playerThread: HandlerThread,
22
+ ): ExoPlayer {
23
+ val loadControl =
24
+ DefaultLoadControl
25
+ .Builder()
26
+ .setBufferDurationsMs(
27
+ // minBufferMs
28
+ 30_000,
29
+ // maxBufferMs
30
+ 120_000,
31
+ // bufferForPlayback
32
+ 2_500,
33
+ // bufferForRebuffer
34
+ 5_000,
35
+ ).setBackBuffer(30_000, /* retainBackBufferFromKeyframe */ true)
36
+ .setTargetBufferBytes(C.LENGTH_UNSET)
37
+ .setPrioritizeTimeOverSizeThresholds(true)
38
+ .build()
30
39
 
31
- val audioAttrs = AudioAttributes.Builder()
32
- .setUsage(C.USAGE_MEDIA)
33
- .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
34
- .build()
40
+ val audioAttrs =
41
+ AudioAttributes
42
+ .Builder()
43
+ .setUsage(C.USAGE_MEDIA)
44
+ .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
45
+ .build()
35
46
 
36
- return ExoPlayer.Builder(context)
47
+ return ExoPlayer
48
+ .Builder(context)
37
49
  .setLooper(playerThread.looper)
38
50
  .setLoadControl(loadControl)
39
51
  .setAudioAttributes(audioAttrs, /* handleAudioFocus */ true)
40
52
  .setHandleAudioBecomingNoisy(true)
53
+ .setWakeMode(C.WAKE_MODE_NETWORK)
41
54
  .setPauseAtEndOfMediaItems(false)
42
55
  .build()
43
56
  }
44
57
 
45
58
  // ── Playback ───────────────────────────────────────────────────────────
46
59
  fun play() = player.play()
60
+
47
61
  fun pause() = player.pause()
62
+
48
63
  fun seekTo(positionMs: Long) = player.seekTo(positionMs)
64
+
49
65
  fun seekToNext() = player.seekToNextMediaItem()
66
+
50
67
  fun hasNextMediaItem(): Boolean = player.hasNextMediaItem()
51
- fun setRepeatMode(mode: Int) { player.repeatMode = mode }
52
- fun setVolume(volume: Float) { player.volume = volume }
68
+
69
+ fun setRepeatMode(mode: Int) {
70
+ player.repeatMode = mode
71
+ }
72
+
73
+ fun setVolume(volume: Float) {
74
+ player.volume = volume
75
+ }
76
+
53
77
  fun setPlaybackSpeed(speed: Float) = player.setPlaybackSpeed(speed)
78
+
54
79
  fun getPlaybackSpeed(): Float = player.playbackParameters.speed
55
80
 
56
81
  // ── Queue mutations ────────────────────────────────────────────────────
57
82
  fun prepare() = player.prepare()
83
+
58
84
  fun seekToDefaultPosition(windowIndex: Int) = player.seekToDefaultPosition(windowIndex)
85
+
59
86
  fun clearMediaItems() = player.clearMediaItems()
60
- fun setMediaItems(items: List<MediaItem>, resetPosition: Boolean = false) =
61
- player.setMediaItems(items, resetPosition)
87
+
88
+ fun setMediaItems(
89
+ items: List<MediaItem>,
90
+ resetPosition: Boolean = false,
91
+ ) = player.setMediaItems(items, resetPosition)
92
+
62
93
  fun addMediaItems(items: List<MediaItem>) = player.addMediaItems(items)
63
- fun removeMediaItems(fromIndex: Int, toIndex: Int) =
64
- player.removeMediaItems(fromIndex, toIndex)
65
- fun replaceMediaItem(index: Int, item: MediaItem) = player.replaceMediaItem(index, item)
94
+
95
+ fun removeMediaItems(
96
+ fromIndex: Int,
97
+ toIndex: Int,
98
+ ) = player.removeMediaItems(fromIndex, toIndex)
99
+
100
+ fun replaceMediaItem(
101
+ index: Int,
102
+ item: MediaItem,
103
+ ) = player.replaceMediaItem(index, item)
66
104
 
67
105
  // ── Listener wiring ────────────────────────────────────────────────────
68
106
  fun addListener(listener: Player.Listener) = player.addListener(listener)
107
+
69
108
  fun removeListener(listener: Player.Listener) = player.removeListener(listener)
70
109
 
71
110
  // ── State reads ────────────────────────────────────────────────────────
@@ -73,7 +112,9 @@ class ExoPlayerCore(context: Context, playerThread: HandlerThread) {
73
112
  val isPlaying: Boolean get() = player.isPlaying
74
113
  var playWhenReady: Boolean
75
114
  get() = player.playWhenReady
76
- set(value) { player.playWhenReady = value }
115
+ set(value) {
116
+ player.playWhenReady = value
117
+ }
77
118
  val currentMediaItem: MediaItem? get() = player.currentMediaItem
78
119
  val currentMediaItemIndex: Int get() = player.currentMediaItemIndex
79
120
  val currentPosition: Long get() = player.currentPosition
@@ -8,7 +8,10 @@ import java.util.concurrent.atomic.AtomicLong
8
8
  * Uses CopyOnWriteArrayList for lock-free iteration and AtomicLong for ID generation.
9
9
  */
10
10
  class ListenerRegistry<T> {
11
- private data class Entry<T>(val id: Long, val callback: T)
11
+ private data class Entry<T>(
12
+ val id: Long,
13
+ val callback: T,
14
+ )
12
15
 
13
16
  private val entries = CopyOnWriteArrayList<Entry<T>>()
14
17
  private val nextId = AtomicLong(0)
@@ -13,50 +13,56 @@ import com.margelo.nitro.nitroplayer.media.NitroPlayerMediaBrowserService
13
13
 
14
14
  /** Called on the main thread from TrackPlayerCore.init via handler.post. */
15
15
  internal fun TrackPlayerCore.setupAndroidAutoDetector() {
16
- androidAutoConnectionDetector = AndroidAutoConnectionDetector(context).apply {
17
- onConnectionChanged = { connected, _ ->
18
- handler.post {
19
- isAndroidAutoConnectedField = connected
20
- NitroPlayerMediaBrowserService.isAndroidAutoConnected = connected
21
- notifyAndroidAutoConnection(connected)
16
+ androidAutoConnectionDetector =
17
+ AndroidAutoConnectionDetector(context).apply {
18
+ onConnectionChanged = { connected, _ ->
19
+ handler.post {
20
+ isAndroidAutoConnectedField = connected
21
+ NitroPlayerMediaBrowserService.isAndroidAutoConnected = connected
22
+ notifyAndroidAutoConnection(connected)
23
+ }
22
24
  }
25
+ registerCarConnectionReceiver()
23
26
  }
24
- registerCarConnectionReceiver()
25
- }
26
27
  }
27
28
 
28
29
  /** Called by MediaBrowserService when the user picks a track in Android Auto. */
29
- suspend fun TrackPlayerCore.playFromPlaylistTrack(mediaId: String) = withPlayerContext {
30
- try {
31
- val colonIndex = mediaId.indexOf(':')
32
- if (colonIndex <= 0 || colonIndex >= mediaId.length - 1) return@withPlayerContext
33
- val playlistId = mediaId.substring(0, colonIndex)
34
- val trackId = mediaId.substring(colonIndex + 1)
35
- val playlist = playlistManager.getPlaylist(playlistId) ?: return@withPlayerContext
36
- val trackIndex = playlist.tracks.indexOfFirst { it.id == trackId }
37
- if (trackIndex < 0) return@withPlayerContext
38
- if (currentPlaylistId != playlistId) {
39
- loadPlaylistInternal(playlistId)
30
+ suspend fun TrackPlayerCore.playFromPlaylistTrack(mediaId: String) =
31
+ withPlayerContext {
32
+ try {
33
+ val colonIndex = mediaId.indexOf(':')
34
+ if (colonIndex <= 0 || colonIndex >= mediaId.length - 1) return@withPlayerContext
35
+ val playlistId = mediaId.substring(0, colonIndex)
36
+ val trackId = mediaId.substring(colonIndex + 1)
37
+ val playlist = playlistManager.getPlaylist(playlistId) ?: return@withPlayerContext
38
+ val trackIndex = playlist.tracks.indexOfFirst { it.id == trackId }
39
+ if (trackIndex < 0) return@withPlayerContext
40
+ if (currentPlaylistId != playlistId) {
41
+ loadPlaylistInternal(playlistId)
42
+ }
43
+ playFromIndexInternal(trackIndex)
44
+ } catch (_: Exception) {
40
45
  }
41
- playFromIndexInternal(trackIndex)
42
- } catch (_: Exception) {}
43
- }
46
+ }
44
47
 
45
48
  private fun TrackPlayerCore.loadPlaylistInternal(playlistId: String) {
46
- playNextStack.clear(); upNextQueue.clear()
49
+ playNextStack.clear()
50
+ upNextQueue.clear()
47
51
  currentTemporaryType = TrackPlayerCore.TemporaryType.NONE
48
52
  val playlist = playlistManager.getPlaylist(playlistId) ?: return
49
53
  currentPlaylistId = playlistId
50
54
  updatePlayerQueue(playlist.tracks)
51
55
  }
52
56
 
53
- suspend fun TrackPlayerCore.setAndroidAutoMediaLibrary(libraryJson: String) = withPlayerContext {
54
- val library = MediaLibraryParser.fromJson(libraryJson)
55
- mediaLibraryManager.setMediaLibrary(library)
56
- NitroPlayerMediaBrowserService.getInstance()?.onPlaylistsUpdated()
57
- }
57
+ suspend fun TrackPlayerCore.setAndroidAutoMediaLibrary(libraryJson: String) =
58
+ withPlayerContext {
59
+ val library = MediaLibraryParser.fromJson(libraryJson)
60
+ mediaLibraryManager.setMediaLibrary(library)
61
+ NitroPlayerMediaBrowserService.getInstance()?.onPlaylistsUpdated()
62
+ }
58
63
 
59
- suspend fun TrackPlayerCore.clearAndroidAutoMediaLibrary() = withPlayerContext {
60
- mediaLibraryManager.clear()
61
- NitroPlayerMediaBrowserService.getInstance()?.onPlaylistsUpdated()
62
- }
64
+ suspend fun TrackPlayerCore.clearAndroidAutoMediaLibrary() =
65
+ withPlayerContext {
66
+ mediaLibraryManager.clear()
67
+ NitroPlayerMediaBrowserService.getInstance()?.onPlaylistsUpdated()
68
+ }
@@ -22,8 +22,9 @@ import kotlinx.coroutines.suspendCancellableCoroutine
22
22
  import kotlin.coroutines.resume
23
23
  import kotlin.coroutines.resumeWithException
24
24
 
25
- class TrackPlayerCore private constructor(internal val context: Context) {
26
-
25
+ class TrackPlayerCore private constructor(
26
+ internal val context: Context,
27
+ ) {
27
28
  // ── Thread infrastructure ──────────────────────────────────────────────
28
29
  /** Main-looper handler — only for Android Auto connection callbacks */
29
30
  internal val handler = Handler(Looper.getMainLooper())
@@ -33,6 +34,7 @@ class TrackPlayerCore private constructor(internal val context: Context) {
33
34
 
34
35
  // ── ExoPlayer wrapper (created on player thread inside initExoAndMedia) ──
35
36
  internal lateinit var exo: ExoPlayerCore
37
+
36
38
  /** Safe initialized check — backing field can only be read from the declaring class. */
37
39
  internal val isExoInitialized: Boolean get() = ::exo.isInitialized
38
40
 
@@ -45,9 +47,11 @@ class TrackPlayerCore private constructor(internal val context: Context) {
45
47
  // ── Playback state ─────────────────────────────────────────────────────
46
48
  @Volatile internal var currentPlaylistId: String? = null
47
49
  internal var isManuallySeeked = false
50
+
48
51
  @Volatile internal var isAndroidAutoConnectedField: Boolean = false
49
52
  internal var androidAutoConnectionDetector: AndroidAutoConnectionDetector? = null
50
53
  internal var previousMediaItem: androidx.media3.common.MediaItem? = null
54
+
51
55
  @Volatile internal var currentRepeatMode: RepeatMode = RepeatMode.OFF
52
56
  internal var lookaheadCount: Int = 5
53
57
  internal var playerListener: androidx.media3.common.Player.Listener? = null
@@ -78,33 +82,35 @@ class TrackPlayerCore private constructor(internal val context: Context) {
78
82
  ListenerRegistry<(Boolean) -> Unit>()
79
83
 
80
84
  // ── Progress & playlist-update runnables ───────────────────────────────
81
- internal val progressUpdateRunnable = object : Runnable {
82
- override fun run() {
83
- if (::exo.isInitialized &&
84
- exo.playbackState != androidx.media3.common.Player.STATE_IDLE
85
- ) {
86
- val pos = exo.currentPosition / 1000.0
87
- val dur = if (exo.duration > 0) exo.duration / 1000.0 else 0.0
88
- notifyPlaybackProgress(pos, dur, if (isManuallySeeked) true else null)
89
- isManuallySeeked = false
85
+ internal val progressUpdateRunnable =
86
+ object : Runnable {
87
+ override fun run() {
88
+ if (::exo.isInitialized &&
89
+ exo.playbackState != androidx.media3.common.Player.STATE_IDLE
90
+ ) {
91
+ val pos = exo.currentPosition / 1000.0
92
+ val dur = if (exo.duration > 0) exo.duration / 1000.0 else 0.0
93
+ notifyPlaybackProgress(pos, dur, if (isManuallySeeked) true else null)
94
+ isManuallySeeked = false
95
+ }
96
+ playerHandler.postDelayed(this, 250)
90
97
  }
91
- playerHandler.postDelayed(this, 250)
92
98
  }
93
- }
94
99
 
95
- internal val updateCurrentPlaylistRunnable = Runnable {
96
- val id = currentPlaylistId ?: return@Runnable
97
- val playlist = playlistManager.getPlaylist(id) ?: return@Runnable
98
- currentTracks = playlist.tracks
99
- if (::exo.isInitialized &&
100
- exo.currentMediaItem != null &&
101
- exo.currentMediaItemIndex >= 0
102
- ) {
103
- rebuildQueueFromCurrentPosition()
104
- } else {
105
- updatePlayerQueue(playlist.tracks)
100
+ internal val updateCurrentPlaylistRunnable =
101
+ Runnable {
102
+ val id = currentPlaylistId ?: return@Runnable
103
+ val playlist = playlistManager.getPlaylist(id) ?: return@Runnable
104
+ currentTracks = playlist.tracks
105
+ if (::exo.isInitialized &&
106
+ exo.currentMediaItem != null &&
107
+ exo.currentMediaItemIndex >= 0
108
+ ) {
109
+ rebuildQueueFromCurrentPosition()
110
+ } else {
111
+ updatePlayerQueue(playlist.tracks)
112
+ }
106
113
  }
107
- }
108
114
 
109
115
  // ── Singleton ──────────────────────────────────────────────────────────
110
116
  companion object {
@@ -130,10 +136,14 @@ class TrackPlayerCore private constructor(internal val context: Context) {
130
136
  internal suspend fun <T> withPlayerContext(block: () -> T): T {
131
137
  if (Looper.myLooper() == playerThread.looper) return block()
132
138
  return suspendCancellableCoroutine { cont ->
133
- val r = Runnable {
134
- try { cont.resume(block()) }
135
- catch (e: Exception) { cont.resumeWithException(e) }
136
- }
139
+ val r =
140
+ Runnable {
141
+ try {
142
+ cont.resume(block())
143
+ } catch (e: Exception) {
144
+ cont.resumeWithException(e)
145
+ }
146
+ }
137
147
  playerHandler.post(r)
138
148
  cont.invokeOnCancellation { playerHandler.removeCallbacks(r) }
139
149
  }
@@ -157,45 +167,40 @@ class TrackPlayerCore private constructor(internal val context: Context) {
157
167
  // ── Simple read-only accessors ─────────────────────────────────────────
158
168
 
159
169
  fun isAndroidAutoConnected(): Boolean = isAndroidAutoConnectedField
170
+
160
171
  fun getCurrentPlaylistId(): String? = currentPlaylistId
172
+
161
173
  fun getPlaylistManager(): PlaylistManager = playlistManager
162
- fun getAllPlaylists(): List<com.margelo.nitro.nitroplayer.playlist.Playlist> =
163
- playlistManager.getAllPlaylists()
174
+
175
+ fun getAllPlaylists(): List<com.margelo.nitro.nitroplayer.playlist.Playlist> = playlistManager.getAllPlaylists()
164
176
 
165
177
  // ── Listener add/remove (returns stable ID for cleanup) ───────────────
166
178
 
167
- fun addOnChangeTrackListener(cb: (TrackItem, Reason?) -> Unit): Long =
168
- onChangeTrackListeners.add(cb)
169
- fun removeOnChangeTrackListener(id: Long): Boolean =
170
- onChangeTrackListeners.remove(id)
171
-
172
- fun addOnPlaybackStateChangeListener(cb: (TrackPlayerState, Reason?) -> Unit): Long =
173
- onPlaybackStateChangeListeners.add(cb)
174
- fun removeOnPlaybackStateChangeListener(id: Long): Boolean =
175
- onPlaybackStateChangeListeners.remove(id)
176
-
177
- fun addOnSeekListener(cb: (Double, Double) -> Unit): Long =
178
- onSeekListeners.add(cb)
179
- fun removeOnSeekListener(id: Long): Boolean =
180
- onSeekListeners.remove(id)
181
-
182
- fun addOnPlaybackProgressChangeListener(cb: (Double, Double, Boolean?) -> Unit): Long =
183
- onProgressListeners.add(cb)
184
- fun removeOnPlaybackProgressChangeListener(id: Long): Boolean =
185
- onProgressListeners.remove(id)
186
-
187
- fun addOnTracksNeedUpdateListener(cb: (List<TrackItem>, Int) -> Unit): Long =
188
- onTracksNeedUpdateListeners.add(cb)
189
- fun removeOnTracksNeedUpdateListener(id: Long): Boolean =
190
- onTracksNeedUpdateListeners.remove(id)
191
-
192
- fun addOnTemporaryQueueChangeListener(cb: (List<TrackItem>, List<TrackItem>) -> Unit): Long =
193
- onTemporaryQueueChangeListeners.add(cb)
194
- fun removeOnTemporaryQueueChangeListener(id: Long): Boolean =
195
- onTemporaryQueueChangeListeners.remove(id)
196
-
197
- fun addOnAndroidAutoConnectionListener(cb: (Boolean) -> Unit): Long =
198
- onAndroidAutoConnectionListeners.add(cb)
199
- fun removeOnAndroidAutoConnectionListener(id: Long): Boolean =
200
- onAndroidAutoConnectionListeners.remove(id)
179
+ fun addOnChangeTrackListener(cb: (TrackItem, Reason?) -> Unit): Long = onChangeTrackListeners.add(cb)
180
+
181
+ fun removeOnChangeTrackListener(id: Long): Boolean = onChangeTrackListeners.remove(id)
182
+
183
+ fun addOnPlaybackStateChangeListener(cb: (TrackPlayerState, Reason?) -> Unit): Long = onPlaybackStateChangeListeners.add(cb)
184
+
185
+ fun removeOnPlaybackStateChangeListener(id: Long): Boolean = onPlaybackStateChangeListeners.remove(id)
186
+
187
+ fun addOnSeekListener(cb: (Double, Double) -> Unit): Long = onSeekListeners.add(cb)
188
+
189
+ fun removeOnSeekListener(id: Long): Boolean = onSeekListeners.remove(id)
190
+
191
+ fun addOnPlaybackProgressChangeListener(cb: (Double, Double, Boolean?) -> Unit): Long = onProgressListeners.add(cb)
192
+
193
+ fun removeOnPlaybackProgressChangeListener(id: Long): Boolean = onProgressListeners.remove(id)
194
+
195
+ fun addOnTracksNeedUpdateListener(cb: (List<TrackItem>, Int) -> Unit): Long = onTracksNeedUpdateListeners.add(cb)
196
+
197
+ fun removeOnTracksNeedUpdateListener(id: Long): Boolean = onTracksNeedUpdateListeners.remove(id)
198
+
199
+ fun addOnTemporaryQueueChangeListener(cb: (List<TrackItem>, List<TrackItem>) -> Unit): Long = onTemporaryQueueChangeListeners.add(cb)
200
+
201
+ fun removeOnTemporaryQueueChangeListener(id: Long): Boolean = onTemporaryQueueChangeListeners.remove(id)
202
+
203
+ fun addOnAndroidAutoConnectionListener(cb: (Boolean) -> Unit): Long = onAndroidAutoConnectionListeners.add(cb)
204
+
205
+ fun removeOnAndroidAutoConnectionListener(id: Long): Boolean = onAndroidAutoConnectionListeners.remove(id)
201
206
  }
@@ -17,15 +17,19 @@ import com.margelo.nitro.nitroplayer.media.NitroPlayerMediaBrowserService
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