我们利用 Wowza 平台来获取直播流,并在 Angular 框架内,利用固有的 websockets 和 RTC PeerConnection 类来促进通过安全 wss 协议将直播流接收到 Angular 应用程序中。
虽然我们能够获取直播,但我们遇到了每分钟就会断线的问题。
这是我们编写的使用 websockets 和 RTC 对等连接获取直播流的代码:
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();
}
}
尽管成功获取了直播流,但我们面临着每分钟都会断线的挑战。
采取的步骤:
解决方案: 增加Wowza中的
webrtc idletimeout
属性。通过延长空闲超时,Wowza 将延迟断开流的连接。如果在指定的空闲超时内收到摄像头流,该流将恢复播放。
连接/断开原因: 由于网络问题导致相机和 Wowza 之间经常断开连接。
此调整旨在增强 WebSocket 连接的稳定性,并解决 Angular 应用程序中 Wowza 直播过程中反复出现的断开连接问题。