iOS / WebView 下 TURN 失败?先核对鉴权 + 是否真的用了 relay
发布于: 2026-02-18
webrtc
turn
coturn
authentication
ios
webview
troubleshooting
桌面端正常但 iOS(或 App 内 WebView)失败时,先确认两件事: (1) 是否真的生成了 relay candidates,(2) TURN 的 username/credential 是否与服务器期望完全一致。
查看清单 + 伪代码
排查清单
- 先强制 relay: 临时设置
iceTransportPolicy: 'relay',确认问题确实在 TURN 路径。
- 确认 relay candidate: 记录 candidate 字符串,搜索
typ relay。
- 不要混用认证方式: 静态 user/pass 与带过期时间的 TURN REST 认证要与服务端配置匹配。
- 检查时间漂移: 临时凭证对系统时间偏差很敏感。
- 浏览器 vs WebView A/B: 只在 WebView 失败时,可用 TURN over TCP/TLS 做诊断回退。
伪代码:强制 relay + 记录 candidate type(标准 WebRTC API)
// PSEUDO-CODE (not WLSDK)
const pc = new RTCPeerConnection({
iceServers: [
{
urls: 'turn:turn.example.com:3478',
username: '<USERNAME>',
credential: '<CREDENTIAL>',
},
],
iceTransportPolicy: 'relay',
});
pc.onicecandidate = (e) => {
if (!e.candidate) return;
const c = e.candidate.candidate;
if (c.includes(' typ relay ')) console.log('relay', c);
};
想把直播嵌入到产品?先对照 集成指南(PHP SDK) 跑通最小流程。需要开发者访问权限?申请 SDK。
云端/PaaS 上没有 srflx?为什么本地 STUN 正常但线上消失
发布于: 2026-02-18
webrtc
ice
stun
srflx
cloud
paas
deployment
troubleshooting
本地能看到 srflx,但生产环境一直不出现,通常是 出站网络/平台限制。第一步是把 candidate 类型记录下来,再决定该查哪里。
查看清单 + 伪代码
排查清单
- 记录所有 candidate: 确认你看到的是
host 还是也有 srflx/relay。
- 验证 UDP egress: 防火墙/策略可能让 STUN “悄悄失败”。
- 用 TURN 做对照: 如果
relay 能出但 srflx 不出,STUN 路径优先排查。
- 最小复现: 在同一部署环境里做“只收集 candidates”的最小页面。
伪代码:记录 ICE candidate 类型(标准 WebRTC API)
// PSEUDO-CODE (not WLSDK)
pc.onicecandidate = (e) => {
if (!e.candidate) return;
const c = e.candidate.candidate;
if (c.includes(' typ host ')) console.log('host', c);
if (c.includes(' typ srflx ')) console.log('srflx', c);
if (c.includes(' typ relay ')) console.log('relay', c);
};
把直播嵌入到产品时,baseline 越可复现,定位越快。你可以先对照 集成指南(PHP SDK) 跑通最小流程。需要访问权限?申请 SDK。
DTLS 报错:no SRTP profile negotiated —— 先抓 offer/answer 再猜参数
发布于: 2026-02-18
webrtc
dtls
srtp
sdp
whip
ffmpeg
interoperability
troubleshooting
看到 no SRTP profile negotiated 时,最快的动作不是“继续试参数”,而是先把 SDP offer/answer 保存下来,确认两端是否真的在按浏览器预期协商 DTLS-SRTP。
查看清单 + 伪代码
排查清单
- 保存两份 SDP: 保留失败场景完整 offer/answer。
- 检查 fingerprint: 核对
a=fingerprint 与 a=setup 是否合理。
- 注意 SDP 重写: signaling/proxy/SFU 的 SDP 处理可能误删关键行。
- 互通场景: 集成 WHIP/FFmpeg/SFU 时,确认是 DTLS-SRTP(不是 SDES-SRTP)。
- 先降复杂度: 先用单一 m-line(仅音频或仅视频)跑通再扩展。
伪代码:输出 offer/answer SDP(标准 WebRTC API)
// PSEUDO-CODE (not WLSDK)
function dump(label, desc) {
console.log(label, desc.type);
console.log(desc.sdp);
}
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
dump('local', pc.localDescription);
// 收到 signaling 回来的 `answer` 后
await pc.setRemoteDescription(answer);
dump('remote', pc.remoteDescription);
想把直播嵌入到产品?先对照 集成指南(PHP SDK) 保持 baseline 清爽。需要访问权限?申请 SDK。
移动端“没声音”:输出路由、setSinkId(如果存在)与 WebView 常见坑
发布于: 2026-02-18
webrtc
audio
routing
devices
webview
ios
android
troubleshooting
移动端“没声音”经常不是采集问题,而是 输出路由 或 autoplay 策略。先枚举音频输出设备,setSinkId 只在存在时使用,并对比浏览器 vs WebView。
查看清单 + 伪代码
排查清单
- 先有交互: 点击后再播放;检查
muted 与系统音量。
- 枚举设备: 用
enumerateDevices() 确认 audiooutput。
- 检查
setSinkId: 不要假设一定存在(浏览器/WebView 支持度不同)。
- 监听切换: 处理
devicechange(蓝牙/耳机)。
- 最小复现: 一个远端 track + 一个
<audio>。
伪代码:枚举输出 + setSinkId(如果支持)
// PSEUDO-CODE (not WLSDK)
async function listAudioOutputs() {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.filter((d) => d.kind === 'audiooutput');
}
async function setOutput(audioEl, deviceId) {
if (typeof audioEl.setSinkId !== 'function') {
console.warn('setSinkId 不支持');
return;
}
await audioEl.setSinkId(deviceId);
}
把直播嵌入到产品时,先对照 集成指南(PHP SDK) 跑通最小流程。需要访问权限?申请 SDK。
已连接但必须重连才看到远端画面?排查 renegotiation(m-line 顺序)+ Perfect Negotiation
发布于: 2026-02-04webrtcnegotiationrenegotiationsdptransceiverm-lineperfect-negotiationtroubleshooting
如果 peer 已经 “connected”,但远端视频必须重连后才出现,优先怀疑 renegotiation:offer collision(glare)或 SDP 的 m=(m-line)顺序漂移(尤其在使用 transceiver 时)。
查看清单 + 伪代码
排查清单
- 记录状态变化: 保存 renegotiation 前后的
signalingState/connectionState/iceConnectionState。 - 对比 m-line 顺序: 保存成功/失败的 SDP,重点对比
m= 段落的顺序。 - 稳定 transceiver: audio/video transceiver 固定顺序创建一次;优先
replaceTrack()。 - 串行发 offer: 避免多个
negotiationneeded 导致 offer 并发;加 gate。 - Perfect Negotiation: polite/impolite + ignoreOffer 一致处理 offer collision。
伪代码:Perfect Negotiation(标准 WebRTC API)
let makingOffer = false;
let ignoreOffer = false;
const polite = true;
pc.onnegotiationneeded = async () => {
try {
makingOffer = true;
await pc.setLocalDescription();
sendSignal({ description: pc.localDescription });
} finally {
makingOffer = false;
}
};
onSignal(async ({ description, candidate }) => {
if (description) {
const offerCollision =
description.type === 'offer' &&
(makingOffer || pc.signalingState !== 'stable');
ignoreOffer = !polite && offerCollision;
if (ignoreOffer) return;
await pc.setRemoteDescription(description);
if (description.type === 'offer') {
await pc.setLocalDescription();
sendSignal({ description: pc.localDescription });
}
} else if (candidate) {
try {
await pc.addIceCandidate(candidate);
} catch (e) {
if (!ignoreOffer) throw e;
}
}
});
想把直播嵌入到产品?先对照 集成指南(PHP SDK) 跑通最小流程。需要开发者访问权限?申请 SDK。
DataChannel 发送二进制就卡死?用 bufferedAmount 做 backpressure
发布于: 2026-02-04webrtcdatachannelbufferedamountbackpressurefile-transfertroubleshootingnodejselectron
如果 DataChannel 小消息正常,但一发送二进制 chunk(文件)就卡住,常见原因是 缺少 backpressure:发送速度超过通道排空速度,bufferedAmount 会不断增长,最终拖慢甚至卡死应用。
查看清单 + 伪代码
排查清单
- 从小开始: 先试 16–64KB chunk。
- 控制发送: 用
dc.bufferedAmount + bufferedamountlow 做节流。 - 检查状态: 仅在
dc.readyState === 'open' 时发送。 - 接收端内存: 尽量增量处理/落盘,避免整文件常驻 RAM。
伪代码:bufferedAmount backpressure(标准 WebRTC API)
const CHUNK = 64 * 1024;
async function sendFile(dc, buf) {
dc.bufferedAmountLowThreshold = 1024 * 1024;
for (let i = 0; i < buf.byteLength; i += CHUNK) {
while (dc.bufferedAmount > dc.bufferedAmountLowThreshold) {
await once(dc, 'bufferedamountlow');
}
dc.send(buf.slice(i, i + CHUNK));
}
}
function once(dc, evt) {
return new Promise((r) =>
dc.addEventListener(evt, () => r(), { once: true })
);
}
把直播嵌入到产品时,保持 baseline 可复现会让排查快很多。你可以先对照 集成指南(PHP SDK) 跑通最小流程。需要访问权限?申请 SDK。
WebRTC 的 H.264 在一个浏览器能播、另一个不行?检查 SDP fmtp(profile-level-id)
发布于: 2026-02-02webrtch264sdpfmtpprofile-level-idinteroperabilitytroubleshooting
如果某个浏览器/设备触发了 ontrack 却黑屏(或只有音频),别急着把锅甩给 ICE。H.264 的互通问题里,SDP 的 fmtp 参数(尤其是 profile-level-id)不匹配非常常见。
查看排查清单
症状:有 track,但解码失败
常见表现是“Chrome 能播,Safari/iOS 不行”或“桌面端 OK,但部分 Android 失败”。连接可以完全建立,但 decoder 仍可能拒绝协商出来的 H.264 profile。
排查清单
- 保留两份 SDP: 保存成功与失败场景的 offer / answer。
- 定位 H.264 的 payload type: 找到
a=rtpmap:<pt> H264/90000 并记录 <pt>。 - 对比
fmtp: 检查 a=fmtp:<pt> ... 里是否包含/一致:profile-level-id、packetization-mode、level-asymmetry-allowed。 - 让 profile 兼容: 如果你能控制 encoder,尽量让各平台协商到兼容的 H.264 profile(快速 A/B:改用 VP8 等 codec,确认问题是否在 codec negotiation)。
- 避免改坏 fmtp: 会重写 SDP 的 proxy/SFU/signaling 层,有时会误删或改写 H.264 参数。
把直播嵌入到产品时,最省时间的方式是让复现步骤尽量最小、且能观察到协商差异。你可以先对照 集成指南(PHP SDK) 跑通最小流程,再逐步加复杂度。需要开发者访问权限?先 申请 SDK。
Firefox 有 track 但黑屏?检查 mDNS ICE candidate(.local)
发布于: 2026-02-02webrtcfirefoxicemdnstroubleshooting
如果在 Chrome/Edge 能播,但 Firefox 只有黑屏(而 track 明明存在),建议先按“连接问题(ICE)”来排查——尤其当候选地址里出现 .local 这样的 mDNS 主机名时。
查看排查清单
症状:有 track,但没有帧
你可能看到 ontrack 触发、媒体轨道也拿到了,但 video 依然黑屏。这常见于:RTP 实际没有走通,或“看起来连上了”但选中的候选路径在你的网络里不可达。
排查清单
- 确认 ICE 状态: 记录
iceConnectionState,并在到达 connected/completed 时抓一份快照。 - 查看选中的 candidate pair: 在 Firefox 可用
about:webrtc 看最终选了哪一对 local/remote candidate。 - 留意
.local: 这通常是 mDNS host candidate。在某些 LAN/AP 结构里,对端可能无法解析 mDNS,结果就是“连接了但没媒体”。 - 热点 A/B 测试: 手机热点 OK、办公室 Wi‑Fi 失败,基本就是网络路径 / 名称解析 / 防火墙策略问题。
- 用 relay-only 验证: 如果你控制 WebRTC 层,可临时用 relay-only(
iceTransportPolicy: 'relay')测试:当走 TURN relay 路径时问题是否消失。
把直播嵌入到产品时,优先保证 baseline 最小且可复现。可以先对照 集成指南(PHP SDK) 跑通最小流程,再逐步加复杂度。需要开发者访问权限?先 申请 SDK。
让播放器语言与页面一致:使用 WLSDK 的 lang 参数
发布于: 2026-01-31i18nlocalizationphpsdkiframe
如果你的网站有多种界面语言,可以在调用 WLSDK::iframe()(或 WLSDK::lazyIframe())时加入 lang,让播放器 UI 语言与页面保持一致。
阅读实作代码
集成指南明确列出的支持范围
集成指南列出播放器支持的语言代码:en, ja, zh-Hant, zh-Hans, vi。当 lang 未指定时,WLSDK 会先读取执行环境语言;若不可用则回退到 en。
示例:指定播放器语言为简体中文
<?php
echo WLSDK::iframe([
'hostLabel' => 'connect',
'streamer' => '<STREAMER_USER_ID>',
'lang' => 'zh-Hans',
]);
?>
详情请参考 集成指南 → “Multi-language integration (optional)”。如果你需要开发者访问权限,可以先 申请 SDK。
优化多流仪表板:使用 PHP 延迟加载 (Lazy Loading) 直播视频
发布于: 2026-01-31performancelazy-loadingphpscalingdashboard
在仪表板页面,同时初始化很多直播播放器可能增加客户端 CPU 与带宽使用量。使用 WLSDK::lazyIframe() 可以把初始化延后到需要加载时再进行。
阅读实作代码
多流仪表板常会一次加载太多内容
如果你的页面一次渲染很多直播视图,就可能在页面加载时同时初始化多个播放器;但当大多数视图在首屏之外时,这通常不是必要的。
使用集成指南的 “Multiple Iframes Lazy Loading” 模式
集成指南提供了使用 WLSDK::lazyIframe() 的多 iframe 示例。建议保持调用最小化,只传入已明确记载的参数。
<!-- 在循环中 -->
<?php foreach ($activeStreamers as $streamerId): ?>
<div class="stream-wrapper">
<?php
echo WLSDK::lazyIframe([
'hostLabel' => 'connect',
'streamer' => $streamerId,
]);
?>
</div>
<?php endforeach; ?>
详情请看 集成指南 → “Multiple Iframes Lazy Loading”。
为什么浏览器 MediaRecorder 不适合直接上线
发布: 2025-12-28录制架构稳定性
在浏览器里用 MediaRecorder 看似简单,但上线后常遇到 无法快进/seek(缺少 metadata)、崩溃就全丢(只存在内存)、以及 可变帧率 (VFR) 导致音画不同步。
展开阅读
“最省事的方案”往往最后最费时
在浏览器里用 MediaRecorder 看似简单:录 chunk、存 Blob。但上线后常遇到:
- 无法快进/seek: 生成的 WebM 往往缺少 metadata(duration/seek table)。
- 崩溃风险: 标签页崩溃或刷新就把 RAM 里的录制全丢。
- 可变帧率 (VFR): 高负载下会掉帧,后期处理中容易音画不同步。
可靠方案:Server-side 录制
在服务端合成与录制,你可以控制 CPU 与网络行为。如果录制/导出(例如 MP4)是刚需,建议把它当作 server-side pipeline 来规划,并在 申请 SDK 时确认支持范围与方案。
WebRTC 如何处理网络切换(WiFi ↔ 4G)
发布: 2025-12-28移动连接ice-restart
用户走出 WiFi 覆盖后 IP 会变化,既有 UDP socket 失效,画面就会冻结。修复方法是 ICE Restart:检测断连并触发 pc.createOffer({ iceRestart: true }) 重新协商,而无需销毁整个流。
展开阅读
“画面冻结”是典型症状
用户走出 WiFi 覆盖后 IP 变化,原来的 UDP socket 失效,视频就会停住。
修复:ICE Restart
监控 iceConnectionState。当状态变成 disconnected 或 failed,触发 ICE Restart(用 iceRestart: true 重新协商)。
pc.createOffer({ iceRestart: true })
这会让浏览器在新的 4G 网络上重新收集 candidate,而不必销毁媒体流。
如果你要把直播嵌入到产品里,重连建议作为状态机的一部分(ICE restart + UI)来设计。可以先对照 集成指南 跑通最小流程,再按需求扩展。
ICE Failed / TURN 连接问题排查清单
发布: 2025-12-25webrtciceturn故障排查
ICE Connection Failed 通常是防火墙或 NAT 导致的。你必须配置 TURN server(UDP 3478),并准备 TCP 443 作为 fallback,以应对企业防火墙或 Symmetric NAT 等 P2P 杀手。
展开阅读
常见症状与快速检查
ICE Connection Failed 或 candidate gathering 卡住通常是防火墙或配置问题。
基础配置检查
- STUN/TURN: 必须配置 TURN server,不要只依赖 STUN-only。
- UDP 3478: 标准端口,企业网络常挡 UDP。
- TCP 443 fallback: 当 UDP 被挡时,确保 TURN 支持 TCP 443(TURNS)。
Symmetric NAT 是 P2P 杀手
对称 NAT 往往会阻断直连,必须走 TURN Relay。
如果你不想自建/运维 TURN 集群,可以考虑托管的 TURN 服务;评估方案时(或 申请 SDK 时)确认网络组件与限制条件。
如何在任何 App 中嵌入直播(快速上手)
发布: 2025-12-25集成iframesdkjavascript
为了安全嵌入,建议在后端生成 iframe,避免长效凭证出现在浏览器端。可以从 WLSDK::setup() + WLSDK::iframe() 起步(参考 集成指南)。
展开阅读
实务做法:在后端生成嵌入码
避免在前端硬编码播放器 URL 或凭证。把嵌入 HTML 交由后端生成并只提供给已验证的用户:秘密留在服务器端,你也可以在同一个地方实现自己的授权规则。
<?php
// Include WebLiveHub SDK class
require_once __DIR__ . '/vendor/autoload.php';
use WebLiveHub\SDK\WLSDK;
// Initialize SDK configuration
WLSDK::setup([
'hb_endpoint' => getenv('WL_HOSTED_BACKEND_URL') ?: '<HOSTED_BACKEND_ENDPOINT>',
'user_id' => '<AUTH_USER_ID>',
'password' => '<AUTH_PASSWORD>'
]);
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<?php echo WLSDK::script(); ?>
</head>
<body>
<?php
echo WLSDK::iframe([
'hostLabel' => 'connect',
'streamer' => '<STREAMER_USER_ID>',
'attrs' => [ 'id' => 'stream-container', 'class' => 'wlh-stream', 'style' => 'border-radius:6px;' ]
]);
?>
</body>
</html>
SDK 会输出 <iframe> 嵌入码。你可以通过 attrs 传入常规 HTML 属性,并在目标浏览器(尤其 iOS/Safari)验证 autoplay/权限行为。
为什么用 SDK?
- 后端优先集成: 长效凭证留在服务器端;由你决定何时向已登录用户输出嵌入码。
- 可靠 baseline: 先按 集成指南(setup → script → iframe)跑通最小流程,再逐步扩展。
👉 获取 SDK 凭证
WebRTC 集成前置检查清单:先把环境打平,再开始排查
发布: 2025-12-22webrtc集成检查清单兼容性
很多“不能播 / 没声音 / 黑画面”其实不是服务挂了,而是环境条件没对齐。先把 baseline 打平,再开始定位问题。
展开阅读
先把前置条件对齐,后面的排查才会快
很多“不能播 / 没声音 / 黑画面”其实不是服务挂了,而是环境条件没对齐。先把 baseline 打平,再开始定位问题。
浏览器与设备
- 优先用最新版 Chrome / Edge 验证(再处理 Safari/iOS)。
- 确认系统确实有可用麦克风/摄像头(且权限已允许)。
- 用无痕窗口测一次,排除扩展干扰。
部署与网络
- 站点必须是 HTTPS(本地可用
localhost)。 - 公司网络/防火墙可能限制 UDP;用手机热点对比可快速定位。
播放与交互
- 自动播放被拦是常态:先用“点击后开始”的流程验证。
- 音频问题先检查:输出设备、页面是否静音、系统音量。
下一步:按 集成指南(PHP SDK) 跑一遍最小可用流程;需要权限请到 申请 SDK。
WebRTC 没声音/没画面怎么查?一条可复用的排查路径
发布: 2025-12-22webrtc故障排查音频视频诊断
排查 WebRTC 最重要的是:把问题变成可观察状态,而不是靠猜。媒体、连接、播放三段式排查。
展开阅读
把问题拆成三段:媒体 → 连接 → 播放
排查 WebRTC 最重要的是:把问题变成可观察状态,而不是靠猜。
1) 媒体:麦克风/摄像头是否真的拿到了?
- 权限是否允许?
- 换一台设备测(排除硬件/驱动问题)。
2) 连接:网络是否允许连接完整建立?
- 公司网络常见:UDP 被限制,导致连接不稳或失败。
- 用手机热点测一次:能用就说明网络策略是主因。
3) 播放:是否被 autoplay / muted / 输出设备绊住?
- 先用用户点击触发播放。
- 确认输出设备没有切错(蓝牙/屏幕输出)。
一次就够的诊断资料
- Chrome:
chrome://webrtc-internals 导出或截图。 - 发生时间、浏览器版本、OS、网络类型。
下一步:对照 集成指南(PHP SDK) 建立 baseline;仍卡住再带上本文诊断资料联系支持。
Safari / iOS WebRTC 常见坑:权限、后台切换与自动播放
发布: 2025-12-22webrtcsafariios兼容性故障排查
iOS 上 WebRTC 的行为更严格,尤其是权限与前台/后台切换。建议把开始动作绑定到点击。
展开阅读
为什么“Chrome OK,iPhone 不行”很常见?
iOS 上 WebRTC 的行为更严格,尤其是权限与前台/后台切换。测试流程需要更规范。
建议测试流程
- 把开始动作绑定到点击:避免 autoplay/permission 的隐性限制。
- 测试后台切换:切到后台再回来,确认是否能恢复。
- 连续测 3 次:偶发问题需要重复测试才会暴露。
反馈时至少提供
- iPhone 型号、iOS 版本、Safari 版本
- 是否使用蓝牙耳机
- 是否省电模式
下一步:先按 集成指南(PHP SDK) 建立 iOS baseline,再逐步加功能。
如何量测直播质量与延迟:你真正该看的指标
发布: 2025-12-22webrtc指标延迟质量可观测性
别只说“卡”:把质量拆成可量化的数字。卡顿通常是丢包、抖动、码率不足或解码扛不住的组合。
展开阅读
别只说“卡”:把质量拆成可量化的数字
卡顿通常是多个因素叠加:丢包、抖动、码率不足,或端上解码扛不住。
最有用的 5 个指标
- Packet loss(丢包)
- Jitter(抖动)
- Bitrate(码率)
- Frames dropped / decode time
- End-to-end latency(端到端延迟)
怎么抓数据
- Chrome:
chrome://webrtc-internals 能快速看出趋势。 - 同场景至少做 3 次测试再比较。
下一步:完成 集成指南(PHP SDK) 后,用本文指标建立你的质量基线。
SPA / 前端生命周期实战:挂载、卸载、重连,避免内存与事件泄漏
发布: 2025-12-22前端SPA生命周期稳定性故障排查
在 SPA 里常见问题是:路由切走后,旧的 listener 或 timer 仍在跑。严守“Mount 注册,Unmount 解除”的生命周期规则。
展开阅读
SPA 的坑:你以为离开页面了,其实事件还活着
在 SPA 里常见问题是:路由切走后,旧的 listener、timer、或媒体仍在跑,导致重复连接、音频被占用,或内存持续上升。
一个可靠的规则
- Mount 时注册:事件、observer、interval。
- Unmount 时成对解除:removeEventListener、clearInterval、释放资源。
- 用状态机处理重连:避免同一时刻触发多次重连。
建议额外处理
visibilitychange:后台/回前台的行为。pagehide:移动端切页更常见。
下一步:把你的集成流程对照 集成指南(PHP SDK),确保初始化与清理在路由切换下仍一致。