@roitium/expo-orpheus 0.5.1 → 0.7.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/README.md +4 -0
- package/android/src/main/java/expo/modules/orpheus/DownloadCache.kt +13 -0
- package/android/src/main/java/expo/modules/orpheus/ExpoOrpheusModule.kt +228 -255
- package/android/src/main/java/expo/modules/orpheus/OrpheusMusicService.kt +55 -89
- package/android/src/main/java/expo/modules/orpheus/utils/DownloadUtil.kt +10 -18
- package/android/src/main/java/expo/modules/orpheus/utils/{Storage.kt → GeneralStorage.kt} +1 -1
- package/android/src/main/java/expo/modules/orpheus/utils/LoudnessStorage.kt +40 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,6 +3,7 @@ package expo.modules.orpheus
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import androidx.media3.common.util.UnstableApi
|
|
5
5
|
import androidx.media3.database.StandaloneDatabaseProvider
|
|
6
|
+
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
|
|
6
7
|
import androidx.media3.datasource.cache.NoOpCacheEvictor
|
|
7
8
|
import androidx.media3.datasource.cache.SimpleCache
|
|
8
9
|
import java.io.File
|
|
@@ -10,6 +11,7 @@ import java.io.File
|
|
|
10
11
|
@UnstableApi
|
|
11
12
|
object DownloadCache {
|
|
12
13
|
private var stableCache: SimpleCache? = null
|
|
14
|
+
private var lruCache: SimpleCache? = null
|
|
13
15
|
|
|
14
16
|
@Synchronized
|
|
15
17
|
fun getStableCache(context: Context): SimpleCache {
|
|
@@ -21,4 +23,15 @@ object DownloadCache {
|
|
|
21
23
|
}
|
|
22
24
|
return stableCache!!
|
|
23
25
|
}
|
|
26
|
+
|
|
27
|
+
@Synchronized
|
|
28
|
+
fun getLruCache(context: Context): SimpleCache {
|
|
29
|
+
if (lruCache == null) {
|
|
30
|
+
val cacheDir = File(context.cacheDir, "media_cache_lru")
|
|
31
|
+
val evictor = LeastRecentlyUsedCacheEvictor(256 * 1024 * 1024)
|
|
32
|
+
val databaseProvider = StandaloneDatabaseProvider(context)
|
|
33
|
+
lruCache = SimpleCache(cacheDir, evictor, databaseProvider)
|
|
34
|
+
}
|
|
35
|
+
return lruCache!!
|
|
36
|
+
}
|
|
24
37
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
package expo.modules.orpheus
|
|
2
2
|
|
|
3
3
|
import android.content.ComponentName
|
|
4
|
-
import android.os.Bundle
|
|
5
4
|
import android.os.Handler
|
|
6
5
|
import android.os.Looper
|
|
7
6
|
import android.util.Log
|
|
@@ -18,26 +17,24 @@ import androidx.media3.exoplayer.offline.DownloadManager
|
|
|
18
17
|
import androidx.media3.exoplayer.offline.DownloadRequest
|
|
19
18
|
import androidx.media3.exoplayer.offline.DownloadService
|
|
20
19
|
import androidx.media3.session.MediaController
|
|
21
|
-
import androidx.media3.session.SessionCommand
|
|
22
|
-
import androidx.media3.session.SessionResult
|
|
23
20
|
import androidx.media3.session.SessionToken
|
|
24
21
|
import com.google.common.util.concurrent.ListenableFuture
|
|
25
|
-
import com.google.common.util.concurrent.MoreExecutors
|
|
26
22
|
import com.google.gson.Gson
|
|
27
|
-
import expo.modules.kotlin.exception.CodedException
|
|
28
23
|
import expo.modules.kotlin.functions.Queues
|
|
29
24
|
import expo.modules.kotlin.modules.Module
|
|
30
25
|
import expo.modules.kotlin.modules.ModuleDefinition
|
|
31
26
|
import expo.modules.orpheus.models.TrackRecord
|
|
32
27
|
import expo.modules.orpheus.utils.DownloadUtil
|
|
33
|
-
import expo.modules.orpheus.utils.
|
|
28
|
+
import expo.modules.orpheus.utils.GeneralStorage
|
|
29
|
+
import expo.modules.orpheus.utils.LoudnessStorage
|
|
34
30
|
import expo.modules.orpheus.utils.toMediaItem
|
|
35
31
|
|
|
36
32
|
@UnstableApi
|
|
37
33
|
class ExpoOrpheusModule : Module() {
|
|
34
|
+
// keep this controller only to make sure MediaLibraryService is init.
|
|
38
35
|
private var controllerFuture: ListenableFuture<MediaController>? = null
|
|
39
36
|
|
|
40
|
-
private var
|
|
37
|
+
private var player: Player? = null
|
|
41
38
|
|
|
42
39
|
private val mainHandler = Handler(Looper.getMainLooper())
|
|
43
40
|
|
|
@@ -51,6 +48,124 @@ class ExpoOrpheusModule : Module() {
|
|
|
51
48
|
|
|
52
49
|
val gson = Gson()
|
|
53
50
|
|
|
51
|
+
private val playerListener = object : Player.Listener {
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 核心:处理切歌、播放结束逻辑
|
|
55
|
+
*/
|
|
56
|
+
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
|
57
|
+
val newId = mediaItem?.mediaId ?: ""
|
|
58
|
+
Log.e("Orpheus", "onMediaItemTransition: $reason")
|
|
59
|
+
|
|
60
|
+
sendEvent(
|
|
61
|
+
"onTrackStarted", mapOf(
|
|
62
|
+
"trackId" to newId,
|
|
63
|
+
"reason" to reason
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
lastMediaId = newId
|
|
68
|
+
saveCurrentPosition()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
|
|
72
|
+
val p = player ?: return
|
|
73
|
+
val currentItem = p.currentMediaItem ?: return
|
|
74
|
+
val mediaId = currentItem.mediaId
|
|
75
|
+
|
|
76
|
+
val duration = p.duration
|
|
77
|
+
Log.d(
|
|
78
|
+
"Orpheus",
|
|
79
|
+
"onTimelineChanged: reason: $reason mediaId: $mediaId duration: $duration"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if (duration != C.TIME_UNSET && duration > 0) {
|
|
83
|
+
durationCache[mediaId] = duration
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
override fun onPositionDiscontinuity(
|
|
88
|
+
oldPosition: Player.PositionInfo,
|
|
89
|
+
newPosition: Player.PositionInfo,
|
|
90
|
+
reason: Int
|
|
91
|
+
) {
|
|
92
|
+
val isAutoTransition = reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION
|
|
93
|
+
val isIndexChanged = oldPosition.mediaItemIndex != newPosition.mediaItemIndex
|
|
94
|
+
val lastMediaItem = oldPosition.mediaItem ?: return
|
|
95
|
+
val currentTime = System.currentTimeMillis()
|
|
96
|
+
if ((currentTime - lastTrackFinishedAt) < 200) {
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
Log.d(
|
|
101
|
+
"Orpheus",
|
|
102
|
+
"onPositionDiscontinuity: isAutoTransition:$isAutoTransition isIndexChanged: $isIndexChanged durationCache:$durationCache"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if (isAutoTransition || isIndexChanged) {
|
|
106
|
+
|
|
107
|
+
val duration = durationCache[lastMediaItem.mediaId] ?: return
|
|
108
|
+
lastTrackFinishedAt = currentTime
|
|
109
|
+
|
|
110
|
+
sendEvent(
|
|
111
|
+
"onTrackFinished", mapOf(
|
|
112
|
+
"trackId" to lastMediaItem.mediaId,
|
|
113
|
+
"finalPosition" to oldPosition.positionMs / 1000.0,
|
|
114
|
+
"duration" to duration / 1000.0,
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 处理播放状态改变
|
|
122
|
+
*/
|
|
123
|
+
override fun onPlaybackStateChanged(state: Int) {
|
|
124
|
+
// state: 1=IDLE, 2=BUFFERING, 3=READY, 4=ENDED
|
|
125
|
+
sendEvent(
|
|
126
|
+
"onPlaybackStateChanged", mapOf(
|
|
127
|
+
"state" to state
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
updateProgressRunnerState()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 处理播放/暂停状态
|
|
136
|
+
*/
|
|
137
|
+
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
|
138
|
+
sendEvent(
|
|
139
|
+
"onIsPlayingChanged", mapOf(
|
|
140
|
+
"status" to isPlaying
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
updateProgressRunnerState()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 处理错误
|
|
148
|
+
*/
|
|
149
|
+
override fun onPlayerError(error: PlaybackException) {
|
|
150
|
+
sendEvent(
|
|
151
|
+
"onPlayerError", mapOf(
|
|
152
|
+
"code" to error.errorCode.toString(),
|
|
153
|
+
"message" to (error.message ?: "Unknown Error")
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
override fun onRepeatModeChanged(repeatMode: Int) {
|
|
159
|
+
super.onRepeatModeChanged(repeatMode)
|
|
160
|
+
GeneralStorage.saveRepeatMode(repeatMode)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
|
|
164
|
+
super.onShuffleModeEnabledChanged(shuffleModeEnabled)
|
|
165
|
+
GeneralStorage.saveShuffleMode(shuffleModeEnabled)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
54
169
|
@OptIn(UnstableApi::class)
|
|
55
170
|
override fun definition() = ModuleDefinition {
|
|
56
171
|
Name("Orpheus")
|
|
@@ -67,7 +182,8 @@ class ExpoOrpheusModule : Module() {
|
|
|
67
182
|
|
|
68
183
|
OnCreate {
|
|
69
184
|
val context = appContext.reactContext ?: return@OnCreate
|
|
70
|
-
|
|
185
|
+
GeneralStorage.initialize(context)
|
|
186
|
+
LoudnessStorage.initialize(context)
|
|
71
187
|
val sessionToken = SessionToken(
|
|
72
188
|
context,
|
|
73
189
|
ComponentName(context, OrpheusMusicService::class.java)
|
|
@@ -75,14 +191,16 @@ class ExpoOrpheusModule : Module() {
|
|
|
75
191
|
controllerFuture = MediaController.Builder(context, sessionToken)
|
|
76
192
|
.setApplicationLooper(Looper.getMainLooper()).buildAsync()
|
|
77
193
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
194
|
+
|
|
195
|
+
OrpheusMusicService.addOnServiceReadyListener { service ->
|
|
196
|
+
mainHandler.post {
|
|
197
|
+
if (this@ExpoOrpheusModule.player != service.player) {
|
|
198
|
+
this@ExpoOrpheusModule.player?.removeListener(playerListener)
|
|
199
|
+
this@ExpoOrpheusModule.player = service.player
|
|
200
|
+
this@ExpoOrpheusModule.player?.addListener(playerListener)
|
|
201
|
+
}
|
|
84
202
|
}
|
|
85
|
-
}
|
|
203
|
+
}
|
|
86
204
|
|
|
87
205
|
downloadManager = DownloadUtil.getDownloadManager(context)
|
|
88
206
|
downloadManager?.addListener(downloadListener)
|
|
@@ -94,14 +212,18 @@ class ExpoOrpheusModule : Module() {
|
|
|
94
212
|
mainHandler.removeCallbacks(downloadProgressRunnable)
|
|
95
213
|
controllerFuture?.let { MediaController.releaseFuture(it) }
|
|
96
214
|
downloadManager?.removeListener(downloadListener)
|
|
215
|
+
player?.removeListener(playerListener)
|
|
216
|
+
OrpheusMusicService.removeOnServiceReadyListener { }
|
|
217
|
+
player = null
|
|
218
|
+
Log.d("Orpheus", "Destroy media controller")
|
|
97
219
|
}
|
|
98
220
|
|
|
99
221
|
Constant("restorePlaybackPositionEnabled") {
|
|
100
|
-
|
|
222
|
+
GeneralStorage.isRestoreEnabled()
|
|
101
223
|
}
|
|
102
224
|
|
|
103
225
|
Constant("loudnessNormalizationEnabled") {
|
|
104
|
-
|
|
226
|
+
GeneralStorage.isLoudnessNormalizationEnabled()
|
|
105
227
|
}
|
|
106
228
|
|
|
107
229
|
Function("setBilibiliCookie") { cookie: String ->
|
|
@@ -109,144 +231,143 @@ class ExpoOrpheusModule : Module() {
|
|
|
109
231
|
}
|
|
110
232
|
|
|
111
233
|
Function("setLoudnessNormalizationEnabled") { enabled: Boolean ->
|
|
112
|
-
|
|
234
|
+
GeneralStorage.setLoudnessNormalizationEnabled(enabled)
|
|
113
235
|
}
|
|
114
236
|
|
|
115
237
|
Function("setRestorePlaybackPositionEnabled") { enabled: Boolean ->
|
|
116
|
-
|
|
238
|
+
GeneralStorage.setRestoreEnabled(enabled)
|
|
117
239
|
}
|
|
118
240
|
|
|
119
241
|
AsyncFunction("getPosition") {
|
|
120
|
-
|
|
121
|
-
|
|
242
|
+
checkPlayer()
|
|
243
|
+
player?.currentPosition?.toDouble()?.div(1000.0) ?: 0.0
|
|
122
244
|
}.runOnQueue(Queues.MAIN)
|
|
123
245
|
|
|
124
246
|
AsyncFunction("getDuration") {
|
|
125
|
-
|
|
126
|
-
val d =
|
|
247
|
+
checkPlayer()
|
|
248
|
+
val d = player?.duration ?: C.TIME_UNSET
|
|
127
249
|
if (d == C.TIME_UNSET) 0.0 else d.toDouble() / 1000.0
|
|
128
250
|
}.runOnQueue(Queues.MAIN)
|
|
129
251
|
|
|
130
252
|
AsyncFunction("getBuffered") {
|
|
131
|
-
|
|
132
|
-
|
|
253
|
+
checkPlayer()
|
|
254
|
+
player?.bufferedPosition?.toDouble()?.div(1000.0) ?: 0.0
|
|
133
255
|
}.runOnQueue(Queues.MAIN)
|
|
134
256
|
|
|
135
257
|
AsyncFunction("getIsPlaying") {
|
|
136
|
-
|
|
137
|
-
|
|
258
|
+
checkPlayer()
|
|
259
|
+
player?.isPlaying ?: false
|
|
138
260
|
}.runOnQueue(Queues.MAIN)
|
|
139
261
|
|
|
140
262
|
AsyncFunction("getCurrentIndex") {
|
|
141
|
-
|
|
142
|
-
|
|
263
|
+
checkPlayer()
|
|
264
|
+
player?.currentMediaItemIndex ?: -1
|
|
143
265
|
}.runOnQueue(Queues.MAIN)
|
|
144
266
|
|
|
145
267
|
AsyncFunction("getCurrentTrack") {
|
|
146
|
-
|
|
147
|
-
val
|
|
148
|
-
val currentItem =
|
|
268
|
+
checkPlayer()
|
|
269
|
+
val p = player ?: return@AsyncFunction null
|
|
270
|
+
val currentItem = p.currentMediaItem ?: return@AsyncFunction null
|
|
149
271
|
|
|
150
272
|
mediaItemToTrackRecord(currentItem)
|
|
151
273
|
}.runOnQueue(Queues.MAIN)
|
|
152
274
|
|
|
153
275
|
AsyncFunction("getShuffleMode") {
|
|
154
|
-
|
|
155
|
-
|
|
276
|
+
checkPlayer()
|
|
277
|
+
player?.shuffleModeEnabled
|
|
156
278
|
}.runOnQueue(Queues.MAIN)
|
|
157
279
|
|
|
158
280
|
AsyncFunction("getIndexTrack") { index: Int ->
|
|
159
|
-
|
|
160
|
-
val
|
|
281
|
+
checkPlayer()
|
|
282
|
+
val p = player ?: return@AsyncFunction null
|
|
161
283
|
|
|
162
|
-
if (index < 0 || index >=
|
|
284
|
+
if (index < 0 || index >= p.mediaItemCount) {
|
|
163
285
|
return@AsyncFunction null
|
|
164
286
|
}
|
|
165
287
|
|
|
166
|
-
val item =
|
|
288
|
+
val item = p.getMediaItemAt(index)
|
|
167
289
|
|
|
168
290
|
mediaItemToTrackRecord(item)
|
|
169
291
|
}.runOnQueue(Queues.MAIN)
|
|
170
292
|
|
|
171
293
|
AsyncFunction("play") {
|
|
172
|
-
|
|
173
|
-
|
|
294
|
+
checkPlayer()
|
|
295
|
+
player?.play()
|
|
174
296
|
}.runOnQueue(Queues.MAIN)
|
|
175
297
|
|
|
176
298
|
AsyncFunction("pause") {
|
|
177
|
-
|
|
178
|
-
|
|
299
|
+
checkPlayer()
|
|
300
|
+
player?.pause()
|
|
179
301
|
}.runOnQueue(Queues.MAIN)
|
|
180
302
|
|
|
181
303
|
AsyncFunction("clear") {
|
|
182
|
-
|
|
183
|
-
|
|
304
|
+
checkPlayer()
|
|
305
|
+
player?.clearMediaItems()
|
|
184
306
|
durationCache.clear()
|
|
185
|
-
DownloadUtil.itemVolumeMap.clear()
|
|
186
307
|
}.runOnQueue(Queues.MAIN)
|
|
187
308
|
|
|
188
309
|
AsyncFunction("skipTo") { index: Int ->
|
|
189
310
|
// 跳转到指定索引的开头
|
|
190
|
-
|
|
191
|
-
|
|
311
|
+
checkPlayer()
|
|
312
|
+
player?.seekTo(index, C.TIME_UNSET)
|
|
192
313
|
}.runOnQueue(Queues.MAIN)
|
|
193
314
|
|
|
194
315
|
AsyncFunction("skipToNext") {
|
|
195
|
-
|
|
196
|
-
if (
|
|
197
|
-
|
|
316
|
+
checkPlayer()
|
|
317
|
+
if (player?.hasNextMediaItem() == true) {
|
|
318
|
+
player?.seekToNextMediaItem()
|
|
198
319
|
}
|
|
199
320
|
}.runOnQueue(Queues.MAIN)
|
|
200
321
|
|
|
201
322
|
AsyncFunction("skipToPrevious") {
|
|
202
|
-
|
|
203
|
-
if (
|
|
204
|
-
|
|
323
|
+
checkPlayer()
|
|
324
|
+
if (player?.hasPreviousMediaItem() == true) {
|
|
325
|
+
player?.seekToPreviousMediaItem()
|
|
205
326
|
}
|
|
206
327
|
}.runOnQueue(Queues.MAIN)
|
|
207
328
|
|
|
208
329
|
AsyncFunction("seekTo") { seconds: Double ->
|
|
209
|
-
|
|
330
|
+
checkPlayer()
|
|
210
331
|
val ms = (seconds * 1000).toLong()
|
|
211
|
-
|
|
332
|
+
player?.seekTo(ms)
|
|
212
333
|
}.runOnQueue(Queues.MAIN)
|
|
213
334
|
|
|
214
335
|
AsyncFunction("setRepeatMode") { mode: Int ->
|
|
215
|
-
|
|
336
|
+
checkPlayer()
|
|
216
337
|
// mode: 0=OFF, 1=TRACK, 2=QUEUE
|
|
217
338
|
val repeatMode = when (mode) {
|
|
218
339
|
1 -> Player.REPEAT_MODE_ONE
|
|
219
340
|
2 -> Player.REPEAT_MODE_ALL
|
|
220
341
|
else -> Player.REPEAT_MODE_OFF
|
|
221
342
|
}
|
|
222
|
-
|
|
343
|
+
player?.repeatMode = repeatMode
|
|
223
344
|
}.runOnQueue(Queues.MAIN)
|
|
224
345
|
|
|
225
346
|
AsyncFunction("setShuffleMode") { enabled: Boolean ->
|
|
226
|
-
|
|
227
|
-
|
|
347
|
+
checkPlayer()
|
|
348
|
+
player?.shuffleModeEnabled = enabled
|
|
228
349
|
}.runOnQueue(Queues.MAIN)
|
|
229
350
|
|
|
230
351
|
AsyncFunction("getRepeatMode") {
|
|
231
|
-
|
|
232
|
-
|
|
352
|
+
checkPlayer()
|
|
353
|
+
player?.repeatMode
|
|
233
354
|
}.runOnQueue(Queues.MAIN)
|
|
234
355
|
|
|
235
356
|
AsyncFunction("removeTrack") { index: Int ->
|
|
236
|
-
|
|
237
|
-
if (index >= 0 && index < (
|
|
238
|
-
|
|
357
|
+
checkPlayer()
|
|
358
|
+
if (index >= 0 && index < (player?.mediaItemCount ?: 0)) {
|
|
359
|
+
player?.removeMediaItem(index)
|
|
239
360
|
}
|
|
240
361
|
}.runOnQueue(Queues.MAIN)
|
|
241
362
|
|
|
242
363
|
AsyncFunction("getQueue") {
|
|
243
|
-
|
|
244
|
-
val
|
|
245
|
-
val count =
|
|
364
|
+
checkPlayer()
|
|
365
|
+
val p = player ?: return@AsyncFunction emptyList<TrackRecord>()
|
|
366
|
+
val count = p.mediaItemCount
|
|
246
367
|
val queue = ArrayList<TrackRecord>(count)
|
|
247
368
|
|
|
248
369
|
for (i in 0 until count) {
|
|
249
|
-
val item =
|
|
370
|
+
val item = p.getMediaItemAt(i)
|
|
250
371
|
queue.add(mediaItemToTrackRecord(item))
|
|
251
372
|
}
|
|
252
373
|
|
|
@@ -254,59 +375,31 @@ class ExpoOrpheusModule : Module() {
|
|
|
254
375
|
}.runOnQueue(Queues.MAIN)
|
|
255
376
|
|
|
256
377
|
AsyncFunction("setSleepTimer") { durationMs: Long ->
|
|
257
|
-
|
|
258
|
-
val command = SessionCommand(CustomCommands.CMD_START_TIMER, Bundle.EMPTY)
|
|
259
|
-
val args = Bundle().apply {
|
|
260
|
-
putLong(CustomCommands.KEY_DURATION, durationMs)
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
controller?.sendCustomCommand(command, args)
|
|
378
|
+
OrpheusMusicService.instance?.startSleepTimer(durationMs)
|
|
264
379
|
return@AsyncFunction null
|
|
265
380
|
}.runOnQueue(Queues.MAIN)
|
|
266
381
|
|
|
267
382
|
AsyncFunction("getSleepTimerEndTime") {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
val command = SessionCommand(CustomCommands.CMD_GET_REMAINING, Bundle.EMPTY)
|
|
271
|
-
val future = controller!!.sendCustomCommand(command, Bundle.EMPTY)
|
|
272
|
-
|
|
273
|
-
val result = try {
|
|
274
|
-
future.get()
|
|
275
|
-
} catch (e: Exception) {
|
|
276
|
-
throw CodedException("ERR_EXECUTION_FAILED", e.message, e)
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (result.resultCode == SessionResult.RESULT_SUCCESS) {
|
|
280
|
-
val extras = result.extras
|
|
281
|
-
if (extras.containsKey(CustomCommands.KEY_STOP_TIME)) {
|
|
282
|
-
val stopTime = extras.getLong(CustomCommands.KEY_STOP_TIME)
|
|
283
|
-
return@AsyncFunction stopTime
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return@AsyncFunction null
|
|
383
|
+
return@AsyncFunction OrpheusMusicService.instance?.getSleepTimerRemaining()
|
|
288
384
|
}.runOnQueue(Queues.MAIN)
|
|
289
385
|
|
|
290
386
|
AsyncFunction("cancelSleepTimer") {
|
|
291
|
-
|
|
292
|
-
val command = SessionCommand(CustomCommands.CMD_CANCEL_TIMER, Bundle.EMPTY)
|
|
293
|
-
controller?.sendCustomCommand(command, Bundle.EMPTY)
|
|
387
|
+
OrpheusMusicService.instance?.cancelSleepTimer()
|
|
294
388
|
return@AsyncFunction null
|
|
295
389
|
}.runOnQueue(Queues.MAIN)
|
|
296
390
|
|
|
297
391
|
AsyncFunction("addToEnd") { tracks: List<TrackRecord>, startFromId: String?, clearQueue: Boolean? ->
|
|
298
|
-
|
|
392
|
+
checkPlayer()
|
|
299
393
|
val mediaItems = tracks.map { track ->
|
|
300
394
|
track.toMediaItem(gson)
|
|
301
395
|
}
|
|
302
|
-
val
|
|
396
|
+
val p = player ?: return@AsyncFunction
|
|
303
397
|
if (clearQueue == true) {
|
|
304
|
-
|
|
398
|
+
p.clearMediaItems()
|
|
305
399
|
durationCache.clear()
|
|
306
|
-
DownloadUtil.itemVolumeMap.clear()
|
|
307
400
|
}
|
|
308
|
-
val initialSize =
|
|
309
|
-
|
|
401
|
+
val initialSize = p.mediaItemCount
|
|
402
|
+
p.addMediaItems(mediaItems)
|
|
310
403
|
|
|
311
404
|
if (!startFromId.isNullOrEmpty()) {
|
|
312
405
|
val relativeIndex = tracks.indexOfFirst { it.id == startFromId }
|
|
@@ -314,50 +407,50 @@ class ExpoOrpheusModule : Module() {
|
|
|
314
407
|
if (relativeIndex != -1) {
|
|
315
408
|
val targetIndex = initialSize + relativeIndex
|
|
316
409
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
410
|
+
p.seekTo(targetIndex, C.TIME_UNSET)
|
|
411
|
+
p.prepare()
|
|
412
|
+
p.play()
|
|
320
413
|
|
|
321
414
|
return@AsyncFunction
|
|
322
415
|
}
|
|
323
416
|
}
|
|
324
417
|
|
|
325
|
-
if (
|
|
326
|
-
|
|
418
|
+
if (p.playbackState == Player.STATE_IDLE) {
|
|
419
|
+
p.prepare()
|
|
327
420
|
}
|
|
328
421
|
}.runOnQueue(Queues.MAIN)
|
|
329
422
|
|
|
330
423
|
AsyncFunction("playNext") { track: TrackRecord ->
|
|
331
|
-
|
|
332
|
-
val
|
|
424
|
+
checkPlayer()
|
|
425
|
+
val p = player ?: return@AsyncFunction
|
|
333
426
|
|
|
334
427
|
val mediaItem = track.toMediaItem(gson)
|
|
335
|
-
val targetIndex =
|
|
428
|
+
val targetIndex = p.currentMediaItemIndex + 1
|
|
336
429
|
|
|
337
430
|
var existingIndex = -1
|
|
338
|
-
for (i in 0 until
|
|
339
|
-
if (
|
|
431
|
+
for (i in 0 until p.mediaItemCount) {
|
|
432
|
+
if (p.getMediaItemAt(i).mediaId == track.id) {
|
|
340
433
|
existingIndex = i
|
|
341
434
|
break
|
|
342
435
|
}
|
|
343
436
|
}
|
|
344
437
|
|
|
345
438
|
if (existingIndex != -1) {
|
|
346
|
-
if (existingIndex ==
|
|
439
|
+
if (existingIndex == p.currentMediaItemIndex) {
|
|
347
440
|
return@AsyncFunction
|
|
348
441
|
}
|
|
349
|
-
val safeTargetIndex = targetIndex.coerceAtMost(
|
|
442
|
+
val safeTargetIndex = targetIndex.coerceAtMost(p.mediaItemCount)
|
|
350
443
|
|
|
351
|
-
|
|
444
|
+
p.moveMediaItem(existingIndex, safeTargetIndex)
|
|
352
445
|
|
|
353
446
|
} else {
|
|
354
|
-
val safeTargetIndex = targetIndex.coerceAtMost(
|
|
447
|
+
val safeTargetIndex = targetIndex.coerceAtMost(p.mediaItemCount)
|
|
355
448
|
|
|
356
|
-
|
|
449
|
+
p.addMediaItem(safeTargetIndex, mediaItem)
|
|
357
450
|
}
|
|
358
451
|
|
|
359
|
-
if (
|
|
360
|
-
|
|
452
|
+
if (p.playbackState == Player.STATE_IDLE) {
|
|
453
|
+
p.prepare()
|
|
361
454
|
}
|
|
362
455
|
}.runOnQueue(Queues.MAIN)
|
|
363
456
|
|
|
@@ -554,139 +647,19 @@ class ExpoOrpheusModule : Module() {
|
|
|
554
647
|
}
|
|
555
648
|
}
|
|
556
649
|
|
|
557
|
-
private fun setupListeners() {
|
|
558
|
-
controller?.addListener(object : Player.Listener {
|
|
559
|
-
|
|
560
|
-
/**
|
|
561
|
-
* 核心:处理切歌、播放结束逻辑
|
|
562
|
-
*/
|
|
563
|
-
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
|
564
|
-
val newId = mediaItem?.mediaId ?: ""
|
|
565
|
-
Log.e("Orpheus", "onMediaItemTransition: $reason")
|
|
566
|
-
|
|
567
|
-
sendEvent(
|
|
568
|
-
"onTrackStarted", mapOf(
|
|
569
|
-
"trackId" to newId,
|
|
570
|
-
"reason" to reason
|
|
571
|
-
)
|
|
572
|
-
)
|
|
573
|
-
|
|
574
|
-
lastMediaId = newId
|
|
575
|
-
saveCurrentPosition()
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
|
|
579
|
-
val player = controller ?: return
|
|
580
|
-
val currentItem = player.currentMediaItem ?: return
|
|
581
|
-
val mediaId = currentItem.mediaId
|
|
582
|
-
|
|
583
|
-
val duration = player.duration
|
|
584
|
-
Log.d(
|
|
585
|
-
"Orpheus",
|
|
586
|
-
"onTimelineChanged: reason: $reason mediaId: $mediaId duration: $duration"
|
|
587
|
-
)
|
|
588
|
-
|
|
589
|
-
if (duration != C.TIME_UNSET && duration > 0) {
|
|
590
|
-
durationCache[mediaId] = duration
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
override fun onPositionDiscontinuity(
|
|
595
|
-
oldPosition: Player.PositionInfo,
|
|
596
|
-
newPosition: Player.PositionInfo,
|
|
597
|
-
reason: Int
|
|
598
|
-
) {
|
|
599
|
-
val isAutoTransition = reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION
|
|
600
|
-
val isIndexChanged = oldPosition.mediaItemIndex != newPosition.mediaItemIndex
|
|
601
|
-
val lastMediaItem = oldPosition.mediaItem ?: return
|
|
602
|
-
val currentTime = System.currentTimeMillis()
|
|
603
|
-
if ((currentTime - lastTrackFinishedAt) < 200) {
|
|
604
|
-
return
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
Log.d(
|
|
608
|
-
"Orpheus",
|
|
609
|
-
"onPositionDiscontinuity: isAutoTransition:$isAutoTransition isIndexChanged: $isIndexChanged durationCache:$durationCache"
|
|
610
|
-
)
|
|
611
|
-
|
|
612
|
-
if (isAutoTransition || isIndexChanged) {
|
|
613
|
-
|
|
614
|
-
val duration = durationCache[lastMediaItem.mediaId] ?: return
|
|
615
|
-
lastTrackFinishedAt = currentTime
|
|
616
|
-
|
|
617
|
-
sendEvent(
|
|
618
|
-
"onTrackFinished", mapOf(
|
|
619
|
-
"trackId" to lastMediaItem.mediaId,
|
|
620
|
-
"finalPosition" to oldPosition.positionMs / 1000.0,
|
|
621
|
-
"duration" to duration / 1000.0,
|
|
622
|
-
)
|
|
623
|
-
)
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
/**
|
|
628
|
-
* 处理播放状态改变
|
|
629
|
-
*/
|
|
630
|
-
override fun onPlaybackStateChanged(state: Int) {
|
|
631
|
-
// state: 1=IDLE, 2=BUFFERING, 3=READY, 4=ENDED
|
|
632
|
-
sendEvent(
|
|
633
|
-
"onPlaybackStateChanged", mapOf(
|
|
634
|
-
"state" to state
|
|
635
|
-
)
|
|
636
|
-
)
|
|
637
|
-
|
|
638
|
-
updateProgressRunnerState()
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
/**
|
|
642
|
-
* 处理播放/暂停状态
|
|
643
|
-
*/
|
|
644
|
-
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
|
645
|
-
sendEvent(
|
|
646
|
-
"onIsPlayingChanged", mapOf(
|
|
647
|
-
"status" to isPlaying
|
|
648
|
-
)
|
|
649
|
-
)
|
|
650
|
-
updateProgressRunnerState()
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
/**
|
|
654
|
-
* 处理错误
|
|
655
|
-
*/
|
|
656
|
-
override fun onPlayerError(error: PlaybackException) {
|
|
657
|
-
sendEvent(
|
|
658
|
-
"onPlayerError", mapOf(
|
|
659
|
-
"code" to error.errorCode.toString(),
|
|
660
|
-
"message" to (error.message ?: "Unknown Error")
|
|
661
|
-
)
|
|
662
|
-
)
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
override fun onRepeatModeChanged(repeatMode: Int) {
|
|
666
|
-
super.onRepeatModeChanged(repeatMode)
|
|
667
|
-
Storage.saveRepeatMode(repeatMode)
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
|
|
671
|
-
super.onShuffleModeEnabledChanged(shuffleModeEnabled)
|
|
672
|
-
Storage.saveShuffleMode(shuffleModeEnabled)
|
|
673
|
-
}
|
|
674
|
-
})
|
|
675
|
-
}
|
|
676
|
-
|
|
677
650
|
private val progressSendEventRunnable = object : Runnable {
|
|
678
651
|
override fun run() {
|
|
679
|
-
val
|
|
652
|
+
val p = player ?: return
|
|
680
653
|
|
|
681
|
-
if (
|
|
682
|
-
val currentMs =
|
|
683
|
-
val durationMs =
|
|
654
|
+
if (p.isPlaying) {
|
|
655
|
+
val currentMs = p.currentPosition
|
|
656
|
+
val durationMs = p.duration
|
|
684
657
|
|
|
685
658
|
sendEvent(
|
|
686
659
|
"onPositionUpdate", mapOf(
|
|
687
660
|
"position" to currentMs / 1000.0,
|
|
688
661
|
"duration" to if (durationMs == C.TIME_UNSET) 0.0 else durationMs / 1000.0,
|
|
689
|
-
"buffered" to
|
|
662
|
+
"buffered" to p.bufferedPosition / 1000.0
|
|
690
663
|
)
|
|
691
664
|
)
|
|
692
665
|
}
|
|
@@ -703,9 +676,9 @@ class ExpoOrpheusModule : Module() {
|
|
|
703
676
|
}
|
|
704
677
|
|
|
705
678
|
private fun updateProgressRunnerState() {
|
|
706
|
-
val
|
|
679
|
+
val p = player
|
|
707
680
|
// 如果正在播放且状态是 READY,则开始轮询
|
|
708
|
-
if (
|
|
681
|
+
if (p != null && p.isPlaying && p.playbackState == Player.STATE_READY) {
|
|
709
682
|
mainHandler.removeCallbacks(progressSendEventRunnable)
|
|
710
683
|
mainHandler.removeCallbacks(progressSaveRunnable)
|
|
711
684
|
mainHandler.post(progressSaveRunnable)
|
|
@@ -739,17 +712,17 @@ class ExpoOrpheusModule : Module() {
|
|
|
739
712
|
}
|
|
740
713
|
|
|
741
714
|
private fun saveCurrentPosition() {
|
|
742
|
-
val
|
|
743
|
-
if (
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
715
|
+
val p = player ?: return
|
|
716
|
+
if (p.playbackState != Player.STATE_IDLE) {
|
|
717
|
+
GeneralStorage.savePosition(
|
|
718
|
+
p.currentMediaItemIndex,
|
|
719
|
+
p.currentPosition
|
|
747
720
|
)
|
|
748
721
|
}
|
|
749
722
|
}
|
|
750
723
|
|
|
751
|
-
private fun
|
|
752
|
-
if (
|
|
724
|
+
private fun checkPlayer() {
|
|
725
|
+
if (player == null) {
|
|
753
726
|
throw ControllerNotInitializedException()
|
|
754
727
|
}
|
|
755
728
|
}
|
|
@@ -2,27 +2,24 @@ package expo.modules.orpheus
|
|
|
2
2
|
|
|
3
3
|
import android.app.PendingIntent
|
|
4
4
|
import android.content.Intent
|
|
5
|
-
import android.
|
|
5
|
+
import android.util.Log
|
|
6
6
|
import androidx.annotation.OptIn
|
|
7
7
|
import androidx.core.net.toUri
|
|
8
8
|
import androidx.media3.common.AudioAttributes
|
|
9
9
|
import androidx.media3.common.C
|
|
10
10
|
import androidx.media3.common.Player
|
|
11
11
|
import androidx.media3.common.Timeline
|
|
12
|
-
import androidx.media3.common.util.Log
|
|
13
12
|
import androidx.media3.common.util.UnstableApi
|
|
14
13
|
import androidx.media3.exoplayer.ExoPlayer
|
|
15
14
|
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
|
16
15
|
import androidx.media3.session.MediaLibraryService
|
|
17
16
|
import androidx.media3.session.MediaSession
|
|
18
|
-
import androidx.media3.session.SessionCommand
|
|
19
|
-
import androidx.media3.session.SessionResult
|
|
20
17
|
import com.google.common.util.concurrent.Futures
|
|
21
18
|
import com.google.common.util.concurrent.ListenableFuture
|
|
22
|
-
import expo.modules.orpheus.bilibili.VolumeData
|
|
23
19
|
import expo.modules.orpheus.utils.DownloadUtil
|
|
24
20
|
import expo.modules.orpheus.utils.SleepTimeController
|
|
25
|
-
import expo.modules.orpheus.utils.
|
|
21
|
+
import expo.modules.orpheus.utils.GeneralStorage
|
|
22
|
+
import expo.modules.orpheus.utils.LoudnessStorage
|
|
26
23
|
import expo.modules.orpheus.utils.calculateLoudnessGain
|
|
27
24
|
import expo.modules.orpheus.utils.fadeInTo
|
|
28
25
|
import kotlinx.coroutines.Job
|
|
@@ -33,17 +30,40 @@ import kotlin.math.abs
|
|
|
33
30
|
|
|
34
31
|
class OrpheusMusicService : MediaLibraryService() {
|
|
35
32
|
|
|
36
|
-
|
|
33
|
+
var player: ExoPlayer? = null
|
|
37
34
|
private var mediaSession: MediaLibrarySession? = null
|
|
38
35
|
private var sleepTimerManager: SleepTimeController? = null
|
|
39
36
|
private var volumeFadeJob: Job? = null
|
|
40
37
|
private var scope = MainScope()
|
|
41
38
|
|
|
39
|
+
companion object {
|
|
40
|
+
var instance: OrpheusMusicService? = null
|
|
41
|
+
private set(value) {
|
|
42
|
+
field = value
|
|
43
|
+
if (value != null) {
|
|
44
|
+
listeners.forEach { it(value) }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private val listeners = mutableListOf<(OrpheusMusicService) -> Unit>()
|
|
49
|
+
|
|
50
|
+
fun addOnServiceReadyListener(listener: (OrpheusMusicService) -> Unit) {
|
|
51
|
+
instance?.let { listener(it) }
|
|
52
|
+
listeners.add(listener)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fun removeOnServiceReadyListener(listener: (OrpheusMusicService) -> Unit) {
|
|
56
|
+
listeners.remove(listener)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
42
60
|
@OptIn(UnstableApi::class)
|
|
43
61
|
override fun onCreate() {
|
|
44
62
|
super.onCreate()
|
|
63
|
+
instance = this
|
|
45
64
|
|
|
46
|
-
|
|
65
|
+
GeneralStorage.initialize(this)
|
|
66
|
+
LoudnessStorage.initialize(this)
|
|
47
67
|
|
|
48
68
|
|
|
49
69
|
val dataSourceFactory = DownloadUtil.getPlayerDataSourceFactory(this)
|
|
@@ -93,18 +113,8 @@ class OrpheusMusicService : MediaLibraryService() {
|
|
|
93
113
|
.setSessionActivity(contentIntent)
|
|
94
114
|
.build()
|
|
95
115
|
|
|
96
|
-
restorePlayerState(
|
|
116
|
+
restorePlayerState(GeneralStorage.isRestoreEnabled())
|
|
97
117
|
sleepTimerManager = SleepTimeController(player!!)
|
|
98
|
-
|
|
99
|
-
// 当有新的响度数据时,如果是当前这首歌的就直接应用,否则是预加载,等待 onMediaItemTransition 处理
|
|
100
|
-
scope.launch {
|
|
101
|
-
DownloadUtil.volumeResolvedEvent.collect { (uri, volumeData) ->
|
|
102
|
-
val currentUri = player?.currentMediaItem?.localConfiguration?.uri?.toString()
|
|
103
|
-
if (currentUri == uri) {
|
|
104
|
-
applyVolumeForCurrentItem(volumeData)
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
118
|
}
|
|
109
119
|
|
|
110
120
|
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession? {
|
|
@@ -113,6 +123,7 @@ class OrpheusMusicService : MediaLibraryService() {
|
|
|
113
123
|
|
|
114
124
|
override fun onDestroy() {
|
|
115
125
|
scope.cancel()
|
|
126
|
+
instance = null
|
|
116
127
|
|
|
117
128
|
mediaSession?.run {
|
|
118
129
|
player.release()
|
|
@@ -122,71 +133,30 @@ class OrpheusMusicService : MediaLibraryService() {
|
|
|
122
133
|
super.onDestroy()
|
|
123
134
|
}
|
|
124
135
|
|
|
136
|
+
fun startSleepTimer(durationMs: Long) {
|
|
137
|
+
sleepTimerManager?.start(durationMs)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fun cancelSleepTimer() {
|
|
141
|
+
sleepTimerManager?.cancel()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
fun getSleepTimerRemaining(): Long? {
|
|
145
|
+
return sleepTimerManager?.getStopTimeMs()
|
|
146
|
+
}
|
|
147
|
+
|
|
125
148
|
var callback: MediaLibrarySession.Callback = @UnstableApi
|
|
126
149
|
object : MediaLibrarySession.Callback {
|
|
127
|
-
private val customCommands = listOf(
|
|
128
|
-
SessionCommand(CustomCommands.CMD_START_TIMER, Bundle.EMPTY),
|
|
129
|
-
SessionCommand(CustomCommands.CMD_CANCEL_TIMER, Bundle.EMPTY),
|
|
130
|
-
SessionCommand(CustomCommands.CMD_GET_REMAINING, Bundle.EMPTY)
|
|
131
|
-
)
|
|
132
150
|
|
|
133
151
|
@OptIn(UnstableApi::class)
|
|
134
152
|
override fun onConnect(
|
|
135
153
|
session: MediaSession,
|
|
136
154
|
controller: MediaSession.ControllerInfo
|
|
137
155
|
): MediaSession.ConnectionResult {
|
|
138
|
-
val availableCommandsBuilder =
|
|
139
|
-
MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
|
|
140
|
-
|
|
141
|
-
for (command in customCommands) {
|
|
142
|
-
availableCommandsBuilder.add(command)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
156
|
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
|
|
146
|
-
.setAvailableSessionCommands(availableCommandsBuilder.build())
|
|
147
157
|
.build()
|
|
148
158
|
}
|
|
149
159
|
|
|
150
|
-
override fun onCustomCommand(
|
|
151
|
-
session: MediaSession,
|
|
152
|
-
controller: MediaSession.ControllerInfo,
|
|
153
|
-
customCommand: SessionCommand,
|
|
154
|
-
args: Bundle
|
|
155
|
-
): ListenableFuture<SessionResult> {
|
|
156
|
-
|
|
157
|
-
Log.d("Orpheus", "onCustomCommand: ${customCommand.customAction}")
|
|
158
|
-
|
|
159
|
-
when (customCommand.customAction) {
|
|
160
|
-
CustomCommands.CMD_START_TIMER -> {
|
|
161
|
-
val durationMs = args.getLong(CustomCommands.KEY_DURATION)
|
|
162
|
-
sleepTimerManager?.start(durationMs)
|
|
163
|
-
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
CustomCommands.CMD_CANCEL_TIMER -> {
|
|
167
|
-
sleepTimerManager?.cancel()
|
|
168
|
-
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
CustomCommands.CMD_GET_REMAINING -> {
|
|
172
|
-
val stopTime = sleepTimerManager?.getStopTimeMs()
|
|
173
|
-
val resultBundle = Bundle()
|
|
174
|
-
if (stopTime != null) {
|
|
175
|
-
resultBundle.putLong(CustomCommands.KEY_STOP_TIME, stopTime)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return Futures.immediateFuture(
|
|
179
|
-
SessionResult(
|
|
180
|
-
SessionResult.RESULT_SUCCESS,
|
|
181
|
-
resultBundle
|
|
182
|
-
)
|
|
183
|
-
)
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return super.onCustomCommand(session, controller, customCommand, args)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
160
|
/**
|
|
191
161
|
* 修复 UnsupportedOperationException 的关键!
|
|
192
162
|
* 当系统尝试恢复播放(比如从“最近播放”卡片点击)时触发。
|
|
@@ -209,15 +179,15 @@ class OrpheusMusicService : MediaLibraryService() {
|
|
|
209
179
|
private fun restorePlayerState(restorePosition: Boolean) {
|
|
210
180
|
val player = player ?: return
|
|
211
181
|
|
|
212
|
-
val restoredItems =
|
|
182
|
+
val restoredItems = GeneralStorage.restoreQueue()
|
|
213
183
|
|
|
214
184
|
if (restoredItems.isNotEmpty()) {
|
|
215
185
|
player.setMediaItems(restoredItems)
|
|
216
186
|
|
|
217
|
-
val savedIndex =
|
|
218
|
-
val savedPosition =
|
|
219
|
-
val savedShuffleMode =
|
|
220
|
-
val savedRepeatMode =
|
|
187
|
+
val savedIndex = GeneralStorage.getSavedIndex()
|
|
188
|
+
val savedPosition = GeneralStorage.getSavedPosition()
|
|
189
|
+
val savedShuffleMode = GeneralStorage.getShuffleMode()
|
|
190
|
+
val savedRepeatMode = GeneralStorage.getRepeatMode()
|
|
221
191
|
|
|
222
192
|
if (savedIndex >= 0 && savedIndex < restoredItems.size) {
|
|
223
193
|
player.seekTo(savedIndex, if (restorePosition) savedPosition else C.TIME_UNSET)
|
|
@@ -235,7 +205,6 @@ class OrpheusMusicService : MediaLibraryService() {
|
|
|
235
205
|
|
|
236
206
|
private fun setupListeners() {
|
|
237
207
|
player?.addListener(object : Player.Listener {
|
|
238
|
-
|
|
239
208
|
@OptIn(UnstableApi::class)
|
|
240
209
|
override fun onMediaItemTransition(
|
|
241
210
|
mediaItem: androidx.media3.common.MediaItem?,
|
|
@@ -244,10 +213,8 @@ class OrpheusMusicService : MediaLibraryService() {
|
|
|
244
213
|
saveCurrentQueue()
|
|
245
214
|
val uri = mediaItem?.localConfiguration?.uri?.toString() ?: return
|
|
246
215
|
|
|
247
|
-
val volumeData =
|
|
248
|
-
|
|
249
|
-
applyVolumeForCurrentItem(volumeData)
|
|
250
|
-
}
|
|
216
|
+
val volumeData = LoudnessStorage.getLoudnessData(uri)
|
|
217
|
+
applyVolumeForCurrentItem(volumeData)
|
|
251
218
|
}
|
|
252
219
|
|
|
253
220
|
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
|
|
@@ -260,21 +227,20 @@ class OrpheusMusicService : MediaLibraryService() {
|
|
|
260
227
|
val player = player ?: return
|
|
261
228
|
val queue = List(player.mediaItemCount) { i -> player.getMediaItemAt(i) }
|
|
262
229
|
if (queue.isNotEmpty()) {
|
|
263
|
-
|
|
230
|
+
GeneralStorage.saveQueue(queue)
|
|
264
231
|
}
|
|
265
232
|
}
|
|
266
233
|
|
|
267
234
|
@OptIn(UnstableApi::class)
|
|
268
|
-
private fun applyVolumeForCurrentItem(
|
|
235
|
+
private fun applyVolumeForCurrentItem(measuredI: Double) {
|
|
236
|
+
Log.d("LoudnessNormalization", "measuredI: $measuredI")
|
|
269
237
|
val player = player ?: return
|
|
270
238
|
volumeFadeJob?.cancel()
|
|
271
|
-
val isLoudnessNormalizationEnabled =
|
|
239
|
+
val isLoudnessNormalizationEnabled = GeneralStorage.isLoudnessNormalizationEnabled()
|
|
272
240
|
if (!isLoudnessNormalizationEnabled) return
|
|
273
241
|
val gain = run {
|
|
274
|
-
val
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (measured == 0.0) 1.0f else calculateLoudnessGain(measured, target)
|
|
242
|
+
val target = -14.0 // bilibili 的这个值似乎是固定的
|
|
243
|
+
if (measuredI == 0.0) 1.0f else calculateLoudnessGain(measuredI, target)
|
|
278
244
|
}
|
|
279
245
|
|
|
280
246
|
val targetVol = 1.0f * gain
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package expo.modules.orpheus.utils
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
+
import android.util.Log
|
|
4
5
|
import androidx.core.net.toUri
|
|
5
6
|
import androidx.media3.common.util.UnstableApi
|
|
6
7
|
import androidx.media3.database.StandaloneDatabaseProvider
|
|
@@ -35,15 +36,6 @@ object DownloadUtil {
|
|
|
35
36
|
|
|
36
37
|
private var downloadNotificationHelper: DownloadNotificationHelper? = null
|
|
37
38
|
|
|
38
|
-
var itemVolumeMap: MutableMap<String, VolumeData> = mutableMapOf()
|
|
39
|
-
|
|
40
|
-
private val _volumeResolvedEvent = MutableSharedFlow<Pair<String, VolumeData>>(
|
|
41
|
-
replay = 0,
|
|
42
|
-
extraBufferCapacity = 1,
|
|
43
|
-
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
|
44
|
-
)
|
|
45
|
-
val volumeResolvedEvent = _volumeResolvedEvent.asSharedFlow()
|
|
46
|
-
|
|
47
39
|
@Synchronized
|
|
48
40
|
fun getDownloadManager(context: Context): DownloadManager {
|
|
49
41
|
if (downloadManager == null) {
|
|
@@ -68,10 +60,16 @@ object DownloadUtil {
|
|
|
68
60
|
val upstreamFactory = getUpstreamFactory()
|
|
69
61
|
|
|
70
62
|
val downloadCache = DownloadCache.getStableCache(context)
|
|
63
|
+
val lruCache = DownloadCache.getLruCache(context)
|
|
64
|
+
|
|
65
|
+
val cacheFactory = CacheDataSource.Factory()
|
|
66
|
+
.setCache(lruCache)
|
|
67
|
+
.setUpstreamDataSourceFactory(upstreamFactory)
|
|
68
|
+
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
|
|
71
69
|
|
|
72
70
|
val downloadFactory = CacheDataSource.Factory()
|
|
73
71
|
.setCache(downloadCache)
|
|
74
|
-
.setUpstreamDataSourceFactory(
|
|
72
|
+
.setUpstreamDataSourceFactory(cacheFactory)
|
|
75
73
|
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
|
|
76
74
|
.setCacheWriteDataSinkFactory(null)
|
|
77
75
|
|
|
@@ -109,10 +107,6 @@ object DownloadUtil {
|
|
|
109
107
|
return downloadNotificationHelper!!
|
|
110
108
|
}
|
|
111
109
|
|
|
112
|
-
suspend fun emitVolumeEvent(uri: String, data: VolumeData) {
|
|
113
|
-
_volumeResolvedEvent.emit(uri to data)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
110
|
private class BilibiliResolver :
|
|
117
111
|
ResolvingDataSource.Resolver {
|
|
118
112
|
override fun resolveDataSpec(dataSpec: DataSpec): DataSpec {
|
|
@@ -132,10 +126,8 @@ object DownloadUtil {
|
|
|
132
126
|
)
|
|
133
127
|
// 在这里保存响度均衡数据,并且直接发一个事件,在 OrpheusMusicService 监听
|
|
134
128
|
if (volume !== null) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
emitVolumeEvent(dataSpec.uri.toString(), volume)
|
|
138
|
-
}
|
|
129
|
+
Log.d("LoudnessNormalization", "uri: ${dataSpec.uri}, measuredI: ${volume.measuredI}")
|
|
130
|
+
LoudnessStorage.setLoudnessData(dataSpec.uri.toString(), volume.measuredI)
|
|
139
131
|
}
|
|
140
132
|
|
|
141
133
|
val headers = HashMap<String, String>()
|
|
@@ -11,7 +11,7 @@ import com.tencent.mmkv.MMKV
|
|
|
11
11
|
import expo.modules.orpheus.models.TrackRecord
|
|
12
12
|
|
|
13
13
|
@OptIn(UnstableApi::class)
|
|
14
|
-
object
|
|
14
|
+
object GeneralStorage {
|
|
15
15
|
private var kv: MMKV? = null
|
|
16
16
|
private val gson = Gson()
|
|
17
17
|
private const val KEY_RESTORE_POSITION_ENABLED = "config_restore_position_enabled"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
package expo.modules.orpheus.utils
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import com.tencent.mmkv.MMKV
|
|
6
|
+
|
|
7
|
+
object LoudnessStorage {
|
|
8
|
+
private var kv: MMKV? = null
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@Synchronized
|
|
12
|
+
fun initialize(context: Context) {
|
|
13
|
+
if (kv == null) {
|
|
14
|
+
MMKV.initialize(context)
|
|
15
|
+
kv = MMKV.mmkvWithID("loudness_normalization_store")
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private val safeKv: MMKV
|
|
20
|
+
get() = kv ?: throw IllegalStateException("LoudnessStorage not initialized")
|
|
21
|
+
|
|
22
|
+
fun setLoudnessData(key: String, measuredI: Double) {
|
|
23
|
+
try {
|
|
24
|
+
Log.d("LoudnessNormalization", "setLoudnessData: $key, $measuredI")
|
|
25
|
+
safeKv.encode(key, measuredI)
|
|
26
|
+
} catch (e: Exception) {
|
|
27
|
+
Log.e("LoudnessStorage", "Failed to set loudness data", e)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fun getLoudnessData(key: String): Double {
|
|
32
|
+
try {
|
|
33
|
+
Log.d("LoudnessNormalization", "getLoudnessData: $key")
|
|
34
|
+
return safeKv.decodeDouble(key)
|
|
35
|
+
} catch (e: Exception) {
|
|
36
|
+
Log.e("LoudnessStorage", "Failed to get loudness data", e)
|
|
37
|
+
return 0.0
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|