@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.
- package/android/build.gradle +1 -0
- package/android/src/main/java/expo/modules/orpheus/CustomCommands.kt +12 -0
- package/android/src/main/java/expo/modules/orpheus/ExpoOrpheusModule.kt +154 -83
- package/android/src/main/java/expo/modules/orpheus/OrpheusService.kt +118 -4
- package/android/src/main/java/expo/modules/orpheus/exceptions.kt +9 -0
- package/android/src/main/java/expo/modules/orpheus/{TrackRecord.kt → models/TrackRecord.kt} +11 -3
- package/android/src/main/java/expo/modules/orpheus/utils/MediaItemStorer.kt +93 -0
- package/android/src/main/java/expo/modules/orpheus/utils/SleepTimeController.kt +63 -0
- package/android/src/main/java/expo/modules/orpheus/utils/TrackRecordExtension.kt +35 -0
- package/build/ExpoOrpheusModule.d.ts +24 -4
- package/build/ExpoOrpheusModule.d.ts.map +1 -1
- package/build/ExpoOrpheusModule.js.map +1 -1
- package/build/hooks/useCurrentTrack.d.ts.map +1 -1
- package/build/hooks/useCurrentTrack.js +3 -1
- package/build/hooks/useCurrentTrack.js.map +1 -1
- package/build/hooks/useShuffleMode.js +1 -1
- package/build/hooks/useShuffleMode.js.map +1 -1
- package/package.json +1 -1
- package/src/ExpoOrpheusModule.ts +33 -6
- package/src/hooks/useCurrentTrack.ts +3 -1
- package/src/hooks/useShuffleMode.ts +1 -1
- package/android/src/main/java/expo/modules/orpheus/SleepTimeController.kt +0 -68
package/android/build.gradle
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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("
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
|
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
|
|
311
|
-
|
|
338
|
+
val newId = mediaItem?.mediaId ?: ""
|
|
339
|
+
Log.e("Orpheus", "onMediaItemTransition: $reason")
|
|
312
340
|
|
|
313
341
|
sendEvent(
|
|
314
|
-
"
|
|
315
|
-
"
|
|
316
|
-
"previousTrackId" to lastMediaId, // 上一首歌是什么
|
|
342
|
+
"onTrackStarted", mapOf(
|
|
343
|
+
"trackId" to newId,
|
|
317
344
|
"reason" to reason
|
|
318
345
|
)
|
|
319
346
|
)
|
|
320
347
|
|
|
321
|
-
|
|
322
|
-
|
|
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
|
|
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
|
-
|
|
450
|
+
mainHandler.removeCallbacks(progressSendEventRunnable)
|
|
451
|
+
mainHandler.removeCallbacks(progressSaveRunnable)
|
|
452
|
+
mainHandler.post(progressSaveRunnable)
|
|
453
|
+
mainHandler.post(progressSendEventRunnable)
|
|
391
454
|
} else {
|
|
392
|
-
|
|
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
|
|
125
|
-
.
|
|
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(
|
|
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
|
+
}
|
|
@@ -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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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":"AAAA,OAAO,EAAuB,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtE,oBAAY,aAAa;IACvB,IAAI,IAAI;IACR,SAAS,IAAI;IACb,KAAK,IAAI;IACT,KAAK,IAAI;CACV;AAED,oBAAY,UAAU;IACpB,GAAG,IAAI;IACP,KAAK,IAAI;IACT,KAAK,IAAI;CACV;AAED,oBAAY,gBAAgB;IAC1B,MAAM,IAAI;IACV,IAAI,IAAI;IACR,IAAI,IAAI;IACR,gBAAgB,IAAI;CACrB;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,CAAC,
|
|
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;
|
|
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;;;
|
|
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("
|
|
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,
|
|
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("
|
|
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,
|
|
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
package/src/ExpoOrpheusModule.ts
CHANGED
|
@@ -27,15 +27,19 @@ export interface Track {
|
|
|
27
27
|
artist?: string;
|
|
28
28
|
artwork?: string;
|
|
29
29
|
duration?: number;
|
|
30
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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(
|
|
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("
|
|
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
|
-
}
|