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.
- package/android/src/main/AndroidManifest.xml +14 -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 +70 -29
- 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 +70 -65
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +23 -12
- 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 +40 -23
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +4 -3
- 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 +12 -3
- package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +43 -34
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +30 -178
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/PlaybackService.kt +40 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +87 -85
- 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
|
@@ -46,22 +46,33 @@ class HybridTrackPlayer : HybridTrackPlayerSpec() {
|
|
|
46
46
|
private val listenerIds = mutableListOf<Pair<String, Long>>()
|
|
47
47
|
|
|
48
48
|
init {
|
|
49
|
-
val context =
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
134
|
-
|
|
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 =
|
|
141
|
-
|
|
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
|
-
|
|
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(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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 =
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
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
|
-
|
|
52
|
-
fun
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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) {
|
|
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>(
|
|
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 =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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) =
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
} catch (_: Exception) {}
|
|
43
|
-
}
|
|
46
|
+
}
|
|
44
47
|
|
|
45
48
|
private fun TrackPlayerCore.loadPlaylistInternal(playlistId: String) {
|
|
46
|
-
playNextStack.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) =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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() =
|
|
60
|
-
|
|
61
|
-
|
|
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(
|
|
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 =
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
exo.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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 =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
exo.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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 =
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
-
|
|
169
|
-
fun removeOnChangeTrackListener(id: Long): Boolean =
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
fun
|
|
178
|
-
|
|
179
|
-
fun
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
fun
|
|
188
|
-
|
|
189
|
-
fun
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
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
|
|