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
12 changes: 12 additions & 0 deletions .github/workflows/debug-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ jobs:
- name: Build and verify Debug
run: ./gradlew --no-daemon :shared:testDebugUnitTest :common:testDebugUnitTest :common:assembleDebugAndroidTest :mobile:lintDebug :mobile:assembleDebug

- name: Upload failed unit test reports
if: failure()
uses: actions/upload-artifact@v4
with:
name: wheelplay-unit-test-results
path: |
shared/build/reports/tests/
shared/build/test-results/
common/build/reports/tests/
common/build/test-results/
if-no-files-found: warn

- name: Upload browser runtime test APK
uses: actions/upload-artifact@v4
with:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Install the app on an Android device and open the displayed address in your car'
## Features

- **Browser display and touch control**: CarPlay streaming, touch input, and fullscreen display.
- **Selectable streaming modes**: Choose WebRTC video passthrough (H.264 / HEVC) or JPEG compatibility mode in settings. If passthrough fails, video automatically falls back to JPEG while audio and touch keep their existing routes.
- **Selectable streaming modes**: Choose WebRTC video passthrough (H.264 / HEVC) or image compatibility mode in settings, with JPEG / WebP format and image quality from 1 to 100. Image streaming retains JPEG compatibility fallback and overlaps up to two frames while audio and touch keep their existing routes.
- **Optional browser audio**: After pairing, the browser can play CarPlay audio. The browser microphone can provide CarPlay voice input through a trusted HTTPS page; Android handles audio and microphone input by default.
- **Easy pairing**: Scan the QR code or enter a code once. Remembered browsers restore pairing automatically and retain a launch button for selecting options before streaming.
- **iPhone connection management**: USB and wireless connections, connection status, and session management.
Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ WheelPlay 是一款 Android 局域网 CarPlay 服务端应用,可将 iPhone
## 功能

- **浏览器显示与触控**:支持 CarPlay 画面串流、触摸操作和全屏显示。
- **可选串流技术**:设置中可选 WebRTC 视频直通(H.264 / HEVC)或 JPEG 兼容模式。直通失败自动回退 JPEG,音频与触控保持原有路径。
- **可选串流技术**:设置中可选 WebRTC 视频直通(H.264 / HEVC)或图片兼容模式,并调整 JPEG / WebP 格式和 1–100 的图片质量。图片串流保留 JPEG 兼容回退,最多两帧并行传输,音频与触控保持原有路径。详见[图片串流与多缓冲](docs/IMAGE_STREAM.md)。
- **可选浏览器音频**:配对后可改由浏览器播放 CarPlay 音频;浏览器麦克风可通过可信 HTTPS 页面接入 CarPlay 语音输入,默认仍由 Android 处理。
- **便捷配对**:首次扫描车机网页二维码或输入配对码,之后自动恢复已记住的浏览器配对;启动前仍可选择网页选项。
- **iPhone 连接管理**:支持 USB 与无线连接,查看连接状态并管理会话。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,37 @@ import java.util.concurrent.LinkedBlockingQueue

@RunWith(AndroidJUnit4::class)
class WebRuntimeTest {
@Test fun staticFrameIsReencodedWhenImageFormatOrQualityChanges() {
val frames = LinkedBlockingQueue<Triple<ByteArray, ImageFormat, Int>>()
val preferred = java.util.concurrent.atomic.AtomicReference(ImageFormat.JPEG)
val quality = java.util.concurrent.atomic.AtomicInteger(75)
val output = OffscreenVideoOutput(320, 180, { true }, { ImageEncodingOptions(preferred.get(), quality.get()) }) { bytes, format, value ->
frames.offer(Triple(bytes, format, value))
}
try {
val canvas = output.surface.lockCanvas(null); canvas.drawColor(Color.RED)
output.surface.unlockCanvasAndPost(canvas)
assertEquals(ImageFormat.JPEG, frames.poll(5, TimeUnit.SECONDS)?.second)
preferred.set(ImageFormat.WEBP)
val webp = requireNotNull(frames.poll(5, TimeUnit.SECONDS))
assertEquals(ImageFormat.WEBP, webp.second)
assertEquals("RIFF", webp.first.copyOfRange(0, 4).toString(Charsets.US_ASCII))
assertEquals("WEBP", webp.first.copyOfRange(8, 12).toString(Charsets.US_ASCII))
val bitmap = BitmapFactory.decodeByteArray(webp.first, 0, webp.first.size)
assertEquals(320, bitmap.width); assertTrue(Color.red(bitmap.getPixel(160, 90)) > 200); bitmap.recycle()
quality.set(40)
val refreshed = requireNotNull(frames.poll(5, TimeUnit.SECONDS))
assertEquals(ImageFormat.WEBP, refreshed.second); assertEquals(40, refreshed.third)
preferred.set(ImageFormat.JPEG)
val jpeg = requireNotNull(frames.poll(5, TimeUnit.SECONDS))
assertEquals(ImageFormat.JPEG, jpeg.second); assertEquals(40, jpeg.third)
assertNull("Static format changes must not start continuous encoding", frames.poll(400, TimeUnit.MILLISECONDS))
} finally { output.close() }
}

@Test fun offscreenThrottlePublishesFinalFrameWithoutAnotherArrival() {
val frames = LinkedBlockingQueue<ByteArray>()
val output = OffscreenVideoOutput(320, 180, { true }) { frames.offer(it) }
val output = OffscreenVideoOutput(320, 180, { true }) { bytes, _, _ -> frames.offer(bytes) }
try {
fun paint(color: Int) {
val canvas = output.surface.lockCanvas(null)
Expand All @@ -42,7 +70,7 @@ class WebRuntimeTest {
@Test fun offscreenSurfaceProducesUprightJpegWithoutActivity() {
val ready = CountDownLatch(1)
var jpeg: ByteArray? = null
val output = OffscreenVideoOutput(320, 180, { true }) { jpeg = it; ready.countDown() }
val output = OffscreenVideoOutput(320, 180, { true }) { bytes, _, _ -> jpeg = bytes; ready.countDown() }
try {
val canvas = output.surface.lockCanvas(null)
val paint = Paint()
Expand Down
Loading
Loading