更新记录

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),iOS VideoToolbox + 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 改用 DOM getBoundingClientRect() 量尺寸 (uni 的 boundingClientRect() 在 App 端是文档坐标,页面一滚就偏),并自动把 window.innerWidth/innerHeight 当作 vpWidth/vpHeight 传下去。iOS 侧单位就是 point, 不需要换算(只多打一行 webview=… 用于核对)。
  • 断线自动重连、无数据超时检测、编码自动纠错(codec: 'h264' 收到 H.265 时自动切换)。
  • open() / createPlayer() 的竞态彻底消除:createPlayer() 的原生视图是异步建的 (Android MAIN.post / iOS DispatchQueue.main.async),而 open() 同步查 player 表, JS 拿到 playerId 立即 open 有概率报 player 不存在: wvp_N(910003)并把整个流程卡死。 现在两端都把 open 挂起(Android PENDING_OPEN / iOS pendingOpen),视图创建完成即刻补执行, 另有 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(Android PixelCopy / 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" />

需要注意的两点(组件已经处理了,自己写代码时要注意):

  1. 原生视图不会自动跟随页面滚动 / tab 切换。所以必须有 utils/page-lifecycle.js 这个 mixin 广播 页面显示隐藏,组件在页面隐藏时会 setVisible(false);页面滚动时会重新测量并同步位置。
  2. 同一区域不要叠 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> 组件内部用的是 DOM getBoundingClientRect() (天生视口坐标),所以滚动页面也能对齐;如果你自己直接调 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= 是原生视图的真实落点 (Android win=(…) screen=(…) WxH z=…,iOS win=(…) WxH offset=(…) scale=… z=…), 和上一行 期望 对不上时会额外打 [webrtc][warn][render] 布局修正 期望 … 实际=…。
  • 点「连接」偶发 player 不存在: wvp_N(910003):createPlayer() 的视图是异步建的 (Android MAIN.post / iOS DispatchQueue.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,而原生视图是异步建的)。 插件已在两端修掉(iOS view.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

隐私、权限声明

1. 本插件需要申请的系统权限列表:

android.permission.INTERNET, android.permission.ACCESS_NETWORK_STATE

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

不采集任何数据;拉取的视频码流仅在本机内存与网络连接中处理

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

无

暂无用户评论。