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.