react-native-queue-player 1.0.2 → 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/cpp/CMakeLists.txt +11 -0
- package/android/src/main/cpp/airplay_jni.cpp +5 -2
- 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 +29 -6
- package/android/src/main/java/com/margelo/nitro/queueplayer/TrackPlayer.kt +51 -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/SleepTimerCoreTest.kt +41 -0
- 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/AudioCategoryMapping.swift +1 -2
- 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 +26 -6
- 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 +9 -8
- 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/SleepTimerCoreTests.swift +39 -0
- package/ios/Tests/VoiceDonationTests.swift +4 -4
- package/ios/TrackPlayer.swift +47 -22
- 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
- package/ios/InterruptionEventMapping.swift +0 -51
- package/ios/Tests/InterruptionEventMappingTests.swift +0 -91
|
@@ -19,8 +19,7 @@ import kotlin.math.sin
|
|
|
19
19
|
* Unit coverage for [FFTProcessorTee] — buffer ingestion across PCM
|
|
20
20
|
* encodings, intervalMs throttling, includeSamples flag, and
|
|
21
21
|
* multi-channel mixdown. The Tee callback contract is mocked
|
|
22
|
-
* directly
|
|
23
|
-
* Maestro layer (no Robolectric harness for ExoPlayer audio output).
|
|
22
|
+
* directly.
|
|
24
23
|
*/
|
|
25
24
|
@RunWith(RobolectricTestRunner::class)
|
|
26
25
|
@Config(sdk = [34])
|
|
@@ -15,8 +15,6 @@ import org.robolectric.annotation.Config
|
|
|
15
15
|
/**
|
|
16
16
|
* Lifecycle smoke coverage for [GaplessEngine] — empty-state
|
|
17
17
|
* sentinels and the idempotent / delegate-clearing release contract.
|
|
18
|
-
* Behavioural coverage of transport / queue mutation runs through
|
|
19
|
-
* the existing TrackPlayer integration suites + Maestro flows.
|
|
20
18
|
*/
|
|
21
19
|
@RunWith(RobolectricTestRunner::class)
|
|
22
20
|
@Config(sdk = [34])
|
|
@@ -19,10 +19,8 @@ import org.robolectric.annotation.Config
|
|
|
19
19
|
* Real Media3 metadata dispatch is not emulated by Robolectric
|
|
20
20
|
* without playback, so the listener body composes its work through
|
|
21
21
|
* the engine-internal `handleReplayGainParse` helper which these
|
|
22
|
-
* tests drive directly with synthetic [ReplayGainData].
|
|
23
|
-
*
|
|
24
|
-
* end-to-end propagation lands in the demo app's `replaygain`
|
|
25
|
-
* Maestro suite. This file covers the no-data baseline,
|
|
22
|
+
* tests drive directly with synthetic [ReplayGainData].
|
|
23
|
+
* This file covers the no-data baseline,
|
|
26
24
|
* mode-change-without-data behaviour, and the
|
|
27
25
|
* `handleReplayGainParse` cache + recompute contract.
|
|
28
26
|
*/
|
|
@@ -147,8 +145,5 @@ class GaplessEngineReplayGainTest {
|
|
|
147
145
|
|
|
148
146
|
// The cache-clear-on-transition path lives inline in the
|
|
149
147
|
// `Player.Listener.onMediaItemTransition` body. Robolectric does not
|
|
150
|
-
// drive that callback without real playback
|
|
151
|
-
// covered at the integration layer by `RNQP-Demo/src/tests/suites/
|
|
152
|
-
// replaygain.ts` (steps `cross-track-active-updates` +
|
|
153
|
-
// `skip-next-mid-playback-resets-from-stale-cache`).
|
|
148
|
+
// drive that callback without real playback.
|
|
154
149
|
}
|
|
@@ -21,11 +21,9 @@ import org.robolectric.annotation.Config
|
|
|
21
21
|
* surface: the default no-task `getTaskConfig` contract and the
|
|
22
22
|
* process-wide wake-lock helper.
|
|
23
23
|
*
|
|
24
|
-
* The full `onStartCommand` chain
|
|
24
|
+
* The full `onStartCommand` chain routes through Media3's
|
|
25
25
|
* `MediaLibraryService.onStartCommand` and into native code that
|
|
26
|
-
* Robolectric can't satisfy
|
|
27
|
-
* [PlaybackServiceLifecycleTest] (which subclasses into the new
|
|
28
|
-
* `HeadlessJsMediaService` base) plus the Maestro demo cycle.
|
|
26
|
+
* Robolectric can't satisfy, so it is out of scope here.
|
|
29
27
|
*/
|
|
30
28
|
@RunWith(RobolectricTestRunner::class)
|
|
31
29
|
@Config(sdk = [34])
|
|
@@ -26,11 +26,10 @@ import org.robolectric.annotation.Config
|
|
|
26
26
|
* both produce items — the file/remote routing difference is a
|
|
27
27
|
* DataSource.Factory concern, not a MediaItem concern).
|
|
28
28
|
*
|
|
29
|
-
* Factory-building is
|
|
29
|
+
* Factory-building is checked by type only (the internal
|
|
30
30
|
* `DefaultHttpDataSource.Factory` is configured via a builder chain
|
|
31
31
|
* whose effective state is hard to read back without reflection —
|
|
32
|
-
* the factory is wired into ExoPlayer at construction time
|
|
33
|
-
* exercised implicitly through the TrackPlayer lifecycle tests).
|
|
32
|
+
* the factory is wired into ExoPlayer at construction time).
|
|
34
33
|
*
|
|
35
34
|
* @Config sdk=34 — Robolectric 4.11.1's max supported SDK.
|
|
36
35
|
*/
|
|
@@ -5,9 +5,7 @@ import org.junit.Test
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Unit tests for [MilestoneTracker] — the pure 25/50/75/90% crossing
|
|
8
|
-
* logic behind `onPlaybackMilestone`.
|
|
9
|
-
* seek, track-change reset) is covered by the `playback-milestones`
|
|
10
|
-
* demo suite.
|
|
8
|
+
* logic behind `onPlaybackMilestone`.
|
|
11
9
|
*/
|
|
12
10
|
class MilestoneTrackerTest {
|
|
13
11
|
|
package/android/src/test/java/com/margelo/nitro/queueplayer/NowPlayingFormatExtractorTest.kt
CHANGED
|
@@ -7,8 +7,7 @@ import org.junit.Assert.assertEquals
|
|
|
7
7
|
import org.junit.Test
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Pure-mapping coverage for [NowPlayingFormatExtractor].
|
|
11
|
-
* `extract(player:)` path is exercised through demo suites; these
|
|
10
|
+
* Pure-mapping coverage for [NowPlayingFormatExtractor]. These
|
|
12
11
|
* tests pin the codec / container tables so the JS-visible enum can't
|
|
13
12
|
* drift without a test failure.
|
|
14
13
|
*/
|
package/android/src/test/java/com/margelo/nitro/queueplayer/PitchAwareAudioProcessorChainTest.kt
CHANGED
|
@@ -17,7 +17,7 @@ import org.robolectric.annotation.Config
|
|
|
17
17
|
/**
|
|
18
18
|
* Routing coverage for [PitchAwareAudioProcessorChain]: which stretcher is
|
|
19
19
|
* actually stretching for each [PitchCorrectionMode] and speed. The native
|
|
20
|
-
* stretch itself
|
|
20
|
+
* stretch itself runs on-device; these assert only the mode gate, which
|
|
21
21
|
* runs without the native library.
|
|
22
22
|
*
|
|
23
23
|
* Note Signalsmith is intentionally ALWAYS active on the float path (so Media3's
|
|
@@ -10,8 +10,8 @@ import org.robolectric.annotation.Config
|
|
|
10
10
|
* Unit coverage for [PitchCorrection.playbackParameters], the pure mapping
|
|
11
11
|
* from a [PitchCorrectionMode] + speed to Media3 [androidx.media3.common.PlaybackParameters].
|
|
12
12
|
*
|
|
13
|
-
* Engine wiring (which player the params land on) is
|
|
14
|
-
*
|
|
13
|
+
* Engine wiring (which player the params land on) is out of scope for
|
|
14
|
+
* this pure test.
|
|
15
15
|
*/
|
|
16
16
|
@RunWith(RobolectricTestRunner::class)
|
|
17
17
|
@Config(sdk = [34])
|
|
@@ -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
|
|
@@ -111,6 +111,8 @@ class SleepTimerCoreTest {
|
|
|
111
111
|
assertTrue(converted.stateChanged)
|
|
112
112
|
assertFalse(t.isEndOfTrack)
|
|
113
113
|
assertTrue(t.isActive)
|
|
114
|
+
// Deadline lands AT the boundary (tail=50s), not 1s past it.
|
|
115
|
+
assertEquals(2000L + 50_000L, t.deadlineEpochMs)
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
@Test
|
|
@@ -122,4 +124,43 @@ class SleepTimerCoreTest {
|
|
|
122
124
|
assertNull(t.deadlineEpochMs)
|
|
123
125
|
assertNull(t.remainingSeconds(0))
|
|
124
126
|
}
|
|
127
|
+
|
|
128
|
+
@Test
|
|
129
|
+
fun `end-of-track fires a pause at the track-end signal`() {
|
|
130
|
+
val t = SleepTimerCore()
|
|
131
|
+
t.armEndOfTrack()
|
|
132
|
+
// Fires at the real track-end signal, before any tail conversion and even
|
|
133
|
+
// when the duration was never known (never ticked).
|
|
134
|
+
val fired = t.fireAtTrackEnd()
|
|
135
|
+
assertTrue(fired.pauseNow)
|
|
136
|
+
assertTrue(fired.stateChanged)
|
|
137
|
+
assertFalse(t.isActive)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@Test
|
|
141
|
+
fun `end-of-track signal fires after tail conversion`() {
|
|
142
|
+
val t = SleepTimerCore()
|
|
143
|
+
t.armEndOfTrack()
|
|
144
|
+
t.tick(1000, trackDurationSec = 200.0, trackPositionSec = 150.0) // converts to fade
|
|
145
|
+
val fired = t.fireAtTrackEnd()
|
|
146
|
+
assertTrue(fired.pauseNow)
|
|
147
|
+
assertFalse(t.isActive)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@Test
|
|
151
|
+
fun `fireAtTrackEnd is idempotent`() {
|
|
152
|
+
val t = SleepTimerCore()
|
|
153
|
+
t.armEndOfTrack()
|
|
154
|
+
t.fireAtTrackEnd()
|
|
155
|
+
assertFalse(t.fireAtTrackEnd().pauseNow)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@Test
|
|
159
|
+
fun `fireAtTrackEnd ignores a duration timer`() {
|
|
160
|
+
val t = SleepTimerCore()
|
|
161
|
+
t.armDuration(seconds = 60.0, nowMs = 0)
|
|
162
|
+
val fired = t.fireAtTrackEnd()
|
|
163
|
+
assertFalse(fired.pauseNow)
|
|
164
|
+
assertTrue(t.isActive)
|
|
165
|
+
}
|
|
125
166
|
}
|
|
@@ -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
|
///
|
|
@@ -4,8 +4,7 @@ import AVFoundation
|
|
|
4
4
|
/// to the AVAudioSession `Mode` + `CategoryOptions` used during
|
|
5
5
|
/// `AudioSession.activate()`.
|
|
6
6
|
///
|
|
7
|
-
/// Pure function; no instance state. Mirrors `PlaybackErrorMapping
|
|
8
|
-
/// + `InterruptionEventMapping`.
|
|
7
|
+
/// Pure function; no instance state. Mirrors `PlaybackErrorMapping`.
|
|
9
8
|
///
|
|
10
9
|
/// `audioContentType == .speech` triggers two effects:
|
|
11
10
|
/// 1. AVAudioSession mode flips to `.spokenAudio` (per Apple HIG —
|
|
@@ -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
|