@roitium/expo-orpheus 0.5.0 → 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,57 +374,32 @@ class ExpoOrpheusModule : Module() {
|
|
|
254
374
|
}.runOnQueue(Queues.MAIN)
|
|
255
375
|
|
|
256
376
|
AsyncFunction("setSleepTimer") { durationMs: Long ->
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
val args = Bundle().apply {
|
|
260
|
-
putLong(CustomCommands.KEY_DURATION, durationMs)
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
controller?.sendCustomCommand(command, args)
|
|
377
|
+
OrpheusMusicService.instance?.startSleepTimer(durationMs)
|
|
378
|
+
return@AsyncFunction null
|
|
264
379
|
}.runOnQueue(Queues.MAIN)
|
|
265
380
|
|
|
266
381
|
AsyncFunction("getSleepTimerEndTime") {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
val command = SessionCommand(CustomCommands.CMD_GET_REMAINING, Bundle.EMPTY)
|
|
270
|
-
val future = controller!!.sendCustomCommand(command, Bundle.EMPTY)
|
|
271
|
-
|
|
272
|
-
val result = try {
|
|
273
|
-
future.get()
|
|
274
|
-
} catch (e: Exception) {
|
|
275
|
-
throw CodedException("ERR_EXECUTION_FAILED", e.message, e)
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (result.resultCode == SessionResult.RESULT_SUCCESS) {
|
|
279
|
-
val extras = result.extras
|
|
280
|
-
if (extras.containsKey(CustomCommands.KEY_STOP_TIME)) {
|
|
281
|
-
val stopTime = extras.getLong(CustomCommands.KEY_STOP_TIME)
|
|
282
|
-
return@AsyncFunction stopTime
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
return@AsyncFunction null
|
|
382
|
+
return@AsyncFunction OrpheusMusicService.instance?.getSleepTimerRemaining()
|
|
287
383
|
}.runOnQueue(Queues.MAIN)
|
|
288
384
|
|
|
289
385
|
AsyncFunction("cancelSleepTimer") {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
controller?.sendCustomCommand(command, Bundle.EMPTY)
|
|
386
|
+
OrpheusMusicService.instance?.cancelSleepTimer()
|
|
387
|
+
return@AsyncFunction null
|
|
293
388
|
}.runOnQueue(Queues.MAIN)
|
|
294
389
|
|
|
295
390
|
AsyncFunction("addToEnd") { tracks: List<TrackRecord>, startFromId: String?, clearQueue: Boolean? ->
|
|
296
|
-
|
|
391
|
+
checkPlayer()
|
|
297
392
|
val mediaItems = tracks.map { track ->
|
|
298
393
|
track.toMediaItem(gson)
|
|
299
394
|
}
|
|
300
|
-
val
|
|
395
|
+
val p = player ?: return@AsyncFunction
|
|
301
396
|
if (clearQueue == true) {
|
|
302
|
-
|
|
397
|
+
p.clearMediaItems()
|
|
303
398
|
durationCache.clear()
|
|
304
399
|
DownloadUtil.itemVolumeMap.clear()
|
|
305
400
|
}
|
|
306
|
-
val initialSize =
|
|
307
|
-
|
|
401
|
+
val initialSize = p.mediaItemCount
|
|
402
|
+
p.addMediaItems(mediaItems)
|
|
308
403
|
|
|
309
404
|
if (!startFromId.isNullOrEmpty()) {
|
|
310
405
|
val relativeIndex = tracks.indexOfFirst { it.id == startFromId }
|
|
@@ -312,50 +407,50 @@ class ExpoOrpheusModule : Module() {
|
|
|
312
407
|
if (relativeIndex != -1) {
|
|
313
408
|
val targetIndex = initialSize + relativeIndex
|
|
314
409
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
410
|
+
p.seekTo(targetIndex, C.TIME_UNSET)
|
|
411
|
+
p.prepare()
|
|
412
|
+
p.play()
|
|
318
413
|
|
|
319
414
|
return@AsyncFunction
|
|
320
415
|
}
|
|
321
416
|
}
|
|
322
417
|
|
|
323
|
-
if (
|
|
324
|
-
|
|
418
|
+
if (p.playbackState == Player.STATE_IDLE) {
|
|
419
|
+
p.prepare()
|
|
325
420
|
}
|
|
326
421
|
}.runOnQueue(Queues.MAIN)
|
|
327
422
|
|
|
328
423
|
AsyncFunction("playNext") { track: TrackRecord ->
|
|
329
|
-
|
|
330
|
-
val
|
|
424
|
+
checkPlayer()
|
|
425
|
+
val p = player ?: return@AsyncFunction
|
|
331
426
|
|
|
332
427
|
val mediaItem = track.toMediaItem(gson)
|
|
333
|
-
val targetIndex =
|
|
428
|
+
val targetIndex = p.currentMediaItemIndex + 1
|
|
334
429
|
|
|
335
430
|
var existingIndex = -1
|
|
336
|
-
for (i in 0 until
|
|
337
|
-
if (
|
|
431
|
+
for (i in 0 until p.mediaItemCount) {
|
|
432
|
+
if (p.getMediaItemAt(i).mediaId == track.id) {
|
|
338
433
|
existingIndex = i
|
|
339
434
|
break
|
|
340
435
|
}
|
|
341
436
|
}
|
|
342
437
|
|
|
343
438
|
if (existingIndex != -1) {
|
|
344
|
-
if (existingIndex ==
|
|
439
|
+
if (existingIndex == p.currentMediaItemIndex) {
|
|
345
440
|
return@AsyncFunction
|
|
346
441
|
}
|
|
347
|
-
val safeTargetIndex = targetIndex.coerceAtMost(
|
|
442
|
+
val safeTargetIndex = targetIndex.coerceAtMost(p.mediaItemCount)
|
|
348
443
|
|
|
349
|
-
|
|
444
|
+
p.moveMediaItem(existingIndex, safeTargetIndex)
|
|
350
445
|
|
|
351
446
|
} else {
|
|
352
|
-
val safeTargetIndex = targetIndex.coerceAtMost(
|
|
447
|
+
val safeTargetIndex = targetIndex.coerceAtMost(p.mediaItemCount)
|
|
353
448
|
|
|
354
|
-
|
|
449
|
+
p.addMediaItem(safeTargetIndex, mediaItem)
|
|
355
450
|
}
|
|
356
451
|
|
|
357
|
-
if (
|
|
358
|
-
|
|
452
|
+
if (p.playbackState == Player.STATE_IDLE) {
|
|
453
|
+
p.prepare()
|
|
359
454
|
}
|
|
360
455
|
}.runOnQueue(Queues.MAIN)
|
|
361
456
|
|
|
@@ -552,139 +647,19 @@ class ExpoOrpheusModule : Module() {
|
|
|
552
647
|
}
|
|
553
648
|
}
|
|
554
649
|
|
|
555
|
-
private fun setupListeners() {
|
|
556
|
-
controller?.addListener(object : Player.Listener {
|
|
557
|
-
|
|
558
|
-
/**
|
|
559
|
-
* 核心:处理切歌、播放结束逻辑
|
|
560
|
-
*/
|
|
561
|
-
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
|
562
|
-
val newId = mediaItem?.mediaId ?: ""
|
|
563
|
-
Log.e("Orpheus", "onMediaItemTransition: $reason")
|
|
564
|
-
|
|
565
|
-
sendEvent(
|
|
566
|
-
"onTrackStarted", mapOf(
|
|
567
|
-
"trackId" to newId,
|
|
568
|
-
"reason" to reason
|
|
569
|
-
)
|
|
570
|
-
)
|
|
571
|
-
|
|
572
|
-
lastMediaId = newId
|
|
573
|
-
saveCurrentPosition()
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
|
|
577
|
-
val player = controller ?: return
|
|
578
|
-
val currentItem = player.currentMediaItem ?: return
|
|
579
|
-
val mediaId = currentItem.mediaId
|
|
580
|
-
|
|
581
|
-
val duration = player.duration
|
|
582
|
-
Log.d(
|
|
583
|
-
"Orpheus",
|
|
584
|
-
"onTimelineChanged: reason: $reason mediaId: $mediaId duration: $duration"
|
|
585
|
-
)
|
|
586
|
-
|
|
587
|
-
if (duration != C.TIME_UNSET && duration > 0) {
|
|
588
|
-
durationCache[mediaId] = duration
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
override fun onPositionDiscontinuity(
|
|
593
|
-
oldPosition: Player.PositionInfo,
|
|
594
|
-
newPosition: Player.PositionInfo,
|
|
595
|
-
reason: Int
|
|
596
|
-
) {
|
|
597
|
-
val isAutoTransition = reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION
|
|
598
|
-
val isIndexChanged = oldPosition.mediaItemIndex != newPosition.mediaItemIndex
|
|
599
|
-
val lastMediaItem = oldPosition.mediaItem ?: return
|
|
600
|
-
val currentTime = System.currentTimeMillis()
|
|
601
|
-
if ((currentTime - lastTrackFinishedAt) < 200) {
|
|
602
|
-
return
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
Log.d(
|
|
606
|
-
"Orpheus",
|
|
607
|
-
"onPositionDiscontinuity: isAutoTransition:$isAutoTransition isIndexChanged: $isIndexChanged durationCache:$durationCache"
|
|
608
|
-
)
|
|
609
|
-
|
|
610
|
-
if (isAutoTransition || isIndexChanged) {
|
|
611
|
-
|
|
612
|
-
val duration = durationCache[lastMediaItem.mediaId] ?: return
|
|
613
|
-
lastTrackFinishedAt = currentTime
|
|
614
|
-
|
|
615
|
-
sendEvent(
|
|
616
|
-
"onTrackFinished", mapOf(
|
|
617
|
-
"trackId" to lastMediaItem.mediaId,
|
|
618
|
-
"finalPosition" to oldPosition.positionMs / 1000.0,
|
|
619
|
-
"duration" to duration / 1000.0,
|
|
620
|
-
)
|
|
621
|
-
)
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
/**
|
|
626
|
-
* 处理播放状态改变
|
|
627
|
-
*/
|
|
628
|
-
override fun onPlaybackStateChanged(state: Int) {
|
|
629
|
-
// state: 1=IDLE, 2=BUFFERING, 3=READY, 4=ENDED
|
|
630
|
-
sendEvent(
|
|
631
|
-
"onPlaybackStateChanged", mapOf(
|
|
632
|
-
"state" to state
|
|
633
|
-
)
|
|
634
|
-
)
|
|
635
|
-
|
|
636
|
-
updateProgressRunnerState()
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
/**
|
|
640
|
-
* 处理播放/暂停状态
|
|
641
|
-
*/
|
|
642
|
-
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
|
643
|
-
sendEvent(
|
|
644
|
-
"onIsPlayingChanged", mapOf(
|
|
645
|
-
"status" to isPlaying
|
|
646
|
-
)
|
|
647
|
-
)
|
|
648
|
-
updateProgressRunnerState()
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
/**
|
|
652
|
-
* 处理错误
|
|
653
|
-
*/
|
|
654
|
-
override fun onPlayerError(error: PlaybackException) {
|
|
655
|
-
sendEvent(
|
|
656
|
-
"onPlayerError", mapOf(
|
|
657
|
-
"code" to error.errorCode.toString(),
|
|
658
|
-
"message" to (error.message ?: "Unknown Error")
|
|
659
|
-
)
|
|
660
|
-
)
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
override fun onRepeatModeChanged(repeatMode: Int) {
|
|
664
|
-
super.onRepeatModeChanged(repeatMode)
|
|
665
|
-
Storage.saveRepeatMode(repeatMode)
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
|
|
669
|
-
super.onShuffleModeEnabledChanged(shuffleModeEnabled)
|
|
670
|
-
Storage.saveShuffleMode(shuffleModeEnabled)
|
|
671
|
-
}
|
|
672
|
-
})
|
|
673
|
-
}
|
|
674
|
-
|
|
675
650
|
private val progressSendEventRunnable = object : Runnable {
|
|
676
651
|
override fun run() {
|
|
677
|
-
val
|
|
652
|
+
val p = player ?: return
|
|
678
653
|
|
|
679
|
-
if (
|
|
680
|
-
val currentMs =
|
|
681
|
-
val durationMs =
|
|
654
|
+
if (p.isPlaying) {
|
|
655
|
+
val currentMs = p.currentPosition
|
|
656
|
+
val durationMs = p.duration
|
|
682
657
|
|
|
683
658
|
sendEvent(
|
|
684
659
|
"onPositionUpdate", mapOf(
|
|
685
660
|
"position" to currentMs / 1000.0,
|
|
686
661
|
"duration" to if (durationMs == C.TIME_UNSET) 0.0 else durationMs / 1000.0,
|
|
687
|
-
"buffered" to
|
|
662
|
+
"buffered" to p.bufferedPosition / 1000.0
|
|
688
663
|
)
|
|
689
664
|
)
|
|
690
665
|
}
|
|
@@ -701,9 +676,9 @@ class ExpoOrpheusModule : Module() {
|
|
|
701
676
|
}
|
|
702
677
|
|
|
703
678
|
private fun updateProgressRunnerState() {
|
|
704
|
-
val
|
|
679
|
+
val p = player
|
|
705
680
|
// 如果正在播放且状态是 READY,则开始轮询
|
|
706
|
-
if (
|
|
681
|
+
if (p != null && p.isPlaying && p.playbackState == Player.STATE_READY) {
|
|
707
682
|
mainHandler.removeCallbacks(progressSendEventRunnable)
|
|
708
683
|
mainHandler.removeCallbacks(progressSaveRunnable)
|
|
709
684
|
mainHandler.post(progressSaveRunnable)
|
|
@@ -737,17 +712,17 @@ class ExpoOrpheusModule : Module() {
|
|
|
737
712
|
}
|
|
738
713
|
|
|
739
714
|
private fun saveCurrentPosition() {
|
|
740
|
-
val
|
|
741
|
-
if (
|
|
715
|
+
val p = player ?: return
|
|
716
|
+
if (p.playbackState != Player.STATE_IDLE) {
|
|
742
717
|
Storage.savePosition(
|
|
743
|
-
|
|
744
|
-
|
|
718
|
+
p.currentMediaItemIndex,
|
|
719
|
+
p.currentPosition
|
|
745
720
|
)
|
|
746
721
|
}
|
|
747
722
|
}
|
|
748
723
|
|
|
749
|
-
private fun
|
|
750
|
-
if (
|
|
724
|
+
private fun checkPlayer() {
|
|
725
|
+
if (player == null) {
|
|
751
726
|
throw ControllerNotInitializedException()
|
|
752
727
|
}
|
|
753
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?,
|