项目中文名:虚拟投屏 · 仓库名:
VirtualCast(GitHub 仓库名只允许 ASCII,中文名会被系统剥离,详见文末说明)
在 Android 设备上列出所有屏幕(含虚拟屏 / 副屏)→ 抓取指定屏幕的画面 → 在窗口里实时显示 → 反向操控那块屏。
配合 Shizuku 使用,无需 root。
典型场景:手机上同时挂着 scrcpy 之类的虚拟屏、或用系统「强制启用副屏」开出的第二块屏,本工具能把那块屏的画面抓进来看,并在上面启动应用、发返回/Home 等按键、force-stop 应用。
| 功能 | 说明 |
|---|---|
| 屏幕检测 | 列出设备上所有 display,标注 displayId、类型(physical / overlay / virtual)、分辨率、是否活跃 |
| 实时投屏 | 抓取选中屏幕的画面并渲染(screencap 并发多路轮询,实测约 3 fps,视设备而定) |
| 触摸注入 | 在投屏窗口上点击/滑动 → 映射坐标 → input -d <id> 注入到目标屏 |
| 按键注入 | 返回 / Home / 最近任务 |
| 应用启动器 | 列出设备上所有可启动应用,点一下就在目标屏全屏启动(自动解析 launcher activity,避开 -p 解析失败) |
| 焦点诊断 | 实时显示 FocusedDisplayId,一眼看出"为什么点不动" |
| 外部接口 | 广播控制:退出 / 暂停 / 打开应用列表 / 指定包名启动 |
| 通知栏控制 | 常驻通知提供「退出投屏」「暂停控制」「打开应用」 |
- Android 8.0+(
minSdk 26,targetSdk 34) - 已安装并启动 Shizuku(或用 SUI / root 方案),且已授权本应用
- 目标屏幕必须已经存在——本工具不创建虚拟屏,只检测并使用设备上已有的屏幕
从 Releases 下载安装包(页面上显示为 虚拟投屏 v1.0.0 APK,实际文件名 VirtualCast-v1.0.0.apk):
adb install -r VirtualCast-v1.0.0.apk- 打开 App → 点 授权 Shizuku → 在系统弹窗选「始终允许」
- 点 检测所有屏幕 → 列表出现,每块屏一个按钮(如
投屏并控制 #4 (virtual 800x1280) 活跃) - 点某一块 → 进入投屏窗口:画面实时刷新,点击/滑动即操控那块屏
- 底部工具条:
返回Home任务应用激活暂停退出投屏
# 退出投屏
adb shell am broadcast -a com.vscreen.cast.STOP -p com.vscreen.cast
# 暂停 / 恢复触摸注入
adb shell am broadcast -a com.vscreen.cast.PAUSE -p com.vscreen.cast
# 打开应用列表
adb shell am broadcast -a com.vscreen.cast.APPS -p com.vscreen.cast
# 在目标屏全屏启动指定包名
adb shell am broadcast -a com.vscreen.cast.LAUNCH -p com.vscreen.cast --es pkg com.tencent.mm
# 直接打开某块屏的投屏窗口(App 未运行时)
adb shell am start --display 0 -n com.vscreen.cast/.MainActivity --ei open 4停止应用:adb shell am force-stop <包名>
需要 JDK 17+ 与 Android SDK(compileSdk 34 / build-tools 34.0.0)。
echo "sdk.dir=/path/to/Android/Sdk" > local.properties
./gradlew assembleDebug
# 产物:app/build/outputs/apk/debug/app-debug.apkapp/src/main/java/com/vscreen/cast/
├── MainActivity.java 主界面:Shizuku 授权、屏幕检测、进入投屏
├── ViewerActivity.java 投屏窗口:渲染、触摸映射、工具条、应用列表、广播处理
├── ScreenPoller.java 并发 screencap 轮询取帧(多路并发换帧率)
├── VDisplay.java 枚举 display、解析 dumpsys display、读物理屏参数
├── ShizukuShell.java Shizuku 侧 shell 命令封装(newProcess + 读输出)
├── InputInjector.java 触摸/按键注入(input -d <id> tap/swipe/keyevent)
├── AppList.java 枚举可启动应用、解析 launcher activity、在指定屏启动
├── CastNotif.java 常驻通知(退出 / 暂停 / 打开应用)
├── StopReceiver.java 广播入口
└── AppLog.java 落盘日志(/sdcard/Android/data/com.vscreen.cast/files/vcast.log)
tools/uitap.py 真机自动化调试脚本(按 resource-id 定位并点击)
- 抓副屏只能用
screencap -d <id>。screenrecord --display-id只认物理屏,对虚拟屏直接报Invalid physical display id。 screencap单帧约 300~650ms(视设备与分辨率),所以用并发 3 路提交换帧率——实测 3 路约 900ms 出 3 帧,比串行快 2.2 倍。并发数在ViewerActivity中构造ScreenPoller时指定。- 投屏窗口必须设成
NOT_FOCUSABLE(FLAG_NOT_FOCUSABLE),否则它会抢走全局输入焦点,注入到目标屏的触摸会被系统全部丢弃。 - 在目标屏启动应用:
am start --display <id> --windowingMode 1 -n 包名/Activity。不加--windowingMode 1会以 freeform 小窗打开;用-p <包名>对部分应用会unable to resolve Intent,所以先cmd package resolve-activity --brief解析出 Activity 再精确启动。 - Android 11+ 包可见性:
<queries>里必须声明MAIN/LAUNCHER,否则应用列表为空。 dumpsys display解析要非贪婪:同一行可能出现多个uniqueId,贪婪匹配会认错屏。
触摸控制是否生效,取决于目标屏是否持有输入焦点。 这是 Android 的输入分发规则——全局只有一块屏能持有焦点。
- 若目标屏本身就是活跃的(例如正连着 scrcpy、或是你正在用的外接屏),点击/滑动可正常注入
- 若目标屏空闲,注入的触摸会被系统丢弃,日志会看到
Focused display #N does not have a focused window
没有任何 shell 命令能强制切换输入焦点(am -h、cmd activity -h、cmd window -h 里都没有)。投屏窗口里的「激活」按钮和不抢焦点的设计都是为缓解这一点。
不受此限制的功能(都是非触摸方式,稳定可用):在目标屏启动任意应用、
force-stop停止应用、返回/Home/最近任务按键。
另外本工具不创建虚拟屏——只检测并使用设备上已有的屏幕。
本项目中文名为 虚拟投屏。应用内显示名、仓库描述、README 标题、Release 标题、APK 下载显示名均为中文。
GitHub 对仓库名与发布资产的文件名只接受 ASCII,其余字符会被服务端直接剥离(不是报错,是静默丢弃)。实测记录:
| 提交内容 | 服务端实际结果 |
|---|---|
仓库名 测试中文名检测xyz |
-xyz |
仓库名 虚拟投屏_v2 |
-_v2 |
仓库名 虚拟投屏 |
报错 name already exists(剥离后与已有仓库重名) |
资产文件名 虚拟投屏-v1.0.0.apk |
-v1.0.0.apk |
所以:
- 仓库名保留英文代号
VirtualCast - APK 文件名保留
VirtualCast-v1.0.0.apk - 中文名通过**仓库描述、README 标题、Release 标题(虚拟投屏 v1.0.0)以及资产的 label 字段(虚拟投屏 v1.0.0 APK)**体现
MIT