From 5fe59d350c085fb42dc4ab15cbd7d6e4326a7f69 Mon Sep 17 00:00:00 2001 From: Seb3thehacker Date: Tue, 13 Oct 2026 14:31:03 -0700 Subject: [PATCH] wireless: send about a third of the cable bitrate at 30fps, so the video keeps up and the music stops cutting out --- .../seb3thehacker/gearslip/GearslipActivity.kt | 1 + .../app/seb3thehacker/gearslip/GearslipRunner.kt | 6 +++++- .../app/seb3thehacker/gearslip/VideoSource.kt | 16 ++++++++++++++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipActivity.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipActivity.kt index 0cb49cb..2f8ee50 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipActivity.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipActivity.kt @@ -367,6 +367,7 @@ class GearslipActivity : ComponentActivity(), Projection { identityProvider = { CertProvider.load(this) }, projection = this, vehicleProfileFor = { info -> VehicleProfiles.find(VehicleProfiles.load(this), info) }, + wireless = true, ) this.runner = runner worker = Thread { runner.run() }.apply { name = "gearslip"; start() } diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt index c48df78..f9ca071 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt @@ -36,6 +36,8 @@ class GearslipRunner( private val identityProvider: () -> CertProvider.Identity, private val projection: Projection? = null, private val vehicleProfileFor: (ServiceDiscovery.HeadUnitInfo) -> VehicleProfile? = { null }, + /** Over Wi-Fi rather than USB: the video is sent smaller so the link keeps up. */ + private val wireless: Boolean = false, ) { private val log = GearslipLog.tagged("PROTO") @@ -943,8 +945,10 @@ class GearslipRunner( videoSource = VideoSource( width = width, height = height, - frameRate = if (config.frameRate == 1) 60 else 30, + // 60fps only over a cable: over Wi-Fi it doubles the data for little the driver sees. + frameRate = if (config.frameRate == 1 && !wireless) 60 else 30, mode = if (useProjection) VideoSource.Mode.SURFACE else VideoSource.Mode.TEST_CARD, + wireless = wireless, onCodecConfig = { csd -> lastCodecConfig = csd send(MSG_MEDIA_CODEC_CONFIG, csd, encrypted = true, channel = videoChannelId) diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/VideoSource.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/VideoSource.kt index 02b74f4..685ab75 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/VideoSource.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/VideoSource.kt @@ -23,6 +23,8 @@ class VideoSource( private val height: Int, private val frameRate: Int, private val mode: Mode = Mode.TEST_CARD, + /** Over Wi-Fi: a fraction of the cable bitrate, so the link keeps up and sound isn't starved. */ + private val wireless: Boolean = false, private val onCodecConfig: (ByteArray) -> Unit, private val onFrame: (data: ByteArray, presentationTimeUs: Long, keyFrame: Boolean) -> Unit, ) { @@ -46,7 +48,9 @@ class VideoSource( fun start() { val level = levelFor(width, height, frameRate) - val bitRate = defaultBitRate(width, height, frameRate, level) + val bitRate = defaultBitRate(width, height, frameRate, level).let { + if (wireless) (it * WIRELESS_SHARE).toInt().coerceAtMost(WIRELESS_MAX_BIT_RATE) else it + } fun format(tuned: Boolean) = MediaFormat.createVideoFormat(MIME, width, height).apply { setInteger( MediaFormat.KEY_COLOR_FORMAT, @@ -99,7 +103,7 @@ class VideoSource( codec = encoder running = true - log.i("encoder started: ${width}x$height @ ${frameRate}fps, ${bitRate / 1000} kbps, H.264 Baseline level ${level.label}, mode=$mode") + log.i("encoder started: ${width}x$height @ ${frameRate}fps, ${bitRate / 1000} kbps${if (wireless) " (wireless)" else ""}, H.264 Baseline level ${level.label}, mode=$mode") worker = Thread { runCatching { pump(encoder) } @@ -258,6 +262,14 @@ class VideoSource( val rate = if (frameRate > 30) 1.5 else 1.0 return (BASE_BIT_RATE * size * rate).toInt().coerceAtMost(minOf(level.maxBitRate, MAX_BIT_RATE)) } + /** + * A car's Wi-Fi is shared, noisy and far slower than its nominal rate, and the sound rides + * the same link behind the video. Real Android Auto sends a few Mbps wirelessly; at the + * cable rate (15-20 Mbps for 720p) a laptop hotspot fell seconds behind and the music + * cut in and out. 720p30 comes out near 5.4 Mbps. + */ + const val WIRELESS_SHARE = 0.35 + const val WIRELESS_MAX_BIT_RATE = 8_000_000 const val I_FRAME_INTERVAL_S = 5 private const val REPEAT_AFTER_US = 100_000L