易控 Android 15/16 视频黑屏问题修复

背景介绍

最近收到用户反馈,在使用易控(EasyControl)时发现部分升级到 Android 15/16 的设备出现了一个棘手的问题:可以正常控制设备,但屏幕就是没有画面输出。更奇怪的是,有些设备第一次打开还能显示,第二次打开就彻底黑屏了。

作为一个基于开源项目 Scrcpy 深度定制的安卓端控制工具,这个问题显然不能忍。经过一番深入排查和调试,终于定位到问题根源并成功修复。今天就来和大家分享一下这次踩坑和填坑的经历。

问题现象

用户报告的问题主要集中在以下几点:

  1. Android 15/16 设备上”可控制但无视频输出”
  2. 第二次打开应用时黑屏或连接断开
  3. 部分设备偶尔有画面,但极不稳定

这就很奇怪了,明明控制指令能正常发送接收,说明 ADB 连接和协议解析都是正常的,为什么偏偏视频流就出不来呢?

问题分析

带着疑问,我开始了对代码的深入分析。通过日志和调试,逐渐锁定了几个可能的原因:

1. 显示捕获链路差异

Android 15/16 对显示捕获机制进行了调整,SurfaceControlVirtualDisplay 路径的稳定性都受到了影响。在新系统上,原有的取屏方式可能会出现”有连接无画面”的情况。

2. 编码输出兼容性问题

部分设备的编码器在帧边界和偏移处理上存在差异,如果客户端收到的帧数据不完整或者包含了无效区域,就会导致解码失败,表现为黑屏。

3. 时间戳处理不当

Android 16 对解码渲染的时间戳使用更加严格,如果使用纳秒级的 presentationTimeUs 作为渲染时钟,可能会导致解码器认为时间未到而拒绝输出画面。

4. 构建流程隐患

Gradle 构建过程中,app 与 server 模块之间的资源复制存在隐式任务依赖告警,可能导致打包时使用了旧版本的 server.jar。

解决方案

针对上述问题,我进行了一系列针对性的修复:

服务端修改

优先使用 VirtualDisplay 镜像路径

VideoEncode.java 中,增加了 Android 16+ 的策略判断,优先走 VirtualDisplay 镜像路径,避免 SurfaceControl 路径在新系统上的不稳定性:

1
2
3
4
5
// Android 16+ 优先使用 VirtualDisplay 镜像路径
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
// 使用镜像方式创建虚拟显示器
displayManager.createVirtualDisplay("scrcopy", videoWidth, videoHeight, surface);
}

增加自动回退机制

setDisplaySurface() 调用失败时,不再直接放弃,而是销毁当前 display 并切换到 VirtualDisplay 路径,防止会话直接断开。

主动请求同步帧

启动编码后,立即请求 IDR 帧(关键帧),缩短首帧可见时间:

1
2
3
4
// 请求同步帧,加速首帧显示
Bundle params = new Bundle();
params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
mediaCodec.setParameters(params);

精确发送帧数据

修复编码输出帧的切片逻辑,严格按照 bufferInfo.offset/size 发送有效数据,避免将 ByteBuffer 中的无效区域发送给客户端:

1
2
3
4
// 只发送有效的编码数据
byte[] frameData = new byte[bufferInfo.size];
outputBuffer.get(frameData, bufferInfo.offset, bufferInfo.size);
sendFrame(frameData);

客户端修改

修复渲染调用

VideoDecode.java 中,修正了输出渲染的调用方式,使用 true 参数让系统自动处理渲染时间戳:

1
2
// 让系统自动处理渲染时间戳,避免 Android 16 不出画面
mediaCodec.releaseOutputBuffer(outIndex, true);

强制使用 H.264

在连接协商阶段,强制设置 supportH265=0,统一走 H.264 编码路径,规避不同设备 HEVC 编码器的兼容性差异。

构建流程优化

app/build.gradle 中增加显式的任务依赖,确保每次构建 app 前都能拿到最新的 server.jar:

1
2
3
4
5
6
7
8
9
android {
// 确保构建前总是复制最新的 server.jar
applicationVariants.all { variant ->
def variantName = variant.name.capitalize()
tasks.named("pre${variantName}Build").configure {
dependsOn ":server:copy${variantName}"
}
}
}

修复结果

经过上述修改后,问题得到了圆满解决:

  • Android 15 设备:视频显示恢复正常
  • Android 16 设备:视频输出稳定,可控且可见
  • 构建流程:Gradle 构建稳定通过,不再有隐式依赖告警
  • 用户体验:首帧显示更快,连接更稳定

截图

经验总结

这次修复过程让我深刻体会到:

  1. 系统升级带来的兼容性挑战:Android 每个大版本更新都可能带来底层 API 的行为变化,需要保持关注并及时适配。

  2. 日志的重要性:通过详细的调试日志,才能快速定位到问题的真正所在。当然,生产环境中要注意收敛日志输出。

  3. 防御性编程:在关键路径上增加异常处理和回退机制,可以在某个方案失效时自动切换到备用方案,提升系统的鲁棒性。

  4. 构建流程的可靠性:看似不起眼的构建配置,也可能成为影响最终产品质量的关键因素。

项目简介

易控(EasyControl)是一个基于 Scrcpy 深度定制的安卓端控制工具,主要特色功能包括:

  • 使用简单,启动迅速
  • 支持音频传输
  • 多设备连接与剪切板同步
  • 共享主控端物理键盘(配合微信/QQ 输入法)
  • 低延迟,分辨率自适应
  • 良好的旋转支持和小窗/全屏显示

如果你对这个项目感兴趣,欢迎访问 GitHub 仓库查看完整代码和更多使用说明。

参考资料


易控 Android 15/16 视频黑屏问题修复
https://miku2024.top/posts/易控/
作者
KB
发布于
2026年3月22日
许可协议