mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-12 05:10:52 +02:00
android auto improvements
This commit is contained in:
+2
@@ -195,6 +195,7 @@ class AstraAudioRouteModule : Module() {
|
||||
AudioDeviceInfo.TYPE_HDMI,
|
||||
AudioDeviceInfo.TYPE_HDMI_ARC,
|
||||
AudioDeviceInfo.TYPE_HDMI_EARC -> "hdmi"
|
||||
AudioDeviceInfo.TYPE_REMOTE_SUBMIX -> "remote"
|
||||
AudioDeviceInfo.TYPE_BUILTIN_EARPIECE,
|
||||
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> "speaker"
|
||||
else -> "unknown"
|
||||
@@ -232,6 +233,7 @@ class AstraAudioRouteModule : Module() {
|
||||
"bluetooth" -> "Bluetooth"
|
||||
"usb" -> "USB audio"
|
||||
"hdmi" -> "HDMI audio"
|
||||
"remote" -> "Remote audio"
|
||||
else -> "Unknown output"
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -10,7 +10,8 @@ internal fun <T> selectPredictedOutputDevice(
|
||||
): T? {
|
||||
predicted.firstOrNull()?.let { return it }
|
||||
if (connected.isEmpty()) return null
|
||||
return connected.firstOrNull { kindFor(it) == "bluetooth" }
|
||||
return connected.firstOrNull { kindFor(it) == "remote" }
|
||||
?: connected.firstOrNull { kindFor(it) == "bluetooth" }
|
||||
?: connected.firstOrNull { kindFor(it) == "wired" }
|
||||
?: connected.firstOrNull { kindFor(it) == "usb" }
|
||||
?: connected.firstOrNull { kindFor(it) == "hdmi" }
|
||||
@@ -49,6 +50,7 @@ private fun addressToken(address: String?): String? {
|
||||
internal fun buildOutputRouteKey(kind: String, label: String, address: String?): String {
|
||||
if (kind == "speaker") return "speaker"
|
||||
if (kind == "wired") return "wired"
|
||||
if (kind == "remote") return "remote"
|
||||
if (kind !in setOf("bluetooth", "usb", "hdmi")) return "unknown"
|
||||
|
||||
addressToken(address)?.let { return "$kind:id:$it" }
|
||||
|
||||
+15
@@ -37,6 +37,20 @@ class OutputDeviceSelectorTest {
|
||||
assertEquals(bluetooth, selected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun legacyFallbackPrefersAnActiveRemoteMixForProjectedMedia() {
|
||||
val bluetooth = Device("car bluetooth", "bluetooth")
|
||||
val remote = Device("android auto", "remote")
|
||||
|
||||
val selected = selectPredictedOutputDevice(
|
||||
predicted = emptyList(),
|
||||
connected = listOf(bluetooth, remote),
|
||||
kindFor = Device::kind,
|
||||
)
|
||||
|
||||
assertEquals(remote, selected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun externalAddressProducesStablePrivacySafeKey() {
|
||||
val first = buildOutputRouteKey("bluetooth", "Sony WH-1000XM5", "AA:BB:CC:DD:EE:FF")
|
||||
@@ -63,6 +77,7 @@ class OutputDeviceSelectorTest {
|
||||
assertEquals("bluetooth", buildOutputRouteKey("bluetooth", "Bluetooth audio", null))
|
||||
assertEquals("usb", buildOutputRouteKey("usb", "USB audio", null))
|
||||
assertEquals("hdmi", buildOutputRouteKey("hdmi", "HDMI audio", null))
|
||||
assertEquals("remote", buildOutputRouteKey("remote", "Remote audio", null))
|
||||
assertEquals("unknown", buildOutputRouteKey("unknown", "Unknown output", null))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,8 @@ const coordinator = new DspStartupCoordinator<
|
||||
console.info('[dsp-startup] base-applied', {
|
||||
at: Date.now(),
|
||||
routeKey: route.key,
|
||||
routeKind: route.kind,
|
||||
nativeType: route.nativeType,
|
||||
eqEnabled: eq.enabled,
|
||||
preampDb: eq.preamp,
|
||||
normalizationEnabled: settings.enabled,
|
||||
@@ -134,6 +136,8 @@ const coordinator = new DspStartupCoordinator<
|
||||
console.info('[dsp-startup] target-applied', {
|
||||
at: Date.now(),
|
||||
routeKey: route.key,
|
||||
routeKind: route.kind,
|
||||
nativeType: route.nativeType,
|
||||
gainSource: resolved.source,
|
||||
activation: target.activation,
|
||||
hasTarget: Boolean(target.url),
|
||||
|
||||
@@ -25,6 +25,7 @@ function isRouteKind(value: unknown): value is AudioOutputRouteKind {
|
||||
value === 'bluetooth' ||
|
||||
value === 'usb' ||
|
||||
value === 'hdmi' ||
|
||||
value === 'remote' ||
|
||||
value === 'unknown'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { AudioOutputRoute, EQBand } from '../types/audio.ts';
|
||||
import {
|
||||
buildAudioOutputRouteKey,
|
||||
createEQRouteProfile,
|
||||
isAudioOutputRouteUsable,
|
||||
normalizeAudioOutputRoute,
|
||||
parseEQRouteProfilesJson,
|
||||
restoreEQRouteProfile,
|
||||
@@ -55,10 +56,16 @@ test('normalizes named external routes and generic class routes', () => {
|
||||
assert.equal(normalizeAudioOutputRoute({ kind: 'usb', label: 'USB DAC' })?.key, 'usb:name:usb-dac');
|
||||
assert.equal(normalizeAudioOutputRoute({ kind: 'usb', label: 'USB audio' })?.key, 'usb');
|
||||
assert.equal(normalizeAudioOutputRoute({ kind: 'hdmi', label: 'Living Room TV' })?.key, 'hdmi:name:living-room-tv');
|
||||
assert.equal(normalizeAudioOutputRoute({ kind: 'remote', label: 'Remote audio' })?.key, 'remote');
|
||||
assert.equal(normalizeAudioOutputRoute({ kind: 'speaker', label: 'Pixel speaker' })?.key, 'speaker');
|
||||
assert.equal(normalizeAudioOutputRoute({ kind: 'nonsense', label: '' })?.key, 'unknown');
|
||||
});
|
||||
|
||||
test('accepts concrete unclassified Android outputs without accepting a missing route', () => {
|
||||
assert.equal(isAudioOutputRouteUsable(route({ kind: 'unknown', nativeType: 25 })), true);
|
||||
assert.equal(isAudioOutputRouteUsable(route({ kind: 'unknown', nativeType: null })), false);
|
||||
});
|
||||
|
||||
test('recovers from corrupt route profile storage', () => {
|
||||
assert.deepEqual(parseEQRouteProfilesJson('{not-json', nextId), {});
|
||||
assert.deepEqual(
|
||||
|
||||
@@ -49,6 +49,7 @@ function isRouteKind(value: unknown): value is AudioOutputRouteKind {
|
||||
value === 'bluetooth' ||
|
||||
value === 'usb' ||
|
||||
value === 'hdmi' ||
|
||||
value === 'remote' ||
|
||||
value === 'unknown'
|
||||
);
|
||||
}
|
||||
@@ -84,6 +85,8 @@ function defaultRouteLabel(kind: AudioOutputRouteKind): string {
|
||||
return 'USB audio';
|
||||
case 'hdmi':
|
||||
return 'HDMI audio';
|
||||
case 'remote':
|
||||
return 'Remote audio';
|
||||
default:
|
||||
return 'Unknown output';
|
||||
}
|
||||
@@ -124,6 +127,8 @@ export function buildAudioOutputRouteKey(kind: AudioOutputRouteKind, label: stri
|
||||
return 'usb';
|
||||
case 'hdmi':
|
||||
return 'hdmi';
|
||||
case 'remote':
|
||||
return 'remote';
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
@@ -148,6 +153,11 @@ export function normalizeAudioOutputRoute(value: unknown): AudioOutputRoute | nu
|
||||
};
|
||||
}
|
||||
|
||||
/** A known kind or a concrete native type is enough to apply the loaded EQ safely. */
|
||||
export function isAudioOutputRouteUsable(route: AudioOutputRoute): boolean {
|
||||
return route.kind !== 'unknown' || route.nativeType != null;
|
||||
}
|
||||
|
||||
function normalizeBands(value: unknown, createId: () => string): EQBand[] | null {
|
||||
if (!Array.isArray(value) || value.length === 0) return null;
|
||||
return value
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
} from '../../modules/astra-audio-route';
|
||||
import { useEQStore } from '@/stores/eqStore';
|
||||
import type { AudioOutputRoute } from '@/types/audio';
|
||||
import { isAudioOutputRouteUsable } from '@/audio/eqRouteProfiles';
|
||||
|
||||
type Subscription = { remove: () => void };
|
||||
|
||||
@@ -30,7 +31,19 @@ export async function refreshEQRouteForPlayback(): Promise<AudioOutputRoute> {
|
||||
await useEQStore.getState().load();
|
||||
const route = AstraAudioRoute.getCurrentRoute();
|
||||
if (!route) throw new Error('Current media output route unavailable');
|
||||
if (route.kind === 'unknown') throw new Error('Current media output route unresolved');
|
||||
if (!isAudioOutputRouteUsable(route)) {
|
||||
throw new Error('Current media output route unresolved');
|
||||
}
|
||||
if (route.kind === 'unknown') {
|
||||
// A real but newer Android route type is still safe: retain/reassert the
|
||||
// loaded EQ state instead of permanently blocking playback. Android Auto
|
||||
// used to reach this path because TYPE_REMOTE_SUBMIX was not classified.
|
||||
console.warn('[eq-route] unclassified native output; retaining current EQ', {
|
||||
nativeType: route.nativeType,
|
||||
nativeId: route.nativeId,
|
||||
label: route.label,
|
||||
});
|
||||
}
|
||||
await useEQStore.getState().setOutputRoute(route);
|
||||
return route;
|
||||
}
|
||||
|
||||
+8
-1
@@ -85,7 +85,14 @@ export interface EQPreset {
|
||||
graphicGains?: number[];
|
||||
}
|
||||
|
||||
export type AudioOutputRouteKind = 'speaker' | 'wired' | 'bluetooth' | 'usb' | 'hdmi' | 'unknown';
|
||||
export type AudioOutputRouteKind =
|
||||
| 'speaker'
|
||||
| 'wired'
|
||||
| 'bluetooth'
|
||||
| 'usb'
|
||||
| 'hdmi'
|
||||
| 'remote'
|
||||
| 'unknown';
|
||||
|
||||
export interface AudioOutputRoute {
|
||||
key: string;
|
||||
|
||||
Reference in New Issue
Block a user