让我用一个 "露营派对" 的故事,带你轻松理解 Wi-Fi P2P 技术,以及如何实现手机投屏到车机!
🌌 故事背景:露营派对上的无线大屏
想象你和朋友去露营,带了手机但想看电影。车机就像一个 "大屏幕投影仪",但传统方式需要数据线连接,很不方便。
这时,你发现手机和车机都有 "魔法无线桥接" 功能(Wi-Fi P2P),可以直接建立连接:
- 手机变成 "内容源"(像 DVD 播放器)
- 车机变成 "显示器"(像投影仪)
- 两者通过 Wi-Fi P2P 直接通信,无需路由器
🧙♂️ Wi-Fi P2P 核心概念与实现
1. Wi-Fi P2P 是什么?
Wi-Fi P2P(Peer-to-Peer)也叫Wi-Fi 直连,允许设备直接互联,无需接入点(路由器)。
类比故事:
露营时,手机和车机就像两个带无线电台的对讲机,直接对话,不需要通过基站(路由器)。
2. Wi-Fi P2P 关键角色
-
Group Owner(GO) :类似 "临时路由器",管理连接(通常由车机担任)
-
Client:连接到 GO 的设备(通常由手机担任)
类比故事:
车机是 "营地广播台"(GO),手机是 "听众"(Client),手机直接接收车机的信号。
🚀 实现手机投屏到车机的步骤
1. 初始化 Wi-Fi P2P
// 1. 获取Wi-Fi P2P管理器
val manager = getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager
val channel = manager.initialize(this, mainLooper, null)
// 2. 注册广播接收器监听P2P状态变化
val intentFilter = IntentFilter().apply {
addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION)
addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION)
addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)
}
registerReceiver(p2pReceiver, intentFilter)
2. 搜索车机设备
// 开始搜索P2P设备(车机)
manager.discoverPeers(channel, object : WifiP2pManager.ActionListener {
override fun onSuccess() {
// 搜索成功,等待广播通知发现的设备
}
override fun onFailure(reason: Int) {
// 搜索失败,处理错误
}
})
// 在广播接收器中处理发现的设备
private val p2pReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> {
// 获取可用设备列表
val peers = intent.getParcelableArrayListExtra<WifiP2pDevice>(
WifiP2pManager.EXTRA_P2P_DEVICE_LIST
)
// 找到车机设备(例如名称包含"CAR")
val carDevice = peers?.find { it.deviceName.contains("CAR") }
carDevice?.let { connectToCar(it) }
}
}
}
}
3. 连接到车机
// 连接到车机设备
private fun connectToCar(device: WifiP2pDevice) {
val config = WifiP2pConfig().apply {
deviceAddress = device.deviceAddress
wps.setup = WpsInfo.PBC // 使用Push Button Configuration
}
manager.connect(channel, config, object : WifiP2pManager.ActionListener {
override fun onSuccess() {
// 连接成功,等待连接状态变化的广播
}
override fun onFailure(reason: Int) {
// 连接失败,处理错误
}
})
}
4. 建立数据通道
// 在连接成功的广播中获取套接字
private val p2pReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> {
val networkInfo = intent.getParcelableExtra<NetworkInfo>(
WifiP2pManager.EXTRA_NETWORK_INFO
)
if (networkInfo?.isConnected == true) {
// 获取Group Owner的IP地址(车机)
val group = intent.getParcelableExtra<WifiP2pGroup>(
WifiP2pManager.EXTRA_WIFI_P2P_GROUP
)
val goIpAddress = group?.groupOwnerAddress?.hostAddress
// 启动投屏服务
goIpAddress?.let { startScreenMirroring(it) }
}
}
}
}
}
5. 实现屏幕镜像(投屏)
// 启动屏幕捕获服务
private fun startScreenMirroring(goIpAddress: String) {
// 1. 请求屏幕捕获权限
val mediaProjectionManager = getSystemService(
Context.MEDIA_PROJECTION_SERVICE
) as MediaProjectionManager
startActivityForResult(
mediaProjectionManager.createScreenCaptureIntent(),
REQUEST_CODE_SCREEN_CAPTURE
)
}
// 在Activity结果中处理屏幕捕获
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_SCREEN_CAPTURE && resultCode == RESULT_OK) {
// 2. 获取MediaProjection对象
val mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data!!)
// 3. 创建虚拟显示器并开始捕获屏幕
val virtualDisplay = mediaProjection.createVirtualDisplay(
"ScreenMirror",
screenWidth, screenHeight, screenDensity,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
surface, null, null
)
// 4. 通过Socket将屏幕数据发送到车机
Thread {
try {
val socket = Socket(goIpAddress, PORT)
val outputStream = socket.getOutputStream()
// 从Surface获取屏幕数据并发送
// 实际实现需要使用MediaCodec编码视频
while (isMirroring) {
val frameData = captureScreenFrame()
outputStream.write(frameData)
}
socket.close()
} catch (e: Exception) {
e.printStackTrace()
}
}.start()
}
}
🚗 车机端接收投屏的简化实现
车机端需要:
-
作为 Group Owner 等待连接
-
接收手机发送的屏幕数据
-
解码并显示
// 车机端:创建ServerSocket等待连接
Thread {
try {
val serverSocket = ServerSocket(PORT)
val socket = serverSocket.accept() // 等待手机连接
val inputStream = socket.getInputStream()
// 初始化解码器
val mediaCodec = MediaCodec.createDecoderByType("video/avc")
val format = MediaFormat.createVideoFormat("video/avc", width, height)
mediaCodec.configure(format, surface, null, 0)
mediaCodec.start()
// 接收数据并解码显示
val buffer = ByteArray(4096)
while (isReceiving) {
val bytesRead = inputStream.read(buffer)
if (bytesRead > 0) {
// 将数据送入解码器
val inputBufferIndex = mediaCodec.dequeueInputBuffer(-1)
if (inputBufferIndex >= 0) {
val inputBuffer = mediaCodec.getInputBuffer(inputBufferIndex)
inputBuffer.clear()
inputBuffer.put(buffer, 0, bytesRead)
mediaCodec.queueInputBuffer(inputBufferIndex, 0, bytesRead, presentationTimeUs, 0)
}
// 获取解码后的数据并显示
val bufferInfo = MediaCodec.BufferInfo()
val outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0)
if (outputBufferIndex >= 0) {
mediaCodec.releaseOutputBuffer(outputBufferIndex, true)
}
}
}
mediaCodec.stop()
mediaCodec.release()
serverSocket.close()
} catch (e: Exception) {
e.printStackTrace()
}
}.start()
🛡️ 注意事项与最佳实践
-
权限要求:
- 需要
android.permission.CHANGE_WIFI_STATE和android.permission.ACCESS_WIFI_STATE - 屏幕捕获需要用户明确授权
- 需要
-
兼容性问题:
- 不同车型的车机系统支持度不同
- 建议先检查设备是否支持 Wi-Fi P2P:
wifiManager.isP2pSupported
-
性能优化:
- 视频编码使用 H.264 或 H.265 以降低带宽需求
- 使用多线程处理以避免 UI 卡顿
🎯 总结:Wi-Fi P2P 投屏的核心流程
-
建立连接:手机和车机通过 Wi-Fi P2P 直接互联
-
权限获取:请求屏幕捕获权限
-
屏幕捕获:使用 MediaProjection API 捕获手机屏幕
-
数据传输:将屏幕数据编码后通过 Socket 发送
-
解码显示:车机接收数据并解码显示
通过这个 "露营派对" 的故事,你现在应该理解 Wi-Fi P2P 投屏的原理啦!如果有具体环节想深入了解,随时喊我~ 😊