angularwebsocketwebrtclive-streamingrtcpeerconnection

The WebSocket connection is being terminated every minute when attempting to establish a live stream from Wowza


We utilize the Wowza platform to obtain the live stream, and within the Angular framework, we leverage the inherent websockets and RTC PeerConnection class to facilitate the receipt of the live stream into the Angular application via the secure wss protocol.

Although we are able to obtain the live stream, we have encountered an issue whereby the stream becomes disconnected every minute.

Here's the code that we wrote to get the live stream using websockets and RTC peerconnection:

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

Solution

  • Despite successfully obtaining the live stream, we face a challenge where the stream disconnects every minute.

    Steps Taken:

    1. Attempted various configuration changes in Wowza and on a code level, but did not observe any improvement.
    2. Verified the issue across different systems and network bandwidths.

    Solution: Increase the webrtc idletimeout property in Wowza. By extending the idletimeout, Wowza will delay disconnecting the stream. If the camera stream is received within the specified idletimeout, the stream will resume playing.

    Reason for Connection/Disconnection: The frequent disconnection between the camera and Wowza results from network issues.


    This adjustment aims to enhance the stability of the WebSocket connection and address the recurring disconnection problem during live streaming from Wowza within an Angular application.