react-native-queue-player 1.0.3 → 1.0.4
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/BufferStateTranslator.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/CarConnectionState.kt +46 -0
- package/android/src/main/java/com/margelo/nitro/queueplayer/Equalizer.kt +1 -3
- package/android/src/main/java/com/margelo/nitro/queueplayer/HeadlessJsMediaService.kt +2 -4
- package/android/src/main/java/com/margelo/nitro/queueplayer/LookaheadCacheWriter.kt +7 -9
- package/android/src/main/java/com/margelo/nitro/queueplayer/MilestoneTracker.kt +1 -2
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackService.kt +5 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/PlaybackServiceCallback.kt +8 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/ProgressEmissionGate.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueMutationArithmetic.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/QueueSkipArithmetic.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/RemoteCommandDeduper.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/SleepTimerCore.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +21 -27
- package/android/src/main/java/com/margelo/nitro/queueplayer/TransportStateTranslator.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/AudioPipelineRenderersFactoryTest.kt +2 -5
- package/android/src/test/java/com/margelo/nitro/queueplayer/BrowseCallbackRegistryTest.kt +1 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineLifecycleTest.kt +1 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineReplayGainTest.kt +3 -7
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerEngineTest.kt +1 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerKtTest.kt +1 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/EqualizerLegacyEngineTest.kt +2 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/FFTProcessorTeeTest.kt +1 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineLifecycleTest.kt +0 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/GaplessEngineReplayGainTest.kt +3 -8
- package/android/src/test/java/com/margelo/nitro/queueplayer/HeadlessJsMediaServiceTest.kt +2 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/MediaItemBuilderTest.kt +2 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/MilestoneTrackerTest.kt +1 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt +1 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt +1 -1
- package/android/src/test/java/com/margelo/nitro/queueplayer/PitchCorrectionTest.kt +2 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/PlaybackServiceCallbackTest.kt +76 -7
- package/android/src/test/java/com/margelo/nitro/queueplayer/ProgressEmissionGateTest.kt +1 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerEventsTest.kt +1 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerMutationTest.kt +2 -4
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerPitchCorrectionTest.kt +1 -2
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerPlaybackModeTest.kt +2 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerReplayGainTest.kt +1 -3
- package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerTransportTest.kt +2 -5
- package/android/src/test/java/com/margelo/nitro/queueplayer/VisualizerKtTest.kt +1 -2
- package/ios/AVPlayerItemQueueItemId.swift +2 -2
- package/ios/AVQueueBuilder.swift +12 -14
- package/ios/CarPlayCoordinator.swift +28 -0
- package/ios/CarPlaySceneDelegate.swift +2 -0
- package/ios/CrossfadeEngine.swift +4 -5
- package/ios/InputGuards.swift +3 -3
- package/ios/LookaheadCache.swift +4 -6
- package/ios/LookaheadCachePrefetcher.swift +1 -1
- package/ios/MilestoneTracker.swift +1 -2
- package/ios/PlaybackErrorMapping.swift +6 -7
- package/ios/PlayerStateDerivation.swift +1 -2
- package/ios/ProgressEmissionGate.swift +1 -1
- package/ios/QueueMutationArithmetic.swift +2 -2
- package/ios/QueueSkipArithmetic.swift +8 -12
- package/ios/Siri/PlayMediaIntentHandler.swift +1 -5
- package/ios/SleepTimerCore.swift +1 -1
- package/ios/Tests/AudioTapProviderReplayGainTests.swift +5 -9
- package/ios/Tests/CarPlayBridgeTests.swift +2 -6
- package/ios/Tests/CarPlayBrowseBuilderTests.swift +1 -2
- package/ios/Tests/CarPlayCoordinatorTests.swift +35 -0
- package/ios/Tests/EQProcessorTests.swift +1 -4
- package/ios/Tests/EQTapTests.swift +1 -2
- package/ios/Tests/EqualizerAudioMixProviderTests.swift +3 -4
- package/ios/Tests/EqualizerHybridTests.swift +1 -3
- package/ios/Tests/FFTProcessorTests.swift +1 -3
- package/ios/Tests/GaplessEngineLifecycleTests.swift +0 -2
- package/ios/Tests/LookaheadCacheEvictionTests.swift +1 -4
- package/ios/Tests/MetadataReaderTests.swift +0 -2
- package/ios/Tests/MilestoneTrackerTests.swift +1 -3
- package/ios/Tests/NowPlayingFormatExtractorTests.swift +3 -4
- package/ios/Tests/PlaybackErrorMappingTests.swift +0 -3
- package/ios/Tests/PlaybackModeStateMachineTests.swift +5 -9
- package/ios/Tests/PlayerStateDerivationTests.swift +1 -2
- package/ios/Tests/ProgressEmissionGateTests.swift +1 -3
- package/ios/Tests/QueueMutationArithmeticTests.swift +0 -3
- package/ios/Tests/RemoteCommandsTests.swift +2 -5
- package/ios/Tests/ReplayGainExtractorTests.swift +0 -5
- package/ios/Tests/SkipCapabilityTests.swift +4 -7
- package/ios/Tests/VoiceDonationTests.swift +4 -4
- package/ios/TrackPlayer.swift +5 -2
- package/lib/typescript/TrackPlayer.nitro.d.ts +17 -0
- package/lib/typescript/TrackPlayer.nitro.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +5 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +1 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/queueplayer/HybridTrackPlayerSpec.kt +4 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +8 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +1 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +12 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +1 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +1 -0
- package/package.json +1 -1
- package/src/TrackPlayer.nitro.ts +18 -0
|
@@ -70,6 +70,9 @@ class PlaybackServiceCallbackTest {
|
|
|
70
70
|
controllerInfo = MediaSession.ControllerInfo.createTestOnlyControllerInfo(
|
|
71
71
|
"test-controller", 0, 0, 0, 0, false, android.os.Bundle.EMPTY, false
|
|
72
72
|
)
|
|
73
|
+
// Process-wide count; reset so headless connect/disconnect assertions
|
|
74
|
+
// start from a known-disconnected state regardless of test order.
|
|
75
|
+
CarConnectionState.reset()
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
@After
|
|
@@ -87,8 +90,7 @@ class PlaybackServiceCallbackTest {
|
|
|
87
90
|
|
|
88
91
|
// Controller-driven `Player.Command` interception (skip-next /
|
|
89
92
|
// skip-previous veto, transport `system`-reason stamping) now lives
|
|
90
|
-
// on [SessionCommandForwardingPlayer]
|
|
91
|
-
// SessionCommandForwardingPlayerTest. This suite keeps the
|
|
93
|
+
// on [SessionCommandForwardingPlayer]. This suite keeps the
|
|
92
94
|
// callback's connect / resumption / available-commands surface.
|
|
93
95
|
|
|
94
96
|
@Test
|
|
@@ -270,9 +272,7 @@ class PlaybackServiceCallbackTest {
|
|
|
270
272
|
// The per-controller iteration body (session.setAvailableCommands)
|
|
271
273
|
// requires a live MediaController that has completed handshake,
|
|
272
274
|
// which is not reachable from Robolectric without standing up a
|
|
273
|
-
// full MediaController.Builder.buildAsync() flow.
|
|
274
|
-
// coverage of the lock-screen-greying behaviour lives in the
|
|
275
|
-
// Maestro suites for repeat-mode + queue-boundary scenarios.
|
|
275
|
+
// full MediaController.Builder.buildAsync() flow.
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
// --- TrackPlayer-side handler implementation
|
|
@@ -615,6 +615,76 @@ class PlaybackServiceCallbackTest {
|
|
|
615
615
|
assertEquals(0, fakeEnv.headlessConnectPackages.size)
|
|
616
616
|
}
|
|
617
617
|
|
|
618
|
+
// --- isCarConnected: the process-wide flag TrackPlayer.isCarConnected() reads
|
|
619
|
+
|
|
620
|
+
@Test
|
|
621
|
+
fun `CarConnectionState is disconnected with no controller attached`() {
|
|
622
|
+
assertFalse(CarConnectionState.isConnected())
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
@Test
|
|
626
|
+
fun `headless connect marks connected, last disconnect marks disconnected`() {
|
|
627
|
+
val cb = PlaybackServiceCallback(RecordingEnvironment(service))
|
|
628
|
+
val gearhead = controllerFor("com.google.android.projection.gearhead")
|
|
629
|
+
cb.onConnect(session, gearhead)
|
|
630
|
+
assertTrue("connected after the first headless controller", CarConnectionState.isConnected())
|
|
631
|
+
cb.onDisconnected(session, gearhead)
|
|
632
|
+
assertFalse("disconnected after the last controller drops", CarConnectionState.isConnected())
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
@Test
|
|
636
|
+
fun `stays connected while any headless controller remains`() {
|
|
637
|
+
val cb = PlaybackServiceCallback(RecordingEnvironment(service))
|
|
638
|
+
val gearhead = controllerFor("com.google.android.projection.gearhead")
|
|
639
|
+
val wear = controllerFor("com.google.android.wearable.app")
|
|
640
|
+
cb.onConnect(session, gearhead)
|
|
641
|
+
cb.onConnect(session, wear)
|
|
642
|
+
cb.onDisconnected(session, gearhead)
|
|
643
|
+
assertTrue("still connected with one controller remaining", CarConnectionState.isConnected())
|
|
644
|
+
cb.onDisconnected(session, wear)
|
|
645
|
+
assertFalse("disconnected once the last controller drops", CarConnectionState.isConnected())
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
@Test
|
|
649
|
+
fun `non-headless controller leaves the flag disconnected`() {
|
|
650
|
+
val cb = PlaybackServiceCallback(RecordingEnvironment(service))
|
|
651
|
+
val phone = controllerFor("com.example.regular.app")
|
|
652
|
+
cb.onConnect(session, phone)
|
|
653
|
+
assertFalse(CarConnectionState.isConnected())
|
|
654
|
+
cb.onDisconnected(session, phone)
|
|
655
|
+
assertFalse(CarConnectionState.isConnected())
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
@Test
|
|
659
|
+
fun `untracked headless disconnect does not mark connected`() {
|
|
660
|
+
val cb = PlaybackServiceCallback(RecordingEnvironment(service))
|
|
661
|
+
val gearhead = controllerFor("com.google.android.projection.gearhead")
|
|
662
|
+
// A stray disconnect for a controller that never connected (service-
|
|
663
|
+
// restart edge) must not mark connected; the count floors at zero.
|
|
664
|
+
cb.onDisconnected(session, gearhead)
|
|
665
|
+
assertFalse(CarConnectionState.isConnected())
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
@Test
|
|
669
|
+
fun `a stray disconnect floors the count so a later connect still reads connected`() {
|
|
670
|
+
val cb = PlaybackServiceCallback(RecordingEnvironment(service))
|
|
671
|
+
val gearhead = controllerFor("com.google.android.projection.gearhead")
|
|
672
|
+
cb.onConnect(session, gearhead)
|
|
673
|
+
cb.onDisconnected(session, gearhead)
|
|
674
|
+
// Extra (stray) disconnect: the count must floor at zero, not go negative.
|
|
675
|
+
cb.onDisconnected(session, gearhead)
|
|
676
|
+
assertFalse("disconnected after the real drop", CarConnectionState.isConnected())
|
|
677
|
+
// If the stray disconnect had driven the count to -1, this connect would
|
|
678
|
+
// land it back at 0 and read disconnected.
|
|
679
|
+
cb.onConnect(session, gearhead)
|
|
680
|
+
assertTrue("connected again after a fresh attach", CarConnectionState.isConnected())
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
private fun controllerFor(packageName: String): MediaSession.ControllerInfo =
|
|
684
|
+
MediaSession.ControllerInfo.createTestOnlyControllerInfo(
|
|
685
|
+
packageName, 0, 0, 0, 0, false, android.os.Bundle.EMPTY, false
|
|
686
|
+
)
|
|
687
|
+
|
|
618
688
|
@Test
|
|
619
689
|
fun `becoming-noisy receiver unregisters in onDestroy`() {
|
|
620
690
|
val handler = RecordingHandler()
|
|
@@ -691,8 +761,7 @@ class PlaybackServiceCallbackTest {
|
|
|
691
761
|
|
|
692
762
|
override fun getCachedSkipCapability(): SkipCapability = capability
|
|
693
763
|
|
|
694
|
-
//
|
|
695
|
-
// suite covers the controller-driven callback routing only.
|
|
764
|
+
// This suite covers the controller-driven callback routing only.
|
|
696
765
|
override fun onMediaPlay() {}
|
|
697
766
|
override fun onMediaPause() {}
|
|
698
767
|
override fun onMediaTogglePlayPause() {}
|
|
@@ -6,9 +6,7 @@ import org.junit.Test
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Unit tests for [ProgressEmissionGate] — the pure throttle behind the
|
|
9
|
-
* configurable / background-reduced `onProgress` cadence.
|
|
10
|
-
* progress tick + the ProcessLifecycleOwner observer) is covered by
|
|
11
|
-
* [TrackPlayerProgressThrottleTest] and the progress demo suites.
|
|
9
|
+
* configurable / background-reduced `onProgress` cadence.
|
|
12
10
|
*
|
|
13
11
|
* Most tests pass `samplerPeriodMs = 0.0` to exercise exact interval semantics;
|
|
14
12
|
* the jitter + quantization tests pass the production `500` to verify the
|
|
@@ -348,8 +348,7 @@ class TrackPlayerEventsTest {
|
|
|
348
348
|
//
|
|
349
349
|
// The measured silence gap rides on onTrackChange's 4th arg only for a
|
|
350
350
|
// natural gapless auto-advance. Every other transition carries `null`.
|
|
351
|
-
// The positive numeric value
|
|
352
|
-
// verified on-device by the `gapless-album` demo suite.
|
|
351
|
+
// The positive numeric value is a native nanoTime delta.
|
|
353
352
|
|
|
354
353
|
@Test
|
|
355
354
|
fun `handleMediaItemTransition PLAYLIST_CHANGED reports null transitionGapMs`() {
|
|
@@ -20,10 +20,8 @@ import org.robolectric.annotation.Config
|
|
|
20
20
|
* ExoPlayer timeline synchronisation (`mediaItemCount`,
|
|
21
21
|
* `currentMediaItemIndex` where applicable).
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* correctness — that the arithmetic result is actually applied
|
|
26
|
-
* to both state structures in concert.
|
|
23
|
+
* These tests focus on wiring correctness — that the arithmetic
|
|
24
|
+
* result is actually applied to both state structures in concert.
|
|
27
25
|
*/
|
|
28
26
|
@RunWith(RobolectricTestRunner::class)
|
|
29
27
|
@Config(sdk = [34])
|
package/android/src/test/java/com/margelo/nitro/queueplayer/TrackPlayerPitchCorrectionTest.kt
CHANGED
|
@@ -19,8 +19,7 @@ import org.robolectric.annotation.Config
|
|
|
19
19
|
* `initHybrid()` → JNI which the Robolectric runtime doesn't load. This
|
|
20
20
|
* file pins the default and the synchronous read path
|
|
21
21
|
* ([TrackPlayer.getPitchCorrectionMode]) against the canonical state
|
|
22
|
-
* field
|
|
23
|
-
* the demo `pitch-correction` suite.
|
|
22
|
+
* field.
|
|
24
23
|
*/
|
|
25
24
|
@RunWith(RobolectricTestRunner::class)
|
|
26
25
|
@Config(sdk = [34])
|
|
@@ -9,9 +9,8 @@ import org.robolectric.annotation.Config
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* `setPlaybackMode` / `getPlaybackMode` imperative spec coverage —
|
|
12
|
-
* the engine-swap path
|
|
13
|
-
*
|
|
14
|
-
* service + ExoPlayer to verify a real swap.
|
|
12
|
+
* the engine-swap path can't be fully bound under Robolectric, which
|
|
13
|
+
* can't stand up the service + ExoPlayer to verify a real swap.
|
|
15
14
|
*/
|
|
16
15
|
@RunWith(RobolectricTestRunner::class)
|
|
17
16
|
@Config(sdk = [34])
|
|
@@ -20,9 +20,7 @@ import org.robolectric.annotation.Config
|
|
|
20
20
|
* [ReplayGainConfig.setMode] then asks the active engine to
|
|
21
21
|
* recompute; this file pins the synchronous read path
|
|
22
22
|
* ([TrackPlayer.getReplayGainMode]) round-trips with
|
|
23
|
-
* [ReplayGainConfig]
|
|
24
|
-
* [ReplayGainConfigTest]. End-to-end Promise dispatch is exercised
|
|
25
|
-
* through the demo `replaygain` Maestro suite.
|
|
23
|
+
* [ReplayGainConfig].
|
|
26
24
|
*/
|
|
27
25
|
@RunWith(RobolectricTestRunner::class)
|
|
28
26
|
@Config(sdk = [34])
|
|
@@ -155,8 +155,7 @@ class TrackPlayerTransportTest {
|
|
|
155
155
|
fun `setPlaybackSpeedInternal round-trips through getPlaybackSpeed and applies the pitch mode`() {
|
|
156
156
|
// Pitch is lib-owned, driven by the pitch-correction mode. Under the
|
|
157
157
|
// default voice mode the engine holds pitch at 1.0 while the speed
|
|
158
|
-
// changes (pitch preserved).
|
|
159
|
-
// covered by PitchCorrectionTest.
|
|
158
|
+
// changes (pitch preserved).
|
|
160
159
|
player.setPlaybackSpeedInternal(1.5)
|
|
161
160
|
assertEquals(1.5, player.getPlaybackSpeed(), 0.001)
|
|
162
161
|
assertEquals(1.0f, player.player!!.playbackParameters.pitch, 0.001f)
|
|
@@ -194,9 +193,7 @@ class TrackPlayerTransportTest {
|
|
|
194
193
|
assertEquals(RepeatMode.TRACK, player.repeatModeState)
|
|
195
194
|
}
|
|
196
195
|
|
|
197
|
-
//
|
|
198
|
-
// shuffle.ts steps and by exercising shuffleQueueInternal in the
|
|
199
|
-
// sibling tests below.
|
|
196
|
+
// The sibling tests below exercise shuffleQueueInternal.
|
|
200
197
|
|
|
201
198
|
// --- basic play/pause/stop no-op
|
|
202
199
|
|
|
@@ -19,8 +19,7 @@ import java.util.concurrent.CopyOnWriteArrayList
|
|
|
19
19
|
/**
|
|
20
20
|
* Wiring coverage for the [Visualizer] HybridObject — subscriber
|
|
21
21
|
* refcount, attach/detach lifecycle against a [PlaybackEngine],
|
|
22
|
-
* engine swap behaviour.
|
|
23
|
-
* [FFTProcessorTeeTest]; this test exercises only the Hybrid surface
|
|
22
|
+
* engine swap behaviour. This test exercises only the Hybrid surface
|
|
24
23
|
* and the PlaybackEngine attach contract.
|
|
25
24
|
*/
|
|
26
25
|
@RunWith(RobolectricTestRunner::class)
|
|
@@ -31,8 +31,8 @@ extension AVPlayerItem {
|
|
|
31
31
|
/// Lib-internal identity for this AVPlayerItem. Set once at
|
|
32
32
|
/// construction by `AVQueueBuilder.makeItem`; never mutated
|
|
33
33
|
/// after. nil only for AVPlayerItems that did not flow through
|
|
34
|
-
/// the lib's queue construction path (e.g.
|
|
35
|
-
///
|
|
34
|
+
/// the lib's queue construction path (e.g. built directly via
|
|
35
|
+
/// `makeItemRaw`).
|
|
36
36
|
var queueItemId: String? {
|
|
37
37
|
get { objc_getAssociatedObject(self, &queueItemIdKey) as? String }
|
|
38
38
|
set {
|
package/ios/AVQueueBuilder.swift
CHANGED
|
@@ -3,13 +3,11 @@ import AVFoundation
|
|
|
3
3
|
/// Builder utility for `[AVPlayerItem]` arrays, with HTTP-header +
|
|
4
4
|
/// user-agent injection on remote URLs.
|
|
5
5
|
///
|
|
6
|
-
/// The
|
|
7
|
-
///
|
|
8
|
-
///
|
|
9
|
-
///
|
|
10
|
-
///
|
|
11
|
-
/// surface consumers care about is the Nitrogen-integrated TrackPlayer
|
|
12
|
-
/// methods, not this builder directly.
|
|
6
|
+
/// The `Raw`-suffix helpers take primitive arguments so they stay clear
|
|
7
|
+
/// of the Swift ↔ C++ interop boundary. They are gated behind
|
|
8
|
+
/// `@_spi(QueuePlayerTests)` so consumers who don't opt into the SPI
|
|
9
|
+
/// never see them — the API surface consumers care about is the
|
|
10
|
+
/// Nitrogen-integrated TrackPlayer methods, not this builder directly.
|
|
13
11
|
///
|
|
14
12
|
/// Constructs AVPlayerItem instances from TrackItem descriptors,
|
|
15
13
|
/// applying the PlayerConfig's custom HTTP headers + user agent.
|
|
@@ -135,10 +133,10 @@ public enum AVQueueBuilder {
|
|
|
135
133
|
item.preferredForwardBufferDuration = 0.0
|
|
136
134
|
}
|
|
137
135
|
|
|
138
|
-
/// Plain-Swift item builder. Takes primitive arguments so it
|
|
139
|
-
///
|
|
140
|
-
///
|
|
141
|
-
///
|
|
136
|
+
/// Plain-Swift item builder. Takes primitive arguments so it stays clear
|
|
137
|
+
/// of the Swift ↔ C++ interop boundary; `makeItem(for:, config:)` is the
|
|
138
|
+
/// thin adapter that unpacks the Nitrogen-generated `TrackItem` +
|
|
139
|
+
/// `PlayerConfig` and delegates here.
|
|
142
140
|
///
|
|
143
141
|
/// Gated behind `@_spi(QueuePlayerTests)` so it stays off the library's
|
|
144
142
|
/// consumer-facing public surface.
|
|
@@ -171,9 +169,9 @@ public enum AVQueueBuilder {
|
|
|
171
169
|
return item
|
|
172
170
|
}
|
|
173
171
|
|
|
174
|
-
/// Plain-Swift merging core — takes primitive arguments so it
|
|
175
|
-
///
|
|
176
|
-
///
|
|
172
|
+
/// Plain-Swift merging core — takes primitive arguments so it stays
|
|
173
|
+
/// clear of the Swift↔C++ interop boundary.
|
|
174
|
+
/// `makeItemRaw(urlString:headers:userAgent:)` below
|
|
177
175
|
/// calls this, and `makeItem(for:, config:)` unpacks its `PlayerConfig`
|
|
178
176
|
/// argument into the same primitive pair and calls `makeItemRaw`.
|
|
179
177
|
///
|
|
@@ -39,4 +39,32 @@ final class CarPlayCoordinator {
|
|
|
39
39
|
self.pending = PendingBrowseRequests()
|
|
40
40
|
self.loader = ArtworkLoader()
|
|
41
41
|
}
|
|
42
|
+
|
|
43
|
+
// MARK: - Connection state
|
|
44
|
+
|
|
45
|
+
/// Whether the CarPlay scene is currently connected. Written by
|
|
46
|
+
/// [CarPlaySceneDelegate] at scene attach / detach — the same points that
|
|
47
|
+
/// fire the `onCarConnect` / `onCarDisconnect` callbacks — and read by the
|
|
48
|
+
/// `isCarConnected()` Hybrid getter. Scene callbacks land on the main
|
|
49
|
+
/// queue; the getter can be read from any Nitro thread, so the flag is
|
|
50
|
+
/// lock-guarded.
|
|
51
|
+
///
|
|
52
|
+
/// A media app has a single CarPlay template scene, so this is a plain
|
|
53
|
+
/// attached/detached flag rather than a connection count (the Android side
|
|
54
|
+
/// counts because its headless controllers — Auto / Wear / Assistant — can
|
|
55
|
+
/// overlap).
|
|
56
|
+
private let connectionLock = NSLock()
|
|
57
|
+
private var carConnectedFlag = false
|
|
58
|
+
|
|
59
|
+
var isCarConnected: Bool {
|
|
60
|
+
connectionLock.lock()
|
|
61
|
+
defer { connectionLock.unlock() }
|
|
62
|
+
return carConnectedFlag
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func setCarConnected(_ connected: Bool) {
|
|
66
|
+
connectionLock.lock()
|
|
67
|
+
defer { connectionLock.unlock() }
|
|
68
|
+
carConnectedFlag = connected
|
|
69
|
+
}
|
|
42
70
|
}
|
|
@@ -122,6 +122,7 @@ internal class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDele
|
|
|
122
122
|
self?.refreshRootTemplate()
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
coordinator.setCarConnected(true)
|
|
125
126
|
coordinator.bridge.carConnectCallback()?()
|
|
126
127
|
}
|
|
127
128
|
|
|
@@ -139,6 +140,7 @@ internal class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDele
|
|
|
139
140
|
}
|
|
140
141
|
self.interfaceController = nil
|
|
141
142
|
self.rootTabBar = nil
|
|
143
|
+
CarPlayCoordinator.shared.setCarConnected(false)
|
|
142
144
|
CarPlayCoordinator.shared.bridge.carDisconnectCallback()?()
|
|
143
145
|
// Pending entries from this scene's session are stale — the
|
|
144
146
|
// next scene attach starts from a fresh snapshot read, so
|
|
@@ -32,11 +32,10 @@ import Foundation
|
|
|
32
32
|
/// hard-cuts (no fade) for that transition. Mid-fade route
|
|
33
33
|
/// change cancels the fade synchronously.
|
|
34
34
|
///
|
|
35
|
-
/// Runtime fade execution
|
|
36
|
-
///
|
|
37
|
-
///
|
|
38
|
-
///
|
|
39
|
-
/// boundary fade-vs-cut decision) are unit-tested.
|
|
35
|
+
/// Runtime fade execution is a multi-second `setVolumeRamp` schedule
|
|
36
|
+
/// plus standby promotion to leading at fade-end, built on
|
|
37
|
+
/// deterministic primitives (curve math, sub-ramp count, route
|
|
38
|
+
/// classification, boundary fade-vs-cut decision).
|
|
40
39
|
final class CrossfadeEngine: PlaybackEngine {
|
|
41
40
|
|
|
42
41
|
// MARK: - Construction
|
package/ios/InputGuards.swift
CHANGED
|
@@ -16,9 +16,9 @@ import Foundation
|
|
|
16
16
|
/// the public API surface (matches the documented contract +
|
|
17
17
|
/// Android parity).
|
|
18
18
|
///
|
|
19
|
-
/// Helpers live here as pure functions
|
|
20
|
-
///
|
|
21
|
-
///
|
|
19
|
+
/// Helpers live here as pure functions, decoupled from `TrackPlayer`
|
|
20
|
+
/// (which depends on AVQueuePlayer + AVAudioSession + Nitrogen-generated
|
|
21
|
+
/// types).
|
|
22
22
|
///
|
|
23
23
|
/// Gated behind `@_spi(QueuePlayerTests)` so the helpers stay off
|
|
24
24
|
/// the library's consumer-facing public surface — same SPI scope
|
package/ios/LookaheadCache.swift
CHANGED
|
@@ -810,13 +810,11 @@ public enum LookaheadCacheError: Error {
|
|
|
810
810
|
}
|
|
811
811
|
|
|
812
812
|
/// Abstraction over `LookaheadCache.download` so the prefetcher
|
|
813
|
-
/// can
|
|
814
|
-
///
|
|
815
|
-
/// `LookaheadCache` itself (which conforms below).
|
|
813
|
+
/// can accept an alternate conformer without touching `URLSession`.
|
|
814
|
+
/// Production code uses `LookaheadCache` itself (which conforms below).
|
|
816
815
|
///
|
|
817
|
-
/// Gated behind `@_spi(QueuePlayerTests)
|
|
818
|
-
///
|
|
819
|
-
/// via `LookaheadCachePrefetcher.init(cache:)`.
|
|
816
|
+
/// Gated behind `@_spi(QueuePlayerTests)`. Production callers reach
|
|
817
|
+
/// `LookaheadCache` directly via `LookaheadCachePrefetcher.init(cache:)`.
|
|
820
818
|
@_spi(QueuePlayerTests)
|
|
821
819
|
public protocol LookaheadCacheDownloader: AnyObject {
|
|
822
820
|
func download(track: TrackItem, headers: [String: String]?) async throws
|
|
@@ -148,7 +148,7 @@ public final class LookaheadCachePrefetcher {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
/// SPI initializer — accepts any `LookaheadCacheDownloader`
|
|
151
|
-
/// conformer so
|
|
151
|
+
/// conformer so callers can inject an alternate. Off the
|
|
152
152
|
/// consumer-facing surface; production calls reach the concrete
|
|
153
153
|
/// `LookaheadCache`-typed init above.
|
|
154
154
|
@_spi(QueuePlayerTests)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/// Tracks fixed playback-progress milestones (25/50/75/90%) for the
|
|
2
|
-
/// current track playthrough. Pure + deterministic (no AVFoundation)
|
|
3
|
-
/// it unit-tests directly.
|
|
2
|
+
/// current track playthrough. Pure + deterministic (no AVFoundation).
|
|
4
3
|
///
|
|
5
4
|
/// A threshold emits only when forward playback crosses it: `seek` moves
|
|
6
5
|
/// the baseline WITHOUT emitting (so thresholds a seek jumps over are
|
|
@@ -77,24 +77,23 @@ public enum PlaybackErrorMapping {
|
|
|
77
77
|
return result
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
// MARK: -
|
|
80
|
+
// MARK: - String-based "Raw" surface
|
|
81
81
|
//
|
|
82
82
|
// NOTES.md §6: Swift's `-cxx-interoperability-mode=default` hides
|
|
83
83
|
// members of functions whose signatures touch Nitrogen-generated
|
|
84
84
|
// C++-backed types when viewed from a different Swift module.
|
|
85
85
|
// `classify` / `isTransient` reference `PlaybackErrorCode` — a
|
|
86
|
-
// Nitrogen type — so
|
|
87
|
-
// parallel surface
|
|
88
|
-
// stringValue)
|
|
89
|
-
// typed surface above; tests use the Raw surface.
|
|
86
|
+
// Nitrogen type — so a different module can't reach them directly.
|
|
87
|
+
// This parallel surface takes/returns Strings (the enum's
|
|
88
|
+
// stringValue). Production code uses the typed surface above.
|
|
90
89
|
|
|
91
|
-
///
|
|
90
|
+
/// String-returning variant of `classify`.
|
|
92
91
|
@_spi(QueuePlayerTests)
|
|
93
92
|
public static func classifyRaw(_ underlying: NSError?) -> String {
|
|
94
93
|
return classify(underlying).stringValue
|
|
95
94
|
}
|
|
96
95
|
|
|
97
|
-
///
|
|
96
|
+
/// String-input variant of `isTransient`.
|
|
98
97
|
@_spi(QueuePlayerTests)
|
|
99
98
|
public static func isTransientRaw(_ codeString: String) -> Bool {
|
|
100
99
|
guard let code = PlaybackErrorCode(fromString: codeString) else { return false }
|
|
@@ -4,8 +4,7 @@ import AVFoundation
|
|
|
4
4
|
/// status. Shared by BOTH engines (gapless + crossfade) via
|
|
5
5
|
/// `PlaybackEngine.timeControlStatus`, so the two report identical states for
|
|
6
6
|
/// identical situations — no per-engine special-casing for consumers. Mirrors
|
|
7
|
-
/// Android's `TransportStateTranslator`; pure + deterministic
|
|
8
|
-
/// directly.
|
|
7
|
+
/// Android's `TransportStateTranslator`; pure + deterministic.
|
|
9
8
|
///
|
|
10
9
|
/// iOS has no native "ended" player state (unlike Android's `STATE_ENDED`), so
|
|
11
10
|
/// `reachedQueueEnd` is a latch that `TrackPlayer` sets when the queue drains
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// Throttles the JS `onProgress` fan-out to a configurable cadence, with an
|
|
2
2
|
/// optional reduced rate while the app is backgrounded. Pure + deterministic
|
|
3
|
-
/// (the caller supplies a monotonic timestamp)
|
|
3
|
+
/// (the caller supplies a monotonic timestamp).
|
|
4
4
|
///
|
|
5
5
|
/// This gates ONLY the JS emission — the native 2 Hz sampler keeps running, so
|
|
6
6
|
/// milestone detection and buffer-state recomputation stay full-rate. A tick
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
|
|
3
3
|
/// Pure-Swift index-shift arithmetic for queue mutations. Extracted
|
|
4
|
-
/// from `TrackPlayer.swift` so the shift semantics
|
|
4
|
+
/// from `TrackPlayer.swift` so the shift semantics stand alone,
|
|
5
5
|
/// without an AVQueuePlayer. Mirrors
|
|
6
6
|
/// `android/.../QueueMutationArithmetic.kt` 1:1 — same function
|
|
7
|
-
/// shapes, same input/output semantics
|
|
7
|
+
/// shapes, same input/output semantics.
|
|
8
8
|
///
|
|
9
9
|
/// The helper only computes the new `currentTrackIndex` and (for
|
|
10
10
|
/// `removeFromQueue`) which original indices apply after dedup /
|
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
/// Pure-Swift skip-index arithmetic. Extracted from `TrackPlayer.swift`
|
|
2
|
-
/// so the logic
|
|
3
|
-
///
|
|
4
|
-
///
|
|
5
|
-
/// `-cxx-interoperability-mode=default` settings
|
|
6
|
-
///
|
|
7
|
-
/// types. By taking primitive arguments, these helpers stay reachable
|
|
8
|
-
/// from the XCTest bundle via plain `import QueuePlayer`.
|
|
2
|
+
/// so the logic stays clear of the Swift ↔ C++ interop boundary —
|
|
3
|
+
/// Nitrogen-generated types (`TrackItem`, `RepeatMode`) don't export
|
|
4
|
+
/// their members to importing modules under Swift's current
|
|
5
|
+
/// `-cxx-interoperability-mode=default` settings. These helpers take
|
|
6
|
+
/// primitive arguments instead.
|
|
9
7
|
///
|
|
10
8
|
/// `TrackPlayer.computeNextIndex` / `.computePreviousIndex` instance
|
|
11
9
|
/// methods are thin adapters that map the Nitrogen `RepeatMode` enum
|
|
12
10
|
/// onto `QueueSkipRepeatMode` and call through here.
|
|
13
11
|
|
|
14
12
|
/// Repeat-mode enum expressed in plain Swift. Value order (0/1/2)
|
|
15
|
-
/// matches `RepeatModeKind` in the nitro.ts spec
|
|
16
|
-
/// asserts the raw integer is robust against accidental reordering
|
|
17
|
-
/// in the Nitrogen spec.
|
|
13
|
+
/// matches `RepeatModeKind` in the nitro.ts spec.
|
|
18
14
|
///
|
|
19
|
-
/// Gated behind `@_spi(QueuePlayerTests)` so the
|
|
20
|
-
///
|
|
15
|
+
/// Gated behind `@_spi(QueuePlayerTests)` so the helper surface stays
|
|
16
|
+
/// off the library's consumer-facing API.
|
|
21
17
|
@_spi(QueuePlayerTests)
|
|
22
18
|
public enum QueueSkipRepeatMode: Int {
|
|
23
19
|
case off = 0
|
|
@@ -189,11 +189,7 @@ internal final class PlayMediaIntentHandler: NSObject, INPlayMediaIntentHandling
|
|
|
189
189
|
/// Map `INPlayMediaIntent.mediaSearch` into the lib's
|
|
190
190
|
/// `MediaSearchRequest` shape so the consumer's JS resolver sees
|
|
191
191
|
/// the same wire format regardless of voice surface (SiriKit on
|
|
192
|
-
/// iOS, Google Assistant on Android).
|
|
193
|
-
/// Maestro integration layer (`e2e/manual/siri-cold-start.md`
|
|
194
|
-
/// S1–S5) — the return type's cxx-interop bridging blocks
|
|
195
|
-
/// `@testable import` from constructing or comparing
|
|
196
|
-
/// `MediaSearchRequest` values in XCTest.
|
|
192
|
+
/// iOS, Google Assistant on Android).
|
|
197
193
|
static func buildRequest(from intent: INPlayMediaIntent) -> MediaSearchRequest {
|
|
198
194
|
let mediaSearch = intent.mediaSearch
|
|
199
195
|
let mediaName = mediaSearch?.mediaName ?? ""
|
package/ios/SleepTimerCore.swift
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
|
|
3
3
|
/// Pure state machine for the sleep timer — no player, volume, or timer
|
|
4
|
-
/// dependencies, so it
|
|
4
|
+
/// dependencies, so it stands alone. The owning `TrackPlayer`
|
|
5
5
|
/// drives it from a repeating native timer and applies the returned decision
|
|
6
6
|
/// (volume fade, pause) to the engine.
|
|
7
7
|
///
|
|
@@ -3,19 +3,15 @@ import XCTest
|
|
|
3
3
|
@testable import QueuePlayer
|
|
4
4
|
|
|
5
5
|
/// Unit tests for the ReplayGain mode + gain-math paths in
|
|
6
|
-
/// `AudioTapProvider`.
|
|
7
|
-
/// recompute walk are covered at the integration layer (auto-suite
|
|
8
|
-
/// `replaygain.ts` step). These tests drive the pure
|
|
6
|
+
/// `AudioTapProvider`. These tests drive the pure
|
|
9
7
|
/// `computeLinearGain(data:mode:)` static helper plus the mode
|
|
10
8
|
/// getter/setter round-trip.
|
|
11
9
|
final class AudioTapProviderReplayGainTests: XCTestCase {
|
|
12
10
|
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// it (NOTES.md §6). The pure gain-math matrix below is the unit
|
|
18
|
-
// surface; mode propagation lands in the integration tests.
|
|
11
|
+
// `getReplayGainMode`'s `ReplayGainMode` return type touches the
|
|
12
|
+
// Swift↔C++ interop graph, so direct equality from `@testable`
|
|
13
|
+
// XCTest can't observe it (NOTES.md §6). The pure gain-math matrix
|
|
14
|
+
// below is the unit surface.
|
|
19
15
|
|
|
20
16
|
// MARK: - computeLinearGain — strict mode + selection
|
|
21
17
|
|
|
@@ -2,9 +2,7 @@ import XCTest
|
|
|
2
2
|
@testable import QueuePlayer
|
|
3
3
|
|
|
4
4
|
/// `CarPlayBridge` tests cover only the registration/dispose/clear
|
|
5
|
-
/// state machine
|
|
6
|
-
/// production scene delegate + browse builder via integration paths;
|
|
7
|
-
/// the unit suite stays focused on the slot semantics. The
|
|
5
|
+
/// state machine — the slot semantics. The
|
|
8
6
|
/// Android-side `BrowseCallbackRegistry` exposes suspending
|
|
9
7
|
/// `await*Callback()` helpers because its dispatch sites coroutine-
|
|
10
8
|
/// poll for cold-start race recovery; iOS uses NotificationCenter
|
|
@@ -18,9 +16,7 @@ final class CarPlayBridgeTests: XCTestCase {
|
|
|
18
16
|
// The browse / playFromId slot reads return a Promise of a
|
|
19
17
|
// Nitrogen-bridged type, which Swift cxx-interop excludes from
|
|
20
18
|
// `@testable import`. Their slots use the identical `Slot<>`
|
|
21
|
-
// machinery asserted here for connect/disconnect
|
|
22
|
-
// dispatch is covered at the integration layer (scene delegate +
|
|
23
|
-
// browse builder).
|
|
19
|
+
// machinery asserted here for connect/disconnect.
|
|
24
20
|
XCTAssertNil(bridge.carConnectCallback())
|
|
25
21
|
XCTAssertNil(bridge.carDisconnectCallback())
|
|
26
22
|
}
|
|
@@ -5,8 +5,7 @@ import XCTest
|
|
|
5
5
|
/// Coverage for [CarPlayBrowseBuilder] — snapshot → template
|
|
6
6
|
/// conversion, item count + accessory mapping, pagination against
|
|
7
7
|
/// `CPListTemplate.maximumItemCount`. Direct method calls on the
|
|
8
|
-
/// builder; the scene delegate's full lifecycle is
|
|
9
|
-
/// CarPlay Sim manual playbook.
|
|
8
|
+
/// builder; the scene delegate's full lifecycle is out of scope here.
|
|
10
9
|
final class CarPlayBrowseBuilderTests: XCTestCase {
|
|
11
10
|
|
|
12
11
|
private func item(
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import QueuePlayer
|
|
3
|
+
|
|
4
|
+
/// `CarPlayCoordinator` connection-flag tests. The flag is written by the
|
|
5
|
+
/// CarPlay scene delegate at scene attach / detach; this suite locks the
|
|
6
|
+
/// get/set contract the `isCarConnected()` getter reads back.
|
|
7
|
+
final class CarPlayCoordinatorTests: XCTestCase {
|
|
8
|
+
|
|
9
|
+
override func setUp() {
|
|
10
|
+
super.setUp()
|
|
11
|
+
// Shared process-wide singleton — start from a known-disconnected state
|
|
12
|
+
// regardless of test order.
|
|
13
|
+
CarPlayCoordinator.shared.setCarConnected(false)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
override func tearDown() {
|
|
17
|
+
// Shared process-wide singleton — leave it disconnected so it can't
|
|
18
|
+
// leak a connected state into another suite.
|
|
19
|
+
CarPlayCoordinator.shared.setCarConnected(false)
|
|
20
|
+
super.tearDown()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func testCarConnectedFlagReflectsSetter() {
|
|
24
|
+
let coordinator = CarPlayCoordinator.shared
|
|
25
|
+
|
|
26
|
+
coordinator.setCarConnected(false)
|
|
27
|
+
XCTAssertFalse(coordinator.isCarConnected, "disconnected after setCarConnected(false)")
|
|
28
|
+
|
|
29
|
+
coordinator.setCarConnected(true)
|
|
30
|
+
XCTAssertTrue(coordinator.isCarConnected, "connected after setCarConnected(true)")
|
|
31
|
+
|
|
32
|
+
coordinator.setCarConnected(false)
|
|
33
|
+
XCTAssertFalse(coordinator.isCarConnected, "disconnected again after setCarConnected(false)")
|
|
34
|
+
}
|
|
35
|
+
}
|