1013 lines
111 KiB
HTML
1013 lines
111 KiB
HTML
|
<!DOCTYPE html>
|
||
|
<html lang="en">
|
||
|
<head>
|
||
|
<meta charset="utf-8">
|
||
|
<style type="text/css">
|
||
|
html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;-moz-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]: after,a[href^="javascript:"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.label{border:1px solid #000}}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}[role=button]{cursor:pointer}h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1,h2,h3{margin-top:20px;margin-bottom:10px}h4,h5,h6{margin-top:10px;margin-bottom:10px}p{margin:0 0 10px}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{
|
||
|
html {
|
||
|
font-size: 16px;
|
||
|
overflow-y: auto;
|
||
|
}
|
||
|
body {
|
||
|
color: #cacaca;
|
||
|
background-color: #242424;
|
||
|
user-select: text;
|
||
|
}
|
||
|
|
||
|
h3,
|
||
|
h4 {
|
||
|
color: #cacaca;
|
||
|
font-weight: 300;
|
||
|
font-size: 18px;
|
||
|
}
|
||
|
|
||
|
h3 {
|
||
|
margin-bottom: 20px;
|
||
|
font-size: 1.5rem;
|
||
|
}
|
||
|
|
||
|
h4 { margin-top: 4px; }
|
||
|
|
||
|
h4 a {
|
||
|
color: #cacaca;
|
||
|
/*margin-left: 4px;*/
|
||
|
}
|
||
|
h4 a.btn {
|
||
|
margin-top: -6px;
|
||
|
float:right;
|
||
|
}
|
||
|
|
||
|
h4 a:focus, h4 a:hover {
|
||
|
color: #cacaca;
|
||
|
text-decoration: none;
|
||
|
}
|
||
|
|
||
|
/* Overwrites */
|
||
|
.text-warning {
|
||
|
color: #de8938;
|
||
|
}
|
||
|
|
||
|
.well {
|
||
|
background-color: #242424;
|
||
|
border: 1px solid #525252;
|
||
|
}
|
||
|
|
||
|
.form-control:focus, .btn:focus {
|
||
|
outline: none;
|
||
|
box-shadow: none;
|
||
|
}
|
||
|
|
||
|
.form-control {
|
||
|
display: inline-block;
|
||
|
}
|
||
|
|
||
|
/* Buttons */
|
||
|
.btn {
|
||
|
letter-spacing: 0.025rem;
|
||
|
border-radius: 4px;
|
||
|
line-height: 18px;
|
||
|
margin-top: -1px;
|
||
|
margin-bottom: 1px;
|
||
|
}
|
||
|
.btn-default {
|
||
|
background-color: #333333;
|
||
|
color: #cacaca;
|
||
|
border-color: #525252;
|
||
|
}
|
||
|
|
||
|
.btn-default:active, .btn-default:focus, .btn-default:hover, .btn-default:active:hover {
|
||
|
color: #cacaca;
|
||
|
background-color: #626262;
|
||
|
background-image: none;
|
||
|
border-color: #525252;
|
||
|
}
|
||
|
|
||
|
.disable {
|
||
|
pointer-events: none;
|
||
|
opacity: 0.4;
|
||
|
}
|
||
|
|
||
|
.collapsed {
|
||
|
display: none;
|
||
|
}
|
||
|
|
||
|
.glyphicon {
|
||
|
margin-top: -4px;
|
||
|
}
|
||
|
|
||
|
/* Selects */
|
||
|
select.form-control {
|
||
|
height:30px;
|
||
|
background-color: #626262;
|
||
|
color: #cacaca;
|
||
|
border: 1px solid #525252;
|
||
|
-webkit-appearance:none;
|
||
|
line-height: 14px;
|
||
|
cursor:pointer;
|
||
|
}
|
||
|
|
||
|
/* Console */
|
||
|
#console {
|
||
|
min-height: 201px;
|
||
|
max-height: 201px;
|
||
|
padding: 0 8px;
|
||
|
overflow-y: auto;
|
||
|
overflow-x: hidden;
|
||
|
font-family: monospace, monospace;
|
||
|
font-size: 0.875rem;
|
||
|
margin-bottom: 0px;
|
||
|
}
|
||
|
#console p { margin: 0 0 4px; }
|
||
|
#console .comment { color: #2C9040; }
|
||
|
#console pre {
|
||
|
color: #aaaaaa;
|
||
|
background-color: #626262;
|
||
|
border: 1px solid #525252;
|
||
|
border-radius: 4px;
|
||
|
}
|
||
|
|
||
|
#console-holder {
|
||
|
position: absolute;
|
||
|
bottom: 0;
|
||
|
left: 0;
|
||
|
right: 0;
|
||
|
}
|
||
|
|
||
|
.panel-action {
|
||
|
position: absolute;
|
||
|
right: 6px;
|
||
|
top: 8px;
|
||
|
}
|
||
|
|
||
|
#controls-holder {
|
||
|
position: absolute;
|
||
|
left: 0;
|
||
|
top: 0;
|
||
|
right: 0;
|
||
|
height: 42px;
|
||
|
}
|
||
|
|
||
|
#view-holder {
|
||
|
position: absolute;
|
||
|
left: 0;
|
||
|
top: 47px;
|
||
|
right: 0;
|
||
|
bottom: 251px;
|
||
|
}
|
||
|
|
||
|
#cam-holder {
|
||
|
width: 342px;
|
||
|
left: 0px;
|
||
|
overflow-x: hidden;
|
||
|
overflow-y: auto;
|
||
|
}
|
||
|
|
||
|
#stream-holder {
|
||
|
left: 342px;
|
||
|
right: 0px;
|
||
|
}
|
||
|
|
||
|
#cam-holder, #stream-holder {
|
||
|
padding: 0px;
|
||
|
height: 100%;
|
||
|
position: absolute;
|
||
|
top: 0px;
|
||
|
bottom: 0px;
|
||
|
}
|
||
|
|
||
|
#cam-holder .row, #stream-holder .row {
|
||
|
margin: 5px;
|
||
|
padding: 5px;
|
||
|
border: 1px solid #525252;
|
||
|
border-radius: 4px;
|
||
|
background-color: #333333;
|
||
|
}
|
||
|
|
||
|
.preview-tbar {
|
||
|
background-color: #000000;
|
||
|
}
|
||
|
|
||
|
.cam-selected {
|
||
|
background-color: #aa0000;
|
||
|
}
|
||
|
|
||
|
#cam-holder img {
|
||
|
cursor: pointer;
|
||
|
}
|
||
|
|
||
|
#stream {
|
||
|
max-width: 100%;
|
||
|
max-height: 100%;
|
||
|
height: 100%;
|
||
|
width:100%;
|
||
|
object-fit: contain;
|
||
|
}
|
||
|
|
||
|
#stream-win {
|
||
|
width: 100%;
|
||
|
height: 100%;
|
||
|
text-align: center;
|
||
|
background-color: black;
|
||
|
}
|
||
|
|
||
|
#resolution {
|
||
|
width:170px;
|
||
|
}
|
||
|
|
||
|
#refresh-mdns-interval {
|
||
|
width:100px;
|
||
|
}
|
||
|
|
||
|
#switch-cams-interval, #refresh-thumbs-interval {
|
||
|
width:54px;
|
||
|
}
|
||
|
|
||
|
#xclk {
|
||
|
width:auto;
|
||
|
height:30px;
|
||
|
background-color: #626262;
|
||
|
color: #cacaca;
|
||
|
border: 1px solid #525252;
|
||
|
}
|
||
|
|
||
|
.sitem, .mitem, .fitem {
|
||
|
position: relative;
|
||
|
border-radius: 0;
|
||
|
}
|
||
|
|
||
|
.sitem {
|
||
|
z-index: 2;
|
||
|
margin-right: -2px;
|
||
|
/*margin-top: -1px;*/
|
||
|
border-top-left-radius: 4px;
|
||
|
border-bottom-left-radius: 4px;
|
||
|
}
|
||
|
|
||
|
.fitem {
|
||
|
z-index: 2;
|
||
|
margin-left: -2px;
|
||
|
border-top-right-radius: 4px;
|
||
|
border-bottom-right-radius: 4px;
|
||
|
}
|
||
|
|
||
|
</style>
|
||
|
<script type="text/javascript">
|
||
|
/*
|
||
|
* Console
|
||
|
*/
|
||
|
function isArray(a){
|
||
|
return (typeof a === "object" && a instanceof Array);
|
||
|
}
|
||
|
|
||
|
function isObject(a){
|
||
|
return (typeof a === "object" && (a instanceof Array) == false);
|
||
|
}
|
||
|
|
||
|
function isFunction(a){
|
||
|
return (typeof a === "function");
|
||
|
}
|
||
|
|
||
|
function isString(a){
|
||
|
return (typeof a === "string" || (typeof a === 'undefined' && a instanceof String));
|
||
|
}
|
||
|
|
||
|
String.prototype.contains = function(it) {
|
||
|
return this.indexOf(it) != -1;
|
||
|
};
|
||
|
|
||
|
function addToConsole(msg, color){
|
||
|
document.getElementById("gCodeLog").innerHTML += '<p class="text-'+color+'">' + msg + '</p>';
|
||
|
let con = document.getElementById("console");
|
||
|
con.scrollTop = con.scrollHeight;
|
||
|
}
|
||
|
|
||
|
function gDebug(msg){ addToConsole(msg, "muted"); }
|
||
|
function gLog(msg){ addToConsole(msg, "info"); }
|
||
|
function gInfo(msg){ addToConsole(msg, "success"); }
|
||
|
function gWarn(msg){ addToConsole(msg, "warning"); }
|
||
|
function gError(msg){ addToConsole(msg, "danger"); }
|
||
|
function gException(e){
|
||
|
var msg = "Error: '"+e+"'";
|
||
|
if(e instanceof Error){
|
||
|
// var stack = e.stack;
|
||
|
// while (stack.indexOf('\n') >= 0) {
|
||
|
// stack = stack.replace('\n', '<br/>');
|
||
|
// }
|
||
|
msg = e.name+": '"+e.message+"', line: "+e.line+", stack: <br/><pre>"+e.stack+"</pre>";
|
||
|
}
|
||
|
gError(msg);
|
||
|
return msg;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Variables
|
||
|
*/
|
||
|
var resolutions = {
|
||
|
// <!-- 5MP -->
|
||
|
"21":"QSXGA(2560x1920)",
|
||
|
"20":"P FHD(1080x1920)",
|
||
|
"19":"WQXGA(2560x1600)",
|
||
|
"18":"QHD(2560x1440)",
|
||
|
// <!-- 3MP -->
|
||
|
"17":"QXGA(2048x1564)",
|
||
|
"16":"P 3MP(864x1564)",
|
||
|
"15":"P HD(720x1280)",
|
||
|
"14":"FHD(1920x1080)",
|
||
|
// <!-- 2MP -->
|
||
|
"13":"UXGA(1600x1200)",
|
||
|
"12":"SXGA(1280x1024)",
|
||
|
"11":"HD(1280x720)",
|
||
|
"10":"XGA(1024x768)",
|
||
|
"9":"SVGA(800x600)",
|
||
|
// <!-- VGA -->
|
||
|
"8":"VGA(640x480)",
|
||
|
"7":"HVGA(480x320)",
|
||
|
"6":"CIF(400x296)",
|
||
|
"5":"QVGA(320x240)",
|
||
|
"4":"240x240",
|
||
|
"3":"HQVGA(240x176)",
|
||
|
"2":"QCIF(176x144)",
|
||
|
"1":"QQVGA(160x120)",
|
||
|
"0":"96x96"
|
||
|
};
|
||
|
|
||
|
var pixformats = [
|
||
|
"RGB565", // 2BPP/RGB565
|
||
|
"YUV422", // 2BPP/YUV422
|
||
|
"GRAYSCALE", // 1BPP/GRAYSCALE
|
||
|
"JPEG", // JPEG/COMPRESSED
|
||
|
"RGB888", // 3BPP/RGB888
|
||
|
"RAW", // RAW
|
||
|
"RGB444", // 3BP2P/RGB444
|
||
|
"RGB555", // 3BP2P/RGB555
|
||
|
];
|
||
|
|
||
|
var cameras = {};
|
||
|
var selectedCamera = null;
|
||
|
var playing = false;
|
||
|
|
||
|
/*
|
||
|
* Camera URLs
|
||
|
*/
|
||
|
|
||
|
function getCamURL(id){
|
||
|
let host = `http://${cameras[id].ip}`;
|
||
|
let port = cameras[id].port;
|
||
|
if(port !== 80){
|
||
|
return `${host}:${port}`;
|
||
|
}
|
||
|
return host;
|
||
|
}
|
||
|
|
||
|
function getCamStreamURL(id){
|
||
|
let host = `http://${cameras[id].ip}`;
|
||
|
let port = cameras[id].txt.stream_port?parseInt(cameras[id].txt.stream_port):cameras[id].port;
|
||
|
if(port !== 80){
|
||
|
return `${host}:${port}/stream`;
|
||
|
}
|
||
|
return `${host}/stream`;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* AJAX
|
||
|
*/
|
||
|
function fetchTimeout(url, options, timeout = 2000) {
|
||
|
return Promise.race([
|
||
|
fetch(url, options),
|
||
|
new Promise((_, reject) =>
|
||
|
setTimeout(() => reject(new Error('timeout')), timeout)
|
||
|
)
|
||
|
]);
|
||
|
}
|
||
|
|
||
|
function fetchUrl(url, type, cb){
|
||
|
function ecb(code, data){
|
||
|
if(data instanceof TypeError && data.message == "cancelled"){
|
||
|
if(cb)cb(200, null);
|
||
|
} else {
|
||
|
gError("Fetch '"+url+"' Failed!");
|
||
|
if(cb)cb(code, gException(data));
|
||
|
}
|
||
|
}
|
||
|
fetchTimeout(url)
|
||
|
.then(function (response) {
|
||
|
if (response.status !== 200) {
|
||
|
ecb(response.status, response.statusText);
|
||
|
} else {
|
||
|
var promis = null;
|
||
|
switch(type){
|
||
|
case "arrayBuffer": promis = response.arrayBuffer(); break;
|
||
|
case "blob": promis = response.blob(); break;
|
||
|
case "formData": promis = response.formData(); break;
|
||
|
case "json": promis = response.json(); break;
|
||
|
default: promis = response.text(); break;
|
||
|
}
|
||
|
if(promis !== null){
|
||
|
promis.then(function(data){
|
||
|
if(cb)cb(200, data, response.headers);
|
||
|
}).catch(function(err) {
|
||
|
ecb(-1, err);
|
||
|
});
|
||
|
} else {
|
||
|
ecb(-1, "Unknown type: "+type);
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
.catch(function(err) {
|
||
|
ecb(-1, err);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Camera Thumbnails
|
||
|
*/
|
||
|
|
||
|
function getCamURL(id){
|
||
|
let host = `http://${cameras[id].ip}`;
|
||
|
let port = cameras[id].port;
|
||
|
if(port !== 80){
|
||
|
return `${host}:${port}`;
|
||
|
}
|
||
|
return host;
|
||
|
}
|
||
|
|
||
|
function getCamStreamURL(id){
|
||
|
let host = `http://${cameras[id].ip}`;
|
||
|
let port = cameras[id].txt.stream_port?parseInt(cameras[id].txt.stream_port):cameras[id].port;
|
||
|
if(port !== 80){
|
||
|
return `${host}:${port}/stream`;
|
||
|
}
|
||
|
return `${host}/stream`;
|
||
|
}
|
||
|
|
||
|
function loadCameraThumbnail(id){
|
||
|
function getCb(id){
|
||
|
return function(){
|
||
|
loadCameraThumbnail(id);
|
||
|
}
|
||
|
}
|
||
|
function copyThumbnailFromPlayer(id) {
|
||
|
let view = document.getElementById('stream');
|
||
|
var canvas = document.createElement("canvas");
|
||
|
canvas.width = view.naturalWidth;
|
||
|
canvas.height = view.naturalHeight;
|
||
|
document.body.appendChild(canvas);
|
||
|
var context = canvas.getContext('2d');
|
||
|
context.drawImage(view,0,0);
|
||
|
try {
|
||
|
canvas.toBlob(function(data){
|
||
|
if(data && cameras.hasOwnProperty(id)){
|
||
|
document.getElementById(id).src = window.URL.createObjectURL(data);
|
||
|
}
|
||
|
}, 'image/jpeg', 0.80);
|
||
|
//document.getElementById(id).src = canvas.toDataURL('image/jpeg');
|
||
|
} catch (e) {
|
||
|
gException(e);
|
||
|
}
|
||
|
canvas.parentNode.removeChild(canvas);
|
||
|
}
|
||
|
|
||
|
function loadRemoteThumbnail(id){
|
||
|
if(cameras.hasOwnProperty(id)){
|
||
|
fetchUrl(getCamURL(id)+'/capture', "blob", function(code, data, headers){
|
||
|
if(code < 0){
|
||
|
removeCamera(id);
|
||
|
} else if(code === 200 && cameras.hasOwnProperty(id)){
|
||
|
if(data){
|
||
|
document.getElementById(id).src = window.URL.createObjectURL(data);
|
||
|
if(selectedCamera == id && !playing){
|
||
|
//copy thumbnail to player
|
||
|
document.getElementById("stream").src = document.getElementById(id).src;
|
||
|
}
|
||
|
}
|
||
|
var to = parseInt(document.getElementById("refresh-thumbs-interval").value);
|
||
|
if(to){
|
||
|
cameras[id].timeout = setTimeout(getCb(id), to * 1000);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(cameras[id].timeout !== null){
|
||
|
clearTimeout(cameras[id].timeout);
|
||
|
cameras[id].timeout = null;
|
||
|
}
|
||
|
if(selectedCamera == id && playing){
|
||
|
copyThumbnailFromPlayer(id);
|
||
|
var to = parseInt(document.getElementById("refresh-thumbs-interval").value);
|
||
|
if(to){
|
||
|
cameras[id].timeout = setTimeout(getCb(id), to * 1000);
|
||
|
}
|
||
|
} else {
|
||
|
loadRemoteThumbnail(id);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Thumbnail Refresh
|
||
|
*/
|
||
|
function refreshThumbnails(){
|
||
|
for(var cam in cameras){
|
||
|
loadCameraThumbnail(cam);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function initThumbnailRefresh(){
|
||
|
var button = document.getElementById("refresh-thumbs");
|
||
|
button.onclick = function(e){
|
||
|
refreshThumbnails();
|
||
|
};
|
||
|
document.getElementById("refresh-thumbs-interval").onchange = function(e){
|
||
|
let value = parseInt(e.target.value);
|
||
|
|
||
|
window.stop();
|
||
|
//reastart playback
|
||
|
if(selectedCamera !== null && playing){
|
||
|
document.getElementById("stream").src = getCamStreamURL(selectedCamera);
|
||
|
}
|
||
|
//clear all timeouts
|
||
|
for(var cam in cameras){
|
||
|
if(cameras[cam].timeout != null){
|
||
|
clearTimeout(cameras[cam].timeout);
|
||
|
cameras[cam].timeout = null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(value > 0){
|
||
|
button.classList.add('disable');
|
||
|
refreshThumbnails();
|
||
|
} else {
|
||
|
button.classList.remove('disable');
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Camera Boxes
|
||
|
*/
|
||
|
|
||
|
function removeCamera(id){
|
||
|
if(!cameras.hasOwnProperty(id)){
|
||
|
return;
|
||
|
}
|
||
|
let item = document.getElementById('item-'+id);
|
||
|
if(item){
|
||
|
gWarn("Remove Camera["+id+"]: "+cameras[id].instance);
|
||
|
item.parentElement.removeChild(item);
|
||
|
}
|
||
|
if(cameras[id].timeout != null){
|
||
|
clearTimeout(cameras[id].timeout);
|
||
|
cameras[id].timeout = null;
|
||
|
}
|
||
|
delete cameras[id];
|
||
|
if(selectedCamera == id){
|
||
|
selectedCamera = null;
|
||
|
if(playing){
|
||
|
onCameraButtonStop(id);
|
||
|
}
|
||
|
document.getElementById("controls-holder").classList.add('disable');
|
||
|
playButtonClear();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function updateCamera(id){
|
||
|
document.getElementById("title-"+id).innerHTML = cameras[id].instance;
|
||
|
let anchor = document.createElement("a");
|
||
|
anchor.href = getCamURL(id);
|
||
|
anchor.target = "_blank";
|
||
|
anchor.classList.add('btn');
|
||
|
anchor.classList.add('btn-sm');
|
||
|
anchor.classList.add('btn-default');
|
||
|
anchor.classList.add('glyphicon');
|
||
|
anchor.classList.add('glyphicon-cog');
|
||
|
document.getElementById("title-"+id).appendChild(anchor);
|
||
|
document.getElementById("tbar-"+id).innerHTML = cameras[id].txt.board+" "+cameras[id].txt.model+" "+pixformats[parseInt(cameras[id].txt.pixformat)]+" "+resolutions[cameras[id].txt.framesize];
|
||
|
}
|
||
|
|
||
|
function addCamera(id){
|
||
|
gLog("Add Camera["+id+"]: "+cameras[id].instance);
|
||
|
|
||
|
let holder = document.getElementById("cam-holder");
|
||
|
let item = document.createElement("div");
|
||
|
let title = document.createElement("h4");
|
||
|
let anchor = document.createElement("a");
|
||
|
let img = document.createElement("img");
|
||
|
let tbar = document.createElement("div");
|
||
|
|
||
|
item.id = "item-"+id;
|
||
|
item.className = "row";
|
||
|
|
||
|
title.id = "title-"+id;
|
||
|
title.className = "text-center";
|
||
|
title.innerHTML = cameras[id].instance;
|
||
|
item.appendChild(title);
|
||
|
|
||
|
anchor.href = getCamURL(id);
|
||
|
anchor.target = "_blank";
|
||
|
anchor.classList.add('btn');
|
||
|
anchor.classList.add('btn-sm');
|
||
|
anchor.classList.add('btn-default');
|
||
|
anchor.classList.add('glyphicon');
|
||
|
anchor.classList.add('glyphicon-cog');
|
||
|
title.appendChild(anchor);
|
||
|
|
||
|
tbar.id = "tbar-"+id;
|
||
|
tbar.className = "text-center preview-tbar";
|
||
|
tbar.innerHTML = cameras[id].txt.board+" "+cameras[id].txt.model+" "+pixformats[parseInt(cameras[id].txt.pixformat)]+" "+resolutions[cameras[id].txt.framesize];
|
||
|
item.appendChild(tbar);
|
||
|
|
||
|
img.id = id;
|
||
|
img.crossorigin = "";
|
||
|
img.setAttribute("width", "100%");
|
||
|
item.appendChild(img);
|
||
|
|
||
|
holder.appendChild(item);
|
||
|
|
||
|
img.onclick = function(e){
|
||
|
document.getElementById("switch-cams-interval").value = 0;
|
||
|
cancelCamSwitcher();
|
||
|
selectCamera(id);
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function handleCamera(cam){
|
||
|
let isNew = document.getElementById(cam.id) === null;
|
||
|
cam.timeout = null;
|
||
|
cameras[cam.id] = cam;
|
||
|
if(isNew){
|
||
|
//new camera
|
||
|
addCamera(cam.id);
|
||
|
} else {
|
||
|
updateCamera(cam.id);
|
||
|
}
|
||
|
loadCameraThumbnail(cam.id);
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Cam Switcher
|
||
|
*/
|
||
|
|
||
|
function switchCam(){
|
||
|
let keys = Object.keys(cameras);
|
||
|
let numCams = keys.length;
|
||
|
if(numCams === 0 || (numCams === 1 && selectedCamera !== null)){
|
||
|
return;
|
||
|
}
|
||
|
if(selectedCamera === null){
|
||
|
selectCamera(cameras[keys[0]].id);
|
||
|
return;
|
||
|
}
|
||
|
//more than one cams and one is selected!
|
||
|
for (var i = 0; i < numCams; i++) {
|
||
|
if(keys[i] === selectedCamera){
|
||
|
if(i === (numCams - 1)){
|
||
|
selectCamera(cameras[keys[0]].id);
|
||
|
} else {
|
||
|
selectCamera(cameras[keys[i+1]].id);
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var camSwitcherTimer = null;
|
||
|
|
||
|
function cancelCamSwitcher(){
|
||
|
if(camSwitcherTimer !== null){
|
||
|
clearInterval(camSwitcherTimer);
|
||
|
camSwitcherTimer = null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function initCamSwitcher(){
|
||
|
document.getElementById("switch-cams").onclick = function(e){
|
||
|
switchCam();
|
||
|
};
|
||
|
function handleChange(){
|
||
|
let value = parseInt(document.getElementById("switch-cams-interval").value);
|
||
|
if(value > 0){
|
||
|
cancelCamSwitcher();
|
||
|
camSwitcherTimer = setInterval(switchCam, value * 1000);
|
||
|
} else {
|
||
|
cancelCamSwitcher();
|
||
|
}
|
||
|
}
|
||
|
document.getElementById("switch-cams-interval").onchange = handleChange;
|
||
|
handleChange();
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Camera Player Controls
|
||
|
*/
|
||
|
function fillResolutionSelect(model){
|
||
|
let s = document.getElementById("resolution");
|
||
|
s.innerHTML = "";
|
||
|
var m = 8;
|
||
|
switch(model){
|
||
|
case "OV2640": m = 13; break;
|
||
|
case "OV3660": m = 17; break;
|
||
|
case "OV5640": m = 21; break;
|
||
|
default: break;
|
||
|
}
|
||
|
for(var i=0; i<=m; i++){
|
||
|
let option = document.createElement("option");
|
||
|
option.value = i;
|
||
|
option.innerHTML = resolutions[''+i];
|
||
|
s.appendChild(option);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function playButtonClear(){
|
||
|
let playButton = document.getElementById("play-stop");
|
||
|
playButton.classList.remove('btn-danger');
|
||
|
playButton.classList.remove('glyphicon-stop');
|
||
|
playButton.classList.add('btn-success');
|
||
|
playButton.classList.add('glyphicon-play');
|
||
|
playButton.onclick = function(){};
|
||
|
playing = false;
|
||
|
}
|
||
|
|
||
|
function onCameraButtonStop(id){
|
||
|
let stream = document.getElementById("stream");
|
||
|
let playButton = document.getElementById("play-stop");
|
||
|
playButton.classList.remove('btn-danger');
|
||
|
playButton.classList.remove('glyphicon-stop');
|
||
|
playButton.classList.add('btn-success');
|
||
|
playButton.classList.add('glyphicon-play');
|
||
|
window.stop();
|
||
|
playing = false;
|
||
|
|
||
|
if(parseInt(document.getElementById("refresh-thumbs-interval").value) > 0){
|
||
|
refreshThumbnails();
|
||
|
} else if(cameras.hasOwnProperty(id)){
|
||
|
loadCameraThumbnail(id);
|
||
|
}
|
||
|
playButton.onclick = function(){
|
||
|
if(cameras.hasOwnProperty(id)){
|
||
|
onCameraButtonPlay(id);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function onCameraButtonPlay(id){
|
||
|
document.getElementById("switch-cams-interval").value = 0;
|
||
|
cancelCamSwitcher();
|
||
|
let stream = document.getElementById("stream");
|
||
|
let playButton = document.getElementById("play-stop");
|
||
|
playButton.classList.remove('btn-success');
|
||
|
playButton.classList.remove('glyphicon-play');
|
||
|
playButton.classList.add('btn-danger');
|
||
|
playButton.classList.add('glyphicon-stop');
|
||
|
stream.src = getCamStreamURL(id);
|
||
|
playing = true;
|
||
|
playButton.onclick = function(){
|
||
|
onCameraButtonStop(id);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function selectCamera(id){
|
||
|
if(selectedCamera != id){
|
||
|
if(selectedCamera !== null){
|
||
|
//mark as unselected?
|
||
|
document.getElementById("tbar-"+selectedCamera).classList.remove('cam-selected');
|
||
|
if(playing){
|
||
|
onCameraButtonStop(selectedCamera);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
let stream = document.getElementById("stream");
|
||
|
let holder = document.getElementById("controls-holder");
|
||
|
let playButton = document.getElementById("play-stop");
|
||
|
|
||
|
selectedCamera = null;
|
||
|
holder.classList.add('disable');
|
||
|
playButtonClear();
|
||
|
|
||
|
stream.src = document.getElementById(id).src;
|
||
|
fillResolutionSelect(cameras[id].txt.model);
|
||
|
document.getElementById("resolution").value = cameras[id].txt.framesize;
|
||
|
selectedCamera = id;
|
||
|
|
||
|
fetchUrl(getCamURL(id)+'/status', "json", function(code, data, headers){
|
||
|
if(code === 200){
|
||
|
if(!stream.src){
|
||
|
stream.src = document.getElementById(id).src;
|
||
|
}
|
||
|
//mark selected
|
||
|
document.getElementById("tbar-"+selectedCamera).classList.add('cam-selected');
|
||
|
document.getElementById("resolution").value = data.framesize;
|
||
|
document.getElementById("xclk").value = data.xclk || 20;
|
||
|
holder.classList.remove('disable');
|
||
|
|
||
|
playButton.onclick = function(){
|
||
|
onCameraButtonPlay(id);
|
||
|
};
|
||
|
} else if(selectedCamera != null){
|
||
|
removeCamera(selectedCamera);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function initCameraPlayerControls(){
|
||
|
//Resolution
|
||
|
document.getElementById("resolution").onchange = function(e){
|
||
|
if(selectedCamera === null){
|
||
|
return;
|
||
|
}
|
||
|
let url = getCamURL(selectedCamera);
|
||
|
fetchUrl(`${url}/control?var=framesize&val=${e.target.value}`, 'text', function(code){
|
||
|
if(code < 0 && selectedCamera != null){
|
||
|
removeCamera(selectedCamera);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
//XCLK
|
||
|
document.getElementById("xclk").onchange = function(e){
|
||
|
if(selectedCamera === null){
|
||
|
return;
|
||
|
}
|
||
|
let value = parseInt(e.target.value);
|
||
|
if(value < 5 || value > 20){
|
||
|
gError("Invalid XCLK Value: "+value);
|
||
|
return;
|
||
|
}
|
||
|
let url = getCamURL(selectedCamera);
|
||
|
fetchUrl(`${url}/xclk?xclk=${value}`, 'text', function(code){
|
||
|
if(code < 0 && selectedCamera != null){
|
||
|
removeCamera(selectedCamera);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
//Save Image
|
||
|
const saveButton = document.getElementById('save-still');
|
||
|
saveButton.onclick = (e) => {
|
||
|
let view = document.getElementById('stream');
|
||
|
var canvas = document.createElement("canvas");
|
||
|
canvas.width = view.naturalWidth;
|
||
|
canvas.height = view.naturalHeight;
|
||
|
document.body.appendChild(canvas);
|
||
|
var context = canvas.getContext('2d');
|
||
|
context.drawImage(view,0,0);
|
||
|
try {
|
||
|
var dataURL = canvas.toDataURL('image/png');
|
||
|
saveButton.href = dataURL;
|
||
|
var d = new Date();
|
||
|
saveButton.download = d.getFullYear() + ("0"+(d.getMonth()+1)).slice(-2) + ("0" + d.getDate()).slice(-2) + ("0" + d.getHours()).slice(-2) + ("0" + d.getMinutes()).slice(-2) + ("0" + d.getSeconds()).slice(-2) + ".png";
|
||
|
} catch (e) {
|
||
|
gException(e);
|
||
|
}
|
||
|
canvas.parentNode.removeChild(canvas);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* MDNS
|
||
|
*/
|
||
|
//const baseHost = 'http://192.168.254.81';
|
||
|
const baseHost = document.location.origin;
|
||
|
|
||
|
function scanMdns(){
|
||
|
fetchUrl(baseHost+'/mdns', "json", function(code, data, headers){
|
||
|
if(code === 200){
|
||
|
if(isArray(data)){
|
||
|
for(var i = 0; i < data.length; i++) {
|
||
|
handleCamera(data[i]);
|
||
|
}
|
||
|
if(data.length > 0 && selectedCamera == null){
|
||
|
selectCamera(data[0].id);
|
||
|
}
|
||
|
} else {
|
||
|
gError("Data is not Array: "+data);
|
||
|
}
|
||
|
} else if(code < 0){
|
||
|
gError("Failed to fetch MDNS");
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function initMdns(){
|
||
|
var mdnsRefreshTimer = null;
|
||
|
scanMdns();
|
||
|
document.getElementById("refresh-mdns").onclick = function(e){
|
||
|
scanMdns();
|
||
|
};
|
||
|
function handleChange(){
|
||
|
if(mdnsRefreshTimer !== null){
|
||
|
clearInterval(mdnsRefreshTimer);
|
||
|
mdnsRefreshTimer = null;
|
||
|
}
|
||
|
let value = parseInt(document.getElementById("refresh-mdns-interval").value);
|
||
|
if(value > 0){
|
||
|
document.getElementById("refresh-mdns").classList.add('disable');
|
||
|
mdnsRefreshTimer = setInterval(scanMdns, value * 60 * 1000);
|
||
|
} else {
|
||
|
document.getElementById("refresh-mdns").classList.remove('disable');
|
||
|
}
|
||
|
}
|
||
|
document.getElementById("refresh-mdns-interval").onchange = handleChange;
|
||
|
handleChange();
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// Startup
|
||
|
//
|
||
|
document.addEventListener("DOMContentLoaded", function(event) {
|
||
|
var consoleVisible = true;
|
||
|
document.getElementById("hide-console").onclick = function(e){
|
||
|
consoleVisible = !consoleVisible;
|
||
|
if(consoleVisible){
|
||
|
e.target.classList.remove('glyphicon-chevron-up');
|
||
|
e.target.classList.add('glyphicon-chevron-down');
|
||
|
document.getElementById("console").classList.remove('collapsed');
|
||
|
document.getElementById("console-holder").style.paddingBottom = "";
|
||
|
document.getElementById("view-holder").style.bottom = "";
|
||
|
} else {
|
||
|
e.target.classList.remove('glyphicon-chevron-down');
|
||
|
e.target.classList.add('glyphicon-chevron-up');
|
||
|
document.getElementById("console").classList.add('collapsed');
|
||
|
document.getElementById("console-holder").style.paddingBottom = 0;
|
||
|
document.getElementById("view-holder").style.bottom = "46px";
|
||
|
}
|
||
|
};
|
||
|
initMdns();
|
||
|
initThumbnailRefresh();
|
||
|
initCameraPlayerControls();
|
||
|
initCamSwitcher();
|
||
|
});
|
||
|
|
||
|
</script>
|
||
|
<title>ESP-EYE Monitor</title>
|
||
|
</head>
|
||
|
<body>
|
||
|
<div id="page">
|
||
|
<div class="row">
|
||
|
<div id="cam-holder">
|
||
|
<div class="row" id="mdns-holder">
|
||
|
|
||
|
<select class="form-control sitem" id="refresh-mdns-interval" placeholder="Search Interval Minutes">
|
||
|
<option value="0" selected="selected">Manual</option>
|
||
|
<option value="1">1 Minute</option>
|
||
|
<option value="2">2 Minutes</option>
|
||
|
<option value="5">5 Minutes</option>
|
||
|
<option value="10">10 Minutes</option>
|
||
|
<option value="15">15 Minutes</option>
|
||
|
<option value="30">30 Minutes</option>
|
||
|
<option value="60">1 Hour</option>
|
||
|
</select>
|
||
|
<button type="button" class="btn btn-sm glyphicon btn-success glyphicon-search fitem disable" id="refresh-mdns" title="Search"></button>
|
||
|
|
||
|
<select class="form-control sitem" id="refresh-thumbs-interval">
|
||
|
<option value="0">OFF</option>
|
||
|
<option value="1">1s</option>
|
||
|
<option value="2" selected="selected">2s</option>
|
||
|
<option value="5">5s</option>
|
||
|
<option value="10">10s</option>
|
||
|
<option value="30">30s</option>
|
||
|
<option value="60">60s</option>
|
||
|
</select>
|
||
|
<button type="button" class="btn btn-sm glyphicon btn-success glyphicon-camera fitem disable" id="refresh-thumbs" title="Refresh"></button>
|
||
|
|
||
|
<select class="form-control sitem" id="switch-cams-interval">
|
||
|
<option value="0">OFF</option>
|
||
|
<option value="1">1s</option>
|
||
|
<option value="2">2s</option>
|
||
|
<option value="5" selected="selected">5s</option>
|
||
|
<option value="10">10s</option>
|
||
|
<option value="30">30s</option>
|
||
|
<option value="60">60s</option>
|
||
|
</select>
|
||
|
<button type="button" class="btn btn-sm glyphicon btn-success glyphicon-step-forward fitem" id="switch-cams" title="Switch Cams"></button>
|
||
|
</div>
|
||
|
</div>
|
||
|
<div id="stream-holder">
|
||
|
<div class="row disable" id="controls-holder">
|
||
|
<a id="save-still" href="#" class="btn btn-sm glyphicon btn-danger glyphicon-picture" download="capture.jpg"></a>
|
||
|
|
||
|
<button type="button" class="btn btn-sm glyphicon btn-success glyphicon-play" id="play-stop" title="Play/Stop"></button>
|
||
|
|
||
|
<select class="form-control" id="resolution" placeholder="Resolution"></select>
|
||
|
|
||
|
<select class="form-control" id="xclk" placeholder="XCLK MHz">
|
||
|
<option value="20" selected="selected">20 MHz</option>
|
||
|
<option value="10">10 MHz</option>
|
||
|
<option value="5">5 MHz</option>
|
||
|
</select>
|
||
|
|
||
|
</div>
|
||
|
<div class="row" id="view-holder">
|
||
|
<div id="stream-win">
|
||
|
<img id="stream" crossorigin>
|
||
|
</div>
|
||
|
</div>
|
||
|
<div class="row" id="console-holder">
|
||
|
<h4 class="text-center">Console<button type="button" class="btn btn-sm glyphicon btn-default glyphicon-chevron-down panel-action" id="hide-console" title="Hide/Show Console"></button></h4>
|
||
|
<div class="well" id="console" title="Console"><div id="gCodeLog"></div></div>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
</body>
|
||
|
</html>
|