react-native-queue-player 1.0.7 → 1.0.9
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/src/main/java/com/margelo/nitro/queueplayer/MediaItemBuilder.kt +21 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaceholderArtwork.kt +30 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaceholderFallbackBitmapLoader.kt +9 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +4 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +48 -5
- package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +55 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaceholderArtworkTest.kt +72 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaceholderFallbackBitmapLoaderTest.kt +21 -9
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceArtworkLoaderTest.kt +9 -9
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAirPlayMetadataWiringTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerAudioFocusTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +3 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLifecycleTest.kt +7 -7
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt +6 -6
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueChangeTest.kt +180 -0
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerQueueTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReadersTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +1 -1
- package/ios/AVQueueBuilder.swift +29 -0
- package/ios/Cast/Core/CastNowPlayingController.swift +7 -4
- package/ios/NowPlayingInfo.swift +7 -9
- package/ios/PlaceholderArtwork.swift +24 -1
- package/ios/QueueState.swift +14 -2
- package/ios/Tests/AVQueueBuilderTests.swift +83 -9
- package/ios/Tests/CastNowPlayingControllerTests.swift +33 -0
- package/ios/Tests/PlaceholderArtworkTests.swift +81 -0
- package/ios/Tests/QueueStateTests.swift +50 -0
- package/ios/TrackPlayer.swift +126 -15
- package/lib/module/hooks/index.js +1 -0
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/useQueue.js +57 -0
- package/lib/module/hooks/useQueue.js.map +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/types.js +7 -0
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/TrackPlayer.nitro.d.ts +27 -1
- package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
- package/lib/typescript/hooks/index.d.ts +2 -0
- package/lib/typescript/hooks/index.d.ts.map +1 -1
- package/lib/typescript/hooks/useQueue.d.ts +20 -0
- package/lib/typescript/hooks/useQueue.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +1 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/types.d.ts +22 -0
- package/lib/typescript/types.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double_QueueChangeReason.hpp +101 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +18 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +1 -0
- package/nitrogen/generated/android/c++/JPlayerConfig.hpp +5 -1
- package/nitrogen/generated/android/c++/JQueueChangeReason.hpp +70 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/Func_void_std__vector_TrackItem__double_QueueChangeReason.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +9 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +7 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/QueueChangeReason.kt +27 -0
- package/nitrogen/generated/android/queueplayerOnLoad.cpp +2 -0
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.cpp +8 -0
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +25 -0
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +11 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__double_QueueChangeReason.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +1 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +26 -0
- package/nitrogen/generated/ios/swift/PlayerConfig.swift +19 -1
- package/nitrogen/generated/ios/swift/QueueChangeReason.swift +56 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +1 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +4 -0
- package/nitrogen/generated/shared/c++/PlayerConfig.hpp +5 -1
- package/nitrogen/generated/shared/c++/QueueChangeReason.hpp +92 -0
- package/package.json +1 -1
- package/src/TrackPlayer.nitro.ts +33 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useQueue.ts +56 -0
- package/src/index.ts +1 -0
- package/src/types.ts +29 -0
|
@@ -68,7 +68,7 @@ class TrackPlayerLifecycleTest {
|
|
|
68
68
|
autoRetries = null,
|
|
69
69
|
retryBackoffMs = null,
|
|
70
70
|
networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null,
|
|
71
|
-
lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null
|
|
71
|
+
lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null
|
|
72
72
|
)
|
|
73
73
|
player.configureInternal(config, context())
|
|
74
74
|
|
|
@@ -92,7 +92,7 @@ class TrackPlayerLifecycleTest {
|
|
|
92
92
|
// public path owns the destroy step. This test drives the
|
|
93
93
|
// destroy + reconstruct shape directly to mirror what
|
|
94
94
|
// `configure()` does end-to-end.
|
|
95
|
-
val empty = PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null)
|
|
95
|
+
val empty = PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null)
|
|
96
96
|
|
|
97
97
|
player.configureInternal(empty, context())
|
|
98
98
|
val firstInstance = player.player
|
|
@@ -125,12 +125,12 @@ class TrackPlayerLifecycleTest {
|
|
|
125
125
|
val first = PlayerConfig(
|
|
126
126
|
httpHeaders = mapOf("A" to "1"), userAgent = "ua-1",
|
|
127
127
|
autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null,
|
|
128
|
-
lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null
|
|
128
|
+
lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null
|
|
129
129
|
)
|
|
130
130
|
val second = PlayerConfig(
|
|
131
131
|
httpHeaders = mapOf("B" to "2"), userAgent = "ua-2",
|
|
132
132
|
autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null,
|
|
133
|
-
lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null
|
|
133
|
+
lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null
|
|
134
134
|
)
|
|
135
135
|
|
|
136
136
|
player.configureInternal(first, context())
|
|
@@ -144,7 +144,7 @@ class TrackPlayerLifecycleTest {
|
|
|
144
144
|
val configured = PlayerConfig(
|
|
145
145
|
httpHeaders = mapOf("h" to "v"), userAgent = "ua",
|
|
146
146
|
autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null,
|
|
147
|
-
lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null
|
|
147
|
+
lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null
|
|
148
148
|
)
|
|
149
149
|
player.configureInternal(configured, context())
|
|
150
150
|
// Non-default playback state the consumer set post-configure.
|
|
@@ -180,7 +180,7 @@ class TrackPlayerLifecycleTest {
|
|
|
180
180
|
@Test
|
|
181
181
|
fun `destroyInternal is safe to call twice`() {
|
|
182
182
|
player.configureInternal(
|
|
183
|
-
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null), context()
|
|
183
|
+
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null), context()
|
|
184
184
|
)
|
|
185
185
|
player.destroyInternal()
|
|
186
186
|
player.destroyInternal()
|
|
@@ -189,7 +189,7 @@ class TrackPlayerLifecycleTest {
|
|
|
189
189
|
|
|
190
190
|
@Test
|
|
191
191
|
fun `configureInternal after destroyInternal rebuilds the ExoPlayer`() {
|
|
192
|
-
val cfg = PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null)
|
|
192
|
+
val cfg = PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null)
|
|
193
193
|
|
|
194
194
|
player.configureInternal(cfg, context())
|
|
195
195
|
val first = player.player
|
package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerLookaheadConfigTest.kt
CHANGED
|
@@ -80,7 +80,7 @@ class TrackPlayerLookaheadConfigTest {
|
|
|
80
80
|
httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null,
|
|
81
81
|
networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null,
|
|
82
82
|
visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = 250.0, lookaheadCacheEvictionPolicy = null,
|
|
83
|
-
progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null
|
|
83
|
+
progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null
|
|
84
84
|
),
|
|
85
85
|
context()
|
|
86
86
|
)
|
|
@@ -120,7 +120,7 @@ class TrackPlayerLookaheadConfigTest {
|
|
|
120
120
|
networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null,
|
|
121
121
|
visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null,
|
|
122
122
|
lookaheadCacheEvictionPolicy = policy,
|
|
123
|
-
progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null
|
|
123
|
+
progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null
|
|
124
124
|
)
|
|
125
125
|
|
|
126
126
|
// --- setLookaheadCacheInternal
|
|
@@ -298,7 +298,7 @@ class TrackPlayerLookaheadConfigTest {
|
|
|
298
298
|
@Test
|
|
299
299
|
fun `re-configure with changed httpHeaders rebuilds the player (writer + cache)`() {
|
|
300
300
|
player.configureInternal(
|
|
301
|
-
PlayerConfig(httpHeaders = mapOf("X-Auth" to "token-1"), userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null),
|
|
301
|
+
PlayerConfig(httpHeaders = mapOf("X-Auth" to "token-1"), userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null),
|
|
302
302
|
context()
|
|
303
303
|
)
|
|
304
304
|
val firstWriter = player.lookaheadCacheWriter
|
|
@@ -307,7 +307,7 @@ class TrackPlayerLookaheadConfigTest {
|
|
|
307
307
|
assertNotNull(firstCache)
|
|
308
308
|
|
|
309
309
|
player.configureInternal(
|
|
310
|
-
PlayerConfig(httpHeaders = mapOf("X-Auth" to "token-2"), userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null),
|
|
310
|
+
PlayerConfig(httpHeaders = mapOf("X-Auth" to "token-2"), userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null),
|
|
311
311
|
context()
|
|
312
312
|
)
|
|
313
313
|
|
|
@@ -339,7 +339,7 @@ class TrackPlayerLookaheadConfigTest {
|
|
|
339
339
|
// simulated here by `destroyInternal` between
|
|
340
340
|
// `configureInternal` calls) yields a fresh writer instance,
|
|
341
341
|
// even when headers are unchanged.
|
|
342
|
-
val cfg = PlayerConfig(httpHeaders = mapOf("X-Auth" to "token"), userAgent = "rnqp/test", autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null)
|
|
342
|
+
val cfg = PlayerConfig(httpHeaders = mapOf("X-Auth" to "token"), userAgent = "rnqp/test", autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null)
|
|
343
343
|
player.configureInternal(cfg, context())
|
|
344
344
|
val firstWriter = player.lookaheadCacheWriter
|
|
345
345
|
|
|
@@ -413,5 +413,5 @@ class TrackPlayerLookaheadConfigTest {
|
|
|
413
413
|
|
|
414
414
|
// --- Helpers
|
|
415
415
|
|
|
416
|
-
private fun emptyConfig() = PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null)
|
|
416
|
+
private fun emptyConfig() = PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null)
|
|
417
417
|
}
|
|
@@ -45,7 +45,7 @@ class TrackPlayerMutationTest {
|
|
|
45
45
|
player = TrackPlayer()
|
|
46
46
|
serviceHandle = bindPlaybackServiceForTest(player, context())
|
|
47
47
|
player.configureInternal(
|
|
48
|
-
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null), context()
|
|
48
|
+
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null), context()
|
|
49
49
|
)
|
|
50
50
|
player.setQueueInternal((1..5).map { track("t$it") })
|
|
51
51
|
// cur defaults to 0 after setQueue; tests needing mid-queue
|
|
@@ -127,7 +127,7 @@ class TrackPlayerMutationTest {
|
|
|
127
127
|
val freshHandle = bindPlaybackServiceForTest(fresh, context())
|
|
128
128
|
try {
|
|
129
129
|
fresh.configureInternal(
|
|
130
|
-
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null), context()
|
|
130
|
+
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null), context()
|
|
131
131
|
)
|
|
132
132
|
assertEquals(-1, fresh.currentTrackIndex)
|
|
133
133
|
assertEquals(Player.STATE_IDLE, fresh.player!!.playbackState)
|
package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerProgressThrottleTest.kt
CHANGED
|
@@ -35,7 +35,7 @@ class TrackPlayerProgressThrottleTest {
|
|
|
35
35
|
httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null,
|
|
36
36
|
networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null,
|
|
37
37
|
visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null,
|
|
38
|
-
progressUpdateIntervalMs = foregroundMs, backgroundProgressUpdateIntervalMs = backgroundMs
|
|
38
|
+
progressUpdateIntervalMs = foregroundMs, backgroundProgressUpdateIntervalMs = backgroundMs, placeholderArtworkUri = null
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
@Before
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
package com.margelo.nitro.queueplayer
|
|
2
|
+
|
|
3
|
+
import androidx.media3.common.util.UnstableApi
|
|
4
|
+
import androidx.test.core.app.ApplicationProvider
|
|
5
|
+
import org.junit.After
|
|
6
|
+
import org.junit.Assert.assertEquals
|
|
7
|
+
import org.junit.Assert.assertTrue
|
|
8
|
+
import org.junit.Before
|
|
9
|
+
import org.junit.Test
|
|
10
|
+
import org.junit.runner.RunWith
|
|
11
|
+
import org.robolectric.RobolectricTestRunner
|
|
12
|
+
import org.robolectric.annotation.Config
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `onQueueChange` emission coverage against a live ExoPlayer via
|
|
16
|
+
* Robolectric. Every queue-content/order mutation fires exactly one
|
|
17
|
+
* event carrying the post-mutation `(queue, index, reason)`; a no-op
|
|
18
|
+
* mutation fires nothing. `emitQueueChange` runs synchronously inside
|
|
19
|
+
* the mutation on the test (main) thread, so no looper idle is needed to
|
|
20
|
+
* observe the fire.
|
|
21
|
+
*/
|
|
22
|
+
@RunWith(RobolectricTestRunner::class)
|
|
23
|
+
@Config(sdk = [34])
|
|
24
|
+
@UnstableApi
|
|
25
|
+
class TrackPlayerQueueChangeTest {
|
|
26
|
+
|
|
27
|
+
private lateinit var player: TrackPlayer
|
|
28
|
+
private lateinit var serviceHandle: PlaybackServiceTestHandle
|
|
29
|
+
|
|
30
|
+
private data class QueueEvent(
|
|
31
|
+
val queue: List<TrackItem>,
|
|
32
|
+
val index: Int,
|
|
33
|
+
val reason: QueueChangeReason,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
private fun context() =
|
|
37
|
+
ApplicationProvider.getApplicationContext<android.content.Context>()
|
|
38
|
+
|
|
39
|
+
private fun track(id: String) = TrackItem(
|
|
40
|
+
id = id, url = "https://example.com/$id.m4a",
|
|
41
|
+
title = null, artist = null, album = null,
|
|
42
|
+
duration = null, artworkUrl = null, extras = null,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
@Before
|
|
46
|
+
fun setUp() {
|
|
47
|
+
player = TrackPlayer()
|
|
48
|
+
serviceHandle = bindPlaybackServiceForTest(player, context())
|
|
49
|
+
player.configureInternal(
|
|
50
|
+
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null), context()
|
|
51
|
+
)
|
|
52
|
+
player.setQueueInternal((1..5).map { track("t$it") })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@After
|
|
56
|
+
fun tearDown() {
|
|
57
|
+
player.destroyInternal()
|
|
58
|
+
serviceHandle.destroy()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Register a capturing listener AFTER the @Before setQueue (which is
|
|
62
|
+
* therefore not captured — onQueueChange does not auto-fire). */
|
|
63
|
+
private fun capture(): Pair<MutableList<QueueEvent>, () -> Unit> {
|
|
64
|
+
val events = mutableListOf<QueueEvent>()
|
|
65
|
+
val dispose = player.onQueueChange { queue, idx, reason ->
|
|
66
|
+
events.add(QueueEvent(queue.toList(), idx.toInt(), reason))
|
|
67
|
+
}
|
|
68
|
+
return events to dispose
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --- one emission per mutation, with the post-mutation snapshot
|
|
72
|
+
|
|
73
|
+
@Test
|
|
74
|
+
fun `setQueue fires SET_QUEUE with the new queue + resolved index`() {
|
|
75
|
+
val (events, dispose) = capture()
|
|
76
|
+
player.setQueueInternal((1..3).map { track("n$it") }, startAtIndex = 1)
|
|
77
|
+
dispose()
|
|
78
|
+
assertEquals(1, events.size)
|
|
79
|
+
assertEquals(QueueChangeReason.SET_QUEUE, events[0].reason)
|
|
80
|
+
assertEquals(3, events[0].queue.size)
|
|
81
|
+
assertEquals(1, events[0].index)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@Test
|
|
85
|
+
fun `addToQueue fires ADD with the grown queue`() {
|
|
86
|
+
val (events, dispose) = capture()
|
|
87
|
+
player.addToQueueInternal(listOf(track("n1")), insertBefore = null)
|
|
88
|
+
dispose()
|
|
89
|
+
assertEquals(1, events.size)
|
|
90
|
+
assertEquals(QueueChangeReason.ADD, events[0].reason)
|
|
91
|
+
assertEquals(6, events[0].queue.size)
|
|
92
|
+
assertEquals("n1", events[0].queue[5].id)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@Test
|
|
96
|
+
fun `removeFromQueue fires REMOVE with the shrunk queue`() {
|
|
97
|
+
val (events, dispose) = capture()
|
|
98
|
+
player.removeFromQueueInternal(listOf(3)) // remove t4; cur=0 pinned
|
|
99
|
+
dispose()
|
|
100
|
+
assertEquals(1, events.size)
|
|
101
|
+
assertEquals(QueueChangeReason.REMOVE, events[0].reason)
|
|
102
|
+
assertEquals(listOf("t1", "t2", "t3", "t5"), events[0].queue.map { it.id })
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@Test
|
|
106
|
+
fun `moveInQueue fires MOVE`() {
|
|
107
|
+
val (events, dispose) = capture()
|
|
108
|
+
player.moveInQueueInternal(from = 3, to = 1) // cur=0 not moved
|
|
109
|
+
dispose()
|
|
110
|
+
assertEquals(1, events.size)
|
|
111
|
+
assertEquals(QueueChangeReason.MOVE, events[0].reason)
|
|
112
|
+
// t4 (index 3) removed + reinserted at 1 → t1,t4,t2,t3,t5.
|
|
113
|
+
assertEquals(listOf("t1", "t4", "t2", "t3", "t5"), events[0].queue.map { it.id })
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@Test
|
|
117
|
+
fun `clearQueue fires CLEAR with an empty queue + index -1`() {
|
|
118
|
+
val (events, dispose) = capture()
|
|
119
|
+
player.clearQueueInternal()
|
|
120
|
+
dispose()
|
|
121
|
+
assertEquals(1, events.size)
|
|
122
|
+
assertEquals(QueueChangeReason.CLEAR, events[0].reason)
|
|
123
|
+
assertEquals(0, events[0].queue.size)
|
|
124
|
+
assertEquals(-1, events[0].index)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@Test
|
|
128
|
+
fun `shuffleQueue fires SHUFFLE with a full queue at index 0`() {
|
|
129
|
+
val (events, dispose) = capture()
|
|
130
|
+
player.shuffleQueueInternal()
|
|
131
|
+
dispose()
|
|
132
|
+
assertEquals(1, events.size)
|
|
133
|
+
assertEquals(QueueChangeReason.SHUFFLE, events[0].reason)
|
|
134
|
+
assertEquals(5, events[0].queue.size)
|
|
135
|
+
assertEquals(0, events[0].index)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// --- no-op mutations do not fire
|
|
139
|
+
|
|
140
|
+
@Test
|
|
141
|
+
fun `empty addToQueue does not fire`() {
|
|
142
|
+
val (events, dispose) = capture()
|
|
143
|
+
player.addToQueueInternal(emptyList(), insertBefore = null)
|
|
144
|
+
dispose()
|
|
145
|
+
assertTrue(events.isEmpty())
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@Test
|
|
149
|
+
fun `no-op move (from == to) does not fire`() {
|
|
150
|
+
val (events, dispose) = capture()
|
|
151
|
+
player.moveInQueueInternal(from = 2, to = 2)
|
|
152
|
+
dispose()
|
|
153
|
+
assertTrue(events.isEmpty())
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@Test
|
|
157
|
+
fun `out-of-range remove does not fire`() {
|
|
158
|
+
val (events, dispose) = capture()
|
|
159
|
+
player.removeFromQueueInternal(listOf(99))
|
|
160
|
+
dispose()
|
|
161
|
+
assertTrue(events.isEmpty())
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@Test
|
|
165
|
+
fun `clearing an already-empty queue does not fire`() {
|
|
166
|
+
player.clearQueueInternal() // empties (fires; no listener yet)
|
|
167
|
+
val (events, dispose) = capture()
|
|
168
|
+
player.clearQueueInternal() // already empty → no-op
|
|
169
|
+
dispose()
|
|
170
|
+
assertTrue(events.isEmpty())
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
@Test
|
|
174
|
+
fun `unsubscribe stops emissions`() {
|
|
175
|
+
val (events, dispose) = capture()
|
|
176
|
+
dispose()
|
|
177
|
+
player.addToQueueInternal(listOf(track("n1")), insertBefore = null)
|
|
178
|
+
assertTrue(events.isEmpty())
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -55,7 +55,7 @@ class TrackPlayerQueueTest {
|
|
|
55
55
|
player = TrackPlayer()
|
|
56
56
|
serviceHandle = bindPlaybackServiceForTest(player, context())
|
|
57
57
|
player.configureInternal(
|
|
58
|
-
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null), context()
|
|
58
|
+
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null), context()
|
|
59
59
|
)
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -42,7 +42,7 @@ class TrackPlayerReadersTest {
|
|
|
42
42
|
httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null,
|
|
43
43
|
networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null,
|
|
44
44
|
visualizationEnabled = null, clampSeekToBuffered = null,
|
|
45
|
-
lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null
|
|
45
|
+
lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null
|
|
46
46
|
),
|
|
47
47
|
context()
|
|
48
48
|
)
|
package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipCapabilityTest.kt
CHANGED
|
@@ -53,7 +53,7 @@ class TrackPlayerSkipCapabilityTest {
|
|
|
53
53
|
player = TrackPlayer()
|
|
54
54
|
serviceHandle = bindPlaybackServiceForTest(player, context())
|
|
55
55
|
player.configureInternal(
|
|
56
|
-
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null), context()
|
|
56
|
+
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null), context()
|
|
57
57
|
)
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -318,7 +318,7 @@ class TrackPlayerSkipCapabilityTest {
|
|
|
318
318
|
player = TrackPlayer()
|
|
319
319
|
serviceHandle = bindPlaybackServiceForTest(player, context())
|
|
320
320
|
player.configureInternal(
|
|
321
|
-
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null), context()
|
|
321
|
+
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null), context()
|
|
322
322
|
)
|
|
323
323
|
}
|
|
324
324
|
}
|
|
@@ -51,7 +51,7 @@ class TrackPlayerSkipTest {
|
|
|
51
51
|
player = TrackPlayer()
|
|
52
52
|
serviceHandle = bindPlaybackServiceForTest(player, context())
|
|
53
53
|
player.configureInternal(
|
|
54
|
-
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null), context()
|
|
54
|
+
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null), context()
|
|
55
55
|
)
|
|
56
56
|
player.tracks = (1..5).map { track("t$it") }
|
|
57
57
|
player.currentTrackIndex = 2 // mid-queue default for most cases
|
|
@@ -50,7 +50,7 @@ class TrackPlayerTransportTest {
|
|
|
50
50
|
player = TrackPlayer()
|
|
51
51
|
serviceHandle = bindPlaybackServiceForTest(player, context())
|
|
52
52
|
player.configureInternal(
|
|
53
|
-
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null), context()
|
|
53
|
+
PlayerConfig(httpHeaders = null, userAgent = null, autoRetries = null, retryBackoffMs = null, networkTimeoutMs = null, audioContentType = null, audioCategoryOptions = null, visualizationEnabled = null, clampSeekToBuffered = null, lookaheadCacheMaxSizeMb = null, lookaheadCacheEvictionPolicy = null, progressUpdateIntervalMs = null, backgroundProgressUpdateIntervalMs = null, placeholderArtworkUri = null), context()
|
|
54
54
|
)
|
|
55
55
|
}
|
|
56
56
|
|
package/ios/AVQueueBuilder.swift
CHANGED
|
@@ -55,6 +55,35 @@ public enum AVQueueBuilder {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/// Best-effort warm-up of a freshly-built item's asset BEFORE it is
|
|
59
|
+
/// inserted into a live `AVQueuePlayer`. When the insert lands in the
|
|
60
|
+
/// immediate-next slot of a playing queue, `AVQueuePlayer` prerolls the
|
|
61
|
+
/// new item on the shared decode/render pipeline while the current item
|
|
62
|
+
/// is still rendering; faulting the asset's `tracks` / `duration` /
|
|
63
|
+
/// `playable` keys in synchronously at that moment briefly disturbs the
|
|
64
|
+
/// currently-playing item. Loading them here first makes that preroll
|
|
65
|
+
/// cheap.
|
|
66
|
+
///
|
|
67
|
+
/// Best-effort: the load races a [timeout] deadline and the caller
|
|
68
|
+
/// proceeds as soon as either wins. `AVAsset`'s async loader honours
|
|
69
|
+
/// task cancellation, so `cancelAll` caps a slow / unreachable remote
|
|
70
|
+
/// asset at [timeout] and the item is inserted unwarmed rather than
|
|
71
|
+
/// deferring the enqueue. Load errors are ignored for the same reason.
|
|
72
|
+
@MainActor
|
|
73
|
+
static func preloadAssetForInsert(_ item: AVPlayerItem, timeout: TimeInterval = 2.0) async {
|
|
74
|
+
let asset = item.asset
|
|
75
|
+
await withTaskGroup(of: Void.self) { group in
|
|
76
|
+
group.addTask { @MainActor in
|
|
77
|
+
_ = try? await asset.load(.tracks, .duration, .isPlayable)
|
|
78
|
+
}
|
|
79
|
+
group.addTask {
|
|
80
|
+
try? await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
|
|
81
|
+
}
|
|
82
|
+
_ = await group.next()
|
|
83
|
+
group.cancelAll()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
58
87
|
/// Single-item builder. Used by addToQueue. Honours the cache
|
|
59
88
|
/// lookup when a cache instance is supplied.
|
|
60
89
|
///
|
|
@@ -35,12 +35,15 @@ final class CastNowPlayingController {
|
|
|
35
35
|
/// `TrackPlayer.configure` via `configureArtwork`.
|
|
36
36
|
private let artworkResolver = ArtworkResolver()
|
|
37
37
|
|
|
38
|
-
///
|
|
39
|
-
///
|
|
40
|
-
|
|
38
|
+
/// Placeholder shown until the real cover resolves, so the lockscreen
|
|
39
|
+
/// never renders artwork-less during the network fetch. Read live —
|
|
40
|
+
/// `PlaceholderArtwork.image` is the consumer-configured image when one
|
|
41
|
+
/// is set (`PlayerConfig.placeholderArtworkUri`), else the bundled one —
|
|
42
|
+
/// so a per-refresh write here never clobbers it back to the built-in.
|
|
43
|
+
private static var placeholderArtwork: MPMediaItemArtwork {
|
|
41
44
|
let image = PlaceholderArtwork.image
|
|
42
45
|
return MPMediaItemArtwork(boundsSize: image.size) { _ in image }
|
|
43
|
-
}
|
|
46
|
+
}
|
|
44
47
|
|
|
45
48
|
/// Snapshot of the active receiver track for the lockscreen surface.
|
|
46
49
|
/// Pushed by `TrackPlayer` when the receiver's current track (resolved
|
package/ios/NowPlayingInfo.swift
CHANGED
|
@@ -43,7 +43,6 @@ final class NowPlayingInfo {
|
|
|
43
43
|
var snapshotProvider: (() -> Snapshot)?
|
|
44
44
|
|
|
45
45
|
private var periodicTimer: Timer?
|
|
46
|
-
private var cachedPlaceholderArtwork: MPMediaItemArtwork?
|
|
47
46
|
/// Subscription to the global cast active-session router. When a
|
|
48
47
|
/// remote session is active, `CastNowPlayingController` owns the
|
|
49
48
|
/// lockscreen surface and this writer becomes a no-op — every
|
|
@@ -176,15 +175,14 @@ final class NowPlayingInfo {
|
|
|
176
175
|
info[MPNowPlayingInfoPropertyPlaybackRate] = rate
|
|
177
176
|
}
|
|
178
177
|
|
|
179
|
-
///
|
|
180
|
-
///
|
|
181
|
-
///
|
|
182
|
-
///
|
|
178
|
+
/// MPMediaItemArtwork wrapper around the active placeholder image —
|
|
179
|
+
/// `PlaceholderArtwork.image` is the consumer-configured image when one
|
|
180
|
+
/// is set (`PlayerConfig.placeholderArtworkUri`), else the bundled one.
|
|
181
|
+
/// Rebuilt per call (cheap: the closure just returns the image, the
|
|
182
|
+
/// system handles scaling) so a placeholder set at `configure` is
|
|
183
|
+
/// reflected rather than serving a stale wrapper.
|
|
183
184
|
private func placeholderArtwork() -> MPMediaItemArtwork {
|
|
184
|
-
if let cached = cachedPlaceholderArtwork { return cached }
|
|
185
185
|
let image = PlaceholderArtwork.image
|
|
186
|
-
|
|
187
|
-
cachedPlaceholderArtwork = mp
|
|
188
|
-
return mp
|
|
186
|
+
return MPMediaItemArtwork(boundsSize: image.size) { _ in image }
|
|
189
187
|
}
|
|
190
188
|
}
|
|
@@ -15,7 +15,30 @@ import UIKit
|
|
|
15
15
|
/// Android skins (mirror that defence on iOS too).
|
|
16
16
|
enum PlaceholderArtwork {
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
/// The active placeholder: the consumer-supplied image when one is
|
|
19
|
+
/// configured + loadable, otherwise the bundled RNQP placeholder.
|
|
20
|
+
static var image: UIImage { customImage ?? bundledImage }
|
|
21
|
+
|
|
22
|
+
/// Set (or clear) the consumer-supplied placeholder from
|
|
23
|
+
/// `PlayerConfig.placeholderArtworkUri`. Loaded from a local `file://`
|
|
24
|
+
/// image the same way the bundled placeholder is (`UIImage(contentsOfFile:)`).
|
|
25
|
+
/// Only `file://` is honoured (scheme match is case-insensitive) — the
|
|
26
|
+
/// built-in is a local file, and a remote URL would break offline +
|
|
27
|
+
/// change behaviour; anything else (or an unloadable file) leaves the
|
|
28
|
+
/// bundled placeholder in effect. Decode is synchronous, so a very large
|
|
29
|
+
/// consumer image briefly blocks the `configure` call. Main-thread only
|
|
30
|
+
/// (called from `configure`).
|
|
31
|
+
static func setCustom(uri: String?) {
|
|
32
|
+
guard let uri = uri, !uri.isEmpty,
|
|
33
|
+
let url = URL(string: uri), url.scheme?.lowercased() == "file" else {
|
|
34
|
+
customImage = nil
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
customImage = UIImage(contentsOfFile: url.path)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private static var customImage: UIImage?
|
|
41
|
+
private static let bundledImage: UIImage = loadImage()
|
|
19
42
|
|
|
20
43
|
private static func loadImage() -> UIImage {
|
|
21
44
|
let frameworkBundle = Bundle(for: BundleToken.self)
|
package/ios/QueueState.swift
CHANGED
|
@@ -23,11 +23,23 @@ internal struct QueueState {
|
|
|
23
23
|
/// Returns the freshly-generated IDs for the inserted slice.
|
|
24
24
|
mutating func insert(_ newTracks: [TrackItem], at index: Int) -> [String] {
|
|
25
25
|
let newIds = newTracks.map { _ in UUID().uuidString }
|
|
26
|
-
|
|
27
|
-
queueItemIds.insert(contentsOf: newIds, at: index)
|
|
26
|
+
insert(newTracks, at: index, ids: newIds)
|
|
28
27
|
return newIds
|
|
29
28
|
}
|
|
30
29
|
|
|
30
|
+
/// Insert with caller-supplied IDs — used when the AVPlayerItems were
|
|
31
|
+
/// built (and their assets preloaded) ahead of the state mutation, so
|
|
32
|
+
/// the same lib-generated `queueItemId` that was stamped on each item
|
|
33
|
+
/// stays 1:1 with `queueItemIds`. `ids` must parallel `newTracks`.
|
|
34
|
+
mutating func insert(_ newTracks: [TrackItem], at index: Int, ids: [String]) {
|
|
35
|
+
precondition(
|
|
36
|
+
ids.count == newTracks.count,
|
|
37
|
+
"QueueState.insert requires ids parallel to newTracks (1:1)"
|
|
38
|
+
)
|
|
39
|
+
tracks.insert(contentsOf: newTracks, at: index)
|
|
40
|
+
queueItemIds.insert(contentsOf: ids, at: index)
|
|
41
|
+
}
|
|
42
|
+
|
|
31
43
|
/// Returns the removed `queueItemId`.
|
|
32
44
|
mutating func remove(at index: Int) -> String {
|
|
33
45
|
let removedId = queueItemIds.remove(at: index)
|