You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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();
    }
}

问题原因分析

  1. WebSocket过早关闭:代码中收到sendResponse指令后立即关闭WebSocket连接,但Wowza的WebRTC信令通道需要保持开启,用于后续的ICE候选更新、会话心跳维持,关闭后无法接收这些关键信息,导致PeerConnection在约1分钟后因NAT超时或会话过期断开。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 02:47:04