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-Hant',
]);
?>
詳情請參考 整合指南 →「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?
- 後端優先整合: 長效憑證留在伺服器端;由你決定何時對已登入使用者輸出嵌入碼。
- 可靠基準: 先依 整合指南(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);若仍卡住,請附上本文診斷資料與我們聯繫。
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、或 media 仍在跑,造成重複連線、音訊被佔用、或記憶體上升。
一個可靠的規則
- Mount 時註冊:事件、observer、interval。
- Unmount 時成對解除:removeEventListener、clearInterval、釋放資源。
- 以狀態機處理重連:避免同時觸發多次重連。
建議額外處理
visibilitychange:背景/回前景的行為。pagehide:行動端切頁更常見。
下一步:把你的整合流程對照 整合指南(PHP SDK),確保初始化與清理在路由切換下仍一致。