A still car screen gets a fresh frame every second

A surface encoder only makes a frame when the screen changes, and the
repeat-last-frame setting should cover a still one. On a Pixel 10 it
didn't: a LuChi box got 11 frames, then nothing, and stayed black
(#58). When the encoder has been quiet for a second, Gearslip now
draws its screen again, which is a new frame for the car.
This commit is contained in:
Seb3thehacker committed 2026-11-04 13:37:36 -08:00
1 parent a70dea7bd5
commit c33875c8f8
5 files changed
+33

No files matched your search

@@ -526,6 +526,10 @@ class GearslipActivity : ComponentActivity(), Projection {
}
}
override fun onStill() {
screenProjector?.redraw()
}
override fun onProjectionStopped() {
// Before the car UI goes, while the media connection is still up.
app.seb3thehacker.gearslip.car.CarServices.pausePhoneMedia()
@@ -1189,6 +1189,7 @@ class GearslipRunner(
onFrame = { data, presentationTimeUs, keyFrame ->
sendVideoFrame(data, presentationTimeUs, keyFrame)
},
onStill = { projection?.onStill() },
).also { source ->
runCatching {
source.start()
@@ -24,6 +24,12 @@ interface Projection {
fun onTouch(action: Int, actionIndex: Int, points: List<TouchPoint>)
fun onProjectionStopped()
/**
* The screen has been still for a moment and the encoder has sent nothing since. Draw it once
* more, so the car gets a fresh frame (see [VideoSource]'s onStill).
*/
fun onStill() {}
}
/** One finger on the car's screen: a stable [id] for as long as it stays down, and where it is. */
@@ -223,6 +223,11 @@ class ScreenProjector(private val context: Context) {
* Points the display at another surface, or none, without restarting what it shows. The
* preview's SurfaceView comes and goes as the phone turns; the car UI on it carries on.
*/
/** Draws the screen again as it is, which hands the display's consumer a new frame. */
fun redraw() {
root?.let { it.post { it.invalidate() } }
}
fun setSurface(surface: Surface?) {
virtualDisplay?.surface = surface
}
@@ -27,6 +27,12 @@ class VideoSource(
private val wireless: Boolean = false,
private val onCodecConfig: (ByteArray) -> Unit,
private val onFrame: (data: ByteArray, presentationTimeUs: Long, keyFrame: Boolean) -> Unit,
/**
* SURFACE mode, nothing encoded for [STILL_MS]. The repeat setting below should cover a still
* screen, but on a Pixel 10 it didn't: a LuChi box got 11 frames, then none, and stayed black
* (issue #58). Whoever draws the screen can draw it again, and that is a frame.
*/
private val onStill: () -> Unit = {},
) {
private val log = GearslipLog.tagged("VIDEO")
@@ -170,6 +176,8 @@ class VideoSource(
private fun pump(encoder: MediaCodec) {
val info = MediaCodec.BufferInfo()
val frameIntervalUs = 1_000_000L / frameRate
var lastOutputAt = 0L
var lastNudgeAt = 0L
while (running) {
// SURFACE mode has no input buffers to fill - frames arrive from the display.
@@ -208,8 +216,16 @@ class VideoSource(
}
}
encoder.releaseOutputBuffer(outIndex, false)
lastOutputAt = System.currentTimeMillis()
outIndex = encoder.dequeueOutputBuffer(info, 0)
}
if (mode == Mode.SURFACE && lastOutputAt > 0) {
val now = System.currentTimeMillis()
if (now - lastOutputAt > STILL_MS && now - lastNudgeAt > STILL_MS) {
lastNudgeAt = now
onStill()
}
}
if (mode != Mode.SURFACE) Thread.sleep(maxOf(1L, frameIntervalUs / 2000))
}
@@ -256,6 +272,7 @@ class VideoSource(
const val MIME = MediaFormat.MIMETYPE_VIDEO_AVC
const val TIMEOUT_US = 10_000L
const val SURFACE_TIMEOUT_US = 100_000L
private const val STILL_MS = 1_000L
/**
* 800x480 at 30fps gets 8 Mbps: about the most that size can show, enough that a moving