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