📺 Wi-Fi P2P 揭秘:手机与车机的无线投屏魔法

517 阅读4分钟

让我用一个 "露营派对" 的故事,带你轻松理解 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()
    }
}

🚗 车机端接收投屏的简化实现

车机端需要:

  1. 作为 Group Owner 等待连接

  2. 接收手机发送的屏幕数据

  3. 解码并显示

// 车机端:创建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()

🛡️ 注意事项与最佳实践

  1. 权限要求

    • 需要 android.permission.CHANGE_WIFI_STATE 和 android.permission.ACCESS_WIFI_STATE
    • 屏幕捕获需要用户明确授权
  2. 兼容性问题

    • 不同车型的车机系统支持度不同
    • 建议先检查设备是否支持 Wi-Fi P2P:wifiManager.isP2pSupported
  3. 性能优化

    • 视频编码使用 H.264 或 H.265 以降低带宽需求
    • 使用多线程处理以避免 UI 卡顿

🎯 总结:Wi-Fi P2P 投屏的核心流程

  1. 建立连接:手机和车机通过 Wi-Fi P2P 直接互联

  2. 权限获取:请求屏幕捕获权限

  3. 屏幕捕获:使用 MediaProjection API 捕获手机屏幕

  4. 数据传输:将屏幕数据编码后通过 Socket 发送

  5. 解码显示:车机接收数据并解码显示

通过这个 "露营派对" 的故事,你现在应该理解 Wi-Fi P2P 投屏的原理啦!如果有具体环节想深入了解,随时喊我~ 😊