Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions common/src/main/java/com/shilapi/xcertplay/DiPlayActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>): ByteArray = ByteArrayOutputStream().apply {
nals.forEach { nal ->
write(byteArrayOf(0, 0, 0, 1))
write(nal)
}
}.toByteArray()

fun annexB(data: ByteArray): List<ByteArray> {
val starts = ArrayList<Pair<Int, Int>>()
var i = 0
Expand Down
1 change: 1 addition & 0 deletions common/src/main/res/values-zh-rCN/strings.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<resources>
<string name="aa_microphone_permission_denied">录音权限已拒绝。Android Auto 将在没有麦克风输入的情况下连接。</string>
<string name="notification_action_previous">上一首</string>
<string name="notification_action_play">播放</string>
<string name="notification_action_pause">暂停</string>
Expand Down
1 change: 1 addition & 0 deletions common/src/main/res/values-zh-rTW/strings.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<resources>
<string name="aa_microphone_permission_denied">錄音權限已拒絕。Android Auto 將在沒有麥克風輸入的情況下連線。</string>
<string name="notification_action_previous">上一首</string>
<string name="notification_action_play">播放</string>
<string name="notification_action_pause">暫停</string>
Expand Down
1 change: 1 addition & 0 deletions common/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<resources>
<string name="aa_microphone_permission_denied">Microphone permission was denied. Android Auto will connect without microphone input.</string>
<string name="app_name" translatable="false">WheelPlay</string>
<string name="notification_action_previous">Previous track</string>
<string name="notification_action_play">Play</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ class AapVideoTest {
private class Output : ProjectionVideoOutput {
override val surface: Surface get() = error("encoded")
val configs = mutableListOf<ByteArray>()
val frameData = mutableListOf<ByteArray>()
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
Expand All @@ -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))
}
}
}
2 changes: 1 addition & 1 deletion website/wrangler.jsonc
Original file line number Diff line number Diff line change
@@ -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",
}
2 changes: 1 addition & 1 deletion website/wrangler.worker.jsonc
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Loading