@roitium/expo-orpheus 0.2.4 → 0.3.1

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,26 @@ 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
12
+ import androidx.media3.common.Timeline
13
13
  import androidx.media3.session.MediaController
14
+ import androidx.media3.session.SessionCommand
15
+ import androidx.media3.session.SessionResult
14
16
  import androidx.media3.session.SessionToken
15
17
  import com.google.common.util.concurrent.ListenableFuture
16
18
  import com.google.common.util.concurrent.MoreExecutors
17
19
  import com.google.gson.Gson
20
+ import expo.modules.kotlin.exception.CodedException
18
21
  import expo.modules.kotlin.functions.Queues
19
22
  import expo.modules.kotlin.modules.Module
20
23
  import expo.modules.kotlin.modules.ModuleDefinition
24
+ import expo.modules.orpheus.models.TrackRecord
25
+ import expo.modules.orpheus.utils.MediaItemStorer
26
+ import expo.modules.orpheus.utils.toMediaItem
21
27
 
22
28
  class ExpoOrpheusModule : Module() {
23
29
  private var controllerFuture: ListenableFuture<MediaController>? = null
@@ -29,10 +35,7 @@ class ExpoOrpheusModule : Module() {
29
35
  // 记录上一首歌曲的 ID,用于在切歌时发送给 JS
30
36
  private var lastMediaId: String? = null
31
37
 
32
- private var currentTrackDuration: Long = 0L
33
-
34
- // 用来暂存上一首歌曲切歌瞬间的进度
35
- private var lastTrackFinalPosition: Long = 0L
38
+ private val durationCache = mutableMapOf<String, Long>()
36
39
 
37
40
  val gson = Gson()
38
41
 
@@ -50,6 +53,7 @@ class ExpoOrpheusModule : Module() {
50
53
 
51
54
  OnCreate {
52
55
  val context = appContext.reactContext ?: return@OnCreate
56
+ MediaItemStorer.initialize(context)
53
57
  val sessionToken = SessionToken(
54
58
  context,
55
59
  ComponentName(context, OrpheusService::class.java)
@@ -68,32 +72,51 @@ class ExpoOrpheusModule : Module() {
68
72
  }
69
73
 
70
74
  OnDestroy {
71
- stopProgressUpdater()
75
+ mainHandler.removeCallbacks(progressSendEventRunnable)
76
+ mainHandler.removeCallbacks(progressSaveRunnable)
72
77
  controllerFuture?.let { MediaController.releaseFuture(it) }
73
78
  }
74
79
 
80
+ Constant("restorePlaybackPositionEnabled") {
81
+ MediaItemStorer.isRestoreEnabled()
82
+ }
83
+
84
+ Function("setBilibiliCookie") { cookie: String ->
85
+ OrpheusConfig.bilibiliCookie = cookie
86
+ }
87
+
88
+ Function("setRestorePlaybackPositionEnabled") { enabled: Boolean ->
89
+ MediaItemStorer.setRestoreEnabled(enabled)
90
+ }
91
+
75
92
  AsyncFunction("getPosition") {
93
+ checkController()
76
94
  controller?.currentPosition?.toDouble()?.div(1000.0) ?: 0.0
77
95
  }.runOnQueue(Queues.MAIN)
78
96
 
79
97
  AsyncFunction("getDuration") {
98
+ checkController()
80
99
  val d = controller?.duration ?: C.TIME_UNSET
81
100
  if (d == C.TIME_UNSET) 0.0 else d.toDouble() / 1000.0
82
101
  }.runOnQueue(Queues.MAIN)
83
102
 
84
103
  AsyncFunction("getBuffered") {
104
+ checkController()
85
105
  controller?.bufferedPosition?.toDouble()?.div(1000.0) ?: 0.0
86
106
  }.runOnQueue(Queues.MAIN)
87
107
 
88
108
  AsyncFunction("getIsPlaying") {
109
+ checkController()
89
110
  controller?.isPlaying ?: false
90
111
  }.runOnQueue(Queues.MAIN)
91
112
 
92
113
  AsyncFunction("getCurrentIndex") {
114
+ checkController()
93
115
  controller?.currentMediaItemIndex ?: -1
94
116
  }.runOnQueue(Queues.MAIN)
95
117
 
96
118
  AsyncFunction("getCurrentTrack") {
119
+ checkController()
97
120
  val player = controller ?: return@AsyncFunction null
98
121
  val currentItem = player.currentMediaItem ?: return@AsyncFunction null
99
122
 
@@ -101,10 +124,12 @@ class ExpoOrpheusModule : Module() {
101
124
  }.runOnQueue(Queues.MAIN)
102
125
 
103
126
  AsyncFunction("getShuffleMode") {
127
+ checkController()
104
128
  controller?.shuffleModeEnabled
105
129
  }.runOnQueue(Queues.MAIN)
106
130
 
107
131
  AsyncFunction("getIndexTrack") { index: Int ->
132
+ checkController()
108
133
  val player = controller ?: return@AsyncFunction null
109
134
 
110
135
  if (index < 0 || index >= player.mediaItemCount) {
@@ -116,57 +141,49 @@ class ExpoOrpheusModule : Module() {
116
141
  mediaItemToTrackRecord(item)
117
142
  }.runOnQueue(Queues.MAIN)
118
143
 
119
- Function("setBilibiliCookie") { cookie: String ->
120
- OrpheusConfig.bilibiliCookie = cookie
121
- }
122
-
123
144
  AsyncFunction("play") {
124
- val player = controller
125
- if (player != null) {
126
- // 获取 player 真正归属的 Looper
127
- val playerLooper = player.applicationLooper
128
-
129
- if (Looper.myLooper() == playerLooper) {
130
- player.play()
131
- } else {
132
- Handler(playerLooper).post {
133
- player.play()
134
- }
135
- }
136
- }
145
+ checkController()
146
+ controller?.play()
137
147
  }.runOnQueue(Queues.MAIN)
138
148
 
139
149
  AsyncFunction("pause") {
150
+ checkController()
140
151
  controller?.pause()
141
152
  }.runOnQueue(Queues.MAIN)
142
153
 
143
154
  AsyncFunction("clear") {
155
+ checkController()
144
156
  controller?.clearMediaItems()
145
157
  }.runOnQueue(Queues.MAIN)
146
158
 
147
159
  AsyncFunction("skipTo") { index: Int ->
148
160
  // 跳转到指定索引的开头
161
+ checkController()
149
162
  controller?.seekTo(index, C.TIME_UNSET)
150
163
  }.runOnQueue(Queues.MAIN)
151
164
 
152
165
  AsyncFunction("skipToNext") {
166
+ checkController()
153
167
  if (controller?.hasNextMediaItem() == true) {
154
168
  controller?.seekToNextMediaItem()
155
169
  }
156
170
  }.runOnQueue(Queues.MAIN)
157
171
 
158
172
  AsyncFunction("skipToPrevious") {
173
+ checkController()
159
174
  if (controller?.hasPreviousMediaItem() == true) {
160
175
  controller?.seekToPreviousMediaItem()
161
176
  }
162
177
  }.runOnQueue(Queues.MAIN)
163
178
 
164
179
  AsyncFunction("seekTo") { seconds: Double ->
180
+ checkController()
165
181
  val ms = (seconds * 1000).toLong()
166
182
  controller?.seekTo(ms)
167
183
  }.runOnQueue(Queues.MAIN)
168
184
 
169
185
  AsyncFunction("setRepeatMode") { mode: Int ->
186
+ checkController()
170
187
  // mode: 0=OFF, 1=TRACK, 2=QUEUE
171
188
  val repeatMode = when (mode) {
172
189
  1 -> Player.REPEAT_MODE_ONE
@@ -177,20 +194,24 @@ class ExpoOrpheusModule : Module() {
177
194
  }.runOnQueue(Queues.MAIN)
178
195
 
179
196
  AsyncFunction("setShuffleMode") { enabled: Boolean ->
197
+ checkController()
180
198
  controller?.shuffleModeEnabled = enabled
181
199
  }.runOnQueue(Queues.MAIN)
182
200
 
183
201
  AsyncFunction("getRepeatMode") {
202
+ checkController()
184
203
  controller?.repeatMode
185
204
  }.runOnQueue(Queues.MAIN)
186
205
 
187
206
  AsyncFunction("removeTrack") { index: Int ->
207
+ checkController()
188
208
  if (index >= 0 && index < (controller?.mediaItemCount ?: 0)) {
189
209
  controller?.removeMediaItem(index)
190
210
  }
191
211
  }
192
212
 
193
213
  AsyncFunction("getQueue") {
214
+ checkController()
194
215
  val player = controller ?: return@AsyncFunction emptyList<TrackRecord>()
195
216
  val count = player.mediaItemCount
196
217
  val queue = ArrayList<TrackRecord>(count)
@@ -203,33 +224,50 @@ class ExpoOrpheusModule : Module() {
203
224
  return@AsyncFunction queue
204
225
  }.runOnQueue(Queues.MAIN)
205
226
 
206
- AsyncFunction("addToEnd") { tracks: List<TrackRecord>, startFromId: String?, clearQueue: Boolean? ->
207
- val mediaItems = tracks.mapNotNull { track ->
208
- try {
209
- val trackJson = gson.toJson(track)
210
- val extras = Bundle()
211
- extras.putString("track_json", trackJson)
212
-
213
- val artUri =
214
- if (!track.artwork.isNullOrEmpty()) track.artwork!!.toUri() else null
215
-
216
- val metadata = MediaMetadata.Builder()
217
- .setTitle(track.title)
218
- .setArtist(track.artist)
219
- .setArtworkUri(artUri)
220
- .setExtras(extras)
221
- .build()
222
-
223
- MediaItem.Builder()
224
- .setMediaId(track.id)
225
- .setUri(track.url)
226
- .setMediaMetadata(metadata)
227
- .build()
228
- } catch (e: Exception) {
229
- e.printStackTrace()
230
- null
227
+ AsyncFunction("setSleepTimer") { durationMs: Long ->
228
+ checkController()
229
+ val command = SessionCommand(CustomCommands.CMD_START_TIMER, Bundle.EMPTY)
230
+ val args = Bundle().apply {
231
+ putLong(CustomCommands.KEY_DURATION, durationMs)
232
+ }
233
+
234
+ controller?.sendCustomCommand(command, args)
235
+ }.runOnQueue(Queues.MAIN)
236
+
237
+ AsyncFunction("getSleepTimerEndTime") {
238
+ checkController()
239
+
240
+ val command = SessionCommand(CustomCommands.CMD_GET_REMAINING, Bundle.EMPTY)
241
+ val future = controller!!.sendCustomCommand(command, Bundle.EMPTY)
242
+
243
+ val result = try {
244
+ future.get()
245
+ } catch (e: Exception) {
246
+ throw CodedException("ERR_EXECUTION_FAILED", e.message, e)
247
+ }
248
+
249
+ if (result.resultCode == SessionResult.RESULT_SUCCESS) {
250
+ val extras = result.extras
251
+ if (extras.containsKey(CustomCommands.KEY_STOP_TIME)) {
252
+ val stopTime = extras.getLong(CustomCommands.KEY_STOP_TIME)
253
+ return@AsyncFunction stopTime
231
254
  }
232
255
  }
256
+
257
+ return@AsyncFunction null
258
+ }.runOnQueue(Queues.MAIN)
259
+
260
+ AsyncFunction("cancelSleepTimer") {
261
+ checkController()
262
+ val command = SessionCommand(CustomCommands.CMD_CANCEL_TIMER, Bundle.EMPTY)
263
+ controller?.sendCustomCommand(command, Bundle.EMPTY)
264
+ }.runOnQueue(Queues.MAIN)
265
+
266
+ AsyncFunction("addToEnd") { tracks: List<TrackRecord>, startFromId: String?, clearQueue: Boolean? ->
267
+ checkController()
268
+ val mediaItems = tracks.map { track ->
269
+ track.toMediaItem(gson)
270
+ }
233
271
  val player = controller ?: return@AsyncFunction
234
272
  if (clearQueue == true) {
235
273
  player.clearMediaItems()
@@ -257,25 +295,10 @@ class ExpoOrpheusModule : Module() {
257
295
  }.runOnQueue(Queues.MAIN)
258
296
 
259
297
  AsyncFunction("playNext") { track: TrackRecord ->
298
+ checkController()
260
299
  val player = controller ?: return@AsyncFunction
261
300
 
262
- val trackJson = gson.toJson(track)
263
- val extras = Bundle()
264
- extras.putString("track_json", trackJson)
265
- val artUri = if (!track.artwork.isNullOrEmpty()) track.artwork!!.toUri() else null
266
-
267
- val metadata = MediaMetadata.Builder()
268
- .setTitle(track.title)
269
- .setArtist(track.artist)
270
- .setArtworkUri(artUri)
271
- .setExtras(extras)
272
- .build()
273
-
274
- val mediaItem = MediaItem.Builder()
275
- .setMediaId(track.id)
276
- .setUri(track.url)
277
- .setMediaMetadata(metadata)
278
- .build()
301
+ val mediaItem = track.toMediaItem(gson)
279
302
  val targetIndex = player.currentMediaItemIndex + 1
280
303
 
281
304
  var existingIndex = -1
@@ -314,6 +337,7 @@ class ExpoOrpheusModule : Module() {
314
337
  */
315
338
  override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
316
339
  val newId = mediaItem?.mediaId ?: ""
340
+ Log.e("Orpheus", "onMediaItemTransition: $reason")
317
341
 
318
342
  sendEvent(
319
343
  "onTrackStarted", mapOf(
@@ -323,7 +347,19 @@ class ExpoOrpheusModule : Module() {
323
347
  )
324
348
 
325
349
  lastMediaId = newId
326
- currentTrackDuration = 0L
350
+ saveCurrentPosition()
351
+ }
352
+
353
+ override fun onTimelineChanged(timeline: Timeline, reason: Int) {
354
+ val player = controller ?: return
355
+ val currentItem = player.currentMediaItem ?: return
356
+ val mediaId = currentItem.mediaId
357
+
358
+ val duration = player.duration
359
+
360
+ if (duration != C.TIME_UNSET && duration > 0) {
361
+ durationCache[mediaId] = duration
362
+ }
327
363
  }
328
364
 
329
365
  override fun onPositionDiscontinuity(
@@ -331,16 +367,18 @@ class ExpoOrpheusModule : Module() {
331
367
  newPosition: Player.PositionInfo,
332
368
  reason: Int
333
369
  ) {
334
- Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT
335
370
  if (oldPosition.mediaItemIndex != newPosition.mediaItemIndex) {
336
371
  val lastMediaItem =
337
372
  controller?.getMediaItemAt(oldPosition.mediaItemIndex) ?: return
338
373
 
374
+ // onPositionDiscontinuity 会被连续调用两次,且两次调用参数相同,很奇怪的行为,所以采用这种方式过滤.没值就直接返回,不发事件。
375
+ val duration = durationCache.remove(lastMediaItem.mediaId) ?: return
376
+
339
377
  sendEvent(
340
378
  "onTrackFinished", mapOf(
341
379
  "trackId" to lastMediaItem.mediaId,
342
380
  "finalPosition" to oldPosition.positionMs / 1000.0,
343
- "duration" to currentTrackDuration / 1000.0,
381
+ "duration" to duration / 1000.0,
344
382
  )
345
383
  )
346
384
  }
@@ -357,11 +395,6 @@ class ExpoOrpheusModule : Module() {
357
395
  )
358
396
  )
359
397
 
360
- if (state == Player.STATE_READY) {
361
- val d = controller?.duration
362
- if (d != C.TIME_UNSET && d != null) currentTrackDuration = d
363
- }
364
-
365
398
  updateProgressRunnerState()
366
399
  }
367
400
 
@@ -391,7 +424,7 @@ class ExpoOrpheusModule : Module() {
391
424
  })
392
425
  }
393
426
 
394
- private val progressRunnable = object : Runnable {
427
+ private val progressSendEventRunnable = object : Runnable {
395
428
  override fun run() {
396
429
  val player = controller ?: return
397
430
 
@@ -412,25 +445,27 @@ class ExpoOrpheusModule : Module() {
412
445
  }
413
446
  }
414
447
 
448
+ private val progressSaveRunnable = object : Runnable {
449
+ override fun run() {
450
+ saveCurrentPosition()
451
+ mainHandler.postDelayed(this, 5000)
452
+ }
453
+ }
454
+
415
455
  private fun updateProgressRunnerState() {
416
456
  val player = controller
417
457
  // 如果正在播放且状态是 READY,则开始轮询
418
458
  if (player != null && player.isPlaying && player.playbackState == Player.STATE_READY) {
419
- startProgressUpdater()
459
+ mainHandler.removeCallbacks(progressSendEventRunnable)
460
+ mainHandler.removeCallbacks(progressSaveRunnable)
461
+ mainHandler.post(progressSaveRunnable)
462
+ mainHandler.post(progressSendEventRunnable)
420
463
  } else {
421
- stopProgressUpdater()
464
+ mainHandler.removeCallbacks(progressSendEventRunnable)
465
+ mainHandler.removeCallbacks(progressSaveRunnable)
422
466
  }
423
467
  }
424
468
 
425
- private fun startProgressUpdater() {
426
- mainHandler.removeCallbacks(progressRunnable)
427
- mainHandler.post(progressRunnable)
428
- }
429
-
430
- private fun stopProgressUpdater() {
431
- mainHandler.removeCallbacks(progressRunnable)
432
- }
433
-
434
469
  private fun mediaItemToTrackRecord(item: MediaItem): TrackRecord {
435
470
  val extras = item.mediaMetadata.extras
436
471
  val trackJson = extras?.getString("track_json")
@@ -452,4 +487,20 @@ class ExpoOrpheusModule : Module() {
452
487
 
453
488
  return track
454
489
  }
490
+
491
+ private fun saveCurrentPosition() {
492
+ val player = controller ?: return
493
+ if (player.playbackState != Player.STATE_IDLE) {
494
+ MediaItemStorer.savePosition(
495
+ player.currentMediaItemIndex,
496
+ player.currentPosition
497
+ )
498
+ }
499
+ }
500
+
501
+ private fun checkController() {
502
+ if (controller == null) {
503
+ throw ControllerNotInitializedException()
504
+ }
505
+ }
455
506
  }
@@ -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,7 +23,10 @@ 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: {
@@ -52,6 +55,7 @@ export type OrpheusEvents = {
52
55
  }): void;
53
56
  };
54
57
  declare class OrpheusModule extends NativeModule<OrpheusEvents> {
58
+ restorePlaybackPositionEnabled: boolean;
55
59
  /**
56
60
  * 获取当前进度(秒)
57
61
  */
@@ -86,6 +90,7 @@ declare class OrpheusModule extends NativeModule<OrpheusEvents> {
86
90
  getIndexTrack(index: number): Promise<Track | null>;
87
91
  getRepeatMode(): Promise<RepeatMode>;
88
92
  setBilibiliCookie(cookie: string): void;
93
+ setRestorePlaybackPositionEnabled(enabled: boolean): void;
89
94
  play(): Promise<void>;
90
95
  pause(): Promise<void>;
91
96
  clear(): Promise<void>;
@@ -113,6 +118,17 @@ declare class OrpheusModule extends NativeModule<OrpheusEvents> {
113
118
  */
114
119
  playNext(track: Track): Promise<void>;
115
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>;
116
132
  }
117
133
  export declare const Orpheus: OrpheusModule;
118
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;AAuHD,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 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 * 获取当前进度(秒)\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(\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\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) {
@@ -25,6 +26,7 @@ export function useCurrentTrack() {
25
26
  }
26
27
  });
27
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,gBAAgB,EAAE,KAAK,IAAI,EAAE;YAC3D,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(\"onTrackStarted\", 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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roitium/expo-orpheus",
3
- "version": "0.2.4",
3
+ "version": "0.3.1",
4
4
  "description": "A player for bbplayer",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -27,7 +27,10 @@ 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 = {
@@ -48,6 +51,9 @@ export type OrpheusEvents = {
48
51
  };
49
52
 
50
53
  declare class OrpheusModule extends NativeModule<OrpheusEvents> {
54
+
55
+ restorePlaybackPositionEnabled: boolean;
56
+
51
57
  /**
52
58
  * 获取当前进度(秒)
53
59
  */
@@ -91,6 +97,8 @@ declare class OrpheusModule extends NativeModule<OrpheusEvents> {
91
97
  getRepeatMode(): Promise<RepeatMode>;
92
98
 
93
99
  setBilibiliCookie(cookie: string): void;
100
+
101
+ setRestorePlaybackPositionEnabled(enabled: boolean): void;
94
102
 
95
103
  play(): Promise<void>;
96
104
 
@@ -135,6 +143,20 @@ declare class OrpheusModule extends NativeModule<OrpheusEvents> {
135
143
  playNext(track: Track): Promise<void>;
136
144
 
137
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>;
138
160
  }
139
161
 
140
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);
@@ -29,6 +30,7 @@ export function useCurrentTrack() {
29
30
  });
30
31
 
31
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);
@@ -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
- }