使用Wowza+Angular实现WebRTC直播时WebSocket每分钟断开问题求助
问题:Wowza WebRTC直播流每分钟自动断开
我们基于Wowza平台获取直播流,在Angular框架中使用原生WebSocket与RTC PeerConnection类,通过安全wss协议将直播流接入应用。目前可正常获取直播流,但存在连接每分钟自动断开的问题,以下是实现代码:
function WebRTCPlayer(wowzaStreamlock, videoID, alias, dateTime) { /* Get these from WowzaStreamingEngine WebRTC Application */ let applicationName = "<APP_NAME>"; let streamName = alias; let wssUrl = "wss://" + wowzaStreamlock + "/webrtc-session.json"; let remoteStream = videoID; let wsConnection, videoElement = null; let retry = 0; let maxRetry = 120; let peerConnection = null; let connectionState = null; let endTime = null; let noOfCycleCompleted = 0; const wsConnect = () => { let _this = this; let streamInfo = { applicationName, streamName }; let userData = {}; const endDate = new Date(); try { wsConnection = new WebSocket(wssUrl); } catch (e) { console.log("WebSocket error: ", e); } wsConnection.binaryType = "arraybuffer"; wsConnection.onopen = () => { peerConnection = new RTCPeerConnection(); peerConnection.onicecandidate = _this.gotIceCandidate; peerConnection.onconnectionstatechange = onConnStateChange; peerConnection.ontrack = (event) => { try { if (noOfCycleCompleted >= 1) { console.log(`Reconnecting... | ${streamName.replace('.stream', '')}`); } videoElement = document.getElementById(remoteStream); videoElement.srcObject = event.streams[0]; } catch (error) { videoElement.src = window.URL.createObjectURL(event.streams[0]); } }; sendPlayGetOffer(); }; wsConnection.onerror = (error) => { console.log("WebSocket error: ", error); } function onConnStateChange(event) { connectionState = peerConnection.connectionState; if (peerConnection.connectionState === "connected") { isStreamConnected = true; state(); noOfCycleCompleted++; } else { if (peerConnection.connectionState === "connecting") state(); else if (peerConnection.connectionState === "failed" || peerConnection.connectionState === "disconnected") { state(); if (noOfCycleCompleted >= 1) { videoElement = document.getElementById(remoteStream); videoElement.src = ""; videoElement.srcObject = null; videoElement.setAttribute('style', 'background: #000 url("./assets/images/camera status/camera-disconnected.gif") center no-repeat; background-size: contain;'); } maxRetry = 120; retry = 0; wsConnect(); } } } const state = () => { console.log(`State: ${peerConnection.connectionState} | ${streamName.replace('.stream', '')}`); }; const sendPlayGetOffer = () => { wsConnection.send( '{"direction":"play", "command":"getOffer", "streamInfo":' + JSON.stringify(streamInfo) + ', "userData":' + JSON.stringify(userData) + '}' ); }; this.Stop = function Stop() { stop(); } const stop = () => { if (peerConnection != null) { peerConnection.onconnectionstatechange = null; peerConnection.close(); } if (wsConnection != null) { wsConnection.onerror = null; wsConnection.close(); } peerConnection = null; wsConnection = null; }; wsConnection.onmessage = function (evt) { let msgJSON = JSON.parse(evt.data); let msgStatus = Number(msgJSON["status"]); let msgCommand = msgJSON["command"]; if (msgStatus != 200) { retry++; if (retry < maxRetry) { setTimeout(sendPlayGetOffer, 500); } } else { maxRetry = 120; retry = 0; let streamInfoResponse = msgJSON["streamInfo"]; if (streamInfoResponse !== undefined) { streamInfo.sessionId = streamInfoResponse.sessionId; } let sdpData = msgJSON["sdp"]; if (sdpData != null) { if (mungeSDP != null) { msgJSON.sdp.sdp = mungeSDP(msgJSON.sdp.sdp); } // Enhance here if Safari is a published stream. peerConnection .setRemoteDescription(new RTCSessionDescription(msgJSON.sdp)) .then(() => peerConnection .createAnswer() .then((description) => { peerConnection .setLocalDescription(description) .then(() => { wsConnection.send( '{"direction":"play", "command":"sendResponse", "streamInfo":' + JSON.stringify(streamInfo) + ', "sdp":' + JSON.stringify( description ) + ', "userData":' + JSON.stringify(userData) + '}' ); }) .catch((err) => { console.log("set local description error", err); }); }) ) .catch((err) => console.log("create answer error", err) ); } let iceCandidates = msgJSON["iceCandidates"]; if (iceCandidates != null) { for (let index in iceCandidates) { peerConnection.addIceCandidate(new RTCIceCandidate(iceCandidates[index])); } } } if ("sendResponse".localeCompare(msgCommand) == 0) { if (wsConnection != null) { wsConnection.close(); } wsConnection = null; } }; wsConnection.onclose = function () { console.log(`WebSocket connection closed | ${streamName.replace('.stream', '')}`); }; }; const mungeSDP = (sdpStr) => { // For greatest playback compatibility, // force H.264 playback to baseline (42e01f). let sdpLines = sdpStr.split(/\r\n/); let sdpStrRet = ""; for (var sdpIndex in sdpLines) { var sdpLine = sdpLines[sdpIndex]; if (sdpLine.length == 0) continue; if (sdpLine.includes("profile-level-id")) { // The profile-level-id string has three parts: XXYYZZ, where // XX: 42 baseline, 4D main, 64 high // YY: constraint // ZZ: level ID // Look for codecs higher than baseline and force downward. let profileLevelId = sdpLine.substr( sdpLine.indexOf("profile-level-id") + 17, 6 ); let profile = Number("0x" + profileLevelId.substr(0, 2)); let constraint = Number("0x" + profileLevelId.substr(2, 2)); let level = Number("0x" + profileLevelId.substr(4, 2)); if (profile > 0x42) { profile = 0x42; constraint = 0xe0; level = 0x1f; } let newProfileLevelId = ("00" + profile.toString(16)).slice(-2).toLowerCase() + ("00" + constraint.toString(16)).slice(-2).toLowerCase() + ("00" + level.toString(16)).slice(-2).toLowerCase(); sdpLine = sdpLine.replace(profileLevelId, newProfileLevelId); } sdpStrRet += sdpLine; sdpStrRet += "\r\n"; } return sdpStrRet; }; /* initialize and play, wire in play button here */ if (applicationName == "" || streamName == "" || wssUrl == "") { alert("Please fill out the connection details"); } else { const startDate = new Date(); videoElement = document.getElementById(remoteStream); videoElement.setAttribute('style', 'background: #000 url("./assets/images/camera status/camera-connecting.gif") center no-repeat; background-size: contain;'); wsConnect(); } }
问题原因分析
- WebSocket过早关闭:代码中收到
sendResponse指令后立即关闭WebSocket连接,但Wowza的WebRTC信令通道需要保持开启,用于后续的ICE候选更新、会话心跳维持,关闭后无法接收这些关键信息,导致PeerConnection在约1分钟后因NAT超时或会话过期断开。 - Wowza默认会话超时:Wowza WebRTC应用默认会话超时时间为60秒,若没有持续的信令交互,服务器会主动断开会话。
修复方案
1. 移除WebSocket过早关闭的代码
找到wsConnection.onmessage中的这段代码并删除:
if ("sendResponse".localeCompare(msgCommand) == 0) { if (wsConnection != null) { wsConnection.close(); } wsConnection = null; }
保持WebSocket连接打开,确保信令通道持续可用,支持ICE心跳和会话维持。
2. 调整Wowza服务器会话超时配置
登录Wowza管理控制台:
- 进入
Applications > 你的WebRTC应用 > WebRTC配置页面 - 修改
Session Timeout参数(单位:秒),建议设置为3600(1小时)或更长 - 保存配置并重启应用
3. 增强ICE连接状态监听
在peerConnection初始化时添加ICE连接状态监听,更精准处理连接异常:
peerConnection.oniceconnectionstatechange = () => { console.log(`ICE连接状态: ${peerConnection.iceConnectionState}`); if (peerConnection.iceConnectionState === 'disconnected' || peerConnection.iceConnectionState === 'failed') { if (retry < maxRetry) { retry++; setTimeout(() => wsConnect(), 1000); } } };
4. 优化重连逻辑
确保旧连接资源完全释放后再发起新重连:
const reconnect = () => { stop(); setTimeout(() => { wsConnect(); }, 1000); };
将onConnStateChange中的wsConnect()替换为reconnect(),避免资源泄漏。
内容的提问来源于stack exchange,提问作者Vishwas Acharya
相关产品推荐
相关产品推荐

