更新记录
1.0.0(2026-10-09)
首个版本。
- 原生 WebSocket 客户端(零第三方依赖):Android 自实现 RFC6455(
java.net.Socket+ 自写帧解析 / 掩码 / 分片 / 关闭握手 / ping-pong),iOS 用URLSessionWebSocketTask;均支持ws://与wss://。 - H.264 / H.265 硬解:Android
MediaCodec(异步模式 + 独立HandlerThread),iOSVideoToolbox+AVSampleBufferDisplayLayer(压缩帧直喂,不做输出队列 / CADisplayLink)。 - 码流自动装配:内置 NAL 解析器,支持
annexb/avcc两种封装与auto嗅探;支持stream/au/nal三种消息语义;从 SPS / VPS 解析分辨率与参数集,自动构造MediaFormat/CMVideoFormatDescription。 - 组件化显示:
webrtcCreatePlayer()在原生层建视图,webrtcUpdateLayout()定位,配合mt-webrtc-player组件即可像普通<view>一样摆放原生画面。 - 多路播放:每个视图有独立
playerId,可同时播放多路码流,各自独立的重连 / 解码 / 统计。 - 挂载容器自愈:优先挑选真正在显示的 webview(
isShown()含祖先可见性), 父容器不可见时顺父链找可见祖先挂载并自动对齐坐标;仍未上屏则每秒换容器重试, 最后才回退surface载体 —— 彻底解决 uniapp 多 webview 环境下挂错容器导致的黑屏。 - 视图/解码器启动竞态已处理:
createPlayer()的视图是异步建的,open()可能更早, 插件会把open挂起到视图就绪(pendingStart),视图一到位再补挂给解码器, 不会出现「收包正常但永远不配置解码器」的黑屏。 - 解码器不再被反复重建(黑屏的另一处根因):
MediaCodec用setCallback()配成异步模式后, 不能再调同步的dequeueInputBuffer()(javadoc 明确抛IllegalStateException), 改为只用onInputBufferAvailable交付的槽位;TextureView.onSurfaceTextureSizeChanged不再当成「换 Surface」处理(布局变化 / 按宽高比重测量都会触发它)。 早期症状是「配置完成 h264 640x360每秒重复一次、解出 0 帧、drop跟着aus涨」。 - 解码器/视图诊断增强:每次释放解码器都打
[webrtc][warn][decoder] 释放解码器 why=…(surface-changed/get-input-failed/queue-input-failed/codec-error:N/codec-switched);MediaCodec报错时带上recoverable / transient / diagnostic;wsStats的why会直接点名 「解码器重建过 N 次」;演示页把重建次数显示在排查提示里。 - 坐标系 / 挂载点 / z 序修复(帧在解但屏幕全黑的三处根因):
- 坐标契约明确为「CSS 逻辑像素 + webview 视口坐标」,新增
vpWidth/vpHeight参数; 原生侧按scale = webview 宽 / vpWidth换算成物理像素,再加 webview 在窗口里的偏移 (拿不到vpWidth退回系统 density)。以前直接把 CSS px 当物理 px 用, 1080×2400 的机器上视频只有屏宽 36% 大、位置还差一个状态栏高度。 - 挂载点从「webview 的父容器」改为
android.R.id.content+bringToFront(): 避免 uniapp 重建 / 切换 webview 后把视图留在不再显示的旧树上, 也避免自定义ViewGroup丢掉leftMargin/topMargin。 - 新增每秒的位置 + z 序体检(
syncLayoutAndZ):不在页面根容器就搬回去、 被上层兄弟盖住就bringToFront()、尺寸/落点偏了就重算并打[webrtc][warn][render] 布局修正 期望 px=(…) 实际=(…) 容器=… z=1/1(top); 流量日志也带上rect=win=(…) screen=(…) WxH z=…。 旧实现的体检在isSurfaceReady()后直接return,这类问题一个都发现不了。 surface兜底载体的setZOrderMediaOverlay(true)改为setZOrderOnTop(true)(不透明 webview 窗口下 media overlay 依然会被盖成黑的)。- 组件
mt-webrtc-player改用 DOMgetBoundingClientRect()量尺寸 (uni 的boundingClientRect()在 App 端是文档坐标,页面一滚就偏),并自动把window.innerWidth/innerHeight当作vpWidth/vpHeight传下去。iOS 侧单位就是 point, 不需要换算(只多打一行webview=…用于核对)。
- 坐标契约明确为「CSS 逻辑像素 + webview 视口坐标」,新增
- 断线自动重连、无数据超时检测、编码自动纠错(
codec: 'h264'收到 H.265 时自动切换)。 open()/createPlayer()的竞态彻底消除:createPlayer()的原生视图是异步建的 (AndroidMAIN.post/ iOSDispatchQueue.main.async),而open()同步查 player 表, JS 拿到playerId立即open有概率报player 不存在: wvp_N(910003)并把整个流程卡死。 现在两端都把open挂起(AndroidPENDING_OPEN/ iOSpendingOpen),视图创建完成即刻补执行, 另有 6×120ms 的重试兜底,返回{ deferred: true }而不是报错。- 布局换算只信「看得见的 webview」:
computePxRect/syncLayoutAndZ在 webview 未上屏 / 不可见 / 宽为 0 时不再重算偏移,保留上一次的 rect —— uniapp 会同时持有 隐藏的 webview,它的窗口偏移恒为(0,0),一旦被选中画面会整整上移一个状态栏高度 (真机实测y从 363 跳到 162)。 - iOS 补齐与 Android 对等的布局自愈:新增每秒
syncLayoutAndZ()(挂载点被换掉就重挂、 位置尺寸偏了就改回、被上层兄弟盖住就bringSubviewToFront(),并打[webrtc][warn][render] 布局修正 期望 frame=(…) 实际=win=(…));取偏移改用webView.convert(.zero, to: parent)(frame.minX/minY只相对直接父视图); 新增vpWidth / vpHeight换算兜底(scale = webview 宽 / 视口宽,超出 0.4–6 视为不可信退回 1); 每秒流量日志追加rect=win=(…) WxH offset=(…) scale=… z=…落点,和 Android 的rect=对齐。 - 截图:
webrtcSnapshot()返回 JPEG base64(AndroidPixelCopy/TextureView.getBitmap,iOS 旁路VTDecompressionSession+CoreImage)。 - 事件轮询模型:
webrtcPollEvents()/webrtcClearEvents(),与同仓库taotao-tcp一致。 - 运行日志:环形缓冲 500 条,
webrtcLogs({ limit, sinceSeq, level })增量拉取。 - 诊断友好:事件里带每秒一条的收包统计
wsStats(frames/bytes/aus), 原生侧每秒一条流量日志(收 N 帧/xKB → 解析 N AU → 解出 N 帧), 「黑屏时哪个数字是 0,断点就在哪一级」,示例工程还提供用系统 WebSocket 交叉验证的「自检」。 - 平台:App-Android(minSdk 21)、App-iOS(deploymentTarget 13.0)。
平台兼容性
uni-app(4.45)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| √ | × | - | - | √ | √ | 5.0 | 13 | - |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| - | - | - | - | - | - | - | - | - | - | - | - |
taotao-webrtc · WebSocket 拉流 + H.264 / H.265 原生硬解播放
一句话:ws:// 上拿到的裸 H.264 / H.265 码流,用系统硬解(Android MediaCodec / iOS VideoToolbox)
渲染成原生视图,在 uni-app 里当成一个普通组件使用。
- 不依赖 WebRTC 协议栈,不需要
RTCPeerConnection,不需要 SRS / ZLMediaKit 之类服务端改造。 - 零第三方依赖:WebSocket 客户端是自己写的(Android 手写 RFC6455、iOS 用系统
URLSessionWebSocketTask), 没引 OkHttp / Starscream,装了插件不会和别的库冲突。 - 支持 多路同时播放、断线自动重连、截图、事件轮询、全链路日志。
| 平台 | 最低版本 | 解码 | WebSocket | 渲染 |
|---|---|---|---|---|
| Android | minSdk 21(Android 5.0) | MediaCodec 异步模式(H.264 / H.265) |
自实现 RFC6455,支持 ws:// wss:// |
SurfaceView / TextureView |
| iOS | iOS 13.0 | VideoToolbox(H.264 / H.265) |
URLSessionWebSocketTask |
AVSampleBufferDisplayLayer |
一、装完先跑通(5 分钟)
1. 装测试推流服务端
仓库里带了一个零依赖的 Node 推流服务端(ws-test-server/,只在开发时用,不随插件发布):
cd ws-test-server
node server.js # 打印出 ws://<电脑IP>:9101/live
node server.js --codec h265
node server.js --unit stream --chunk 1000 # 故意把起始码切开,测拆包粘连
2. 页面里用它
<template>
<view class="page">
<mt-webrtc-player
ref="player"
src="ws://192.168.1.20:9101/live"
codec="h264"
object-fit="contain"
class="stage"
@ready="onReady"
@event="onEvent"
@error="onError"
@snapshot="onSnapshot" />
<button @click="$refs.player.snapshot(85)">截图</button>
</view>
</template>
<script>
import pageLifecycle from '@/utils/page-lifecycle.js'
export default {
// 这个 mixin 负责广播页面显示 / 隐藏(原生视图需要跟着藏起来),必须加
mixins: [pageLifecycle],
methods: {
onReady(e) { console.log('原生视图好了', e.playerId) },
onEvent(e) { console.log('事件', e.event, e) }, // wsOpen / videoStart / videoStats ...
onError(e) { console.warn('出错', e.errCode, e.errMsg) },
onSnapshot(e) { uni.previewImage({ urls: ['data:image/jpeg;base64,' + e.base64] }) }
}
}
</script>
<style>
.stage { height: 420rpx; background-color: #000; border-radius: 12rpx; }
</style>
mt-webrtc-player 组件随示例工程提供(components/mt-webrtc-player/,把插件装进你自己的工程后,
把这个目录一起复制过去即可),
它把「量位置 → 建原生视图 → 页面滚动 / 切后台同步 → 卸载销毁 → 事件分发」都封好了。
不是必须用它,但强烈建议先用它跑通。
3. 请求权限(Android)
插件已带 INTERNET / ACCESS_NETWORK_STATE,一般不用额外配置。
iOS 想连内网 ws://(非 https)需要在 manifest.json → App 模块配置 → iOS 里
保留明文访问(插件自带的 info.plist 已经放开 NSAllowsArbitraryLoads,
上线前请按需收紧成只允许你的域名)。
二、为什么是「组件形式」
App 端原生视频只能画在原生层,画不进 webview 的 DOM,所以做法是:
原生视图浮在 webview 之上,uni-app 层放一个 <view> 当「占位 + 量尺」,插件按占位元素的坐标
把原生视图盖上去。mt-webrtc-player 就是这个思路的完整封装,用起来和普通组件一样:
<mt-webrtc-player src="ws://..." codec="h264" />
<mt-webrtc-player src="ws://..." codec="h265" style="height: 300rpx" />
需要注意的两点(组件已经处理了,自己写代码时要注意):
- 原生视图不会自动跟随页面滚动 / tab 切换。所以必须有
utils/page-lifecycle.js这个 mixin 广播 页面显示隐藏,组件在页面隐藏时会setVisible(false);页面滚动时会重新测量并同步位置。 - 同一区域不要叠 uni 层的普通元素(会被原生画面盖住)。要在画面上叠字 / 按钮,
用 nvue 的
<cover-view>,或者用插件自带的debugOverlay(原生层画字)。
三、API
import * as webrtc from '@/uni_modules/taotao-webrtc'
本插件是 UTS API 插件,只能用上面的具名导入,没有
uni.webrtcXxx(...)形式。
视图
| API | 说明 |
|---|---|
webrtcCreatePlayer(options) |
创建原生视频视图,返回 { errCode, errMsg, playerId } |
webrtcUpdateLayout(options) |
更新位置 / 大小 / 层级(滚动、横竖屏、折叠屏切换时调) |
webrtcSetVisible({ playerId, visible }) |
显示 / 隐藏 |
webrtcSetZIndex({ playerId, zIndex }) |
调层级 |
webrtcDestroyPlayer({ playerId }) |
销毁(先断流、再释放解码器) |
webrtcDestroyAll() |
销毁全部 |
webrtcPlayers() |
所有视图的运行时快照(状态、分辨率、帧率、字节数……) |
webrtcCreatePlayer / webrtcUpdateLayout 的坐标是 CSS 逻辑像素、相对 webview 视口左上角,
也就是 boundingClientRect() 给的那套值——不要乘 density、也不要加状态栏高度,
原生侧会自己换算成物理像素(scale = webview 宽 / vpWidth,拿不到 vpWidth 就退回系统 density)。
uni.createSelectorQuery().select('#stage').boundingClientRect((r) => {
const info = uni.getSystemInfoSync()
const p = webrtc.webrtcCreatePlayer({
x: r.left, y: r.top, width: r.width, height: r.height,
// 视口 CSS 尺寸:原生靠它把 CSS px 换成物理 px,强烈建议传
vpWidth: info.windowWidth, vpHeight: info.windowHeight,
objectFit: 'contain', // contain / cover / fill
renderMode: 'texture', // Android:texture(默认)/ surface,黑屏先试 texture
backgroundColor: '#000000',
cornerRadius: 8, // 圆角(px)
debugOverlay: false, // 原生层叠加分辨率 / 帧率
tag: '客厅摄像头'
})
webrtc.webrtcOpen({ playerId: p.playerId, url: 'ws://192.168.1.20:9101/live' })
}).exec()
⚠️
boundingClientRect()在 App 端返回的是文档坐标(含页面滚动),而原生覆盖层是「相对窗口」的, 页面一滚就会偏。本仓库的<mt-webrtc-player>组件内部用的是 DOMgetBoundingClientRect()(天生视口坐标),所以滚动页面也能对齐;如果你自己直接调 API,请用x - window.scrollX、y - window.scrollY换成视口坐标,并传上vpWidth/vpHeight。
码流
| API | 说明 |
|---|---|
webrtcOpen(options) |
连 WS → 解析 NAL → 硬解 → 上屏 |
webrtcClose({ playerId }) |
断开码流(视图留着,可以再 webrtcOpen 复用) |
webrtcFlush({ playerId }) |
冲刷解码缓冲,等下一个 IDR |
webrtcFeed({ playerId, data }) |
JS 层自己收流时手动喂 ArrayBuffer |
webrtcFeedBase64({ playerId, base64 }) |
手动喂 base64(调试用) |
webrtcSnapshot({ playerId, quality }) |
截图,结果走 snapshot 事件(JPEG base64) |
webrtcOpen 的完整参数:
webrtc.webrtcOpen({
playerId: 'p1',
url: 'ws://192.168.1.20:9101/live', // ws:// 或 wss://
codec: 'h264', // h264 | h265
framing: 'auto', // auto | annexb | avcc
nalLengthSize: 4, // framing='avcc' 时的长度字段字节数
messageUnit: 'stream', // stream | au | nal
headers: [ { name: 'Authorization', value: 'Bearer x' } ],
protocols: ['binary'], // 子协议
timeoutMs: 8000, // 连接 + 握手超时
reconnect: true, // 断线自动重连
reconnectDelayMs: 1500,
maxReconnect: 0, // 0 = 不限
dropLateFrames: true, // 积压时丢非关键帧,压延迟
maxQueue: 12, // 解码前最多积压几帧
videoWidth: 0, // 拿不到 SPS 时的宽高提示
videoHeight: 0,
autoSelectCodec: true, // codec 填错时自动纠一次
noDataTimeoutMs: 10000 // 多久没数据算异常(0 = 不检测)
})
这三个参数是「能不能出画面」的关键,拿不准就按服务端实际情况试:
| 参数 | 含义 | 怎么判断 |
|---|---|---|
framing |
annexb=NAL 之间是 00 00 00 01 起始码;avcc=每个 NAL 前面 4 字节大端长度 |
抓包看前 4 字节。auto(默认)会嗅探首包并粘住 |
messageUnit |
stream=一条消息是码流的一段;au=一条消息正好一帧;nal=一条消息一个 NAL |
看服务端代码。拿不准就用 stream(最稳,代价是多攒一个 NAL) |
codec |
h264 / h265 |
H.265 流的 NAL 头第一个字节通常是 0x40(VPS)/0x42(SPS);H.264 一般是 0x67/0x68 |
事件(轮询模型)
原生回调跨线程,直接回调 JS 有生命周期风险,所以统一写进事件队列,由 JS 主动拉取
(和同作者的 taotao-tcp 一致):
setInterval(() => {
const list = JSON.parse(webrtc.webrtcPollEvents()) // 拉取并清空
for (const e of list) {
switch (e.event) {
case 'wsOpen': /* 连上了 */ break
case 'videoConfig': /* e.width / e.height / e.codec */ break
case 'videoStart': /* 首帧上屏 */ break
case 'videoStats': /* e.fps / e.decoded / e.dropped / e.bytes / e.queue / e.delayMs */ break
case 'wsStats': /* e.frames / e.bytes / e.aus / e.target —— 每秒一条的**收包统计**,
排「黑屏/花屏」时先看这条:哪个数字是 0,断点就在哪一级 */ break
case 'videoError': /* e.errCode / e.errMsg */ break
case 'noData': /* e.idleMs,超时没数据 */ break
case 'snapshot': /* e.base64 / e.width / e.height */ break
}
}
}, 200)
完整事件列表见 utssdk/interface.uts 里 WebrtcEvent 的注释。
⚠️ 事件队列是全局的,多个 player 共用一条队列。如果你自己起了多个定时器各自
webrtcPollEvents(), 会互相抢事件。用utils/webrtc-poller.js(已随示例工程提供)做单例轮询 + 按playerId分发。
日志
| API | 说明 |
|---|---|
webrtcLogs({ limit, sinceSeq, level }) |
读环形缓冲日志(最多 500 条),sinceSeq 传上次的 maxSeq 只取增量 |
webrtcClearLogs() |
清空 |
webrtcSetLogEnabled(true) |
开关(默认关,调试时打开) |
webrtcIsLogEnabled() |
当前状态 |
webrtcHasH264() / webrtcHasH265() |
设备有没有对应解码器 |
webrtcIsSupported() |
平台是否支持(App 端恒 true) |
Android 日志同时打到 Logcat(TAG taotao-webrtc),iOS 同时 NSLog。
💡 想把原生日志直接看到 js 控制台? 示例工程里的
utils/webrtc-poller.js提供了镜像:import { setConsoleMirror, subscribeLogs } from '@/utils/webrtc-poller.js' setConsoleMirror(true) // 原生日志 → console setConsoleMirror(true, 'warn') // 只镜像 warn / error,过滤太吵的 debug subscribeLogs((l) => { /* l = {seq,time,level,tag,message} */ })打开后 HBuilderX 控制台里就能看到 Android Logcat / iOS NSLog 的同一批日志, 前缀是
[webrtc][级别][标签],不用连 Android Studio / Xcode。
四、事件 / 错误码
| 错误码 | 含义 |
|---|---|
910001 |
平台不支持 |
910002 |
参数非法 |
910003 |
playerId 不存在(open() 早于 createPlayer 完成时会自动挂起补执行,正常情况下不会再报) |
910004 |
playerId 重复 |
910005 |
WebSocket 层错误(详见 errMsg) |
910006 |
解码器创建失败(一般是设备不支持该编码) |
910007 |
解码失败 |
910008 |
没有可用的渲染 Surface(Android Surface 还没就绪) |
910009 |
编码不匹配(codec 与服务端码流不一致) |
910010 |
原生视图创建 / 挂载失败 |
910011 |
截图失败 |
910101 |
WS 连接失败(地址非法 / 网络不可达) |
910102 |
WS 握手失败(HTTP 101 未成功、子协议未协商) |
910103 |
WS 收发 IO 错误 / 超时 |
910104 |
WS 协议错误(帧格式非法) |
五、性能与延迟
- 延迟:解码走硬解、渲染不等垂直同步(iOS 用
kCMSampleAttachmentKey_DisplayImmediately, Android Surface 直出),端到端延迟基本等于「服务端编码延迟 + 网络」,插件自身不额外缓存。videoStats事件里的delayMs是解码队列的等待时间。 - 丢帧策略:
dropLateFrames: true时,如果解码队列积压超过maxQueue, 非关键帧直接丢,只等下一个 IDR —— 宁可跳一下也不越播越延迟。 - 多路播放:每路一个 WebSocket + 一个解码器,4~8 路 1080p 在主流机器上没问题; 再往上就要考虑降分辨率或降低路数。
六、常见问题
Q:不知道 codec / framing / messageUnit 该填什么?
用示例工程里的诊断脚本,连上去抓 40 个包直接给答案(零依赖):
node ws-test-server/tools/probe-client.js ws://192.168.31.123:9101/live
# codec: h264 framing: annexb messageUnit: au
# 前 8 条消息的 NAL 类型: [7,8,6,5] [1] [1] [1] [1] [1] [1] [1]
它还会告出「参数集在整段里只出现 1 次」这种连上重连必黑屏的服务端问题。
Q:黑屏,怎么一步步定位?
按顺序看事件,哪一环断了就是哪一环的问题(演示页会在视频区下面直接显示同一句提示,
并打到控制台 [webrtc] 排查提示:…):
| 现象 | 断在哪 | 怎么修 |
|---|---|---|
没有 playerCreated |
原生视图没建起来 | 看 viewError;确认用的是自定义基座、不是标准基座 |
只有 wsConnecting / wsError |
连不上 WebSocket | 手机和服务端不同网段、防火墙、地址写错;点演示页的「自检」用系统 WebSocket 再验一次 |
Android 有 playerCreated 但没有 surfaceReady |
绘制 Surface 没就绪 | 把 renderMode 切到 'texture'(默认值) |
wsOpen 了但 wsStats.bytes 一直是 0 |
一个字节都没收到 | 服务端没推 / 只在带参数时才推 / 要鉴权 header。这是服务端侧问题,插件没错 |
wsStats.bytes 在涨但 wsStats.aus 是 0 |
收到了,但一条 AU 都没切出来 | framing / messageUnit 选错,先用 npm run probe 确认真实封装 |
aus 在涨但没有 videoConfig |
切出 AU 了但没有关键帧 | 服务端没给 IDR(等下一个关键帧),或 codec 选错 |
日志里 挂载完成 ... shown=false(连父容器也是 shown=false) |
挂到了没在显示的 webview 容器上(uniapp 运行时有多个 webview,第一个未必是当前页) | 插件已自愈:优先挑 isShown() 的 webview,父容器不显示就顺父链找可见祖先挂上去(坐标自动对齐),仍不行就每秒重试换容器 |
wsStats.target 是 false(日志里 render=未就绪[...]) |
原生视图的渲染目标没上屏,解码器拿不到输出 | iOS:视图没挂到 window 上(已内置修复)。Android:看日志里那行视图状态——hw=false 说明窗口没开硬件加速(TextureView 在软件渲染窗口里永远不会创建 Surface),插件已会自动回退 surface 并给 renderFallback 事件;shown=false 说明挂到了不可见的父容器 |
有 videoConfig 没有 videoStart |
解码器建好了但没出帧 | ① 看有没有 [webrtc][warn][decoder] 释放解码器 why=…:有就是解码器被反复重建(每次都掉回「等关键帧」),本插件 v1.0.0 已修掉已知的两种触发源(异步模式误用同步 API、渲染尺寸变化),若还出现,why= 后面就是病根;② 没有重建日志 → codec 选错(H.265 流配了 h264),或参数集只在开头发过一次 |
日志里 每秒重复一条 [webrtc][info][decoder] 配置完成 …,且 解出 0 帧、drop 跟着 aus 一起涨 |
解码器在反复重建(配置→没喂进去→释放→等下一个关键帧) | v1.0.0 已修:MediaCodec 用 setCallback 配成异步模式后就不能再调同步的 dequeueInputBuffer()(javadoc 明确抛 IllegalStateException),改为只用 onInputBufferAvailable 交付的槽位 |
有 videoStart、decoded 在涨,还是黑 |
解码没问题,是渲染载体 | 切 renderMode 到 'texture';确认原生视图没被页面背景挡住 |
前三条最常见。补充说明:
wsStats.bytes看的是"WS 真收到的字节",和videoStats.bytes不是一回事:后者要解码 起来才会发,黑屏时它永远是 0 —— 排查时别用它下结论。演示页的排查提示已经改用wsStats。- 分不清是「网络/服务端」还是「插件」时,用演示页的 「自检」:它用系统 WebSocket
(
uni.connectSocket)连同一个地址数 3 秒收包,结果直接弹窗。这段是纯 JS,改完立刻生效, 不用重打自定义基座。 - 原生侧每秒会打一条流量日志
收 25 帧/98KB → 解析 25 AU → 解出 25 帧 drop 0 … rect=…, 哪个数字是 0,断点就在哪一级;末尾的rect=是原生视图的真实落点 (Androidwin=(…) screen=(…) WxH z=…,iOSwin=(…) WxH offset=(…) scale=… z=…), 和上一行期望对不上时会额外打[webrtc][warn][render] 布局修正 期望 … 实际=…。 - 点「连接」偶发
player 不存在: wvp_N(910003):createPlayer()的视图是异步建的 (AndroidMAIN.post/ iOSDispatchQueue.main.async),而open()是同步查表, JS 拿到playerId立刻open有概率撞上窗口期、整个流程直接断掉。 当前版本已把open挂起并在创建完成后补执行(另有 6×120ms 重试兜底), 日志上是[webrtc][warn][player] open 早于创建完成,已挂起等视图就绪 wvp_N紧跟一条创建完成,补执行 open wvp_N,属于正常自愈。 - 隐藏的 webview 它的窗口偏移是不可信的:uniapp 同时持有多个 webview,
没在显示的那个
isShown()==false、窗口偏移恒为(0,0)。布局换算只在 「已上屏 + 可见 + 宽度 > 0」时才重算偏移,否则保留上一次的 rect —— 否则画面会突然上移一个状态栏高度(真机实测y从 363 跳到 162)。 - iOS 侧也有每秒的位置 / z 序体检(
syncLayoutAndZ):视图被换到别的容器、 位置尺寸偏了、被上层兄弟盖住,都会自动修正并打[webrtc][warn][render] 布局修正 期望 frame=(…) 实际=win=(…) z=1/1(top); iOS 取偏移用webView.convert(.zero, to: parent)(frame.minX/minY只是相对直接父视图, 父链一旦不是直属关系就会偏)。 render=未就绪+dec=idle+ drop 每秒涨 = 解码器一个都没解:这是「视图还没建好, 解码器就已经创建了」的竞态(open()紧跟createPlayer,而原生视图是异步建的)。 插件已在两端修掉(iOSview.didSet时把视图补挂给解码器,Android 视图建好即补 Surface), 你只需用当前版本重新打自定义基座。- 解码器被反复重建时,原生侧一定会打
[webrtc][warn][decoder] 释放解码器 why=…,why取值:surface-changed(换了真 Surface)、get-input-failed/queue-input-failed(codec 状态异常)、codec-error:N(MediaCodec报错,同一条 error 日志带recoverable/transient/diagnostic)、codec-switched(自动切编码)。 演示页也会把重建次数算出来并写在视频区的排查提示里。 - 参数集只在开头发一次的推送端,断开重连后必黑屏 —— 需要服务端
repeat-headers(示例服务端已经这么做了,诊断工具也会告警)。 - Android 默认
texture就是为这个坑准备的:surface模式(SurfaceView)在 webview 浮层里 要自己处理 z-order,表面上看都是"黑屏",切回texture能直接排除渲染因素。
Q:花屏、绿屏、上一帧残影?
基本是 NAL 边界切错(framing / messageUnit),或者丢包后没等到关键帧。
收到数据但缺参数集时,插件会等下一个 IDR 再恢复,不会一直花屏。
Q:原生的画面盖住了我 uni 层的按钮 / 文字?
原生视图在最上层,这是平台限制。解决办法:把控件放到画面外,或用 nvue 的 <cover-view>。
Q:切换页面后,原生画面还留在新页面上?
没加 utils/page-lifecycle.js 这个 mixin。它会在页面 onHide 时广播事件,组件收到后
setVisible(false)。(插件内部另有兜底:iOS 每秒在校正视图归属,Android 在 onResume 时重建。)
Q:为什么不用 plus.video / video 标签?
video 标签吃的是能自己解封装的容器(mp4 / hls / flv),裸 H.264/H.265 流它不认;
flv.js 那套要跑在 webview 里再用 <video> 软解,功耗和延迟都下不来。
要走硬解 + 低延迟,只能在原生层解。
Q:能不能直接播 RTSP?
本插件只做 WebSocket 传输。RTSP 可以在你自己的服务端用 ffmpeg / ZLMediaKit 转成
ws 推流(本插件的 --source ffmpeg 就是个例子)。
MIT © MemoTalk Team

收藏人数:
购买普通授权版(
试用
使用 HBuilderX 导入示例项目
赞赏(0)
下载 6
赞赏 0
下载 12660675
赞赏 1955
赞赏
京公网安备:11010802035340号