From dfebae93b389bb2c596b95c5b6c9290e8a7b7b6e Mon Sep 17 00:00:00 2001 From: Seb3thehacker Date: Tue, 6 Oct 2026 15:26:45 -0700 Subject: [PATCH] higher video bitrate, scaled to the car's screen 800x480 at 30fps gets 8 Mbps instead of 4, and larger screens scale from there. The encoder now declares the H.264 level each size needs, and the bitrate stays within that level's limit. --- .../app/seb3thehacker/gearslip/VideoSource.kt | 45 ++++++++++++++++--- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/VideoSource.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/VideoSource.kt index 8a7aa81..02b74f4 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/VideoSource.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/VideoSource.kt @@ -45,7 +45,8 @@ class VideoSource( private var frameIndex = 0L fun start() { - val bitRate = (width.toLong() * height * frameRate * BITS_PER_PIXEL).toInt().coerceIn(MIN_BIT_RATE, MAX_BIT_RATE) + val level = levelFor(width, height, frameRate) + val bitRate = defaultBitRate(width, height, frameRate, level) fun format(tuned: Boolean) = MediaFormat.createVideoFormat(MIME, width, height).apply { setInteger( MediaFormat.KEY_COLOR_FORMAT, @@ -74,7 +75,7 @@ class VideoSource( setFloat(MediaFormat.KEY_MAX_FPS_TO_ENCODER, frameRate.toFloat()) } setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline) - setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel31) + setInteger(MediaFormat.KEY_LEVEL, level.codecLevel) if (tuned) { // Hints only: a car screen wants each frame now, not the best frame a moment later. setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR) @@ -98,7 +99,7 @@ class VideoSource( codec = encoder running = true - log.i("encoder started: ${width}x$height @ ${frameRate}fps, ${bitRate / 1000} kbps, H.264 Baseline, mode=$mode") + log.i("encoder started: ${width}x$height @ ${frameRate}fps, ${bitRate / 1000} kbps, H.264 Baseline level ${level.label}, mode=$mode") worker = Thread { runCatching { pump(encoder) } @@ -223,10 +224,40 @@ class VideoSource( const val TIMEOUT_US = 10_000L const val SURFACE_TIMEOUT_US = 100_000L - /** Enough for sharp text and map lines; 800x480 at 30fps comes out at 4 Mbps. */ - const val BITS_PER_PIXEL = 0.3 - const val MIN_BIT_RATE = 4_000_000 - const val MAX_BIT_RATE = 12_000_000 + /** + * 800x480 at 30fps gets 8 Mbps: about the most that size can show, enough that a moving + * video in the browser stays sharp. Bigger screens get more, but less than in proportion, + * since large frames compress better: bitrate grows with pixels^0.75. 60fps gets 1.5x, + * because frames that close together are mostly alike. + */ + const val BASE_BIT_RATE = 8_000_000.0 + const val BASE_PIXELS = 800.0 * 480 + /** No car's decoder is asked for more than this, whatever its level allows. */ + const val MAX_BIT_RATE = 25_000_000 + + /** + * The H.264 levels a car screen can need, smallest first: macroblocks a second each + * decodes, and the Baseline bitrate it must handle. The encoder declares the smallest + * that fits, and the bitrate stays within it, so a car decoder isn't asked for more than + * the level it was told about. + */ + enum class Level(val label: String, val codecLevel: Int, val macroblocksPerSecond: Int, val maxBitRate: Int) { + L31("3.1", MediaCodecInfo.CodecProfileLevel.AVCLevel31, 108_000, 14_000_000), + L32("3.2", MediaCodecInfo.CodecProfileLevel.AVCLevel32, 216_000, 20_000_000), + L4("4", MediaCodecInfo.CodecProfileLevel.AVCLevel4, 245_760, 20_000_000), + L42("4.2", MediaCodecInfo.CodecProfileLevel.AVCLevel42, 522_240, 50_000_000), + } + + fun levelFor(width: Int, height: Int, frameRate: Int): Level { + val perSecond = ((width + 15) / 16) * ((height + 15) / 16) * frameRate + return Level.entries.firstOrNull { perSecond <= it.macroblocksPerSecond } ?: Level.L42 + } + + fun defaultBitRate(width: Int, height: Int, frameRate: Int, level: Level): Int { + val size = Math.pow(width.toDouble() * height / BASE_PIXELS, 0.75) + val rate = if (frameRate > 30) 1.5 else 1.0 + return (BASE_BIT_RATE * size * rate).toInt().coerceAtMost(minOf(level.maxBitRate, MAX_BIT_RATE)) + } const val I_FRAME_INTERVAL_S = 5 private const val REPEAT_AFTER_US = 100_000L