fix: 三协议架构代码审查修复
- MiracastService.kt 初始化顺序修复(mediaRenderer 在 AirDisplayProtocol 之前) - 补充误删的 protocolManager.setListener() 回调 - 所有 12 个 lateinit 变量使用前初始化验证 此提交对应全面代码审查发现的问题
This commit is contained in:
309
ARCHITECTURE.md
Normal file
309
ARCHITECTURE.md
Normal 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
|
||||
|
||||
自定义轻量屏幕投屏协议。设计目标:低延迟、简单可靠、跨平台。
|
||||
|
||||
#### 发现阶段
|
||||
|
||||
| 场景 | 发现方式 |
|
||||
|------|---------|
|
||||
| Wi‑Fi 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) ←── Wi‑Fi Direct ──→ PC
|
||||
IP: 192.168.49.1
|
||||
|
||||
LAN 模式: 手机 ──── Wi‑Fi 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 原生投屏
|
||||
- 音频支持
|
||||
|
||||
---
|
||||
|
||||
*三协议并行,不受系统权限限制,覆盖全部桌面端。*
|
||||
@@ -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 IE,PC 原生发现
|
||||
* - 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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
258
app/src/main/java/com/qwenpaw/miracast/protocol/MdnsService.kt
Normal file
258
app/src/main/java/com/qwenpaw/miracast/protocol/MdnsService.kt
Normal 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 模式:Wi‑Fi 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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 服务发现。
|
||||
*
|
||||
* 在 Wi‑Fi Direct 模式下,桌面客户端可以通过 P2P 服务发现
|
||||
* 检索到本设备支持 ADP 协议,无需 mDNS(P2P 模式下 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)
|
||||
|
||||
Reference in New Issue
Block a user