mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-21 01:01:41 +00:00
Currently, there is a difficult-to-reproduce bug where the backend reports that camera calibration was successful in logs via the logger but then throws an exception causing the backend to return a 500 error code with no request body which causes the frontend to interpret this as a failed calibration attempt. This ultimately leads to the entire instance of photonvision crashing and requiring the entire pi to be restarted. It is believed this issue resides inside the ConfigManager's saving action following the calibration update but is not confirmed.
307 lines
12 KiB
HTML
307 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
|
|
<head>
|
|
<title>ThinClient</title>
|
|
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
.img-container {
|
|
height: 100%;
|
|
width: 40vw;
|
|
}
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
<hr>
|
|
<div class="img-container">
|
|
<img id="streamImg" src='' alt="">
|
|
</div>
|
|
<hr>
|
|
<form id="frm1">
|
|
Host <input type="text" id="host" value="photonvision.local"><br>
|
|
Port <input type="text" id="port" value="1181"><br>
|
|
</form>
|
|
<button>Start Stream</button>
|
|
<script type="module">
|
|
class WebsocketVideoStream{
|
|
|
|
constructor(drawDiv, streamPort, host) {
|
|
|
|
this.drawDiv = drawDiv;
|
|
this.image = document.getElementById(this.drawDiv);
|
|
this.streamPort = streamPort;
|
|
this.newStreamPortReq = null;
|
|
this.serverAddr = "ws://" + host + "/websocket_cameras";
|
|
this.dispNoStream();
|
|
this.ws_connect();
|
|
this.imgData = null;
|
|
this.imgDataTime = -1;
|
|
this.imgObjURL = null;
|
|
this.frameRxCount = 0;
|
|
|
|
//Display state machine
|
|
this.DSM_DISCONNECTED = "DISCONNECTED";
|
|
this.DSM_WAIT_FOR_VALID_PORT = "WAIT_FOR_VALID_PORT";
|
|
this.DSM_SUBSCRIBE = "SUBSCRIBE";
|
|
this.DSM_WAIT_FOR_FIRST_FRAME = "WAIT_FOR_FIRST_FRAME";
|
|
this.DSM_SHOWING = "SHOWING";
|
|
this.DSM_RESTART_UNSUBSCRIBE = "UNSUBSCRIBE";
|
|
this.DSM_RESTART_WAIT = "WAIT_BEFORE_SUBSCRIBE";
|
|
|
|
this.dsm_cur_state = this.DSM_DISCONNECTED;
|
|
this.dsm_prev_state = this.DSM_DISCONNECTED;
|
|
this.dsm_restart_start_time = window.performance.now();
|
|
|
|
requestAnimationFrame(()=>this.animationLoop());
|
|
}
|
|
|
|
dispImageData(){
|
|
//From https://stackoverflow.com/questions/67507616/set-image-src-from-image-blob/67507685#67507685
|
|
if(this.imgObjURL != null){
|
|
URL.revokeObjectURL(this.imgObjURL)
|
|
}
|
|
this.imgObjURL = URL.createObjectURL(this.imgData);
|
|
|
|
//Update the image with the new mimetype and image
|
|
this.image.src = this.imgObjURL;
|
|
}
|
|
|
|
dispNoStream() {
|
|
this.image.src = "loading.svg";
|
|
}
|
|
|
|
animationLoop(){
|
|
// Update time metrics
|
|
const now = window.performance.now();
|
|
const timeInState = now - this.dsm_restart_start_time;
|
|
|
|
// Save previous state
|
|
this.dsm_prev_state = this.dsm_cur_state;
|
|
|
|
// Evaluate state transitions
|
|
if(!this.serverConnectionActive){
|
|
//Any state - if the server connection goes false, always transition to disconnected
|
|
this.dsm_cur_state = this.DSM_DISCONNECTED;
|
|
} else {
|
|
//Conditional transitions
|
|
switch(this.dsm_cur_state) {
|
|
case this.DSM_DISCONNECTED:
|
|
//Immediately transition to waiting for the first frame
|
|
this.dsm_cur_state = this.DSM_WAIT_FOR_VALID_PORT;
|
|
break;
|
|
case this.DSM_WAIT_FOR_VALID_PORT:
|
|
// Wait until the user has configured a valid port
|
|
if(this.streamPort > 0){
|
|
this.dsm_cur_state = this.DSM_SUBSCRIBE;
|
|
} else {
|
|
this.dsm_cur_state = this.DSM_WAIT_FOR_VALID_PORT;
|
|
}
|
|
break;
|
|
case this.DSM_SUBSCRIBE:
|
|
// Immediately transition after subscriptions is sent
|
|
this.dsm_cur_state = this.DSM_WAIT_FOR_FIRST_FRAME;
|
|
break;
|
|
case this.DSM_WAIT_FOR_FIRST_FRAME:
|
|
if(this.imgData != null){
|
|
//we got some image data, start showing it
|
|
this.dsm_cur_state = this.DSM_SHOWING;
|
|
} else if (this.newStreamPortReq != null){
|
|
//Stream port requested changed, unsubscribe and restart
|
|
this.dsm_cur_state = this.DSM_RESTART_UNSUBSCRIBE;
|
|
} else {
|
|
this.dsm_cur_state = this.DSM_WAIT_FOR_FIRST_FRAME;
|
|
}
|
|
break;
|
|
case this.DSM_SHOWING:
|
|
if((now - this.imgDataTime) > 2500){
|
|
//timeout, begin the restart sequence
|
|
this.dsm_cur_state = this.DSM_RESTART_UNSUBSCRIBE;
|
|
} else if (this.newStreamPortReq != null){
|
|
//Stream port requested changed, unsubscribe and restart
|
|
this.dsm_cur_state = this.DSM_RESTART_UNSUBSCRIBE;
|
|
} else {
|
|
//stay in this state.
|
|
this.dsm_cur_state = this.DSM_SHOWING;
|
|
}
|
|
break;
|
|
case this.DSM_RESTART_UNSUBSCRIBE:
|
|
//Only should spend one loop in Unsubscribe, immediately transition
|
|
this.dsm_cur_state = this.DSM_RESTART_WAIT;
|
|
break;
|
|
case this.DSM_RESTART_WAIT:
|
|
if (timeInState > 250) {
|
|
//we've waited long enough, go to try to re-subscribe
|
|
this.dsm_cur_state = this.DSM_WAIT_FOR_VALID_PORT;
|
|
} else {
|
|
//stay in this state.
|
|
this.dsm_cur_state = this.DSM_RESTART_WAIT;
|
|
}
|
|
break;
|
|
default:
|
|
// Shouldn't get here, default back to init
|
|
this.dsm_cur_state = this.DSM_DISCONNECTED;
|
|
}
|
|
}
|
|
|
|
//take current-state or state-transition actions
|
|
|
|
if(this.dsm_cur_state !== this.dsm_prev_state){
|
|
//Any state transition
|
|
console.log("State Change: " + this.dsm_prev_state + " -> " + this.dsm_cur_state);
|
|
}
|
|
|
|
if(this.dsm_cur_state === this.DSM_SHOWING){
|
|
// Currently in SHOWING
|
|
this.dispImageData();
|
|
}
|
|
|
|
if(this.dsm_cur_state !== this.DSM_SHOWING && this.dsm_prev_state === this.DSM_SHOWING ){
|
|
//Any transition out of showing - no stream
|
|
this.dispNoStream();
|
|
}
|
|
|
|
if(this.dsm_cur_state === this.DSM_RESTART_UNSUBSCRIBE){
|
|
// Currently in UNSUBSCRIBE, do the unsubscribe actions
|
|
this.stopStream();
|
|
this.dsm_restart_start_time = now;
|
|
}
|
|
|
|
if(this.dsm_cur_state === this.DSM_SUBSCRIBE){
|
|
// Currently in SUBSCRIBE, do the subscribe actions
|
|
this.startStream();
|
|
this.dsm_restart_start_time = now;
|
|
}
|
|
|
|
if(this.dsm_cur_state === this.DSM_WAIT_FOR_VALID_PORT){
|
|
// Currently waiting for a valid port to be requested
|
|
if(this.newStreamPortReq != null){
|
|
this.streamPort = this.newStreamPortReq;
|
|
this.newStreamPortReq = null;
|
|
}
|
|
}
|
|
|
|
requestAnimationFrame(()=>this.animationLoop());
|
|
}
|
|
|
|
startStream() {
|
|
console.log("Subscribing to port " + this.streamPort);
|
|
this.imgData = null;
|
|
this.ws.send(JSON.stringify({"cmd": "subscribe", "port":this.streamPort}));
|
|
}
|
|
|
|
stopStream() {
|
|
console.log("Unsubscribing");
|
|
this.ws.send(JSON.stringify({"cmd": "unsubscribe"}));
|
|
this.imgData = null;
|
|
}
|
|
|
|
setPort(streamPort){
|
|
console.log("Port set to " + streamPort);
|
|
this.newStreamPortReq = streamPort;
|
|
}
|
|
|
|
ws_onOpen() {
|
|
// Set the flag allowing general server communication
|
|
this.serverConnectionActive = true;
|
|
console.log("Connected!");
|
|
}
|
|
|
|
ws_onClose(e) {
|
|
//Clear flags to stop server communication
|
|
this.ws = null;
|
|
this.serverConnectionActive = false;
|
|
|
|
console.log('Camera Socket is closed. Reconnect will be attempted in 0.5 second.', e.reason);
|
|
setTimeout(this.ws_connect.bind(this), 500);
|
|
|
|
if(!e.wasClean){
|
|
console.error('Socket encountered error!');
|
|
}
|
|
|
|
}
|
|
|
|
ws_onError(e){
|
|
e; //prevent unused failure
|
|
this.ws.close();
|
|
}
|
|
|
|
ws_onMessage(e){
|
|
if(typeof e.data === 'string'){
|
|
//string data from host
|
|
//TODO - anything to receive info here? Maybe "available streams?"
|
|
} else {
|
|
if(e.data.size > 0){
|
|
//binary data - a frame
|
|
this.imgData = e.data;
|
|
this.imgDataTime = window.performance.now();
|
|
this.frameRxCount++;
|
|
} else {
|
|
//TODO - server is sending empty frames?
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ws_connect() {
|
|
this.serverConnectionActive = false;
|
|
this.ws = new WebSocket(this.serverAddr);
|
|
this.ws.binaryType = "blob";
|
|
this.ws.onopen = this.ws_onOpen.bind(this);
|
|
this.ws.onmessage = this.ws_onMessage.bind(this);
|
|
this.ws.onclose = this.ws_onClose.bind(this);
|
|
this.ws.onerror = this.ws_onError.bind(this);
|
|
console.log("Connecting to server " + this.serverAddr);
|
|
}
|
|
|
|
ws_close(){
|
|
this.ws.close();
|
|
}
|
|
|
|
}
|
|
|
|
let stream = null;
|
|
|
|
function streamStartRequest() {
|
|
const host = document.getElementById("host").value + ":5800";
|
|
const port = document.getElementById("port").value;
|
|
if(stream == null){
|
|
stream = new WebsocketVideoStream("streamImg",port,host);
|
|
} else {
|
|
stream.setPort(port);
|
|
}
|
|
|
|
}
|
|
|
|
// Attach listener
|
|
document.querySelector('button').addEventListener('click', streamStartRequest);
|
|
|
|
// Deal with URLParams, validating inputs
|
|
const queryString = window.location.search;
|
|
const urlParams = new URLSearchParams(queryString);
|
|
const port_in = urlParams.get('port')
|
|
const host_in = urlParams.get('host')
|
|
if(port_in !== ""){
|
|
document.getElementById("port").value = port_in;
|
|
}
|
|
|
|
if(host_in !== ""){
|
|
document.getElementById("host").value = host_in;
|
|
}
|
|
|
|
if(port_in !== "" && host_in !== ""){
|
|
streamStartRequest(); //we got valid inputs, auto-start the stream
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|