@roitium/expo-orpheus 0.2.3 → 0.3.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.
@@ -48,4 +48,5 @@ dependencies {
48
48
  implementation "com.squareup.retrofit2:retrofit:2.9.0"
49
49
  implementation "com.google.code.gson:gson:2.13.2"
50
50
  implementation "com.squareup.retrofit2:converter-gson:2.9.0"
51
+ implementation 'com.tencent:mmkv:2.3.0'
51
52
  }
@@ -0,0 +1,12 @@
1
+ package expo.modules.orpheus
2
+
3
+ object CustomCommands {
4
+ // start related to sleep timer
5
+ const val CMD_START_TIMER = "cmd_start_sleep_timer"
6
+ const val CMD_CANCEL_TIMER = "cmd_cancel_sleep_timer"
7
+ const val CMD_GET_REMAINING = "cmd_get_sleep_timer_remaining"
8
+
9
+ const val KEY_DURATION = "key_duration"
10
+ const val KEY_STOP_TIME = "key_stop_time"
11
+ // end related to sleep timer
12
+ }
@@ -4,20 +4,25 @@ import android.content.ComponentName
4
4
  import android.os.Bundle
5
5
  import android.os.Handler
6
6
  import android.os.Looper
7
- import androidx.core.net.toUri
7
+ import android.util.Log
8
8
  import androidx.media3.common.C
9
9
  import androidx.media3.common.MediaItem
10
- import androidx.media3.common.MediaMetadata
11
10
  import androidx.media3.common.PlaybackException
12
11
  import androidx.media3.common.Player
13
12
  import androidx.media3.session.MediaController
13
+ import androidx.media3.session.SessionCommand
14
+ import androidx.media3.session.SessionResult
14
15
  import androidx.media3.session.SessionToken
15
16
  import com.google.common.util.concurrent.ListenableFuture
16
17
  import com.google.common.util.concurrent.MoreExecutors
17
18
  import com.google.gson.Gson
19
+ import expo.modules.kotlin.exception.CodedException
18
20
  import expo.modules.kotlin.functions.Queues
19
21
  import expo.modules.kotlin.modules.Module
20
22
  import expo.modules.kotlin.modules.ModuleDefinition
23
+ import expo.modules.orpheus.models.TrackRecord
24
+ import expo.modules.orpheus.utils.MediaItemStorer
25
+ import expo.modules.orpheus.utils.toMediaItem
21
26
 
22
27
  class ExpoOrpheusModule : Module() {
23
28
  private var controllerFuture: ListenableFuture<MediaController>? = null
@@ -29,6 +34,8 @@ class ExpoOrpheusModule : Module() {
29
34
  // 记录上一首歌曲的 ID,用于在切歌时发送给 JS
30
35
  private var lastMediaId: String? = null
31
36
 
37
+ private var currentTrackDuration: Long = 0L
38
+
32
39
  val gson = Gson()
33
40
 
34
41
  override fun definition() = ModuleDefinition {
@@ -36,14 +43,16 @@ class ExpoOrpheusModule : Module() {
36
43
 
37
44
  Events(
38
45
  "onPlaybackStateChanged",
39
- "onTrackTransition",
40
46
  "onPlayerError",
41
47
  "onPositionUpdate",
42
- "onIsPlayingChanged"
48
+ "onIsPlayingChanged",
49
+ "onTrackFinished",
50
+ "onTrackStarted"
43
51
  )
44
52
 
45
53
  OnCreate {
46
54
  val context = appContext.reactContext ?: return@OnCreate
55
+ MediaItemStorer.initialize(context)
47
56
  val sessionToken = SessionToken(
48
57
  context,
49
58
  ComponentName(context, OrpheusService::class.java)
@@ -62,32 +71,51 @@ class ExpoOrpheusModule : Module() {
62
71
  }
63
72
 
64
73
  OnDestroy {
65
- stopProgressUpdater()
74
+ mainHandler.removeCallbacks(progressSendEventRunnable)
75
+ mainHandler.removeCallbacks(progressSaveRunnable)
66
76
  controllerFuture?.let { MediaController.releaseFuture(it) }
67
77
  }
68
78
 
79
+ Constant("restorePlaybackPositionEnabled") {
80
+ MediaItemStorer.isRestoreEnabled()
81
+ }
82
+
83
+ Function("setBilibiliCookie") { cookie: String ->
84
+ OrpheusConfig.bilibiliCookie = cookie
85
+ }
86
+
87
+ Function("setRestorePlaybackPositionEnabled") { enabled: Boolean ->
88
+ MediaItemStorer.setRestoreEnabled(enabled)
89
+ }
90
+
69
91
  AsyncFunction("getPosition") {
92
+ checkController()
70
93
  controller?.currentPosition?.toDouble()?.div(1000.0) ?: 0.0
71
94
  }.runOnQueue(Queues.MAIN)
72
95
 
73
96
  AsyncFunction("getDuration") {
97
+ checkController()
74
98
  val d = controller?.duration ?: C.TIME_UNSET
75
99
  if (d == C.TIME_UNSET) 0.0 else d.toDouble() / 1000.0
76
100
  }.runOnQueue(Queues.MAIN)
77
101
 
78
102
  AsyncFunction("getBuffered") {
103
+ checkController()
79
104
  controller?.bufferedPosition?.toDouble()?.div(1000.0) ?: 0.0
80
105
  }.runOnQueue(Queues.MAIN)
81
106
 
82
107
  AsyncFunction("getIsPlaying") {
108
+ checkController()
83
109
  controller?.isPlaying ?: false
84
110
  }.runOnQueue(Queues.MAIN)
85
111
 
86
112
  AsyncFunction("getCurrentIndex") {
113
+ checkController()
87
114
  controller?.currentMediaItemIndex ?: -1
88
115
  }.runOnQueue(Queues.MAIN)
89
116
 
90
117
  AsyncFunction("getCurrentTrack") {
118
+ checkController()
91
119
  val player = controller ?: return@AsyncFunction null
92
120
  val currentItem = player.currentMediaItem ?: return@AsyncFunction null
93
121
 
@@ -95,10 +123,12 @@ class ExpoOrpheusModule : Module() {
95
123
  }.runOnQueue(Queues.MAIN)
96
124
 
97
125
  AsyncFunction("getShuffleMode") {
126
+ checkController()
98
127
  controller?.shuffleModeEnabled
99
128
  }.runOnQueue(Queues.MAIN)
100
129
 
101
130
  AsyncFunction("getIndexTrack") { index: Int ->
131
+ checkController()
102
132
  val player = controller ?: return@AsyncFunction null
103
133
 
104
134
  if (index < 0 || index >= player.mediaItemCount) {
@@ -110,57 +140,49 @@ class ExpoOrpheusModule : Module() {
110
140
  mediaItemToTrackRecord(item)
111
141
  }.runOnQueue(Queues.MAIN)
112
142
 
113
- Function("setBilibiliCookie") { cookie: String ->
114
- OrpheusConfig.bilibiliCookie = cookie
115
- }
116
-
117
143
  AsyncFunction("play") {
118
- val player = controller
119
- if (player != null) {
120
- // 获取 player 真正归属的 Looper
121
- val playerLooper = player.applicationLooper
122
-
123
- if (Looper.myLooper() == playerLooper) {
124
- player.play()
125
- } else {
126
- Handler(playerLooper).post {
127
- player.play()
128
- }
129
- }
130
- }
144
+ checkController()
145
+ controller?.play()
131
146
  }.runOnQueue(Queues.MAIN)
132
147
 
133
148
  AsyncFunction("pause") {
149
+ checkController()
134
150
  controller?.pause()
135
151
  }.runOnQueue(Queues.MAIN)
136
152
 
137
153
  AsyncFunction("clear") {
154
+ checkController()
138
155
  controller?.clearMediaItems()
139
156
  }.runOnQueue(Queues.MAIN)
140
157
 
141
158
  AsyncFunction("skipTo") { index: Int ->
142
159
  // 跳转到指定索引的开头
160
+ checkController()
143
161
  controller?.seekTo(index, C.TIME_UNSET)
144
162
  }.runOnQueue(Queues.MAIN)
145
163
 
146
164
  AsyncFunction("skipToNext") {
165
+ checkController()
147
166
  if (controller?.hasNextMediaItem() == true) {
148
167
  controller?.seekToNextMediaItem()
149
168
  }
150
169
  }.runOnQueue(Queues.MAIN)
151
170
 
152
171
  AsyncFunction("skipToPrevious") {
172
+ checkController()
153
173
  if (controller?.hasPreviousMediaItem() == true) {
154
174
  controller?.seekToPreviousMediaItem()
155
175
  }
156
176
  }.runOnQueue(Queues.MAIN)
157
177
 
158
178
  AsyncFunction("seekTo") { seconds: Double ->
179
+ checkController()
159
180
  val ms = (seconds * 1000).toLong()
160
181
  controller?.seekTo(ms)
161
182
  }.runOnQueue(Queues.MAIN)
162
183
 
163
184
  AsyncFunction("setRepeatMode") { mode: Int ->
185
+ checkController()
164
186
  // mode: 0=OFF, 1=TRACK, 2=QUEUE
165
187
  val repeatMode = when (mode) {
166
188
  1 -> Player.REPEAT_MODE_ONE
@@ -171,20 +193,24 @@ class ExpoOrpheusModule : Module() {
171
193
  }.runOnQueue(Queues.MAIN)
172
194
 
173
195
  AsyncFunction("setShuffleMode") { enabled: Boolean ->
196
+ checkController()
174
197
  controller?.shuffleModeEnabled = enabled
175
198
  }.runOnQueue(Queues.MAIN)
176
199
 
177
200
  AsyncFunction("getRepeatMode") {
201
+ checkController()
178
202
  controller?.repeatMode
179
203
  }.runOnQueue(Queues.MAIN)
180
204
 
181
205
  AsyncFunction("removeTrack") { index: Int ->
206
+ checkController()
182
207
  if (index >= 0 && index < (controller?.mediaItemCount ?: 0)) {
183
208
  controller?.removeMediaItem(index)
184
209
  }
185
210
  }
186
211
 
187
212
  AsyncFunction("getQueue") {
213
+ checkController()
188
214
  val player = controller ?: return@AsyncFunction emptyList<TrackRecord>()
189
215
  val count = player.mediaItemCount
190
216
  val queue = ArrayList<TrackRecord>(count)
@@ -197,33 +223,50 @@ class ExpoOrpheusModule : Module() {
197
223
  return@AsyncFunction queue
198
224
  }.runOnQueue(Queues.MAIN)
199
225
 
200
- AsyncFunction("addToEnd") { tracks: List<TrackRecord>, startFromId: String?, clearQueue: Boolean? ->
201
- val mediaItems = tracks.mapNotNull { track ->
202
- try {
203
- val trackJson = gson.toJson(track)
204
- val extras = Bundle()
205
- extras.putString("track_json", trackJson)
206
-
207
- val artUri =
208
- if (!track.artwork.isNullOrEmpty()) track.artwork!!.toUri() else null
209
-
210
- val metadata = MediaMetadata.Builder()
211
- .setTitle(track.title)
212
- .setArtist(track.artist)
213
- .setArtworkUri(artUri)
214
- .setExtras(extras)
215
- .build()
216
-
217
- MediaItem.Builder()
218
- .setMediaId(track.id)
219
- .setUri(track.url)
220
- .setMediaMetadata(metadata)
221
- .build()
222
- } catch (e: Exception) {
223
- e.printStackTrace()
224
- null
226
+ AsyncFunction("setSleepTimer") { durationMs: Long ->
227
+ checkController()
228
+ val command = SessionCommand(CustomCommands.CMD_START_TIMER, Bundle.EMPTY)
229
+ val args = Bundle().apply {
230
+ putLong(CustomCommands.KEY_DURATION, durationMs)
231
+ }
232
+
233
+ controller?.sendCustomCommand(command, args)
234
+ }.runOnQueue(Queues.MAIN)
235
+
236
+ AsyncFunction("getSleepTimerEndTime") {
237
+ checkController()
238
+
239
+ val command = SessionCommand(CustomCommands.CMD_GET_REMAINING, Bundle.EMPTY)
240
+ val future = controller!!.sendCustomCommand(command, Bundle.EMPTY)
241
+
242
+ val result = try {
243
+ future.get()
244
+ } catch (e: Exception) {
245
+ throw CodedException("ERR_EXECUTION_FAILED", e.message, e)
246
+ }
247
+
248
+ if (result.resultCode == SessionResult.RESULT_SUCCESS) {
249
+ val extras = result.extras
250
+ if (extras.containsKey(CustomCommands.KEY_STOP_TIME)) {
251
+ val stopTime = extras.getLong(CustomCommands.KEY_STOP_TIME)
252
+ return@AsyncFunction stopTime
225
253
  }
226
254
  }
255
+
256
+ return@AsyncFunction null
257
+ }.runOnQueue(Queues.MAIN)
258
+
259
+ AsyncFunction("cancelSleepTimer") {
260
+ checkController()
261
+ val command = SessionCommand(CustomCommands.CMD_CANCEL_TIMER, Bundle.EMPTY)
262
+ controller?.sendCustomCommand(command, Bundle.EMPTY)
263
+ }.runOnQueue(Queues.MAIN)
264
+
265
+ AsyncFunction("addToEnd") { tracks: List<TrackRecord>, startFromId: String?, clearQueue: Boolean? ->
266
+ checkController()
267
+ val mediaItems = tracks.map { track ->
268
+ track.toMediaItem(gson)
269
+ }
227
270
  val player = controller ?: return@AsyncFunction
228
271
  if (clearQueue == true) {
229
272
  player.clearMediaItems()
@@ -251,25 +294,10 @@ class ExpoOrpheusModule : Module() {
251
294
  }.runOnQueue(Queues.MAIN)
252
295
 
253
296
  AsyncFunction("playNext") { track: TrackRecord ->
297
+ checkController()
254
298
  val player = controller ?: return@AsyncFunction
255
299
 
256
- val trackJson = gson.toJson(track)
257
- val extras = Bundle()
258
- extras.putString("track_json", trackJson)
259
- val artUri = if (!track.artwork.isNullOrEmpty()) track.artwork!!.toUri() else null
260
-
261
- val metadata = MediaMetadata.Builder()
262
- .setTitle(track.title)
263
- .setArtist(track.artist)
264
- .setArtworkUri(artUri)
265
- .setExtras(extras)
266
- .build()
267
-
268
- val mediaItem = MediaItem.Builder()
269
- .setMediaId(track.id)
270
- .setUri(track.url)
271
- .setMediaMetadata(metadata)
272
- .build()
300
+ val mediaItem = track.toMediaItem(gson)
273
301
  val targetIndex = player.currentMediaItemIndex + 1
274
302
 
275
303
  var existingIndex = -1
@@ -307,19 +335,39 @@ class ExpoOrpheusModule : Module() {
307
335
  * 核心:处理切歌、播放结束逻辑
308
336
  */
309
337
  override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
310
- val currentTrackId = mediaItem?.mediaId ?: ""
311
- Player.MEDIA_ITEM_TRANSITION_REASON_AUTO
338
+ val newId = mediaItem?.mediaId ?: ""
339
+ Log.e("Orpheus", "onMediaItemTransition: $reason")
312
340
 
313
341
  sendEvent(
314
- "onTrackTransition", mapOf(
315
- "currentTrackId" to currentTrackId,
316
- "previousTrackId" to lastMediaId, // 上一首歌是什么
342
+ "onTrackStarted", mapOf(
343
+ "trackId" to newId,
317
344
  "reason" to reason
318
345
  )
319
346
  )
320
347
 
321
- // 更新本地记录,为下一次切歌做准备
322
- lastMediaId = currentTrackId
348
+ lastMediaId = newId
349
+ currentTrackDuration = 0L
350
+ saveCurrentPosition()
351
+ }
352
+
353
+ override fun onPositionDiscontinuity(
354
+ oldPosition: Player.PositionInfo,
355
+ newPosition: Player.PositionInfo,
356
+ reason: Int
357
+ ) {
358
+ Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT
359
+ if (oldPosition.mediaItemIndex != newPosition.mediaItemIndex) {
360
+ val lastMediaItem =
361
+ controller?.getMediaItemAt(oldPosition.mediaItemIndex) ?: return
362
+
363
+ sendEvent(
364
+ "onTrackFinished", mapOf(
365
+ "trackId" to lastMediaItem.mediaId,
366
+ "finalPosition" to oldPosition.positionMs / 1000.0,
367
+ "duration" to currentTrackDuration / 1000.0,
368
+ )
369
+ )
370
+ }
323
371
  }
324
372
 
325
373
  /**
@@ -333,6 +381,11 @@ class ExpoOrpheusModule : Module() {
333
381
  )
334
382
  )
335
383
 
384
+ if (state == Player.STATE_READY) {
385
+ val d = controller?.duration
386
+ if (d != C.TIME_UNSET && d != null) currentTrackDuration = d
387
+ }
388
+
336
389
  updateProgressRunnerState()
337
390
  }
338
391
 
@@ -362,7 +415,7 @@ class ExpoOrpheusModule : Module() {
362
415
  })
363
416
  }
364
417
 
365
- private val progressRunnable = object : Runnable {
418
+ private val progressSendEventRunnable = object : Runnable {
366
419
  override fun run() {
367
420
  val player = controller ?: return
368
421
 
@@ -383,25 +436,27 @@ class ExpoOrpheusModule : Module() {
383
436
  }
384
437
  }
385
438
 
439
+ private val progressSaveRunnable = object : Runnable {
440
+ override fun run() {
441
+ saveCurrentPosition()
442
+ mainHandler.postDelayed(this, 5000)
443
+ }
444
+ }
445
+
386
446
  private fun updateProgressRunnerState() {
387
447
  val player = controller
388
448
  // 如果正在播放且状态是 READY,则开始轮询
389
449
  if (player != null && player.isPlaying && player.playbackState == Player.STATE_READY) {
390
- startProgressUpdater()
450
+ mainHandler.removeCallbacks(progressSendEventRunnable)
451
+ mainHandler.removeCallbacks(progressSaveRunnable)
452
+ mainHandler.post(progressSaveRunnable)
453
+ mainHandler.post(progressSendEventRunnable)
391
454
  } else {
392
- stopProgressUpdater()
455
+ mainHandler.removeCallbacks(progressSendEventRunnable)
456
+ mainHandler.removeCallbacks(progressSaveRunnable)
393
457
  }
394
458
  }
395
459
 
396
- private fun startProgressUpdater() {
397
- mainHandler.removeCallbacks(progressRunnable)
398
- mainHandler.post(progressRunnable)
399
- }
400
-
401
- private fun stopProgressUpdater() {
402
- mainHandler.removeCallbacks(progressRunnable)
403
- }
404
-
405
460
  private fun mediaItemToTrackRecord(item: MediaItem): TrackRecord {
406
461
  val extras = item.mediaMetadata.extras
407
462
  val trackJson = extras?.getString("track_json")
@@ -423,4 +478,20 @@ class ExpoOrpheusModule : Module() {
423
478
 
424
479
  return track
425
480
  }
481
+
482
+ private fun saveCurrentPosition() {
483
+ val player = controller ?: return
484
+ if (player.playbackState != Player.STATE_IDLE) {
485
+ MediaItemStorer.savePosition(
486
+ player.currentMediaItemIndex,
487
+ player.currentPosition
488
+ )
489
+ }
490
+ }
491
+
492
+ private fun checkController() {
493
+ if (controller == null) {
494
+ throw ControllerNotInitializedException()
495
+ }
496
+ }
426
497
  }
@@ -1,9 +1,13 @@
1
1
  package expo.modules.orpheus
2
2
 
3
+ import android.os.Bundle
3
4
  import androidx.annotation.OptIn
4
5
  import androidx.core.net.toUri
5
6
  import androidx.media3.common.AudioAttributes
6
7
  import androidx.media3.common.C
8
+ import androidx.media3.common.Player
9
+ import androidx.media3.common.Timeline
10
+ import androidx.media3.common.util.Log
7
11
  import androidx.media3.common.util.UnstableApi
8
12
  import androidx.media3.datasource.DataSpec
9
13
  import androidx.media3.datasource.DefaultHttpDataSource
@@ -13,19 +17,28 @@ import androidx.media3.exoplayer.ExoPlayer
13
17
  import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
14
18
  import androidx.media3.session.MediaLibraryService
15
19
  import androidx.media3.session.MediaSession
20
+ import androidx.media3.session.SessionCommand
21
+ import androidx.media3.session.SessionResult
16
22
  import com.google.common.util.concurrent.Futures
17
23
  import com.google.common.util.concurrent.ListenableFuture
18
24
  import expo.modules.orpheus.bilibili.BilibiliRepository
25
+ import expo.modules.orpheus.utils.MediaItemStorer
26
+ import expo.modules.orpheus.utils.SleepTimeController
19
27
  import java.io.IOException
20
28
 
21
29
  class OrpheusService : MediaLibraryService() {
22
30
 
23
31
  private var player: ExoPlayer? = null
24
32
  private var mediaSession: MediaLibrarySession? = null
33
+ private var sleepTimerManager: SleepTimeController? = null
25
34
 
26
35
  @OptIn(UnstableApi::class)
27
36
  override fun onCreate() {
28
37
  super.onCreate()
38
+
39
+ MediaItemStorer.initialize(this)
40
+
41
+
29
42
  val httpDataSourceFactory = DefaultHttpDataSource.Factory()
30
43
  .setUserAgent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36")
31
44
  .setAllowCrossProtocolRedirects(true)
@@ -96,9 +109,14 @@ class OrpheusService : MediaLibraryService() {
96
109
  )
97
110
  .build()
98
111
 
112
+ setupListeners()
113
+
99
114
  mediaSession = MediaLibrarySession.Builder(this, player!!, callback)
100
115
  .setId("OrpheusSession")
101
116
  .build()
117
+
118
+ restorePlayerState(MediaItemStorer.isRestoreEnabled())
119
+ sleepTimerManager = SleepTimeController(player!!)
102
120
  }
103
121
 
104
122
  override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession? {
@@ -116,19 +134,69 @@ class OrpheusService : MediaLibraryService() {
116
134
 
117
135
  var callback: MediaLibrarySession.Callback = @UnstableApi
118
136
  object : MediaLibrarySession.Callback {
137
+ private val customCommands = listOf(
138
+ SessionCommand(CustomCommands.CMD_START_TIMER, Bundle.EMPTY),
139
+ SessionCommand(CustomCommands.CMD_CANCEL_TIMER, Bundle.EMPTY),
140
+ SessionCommand(CustomCommands.CMD_GET_REMAINING, Bundle.EMPTY)
141
+ )
142
+
119
143
  @OptIn(UnstableApi::class)
120
144
  override fun onConnect(
121
145
  session: MediaSession,
122
146
  controller: MediaSession.ControllerInfo
123
147
  ): MediaSession.ConnectionResult {
124
- val sessionCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
125
- .build()
148
+ val availableCommandsBuilder =
149
+ MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
150
+
151
+ for (command in customCommands) {
152
+ availableCommandsBuilder.add(command)
153
+ }
126
154
 
127
155
  return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
128
- .setAvailableSessionCommands(sessionCommands)
156
+ .setAvailableSessionCommands(availableCommandsBuilder.build())
129
157
  .build()
130
158
  }
131
159
 
160
+ override fun onCustomCommand(
161
+ session: MediaSession,
162
+ controller: MediaSession.ControllerInfo,
163
+ customCommand: SessionCommand,
164
+ args: Bundle
165
+ ): ListenableFuture<SessionResult> {
166
+
167
+ Log.d("Orpheus", "onCustomCommand: ${customCommand.customAction}")
168
+
169
+ when (customCommand.customAction) {
170
+ CustomCommands.CMD_START_TIMER -> {
171
+ val durationMs = args.getLong(CustomCommands.KEY_DURATION)
172
+ sleepTimerManager?.start(durationMs)
173
+ return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
174
+ }
175
+
176
+ CustomCommands.CMD_CANCEL_TIMER -> {
177
+ sleepTimerManager?.cancel()
178
+ return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
179
+ }
180
+
181
+ CustomCommands.CMD_GET_REMAINING -> {
182
+ val stopTime = sleepTimerManager?.getStopTimeMs()
183
+ val resultBundle = Bundle()
184
+ if (stopTime != null) {
185
+ resultBundle.putLong(CustomCommands.KEY_STOP_TIME, stopTime)
186
+ }
187
+
188
+ return Futures.immediateFuture(
189
+ SessionResult(
190
+ SessionResult.RESULT_SUCCESS,
191
+ resultBundle
192
+ )
193
+ )
194
+ }
195
+ }
196
+
197
+ return super.onCustomCommand(session, controller, customCommand, args)
198
+ }
199
+
132
200
  /**
133
201
  * 修复 UnsupportedOperationException 的关键!
134
202
  * 当系统尝试恢复播放(比如从“最近播放”卡片点击)时触发。
@@ -146,4 +214,50 @@ class OrpheusService : MediaLibraryService() {
146
214
  )
147
215
  }
148
216
  }
149
- }
217
+
218
+ @OptIn(UnstableApi::class)
219
+ private fun restorePlayerState(restorePosition: Boolean) {
220
+ val player = player ?: return
221
+
222
+ val restoredItems = MediaItemStorer.restoreQueue()
223
+
224
+ if (restoredItems.isNotEmpty()) {
225
+ player.setMediaItems(restoredItems)
226
+
227
+ val savedIndex = MediaItemStorer.getSavedIndex()
228
+ val savedPosition = MediaItemStorer.getSavedPosition()
229
+
230
+ if (savedIndex >= 0 && savedIndex < restoredItems.size) {
231
+ player.seekTo(savedIndex, if (restorePosition) savedPosition else C.TIME_UNSET)
232
+ } else {
233
+ player.seekTo(0, 0L)
234
+ }
235
+
236
+ player.playWhenReady = false
237
+ player.prepare()
238
+ }
239
+ }
240
+
241
+ private fun setupListeners() {
242
+ player?.addListener(object : Player.Listener {
243
+ override fun onMediaItemTransition(
244
+ mediaItem: androidx.media3.common.MediaItem?,
245
+ reason: Int
246
+ ) {
247
+ saveCurrentQueue()
248
+ }
249
+
250
+ override fun onTimelineChanged(timeline: Timeline, reason: Int) {
251
+ saveCurrentQueue()
252
+ }
253
+ })
254
+ }
255
+
256
+ private fun saveCurrentQueue() {
257
+ val player = player ?: return
258
+ val queue = List(player.mediaItemCount) { i -> player.getMediaItemAt(i) }
259
+ if (queue.isNotEmpty()) {
260
+ MediaItemStorer.saveQueue(queue)
261
+ }
262
+ }
263
+ }
@@ -0,0 +1,9 @@
1
+ package expo.modules.orpheus
2
+
3
+ import expo.modules.kotlin.exception.CodedException
4
+
5
+ class ControllerNotInitializedException : CodedException(
6
+ "ERR_CONTROLLER_NOT_INIT",
7
+ "The MediaController is not initialized. Connect to service first.",
8
+ null
9
+ )
@@ -1,8 +1,16 @@
1
- package expo.modules.orpheus
1
+ package expo.modules.orpheus.models
2
2
 
3
3
  import expo.modules.kotlin.records.Field
4
4
  import expo.modules.kotlin.records.Record
5
5
 
6
+ class LoudnessRecord : Record {
7
+ @Field
8
+ var measured_i: Double = 0.0
9
+
10
+ @Field
11
+ var target_i: Double = 0.0
12
+ }
13
+
6
14
  class TrackRecord : Record {
7
15
  @Field
8
16
  var id: String = ""
@@ -23,6 +31,6 @@ class TrackRecord : Record {
23
31
  @Field
24
32
  var duration: Double? = null
25
33
 
26
- // @Field
27
- // var loudness: Map<String, Any>? = null
34
+ @Field
35
+ var loudness: LoudnessRecord? = null
28
36
  }
@@ -0,0 +1,93 @@
1
+ package expo.modules.orpheus.utils
2
+
3
+ import android.content.Context
4
+ import androidx.annotation.OptIn
5
+ import androidx.media3.common.MediaItem
6
+ import androidx.media3.common.util.Log
7
+ import androidx.media3.common.util.UnstableApi
8
+ import com.google.gson.Gson
9
+ import com.google.gson.reflect.TypeToken
10
+ import com.tencent.mmkv.MMKV
11
+ import expo.modules.orpheus.models.TrackRecord
12
+
13
+ @OptIn(UnstableApi::class)
14
+ object MediaItemStorer {
15
+ private var kv: MMKV? = null
16
+ private val gson = Gson()
17
+ private const val KEY_RESTORE_POSITION_ENABLED = "config_restore_position_enabled"
18
+ private const val KEY_SAVED_QUEUE = "saved_queue_json_list"
19
+ private const val KEY_SAVED_INDEX = "saved_index"
20
+ private const val KEY_SAVED_POSITION = "saved_position"
21
+
22
+
23
+ fun initialize(context: Context) {
24
+ if (kv == null) {
25
+ MMKV.initialize(context)
26
+ kv = MMKV.mmkvWithID("player_queue_store")
27
+ }
28
+ }
29
+
30
+ private val safeKv: MMKV
31
+ get() = kv ?: throw IllegalStateException("MediaItemStorer not initialized")
32
+
33
+ fun setRestoreEnabled(enabled: Boolean) {
34
+ try {
35
+ safeKv.encode(KEY_RESTORE_POSITION_ENABLED, enabled)
36
+ } catch (e: Exception) {
37
+ Log.e("MediaItemStorer", "Failed to set restore position enabled", e)
38
+ }
39
+ }
40
+
41
+ fun isRestoreEnabled(): Boolean {
42
+ return safeKv.decodeBool(KEY_RESTORE_POSITION_ENABLED, false)
43
+ }
44
+
45
+ @OptIn(UnstableApi::class)
46
+ fun saveQueue(mediaItems: List<MediaItem>) {
47
+ try {
48
+ val jsonList = mediaItems.mapNotNull { item ->
49
+ item.mediaMetadata.extras?.getString("track_json")
50
+ }
51
+
52
+ val jsonListString = gson.toJson(jsonList)
53
+ safeKv.encode(KEY_SAVED_QUEUE, jsonListString)
54
+
55
+ } catch (e: Exception) {
56
+ Log.e("MediaItemStorer", "Failed to save queue", e)
57
+ }
58
+ }
59
+
60
+ fun restoreQueue(): List<MediaItem> {
61
+ return try {
62
+ val jsonListString = kv?.decodeString(KEY_SAVED_QUEUE)
63
+
64
+ if (jsonListString.isNullOrEmpty()) return emptyList()
65
+
66
+ val listType = object : TypeToken<List<String>>() {}.type
67
+ val trackJsonList: List<String> = gson.fromJson(jsonListString, listType)
68
+
69
+ trackJsonList.mapNotNull { trackJson ->
70
+ try {
71
+ val track = gson.fromJson(trackJson, TrackRecord::class.java)
72
+
73
+ track.toMediaItem(gson)
74
+
75
+ } catch (e: Exception) {
76
+ Log.e("MediaItemStorer", "Failed to parse track json: $trackJson", e)
77
+ null
78
+ }
79
+ }
80
+ } catch (e: Exception) {
81
+ Log.e("MediaItemStorer", "Failed to restore queue", e)
82
+ emptyList()
83
+ }
84
+ }
85
+
86
+ fun savePosition(index: Int, position: Long) {
87
+ safeKv.encode(KEY_SAVED_INDEX, index)
88
+ safeKv.encode(KEY_SAVED_POSITION, position)
89
+ }
90
+
91
+ fun getSavedIndex() = kv?.decodeInt(KEY_SAVED_INDEX, 0) ?: 0
92
+ fun getSavedPosition() = kv?.decodeLong(KEY_SAVED_POSITION, 0L) ?: 0L
93
+ }
@@ -0,0 +1,63 @@
1
+ package expo.modules.orpheus.utils
2
+
3
+ import android.os.Handler
4
+ import android.os.Looper
5
+ import android.os.SystemClock
6
+ import androidx.media3.common.Player
7
+
8
+ class SleepTimeController(private val player: Player) {
9
+
10
+ private val handler = Handler(Looper.getMainLooper())
11
+
12
+ private var internalStopTargetMs: Long? = null
13
+
14
+ private val stopRunnable = Runnable {
15
+ performStop()
16
+ }
17
+
18
+ /**
19
+ * 开启定时器
20
+ * @param durationMs 多少毫秒后停止
21
+ */
22
+ fun start(durationMs: Long) {
23
+ if (durationMs <= 0) {
24
+ cancel()
25
+ return
26
+ }
27
+
28
+ cancel()
29
+
30
+ internalStopTargetMs = SystemClock.elapsedRealtime() + durationMs
31
+
32
+ handler.postDelayed(stopRunnable, durationMs)
33
+ }
34
+
35
+ /**
36
+ * 取消定时器
37
+ */
38
+ fun cancel() {
39
+ internalStopTargetMs = null
40
+ handler.removeCallbacks(stopRunnable)
41
+ }
42
+
43
+ /**
44
+ * @return 返回的是标准的 UTC 时间戳 (System.currentTimeMillis 格式),如果没开启则返回 null
45
+ */
46
+ fun getStopTimeMs(): Long? {
47
+ val target = internalStopTargetMs ?: return null
48
+ val nowElapsed = SystemClock.elapsedRealtime()
49
+
50
+ val remainingMs = target - nowElapsed
51
+
52
+ if (remainingMs <= 0) {
53
+ return null
54
+ }
55
+
56
+ return System.currentTimeMillis() + remainingMs
57
+ }
58
+
59
+ private fun performStop() {
60
+ player.pause()
61
+ cancel()
62
+ }
63
+ }
@@ -0,0 +1,35 @@
1
+ package expo.modules.orpheus.utils
2
+
3
+ import android.os.Bundle
4
+ import androidx.core.net.toUri
5
+ import androidx.media3.common.MediaItem
6
+ import androidx.media3.common.MediaMetadata
7
+ import com.google.gson.Gson
8
+ import expo.modules.orpheus.models.TrackRecord
9
+
10
+ fun TrackRecord.toMediaItem(gson: Gson): MediaItem {
11
+ val trackJson = gson.toJson(this)
12
+
13
+ val extras = Bundle()
14
+ extras.putString("track_json", trackJson)
15
+
16
+ this.loudness?.let {
17
+ extras.putDouble("loudness_measured_i", it.measured_i)
18
+ extras.putDouble("loudness_target_i", it.target_i)
19
+ }
20
+
21
+ val artUri = if (!this.artwork.isNullOrEmpty()) this.artwork?.toUri() else null
22
+
23
+ val metadata = MediaMetadata.Builder()
24
+ .setTitle(this.title)
25
+ .setArtist(this.artist)
26
+ .setArtworkUri(artUri)
27
+ .setExtras(extras)
28
+ .build()
29
+
30
+ return MediaItem.Builder()
31
+ .setMediaId(this.id)
32
+ .setUri(this.url)
33
+ .setMediaMetadata(metadata)
34
+ .build()
35
+ }
@@ -23,17 +23,24 @@ export interface Track {
23
23
  artist?: string;
24
24
  artwork?: string;
25
25
  duration?: number;
26
- [key: string]: any;
26
+ loudness?: {
27
+ measured_i: number;
28
+ target_i: number;
29
+ };
27
30
  }
28
31
  export type OrpheusEvents = {
29
32
  onPlaybackStateChanged(event: {
30
33
  state: PlaybackState;
31
34
  }): void;
32
- onTrackTransition(event: {
33
- currentTrackId: string;
34
- previousTrackId?: string;
35
+ onTrackStarted(event: {
36
+ trackId: string;
35
37
  reason: TransitionReason;
36
38
  }): void;
39
+ onTrackFinished(event: {
40
+ trackId: string;
41
+ finalPosition: number;
42
+ duration: number;
43
+ }): void;
37
44
  onPlayerError(event: {
38
45
  code: string;
39
46
  message: string;
@@ -48,6 +55,7 @@ export type OrpheusEvents = {
48
55
  }): void;
49
56
  };
50
57
  declare class OrpheusModule extends NativeModule<OrpheusEvents> {
58
+ restorePlaybackPositionEnabled: boolean;
51
59
  /**
52
60
  * 获取当前进度(秒)
53
61
  */
@@ -82,6 +90,7 @@ declare class OrpheusModule extends NativeModule<OrpheusEvents> {
82
90
  getIndexTrack(index: number): Promise<Track | null>;
83
91
  getRepeatMode(): Promise<RepeatMode>;
84
92
  setBilibiliCookie(cookie: string): void;
93
+ setRestorePlaybackPositionEnabled(enabled: boolean): void;
85
94
  play(): Promise<void>;
86
95
  pause(): Promise<void>;
87
96
  clear(): Promise<void>;
@@ -109,6 +118,17 @@ declare class OrpheusModule extends NativeModule<OrpheusEvents> {
109
118
  */
110
119
  playNext(track: Track): Promise<void>;
111
120
  removeTrack(index: number): Promise<void>;
121
+ /**
122
+ * 设置睡眠定时器
123
+ * @param durationMs 单位毫秒
124
+ */
125
+ setSleepTimer(durationMs: number): Promise<void>;
126
+ /**
127
+ * 获取睡眠定时器结束时间
128
+ * @returns 单位毫秒,如果没有设置则返回 null
129
+ */
130
+ getSleepTimerEndTime(): Promise<number | null>;
131
+ cancelSleepTimer(): Promise<void>;
112
132
  }
113
133
  export declare const Orpheus: OrpheusModule;
114
134
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoOrpheusModule.d.ts","sourceRoot":"","sources":["../src/ExpoOrpheusModule.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ExpoOrpheusModule.d.ts","sourceRoot":"","sources":["../src/ExpoOrpheusModule.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoOrpheusModule.js","sourceRoot":"","sources":["../src/ExpoOrpheusModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAgB,MAAM,mBAAmB,CAAC;AAEtE,MAAM,CAAN,IAAY,aAKX;AALD,WAAY,aAAa;IACvB,iDAAQ,CAAA;IACR,2DAAa,CAAA;IACb,mDAAS,CAAA;IACT,mDAAS,CAAA;AACX,CAAC,EALW,aAAa,KAAb,aAAa,QAKxB;AAED,MAAM,CAAN,IAAY,UAIX;AAJD,WAAY,UAAU;IACpB,yCAAO,CAAA;IACP,6CAAS,CAAA;IACT,6CAAS,CAAA;AACX,CAAC,EAJW,UAAU,KAAV,UAAU,QAIrB;AAED,MAAM,CAAN,IAAY,gBAKX;AALD,WAAY,gBAAgB;IAC1B,2DAAU,CAAA;IACV,uDAAQ,CAAA;IACR,uDAAQ,CAAA;IACR,+EAAoB,CAAA;AACtB,CAAC,EALW,gBAAgB,KAAhB,gBAAgB,QAK3B;AAkHD,MAAM,CAAC,MAAM,OAAO,GAAG,mBAAmB,CAAgB,SAAS,CAAC,CAAC","sourcesContent":["import { requireNativeModule, NativeModule } from \"expo-modules-core\";\n\nexport enum PlaybackState {\n IDLE = 1,\n BUFFERING = 2,\n READY = 3,\n ENDED = 4,\n}\n\nexport enum RepeatMode {\n OFF = 0,\n TRACK = 1,\n QUEUE = 2,\n}\n\nexport enum TransitionReason {\n REPEAT = 0,\n AUTO = 1,\n SEEK = 2,\n PLAYLIST_CHANGED = 3,\n}\n\nexport interface Track {\n id: string;\n url: string;\n title?: string;\n artist?: string;\n artwork?: string;\n duration?: number;\n [key: string]: any;\n}\n\nexport type OrpheusEvents = {\n onPlaybackStateChanged(event: { state: PlaybackState }): void;\n onTrackTransition(event: {\n currentTrackId: string;\n previousTrackId?: string;\n reason: TransitionReason;\n }): void;\n onPlayerError(event: { code: string; message: string }): void;\n onPositionUpdate(event: {\n position: number;\n duration: number;\n buffered: number;\n }): void;\n onIsPlayingChanged(event: { status: boolean }): void;\n};\n\ndeclare class OrpheusModule extends NativeModule<OrpheusEvents> {\n /**\n * 获取当前进度(秒)\n */\n getPosition(): Promise<number>;\n\n /**\n * 获取总时长(秒)\n */\n getDuration(): Promise<number>;\n\n /**\n * 获取缓冲进度(秒)\n */\n getBuffered(): Promise<number>;\n\n /**\n * 获取是否正在播放\n */\n getIsPlaying(): Promise<boolean>;\n\n /**\n * 获取当前播放索引\n */\n getCurrentIndex(): Promise<number>;\n\n /**\n * 获取当前播放的 Track 对象\n */\n getCurrentTrack(): Promise<Track | null>;\n\n /**\n * 获取随机模式状态\n */\n getShuffleMode(): Promise<boolean>;\n\n /**\n * 获取指定索引的 Track\n */\n getIndexTrack(index: number): Promise<Track | null>;\n\n getRepeatMode(): Promise<RepeatMode>;\n\n setBilibiliCookie(cookie: string): void;\n\n play(): Promise<void>;\n\n pause(): Promise<void>;\n\n clear(): Promise<void>;\n\n skipTo(index: number): Promise<void>;\n\n skipToNext(): Promise<void>;\n\n skipToPrevious(): Promise<void>;\n\n /**\n * 跳转进度\n * @param seconds 秒数\n */\n seekTo(seconds: number): Promise<void>;\n\n setRepeatMode(mode: RepeatMode): Promise<void>;\n\n setShuffleMode(enabled: boolean): Promise<void>;\n\n getQueue(): Promise<Track[]>;\n\n /**\n * 添加到队列末尾,且不去重。\n * @param tracks\n * @param startFromId 可选,添加后立即播放该 ID 的曲目\n * @param clearQueue 可选,是否清空当前队列\n */\n addToEnd(tracks: Track[], startFromId?: string, clearQueue?: boolean): Promise<void>;\n\n /**\n * 播放下一首\n * @param track\n */\n playNext(track: Track): Promise<void>;\n\n removeTrack(index: number): Promise<void>;\n}\n\nexport const Orpheus = requireNativeModule<OrpheusModule>(\"Orpheus\");\n"]}
1
+ {"version":3,"file":"ExpoOrpheusModule.js","sourceRoot":"","sources":["../src/ExpoOrpheusModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAgB,MAAM,mBAAmB,CAAC;AAEtE,MAAM,CAAN,IAAY,aAKX;AALD,WAAY,aAAa;IACvB,iDAAQ,CAAA;IACR,2DAAa,CAAA;IACb,mDAAS,CAAA;IACT,mDAAS,CAAA;AACX,CAAC,EALW,aAAa,KAAb,aAAa,QAKxB;AAED,MAAM,CAAN,IAAY,UAIX;AAJD,WAAY,UAAU;IACpB,yCAAO,CAAA;IACP,6CAAS,CAAA;IACT,6CAAS,CAAA;AACX,CAAC,EAJW,UAAU,KAAV,UAAU,QAIrB;AAED,MAAM,CAAN,IAAY,gBAKX;AALD,WAAY,gBAAgB;IAC1B,2DAAU,CAAA;IACV,uDAAQ,CAAA;IACR,uDAAQ,CAAA;IACR,+EAAoB,CAAA;AACtB,CAAC,EALW,gBAAgB,KAAhB,gBAAgB,QAK3B;AA6ID,MAAM,CAAC,MAAM,OAAO,GAAG,mBAAmB,CAAgB,SAAS,CAAC,CAAC","sourcesContent":["import { requireNativeModule, NativeModule } from \"expo-modules-core\";\n\nexport enum PlaybackState {\n IDLE = 1,\n BUFFERING = 2,\n READY = 3,\n ENDED = 4,\n}\n\nexport enum RepeatMode {\n OFF = 0,\n TRACK = 1,\n QUEUE = 2,\n}\n\nexport enum TransitionReason {\n REPEAT = 0,\n AUTO = 1,\n SEEK = 2,\n PLAYLIST_CHANGED = 3,\n}\n\nexport interface Track {\n id: string;\n url: string;\n title?: string;\n artist?: string;\n artwork?: string;\n duration?: number;\n loudness?: {\n measured_i: number;\n target_i: number;\n }\n}\n\nexport type OrpheusEvents = {\n onPlaybackStateChanged(event: { state: PlaybackState }): void;\n onTrackStarted(event: { trackId: string; reason: TransitionReason }): void;\n onTrackFinished(event: {\n trackId: string;\n finalPosition: number;\n duration: number;\n }): void;\n onPlayerError(event: { code: string; message: string }): void;\n onPositionUpdate(event: {\n position: number;\n duration: number;\n buffered: number;\n }): void;\n onIsPlayingChanged(event: { status: boolean }): void;\n};\n\ndeclare class OrpheusModule extends NativeModule<OrpheusEvents> {\n \n restorePlaybackPositionEnabled: boolean;\n\n /**\n * 获取当前进度(秒)\n */\n getPosition(): Promise<number>;\n\n /**\n * 获取总时长(秒)\n */\n getDuration(): Promise<number>;\n\n /**\n * 获取缓冲进度(秒)\n */\n getBuffered(): Promise<number>;\n\n /**\n * 获取是否正在播放\n */\n getIsPlaying(): Promise<boolean>;\n\n /**\n * 获取当前播放索引\n */\n getCurrentIndex(): Promise<number>;\n\n /**\n * 获取当前播放的 Track 对象\n */\n getCurrentTrack(): Promise<Track | null>;\n\n /**\n * 获取随机模式状态\n */\n getShuffleMode(): Promise<boolean>;\n\n /**\n * 获取指定索引的 Track\n */\n getIndexTrack(index: number): Promise<Track | null>;\n\n getRepeatMode(): Promise<RepeatMode>;\n\n setBilibiliCookie(cookie: string): void;\n \n setRestorePlaybackPositionEnabled(enabled: boolean): void;\n\n play(): Promise<void>;\n\n pause(): Promise<void>;\n\n clear(): Promise<void>;\n\n skipTo(index: number): Promise<void>;\n\n skipToNext(): Promise<void>;\n\n skipToPrevious(): Promise<void>;\n\n /**\n * 跳转进度\n * @param seconds 秒数\n */\n seekTo(seconds: number): Promise<void>;\n\n setRepeatMode(mode: RepeatMode): Promise<void>;\n\n setShuffleMode(enabled: boolean): Promise<void>;\n\n getQueue(): Promise<Track[]>;\n\n /**\n * 添加到队列末尾,且不去重。\n * @param tracks\n * @param startFromId 可选,添加后立即播放该 ID 的曲目\n * @param clearQueue 可选,是否清空当前队列\n */\n addToEnd(\n tracks: Track[],\n startFromId?: string,\n clearQueue?: boolean\n ): Promise<void>;\n\n /**\n * 播放下一首\n * @param track\n */\n playNext(track: Track): Promise<void>;\n\n removeTrack(index: number): Promise<void>;\n\n /**\n * 设置睡眠定时器\n * @param durationMs 单位毫秒\n */\n setSleepTimer(durationMs: number): Promise<void>;\n\n /**\n * 获取睡眠定时器结束时间\n * @returns 单位毫秒,如果没有设置则返回 null\n */\n getSleepTimerEndTime(): Promise<number | null>;\n\n cancelSleepTimer(): Promise<void>;\n}\n\nexport const Orpheus = requireNativeModule<OrpheusModule>(\"Orpheus\");\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"useCurrentTrack.d.ts","sourceRoot":"","sources":["../../src/hooks/useCurrentTrack.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAW,MAAM,sBAAsB,CAAC;AAEtD,wBAAgB,eAAe;;;EA0C9B"}
1
+ {"version":3,"file":"useCurrentTrack.d.ts","sourceRoot":"","sources":["../../src/hooks/useCurrentTrack.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAW,MAAM,sBAAsB,CAAC;AAEtD,wBAAgB,eAAe;;;EA4C9B"}
@@ -9,6 +9,7 @@ export function useCurrentTrack() {
9
9
  Orpheus.getCurrentTrack(),
10
10
  Orpheus.getCurrentIndex(),
11
11
  ]);
12
+ console.log(currentTrack);
12
13
  return { currentTrack, currentIndex };
13
14
  }
14
15
  catch (e) {
@@ -24,7 +25,8 @@ export function useCurrentTrack() {
24
25
  setIndex(currentIndex);
25
26
  }
26
27
  });
27
- const sub = Orpheus.addListener("onTrackTransition", async () => {
28
+ const sub = Orpheus.addListener("onTrackStarted", async () => {
29
+ console.log("Track Started");
28
30
  const { currentTrack, currentIndex } = await fetchTrack();
29
31
  if (isMounted) {
30
32
  setTrack(currentTrack);
@@ -1 +1 @@
1
- {"version":3,"file":"useCurrentTrack.js","sourceRoot":"","sources":["../../src/hooks/useCurrentTrack.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAC5C,OAAO,EAAS,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAEtD,MAAM,UAAU,eAAe;IAC7B,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAe,IAAI,CAAC,CAAC;IACvD,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAS,CAAC,CAAC,CAAC,CAAC;IAE/C,MAAM,UAAU,GAAG,KAAK,IAAI,EAAE;QAC5B,IAAI,CAAC;YACH,MAAM,CAAC,YAAY,EAAE,YAAY,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;gBACrD,OAAO,CAAC,eAAe,EAAE;gBACzB,OAAO,CAAC,eAAe,EAAE;aAC1B,CAAC,CAAC;YACH,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,CAAC;QACxC,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,CAAC,IAAI,CAAC,+BAA+B,EAAE,CAAC,CAAC,CAAC;YACjD,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC,EAAE,CAAC;QAClD,CAAC;IACH,CAAC,CAAC;IAEF,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,SAAS,GAAG,IAAI,CAAC;QAErB,UAAU,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,YAAY,EAAE,YAAY,EAAE,EAAE,EAAE;YACnD,IAAI,SAAS,EAAE,CAAC;gBACd,QAAQ,CAAC,YAAY,CAAC,CAAC;gBACvB,QAAQ,CAAC,YAAY,CAAC,CAAC;YACzB,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,MAAM,GAAG,GAAG,OAAO,CAAC,WAAW,CAAC,mBAAmB,EAAE,KAAK,IAAI,EAAE;YAC9D,MAAM,EAAE,YAAY,EAAE,YAAY,EAAE,GAAG,MAAM,UAAU,EAAE,CAAC;YAC1D,IAAI,SAAS,EAAE,CAAC;gBACd,QAAQ,CAAC,YAAY,CAAC,CAAC;gBACvB,QAAQ,CAAC,YAAY,CAAC,CAAC;YACzB,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,OAAO,GAAG,EAAE;YACV,SAAS,GAAG,KAAK,CAAC;YAClB,GAAG,CAAC,MAAM,EAAE,CAAC;QACf,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAC1B,CAAC","sourcesContent":["import { useState, useEffect } from \"react\";\nimport { Track, Orpheus } from \"../ExpoOrpheusModule\";\n\nexport function useCurrentTrack() {\n const [track, setTrack] = useState<Track | null>(null);\n const [index, setIndex] = useState<number>(-1);\n\n const fetchTrack = async () => {\n try {\n const [currentTrack, currentIndex] = await Promise.all([\n Orpheus.getCurrentTrack(),\n Orpheus.getCurrentIndex(),\n ]);\n return { currentTrack, currentIndex };\n } catch (e) {\n console.warn(\"Failed to fetch current track\", e);\n return { currentTrack: null, currentIndex: -1 };\n }\n };\n\n useEffect(() => {\n let isMounted = true;\n\n fetchTrack().then(({ currentTrack, currentIndex }) => {\n if (isMounted) {\n setTrack(currentTrack);\n setIndex(currentIndex);\n }\n });\n\n const sub = Orpheus.addListener(\"onTrackTransition\", async () => {\n const { currentTrack, currentIndex } = await fetchTrack();\n if (isMounted) {\n setTrack(currentTrack);\n setIndex(currentIndex);\n }\n });\n\n return () => {\n isMounted = false;\n sub.remove();\n };\n }, []);\n\n return { track, index };\n}\n"]}
1
+ {"version":3,"file":"useCurrentTrack.js","sourceRoot":"","sources":["../../src/hooks/useCurrentTrack.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAC5C,OAAO,EAAS,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAEtD,MAAM,UAAU,eAAe;IAC7B,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAe,IAAI,CAAC,CAAC;IACvD,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAS,CAAC,CAAC,CAAC,CAAC;IAE/C,MAAM,UAAU,GAAG,KAAK,IAAI,EAAE;QAC5B,IAAI,CAAC;YACH,MAAM,CAAC,YAAY,EAAE,YAAY,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;gBACrD,OAAO,CAAC,eAAe,EAAE;gBACzB,OAAO,CAAC,eAAe,EAAE;aAC1B,CAAC,CAAC;YACH,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;YACzB,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,CAAC;QACxC,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,CAAC,IAAI,CAAC,+BAA+B,EAAE,CAAC,CAAC,CAAC;YACjD,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC,EAAE,CAAC;QAClD,CAAC;IACH,CAAC,CAAC;IAEF,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,SAAS,GAAG,IAAI,CAAC;QAErB,UAAU,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,YAAY,EAAE,YAAY,EAAE,EAAE,EAAE;YACnD,IAAI,SAAS,EAAE,CAAC;gBACd,QAAQ,CAAC,YAAY,CAAC,CAAC;gBACvB,QAAQ,CAAC,YAAY,CAAC,CAAC;YACzB,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,MAAM,GAAG,GAAG,OAAO,CAAC,WAAW,CAAC,gBAAgB,EAAE,KAAK,IAAI,EAAE;YAC3D,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;YAC7B,MAAM,EAAE,YAAY,EAAE,YAAY,EAAE,GAAG,MAAM,UAAU,EAAE,CAAC;YAC1D,IAAI,SAAS,EAAE,CAAC;gBACd,QAAQ,CAAC,YAAY,CAAC,CAAC;gBACvB,QAAQ,CAAC,YAAY,CAAC,CAAC;YACzB,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,OAAO,GAAG,EAAE;YACV,SAAS,GAAG,KAAK,CAAC;YAClB,GAAG,CAAC,MAAM,EAAE,CAAC;QACf,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAC1B,CAAC","sourcesContent":["import { useState, useEffect } from \"react\";\nimport { Track, Orpheus } from \"../ExpoOrpheusModule\";\n\nexport function useCurrentTrack() {\n const [track, setTrack] = useState<Track | null>(null);\n const [index, setIndex] = useState<number>(-1);\n\n const fetchTrack = async () => {\n try {\n const [currentTrack, currentIndex] = await Promise.all([\n Orpheus.getCurrentTrack(),\n Orpheus.getCurrentIndex(),\n ]);\n console.log(currentTrack)\n return { currentTrack, currentIndex };\n } catch (e) {\n console.warn(\"Failed to fetch current track\", e);\n return { currentTrack: null, currentIndex: -1 };\n }\n };\n\n useEffect(() => {\n let isMounted = true;\n\n fetchTrack().then(({ currentTrack, currentIndex }) => {\n if (isMounted) {\n setTrack(currentTrack);\n setIndex(currentIndex);\n }\n });\n\n const sub = Orpheus.addListener(\"onTrackStarted\", async () => {\n console.log(\"Track Started\");\n const { currentTrack, currentIndex } = await fetchTrack();\n if (isMounted) {\n setTrack(currentTrack);\n setIndex(currentIndex);\n }\n });\n\n return () => {\n isMounted = false;\n sub.remove();\n };\n }, []);\n\n return { track, index };\n}\n"]}
@@ -8,7 +8,7 @@ export function useShuffleMode() {
8
8
  };
9
9
  useEffect(() => {
10
10
  refresh();
11
- const sub = Orpheus.addListener("onTrackTransition", refresh);
11
+ const sub = Orpheus.addListener("onTrackStarted", refresh);
12
12
  return () => sub.remove();
13
13
  }, []);
14
14
  const toggleShuffle = async () => {
@@ -1 +1 @@
1
- {"version":3,"file":"useShuffleMode.js","sourceRoot":"","sources":["../../src/hooks/useShuffleMode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAE/C,MAAM,UAAU,cAAc;IAC5B,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAEtD,MAAM,OAAO,GAAG,KAAK,IAAI,EAAE;QACzB,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,cAAc,EAAE,CAAC;QAC3C,cAAc,CAAC,GAAG,CAAC,CAAC;IACtB,CAAC,CAAC;IAEF,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,EAAE,CAAC;QACV,MAAM,GAAG,GAAG,OAAO,CAAC,WAAW,CAAC,mBAAmB,EAAE,OAAO,CAAC,CAAC;QAC9D,OAAO,GAAG,EAAE,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;IAC5B,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,aAAa,GAAG,KAAK,IAAI,EAAE;QAC/B,MAAM,MAAM,GAAG,CAAC,WAAW,CAAC;QAC5B,cAAc,CAAC,MAAM,CAAC,CAAC;QACvB,MAAM,OAAO,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;QACrC,OAAO,EAAE,CAAC;IACZ,CAAC,CAAC;IAEF,OAAO,CAAC,WAAW,EAAE,aAAa,CAAU,CAAC;AAC/C,CAAC","sourcesContent":["import { useState, useEffect } from \"react\";\nimport { Orpheus } from \"../ExpoOrpheusModule\";\n\nexport function useShuffleMode() {\n const [shuffleMode, setShuffleMode] = useState(false);\n\n const refresh = async () => {\n const val = await Orpheus.getShuffleMode();\n setShuffleMode(val);\n };\n\n useEffect(() => {\n refresh();\n const sub = Orpheus.addListener(\"onTrackTransition\", refresh);\n return () => sub.remove();\n }, []);\n\n const toggleShuffle = async () => {\n const newVal = !shuffleMode;\n setShuffleMode(newVal);\n await Orpheus.setShuffleMode(newVal);\n refresh();\n };\n\n return [shuffleMode, toggleShuffle] as const;\n}\n"]}
1
+ {"version":3,"file":"useShuffleMode.js","sourceRoot":"","sources":["../../src/hooks/useShuffleMode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAE/C,MAAM,UAAU,cAAc;IAC5B,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAEtD,MAAM,OAAO,GAAG,KAAK,IAAI,EAAE;QACzB,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,cAAc,EAAE,CAAC;QAC3C,cAAc,CAAC,GAAG,CAAC,CAAC;IACtB,CAAC,CAAC;IAEF,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,EAAE,CAAC;QACV,MAAM,GAAG,GAAG,OAAO,CAAC,WAAW,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;QAC3D,OAAO,GAAG,EAAE,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;IAC5B,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,aAAa,GAAG,KAAK,IAAI,EAAE;QAC/B,MAAM,MAAM,GAAG,CAAC,WAAW,CAAC;QAC5B,cAAc,CAAC,MAAM,CAAC,CAAC;QACvB,MAAM,OAAO,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;QACrC,OAAO,EAAE,CAAC;IACZ,CAAC,CAAC;IAEF,OAAO,CAAC,WAAW,EAAE,aAAa,CAAU,CAAC;AAC/C,CAAC","sourcesContent":["import { useState, useEffect } from \"react\";\nimport { Orpheus } from \"../ExpoOrpheusModule\";\n\nexport function useShuffleMode() {\n const [shuffleMode, setShuffleMode] = useState(false);\n\n const refresh = async () => {\n const val = await Orpheus.getShuffleMode();\n setShuffleMode(val);\n };\n\n useEffect(() => {\n refresh();\n const sub = Orpheus.addListener(\"onTrackStarted\", refresh);\n return () => sub.remove();\n }, []);\n\n const toggleShuffle = async () => {\n const newVal = !shuffleMode;\n setShuffleMode(newVal);\n await Orpheus.setShuffleMode(newVal);\n refresh();\n };\n\n return [shuffleMode, toggleShuffle] as const;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roitium/expo-orpheus",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "A player for bbplayer",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -27,15 +27,19 @@ export interface Track {
27
27
  artist?: string;
28
28
  artwork?: string;
29
29
  duration?: number;
30
- [key: string]: any;
30
+ loudness?: {
31
+ measured_i: number;
32
+ target_i: number;
33
+ }
31
34
  }
32
35
 
33
36
  export type OrpheusEvents = {
34
37
  onPlaybackStateChanged(event: { state: PlaybackState }): void;
35
- onTrackTransition(event: {
36
- currentTrackId: string;
37
- previousTrackId?: string;
38
- reason: TransitionReason;
38
+ onTrackStarted(event: { trackId: string; reason: TransitionReason }): void;
39
+ onTrackFinished(event: {
40
+ trackId: string;
41
+ finalPosition: number;
42
+ duration: number;
39
43
  }): void;
40
44
  onPlayerError(event: { code: string; message: string }): void;
41
45
  onPositionUpdate(event: {
@@ -47,6 +51,9 @@ export type OrpheusEvents = {
47
51
  };
48
52
 
49
53
  declare class OrpheusModule extends NativeModule<OrpheusEvents> {
54
+
55
+ restorePlaybackPositionEnabled: boolean;
56
+
50
57
  /**
51
58
  * 获取当前进度(秒)
52
59
  */
@@ -90,6 +97,8 @@ declare class OrpheusModule extends NativeModule<OrpheusEvents> {
90
97
  getRepeatMode(): Promise<RepeatMode>;
91
98
 
92
99
  setBilibiliCookie(cookie: string): void;
100
+
101
+ setRestorePlaybackPositionEnabled(enabled: boolean): void;
93
102
 
94
103
  play(): Promise<void>;
95
104
 
@@ -121,7 +130,11 @@ declare class OrpheusModule extends NativeModule<OrpheusEvents> {
121
130
  * @param startFromId 可选,添加后立即播放该 ID 的曲目
122
131
  * @param clearQueue 可选,是否清空当前队列
123
132
  */
124
- addToEnd(tracks: Track[], startFromId?: string, clearQueue?: boolean): Promise<void>;
133
+ addToEnd(
134
+ tracks: Track[],
135
+ startFromId?: string,
136
+ clearQueue?: boolean
137
+ ): Promise<void>;
125
138
 
126
139
  /**
127
140
  * 播放下一首
@@ -130,6 +143,20 @@ declare class OrpheusModule extends NativeModule<OrpheusEvents> {
130
143
  playNext(track: Track): Promise<void>;
131
144
 
132
145
  removeTrack(index: number): Promise<void>;
146
+
147
+ /**
148
+ * 设置睡眠定时器
149
+ * @param durationMs 单位毫秒
150
+ */
151
+ setSleepTimer(durationMs: number): Promise<void>;
152
+
153
+ /**
154
+ * 获取睡眠定时器结束时间
155
+ * @returns 单位毫秒,如果没有设置则返回 null
156
+ */
157
+ getSleepTimerEndTime(): Promise<number | null>;
158
+
159
+ cancelSleepTimer(): Promise<void>;
133
160
  }
134
161
 
135
162
  export const Orpheus = requireNativeModule<OrpheusModule>("Orpheus");
@@ -11,6 +11,7 @@ export function useCurrentTrack() {
11
11
  Orpheus.getCurrentTrack(),
12
12
  Orpheus.getCurrentIndex(),
13
13
  ]);
14
+ console.log(currentTrack)
14
15
  return { currentTrack, currentIndex };
15
16
  } catch (e) {
16
17
  console.warn("Failed to fetch current track", e);
@@ -28,7 +29,8 @@ export function useCurrentTrack() {
28
29
  }
29
30
  });
30
31
 
31
- const sub = Orpheus.addListener("onTrackTransition", async () => {
32
+ const sub = Orpheus.addListener("onTrackStarted", async () => {
33
+ console.log("Track Started");
32
34
  const { currentTrack, currentIndex } = await fetchTrack();
33
35
  if (isMounted) {
34
36
  setTrack(currentTrack);
@@ -11,7 +11,7 @@ export function useShuffleMode() {
11
11
 
12
12
  useEffect(() => {
13
13
  refresh();
14
- const sub = Orpheus.addListener("onTrackTransition", refresh);
14
+ const sub = Orpheus.addListener("onTrackStarted", refresh);
15
15
  return () => sub.remove();
16
16
  }, []);
17
17
 
@@ -1,68 +0,0 @@
1
- package expo.modules.orpheus
2
-
3
- import android.os.Handler
4
- import android.os.Looper
5
- import android.os.SystemClock
6
- import androidx.media3.common.Player
7
-
8
- class SleepTimerManager(private val player: Player) {
9
- private val handler = Handler(Looper.getMainLooper())
10
-
11
- private var targetTimeRaw: Long = 0
12
- private var isTimerActive = false
13
-
14
- private val checkRunnable = object : Runnable {
15
- override fun run() {
16
- if (!isTimerActive) return
17
-
18
- val now = SystemClock.elapsedRealtime()
19
- val timeLeft = targetTimeRaw - now
20
-
21
- if (timeLeft <= 0) {
22
- performStop()
23
- } else {
24
- handler.postDelayed(this, timeLeft)
25
- }
26
- }
27
- }
28
-
29
- /**
30
- * 开启定时器
31
- * @param duration 多少秒后停止
32
- */
33
- fun start(duration: Long) {
34
- if (duration <= 0) {
35
- cancel()
36
- return
37
- }
38
-
39
- targetTimeRaw = SystemClock.elapsedRealtime() + (duration.times(1000L))
40
- isTimerActive = true
41
-
42
- handler.removeCallbacks(checkRunnable)
43
- handler.postDelayed(checkRunnable, duration)
44
- }
45
-
46
- /**
47
- * 取消定时器
48
- */
49
- fun cancel() {
50
- isTimerActive = false
51
- handler.removeCallbacks(checkRunnable)
52
- targetTimeRaw = 0
53
- }
54
-
55
- /**
56
- * 获取剩余时间(单位:秒)
57
- */
58
- fun getRemainingTime(): Long {
59
- if (!isTimerActive) return -1L
60
- val remaining = (targetTimeRaw - SystemClock.elapsedRealtime()).div(1000L)
61
- return if (remaining > 0) remaining else 0
62
- }
63
-
64
- private fun performStop() {
65
- isTimerActive = false
66
- player.pause()
67
- }
68
- }