Initial Commit
Update Dockerfile_cleaner, Dockerfile_recorder, and 12 more files...
This commit is contained in:
11
htdocs/.htaccess
Normal file
11
htdocs/.htaccess
Normal file
@@ -0,0 +1,11 @@
|
||||
# Prevent access to .htaccess
|
||||
<Files ~ "^.*\.([Hh][Tt][Aa]|[Pp][Yy])">
|
||||
Require all denied
|
||||
</Files>
|
||||
|
||||
# Fixing Apache's autistic fit with .mkv files
|
||||
AddType video/x-matroska mkv
|
||||
|
||||
# Allowing indexes
|
||||
Options +Indexes -FollowSymlinks -ExecCGI
|
||||
ServerSignature Off
|
1
htdocs/css/simplette.all.min.css
vendored
Normal file
1
htdocs/css/simplette.all.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
htdocs/favicon.ico
Normal file
BIN
htdocs/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
1
htdocs/favicon.svg
Normal file
1
htdocs/favicon.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 24 KiB |
365
htdocs/index.php
Normal file
365
htdocs/index.php
Normal file
@@ -0,0 +1,365 @@
|
||||
<?php
|
||||
// List of all available cameras
|
||||
$camsInfo = [
|
||||
# Format: [camId, camName]
|
||||
["cam1", "Cam #1"],
|
||||
["cam2", "Cam #2"]
|
||||
];
|
||||
|
||||
// Root location of all recordings. (Not used yet)
|
||||
$rootLocation = "./data/";
|
||||
|
||||
// Grabbing the requested cam's ID
|
||||
// The id should be the same as the sub-folder into which this camera's recordings are located.
|
||||
$camId = $_GET['cam'] ?? null;
|
||||
|
||||
// Determining if the ID is valid and the name that should be shown.
|
||||
// If the ID is invalid, it is set back to `null`.
|
||||
if(is_null($camId)) {
|
||||
$camName = "None";
|
||||
} else {
|
||||
$isCamValid = false;
|
||||
|
||||
foreach ($camsInfo as $singleCamInfo) {
|
||||
if($singleCamInfo[0] == $camId) {
|
||||
$camName = $singleCamInfo[1];
|
||||
$isCamValid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!$isCamValid) {
|
||||
$camName = "Unknown";
|
||||
$camId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Grabbing the list of recordings if needed.
|
||||
if(is_null($camId)) {
|
||||
// No cam selected, we just use empty variables.
|
||||
$basePath = "./";
|
||||
$files = [];
|
||||
} else {
|
||||
$basePath = "/data/".$camId."/";
|
||||
$files = array_values(array_diff(scandir("./data/".$camId."/"), array('.', '..')));
|
||||
// Removing the newest one as it is highly likely to currently being written to by ffmpeg.
|
||||
array_pop($files);
|
||||
}
|
||||
|
||||
// If we only need to send the JSON, we send it and don't go further.
|
||||
$returnJsonOnly = !is_null($_GET['json'] ?? null);
|
||||
if($returnJsonOnly) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($files);
|
||||
exit();
|
||||
}
|
||||
|
||||
// Function used to calculate the disk space taken by recordings later on.
|
||||
function folderSize($dir){
|
||||
$count_size = 0;
|
||||
$count = 0;
|
||||
$dir_array = scandir($dir);
|
||||
foreach($dir_array as $key=>$filename){
|
||||
if($filename!=".." && $filename!="."){
|
||||
if(is_dir($dir."/".$filename)){
|
||||
$new_foldersize = foldersize($dir."/".$filename);
|
||||
$count_size = $count_size+ $new_foldersize;
|
||||
} else if(is_file($dir."/".$filename)) {
|
||||
$count_size = $count_size + filesize($dir."/".$filename);
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $count_size;
|
||||
}
|
||||
|
||||
// Function used to format the disk space taken by recordings later on.
|
||||
function sizeFormat($bytes) {
|
||||
$kb = 1024;
|
||||
$mb = $kb * 1024;
|
||||
$gb = $mb * 1024;
|
||||
$tb = $gb * 1024;
|
||||
if (($bytes >= 0) && ($bytes < $kb)) {
|
||||
return $bytes . ' B';
|
||||
} elseif (($bytes >= $kb) && ($bytes < $mb)) {
|
||||
return ceil($bytes / $kb) . ' KiB';
|
||||
} elseif (($bytes >= $mb) && ($bytes < $gb)) {
|
||||
return ceil($bytes / $mb) . ' MiB';
|
||||
} elseif (($bytes >= $gb) && ($bytes < $tb)) {
|
||||
return ceil($bytes / $gb) . ' GiB';
|
||||
} elseif ($bytes >= $tb) {
|
||||
return ceil($bytes / $tb) . ' TiB';
|
||||
} else {
|
||||
return $bytes . ' B';
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>NibblePoker's Mini CCTV NVR</title>
|
||||
<link rel="stylesheet" href="/css/simplette.all.min.css">
|
||||
<style>
|
||||
#video-selector {
|
||||
min-width: 95%;
|
||||
}
|
||||
video {
|
||||
max-height: 60vh;
|
||||
max-width: 95vw;
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
#skippers a {
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
user-select: none;
|
||||
}
|
||||
input[type=range] {
|
||||
box-shadow: none;
|
||||
}
|
||||
#video-caching {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="margin-container">
|
||||
<ul class="link-list">
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/data">Raw recordings</a></li>
|
||||
<?php
|
||||
// Adding the cameras in the navbar
|
||||
foreach ($camsInfo as $singleCamInfo) {
|
||||
echo("<li><a href=\"/?cam=" . $singleCamInfo[0] . "\">" . $singleCamInfo[1] . "</a></li>");
|
||||
}
|
||||
?>
|
||||
</ul>
|
||||
</nav>
|
||||
<header><h1><b>NibblePoker's Mini CCTV NVR</b></h1></header>
|
||||
<hr><hr>
|
||||
<div class="margin-container auto-paragraph-margin">
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<td>
|
||||
<h3 style="width: 100%;">Caméra: <i><?php echo($camName); ?></i></h3>
|
||||
</td>
|
||||
<td>
|
||||
<span style="float: right;"><?php
|
||||
// Printing the space taken by all cams and the current one if possible.
|
||||
$totalSize = sizeFormat(folderSize("./data/"));
|
||||
if(is_null($camId)) {
|
||||
echo($totalSize);
|
||||
} else {
|
||||
echo(sizeFormat(folderSize("./data/" . $camId . "/")) . " / " . $totalSize);
|
||||
}
|
||||
?></span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="margin-container">
|
||||
<?php
|
||||
if(is_null($camId)) {
|
||||
// No camera selected.
|
||||
echo("<p class=\"h5\" style=\"margin-bottom: 0.5em;\">Select one camera:</p>");
|
||||
echo("<ul class=\"link-list h5 indent-container\">");
|
||||
|
||||
// Adding the cameras in the URL list
|
||||
foreach ($camsInfo as $singleCamInfo) {
|
||||
echo("<li><a href=\"/?cam=" . $singleCamInfo[0] . "\">" . $singleCamInfo[1] . "</a></li>");
|
||||
}
|
||||
|
||||
echo("</ul>");
|
||||
} else {
|
||||
// We have selected one, we add the video, slider, jumpers and filename placeholder.
|
||||
echo("<center>");
|
||||
echo("<video id=\"cctv-out\" controls></video>");
|
||||
echo("<br>");
|
||||
echo("<input type=\"range\" id=\"video-selector\" min=\"0\" max=\"" . count($files) . "\" value=\"0\">");
|
||||
echo("<br>");
|
||||
echo("<p id=\"skippers\">");
|
||||
echo("<a id=\"skip-minus-25\"><<<< 25</a>");
|
||||
echo("<a id=\"skip-minus-10\"><<< 10</a>");
|
||||
echo("<a id=\"skip-minus-5\"><< 5</a>");
|
||||
echo("<a id=\"skip-minus-1\">< 1</a>");
|
||||
echo("<a id=\"skip-plus-1\">1 ></a>");
|
||||
echo("<a id=\"skip-plus-5\">5 >></a>");
|
||||
echo("<a id=\"skip-plus-10\">10 >>></a>");
|
||||
echo("<a id=\"skip-plus-25\">25 >>>></a>");
|
||||
echo("</p>");
|
||||
echo("<br>");
|
||||
echo("<p>File: <a id=\"url-video\" href=\"#\">Non définis</a> (<span id=\"vid-count-current\">0</span>/<span id=\"vid-count-total\">0</span>)</p>");
|
||||
echo("</center>");
|
||||
echo("<br>");
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<hr><hr>
|
||||
<footer>
|
||||
<!-- Feel free to change this to something less invasive, or simply stats. Your imagination is the limit :) -->
|
||||
<p>Made by <a href="https://github.com/aziascreations">BOZET Herwin</a> on <a href="https://github.com/aziascreations/Docker-Mini-CCTV-NVR">Github</a></p>
|
||||
</footer>
|
||||
<script>
|
||||
<?php
|
||||
// Adding the base path and initial file listing as JS variables.
|
||||
echo("const basePath = \"" . $basePath . "\";");
|
||||
echo("let files = " . json_encode($files) . ";");
|
||||
?>
|
||||
|
||||
const videoCaching = document.createElement('video');
|
||||
const videoListUpdateIntervalMs = 10 * 1000;
|
||||
|
||||
const startOffset = 2;
|
||||
let iCurrentVideo = files.length - startOffset;
|
||||
if(iCurrentVideo < 0) {
|
||||
iCurrentVideo = 0;
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
videoCaching.preload = 'auto';
|
||||
videoCaching.id = 'video-caching';
|
||||
|
||||
const eVideo = document.getElementById("cctv-out");
|
||||
const eVideoSelector = document.getElementById("video-selector");
|
||||
|
||||
// If we have a video element in the DOM, we set it up.
|
||||
if(eVideo !== null) {
|
||||
// Handles every change of video
|
||||
const playNextVideo = () => {
|
||||
if(files.length > 0 && iCurrentVideo <= files.length) {
|
||||
const newSource = basePath + files[iCurrentVideo];
|
||||
eVideo.src = newSource;
|
||||
|
||||
// Setting the MIME type on the visible player, just in case.
|
||||
if(newSource.endsWith(".mkv")) {
|
||||
eVideo.type = 'video/x-matroska';
|
||||
} else if(newSource.endsWith(".mp4")) {
|
||||
eVideo.type = 'video/mp4';
|
||||
} else {
|
||||
eVideo.type = '';
|
||||
}
|
||||
|
||||
eVideoSelector.value = iCurrentVideo;
|
||||
document.getElementById("url-video").href = newSource;
|
||||
document.getElementById("url-video").text = newSource;
|
||||
document.getElementById("vid-count-current").textContent = iCurrentVideo;
|
||||
eVideo.play();
|
||||
|
||||
// If there is a next video in the list, we attempt to cache it via a hidden player.
|
||||
if(iCurrentVideo + 1 < files.length) {
|
||||
videoCaching.preload = 'auto';
|
||||
videoCaching.src = basePath + files[iCurrentVideo + 1];
|
||||
|
||||
// Setting the MIME type on the caching player, just in case.
|
||||
if(files[iCurrentVideo + 1].endsWith(".mkv")) {
|
||||
videoCaching.type = 'video/x-matroska';
|
||||
} else if(files[iCurrentVideo + 1].endsWith(".mp4")) {
|
||||
videoCaching.type = 'video/mp4';
|
||||
} else {
|
||||
videoCaching.type = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Repeated function that updates the list of available videos every now and then.
|
||||
const updateVideoList = () => {
|
||||
fetch(window.location + "&json=1")
|
||||
.then(response => {
|
||||
if(!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
let newIndex = data.indexOf(files[iCurrentVideo]);
|
||||
files = data;
|
||||
if(newIndex === -1) {
|
||||
newIndex = files.length - startOffset;
|
||||
}
|
||||
iCurrentVideo = newIndex;
|
||||
eVideoSelector.value = iCurrentVideo;
|
||||
eVideoSelector.max = files.length;
|
||||
document.getElementById("vid-count-current").textContent = iCurrentVideo;
|
||||
document.getElementById("vid-count-total").textContent = files.length;
|
||||
setTimeout(updateVideoList, videoListUpdateIntervalMs);
|
||||
})
|
||||
.catch(error => {
|
||||
setTimeout(updateVideoList, videoListUpdateIntervalMs);
|
||||
});
|
||||
};
|
||||
|
||||
// Trigerred when a video ends.
|
||||
eVideo.addEventListener("ended", () => {
|
||||
iCurrentVideo++;
|
||||
playNextVideo();
|
||||
});
|
||||
|
||||
// Trigerred every time a video plays.
|
||||
// Used to keep the video's frame at a constant size.
|
||||
// It looks like ass otherwise since it "flickers" between 2 sizes.
|
||||
eVideo.addEventListener("playing", function() {
|
||||
eVideo.width = eVideo.offsetWidth;
|
||||
eVideo.height = eVideo.offsetHeight;
|
||||
eVideo.style.minWidth = eVideo.offsetWidth+"px";
|
||||
eVideo.style.minHeight = eVideo.offsetHeight+"px";
|
||||
});
|
||||
|
||||
// Changes the "current video" number when moving the slider.
|
||||
eVideoSelector.oninput = function() {
|
||||
document.getElementById("vid-count-current").textContent = eVideoSelector.value;
|
||||
};
|
||||
|
||||
// Plays the correct video once the slider is released.
|
||||
eVideoSelector.onchange = function() {
|
||||
iCurrentVideo = eVideoSelector.value;
|
||||
playNextVideo();
|
||||
};
|
||||
|
||||
// Quick jumps
|
||||
document.getElementById('skip-minus-25').addEventListener('click', function() {
|
||||
iCurrentVideo = Math.max(0, iCurrentVideo - 25);
|
||||
playNextVideo();
|
||||
});
|
||||
document.getElementById('skip-minus-10').addEventListener('click', function() {
|
||||
iCurrentVideo = Math.max(0, iCurrentVideo - 10);
|
||||
playNextVideo();
|
||||
});
|
||||
document.getElementById('skip-minus-5').addEventListener('click', function() {
|
||||
iCurrentVideo = Math.max(0, iCurrentVideo - 5);
|
||||
playNextVideo();
|
||||
});
|
||||
document.getElementById('skip-minus-1').addEventListener('click', function() {
|
||||
iCurrentVideo = Math.max(0, iCurrentVideo - 1);
|
||||
playNextVideo();
|
||||
});
|
||||
document.getElementById('skip-plus-1').addEventListener('click', function() {
|
||||
iCurrentVideo = Math.min(files.length - 1, iCurrentVideo + 1);
|
||||
playNextVideo();
|
||||
});
|
||||
document.getElementById('skip-plus-5').addEventListener('click', function() {
|
||||
iCurrentVideo = Math.min(files.length - 1, iCurrentVideo + 5);
|
||||
playNextVideo();
|
||||
});
|
||||
document.getElementById('skip-plus-10').addEventListener('click', function() {
|
||||
iCurrentVideo = Math.min(files.length - 1, iCurrentVideo + 10);
|
||||
playNextVideo();
|
||||
});
|
||||
document.getElementById('skip-plus-25').addEventListener('click', function() {
|
||||
iCurrentVideo = Math.min(files.length - 1, iCurrentVideo + 25);
|
||||
playNextVideo();
|
||||
});
|
||||
|
||||
// Starting up the player, the list updater loop and setting the currently played vid number.
|
||||
document.getElementById("vid-count-total").textContent = files.length;
|
||||
playNextVideo();
|
||||
setTimeout(updateVideoList, videoListUpdateIntervalMs);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Reference in New Issue
Block a user