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 是整个系统的入口类,负责:
- 初始化 :创建
EglBase、PeerConnectionFactory、PeerConnection、DataChannel - 信令绑定 :通过
SignalingClient收发 Offer/Answer/ICE 候选 - 数据通信 :通过
P2PMessageManager在DataChannel上收发消息 - 心跳维护:每 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" // 置顶显示
)
播放流程
- 收到信令服务器的
"live"事件 - 提取
fileHash和link - 通过
FlowBus分发StreamMessageEvent EasyVlcPlayer.play(link)被调用- 强制将 URL Scheme 改为
https - 创建
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 处理 :签名时
payload为null时使用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.视频直播实现效果:
