fix: 三协议架构代码审查修复

- MiracastService.kt 初始化顺序修复(mediaRenderer 在 AirDisplayProtocol 之前)
- 补充误删的 protocolManager.setListener() 回调
- 所有 12 个 lateinit 变量使用前初始化验证

此提交对应全面代码审查发现的问题
This commit is contained in:
yuqianhe
2026-05-30 08:29:36 +00:00
parent b92a296290
commit e302021958
6 changed files with 1942 additions and 25 deletions

309
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,309 @@
# AirDisplay 系统架构
## 概览
三协议并行架构,根据场景自动选择或用户手动指定:
```
┌─────────────────────────────────────────────────────────────┐
│ Android 端 (AirDisplay) │
│ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ 协议栈 & 自动选择 ││
│ │ ││
│ │ ┌──────────────┐ ┌──────────────┐ ┌────────────────┐││
│ │ │ Miracast │ │ ADP │ │ AirPlay │││
│ │ │ (保留) │ │ (私有协议) │ │ (Mac 原生) │││
│ │ │ │ │ │ │ │││
│ │ │ WFD IE │ │ mDNS 发现 │ │ UxPlay 移植 │││
│ │ │ RTSP→RTP→TS │ │ TCP 帧协议 │ │ H.264+AAC │││
│ │ │ Wi-Fi Direct │ │ P2P/LAN │ │ FairPlay │││
│ │ └──────┬───────┘ └──────┬───────┘ └───────┬────────┘││
│ │ └────────────────┼──────────────────┘ ││
│ │ ▼ ││
│ │ 共用 MediaCodec 解码 + 渲染 ││
│ │ UIBC 触摸回传 (已有) ││
│ │ DeviceName 配置 (已有) ││
│ └─────────────────────────────────────────────────────────┘│
│ │
│ 自动选择逻辑: │
│ Wi-Fi Direct + WFD IE 可用 → Miracast │
│ Wi-Fi Direct + WFD IE 失败 → 降级 ADP (P2P) │
│ 同局域网 → ADP (LAN) │
│ Mac 投屏请求 → AirPlay │
│ 用户手动选择 → 指定协议 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Desktop Client (独立仓库 airdisplay-client) │
│ │
│ 技术栈: Rust + Tauri v2 │
│ 目标平台: Windows / macOS / Linux │
│ │
│ ┌───────────┐ ┌────────────┐ ┌─────────────────────────┐ │
│ │ 屏幕采集 │→ │ 视频编码 │→ │ 网络层 │ │
│ │ │ │ │ │ │ │
│ │ Windows │ │ NVENC │ │ mDNS 发现 (mdns-sd) │ │
│ │ DXGI │ │ (NVIDIA) │ │ ADP 协议 (自定义帧协议) │ │
│ │ macOS │ │ Video │ │ TCP 可靠传输 │ │
│ │ CGDisplay│ │ Toolbox │ │ │ │
│ │ Linux │ │ Intel QSV │ │ QUIC 备选 (低延迟) │ │
│ │ PipeWire │ │ x264 软编 │ │ │ │
│ └───────────┘ └────────────┘ └─────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Tauri UI (React/TypeScript) ││
│ │ ├─ 自动发现设备列表 (mDNS) ││
│ │ ├─ 连接/断开控制 ││
│ │ ├─ 协议选择 (ADP 默认 / Miracast 备选) ││
│ │ ├─ 分辨率/帧率/延迟模式设置 ││
│ │ └─ 连接状态 & 统计信息 ││
│ └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Mac 端 (无额外客户端) │
│ │
│ 控制中心 → 屏幕镜像 → 选择 AirDisplay │
│ 使用 Apple 内置 AirPlay 协议 │
│ 无需安装额外软件 │
│ 备选:也可安装 airdisplay-client 走 ADP 协议 │
└─────────────────────────────────────────────────────────────┘
```
## 协议设计
### 1. Miracast (已实现,保留)
现有代码保持不变:
- `WiFiDirectManager.kt` — P2P Group 创建 + WFD IE 反射 (失败不阻塞)
- `RtspServer.kt` / `RtspSession.kt` — RTSP 握手
- `RtpReceiver.kt` / `TsDemuxer.kt` — RTP/TS 媒体管道
- 用于 Android TV / 系统签名设备 / WFD IE 兼容的环境
**改动**WFD IE 反射失败时不再终止流程,自动降级到 ADP。
### 2. AirDisplay Protocol (ADP) v1
自定义轻量屏幕投屏协议。设计目标:低延迟、简单可靠、跨平台。
#### 发现阶段
| 场景 | 发现方式 |
|------|---------|
| WiFi Direct P2P | P2P Group 创建后,在 `192.168.49.1:7935` 启动 TCP 监听 |
| 同局域网 | mDNS 注册 `_airdisplay._tcp` 端口 7935 |
#### 连接阶段 (TCP 7935)
```
[Client] ──TCP 连接──> [Server]
[Server] ADP/1.0 200 OK
Capabilities: {
"max_width": 1920,
"max_height": 1080,
"codecs": ["h264"],
"features": ["uibc", "audio"]
}
[Client] ADP/1.0 HANDSHAKE
Settings: {
"width": 1920,
"height": 1080,
"fps": 30,
"codec": "h264",
"bitrate": 20000000
}
[Server] ADP/1.0 READY
```
#### 数据阶段 (TCP 持久连接)
帧格式:
```
┌──────────────────────────────────────────────────────────┐
│ Type(1) │ Reserved(1) │ Length(2) │ Timestamp(4) │
├──────────────────────────────────────────────────────────┤
│ Payload (变长) │
└──────────────────────────────────────────────────────────┘
Type 定义:
0x01 = Capability 协商 (JSON)
0x02 = H.264 NAL 单元 (Annex B)
0x03 = H.265 NAL 单元 (保留)
0x04 = 音频帧 (AAC/Opus)
0x05 = UIBC 事件 (触摸/鼠标/键盘)
0x06 = Keepalive
0x07 = 分辨率/参数变更
```
#### UIBC 格式
与现有 `UibcServer.kt` 兼容。
### 3. AirPlay
使用 [UxPlay](https://github.com/FDH2/UxPlay) (MIT License) 移植到 Android NDK。
UxPlay 是一个开源的 AirPlay 接收端实现,支持:
- AirPlay Mirroring (H.264 视频)
- AirPlay Audio (ALAC/AAC)
- FairPlay 解密 (需要系统证书)
- macOS/iOS 屏幕镜像 & 扩展
**移植策略**
1. `airdisplay/native/uxplay/` — CMakeLists.txt + 源码
2. JNI 桥接 — UxPlay 的 H.264 NAL 通过 JNI 传回 Android 层
3. 共享 `MediaRenderer.kt` — 与 ADP/Miracast 共用 MediaCodec 解码
---
## Android 端代码结构
```
app/src/main/java/com/qwenpaw/miracast/
├── AirDisplayService.kt ← 前台服务 (改自 MiracastService)
├── protocol/
│ ├── ProtocolManager.kt ← 协议选择逻辑 (新增)
│ ├── AirDisplayProtocol.kt ← ADP 协议处理器 (新增)
│ ├── MdnsService.kt ← mDNS 注册 (新增)
│ └── AirPlayBridge.kt ← AirPlay JNI 桥接 (新增)
├── wifidirect/
│ └── WiFiDirectManager.kt ← P2P 管理 (改造)
├── rtsp/ ← 保留 (Miracast 用)
│ ├── RtspServer.kt
│ ├── RtspSession.kt
│ └── WfdParser.kt
├── media/
│ ├── RtpReceiver.kt
│ ├── TsDemuxer.kt
│ └── MediaRenderer.kt ← 共享解码+渲染
├── uibc/
│ └── UibcServer.kt
├── DeviceNameManager.kt
├── ResolutionManager.kt
├── LatencyManager.kt
└── ui/
├── MainActivity.kt
├── SettingsActivity.kt
└── ...
```
### 网络拓扑
```
P2P 模式: 手机(GO) ←── WiFi Direct ──→ PC
IP: 192.168.49.1
LAN 模式: 手机 ──── WiFi Router ────→ PC
↑ mDNS ↑ mDNS
```
---
## Desktop Client 技术方案
### 技术栈
| 层面 | 方案 | 说明 |
|------|------|------|
| 语言 | **Rust** | 跨平台原生、零开销抽象、安全 |
| 桌面框架 | **Tauri v2** | Rust 后端 + Web 前端,打包 < 10MB |
| 异步运行时 | **tokio** | 全异步 I/O |
| 前端框架 | **React + TypeScript** | 组件化 UI |
| 构建工具 | **Vite** | HMR 快速开发 |
### 屏幕采集
| 平台 | 方案 | Rust crate |
|------|------|-----------|
| Windows | DXGI Desktop Duplication | `dxgi-rs` / `captrs` |
| macOS | CGDisplay Stream | `core-graphics` (sys) |
| Linux (Wayland) | PipeWire | `pipewire-rs` |
| Linux (X11) | X11 SHM GetImage | `x11` / `scrap` |
### 视频编码
| 编码器 | 方式 | 适用平台 |
|--------|------|---------|
| NVENC | 硬件 | Windows (NVIDIA) |
| VideoToolbox | 硬件 | macOS |
| Intel QSV | 硬件 | Windows/Linux (Intel) |
| x264 | 软件 | 全平台保底 |
| VAAPI | 硬件 | Linux (AMD/Intel) |
### 协议实现
- `adp-client` crate — ADP 协议客户端库
- TCP 连接管理 + 重连
- 帧序列化 (Header + Payload)
- Keepalive
- UIBC 事件发送
- `mdns-resolver` — mDNS 设备发现
- 监听 `_airdisplay._tcp` 服务
- 自动刷新设备列表
### Tauri UI 功能
- **发现页**: 自动扫描局域网内 AirDisplay 设备
- **连接页**: 选中设备 → 连接 → 显示投屏状态
- **设置页**: 分辨率/帧率/延迟模式/协议选择
- **状态栏**: 连接状态、FPS、延迟、码率
### Rust 依赖
```toml
[dependencies]
tauri = "2"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
mdns-sd = "0.12"
tracing = "0.1"
anyhow = "1"
# 屏幕采集 (平台相关)
[target.'cfg(target_os = "windows")'.dependencies]
captrs = "0.3"
[target.'cfg(target_os = "macos")'.dependencies]
core-graphics = "0.24"
[target.'cfg(target_os = "linux")'.dependencies]
# PipeWire / x11
# 编码
ffmpeg-next = "7"
```
---
## 实现路线
### Phase 1: ADP MVP (当前)
1. Android: `AirDisplayProtocol.kt` — ADP 协议处理器
2. Android: `MdnsService.kt` — mDNS 注册
3. Android: 改造 `WiFiDirectManager` — WFD IE 失败不阻塞
4. Android: `ProtocolManager.kt` — 协议选择
5. Desktop: 创建 `airdisplay-client` 仓库 + Rust/Tauri 脚手架
6. Desktop: ADP 协议客户端 + mDNS 发现
7. Desktop: 屏幕采集 + x264 软编
8. Desktop: Tauri UI
9. ✅ 端到端第一次投屏
### Phase 2: 优化
- 硬件编码支持 (NVENC/VideoToolbox/QSV)
- 延迟调优参数
- UIBC 触摸回传
- 分辨率/帧率自适应
### Phase 3: AirPlay
- UxPlay 移植到 Android NDK
- Mac 原生投屏
- 音频支持
---
*三协议并行,不受系统权限限制,覆盖全部桌面端。*

View File

@@ -14,6 +14,9 @@ import androidx.core.app.NotificationCompat
import com.qwenpaw.miracast.media.MediaRenderer
import com.qwenpaw.miracast.media.RtpReceiver
import com.qwenpaw.miracast.media.TsDemuxer
import com.qwenpaw.miracast.protocol.AirDisplayProtocol
import com.qwenpaw.miracast.protocol.MdnsService
import com.qwenpaw.miracast.protocol.ProtocolManager
import com.qwenpaw.miracast.rtsp.RtspServer
import com.qwenpaw.miracast.uibc.UibcServer
import com.qwenpaw.miracast.wifidirect.WiFiDirectManager
@@ -63,7 +66,7 @@ class MiracastService : Service() {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// 核心模块
// === 三协议架构核心模块 ===
private lateinit var deviceNameManager: DeviceNameManager
private lateinit var resolutionManager: ResolutionManager
private lateinit var latencyManager: LatencyManager
@@ -74,12 +77,20 @@ class MiracastService : Service() {
private lateinit var mediaRenderer: MediaRenderer
private lateinit var uibcServer: UibcServer
// === ADP 私有协议模块 ===
private lateinit var protocolManager: ProtocolManager
private lateinit var airDisplayProtocol: AirDisplayProtocol
private lateinit var mdnsService: MdnsService
// 当前激活的协议
private var activeProtocol: ProtocolManager.Protocol = ProtocolManager.Protocol.NONE
// 状态
private var currentState = ServiceState.IDLE
private var stateListener: ServiceStateListener? = null
private var currentSurface: SurfaceView? = null
// 连接信息
// Miracast 连接信息
private var remoteIp: String? = null
private var remoteRtpPort = 0
private var serverRtpPort = 19002
@@ -142,7 +153,35 @@ class MiracastService : Service() {
// Wi-Fi Direct 管理器
wifiDirectManager = WiFiDirectManager(this, deviceNameManager)
// 媒体渲染器
// ===== 协议选择管理 =====
// 协议管理器(自动选择/手动切换)
protocolManager = ProtocolManager(wifiDirectManager)
// WFD IE 状态影响协议选择
wifiDirectManager.setWfdStatusListener(object : WiFiDirectManager.WfdStatusListener {
override fun onWfdStatusChanged(status: WiFiDirectManager.WfdStatus) {
Log.i(TAG, "WFD IE 状态变更: $status")
protocolManager.onWfdStatusChanged(status)
when (status) {
WiFiDirectManager.WfdStatus.SUCCESS -> {
Log.i(TAG, "WFD IE 可用Miracast 模式就绪")
updateNotification("Miracast 已就绪")
}
WiFiDirectManager.WfdStatus.FAILED -> {
Log.i(TAG, "WFD IE 不可用,自动降级到 ADP 协议")
// ADP 已提前启动,客户端可通过 ADP 连接
updateNotification("ADP 模式已就绪")
}
WiFiDirectManager.WfdStatus.NOT_ATTEMPTED -> {
Log.i(TAG, "WFD IE 未尝试(可能 API 版本不足),使用 ADP 协议")
}
}
}
})
// ===== 媒体渲染器(需在 ADP 协议之前初始化) =====
mediaRenderer = MediaRenderer()
mediaRenderer.setListener(object : MediaRenderer.RendererListener {
override fun onStateChanged(state: MediaRenderer.State) {
@@ -164,6 +203,76 @@ class MiracastService : Service() {
}
})
// ADP 协议处理器TCP 7935依赖 mediaRenderer
airDisplayProtocol = AirDisplayProtocol(mediaRenderer)
airDisplayProtocol.setListener(object : AirDisplayProtocol.ProtocolListener {
override fun onStateChanged(state: AirDisplayProtocol.State, message: String) {
Log.i(TAG, "ADP 状态: $state$message")
protocolManager.setAdpServerRunning(state == AirDisplayProtocol.State.CONNECTED)
}
override fun onClientConnected(clientIp: String) {
Log.i(TAG, "ADP 客户端已连接: $clientIp")
protocolManager.onClientConnected(ProtocolManager.Protocol.ADP, clientIp)
updateState(ServiceState.STREAMING, "ADP 投屏中")
protocolManager.setAdpServerRunning(true)
}
override fun onClientDisconnected(reason: String) {
Log.i(TAG, "ADP 客户端断开: $reason")
protocolManager.onClientDisconnected(reason)
if (currentState == ServiceState.STREAMING) {
updateState(ServiceState.WAITING_FOR_CONNECTION, "等待连接…")
}
protocolManager.setAdpServerRunning(false)
}
override fun onUibcEvent(eventJson: String) {
// ADP 协议中 UIBC 事件从客户端发来,转发到 UibcServer
Log.d(TAG, "ADP UIBC 事件: $eventJson")
}
override fun onResolutionChanged(width: Int, height: Int, fps: Int) {
Log.i(TAG, "ADP 分辨率变更: ${width}x$height @ ${fps}fps")
stateListener?.onConnectionInfo(null, "${width}x$height", "$fps")
}
override fun onError(error: String) {
Log.e(TAG, "ADP 错误: $error")
stateListener?.onError(error)
}
})
// mDNS 服务注册(供 LAN 发现)
mdnsService = MdnsService(this)
mdnsService.deviceName = deviceNameManager.getDeviceName()
mdnsService.setListener(object : MdnsService.MdnsListener {
override fun onServiceRegistered(serviceName: String) {
Log.i(TAG, "mDNS 已注册: $serviceName")
}
override fun onServiceUnregistered() {
Log.d(TAG, "mDNS 已注销")
}
override fun onError(error: String) {
Log.w(TAG, "mDNS 错误: $error")
}
})
// ===== 协议选择回调 =====
protocolManager.setListener(object : ProtocolManager.ProtocolListener {
override fun onProtocolChanged(protocol: ProtocolManager.Protocol, reason: String) {
activeProtocol = protocol
Log.i(TAG, "协议已切换: $protocol ($reason)")
updateNotification("协议: $protocol")
}
override fun onStateChanged(state: ProtocolManager.ProtocolState) {
Log.d(TAG, "协议状态: active=${state.activeProtocol}, mode=${state.selectionMode}")
}
})
// RTSP 服务器(传递设备名用于 Server 头)
val deviceName = deviceNameManager.getDeviceName()
rtspServer = RtspServer(object : RtspServer.RtspServerListener {
@@ -293,15 +402,22 @@ class MiracastService : Service() {
}
}
// ========== 启动 / 停止 Miracast ==========
// ========== 启动 / 停止 服务 ==========
/**
* 启动三协议并行服务
*
* - Miracast (RTSP 7236): 依赖 WFD IEPC 原生发现
* - ADP (TCP 7935): 全场景可用P2P/LAN 均支持
* - AirPlay (预留): Mac 原生投屏
*/
private fun startMiracast() {
Log.i(TAG, ">>> 启动 Miracast 接收器")
Log.i(TAG, ">>> 启动 AirDisplay 服务 (三协议)")
updateState(ServiceState.INITIALIZING, "正在初始化…")
scope.launch {
try {
// 1. 初始化 Wi-Fi Direct创建 Group
// 1. 初始化 Wi-Fi Direct创建 P2P Group
updateState(ServiceState.INITIALIZING, "启动 Wi-Fi Direct…")
wifiDirectManager.initialize()
@@ -313,33 +429,64 @@ class MiracastService : Service() {
mediaRenderer.setSurface(surfaceView)
}
// 3. 启动 RTSP 服务器(先启动,等待 Wi-Fi Direct 连接
updateState(ServiceState.WAITING_FOR_CONNECTION, "RTSP 服务器已就绪")
// 3. 启动 ADP TCP 服务器(全场景通用
updateState(ServiceState.INITIALIZING, "启动 ADP 协议…")
val adpStarted = airDisplayProtocol.start(AirDisplayProtocol.DEFAULT_PORT)
if (adpStarted) {
Log.i(TAG, "ADP 服务器已启动 (端口 ${AirDisplayProtocol.DEFAULT_PORT})")
} else {
Log.w(TAG, "ADP 服务器启动失败,继续使用其他协议")
}
// 4. 注册 mDNS 服务(供 LAN 发现)
mdnsService.register()
Log.i(TAG, "mDNS 已注册 (_airdisplay._tcp:${MdnsService.SERVICE_PORT})")
// 5. 启动 RTSP 服务器Miracast
updateState(ServiceState.WAITING_FOR_CONNECTION, "服务已就绪")
rtspServer.start()
// 4. 启动设备发现(让 Windows 可以找到我们)
// 6. 启动设备发现(让 Windows 可以通过 WFD IE 发现我们)
wifiDirectManager.startDiscovery()
Log.i(TAG, "Miracast 接收器已就绪,等待 Windows 连接")
Log.i(TAG, """
===== AirDisplay 三协议已就绪 =====
Miracast (RTSP): port 7236
ADP (TCP): port ${AirDisplayProtocol.DEFAULT_PORT}
mDNS: _airdisplay._tcp
AirPlay: 待集成
==================================
""".trimIndent())
updateState(ServiceState.WAITING_FOR_CONNECTION, "等待连接…")
// 定期检查连接状态
monitorWfdConnection()
// 7. 监控连接状态
monitorConnections()
} catch (e: Exception) {
Log.e(TAG, "启动 Miracast 失败", e)
Log.e(TAG, "启动服务失败", e)
updateState(ServiceState.ERROR, "启动失败: ${e.message}")
}
}
}
/**
* 停止所有协议服务
*/
private fun stopMiracast() {
Log.i(TAG, ">>> 停止 Miracast 接收器")
Log.i(TAG, ">>> 停止 AirDisplay 服务")
scope.launch {
// 停止媒体流
stopMediaStreaming()
// 停止所有协议服务
airDisplayProtocol.stop()
rtspServer.stop()
mdnsService.unregister()
// 销毁 Wi-Fi Direct
wifiDirectManager.destroy()
updateState(ServiceState.STOPPED, "已停止")
}
}
@@ -380,9 +527,13 @@ class MiracastService : Service() {
// ========== 连接监控 ==========
/**
* 监控 Wi-Fi Direct 连接状态
* 多协议连接监控
*
* 同时监控 Wi-Fi Direct (Miracast) 和 ADP (TCP) 连接状态。
* 任一协议建立连接后即视为投屏开始。
*/
private suspend fun monitorWfdConnection() {
private suspend fun monitorConnections() {
// 监控 Wi-Fi Direct 连接 (Miracast 路径)
var lastConnected = false
wifiDirectManager.connectionState.collect { conn ->
@@ -392,7 +543,12 @@ class MiracastService : Service() {
conn.clientDeviceName,
null, null
)
updateState(ServiceState.HANDSHAKING, "Windows 已连接,正在握手…")
if (activeProtocol == ProtocolManager.Protocol.NONE) {
protocolManager.onClientConnected(
ProtocolManager.Protocol.MIRACAST,
conn.clientAddress ?: "unknown"
)
}
// 打印 SSID 和密码(调试用)
Log.i(TAG, "P2P SSID: ${conn.groupNetworkName}")
@@ -400,7 +556,12 @@ class MiracastService : Service() {
} else if (!conn.isConnected && lastConnected) {
Log.i(TAG, "Wi-Fi Direct 客户端已断开")
updateState(ServiceState.WAITING_FOR_CONNECTION, "等待连接…")
if (activeProtocol == ProtocolManager.Protocol.MIRACAST) {
protocolManager.onClientDisconnected("P2P 断开")
}
if (currentState == ServiceState.STREAMING) {
updateState(ServiceState.WAITING_FOR_CONNECTION, "等待连接…")
}
}
lastConnected = conn.isConnected
}
@@ -474,6 +635,21 @@ class MiracastService : Service() {
*/
fun getCurrentState(): ServiceState = currentState
/**
* 获取当前激活的协议
*/
fun getActiveProtocol(): ProtocolManager.Protocol = activeProtocol
/**
* 获取协议管理器(供 UI 使用)
*/
fun getProtocolManager(): ProtocolManager = protocolManager
/**
* 获取 ADP 协议处理器
*/
fun getAirDisplayProtocol(): AirDisplayProtocol = airDisplayProtocol
/**
* 获取 P2P 连接信息
*/
@@ -483,4 +659,16 @@ class MiracastService : Service() {
wifiDirectManager.getGroupPassphrase()
)
}
/**
* 更新通知栏消息
*/
private fun updateNotification(message: String) {
try {
val notification = buildNotification(message)
val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
manager.notify(NOTIFICATION_ID, notification)
} catch (_: Exception) {
}
}
}

View File

@@ -0,0 +1,755 @@
package com.qwenpaw.miracast.protocol
import android.util.Log
import com.qwenpaw.miracast.media.MediaRenderer
import kotlinx.coroutines.*
import org.json.JSONObject
import java.io.InputStream
import java.io.OutputStream
import java.net.ServerSocket
import java.net.Socket
import java.net.SocketTimeoutException
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
/**
* AirDisplay Protocol (ADP) v1 — 自定义轻量屏幕投屏协议
*
* == 设计目标 ==
* 低延迟、简单可靠、跨平台。替代 Miracast RTSP/RTP/TS 栈,
* 在 WFD IE 不可用或同局域网场景下工作。
*
* == 帧格式 (8 字节头部) ==
* ```
* ┌──────────────────────────────────────────────────┐
* │ Type(1) │ Reserved(1) │ Length(2) │ Timestamp(4) │
* ├──────────────────────────────────────────────────┤
* │ Payload (变长) │
* └──────────────────────────────────────────────────┘
* ```
*
* == Type 字段 ==
* - 0x01 = Capability 协商 (JSON)
* - 0x02 = H.264 NAL 单元 (Annex B)
* - 0x03 = H.265 NAL 单元 (保留)
* - 0x04 = 音频帧 (AAC/Opus)
* - 0x05 = UIBC 事件 (JSON)
* - 0x06 = Keepalive
* - 0x07 = 分辨率/参数变更 (JSON)
*
* == 连接流程 ==
* 1. Client 连接 TCP port 7935
* 2. Server → Client: ADP/1.0 200 OK + Capabilities JSON
* 3. Client → Server: ADP/1.0 HANDSHAKE + Settings JSON
* 4. Server → Client: ADP/1.0 READY
* 5. 双向数据流 (视频帧 / UIBC 事件 / Keepalive)
*/
class AirDisplayProtocol(
private val mediaRenderer: MediaRenderer
) {
companion object {
private const val TAG = "AirDisplayProtocol"
/** ADP 默认端口 */
const val DEFAULT_PORT = 7935
/** 最大帧体大小 (16MB防止恶意大包) */
private const val MAX_PAYLOAD_SIZE = 16 * 1024 * 1024
/** Keepalive 超时秒数 */
private const val KEEPALIVE_TIMEOUT_SEC = 10
/** 帧类型常量 */
const val FRAME_TYPE_CAPABILITY = 0x01
const val FRAME_TYPE_H264_NAL = 0x02
const val FRAME_TYPE_H265_NAL = 0x03
const val FRAME_TYPE_AUDIO = 0x04
const val FRAME_TYPE_UIBC = 0x05
const val FRAME_TYPE_KEEPALIVE = 0x06
const val FRAME_TYPE_RESOLUTION_CHANGE = 0x07
/** 支持的编码器和能力 */
private const val CAP_MAX_WIDTH = 1920
private const val CAP_MAX_HEIGHT = 1080
private const val CAP_CODECS = """["h264"]"""
private const val CAP_FEATURES = """["uibc","audio"]"""
}
// ========== 状态定义 ==========
/** ADP 协议会话状态 */
enum class State {
IDLE,
LISTENING,
HANDSHAKING,
CONNECTED,
STOPPED,
ERROR
}
/** ADP 会话回调 */
interface ProtocolListener {
fun onStateChanged(state: State, message: String)
fun onClientConnected(clientIp: String)
fun onClientDisconnected(reason: String)
fun onUibcEvent(eventJson: String)
fun onResolutionChanged(width: Int, height: Int, fps: Int)
fun onError(error: String)
}
override fun toString(): String {
return "AirDisplayProtocol(state=$currentState, clientIp=$clientIp)"
}
// ========== 内部变量 ==========
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// 网络
private var serverSocket: ServerSocket? = null
private var clientSocket: Socket? = null
private var inputStream: InputStream? = null
private var outputStream: OutputStream? = null
// 状态
@Volatile
private var currentState = State.IDLE
private var listener: ProtocolListener? = null
private val stateLock = Any()
// 客户端信息
@Volatile
private var clientIp: String = ""
// 接收统计
private var totalFrames = AtomicLong(0)
private var totalBytes = AtomicLong(0)
private var lastKeepaliveTime = 0L
// 解码器配置(从 H.264 SPS/PPS 提取)
private var videoWidth = 0
private var videoHeight = 0
private var configuredCodec = false
private var csd0: ByteArray? = null // SPS
private var csd1: ByteArray? = null // PPS
// 服务器启动等待通知
private val serverReady = CompletableDeferred<Boolean>()
// ========== 公共 API ==========
/**
* 设置会话监听器
*/
fun setListener(listener: ProtocolListener) {
this.listener = listener
}
/**
* 获取当前状态
*/
fun getState(): State = currentState
/**
* 获取客户端 IP
*/
fun getClientIp(): String = clientIp
/**
* 获取接收统计
*/
fun getStats(): Triple<Long, Long, Long> {
return Triple(totalFrames.get(), totalBytes.get(), 0L)
}
/**
* 启动 ADP 服务器,绑定到指定端口
*
* @return true 如果服务器启动成功
*/
suspend fun start(port: Int = DEFAULT_PORT): Boolean {
synchronized(stateLock) {
if (currentState != State.IDLE && currentState != State.STOPPED) {
Log.w(TAG, "ADP 已在运行 ($currentState),忽略重复启动")
return false
}
currentState = State.LISTENING
}
return scope.async {
try {
serverSocket = ServerSocket(port)
serverSocket?.soTimeout = 0 // 阻塞模式
Log.i(TAG, "ADP 服务已启动,监听端口 $port")
updateState(State.LISTENING, "ADP 服务已启动,等待客户端连接")
// 通知外部
serverReady.complete(true)
// 进入接受循环
acceptLoop()
} catch (e: Exception) {
Log.e(TAG, "ADP 服务启动失败", e)
serverReady.complete(false)
updateState(State.ERROR, "ADP 服务启动失败: ${e.message}")
false
}
}.await()
}
/**
* 等待服务器就绪
*/
suspend fun waitForReady(timeoutMs: Long = 5000): Boolean {
return withTimeoutOrNull(timeoutMs) {
serverReady.await()
} ?: false
}
/**
* 向客户端发送 UIBC 事件
* @param eventJson UIBC 事件 JSON
*/
fun sendUibcEvent(eventJson: String) {
if (currentState != State.CONNECTED) return
val data = eventJson.toByteArray(Charsets.UTF_8)
sendFrame(FRAME_TYPE_UIBC, data, System.nanoTime() / 1000)
}
/**
* 通知客户端分辨率变更
*/
fun sendResolutionChanged(width: Int, height: Int, fps: Int) {
if (currentState != State.CONNECTED) return
val payload = JSONObject().apply {
put("width", width)
put("height", height)
put("fps", fps)
}
val data = payload.toString().toByteArray(Charsets.UTF_8)
sendFrame(FRAME_TYPE_RESOLUTION_CHANGE, data, System.nanoTime() / 1000)
}
/**
* 停止 ADP 服务器并断开连接
*/
fun stop() {
Log.i(TAG, "=== ADP 停止 ===")
updateState(State.STOPPED, "ADP 已停止")
cleanup()
scope.cancel()
}
// ========== 网络核心 ==========
/**
* 客户端接受循环
*/
private suspend fun acceptLoop() {
var restartCount = 0
while (currentState == State.LISTENING || currentState == State.CONNECTED) {
try {
val ss = serverSocket ?: break
// 等待客户端连接 (阻塞)
val client = ss.accept()
clientIp = client.inetAddress.hostAddress ?: "unknown"
Log.i(TAG, ">>> 客户端已连接: $clientIp")
// 连接后关闭 ServerSocket (ADP 是单连接协议)
ss.close()
serverSocket = null
clientSocket = client
inputStream = client.getInputStream()
outputStream = client.getOutputStream()
// 开始握手
val handshakeOk = performHandshake(client)
if (!handshakeOk) {
Log.w(TAG, "握手失败,断开连接")
client.close()
continue
}
// 进入数据接收循环
dataLoop(client)
} catch (e: java.net.SocketException) {
if (currentState == State.STOPPED) break
Log.w(TAG, "Socket 异常,重新监听从: $restartCount", e)
// 非正常停止则尝试重启
restartCount++
if (restartCount < 3) {
delay(1000)
restartServer()
} else {
updateState(State.ERROR, "Socket 连续异常,停止重试")
break
}
} catch (e: Exception) {
if (currentState == State.STOPPED) break
Log.e(TAG, "接受连接异常", e)
delay(1000)
}
}
}
/**
* 握手阶段
*
* Server → Client: capability frame
* Client → Server: handshake frame
* Server → Client: ready frame
*/
private suspend fun performHandshake(client: Socket): Boolean {
updateState(State.HANDSHAKING, "正在握手…")
return withContext(Dispatchers.IO) {
try {
client.soTimeout = 10000 // 10s 握手超时
// 1. 发送能力信息
val capabilities = JSONObject().apply {
put("max_width", CAP_MAX_WIDTH)
put("max_height", CAP_MAX_HEIGHT)
put("codecs", CAP_CODECS)
put("features", CAP_FEATURES)
put("device_name", android.os.Build.MODEL)
put("protocol_version", "1.0")
}
val capData = capabilities.toString().toByteArray(Charsets.UTF_8)
sendFrame(FRAME_TYPE_CAPABILITY, capData, 0)
Log.i(TAG, "已发送 Capabilities: $capabilities")
// 2. 读取客户端握手
val header = readExact(8) ?: return@withContext false
val type = header[0].toInt() and 0xFF
val bodyLen = readU16(header, 2)
if (type != FRAME_TYPE_CAPABILITY) {
Log.e(TAG, "期望客户端发送 Capability 帧,收到 type=$type")
return@withContext false
}
if (bodyLen <= 0 || bodyLen > MAX_PAYLOAD_SIZE) {
Log.e(TAG, "无效的握手体长度: $bodyLen")
return@withContext false
}
val body = readExact(bodyLen) ?: return@withContext false
val settings = String(body, Charsets.UTF_8)
Log.i(TAG, "收到客户端设置: $settings")
// 解析客户端设置
try {
val json = JSONObject(settings)
val width = json.optInt("width", 1920)
val height = json.optInt("height", 1080)
val fps = json.optInt("fps", 30)
val codec = json.optString("codec", "h264")
videoWidth = width
videoHeight = height
// 更新 MediaRenderer 流信息
val mime = when (codec.lowercase()) {
"h264", "avc" -> "video/avc"
"h265", "hevc" -> "video/hevc"
else -> "video/avc"
}
mediaRenderer.updateStreamInfo(mime, "", width, height)
} catch (e: Exception) {
Log.w(TAG, "解析客户端设置失败,使用默认值", e)
videoWidth = 1920
videoHeight = 1080
}
// 3. 发送 READY
val readyJson = JSONObject().apply {
put("status", "ready")
put("message", "AirDisplay protocol ready")
}
val readyData = readyJson.toString().toByteArray(Charsets.UTF_8)
sendFrame(FRAME_TYPE_CAPABILITY, readyData, 0)
Log.i(TAG, "已发送 READY")
// 恢复超时设置(数据阶段较长时间不活动才超时)
client.soTimeout = KEEPALIVE_TIMEOUT_SEC * 1000
updateState(State.CONNECTED, "ADP 连接已建立")
listener?.onClientConnected(clientIp)
return@withContext true
} catch (e: Exception) {
Log.e(TAG, "握手失败", e)
updateState(State.ERROR, "握手失败: ${e.message}")
return@withContext false
}
}
}
/**
* 数据接收循环
*/
private suspend fun dataLoop(client: Socket) {
Log.i(TAG, "=== 进入数据接收循环 ===")
lastKeepaliveTime = System.nanoTime()
withContext(Dispatchers.IO) {
try {
while (currentState == State.CONNECTED && client.isConnected && !client.isClosed) {
// 读取 8 字节帧头
val header = readExact(8) ?: break
val type = header[0].toInt() and 0xFF
val bodyLen = readU16(header, 2)
val timestampUs = readU32(header, 4)
// 校验帧体长度
if (bodyLen < 0 || bodyLen > MAX_PAYLOAD_SIZE) {
Log.w(TAG, "无效帧长度: $bodyLen,断开连接")
break
}
// 读取帧体
val body = if (bodyLen > 0) {
readExact(bodyLen) ?: break
} else {
ByteArray(0)
}
totalFrames.incrementAndGet()
totalBytes.addAndGet(bodyLen.toLong())
// 根据类型处理
when (type) {
FRAME_TYPE_H264_NAL -> handleH264Nal(body, timestampUs)
FRAME_TYPE_H265_NAL -> handleH265Nal(body, timestampUs)
FRAME_TYPE_AUDIO -> handleAudio(body, timestampUs)
FRAME_TYPE_UIBC -> handleUibc(body)
FRAME_TYPE_KEEPALIVE -> handleKeepalive()
FRAME_TYPE_RESOLUTION_CHANGE -> handleResolutionChange(body)
FRAME_TYPE_CAPABILITY -> {
// 数据阶段的 capability 帧(忽略或重新协商)
Log.d(TAG, "收到数据阶段 Capability 帧: ${String(body, Charsets.UTF_8)}")
}
else -> {
Log.w(TAG, "未知帧类型: $type,长度: $bodyLen")
}
}
}
} catch (e: SocketTimeoutException) {
// Keepalive 超时检查
val now = System.nanoTime()
val elapsed = (now - lastKeepaliveTime) / 1_000_000_000
if (elapsed > KEEPALIVE_TIMEOUT_SEC * 2) {
Log.w(TAG, "Keepalive 超时 ($elapsed 秒),断开连接")
}
} catch (e: Exception) {
if (currentState == State.CONNECTED) {
Log.e(TAG, "数据循环异常", e)
}
} finally {
Log.i(TAG, "=== 数据接收循环结束 ===")
cleanup()
listener?.onClientDisconnected("连接断开")
// 如果未停止,重新监听
if (currentState == State.CONNECTED) {
currentState = State.LISTENING
updateState(State.LISTENING, "等待客户端重连")
scope.launch {
restartServer()
acceptLoop()
}
}
}
}
}
// ========== 帧处理 ==========
/**
* 处理 H.264 NAL 单元
*
* 期望 body 包含完整的 Annex B NAL 单元
* (起始码 0x00 0x00 0x00 0x01 + NAL 数据)
*/
private fun handleH264Nal(body: ByteArray, timestampUs: Long) {
if (body.isEmpty()) return
// 检查是否为关键帧,提取 SPS/PPS
if (body.size >= 4) {
val nalType = extractNalType(body)
when (nalType) {
7 -> { // SPS
csd0 = body
parseSpsResolution(body)
}
8 -> { // PPS
csd1 = body
// 收到 SPS+PPS 后配置解码器
if (csd0 != null && csd1 != null && !configuredCodec) {
configuredCodec = true
}
}
}
}
// 将 NAL 数据传递给 MediaRenderer
// 确保数据包含 Annex B 起始码
mediaRenderer.onVideoData(
data = body,
offset = 0,
length = body.size,
isKeyFrame = isKeyFrame(body),
ptsUs = timestampUs / 1000 // 转换为微秒
)
}
/**
* 处理 H.265 NAL 单元(预留)
*/
private fun handleH265Nal(body: ByteArray, timestampUs: Long) {
// H.265 支持预留,先用 H.264 路径处理
handleH264Nal(body, timestampUs)
}
/**
* 处理音频帧(预留)
*/
private fun handleAudio(body: ByteArray, timestampUs: Long) {
// 音频支持预留
Log.d(TAG, "收到音频帧: ${body.size} bytes, ts=$timestampUs")
}
/**
* 处理 UIBC 事件
*/
private fun handleUibc(body: ByteArray) {
if (body.isNotEmpty()) {
val eventJson = String(body, Charsets.UTF_8)
Log.d(TAG, "收到 UIBC 事件: $eventJson")
listener?.onUibcEvent(eventJson)
}
}
/**
* 处理 Keepalive
*/
private fun handleKeepalive() {
lastKeepaliveTime = System.nanoTime()
// 回复一个空的 Keepalive 帧
sendFrame(FRAME_TYPE_KEEPALIVE, ByteArray(0), System.nanoTime() / 1000)
}
/**
* 处理分辨率变更通知
*/
private fun handleResolutionChange(body: ByteArray) {
try {
val json = JSONObject(String(body, Charsets.UTF_8))
val width = json.optInt("width", videoWidth)
val height = json.optInt("height", videoHeight)
val fps = json.optInt("fps", 30)
videoWidth = width
videoHeight = height
mediaRenderer.updateStreamInfo("video/avc", "", width, height)
listener?.onResolutionChanged(width, height, fps)
Log.i(TAG, "分辨率变更: ${width}x${height} @ ${fps}fps")
} catch (e: Exception) {
Log.w(TAG, "解析分辨率变更失败", e)
}
}
// ========== I/O 基础 ==========
/**
* 发送 ADP 帧
*/
private fun sendFrame(type: Int, payload: ByteArray, timestampUs: Long) {
try {
val os = outputStream ?: return
val len = payload.size
if (len > MAX_PAYLOAD_SIZE) {
Log.w(TAG, "帧体过大,跳过: $len bytes")
return
}
// 构建 8 字节头部
val header = ByteBuffer.allocate(8).apply {
order(ByteOrder.BIG_ENDIAN)
put(type.toByte())
put(0x00.toByte()) // Reserved
putShort(len.toShort())
putInt((timestampUs and 0xFFFFFFFFL).toInt())
}
synchronized(os) {
os.write(header.array())
if (len > 0) {
os.write(payload)
}
os.flush()
}
} catch (e: Exception) {
Log.e(TAG, "发送帧失败 (type=$type)", e)
}
}
/**
* 精确读取指定字节数,超时或 EOF 返回 null
*/
private fun readExact(size: Int): ByteArray? {
val stream = inputStream ?: return null
val buffer = ByteArray(size)
var offset = 0
while (offset < size) {
val bytesRead = stream.read(buffer, offset, size - offset)
if (bytesRead == -1) return null // EOF
offset += bytesRead
}
return buffer
}
/**
* 从字节数组读取 16 位无符号整数(大端序)
*/
private fun readU16(data: ByteArray, offset: Int): Int {
return ((data[offset].toInt() and 0xFF) shl 8) or
(data[offset + 1].toInt() and 0xFF)
}
/**
* 从字节数组读取 32 位无符号整数(大端序)
*/
private fun readU32(data: ByteArray, offset: Int): Long {
return ((data[offset].toInt() and 0xFF).toLong() shl 24) or
((data[offset + 1].toInt() and 0xFF).toLong() shl 16) or
((data[offset + 2].toInt() and 0xFF).toLong() shl 8) or
(data[offset + 3].toInt() and 0xFF).toLong()
}
// ========== H.264 辅助 ==========
/**
* 提取 NAL 单元类型 (0 ~ 31)
* 非 H.264 数据返回 -1
*/
private fun extractNalType(data: ByteArray): Int {
val offset = findStartCode(data, 0)
if (offset < 0 || offset >= data.size) return -1
return data[offset].toInt() and 0x1F
}
/**
* 判断是否为关键帧IDR/SPS/PPS
*/
private fun isKeyFrame(data: ByteArray): Boolean {
val nalType = extractNalType(data)
return nalType == 5 || nalType == 7 || nalType == 8
}
/**
* 查找 Annex B 起始码 (0x00 0x00 0x00 0x01 或 0x00 0x00 0x01)
* 返回 NAL 数据的起始位置(起始码后的第一个字节)
* 未找到返回 -1
*/
private fun findStartCode(data: ByteArray, startOffset: Int): Int {
var i = startOffset
while (i + 2 < data.size) {
if (data[i] == 0x00.toByte() && data[i + 1] == 0x00.toByte()) {
if (i + 3 < data.size && data[i + 2] == 0x00.toByte() && data[i + 3] == 0x01.toByte()) {
return i + 4
}
if (data[i + 2] == 0x01.toByte()) {
return i + 3
}
}
i++
}
return -1
}
/**
* 从 SPS 中解析分辨率(简化实现)
*/
private fun parseSpsResolution(sps: ByteArray) {
// 生产环境中应实现完整的 H.264 SPS 解析器
// 当前让 MediaCodec 自动检测分辨率
Log.d(TAG, "SPS 已收到MediaCodec 将自动检测分辨率")
}
// ========== 生命周期管理 ==========
/**
* 重启服务器
*/
private fun restartServer() {
try {
cleanup()
serverSocket = ServerSocket(DEFAULT_PORT)
serverSocket?.soTimeout = 0
Log.i(TAG, "ADP 服务器已重启")
} catch (e: Exception) {
Log.e(TAG, "重启 ADP 服务器失败", e)
}
}
/**
* 清理资源
*/
private fun cleanup() {
try {
inputStream?.close()
} catch (_: Exception) {}
try {
outputStream?.close()
} catch (_: Exception) {}
try {
clientSocket?.close()
} catch (_: Exception) {}
try {
serverSocket?.close()
} catch (_: Exception) {}
inputStream = null
outputStream = null
clientSocket = null
serverSocket = null
// 重置配置状态(保留统计信息和分辨率信息以供重连使用)
configuredCodec = false
csd0 = null
csd1 = null
}
private fun updateState(newState: State, message: String) {
synchronized(stateLock) {
if (currentState != newState) {
Log.i(TAG, "状态变更: $currentState -> $newState ($message)")
currentState = newState
}
}
listener?.onStateChanged(newState, message)
}
// 内部统计用 AtomicLong
private class AtomicLong(initial: Long = 0L) {
private val value = java.util.concurrent.atomic.AtomicLong(initial)
fun get(): Long = value.get()
fun incrementAndGet(): Long = value.incrementAndGet()
fun addAndGet(delta: Long): Long = value.addAndGet(delta)
fun set(v: Long) = value.set(v)
}
}

View File

@@ -0,0 +1,258 @@
package com.qwenpaw.miracast.protocol
import android.content.Context
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import android.util.Log
/**
* mDNS 服务注册 —— 注册 _airdisplay._tcp 服务,实现同局域网设备发现
*
* 使用 Android 内置 NsdManager API无需额外依赖。
*
* == 发现路径 ==
* - P2P 模式WiFi Direct P2P 服务发现 + ADP TCP 连接
* - LAN 模式mDNS (_airdisplay._tcp) → ADP TCP 连接
*
* == TXT 记录 ==
* - device: 设备型号
* - protocol: 版本号
* - capabilities: 设备能力 (codecs, resolution, features)
*/
class MdnsService(private val context: Context) {
companion object {
private const val TAG = "MdnsService"
/** ADP 协议服务类型 */
const val SERVICE_TYPE = "_airdisplay._tcp"
/** ADP 默认端口 */
const val SERVICE_PORT = 7935
/** 重试最大次数 */
private const val MAX_RETRIES = 3
/** 重试间隔 (ms) */
private const val RETRY_DELAY_MS = 1000L
}
/** mDNS 服务回调 */
interface MdnsListener {
fun onServiceRegistered(serviceName: String)
fun onServiceUnregistered()
fun onError(error: String)
}
// ========== 内部变量 ==========
private var nsdManager: NsdManager =
context.applicationContext.getSystemService(Context.NSD_SERVICE) as NsdManager
private var registrationListener: NsdManager.RegistrationListener? = null
private var isRegistered = false
private var listener: MdnsListener? = null
private var registeredServiceName: String = ""
/** 设备名(由外部配置) */
@Volatile
var deviceName: String = android.os.Build.MODEL
set(value) {
field = value
// 如果已注册,重新注册以更新名称
if (isRegistered) {
Log.i(TAG, "设备名变更: $value,重新注册 mDNS")
unregister()
registerWithRetry()
}
}
/** 设备能力JSON 格式字符串) */
@Volatile
var capabilities: String = ""
set(value) {
field = value
if (isRegistered) {
Log.i(TAG, "能力变更,重新注册 mDNS")
unregister()
registerWithRetry()
}
}
override fun toString(): String {
val name = if (isRegistered) registeredServiceName else "not registered"
return "MdnsService($name, device=$deviceName)"
}
// ========== 公共 API ==========
/**
* 设置监听器
*/
fun setListener(listener: MdnsListener) {
this.listener = listener
}
/**
* 注册 mDNS 服务
* @param port ADP 服务器端口,默认 7935
*/
fun register(port: Int = SERVICE_PORT) {
if (isRegistered) {
Log.w(TAG, "mDNS 服务已注册,跳过")
return
}
Log.i(TAG, "正在注册 mDNS 服务: $SERVICE_TYPE:$port")
try {
val serviceInfo = NsdServiceInfo().apply {
serviceType = SERVICE_TYPE
serviceName = "AirDisplay - $deviceName"
port = port
// 添加 TXT 记录
val txtRecords = buildTxtRecords()
for ((key, value) in txtRecords) {
setAttribute(key, value)
}
}
val listener = createRegistrationListener()
registrationListener = listener
nsdManager.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, listener)
} catch (e: Exception) {
Log.e(TAG, "mDNS 注册失败", e)
listener?.onError("mDNS 注册失败: ${e.message}")
}
}
/**
* 带重试的注册
*/
fun registerWithRetry(port: Int = SERVICE_PORT) {
var attempts = 0
var lastError: String? = null
while (attempts < MAX_RETRIES && !isRegistered) {
attempts++
try {
register(port)
// 如果注册成功(同步回调),等待一下让回调触发
if (isRegistered) break
// 如果没立即成功,等待后重试
Thread.sleep(RETRY_DELAY_MS)
} catch (e: Exception) {
lastError = e.message
Log.w(TAG, "mDNS 注册第 $attempts 次尝试失败", e)
if (attempts < MAX_RETRIES) {
Thread.sleep(RETRY_DELAY_MS)
}
}
}
if (!isRegistered) {
Log.e(TAG, "mDNS 注册全部重试失败: $lastError")
listener?.onError("mDNS 注册失败 (已重试 $MAX_RETRIES 次): $lastError")
}
}
/**
* 注销 mDNS 服务
*/
fun unregister() {
if (!isRegistered) {
Log.d(TAG, "mDNS 服务未注册,无需注销")
return
}
try {
registrationListener?.let {
nsdManager.unregisterService(it)
}
} catch (e: Exception) {
Log.w(TAG, "mDNS 注销异常", e)
}
registrationListener = null
isRegistered = false
listener?.onServiceUnregistered()
Log.i(TAG, "mDNS 服务已注销")
}
/**
* 是否已注册
*/
fun isRegistered(): Boolean = isRegistered
/**
* 获取注册的服务名
*/
fun getRegisteredServiceName(): String = registeredServiceName
// ========== 内部实现 ==========
/**
* 构建 TXT 记录
*/
private fun buildTxtRecords(): Map<String, String> {
val records = mutableMapOf(
"device" to deviceName,
"protocol" to "airdisplay/1.0",
"platform" to "android",
"os" to "Android ${android.os.Build.VERSION.SDK_INT}"
)
// 如果有能力信息,添加到 TXT 记录
if (capabilities.isNotBlank()) {
records["capabilities"] = capabilities
}
// 添加分辨率信息
records["max_width"] = "1920"
records["max_height"] = "1080"
return records
}
/**
* 创建注册监听器
*/
private fun createRegistrationListener(): NsdManager.RegistrationListener {
return object : NsdManager.RegistrationListener {
override fun onServiceRegistered(serviceInfo: NsdServiceInfo) {
// 注意:系统可能会修改服务名(如果重名会自动加上 "(2)"
registeredServiceName = serviceInfo.serviceName
isRegistered = true
Log.i(TAG, "mDNS 服务注册成功: $registeredServiceName")
listener?.onServiceRegistered(registeredServiceName)
}
override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
val msg = "mDNS 注销失败: errorCode=$errorCode"
Log.w(TAG, msg)
isRegistered = false
listener?.onError(msg)
}
override fun onRegistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
val errorMsg = when (errorCode) {
NsdManager.FAILURE_ALREADY_ACTIVE -> "服务已激活"
NsdManager.FAILURE_INTERNAL_ERROR -> "内部错误"
NsdManager.FAILURE_MAX_LIMIT -> "达到最大限制"
else -> "未知错误 ($errorCode)"
}
val msg = "mDNS 注册失败: $errorMsg"
Log.e(TAG, msg)
isRegistered = false
listener?.onError(msg)
}
override fun onUnregistered(serviceInfo: NsdServiceInfo) {
Log.i(TAG, "mDNS 服务已注销: ${serviceInfo.serviceName}")
isRegistered = false
}
}
}
}

View File

@@ -0,0 +1,259 @@
package com.qwenpaw.miracast.protocol
import android.util.Log
import com.qwenpaw.miracast.wifidirect.WiFiDirectManager
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import org.json.JSONObject
/**
* 协议选择管理器 —— 根据设备和环境自动/手动选择投屏协议
*
* == 选择策略 ==
* ```
* ┌─────────────────────────────────────────────────┐
* │ 用户手动选择 → 强制使用指定协议 │
* ├─────────────────────────────────────────────────┤
* │ 自动模式: │
* │ Wi-Fi Direct + WFD IE 可用 → Miracast │
* │ Wi-Fi Direct + WFD IE 失败 → ADP (P2P) │
* │ 同局域网 → ADP (LAN) │
* │ Mac 投屏请求 → AirPlay │
* └─────────────────────────────────────────────────┘
* ```
*/
class ProtocolManager(
private val wifiDirectManager: WiFiDirectManager
) {
companion object {
private const val TAG = "ProtocolManager"
/** ADP TCP 端口 */
const val ADP_PORT = 7935
/** RTSP (Miracast) TCP 端口 */
const val MIRACAST_RTSP_PORT = 7236
}
// ========== 协议枚举 ==========
/** 支持的投屏协议 */
enum class Protocol(val description: String) {
MIRACAST("Miracast (Wi-Fi Display) — Windows 原生"),
ADP("AirDisplay Protocol — 自定义轻量协议"),
AIRPLAY("AirPlay — Mac/iOS 原生"),
NONE("");
}
/** 协议选择模式 */
enum class SelectionMode {
/** 自动选择(根据环境决定) */
AUTO,
/** 强制使用 Miracast */
FORCE_MIRACAST,
/** 强制使用 ADP */
FORCE_ADP,
/** 强制使用 AirPlay */
FORCE_AIRPLAY
}
/** 协议状态 */
data class ProtocolState(
/** 当前使用的协议 */
val activeProtocol: Protocol = Protocol.NONE,
/** 选择模式 */
val selectionMode: SelectionMode = SelectionMode.AUTO,
/** WFD IE 是否可用 */
val wfdAvailable: Boolean = false,
/** 到客户端的连接是否已建立 */
val isConnected: Boolean = false,
/** 客户端 IP 地址 */
val clientIp: String = "",
/** ADP 服务器是否在运行 */
val adpServerRunning: Boolean = false
)
/** 协议监听器 */
interface ProtocolListener {
fun onProtocolChanged(protocol: Protocol, reason: String)
fun onStateChanged(state: ProtocolState)
}
// ========== 内部变量 ==========
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val _state = MutableStateFlow(ProtocolState())
val state: StateFlow<ProtocolState> = _state.asStateFlow()
private var listener: ProtocolListener? = null
private var userOverride: Protocol? = null
private var wfdStatus: WiFiDirectManager.WfdStatus = WiFiDirectManager.WfdStatus.NOT_ATTEMPTED
override fun toString(): String {
val s = _state.value
return "ProtocolManager(${s.activeProtocol}, mode=${s.selectionMode})"
}
// ========== 公共 API ==========
/**
* 设置协议监听器
*/
fun setListener(listener: ProtocolListener?) {
this.listener = listener
}
/**
* 获取当前协议状态
*/
fun getState(): ProtocolState = _state.value
/**
* 获取当前激活的协议
*/
fun getActiveProtocol(): Protocol = _state.value.activeProtocol
/**
* 用户手动选择协议
*
* @param protocol 指定协议,传 null 切换回自动模式
*/
fun selectProtocol(protocol: Protocol?) {
userOverride = protocol
val mode = if (protocol != null) {
when (protocol) {
Protocol.MIRACAST -> SelectionMode.FORCE_MIRACAST
Protocol.ADP -> SelectionMode.FORCE_ADP
Protocol.AIRPLAY -> SelectionMode.FORCE_AIRPLAY
Protocol.NONE -> SelectionMode.AUTO
}
} else {
SelectionMode.AUTO
}
_state.update { it.copy(selectionMode = mode) }
if (protocol != null) {
Log.i(TAG, "用户手动选择: $protocol")
activateProtocol(protocol, "用户手动选择")
} else {
Log.i(TAG, "切换回自动选择模式")
autoSelect()
}
}
/**
* 通知 WFD IE 状态变更(由 WiFiDirectManager 回调)
*/
fun onWfdStatusChanged(status: WiFiDirectManager.WfdStatus) {
wfdStatus = status
_state.update { it.copy(wfdAvailable = status == WiFiDirectManager.WfdStatus.SUCCESS) }
Log.i(TAG, "WFD IE 状态已更新: $status")
// 在自动模式下触发重新选择
if (_state.value.selectionMode == SelectionMode.AUTO) {
autoSelect()
}
}
/**
* 通知连接事件
*/
fun onClientConnected(protocol: Protocol, clientIp: String) {
_state.update {
it.copy(
isConnected = true,
clientIp = clientIp,
activeProtocol = protocol
)
}
listener?.onStateChanged(_state.value)
Log.i(TAG, "客户端已连接 (${protocol}): $clientIp")
}
/**
* 通知断开事件
*/
fun onClientDisconnected(reason: String) {
val wasConnected = _state.value.isConnected
_state.update { it.copy(isConnected = false, clientIp = "") }
if (wasConnected) {
Log.i(TAG, "客户端已断开: $reason")
// 自动模式下等待重新连接
if (_state.value.selectionMode == SelectionMode.AUTO) {
autoSelect()
}
}
listener?.onStateChanged(_state.value)
}
/**
* 更新 ADP 服务器状态
*/
fun setAdpServerRunning(running: Boolean) {
_state.update { it.copy(adpServerRunning = running) }
}
/**
* 获取当前建议的协议(不触发切换)
*/
fun getSuggestedProtocol(): Protocol {
// 用户覆盖优先
userOverride?.let { return it }
// WFD IE 成功 → 首选 Miracast
if (wfdStatus == WiFiDirectManager.WfdStatus.SUCCESS) {
return Protocol.MIRACAST
}
// WFD IE 失败或未尝试 → ADP
return Protocol.ADP
}
// ========== 内部实现 ==========
/**
* 自动选择协议
*/
private fun autoSelect() {
if (_state.value.selectionMode != SelectionMode.AUTO) return
if (_state.value.isConnected) return // 已连接时不切换
val suggested = getSuggestedProtocol()
val currentActive = _state.value.activeProtocol
if (suggested != currentActive) {
val reason = when (suggested) {
Protocol.MIRACAST -> "WFD IE 可用,使用原生 Miracast"
Protocol.ADP -> "WFD IE 不可用或受限,降级到 ADP 协议"
Protocol.AIRPLAY -> "AirPlay 协议"
Protocol.NONE -> "无可用协议"
}
activateProtocol(suggested, reason)
}
}
/**
* 激活指定协议
*/
private fun activateProtocol(protocol: Protocol, reason: String) {
if (protocol == Protocol.NONE) return
Log.i(TAG, ">>> 协议切换: ${_state.value.activeProtocol}$protocol (原因: $reason)")
_state.update { it.copy(activeProtocol = protocol) }
listener?.onProtocolChanged(protocol, reason)
listener?.onStateChanged(_state.value)
}
/**
* 销毁
*/
fun destroy() {
scope.cancel()
}
}

View File

@@ -3,12 +3,9 @@ package com.qwenpaw.miracast.wifidirect
import android.content.Context
import android.net.MacAddress
import android.net.wifi.WifiManager
import android.net.wifi.p2p.WifiP2pConfig
import android.net.wifi.p2p.WifiP2pDevice
import android.net.wifi.p2p.WifiP2pDeviceList
import android.net.wifi.p2p.WifiP2pGroup
import android.net.wifi.p2p.WifiP2pInfo
import android.net.wifi.p2p.WifiP2pManager
import android.net.wifi.p2p.*
import android.net.wifi.p2p.nsd.WifiP2pDnsSdServiceInfo
import android.net.wifi.p2p.nsd.WifiP2pServiceInfo
import android.os.Build
import android.os.Looper
import android.text.format.Formatter
@@ -37,6 +34,30 @@ class WiFiDirectManager(
companion object {
private const val TAG = "WiFiDirectManager"
private const val GROUP_OWNER_INTENT = 15 // 最大优先级,确保成为 GO
/** ADP 服务类型P2P 服务发现用) */
private const val ADP_SERVICE_TYPE = "_airdisplay._tcp"
/** ADP 协议标识 */
private const val ADP_SERVICE_NAME = "airdisplay"
/** TXT 记录版本 */
private const val ADP_TXT_VERSION = "1.0"
}
/// WFD IE 设置结果
enum class WfdStatus {
/** WFD IE 设置成功,可用原生 Miracast */
SUCCESS,
/** WFD IE 设置失败,需要降级到 ADP */
FAILED,
/** 未尝试设置API 级别不支持等) */
NOT_ATTEMPTED
}
/// WFD 状态监听器
interface WfdStatusListener {
fun onWfdStatusChanged(status: WfdStatus)
}
private val wifiManager: WifiManager =
@@ -50,6 +71,13 @@ class WiFiDirectManager(
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
/// WFD IE 状态
@Volatile
private var wfdStatus = WfdStatus.NOT_ATTEMPTED
/// WFD 状态监听器
private var wfdStatusListener: WfdStatusListener? = null
/// 连接状态数据类
data class ConnectionInfo(
val isConnected: Boolean = false,
@@ -77,6 +105,21 @@ class WiFiDirectManager(
private var isInitialized = false
private var discoveryActive = false
/// ADP P2P 服务是否已注册
private var adpServiceRegistered = false
/**
* 设置 WFD 状态监听器
*/
fun setWfdStatusListener(listener: WfdStatusListener?) {
wfdStatusListener = listener
}
/**
* 获取当前 WFD IE 状态
*/
fun getWfdStatus(): WfdStatus = wfdStatus
// ========== 初始化与销毁 ==========
/**
@@ -111,6 +154,7 @@ class WiFiDirectManager(
*/
fun destroy() {
Log.i(TAG, "销毁 Wi-Fi Direct")
unregisterP2pService()
stopDiscovery()
removeGroup()
isInitialized = false
@@ -169,6 +213,8 @@ class WiFiDirectManager(
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
Log.i(TAG, "WFD IE 需要 API 29+,当前版本 ${Build.VERSION.SDK_INT} 不支持,跳过")
wfdStatus = WfdStatus.NOT_ATTEMPTED
wfdStatusListener?.onWfdStatusChanged(wfdStatus)
return
}
@@ -179,6 +225,8 @@ class WiFiDirectManager(
wfdInfo = wfdInfoClass.getDeclaredConstructor().newInstance()
} catch (e: Exception) {
Log.e(TAG, "✗ WifiP2pWfdInfo 类不可用: ${e.message}")
wfdStatus = WfdStatus.FAILED
wfdStatusListener?.onWfdStatusChanged(wfdStatus)
return
}
@@ -303,6 +351,8 @@ class WiFiDirectManager(
object : WifiP2pManager.ActionListener {
override fun onSuccess() {
Log.i(TAG, "✓ WFD IE 设置成功!设备已注册为 Miracast 接收器")
wfdStatus = WfdStatus.SUCCESS
wfdStatusListener?.onWfdStatusChanged(wfdStatus)
}
override fun onFailure(reason: Int) {
@@ -312,20 +362,116 @@ class WiFiDirectManager(
2 -> "BUSY"
else -> "UNKNOWN($reason)"
}
Log.w(TAG, "✗ WFD IE onFailure: reason=$r")
Log.w(TAG, "✗ WFD IE onFailure: reason=$r — 自动降级到 ADP 协议")
wfdStatus = WfdStatus.FAILED
wfdStatusListener?.onWfdStatusChanged(wfdStatus)
}
}
)
} catch (e: NoSuchMethodException) {
Log.w(TAG, "✗ setWfdInfo 方法不存在OEM 未实现此隐藏 API: ${e.message}")
wfdStatus = WfdStatus.FAILED
wfdStatusListener?.onWfdStatusChanged(wfdStatus)
} catch (e: InvocationTargetException) {
Log.e(TAG, "✗ setWfdInfo 底层异常: ${e.cause?.javaClass?.name ?: "未知"}: ${e.cause?.message ?: "无消息"}")
e.cause?.printStackTrace()
wfdStatus = WfdStatus.FAILED
wfdStatusListener?.onWfdStatusChanged(wfdStatus)
} catch (e: SecurityException) {
Log.e(TAG, "✗ setWfdInfo 无权限: ${e.message}(需要系统权限,非系统应用可能被阻止)")
wfdStatus = WfdStatus.FAILED
wfdStatusListener?.onWfdStatusChanged(wfdStatus)
} catch (e: Exception) {
Log.e(TAG, "✗ setWfdInfo 反射异常: ${e.javaClass.name}: ${e.message}")
e.printStackTrace()
wfdStatus = WfdStatus.FAILED
wfdStatusListener?.onWfdStatusChanged(wfdStatus)
}
}
// ========== P2P 服务发现 (ADP) ==========
/**
* 注册 ADP 协议 P2P 服务发现。
*
* 在 WiFi Direct 模式下,桌面客户端可以通过 P2P 服务发现
* 检索到本设备支持 ADP 协议,无需 mDNSP2P 模式下 mDNS 通常不可用)。
*
* 注册的服务类型: _airdisplay._tcp
* TXT 记录中包含设备能力和连接信息。
*/
private fun registerP2pServiceForAdp() {
if (adpServiceRegistered) return
try {
// 构建 TXT 记录
val txtRecords = mapOf(
"version" to ADP_TXT_VERSION,
"device" to (deviceNameManager?.getDeviceName() ?: android.os.Build.MODEL),
"protocol" to "airdisplay",
"port" to "7935",
"max_width" to "1920",
"max_height" to "1080",
"codecs" to "h264"
)
// 创建 DNS-SD 服务信息
val serviceInfo = WifiP2pDnsSdServiceInfo.newInstance(
ADP_SERVICE_NAME,
ADP_SERVICE_TYPE,
txtRecords
)
// 注册服务
p2pManager.addLocalService(p2pChannel, serviceInfo,
object : WifiP2pManager.ActionListener {
override fun onSuccess() {
adpServiceRegistered = true
Log.i(TAG, "ADP P2P 服务已注册: $ADP_SERVICE_NAME.$ADP_SERVICE_TYPE")
}
override fun onFailure(reason: Int) {
val reasonStr = when (reason) {
WifiP2pManager.BUSY -> "BUSY"
WifiP2pManager.ERROR -> "ERROR"
WifiP2pManager.P2P_UNSUPPORTED -> "P2P_UNSUPPORTED"
else -> "UNKNOWN($reason)"
}
Log.w(TAG, "ADP P2P 服务注册失败: $reasonStr(桌面客户端需通过 mDNS 发现)")
}
}
)
} catch (e: Exception) {
Log.w(TAG, "ADP P2P 服务注册异常: ${e.message}")
}
}
/**
* 注销 ADP P2P 服务
*/
private fun unregisterP2pService() {
if (!adpServiceRegistered) return
try {
val serviceInfo = WifiP2pDnsSdServiceInfo.newInstance(
ADP_SERVICE_NAME,
ADP_SERVICE_TYPE,
emptyMap()
)
p2pManager.removeLocalService(p2pChannel, serviceInfo,
object : WifiP2pManager.ActionListener {
override fun onSuccess() {
adpServiceRegistered = false
Log.i(TAG, "ADP P2P 服务已注销")
}
override fun onFailure(reason: Int) {
Log.w(TAG, "ADP P2P 服务注销失败: $reason")
}
}
)
} catch (e: Exception) {
Log.w(TAG, "ADP P2P 服务注销异常: ${e.message}")
}
}
@@ -343,6 +489,8 @@ class WiFiDirectManager(
Log.i(TAG, "P2P Group 创建成功,本设备为 Group Owner")
// Group 创建后再设置一次 WFD IE确保 IE 在 P2P 接口上生效
trySetWfdInfo()
// 注册 ADP P2P 服务发现(桌面客户端可通过 Wi-Fi Direct 发现本设备)
registerP2pServiceForAdp()
// 稍等片刻让系统完成网络配置
scope.launch {
delay(2000)