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
|
@@ -36,6 +36,13 @@ target_include_directories(alac PUBLIC
|
|
|
36
36
|
${THIRD_PARTY}/alac/codec
|
|
37
37
|
${THIRD_PARTY}/alac/addons
|
|
38
38
|
)
|
|
39
|
+
# Apple's vendored ALAC reference codec carries benign unused-variable noise we
|
|
40
|
+
# don't patch upstream; silence just those two categories for this third-party
|
|
41
|
+
# target so a consumer's release build stays quiet. Our own code keeps -Wall.
|
|
42
|
+
target_compile_options(alac PRIVATE
|
|
43
|
+
-Wno-unused-const-variable
|
|
44
|
+
-Wno-unused-but-set-variable
|
|
45
|
+
)
|
|
39
46
|
|
|
40
47
|
# --- Curve25519 / Ed25519 (retained for libraop / AP1 only) ---
|
|
41
48
|
add_library(curve25519 STATIC
|
|
@@ -70,6 +77,10 @@ add_library(pair_ap STATIC
|
|
|
70
77
|
)
|
|
71
78
|
target_compile_definitions(pair_ap PRIVATE
|
|
72
79
|
CONFIG_OPENSSL=1
|
|
80
|
+
# pair.c uses OpenSSL's legacy SHA*_Final API, deprecated in OpenSSL 3.0.
|
|
81
|
+
# OPENSSL_SUPPRESS_DEPRECATED is OpenSSL's own opt-in for intentional
|
|
82
|
+
# legacy-API use; the raop + airplay2 targets already set it.
|
|
83
|
+
OPENSSL_SUPPRESS_DEPRECATED
|
|
73
84
|
)
|
|
74
85
|
target_include_directories(pair_ap PUBLIC
|
|
75
86
|
${PAIR_AP_DIR}
|
|
@@ -67,14 +67,17 @@ struct JniSession {
|
|
|
67
67
|
|
|
68
68
|
// -- Drain thread --
|
|
69
69
|
pthread_t thread;
|
|
70
|
-
|
|
70
|
+
// Stop flag: set on the JNI/control thread, polled in the drain thread's
|
|
71
|
+
// loop. Atomic so the stop is a data-race-free, promptly-visible read
|
|
72
|
+
// (the ring buffer has its own mutex; this only gates the loop).
|
|
73
|
+
std::atomic<bool> drain_running{false};
|
|
71
74
|
|
|
72
75
|
// -- Tracking --
|
|
73
76
|
std::atomic<int64_t> bytes_sent{0}; // total PCM bytes sent to RAOP
|
|
74
77
|
std::atomic<int64_t> chunks_sent{0}; // total chunks sent
|
|
75
78
|
|
|
76
79
|
JniSession() : raop(nullptr), write_pos(0), read_pos(0),
|
|
77
|
-
|
|
80
|
+
thread{} {
|
|
78
81
|
memset(ring, 0, sizeof(ring));
|
|
79
82
|
pthread_mutex_init(&mutex, nullptr);
|
|
80
83
|
}
|
|
@@ -9,7 +9,7 @@ import androidx.media3.common.Player
|
|
|
9
9
|
*
|
|
10
10
|
* Extracted from [TrackPlayer.computeBufferState] so the discriminator
|
|
11
11
|
* between an initial `BUFFERING` load and a mid-playback `STALLED`
|
|
12
|
-
* rebuffer
|
|
12
|
+
* rebuffer stands alone without a live ExoPlayer, matching the
|
|
13
13
|
* [TransportStateTranslator] pattern.
|
|
14
14
|
*/
|
|
15
15
|
internal object BufferStateTranslator {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
package com.margelo.nitro.queueplayer
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.VisibleForTesting
|
|
4
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Process-wide count of attached headless (automotive) controllers, backing
|
|
8
|
+
* the `isCarConnected()` getter. Holds the same "a car is connected" state
|
|
9
|
+
* the `onCarConnect` / `onCarDisconnect` events transition between, so a
|
|
10
|
+
* consumer that missed the connect event — because it fired before the
|
|
11
|
+
* consumer registered its handler in this process — can still gate car-only
|
|
12
|
+
* work (e.g. browse-snapshot pushes) on the current connection.
|
|
13
|
+
*
|
|
14
|
+
* The count is process-scoped rather than tied to a [PlaybackService]
|
|
15
|
+
* instance so it stays correct across a service restart while a controller
|
|
16
|
+
* remains attached: the new instance's `onConnect` and the dying instance's
|
|
17
|
+
* late `onConnect`/`onDisconnected` all mutate this single source of truth, so
|
|
18
|
+
* the getter can't momentarily read disconnected mid-handoff. It is
|
|
19
|
+
* floor-guarded so a stray disconnect can't drive it negative, and process
|
|
20
|
+
* death clears it (a fresh process starts disconnected), which is correct.
|
|
21
|
+
*
|
|
22
|
+
* Only [PlaybackServiceCallback] mutates this — at the same headless
|
|
23
|
+
* connect / disconnect points that drive the events. This count is separate
|
|
24
|
+
* from that callback's per-instance counter, which stays per-service so the
|
|
25
|
+
* events keep their per-session first/last edge semantics.
|
|
26
|
+
*/
|
|
27
|
+
internal object CarConnectionState {
|
|
28
|
+
private val count = AtomicInteger(0)
|
|
29
|
+
|
|
30
|
+
fun isConnected(): Boolean = count.get() > 0
|
|
31
|
+
|
|
32
|
+
/** A headless controller attached. */
|
|
33
|
+
fun retain() {
|
|
34
|
+
count.incrementAndGet()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A headless controller detached; never drops below zero. */
|
|
38
|
+
fun release() {
|
|
39
|
+
count.updateAndGet { if (it > 0) it - 1 else 0 }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@VisibleForTesting
|
|
43
|
+
fun reset() {
|
|
44
|
+
count.set(0)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -25,9 +25,7 @@ import com.margelo.nitro.core.Promise
|
|
|
25
25
|
* The JS-facing methods that mutate state return `Promise<Unit>`
|
|
26
26
|
* via `Promise.async` per the spec, but each delegates to a
|
|
27
27
|
* synchronous `*Internal` method that owns the actual mutation.
|
|
28
|
-
*
|
|
29
|
-
* JS-interface adapter and is exercised at integration via demo
|
|
30
|
-
* Maestro.
|
|
28
|
+
* The async wrapper is a thin JS-interface adapter.
|
|
31
29
|
*
|
|
32
30
|
* Listener registries follow the same shape as other event
|
|
33
31
|
* listeners on this lib: stable numeric IDs returned via the
|
|
@@ -141,8 +141,7 @@ public abstract class HeadlessJsMediaService :
|
|
|
141
141
|
/**
|
|
142
142
|
* The host's [ReactHost]. Returns `null` when the host
|
|
143
143
|
* `Application` does not implement [ReactApplication] — production
|
|
144
|
-
* Expo / RN consumers always do
|
|
145
|
-
* Robolectric / unit-test path where the application is a stub.
|
|
144
|
+
* Expo / RN consumers always do.
|
|
146
145
|
*/
|
|
147
146
|
protected val reactHost: ReactHost?
|
|
148
147
|
get() = (application as? ReactApplication)?.reactHost
|
|
@@ -151,8 +150,7 @@ public abstract class HeadlessJsMediaService :
|
|
|
151
150
|
* The live [ReactContext] from the consumer app's [ReactHost], or
|
|
152
151
|
* `null` when no context has been created yet (the service kicks
|
|
153
152
|
* one off via [createReactContextAndScheduleTask] in that case) OR
|
|
154
|
-
* when the host application isn't a [ReactApplication] at all
|
|
155
|
-
* unit-test stubs).
|
|
153
|
+
* when the host application isn't a [ReactApplication] at all.
|
|
156
154
|
*/
|
|
157
155
|
protected val reactContext: ReactContext?
|
|
158
156
|
get() = reactHost?.currentReactContext
|
|
@@ -285,9 +285,8 @@ class LookaheadCacheWriter @VisibleForTesting internal constructor(
|
|
|
285
285
|
* cache state.
|
|
286
286
|
*
|
|
287
287
|
* `runCatching` swallows the "Method myLooper in android.os.Looper
|
|
288
|
-
* not mocked" exception that fires
|
|
289
|
-
*
|
|
290
|
-
* fast); production reads the looper normally.
|
|
288
|
+
* not mocked" exception that fires on a JVM host without the Android
|
|
289
|
+
* framework present; production reads the looper normally.
|
|
291
290
|
*/
|
|
292
291
|
private fun requireMainThread() {
|
|
293
292
|
val onMain = runCatching {
|
|
@@ -429,12 +428,11 @@ class LookaheadCacheWriter @VisibleForTesting internal constructor(
|
|
|
429
428
|
// Errors (OutOfMemoryError etc.) propagate — those are
|
|
430
429
|
// not transient and shouldn't be hidden.
|
|
431
430
|
//
|
|
432
|
-
// Log via runCatching:
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
// log normally; tests get a silent no-op.
|
|
431
|
+
// Log via runCatching: on a JVM host without the Android
|
|
432
|
+
// framework present, android.util.Log is not stubbed, so a
|
|
433
|
+
// Log.w call would throw "Method w in android.util.Log not
|
|
434
|
+
// mocked" and terminate the prefetch loop. Production reads
|
|
435
|
+
// this log normally.
|
|
438
436
|
runCatching { Log.w(TAG, "Lookahead prefetch failed for ${track.url}: ${e.message}") }
|
|
439
437
|
}
|
|
440
438
|
|
|
@@ -2,8 +2,7 @@ package com.margelo.nitro.queueplayer
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Tracks fixed playback-progress milestones (25/50/75/90%) for the
|
|
5
|
-
* current track playthrough. Pure + deterministic (no Media3)
|
|
6
|
-
* unit-tests directly.
|
|
5
|
+
* current track playthrough. Pure + deterministic (no Media3).
|
|
7
6
|
*
|
|
8
7
|
* A threshold emits only when forward playback crosses it: [seek] moves
|
|
9
8
|
* the baseline WITHOUT emitting (so thresholds a seek jumps over are
|
|
@@ -150,8 +150,8 @@ class PlaybackService : HeadlessJsMediaService() {
|
|
|
150
150
|
* [PendingIntentBuffer] TTL: ~3 s for bridge boot + ~17 s of
|
|
151
151
|
* headroom for the consumer's network calls before the controller
|
|
152
152
|
* surfaces "no results" and the user retries. The two platforms'
|
|
153
|
-
* cold-start windows are deliberately the same so the
|
|
154
|
-
*
|
|
153
|
+
* cold-start windows are deliberately kept the same so the
|
|
154
|
+
* behaviour is identical on iPhone + Android.
|
|
155
155
|
*/
|
|
156
156
|
private const val HEADLESS_DISPATCH_TIMEOUT_MS = 20_000L
|
|
157
157
|
|
|
@@ -1455,10 +1455,9 @@ class PlaybackService : HeadlessJsMediaService() {
|
|
|
1455
1455
|
override fun getTaskConfig(intent: Intent?): com.facebook.react.jstasks.HeadlessJsTaskConfig? {
|
|
1456
1456
|
val action = intent?.action ?: return null
|
|
1457
1457
|
if (action !in HEADLESS_WAKE_ACTIONS) return null
|
|
1458
|
-
//
|
|
1459
|
-
//
|
|
1460
|
-
//
|
|
1461
|
-
// classpath.
|
|
1458
|
+
// Non-ReactApplication hosts cannot spawn a JS task —
|
|
1459
|
+
// short-circuit here so `Arguments.createMap()` never has to load
|
|
1460
|
+
// the React native libs such a host lacks.
|
|
1462
1461
|
if ((application as? com.facebook.react.ReactApplication) == null) return null
|
|
1463
1462
|
// Belt-and-braces: if a wake intent somehow lands while a React
|
|
1464
1463
|
// context is already alive (foreground app, prior wake already
|
|
@@ -73,7 +73,7 @@ internal class PlaybackServiceCallback(
|
|
|
73
73
|
* package (Android Auto, Wear OS, Google Assistant) connects.
|
|
74
74
|
* Production wires the React-context cold-start through here so
|
|
75
75
|
* "Hey Google, play …" works from a phone with the app killed.
|
|
76
|
-
* Default no-op so
|
|
76
|
+
* Default no-op so callers without a wired service fall
|
|
77
77
|
* through harmlessly. */
|
|
78
78
|
fun onHeadlessControllerConnect(packageName: String) {}
|
|
79
79
|
|
|
@@ -287,6 +287,9 @@ internal class PlaybackServiceCallback(
|
|
|
287
287
|
): MediaSession.ConnectionResult {
|
|
288
288
|
if (controller.packageName in HEADLESS_ELIGIBLE_PACKAGES) {
|
|
289
289
|
val previousCount = headlessControllerCount.getAndIncrement()
|
|
290
|
+
// A headless controller is now attached — record it in the process-wide
|
|
291
|
+
// count the `isCarConnected()` getter reads.
|
|
292
|
+
CarConnectionState.retain()
|
|
290
293
|
environment.onHeadlessControllerConnect(controller.packageName)
|
|
291
294
|
// Fire onCarConnect only on the FIRST headless controller of
|
|
292
295
|
// this service-lifetime session — subsequent connects (Wear
|
|
@@ -348,6 +351,10 @@ internal class PlaybackServiceCallback(
|
|
|
348
351
|
} else if (previousCount > 0 && remaining == 0) {
|
|
349
352
|
environment.onHeadlessControllerDisconnect()
|
|
350
353
|
}
|
|
354
|
+
// Drop this controller from the process-wide connection count the
|
|
355
|
+
// `isCarConnected()` getter reads (floor-guarded against a stray
|
|
356
|
+
// disconnect, so it can't go negative).
|
|
357
|
+
CarConnectionState.release()
|
|
351
358
|
}
|
|
352
359
|
super.onDisconnected(session, controller)
|
|
353
360
|
}
|
|
@@ -3,7 +3,7 @@ package com.margelo.nitro.queueplayer
|
|
|
3
3
|
/**
|
|
4
4
|
* Throttles the JS `onProgress` fan-out to a configurable cadence, with an
|
|
5
5
|
* optional reduced rate while the app is backgrounded. Pure + deterministic
|
|
6
|
-
* (the caller supplies a monotonic timestamp)
|
|
6
|
+
* (the caller supplies a monotonic timestamp).
|
|
7
7
|
*
|
|
8
8
|
* This gates ONLY the JS emission — the native 2 Hz sampler keeps running, so
|
|
9
9
|
* milestone detection and buffer-state recomputation stay full-rate. A tick
|
|
@@ -2,7 +2,7 @@ package com.margelo.nitro.queueplayer
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Pure-Kotlin index-shift arithmetic for queue mutations. Extracted
|
|
5
|
-
* from [TrackPlayer] so the shift semantics
|
|
5
|
+
* from [TrackPlayer] so the shift semantics stand alone without
|
|
6
6
|
* an ExoPlayer. Mirrors (and lightly supersedes) the inline math
|
|
7
7
|
* inside iOS `TrackPlayer.swift`'s `addToQueueBody` /
|
|
8
8
|
* `removeFromQueueBody` / `moveInQueueBody` — the iOS backlog carries
|
|
@@ -4,7 +4,7 @@ package com.margelo.nitro.queueplayer
|
|
|
4
4
|
* Pure-Kotlin skip-index arithmetic. Mirrors the iOS sibling
|
|
5
5
|
* `ios/QueueSkipArithmetic.swift` 1-for-1.
|
|
6
6
|
*
|
|
7
|
-
* Extracted from [TrackPlayer] so the logic
|
|
7
|
+
* Extracted from [TrackPlayer] so the logic stands alone without
|
|
8
8
|
* instantiating an ExoPlayer. [TrackPlayer.computeNextIndex] /
|
|
9
9
|
* [TrackPlayer.computePreviousIndex] are thin adapters that translate
|
|
10
10
|
* the Nitrogen-generated [RepeatMode] enum into [QueueSkipRepeatMode]
|
|
@@ -29,7 +29,7 @@ package com.margelo.nitro.queueplayer
|
|
|
29
29
|
*
|
|
30
30
|
* Bias is toward emitting: when in doubt, act rather than swallow a real press.
|
|
31
31
|
*
|
|
32
|
-
* Pure and time-injected ([nowUptimeMs] is passed in) so it
|
|
32
|
+
* Pure and time-injected ([nowUptimeMs] is passed in), so it needs no
|
|
33
33
|
* Android runtime. [shouldEmit] is `@Synchronized` so it is safe regardless of
|
|
34
34
|
* which thread an emit site runs on (today they are all main-thread-confined).
|
|
35
35
|
*/
|
|
@@ -2,7 +2,7 @@ package com.margelo.nitro.queueplayer
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Pure state machine for the sleep timer — no player, volume, or handler
|
|
5
|
-
* dependencies, so it
|
|
5
|
+
* dependencies, so it stands alone. The owning [TrackPlayer]
|
|
6
6
|
* drives it from a repeating main-looper runnable and applies the returned
|
|
7
7
|
* decision (volume fade, pause) to the engine.
|
|
8
8
|
*
|
|
@@ -42,6 +42,12 @@ class SleepTimerCore(
|
|
|
42
42
|
// defer the pause forever.
|
|
43
43
|
private var tailExtended = false
|
|
44
44
|
|
|
45
|
+
// True while an end-of-track timer is pending, and survives the conversion to
|
|
46
|
+
// a DURATION fade so the real track-end signal (fireAtTrackEnd) knows to fire
|
|
47
|
+
// — the poll can't hit the boundary exactly and never fires at all when the
|
|
48
|
+
// duration is unknown (live/streaming).
|
|
49
|
+
private var originEndOfTrack = false
|
|
50
|
+
|
|
45
51
|
val isActive: Boolean get() = mode != Mode.INACTIVE
|
|
46
52
|
val isEndOfTrack: Boolean get() = mode == Mode.END_OF_TRACK
|
|
47
53
|
|
|
@@ -49,18 +55,33 @@ class SleepTimerCore(
|
|
|
49
55
|
mode = Mode.DURATION
|
|
50
56
|
deadlineEpochMs = nowMs + (seconds * 1000).toLong()
|
|
51
57
|
tailExtended = false
|
|
58
|
+
originEndOfTrack = false
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
fun armEndOfTrack() {
|
|
55
62
|
mode = Mode.END_OF_TRACK
|
|
56
63
|
deadlineEpochMs = null
|
|
57
64
|
tailExtended = false
|
|
65
|
+
originEndOfTrack = true
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
fun clear() {
|
|
61
69
|
mode = Mode.INACTIVE
|
|
62
70
|
deadlineEpochMs = null
|
|
63
71
|
tailExtended = false
|
|
72
|
+
originEndOfTrack = false
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Fire the pending end-of-track timer at the current track's real end. The
|
|
77
|
+
* owning engine calls this from its Media3 auto-transition signal so the pause
|
|
78
|
+
* lands exactly at the boundary (and fires even when the duration was never
|
|
79
|
+
* known). No-op unless an end-of-track timer is pending.
|
|
80
|
+
*/
|
|
81
|
+
fun fireAtTrackEnd(): Tick {
|
|
82
|
+
if (!originEndOfTrack) return Tick.IDLE
|
|
83
|
+
clear()
|
|
84
|
+
return Tick(1.0, pauseNow = true, stateChanged = true)
|
|
64
85
|
}
|
|
65
86
|
|
|
66
87
|
/** Whole seconds until the fixed deadline, or `null` when inactive / awaiting track end. */
|
|
@@ -78,9 +99,13 @@ class SleepTimerCore(
|
|
|
78
99
|
val tail = trackRemaining(trackDurationSec, trackPositionSec)
|
|
79
100
|
if (tail != null && tail <= tailGraceSeconds) {
|
|
80
101
|
mode = Mode.DURATION
|
|
81
|
-
|
|
102
|
+
// Deadline lands AT the track boundary (not 1s past it) so the fade
|
|
103
|
+
// completes at the end; on the local engine the real track-end signal
|
|
104
|
+
// (fireAtTrackEnd) owns the actual pause, and this deadline is the
|
|
105
|
+
// fallback the cast path rides out on the receiver's clock.
|
|
106
|
+
deadlineEpochMs = nowMs + (tail * 1000).toLong()
|
|
82
107
|
tailExtended = true
|
|
83
|
-
return Tick(fadeFraction(tail
|
|
108
|
+
return Tick(fadeFraction(tail), pauseNow = false, stateChanged = true)
|
|
84
109
|
}
|
|
85
110
|
return Tick.IDLE
|
|
86
111
|
}
|
|
@@ -103,9 +128,7 @@ class SleepTimerCore(
|
|
|
103
128
|
return Tick(fadeFraction(tail + 1), pauseNow = false, stateChanged = true)
|
|
104
129
|
}
|
|
105
130
|
// Fire.
|
|
106
|
-
|
|
107
|
-
deadlineEpochMs = null
|
|
108
|
-
tailExtended = false
|
|
131
|
+
clear()
|
|
109
132
|
return Tick(1.0, pauseNow = true, stateChanged = true)
|
|
110
133
|
}
|
|
111
134
|
}
|
|
@@ -46,10 +46,9 @@ class TrackPlayer : HybridTrackPlayerSpec(), PlaybackEngineDelegate {
|
|
|
46
46
|
// --- Stored state
|
|
47
47
|
//
|
|
48
48
|
// Mirrors iOS TrackPlayer.swift's "Stored state" block. Field
|
|
49
|
-
// visibility is `internal` + `@VisibleForTesting` so
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
// the JVM unit-test runtime).
|
|
49
|
+
// visibility is `internal` + `@VisibleForTesting` so lifecycle
|
|
50
|
+
// transitions can be observed without going through the JNI-backed
|
|
51
|
+
// Nitro Promise machinery (which does not load on a plain JVM).
|
|
53
52
|
|
|
54
53
|
/**
|
|
55
54
|
* `true` after [configureInternal] has bound + configured the
|
|
@@ -158,8 +157,7 @@ class TrackPlayer : HybridTrackPlayerSpec(), PlaybackEngineDelegate {
|
|
|
158
157
|
private var lastEmittedFormatMediaItem: MediaItem? = null
|
|
159
158
|
|
|
160
159
|
// The fields below are written on main (via `onMain` / `syncMain`
|
|
161
|
-
// from the Promise adapter path
|
|
162
|
-
// which run on main). They are read off-main by the synchronous
|
|
160
|
+
// from the Promise adapter path). They are read off-main by the synchronous
|
|
163
161
|
// snapshot accessors — `getCurrentTrackIndex`, `getQueue` —
|
|
164
162
|
// dispatched on the JS thread by Nitro. `@Volatile` gives the
|
|
165
163
|
// read path a happens-before edge on the most recent main-thread
|
|
@@ -581,7 +579,7 @@ class TrackPlayer : HybridTrackPlayerSpec(), PlaybackEngineDelegate {
|
|
|
581
579
|
private val progressEmissionGate = ProgressEmissionGate()
|
|
582
580
|
// ProcessLifecycleOwner observer driving the gate's background state. Held in
|
|
583
581
|
// a nullable field so a re-configureInternal without an intervening
|
|
584
|
-
// destroyInternal
|
|
582
|
+
// destroyInternal is a no-op instead of leaking a second
|
|
585
583
|
// observer. Registered in configureInternal, removed in destroyInternal.
|
|
586
584
|
private var progressLifecycleObserver: DefaultLifecycleObserver? = null
|
|
587
585
|
private val errorListeners = ListenerRegistry<(PlaybackError) -> Unit>()
|
|
@@ -866,11 +864,10 @@ class TrackPlayer : HybridTrackPlayerSpec(), PlaybackEngineDelegate {
|
|
|
866
864
|
}
|
|
867
865
|
|
|
868
866
|
/**
|
|
869
|
-
* Lifecycle configure body. `internal
|
|
870
|
-
*
|
|
871
|
-
* to the
|
|
872
|
-
*
|
|
873
|
-
* run on Robolectric's main-thread dispatcher.
|
|
867
|
+
* Lifecycle configure body. `internal`, bypassing the Nitro Promise
|
|
868
|
+
* path. Callers MUST hop to the main thread first — ExoPlayer
|
|
869
|
+
* construction binds to the calling Looper. Production path goes
|
|
870
|
+
* through `onMain`.
|
|
874
871
|
*
|
|
875
872
|
* Builds the ExoPlayer with a [MediaSource.Factory] that attaches
|
|
876
873
|
* the config's HTTP headers + User-Agent on every network request.
|
|
@@ -879,9 +876,8 @@ class TrackPlayer : HybridTrackPlayerSpec(), PlaybackEngineDelegate {
|
|
|
879
876
|
* Per the universal auto-destroy contract every `configure()` call
|
|
880
877
|
* tears the player down upstream so this body is purely
|
|
881
878
|
* constructive — `player` is guaranteed null on entry from the
|
|
882
|
-
* production path.
|
|
883
|
-
*
|
|
884
|
-
* between configures.
|
|
879
|
+
* production path. Any caller driving `configureInternal` directly
|
|
880
|
+
* must call `destroyInternal` first between configures.
|
|
885
881
|
*/
|
|
886
882
|
@VisibleForTesting
|
|
887
883
|
@MainThread
|
|
@@ -2442,9 +2438,9 @@ class TrackPlayer : HybridTrackPlayerSpec(), PlaybackEngineDelegate {
|
|
|
2442
2438
|
/**
|
|
2443
2439
|
* Main-thread body for [setPlaybackSpeed]. The engine applies the
|
|
2444
2440
|
* active pitch-correction mode's pitch alongside the speed (voice/music
|
|
2445
|
-
* preserve pitch; none lets it follow rate). Exposed `@VisibleForTesting
|
|
2446
|
-
*
|
|
2447
|
-
*
|
|
2441
|
+
* preserve pitch; none lets it follow rate). Exposed `@VisibleForTesting`;
|
|
2442
|
+
* centralizes the cast + speed-application logic rather than routing
|
|
2443
|
+
* straight through `ExoPlayer.playbackParameters`.
|
|
2448
2444
|
*
|
|
2449
2445
|
* Diverges from iOS: on iOS `player.rate = Float(rate)` implicitly
|
|
2450
2446
|
* starts playback when the player was paused. Android's
|
|
@@ -2629,6 +2625,14 @@ class TrackPlayer : HybridTrackPlayerSpec(), PlaybackEngineDelegate {
|
|
|
2629
2625
|
positionSec = maxOf(0L, serviceBinder?.engine?.currentPositionMs ?: 0L) / 1000.0
|
|
2630
2626
|
}
|
|
2631
2627
|
val result = sleepTimerCore.tick(System.currentTimeMillis(), durationSec, positionSec)
|
|
2628
|
+
applySleepTimerResult(result)
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
/**
|
|
2632
|
+
* Apply a [SleepTimerCore.Tick] decision — fade, state emit, boundary pause.
|
|
2633
|
+
* Shared by the polling tick and the real track-end signal ([fireEndOfTrackSleepTimer]).
|
|
2634
|
+
*/
|
|
2635
|
+
private fun applySleepTimerResult(result: SleepTimerCore.Tick) {
|
|
2632
2636
|
applySleepTimerFade(result.fadeFraction)
|
|
2633
2637
|
if (result.stateChanged) emitSleepTimerChanged()
|
|
2634
2638
|
if (result.pauseNow) {
|
|
@@ -2643,6 +2647,17 @@ class TrackPlayer : HybridTrackPlayerSpec(), PlaybackEngineDelegate {
|
|
|
2643
2647
|
if (!sleepTimerCore.isActive) stopSleepTimerTick()
|
|
2644
2648
|
}
|
|
2645
2649
|
|
|
2650
|
+
/**
|
|
2651
|
+
* Fire the end-of-track sleep timer at a real auto-transition boundary (the
|
|
2652
|
+
* current track finished and Media3 advanced). Called from the engine's
|
|
2653
|
+
* track-transition callback so the pause lands at the boundary and fires even
|
|
2654
|
+
* when the track duration was never known — no-op unless end-of-track is armed.
|
|
2655
|
+
*/
|
|
2656
|
+
private fun fireEndOfTrackSleepTimer() {
|
|
2657
|
+
val result = sleepTimerCore.fireAtTrackEnd()
|
|
2658
|
+
if (result.pauseNow) applySleepTimerResult(result)
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2646
2661
|
private fun applySleepTimerFade(fraction: Double) {
|
|
2647
2662
|
// Fade is local-only: the receiver's volume is user/system-owned and
|
|
2648
2663
|
// `volumeState` tracks the local level, so ramping it on the receiver would
|
|
@@ -3013,6 +3028,8 @@ class TrackPlayer : HybridTrackPlayerSpec(), PlaybackEngineDelegate {
|
|
|
3013
3028
|
override fun onCarDisconnect(callback: () -> Unit): () -> Unit =
|
|
3014
3029
|
deferredRegister { it.registerOnCarDisconnect(callback) }
|
|
3015
3030
|
|
|
3031
|
+
override fun isCarConnected(): Boolean = CarConnectionState.isConnected()
|
|
3032
|
+
|
|
3016
3033
|
override fun onServiceReady(
|
|
3017
3034
|
callback: (reason: ServiceReadyReason) -> Unit
|
|
3018
3035
|
): () -> Unit = deferredRegister { it.registerOnServiceReady(callback) }
|
|
@@ -3599,6 +3616,11 @@ class TrackPlayer : HybridTrackPlayerSpec(), PlaybackEngineDelegate {
|
|
|
3599
3616
|
internal fun maybeEmitQueueEnd(playbackState: Int) {
|
|
3600
3617
|
if (playbackState != Player.STATE_ENDED) return
|
|
3601
3618
|
if (tracks.isEmpty()) return
|
|
3619
|
+
// End-of-track sleep timer on the LAST track: STATE_ENDED fires no
|
|
3620
|
+
// MEDIA_ITEM_TRANSITION, so cover the final boundary here — matching iOS,
|
|
3621
|
+
// whose per-item handlePlayerItemDidPlayToEndTime fires on the last track
|
|
3622
|
+
// too. Idempotent, so it can't double-fire with the poll / transition path.
|
|
3623
|
+
fireEndOfTrackSleepTimer()
|
|
3602
3624
|
queueEndListeners.forEach { it() }
|
|
3603
3625
|
emitState(PlayerState.ENDED, StateChangeReason.QUEUE_END)
|
|
3604
3626
|
}
|
|
@@ -3622,6 +3644,12 @@ class TrackPlayer : HybridTrackPlayerSpec(), PlaybackEngineDelegate {
|
|
|
3622
3644
|
val p = player ?: return
|
|
3623
3645
|
val translated = translateTransitionReason(reason, p)
|
|
3624
3646
|
|
|
3647
|
+
// End-of-track sleep timer: a natural track boundary — auto-advance to the
|
|
3648
|
+
// next track OR a repeat-one loop of the same track (both translate to
|
|
3649
|
+
// AUTO_ADVANCE) — is the real end of the current track, so pause here.
|
|
3650
|
+
// fireAtTrackEnd is idempotent, so a deduped crossfade echo can't double-fire.
|
|
3651
|
+
if (translated == TrackChangeReason.AUTO_ADVANCE) fireEndOfTrackSleepTimer()
|
|
3652
|
+
|
|
3625
3653
|
// Natural track change → reset error dedup so a new item's
|
|
3626
3654
|
// identical-coded error fires. Without this, a TIMEOUT on
|
|
3627
3655
|
// track 2 (after a TIMEOUT on track 1) would be silently
|
|
@@ -4127,11 +4155,9 @@ class TrackPlayer : HybridTrackPlayerSpec(), PlaybackEngineDelegate {
|
|
|
4127
4155
|
}
|
|
4128
4156
|
|
|
4129
4157
|
/**
|
|
4130
|
-
*
|
|
4158
|
+
* Stash a [TrackChangeReason] that the next
|
|
4131
4159
|
* [handleMediaItemTransition] with `REASON_SEEK` will consume.
|
|
4132
|
-
* Production code reaches this field via `skipTo*Internal`
|
|
4133
|
-
* bodies; the hook just lets Robolectric tests simulate the
|
|
4134
|
-
* stash without driving a real skip + ExoPlayer seek.
|
|
4160
|
+
* Production code reaches this field via `skipTo*Internal` bodies.
|
|
4135
4161
|
*/
|
|
4136
4162
|
@VisibleForTesting
|
|
4137
4163
|
internal fun setPendingTrackChangeReasonForTest(reason: TrackChangeReason?) {
|
|
@@ -4139,9 +4165,8 @@ class TrackPlayer : HybridTrackPlayerSpec(), PlaybackEngineDelegate {
|
|
|
4139
4165
|
}
|
|
4140
4166
|
|
|
4141
4167
|
/**
|
|
4142
|
-
*
|
|
4143
|
-
*
|
|
4144
|
-
* NONE state without going through a real Player.Listener fire.
|
|
4168
|
+
* Reset the [lastReportedState] dedup edge, forcing [emitStateIfChanged]
|
|
4169
|
+
* to emit from an already-NONE state without a real Player.Listener fire.
|
|
4145
4170
|
*/
|
|
4146
4171
|
@VisibleForTesting
|
|
4147
4172
|
internal fun resetLastReportedStateForTest() {
|
|
@@ -4220,8 +4245,7 @@ class TrackPlayer : HybridTrackPlayerSpec(), PlaybackEngineDelegate {
|
|
|
4220
4245
|
|
|
4221
4246
|
/**
|
|
4222
4247
|
* Decide which [ServiceReadyReason] (if any) to dispatch on a
|
|
4223
|
-
* fresh service-bind. Pure function
|
|
4224
|
-
* is unit-testable without spinning up a service controller.
|
|
4248
|
+
* fresh service-bind. Pure function — no service controller needed.
|
|
4225
4249
|
*
|
|
4226
4250
|
* - First-ever bind ([prior] == null): [ServiceReadyReason.FIRST_BIND].
|
|
4227
4251
|
* - Bind against a different binder identity:
|
|
@@ -7,8 +7,8 @@ import androidx.media3.common.Player
|
|
|
7
7
|
* isPlaying, playWhenReady, playbackSuppressionReason, hasTracks,
|
|
8
8
|
* currentTrackIndex)` tuple to the library's [PlayerState] enum.
|
|
9
9
|
*
|
|
10
|
-
* Extracted from [TrackPlayer.getStateInternal] so every branch
|
|
11
|
-
*
|
|
10
|
+
* Extracted from [TrackPlayer.getStateInternal] so every branch
|
|
11
|
+
* stands alone without a live ExoPlayer. Matches the pattern of
|
|
12
12
|
* [QueueSkipArithmetic] + [QueueMutationArithmetic] — TrackPlayer
|
|
13
13
|
* is a thin adapter that reads the live player's state and
|
|
14
14
|
* delegates the translation here.
|
package/android/src/test/java/com/margelo/nitro/queueplayer/AudioPipelineRenderersFactoryTest.kt
CHANGED
|
@@ -14,12 +14,9 @@ import java.lang.reflect.Field
|
|
|
14
14
|
/**
|
|
15
15
|
* Unit coverage for [AudioPipelineRenderersFactory] construction — it wraps a
|
|
16
16
|
* caller-supplied [androidx.media3.common.audio.AudioProcessorChain] and leaves
|
|
17
|
-
* the produced sink at 16-bit (float output off).
|
|
18
|
-
* `buildAudioSink` via `ExoPlayer.Builder.build()` in
|
|
19
|
-
* [GaplessEngineLifecycleTest] / [CrossfadeEngineLifecycleTest]; `buildAudioSink`
|
|
17
|
+
* the produced sink at 16-bit (float output off). `buildAudioSink`
|
|
20
18
|
* itself is `protected` on [DefaultRenderersFactory] and cannot be called
|
|
21
|
-
* outside the Media3 internal call path.
|
|
22
|
-
* [PitchAwareAudioProcessorChainTest].
|
|
19
|
+
* outside the Media3 internal call path.
|
|
23
20
|
*/
|
|
24
21
|
@RunWith(RobolectricTestRunner::class)
|
|
25
22
|
@Config(sdk = [34])
|
|
@@ -9,10 +9,7 @@ import org.junit.Test
|
|
|
9
9
|
/**
|
|
10
10
|
* Pure-Kotlin coverage for [BrowseCallbackRegistry]'s slot-replace
|
|
11
11
|
* + dispose semantics. The Promise-typed callbacks are stored as
|
|
12
|
-
* function references; tests use simple stand-in lambdas
|
|
13
|
-
* dispatch coverage lives at the integration layer
|
|
14
|
-
* (`PlaybackServiceCallbackSearchTest` for the search dispatch
|
|
15
|
-
* path; future Maestro flows for end-to-end Auto cold-wake).
|
|
12
|
+
* function references; tests use simple stand-in lambdas.
|
|
16
13
|
*/
|
|
17
14
|
@UnstableApi
|
|
18
15
|
class BrowseCallbackRegistryTest {
|
|
@@ -15,8 +15,7 @@ import org.robolectric.annotation.Config
|
|
|
15
15
|
/**
|
|
16
16
|
* Lifecycle + fade-math coverage for [CrossfadeEngine].
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
* Maestro / demo-app integration layer; this suite exercises:
|
|
18
|
+
* This suite exercises:
|
|
20
19
|
* empty-state sentinels, role-swap arithmetic, equal-power gain
|
|
21
20
|
* curve, duration-clamp formula, manual-skip cancellation,
|
|
22
21
|
* release idempotence + delegate clearing.
|
package/android/src/test/java/com/margelo/nitro/queueplayer/CrossfadeEngineReplayGainTest.kt
CHANGED
|
@@ -24,9 +24,7 @@ import org.robolectric.annotation.Config
|
|
|
24
24
|
* Media3 metadata dispatch is not emulated by Robolectric without
|
|
25
25
|
* playback, so the listener body composes its work through the
|
|
26
26
|
* engine-internal `handleReplayGainParse(isA, parsed)` helper which
|
|
27
|
-
* these tests drive directly with synthetic [ReplayGainData].
|
|
28
|
-
* pure-math piece is covered in [ReplayGainGainTest]; end-to-end
|
|
29
|
-
* propagation lands in the demo app's `replaygain` Maestro suite.
|
|
27
|
+
* these tests drive directly with synthetic [ReplayGainData].
|
|
30
28
|
* This file covers the no-data baseline + the per-leg
|
|
31
29
|
* `handleReplayGainParse` cache + recompute contract.
|
|
32
30
|
*/
|
|
@@ -173,8 +171,6 @@ class CrossfadeEngineReplayGainTest {
|
|
|
173
171
|
|
|
174
172
|
// The per-leg cache-clear-on-transition path lives inline in each
|
|
175
173
|
// leg's `Player.Listener.onMediaItemTransition` body. Robolectric
|
|
176
|
-
// does not drive that callback without real playback
|
|
177
|
-
//
|
|
178
|
-
// `cross-track-active-updates` +
|
|
179
|
-
// `skip-next-mid-playback-resets-from-stale-cache`).
|
|
174
|
+
// does not drive that callback without real playback, so it is out
|
|
175
|
+
// of scope here.
|
|
180
176
|
}
|
|
@@ -13,8 +13,7 @@ import org.robolectric.annotation.Config
|
|
|
13
13
|
/**
|
|
14
14
|
* State-mirror coverage for [EqualizerEngine] on the API 28+ path.
|
|
15
15
|
* Robolectric stubs the audio-effect subsystem at this SDK level so
|
|
16
|
-
* `attach` exercises the construction path without throwing
|
|
17
|
-
* audible effect is verified at integration via demo Maestro.
|
|
16
|
+
* `attach` exercises the construction path without throwing.
|
|
18
17
|
*/
|
|
19
18
|
@RunWith(RobolectricTestRunner::class)
|
|
20
19
|
@Config(sdk = [34])
|
|
@@ -16,9 +16,7 @@ import org.robolectric.annotation.Config
|
|
|
16
16
|
/**
|
|
17
17
|
* Wiring coverage for the `Equalizer` HybridObject — engine
|
|
18
18
|
* selection, JS-facing surface routing through the synchronous
|
|
19
|
-
* `*Internal` core, listener registry behaviour.
|
|
20
|
-
* effect is verified at the integration layer via demo Maestro
|
|
21
|
-
* once the engine is hooked to ExoPlayer's audio session.
|
|
19
|
+
* `*Internal` core, listener registry behaviour.
|
|
22
20
|
*/
|
|
23
21
|
@RunWith(RobolectricTestRunner::class)
|
|
24
22
|
@Config(sdk = [34])
|
|
@@ -13,9 +13,8 @@ import org.robolectric.annotation.Config
|
|
|
13
13
|
* State-mirror coverage for [EqualizerLegacyEngine] on the API <28 path.
|
|
14
14
|
* Robolectric's audio-effect shadow doesn't model the hardware-side
|
|
15
15
|
* band centre frequencies + gain range; the band-mapping + dB↔mB
|
|
16
|
-
* conversion are the
|
|
17
|
-
*
|
|
18
|
-
* behaviour varies per OEM).
|
|
16
|
+
* conversion are the invariants this covers. Legacy AudioEffect
|
|
17
|
+
* behaviour varies per OEM.
|
|
19
18
|
*/
|
|
20
19
|
@RunWith(RobolectricTestRunner::class)
|
|
21
20
|
@Config(sdk = [27])
|