@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 controller: MediaController? = null
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
- controllerFuture?.addListener({
79
- try {
80
- controller = controllerFuture?.get()
81
- setupListeners()
82
- } catch (e: Exception) {
83
- e.printStackTrace()
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
- }, MoreExecutors.directExecutor())
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
- checkController()
121
- controller?.currentPosition?.toDouble()?.div(1000.0) ?: 0.0
240
+ checkPlayer()
241
+ player?.currentPosition?.toDouble()?.div(1000.0) ?: 0.0
122
242
  }.runOnQueue(Queues.MAIN)
123
243
 
124
244
  AsyncFunction("getDuration") {
125
- checkController()
126
- val d = controller?.duration ?: C.TIME_UNSET
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
- checkController()
132
- controller?.bufferedPosition?.toDouble()?.div(1000.0) ?: 0.0
251
+ checkPlayer()
252
+ player?.bufferedPosition?.toDouble()?.div(1000.0) ?: 0.0
133
253
  }.runOnQueue(Queues.MAIN)
134
254
 
135
255
  AsyncFunction("getIsPlaying") {
136
- checkController()
137
- controller?.isPlaying ?: false
256
+ checkPlayer()
257
+ player?.isPlaying ?: false
138
258
  }.runOnQueue(Queues.MAIN)
139
259
 
140
260
  AsyncFunction("getCurrentIndex") {
141
- checkController()
142
- controller?.currentMediaItemIndex ?: -1
261
+ checkPlayer()
262
+ player?.currentMediaItemIndex ?: -1
143
263
  }.runOnQueue(Queues.MAIN)
144
264
 
145
265
  AsyncFunction("getCurrentTrack") {
146
- checkController()
147
- val player = controller ?: return@AsyncFunction null
148
- val currentItem = player.currentMediaItem ?: return@AsyncFunction null
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
- checkController()
155
- controller?.shuffleModeEnabled
274
+ checkPlayer()
275
+ player?.shuffleModeEnabled
156
276
  }.runOnQueue(Queues.MAIN)
157
277
 
158
278
  AsyncFunction("getIndexTrack") { index: Int ->
159
- checkController()
160
- val player = controller ?: return@AsyncFunction null
279
+ checkPlayer()
280
+ val p = player ?: return@AsyncFunction null
161
281
 
162
- if (index < 0 || index >= player.mediaItemCount) {
282
+ if (index < 0 || index >= p.mediaItemCount) {
163
283
  return@AsyncFunction null
164
284
  }
165
285
 
166
- val item = player.getMediaItemAt(index)
286
+ val item = p.getMediaItemAt(index)
167
287
 
168
288
  mediaItemToTrackRecord(item)
169
289
  }.runOnQueue(Queues.MAIN)
170
290
 
171
291
  AsyncFunction("play") {
172
- checkController()
173
- controller?.play()
292
+ checkPlayer()
293
+ player?.play()
174
294
  }.runOnQueue(Queues.MAIN)
175
295
 
176
296
  AsyncFunction("pause") {
177
- checkController()
178
- controller?.pause()
297
+ checkPlayer()
298
+ player?.pause()
179
299
  }.runOnQueue(Queues.MAIN)
180
300
 
181
301
  AsyncFunction("clear") {
182
- checkController()
183
- controller?.clearMediaItems()
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
- checkController()
191
- controller?.seekTo(index, C.TIME_UNSET)
310
+ checkPlayer()
311
+ player?.seekTo(index, C.TIME_UNSET)
192
312
  }.runOnQueue(Queues.MAIN)
193
313
 
194
314
  AsyncFunction("skipToNext") {
195
- checkController()
196
- if (controller?.hasNextMediaItem() == true) {
197
- controller?.seekToNextMediaItem()
315
+ checkPlayer()
316
+ if (player?.hasNextMediaItem() == true) {
317
+ player?.seekToNextMediaItem()
198
318
  }
199
319
  }.runOnQueue(Queues.MAIN)
200
320
 
201
321
  AsyncFunction("skipToPrevious") {
202
- checkController()
203
- if (controller?.hasPreviousMediaItem() == true) {
204
- controller?.seekToPreviousMediaItem()
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
- checkController()
329
+ checkPlayer()
210
330
  val ms = (seconds * 1000).toLong()
211
- controller?.seekTo(ms)
331
+ player?.seekTo(ms)
212
332
  }.runOnQueue(Queues.MAIN)
213
333
 
214
334
  AsyncFunction("setRepeatMode") { mode: Int ->
215
- checkController()
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
- controller?.repeatMode = repeatMode
342
+ player?.repeatMode = repeatMode
223
343
  }.runOnQueue(Queues.MAIN)
224
344
 
225
345
  AsyncFunction("setShuffleMode") { enabled: Boolean ->
226
- checkController()
227
- controller?.shuffleModeEnabled = enabled
346
+ checkPlayer()
347
+ player?.shuffleModeEnabled = enabled
228
348
  }.runOnQueue(Queues.MAIN)
229
349
 
230
350
  AsyncFunction("getRepeatMode") {
231
- checkController()
232
- controller?.repeatMode
351
+ checkPlayer()
352
+ player?.repeatMode
233
353
  }.runOnQueue(Queues.MAIN)
234
354
 
235
355
  AsyncFunction("removeTrack") { index: Int ->
236
- checkController()
237
- if (index >= 0 && index < (controller?.mediaItemCount ?: 0)) {
238
- controller?.removeMediaItem(index)
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
- checkController()
244
- val player = controller ?: return@AsyncFunction emptyList<TrackRecord>()
245
- val count = player.mediaItemCount
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 = player.getMediaItemAt(i)
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
- checkController()
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
- checkController()
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
- checkController()
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
- checkController()
391
+ checkPlayer()
299
392
  val mediaItems = tracks.map { track ->
300
393
  track.toMediaItem(gson)
301
394
  }
302
- val player = controller ?: return@AsyncFunction
395
+ val p = player ?: return@AsyncFunction
303
396
  if (clearQueue == true) {
304
- player.clearMediaItems()
397
+ p.clearMediaItems()
305
398
  durationCache.clear()
306
399
  DownloadUtil.itemVolumeMap.clear()
307
400
  }
308
- val initialSize = player.mediaItemCount
309
- player.addMediaItems(mediaItems)
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
- player.seekTo(targetIndex, C.TIME_UNSET)
318
- player.prepare()
319
- player.play()
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 (player.playbackState == Player.STATE_IDLE) {
326
- player.prepare()
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
- checkController()
332
- val player = controller ?: return@AsyncFunction
424
+ checkPlayer()
425
+ val p = player ?: return@AsyncFunction
333
426
 
334
427
  val mediaItem = track.toMediaItem(gson)
335
- val targetIndex = player.currentMediaItemIndex + 1
428
+ val targetIndex = p.currentMediaItemIndex + 1
336
429
 
337
430
  var existingIndex = -1
338
- for (i in 0 until player.mediaItemCount) {
339
- if (player.getMediaItemAt(i).mediaId == track.id) {
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 == player.currentMediaItemIndex) {
439
+ if (existingIndex == p.currentMediaItemIndex) {
347
440
  return@AsyncFunction
348
441
  }
349
- val safeTargetIndex = targetIndex.coerceAtMost(player.mediaItemCount)
442
+ val safeTargetIndex = targetIndex.coerceAtMost(p.mediaItemCount)
350
443
 
351
- player.moveMediaItem(existingIndex, safeTargetIndex)
444
+ p.moveMediaItem(existingIndex, safeTargetIndex)
352
445
 
353
446
  } else {
354
- val safeTargetIndex = targetIndex.coerceAtMost(player.mediaItemCount)
447
+ val safeTargetIndex = targetIndex.coerceAtMost(p.mediaItemCount)
355
448
 
356
- player.addMediaItem(safeTargetIndex, mediaItem)
449
+ p.addMediaItem(safeTargetIndex, mediaItem)
357
450
  }
358
451
 
359
- if (player.playbackState == Player.STATE_IDLE) {
360
- player.prepare()
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 player = controller ?: return
652
+ val p = player ?: return
680
653
 
681
- if (player.isPlaying) {
682
- val currentMs = player.currentPosition
683
- val durationMs = player.duration
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 player.bufferedPosition / 1000.0
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 player = controller
679
+ val p = player
707
680
  // 如果正在播放且状态是 READY,则开始轮询
708
- if (player != null && player.isPlaying && player.playbackState == Player.STATE_READY) {
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 player = controller ?: return
743
- if (player.playbackState != Player.STATE_IDLE) {
715
+ val p = player ?: return
716
+ if (p.playbackState != Player.STATE_IDLE) {
744
717
  Storage.savePosition(
745
- player.currentMediaItemIndex,
746
- player.currentPosition
718
+ p.currentMediaItemIndex,
719
+ p.currentPosition
747
720
  )
748
721
  }
749
722
  }
750
723
 
751
- private fun checkController() {
752
- if (controller == null) {
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
- private var player: ExoPlayer? = null
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?,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roitium/expo-orpheus",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "A player for bbplayer",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",