@roitium/expo-orpheus 0.2.4 → 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 +119 -77
- 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 +17 -1
- 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 +2 -0
- package/build/hooks/useCurrentTrack.js.map +1 -1
- package/package.json +1 -1
- package/src/ExpoOrpheusModule.ts +23 -1
- package/src/hooks/useCurrentTrack.ts +2 -0
- 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
|
|
@@ -31,9 +36,6 @@ class ExpoOrpheusModule : Module() {
|
|
|
31
36
|
|
|
32
37
|
private var currentTrackDuration: Long = 0L
|
|
33
38
|
|
|
34
|
-
// 用来暂存上一首歌曲切歌瞬间的进度
|
|
35
|
-
private var lastTrackFinalPosition: Long = 0L
|
|
36
|
-
|
|
37
39
|
val gson = Gson()
|
|
38
40
|
|
|
39
41
|
override fun definition() = ModuleDefinition {
|
|
@@ -50,6 +52,7 @@ class ExpoOrpheusModule : Module() {
|
|
|
50
52
|
|
|
51
53
|
OnCreate {
|
|
52
54
|
val context = appContext.reactContext ?: return@OnCreate
|
|
55
|
+
MediaItemStorer.initialize(context)
|
|
53
56
|
val sessionToken = SessionToken(
|
|
54
57
|
context,
|
|
55
58
|
ComponentName(context, OrpheusService::class.java)
|
|
@@ -68,32 +71,51 @@ class ExpoOrpheusModule : Module() {
|
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
OnDestroy {
|
|
71
|
-
|
|
74
|
+
mainHandler.removeCallbacks(progressSendEventRunnable)
|
|
75
|
+
mainHandler.removeCallbacks(progressSaveRunnable)
|
|
72
76
|
controllerFuture?.let { MediaController.releaseFuture(it) }
|
|
73
77
|
}
|
|
74
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
|
+
|
|
75
91
|
AsyncFunction("getPosition") {
|
|
92
|
+
checkController()
|
|
76
93
|
controller?.currentPosition?.toDouble()?.div(1000.0) ?: 0.0
|
|
77
94
|
}.runOnQueue(Queues.MAIN)
|
|
78
95
|
|
|
79
96
|
AsyncFunction("getDuration") {
|
|
97
|
+
checkController()
|
|
80
98
|
val d = controller?.duration ?: C.TIME_UNSET
|
|
81
99
|
if (d == C.TIME_UNSET) 0.0 else d.toDouble() / 1000.0
|
|
82
100
|
}.runOnQueue(Queues.MAIN)
|
|
83
101
|
|
|
84
102
|
AsyncFunction("getBuffered") {
|
|
103
|
+
checkController()
|
|
85
104
|
controller?.bufferedPosition?.toDouble()?.div(1000.0) ?: 0.0
|
|
86
105
|
}.runOnQueue(Queues.MAIN)
|
|
87
106
|
|
|
88
107
|
AsyncFunction("getIsPlaying") {
|
|
108
|
+
checkController()
|
|
89
109
|
controller?.isPlaying ?: false
|
|
90
110
|
}.runOnQueue(Queues.MAIN)
|
|
91
111
|
|
|
92
112
|
AsyncFunction("getCurrentIndex") {
|
|
113
|
+
checkController()
|
|
93
114
|
controller?.currentMediaItemIndex ?: -1
|
|
94
115
|
}.runOnQueue(Queues.MAIN)
|
|
95
116
|
|
|
96
117
|
AsyncFunction("getCurrentTrack") {
|
|
118
|
+
checkController()
|
|
97
119
|
val player = controller ?: return@AsyncFunction null
|
|
98
120
|
val currentItem = player.currentMediaItem ?: return@AsyncFunction null
|
|
99
121
|
|
|
@@ -101,10 +123,12 @@ class ExpoOrpheusModule : Module() {
|
|
|
101
123
|
}.runOnQueue(Queues.MAIN)
|
|
102
124
|
|
|
103
125
|
AsyncFunction("getShuffleMode") {
|
|
126
|
+
checkController()
|
|
104
127
|
controller?.shuffleModeEnabled
|
|
105
128
|
}.runOnQueue(Queues.MAIN)
|
|
106
129
|
|
|
107
130
|
AsyncFunction("getIndexTrack") { index: Int ->
|
|
131
|
+
checkController()
|
|
108
132
|
val player = controller ?: return@AsyncFunction null
|
|
109
133
|
|
|
110
134
|
if (index < 0 || index >= player.mediaItemCount) {
|
|
@@ -116,57 +140,49 @@ class ExpoOrpheusModule : Module() {
|
|
|
116
140
|
mediaItemToTrackRecord(item)
|
|
117
141
|
}.runOnQueue(Queues.MAIN)
|
|
118
142
|
|
|
119
|
-
Function("setBilibiliCookie") { cookie: String ->
|
|
120
|
-
OrpheusConfig.bilibiliCookie = cookie
|
|
121
|
-
}
|
|
122
|
-
|
|
123
143
|
AsyncFunction("play") {
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
}
|
|
144
|
+
checkController()
|
|
145
|
+
controller?.play()
|
|
137
146
|
}.runOnQueue(Queues.MAIN)
|
|
138
147
|
|
|
139
148
|
AsyncFunction("pause") {
|
|
149
|
+
checkController()
|
|
140
150
|
controller?.pause()
|
|
141
151
|
}.runOnQueue(Queues.MAIN)
|
|
142
152
|
|
|
143
153
|
AsyncFunction("clear") {
|
|
154
|
+
checkController()
|
|
144
155
|
controller?.clearMediaItems()
|
|
145
156
|
}.runOnQueue(Queues.MAIN)
|
|
146
157
|
|
|
147
158
|
AsyncFunction("skipTo") { index: Int ->
|
|
148
159
|
// 跳转到指定索引的开头
|
|
160
|
+
checkController()
|
|
149
161
|
controller?.seekTo(index, C.TIME_UNSET)
|
|
150
162
|
}.runOnQueue(Queues.MAIN)
|
|
151
163
|
|
|
152
164
|
AsyncFunction("skipToNext") {
|
|
165
|
+
checkController()
|
|
153
166
|
if (controller?.hasNextMediaItem() == true) {
|
|
154
167
|
controller?.seekToNextMediaItem()
|
|
155
168
|
}
|
|
156
169
|
}.runOnQueue(Queues.MAIN)
|
|
157
170
|
|
|
158
171
|
AsyncFunction("skipToPrevious") {
|
|
172
|
+
checkController()
|
|
159
173
|
if (controller?.hasPreviousMediaItem() == true) {
|
|
160
174
|
controller?.seekToPreviousMediaItem()
|
|
161
175
|
}
|
|
162
176
|
}.runOnQueue(Queues.MAIN)
|
|
163
177
|
|
|
164
178
|
AsyncFunction("seekTo") { seconds: Double ->
|
|
179
|
+
checkController()
|
|
165
180
|
val ms = (seconds * 1000).toLong()
|
|
166
181
|
controller?.seekTo(ms)
|
|
167
182
|
}.runOnQueue(Queues.MAIN)
|
|
168
183
|
|
|
169
184
|
AsyncFunction("setRepeatMode") { mode: Int ->
|
|
185
|
+
checkController()
|
|
170
186
|
// mode: 0=OFF, 1=TRACK, 2=QUEUE
|
|
171
187
|
val repeatMode = when (mode) {
|
|
172
188
|
1 -> Player.REPEAT_MODE_ONE
|
|
@@ -177,20 +193,24 @@ class ExpoOrpheusModule : Module() {
|
|
|
177
193
|
}.runOnQueue(Queues.MAIN)
|
|
178
194
|
|
|
179
195
|
AsyncFunction("setShuffleMode") { enabled: Boolean ->
|
|
196
|
+
checkController()
|
|
180
197
|
controller?.shuffleModeEnabled = enabled
|
|
181
198
|
}.runOnQueue(Queues.MAIN)
|
|
182
199
|
|
|
183
200
|
AsyncFunction("getRepeatMode") {
|
|
201
|
+
checkController()
|
|
184
202
|
controller?.repeatMode
|
|
185
203
|
}.runOnQueue(Queues.MAIN)
|
|
186
204
|
|
|
187
205
|
AsyncFunction("removeTrack") { index: Int ->
|
|
206
|
+
checkController()
|
|
188
207
|
if (index >= 0 && index < (controller?.mediaItemCount ?: 0)) {
|
|
189
208
|
controller?.removeMediaItem(index)
|
|
190
209
|
}
|
|
191
210
|
}
|
|
192
211
|
|
|
193
212
|
AsyncFunction("getQueue") {
|
|
213
|
+
checkController()
|
|
194
214
|
val player = controller ?: return@AsyncFunction emptyList<TrackRecord>()
|
|
195
215
|
val count = player.mediaItemCount
|
|
196
216
|
val queue = ArrayList<TrackRecord>(count)
|
|
@@ -203,33 +223,50 @@ class ExpoOrpheusModule : Module() {
|
|
|
203
223
|
return@AsyncFunction queue
|
|
204
224
|
}.runOnQueue(Queues.MAIN)
|
|
205
225
|
|
|
206
|
-
AsyncFunction("
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
231
253
|
}
|
|
232
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
|
+
}
|
|
233
270
|
val player = controller ?: return@AsyncFunction
|
|
234
271
|
if (clearQueue == true) {
|
|
235
272
|
player.clearMediaItems()
|
|
@@ -257,25 +294,10 @@ class ExpoOrpheusModule : Module() {
|
|
|
257
294
|
}.runOnQueue(Queues.MAIN)
|
|
258
295
|
|
|
259
296
|
AsyncFunction("playNext") { track: TrackRecord ->
|
|
297
|
+
checkController()
|
|
260
298
|
val player = controller ?: return@AsyncFunction
|
|
261
299
|
|
|
262
|
-
val
|
|
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()
|
|
300
|
+
val mediaItem = track.toMediaItem(gson)
|
|
279
301
|
val targetIndex = player.currentMediaItemIndex + 1
|
|
280
302
|
|
|
281
303
|
var existingIndex = -1
|
|
@@ -314,6 +336,7 @@ class ExpoOrpheusModule : Module() {
|
|
|
314
336
|
*/
|
|
315
337
|
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
|
316
338
|
val newId = mediaItem?.mediaId ?: ""
|
|
339
|
+
Log.e("Orpheus", "onMediaItemTransition: $reason")
|
|
317
340
|
|
|
318
341
|
sendEvent(
|
|
319
342
|
"onTrackStarted", mapOf(
|
|
@@ -324,6 +347,7 @@ class ExpoOrpheusModule : Module() {
|
|
|
324
347
|
|
|
325
348
|
lastMediaId = newId
|
|
326
349
|
currentTrackDuration = 0L
|
|
350
|
+
saveCurrentPosition()
|
|
327
351
|
}
|
|
328
352
|
|
|
329
353
|
override fun onPositionDiscontinuity(
|
|
@@ -391,7 +415,7 @@ class ExpoOrpheusModule : Module() {
|
|
|
391
415
|
})
|
|
392
416
|
}
|
|
393
417
|
|
|
394
|
-
private val
|
|
418
|
+
private val progressSendEventRunnable = object : Runnable {
|
|
395
419
|
override fun run() {
|
|
396
420
|
val player = controller ?: return
|
|
397
421
|
|
|
@@ -412,25 +436,27 @@ class ExpoOrpheusModule : Module() {
|
|
|
412
436
|
}
|
|
413
437
|
}
|
|
414
438
|
|
|
439
|
+
private val progressSaveRunnable = object : Runnable {
|
|
440
|
+
override fun run() {
|
|
441
|
+
saveCurrentPosition()
|
|
442
|
+
mainHandler.postDelayed(this, 5000)
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
415
446
|
private fun updateProgressRunnerState() {
|
|
416
447
|
val player = controller
|
|
417
448
|
// 如果正在播放且状态是 READY,则开始轮询
|
|
418
449
|
if (player != null && player.isPlaying && player.playbackState == Player.STATE_READY) {
|
|
419
|
-
|
|
450
|
+
mainHandler.removeCallbacks(progressSendEventRunnable)
|
|
451
|
+
mainHandler.removeCallbacks(progressSaveRunnable)
|
|
452
|
+
mainHandler.post(progressSaveRunnable)
|
|
453
|
+
mainHandler.post(progressSendEventRunnable)
|
|
420
454
|
} else {
|
|
421
|
-
|
|
455
|
+
mainHandler.removeCallbacks(progressSendEventRunnable)
|
|
456
|
+
mainHandler.removeCallbacks(progressSaveRunnable)
|
|
422
457
|
}
|
|
423
458
|
}
|
|
424
459
|
|
|
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
460
|
private fun mediaItemToTrackRecord(item: MediaItem): TrackRecord {
|
|
435
461
|
val extras = item.mediaMetadata.extras
|
|
436
462
|
val trackJson = extras?.getString("track_json")
|
|
@@ -452,4 +478,20 @@ class ExpoOrpheusModule : Module() {
|
|
|
452
478
|
|
|
453
479
|
return track
|
|
454
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
|
+
}
|
|
455
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,7 +23,10 @@ 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: {
|
|
@@ -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":"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) {
|
|
@@ -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
package/src/ExpoOrpheusModule.ts
CHANGED
|
@@ -27,7 +27,10 @@ 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 = {
|
|
@@ -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
|
-
}
|