Android中WebRTC通信

Android中WebRTC通信

基于代码分析整理,涵盖 WebRTC P2P 数据通信、信令交换、VLC 视频直播等完整流程。


1、系统概述

本系统是一个基于 Android 的 WebRTC 点对点通信方案,主要包含以下能力:

模块 功能 对应类
WebRTC 核心 P2P 连接建立、DataChannel 数据传输 WebRtcManager
信令客户端 WebSocket 信令收发(SDP/ICE) SignalingClient
P2P 消息管理 消息封装、确认机制、心跳、重试 P2PMessageManager
VLC 播放器 RTSP/RTMP/HTTPS 视频流播放 EasyVlcPlayer
签名工具 请求签名认证(SigV1 / Bearer) SigUtil

系统架构图


2、核心组件详解

2.1 WebRtcManager --- WebRTC 管理核心

WebRtcManager 是整个系统的入口类,负责:

  • 初始化 :创建 EglBasePeerConnectionFactoryPeerConnectionDataChannel
  • 信令绑定 :通过 SignalingClient 收发 Offer/Answer/ICE 候选
  • 数据通信 :通过 P2PMessageManagerDataChannel 上收发消息
  • 心跳维护:每 30 秒发送一次心跳消息
  • 资源释放:断开连接时清理所有资源

构造函数参数

javascript 复制代码
class WebRtcManager(
    context: Context,                    // Android 上下文
    signalingClient: SignalingClient,     // 信令客户端
    remoteUserId: String? = null,        // 远端用户 ID
    turnConfig: TurnServerConfig? = null // TURN 服务器配置
)

关键配置

kotlin 复制代码
companion object {
    private const val STUN_SERVER = "stun:120.79.206.28:3478"
    private const val DATA_CHANNEL_LABEL = "webrtc_data_channel"
    private const val HEARTBEAT_INTERVAL_MS = 30000L  // 30秒心跳
    private const val MAX_BUFFER_AMOUNT = 1024 * 1024 // 1MB 缓冲上限
}

2.2 SignalingClient --- 信令客户端

通过 WebSocket 与信令服务器通信,负责 WebRTC 握手过程中的信令转发。

信令消息格式

所有信令消息前缀为 "44",采用 JSON 格式:

json 复制代码
{
  "event": "offer|answer|candidate|live|data_message",
  "from": "localUserId",
  "to": "remoteUserId",
  "data": "..."
}

发送时封装

scss 复制代码
val jsonString = "44" + Gson().toJson(message)
webSocketUtils.mWebSocketManager?.send(jsonString)

ICE 候选发送优化

  • 去重 :使用 ConcurrentHashMap 记录已发送的 ICE 候选
  • 限流 :最小发送间隔 100ms
  • sdpMid 强制为空字符串:与服务器示例保持一致
vbscript 复制代码
val iceData = mapOf(
    "candidate" to candidate.sdp,
    "sdpMid" to "",                          // 强制设为空字符串
    "sdpMLineIndex" to candidate.sdpMLineIndex,
    "usernameFragment" to null               // 显式保留,值为 null
)

2.3 P2PMessageManager --- P2P 消息管理

DataChannel 之上构建可靠的消息传输层。

消息格式

json 复制代码
{
  "id": "msg_1697000000000_0",
  "type": "TEXT|COMMAND|ACK|HEARTBEAT|FILE_META|FILE_CHUNK",
  "payload": "消息内容",
  "timestamp": 1697000000000
}

消息类型

类型 说明 是否需要 ACK
TEXT 文本消息 ✅ 是
COMMAND 控制命令 ✅ 是
ACK 确认消息 ❌ 否
HEARTBEAT 心跳消息 ❌ 否
FILE_META 文件元数据 ✅ 是
FILE_CHUNK 文件分片 ✅ 是

确认与重试机制

复制代码
发送消息
  ├── 添加到 pendingMessages(非 ACK/HEARTBEAT)
  ├── 启动超时检查线程(5000ms)
  ├── 超时未收到 ACK → 重试(最多 3 次)
  └── 超过最大重试次数 → 回调失败

2.4 EasyVlcPlayer --- VLC 视频播放器

基于 libVLC 的视频播放器,用于播放设备端推送的直播流。

初始化参数

java 复制代码
val options = mutableListOf(
    "--no-video-title-show",          // 隐藏视频标题栏
    "--network-caching=800",           // 网络缓冲 800ms
    "--demux=hevc",                    // 强制 H.265 解复用
    "--codec=hevc",                    // 强制 H.265 解码
    "--http-reconnect",                // HTTP 断连自动重连
    "--vout=android_display",          // Android 原生显示模块
    "--live-caching=800",              // 直播流缓存 800ms
    "-vvv",                            // 详细日志(发布时移除)
    "--hevc-fps=20",                   // 帧率 20fps
    "--avcodec-skiploopfilter=all",    // 减少解码器压力
    "--android-display-chroma=RV32",   // 颜色格式 RV32
    "--avcodec-hw=none",               // 禁用硬件解码
    "--video-on-top"                   // 置顶显示
)

播放流程

  1. 收到信令服务器的 "live" 事件
  2. 提取 fileHashlink
  3. 通过 FlowBus 分发 StreamMessageEvent
  4. EasyVlcPlayer.play(link) 被调用
  5. 强制将 URL Scheme 改为 https
  6. 创建 Media 对象并播放

2.5 SigUtil --- 请求签名工具

用于对 WebSocket/HTTP 请求进行签名认证。

签名流程

scss 复制代码
1. 查询参数规范化 (getQueryStr)
   ├── Key: URLEncoder.encode + "+" → "%20"
   ├── Key 按字典序升序排列
   ├── Value 按字典序升序排列
   └── 拼接: key1=value1&key1=value2&key2=value3
​
2. Payload 哈希 (getPayloadHash)
   ├── null → ByteArray(0)
   └── SHA-256 → 64位小写十六进制
​
3. 构造待签名字符串
   └── "urlStr\nqueryString\npayloadHash"
​
4. HMAC-SHA256 签名
   └── HmacSHA256(strToSign, secretKey) → 64位小写十六进制
​
5. 输出格式
   ├── sigV1() → "SigV1 {signHex}"
   └── sigBearer() → "Bearer {signHex}"

3、通信流程

3.1 WebRTC 信令交换时序

完整握手流程

scss 复制代码
┌─────────────┐         ┌──────────────┐         ┌─────────────┐
│  Android App │         │ 信令服务器    │         │   设备端     │
└──────┬──────┘         └──────┬───────┘         └──────┬──────┘
       │                       │                        │
       │ 1. WebRtcManager()   │                        │
       │ 2. SignalingClient.connect()                   │
       │──────────────────────>│                        │
       │                       │ 3. WebSocket 连接       │
       │                       │───────────────────────>│
       │                       │                        │
       │ 4. createAndSendOffer()                       │
       │    createOffer()     │                        │
       │    setLocalDescription(offer)                │
       │    sendOffer(offer)  │                        │
       │──────────────────────>│                        │
       │                       │ 5. 转发 Offer           │
       │                       │───────────────────────>│
       │                       │                        │
       │                       │ 6. setRemoteDescription(offer)
       │                       │                        │
       │                       │ 7. createAnswer()      │
       │                       │    setLocalDescription(answer)
       │                       │    sendAnswer(answer)    │
       │                       │<───────────────────────│
       │ 8. 转发 Answer        │                        │
       │<──────────────────────│                        │
       │    setRemoteDescription(answer)                │
       │                       │                        │
       │ 9. onIceCandidate()   │                        │
       │    sendIceCandidate() │                        │
       │──────────────────────>│                        │
       │                       │ 10. 转发 ICE           │
       │                       │───────────────────────>│
       │                       │    addIceCandidate()   │
       │                       │                        │
       │                       │ 11. 设备端发送 ICE      │
       │                       │<───────────────────────│
       │ 12. 转发 ICE          │                        │
       │<──────────────────────│                        │
       │    addIceCandidate()  │                        │
       │                       │                        │
       │ ═══════════════════════════════════════════════ │
       │         ICE 连接成功 → DataChannel OPEN         │
       │ ═══════════════════════════════════════════════ │

3.2 DataChannel P2P 通信流程

一旦 ICE 连接建立,DataChannel 状态变为 OPEN,即可开始 P2P 通信:

scss 复制代码
App 发送消息
  ├── sendP2PMessage(payload, type=TEXT)
  │     └── P2PMessageManager.sendMessage()
  │           ├── 生成消息 ID: "msg_{timestamp}_{counter}"
  │           ├── 封装 JSON: {id, type, payload, timestamp}
  │           ├── dataChannel.send(buffer)
  │           └── 添加到 pendingMessages(非 ACK/HEARTBEAT)
  │
  ├── WebRTC P2P 传输(端到端加密)
  │
  └── 设备端接收
        ├── onMessage(buffer)
        ├── handleReceivedMessage()
        ├── 解析 JSON
        ├── 如果是 TEXT/COMMAND → 发送 ACK
        └── 回调 onMessageReceived

3.3 视频直播流程

csharp 复制代码
信令服务器
  └── 发送 "live" 事件
        └── {
              "event": "live",
              "data": {
                "fileHash": "xxx",
                "link": "https://..."
              }
            }
​
App 端处理
  ├── SignalingClient.onMessage()
  ├── 解析为 StreamMessageEvent
  ├── FlowBus.post("start_live", event)
  └── 订阅者调用 EasyVlcPlayer.play(link)
        ├── URL 强制改为 https://
        ├── Media(libVLC, uri)
        ├── mediaPlayer.media = media
        └── mediaPlayer.play()

4、组件关系

类依赖关系

复制代码
WebRtcManager
  ├── 持有 → SignalingClient(信令通信)
  ├── 持有 → P2PMessageManager(DataChannel 消息)
  ├── 依赖 → TurnServerConfig(TURN 服务器配置)
  └── 实现 → WebRtcListener(状态回调)
​
SignalingClient
  ├── 持有 → IotWebSocketUtils(WebSocket 连接)
  └── 实现 → SignalingListener(信令回调)
​
P2PMessageManager
  ├── 持有 → DataChannel(WebRTC 数据通道)
  └── 提供 → onMessageReceived / onRawMessageReceived 回调
​
EasyVlcPlayer
  ├── 持有 → LibVLC / MediaPlayer
  └── 实现 → OnPlayListener(播放状态回调)
​
SigUtil
  └── 加密工具类

5、状态流转

5.1 WebRTC 连接状态

scss 复制代码
[未初始化]
    │
    ▼
[初始化中] ──► WebRtcManager() 构造函数
    │
    ▼
[信令连接中] ──► SignalingClient.connect()
    │
    ▼
[信令已连接] ──► onSignalingConnected()
    │
    ▼
[Offer 发送中] ──► createAndSendOffer()
    │
    ▼
[Answer 接收中] ──► onAnswerReceived()
    │
    ▼
[ICE 交换中] ──► onIceCandidate() / addIceCandidate()
    │
    ▼
[ICE 已连接] ──► IceConnectionState.CONNECTED
    │
    ├──► [DataChannel OPEN] ──► onDataChannelConnected()
    │         │
    │         ├──► [心跳中] ──► 每 30s 发送 HEARTBEAT
    │         │
    │         └──► [P2P 通信中] ──► sendP2PMessage()
    │
    ▼
[断开连接] ──► disconnect()
    │
    └──► [已释放]

5.2 DataChannel 状态

objectivec 复制代码
CONNECTING ──► OPEN ──► CLOSED
                  │
                  ├──► 启动心跳协程
                  └──► 可收发消息

6、关键配置汇总

6.1 WebRTC 配置

配置项 说明
STUN 服务器 stun:120.79.206.28:3478 公网打洞
DataChannel Label webrtc_data_channel 通道标识
心跳间隔 30000ms 30 秒
消息超时 5000ms 5 秒
最大重试 3 次 消息重发
ICE 发送间隔 100ms 候选限流
缓冲上限 1MB DataChannel 缓冲

6.2 VLC 配置

参数 说明
--network-caching 800 网络缓冲
--live-caching 800 直播缓冲
--demux hevc H.265 解复用
--codec hevc H.265 解码
--hevc-fps 20 帧率
--avcodec-hw none 禁用硬解
--android-display-chroma RV32 颜色格式
--http-reconnect - 自动重连

7、资源释放顺序

scss 复制代码
// 1. 停止心跳
stopHeartbeat()
​
// 2. 关闭 PeerConnection
peerConnection.close()
​
// 3. 释放 EGL
eglBase.release()
​
// 4. 断开信令
signalingClient.disconnect()      // 清空 ICE 记录
signalingClient.closeWebSocket()  // 断开 WebSocket
​
// 5. 取消协程
coroutineScope.cancel()
​
// 6. VLC 释放(独立)
mediaPlayer?.stop()
mediaPlayer?.release()
libVLC?.release()

8、注意事项

  • 信令消息前缀 :所有发送的信令消息必须以 "44" 开头
  • ICE 候选去重 :同一候选不会重复发送,通过 candidateId 标识
  • sdpMid 处理 :发送 ICE 候选时 sdpMid 强制设为空字符串 ""
  • VLC 协议强制 :播放 URL 会被强制改为 https:// scheme
  • 线程安全sentIceCandidates 使用 ConcurrentHashMap 保证线程安全
  • 空 Payload 处理 :签名时 payloadnull 时使用 ByteArray(0)
  • 查询参数排序:Key 和 Value 均需按字典序升序排列

9.简单使用:

kotlin 复制代码
package com.xx.xx.iot.ui.activity
​
import android.Manifest
import android.annotation.SuppressLint
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.Surface
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.ImageView
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.lifecycleScope
import com.google.gson.Gson
import com.permissionx.guolindev.PermissionX
import com.xx.xx.iot.R
import com.xx.xx.iot.bean.LoginBean
import com.xx.xx.iot.bean.PreAddDeviceBean
import com.xx.xx.iot.bean.UserLoginBean
import com.xx.xx.iot.event.StreamMessageEvent
import com.xx.xx.iot.exception.AuthException
import com.xx.xx.iot.http.ApiException
import com.xx.xx.iot.http.ApiResult
import com.xx.xx.iot.http.EasyHttpUtil
import com.xx.xx.iot.http.weakHashMapOf
import com.xx.xx.iot.utils.IotConstants
import com.xx.xx.iot.utils.DeviceUtils
import com.xx.xx.iot.utils.FlowBus
import com.xx.xx.iot.utils.LogUtils
import com.xx.xx.iot.utils.QRCodeProvisioning
import com.xx.xx.iot.utils.SigUtil
import com.xx.xx.iot.utils.ToastUtils.showToast
import com.xx.xx.iot.utils.IotUserInfoManager
import com.xx.xx.iot.utils.ZXingUtils
import com.xx.xx.iot.webrtc.SignalingClient
import com.xx.xx.iot.webrtc.TurnServerConfig
import com.xx.xx.iot.webrtc.WebRtcManager
import com.xx.xx.iot.widget.EmptyControlVideo
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONObject
import org.webrtc.PeerConnection
import java.util.WeakHashMap
​
/**
 * @作者: smile
 * @时间: 2025/9/29 19:38
 * @描述: p2p通信测试类
 **/
class P2PCallActivity : AppCompatActivity(), WebRtcManager.WebRtcListener, SurfaceHolder.Callback {
    private val TAG = "P2PCallActivity"
    private lateinit var etMessage: EditText
    private lateinit var btnSend: Button
    private lateinit var btnCall: Button
    private lateinit var btnHangup: Button
    private lateinit var btnLogin: Button
    private lateinit var btnStart: Button
    private lateinit var btnStop: Button
    private lateinit var videoPlayer: EmptyControlVideo
​
    // 核心实例
    private lateinit var signalingClient: SignalingClient
    private var webRtcManager: WebRtcManager? = null
    private val LOCAL_USER_ID = "" // 本地用户 ID
    private val REMOTE_USER_ID = IotConstants.REMOTE_USER_ID // 目标用户 ID(通话对象)
    private lateinit var refreshToken: String
    private val ivQRCode by lazy { findViewById<ImageView>(R.id.iv_qrcode) }
    private val POLL_INTERVAL = 5000L
    private var currentIndex = 0
    private var currentJob: Job? = null
    private val liveRequestScope = CoroutineScope(Dispatchers.IO)
    private var liveRequestJob: Job? = null
​
    //private val  deviceId = "6ca7d495ff317fe1ecpkjk"
    private val deviceId = "6ca7d495ff317fe1ecpkjj"
    private lateinit var surfaceView: SurfaceView
    private var surfaceHolder: SurfaceHolder? = null
    private var downLoadJob: Job? = null
    private var videoUrl: String? = ""
    private var currentPlayUrl: String? = null
    private var isPlaying = false
​
    @SuppressLint("MissingInflatedId")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(R.layout.activity_p2p_call)
        // 初始化视图
        initView()
        // 申请相机/麦克风权限
        requestPermissions()
        initSurFaceView()
        setUpEdgeToEdge()
    }
​
    private fun setUpEdgeToEdge() {
        val mainView = findViewById<ConstraintLayout>(R.id.p2p_root)
        ViewCompat.setOnApplyWindowInsetsListener(
            mainView
        ) { v: View, windowInsets: WindowInsetsCompat? ->
            val systemBars = windowInsets!!.getInsets(WindowInsetsCompat.Type.systemBars())
            val ime = windowInsets.getInsets(WindowInsetsCompat.Type.ime())
            val displayCutout = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
            val left = maxOf(systemBars.left, displayCutout.left)
            val top = maxOf(systemBars.top, displayCutout.top)
            val right = maxOf(systemBars.right, displayCutout.right)
            val bottom = systemBars.bottom + ime.bottom
            v.setPadding(left, top, right, bottom)
            windowInsets
        }
    }
​
    private fun initSurFaceView() {
        surfaceView.let {
            surfaceHolder = it.holder.apply {
                addCallback(this@P2PCallActivity)
            }
        }
    }
​
    /**
     * 初始化视图组件
     */
    private fun initView() {
        etMessage = findViewById(R.id.et_message)
        btnSend = findViewById(R.id.btn_send)
        btnCall = findViewById(R.id.btn_call)
        btnHangup = findViewById(R.id.btn_hangup)
        btnLogin = findViewById(R.id.btn_login)
        btnStart = findViewById(R.id.btn_start_live)
        btnStop = findViewById(R.id.btn_stop_live)
        surfaceView = findViewById(R.id.surfaceView)
        videoPlayer = findViewById(R.id.video_player)
        // 按钮点击事件
        btnCall.setOnClickListener { startP2PCall() }
        btnHangup.setOnClickListener { hangupCall() }
        btnSend.setOnClickListener { sendP2PMessage() }
        btnLogin.setOnClickListener { initPassWordLogin() }
        btnStart.setOnClickListener {
            val userId = IotUserInfoManager.getIotUserId()
            if (TextUtils.isEmpty(userId)) {
                this.showToast("请先登录")
                return@setOnClickListener
            }
            getStartLiveLoop(deviceId)
        }
        btnStop.setOnClickListener { getStopLive(deviceId) }
​
        FlowBus.with<StreamMessageEvent>("start_live").register(this@P2PCallActivity) { it ->
            LogUtils.d(TAG, "===接收到的消息为===" + Gson().toJson(it))
            LogUtils.d(TAG, "===接收到的消息url为===" + it.link)
            val newUrl = it.link
            if (newUrl.isNullOrEmpty()) {//newUrl == currentPlayUrl为单帧
                return@register
            }
            lifecycleScope.launch {
                withContext(Dispatchers.Main) {
                    performSmoothStreamSwitch(newUrl)
                }
            }
        }
        FlowBus.with<StreamMessageEvent>("stop_live").register(this@P2PCallActivity) {
            LogUtils.d(TAG, "===接收到的消息为===" + Gson().toJson(it))
        }
    }
​
    private fun performSmoothStreamSwitch(newUrl: String) {
        try {
            currentPlayUrl = newUrl
            val playState = videoPlayer.currentState
            LogUtils.d(TAG, "===播放状态为=== $playState")
            videoPlayer.setUp(newUrl, false, "")
            if (!isPlaying) {
                videoPlayer.startPlayLogic()
                isPlaying = true
            } else {
                videoPlayer.onVideoPause()
                videoPlayer.onVideoResume()
            }
            LogUtils.d(TAG, "===播放状态为1111=== $isPlaying")
        } catch (e: Exception) {
            Log.e(TAG, "流切换异常: ${e.message}")
            currentPlayUrl = null
            isPlaying = false
        }
    }
​
    private fun initPassWordLogin() {
        lifecycleScope.launch {
            EasyHttpUtil.post(
                urlStr = IotConstants.BASE_URL + IotConstants.USER_LOGIN,
                bodyParams = weakHashMapOf("account" to "18709287807", "password" to "a123456"),
                clazz = LoginBean::class.java,
                mapKey = null
            ).catch { e ->
                val errorMsg = "${e.message}"
                LogUtils.e(TAG, errorMsg)
            }.first().let { result ->
                when (result) {
                    is ApiResult.Success -> {
                        val userInfo = result.data
                        LogUtils.d(TAG, "===密码登录成功(获取 token)===${userInfo?.userInfo?.token}")
                        initAuthCode(token = userInfo?.userInfo?.token.toString())
                    }
                    is ApiResult.Error -> {
                        throw ApiException(result.code, result.message)
                    }
                }
            }
        }
    }
​
    private fun initAuthCode(token: String) {
        lifecycleScope.launch {
            EasyHttpUtil.post(
                urlStr = IotConstants.BASE_URL + IotConstants.GET_AUTH_CODE,
                bodyParams = weakHashMapOf("" to "", "" to ""),
                headers = weakHashMapOf("token" to token, "site-label" to "cn"),
                clazz = String::class.java
            ).catch { e ->
                val errorMsg = "${e.message}"
                LogUtils.e(TAG, "===授权码获取失败===$errorMsg")
​
            }.collect { code ->
                LogUtils.d(TAG, "===授权码获取成功===$code")
                initLogin(code.toString())
            }
        }
    }
​
    private fun initLogin(code: String) {
        val jsonObject = JSONObject().apply {
            put("code", code)
            put("channel", IotConstants.CHANNEL_PETGUGU)
        }
        val apkSignKey = IotConstants.APP_SECRET_KEY_PETGUGU
        val url = IotConstants.USER_LOGIN
        val payLoad = jsonObject.toString().toByteArray(Charsets.UTF_8)
        val authorization = SigUtil.sigV1(apkSignKey, url, emptyMap(), payLoad)
        val baseHeaders = WeakHashMap<String, String>().apply {
            put("authorization", authorization)
            put("accept-language", "en-us")
            put("x-app-id", "10003")
            put("x-app-store", "google")
            put("x-sdk-version", "100")
            put("x-platform", "android")
            put("x-app-version", "10003")
            put("Content-Type", "application/json; charset=utf-8")
            put("x-app-pkgname", IotConstants.APP_PACKAGE_PET_GUGU)
        }
        lifecycleScope.launch {
            try {
                EasyHttpUtil.post(
                    urlStr = IotConstants.BASE_IOT_URL + IotConstants.USER_LOGIN,
                    bodyParams = weakHashMapOf(
                        "code" to code,
                        "channel" to IotConstants.CHANNEL_PETGUGU
                    ),
                    headers = baseHeaders,
                    clazz = UserLoginBean::class.java
                ).catch { e ->
                    val errorMsg = "${e.message}"
                    LogUtils.e(TAG, "用户鉴权登录失败 = $errorMsg")
                }.first().let { result ->
                    when (result) {
                        is ApiResult.Success -> {
                            val userInfo = result.data
                            LogUtils.d(TAG, "===用户鉴权登录成功(获取用户信息)===$userInfo")
                            userInfo?.let {
                                IotUserInfoManager.setIotUserId(it.user_id.toString())
                                IotUserInfoManager.setIotAccessToken(it.access_token.toString())
                                IotUserInfoManager.setIotRefreshToken(it.refresh_token.toString())
                                preAddDevice(it.user_id.toString())
                            }
                        }
                        is ApiResult.Error -> {
                            throw ApiException(result.code, result.message)
                        }
                    }
                }
            } catch (e: AuthException) {
                LogUtils.d(TAG, "用户鉴权登录失败 = ${e.message}")
            }
        }
    }
​
    private fun preAddDevice(userId: String) {
        if (TextUtils.isEmpty(IotUserInfoManager.getIotUserId())) {
            this.showToast("请先登录")
            return
        }
        val accessToken = IotUserInfoManager.getIotAccessToken()
        val requestBody = weakHashMapOf<String, Any>("timezone" to "GMT+8,Asia/Shanghai")
        val baseHeaders = WeakHashMap<String, String>().apply {
            put("authorization", "Bearer $accessToken")
            put("accept-language", "en-us")
            put("x-app-id", "10003")
            put("x-app-store", "google")
            put("x-sdk-version", "100")
            put("x-platform", "android")
            put("x-app-version", "10003")
            put("Content-Type", "application/json; charset=utf-8")
            put("x-app-pkgname", IotConstants.APP_PACKAGE_PET_GUGU)
        }
        lifecycleScope.launch {
            EasyHttpUtil.post(
                urlStr = IotConstants.BASE_IOT_URL + IotConstants.USER_DEVICE_PRO_ADD,
                bodyParams = requestBody,
                headers = baseHeaders,
                clazz = PreAddDeviceBean::class.java
            ).onStart {
                LogUtils.d(TAG, "用户预添加设备开始")
            }.catch { exception ->
                val errorMsg = exception.message.toString()
                LogUtils.e(TAG, "用户预添加设备失败= $errorMsg")
            }.first().let { result ->
                when (result) {
                    is ApiResult.Success -> {
                        val preAddDeviceBean = result.data
                        LogUtils.d(TAG, "用户预添加成功: ${preAddDeviceBean?.token}")
                        createQrcode(userId, preAddDeviceBean?.token.toString())
                    }
                    is ApiResult.Error -> {
                    }
                }
            }
        }
    }
​
    private fun createQrcode(userId: String, token: String) {
        val ssid = "UWIFI-35-2.4G"
        val password = "UW123456"
        val currentQrStr: Array<String> =
            QRCodeProvisioning.getProvisioningStr(ssid, password, userId, token)
        LogUtils.d(TAG, "===生成的二维码为=== ${Gson().toJson(currentQrStr)}")
        val qrCodeUrl = mutableListOf<String>().apply {
            currentQrStr.forEach { str -> add(str) }
        }
        val qrBitmaps = try {
            qrCodeUrl.map { url ->
                ZXingUtils.Create2DCode(
                    url,
                    DeviceUtils.dpToPx(this@P2PCallActivity, 300),
                    DeviceUtils.dpToPx(this@P2PCallActivity, 300)
                ) ?: throw IllegalArgumentException("生成二维码失败:$url")
            }
        } catch (e: Exception) {
            LogUtils.e(TAG, "二维码预生成失败 ${e.message}")
            return
        }
        currentJob?.cancel()
        currentIndex = 0
        ivQRCode.setImageBitmap(qrBitmaps[currentIndex])
        currentJob = lifecycleScope.launch {
            while (true) {
                delay(POLL_INTERVAL)
                currentIndex = (currentIndex + 1) % qrBitmaps.size
                ivQRCode.setImageBitmap(qrBitmaps[currentIndex])
            }
        }
    }
​
    /**
     * 下发开始播放设备指令
     */
    private fun getStartLiveLoop(deviceId: String) {
        liveRequestJob?.cancel()
        liveRequestJob = liveRequestScope.launch {
            while (isActive) {
                // 每隔1秒执行1次
                delay(1000)
                val requestBody = weakHashMapOf<String, Any>(
                    "device_id" to deviceId,
                    "directive" to IotConstants.USER_START_LIVE
                )
                val requestHeaders = weakHashMapOf(
                    "x-app-pkgname" to IotConstants.APP_PACKAGE_PET_GUGU,
                    "accept-language" to "en-us",
                    "x-app-id" to "10003"
                )
                EasyHttpUtil.post(
                    urlStr = IotConstants.BASE_IOT_URL + IotConstants.USER_START_LIVE,
                    bodyParams = requestBody,
                    headers = requestHeaders,
                    clazz = Int::class.java
                ).onStart {
                    LogUtils.d(TAG, "开始播放指令下发开始:")
                }.catch { exception ->
                    val errorMsg = exception.message.toString()
                    LogUtils.e(TAG, "开始播放指令下发失败: $errorMsg")
                }.first().let { result ->
                    when (result) {
                        is ApiResult.Success -> {
                            val requestSuccess = result.data == 200
                            LogUtils.d(TAG, "指令下发:${if (requestSuccess) "成功" else "失败"}")
                        }
                        is ApiResult.Error -> {
                        }
                    }
                }
            }
        }
    }
​
​
    private fun stopLiveLoop() {
        liveRequestJob?.cancel()
    }
​
    private fun getStopLive(deviceId: String) {
        val requestBody = weakHashMapOf<String, Any>(
            "device_id" to deviceId, "directive" to IotConstants.USER_STOP_LIVE
        )
        val requestHeaders = weakHashMapOf(
            "x-app-pkgname" to IotConstants.APP_PACKAGE_PET_GUGU,
            "accept-language" to "en-us",
            "x-app-id" to "10003"
        )
        lifecycleScope.launch {
            EasyHttpUtil.post(
                urlStr = IotConstants.BASE_IOT_URL + IotConstants.USER_STOP_LIVE,
                bodyParams = requestBody,
                headers = requestHeaders,
                clazz = Int::class.java
            ).onStart {
                LogUtils.d(TAG, "停止播放指令下发开始:")
            }.catch { exception ->
                val errorMsg = exception.message.toString()
                LogUtils.d(TAG, "停止播放指令下发出错: $errorMsg")
            }.first().let { result ->
                when (result) {
                    is ApiResult.Success -> {
                        val requestSuccess = result.data == 200
                        LogUtils.d(TAG, "停止播放指令下发成功: $requestSuccess")
                    }
                    is ApiResult.Error -> {
                    }
                }
            }
        }
    }
​
    /**
     * 申请相机/麦克风权限
     */
    private fun requestPermissions() {
        PermissionX.init(this)
            .permissions(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
            .onExplainRequestReason { scope, deniedList ->
                scope.showRequestReasonDialog(
                    deniedList,
                    "需要相机和麦克风权限才能进行 P2P 通话",
                    "确定",
                    "取消"
                )
            }
            .onForwardToSettings { scope, deniedList ->
                scope.showForwardToSettingsDialog(
                    deniedList,
                    "请在设置中开启相机和麦克风权限",
                    "确定",
                    "取消"
                )
            }
            .request { allGranted, _, _ ->
                if (allGranted) {
                    initP2P() // 权限通过,初始化 P2P 组件
                } else {
                    Toast.makeText(this, "缺少必要权限,无法进行 P2P 通话", Toast.LENGTH_SHORT)
                        .show()
                    finish()
                }
            }
    }
​
    /**
     * 初始化 P2P 组件(信令 + WebRTC)
     */
    private fun initP2P() {
        val userId = IotUserInfoManager.getIotUserId()
        LogUtils.d(TAG, "===用户id为===$userId")
        if (TextUtils.isEmpty(userId)) {
            this.showToast("请先登录")
            return
        }
        // 1. 初始化信令客户端
        signalingClient = SignalingClient(
            localUserId = userId,
            remoteUseId = REMOTE_USER_ID
        )
        // 2. 配置 TURN 服务器(根据项目实际地址修改,这里都是模拟的地址)
        val turnConfig = TurnServerConfig(
            turnUrl = "turn:127.79.206.28:3477",
            username = "anxt",
            password = "aa123456"
        )
        // 3. 初始化 WebRtcManager
        webRtcManager = WebRtcManager(
            context = this,
            signalingClient = signalingClient,
            remoteUserId = REMOTE_USER_ID,
            turnConfig = turnConfig
        )
        webRtcManager?.setListener(this) // 绑定监听
        signalingClient.connect() // 连接信令服务器
    }
​
    /**
     * 发起 P2P 通话(发送 Offer)
     */
    private fun startP2PCall() {
        webRtcManager?.createAndSendOffer()
        btnCall.isEnabled = false
        btnCall.alpha = 0.4f
        this.showToast("正在发起通话...")
    }
​
    /**
     * 挂断 P2P 通话
     */
    private fun hangupCall() {
        webRtcManager?.disconnect()
        btnCall.isEnabled = true
        btnSend.isEnabled = false
        btnCall.alpha = 1f
        btnSend.alpha = 0.4f
        this.showToast("通话已挂断")
    }
​
    /**
     * 发送 P2P 消息(文字)
     */
    private fun sendP2PMessage() {
        val message = etMessage.text.toString().trim()
        if (message.isEmpty()) {
            this.showToast("消息不能为空")
            return
        }
        webRtcManager?.sendP2PMessage(message) { success, error ->
            if (success) {
                LogUtils.d(TAG, "消息发送成功")
            } else {
                LogUtils.e(TAG, "消息发送失败: $error")
            }
        }
        etMessage.text.clear()
    }
​
    override fun onInitialized() {
        runOnUiThread {
            this.showToast("WebRTC 初始化成功")
        }
    }
​
    override fun onSignalingConnected() {
        runOnUiThread {
            this.showToast("信令服务器连接成功")
        }
    }
​
    override fun onConnected() {
        LogUtils.d(TAG, "===p2p 通话连接成功===")
        runOnUiThread {
            this.showToast("P2P 通话连接成功")
            btnSend.isEnabled = true // 连接成功后,启用消息发送按钮
            btnSend.alpha = 1f
        }
    }
​
    override fun onCompleted() {
        LogUtils.d(TAG, "===p2p 通话连接完成===")
    }
​
    override fun onDisconnected() {
        LogUtils.d(TAG, "===p2p 通话连接断开===")
        runOnUiThread {
            this.showToast("P2P 连接已断开")
            btnCall.isEnabled = true
            btnSend.isEnabled = false
            btnCall.alpha = 1f
            btnSend.alpha = 0.4f
        }
    }
​
    override fun onDataChannelConnected() {
        runOnUiThread {
            this.showToast("数据通道已打开,可发送消息")
        }
    }
​
    override fun onDataChannelConnecting() {
        runOnUiThread {
            this.showToast("数据通道已连接成功")
        }
    }
​
    override fun onDataChannelClosed() {
        runOnUiThread {
            this.showToast("数据通道已关闭")
        }
    }
​
    override fun onDataChannelError(message: String) {
        runOnUiThread {
            this.showToast("数据通道出错:$message")
        }
    }
​
    override fun onMessageReceived(message: String) {
        LogUtils.d(TAG, "收到消息: $message")
        runOnUiThread {
            this.showToast("收到消息: $message")
        }
    }
​
    override fun onConnectionStateChange(state: PeerConnection.IceConnectionState) {
        // 可根据状态更新 UI(如显示"连接中""已断开")
    }
​
    override fun onSignalingStateChange(state: PeerConnection.SignalingState) {}
​
    override fun onIceGatheringStateChange(state: PeerConnection.IceGatheringState) {}
​
​
    override fun onError(message: String) {
        runOnUiThread {
            this.showToast("错误: $message")
            LogUtils.e(TAG, "WebRTC 错误: $message")
        }
    }
​
    override fun surfaceCreated(holder: SurfaceHolder) {
        Log.d(TAG, "surfaceCreated")
        lifecycleScope.launch(Dispatchers.IO) {
            try {
                // parseAndPlay(holder.surface)
            } catch (e: Exception) {
                Log.e(TAG, "解析播放异常" + e.message)
            }
        }
    }
​
    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
​
    }
​
    override fun surfaceDestroyed(holder: SurfaceHolder) {
        downLoadJob?.cancel()
    }
​
    private fun parseAndPlay(surface: Surface) {
        downLoadJob?.cancel()
        downLoadJob = lifecycleScope.launch(Dispatchers.IO) {
            try {
                LogUtils.d(TAG, "===视频下载url为=== $videoUrl")
                if (videoUrl.isNullOrEmpty()) {
                    return@launch
                }
                if (videoUrl!!.isNotEmpty() && videoUrl!!.startsWith("http") || videoUrl!!.startsWith(
                        "https"
                    )
                ) {
​
                }
            } catch (e: Exception) {
                Log.e(TAG, "下载/解析/播放异常", e)
            }
        }
    }
​
​
    override fun onResume() {
        super.onResume()
        videoPlayer.onVideoResume()
    }
​
    override fun onPause() {
        super.onPause()
        videoPlayer.onVideoPause()
        videoPlayer.isShowPauseCover = true
    }
​
    override fun onDestroy() {
        super.onDestroy()
        //暂时不断开websocket连接
        /*        if (::webRtcManager.isInitialized) {
                    webRtcManager.disconnect()
                }*/
        stopLiveLoop()
        //getStopLive(deviceId)
        videoPlayer.release()
    }
}
css 复制代码
📤 构造POST请求体:{"IP":"192.168.1.162","Mode":"Map","Port":"12305","Type":"Register"}
2025-12-08 15:54:56.172 23245-23298 P2pManager 
📲 设备连接成功(P2P建立):192.168.0.110:39398
2025-12-08 15:54:56.174 23245-24762 P2pManager 
✅ POST请求成功,服务器响应:{"Result": "Register OK"}
📥 接收设备二进制数据(192.168.0.110):
 原始字节数:1448
2025-12-08 15:54:56.414 23245-23298 P2pManager com.example.tcpdemo D 16进制:00 00 00 00 02 59 02 59 0B B8 0B B8 00 00 2C EA 72 08 00 01 60 BD D0 09 1F FF 01 00 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF E7 2F FD 55 6A 92 82 2F 40 7F 97 00 81 2F D0 1F 96 00 81 2F F4 04 96 00 81 3F FD 00 3F 97 00 81 2F 40 03 96 00 7E 5F FD 55 75 50 00 96 00 7F 3F 40 15 00 C3 01 80 4F 40 00 00 00 C3 01 6F 0C 36 05 4F D0 00 00 00 C3 01 70 29 40 0F 47 02 10 F4 96 00 0A 13 00 0F 02 00 61 1A D0 1A 01 10 FD 96 00 0F C3 01 6F 1B F4 DD 02 00 59 02 0A A9 00 0F 02 00 61 1B FD 47 02 01 59 02 1F 5F 8A 00 61 09 02 00 1A 40 47 02 69 F5 50 00 00 00 07 22 00 0F 02 00 61 1B D0 2D 01 00 F0 02 1F 01 89 00 61 09 02 00 1B F4 74 03 10 55 F0 02 10 4F 34 04 0F 6B 09 69 1B FD 74 03 BF 50 00 00 05 50 03 FF FF FD 40 00 6B 09 69 1A 40 64 06 50 D4 01 00 05 40 86 03 2F 40 00 6B 09 69 1A D0 2D 01 41 FD 00 00 55 D5 01 1F D0 F5 02 6A 00 DF 02 08 54 09 40 40 00 5F 40 0C 07 1F F4 F5 02 6A 00 CF 05 08 CE 05 40 D0 00 55 50 A8 00 2F C0 00 59 02 6D 00 48 02 04 02 00 20 F5 55 87 03 4F 0F FF C0 00 59 02 6D 0A 4A 05 00 3E 01 10 00 96 00 0F 4E 05 69 00 59 02 08 2D 01 00 5B 02 70 00 00 3C 00 00 05 41 0D 00 0F 02 00 64 01 1B 01 09 77 06 00 0D 07 5F 0C 00 00 00 55 B2 04 6C 0A 77 06 01 3E 01 4F C0 00 00 17 28 01 64 01 02 00 00 1B 01 09 D1 08 01 3E 01 9F 30 00 00 01 55 55 55 FF 55 42 08 64 10 D4 2C 01 09 F1 02 00 59 02 00 C3 01 5F 00 00 00 7F D0 F4 02 63 21 D5 54 7F 00 08 97 00 01 3E 01 02 02 00 2F 1D 54 4D 05 63 01 46 02 00 02 00 01 CF 01 01 02 00 05 13 00 3F 00 00 07 A6 07 64 01 46 02 00 02 00 06 88 03 05 13 00 4F 00 00 01 50 4D 05 64 00 46 02 01 02 00 01 28 01 01 02 00 05 13 00 00 02 00 0F 59 02 60 40 FD 55 55 D5 7B 00 00 02 00 06 2D 01 07 59 02 2F 00 00 FF 09 61 30 40 00 7F 7B 00 00 02 00 00 A5 07 63 F5 55 55 75 55 55 01 03 02 02 00 0F 68 09 61 15 D0 05 04 00 2A 08 50 FF FD 00 00 1D 0B 00 06 02 00 21 01 57 D5 01 0F 02 00 5B 07 70 03 01 1D 04 10 40 8C 0A 03 94 00 10 01 08 00 1F 01 78 06 61 27 FD 54 2D 01 02 3C 01 06 02 00 00 88 0D 0F 67 09 63 07 07 04 00 78 06 08 3C 01 68 00 14 10 00 00 54 AA 0A 0F 02 00 57 16 F0 1C 01 00 DD 0C 06 0E 00 3F 00 00 05 ED 02 64 00 02 00 07 97 00 45 3F FF FF 55 57 02 01 44 05 0F 55 0C 67 00 81 00 02 02 00 00 B3 04 02 AE 07 01 02 00 20 01 75 37 08 00 BA 00 0F 02 00 60 11 FC 83 00 00 02 00 00 C8 05 03 69 02 00 33 01 3F 15 55 55 FB 09 68 10 C0 92 00 00 02 00 01 07 04 00 90 00 02 96 00 3F 00 00 14 07 07 64 01 02 00 05 97 00 00 C9 05 41 D5 00 00 10 03 00 4F 00 00 00 04 58 02 67 07 5B 02 21 05 FF 64 10 13 04 43 05 2F 00 01 AD 0E 6A 05 5B 02 90 7F 55 FF D5 40 05 47 40 05 C5 08 5F 00 00 00 40 15 DC 05 61 04 02 00 04 1F 04 20 15 50 9C 07 40 15 50 00 15 AD 04 0F 64 13 6E 01 83 00 00 02 00 13 14 8F 00 03 39 08 0F 22 0B 63 04 02 00 05 C4 01 03 95 00 20 00 11 A3 00 2F 00 00 C2 01 6A 06 88 03 12 01 1A 04 01 05 07 0F C2 01 6E 06 2D 01 03 96 00 11 04 C9 08 0F 84 03 6A 00 02 00 04 F1 02 02 24 01 31 00 04 00 85 03 0F CA 08 63 07 02 00 05 4C 05 01 02 00 31 05 00 04 08 00 00 59 05 0F 02 00 69 04 96 00 51 15 40 05 00 44 92 00 00 02 00 00 1C 07 0F 02 00 69 04 96 00 50 01 40 00 10 10 76 06 01 02 00 01 6F 10 0F 97 00 68 04 59 02 00 E4 02 13 01 C0 01 02 02 00 0F E0 05 69 05 86 03 00 6A 06 05 8C 03 00 02 00 0F 5C 02 69 04 C2 01 00 02 00 25 10 40 C3 01 6F 00 00 00 05 57 F5 33 12 64 28 55 43 96 00 29 04 10 C3 01 2F 01 55 D3 08 64 09 C7 0F 24 00 01 CC 08 05 02 00 0F 11 07 63 08 74 0D 00 72 0D 01 E2 0C 06 02 00 2F 01 00 C4 01 62 03 B0 0B 0F 02 00 01 02 84 06 1F 54 BA 04 62 02 7C 00 0F 02 00 01 02 84 06 1F 15 C4 01 62 0A 59 02 08 02 00 7F 04 00 05 40 00 00 15 13 07 62 08 59 02 09 02 00 9F 10 01 00 05 D0 00 00 05 40 B7 04 61 0F 59 02 06 AF 05 40 40 01 55 50 00 15 40 05 E7 16 61 0F 59 02 06 8F 05 11 50 01 74 00 07 50 B4 04 
2025-12-08 15:54:56.415 23245-23298 P2pManager com.example.tcpdemo D 📤 向设备发送二进制响应:41 50 50 5F 52 45 43 45 49 56 45 44 5F 4F 4B
2025-12-08 15:54:56.418 23245-23298 P2pManager com.example.tcpdemo D 📥 接收设备二进制数据(192.168.0.110):
 原始字节数:1088
 16进制:13 5D 05 59 02 02 49 0C 24 00 17 A8 00 10 14 9B 00 41 1F F4 00 05 25 0E 0F 97 00 5C 04 59 02 22 01 40 49 0C 30 07 FF D5 4D 02 03 CA 01 22 07 FD 68 13 3F 00 00 C0 98 00 5A 05 59 02 21 15 15 8E 00 11 15 0B 11 00 88 00 54 00 15 41 50 01 3A 01 0F 5D 02 5D 03 83 00 31 00 00 07 F2 13 00 8F 07 03 13 00 61 07 D5 74 05 7F D5 58 02 01 02 00 00 33 18 0F 02 00 55 06 59 02 01 CD 01 25 01 55 91 14 12 01 6F 27 31 55 00 30 95 00 0F 5D 02 5A 01 8C 00 01 02 00 11 7D 06 00 21 00 7F A6 04 00 02 00 03 51 0C 20 C0 0F 0D 00 01 35 0B 0F 2B 01 55 06 D8 16 22 55 5F BA 01 32 1F D0 01 F9 02 08 8A 14 02 76 0C 0F D8 08 58 02 8C 00 42 00 00 5F FF 0B 1B 24 07 F4 BC 04 00 64 02 04 02 00 02 E1 0B 03 3E 01 0F 02 00 50 04 59 02 13 FF BA 01 15 01 97 03 18 01 7E 00 01 3A 0E 0F D8 08 57 03 59 02 60 03 FF D0 00 00 55 01 00 14 7F 3E 01 0A F3 09 00 97 00 08 3E 01 0F 02 00 4B 03 1B 01 30 0F FF F4 70 06 00 85 1B 04 3E 01 08 59 02 02 58 12 09 3E 01 0F 02 00 4B 02 1B 01 10 3F F8 02 11 05 BC 12 03 3E 01 2B 00 00 F3 09 12 F0 9D 17 0F 02 00 53 01 1B 01 00 6B 00 00 78 06 25 00 57 3E 01 0E F6 13 2F C0 10 EF 02 57 00 1B 01 00 B8 05 00 FE 03 11 50 2D 01 02 3E 01 00 B9 0E 0B 20 0B 1B F5 10 00 0F 02 00 48 00 59 02 00 70 0A 00 96 00 11 14 FE 03 01 3E 01 01 02 00 0F DF 02 57 0D 02 00 21 40 40 11 08 00 65 03 11 05 73 28 00 3E 01 02 02 00 11 01 1A 00 0F 02 00 62 12 D5 98 01 00 96 00 11 01 73 28 00 3E 01 03 02 00 00 45 01 0F 02 00 6A 10 C0 BD 0B 1A 54 59 02 02 07 03 0F 02 00 69 01 36 19 01 59 02 13 D5 09 07 00 02 00 00 08 03 0F 02 00 69 60 00 00 07 D0 00 15 57 06 03 58 02 10 04 FF 13 0F 4B 0C 5F 0A 02 00 40 00 00 55 F4 96 00 10 07 50 05 01 02 00 11 40 23 04 0A 26 00 0F 02 00 5C 80 F0 00 00 5F FD 00 07 40 59 02 01 8E 00 01 BF 04 0F 2D 01 6D 10 F0 76 06 30 55 55 D0 B2 04 01 2D 0B 02 C1 15 0C 2F 29 0F 02 00 5C 12 F0 AC 01 62 F5 50 00 1F F5 55 2D 01 00 2D 12 00 02 00 0F 8A 00 5C 08 02 00 03 96 00 71 FF F4 00 07 F5 00 15 06 0E 01 14 0E 0F C4 01 6B 10 FC 22 2D 01 33 0B 43 01 FD 00 01 C5 0B 03 02 00 01 19 00 0F 02 00 63 04 96 00 61 40 00 7F 40 00 1F 2D 01 04 02 00 02 FD 05 0F 02 00 62 13 F0 31 05 63 40 00 15 50 00 05 5B 02 02 02 00 0F A5 07 68 04 02 00 54 D0 00 05 00 14 5F 1D 02 02 00 0F 84 0A 58 0F 02 00 05 53 F5 55 55 40 55 77 10 04 02 00 0F EE 09 57 0F 02 00 07 36 FD 40 5F 63 09 00 3A 23 0F 0E 18 62 0D 02 00 23 40 1F 0B 07 0E 26 0B 0F 02 00 68 31 D0 07 FD 6D 06 00 02 00 0F 54 02 70 03 02 00 20 F5 55 88 0D 03 02 00 0F 54 02 71 04 02 00 06 90 0A 0F 31 0F 57 0F 02 00 10 02 0B 07 01 95 14 0F 2E 00 10 0F 02 00 55 05 86 03 0F 59 02 77 45 FF FF FF F5 2D 01 0F 59 02 79 00 0E 11 02 02 00 0F C3 01 7A 05 B3 04 0F C3 01 7A 06 86 03 0F C3 01 7A 05 59 02 0F 1C 04 7A 02 86 03 1F 03 C9 08 72 06 02 00 02 59 02 2F 01 D0 B7 12 59 0F 02 00 0F 02 59 02 2F 01 74 C3 01 7C 02 59 02 1F 7D C3 01 7C 02 59 02 11 1F B9 0E 0F 02 00 77 10 F5 53 16 3F 55 57 D5 31 2D 69 0F 02 00 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF 69 50 FF FF FF FF C0
2025-12-08 15:54:56.418 23245-23298 P2pManager com.example.tcpdemo D 📤 向设备发送二进制响应:41 50 50 5F 52 45 43 45 49 56 45 44 5F 4F 4B
2025-12-08 15:54:57.427 23245-23298 P2pManager com.example.tcpdemo D 📥 接收设备二进制数据(192.168.0.110):
 原始字节数:2536
2025-12-08 15:54:57.427 23245-23298 P2pManager com.example.tcpdemo D 16进制:00 00 00 00 02 59 02 59 0B B8 0B B8 00 00 2C EA 72 08 00 01 60 BD D0 09 1F FF 01 00 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF E7 2F FD 55 6A 92 82 2F 40 7F 97 00 81 2F D0 1F 96 00 81 2F F4 04 96 00 81 3F FD 00 3F 97 00 81 2F 40 03 96 00 7E 5F FD 55 75 50 00 96 00 7F 3F 40 15 00 C3 01 80 4F 40 00 00 00 C3 01 6F 0C 36 05 4F D0 00 00 00 C3 01 70 29 40 0F 47 02 10 F4 96 00 0A 13 00 0F 02 00 61 1A D0 1A 01 10 FD 96 00 0F C3 01 6F 1B F4 DD 02 00 59 02 0A A9 00 0F 02 00 61 1B FD 47 02 01 59 02 1F 5F 8A 00 61 09 02 00 1A 40 47 02 69 F5 50 00 00 00 07 22 00 0F 02 00 61 1B D0 2D 01 00 F0 02 1F 01 89 00 61 09 02 00 1B F4 74 03 10 55 F0 02 10 4F 34 04 0F 6B 09 69 1B FD 74 03 BF 50 00 00 05 50 03 FF FF FD 40 00 6B 09 69 1A 40 64 06 50 D4 01 00 05 40 86 03 2F 40 00 6B 09 69 1A D0 2D 01 41 FD 00 00 55 D5 01 1F D0 F5 02 6A 00 DF 02 08 54 09 40 40 00 5F 40 0C 07 1F F4 F5 02 6A 00 CF 05 08 CE 05 40 D0 00 55 50 A8 00 2F C0 00 59 02 6D 00 48 02 04 02 00 20 F5 55 87 03 4F 0F FF C0 00 59 02 6D 0A 4A 05 00 3E 01 10 00 96 00 0F 4E 05 69 00 59 02 08 2D 01 00 5B 02 70 00 00 3C 00 00 05 41 0D 00 0F 02 00 64 01 1B 01 09 77 06 00 0D 07 5F 0C 00 00 00 55 B2 04 6C 0A 77 06 01 3E 01 4F C0 00 00 17 28 01 64 01 02 00 00 1B 01 09 D1 08 01 3E 01 9F 30 00 00 01 55 55 55 FF 55 42 08 64 10 D4 2C 01 09 F1 02 00 59 02 00 C3 01 5F 00 00 00 7F D0 F4 02 63 21 D5 54 7F 00 08 97 00 01 3E 01 02 02 00 2F 1D 54 4D 05 63 01 46 02 00 02 00 01 CF 01 01 02 00 05 13 00 3F 00 00 07 A6 07 64 01 46 02 00 02 00 06 88 03 05 13 00 4F 00 00 01 50 4D 05 64 00 46 02 01 02 00 01 28 01 01 02 00 05 13 00 00 02 00 0F 59 02 60 40 FD 55 55 D5 7B 00 00 02 00 06 2D 01 07 59 02 2F 00 00 FF 09 61 30 40 00 7F 7B 00 00 02 00 00 A5 07 63 F5 55 55 75 55 55 01 03 02 02 00 0F 68 09 61 15 D0 05 04 00 2A 08 50 FF FD 00 00 1D 0B 00 06 02 00 21 01 57 D5 01 0F 02 00 5B 07 70 03 01 1D 04 10 40 8C 0A 03 94 00 10 01 08 00 1F 01 78 06 61 27 FD 54 2D 01 02 3C 01 06 02 00 00 88 0D 0F 67 09 63 07 07 04 00 78 06 08 3C 01 68 00 14 10 00 00 54 AA 0A 0F 02 00 57 16 F0 1C 01 00 DD 0C 06 0E 00 3F 00 00 05 ED 02 64 00 02 00 07 97 00 45 3F FF FF 55 57 02 01 44 05 0F 55 0C 67 00 81 00 02 02 00 00 B3 04 02 AE 07 01 02 00 20 01 75 37 08 00 BA 00 0F 02 00 60 11 FC 83 00 00 02 00 00 C8 05 03 69 02 00 33 01 3F 15 55 55 FB 09 68 10 C0 92 00 00 02 00 01 07 04 00 90 00 02 96 00 3F 00 00 14 07 07 64 01 02 00 05 97 00 00 C9 05 41 D5 00 00 10 03 00 4F 00 00 00 04 58 02 67 07 5B 02 21 05 FF 64 10 13 04 43 05 2F 00 01 AD 0E 6A 05 5B 02 90 7F 55 FF D5 40 05 47 40 05 C5 08 5F 00 00 00 40 15 DC 05 61 04 02 00 04 1F 04 20 15 50 9C 07 40 15 50 00 15 AD 04 0F 64 13 6E 01 83 00 00 02 00 13 14 8F 00 03 39 08 0F 22 0B 63 04 02 00 05 C4 01 03 95 00 20 00 11 A3 00 2F 00 00 C2 01 6A 06 88 03 12 01 1A 04 01 05 07 0F C2 01 6E 06 2D 01 03 96 00 11 04 C9 08 0F 84 03 6A 00 02 00 04 F1 02 02 24 01 31 00 04 00 85 03 0F CA 08 63 07 02 00 05 4C 05 01 02 00 31 05 00 04 08 00 00 59 05 0F 02 00 69 04 96 00 51 15 40 05 00 44 92 00 00 02 00 00 1C 07 0F 02 00 69 04 96 00 50 01 40 00 10 10 76 06 01 02 00 01 6F 10 0F 97 00 68 04 59 02 00 E4 02 13 01 C0 01 02 02 00 0F E0 05 69 05 86 03 00 6A 06 05 8C 03 00 02 00 0F 5C 02 69 04 C2 01 00 02 00 25 10 40 C3 01 6F 00 00 00 05 57 F5 33 12 64 28 55 43 96 00 29 04 10 C3 01 2F 01 55 D3 08 64 09 C7 0F 24 00 01 CC 08 05 02 00 0F 11 07 63 08 74 0D 00 72 0D 01 E2 0C 06 02 00 2F 01 00 C4 01 62 03 B0 0B 0F 02 00 01 02 84 06 1F 54 BA 04 62 02 7C 00 0F 02 00 01 02 84 06 1F 15 C4 01 62 0A 59 02 08 02 00 7F 04 00 05 40 00 00 15 13 07 62 08 59 02 09 02 00 9F 10 01 00 05 D0 00 00 05 40 B7 04 61 0F 59 02 06 AF 05 40 40 01 55 50 00 15 40 05 E7 16 61 0F 59 02 06 8F 05 11 50 01 74 00 07 50 B4 04 
2025-12-08 15:54:57.428 23245-23298 P2pManager 
📤 向设备发送二进制响应:41 50 50 5F 52 45 43 45 49 56 45 44 5F 4F 4B
2025-12-08 15:54:58.436 23245-23298 P2pManager 
📥 接收设备二进制数据(192.168.0.110):

10.视频直播实现效果:

相关推荐
KevinWang_4 小时前
Android Hilt 依赖注入
android
古法安卓4 小时前
Android-深入理解 Android 回调(Callback)机制
android·性能优化·android studio
古法安卓4 小时前
Android-车机 GNSS 定位数据接收问题排查
android·java·android studio
FungLeo6 小时前
Flutter 吸顶分组列表实战:语义桶分组 + 点击头平滑滚动
android·flutter
阿巴斯甜7 小时前
Android 自定义权限
android
FungLeo8 小时前
Flutter 超长 StatefulWidget 拆分术:part of + extension on State 实战
android·flutter·dart
小王C语言9 小时前
MySQL 数据类型:数值类型、字符串类型、日期和时间类型、enum 和 set、find_in_set 查询
android·数据库·mysql
我命由我123459 小时前
Android 控件 - CardView(快速实现圆角)
android·java·java-ee·kotlin·android studio·android-studio·android runtime
码云数智-园园9 小时前
Android 全方位性能优化合集
android·性能优化