diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipActivity.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipActivity.kt index dc2601c..c267e05 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipActivity.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipActivity.kt @@ -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() diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt index d2fbb87..7cf04ca 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt @@ -1189,6 +1189,7 @@ class GearslipRunner( onFrame = { data, presentationTimeUs, keyFrame -> sendVideoFrame(data, presentationTimeUs, keyFrame) }, + onStill = { projection?.onStill() }, ).also { source -> runCatching { source.start() diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/Projection.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/Projection.kt index 0822f10..6175b52 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/Projection.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/Projection.kt @@ -24,6 +24,12 @@ interface Projection { fun onTouch(action: Int, actionIndex: Int, points: List) 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. */ diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ScreenProjector.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ScreenProjector.kt index 75d873d..1188464 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ScreenProjector.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ScreenProjector.kt @@ -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 } diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/VideoSource.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/VideoSource.kt index 1d587e4..2a95d45 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/VideoSource.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/VideoSource.kt @@ -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