react-native-queue-player 1.0.8 → 1.1.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 +12 -6
- 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/QueueSkipArithmetic.kt +57 -10
- package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +53 -20
- 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/QueueSkipArithmeticTest.kt +103 -0
- 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 +1 -1
- 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 +41 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerSkipTest.kt +34 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +1 -1
- package/ios/Cast/Core/CastNowPlayingController.swift +7 -4
- package/ios/NowPlayingInfo.swift +7 -9
- package/ios/PlaceholderArtwork.swift +24 -1
- package/ios/QueueSkipArithmetic.swift +55 -9
- package/ios/Tests/AVQueueBuilderTests.swift +9 -9
- package/ios/Tests/CastNowPlayingControllerTests.swift +33 -0
- package/ios/Tests/PlaceholderArtworkTests.swift +81 -0
- package/ios/Tests/SkipIndexTests.swift +97 -0
- package/ios/TrackPlayer.swift +42 -14
- package/lib/module/index.js.map +1 -1
- package/lib/module/types.js +9 -0
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/TrackPlayer.nitro.d.ts +7 -7
- package/lib/typescript/index.d.ts +1 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/types.d.ts +41 -0
- package/lib/typescript/types.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +4 -0
- package/nitrogen/generated/android/c++/JPlayerConfig.hpp +11 -1
- package/nitrogen/generated/android/c++/JSkipToPreviousBehavior.hpp +58 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/PlayerConfig.kt +12 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/SkipToPreviousBehavior.kt +23 -0
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Bridge.hpp +18 -0
- package/nitrogen/generated/ios/QueuePlayer-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/swift/PlayerConfig.swift +30 -1
- package/nitrogen/generated/ios/swift/SkipToPreviousBehavior.swift +40 -0
- package/nitrogen/generated/shared/c++/PlayerConfig.hpp +12 -1
- package/nitrogen/generated/shared/c++/SkipToPreviousBehavior.hpp +76 -0
- package/package.json +1 -1
- package/src/TrackPlayer.nitro.ts +7 -7
- package/src/index.ts +1 -0
- package/src/types.ts +42 -0
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)
|
|
@@ -48,9 +48,10 @@ public enum QueueSkipArithmetic {
|
|
|
48
48
|
|
|
49
49
|
/// Previous-track index. Same early-bail rules as `computeNext`.
|
|
50
50
|
///
|
|
51
|
-
/// `.off` and `.track` both return nil at index 0 (no previous
|
|
52
|
-
///
|
|
53
|
-
///
|
|
51
|
+
/// `.off` and `.track` both return nil at index 0 (no previous track,
|
|
52
|
+
/// no wrap). The "restart current when past the threshold" model is
|
|
53
|
+
/// available via `PlayerConfig.skipToPreviousBehavior` (resolved by
|
|
54
|
+
/// `resolveSkipToPrevious`).
|
|
54
55
|
public static func computePrevious(
|
|
55
56
|
trackCount: Int,
|
|
56
57
|
currentIndex: Int,
|
|
@@ -87,19 +88,64 @@ public enum QueueSkipArithmetic {
|
|
|
87
88
|
/// `.off` and `.track` are false at index 0 (no wrap); `.queue` is
|
|
88
89
|
/// always true with an active track (wraps).
|
|
89
90
|
///
|
|
90
|
-
///
|
|
91
|
-
///
|
|
92
|
-
///
|
|
93
|
-
///
|
|
94
|
-
/// primitive (skip if there's a track to skip to), not the policy.
|
|
91
|
+
/// When `restartEnabled` is true (`PlayerConfig.skipToPreviousBehavior
|
|
92
|
+
/// == .restartOrPrevious`), Previous is enabled whenever a track is
|
|
93
|
+
/// loaded — even at index 0, where past the threshold it restarts the
|
|
94
|
+
/// current track — so the control never greys out.
|
|
95
95
|
public static func canSkipPrevious(
|
|
96
96
|
trackCount: Int,
|
|
97
97
|
currentIndex: Int,
|
|
98
|
-
repeatMode: QueueSkipRepeatMode
|
|
98
|
+
repeatMode: QueueSkipRepeatMode,
|
|
99
|
+
restartEnabled: Bool = false
|
|
99
100
|
) -> Bool {
|
|
101
|
+
if restartEnabled { return trackCount > 0 }
|
|
100
102
|
return computePrevious(
|
|
101
103
|
trackCount: trackCount,
|
|
102
104
|
currentIndex: currentIndex,
|
|
103
105
|
repeatMode: repeatMode) != nil
|
|
104
106
|
}
|
|
107
|
+
|
|
108
|
+
/// Position (ms) at or below which a `.restartOrPrevious` skip-back goes
|
|
109
|
+
/// to the previous track; past it, the current track restarts. Matches
|
|
110
|
+
/// Media3's default `maxSeekToPreviousPosition` (the industry-standard 3 s).
|
|
111
|
+
public static let restartThresholdMs: Int = 3000
|
|
112
|
+
|
|
113
|
+
/// What a skip-to-previous should do, resolved from queue position + the
|
|
114
|
+
/// configured behavior.
|
|
115
|
+
public enum SkipToPreviousAction: Equatable {
|
|
116
|
+
/// Navigate to this queue index (a real track change).
|
|
117
|
+
case previous(Int)
|
|
118
|
+
/// Restart the current track (seek to 0) — not a track change.
|
|
119
|
+
case restartCurrent
|
|
120
|
+
/// Nothing to do (empty queue).
|
|
121
|
+
case noOp
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Resolves a skip-to-previous. When `restartEnabled` is false this is
|
|
125
|
+
/// exactly `computePrevious` (previous track, or nothing). When true it
|
|
126
|
+
/// applies the threshold model, matching Media3 `BasePlayer.seekToPrevious`:
|
|
127
|
+
/// a previous track exists AND `positionMs <= thresholdMs` → previous;
|
|
128
|
+
/// otherwise (past threshold, or first track with no previous) → restart
|
|
129
|
+
/// the current track.
|
|
130
|
+
public static func resolveSkipToPrevious(
|
|
131
|
+
trackCount: Int,
|
|
132
|
+
currentIndex: Int,
|
|
133
|
+
repeatMode: QueueSkipRepeatMode,
|
|
134
|
+
restartEnabled: Bool,
|
|
135
|
+
positionMs: Int,
|
|
136
|
+
thresholdMs: Int = restartThresholdMs
|
|
137
|
+
) -> SkipToPreviousAction {
|
|
138
|
+
guard trackCount > 0, currentIndex >= 0 else { return .noOp }
|
|
139
|
+
let prev = computePrevious(
|
|
140
|
+
trackCount: trackCount,
|
|
141
|
+
currentIndex: currentIndex,
|
|
142
|
+
repeatMode: repeatMode)
|
|
143
|
+
if !restartEnabled {
|
|
144
|
+
return prev.map { .previous($0) } ?? .noOp
|
|
145
|
+
}
|
|
146
|
+
if let prev, positionMs <= thresholdMs {
|
|
147
|
+
return .previous(prev)
|
|
148
|
+
}
|
|
149
|
+
return .restartCurrent
|
|
150
|
+
}
|
|
105
151
|
}
|
|
@@ -169,7 +169,7 @@ final class AVQueueBuilderTests: XCTestCase {
|
|
|
169
169
|
let item = AVQueueBuilder.makeItem(
|
|
170
170
|
for: track,
|
|
171
171
|
queueItemId: UUID().uuidString,
|
|
172
|
-
config: PlayerConfig(httpHeaders: nil, userAgent: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
|
|
172
|
+
config: PlayerConfig(httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, skipToPreviousBehavior: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
|
|
173
173
|
cache: cache
|
|
174
174
|
)
|
|
175
175
|
XCTAssertNotNil(item)
|
|
@@ -186,7 +186,7 @@ final class AVQueueBuilderTests: XCTestCase {
|
|
|
186
186
|
func testMakeItemFallsBackToRemoteWhenCacheMisses() {
|
|
187
187
|
// No cache provided → behaves identically to the no-cache path.
|
|
188
188
|
let track = makeTrack(id: "t1", url: "https://example.com/track.m4a")
|
|
189
|
-
let config = PlayerConfig(httpHeaders: nil, userAgent: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil)
|
|
189
|
+
let config = PlayerConfig(httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, skipToPreviousBehavior: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil)
|
|
190
190
|
let item = AVQueueBuilder.makeItem(
|
|
191
191
|
for: track, queueItemId: UUID().uuidString, config: config, cache: nil)
|
|
192
192
|
XCTAssertNotNil(item)
|
|
@@ -214,7 +214,7 @@ final class AVQueueBuilderTests: XCTestCase {
|
|
|
214
214
|
let item = AVQueueBuilder.makeItem(
|
|
215
215
|
for: track,
|
|
216
216
|
queueItemId: UUID().uuidString,
|
|
217
|
-
config: PlayerConfig(httpHeaders: nil, userAgent: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
|
|
217
|
+
config: PlayerConfig(httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, skipToPreviousBehavior: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
|
|
218
218
|
cache: cache
|
|
219
219
|
)
|
|
220
220
|
XCTAssertNotNil(item)
|
|
@@ -260,7 +260,7 @@ final class AVQueueBuilderTests: XCTestCase {
|
|
|
260
260
|
let item = AVQueueBuilder.makeItem(
|
|
261
261
|
for: track,
|
|
262
262
|
queueItemId: UUID().uuidString,
|
|
263
|
-
config: PlayerConfig(httpHeaders: nil, userAgent: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
|
|
263
|
+
config: PlayerConfig(httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, skipToPreviousBehavior: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
|
|
264
264
|
cache: cache
|
|
265
265
|
)
|
|
266
266
|
XCTAssertNotNil(item)
|
|
@@ -296,7 +296,7 @@ final class AVQueueBuilderTests: XCTestCase {
|
|
|
296
296
|
let items = AVQueueBuilder.buildPlayerItems(
|
|
297
297
|
tracks: [trackA, trackB],
|
|
298
298
|
queueItemIds: [UUID().uuidString, UUID().uuidString],
|
|
299
|
-
config: PlayerConfig(httpHeaders: nil, userAgent: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
|
|
299
|
+
config: PlayerConfig(httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, skipToPreviousBehavior: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
|
|
300
300
|
cache: cache
|
|
301
301
|
)
|
|
302
302
|
XCTAssertEqual(items.count, 2)
|
|
@@ -371,12 +371,12 @@ final class AVQueueBuilderTests: XCTestCase {
|
|
|
371
371
|
let queueItemIdB = UUID().uuidString
|
|
372
372
|
let itemA = AVQueueBuilder.makeItem(
|
|
373
373
|
for: track, queueItemId: queueItemIdA,
|
|
374
|
-
config: PlayerConfig(httpHeaders: nil, userAgent: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
|
|
374
|
+
config: PlayerConfig(httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, skipToPreviousBehavior: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
|
|
375
375
|
cache: nil
|
|
376
376
|
)
|
|
377
377
|
let itemB = AVQueueBuilder.makeItem(
|
|
378
378
|
for: track, queueItemId: queueItemIdB,
|
|
379
|
-
config: PlayerConfig(httpHeaders: nil, userAgent: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
|
|
379
|
+
config: PlayerConfig(httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, skipToPreviousBehavior: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
|
|
380
380
|
cache: nil
|
|
381
381
|
)
|
|
382
382
|
XCTAssertNotNil(itemA)
|
|
@@ -401,7 +401,7 @@ final class AVQueueBuilderTests: XCTestCase {
|
|
|
401
401
|
let queueItemId = UUID().uuidString
|
|
402
402
|
let item = AVQueueBuilder.makeItem(
|
|
403
403
|
for: track, queueItemId: queueItemId,
|
|
404
|
-
config: PlayerConfig(httpHeaders: nil, userAgent: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
|
|
404
|
+
config: PlayerConfig(httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, skipToPreviousBehavior: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
|
|
405
405
|
cache: nil
|
|
406
406
|
)
|
|
407
407
|
XCTAssertNotNil(item)
|
|
@@ -417,7 +417,7 @@ final class AVQueueBuilderTests: XCTestCase {
|
|
|
417
417
|
let queueItemId = UUID().uuidString
|
|
418
418
|
let item = AVQueueBuilder.makeItem(
|
|
419
419
|
for: track, queueItemId: queueItemId,
|
|
420
|
-
config: PlayerConfig(httpHeaders: nil, userAgent: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
|
|
420
|
+
config: PlayerConfig(httpHeaders: nil, userAgent: nil, placeholderArtworkUri: nil, autoRetries: nil, retryBackoffMs: nil, networkTimeoutMs: nil, audioContentType: nil, audioCategoryOptions: nil, visualizationEnabled: nil, clampSeekToBuffered: nil, skipToPreviousBehavior: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil, progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil),
|
|
421
421
|
cache: nil
|
|
422
422
|
)
|
|
423
423
|
XCTAssertNotNil(item)
|
|
@@ -132,6 +132,39 @@ final class CastNowPlayingControllerTests: XCTestCase {
|
|
|
132
132
|
"activation renders the most recent cached metadata"
|
|
133
133
|
)
|
|
134
134
|
}
|
|
135
|
+
|
|
136
|
+
func testSeededPlaceholderReflectsConfiguredCustomImage() throws {
|
|
137
|
+
let tmp = FileManager.default.temporaryDirectory
|
|
138
|
+
.appendingPathComponent("rnqp-cast-placeholder-\(UUID().uuidString).jpg")
|
|
139
|
+
try makeTinyJpeg().write(to: tmp)
|
|
140
|
+
defer {
|
|
141
|
+
try? FileManager.default.removeItem(at: tmp)
|
|
142
|
+
PlaceholderArtwork.setCustom(uri: nil)
|
|
143
|
+
}
|
|
144
|
+
let builtInSize = PlaceholderArtwork.image.size
|
|
145
|
+
PlaceholderArtwork.setCustom(uri: tmp.absoluteString)
|
|
146
|
+
let customSize = PlaceholderArtwork.image.size
|
|
147
|
+
// Precondition: the fixture is a different size from the built-in,
|
|
148
|
+
// else the size check below can't tell them apart.
|
|
149
|
+
XCTAssertNotEqual(customSize, builtInSize)
|
|
150
|
+
|
|
151
|
+
try router.setActive(ControllableRemoteSession())
|
|
152
|
+
|
|
153
|
+
let artwork = info?[MPMediaItemPropertyArtwork] as? MPMediaItemArtwork
|
|
154
|
+
XCTAssertEqual(
|
|
155
|
+
artwork?.image(at: customSize)?.size, customSize,
|
|
156
|
+
"the cast lockscreen seeds the live custom placeholder, not a frozen built-in"
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private func makeTinyJpeg() -> Data {
|
|
161
|
+
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 1, height: 1))
|
|
162
|
+
let image = renderer.image { ctx in
|
|
163
|
+
UIColor.green.setFill()
|
|
164
|
+
ctx.fill(CGRect(x: 0, y: 0, width: 1, height: 1))
|
|
165
|
+
}
|
|
166
|
+
return image.jpegData(compressionQuality: 1.0) ?? Data()
|
|
167
|
+
}
|
|
135
168
|
}
|
|
136
169
|
|
|
137
170
|
// MARK: - Listener-firing fake
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import QueuePlayer
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
/// `PlaceholderArtwork` swaps in a consumer-supplied cover-art placeholder
|
|
6
|
+
/// (`PlayerConfig.placeholderArtworkUri`) in place of the bundled RNQP
|
|
7
|
+
/// image. Only a local `file://` image is honoured; a remote or unset URI,
|
|
8
|
+
/// or an unloadable file, keeps the bundled placeholder.
|
|
9
|
+
@MainActor
|
|
10
|
+
final class PlaceholderArtworkTests: XCTestCase {
|
|
11
|
+
|
|
12
|
+
override func tearDown() {
|
|
13
|
+
// Process-wide singleton — reset so a set custom image does not leak
|
|
14
|
+
// into other suites that read the bundled placeholder.
|
|
15
|
+
PlaceholderArtwork.setCustom(uri: nil)
|
|
16
|
+
super.tearDown()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func testCustomFileUriReplacesTheBundledPlaceholder() throws {
|
|
20
|
+
let bundled = PlaceholderArtwork.image
|
|
21
|
+
let tmp = FileManager.default.temporaryDirectory
|
|
22
|
+
.appendingPathComponent("rnqp-placeholder-\(UUID().uuidString).jpg")
|
|
23
|
+
try makeJpegBytes().write(to: tmp)
|
|
24
|
+
defer { try? FileManager.default.removeItem(at: tmp) }
|
|
25
|
+
|
|
26
|
+
PlaceholderArtwork.setCustom(uri: tmp.absoluteString)
|
|
27
|
+
|
|
28
|
+
let image = PlaceholderArtwork.image
|
|
29
|
+
XCTAssertFalse(image === bundled,
|
|
30
|
+
"a loadable file:// image replaces the bundled placeholder")
|
|
31
|
+
XCTAssertTrue(image === PlaceholderArtwork.image,
|
|
32
|
+
"the custom image is stable across reads")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func testUnsetUriKeepsTheBundledPlaceholder() {
|
|
36
|
+
let bundled = PlaceholderArtwork.image
|
|
37
|
+
PlaceholderArtwork.setCustom(uri: nil)
|
|
38
|
+
XCTAssertTrue(PlaceholderArtwork.image === bundled)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func testNonFileUriIsRejectedAndKeepsTheBundledPlaceholder() {
|
|
42
|
+
let bundled = PlaceholderArtwork.image
|
|
43
|
+
PlaceholderArtwork.setCustom(uri: "https://example.com/logo.png")
|
|
44
|
+
XCTAssertTrue(PlaceholderArtwork.image === bundled,
|
|
45
|
+
"a remote URI is rejected — offline-only file:// contract")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func testUnloadableFileUriFallsBackToTheBundledPlaceholder() {
|
|
49
|
+
let bundled = PlaceholderArtwork.image
|
|
50
|
+
PlaceholderArtwork.setCustom(uri: "file:///nonexistent/path/missing.jpg")
|
|
51
|
+
XCTAssertTrue(PlaceholderArtwork.image === bundled)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func testEmptyUriKeepsTheBundledPlaceholder() {
|
|
55
|
+
let bundled = PlaceholderArtwork.image
|
|
56
|
+
PlaceholderArtwork.setCustom(uri: "")
|
|
57
|
+
XCTAssertTrue(PlaceholderArtwork.image === bundled)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func testUppercaseFileSchemeIsHonoured() throws {
|
|
61
|
+
let bundled = PlaceholderArtwork.image
|
|
62
|
+
let tmp = FileManager.default.temporaryDirectory
|
|
63
|
+
.appendingPathComponent("rnqp-placeholder-\(UUID().uuidString).jpg")
|
|
64
|
+
try makeJpegBytes().write(to: tmp)
|
|
65
|
+
defer { try? FileManager.default.removeItem(at: tmp) }
|
|
66
|
+
|
|
67
|
+
PlaceholderArtwork.setCustom(uri: "FILE://" + tmp.path)
|
|
68
|
+
|
|
69
|
+
XCTAssertFalse(PlaceholderArtwork.image === bundled,
|
|
70
|
+
"an uppercase FILE:// scheme is honoured (case-insensitive)")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private func makeJpegBytes() -> Data {
|
|
74
|
+
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 1, height: 1))
|
|
75
|
+
let image = renderer.image { ctx in
|
|
76
|
+
UIColor.blue.setFill()
|
|
77
|
+
ctx.fill(CGRect(x: 0, y: 0, width: 1, height: 1))
|
|
78
|
+
}
|
|
79
|
+
return image.jpegData(compressionQuality: 1.0) ?? Data()
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -256,4 +256,101 @@ final class SkipIndexTests: XCTestCase {
|
|
|
256
256
|
XCTAssertFalse(QueueSkipArithmetic.canSkipPrevious(
|
|
257
257
|
trackCount: 1, currentIndex: 0, repeatMode: .track))
|
|
258
258
|
}
|
|
259
|
+
|
|
260
|
+
// MARK: - resolveSkipToPrevious — mode OFF (position ignored, current behavior)
|
|
261
|
+
|
|
262
|
+
private func resolve(
|
|
263
|
+
count: Int = 3, index: Int, mode: QueueSkipRepeatMode,
|
|
264
|
+
restart: Bool, positionMs: Int
|
|
265
|
+
) -> QueueSkipArithmetic.SkipToPreviousAction {
|
|
266
|
+
QueueSkipArithmetic.resolveSkipToPrevious(
|
|
267
|
+
trackCount: count, currentIndex: index, repeatMode: mode,
|
|
268
|
+
restartEnabled: restart, positionMs: positionMs)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
func testResolveModeOffIgnoresPositionAndGoesPrevious() {
|
|
272
|
+
// Past-threshold position must NOT restart when the option is off.
|
|
273
|
+
XCTAssertEqual(resolve(index: 2, mode: .off, restart: false, positionMs: 99_000),
|
|
274
|
+
.previous(1))
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
func testResolveModeOffFirstTrackIsNoOp() {
|
|
278
|
+
XCTAssertEqual(resolve(index: 0, mode: .off, restart: false, positionMs: 99_000), .noOp)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
func testResolveModeOffQueueWraps() {
|
|
282
|
+
XCTAssertEqual(resolve(index: 0, mode: .queue, restart: false, positionMs: 0), .previous(2))
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
func testResolveModeOffEmptyQueueIsNoOp() {
|
|
286
|
+
XCTAssertEqual(resolve(count: 0, index: -1, mode: .off, restart: false, positionMs: 0), .noOp)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// MARK: - resolveSkipToPrevious — mode ON (3s threshold model)
|
|
290
|
+
|
|
291
|
+
func testResolvePastThresholdRestartsEvenWithPrevious() {
|
|
292
|
+
for pos in [3001, 5000, 600_000] {
|
|
293
|
+
XCTAssertEqual(resolve(index: 2, mode: .off, restart: true, positionMs: pos),
|
|
294
|
+
.restartCurrent, "position \(pos) > 3000 should restart")
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
func testResolveThresholdBoundaryGoesPrevious() {
|
|
299
|
+
// <= threshold → previous; one ms past → restart.
|
|
300
|
+
XCTAssertEqual(resolve(index: 2, mode: .off, restart: true, positionMs: 3000), .previous(1))
|
|
301
|
+
XCTAssertEqual(resolve(index: 2, mode: .off, restart: true, positionMs: 3001), .restartCurrent)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
func testResolveUnderThresholdGoesPrevious() {
|
|
305
|
+
for pos in [0, 1, 2999] {
|
|
306
|
+
XCTAssertEqual(resolve(index: 2, mode: .off, restart: true, positionMs: pos),
|
|
307
|
+
.previous(1), "position \(pos) <= 3000 should go previous")
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
func testResolveFirstTrackUnderThresholdRestartsWhenNoPrevious() {
|
|
312
|
+
XCTAssertEqual(resolve(index: 0, mode: .off, restart: true, positionMs: 1000), .restartCurrent)
|
|
313
|
+
XCTAssertEqual(resolve(index: 0, mode: .track, restart: true, positionMs: 1000), .restartCurrent)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
func testResolveFirstTrackUnderThresholdQueueWraps() {
|
|
317
|
+
// queue mode has a previous (wraps), so under threshold goes to the last track.
|
|
318
|
+
XCTAssertEqual(resolve(index: 0, mode: .queue, restart: true, positionMs: 1000), .previous(2))
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
func testResolveFirstTrackPastThresholdRestarts() {
|
|
322
|
+
for mode in [QueueSkipRepeatMode.off, .track, .queue] {
|
|
323
|
+
XCTAssertEqual(resolve(index: 0, mode: mode, restart: true, positionMs: 9000),
|
|
324
|
+
.restartCurrent, "first track past threshold restarts under \(mode)")
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
func testResolveModeOnEmptyQueueIsNoOp() {
|
|
329
|
+
XCTAssertEqual(resolve(count: 0, index: -1, mode: .off, restart: true, positionMs: 9000), .noOp)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// MARK: - canSkipPrevious with restartEnabled (forced true)
|
|
333
|
+
|
|
334
|
+
func testCanSkipPreviousForcedTrueAtEveryIndexAndMode() {
|
|
335
|
+
for mode in [QueueSkipRepeatMode.off, .track, .queue] {
|
|
336
|
+
for index in 0..<3 {
|
|
337
|
+
XCTAssertTrue(QueueSkipArithmetic.canSkipPrevious(
|
|
338
|
+
trackCount: 3, currentIndex: index, repeatMode: mode, restartEnabled: true),
|
|
339
|
+
"restart mode forces canSkipPrevious true at index \(index) under \(mode)")
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
func testCanSkipPreviousRestartEnabledFalseForEmptyQueue() {
|
|
345
|
+
XCTAssertFalse(QueueSkipArithmetic.canSkipPrevious(
|
|
346
|
+
trackCount: 0, currentIndex: -1, repeatMode: .off, restartEnabled: true))
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
func testCanSkipPreviousRestartDisabledMatchesExisting() {
|
|
350
|
+
// Default restartEnabled=false path is unchanged: false at index 0 under .off.
|
|
351
|
+
XCTAssertFalse(QueueSkipArithmetic.canSkipPrevious(
|
|
352
|
+
trackCount: 3, currentIndex: 0, repeatMode: .off, restartEnabled: false))
|
|
353
|
+
XCTAssertTrue(QueueSkipArithmetic.canSkipPrevious(
|
|
354
|
+
trackCount: 3, currentIndex: 1, repeatMode: .off, restartEnabled: false))
|
|
355
|
+
}
|
|
259
356
|
}
|
package/ios/TrackPlayer.swift
CHANGED
|
@@ -97,14 +97,14 @@ class TrackPlayer: HybridTrackPlayerSpec {
|
|
|
97
97
|
PlayerConfig(
|
|
98
98
|
httpHeaders: nil,
|
|
99
99
|
userAgent: nil,
|
|
100
|
-
autoRetries: nil,
|
|
100
|
+
placeholderArtworkUri: nil, autoRetries: nil,
|
|
101
101
|
retryBackoffMs: nil,
|
|
102
102
|
networkTimeoutMs: nil,
|
|
103
103
|
audioContentType: nil,
|
|
104
104
|
audioCategoryOptions: nil,
|
|
105
105
|
visualizationEnabled: nil,
|
|
106
106
|
clampSeekToBuffered: nil,
|
|
107
|
-
lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil,
|
|
107
|
+
skipToPreviousBehavior: nil, lookaheadCacheMaxSizeMb: nil, lookaheadCacheEvictionPolicy: nil,
|
|
108
108
|
progressUpdateIntervalMs: nil, backgroundProgressUpdateIntervalMs: nil
|
|
109
109
|
)
|
|
110
110
|
|
|
@@ -609,6 +609,10 @@ class TrackPlayer: HybridTrackPlayerSpec {
|
|
|
609
609
|
// each track-change resolve.
|
|
610
610
|
self.artworkResolver.configHeaders = config.httpHeaders
|
|
611
611
|
self.artworkResolver.configUserAgent = config.userAgent
|
|
612
|
+
// Swap the cover-art placeholder to the consumer-supplied image
|
|
613
|
+
// (or back to the built-in when unset). Every placeholder site
|
|
614
|
+
// reads `PlaceholderArtwork.image`, so this is the only hook needed.
|
|
615
|
+
PlaceholderArtwork.setCustom(uri: config.placeholderArtworkUri)
|
|
612
616
|
// Same live config for the cast lockscreen's own artwork
|
|
613
617
|
// resolver so authenticated covers resolve during cast.
|
|
614
618
|
CastNowPlayingController.shared.configureArtwork(
|
|
@@ -2115,21 +2119,37 @@ class TrackPlayer: HybridTrackPlayerSpec {
|
|
|
2115
2119
|
}
|
|
2116
2120
|
}
|
|
2117
2121
|
|
|
2118
|
-
///
|
|
2119
|
-
///
|
|
2120
|
-
///
|
|
2121
|
-
///
|
|
2122
|
-
///
|
|
2123
|
-
///
|
|
2124
|
-
/// track to skip to), not the policy.
|
|
2122
|
+
/// Skip-previous, resolved by `QueueSkipArithmetic.resolveSkipToPrevious`
|
|
2123
|
+
/// from `PlayerConfig.skipToPreviousBehavior`. With the default `.previous`
|
|
2124
|
+
/// this goes to the previous track if there is one (`.off`/`.track` don't
|
|
2125
|
+
/// wrap, `.queue` wraps; `.track` navigates like `.off` and repeat-one
|
|
2126
|
+
/// persists). With `.restartOrPrevious` it restarts the current track (seek
|
|
2127
|
+
/// to 0) when past the threshold, otherwise goes to the previous track.
|
|
2125
2128
|
func skipToPrevious() throws -> Promise<Void> {
|
|
2126
2129
|
return Promise<Void>.async {
|
|
2127
2130
|
if CastTransportRouter.routeSkipToPrevious() { return }
|
|
2128
2131
|
await self.onMain {
|
|
2129
|
-
guard self.engine
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2132
|
+
guard let engine = self.engine else { return }
|
|
2133
|
+
let positionMs = Int(max(0, engine.currentPositionSeconds) * 1000)
|
|
2134
|
+
let action = QueueSkipArithmetic.resolveSkipToPrevious(
|
|
2135
|
+
trackCount: self.tracks.count,
|
|
2136
|
+
currentIndex: self.currentTrackIndex,
|
|
2137
|
+
repeatMode: self.repeatModeAsSkipMode(),
|
|
2138
|
+
restartEnabled: self.skipRestartEnabled(),
|
|
2139
|
+
positionMs: positionMs)
|
|
2140
|
+
switch action {
|
|
2141
|
+
case .previous(let idx):
|
|
2142
|
+
self.applyNewCurrentIndex(newIndex: idx, reason: .userSkipPrevious)
|
|
2143
|
+
case .restartCurrent:
|
|
2144
|
+
// Restart the current track from 0 — a seek, not a track change.
|
|
2145
|
+
// Same-index `applyNewCurrentIndex` takes its restart branch
|
|
2146
|
+
// (currentItem unchanged → no `onTrackChange`), engine-aware across
|
|
2147
|
+
// gapless (live / drained-rebuild) + crossfade.
|
|
2148
|
+
self.applyNewCurrentIndex(
|
|
2149
|
+
newIndex: self.currentTrackIndex, reason: .userSkipPrevious)
|
|
2150
|
+
case .noOp:
|
|
2151
|
+
return
|
|
2152
|
+
}
|
|
2133
2153
|
}
|
|
2134
2154
|
}
|
|
2135
2155
|
}
|
|
@@ -2156,6 +2176,13 @@ class TrackPlayer: HybridTrackPlayerSpec {
|
|
|
2156
2176
|
/// Map the Nitrogen-generated `RepeatMode` enum onto the pure-Swift
|
|
2157
2177
|
/// `QueueSkipRepeatMode` so `QueueSkipArithmetic` can stay off the
|
|
2158
2178
|
/// Swift ↔ C++ interop path.
|
|
2179
|
+
/// Whether skip-to-previous restarts the current track past the threshold
|
|
2180
|
+
/// (`PlayerConfig.skipToPreviousBehavior == .restartOrPrevious`). Read live
|
|
2181
|
+
/// from the current config at each skip + capability recompute.
|
|
2182
|
+
private func skipRestartEnabled() -> Bool {
|
|
2183
|
+
return self.config.skipToPreviousBehavior == .restartOrPrevious
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2159
2186
|
private func repeatModeAsSkipMode() -> QueueSkipRepeatMode {
|
|
2160
2187
|
switch self.repeatModeState {
|
|
2161
2188
|
case .off: return .off
|
|
@@ -2203,7 +2230,8 @@ class TrackPlayer: HybridTrackPlayerSpec {
|
|
|
2203
2230
|
let count = tracks.count
|
|
2204
2231
|
let cur = currentTrackIndex
|
|
2205
2232
|
let newPrev = QueueSkipArithmetic.canSkipPrevious(
|
|
2206
|
-
trackCount: count, currentIndex: cur, repeatMode: mode
|
|
2233
|
+
trackCount: count, currentIndex: cur, repeatMode: mode,
|
|
2234
|
+
restartEnabled: skipRestartEnabled())
|
|
2207
2235
|
let newNext = QueueSkipArithmetic.canSkipNext(
|
|
2208
2236
|
trackCount: count, currentIndex: cur, repeatMode: mode)
|
|
2209
2237
|
let current = cachedSkipCapability
|
package/lib/module/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["getCastManager","getEqualizer","getTrackPlayer","getVisualizer","registerPlaybackService","unregisterPlaybackService","crossfadeDurationBounds","SectionIcon","EQ_BAND_COUNT","EQ_BAND_FREQUENCIES_HZ","EQ_FLAT_PRESET_NAME","EQ_GAIN_MAX_DB","EQ_GAIN_MIN_DB","clampGain","gainsFromBands","gainsFromMap","isBuiltinPreset","nearestBandIndex","Cast"],"sourceRoot":"../../src","sources":["index.ts"],"mappings":";;AAAA;;AAMA;;
|
|
1
|
+
{"version":3,"names":["getCastManager","getEqualizer","getTrackPlayer","getVisualizer","registerPlaybackService","unregisterPlaybackService","crossfadeDurationBounds","SectionIcon","EQ_BAND_COUNT","EQ_BAND_FREQUENCIES_HZ","EQ_FLAT_PRESET_NAME","EQ_GAIN_MAX_DB","EQ_GAIN_MIN_DB","clampGain","gainsFromBands","gainsFromMap","isBuiltinPreset","nearestBandIndex","Cast"],"sourceRoot":"../../src","sources":["index.ts"],"mappings":";;AAAA;;AAMA;;AAwEA;AACA;AACA;AACA,SAASA,cAAc,EAAEC,YAAY,EAAEC,cAAc,EAAEC,aAAa,QAAQ,cAAW;;AAEvF;AACA;AACA;AACA,SACEC,uBAAuB,EACvBC,yBAAyB,QACpB,sBAAmB;AAM1B;AACA;AACA,SAASC,uBAAuB,QAAQ,YAAS;;AAEjD;AACA;AACA;AACA,SAASC,WAAW,QAAQ,YAAS;;AAErC;AACA;AACA;AACA,SACEC,aAAa,EACbC,sBAAsB,EACtBC,mBAAmB,EACnBC,cAAc,EACdC,cAAc,EACdC,SAAS,EACTC,cAAc,EACdC,YAAY,EACZC,eAAe,EACfC,gBAAgB,QACX,gBAAa;;AAEpB;AACA,cAAc,kBAAS;;AAEvB;AACA;AACA;AACA,OAAO,KAAKC,IAAI,MAAM,iBAAQ","ignoreList":[]}
|
package/lib/module/types.js
CHANGED
|
@@ -99,6 +99,15 @@ export const crossfadeDurationBounds = {
|
|
|
99
99
|
* one that loops.
|
|
100
100
|
*/
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* How `skipToPrevious` (and the OS Previous control) behaves.
|
|
104
|
+
* - `'previous'` — always go to the previous track (the default; current
|
|
105
|
+
* behavior).
|
|
106
|
+
* - `'restart-or-previous'` — restart the current track (seek to 0) when it
|
|
107
|
+
* is past ~3 s, otherwise go to the previous track. The standard
|
|
108
|
+
* music-player model. See `PlayerConfig.skipToPreviousBehavior`.
|
|
109
|
+
*/
|
|
110
|
+
|
|
102
111
|
/**
|
|
103
112
|
* How the lock-screen / notification skip controls behave: skip between queue
|
|
104
113
|
* tracks (`'track'`) or jump forward/back within the current track by a fixed
|
package/lib/module/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["crossfadeDurationBounds","minMs","maxMs","stepMs","SectionIcon"],"sourceRoot":"../../src","sources":["types.ts"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AA2BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;
|
|
1
|
+
{"version":3,"names":["crossfadeDurationBounds","minMs","maxMs","stepMs","SectionIcon"],"sourceRoot":"../../src","sources":["types.ts"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AA2BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAmLA;AACA;AACA;AACA;AACA;;AAGA;AACA;AACA;AACA;;AA+BA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAGA;AACA;AACA;AACA;AACA;AACA;;AAyBA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMA,uBAAuB,GAAG;EACrC;EACAC,KAAK,EAAE,KAAK;EACZ;EACAC,KAAK,EAAE,MAAM;EACb;EACAC,MAAM,EAAE;AACV,CAAU;;AAEV;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAGA;AACA;AACA;AACA;AACA;;AAGA;AACA;AACA;AACA;;AAUA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAYA;AACA;AACA;AACA;AACA;AACA;;AA0BA;AACA;AACA;AACA;;AAOe;;AAEf;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAUA;AACA;AACA;AACA;AACA;;AAWA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AA6EA;;AAMA;;AAcA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AA+DA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAQA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAgBA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AA0BA;AACA;AACA;;AASA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAGA;AACA;AACA;AACA;;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;;AAMA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAMA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AA4FA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,WAAYC,WAAW,0BAAXA,WAAW;EAAXA,WAAW;EAAXA,WAAW;EAAXA,WAAW;EAAXA,WAAW;EAAXA,WAAW;EAAXA,WAAW;EAAXA,WAAW;EAAXA,WAAW;EAAXA,WAAW;EAAXA,WAAW;EAAXA,WAAW;EAAXA,WAAW;EAAXA,WAAW;EAAA,OAAXA,WAAW;AAAA;;AAgBvB;AACA;AACA;AACA;AACA;AACA;AACA;;AAuBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAOA;AACA;AACA;;AAcA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAuDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAQA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAGA;;AASA;AACA;AACA;AACA;;AAWA;AACA;AACA;;AAGA;AACA;AACA;AACA;;AAYA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAaA;AACA;AACA;AACA;AACA;;AA0BA;AACA;AACA;AACA;AACA;;AAKA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAMA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAgBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","ignoreList":[]}
|
|
@@ -266,17 +266,17 @@ export interface TrackPlayer extends HybridObject<{
|
|
|
266
266
|
getCanSkipNext(): boolean;
|
|
267
267
|
/**
|
|
268
268
|
* Whether the user-facing Previous button should be enabled.
|
|
269
|
-
* Symmetric with `getCanSkipNext` —
|
|
269
|
+
* Symmetric with `getCanSkipNext` — with the default
|
|
270
|
+
* `skipToPreviousBehavior: 'previous'`, true when there's a previous
|
|
270
271
|
* track to skip to under the current repeat mode:
|
|
271
272
|
* - `'off'` and `'track'` at index 0: false (no previous, no wrap)
|
|
272
273
|
* - `'queue'`: true with an active track (wraps)
|
|
273
274
|
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
* not the policy.
|
|
275
|
+
* With `PlayerConfig.skipToPreviousBehavior: 'restart-or-previous'` the
|
|
276
|
+
* lib applies the standard music-player "restart when past ~3 s, else
|
|
277
|
+
* previous" model natively, and this reports `true` whenever a track is
|
|
278
|
+
* loaded (even at index 0, where past the threshold Previous restarts the
|
|
279
|
+
* current track) so the control never greys out.
|
|
280
280
|
*/
|
|
281
281
|
getCanSkipPrevious(): boolean;
|
|
282
282
|
/**
|
|
@@ -2,7 +2,7 @@ export type { TrackPlayer } from './TrackPlayer.nitro';
|
|
|
2
2
|
export type { Equalizer } from './Equalizer.nitro';
|
|
3
3
|
export type { Visualizer } from './Visualizer.nitro';
|
|
4
4
|
export type { CastManager } from './CastManager.nitro';
|
|
5
|
-
export type { TrackItem, PlayerConfig, PlayerState, BufferState, PlaybackMode, PlaybackModeKind, RepeatMode, RemoteSkipMode, RemoteControlOptions, StateChangeReason, TrackChangeReason, QueueChangeReason, TrackSource, PlayerProgress, PlaybackError, PlaybackErrorCode, ShuffleResult, SkipCapability, SleepTimerState, NowPlayingFormat, AudioCodec, AudioContainer, LookaheadCacheConfig, EvictionPolicy, CacheStatus, MediaSearchRequest, MediaSearchType, MediaSearchOrigin, MediaSearchReference, MediaSearchRepeatMode, MediaSearchReleaseDateRange, MediaSearchQueueLocation, BrowseItem, BrowseSection, BrowseSnapshot, ServiceReadyReason, EqualizerBand, EqualizerPreset, ReplayGainMode, PitchCorrectionMode, VisualizerConfig, VisualizerFftSize, VisualizerFrame, VisualizerErrorReason, CastProtocol, CastStreamingModel, CastDeviceKind, CastSessionEndReason, CastLocalNetworkPermission, CastReceiverCapabilities, CastReceiver, CastRoute, CastDiscoveryState, CastSessionDiedEvent, CastLocalNetworkPermissionEvent, CastConnectOptions, AudioRouteKind, AudioRoute, } from './types';
|
|
5
|
+
export type { TrackItem, PlayerConfig, PlayerState, BufferState, PlaybackMode, PlaybackModeKind, RepeatMode, SkipToPreviousBehavior, RemoteSkipMode, RemoteControlOptions, StateChangeReason, TrackChangeReason, QueueChangeReason, TrackSource, PlayerProgress, PlaybackError, PlaybackErrorCode, ShuffleResult, SkipCapability, SleepTimerState, NowPlayingFormat, AudioCodec, AudioContainer, LookaheadCacheConfig, EvictionPolicy, CacheStatus, MediaSearchRequest, MediaSearchType, MediaSearchOrigin, MediaSearchReference, MediaSearchRepeatMode, MediaSearchReleaseDateRange, MediaSearchQueueLocation, BrowseItem, BrowseSection, BrowseSnapshot, ServiceReadyReason, EqualizerBand, EqualizerPreset, ReplayGainMode, PitchCorrectionMode, VisualizerConfig, VisualizerFftSize, VisualizerFrame, VisualizerErrorReason, CastProtocol, CastStreamingModel, CastDeviceKind, CastSessionEndReason, CastLocalNetworkPermission, CastReceiverCapabilities, CastReceiver, CastRoute, CastDiscoveryState, CastSessionDiedEvent, CastLocalNetworkPermissionEvent, CastConnectOptions, AudioRouteKind, AudioRoute, } from './types';
|
|
6
6
|
export { getCastManager, getEqualizer, getTrackPlayer, getVisualizer } from './clients';
|
|
7
7
|
export { registerPlaybackService, unregisterPlaybackService, } from './playbackService';
|
|
8
8
|
export type { PlaybackServiceHandler, PlaybackServiceFactory, } from './playbackService';
|