双屏异显原理及实现详解
基于 Android 13 POS + AM8268N USB转HDMI + MiraPlug + Presentation API 的实测验证
背景
android手持主机,采用MTK方案,希望通过usb2.0 口扩展屏幕,且实现主屏幕和扩展屏幕的不同显示。
AM8268N USB转HDMI,是一个usb拓展坞,JD购买,型号: HY41-T4(双HMDI版)
一、整体架构
┌─────────────────────────────────────────────────────────────────────────┐
│ POS 主机 (Android 13) │
│ │
│ ┌─────────────┐ ┌──────────────────────┐ │
│ │ Display 0 │ │ Display N (虚拟) │ N=动态ID (4/6/...) │
│ │ 720×1600 │ │ 496×1088 @60fps │ │
│ │ 主屏Activity│ │ Presentation (独立UI) │ │
│ └──────┬──────┘ └───────────┬──────────┘ │
│ │ │ │
│ │ ┌────────────▼───────────┐ │
│ │ │ VirtualDisplay Surface │ layerStack=N │
│ │ └────────────┬───────────┘ │
│ │ │ │
│ │ SurfaceFlinger 合成 │
│ │ │ │
│ ┌──────▼────────────────────────▼──────────┐ │
│ │ MiraPlug (com.actionsmicro.usbdisplay) │
│ │ │
│ │ ┌──────────────────────────────────────────────────────┐ │
│ │ │ MediaProjection │ │
│ │ │ ├─ 路径A: 抓取主屏(Display 0) → JPEG压缩 │ 镜像模式 │
│ │ │ ├─ 路径B: 抓取VirtualDisplay(N) → 副屏内容 │ 异显模式 │
│ │ │ └─ DisplayPass Protocol 封装 │ │
│ │ └──────────────────────────────────────────────────────┘ │
│ └──────────────────────────┬───────────────────────┘ │
│ │ USB Bulk Transfer │
└─────────────────────────────┼───────────────────────────────────────────┘
│
┌─────────▼──────────┐
│ USB Hub (触点底座) │
└─────────┬──────────┘
│
┌─────────▼──────────┐
│ AM8268N 芯片 │
│ (USB 2.0→HDMI) │
│ JPEG硬解 → HDMI TX │
└─────────┬──────────┘
│ HDMI
┌─────────▼──────────┐
│ 外接显示器 │
│ 496×1088 竖屏 │
└────────────────────┘
二、核心链路:AM8268N USB转HDMI
2.1 AM8268N 芯片
| 参数 | 规格 |
|---|---|
| 厂商 | 炬力北方 (Actions Micro) |
| 封装 | QFN68, 内置512Mb DDR2 |
| 输入 | USB 2.0 (Host端) |
| 输出 | HDMI 1.2, 最高 1080P@60Hz |
| 协议 | DisplayPass Protocol (私有) |
| 解码能力 | JPEG硬解 + H.264硬解 |
| Android支持 | 9.0+ (官网确认) |
2.2 数据传输原理
AM8268N 不走标准HDMI Alt Mode,也不走DisplayPort over USB-C。它的工作方式是:
Android Host AM8268N
│ │
│ 逐帧 JPEG 压缩 (软件/硬件) │
│ ──────────────────────────────► │
│ USB Bulk Transfer (帧数据) │ JPEG硬解码
│ ──────────────────────────────► │
│ DisplayPass 控制指令 │ HDMI TX → 显示器
│ ──────────────────────────────► │
│ │
关键特性:
- 不是像素流:传输的是压缩后的JPEG帧(通常30-50KB/帧),而非原始RGB像素
- 不是视频流:是逐帧非连续传输,由Host端按需推送
- USB 2.0 带宽足够:30fps × 50KB = 1.5MB/s,USB 2.0实际带宽约30MB/s,余量充足
- 延迟可控:JPEG压缩→USB传输→硬解→HDMI输出,总延迟约30-80ms
三、MiraPlug 的角色
3.1 包信息
包名: com.actionsmicro.usbdisplay
UID: 10107
核心Activity:
- MainActivity : 用户交互界面(开始/停止投屏)
- ProjectionActivity : 屏幕捕获服务(MediaProjection)
- UsbDeviceActivity : USB设备连接处理
3.2 启动流程(从logcat追踪得出)
1. 用户点击"开始投屏"
│
2. MainActivity → start ProjectionActivity
│
3. 系统弹出 MediaProjectionPermissionActivity(Android安全机制)
"是否允许录制屏幕?" → 用户点"立即开始"
│
4. ProjectionActivity 获得 MediaProjection 授权
│
5. USB_DEVICE_ATTACHED 广播 → 检测AM8268N
│
6. createVirtualDisplay("SCREENCAST_VIRTUAL", 496×1088, FLAG_PRESENTATION)
│
7. Display N 注册到系统,开始投屏
3.3 VirtualDisplay 创建细节
从 dumpsys display 分析,MiraPlug 创建的VirtualDisplay关键参数:
DisplayManager.createVirtualDisplay(
"SCREENCAST_VIRTUAL", // name
496, 1088, // width, height
240, // densityDpi (hdpi)
targetSurface, // 输出Surface → USB发送
VIRTUAL_DISPLAY_FLAG_PRESENTATION // 支持Presentation API
);
重要发现:Display ID 是动态的
| 状态 | Display ID | 原因 |
|---|---|---|
| MiraPlug未运行 | 无虚拟Display | VirtualDisplay未创建 |
| 第一次启动投屏 | Display 4 | 系统按序分配 |
| 重新启动投屏 | Display 6 | 销毁旧的,创建新的,ID递增 |
| 再重启 | Display 8… | 持续递增 |
因此应用中不能硬编码 Display ID,必须通过 DisplayManager.DISPLAY_CATEGORY_PRESENTATION 动态查找。
四、双屏异显的核心:Android Presentation API
4.1 原理
Presentation 是 Android 4.2 (API 17) 引入的API,允许在非主屏上显示独立窗口:
class SecondScreenPresentation(context: Context, display: Display)
: Presentation(context, display) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.second_screen) // 独立的布局
}
}
4.2 渲染管线
┌───────────────────┐ ┌───────────────────┐
│ Display 0 │ │ Display N │
│ layerStack=0 │ │ layerStack=N │
│ │ │ │
│ MainActivity │ │ Presentation │
│ (Window #1) │ │ (Window #1) │
│ ↓ │ │ ↓ │
│ SurfaceFlinger │ │ SurfaceFlinger │
│ (layerStack 0) │ │ (layerStack N) │
│ ↓ │ │ ↓ │
│ 物理屏幕Surface │ │ VirtualDisplay │
│ │ │ Surface │
└───────────────────┘ └────────┬──────────┘
│
MiraPlug 读取此Surface
→ JPEG压缩 → USB → AM8268N → HDMI
关键点:
- Display 0 和 Display N 各自拥有独立的 layerStack
- SurfaceFlinger 在每个 layerStack 上独立合成
- Presentation 的 UI 渲染到 VirtualDisplay 的 Surface 上
- MiraPlug 读取这个 Surface 的内容发送到 AM8268N
4.3 为什么 mHasContent=false 也能工作?
dumpsys display 显示 VirtualDisplay 的 mHasContent=false。这是因为 MiraPlug 不通过 layerStack 渲染镜像内容(它用 MediaProjection 直接抓主屏 Surface),但 Presentation 窗口直接渲染到 VirtualDisplay 的 Surface,SurfaceFlinger 在独立的 layerStack 上合成,MiraPlug 读取的是合成后的最终 Surface 输出。
4.4 FLAG_PRESENTATION 的作用
VIRTUAL_DISPLAY_FLAG_PRESENTATION
- 使 Display 出现在
DisplayManager.DISPLAY_CATEGORY_PRESENTATION分类中 - 允许在该 Display 上创建 Presentation 窗口
- Presentation 的 Context 自动关联到此 Display
- 资源(metrics, density)基于此 Display 配置
五、关键实现代码
5.1 动态查找副屏
val dm = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
val presentationDisplays = dm.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION)
// 优先匹配包含"SCREENCAST"的(MiraPlug注册的)
val externalDisplay = presentationDisplays.firstOrNull {
it.name?.contains("SCREENCAST") == true
} ?: presentationDisplays.firstOrNull()
5.2 监听 Display 变化
dm.registerDisplayListener(object : DisplayManager.DisplayListener {
override fun onDisplayAdded(displayId: Int) {
// MiraPlug启动 → 重新扫描
scanDisplays()
}
override fun onDisplayRemoved(displayId: Int) {
// MiraPlug断开 → 关闭Presentation
if (externalDisplay?.displayId == displayId) {
dismissPresentation()
}
}
override fun onDisplayChanged(displayId: Int) {}
}, null)
5.3 显示/关闭副屏
fun showOnExternal() {
presentation = SecondScreenPresentation(context, externalDisplay!!)
presentation?.show()
}
fun hideExternal() {
presentation?.dismiss()
presentation = null
}
六、与传统方案对比
| 维度 | 传统HDMI Alt Mode | AM8268N + Presentation |
|---|---|---|
| 物理接口 | USB-C DP Alt Mode / 独立HDMI口 | 任意USB 2.0口 |
| SoC要求 | 需原生HDMI或DP输出 | 无要求(USB即可) |
| 成本 | HDMI控制器+连接器 | AM8268N ¥8~12 |
| 传输方式 | 原始像素流 | JPEG压缩帧 |
| 分辨率支持 | 4K@60 (DP1.4) | 1080P@60 (HDMI 1.2) |
| 延迟 | <1ms | 30-80ms |
| 适用场景 | 视频播放、游戏 | 静态/半静态内容(营业厅叫号等) |
| 双屏异显 | 原生支持 | Presentation API |
| 驱动复杂度 | Kernel层Display驱动 | 用户态APK即可 |
七、适用场景与限制
适用场景
- 营业厅叫号系统(主屏操作,副屏显示号码)
- POS收银(主屏收银,副屏显示客户信息)
- 信息展示(主屏管理,副屏展示公告/广告)
- 身份核验(主屏录入,副屏显示比对结果)
限制
- 副屏不支持触摸输入(AM8268N 无触控回传通道)
- 动态画面(视频、动画)质量受JPEG压缩影响
- 延迟30-80ms,不适合实时交互
- 依赖MiraPlug进程运行(VirtualDisplay由它创建)
- AM8268N 无音频通道
八、实测数据
| 项目 | 值 |
|---|---|
| POS系统 | Android 13 (API 33) |
| 主屏分辨率 | 720×1600, 240dpi |
| 副屏分辨率 | 496×1088, 240dpi |
| 副屏类型 | VIRTUAL, FLAG_PRESENTATION |
| 副屏owner | com.actionsmicro.usbdisplay (uid=10107) |
| 连接方式 | POS 触点底座 → USB Hub → AM8268N → HDMI |
| ADB连接 | WiFi: 192.168.31.54:5555 |
| 编译环境 | Android SDK 36, Gradle 8.13, JDK 21 |
| 工程路径 | android_demo\DualScreenTest |
| APK大小 | ~23MB (Debug, 含Compose依赖) |
文档生成日期: 2026-06-09

1089

被折叠的 条评论
为什么被折叠?



