Skip to main content

HTML Video over Websockets

Shenzhen, China

This is a frontend client for the INSTAR WQHD (IN-9408 2k+) camera websocket server. It connects to the server and displays the camera's live video stream inside an HTML5 video tag.

HTML Frontend (WS Client)

HTML Video over Websockets

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="connect-src * 'unsafe-inline';">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-F3w7mX95PdgyTmZZMECAngseQB83DfGTowi0iMjiWaeVhAn4FJkqJByhZMI3AhiU" crossorigin="anonymous"><link rel="stylesheet" type="text/css" href="https://unpkg.com/notie/dist/notie.min.css">
<style>
/* override styles here */
.notie-container {
box-shadow: none;
}
</style>
</head>
<body>
<nav class="navbar sticky-top navbar-dark bg-dark mb-3">
<div class="container-fluid">
<a class="navbar-brand" href="#">
<img src="instar.svg" alt="" width="156" height="45" class="d-inline-block">
</a>
<div class="d-flex text-light me-2" id="status">
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row justify-content-between">
<div class="col">
<div class="container-md" id="connection_paramter">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Websocket</th>
<th scope="col">Configuration</th>
</tr>
</thead>
<tbody>
<tr>
<th width="200px" scope="row">WS Protocol</th>
<td>
<select id="protocol" class="form-select">
<option value="ws" selected="selected">ws</option>
<option value="wss">wss</option>
</select>
</td>
</tr>
<tr>
<th scope="row">WS Hostname</th>
<td>
<div class="input-group">
<input type="text" class="form-control" id="hostname" value="192.168.2.19"/>
</div>
</td>
</tr>
<tr>
<th scope="row">WS Port</th>
<td>
<div class="input-group">
<input type="text" class="form-control" id="port" value="80"/>
</div>
</td>
</tr>
<tr>
<th scope="row">WS Endpoint</th>
<td>
<div class="input-group">
<input type="text" class="form-control" id="endpoint" value="/ws" />
</div>
</td>
</tr>
<tr>
<th scope="row">Connection</th>
<td>
<div class="d-flex gap-2">
<button type="button" class="btn btn-primary p-2 flex-grow-1" id="btnConnect" onclick="onConnectClick()">
Connect
</button>
<button type="button" class="btn btn-danger p-2 flex-grow-1" id="btnDisconnect" onclick="onDisconnectClick()" disabled>
Disconnect
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col">
<div class="container-md" id="message">
<table class="table table-hover">
<thead>
<tr>
<th colspan="2" scope="col">h.264 Stream</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Username</th>
<td>
<div class="input-group">
<input type="text" class="form-control" id="username" value="admin"/>
</div>
</td>
</tr>
<tr>
<th scope="row">Password</th>
<td>
<div class="input-group">
<input type="password" class="form-control" id="password" value="instar" />
</div>
</td>
</tr>
<tr>
<td colspan="2">
<div class="input-group">
<select id="wsmessage" class="form-select">
<option value="livestream" selected="selected">Start</option>
<option value="stop">Stop</option>
</select>
<!-- <input type="text" class="form-control" id="wsmessage" value="livestream"> -->
</div>
</td>
</tr>
<tr>
<td colspan="2">
<div class="d-flex">
<button type="button" class="btn btn-success p-2 flex-grow-1" id="btnSend" onclick="onSendClick()" disabled>
Send Message
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="container-fluid">
<div class="row" id="message">

<div class="col align-self-center">
<video playsinline muted controls preload="none" width="100%"></video>
</div>

<div class="accordion accordion mb-5" id="accordionFlushExample">
<div class="accordion-item">
<h2 class="accordion-header" id="headingOne">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="false" aria-controls="collapseOne">
Incoming Messages
</button>
</h2>
<div id="collapseOne" class="accordion-collapse collapse" aria-labelledby="headingOne" data-bs-parent="#accordionFlushExample">
<div class="input-group">
<textarea class="form-control" id="incomingMsgOutput" rows="10" cols="20" disabled="disabled"></textarea>
</div>
</div>
</div>
</div>

</div>
</div>
</div>
</div>

<!-- Scripts -->
<!-- Websocket Client -->
<script src="wsclient.js"></script>
<!-- Bootstrap JS and Popper.js -->
<!-- <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.3/dist/umd/popper.min.js" integrity="sha384-W8fXfP3gkOKtndU4JGtKDvXbO53Wy8SZCQHczT5FMiiqmQfUpWbYdTil/SxwZgAN" crossorigin="anonymous"></script> -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/js/bootstrap.min.js" integrity="sha384-skAcpIdS7UcVUC05LJ9Dxay8AXcDYfBJqt1CJ85S/CFujBsIzCIv+l9liuYLaMQ/" crossorigin="anonymous"></script>
<script src="https://unpkg.com/notie"></script>
<script src="reconnecting-websocket.min.js"></script>
</body>
</html>

Websocket Server


var ws_protocol = document.getElementById("protocol");
var ws_hostname = document.getElementById("hostname");
var ws_port = document.getElementById("port");
var ws_endpoint = document.getElementById("endpoint");
var cam_username = document.getElementById("username");
var cam_password = document.getElementById("password");

var queue = [];
var video = null;
var webSocket = null;
var sourceBuffer = null;
var streamingStarted = false;

// Display ws pre-connected state
var statusBadge = document.getElementById("status");
const idle = `<h4><span class="badge bg-primary">WS Client</span></h4>`
statusBadge.innerHTML = idle;

// Init the Media Source and add event listener
function initMediaSource() {
video = document.querySelector('video');
video.onerror = elementError;
video.loop = false;
video.addEventListener('canplay', (event) => {
console.log('Video can start, but not sure it will play through.');
video.play();
});
video.addEventListener('paused', (event) => {
console.log('Video paused for buffering...');
setTimeout(function() { video.play(); }, 2000);
});

/* NOTE: Chrome will not play the video if we define audio here
* and the stream does not include audio */
var mimeCodec = 'video/mp4; codecs="avc1.4D0033, mp4a.40.2"';
//var mimeCodec = 'video/mp4; codecs=avc1.42E01E,mp4a.40.2'; baseline
//var mimeCodec = 'video/mp4; codecs=avc1.4d002a,mp4a.40.2'; main
//var mimeCodec = 'video/mp4; codecs="avc1.64001E, mp4a.40.2"'; high

if (!window.MediaSource) {
console.error("No Media Source API available");
document.getElementById("incomingMsgOutput").value += "error: No Media Source API available" + "\r\n";
return;
}

if (!MediaSource.isTypeSupported(mimeCodec)) {
console.error("Unsupported MIME type or codec: " + mimeCodec);
document.getElementById("incomingMsgOutput").value += "error: Unsupported MIME type or codec" + "\r\n";
return;
}

var ms = new MediaSource();
video.src = window.URL.createObjectURL(ms);
ms.addEventListener('sourceopen', onMediaSourceOpen);

function onMediaSourceOpen() {
sourceBuffer = ms.addSourceBuffer(mimeCodec);
sourceBuffer.addEventListener("updateend",loadPacket);
sourceBuffer.addEventListener("onerror", sourceError);
}

function loadPacket() { // called when sourceBuffer is ready for more
if (!sourceBuffer.updating) {
if (queue.length>0) {
data = queue.shift(); // pop from the beginning
appendToBuffer(data);
} else { // the queue runs empty, so we must force-feed the next packet
streamingStarted = false;
}
}
else {}
}

function sourceError(event) {
console.log("Media source error");
}

function elementError(event) {
console.log("Media element error");
}
}

// Append AV data to source buffer
function appendToBuffer(videoChunk) {
if (videoChunk) {
sourceBuffer.appendBuffer(videoChunk);
}
}

// Event handler for clicking on button "Connect"
function onConnectClick() {
// Makes sure that user typed username and message before sending
if ((ws_protocol.value === '') || (ws_hostname.value === '') || (ws_port.value === '') || (ws_endpoint.value === '') ||(cam_username === '') || (cam_password === '')) {
errorToast("Please fill out all the configuration fields above!");
return false;
} else {
initMediaSource();
document.getElementById("incomingMsgOutput").value = "";
document.getElementById("btnConnect").disabled = true;
openWSConnection(ws_protocol.value, ws_hostname.value, ws_port.value, ws_endpoint.value);
successToast("Send the 'Start' message to start the video stream.");
}
}

// Event handler for clicking on button "Disconnect"
function onDisconnectClick() {
document.getElementById("btnDisconnect").disabled = true;
webSocket.close();
video.pause();
}

// Adding confirmations with notie.js
function successToast(msg) {
notie.alert({
type: 'success', // optional, default = 4, enum: [1, 2, 3, 4, 5, 'success', 'warning', 'error', 'info', 'neutral']
text: msg,
stay: false, // optional, default = false
time: 3, // optional, default = 3, minimum = 1,
position: 'bottom' // optional, default = 'top', enum: ['top', 'bottom']
})
}

//Adding alerts with notie.js
function errorToast(msg) {
notie.alert({
type: 'error', // optional, default = 4, enum: [1, 2, 3, 4, 5, 'success', 'warning', 'error', 'info', 'neutral']
text: msg,
stay: false, // optional, default = false
time: 3, // optional, default = 3, minimum = 1,
position: 'bottom' // optional, default = 'top', enum: ['top', 'bottom']
})
}

// Open a new WebSocket connection using the given parameters
function openWSConnection(protocol, hostname, port, endpoint) {

var webSocketURL = null;
var keepAliveCount = 0;

webSocketURL = protocol + "://" + hostname + ":" + port + endpoint;
console.log("openWSConnection::Connecting to: " + webSocketURL);

const offline = `<h4><span class="badge bg-danger">Disconnected</span></h4>`
const online = `<h4><span class="badge bg-success">Connected</span></h4>`

let statusBadge = document.getElementById("status");

try {
// webSocket = new WebSocket(webSocketURL);
webSocket = new ReconnectingWebSocket(webSocketURL);
webSocket.debug = true;
webSocket.timeoutInterval = 3000;
webSocket.onopen = function(openEvent) {
var open = JSON.stringify(openEvent, null, 4);
console.log("WebSocket open");
document.getElementById("btnSend").disabled = false;
document.getElementById("btnConnect").disabled = true;
document.getElementById("btnDisconnect").disabled = false;
document.getElementById("incomingMsgOutput").value += "WebSocket connected" + "\r\n";
statusBadge.innerHTML = online
};
webSocket.onclose = function (closeEvent) {
var closed = JSON.stringify(closeEvent, null, 4);
console.log("WebSocket closed");
document.getElementById("btnSend").disabled = true;
document.getElementById("btnConnect").disabled = false;
document.getElementById("btnDisconnect").disabled = true;
document.getElementById("incomingMsgOutput").value += "WebSocket closed" + "\r\n";
statusBadge.innerHTML = offline
};
webSocket.onerror = function (errorEvent) {
var error = JSON.stringify(errorEvent, null, 4);
console.log("WebSocket ERROR: " + error);
document.getElementById("btnConnect").disabled = false;
document.getElementById("incomingMsgOutput").value += "error: Websocket connection failed" + "\r\n";
statusBadge.innerHTML = offline
};
webSocket.onmessage = function (messageEvent) {
var wsMsg = messageEvent.data;
if (typeof wsMsg === 'string') {
if (wsMsg.indexOf("error:") == 0) {
document.getElementById("incomingMsgOutput").value += wsMsg + "\r\n";
} else {
document.getElementById("incomingMsgOutput").value += "echo message: " + wsMsg + "\r\n";
}
} else {
var arrayBuffer;
var fileReader = new FileReader();
fileReader.onload = function(event) {
arrayBuffer = event.target.result;
var data = new Uint8Array(arrayBuffer);
document.getElementById("incomingMsgOutput").value += "received: " + data.length + " bytes\r\n";
if (!streamingStarted) {
appendToBuffer(arrayBuffer);
streamingStarted=true;
return;
}
queue.push(arrayBuffer); // add to the end
};
fileReader.readAsArrayBuffer(wsMsg);
/* NOTE: the web server has a idle-timeout of 60 seconds,
so we need to send a keep-alive message regulary */
keepAliveCount++;
if (keepAliveCount >= 10 && webSocket.readyState == WebSocket.OPEN) {
keepAliveCount = 0;
webSocket.send("keep-alive");
}
}
};
} catch (exception) {
console.error(exception);
}
}

// Send a message to the WebSocket server
function onSendClick() {
if (webSocket.readyState != WebSocket.OPEN) {
console.error("webSocket is not open: " + webSocket.readyState);
return;
}
var msg = document.getElementById("wsmessage").value;
webSocket.send(msg);
}

User Notifications

Adding toast notifications with Notie.js.

Auto-Reconnect the Websocket

Make sure that the connection is always re-established with ReconnectingWebsockets.