mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-12 05:10:52 +02:00
fix android auto quirks
This commit is contained in:
@@ -16,3 +16,7 @@ android {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
}
|
||||
|
||||
+19
-8
@@ -1,6 +1,7 @@
|
||||
package expo.modules.astraaudioroute
|
||||
|
||||
import android.content.Context
|
||||
import android.media.AudioAttributes
|
||||
import android.media.AudioDeviceCallback
|
||||
import android.media.AudioDeviceInfo
|
||||
import android.media.AudioManager
|
||||
@@ -162,20 +163,30 @@ class AstraAudioRouteModule : Module() {
|
||||
} catch (_: Throwable) {
|
||||
emptyList()
|
||||
}
|
||||
if (outputs.isEmpty()) return null
|
||||
return outputs.firstOrNull { kindForType(it.type) == "bluetooth" }
|
||||
?: outputs.firstOrNull { kindForType(it.type) == "wired" }
|
||||
?: outputs.firstOrNull { kindForType(it.type) == "usb" }
|
||||
?: outputs.firstOrNull { kindForType(it.type) == "hdmi" }
|
||||
?: outputs.firstOrNull { kindForType(it.type) == "speaker" }
|
||||
?: outputs.first()
|
||||
val predicted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
try {
|
||||
val mediaAttributes = AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.build()
|
||||
audioManager().getAudioDevicesForAttributes(mediaAttributes).filter { it.isSink }
|
||||
} catch (_: Throwable) {
|
||||
emptyList()
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
return selectPredictedOutputDevice(predicted, outputs) { kindForType(it.type) }
|
||||
}
|
||||
|
||||
private fun kindForType(type: Int): String =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
when (type) {
|
||||
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
|
||||
AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> "bluetooth"
|
||||
AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
|
||||
AudioDeviceInfo.TYPE_BLE_HEADSET,
|
||||
AudioDeviceInfo.TYPE_BLE_SPEAKER,
|
||||
AudioDeviceInfo.TYPE_BLE_BROADCAST -> "bluetooth"
|
||||
AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
|
||||
AudioDeviceInfo.TYPE_WIRED_HEADSET -> "wired"
|
||||
AudioDeviceInfo.TYPE_USB_ACCESSORY,
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package expo.modules.astraaudioroute
|
||||
|
||||
/** Prefer Android's media-attribute prediction, then retain the legacy fallback. */
|
||||
internal fun <T> selectPredictedOutputDevice(
|
||||
predicted: List<T>,
|
||||
connected: List<T>,
|
||||
kindFor: (T) -> String,
|
||||
): T? {
|
||||
predicted.firstOrNull()?.let { return it }
|
||||
if (connected.isEmpty()) return null
|
||||
return connected.firstOrNull { kindFor(it) == "bluetooth" }
|
||||
?: connected.firstOrNull { kindFor(it) == "wired" }
|
||||
?: connected.firstOrNull { kindFor(it) == "usb" }
|
||||
?: connected.firstOrNull { kindFor(it) == "hdmi" }
|
||||
?: connected.firstOrNull { kindFor(it) == "speaker" }
|
||||
?: connected.first()
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package expo.modules.astraaudioroute
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class OutputDeviceSelectorTest {
|
||||
private data class Device(val name: String, val kind: String)
|
||||
|
||||
@Test
|
||||
fun predictedMediaDeviceWinsOverConnectedBluetoothPriority() {
|
||||
val bluetooth = Device("car bluetooth", "bluetooth")
|
||||
val usb = Device("android auto usb", "usb")
|
||||
|
||||
val selected = selectPredictedOutputDevice(
|
||||
predicted = listOf(usb),
|
||||
connected = listOf(bluetooth, usb),
|
||||
kindFor = Device::kind,
|
||||
)
|
||||
|
||||
assertEquals(usb, selected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun legacyFallbackKeepsExistingPriorityWithoutPrediction() {
|
||||
val speaker = Device("phone", "speaker")
|
||||
val usb = Device("dac", "usb")
|
||||
val bluetooth = Device("headphones", "bluetooth")
|
||||
|
||||
val selected = selectPredictedOutputDevice(
|
||||
predicted = emptyList(),
|
||||
connected = listOf(speaker, usb, bluetooth),
|
||||
kindFor = Device::kind,
|
||||
)
|
||||
|
||||
assertEquals(bluetooth, selected)
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ declare class AstraAudioRouteModuleType extends NativeModule<AstraAudioRouteEven
|
||||
|
||||
const native = requireOptionalNativeModule<AstraAudioRouteModuleType>('AstraAudioRoute');
|
||||
|
||||
export const isAstraAudioRouteAvailable = native !== null;
|
||||
|
||||
export const AstraAudioRoute = native ?? {
|
||||
addListener: () => ({ remove: () => {} }),
|
||||
removeAllListeners: () => {},
|
||||
|
||||
@@ -33,3 +33,7 @@ android {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
}
|
||||
|
||||
@@ -79,6 +79,12 @@ class AstraScopeModule : Module() {
|
||||
GainBridge.activateSmoothFor(url)
|
||||
}
|
||||
|
||||
// Hard-prime while paused; unlike activateTrackGain this must not spend the
|
||||
// first audible second gliding down from the process default (unity).
|
||||
Function("primeTrackGain") { url: String ->
|
||||
GainBridge.primeFor(url)
|
||||
}
|
||||
|
||||
// Conservative temp gain applied when a transition hits an unregistered URL
|
||||
// (unanalyzed track). JS keeps this at 1 while normalization is disabled.
|
||||
Function("setFallbackGain") { linear: Double ->
|
||||
|
||||
@@ -89,6 +89,15 @@ object GainBridge {
|
||||
setTarget(gains[url] ?: fallbackGain, CORRECTION_RAMP_MS)
|
||||
}
|
||||
|
||||
/**
|
||||
* Prime a paused player before play is released. There is no live signal to
|
||||
* declick here, so publishing a zero-duration target guarantees the first
|
||||
* processed frame is already at the requested gain.
|
||||
*/
|
||||
fun primeFor(url: String) {
|
||||
setTarget(gains[url] ?: fallbackGain, 0)
|
||||
}
|
||||
|
||||
/** Glide to an explicit gain (unity paths: no track / remote track / disabled). */
|
||||
fun setGainSmooth(linear: Float) {
|
||||
setTarget(linear, CORRECTION_RAMP_MS)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package expo.modules.astrascope
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class GainBridgeTest {
|
||||
@Test
|
||||
fun pausedPrimePublishesTargetWithoutCorrectionRamp() {
|
||||
GainBridge.putGain("test-track", 0.25f)
|
||||
|
||||
GainBridge.primeFor("test-track")
|
||||
|
||||
assertEquals(0.25f, GainBridge.targetGain, 0.0001f)
|
||||
assertEquals(0, GainBridge.rampMs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pausedPrimeUsesFallbackForUnanalyzedTrack() {
|
||||
GainBridge.fallbackGain = 0.5f
|
||||
|
||||
GainBridge.primeFor("missing-track")
|
||||
|
||||
assertEquals(0.5f, GainBridge.targetGain, 0.0001f)
|
||||
assertEquals(0, GainBridge.rampMs)
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,8 @@ declare class AstraScopeModuleType extends NativeModule {
|
||||
setTrackGains(entries: Record<string, number>, clearExisting: boolean): void;
|
||||
/** Glide to the registered gain for this URL now (mount/settings/late measurement). */
|
||||
activateTrackGain(url: string): void;
|
||||
/** Apply the registered gain immediately while playback is still paused. */
|
||||
primeTrackGain(url: string): void;
|
||||
/**
|
||||
* Conservative temp gain (linear) applied when a media-item transition hits a URL
|
||||
* with no registered gain (unanalyzed track). Keep at 1 while normalization is off.
|
||||
|
||||
Reference in New Issue
Block a user