From 52a566ac7446f6ca911799ba1db2ed26c4eb73ff Mon Sep 17 00:00:00 2001 From: Siubeng Date: Sat, 24 Oct 2026 07:24:13 +0000 Subject: [PATCH 1/2] [CF-Pages-Skip] fix: address Android Auto video and mic issues --- .../com/shilapi/xcertplay/DiPlayActivity.kt | 23 ++++++++++++++++ .../projection/androidauto/AapVideo.kt | 9 ++++++- common/src/main/res/values-zh-rCN/strings.xml | 1 + common/src/main/res/values-zh-rTW/strings.xml | 1 + common/src/main/res/values/strings.xml | 1 + .../projection/androidauto/AapVideoTest.kt | 27 ++++++++++++++++++- 6 files changed, 60 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/com/shilapi/xcertplay/DiPlayActivity.kt b/common/src/main/java/com/shilapi/xcertplay/DiPlayActivity.kt index 8bbfaa7..fd8e34e 100644 --- a/common/src/main/java/com/shilapi/xcertplay/DiPlayActivity.kt +++ b/common/src/main/java/com/shilapi/xcertplay/DiPlayActivity.kt @@ -136,6 +136,14 @@ class DiPlayActivity : AppCompatActivity() { private var initialLaunch = true private var notificationTransport = true private var exportInProgress = false + private var pendingAndroidAutoSourceId: String? = null + private val androidAutoMicrophonePermission = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> + val sourceId = pendingAndroidAutoSourceId ?: return@registerForActivityResult + pendingAndroidAutoSourceId = null + if (!granted) toast(getString(UiR.string.aa_microphone_permission_denied)) + startProjectionSourceNow(sourceId) + } private val notificationPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { connect(notificationTransport) } @@ -227,6 +235,7 @@ class DiPlayActivity : AppCompatActivity() { ServerWindow.showSystemBars(window) refreshAuthentication() selectedPrivateKey = savedInstanceState?.getString("selectedPrivateKey")?.let(Uri::parse) + pendingAndroidAutoSourceId = savedInstanceState?.getString("pendingAndroidAutoSourceId") awaitingTeslaVpnConsent = savedInstanceState?.getBoolean("awaitingTeslaVpnConsent") ?: false page = savedInstanceState?.getString("page") ?: intent.getStringExtra("page") ?: if (setupError == null) "service" else "settings" @@ -264,6 +273,7 @@ class DiPlayActivity : AppCompatActivity() { override fun onSaveInstanceState(outState: Bundle) { outState.putString("page", page) outState.putString("selectedPrivateKey", selectedPrivateKey?.toString()) + outState.putString("pendingAndroidAutoSourceId", pendingAndroidAutoSourceId) outState.putBoolean("awaitingTeslaVpnConsent", awaitingTeslaVpnConsent) super.onSaveInstanceState(outState) } @@ -754,6 +764,19 @@ class DiPlayActivity : AppCompatActivity() { } private fun startProjectionSource(id: String) { + if (isAndroidAutoSource(id) && + AirPlayPersistence.loadMicrophoneSource(this) == CarPlayMicrophoneSource.ANDROID && + checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { + pendingAndroidAutoSourceId = id + androidAutoMicrophonePermission.launch(Manifest.permission.RECORD_AUDIO) + return + } + startProjectionSourceNow(id) + } + + private fun isAndroidAutoSource(id: String) = id == LocalAndroidAuto.SOURCE_ID || id.startsWith("android-auto-") + + private fun startProjectionSourceNow(id: String) { registerProjectionSources(this) startWebServiceIfReady() projectionSessions.switchTo(id).whenComplete { _, failure -> runOnUiThread { diff --git a/common/src/main/java/com/shilapi/xcertplay/projection/androidauto/AapVideo.kt b/common/src/main/java/com/shilapi/xcertplay/projection/androidauto/AapVideo.kt index 7d4a6bd..a0c2c78 100644 --- a/common/src/main/java/com/shilapi/xcertplay/projection/androidauto/AapVideo.kt +++ b/common/src/main/java/com/shilapi/xcertplay/projection/androidauto/AapVideo.kt @@ -21,9 +21,16 @@ internal class AapVideo(private val output: ProjectionVideoOutput, private val k if (config?.contentEquals(next) != true) { revision = output.configure(VideoCodec.H264, next, keyframe); config = next } } return revision != 0L && nals.any { it[0].toInt() and 31 in 1..5 } && - output.frame(revision, HeapVideoFrame(unit.data), unit.ptsUs) + output.frame(revision, HeapVideoFrame(canonicalAnnexB(nals)), unit.ptsUs) } companion object { + private fun canonicalAnnexB(nals: List): ByteArray = ByteArrayOutputStream().apply { + nals.forEach { nal -> + write(byteArrayOf(0, 0, 0, 1)) + write(nal) + } + }.toByteArray() + fun annexB(data: ByteArray): List { val starts = ArrayList>() var i = 0 diff --git a/common/src/main/res/values-zh-rCN/strings.xml b/common/src/main/res/values-zh-rCN/strings.xml index d45d75a..0a7ab60 100644 --- a/common/src/main/res/values-zh-rCN/strings.xml +++ b/common/src/main/res/values-zh-rCN/strings.xml @@ -1,4 +1,5 @@ + 录音权限已拒绝。Android Auto 将在没有麦克风输入的情况下连接。 上一首 播放 暂停 diff --git a/common/src/main/res/values-zh-rTW/strings.xml b/common/src/main/res/values-zh-rTW/strings.xml index 0fc3dba..a6185e5 100644 --- a/common/src/main/res/values-zh-rTW/strings.xml +++ b/common/src/main/res/values-zh-rTW/strings.xml @@ -1,4 +1,5 @@ + 錄音權限已拒絕。Android Auto 將在沒有麥克風輸入的情況下連線。 上一首 播放 暫停 diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 1615520..825fcff 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -1,4 +1,5 @@ + Microphone permission was denied. Android Auto will connect without microphone input. WheelPlay Previous track Play diff --git a/common/src/test/java/com/shilapi/xcertplay/projection/androidauto/AapVideoTest.kt b/common/src/test/java/com/shilapi/xcertplay/projection/androidauto/AapVideoTest.kt index 75e26d4..5ebfe3d 100644 --- a/common/src/test/java/com/shilapi/xcertplay/projection/androidauto/AapVideoTest.kt +++ b/common/src/test/java/com/shilapi/xcertplay/projection/androidauto/AapVideoTest.kt @@ -11,12 +11,16 @@ class AapVideoTest { private class Output : ProjectionVideoOutput { override val surface: Surface get() = error("encoded") val configs = mutableListOf() + val frameData = mutableListOf() var frames = 0 override fun configure(codec: VideoCodec, codecData: ByteArray, requestKeyframe: () -> Unit): Long { configs += codecData; return configs.size.toLong() } override fun frame(formatRevision: Long, frame: CompressedVideoFrame, ptsUs: Long): Boolean { - assertEquals(configs.size.toLong(), formatRevision); frames++; return true + assertEquals(configs.size.toLong(), formatRevision) + frameData += frame.toByteArray() + frames++ + return true } override fun framebuffer(snapshot: ProjectionFramebuffer.Snapshot) = false override fun close() = Unit @@ -37,4 +41,25 @@ class AapVideoTest { video.accept(AapMediaAssembler.AccessUnit(nal(sps.copyOf().also { it[4] = 3 }) + nal(pps), 0)) assertEquals(2, out.configs.size) } + + @Test fun threeByteFourByteAndMixedStartCodesReachThePublicImageDecoderAsCanonicalAnnexB() { + val out = Output(); val video = AapVideo(out) {} + val sps = byteArrayOf(0x67, 0x42, 0, 0x1e, 1) + val pps = byteArrayOf(0x68, 2) + val start3 = byteArrayOf(0, 0, 1) + val start4 = byteArrayOf(0, 0, 0, 1) + video.accept(AapMediaAssembler.AccessUnit(start3 + sps + start4 + pps, 0)) + assertEquals(1, out.configs.size) + + val idr = byteArrayOf(0x65, 4) + val slice = byteArrayOf(0x41, 5) + val inputs = listOf(start3 + idr, start4 + idr, start3 + idr + start4 + slice) + val expected = listOf(start4 + idr, start4 + idr, start4 + idr + start4 + slice) + inputs.forEachIndexed { index, input -> + video.accept(AapMediaAssembler.AccessUnit(input, 42L + index)) + val encodedFrame = out.frameData[index] + assertArrayEquals(expected[index], encodedFrame) + assertArrayEquals(encodedFrame, com.shilapi.xcertplay.media.MediaCodecSupport.toAnnexB(encodedFrame)) + } + } } From 2ec0544c6ffab1b31b871e53e4a5182acdb9faa5 Mon Sep 17 00:00:00 2001 From: Siubeng Date: Sat, 24 Oct 2026 07:36:33 +0000 Subject: [PATCH 2/2] chore: align Cloudflare Wrangler project names --- website/wrangler.jsonc | 2 +- website/wrangler.worker.jsonc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/website/wrangler.jsonc b/website/wrangler.jsonc index 556f205..ab82887 100644 --- a/website/wrangler.jsonc +++ b/website/wrangler.jsonc @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/cloudflare/workers-sdk/main/packages/wrangler/config-schema.json", - "name": "wheelplay-website", + "name": "wheelplay", "compatibility_date": "2026-10-23", "pages_build_output_dir": "./dist", } diff --git a/website/wrangler.worker.jsonc b/website/wrangler.worker.jsonc index e4549d0..c59cf6d 100644 --- a/website/wrangler.worker.jsonc +++ b/website/wrangler.worker.jsonc @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/cloudflare/workers-sdk/main/packages/wrangler/config-schema.json", - "name": "wheelplay-website", + "name": "wheelplay", "main": "./worker/index.ts", "compatibility_date": "2026-10-23", // Required by `wrangler preview`; static assets remain configured at the top level.