@roitium/expo-orpheus 0.5.1 → 0.6.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/README.md
CHANGED
|
@@ -15,12 +15,16 @@ Orpheus 通过特殊的 uri 识别来自 bilibili 的资源,格式为 `orpheus
|
|
|
15
15
|
Orpheus 内部有两层缓存:
|
|
16
16
|
|
|
17
17
|
1. 用户手动下载的缓存
|
|
18
|
-
2. 边下边播:LRU 缓存,256mb
|
|
18
|
+
2. ~~ 边下边播:LRU 缓存,256mb ~~ (移除了这一层缓存,方便响度均衡实现)
|
|
19
19
|
|
|
20
20
|
## 下载系统
|
|
21
21
|
|
|
22
22
|
Orpheus 集成了 Media3 的 DownloadManager,抛弃了原先 BBPlayer 中繁琐的下载实现。
|
|
23
23
|
|
|
24
|
+
## 响度均衡
|
|
25
|
+
|
|
26
|
+
默认启用,只对未缓存的 b 站音频生效
|
|
27
|
+
|
|
24
28
|
## 使用
|
|
25
29
|
|
|
26
30
|
虽然该包是公开的,但仍然主要供 BBPlayer 内部使用。可能不会有完整的文档覆盖。我们欢迎你 fork 后自行修改使用。
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
package expo.modules.orpheus
|
|
2
2
|
|
|
3
3
|
import android.content.ComponentName
|
|
4
|
-
import android.os.Bundle
|
|
5
4
|
import android.os.Handler
|
|
6
5
|
import android.os.Looper
|
|
7
6
|
import android.util.Log
|
|
@@ -18,13 +17,9 @@ import androidx.media3.exoplayer.offline.DownloadManager
|
|
|
18
17
|
import androidx.media3.exoplayer.offline.DownloadRequest
|
|
19
18
|
import androidx.media3.exoplayer.offline.DownloadService
|
|
20
19
|
import androidx.media3.session.MediaController
|
|
21
|
-
import androidx.media3.session.SessionCommand
|
|
22
|
-
import androidx.media3.session.SessionResult
|
|
23
20
|
import androidx.media3.session.SessionToken
|
|
24
21
|
import com.google.common.util.concurrent.ListenableFuture
|
|
25
|
-
import com.google.common.util.concurrent.MoreExecutors
|
|
26
22
|
import com.google.gson.Gson
|
|
27
|
-
import expo.modules.kotlin.exception.CodedException
|
|
28
23
|
import expo.modules.kotlin.functions.Queues
|
|
29
24
|
import expo.modules.kotlin.modules.Module
|
|
30
25
|
import expo.modules.kotlin.modules.ModuleDefinition
|
|
@@ -35,9 +30,10 @@ import expo.modules.orpheus.utils.toMediaItem
|
|
|
35
30
|
|
|
36
31
|
@UnstableApi
|
|
37
32
|
class ExpoOrpheusModule : Module() {
|
|
33
|
+
// keep this controller only to make sure MediaLibraryService is init.
|
|
38
34
|
private var controllerFuture: ListenableFuture<MediaController>? = null
|
|
39
35
|
|
|
40
|
-
private var
|
|
36
|
+
private var player: Player? = null
|
|
41
37
|
|
|
42
38
|
private val mainHandler = Handler(Looper.getMainLooper())
|
|
43
39
|
|
|
@@ -51,6 +47,124 @@ class ExpoOrpheusModule : Module() {
|
|
|
51
47
|
|
|
52
48
|
val gson = Gson()
|
|
53
49
|
|
|
50
|
+
private val playerListener = object : Player.Listener {
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 核心:处理切歌、播放结束逻辑
|
|
54
|
+
*/
|
|
55
|
+
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
|
56
|
+
val newId = mediaItem?.mediaId ?: ""
|
|
57
|
+
Log.e("Orpheus", "onMediaItemTransition: $reason")
|
|
58
|
+
|
|
59
|
+
sendEvent(
|
|
60
|
+
"onTrackStarted", mapOf(
|
|
61
|
+
"trackId" to newId,
|
|
62
|
+
"reason" to reason
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
lastMediaId = newId
|
|
67
|
+
saveCurrentPosition()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
|
|
71
|
+
val p = player ?: return
|
|
72
|
+
val currentItem = p.currentMediaItem ?: return
|
|
73
|
+
val mediaId = currentItem.mediaId
|
|
74
|
+
|
|
75
|
+
val duration = p.duration
|
|
76
|
+
Log.d(
|
|
77
|
+
"Orpheus",
|
|
78
|
+
"onTimelineChanged: reason: $reason mediaId: $mediaId duration: $duration"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if (duration != C.TIME_UNSET && duration > 0) {
|
|
82
|
+
durationCache[mediaId] = duration
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
override fun onPositionDiscontinuity(
|
|
87
|
+
oldPosition: Player.PositionInfo,
|
|
88
|
+
newPosition: Player.PositionInfo,
|
|
89
|
+
reason: Int
|
|
90
|
+
) {
|
|
91
|
+
val isAutoTransition = reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION
|
|
92
|
+
val isIndexChanged = oldPosition.mediaItemIndex != newPosition.mediaItemIndex
|
|
93
|
+
val lastMediaItem = oldPosition.mediaItem ?: return
|
|
94
|
+
val currentTime = System.currentTimeMillis()
|
|
95
|
+
if ((currentTime - lastTrackFinishedAt) < 200) {
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
Log.d(
|
|
100
|
+
"Orpheus",
|
|
101
|
+
"onPositionDiscontinuity: isAutoTransition:$isAutoTransition isIndexChanged: $isIndexChanged durationCache:$durationCache"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if (isAutoTransition || isIndexChanged) {
|
|
105
|
+
|
|
106
|
+
val duration = durationCache[lastMediaItem.mediaId] ?: return
|
|
107
|
+
lastTrackFinishedAt = currentTime
|
|
108
|
+
|
|
109
|
+
sendEvent(
|
|
110
|
+
"onTrackFinished", mapOf(
|
|
111
|
+
"trackId" to lastMediaItem.mediaId,
|
|
112
|
+
"finalPosition" to oldPosition.positionMs / 1000.0,
|
|
113
|
+
"duration" to duration / 1000.0,
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 处理播放状态改变
|
|
121
|
+
*/
|
|
122
|
+
override fun onPlaybackStateChanged(state: Int) {
|
|
123
|
+
// state: 1=IDLE, 2=BUFFERING, 3=READY, 4=ENDED
|
|
124
|
+
sendEvent(
|
|
125
|
+
"onPlaybackStateChanged", mapOf(
|
|
126
|
+
"state" to state
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
updateProgressRunnerState()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 处理播放/暂停状态
|
|
135
|
+
*/
|
|
136
|
+
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
|
137
|
+
sendEvent(
|
|
138
|
+
"onIsPlayingChanged", mapOf(
|
|
139
|
+
"status" to isPlaying
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
updateProgressRunnerState()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 处理错误
|
|
147
|
+
*/
|
|
148
|
+
override fun onPlayerError(error: PlaybackException) {
|
|
149
|
+
sendEvent(
|
|
150
|
+
"onPlayerError", mapOf(
|
|
151
|
+
"code" to error.errorCode.toString(),
|
|
152
|
+
"message" to (error.message ?: "Unknown Error")
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
override fun onRepeatModeChanged(repeatMode: Int) {
|
|
158
|
+
super.onRepeatModeChanged(repeatMode)
|
|
159
|
+
Storage.saveRepeatMode(repeatMode)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
|
|
163
|
+
super.onShuffleModeEnabledChanged(shuffleModeEnabled)
|
|
164
|
+
Storage.saveShuffleMode(shuffleModeEnabled)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
54
168
|
@OptIn(UnstableApi::class)
|
|
55
169
|
override fun definition() = ModuleDefinition {
|
|
56
170
|
Name("Orpheus")
|
|
@@ -75,14 +189,16 @@ class ExpoOrpheusModule : Module() {
|
|
|
75
189
|
controllerFuture = MediaController.Builder(context, sessionToken)
|
|
76
190
|
.setApplicationLooper(Looper.getMainLooper()).buildAsync()
|
|
77
191
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
192
|
+
|
|
193
|
+
OrpheusMusicService.addOnServiceReadyListener { service ->
|
|
194
|
+
mainHandler.post {
|
|
195
|
+
if (this@ExpoOrpheusModule.player != service.player) {
|
|
196
|
+
this@ExpoOrpheusModule.player?.removeListener(playerListener)
|
|
197
|
+
this@ExpoOrpheusModule.player = service.player
|
|
198
|
+
this@ExpoOrpheusModule.player?.addListener(playerListener)
|
|
199
|
+
}
|
|
84
200
|
}
|
|
85
|
-
}
|
|
201
|
+
}
|
|
86
202
|
|
|
87
203
|
downloadManager = DownloadUtil.getDownloadManager(context)
|
|
88
204
|
downloadManager?.addListener(downloadListener)
|
|
@@ -94,6 +210,10 @@ class ExpoOrpheusModule : Module() {
|
|
|
94
210
|
mainHandler.removeCallbacks(downloadProgressRunnable)
|
|
95
211
|
controllerFuture?.let { MediaController.releaseFuture(it) }
|
|
96
212
|
downloadManager?.removeListener(downloadListener)
|
|
213
|
+
player?.removeListener(playerListener)
|
|
214
|
+
OrpheusMusicService.removeOnServiceReadyListener { }
|
|
215
|
+
player = null
|
|
216
|
+
Log.d("Orpheus", "Destroy media controller")
|
|
97
217
|
}
|
|
98
218
|
|
|
99
219
|
Constant("restorePlaybackPositionEnabled") {
|
|
@@ -117,136 +237,136 @@ class ExpoOrpheusModule : Module() {
|
|
|
117
237
|
}
|
|
118
238
|
|
|
119
239
|
AsyncFunction("getPosition") {
|
|
120
|
-
|
|
121
|
-
|
|
240
|
+
checkPlayer()
|
|
241
|
+
player?.currentPosition?.toDouble()?.div(1000.0) ?: 0.0
|
|
122
242
|
}.runOnQueue(Queues.MAIN)
|
|
123
243
|
|
|
124
244
|
AsyncFunction("getDuration") {
|
|
125
|
-
|
|
126
|
-
val d =
|
|
245
|
+
checkPlayer()
|
|
246
|
+
val d = player?.duration ?: C.TIME_UNSET
|
|
127
247
|
if (d == C.TIME_UNSET) 0.0 else d.toDouble() / 1000.0
|
|
128
248
|
}.runOnQueue(Queues.MAIN)
|
|
129
249
|
|
|
130
250
|
AsyncFunction("getBuffered") {
|
|
131
|
-
|
|
132
|
-
|
|
251
|
+
checkPlayer()
|
|
252
|
+
player?.bufferedPosition?.toDouble()?.div(1000.0) ?: 0.0
|
|
133
253
|
}.runOnQueue(Queues.MAIN)
|
|
134
254
|
|
|
135
255
|
AsyncFunction("getIsPlaying") {
|
|
136
|
-
|
|
137
|
-
|
|
256
|
+
checkPlayer()
|
|
257
|
+
player?.isPlaying ?: false
|
|
138
258
|
}.runOnQueue(Queues.MAIN)
|
|
139
259
|
|
|
140
260
|
AsyncFunction("getCurrentIndex") {
|
|
141
|
-
|
|
142
|
-
|
|
261
|
+
checkPlayer()
|
|
262
|
+
player?.currentMediaItemIndex ?: -1
|
|
143
263
|
}.runOnQueue(Queues.MAIN)
|
|
144
264
|
|
|
145
265
|
AsyncFunction("getCurrentTrack") {
|
|
146
|
-
|
|
147
|
-
val
|
|
148
|
-
val currentItem =
|
|
266
|
+
checkPlayer()
|
|
267
|
+
val p = player ?: return@AsyncFunction null
|
|
268
|
+
val currentItem = p.currentMediaItem ?: return@AsyncFunction null
|
|
149
269
|
|
|
150
270
|
mediaItemToTrackRecord(currentItem)
|
|
151
271
|
}.runOnQueue(Queues.MAIN)
|
|
152
272
|
|
|
153
273
|
AsyncFunction("getShuffleMode") {
|
|
154
|
-
|
|
155
|
-
|
|
274
|
+
checkPlayer()
|
|
275
|
+
player?.shuffleModeEnabled
|
|
156
276
|
}.runOnQueue(Queues.MAIN)
|
|
157
277
|
|
|
158
278
|
AsyncFunction("getIndexTrack") { index: Int ->
|
|
159
|
-
|
|
160
|
-
val
|
|
279
|
+
checkPlayer()
|
|
280
|
+
val p = player ?: return@AsyncFunction null
|
|
161
281
|
|
|
162
|
-
if (index < 0 || index >=
|
|
282
|
+
if (index < 0 || index >= p.mediaItemCount) {
|
|
163
283
|
return@AsyncFunction null
|
|
164
284
|
}
|
|
165
285
|
|
|
166
|
-
val item =
|
|
286
|
+
val item = p.getMediaItemAt(index)
|
|
167
287
|
|
|
168
288
|
mediaItemToTrackRecord(item)
|
|
169
289
|
}.runOnQueue(Queues.MAIN)
|
|
170
290
|
|
|
171
291
|
AsyncFunction("play") {
|
|
172
|
-
|
|
173
|
-
|
|
292
|
+
checkPlayer()
|
|
293
|
+
player?.play()
|
|
174
294
|
}.runOnQueue(Queues.MAIN)
|
|
175
295
|
|
|
176
296
|
AsyncFunction("pause") {
|
|
177
|
-
|
|
178
|
-
|
|
297
|
+
checkPlayer()
|
|
298
|
+
player?.pause()
|
|
179
299
|
}.runOnQueue(Queues.MAIN)
|
|
180
300
|
|
|
181
301
|
AsyncFunction("clear") {
|
|
182
|
-
|
|
183
|
-
|
|
302
|
+
checkPlayer()
|
|
303
|
+
player?.clearMediaItems()
|
|
184
304
|
durationCache.clear()
|
|
185
305
|
DownloadUtil.itemVolumeMap.clear()
|
|
186
306
|
}.runOnQueue(Queues.MAIN)
|
|
187
307
|
|
|
188
308
|
AsyncFunction("skipTo") { index: Int ->
|
|
189
309
|
// 跳转到指定索引的开头
|
|
190
|
-
|
|
191
|
-
|
|
310
|
+
checkPlayer()
|
|
311
|
+
player?.seekTo(index, C.TIME_UNSET)
|
|
192
312
|
}.runOnQueue(Queues.MAIN)
|
|
193
313
|
|
|
194
314
|
AsyncFunction("skipToNext") {
|
|
195
|
-
|
|
196
|
-
if (
|
|
197
|
-
|
|
315
|
+
checkPlayer()
|
|
316
|
+
if (player?.hasNextMediaItem() == true) {
|
|
317
|
+
player?.seekToNextMediaItem()
|
|
198
318
|
}
|
|
199
319
|
}.runOnQueue(Queues.MAIN)
|
|
200
320
|
|
|
201
321
|
AsyncFunction("skipToPrevious") {
|
|
202
|
-
|
|
203
|
-
if (
|
|
204
|
-
|
|
322
|
+
checkPlayer()
|
|
323
|
+
if (player?.hasPreviousMediaItem() == true) {
|
|
324
|
+
player?.seekToPreviousMediaItem()
|
|
205
325
|
}
|
|
206
326
|
}.runOnQueue(Queues.MAIN)
|
|
207
327
|
|
|
208
328
|
AsyncFunction("seekTo") { seconds: Double ->
|
|
209
|
-
|
|
329
|
+
checkPlayer()
|
|
210
330
|
val ms = (seconds * 1000).toLong()
|
|
211
|
-
|
|
331
|
+
player?.seekTo(ms)
|
|
212
332
|
}.runOnQueue(Queues.MAIN)
|
|
213
333
|
|
|
214
334
|
AsyncFunction("setRepeatMode") { mode: Int ->
|
|
215
|
-
|
|
335
|
+
checkPlayer()
|
|
216
336
|
// mode: 0=OFF, 1=TRACK, 2=QUEUE
|
|
217
337
|
val repeatMode = when (mode) {
|
|
218
338
|
1 -> Player.REPEAT_MODE_ONE
|
|
219
339
|
2 -> Player.REPEAT_MODE_ALL
|
|
220
340
|
else -> Player.REPEAT_MODE_OFF
|
|
221
341
|
}
|
|
222
|
-
|
|
342
|
+
player?.repeatMode = repeatMode
|
|
223
343
|
}.runOnQueue(Queues.MAIN)
|
|
224
344
|
|
|
225
345
|
AsyncFunction("setShuffleMode") { enabled: Boolean ->
|
|
226
|
-
|
|
227
|
-
|
|
346
|
+
checkPlayer()
|
|
347
|
+
player?.shuffleModeEnabled = enabled
|
|
228
348
|
}.runOnQueue(Queues.MAIN)
|
|
229
349
|
|
|
230
350
|
AsyncFunction("getRepeatMode") {
|
|
231
|
-
|
|
232
|
-
|
|
351
|
+
checkPlayer()
|
|
352
|
+
player?.repeatMode
|
|
233
353
|
}.runOnQueue(Queues.MAIN)
|
|
234
354
|
|
|
235
355
|
AsyncFunction("removeTrack") { index: Int ->
|
|
236
|
-
|
|
237
|
-
if (index >= 0 && index < (
|
|
238
|
-
|
|
356
|
+
checkPlayer()
|
|
357
|
+
if (index >= 0 && index < (player?.mediaItemCount ?: 0)) {
|
|
358
|
+
player?.removeMediaItem(index)
|
|
239
359
|
}
|
|
240
360
|
}.runOnQueue(Queues.MAIN)
|
|
241
361
|
|
|
242
362
|
AsyncFunction("getQueue") {
|
|
243
|
-
|
|
244
|
-
val
|
|
245
|
-
val count =
|
|
363
|
+
checkPlayer()
|
|
364
|
+
val p = player ?: return@AsyncFunction emptyList<TrackRecord>()
|
|
365
|
+
val count = p.mediaItemCount
|
|
246
366
|
val queue = ArrayList<TrackRecord>(count)
|
|
247
367
|
|
|
248
368
|
for (i in 0 until count) {
|
|
249
|
-
val item =
|
|
369
|
+
val item = p.getMediaItemAt(i)
|
|
250
370
|
queue.add(mediaItemToTrackRecord(item))
|
|
251
371
|
}
|
|
252
372
|
|
|
@@ -254,59 +374,32 @@ class ExpoOrpheusModule : Module() {
|
|
|
254
374
|
}.runOnQueue(Queues.MAIN)
|
|
255
375
|
|
|
256
376
|
AsyncFunction("setSleepTimer") { durationMs: Long ->
|
|
257
|
-
|
|
258
|
-
val command = SessionCommand(CustomCommands.CMD_START_TIMER, Bundle.EMPTY)
|
|
259
|
-
val args = Bundle().apply {
|
|
260
|
-
putLong(CustomCommands.KEY_DURATION, durationMs)
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
controller?.sendCustomCommand(command, args)
|
|
377
|
+
OrpheusMusicService.instance?.startSleepTimer(durationMs)
|
|
264
378
|
return@AsyncFunction null
|
|
265
379
|
}.runOnQueue(Queues.MAIN)
|
|
266
380
|
|
|
267
381
|
AsyncFunction("getSleepTimerEndTime") {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
val command = SessionCommand(CustomCommands.CMD_GET_REMAINING, Bundle.EMPTY)
|
|
271
|
-
val future = controller!!.sendCustomCommand(command, Bundle.EMPTY)
|
|
272
|
-
|
|
273
|
-
val result = try {
|
|
274
|
-
future.get()
|
|
275
|
-
} catch (e: Exception) {
|
|
276
|
-
throw CodedException("ERR_EXECUTION_FAILED", e.message, e)
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (result.resultCode == SessionResult.RESULT_SUCCESS) {
|
|
280
|
-
val extras = result.extras
|
|
281
|
-
if (extras.containsKey(CustomCommands.KEY_STOP_TIME)) {
|
|
282
|
-
val stopTime = extras.getLong(CustomCommands.KEY_STOP_TIME)
|
|
283
|
-
return@AsyncFunction stopTime
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return@AsyncFunction null
|
|
382
|
+
return@AsyncFunction OrpheusMusicService.instance?.getSleepTimerRemaining()
|
|
288
383
|
}.runOnQueue(Queues.MAIN)
|
|
289
384
|
|
|
290
385
|
AsyncFunction("cancelSleepTimer") {
|
|
291
|
-
|
|
292
|
-
val command = SessionCommand(CustomCommands.CMD_CANCEL_TIMER, Bundle.EMPTY)
|
|
293
|
-
controller?.sendCustomCommand(command, Bundle.EMPTY)
|
|
386
|
+
OrpheusMusicService.instance?.cancelSleepTimer()
|
|
294
387
|
return@AsyncFunction null
|
|
295
388
|
}.runOnQueue(Queues.MAIN)
|
|
296
389
|
|
|
297
390
|
AsyncFunction("addToEnd") { tracks: List<TrackRecord>, startFromId: String?, clearQueue: Boolean? ->
|
|
298
|
-
|
|
391
|
+
checkPlayer()
|
|
299
392
|
val mediaItems = tracks.map { track ->
|
|
300
393
|
track.toMediaItem(gson)
|
|
301
394
|
}
|
|
302
|
-
val
|
|
395
|
+
val p = player ?: return@AsyncFunction
|
|
303
396
|
if (clearQueue == true) {
|
|
304
|
-
|
|
397
|
+
p.clearMediaItems()
|
|
305
398
|
durationCache.clear()
|
|
306
399
|
DownloadUtil.itemVolumeMap.clear()
|
|
307
400
|
}
|
|
308
|
-
val initialSize =
|
|
309
|
-
|
|
401
|
+
val initialSize = p.mediaItemCount
|
|
402
|
+
p.addMediaItems(mediaItems)
|
|
310
403
|
|
|
311
404
|
if (!startFromId.isNullOrEmpty()) {
|
|
312
405
|
val relativeIndex = tracks.indexOfFirst { it.id == startFromId }
|
|
@@ -314,50 +407,50 @@ class ExpoOrpheusModule : Module() {
|
|
|
314
407
|
if (relativeIndex != -1) {
|
|
315
408
|
val targetIndex = initialSize + relativeIndex
|
|
316
409
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
410
|
+
p.seekTo(targetIndex, C.TIME_UNSET)
|
|
411
|
+
p.prepare()
|
|
412
|
+
p.play()
|
|
320
413
|
|
|
321
414
|
return@AsyncFunction
|
|
322
415
|
}
|
|
323
416
|
}
|
|
324
417
|
|
|
325
|
-
if (
|
|
326
|
-
|
|
418
|
+
if (p.playbackState == Player.STATE_IDLE) {
|
|
419
|
+
p.prepare()
|
|
327
420
|
}
|
|
328
421
|
}.runOnQueue(Queues.MAIN)
|
|
329
422
|
|
|
330
423
|
AsyncFunction("playNext") { track: TrackRecord ->
|
|
331
|
-
|
|
332
|
-
val
|
|
424
|
+
checkPlayer()
|
|
425
|
+
val p = player ?: return@AsyncFunction
|
|
333
426
|
|
|
334
427
|
val mediaItem = track.toMediaItem(gson)
|
|
335
|
-
val targetIndex =
|
|
428
|
+
val targetIndex = p.currentMediaItemIndex + 1
|
|
336
429
|
|
|
337
430
|
var existingIndex = -1
|
|
338
|
-
for (i in 0 until
|
|
339
|
-
if (
|
|
431
|
+
for (i in 0 until p.mediaItemCount) {
|
|
432
|
+
if (p.getMediaItemAt(i).mediaId == track.id) {
|
|
340
433
|
existingIndex = i
|
|
341
434
|
break
|
|
342
435
|
}
|
|
343
436
|
}
|
|
344
437
|
|
|
345
438
|
if (existingIndex != -1) {
|
|
346
|
-
if (existingIndex ==
|
|
439
|
+
if (existingIndex == p.currentMediaItemIndex) {
|
|
347
440
|
return@AsyncFunction
|
|
348
441
|
}
|
|
349
|
-
val safeTargetIndex = targetIndex.coerceAtMost(
|
|
442
|
+
val safeTargetIndex = targetIndex.coerceAtMost(p.mediaItemCount)
|
|
350
443
|
|
|
351
|
-
|
|
444
|
+
p.moveMediaItem(existingIndex, safeTargetIndex)
|
|
352
445
|
|
|
353
446
|
} else {
|
|
354
|
-
val safeTargetIndex = targetIndex.coerceAtMost(
|
|
447
|
+
val safeTargetIndex = targetIndex.coerceAtMost(p.mediaItemCount)
|
|
355
448
|
|
|
356
|
-
|
|
449
|
+
p.addMediaItem(safeTargetIndex, mediaItem)
|
|
357
450
|
}
|
|
358
451
|
|
|
359
|
-
if (
|
|
360
|
-
|
|
452
|
+
if (p.playbackState == Player.STATE_IDLE) {
|
|
453
|
+
p.prepare()
|
|
361
454
|
}
|
|
362
455
|
}.runOnQueue(Queues.MAIN)
|
|
363
456
|
|
|
@@ -554,139 +647,19 @@ class ExpoOrpheusModule : Module() {
|
|
|
554
647
|
}
|
|
555
648
|
}
|
|
556
649
|
|
|
557
|
-
private fun setupListeners() {
|
|
558
|
-
controller?.addListener(object : Player.Listener {
|
|
559
|
-
|
|
560
|
-
/**
|
|
561
|
-
* 核心:处理切歌、播放结束逻辑
|
|
562
|
-
*/
|
|
563
|
-
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
|
564
|
-
val newId = mediaItem?.mediaId ?: ""
|
|
565
|
-
Log.e("Orpheus", "onMediaItemTransition: $reason")
|
|
566
|
-
|
|
567
|
-
sendEvent(
|
|
568
|
-
"onTrackStarted", mapOf(
|
|
569
|
-
"trackId" to newId,
|
|
570
|
-
"reason" to reason
|
|
571
|
-
)
|
|
572
|
-
)
|
|
573
|
-
|
|
574
|
-
lastMediaId = newId
|
|
575
|
-
saveCurrentPosition()
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
|
|
579
|
-
val player = controller ?: return
|
|
580
|
-
val currentItem = player.currentMediaItem ?: return
|
|
581
|
-
val mediaId = currentItem.mediaId
|
|
582
|
-
|
|
583
|
-
val duration = player.duration
|
|
584
|
-
Log.d(
|
|
585
|
-
"Orpheus",
|
|
586
|
-
"onTimelineChanged: reason: $reason mediaId: $mediaId duration: $duration"
|
|
587
|
-
)
|
|
588
|
-
|
|
589
|
-
if (duration != C.TIME_UNSET && duration > 0) {
|
|
590
|
-
durationCache[mediaId] = duration
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
override fun onPositionDiscontinuity(
|
|
595
|
-
oldPosition: Player.PositionInfo,
|
|
596
|
-
newPosition: Player.PositionInfo,
|
|
597
|
-
reason: Int
|
|
598
|
-
) {
|
|
599
|
-
val isAutoTransition = reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION
|
|
600
|
-
val isIndexChanged = oldPosition.mediaItemIndex != newPosition.mediaItemIndex
|
|
601
|
-
val lastMediaItem = oldPosition.mediaItem ?: return
|
|
602
|
-
val currentTime = System.currentTimeMillis()
|
|
603
|
-
if ((currentTime - lastTrackFinishedAt) < 200) {
|
|
604
|
-
return
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
Log.d(
|
|
608
|
-
"Orpheus",
|
|
609
|
-
"onPositionDiscontinuity: isAutoTransition:$isAutoTransition isIndexChanged: $isIndexChanged durationCache:$durationCache"
|
|
610
|
-
)
|
|
611
|
-
|
|
612
|
-
if (isAutoTransition || isIndexChanged) {
|
|
613
|
-
|
|
614
|
-
val duration = durationCache[lastMediaItem.mediaId] ?: return
|
|
615
|
-
lastTrackFinishedAt = currentTime
|
|
616
|
-
|
|
617
|
-
sendEvent(
|
|
618
|
-
"onTrackFinished", mapOf(
|
|
619
|
-
"trackId" to lastMediaItem.mediaId,
|
|
620
|
-
"finalPosition" to oldPosition.positionMs / 1000.0,
|
|
621
|
-
"duration" to duration / 1000.0,
|
|
622
|
-
)
|
|
623
|
-
)
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
/**
|
|
628
|
-
* 处理播放状态改变
|
|
629
|
-
*/
|
|
630
|
-
override fun onPlaybackStateChanged(state: Int) {
|
|
631
|
-
// state: 1=IDLE, 2=BUFFERING, 3=READY, 4=ENDED
|
|
632
|
-
sendEvent(
|
|
633
|
-
"onPlaybackStateChanged", mapOf(
|
|
634
|
-
"state" to state
|
|
635
|
-
)
|
|
636
|
-
)
|
|
637
|
-
|
|
638
|
-
updateProgressRunnerState()
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
/**
|
|
642
|
-
* 处理播放/暂停状态
|
|
643
|
-
*/
|
|
644
|
-
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
|
645
|
-
sendEvent(
|
|
646
|
-
"onIsPlayingChanged", mapOf(
|
|
647
|
-
"status" to isPlaying
|
|
648
|
-
)
|
|
649
|
-
)
|
|
650
|
-
updateProgressRunnerState()
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
/**
|
|
654
|
-
* 处理错误
|
|
655
|
-
*/
|
|
656
|
-
override fun onPlayerError(error: PlaybackException) {
|
|
657
|
-
sendEvent(
|
|
658
|
-
"onPlayerError", mapOf(
|
|
659
|
-
"code" to error.errorCode.toString(),
|
|
660
|
-
"message" to (error.message ?: "Unknown Error")
|
|
661
|
-
)
|
|
662
|
-
)
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
override fun onRepeatModeChanged(repeatMode: Int) {
|
|
666
|
-
super.onRepeatModeChanged(repeatMode)
|
|
667
|
-
Storage.saveRepeatMode(repeatMode)
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
|
|
671
|
-
super.onShuffleModeEnabledChanged(shuffleModeEnabled)
|
|
672
|
-
Storage.saveShuffleMode(shuffleModeEnabled)
|
|
673
|
-
}
|
|
674
|
-
})
|
|
675
|
-
}
|
|
676
|
-
|
|
677
650
|
private val progressSendEventRunnable = object : Runnable {
|
|
678
651
|
override fun run() {
|
|
679
|
-
val
|
|
652
|
+
val p = player ?: return
|
|
680
653
|
|
|
681
|
-
if (
|
|
682
|
-
val currentMs =
|
|
683
|
-
val durationMs =
|
|
654
|
+
if (p.isPlaying) {
|
|
655
|
+
val currentMs = p.currentPosition
|
|
656
|
+
val durationMs = p.duration
|
|
684
657
|
|
|
685
658
|
sendEvent(
|
|
686
659
|
"onPositionUpdate", mapOf(
|
|
687
660
|
"position" to currentMs / 1000.0,
|
|
688
661
|
"duration" to if (durationMs == C.TIME_UNSET) 0.0 else durationMs / 1000.0,
|
|
689
|
-
"buffered" to
|
|
662
|
+
"buffered" to p.bufferedPosition / 1000.0
|
|
690
663
|
)
|
|
691
664
|
)
|
|
692
665
|
}
|
|
@@ -703,9 +676,9 @@ class ExpoOrpheusModule : Module() {
|
|
|
703
676
|
}
|
|
704
677
|
|
|
705
678
|
private fun updateProgressRunnerState() {
|
|
706
|
-
val
|
|
679
|
+
val p = player
|
|
707
680
|
// 如果正在播放且状态是 READY,则开始轮询
|
|
708
|
-
if (
|
|
681
|
+
if (p != null && p.isPlaying && p.playbackState == Player.STATE_READY) {
|
|
709
682
|
mainHandler.removeCallbacks(progressSendEventRunnable)
|
|
710
683
|
mainHandler.removeCallbacks(progressSaveRunnable)
|
|
711
684
|
mainHandler.post(progressSaveRunnable)
|
|
@@ -739,17 +712,17 @@ class ExpoOrpheusModule : Module() {
|
|
|
739
712
|
}
|
|
740
713
|
|
|
741
714
|
private fun saveCurrentPosition() {
|
|
742
|
-
val
|
|
743
|
-
if (
|
|
715
|
+
val p = player ?: return
|
|
716
|
+
if (p.playbackState != Player.STATE_IDLE) {
|
|
744
717
|
Storage.savePosition(
|
|
745
|
-
|
|
746
|
-
|
|
718
|
+
p.currentMediaItemIndex,
|
|
719
|
+
p.currentPosition
|
|
747
720
|
)
|
|
748
721
|
}
|
|
749
722
|
}
|
|
750
723
|
|
|
751
|
-
private fun
|
|
752
|
-
if (
|
|
724
|
+
private fun checkPlayer() {
|
|
725
|
+
if (player == null) {
|
|
753
726
|
throw ControllerNotInitializedException()
|
|
754
727
|
}
|
|
755
728
|
}
|
|
@@ -2,21 +2,17 @@ package expo.modules.orpheus
|
|
|
2
2
|
|
|
3
3
|
import android.app.PendingIntent
|
|
4
4
|
import android.content.Intent
|
|
5
|
-
import android.os.Bundle
|
|
6
5
|
import androidx.annotation.OptIn
|
|
7
6
|
import androidx.core.net.toUri
|
|
8
7
|
import androidx.media3.common.AudioAttributes
|
|
9
8
|
import androidx.media3.common.C
|
|
10
9
|
import androidx.media3.common.Player
|
|
11
10
|
import androidx.media3.common.Timeline
|
|
12
|
-
import androidx.media3.common.util.Log
|
|
13
11
|
import androidx.media3.common.util.UnstableApi
|
|
14
12
|
import androidx.media3.exoplayer.ExoPlayer
|
|
15
13
|
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
|
16
14
|
import androidx.media3.session.MediaLibraryService
|
|
17
15
|
import androidx.media3.session.MediaSession
|
|
18
|
-
import androidx.media3.session.SessionCommand
|
|
19
|
-
import androidx.media3.session.SessionResult
|
|
20
16
|
import com.google.common.util.concurrent.Futures
|
|
21
17
|
import com.google.common.util.concurrent.ListenableFuture
|
|
22
18
|
import expo.modules.orpheus.bilibili.VolumeData
|
|
@@ -33,15 +29,37 @@ import kotlin.math.abs
|
|
|
33
29
|
|
|
34
30
|
class OrpheusMusicService : MediaLibraryService() {
|
|
35
31
|
|
|
36
|
-
|
|
32
|
+
var player: ExoPlayer? = null
|
|
37
33
|
private var mediaSession: MediaLibrarySession? = null
|
|
38
34
|
private var sleepTimerManager: SleepTimeController? = null
|
|
39
35
|
private var volumeFadeJob: Job? = null
|
|
40
36
|
private var scope = MainScope()
|
|
41
37
|
|
|
38
|
+
companion object {
|
|
39
|
+
var instance: OrpheusMusicService? = null
|
|
40
|
+
private set(value) {
|
|
41
|
+
field = value
|
|
42
|
+
if (value != null) {
|
|
43
|
+
listeners.forEach { it(value) }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private val listeners = mutableListOf<(OrpheusMusicService) -> Unit>()
|
|
48
|
+
|
|
49
|
+
fun addOnServiceReadyListener(listener: (OrpheusMusicService) -> Unit) {
|
|
50
|
+
instance?.let { listener(it) }
|
|
51
|
+
listeners.add(listener)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fun removeOnServiceReadyListener(listener: (OrpheusMusicService) -> Unit) {
|
|
55
|
+
listeners.remove(listener)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
42
59
|
@OptIn(UnstableApi::class)
|
|
43
60
|
override fun onCreate() {
|
|
44
61
|
super.onCreate()
|
|
62
|
+
instance = this
|
|
45
63
|
|
|
46
64
|
Storage.initialize(this)
|
|
47
65
|
|
|
@@ -113,6 +131,7 @@ class OrpheusMusicService : MediaLibraryService() {
|
|
|
113
131
|
|
|
114
132
|
override fun onDestroy() {
|
|
115
133
|
scope.cancel()
|
|
134
|
+
instance = null
|
|
116
135
|
|
|
117
136
|
mediaSession?.run {
|
|
118
137
|
player.release()
|
|
@@ -122,71 +141,30 @@ class OrpheusMusicService : MediaLibraryService() {
|
|
|
122
141
|
super.onDestroy()
|
|
123
142
|
}
|
|
124
143
|
|
|
144
|
+
fun startSleepTimer(durationMs: Long) {
|
|
145
|
+
sleepTimerManager?.start(durationMs)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fun cancelSleepTimer() {
|
|
149
|
+
sleepTimerManager?.cancel()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
fun getSleepTimerRemaining(): Long? {
|
|
153
|
+
return sleepTimerManager?.getStopTimeMs()
|
|
154
|
+
}
|
|
155
|
+
|
|
125
156
|
var callback: MediaLibrarySession.Callback = @UnstableApi
|
|
126
157
|
object : MediaLibrarySession.Callback {
|
|
127
|
-
private val customCommands = listOf(
|
|
128
|
-
SessionCommand(CustomCommands.CMD_START_TIMER, Bundle.EMPTY),
|
|
129
|
-
SessionCommand(CustomCommands.CMD_CANCEL_TIMER, Bundle.EMPTY),
|
|
130
|
-
SessionCommand(CustomCommands.CMD_GET_REMAINING, Bundle.EMPTY)
|
|
131
|
-
)
|
|
132
158
|
|
|
133
159
|
@OptIn(UnstableApi::class)
|
|
134
160
|
override fun onConnect(
|
|
135
161
|
session: MediaSession,
|
|
136
162
|
controller: MediaSession.ControllerInfo
|
|
137
163
|
): MediaSession.ConnectionResult {
|
|
138
|
-
val availableCommandsBuilder =
|
|
139
|
-
MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
|
|
140
|
-
|
|
141
|
-
for (command in customCommands) {
|
|
142
|
-
availableCommandsBuilder.add(command)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
164
|
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
|
|
146
|
-
.setAvailableSessionCommands(availableCommandsBuilder.build())
|
|
147
165
|
.build()
|
|
148
166
|
}
|
|
149
167
|
|
|
150
|
-
override fun onCustomCommand(
|
|
151
|
-
session: MediaSession,
|
|
152
|
-
controller: MediaSession.ControllerInfo,
|
|
153
|
-
customCommand: SessionCommand,
|
|
154
|
-
args: Bundle
|
|
155
|
-
): ListenableFuture<SessionResult> {
|
|
156
|
-
|
|
157
|
-
Log.d("Orpheus", "onCustomCommand: ${customCommand.customAction}")
|
|
158
|
-
|
|
159
|
-
when (customCommand.customAction) {
|
|
160
|
-
CustomCommands.CMD_START_TIMER -> {
|
|
161
|
-
val durationMs = args.getLong(CustomCommands.KEY_DURATION)
|
|
162
|
-
sleepTimerManager?.start(durationMs)
|
|
163
|
-
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
CustomCommands.CMD_CANCEL_TIMER -> {
|
|
167
|
-
sleepTimerManager?.cancel()
|
|
168
|
-
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
CustomCommands.CMD_GET_REMAINING -> {
|
|
172
|
-
val stopTime = sleepTimerManager?.getStopTimeMs()
|
|
173
|
-
val resultBundle = Bundle()
|
|
174
|
-
if (stopTime != null) {
|
|
175
|
-
resultBundle.putLong(CustomCommands.KEY_STOP_TIME, stopTime)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return Futures.immediateFuture(
|
|
179
|
-
SessionResult(
|
|
180
|
-
SessionResult.RESULT_SUCCESS,
|
|
181
|
-
resultBundle
|
|
182
|
-
)
|
|
183
|
-
)
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return super.onCustomCommand(session, controller, customCommand, args)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
168
|
/**
|
|
191
169
|
* 修复 UnsupportedOperationException 的关键!
|
|
192
170
|
* 当系统尝试恢复播放(比如从“最近播放”卡片点击)时触发。
|
|
@@ -235,7 +213,6 @@ class OrpheusMusicService : MediaLibraryService() {
|
|
|
235
213
|
|
|
236
214
|
private fun setupListeners() {
|
|
237
215
|
player?.addListener(object : Player.Listener {
|
|
238
|
-
|
|
239
216
|
@OptIn(UnstableApi::class)
|
|
240
217
|
override fun onMediaItemTransition(
|
|
241
218
|
mediaItem: androidx.media3.common.MediaItem?,
|