react-native-nitro-player 1.5.0 → 1.5.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +18 -7
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +71 -56
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +82 -9
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +10 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +84 -70
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueue.kt +4 -2
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +43 -11
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +6 -2
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerTempQueue.kt +67 -53
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +42 -11
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +26 -4
- package/ios/HybridTrackPlayer.swift +52 -29
- package/ios/core/TrackPlayerCore.swift +9 -1
- package/ios/core/TrackPlayerListener.swift +20 -5
- package/ios/core/TrackPlayerNotify.swift +9 -7
- package/ios/core/TrackPlayerPlayback.swift +75 -39
- package/ios/core/TrackPlayerQueue.swift +28 -4
- package/ios/core/TrackPlayerQueueBuild.swift +100 -40
- package/ios/core/TrackPlayerRecovery.swift +1 -1
- package/ios/core/TrackPlayerTempQueue.swift +129 -122
- package/ios/core/TrackPlayerUrlLoader.swift +34 -10
- package/ios/download/DownloadDatabase.swift +41 -13
- package/ios/media/MediaSessionManager.swift +19 -12
- package/ios/queue/HybridPlayerQueue.swift +10 -5
- package/lib/hooks/callbackManager.d.ts +17 -0
- package/lib/hooks/callbackManager.js +67 -1
- package/lib/hooks/useActualQueue.js +18 -12
- package/lib/hooks/useNowPlaying.js +10 -2
- package/package.json +1 -1
- package/src/components/CastButton.tsx +14 -2
- package/src/hooks/callbackManager.ts +87 -1
- package/src/hooks/useActualQueue.ts +18 -13
- package/src/hooks/useNowPlaying.ts +12 -2
|
@@ -8,7 +8,7 @@ import com.margelo.nitro.NitroModules
|
|
|
8
8
|
import com.margelo.nitro.core.NullType
|
|
9
9
|
import com.margelo.nitro.core.Promise
|
|
10
10
|
import com.margelo.nitro.nitroplayer.core.TrackPlayerCore
|
|
11
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
11
|
+
import com.margelo.nitro.nitroplayer.core.loadPlaylistOnQueue
|
|
12
12
|
import com.margelo.nitro.nitroplayer.core.updatePlaylist
|
|
13
13
|
import com.margelo.nitro.nitroplayer.playlist.PlaylistManager
|
|
14
14
|
import java.util.UUID
|
|
@@ -109,13 +109,24 @@ class HybridPlayerQueue : HybridPlayerQueueSpec() {
|
|
|
109
109
|
|
|
110
110
|
// ── Playback control ──────────────────────────────────────────────────────
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Enqueued synchronously on the player looper so a `loadPlaylist()` followed by
|
|
114
|
+
* `play()` from JS cannot be reordered — see HybridTrackPlayer.enqueue.
|
|
115
|
+
*/
|
|
116
|
+
override fun loadPlaylist(
|
|
117
|
+
playlistId: String,
|
|
118
|
+
index: Double?,
|
|
119
|
+
): Promise<Unit> {
|
|
120
|
+
val startIndex = index?.toInt()
|
|
121
|
+
val promise = Promise<Unit>()
|
|
122
|
+
core.enqueue {
|
|
123
|
+
if (playlistManager.loadPlaylist(playlistId, startIndex)) {
|
|
124
|
+
core.loadPlaylistOnQueue(playlistId, startIndex)
|
|
125
|
+
}
|
|
126
|
+
promise.resolve(Unit)
|
|
118
127
|
}
|
|
128
|
+
return promise
|
|
129
|
+
}
|
|
119
130
|
|
|
120
131
|
override fun getCurrentPlaylistId(): Variant_NullType_String {
|
|
121
132
|
val id = core.getCurrentPlaylistId()
|
|
@@ -7,35 +7,30 @@ import com.facebook.proguard.annotations.DoNotStrip
|
|
|
7
7
|
import com.margelo.nitro.NitroModules
|
|
8
8
|
import com.margelo.nitro.core.Promise
|
|
9
9
|
import com.margelo.nitro.nitroplayer.core.TrackPlayerCore
|
|
10
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
11
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
12
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
13
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
14
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
15
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
16
|
-
import com.margelo.nitro.nitroplayer.core.getNextTracks
|
|
17
|
-
import com.margelo.nitro.nitroplayer.core.getPlayBackSpeed
|
|
18
|
-
import com.margelo.nitro.nitroplayer.core.getPlayNextQueue
|
|
10
|
+
import com.margelo.nitro.nitroplayer.core.addToUpNextInternal
|
|
11
|
+
import com.margelo.nitro.nitroplayer.core.clearPlayNextOnQueue
|
|
12
|
+
import com.margelo.nitro.nitroplayer.core.clearUpNextOnQueue
|
|
13
|
+
import com.margelo.nitro.nitroplayer.core.configureOnQueue
|
|
14
|
+
import com.margelo.nitro.nitroplayer.core.getActualQueueInternal
|
|
15
|
+
import com.margelo.nitro.nitroplayer.core.getNextTracksInternal
|
|
19
16
|
import com.margelo.nitro.nitroplayer.core.getRepeatMode
|
|
20
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
21
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
22
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
23
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
24
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
25
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
26
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
27
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
28
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
29
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
30
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
31
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
32
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
33
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
34
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
35
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
36
|
-
import com.margelo.nitro.nitroplayer.core.
|
|
37
|
-
import com.margelo.nitro.nitroplayer.core.skipToPrevious
|
|
38
|
-
import com.margelo.nitro.nitroplayer.core.updateTracks
|
|
17
|
+
import com.margelo.nitro.nitroplayer.core.getStateInternal
|
|
18
|
+
import com.margelo.nitro.nitroplayer.core.getTracksNeedingUrlsInternal
|
|
19
|
+
import com.margelo.nitro.nitroplayer.core.pauseOnQueue
|
|
20
|
+
import com.margelo.nitro.nitroplayer.core.playNextInternal
|
|
21
|
+
import com.margelo.nitro.nitroplayer.core.playOnQueue
|
|
22
|
+
import com.margelo.nitro.nitroplayer.core.playSongInternal
|
|
23
|
+
import com.margelo.nitro.nitroplayer.core.removeFromPlayNextOnQueue
|
|
24
|
+
import com.margelo.nitro.nitroplayer.core.removeFromUpNextOnQueue
|
|
25
|
+
import com.margelo.nitro.nitroplayer.core.reorderTemporaryTrackOnQueue
|
|
26
|
+
import com.margelo.nitro.nitroplayer.core.seekOnQueue
|
|
27
|
+
import com.margelo.nitro.nitroplayer.core.setPlayBackSpeedOnQueue
|
|
28
|
+
import com.margelo.nitro.nitroplayer.core.setRepeatModeOnQueue
|
|
29
|
+
import com.margelo.nitro.nitroplayer.core.setVolumeOnQueue
|
|
30
|
+
import com.margelo.nitro.nitroplayer.core.skipToIndexInternal
|
|
31
|
+
import com.margelo.nitro.nitroplayer.core.skipToNextOnQueue
|
|
32
|
+
import com.margelo.nitro.nitroplayer.core.skipToPreviousOnQueue
|
|
33
|
+
import com.margelo.nitro.nitroplayer.core.updateTracksOnQueue
|
|
39
34
|
|
|
40
35
|
@DoNotStrip
|
|
41
36
|
@Keep
|
|
@@ -52,79 +47,99 @@ class HybridTrackPlayer : HybridTrackPlayerSpec() {
|
|
|
52
47
|
core = TrackPlayerCore.getInstance(context)
|
|
53
48
|
}
|
|
54
49
|
|
|
50
|
+
// ── Ordered dispatch ──────────────────────────────────────────────────────
|
|
51
|
+
//
|
|
52
|
+
// `Promise.async { … }` launches an unordered coroutine, so the hop onto the player
|
|
53
|
+
// looper happened at an arbitrary later point: two calls made in order from JS could
|
|
54
|
+
// reach the player in reverse order (play-then-pause landing as pause-then-play).
|
|
55
|
+
// `core.enqueue` appends synchronously, on the JS thread at call time, so execution
|
|
56
|
+
// order equals JS call order.
|
|
57
|
+
|
|
58
|
+
private fun <T> enqueue(block: () -> T): Promise<T> {
|
|
59
|
+
val promise = Promise<T>()
|
|
60
|
+
core.enqueue {
|
|
61
|
+
try {
|
|
62
|
+
promise.resolve(block())
|
|
63
|
+
} catch (e: Throwable) {
|
|
64
|
+
promise.reject(e)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return promise
|
|
68
|
+
}
|
|
69
|
+
|
|
55
70
|
// ── Playback ─────────────────────────────────────────────────────────────
|
|
56
71
|
|
|
57
|
-
override fun play(): Promise<Unit> =
|
|
72
|
+
override fun play(): Promise<Unit> = enqueue { core.playOnQueue() }
|
|
58
73
|
|
|
59
|
-
override fun pause(): Promise<Unit> =
|
|
74
|
+
override fun pause(): Promise<Unit> = enqueue { core.pauseOnQueue() }
|
|
60
75
|
|
|
61
|
-
override fun seek(position: Double): Promise<Unit> =
|
|
76
|
+
override fun seek(position: Double): Promise<Unit> = enqueue { core.seekOnQueue(position) }
|
|
62
77
|
|
|
63
|
-
override fun skipToNext(): Promise<Unit> =
|
|
78
|
+
override fun skipToNext(): Promise<Unit> = enqueue { core.skipToNextOnQueue() }
|
|
64
79
|
|
|
65
|
-
override fun skipToPrevious(): Promise<Unit> =
|
|
80
|
+
override fun skipToPrevious(): Promise<Unit> = enqueue { core.skipToPreviousOnQueue() }
|
|
66
81
|
|
|
67
82
|
override fun playSong(
|
|
68
83
|
songId: String,
|
|
69
84
|
fromPlaylist: String?,
|
|
70
|
-
): Promise<Unit> =
|
|
85
|
+
): Promise<Unit> = enqueue { core.playSongInternal(songId, fromPlaylist) }
|
|
71
86
|
|
|
72
|
-
override fun skipToIndex(index: Double): Promise<Boolean> =
|
|
87
|
+
override fun skipToIndex(index: Double): Promise<Boolean> = enqueue { core.skipToIndexInternal(index.toInt()) }
|
|
73
88
|
|
|
74
|
-
override fun setRepeatMode(mode: RepeatMode): Promise<Unit> =
|
|
89
|
+
override fun setRepeatMode(mode: RepeatMode): Promise<Unit> = enqueue { core.setRepeatModeOnQueue(mode) }
|
|
75
90
|
|
|
76
91
|
override fun getRepeatMode(): RepeatMode = core.getRepeatMode()
|
|
77
92
|
|
|
78
|
-
override fun setVolume(volume: Double): Promise<Unit> =
|
|
93
|
+
override fun setVolume(volume: Double): Promise<Unit> = enqueue { core.setVolumeOnQueue(volume) }
|
|
79
94
|
|
|
80
|
-
override fun configure(config: PlayerConfig): Promise<Unit> =
|
|
95
|
+
override fun configure(config: PlayerConfig): Promise<Unit> = enqueue { core.configureOnQueue(config) }
|
|
81
96
|
|
|
82
97
|
// ── Queue / state reads ───────────────────────────────────────────────────
|
|
83
98
|
|
|
84
|
-
override fun getActualQueue(): Promise<Array<TrackItem>> =
|
|
99
|
+
override fun getActualQueue(): Promise<Array<TrackItem>> = enqueue { core.getActualQueueInternal().toTypedArray() }
|
|
85
100
|
|
|
86
|
-
override fun getState(): Promise<PlayerState> =
|
|
101
|
+
override fun getState(): Promise<PlayerState> = enqueue { core.getStateInternal() }
|
|
87
102
|
|
|
88
|
-
override fun getTracksById(trackIds: Array<String>): Promise<Array<TrackItem>> =
|
|
103
|
+
override fun getTracksById(trackIds: Array<String>): Promise<Array<TrackItem>> = enqueue { core.getPlaylistManager().getTracksById(trackIds.toList()).toTypedArray() }
|
|
89
104
|
|
|
90
|
-
override fun getTracksNeedingUrls(): Promise<Array<TrackItem>> =
|
|
105
|
+
override fun getTracksNeedingUrls(): Promise<Array<TrackItem>> = enqueue { core.getTracksNeedingUrlsInternal().toTypedArray() }
|
|
91
106
|
|
|
92
|
-
override fun getNextTracks(count: Double): Promise<Array<TrackItem>> =
|
|
107
|
+
override fun getNextTracks(count: Double): Promise<Array<TrackItem>> = enqueue { core.getNextTracksInternal(count.toInt()).toTypedArray() }
|
|
93
108
|
|
|
94
|
-
override fun getCurrentTrackIndex(): Promise<Double> =
|
|
109
|
+
override fun getCurrentTrackIndex(): Promise<Double> = enqueue { core.currentTrackIndex.toDouble() }
|
|
95
110
|
|
|
96
111
|
// ── URL updates ───────────────────────────────────────────────────────────
|
|
97
112
|
|
|
98
|
-
override fun updateTracks(tracks: Array<TrackItem>): Promise<Unit> =
|
|
113
|
+
override fun updateTracks(tracks: Array<TrackItem>): Promise<Unit> = enqueue { core.updateTracksOnQueue(tracks.toList()) }
|
|
99
114
|
|
|
100
115
|
// ── Temporary queue ───────────────────────────────────────────────────────
|
|
101
116
|
|
|
102
|
-
override fun addToUpNext(trackId: String): Promise<Unit> =
|
|
117
|
+
override fun addToUpNext(trackId: String): Promise<Unit> = enqueue { core.addToUpNextInternal(trackId) }
|
|
103
118
|
|
|
104
|
-
override fun playNext(trackId: String): Promise<Unit> =
|
|
119
|
+
override fun playNext(trackId: String): Promise<Unit> = enqueue { core.playNextInternal(trackId) }
|
|
105
120
|
|
|
106
|
-
override fun removeFromPlayNext(trackId: String): Promise<Boolean> =
|
|
121
|
+
override fun removeFromPlayNext(trackId: String): Promise<Boolean> = enqueue { core.removeFromPlayNextOnQueue(trackId) }
|
|
107
122
|
|
|
108
|
-
override fun removeFromUpNext(trackId: String): Promise<Boolean> =
|
|
123
|
+
override fun removeFromUpNext(trackId: String): Promise<Boolean> = enqueue { core.removeFromUpNextOnQueue(trackId) }
|
|
109
124
|
|
|
110
|
-
override fun clearPlayNext(): Promise<Unit> =
|
|
125
|
+
override fun clearPlayNext(): Promise<Unit> = enqueue { core.clearPlayNextOnQueue() }
|
|
111
126
|
|
|
112
|
-
override fun clearUpNext(): Promise<Unit> =
|
|
127
|
+
override fun clearUpNext(): Promise<Unit> = enqueue { core.clearUpNextOnQueue() }
|
|
113
128
|
|
|
114
129
|
override fun reorderTemporaryTrack(
|
|
115
130
|
trackId: String,
|
|
116
131
|
newIndex: Double,
|
|
117
|
-
): Promise<Boolean> =
|
|
132
|
+
): Promise<Boolean> = enqueue { core.reorderTemporaryTrackOnQueue(trackId, newIndex.toInt()) }
|
|
118
133
|
|
|
119
|
-
override fun getPlayNextQueue(): Promise<Array<TrackItem>> =
|
|
134
|
+
override fun getPlayNextQueue(): Promise<Array<TrackItem>> = enqueue { core.playNextStack.toTypedArray() }
|
|
120
135
|
|
|
121
|
-
override fun getUpNextQueue(): Promise<Array<TrackItem>> =
|
|
136
|
+
override fun getUpNextQueue(): Promise<Array<TrackItem>> = enqueue { core.upNextQueue.toTypedArray() }
|
|
122
137
|
|
|
123
138
|
// ── Playback speed ────────────────────────────────────────────────────────
|
|
124
139
|
|
|
125
|
-
override fun setPlaybackSpeed(speed: Double): Promise<Unit> =
|
|
140
|
+
override fun setPlaybackSpeed(speed: Double): Promise<Unit> = enqueue { core.setPlayBackSpeedOnQueue(speed) }
|
|
126
141
|
|
|
127
|
-
override fun getPlaybackSpeed(): Promise<Double> =
|
|
142
|
+
override fun getPlaybackSpeed(): Promise<Double> = enqueue { if (core.isExoInitialized) core.exo.getPlaybackSpeed().toDouble() else 1.0 }
|
|
128
143
|
|
|
129
144
|
// ── Android Auto ──────────────────────────────────────────────────────────
|
|
130
145
|
|
|
@@ -94,6 +94,7 @@ class TrackPlayerCore private constructor(
|
|
|
94
94
|
ListenerRegistry<(com.margelo.nitro.nitroplayer.CastState, String?) -> Unit>()
|
|
95
95
|
|
|
96
96
|
// ── Google Cast ──────────────────────────────────────────────────────────
|
|
97
|
+
|
|
97
98
|
/** Set once the playback service has a usable CastContext; null when Cast is unavailable. */
|
|
98
99
|
internal var castSessionController: com.margelo.nitro.nitroplayer.media.CastSessionController? = null
|
|
99
100
|
|
|
@@ -104,18 +105,23 @@ class TrackPlayerCore private constructor(
|
|
|
104
105
|
internal var lastCastWaitTrackId: String? = null
|
|
105
106
|
|
|
106
107
|
// ── Progress & playlist-update runnables ───────────────────────────────
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Emits progress once per second while actually playing (matching iOS), instead of
|
|
111
|
+
* four times a second regardless of state. A paused player has nothing to report,
|
|
112
|
+
* and every tick is a JS callback that re-renders every `useNowPlaying` consumer.
|
|
113
|
+
* A seek still reports immediately via `onPositionDiscontinuity`.
|
|
114
|
+
*/
|
|
107
115
|
internal val progressUpdateRunnable =
|
|
108
116
|
object : Runnable {
|
|
109
117
|
override fun run() {
|
|
110
|
-
if (::exo.isInitialized &&
|
|
111
|
-
exo.playbackState != androidx.media3.common.Player.STATE_IDLE
|
|
112
|
-
) {
|
|
118
|
+
if (::exo.isInitialized && exo.isPlaying) {
|
|
113
119
|
val pos = exo.currentPosition / 1000.0
|
|
114
120
|
val dur = if (exo.duration > 0) exo.duration / 1000.0 else 0.0
|
|
115
121
|
notifyPlaybackProgress(pos, dur, if (isManuallySeeked) true else null)
|
|
116
122
|
isManuallySeeked = false
|
|
117
123
|
}
|
|
118
|
-
playerHandler.postDelayed(this,
|
|
124
|
+
playerHandler.postDelayed(this, PROGRESS_INTERVAL_MS)
|
|
119
125
|
}
|
|
120
126
|
}
|
|
121
127
|
|
|
@@ -155,7 +161,8 @@ class TrackPlayerCore private constructor(
|
|
|
155
161
|
}
|
|
156
162
|
try {
|
|
157
163
|
context.unbindService(this)
|
|
158
|
-
} catch (_: Exception) {
|
|
164
|
+
} catch (_: Exception) {
|
|
165
|
+
}
|
|
159
166
|
serviceBound = false
|
|
160
167
|
if (rebindAttempts < 3) {
|
|
161
168
|
rebindAttempts++
|
|
@@ -192,6 +199,8 @@ class TrackPlayerCore private constructor(
|
|
|
192
199
|
|
|
193
200
|
// ── Singleton ──────────────────────────────────────────────────────────
|
|
194
201
|
companion object {
|
|
202
|
+
internal const val PROGRESS_INTERVAL_MS = 1000L
|
|
203
|
+
|
|
195
204
|
@Volatile
|
|
196
205
|
@Suppress("ktlint:standard:property-naming")
|
|
197
206
|
private var INSTANCE: TrackPlayerCore? = null
|
|
@@ -206,8 +215,18 @@ class TrackPlayerCore private constructor(
|
|
|
206
215
|
// Defer service start/bind to the main thread so it doesn't run
|
|
207
216
|
// synchronously on the JNI thread during HybridObject creation.
|
|
208
217
|
handler.post {
|
|
209
|
-
|
|
210
|
-
|
|
218
|
+
// startService throws BackgroundServiceStartNotAllowedException on Android 12+
|
|
219
|
+
// when the process is in the background — which is exactly what happens when JS
|
|
220
|
+
// loads the module from a headless/backgrounded start. It is only here to keep
|
|
221
|
+
// the service alive across unbind; BIND_AUTO_CREATE below already creates it,
|
|
222
|
+
// and the service promotes itself to foreground once playback starts.
|
|
223
|
+
try {
|
|
224
|
+
context.startService(Intent(context, NitroPlayerPlaybackService::class.java))
|
|
225
|
+
} catch (e: Exception) {
|
|
226
|
+
NitroPlayerLogger.log("TrackPlayerCore") {
|
|
227
|
+
"startService skipped (app in background): ${e.message}"
|
|
228
|
+
}
|
|
229
|
+
}
|
|
211
230
|
|
|
212
231
|
val bindIntent =
|
|
213
232
|
Intent(context, NitroPlayerPlaybackService::class.java).apply {
|
|
@@ -219,6 +238,58 @@ class TrackPlayerCore private constructor(
|
|
|
219
238
|
|
|
220
239
|
// ── Coroutine bridge to player looper (main thread) ────────────────────
|
|
221
240
|
|
|
241
|
+
// ── Ordered command dispatch ───────────────────────────────────────────
|
|
242
|
+
//
|
|
243
|
+
// `Promise.async { … }` launches an unordered coroutine, so the hop onto the
|
|
244
|
+
// player looper used to happen at an arbitrary later point: two calls made in
|
|
245
|
+
// order from JS could reach the player in reverse order (play-then-pause landing
|
|
246
|
+
// as pause-then-play). `enqueue` appends to a queue synchronously, on the JS
|
|
247
|
+
// thread at call time, so execution order equals JS call order — including for
|
|
248
|
+
// commands issued before the playback service has bound.
|
|
249
|
+
|
|
250
|
+
private val pendingCommands = ArrayDeque<() -> Unit>()
|
|
251
|
+
private var commandsDraining = false
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Runs [block] on the player looper, preserving the order in which callers
|
|
255
|
+
* enqueued. Safe to call before the service binds — commands buffer until then.
|
|
256
|
+
*/
|
|
257
|
+
internal fun enqueue(block: () -> Unit) {
|
|
258
|
+
synchronized(pendingCommands) {
|
|
259
|
+
pendingCommands.addLast(block)
|
|
260
|
+
if (commandsDraining) return
|
|
261
|
+
if (!::playerHandler.isInitialized) return
|
|
262
|
+
commandsDraining = true
|
|
263
|
+
}
|
|
264
|
+
playerHandler.post { drainCommands() }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private fun drainCommands() {
|
|
268
|
+
while (true) {
|
|
269
|
+
val next =
|
|
270
|
+
synchronized(pendingCommands) {
|
|
271
|
+
val head = pendingCommands.removeFirstOrNull()
|
|
272
|
+
if (head == null) commandsDraining = false
|
|
273
|
+
head
|
|
274
|
+
} ?: return
|
|
275
|
+
next()
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Called once the player looper exists — flushes anything queued before binding. */
|
|
280
|
+
internal fun startCommandDraining() {
|
|
281
|
+
val shouldPost =
|
|
282
|
+
synchronized(pendingCommands) {
|
|
283
|
+
if (commandsDraining || pendingCommands.isEmpty()) {
|
|
284
|
+
false
|
|
285
|
+
} else {
|
|
286
|
+
commandsDraining = true
|
|
287
|
+
true
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (shouldPost) playerHandler.post { drainCommands() }
|
|
291
|
+
}
|
|
292
|
+
|
|
222
293
|
internal suspend fun <T> withPlayerContext(block: () -> T): T {
|
|
223
294
|
// Wait until the service is bound and player is initialized
|
|
224
295
|
serviceReady.await()
|
|
@@ -256,13 +327,15 @@ class TrackPlayerCore private constructor(
|
|
|
256
327
|
}
|
|
257
328
|
}
|
|
258
329
|
scope.cancel()
|
|
259
|
-
com.margelo.nitro.nitroplayer.media.AuthAwareHttpDataSourceFactory
|
|
330
|
+
com.margelo.nitro.nitroplayer.media.AuthAwareHttpDataSourceFactory
|
|
331
|
+
.clear()
|
|
260
332
|
// Do NOT stop the service — it owns the player.
|
|
261
333
|
// Unbind so Android can clean up if needed.
|
|
262
334
|
if (serviceBound) {
|
|
263
335
|
try {
|
|
264
336
|
context.unbindService(serviceConnection)
|
|
265
|
-
} catch (_: Exception) {
|
|
337
|
+
} catch (_: Exception) {
|
|
338
|
+
}
|
|
266
339
|
serviceBound = false
|
|
267
340
|
}
|
|
268
341
|
}
|
|
@@ -57,6 +57,16 @@ internal class TrackPlayerEventListener(
|
|
|
57
57
|
if (newIdx >= 0 && newIdx != currentTrackIndex) currentTrackIndex = newIdx
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// Top up the windowed timeline — the item we just left freed a slot.
|
|
61
|
+
// Only when it is actually short: rebuildQueueFromCurrentPosition can fall
|
|
62
|
+
// into the substitute-jump path (setMediaItems), which re-enters this very
|
|
63
|
+
// callback. The length check makes any such re-entry a no-op and terminate.
|
|
64
|
+
if (mediaItem != null &&
|
|
65
|
+
exo.mediaItemCount - exo.currentMediaItemIndex - 1 < QUEUE_WINDOW_SIZE
|
|
66
|
+
) {
|
|
67
|
+
rebuildQueueFromCurrentPosition()
|
|
68
|
+
}
|
|
69
|
+
|
|
60
70
|
val track = getCurrentTrack() ?: return
|
|
61
71
|
val r =
|
|
62
72
|
when (reason) {
|
|
@@ -15,90 +15,103 @@ import com.margelo.nitro.nitroplayer.media.NitroPlayerPlaybackService
|
|
|
15
15
|
* via withPlayerContext.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
suspend fun TrackPlayerCore.play() = withPlayerContext {
|
|
18
|
+
suspend fun TrackPlayerCore.play() = withPlayerContext { playOnQueue() }
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
internal fun TrackPlayerCore.playOnQueue() = exo.play()
|
|
21
21
|
|
|
22
|
-
suspend fun TrackPlayerCore.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
suspend fun TrackPlayerCore.pause() = withPlayerContext { pauseOnQueue() }
|
|
23
|
+
|
|
24
|
+
internal fun TrackPlayerCore.pauseOnQueue() = exo.pause()
|
|
25
|
+
|
|
26
|
+
suspend fun TrackPlayerCore.seek(position: Double) = withPlayerContext { seekOnQueue(position) }
|
|
27
|
+
|
|
28
|
+
internal fun TrackPlayerCore.seekOnQueue(position: Double) {
|
|
29
|
+
isManuallySeeked = true
|
|
30
|
+
exo.seekTo((position * 1000).toLong())
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
suspend fun TrackPlayerCore.skipToNext() = withPlayerContext { skipToNextOnQueue() }
|
|
34
|
+
|
|
35
|
+
internal fun TrackPlayerCore.skipToNextOnQueue() {
|
|
36
|
+
// The timeline is windowed, so hasNextMediaItem() is not the logical answer —
|
|
37
|
+
// top up first when more tracks remain in currentTracks / the temp queues.
|
|
38
|
+
if (!exo.hasNextMediaItem()) rebuildQueueFromCurrentPosition()
|
|
39
|
+
if (exo.hasNextMediaItem()) {
|
|
40
|
+
exo.seekToNext()
|
|
41
|
+
checkUpcomingTracksForUrls(lookaheadCount)
|
|
26
42
|
}
|
|
43
|
+
}
|
|
27
44
|
|
|
28
|
-
suspend fun TrackPlayerCore.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
45
|
+
suspend fun TrackPlayerCore.skipToPrevious() = withPlayerContext { skipToPreviousOnQueue() }
|
|
46
|
+
|
|
47
|
+
internal fun TrackPlayerCore.skipToPreviousOnQueue() {
|
|
48
|
+
val currentPosition = exo.currentPosition
|
|
49
|
+
when {
|
|
50
|
+
currentPosition > 2000 -> {
|
|
51
|
+
exo.seekTo(0)
|
|
33
52
|
}
|
|
34
|
-
}
|
|
35
53
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
54
|
+
currentTemporaryType != TrackPlayerCore.TemporaryType.NONE -> {
|
|
55
|
+
val trackId = exo.currentMediaItem?.mediaId?.let { extractTrackId(it) }
|
|
56
|
+
if (trackId != null) {
|
|
57
|
+
when (currentTemporaryType) {
|
|
58
|
+
TrackPlayerCore.TemporaryType.PLAY_NEXT -> {
|
|
59
|
+
val idx = playNextStack.indexOfFirst { it.id == trackId }
|
|
60
|
+
if (idx >= 0) playNextStack.removeAt(idx)
|
|
61
|
+
}
|
|
43
62
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
when (currentTemporaryType) {
|
|
48
|
-
TrackPlayerCore.TemporaryType.PLAY_NEXT -> {
|
|
49
|
-
val idx = playNextStack.indexOfFirst { it.id == trackId }
|
|
50
|
-
if (idx >= 0) playNextStack.removeAt(idx)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
TrackPlayerCore.TemporaryType.UP_NEXT -> {
|
|
54
|
-
val idx = upNextQueue.indexOfFirst { it.id == trackId }
|
|
55
|
-
if (idx >= 0) upNextQueue.removeAt(idx)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
else -> {}
|
|
63
|
+
TrackPlayerCore.TemporaryType.UP_NEXT -> {
|
|
64
|
+
val idx = upNextQueue.indexOfFirst { it.id == trackId }
|
|
65
|
+
if (idx >= 0) upNextQueue.removeAt(idx)
|
|
59
66
|
}
|
|
67
|
+
|
|
68
|
+
else -> {}
|
|
60
69
|
}
|
|
61
|
-
currentTemporaryType = TrackPlayerCore.TemporaryType.NONE
|
|
62
|
-
playFromIndexInternal(currentTrackIndex)
|
|
63
70
|
}
|
|
71
|
+
currentTemporaryType = TrackPlayerCore.TemporaryType.NONE
|
|
72
|
+
playFromIndexInternal(currentTrackIndex)
|
|
73
|
+
}
|
|
64
74
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
currentTrackIndex > 0 -> {
|
|
76
|
+
playFromIndexInternal(currentTrackIndex - 1)
|
|
77
|
+
}
|
|
68
78
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
79
|
+
else -> {
|
|
80
|
+
exo.seekTo(0)
|
|
72
81
|
}
|
|
73
|
-
checkUpcomingTracksForUrls(lookaheadCount)
|
|
74
82
|
}
|
|
83
|
+
checkUpcomingTracksForUrls(lookaheadCount)
|
|
84
|
+
}
|
|
75
85
|
|
|
76
|
-
suspend fun TrackPlayerCore.setRepeatMode(mode: RepeatMode) =
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
suspend fun TrackPlayerCore.setRepeatMode(mode: RepeatMode) = withPlayerContext { setRepeatModeOnQueue(mode) }
|
|
87
|
+
|
|
88
|
+
internal fun TrackPlayerCore.setRepeatModeOnQueue(mode: RepeatMode) {
|
|
89
|
+
currentRepeatMode = mode
|
|
90
|
+
exo.setRepeatMode(
|
|
91
|
+
when (mode) {
|
|
92
|
+
RepeatMode.TRACK -> Player.REPEAT_MODE_ONE
|
|
93
|
+
else -> Player.REPEAT_MODE_OFF
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
}
|
|
86
97
|
|
|
87
98
|
fun TrackPlayerCore.getRepeatMode(): RepeatMode = currentRepeatMode
|
|
88
99
|
|
|
89
|
-
suspend fun TrackPlayerCore.setVolume(volume: Double) =
|
|
90
|
-
withPlayerContext {
|
|
91
|
-
val clamped = volume.coerceIn(0.0, 100.0)
|
|
92
|
-
exo.setVolume((clamped / 100.0).toFloat())
|
|
93
|
-
}
|
|
100
|
+
suspend fun TrackPlayerCore.setVolume(volume: Double) = withPlayerContext { setVolumeOnQueue(volume) }
|
|
94
101
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
internal fun TrackPlayerCore.setVolumeOnQueue(volume: Double) {
|
|
103
|
+
val clamped = volume.coerceIn(0.0, 100.0)
|
|
104
|
+
exo.setVolume((clamped / 100.0).toFloat())
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
suspend fun TrackPlayerCore.configure(config: PlayerConfig) = withPlayerContext { configureOnQueue(config) }
|
|
108
|
+
|
|
109
|
+
internal fun TrackPlayerCore.configureOnQueue(config: PlayerConfig) {
|
|
110
|
+
config.androidAutoEnabled?.let { NitroPlayerMediaBrowserService.isAndroidAutoEnabled = it }
|
|
111
|
+
config.lookaheadCount?.let { lookaheadCount = it.toInt() }
|
|
112
|
+
config.androidNotificationIcon?.let { NitroPlayerPlaybackService.notificationSmallIconResName = it }
|
|
113
|
+
mediaSessionManager?.configure(config.androidAutoEnabled, config.carPlayEnabled, config.showInNotification)
|
|
114
|
+
}
|
|
102
115
|
|
|
103
116
|
suspend fun TrackPlayerCore.playSong(
|
|
104
117
|
songId: String,
|
|
@@ -181,11 +194,12 @@ internal fun TrackPlayerCore.emitStateChange(reason: Reason? = null) {
|
|
|
181
194
|
|
|
182
195
|
// ── Playback speed ────────────────────────────────────────────────────────
|
|
183
196
|
|
|
184
|
-
suspend fun TrackPlayerCore.setPlayBackSpeed(speed: Double) =
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
197
|
+
suspend fun TrackPlayerCore.setPlayBackSpeed(speed: Double) = withPlayerContext { setPlayBackSpeedOnQueue(speed) }
|
|
198
|
+
|
|
199
|
+
internal fun TrackPlayerCore.setPlayBackSpeedOnQueue(speed: Double) {
|
|
200
|
+
if (speed <= 0.0) throw IllegalArgumentException("Speed must be greater than 0")
|
|
201
|
+
if (isExoInitialized) exo.setPlaybackSpeed(speed.toFloat())
|
|
202
|
+
}
|
|
189
203
|
|
|
190
204
|
suspend fun TrackPlayerCore.getPlayBackSpeed(): Double =
|
|
191
205
|
withPlayerContext {
|
|
@@ -59,7 +59,9 @@ internal fun TrackPlayerCore.getStateInternal(): PlayerState {
|
|
|
59
59
|
totalDuration = duration,
|
|
60
60
|
currentState = state,
|
|
61
61
|
currentPlaylistId = currentPlaylistId?.let { Variant_NullType_String.create(it) },
|
|
62
|
-
|
|
62
|
+
// Playlist index, not the index inside ExoPlayer's (windowed, rebuilt-from-N)
|
|
63
|
+
// timeline — that reads 0 after any jump. Matches iOS and the documented contract.
|
|
64
|
+
currentIndex = if (currentTrackIndex >= 0) currentTrackIndex.toDouble() else -1.0,
|
|
63
65
|
currentPlayingType = playingType,
|
|
64
66
|
)
|
|
65
67
|
}
|
|
@@ -128,7 +130,7 @@ internal fun TrackPlayerCore.getActualQueueInternal(): List<TrackItem> {
|
|
|
128
130
|
|
|
129
131
|
suspend fun TrackPlayerCore.skipToIndex(index: Int): Boolean = withPlayerContext { skipToIndexInternal(index) }
|
|
130
132
|
|
|
131
|
-
|
|
133
|
+
internal fun TrackPlayerCore.skipToIndexInternal(index: Int): Boolean {
|
|
132
134
|
if (!isExoInitialized) return false
|
|
133
135
|
val actualQueue = getActualQueueInternal()
|
|
134
136
|
if (index < 0 || index >= actualQueue.size) return false
|