mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
fix silent audio on cars with an AAC sink, keep location out of logs
Cars like the 2022 Dacia Jogger list a compressed audio channel before the raw one, and Gearslip was sending raw audio to it. The Exit tile no longer shows as running, and the log leaves out the weather town and the car's GPS.
This commit is contained in:
1 parent
cf0c324d80
commit
7d197bf021
5 files changed
+72
-8
No files matched your search
@@ -357,6 +357,7 @@ class GearslipRunner(
|
||||
val info = ServiceDiscovery.findHeadUnitInfo(body)
|
||||
log.i("head unit: $info")
|
||||
SessionReport.headUnit(info.toString())
|
||||
SessionReport.headUnitInfo = info
|
||||
vehicleProfile = runCatching { vehicleProfileFor(info) }.getOrNull()
|
||||
val profile = vehicleProfile
|
||||
SessionReport.profile(profile?.name)
|
||||
@@ -430,13 +431,16 @@ class GearslipRunner(
|
||||
sink.configs.joinToString(prefix = "[", postfix = "]") { "${it.sampleRate}Hz/${it.bits}bit/x${it.channels}" },
|
||||
)
|
||||
}
|
||||
val media = sinks.firstOrNull { it.streamType == STREAM_MEDIA } ?: run {
|
||||
val media = ServiceDiscovery.pickMediaSink(sinks) ?: run {
|
||||
log.w("no media audio sink advertised - media apps will play on the phone only")
|
||||
SessionReport.audio(
|
||||
"no MEDIA sink; head unit offered ${sinks.joinToString { it.streamName }.ifEmpty { "none" }}",
|
||||
)
|
||||
return
|
||||
}
|
||||
if (media.codecType != ServiceDiscovery.CODEC_PCM) {
|
||||
log.w("the car offers no PCM media sink, only ${media.codecName}; it may play our PCM as silence")
|
||||
}
|
||||
val first = media.configs.firstOrNull()
|
||||
SessionReport.audio(
|
||||
"MEDIA sink on channel ${media.serviceId}, ${media.codecName}, " +
|
||||
@@ -481,7 +485,11 @@ class GearslipRunner(
|
||||
|
||||
private fun logSensorEvent(body: ByteArray) {
|
||||
val types = Wire.fields(body).map { it.number }.toSet()
|
||||
if (sensorTypesLogged.addAll(types)) log.i("<- SensorEvent (first of its kind):\n" + Protobuf.describe(body))
|
||||
if (sensorTypesLogged.addAll(types)) {
|
||||
// Type 1 is the car's GPS position; logs get shared, so say it arrived and nothing more.
|
||||
if (SENSOR_LOCATION in types) log.i("<- SensorEvent (first of its kind): types=$types, location left out")
|
||||
else log.i("<- SensorEvent (first of its kind):\n" + Protobuf.describe(body))
|
||||
}
|
||||
sensorEventsSinceLog++
|
||||
val now = System.currentTimeMillis()
|
||||
if (sensorLoggedAt == 0L) sensorLoggedAt = now
|
||||
@@ -763,6 +771,7 @@ class GearslipRunner(
|
||||
log.e("unsupported resolution ${config.resolutionName}")
|
||||
return
|
||||
}
|
||||
config.visibleSize()?.let { (w, h) -> SessionReport.carScreen("${w}x$h", config.density) }
|
||||
|
||||
val start = Protobuf.varintField(1, SESSION_ID.toLong()) +
|
||||
Protobuf.varintField(2, index.toLong())
|
||||
@@ -1127,6 +1136,7 @@ class GearslipRunner(
|
||||
const val MSG_SENSOR_START_RESPONSE = 32770
|
||||
const val MSG_SENSOR_EVENT_INDICATION = 32771
|
||||
const val MSG_SENSOR_ERROR = 32772
|
||||
private const val SENSOR_LOCATION = 1
|
||||
private const val SENSOR_LOG_EVERY_MS = 10_000L
|
||||
|
||||
const val STREAM_MEDIA = 3
|
||||
|
||||
@@ -99,6 +99,20 @@ object ServiceDiscovery {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The media sink to stream to. Gearslip sends raw PCM, so a PCM sink wins: the 2022 Dacia
|
||||
* Jogger (LGE ULC 4.5) lists an AAC media sink on channel 3 before a PCM one on channel 5,
|
||||
* and PCM pushed into the AAC sink was acked but played as silence. With no PCM sink, the
|
||||
* first media sink is the best there is.
|
||||
*/
|
||||
fun pickMediaSink(sinks: List<AudioService>): AudioService? {
|
||||
val media = sinks.filter { it.streamType == STREAM_MEDIA }
|
||||
return media.firstOrNull { it.codecType == CODEC_PCM } ?: media.firstOrNull()
|
||||
}
|
||||
|
||||
const val STREAM_MEDIA = 3
|
||||
const val CODEC_PCM = 1
|
||||
|
||||
private val STREAMS = mapOf(0 to "NONE", 1 to "GUIDANCE", 2 to "SYSTEM_AUDIO", 3 to "MEDIA", 4 to "TELEPHONY")
|
||||
|
||||
private val RESOLUTIONS = mapOf(
|
||||
|
||||
@@ -47,7 +47,6 @@ import androidx.compose.ui.graphics.vector.PathParser
|
||||
/** What a built-in app's id carries in a pin, so it can't collide with an installed app's component. */
|
||||
internal const val BUILT_IN_PREFIX = "gearslip:"
|
||||
|
||||
/** One of Gearslip's own apps: a screen, with nothing running behind it once you leave. */
|
||||
/** Material's "directions car", drawn here because only the core icon set is bundled. */
|
||||
private val CarGlyph: ImageVector = ImageVector.Builder("Car", 24.dp, 24.dp, 24f, 24f).addPath(
|
||||
PathParser().parsePathString(
|
||||
@@ -60,11 +59,15 @@ private val CarGlyph: ImageVector = ImageVector.Builder("Car", 24.dp, 24.dp, 24f
|
||||
fill = SolidColor(Color.Black),
|
||||
).build()
|
||||
|
||||
/**
|
||||
* One of Gearslip's own apps: a screen, with nothing running behind it once you leave. [screen] is
|
||||
* null for an action like Exit, which has no screen and so never shows as open.
|
||||
*/
|
||||
internal class BuiltInApp(
|
||||
val id: String,
|
||||
val label: String,
|
||||
val glyph: ImageVector,
|
||||
val screen: CarScreen,
|
||||
val screen: CarScreen?,
|
||||
val open: (CarNavigator) -> Unit,
|
||||
)
|
||||
|
||||
@@ -76,7 +79,7 @@ internal object BuiltInApps {
|
||||
val Settings = BuiltInApp("settings", "Settings", Icons.Filled.Settings, CarScreen.Settings) { it.settings() }
|
||||
|
||||
/** Hands the screen back to the car's own interface, like Android Auto's Exit. */
|
||||
val Exit = BuiltInApp("exit", "Exit", CarGlyph, CarScreen.Home) { CarFocus.exitToCar() }
|
||||
val Exit = BuiltInApp("exit", "Exit", CarGlyph, screen = null) { CarFocus.exitToCar() }
|
||||
|
||||
val all = listOf(Web, ScreenSharing, Phone, VehicleData, Settings, Exit)
|
||||
val entries: List<Entry> = all.map { Entry(it.label, icon = null, builtIn = it) }
|
||||
@@ -128,7 +131,7 @@ internal fun rememberRunningApps(): Map<String, RunningSlot> {
|
||||
else -> false
|
||||
}
|
||||
mediaApp?.component?.flattenToString()?.takeIf { mediaLive }?.let { put(it, RunningSlot.MEDIA) }
|
||||
BuiltInApps.all.firstOrNull { it.screen == current }?.let { put(BUILT_IN_PREFIX + it.id, RunningSlot.SCREEN) }
|
||||
BuiltInApps.all.firstOrNull { it.screen != null && it.screen == current }?.let { put(BUILT_IN_PREFIX + it.id, RunningSlot.SCREEN) }
|
||||
(current as? CarScreen.Messages)?.let { put(MESSAGING_PREFIX + it.packageName, RunningSlot.SCREEN) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,11 +84,13 @@ object Weather {
|
||||
return
|
||||
}
|
||||
val data = runCatching { fetch(context, location) }
|
||||
.onFailure { GearslipLog.w("weather fetch failed: ${it.message}") }
|
||||
// Only the kind of error: some messages carry the request URL, and that holds the location.
|
||||
.onFailure { GearslipLog.w("weather fetch failed: ${it.javaClass.simpleName}") }
|
||||
.getOrNull()
|
||||
if (data != null) {
|
||||
flow.value = WeatherState.Ready(data)
|
||||
GearslipLog.i("weather: ${data.place} ${data.temp}${data.tempUnit} ${describe(data.code)}")
|
||||
// No place name: logs get shared in bug reports.
|
||||
GearslipLog.i("weather: loaded")
|
||||
}
|
||||
else if (flow.value !is WeatherState.Ready) flow.value = WeatherState.Unavailable("Could not reach the weather service.")
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user