Merge branch 'dashboard-v3' into 'data-port'

# Conflicts:
#   libs/ffmpeg.js
montage-api
Moe 2021-12-19 15:17:13 +00:00
commit 41037abaf9
70 changed files with 4158 additions and 1505 deletions

View File

@ -100,8 +100,8 @@ RUN sed -i -e 's/\r//g' /home/Shinobi/Docker/init.sh
VOLUME ["/home/Shinobi/videos"] VOLUME ["/home/Shinobi/videos"]
VOLUME ["/home/Shinobi/plugins"] VOLUME ["/home/Shinobi/plugins"]
VOLUME ["/home/Shinobi/libs/customAutoLoad"]
VOLUME ["/config"] VOLUME ["/config"]
VOLUME ["/customAutoLoad"]
VOLUME ["/var/lib/mysql"] VOLUME ["/var/lib/mysql"]
EXPOSE 8080 443 21 25 EXPOSE 8080 443 21 25

View File

@ -98,8 +98,8 @@ RUN chmod -f +x /home/Shinobi/Docker/init.sh
VOLUME ["/home/Shinobi/videos"] VOLUME ["/home/Shinobi/videos"]
VOLUME ["/home/Shinobi/plugins"] VOLUME ["/home/Shinobi/plugins"]
VOLUME ["/home/Shinobi/libs/customAutoLoad"]
VOLUME ["/config"] VOLUME ["/config"]
VOLUME ["/customAutoLoad"]
VOLUME ["/var/lib/mysql"] VOLUME ["/var/lib/mysql"]
EXPOSE 8080 EXPOSE 8080

View File

@ -107,8 +107,8 @@ RUN sed -i -e 's/\r//g' /home/Shinobi/Docker/init.sh
VOLUME ["/home/Shinobi/videos"] VOLUME ["/home/Shinobi/videos"]
VOLUME ["/home/Shinobi/plugins"] VOLUME ["/home/Shinobi/plugins"]
VOLUME ["/home/Shinobi/libs/customAutoLoad"]
VOLUME ["/config"] VOLUME ["/config"]
VOLUME ["/customAutoLoad"]
VOLUME ["/var/lib/mysql"] VOLUME ["/var/lib/mysql"]
EXPOSE 8080 443 21 25 EXPOSE 8080 443 21 25

View File

@ -125,7 +125,7 @@ if [ "${ffmpeginstall^}" = "Y" ]; then
elif [ "$version" = 8 ]; then elif [ "$version" = 8 ]; then
#Enable Negativo17 repo for FFMPEG (CentOS 8) #Enable Negativo17 repo for FFMPEG (CentOS 8)
sudo dnf install epel-release dnf-utils -y -q -e 0 sudo dnf install epel-release dnf-utils -y -q -e 0
sudo yum-config-manager --set-enabled PowerTools sudo yum-config-manager --set-enabled powertools
sudo yum-config-manager --add-repo=https://negativo17.org/repos/epel-multimedia.repo sudo yum-config-manager --add-repo=https://negativo17.org/repos/epel-multimedia.repo
sudo dnf install ffmpeg ffmpeg-devel -y -q -e 0 sudo dnf install ffmpeg ffmpeg-devel -y -q -e 0
fi fi
@ -148,7 +148,7 @@ if [ "${installdbserver^}" = "Y" ] || [ "${installdbserver^}" = "" ]; then
if [ "${securedbserver^}" = "Y" ]; then if [ "${securedbserver^}" = "Y" ]; then
#Configure basic security for MariaDB #Configure basic security for MariaDB
sudo mysql_secure_installation sudo mariadb-secure-installation
else else
echo "=========================================================" echo "========================================================="
echo "Skipping database server security configuration..." echo "Skipping database server security configuration..."

View File

@ -76,7 +76,7 @@ if [ "$mysqlagree" = "y" ] || [ "$mysqlagree" = "Y" ]; then
sudo systemctl start mariadb sudo systemctl start mariadb
sudo systemctl enable mariadb sudo systemctl enable mariadb
#Run mysql install #Run mysql install
sudo mysql_secure_installation sudo mariadb-secure-installation
fi fi
echo "=============" echo "============="
echo "Shinobi - Database Installation" echo "Shinobi - Database Installation"

36
INSTALL/cuda-11.sh Normal file
View File

@ -0,0 +1,36 @@
#!/bin/sh
echo "------------------------------------------"
echo "-- Installing CUDA Toolkit and CUDA DNN --"
echo "------------------------------------------"
# Install CUDA Drivers and Toolkit
if [ -x "$(command -v apt)" ]; then
wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/cuda-ubuntu2004.pin
sudo mv cuda-ubuntu2004.pin /etc/apt/preferences.d/cuda-repository-pin-600
wget http://developer.download.nvidia.com/compute/cuda/11.0.2/local_installers/cuda-repo-ubuntu2004-11-0-local_11.0.2-450.51.05-1_amd64.deb
sudo dpkg -i cuda-repo-ubuntu2004-11-0-local_11.0.2-450.51.05-1_amd64.deb
sudo apt-key add /var/cuda-repo-ubuntu2004-11-0-local/7fa2af80.pub
sudo apt-get update -y
sudo apt-get -o Dpkg::Options::="--force-overwrite" install cuda-toolkit-11-0 -y --no-install-recommends
sudo apt-get -o Dpkg::Options::="--force-overwrite" install --fix-broken -y
sudo apt install nvidia-utils-495 nvidia-headless-495 -y
# Install CUDA DNN
wget https://cdn.shinobi.video/installers/libcudnn8_8.2.1.32-1+cuda11.3_amd64.deb -O cuda-dnn.deb
sudo dpkg -i cuda-dnn.deb
wget https://cdn.shinobi.video/installers/libcudnn8-dev_8.2.0.53-1+cuda11.3_amd64.deb -O cuda-dnn-dev.deb
sudo dpkg -i cuda-dnn-dev.deb
echo "-- Cleaning Up --"
# Cleanup
sudo rm cuda-dnn.deb
sudo rm cuda-dnn-dev.deb
fi
echo "------------------------------"
echo "Reboot is required. Do it now?"
echo "------------------------------"
echo "(y)es or (N)o. Default is No."
read rebootTheMachineHomie
if [ "$rebootTheMachineHomie" = "y" ] || [ "$rebootTheMachineHomie" = "Y" ]; then
sudo reboot
fi

View File

@ -92,8 +92,8 @@ const deleteVideo = (x) => {
const deleteFileBinEntry = (x) => { const deleteFileBinEntry = (x) => {
postMessage({f:'s.deleteFileBinEntry',file:x}) postMessage({f:'s.deleteFileBinEntry',file:x})
} }
const setDiskUsedForGroup = (groupKey,size,target) => { const setDiskUsedForGroup = (groupKey,size,target,videoRow) => {
postMessage({f:'s.setDiskUsedForGroup', ke: groupKey, size: size, target: target}) postMessage({f:'s.setDiskUsedForGroup', ke: groupKey, size: size, target: target, videoRow: videoRow})
} }
const getVideoDirectory = function(e){ const getVideoDirectory = function(e){
if(e.mid&&!e.id){e.id=e.mid}; if(e.mid&&!e.id){e.id=e.mid};
@ -185,6 +185,7 @@ const checkFilterRules = function(v){
}) })
} }
const deleteVideosByDays = async (v,days,addedQueries) => { const deleteVideosByDays = async (v,days,addedQueries) => {
const groupKey = v.ke;
const whereQuery = [ const whereQuery = [
['ke','=',v.ke], ['ke','=',v.ke],
['time','<', sqlDate(days+' DAY')], ['time','<', sqlDate(days+' DAY')],
@ -207,7 +208,8 @@ const deleteVideosByDays = async (v,days,addedQueries) => {
const filename = formattedTime(row.time) + '.' + row.ext const filename = formattedTime(row.time) + '.' + row.ext
try{ try{
await fs.promises.unlink(dir + filename) await fs.promises.unlink(dir + filename)
row.size += clearSize const fileSizeMB = row.size / 1048576;
setDiskUsedForGroup(groupKey,-fileSizeMB,null,row)
sendToWebSocket({ sendToWebSocket({
f: 'video_delete', f: 'video_delete',
filename: filename + '.' + row.ext, filename: filename + '.' + row.ext,
@ -227,7 +229,6 @@ const deleteVideosByDays = async (v,days,addedQueries) => {
where: whereQuery where: whereQuery
}) })
affectedRows = deleteResponse.rows || 0 affectedRows = deleteResponse.rows || 0
setDiskUsedForGroup(v.ke,-clearSize)
} }
return { return {
ok: true, ok: true,

View File

@ -120,6 +120,7 @@ module.exports = function(s,config,lang){
{ {
"fieldType": 'div', "fieldType": 'div',
"id": "monitorPresetsSelection", "id": "monitorPresetsSelection",
"style": "max-height:400px;overflow:auto;",
"class": "mdl-list" "class": "mdl-list"
}, },
{ {
@ -497,23 +498,23 @@ module.exports = function(s,config,lang){
"form-group-class": "input-mapping", "form-group-class": "input-mapping",
"possible": [ "possible": [
{ {
"name": lang['All streams in first feed'] + '(0, ' + lang.Default + ')', "name": lang['All streams in first feed'] + ' (0, ' + lang.Default + ')',
"value": "0" "value": "0"
}, },
{ {
"name": lang['First stream in feed'] + '(0:0)', "name": lang['First stream in feed'] + ' (0:0)',
"value": "0:0" "value": "0:0"
}, },
{ {
"name": lang['Second stream in feed'] + "(0:1)", "name": lang['Second stream in feed'] + " (0:1)",
"value": "0:1" "value": "0:1"
}, },
{ {
"name": lang['Video streams only'] + "(0:v)", "name": lang['Video streams only'] + " (0:v)",
"value": "0:v" "value": "0:v"
}, },
{ {
"name": lang['Video stream only from first feed'] + "(0:v:0)", "name": lang['Video stream only from first feed'] + " (0:v:0)",
"value": "0:v:0" "value": "0:v:0"
} }
] ]
@ -764,6 +765,10 @@ module.exports = function(s,config,lang){
"name": lang['HLS (includes Audio)'], "name": lang['HLS (includes Audio)'],
"value": "hls", "value": "hls",
"info": "Similar method to facebook live streams. <b>Includes audio</b> if input provides it. There is a delay of about 4-6 seconds because this method records segments then pushes them to the client rather than push as while it creates them." "info": "Similar method to facebook live streams. <b>Includes audio</b> if input provides it. There is a delay of about 4-6 seconds because this method records segments then pushes them to the client rather than push as while it creates them."
},
{
"name": lang.useSubStreamOnlyWhenWatching,
"value": "useSubstream",
} }
] ]
}, },
@ -1285,16 +1290,20 @@ module.exports = function(s,config,lang){
isAdvanced: true, isAdvanced: true,
"isSection": true, "isSection": true,
"id": "monSectionSubstream", "id": "monSectionSubstream",
"blockquote": lang.substreamText,
"blockquoteClass": 'global_tip',
"info": [ "info": [
{ {
"name": lang['Connection'], "name": lang['Connection'],
"color": "orange", "color": "orange",
id: "monSectionSubstreamInput", id: "monSectionSubstreamInput",
"blockquote": lang.substreamConnectionText,
"blockquoteClass": 'global_tip',
isSection: true, isSection: true,
isFormGroupGroup: true, isFormGroupGroup: true,
"info": [ "info": [
{ {
name:'map-detail=type', name:'detail-substream-input=type',
field:lang['Input Type'], field:lang['Input Type'],
default:'h264', default:'h264',
attribute:'selector="h_i_SUBSTREAM_FIELDS"', attribute:'selector="h_i_SUBSTREAM_FIELDS"',
@ -1303,7 +1312,8 @@ module.exports = function(s,config,lang){
possible:[ possible:[
{ {
"name": "H.264 / H.265 / H.265+", "name": "H.264 / H.265 / H.265+",
"value": "h264" "value": "h264",
selected: true,
}, },
{ {
"name": "JPEG", "name": "JPEG",
@ -1371,6 +1381,7 @@ module.exports = function(s,config,lang){
{ {
"name": lang.Yes, "name": lang.Yes,
"value": "1", "value": "1",
selected: true,
} }
] ]
}, },
@ -1385,7 +1396,8 @@ module.exports = function(s,config,lang){
{ {
"name": lang.Auto, "name": lang.Auto,
"value": "", "value": "",
"info": "Let FFMPEG decide. Normally it will try UDP first." "info": "Let FFMPEG decide. Normally it will try UDP first.",
selected: true,
}, },
{ {
"name": "TCP", "name": "TCP",
@ -1410,6 +1422,7 @@ module.exports = function(s,config,lang){
{ {
"name": lang.No, "name": lang.No,
"value": "0", "value": "0",
selected: true,
}, },
{ {
"name": lang.Yes, "name": lang.Yes,
@ -1438,7 +1451,8 @@ module.exports = function(s,config,lang){
possible:[ possible:[
{ {
"name": lang.Auto + '('+lang.Recommended+')', "name": lang.Auto + '('+lang.Recommended+')',
"value": "" "value": "",
selected: true,
}, },
{ {
"name": lang.NVIDIA, "name": lang.NVIDIA,
@ -1516,6 +1530,8 @@ module.exports = function(s,config,lang){
"name": lang['Output'], "name": lang['Output'],
"color": "blue", "color": "blue",
id: "monSectionSubstreamOutput", id: "monSectionSubstreamOutput",
"blockquote": lang.substreamOutputText,
"blockquoteClass": 'global_tip',
isSection: true, isSection: true,
isFormGroupGroup: true, isFormGroupGroup: true,
"info": [ "info": [
@ -1531,41 +1547,22 @@ module.exports = function(s,config,lang){
{ {
"name": lang.Poseidon, "name": lang.Poseidon,
"value": "mp4", "value": "mp4",
"info": "Poseidon is built on Kevin Godell's MP4 processing code. It simulates a streaming MP4 file but using the data of a live stream. Includes Audio. Some browsers can play it like a regular MP4 file. Streams over HTTP or WebSocket."
},
{
"name": lang["RTMP Stream"],
"value": "rtmp",
}, },
{ {
"name": lang['MJPEG'], "name": lang['MJPEG'],
"value": "mjpeg", "value": "mjpeg",
"info": "Standard Motion JPEG image. No audio."
}, },
{ {
"name": lang['FLV'], "name": lang['FLV'],
"value": "flv", "value": "flv",
"info": "Sending FLV encoded frames over WebSocket."
}, },
{ {
"name": lang['HLS (includes Audio)'], "name": lang['HLS (includes Audio)'],
"value": "hls", "value": "hls",
"info": "Similar method to facebook live streams. <b>Includes audio</b> if input provides it. There is a delay of about 4-6 seconds because this method records segments then pushes them to the client rather than push as while it creates them." selected: true,
} }
] ]
}, },
{
"field": lang['Server URL'],
"name": `detail-substream-output="rtmp_server_url"`,
"form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_rtmp",
"example": "rtmp://live-api.facebook.com:80/rtmp/",
},
{
"field": lang['Stream Key'],
"name": `detail-substream-output="rtmp_stream_key"`,
"form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_rtmp",
"example": "1111111111?ds=1&a=xxxxxxxxxx",
},
{ {
"field": lang['# of Allow MJPEG Clients'], "field": lang['# of Allow MJPEG Clients'],
"name": `detail-substream-output="stream_mjpeg_clients"`, "name": `detail-substream-output="stream_mjpeg_clients"`,
@ -1577,14 +1574,15 @@ module.exports = function(s,config,lang){
"name": `detail-substream-output="stream_vcodec"`, "name": `detail-substream-output="stream_vcodec"`,
"description": "Video codec for streaming.", "description": "Video codec for streaming.",
"default": "copy", "default": "copy",
"form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_hls h_st_channel_SUBSTREAM_FIELDS_rtmp h_st_channel_SUBSTREAM_FIELDS_flv h_st_channel_SUBSTREAM_FIELDS_mp4 h_st_channel_SUBSTREAM_FIELDS_h264", "form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_hls h_st_channel_SUBSTREAM_FIELDS_rtmp h_st_channel_SUBSTREAM_FIELDS_flv h_st_channel_SUBSTREAM_FIELDS_mp4 h_st_channel_SUBSTREAM_FIELDS_h264",
"fieldType": "select", "fieldType": "select",
"selector": "h_hls_v_channel_SUBSTREAM_FIELDS", "selector": "h_hls_v_channel_SUBSTREAM_FIELDS",
"possible": [ "possible": [
{ {
"name": lang.Auto, "name": lang.Auto,
"value": "no", "value": "no",
"info": "Let FFMPEG choose." "info": "Let FFMPEG choose.",
selected: true,
}, },
{ {
"name": "libx264", "name": "libx264",
@ -1659,12 +1657,13 @@ module.exports = function(s,config,lang){
"default": "", "default": "",
"example": "", "example": "",
"fieldType": "select", "fieldType": "select",
"form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_hls h_st_channel_SUBSTREAM_FIELDS_rtmp h_st_channel_SUBSTREAM_FIELDS_flv h_st_channel_SUBSTREAM_FIELDS_mp4 h_st_channel_SUBSTREAM_FIELDS_h264", "form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_hls h_st_channel_SUBSTREAM_FIELDS_rtmp h_st_channel_SUBSTREAM_FIELDS_flv h_st_channel_SUBSTREAM_FIELDS_mp4 h_st_channel_SUBSTREAM_FIELDS_h264",
"possible": [ "possible": [
{ {
"name": lang.Auto, "name": lang.Auto,
"info": "Let FFMPEG choose.", "info": "Let FFMPEG choose.",
"value": "" "value": "",
selected: true,
}, },
{ {
"name": lang["No Audio"], "name": lang["No Audio"],
@ -1730,7 +1729,7 @@ module.exports = function(s,config,lang){
"description": "Low number means higher quality. Higher number means less quality.", "description": "Low number means higher quality. Higher number means less quality.",
"default": "15", "default": "15",
"example": "1", "example": "1",
"form-group-class-pre-layer": "h_hls_v_channel_SUBSTREAM_FIELDS_input h_hls_v_channel_SUBSTREAM_FIELDS_libx264 h_hls_v_channel_SUBSTREAM_FIELDS_libx265 h_hls_v_channel_SUBSTREAM_FIELDS_h264_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_hevc_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_no", // "form-group-class-pre-layer": "h_hls_v_channel_SUBSTREAM_FIELDS_input h_hls_v_channel_SUBSTREAM_FIELDS_libx264 h_hls_v_channel_SUBSTREAM_FIELDS_libx265 h_hls_v_channel_SUBSTREAM_FIELDS_h264_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_hevc_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_no",
"form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_mjpeg h_st_channel_SUBSTREAM_FIELDS_hls h_st_channel_SUBSTREAM_FIELDS_rtmp h_st_channel_SUBSTREAM_FIELDS_jsmpeg h_st_channel_SUBSTREAM_FIELDS_flv h_st_channel_SUBSTREAM_FIELDS_mp4 h_st_channel_SUBSTREAM_FIELDS_h264", "form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_mjpeg h_st_channel_SUBSTREAM_FIELDS_hls h_st_channel_SUBSTREAM_FIELDS_rtmp h_st_channel_SUBSTREAM_FIELDS_jsmpeg h_st_channel_SUBSTREAM_FIELDS_flv h_st_channel_SUBSTREAM_FIELDS_mp4 h_st_channel_SUBSTREAM_FIELDS_h264",
"possible": "1-23" "possible": "1-23"
}, },
@ -1752,7 +1751,7 @@ module.exports = function(s,config,lang){
"name": "detail-substream-output=stream_fps", "name": "detail-substream-output=stream_fps",
"field": lang['Frame Rate'], "field": lang['Frame Rate'],
"description": "The speed in which frames are displayed to clients, in Frames Per Second. Be aware there is no default. This can lead to high bandwidth usage.", "description": "The speed in which frames are displayed to clients, in Frames Per Second. Be aware there is no default. This can lead to high bandwidth usage.",
"form-group-class-pre-layer": "h_hls_v_channel_SUBSTREAM_FIELDS_input h_hls_v_channel_SUBSTREAM_FIELDS_libx264 h_hls_v_channel_SUBSTREAM_FIELDS_libx265 h_hls_v_channel_SUBSTREAM_FIELDS_h264_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_hevc_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_no", // "form-group-class-pre-layer": "h_hls_v_channel_SUBSTREAM_FIELDS_input h_hls_v_channel_SUBSTREAM_FIELDS_libx264 h_hls_v_channel_SUBSTREAM_FIELDS_libx265 h_hls_v_channel_SUBSTREAM_FIELDS_h264_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_hevc_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_no",
"form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_mjpeg h_st_channel_SUBSTREAM_FIELDS_hls h_st_channel_SUBSTREAM_FIELDS_rtmp h_st_channel_SUBSTREAM_FIELDS_jsmpeg h_st_channel_SUBSTREAM_FIELDS_flv h_st_channel_SUBSTREAM_FIELDS_mp4 h_st_channel_SUBSTREAM_FIELDS_h264", "form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_mjpeg h_st_channel_SUBSTREAM_FIELDS_hls h_st_channel_SUBSTREAM_FIELDS_rtmp h_st_channel_SUBSTREAM_FIELDS_jsmpeg h_st_channel_SUBSTREAM_FIELDS_flv h_st_channel_SUBSTREAM_FIELDS_mp4 h_st_channel_SUBSTREAM_FIELDS_h264",
}, },
{ {
@ -1762,7 +1761,7 @@ module.exports = function(s,config,lang){
"fieldType": "number", "fieldType": "number",
"numberMin": "1", "numberMin": "1",
"example": "640", "example": "640",
"form-group-class-pre-layer": "h_hls_v_channel_SUBSTREAM_FIELDS_input h_hls_v_channel_SUBSTREAM_FIELDS_libx264 h_hls_v_channel_SUBSTREAM_FIELDS_libx265 h_hls_v_channel_SUBSTREAM_FIELDS_h264_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_hevc_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_no", // "form-group-class-pre-layer": "h_hls_v_channel_SUBSTREAM_FIELDS_input h_hls_v_channel_SUBSTREAM_FIELDS_libx264 h_hls_v_channel_SUBSTREAM_FIELDS_libx265 h_hls_v_channel_SUBSTREAM_FIELDS_h264_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_hevc_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_no",
"form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_mjpeg h_st_channel_SUBSTREAM_FIELDS_hls h_st_channel_SUBSTREAM_FIELDS_rtmp h_st_channel_SUBSTREAM_FIELDS_jsmpeg h_st_channel_SUBSTREAM_FIELDS_flv h_st_channel_SUBSTREAM_FIELDS_mp4 h_st_channel_SUBSTREAM_FIELDS_h264", "form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_mjpeg h_st_channel_SUBSTREAM_FIELDS_hls h_st_channel_SUBSTREAM_FIELDS_rtmp h_st_channel_SUBSTREAM_FIELDS_jsmpeg h_st_channel_SUBSTREAM_FIELDS_flv h_st_channel_SUBSTREAM_FIELDS_mp4 h_st_channel_SUBSTREAM_FIELDS_h264",
}, },
{ {
@ -1772,7 +1771,7 @@ module.exports = function(s,config,lang){
"fieldType": "number", "fieldType": "number",
"numberMin": "1", "numberMin": "1",
"example": "480", "example": "480",
"form-group-class-pre-layer": "h_hls_v_channel_SUBSTREAM_FIELDS_input h_hls_v_channel_SUBSTREAM_FIELDS_libx264 h_hls_v_channel_SUBSTREAM_FIELDS_libx265 h_hls_v_channel_SUBSTREAM_FIELDS_h264_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_hevc_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_no", // "form-group-class-pre-layer": "h_hls_v_channel_SUBSTREAM_FIELDS_input h_hls_v_channel_SUBSTREAM_FIELDS_libx264 h_hls_v_channel_SUBSTREAM_FIELDS_libx265 h_hls_v_channel_SUBSTREAM_FIELDS_h264_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_hevc_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_no",
"form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_mjpeg h_st_channel_SUBSTREAM_FIELDS_hls h_st_channel_SUBSTREAM_FIELDS_rtmp h_st_channel_SUBSTREAM_FIELDS_jsmpeg h_st_channel_SUBSTREAM_FIELDS_flv h_st_channel_SUBSTREAM_FIELDS_mp4 h_st_channel_SUBSTREAM_FIELDS_h264", "form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_mjpeg h_st_channel_SUBSTREAM_FIELDS_hls h_st_channel_SUBSTREAM_FIELDS_rtmp h_st_channel_SUBSTREAM_FIELDS_jsmpeg h_st_channel_SUBSTREAM_FIELDS_flv h_st_channel_SUBSTREAM_FIELDS_mp4 h_st_channel_SUBSTREAM_FIELDS_h264",
}, },
{ {
@ -1780,7 +1779,7 @@ module.exports = function(s,config,lang){
"field": lang["Rotate"], "field": lang["Rotate"],
"description": "Change the viewing angle of the video stream.", "description": "Change the viewing angle of the video stream.",
"fieldType": "select", "fieldType": "select",
"form-group-class-pre-layer": "h_hls_v_channel_SUBSTREAM_FIELDS_input h_hls_v_channel_SUBSTREAM_FIELDS_libx264 h_hls_v_channel_SUBSTREAM_FIELDS_libx265 h_hls_v_channel_SUBSTREAM_FIELDS_h264_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_hevc_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_no", // "form-group-class-pre-layer": "h_hls_v_channel_SUBSTREAM_FIELDS_input h_hls_v_channel_SUBSTREAM_FIELDS_libx264 h_hls_v_channel_SUBSTREAM_FIELDS_libx265 h_hls_v_channel_SUBSTREAM_FIELDS_h264_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_hevc_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_no",
"form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_mjpeg h_st_channel_SUBSTREAM_FIELDS_hls h_st_channel_SUBSTREAM_FIELDS_rtmp h_st_channel_SUBSTREAM_FIELDS_jsmpeg h_st_channel_SUBSTREAM_FIELDS_flv h_st_channel_SUBSTREAM_FIELDS_mp4 h_st_channel_SUBSTREAM_FIELDS_h264", "form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_mjpeg h_st_channel_SUBSTREAM_FIELDS_hls h_st_channel_SUBSTREAM_FIELDS_rtmp h_st_channel_SUBSTREAM_FIELDS_jsmpeg h_st_channel_SUBSTREAM_FIELDS_flv h_st_channel_SUBSTREAM_FIELDS_mp4 h_st_channel_SUBSTREAM_FIELDS_h264",
"possible": [ "possible": [
{ {
@ -1813,7 +1812,7 @@ module.exports = function(s,config,lang){
"name": "detail-substream-output=svf", "name": "detail-substream-output=svf",
"field": lang["Video Filter"], "field": lang["Video Filter"],
"description": "Place FFMPEG video filters in this box to affect the streaming portion. No spaces.", "description": "Place FFMPEG video filters in this box to affect the streaming portion. No spaces.",
"form-group-class-pre-layer": "h_hls_v_channel_SUBSTREAM_FIELDS_input h_hls_v_channel_SUBSTREAM_FIELDS_libx264 h_hls_v_channel_SUBSTREAM_FIELDS_libx265 h_hls_v_channel_SUBSTREAM_FIELDS_h264_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_hevc_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_no", // "form-group-class-pre-layer": "h_hls_v_channel_SUBSTREAM_FIELDS_input h_hls_v_channel_SUBSTREAM_FIELDS_libx264 h_hls_v_channel_SUBSTREAM_FIELDS_libx265 h_hls_v_channel_SUBSTREAM_FIELDS_h264_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_hevc_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_no",
"form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_mjpeg h_st_channel_SUBSTREAM_FIELDS_hls h_st_channel_SUBSTREAM_FIELDS_rtmp h_st_channel_SUBSTREAM_FIELDS_jsmpeg h_st_channel_SUBSTREAM_FIELDS_flv h_st_channel_SUBSTREAM_FIELDS_mp4 h_st_channel_SUBSTREAM_FIELDS_h264", "form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_mjpeg h_st_channel_SUBSTREAM_FIELDS_hls h_st_channel_SUBSTREAM_FIELDS_rtmp h_st_channel_SUBSTREAM_FIELDS_jsmpeg h_st_channel_SUBSTREAM_FIELDS_flv h_st_channel_SUBSTREAM_FIELDS_mp4 h_st_channel_SUBSTREAM_FIELDS_h264",
}, },
{ {
@ -2425,6 +2424,10 @@ module.exports = function(s,config,lang){
"name": `.5 ${lang.minutes}`, "name": `.5 ${lang.minutes}`,
"value": "30" "value": "30"
}, },
{
"name": `1 ${lang.minute}`,
"value": "60"
},
{ {
"name": `5 ${lang.minutes}`, "name": `5 ${lang.minutes}`,
"value": "300" "value": "300"
@ -5103,11 +5106,22 @@ module.exports = function(s,config,lang){
"blockquoteClass": "global_tip", "blockquoteClass": "global_tip",
"blockquote": lang.onvifdeviceManagerGlobalTip, "blockquote": lang.onvifdeviceManagerGlobalTip,
"info": [ "info": [
{
"field": lang["Monitor"],
"fieldType": "select",
"class": "monitors_list",
"possible": []
},
{ {
"fieldType": "btn", "fieldType": "btn",
"class": `btn-warning onvif-device-reboot`, "class": `btn-warning onvif-device-reboot`,
"btnContent": `<i class="fa fa-refresh"></i> &nbsp; ${lang['Reboot Camera']}`, "btnContent": `<i class="fa fa-refresh"></i> &nbsp; ${lang['Reboot Camera']}`,
}, },
{
"fieldType": "div",
"class": "p-2",
"divContent": `<pre class="bg-dark text-white" style="max-height: 400px;overflow: auto;" id="onvifDeviceManagerInfo"></pre>`,
}
] ]
}, },
"Network": { "Network": {
@ -5987,6 +6001,21 @@ module.exports = function(s,config,lang){
"Schedules": { "Schedules": {
"section": "Schedules", "section": "Schedules",
"blocks": { "blocks": {
"Info": {
"name": lang["Monitor States and Schedules"],
"color": "blue",
"section-pre-class": "col-md-12",
"blockquoteClass": "global_tip",
"blockquote": lang.MonitorStatesText,
"info": [
{
"fieldType": "btn",
"attribute": `page-open="monitorStates"`,
"class": `btn-primary`,
"btnContent": `<i class="fa fa-align-right"></i> &nbsp; ${lang["Monitor States"]}`,
},
]
},
"Schedules": { "Schedules": {
"name": lang["Schedules"], "name": lang["Schedules"],
"color": "orange", "color": "orange",
@ -6118,8 +6147,23 @@ module.exports = function(s,config,lang){
"Monitor States": { "Monitor States": {
"section": "Monitor States", "section": "Monitor States",
"blocks": { "blocks": {
"Info": {
"name": lang["Monitor States and Schedules"],
"color": "blue",
"section-pre-class": "col-md-12",
"blockquoteClass": "global_tip",
"blockquote": lang.MonitorStatesText,
"info": [
{
"fieldType": "btn",
"attribute": `page-open="schedules"`,
"class": `btn-primary`,
"btnContent": `<i class="fa fa-clock"></i> &nbsp; ${lang["Schedules"]}`,
},
]
},
"Monitor States": { "Monitor States": {
"name": lang["Monitor States"], noHeader: true,
"color": "green", "color": "green",
"section-pre-class": "col-md-6", "section-pre-class": "col-md-6",
"info": [ "info": [
@ -6758,7 +6802,7 @@ module.exports = function(s,config,lang){
"section": "Monitor Settings Additional Input Map", "section": "Monitor Settings Additional Input Map",
"blocks": { "blocks": {
"Connection" : { "Connection" : {
"id": `monSectionMap$[NUMBER]`, "id": `monSectionMap$[TEMP_ID]`,
"name": `${lang['Input Map']} $[NUMBER]`, "name": `${lang['Input Map']} $[NUMBER]`,
"section-class": "input-map", "section-class": "input-map",
"color": "orange", "color": "orange",
@ -7350,8 +7394,11 @@ module.exports = function(s,config,lang){
<div class="monitor_details"> <div class="monitor_details">
<div class="pull-left"> <div class="pull-left">
<a title="${lang['Options']}" class="btn btn-sm badge btn-secondary toggle-live-grid-monitor-menu"><i class="fa fa-bars"></i></a> <a title="${lang['Options']}" class="btn btn-sm badge btn-secondary toggle-live-grid-monitor-menu"><i class="fa fa-bars"></i></a>
<a title="${lang['Edit']}" class="btn btn-sm badge btn-primary open-monitor-settings"><i class="fa fa-wrench"></i></a>
<a title="${lang['Toggle Substream']}" class="btn btn-sm badge btn-secondary toggle-monitor-substream"><i class="fa fa-eye"></i></a>
<a title="${lang['Snapshot']}" class="btn btn-sm badge btn-warning snapshot-live-grid-monitor"><i class="fa fa-camera"></i></a> <a title="${lang['Snapshot']}" class="btn btn-sm badge btn-warning snapshot-live-grid-monitor"><i class="fa fa-camera"></i></a>
<a title="${lang['Videos List']}" class="btn btn-sm badge btn-secondary open-videos"><i class="fa fa-film"></i></a> <a title="${lang['Videos List']}" class="btn btn-sm badge btn-secondary open-videos"><i class="fa fa-film"></i></a>
<a title="${lang['Show Logs']}" class="btn btn-sm badge btn-warning toggle-live-grid-monitor-logs"><i class="fa fa-exclamation-triangle"></i></a>
<a title="${lang['Close']}" class="btn btn-sm badge btn-danger close-live-grid-monitor"><i class="fa fa-times"></i></a> <a title="${lang['Close']}" class="btn btn-sm badge btn-danger close-live-grid-monitor"><i class="fa fa-times"></i></a>
</div> </div>
<div><span class="monitor_name">$MONITOR_NAME</span></div> <div><span class="monitor_name">$MONITOR_NAME</span></div>
@ -7380,6 +7427,11 @@ module.exports = function(s,config,lang){
"class": "warning toggle-live-grid-monitor-logs", "class": "warning toggle-live-grid-monitor-logs",
"icon": "exclamation-triangle" "icon": "exclamation-triangle"
}, },
"Show Logs": {
"label": lang['Toggle Substream'],
"class": "warning toggle-monitor-substream",
"icon": "eye"
},
"Control": { "Control": {
"label": lang['Control'], "label": lang['Control'],
"class": "default toggle-live-grid-monitor-ptz-controls", "class": "default toggle-live-grid-monitor-ptz-controls",
@ -7523,12 +7575,48 @@ module.exports = function(s,config,lang){
label: `${lang['Calendar']}`, label: `${lang['Calendar']}`,
pageOpen: 'calendarView', pageOpen: 'calendarView',
}, },
{
icon: 'fast-forward',
label: `${lang['Time-lapse']}`,
pageOpen: 'timelapseViewer',
},
{
divider: true,
},
{ {
icon: 'wrench', icon: 'wrench',
label: `${lang['Monitor Settings']}`, label: `${lang['Monitor Settings']}`,
pageOpen: 'monitorSettings', pageOpen: 'monitorSettings',
addUl: true, addUl: true,
}, },
{
icon: 'grav',
label: `${lang['Region Editor']}`,
pageOpen: 'regionEditor',
},
{
icon: 'filter',
label: `${lang['Event Filters']}`,
pageOpen: 'eventFilters',
},
{
icon: 'align-right',
label: `${lang['Monitor States']}`,
pageOpen: 'monitorStates',
},
{
icon: 'clock',
label: `${lang['Schedules']}`,
pageOpen: 'schedules',
},
{
icon: 'exclamation-triangle',
label: `${lang['Logs']}`,
pageOpen: 'logViewer',
},
{
divider: true,
},
{ {
icon: 'gears', icon: 'gears',
label: `${lang['Account Settings']}`, label: `${lang['Account Settings']}`,
@ -7541,43 +7629,13 @@ module.exports = function(s,config,lang){
pageOpen: 'subAccountManager', pageOpen: 'subAccountManager',
addUl: true, addUl: true,
}, },
{
icon: 'compass',
label: `${lang['ShinobiHub']}`,
pageOpen: 'configFinder',
addUl: true,
},
{
icon: 'grav',
label: `${lang['Region Editor']}`,
pageOpen: 'regionEditor',
addUl:true
},
{ {
icon: 'key', icon: 'key',
label: `${lang['API Keys']}`, label: `${lang['API Keys']}`,
pageOpen: 'apiKeys', pageOpen: 'apiKeys',
}, },
{ {
icon: 'align-right', divider: true,
label: `${lang['Monitor States']}`,
pageOpen: 'monitorStates',
},
{
icon: 'clock',
label: `${lang['Schedules']}`,
pageOpen: 'schedules',
},
{
icon: 'fast-forward',
label: `${lang['Time-lapse']}`,
pageOpen: 'timelapseViewer',
},
{
icon: 'filter',
label: `${lang['Event Filters']}`,
pageOpen: 'eventFilters',
addUl:true
}, },
{ {
icon: 'search', icon: 'search',
@ -7585,15 +7643,29 @@ module.exports = function(s,config,lang){
pageOpen: 'onvifScanner', pageOpen: 'onvifScanner',
addUl:true addUl:true
}, },
{
icon: 'opera',
label: `${lang['ONVIF Device Manager']}`,
pageOpen: 'onvifDeviceManager',
},
{ {
icon: 'eyedropper', icon: 'eyedropper',
label: `${lang['FFprobe']}`, label: `${lang['FFprobe']}`,
pageOpen: 'cameraProbe', pageOpen: 'cameraProbe',
}, },
{ {
icon: 'exclamation-triangle', icon: 'compass',
label: `${lang['Logs']}`, label: `${lang['ShinobiHub']}`,
pageOpen: 'logViewer', pageOpen: 'configFinder',
addUl: true,
},
{
divider: true,
},
{
icon: 'info-circle',
label: `${lang['Help']}`,
pageOpen: 'helpWindow',
}, },
// { // {
// icon: 'exclamation-circle', // icon: 'exclamation-circle',
@ -7710,61 +7782,6 @@ module.exports = function(s,config,lang){
"Power Viewer": { "Power Viewer": {
"section": lang["Power Viewer"], "section": lang["Power Viewer"],
"blocks": { "blocks": {
"Search Settings": {
id: "powerVideoTabs",
"color": "blue",
noHeader: true,
noDefaultSectionClasses: true,
attribute: `tab-chooser-parent`,
"section-pre-class": "col-md-4",
"info": [
{
"color": "blue",
noHeader: true,
isSection: true,
isFormGroupGroup: true,
"info": [
{
"field": lang['Monitors'],
"id": "powerVideoMonitorsList",
"form-group-attribute": 'tab-section=monitors',
"attribute": "multiple",
"fieldType": "select",
},
{
"id": "powerVideoDateRange",
"field": lang['Date Range'],
},
{
"id": "powerVideoVideoLimit",
"field": lang['Video Limit'] + ` (${lang['Per Monitor']})`,
"placeholder": "0",
},
{
"id": "powerVideoEventLimit",
"field": lang['Event Limit'] + ` (${lang['Per Monitor']})`,
"placeholder": "500",
},
{
id:'powerVideoSet',
field: lang['Video Set'],
default:'h264',
"fieldType": "select",
possible:[
{
"name": lang.Local,
"value": "local"
},
{
"name": lang.Cloud,
"value": "cloud"
},
]
},
]
},
]
},
"Video Playback": { "Video Playback": {
id: "powerVideoVideoPlayback", id: "powerVideoVideoPlayback",
noHeader: true, noHeader: true,
@ -7856,13 +7873,60 @@ module.exports = function(s,config,lang){
}, },
] ]
}, },
{
id: "powerVideoTabs",
attribute: `tab-chooser-parent`,
"color": "blue",
noHeader: true,
isSection: true,
isFormGroupGroup: true,
"info": [
{
"field": lang['Monitors'],
"id": "powerVideoMonitorsList",
"form-group-attribute": 'tab-section=monitors',
"attribute": "multiple",
"fieldType": "select",
},
{
"id": "powerVideoDateRange",
"field": lang['Date Range'],
},
{
"id": "powerVideoVideoLimit",
"field": lang['Video Limit'] + ` (${lang['Per Monitor']})`,
"placeholder": "0",
},
{
"id": "powerVideoEventLimit",
"field": lang['Event Limit'] + ` (${lang['Per Monitor']})`,
"placeholder": "500",
},
{
id:'powerVideoSet',
field: lang['Video Set'],
default:'h264',
"fieldType": "select",
possible:[
{
"name": lang.Local,
"value": "local"
},
{
"name": lang.Cloud,
"value": "cloud"
},
]
},
]
},
] ]
}, },
"Time Strip": { "Time Strip": {
id: "powerVideoTimelineStripsContainer", id: "powerVideoTimelineStripsContainer",
noHeader: true, noHeader: true,
"color": "bg-gradient-blue text-white", "color": "bg-gradient-blue text-white",
"section-pre-class": "col-md-12 mt-3", "section-pre-class": "col-md-4",
"info": [ "info": [
{ {
"id": "powerVideoTimelineStrips", "id": "powerVideoTimelineStrips",
@ -7870,7 +7934,7 @@ module.exports = function(s,config,lang){
"divContent": `<div class="loading"><i class="fa fa-hand-pointer-o"></i><div class="epic-text">${lang['Select a Monitor']}</div></div>`, "divContent": `<div class="loading"><i class="fa fa-hand-pointer-o"></i><div class="epic-text">${lang['Select a Monitor']}</div></div>`,
}, },
] ]
} },
} }
}, },
"Calendar": { "Calendar": {

View File

@ -23,8 +23,14 @@
"Use Raw Snapshot": "Use Raw Snapshot", "Use Raw Snapshot": "Use Raw Snapshot",
"Login": "Login", "Login": "Login",
"Substream": "Substream", "Substream": "Substream",
"Use Substream": "Use Substream",
"useSubStreamOnlyWhenWatching": "Only When Watching, Use Substream",
"substreamText": "This is an On-Demand method of viewing the Live Stream. You can make it so the viewing process is available only when someone is watching or to be used for switching between Low and High Resolution.",
"substreamConnectionText": "You can leave the Connection detail as-is if you want it to use the main Connection information set above.",
"substreamOutputText": "Here you can set the On-Demand Stream's configuration. Learn about <a href='https://hub.shinobi.video/articles/view/Eug1dxIdhwY6zTw' target='_blank'>latency of Stream types here.</a>",
"Toggle Substream": "Toggle Substream", "Toggle Substream": "Toggle Substream",
"Output": "Output", "Output": "Output",
"SubstreamNotConfigured": "Substream not configured. Open your Monitor Settings and configure it.",
"Substream Process": "Substream Process", "Substream Process": "Substream Process",
"Welcome": "Welcome!", "Welcome": "Welcome!",
"API Key Action Failed": "API Key Action Failed", "API Key Action Failed": "API Key Action Failed",
@ -508,6 +514,7 @@
"ago": "ago", "ago": "ago",
"a few seconds": "a few seconds", "a few seconds": "a few seconds",
"a minute": "a minute", "a minute": "a minute",
"minute": "minute",
"minutes": "minutes", "minutes": "minutes",
"an hour": "an hour", "an hour": "an hour",
"hours": "hours", "hours": "hours",
@ -577,6 +584,7 @@
"Creation Interval": "Creation Interval", "Creation Interval": "Creation Interval",
"Plugin": "Plugin", "Plugin": "Plugin",
"Plugin Manager": "Plugin Manager", "Plugin Manager": "Plugin Manager",
"MonitorStatesText": "You can learn about how to use this <a href='https://hub.shinobi.video/articles/view/6ylYHj9MemlZwrM' target='_blank'>here on ShinobiHub</a>.",
"IdentityText1": "This is how the system will identify the data for this stream. You cannot change the <b>Monitor ID</b> once you have pressed save. If you want you can make the <b>Monitor ID</b> more human readable before you continue.", "IdentityText1": "This is how the system will identify the data for this stream. You cannot change the <b>Monitor ID</b> once you have pressed save. If you want you can make the <b>Monitor ID</b> more human readable before you continue.",
"IdentityText2": "You can duplicate a monitor by modifying the <b>Monitor ID</b> then pressing save. You <b>cannot</b> use the ID of a monitor that already exists or it will save over that monitor's database information.", "IdentityText2": "You can duplicate a monitor by modifying the <b>Monitor ID</b> then pressing save. You <b>cannot</b> use the ID of a monitor that already exists or it will save over that monitor's database information.",
"opencvCascadesText": "If you see nothing here then just download this package of <a href=\"https://cdn.shinobi.video/weights/cascades.zip\">cascades</a>. Drop them into <code>plugins/opencv/cascades</code> then press refresh <i class=\"fa fa-retweet\"></i>.", "opencvCascadesText": "If you see nothing here then just download this package of <a href=\"https://cdn.shinobi.video/weights/cascades.zip\">cascades</a>. Drop them into <code>plugins/opencv/cascades</code> then press refresh <i class=\"fa fa-retweet\"></i>.",
@ -727,6 +735,7 @@
"NotifyErrorText": "Sending Notification caused an Error", "NotifyErrorText": "Sending Notification caused an Error",
"Check the Channel ID": "Check the Channel ID", "Check the Channel ID": "Check the Channel ID",
"Check the Recipient ID": "Check the Recipient ID", "Check the Recipient ID": "Check the Recipient ID",
"AppNotEnabledText": "App Not Enabled, Enable it in your Account Settings.",
"DiscordNotEnabledText": "Discord Bot Not Enabled, Enable it in your Account Settings.", "DiscordNotEnabledText": "Discord Bot Not Enabled, Enable it in your Account Settings.",
"Account Settings": "Account Settings", "Account Settings": "Account Settings",
"How to Record": "How to Record", "How to Record": "How to Record",
@ -1247,6 +1256,7 @@
"Close All Monitors": "Close All Monitors", "Close All Monitors": "Close All Monitors",
"Daily Events": "Daily Events", "Daily Events": "Daily Events",
"Send Notification": "Send Notification", "Send Notification": "Send Notification",
"Send to": "Send to",
"setMaxStorageAmountText": "You should set your Max Storage Amount in your Account Settings located on the left. Find the option under the Profile section. Default is 10 GB.", "setMaxStorageAmountText": "You should set your Max Storage Amount in your Account Settings located on the left. Find the option under the Profile section. Default is 10 GB.",
"Save Events": "Save Events", "Save Events": "Save Events",
"Original Choice": "Original Choice", "Original Choice": "Original Choice",

View File

@ -51,7 +51,7 @@
"Enable Nightvision": "Ativar Visão Noturna", "Enable Nightvision": "Ativar Visão Noturna",
"Disable Nightvision": "Desativar Visão Noturna", "Disable Nightvision": "Desativar Visão Noturna",
"Current": "Atual", "Current": "Atual",
"Monitors": "Monitors", "Monitors": "Monitores",
"Video": "Video", "Video": "Video",
"Videos": "Videos", "Videos": "Videos",
"Events": "Eventos", "Events": "Eventos",
@ -122,12 +122,12 @@
"Execute Command": "Executar Comando", "Execute Command": "Executar Comando",
"for Global Access": "para Acesso Global", "for Global Access": "para Acesso Global",
"Help": "Ajuda", "Help": "Ajuda",
"Don't show this anymore": "Não mostre isso mais", "Don't show this anymore": "Não me mostre isso mais",
"Chat on Discord": "Conversar no Discord", "Chat on Discord": "Conversar no Discord",
"Documentation": "Documentação", "Documentation": "Documentação",
"All Monitors": "Todos Monitores", "All Monitors": "Todos Monitores",
"Motion Meter": "Medidos de Movimento", "Motion Meter": "Medição de movimentos",
"FFmpegTip": "FFprobe is a simple multimedia streams analyzer. You can use it to output all kinds of information about an input including duration, frame rate, frame size, etc.", "FFmpegTip": "FFprobe é um simples analisador de streams. Você pode usá-lo para extrar todos os tipos de informação sobre uma entrada incluindo duração, taxa de frames por segundo, tamanho do frame, etc;",
"Complete Stream URL": "URL de transmissão completa", "Complete Stream URL": "URL de transmissão completa",
"ONVIF Scanner": "ONVIF Scanner", "ONVIF Scanner": "ONVIF Scanner",
"Scan Settings": "Configurações de digitalização", "Scan Settings": "Configurações de digitalização",
@ -162,7 +162,7 @@
"Browser Console Log": "Navegador de logs", "Browser Console Log": "Navegador de logs",
"All Monitors and Privileges": "Todos monitores e privilégios", "All Monitors and Privileges": "Todos monitores e privilégios",
"Permissions": "Permissões", "Permissions": "Permissões",
"Time-lapse Tool": "Ferramenta Time-laps", "Time-lapse Tool": "Time-lapse",
"total": "total", "total": "total",
"MB": "MB", "MB": "MB",
"Calendar": "Calendário", "Calendar": "Calendário",
@ -187,11 +187,11 @@
"Add New": "Adicionar novo", "Add New": "Adicionar novo",
"Delete Selected Videos": "Excluir vídeos selecionados", "Delete Selected Videos": "Excluir vídeos selecionados",
"DeleteSelectedVideosMsg": "Deseja excluir esses vídeos? Você não poderá recuperá-los.", "DeleteSelectedVideosMsg": "Deseja excluir esses vídeos? Você não poderá recuperá-los.",
"clientStreamFailedattemptingReconnect": "A verificação ctream do lado do cliente falhou, tentando reconectar.", "clientStreamFailedattemptingReconnect": "A verificação do stream falhou, tentando se reconectar.",
"Delete Filter": "Excluir filtro", "Delete Filter": "Excluir filtro",
"confirmDeleteFilter": "Deseja excluir este filtro? Você não poderá recuperá-lo.", "confirmDeleteFilter": "Deseja excluir este filtro? Você não poderá recuperá-lo.",
"Fix Video": "Corrigir Vídeo", "Fix Video": "Corrigir Vídeo",
"FixVideoMsg": "Você deseja corrigir esse vídeo? Você não poderá desfazer essa ação..", "FixVideoMsg": "Você deseja corrigir o vídeo? Você não poderá desfazer essa ação..",
"DeleteVideoMsg": "Deseja excluir este vídeo? Você não poderá recuperá-lo.", "DeleteVideoMsg": "Deseja excluir este vídeo? Você não poderá recuperá-lo.",
"dropBoxSuccess": "Sucesso! Arquivos salvos em seu Dropbox.", "dropBoxSuccess": "Sucesso! Arquivos salvos em seu Dropbox.",
"API Key Deleted": "Chave da API excluída", "API Key Deleted": "Chave da API excluída",
@ -246,13 +246,13 @@
"Connected": "Conectado", "Connected": "Conectado",
"Not Connected": "Não conectado", "Not Connected": "Não conectado",
"Lisence Plate Detector": "Detector de placas", "Lisence Plate Detector": "Detector de placas",
"OpenCV Cascades": "OpenCV Cascatas", "OpenCV Cascades": "Cascatas OpenCV ",
"Refresh List of Cascades": "Atualizar Lista de Cascatas", "Refresh List of Cascades": "Atualizar Lista de Cascatas",
"\"No Motion\" Detector": "\"Sem movimento\" Detector", "\"No Motion\" Detector": "\"Sem movimento\" Detector",
"Control": "Controle", "Control": "Controle",
"Grouping": "Agrupando &nbsp; <small>Adicione grupos em <b>Configurações</b></small>", "Grouping": "Agrupando &nbsp; <small>Adicione grupos em <b>Configurações</b></small>",
"Logging": "Logging", "Logging": "Logging",
"IdentityText1": "É assim que o sistema irá identificar os dados para este fluxo. Você não pode alterar o <b>ID do Monitor</b> uma vez que você pressionou salvar. Se você quiser, você pode fazer o <b>ID do Monitor</b> mais legível para humanos antes de continuar.", "IdentityText1": "É assim que o sistema irá identificar os dados para este fluxo. Você não pode alterar o <b>ID do Monitor</b> uma vez que você o salvou. Se você quiser, você pode fazer o <b>ID do Monitor</b> mais legível para humanos antes de continuar.",
"IdentityText2": "Você pode duplicar um monitor modificando o <b>ID do Monitor</b> e depois pressionando salvar. Você <b>não pode</b> usar o ID de um monitor que já existe ou ele economizará sobre as informações do banco de dados desse monitor.", "IdentityText2": "Você pode duplicar um monitor modificando o <b>ID do Monitor</b> e depois pressionando salvar. Você <b>não pode</b> usar o ID de um monitor que já existe ou ele economizará sobre as informações do banco de dados desse monitor.",
"noSpecialCharacters": "Sem espaços ou caracteres especiais.", "noSpecialCharacters": "Sem espaços ou caracteres especiais.",
"NotesPlacholder": "Comentários que você quer deixar para as configurações desta câmera.", "NotesPlacholder": "Comentários que você quer deixar para as configurações desta câmera.",
@ -273,7 +273,7 @@
"Path": "Caminho", "Path": "Caminho",
"Monitor Capture Rate": "Taxa de captura do monitor <small>(FPS)</small>", "Monitor Capture Rate": "Taxa de captura do monitor <small>(FPS)</small>",
"Analyzation Duration": "Duração da análise", "Analyzation Duration": "Duração da análise",
"Probe Size": "Probe Size", "Probe Size": "Tamanho da sonda",
"Stream Type": "Tipo de transmissão", "Stream Type": "Tipo de transmissão",
"# of Allow MJPEG Clients": "# para permitir clientes MJPEG <small>0 para infinito</small>", "# of Allow MJPEG Clients": "# para permitir clientes MJPEG <small>0 para infinito</small>",
"HLS Video Encoder": "Codificador de vídeo HLS", "HLS Video Encoder": "Codificador de vídeo HLS",
@ -281,9 +281,9 @@
"HLS Segment Length": "Comprimento do segmento HLS <small>em segundos</small>", "HLS Segment Length": "Comprimento do segmento HLS <small>em segundos</small>",
"HLS Preset": "Pré-definição HLS", "HLS Preset": "Pré-definição HLS",
"HLS List Size": "Tamanho da lista HLS", "HLS List Size": "Tamanho da lista HLS",
"Check Signal Interval": "Verifique o intervalo do sinal <small>em minutos</small>", "Check Signal Interval": "Verificar o intervalo do sinal <small>em minutos</small>",
"Log Signal Event": "Evento de sinal de registro <small>Apenas cliente</small>", "Log Signal Event": "Evento de sinal de registro <small>Apenas cliente</small>",
"Quality": "Qualidade <small>1 para alta, 23 para Low</small>", "Quality": "Qualidade <small>1 para alta, 23 para baixa</small>",
"Rate": "Taxa <small>(FPS)</small>", "Rate": "Taxa <small>(FPS)</small>",
"Width": "Largura", "Width": "Largura",
"Height": "Altura", "Height": "Altura",
@ -444,13 +444,13 @@
"Process Unexpected Exit": "Saída inesperado do processo", "Process Unexpected Exit": "Saída inesperado do processo",
"Process Crashed for Monitor": "Processo de Monitor quebrado", "Process Crashed for Monitor": "Processo de Monitor quebrado",
"FFmpegCantStart": "FFmpeg não pôde iniciar", "FFmpegCantStart": "FFmpeg não pôde iniciar",
"FFmpegCantStartText": "O mecanismo de gravação para esta câmera não pôde começar. Pode haver algo errado com a configuração da sua câmera. Se houver algum registro diferente deste, por favor, coloque-os em <b> Problemas </b> no Github.", "FFmpegCantStartText": "O FFMpeg não pode inicializar. Pode haver algo de errado com a configuração da sua câmera. Se houver alguma configuração diferente, por favor, reporte no Gitlab.",
"JPEG Error": "Erro JPEG", "JPEG Error": "Erro JPEG",
"JPEGErrorText": "Houve um problema ao obter dados da sua câmera.", "JPEGErrorText": "Houve um problema ao obter dados da sua câmera.",
"Fatal Maximum Reached": "Máximo atingido, parando câmera.", "Fatal Maximum Reached": "Máximo atingido, parando câmera.",
"FatalMaximumReachedText": "Erro JPEG fatal.", "FatalMaximumReachedText": "Erro fatal de JPEG",
"Incorrect Settings Chosen": "Configuração incorreta escolhida", "Incorrect Settings Chosen": "Configuração incorreta escolhida",
"Can't Connect": "Não pode conectar", "Can't Connect": "Não foi possível conectar",
"Video Finished": "Vídeo finalizado", "Video Finished": "Vídeo finalizado",
"No Monitor Found, Ignoring Request": "Monitor não encontrado, ignorando requisição", "No Monitor Found, Ignoring Request": "Monitor não encontrado, ignorando requisição",
"Event": "Evento", "Event": "Evento",

View File

@ -251,7 +251,9 @@ const initialize = (config,lang) => {
connectionToP2PServer.on('disconnect',onDisconnect) connectionToP2PServer.on('disconnect',onDisconnect)
} }
startBridge() startBridge()
setInterval(() => { setInterval(function(){
startBridge(true) if(!connectionToP2PServer || !connectionToP2PServer.connected){
},1000 * 60 * 60 * 15) connectionToP2PServer.connect()
}
},1000 * 60 * 15)
} }

View File

@ -123,13 +123,24 @@ module.exports = function(s,config,lang,app,io){
} }
} }
async function getSnapshotFromOnvif(onvifOptions){ async function getSnapshotFromOnvif(onvifOptions){
return await createSnapshot({ let theUrl;
output: ['-s 400x400'], if(onvifOptions.mid && onvifOptions.ke){
url: addCredentialsToStreamLink({ const groupKey = onvifOptions.ke
const monitorId = onvifOptions.mid
const theDevice = s.group[groupKey].activeMonitors[monitorId].onvifConnection
theUrl = (await theDevice.services.media.getSnapshotUri({
ProfileToken : theDevice.current_profile.token,
})).GetSnapshotUriResponse.MediaUri.Uri;
}else{
theUrl = addCredentialsToStreamLink({
username: onvifOptions.username, username: onvifOptions.username,
password: onvifOptions.password, password: onvifOptions.password,
url: onvifOptions.uri url: onvifOptions.uri
}), })
}
return await createSnapshot({
output: ['-s 400x400'],
url: theUrl,
}) })
} }
/** /**

View File

@ -177,6 +177,7 @@ module.exports = function(s,config){
] ]
const monitorRestrictions = options.monitorRestrictions const monitorRestrictions = options.monitorRestrictions
var frameLimit = options.limit var frameLimit = options.limit
const noLimit = options.noLimit === '1'
const endIsStartTo = options.endIsStartTo const endIsStartTo = options.endIsStartTo
const chosenDate = options.date const chosenDate = options.date
const startDate = options.startDate ? stringToSqlTime(options.startDate) : null const startDate = options.startDate ? stringToSqlTime(options.startDate) : null
@ -217,6 +218,7 @@ module.exports = function(s,config){
whereQuery.push(['filename','=',options.filename]) whereQuery.push(['filename','=',options.filename])
frameLimit = "1"; frameLimit = "1";
} }
if(noLimit)frameLimit = '0';
options.orderBy = options.orderBy ? options.orderBy : ['time','desc'] options.orderBy = options.orderBy ? options.orderBy : ['time','desc']
if(options.count)options.groupBy = options.groupBy ? options.groupBy : options.orderBy[0] if(options.count)options.groupBy = options.groupBy ? options.groupBy : options.orderBy[0]
knexQuery({ knexQuery({
@ -337,7 +339,7 @@ module.exports = function(s,config){
endDate: endTime, endDate: endTime,
startOperator: startTimeOperator, startOperator: startTimeOperator,
endOperator: endTimeOperator, endOperator: endTimeOperator,
limit: options.limit, limit: options.noLimit === '1' ? '0' : options.limit,
archived: archived, archived: archived,
rowType: rowName, rowType: rowName,
endIsStartTo: endIsStartTo endIsStartTo: endIsStartTo

View File

@ -628,7 +628,7 @@ module.exports = (s,config,lang,app,io) => {
save : false, save : false,
webhook : false, webhook : false,
command : false, command : false,
record : false, record : true,
forceRecord : false, forceRecord : false,
indifference : false, indifference : false,
countObjects : false countObjects : false
@ -646,8 +646,7 @@ module.exports = (s,config,lang,app,io) => {
}) })
const eventDetails = d.details const eventDetails = d.details
const passedEventFilters = checkEventFilters(d,monitorDetails,filter) const passedEventFilters = checkEventFilters(d,monitorDetails,filter)
if(!passedEventFilters)return if(!passedEventFilters)return;
const detailString = JSON.stringify(eventDetails)
const eventTime = new Date() const eventTime = new Date()
if( if(
filter.addToMotionCounter && filter.addToMotionCounter &&

View File

@ -34,8 +34,7 @@ module.exports = async (s,config,lang,onFinish) => {
mid: e.mid, mid: e.mid,
} }
const ffmpegCommand = [`-progress pipe:5`]; const ffmpegCommand = [`-progress pipe:5`];
([ const allOutputs = [
buildMainInput(e),
buildMainStream(e), buildMainStream(e),
buildJpegApiOutput(e), buildJpegApiOutput(e),
buildMainRecording(e), buildMainRecording(e),
@ -43,21 +42,25 @@ module.exports = async (s,config,lang,onFinish) => {
buildMainDetector(e), buildMainDetector(e),
buildEventRecordingOutput(e), buildEventRecordingOutput(e),
buildTimelapseOutput(e), buildTimelapseOutput(e),
]).forEach(function(commandStringPart){ ];
ffmpegCommand.push(commandStringPart) if(allOutputs.filter(output => !!output).length > 0){
}) ([
s.onFfmpegCameraStringCreationExtensions.forEach(function(extender){ buildMainInput(e),
extender(e,ffmpegCommand) ]).concat(allOutputs).forEach(function(commandStringPart){
}) ffmpegCommand.push(commandStringPart)
const stdioPipes = createPipeArray(e) })
const ffmpegCommandString = ffmpegCommand.join(' ') s.onFfmpegCameraStringCreationExtensions.forEach(function(extender){
//hold ffmpeg command for log stream extender(e,ffmpegCommand)
s.group[e.ke].activeMonitors[e.mid].ffmpeg = sanitizedFfmpegCommand(e,ffmpegCommandString) })
//clean the string of spatial impurities and split for spawn() const stdioPipes = createPipeArray(e)
const ffmpegCommandParsed = splitForFFPMEG(ffmpegCommandString) const ffmpegCommandString = ffmpegCommand.join(' ')
try{ //hold ffmpeg command for log stream
fs.unlinkSync(e.sdir + 'cmd.txt') activeMonitor.ffmpeg = sanitizedFfmpegCommand(e,ffmpegCommandString)
}catch(err){ //clean the string of spatial impurities and split for spawn()
const ffmpegCommandParsed = splitForFFPMEG(ffmpegCommandString)
try{
fs.unlinkSync(e.sdir + 'cmd.txt')
}catch(err){
} }
fs.writeFileSync(e.sdir + 'cmd.txt',JSON.stringify({ fs.writeFileSync(e.sdir + 'cmd.txt',JSON.stringify({
@ -69,20 +72,31 @@ module.exports = async (s,config,lang,onFinish) => {
config: config, config: config,
isAtleatOneDetectorPluginConnected: s.isAtleatOneDetectorPluginConnected isAtleatOneDetectorPluginConnected: s.isAtleatOneDetectorPluginConnected
} }
},null,3),'utf8') fs.writeFileSync(e.sdir + 'cmd.txt',JSON.stringify({
var cameraCommandParams = [ cmd: ffmpegCommandParsed,
config.monitorDaemonPath ? config.monitorDaemonPath : __dirname + '/cameraThread/singleCamera.js', pipes: stdioPipes.length,
config.ffmpegDir, rawMonitorConfig: s.group[e.ke].rawMonitorConfigurations[e.id],
e.sdir + 'cmd.txt' globalInfo: {
] config: config,
const cameraProcess = spawn('node',cameraCommandParams,{detached: true,stdio: stdioPipes}) isAtleatOneDetectorPluginConnected: s.isAtleatOneDetectorPluginConnected
if(config.debugLog === true && config.debugLogMonitors === true){ }
cameraProcess.stderr.on('data',(data) => { },null,3),'utf8')
console.log(`${e.ke} ${e.mid}`) var cameraCommandParams = [
console.log(data.toString()) config.monitorDaemonPath ? config.monitorDaemonPath : __dirname + '/cameraThread/singleCamera.js',
}) config.ffmpegDir,
e.sdir + 'cmd.txt'
]
const cameraProcess = spawn('node',cameraCommandParams,{detached: true,stdio: stdioPipes})
if(config.debugLog === true && config.debugLogMonitors === true){
cameraProcess.stderr.on('data',(data) => {
console.log(`${e.ke} ${e.mid}`)
console.log(data.toString())
})
}
return cameraProcess
}else{
return null
} }
return cameraProcess
}catch(err){ }catch(err){
s.systemLog(err) s.systemLog(err)
return null return null

View File

@ -185,7 +185,7 @@ module.exports = (s,config,lang) => {
const createStreamChannel = function(e,number,channel){ const createStreamChannel = function(e,number,channel){
//`e` is the monitor object //`e` is the monitor object
//`x` is an object used to contain temporary values. //`x` is an object used to contain temporary values.
const channelStreamDirectory = !isNaN(parseInt(number)) ? `${e.sdir}channel${number}/` : e.sdir const channelStreamDirectory = !isNaN(parseInt(number)) ? `${e.sdir || s.getStreamsDirectory(e)}channel${number}/` : e.sdir
if(channelStreamDirectory !== e.sdir && !fs.existsSync(channelStreamDirectory)){ if(channelStreamDirectory !== e.sdir && !fs.existsSync(channelStreamDirectory)){
try{ try{
fs.mkdirSync(channelStreamDirectory) fs.mkdirSync(channelStreamDirectory)
@ -377,7 +377,7 @@ module.exports = (s,config,lang) => {
//x = temporary values //x = temporary values
const streamFlags = [] const streamFlags = []
const streamType = e.details.stream_type ? e.details.stream_type : 'hls' const streamType = e.details.stream_type ? e.details.stream_type : 'hls'
if(streamType !== 'jpeg'){ if(streamType !== 'jpeg' && streamType !== 'useSubstream'){
const isCudaEnabled = hasCudaEnabled(e) const isCudaEnabled = hasCudaEnabled(e)
const streamFilters = [] const streamFilters = []
const videoCodecisCopy = e.details.stream_vcodec === 'copy' const videoCodecisCopy = e.details.stream_vcodec === 'copy'

View File

@ -35,6 +35,10 @@ module.exports = function(s,config,lang){
cameraDestroy, cameraDestroy,
monitorConfigurationMigrator, monitorConfigurationMigrator,
attachStreamChannelHandlers, attachStreamChannelHandlers,
setActiveViewer,
getActiveViewerCount,
destroySubstreamProcess,
attachMainProcessHandlers,
} = require('./monitor/utils.js')(s,config,lang) } = require('./monitor/utils.js')(s,config,lang)
const { const {
addEventDetailsToString, addEventDetailsToString,
@ -66,7 +70,7 @@ module.exports = function(s,config,lang){
if(!activeMonitor.contentWriter){activeMonitor.contentWriter={}}; if(!activeMonitor.contentWriter){activeMonitor.contentWriter={}};
if(!activeMonitor.childNodeStreamWriters){activeMonitor.childNodeStreamWriters={}}; if(!activeMonitor.childNodeStreamWriters){activeMonitor.childNodeStreamWriters={}};
if(!activeMonitor.eventBasedRecording){activeMonitor.eventBasedRecording={}}; if(!activeMonitor.eventBasedRecording){activeMonitor.eventBasedRecording={}};
if(!activeMonitor.watch){activeMonitor.watch={}}; if(!activeMonitor.watch){activeMonitor.watch = []};
if(!activeMonitor.fixingVideos){activeMonitor.fixingVideos={}}; if(!activeMonitor.fixingVideos){activeMonitor.fixingVideos={}};
// if(!activeMonitor.viewerConnection){activeMonitor.viewerConnection={}}; // if(!activeMonitor.viewerConnection){activeMonitor.viewerConnection={}};
// if(!activeMonitor.viewerConnectionCount){activeMonitor.viewerConnectionCount=0}; // if(!activeMonitor.viewerConnectionCount){activeMonitor.viewerConnectionCount=0};
@ -151,7 +155,7 @@ module.exports = function(s,config,lang){
return x.ar; return x.ar;
} }
s.getStreamsDirectory = (monitor) => { s.getStreamsDirectory = (monitor) => {
return s.dir.streams + monitor.ke + '/' + monitor.mid + '/' return s.dir.streams + monitor.ke + '/' + (monitor.mid || monitor.id) + '/'
} }
s.getRawSnapshotFromMonitor = function(monitor,options){ s.getRawSnapshotFromMonitor = function(monitor,options){
return new Promise((resolve,reject) => { return new Promise((resolve,reject) => {
@ -513,20 +517,7 @@ module.exports = function(s,config,lang){
s.checkDetails(e) s.checkDetails(e)
if(e.ke && config.doSnapshot === true){ if(e.ke && config.doSnapshot === true){
if(s.group[e.ke] && s.group[e.ke].rawMonitorConfigurations && s.group[e.ke].rawMonitorConfigurations[e.mid] && s.group[e.ke].rawMonitorConfigurations[e.mid].mode !== 'stop'){ if(s.group[e.ke] && s.group[e.ke].rawMonitorConfigurations && s.group[e.ke].rawMonitorConfigurations[e.mid] && s.group[e.ke].rawMonitorConfigurations[e.mid].mode !== 'stop'){
if(s.group[e.ke].activeMonitors[e.mid].onvifConnection){ async function getRaw(){
const screenShot = await s.getSnapshotFromOnvif({
username: onvifUsername,
password: onvifPassword,
uri: cameraResponse.uri,
});
s.tx({
f: 'monitor_snapshot',
snapshot: screenShot.toString('base64'),
snapshot_format: 'b64',
mid: e.mid,
ke: e.ke
},'GRP_'+e.ke)
}else{
var pathDir = s.dir.streams+e.ke+'/'+e.mid+'/' var pathDir = s.dir.streams+e.ke+'/'+e.mid+'/'
const {screenShot, isStaticFile} = await s.getRawSnapshotFromMonitor(s.group[e.ke].rawMonitorConfigurations[e.mid],options) const {screenShot, isStaticFile} = await s.getRawSnapshotFromMonitor(s.group[e.ke].rawMonitorConfigurations[e.mid],options)
if(screenShot){ if(screenShot){
@ -540,7 +531,27 @@ module.exports = function(s,config,lang){
}else{ }else{
s.debugLog('Damaged Snapshot Data') s.debugLog('Damaged Snapshot Data')
s.tx({f:'monitor_snapshot',snapshot:e.mon.name,snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke) s.tx({f:'monitor_snapshot',snapshot:e.mon.name,snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke)
} }
}
if(s.group[e.ke].activeMonitors[e.mid].onvifConnection){
try{
const screenShot = await s.getSnapshotFromOnvif({
ke: e.ke,
mid: e.mid,
});
s.tx({
f: 'monitor_snapshot',
snapshot: screenShot.toString('base64'),
snapshot_format: 'b64',
mid: e.mid,
ke: e.ke
},'GRP_'+e.ke)
}catch(err){
s.debugLog(err)
await getRaw()
}
}else{
await getRaw()
} }
}else{ }else{
s.tx({f:'monitor_snapshot',snapshot:'Disabled',snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke) s.tx({f:'monitor_snapshot',snapshot:'Disabled',snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke)
@ -765,55 +776,8 @@ module.exports = function(s,config,lang){
code: e.wantedStatusCode code: e.wantedStatusCode
}); });
//on unexpected exit restart //on unexpected exit restart
s.group[e.ke].activeMonitors[e.id].spawn_exit = function(){ if(s.group[e.ke].activeMonitors[e.id].spawn)attachMainProcessHandlers(e,fatalError)
if(s.group[e.ke].activeMonitors[e.id].isStarted === true){ return s.group[e.ke].activeMonitors[e.id].spawn
if(e.details.loglevel!=='quiet'){
s.userLog(e,{type:lang['Process Unexpected Exit'],msg:{msg:lang.unexpectedExitText,cmd:s.group[e.ke].activeMonitors[e.id].ffmpeg}});
}
fatalError(e,'Process Unexpected Exit');
scanForOrphanedVideos(e,{
forceCheck: true,
checkMax: 2
})
s.onMonitorUnexpectedExitExtensions.forEach(function(extender){
extender(Object.assign(s.group[e.ke].rawMonitorConfigurations[e.id],{}),e)
})
}
}
s.group[e.ke].activeMonitors[e.id].spawn.on('end',s.group[e.ke].activeMonitors[e.id].spawn_exit)
s.group[e.ke].activeMonitors[e.id].spawn.on('exit',s.group[e.ke].activeMonitors[e.id].spawn_exit)
s.group[e.ke].activeMonitors[e.id].spawn.on('error',function(er){
s.userLog(e,{type:'Spawn Error',msg:er});fatalError(e,'Spawn Error')
})
s.userLog(e,{type:lang['Process Started'],msg:{cmd:s.group[e.ke].activeMonitors[e.id].ffmpeg}})
if(s.isWin === false){
var strippedHost = s.stripAuthFromHost(e)
var sendProcessCpuUsage = function(){
s.getMonitorCpuUsage(e,function(percent){
s.group[e.ke].activeMonitors[e.id].currentCpuUsage = percent
s.tx({
f: 'camera_cpu_usage',
ke: e.ke,
id: e.id,
percent: percent
},'MON_STREAM_'+e.ke+e.id)
})
}
clearInterval(s.group[e.ke].activeMonitors[e.id].getMonitorCpuUsage)
s.group[e.ke].activeMonitors[e.id].getMonitorCpuUsage = setInterval(function(){
if(e.details.skip_ping !== '1'){
connectionTester.test(strippedHost,e.port,2000,function(err,response){
if(response.success){
sendProcessCpuUsage()
}else{
launchMonitorProcesses(e)
}
})
}else{
sendProcessCpuUsage()
}
},1000 * 60)
}
} }
const createEventCounter = function(monitor){ const createEventCounter = function(monitor){
if(monitor.details.detector_obj_count === '1'){ if(monitor.details.detector_obj_count === '1'){
@ -1235,27 +1199,29 @@ module.exports = function(s,config,lang){
if(pingResponse.success === true){ if(pingResponse.success === true){
activeMonitor.isRecording = true activeMonitor.isRecording = true
try{ try{
createCameraFfmpegProcess(e) var mainProcess = createCameraFfmpegProcess(e)
createCameraStreamHandlers(e)
createEventCounter(e) createEventCounter(e)
if(e.type === 'dashcam' || e.type === 'socket'){ if(mainProcess){
setTimeout(function(){ createCameraStreamHandlers(e)
activeMonitor.allowStdinWrite = true if(e.type === 'dashcam' || e.type === 'socket'){
s.txToDashcamUsers({ setTimeout(function(){
f : 'enable_stream', activeMonitor.allowStdinWrite = true
ke : e.ke, s.txToDashcamUsers({
mid : e.id f : 'enable_stream',
},e.ke) ke : e.ke,
},30000) mid : e.id
} },e.ke)
if( },30000)
e.functionMode === 'record' || }
e.type === 'mjpeg' || if(
e.type === 'h264' || e.functionMode === 'record' ||
e.type === 'local' e.type === 'mjpeg' ||
){ e.type === 'h264' ||
catchNewSegmentNames(e) e.type === 'local'
cameraFilterFfmpegLog(e) ){
catchNewSegmentNames(e)
cameraFilterFfmpegLog(e)
}
} }
clearTimeout(activeMonitor.onMonitorStartTimer) clearTimeout(activeMonitor.onMonitorStartTimer)
activeMonitor.onMonitorStartTimer = setTimeout(() => { activeMonitor.onMonitorStartTimer = setTimeout(() => {
@ -1511,26 +1477,23 @@ module.exports = function(s,config,lang){
s.initiateMonitorObject({ke:e.ke,mid:e.id}) s.initiateMonitorObject({ke:e.ke,mid:e.id})
switch(e.functionMode){ switch(e.functionMode){
case'watch_on'://live streamers - join case'watch_on'://live streamers - join
if(!cn.monitorsCurrentlyWatching){cn.monitorsCurrentlyWatching = {}} if(!cn.monitorsCurrentlyWatching){cn.monitorsCurrentlyWatching = {}}
if(!cn.monitorsCurrentlyWatching[e.id]){cn.monitorsCurrentlyWatching[e.id]={ke:e.ke}} if(!cn.monitorsCurrentlyWatching[e.id]){cn.monitorsCurrentlyWatching[e.id]={ke:e.ke}}
s.group[e.ke].activeMonitors[e.id].watch[cn.id]={}; setActiveViewer(e.ke,e.id,cn.id,true)
var numberOfViewers = Object.keys(s.group[e.ke].activeMonitors[e.id].watch).length s.group[e.ke].activeMonitors[e.id].allowDestroySubstream = false
s.tx({ clearTimeout(s.group[e.ke].activeMonitors[e.id].noViewerCountDisableSubstream)
viewers: numberOfViewers,
ke: e.ke,
id: e.id
},'MON_'+e.ke+e.id)
break; break;
case'watch_off'://live streamers - leave case'watch_off'://live streamers - leave
if(cn.monitorsCurrentlyWatching){delete(cn.monitorsCurrentlyWatching[e.id])} if(cn.monitorsCurrentlyWatching){delete(cn.monitorsCurrentlyWatching[e.id])}
var numberOfViewers = 0 setActiveViewer(e.ke,e.id,cn.id,false)
delete(s.group[e.ke].activeMonitors[e.id].watch[cn.id]); clearTimeout(s.group[e.ke].activeMonitors[e.id].noViewerCountDisableSubstream)
numberOfViewers = Object.keys(s.group[e.ke].activeMonitors[e.id].watch).length s.group[e.ke].activeMonitors[e.id].noViewerCountDisableSubstream = setTimeout(async () => {
s.tx({ let currentCount = getActiveViewerCount(e.ke,e.id)
viewers: numberOfViewers, if(currentCount === 0 && s.group[e.ke].activeMonitors[e.id].subStreamProcess){
ke: e.ke, s.group[e.ke].activeMonitors[e.id].allowDestroySubstream = true
id: e.id await destroySubstreamProcess(s.group[e.ke].activeMonitors[e.id])
},'MON_'+e.ke+e.id) }
},10000)
break; break;
case'restart'://restart monitor case'restart'://restart monitor
s.sendMonitorStatus({ s.sendMonitorStatus({

View File

@ -3,7 +3,11 @@ const treekill = require('tree-kill');
const spawn = require('child_process').spawn; const spawn = require('child_process').spawn;
const events = require('events'); const events = require('events');
const Mp4Frag = require('mp4frag'); const Mp4Frag = require('mp4frag');
const streamViewerCountTimeouts = {}
module.exports = (s,config,lang) => { module.exports = (s,config,lang) => {
const {
scanForOrphanedVideos
} = require('../video/utils.js')(s,config,lang)
const { const {
createPipeArray, createPipeArray,
splitForFFPMEG, splitForFFPMEG,
@ -17,6 +21,10 @@ module.exports = (s,config,lang) => {
const processKill = (proc) => { const processKill = (proc) => {
const response = {ok: true} const response = {ok: true}
return new Promise((resolve,reject) => { return new Promise((resolve,reject) => {
if(!proc){
resolve(response)
return
}
function sendError(err){ function sendError(err){
response.ok = false response.ok = false
response.err = err response.err = err
@ -94,13 +102,17 @@ module.exports = (s,config,lang) => {
if(activeMonitor.onChildNodeExit){ if(activeMonitor.onChildNodeExit){
activeMonitor.onChildNodeExit() activeMonitor.onChildNodeExit()
} }
activeMonitor.spawn.stdio.forEach(function(stdio){ try{
try{ activeMonitor.spawn.stdio.forEach(function(stdio){
stdio.unpipe() try{
}catch(err){ stdio.unpipe()
console.log(err) }catch(err){
} console.log(err)
}) }
})
}catch(err){
// s.debugLog(err)
}
if(activeMonitor.mp4frag){ if(activeMonitor.mp4frag){
var mp4FragChannels = Object.keys(activeMonitor.mp4frag) var mp4FragChannels = Object.keys(activeMonitor.mp4frag)
mp4FragChannels.forEach(function(channel){ mp4FragChannels.forEach(function(channel){
@ -116,6 +128,7 @@ module.exports = (s,config,lang) => {
}else{ }else{
processKill(proc).then((response) => { processKill(proc).then((response) => {
s.debugLog(`cameraDestroy`,response) s.debugLog(`cameraDestroy`,response)
activeMonitor.allowDestroySubstream = true
destroySubstreamProcess(activeMonitor).then((response) => { destroySubstreamProcess(activeMonitor).then((response) => {
if(response.hadSubStream)s.debugLog(`cameraDestroy`,response.closeResponse) if(response.hadSubStream)s.debugLog(`cameraDestroy`,response.closeResponse)
}) })
@ -209,13 +222,19 @@ module.exports = (s,config,lang) => {
const spawnSubstreamProcess = function(e){ const spawnSubstreamProcess = function(e){
// e = monitorConfig // e = monitorConfig
try{ try{
const monitorConfig = s.group[e.ke].rawMonitorConfigurations[e.mid] const groupKey = e.ke
const monitorId = e.mid
const monitorConfig = Object.assign({},s.group[groupKey].rawMonitorConfigurations[monitorId])
const monitorDetails = monitorConfig.details const monitorDetails = monitorConfig.details
const activeMonitor = s.group[e.ke].activeMonitors[e.mid] const activeMonitor = s.group[e.ke].activeMonitors[e.mid]
const channelNumber = 1 + (monitorDetails.stream_channels || []).length const channelNumber = 1 + (monitorDetails.stream_channels || []).length
const ffmpegCommand = [`-progress pipe:5`]; const ffmpegCommand = [`-progress pipe:5`];
const logLevel = monitorDetails.loglevel ? e.details.loglevel : 'warning' const logLevel = monitorDetails.loglevel ? e.details.loglevel : 'warning'
const stdioPipes = createPipeArray({}, 2) const stdioPipes = createPipeArray({}, 2)
const substreamConfig = monitorConfig.details.substream
substreamConfig.input.type = !substreamConfig.input.fulladdress ? monitorConfig.type : substreamConfig.input.type || monitorConfig.details.rtsp_transport
substreamConfig.input.fulladdress = substreamConfig.input.fulladdress || s.buildMonitorUrl(monitorConfig)
substreamConfig.input.rtsp_transport = substreamConfig.input.rtsp_transport || monitorConfig.details.rtsp_transport
const { const {
inputAndConnectionFields, inputAndConnectionFields,
outputFields, outputFields,
@ -259,8 +278,7 @@ module.exports = (s,config,lang) => {
s.userLog({ s.userLog({
ke: e.ke, ke: e.ke,
mid: e.mid, mid: e.mid,
}, },{
{
type: lang["Substream Process"], type: lang["Substream Process"],
msg: data.toString() msg: data.toString()
}) })
@ -302,21 +320,25 @@ module.exports = (s,config,lang) => {
hadSubStream: false, hadSubStream: false,
alreadyClosing: false alreadyClosing: false
} }
if(activeMonitor.subStreamProcessClosing){ try{
response.alreadyClosing = true if(activeMonitor.subStreamProcessClosing){
}else if(activeMonitor.subStreamProcess){ response.alreadyClosing = true
activeMonitor.subStreamProcessClosing = true }else if(activeMonitor.subStreamProcess){
activeMonitor.subStreamChannel = null; activeMonitor.subStreamProcessClosing = true
const closeResponse = await processKill(activeMonitor.subStreamProcess) activeMonitor.subStreamChannel = null;
response.hadSubStream = true const closeResponse = await processKill(activeMonitor.subStreamProcess)
response.closeResponse = closeResponse response.hadSubStream = true
delete(activeMonitor.subStreamProcess) response.closeResponse = closeResponse
s.tx({ delete(activeMonitor.subStreamProcess)
f: 'substream_end', s.tx({
mid: activeMonitor.mid, f: 'substream_end',
ke: activeMonitor.ke mid: activeMonitor.mid,
},'GRP_'+activeMonitor.ke); ke: activeMonitor.ke
activeMonitor.subStreamProcessClosing = false },'GRP_'+activeMonitor.ke);
activeMonitor.subStreamProcessClosing = false
}
}catch(err){
s.debugLog('destroySubstreamProcess',err)
} }
return response return response
} }
@ -360,6 +382,97 @@ module.exports = (s,config,lang) => {
ffmpegProcess.stdio[pipeNumber].on('data',frameToStreamAdded) ffmpegProcess.stdio[pipeNumber].on('data',frameToStreamAdded)
} }
} }
function setActiveViewer(groupKey,monitorId,connectionId,isBeingAdded){
const viewerList = s.group[groupKey].activeMonitors[monitorId].watch;
if(isBeingAdded){
if(viewerList.indexOf(connectionId) > -1)viewerList.push(connectionId);
}else{
viewerList.splice(viewerList.indexOf(connectionId), 1)
}
const numberOfViewers = viewerList.length
s.tx({
f: 'viewer_count',
viewers: numberOfViewers,
ke: groupKey,
id: monitorId
},'MON_' + groupKey + monitorId)
return numberOfViewers;
}
function getActiveViewerCount(groupKey,monitorId){
const viewerList = s.group[groupKey].activeMonitors[monitorId].watch;
const numberOfViewers = viewerList.length
return numberOfViewers;
}
function setTimedActiveViewerForHttp(req){
const groupKey = req.params.ke
const connectionId = req.params.auth
const loggedInUser = s.group[groupKey].users[connectionId]
if(!loggedInUser){
const monitorId = req.params.id
const viewerList = s.group[groupKey].activeMonitors[monitorId].watch
const theViewer = viewerList[connectionId]
if(!theViewer){
setActiveViewer(groupKey,monitorId,connectionId,true)
}
clearTimeout(streamViewerCountTimeouts[req.originalUrl])
streamViewerCountTimeouts[req.originalUrl] = setTimeout(() => {
setActiveViewer(groupKey,monitorId,connectionId,false)
},5000)
}else{
s.debugLog(`User is Logged in, Don't add to viewer count`);
}
}
function attachMainProcessHandlers(e,fatalError){
s.group[e.ke].activeMonitors[e.id].spawn_exit = function(){
if(s.group[e.ke].activeMonitors[e.id].isStarted === true){
if(e.details.loglevel!=='quiet'){
s.userLog(e,{type:lang['Process Unexpected Exit'],msg:{msg:lang.unexpectedExitText,cmd:s.group[e.ke].activeMonitors[e.id].ffmpeg}});
}
fatalError(e,'Process Unexpected Exit');
scanForOrphanedVideos(e,{
forceCheck: true,
checkMax: 2
})
s.onMonitorUnexpectedExitExtensions.forEach(function(extender){
extender(Object.assign(s.group[e.ke].rawMonitorConfigurations[e.id],{}),e)
})
}
}
s.group[e.ke].activeMonitors[e.id].spawn.on('end',s.group[e.ke].activeMonitors[e.id].spawn_exit)
s.group[e.ke].activeMonitors[e.id].spawn.on('exit',s.group[e.ke].activeMonitors[e.id].spawn_exit)
s.group[e.ke].activeMonitors[e.id].spawn.on('error',function(er){
s.userLog(e,{type:'Spawn Error',msg:er});fatalError(e,'Spawn Error')
})
s.userLog(e,{type:lang['Process Started'],msg:{cmd:s.group[e.ke].activeMonitors[e.id].ffmpeg}})
// if(s.isWin === false){
// var strippedHost = s.stripAuthFromHost(e)
// var sendProcessCpuUsage = function(){
// s.getMonitorCpuUsage(e,function(percent){
// s.group[e.ke].activeMonitors[e.id].currentCpuUsage = percent
// s.tx({
// f: 'camera_cpu_usage',
// ke: e.ke,
// id: e.id,
// percent: percent
// },'MON_STREAM_'+e.ke+e.id)
// })
// }
// clearInterval(s.group[e.ke].activeMonitors[e.id].getMonitorCpuUsage)
// s.group[e.ke].activeMonitors[e.id].getMonitorCpuUsage = setInterval(function(){
// if(e.details.skip_ping !== '1'){
// connectionTester.test(strippedHost,e.port,2000,function(err,response){
// if(response.success){
// sendProcessCpuUsage()
// }else{
// launchMonitorProcesses(e)
// }
// })
// }else{
// sendProcessCpuUsage()
// }
// },1000 * 60)
// }
}
return { return {
cameraDestroy: cameraDestroy, cameraDestroy: cameraDestroy,
createSnapshot: createSnapshot, createSnapshot: createSnapshot,
@ -369,5 +482,9 @@ module.exports = (s,config,lang) => {
spawnSubstreamProcess: spawnSubstreamProcess, spawnSubstreamProcess: spawnSubstreamProcess,
destroySubstreamProcess: destroySubstreamProcess, destroySubstreamProcess: destroySubstreamProcess,
attachStreamChannelHandlers: attachStreamChannelHandlers, attachStreamChannelHandlers: attachStreamChannelHandlers,
setActiveViewer: setActiveViewer,
getActiveViewerCount: getActiveViewerCount,
setTimedActiveViewerForHttp: setTimedActiveViewerForHttp,
attachMainProcessHandlers: attachMainProcessHandlers,
} }
} }

View File

@ -0,0 +1,412 @@
var fs = require('fs');
const {
template,
checkEmail,
} = require("./emailUtils.js")
module.exports = function (s, config, lang, getSnapshot) {
const { getEventBasedRecordingUponCompletion } = require('../events/utils.js')(s, config, lang);
const nodeMailer = require('nodemailer');
try {
const sendMessage = async function (sendBody, files, groupKey) {
const transporter = s.group[groupKey].emailClient;
if (!transporter) {
s.userLog(
{ ke: groupKey, mid: '$USER' },
{
type: lang.NotifyErrorText,
msg: {
msg: lang.AppNotEnabledText,
app: lang.Email
},
}
);
return;
}
try {
const appOptions = s.group[groupKey].emailClientOptions.transport;
const sendTo = appOptions.sendTo;
const sendData = {
from: `"shinobi.video" <${transportOptions.auth.user}>`,
to: sendTo,
subject: sendBody.subject,
html: sendBody.html,
};
if (files.length > 0) {
// sadly pushover allows only ONE single attachment
msg.file = {
name: files[0].name,
data: files[0].attachment,
};
}
transporter.sendMail(sendData, function (err, result) {
if (err) {
throw err;
}
s.userLog(result);
s.debugLog(result);
});
} catch (err) {
s.userLog(
{ ke: groupKey, mid: '$USER' },
{ type: lang.NotifyErrorText, msg: err }
);
}
};
const loadAppForUser = function (user) {
const userDetails = s.parseJSON(user.details);
const optionsHost = userDetails.emailClient_host
const optionsUser = userDetails.emailClient_user
const optionsSendTo = userDetails.emailClient_sendTo || ''
if (
!s.group[user.ke].emailClient &&
userDetails.emailClient === '1' &&
optionsHost &&
optionsUser &&
optionsSendTo
){
const optionsPass = userDetails.emailClient_pass || ''
const optionsSecure = userDetails.emailClient_secure === '1' ? true : false
const optionsPort = isNaN(userDetails.emailClient_port) ? (optionsSecure ? 465 : 587) : parseInt(userDetails.emailClient_port)
const clientOptions = {
host: optionsHost,
port: optionsPort,
secure: optionsSecure,
auth: {
user: optionsUser,
pass: optionsPass
}
}
s.group[user.ke].emailClientOptions = {
transport: clientOptions,
sendTo: optionsSendTo,
}
s.group[user.ke].emailClient = nodeMailer.createTransport(clientOptions)
}
};
const unloadAppForUser = function (user) {
if (
s.group[user.ke].emailClient &&
s.group[user.ke].emailClient.close
) {
s.group[user.ke].emailClient.close();
}
delete s.group[user.ke].emailClient;
delete s.group[user.ke].emailClientOptions;
};
const onTwoFactorAuthCodeNotificationForApp = function (r) {
// r = user
if (r.details.factor_emailClient === '1') {
sendMessage({
subject: r.lang['2-Factor Authentication'],
html: template.createFramework({
title: r.lang['2-Factor Authentication'],
subtitle: r.lang['Enter this code to proceed'],
body: '<b style="font-size: 20pt;">'+s.factorAuth[r.ke][r.uid].key+'</b><br><br>'+r.lang.FactorAuthText1,
}),
},[],r.ke);
}
};
const onEventTriggerForApp = async (d, filter) => {
const monitorConfig = s.group[d.ke].rawMonitorConfigurations[d.id];
// d = event object
if (
s.group[d.ke].emailClient &&
(filter.emailClient || monitorConfig.details.notify_emailClient === '1') &&
!s.group[d.ke].activeMonitors[d.id].detector_emailClient
) {
var detector_emailClient_timeout;
if (
!monitorConfig.details.detector_emailClient_timeout ||
monitorConfig.details.detector_emailClient_timeout === ''
) {
detector_emailClient_timeout = 1000 * 60 * 10;
} else {
detector_emailClient_timeout =
parseFloat(
monitorConfig.details.detector_emailClient_timeout
) *
1000 *
60;
}
s.group[d.ke].activeMonitors[d.id].detector_emailClient = setTimeout(function () {
s.group[d.ke].activeMonitors[d.id].detector_emailClient = null;
}, detector_emailClient_timeout);
// lock passed
const sendMail = function(files){
const infoRows = []
Object.keys(d.details).forEach(function(key){
var value = d.details[key]
var text = value
if(value instanceof Object){
text = JSON.stringify(value,null,3)
}
infoRows.push(template.createRow({
title: key,
text: text
}))
})
sendMessage({
subject: lang.Event+' - '+d.screenshotName,
html: template.createFramework({
title: lang.EventText1 + ' ' + d.currentTimestamp,
subtitle: lang.Event,
body: infoRows.join(''),
}),
},files || [],r.ke)
}
if(monitorConfig.details.detector_mail_send_video === '1'){
let videoPath = null
let videoName = null
const eventBasedRecording = await getEventBasedRecordingUponCompletion({
ke: d.ke,
mid: d.mid
})
if(eventBasedRecording.filePath){
videoPath = eventBasedRecording.filePath
videoName = eventBasedRecording.filename
}else{
const siftedVideoFileFromRam = await s.mergeDetectorBufferChunks(d)
videoPath = siftedVideoFileFromRam.filePath
videoName = siftedVideoFileFromRam.filename
}
if(videoPath){
fs.readFile(mergedFilepath,function(err,buffer){
if(buffer){
sendMail([
{
filename: videoName,
content: buffer
}
])
}
})
}
}
await getSnapshot(d,monitorConfig)
sendMail([
{
filename: d.screenshotName + '.jpg',
content: d.screenshotBuffer
}
])
}
};
const onEventTriggerBeforeFilterForApp = function (d, filter) {
filter.emailClient = false;
};
const onDetectorNoTriggerTimeoutForApp = function (e) {
//e = monitor object
var currentTime = new Date();
if (e.details.detector_notrigger_emailClient === '1') {
var html =
'*' +
lang.NoMotionEmailText2 +
' ' +
(e.details.detector_notrigger_timeout || 10) +
' ' +
lang.minutes +
'.*\n';
html +=
'**' + lang['Monitor Name'] + '** : ' + e.name + '\n';
html += '**' + lang['Monitor ID'] + '** : ' + e.id + '\n';
html += currentTime;
sendMessage({
subject: lang['"No Motion" Detector'],
html: template.createFramework({
title: lang['"No Motion" Detector'],
subtitle: 'Shinobi Event',
body: html,
}),
},[],e.ke);
}
};
const onMonitorUnexpectedExitForApp = (monitorConfig) => {
if (
monitorConfig.details.notify_emailClient === '1' &&
monitorConfig.details.notify_onUnexpectedExit === '1'
){
const ffmpegCommand = s.group[monitorConfig.ke].activeMonitors[monitorConfig.mid].ffmpeg
const subject = lang['Process Unexpected Exit'] + ' : ' + monitorConfig.name
const currentTime = new Date();
sendMessage({
subject: subject,
html: template.createFramework({
title: subject,
subtitle: lang['Process Crashed for Monitor'],
body: ffmpegCommand,
footerText: currentTime
}),
},[],monitorConfig.ke);
}
};
s.loadGroupAppExtender(loadAppForUser);
s.unloadGroupAppExtender(unloadAppForUser);
s.onTwoFactorAuthCodeNotification(onTwoFactorAuthCodeNotificationForApp);
s.onEventTrigger(onEventTriggerForApp);
s.onEventTriggerBeforeFilter(onEventTriggerBeforeFilterForApp);
s.onDetectorNoTriggerTimeout(onDetectorNoTriggerTimeoutForApp);
s.onMonitorUnexpectedExit(onMonitorUnexpectedExitForApp);
s.definitions['Monitor Settings'].blocks[
'Notifications'
].info[0].info.push({
name: 'detail=notify_emailClient',
field: lang.Email,
description: '',
default: '0',
example: '',
selector: 'h_det_emailClient',
fieldType: 'select',
possible: [
{
name: lang.No,
value: '0',
},
{
name: lang.Yes,
value: '1',
},
],
});
s.definitions['Monitor Settings'].blocks['Notifications'].info.push(
{
evaluation: "$user.details.use_emailClient !== '0'",
isFormGroupGroup: true,
name: lang.Email,
color: 'blue',
'section-class': 'h_det_emailClient_input h_det_emailClient_1',
info: [
{
name: 'detail=detector_emailClient_timeout',
field: `${lang['Allow Next Alert']} (${lang['on Event']})`,
default: '10',
},
],
}
);
s.definitions['Account Settings'].blocks[
'2-Factor Authentication'
].info.push({
name: 'detail=factor_emailClient',
field: lang.Email,
default: '1',
example: '',
fieldType: 'select',
possible: [
{
name: lang.No,
value: '0',
},
{
name: lang.Yes,
value: '1',
},
],
});
s.definitions['Account Settings'].blocks['Email'] = {
evaluation: "$user.details.use_emailClient !== '0'",
field: lang.Email,
color: 'blue',
info: [
{
name: 'detail=emailClient',
selector: 'u_emailClient',
field: lang.Enabled,
default: '0',
example: '',
fieldType: 'select',
possible: [
{
name: lang.No,
value: '0',
},
{
name: lang.Yes,
value: '1',
},
],
},
{
hidden: true,
field: lang.Host,
name: 'detail=emailClient_host',
example: 'smtp.gmail.com',
'form-group-class': 'u_emailClient_input u_emailClient_1',
},
{
hidden: true,
field: lang.Port,
name: 'detail=emailClient_port',
example: '587',
'form-group-class': 'u_emailClient_input u_emailClient_1',
},
{
name: 'detail=emailClient_secure',
field: lang.Secure,
default: '0',
example: '',
fieldType: 'select',
possible: [
{
name: lang.No,
value: '0',
},
{
name: lang.Yes,
value: '1',
},
],
},
{
hidden: true,
field: lang.Email,
name: 'detail=emailClient_user',
example: 'test@gmail.com',
'form-group-class': 'u_emailClient_input u_emailClient_1',
},
{
hidden: true,
field: lang.Password,
name: 'detail=emailClient_pass',
'form-group-class': 'u_emailClient_input u_emailClient_1',
},
{
hidden: true,
field: lang['Send to'],
name: 'detail=emailClient_sendTo',
'form-group-class': 'u_emailClient_input u_emailClient_1',
},
],
};
s.definitions["Event Filters"].blocks["Action for Selected"].info.push({
"name": "actions=emailClient",
"field": lang['Email'],
"fieldType": "select",
"form-group-class": "actions-row",
"default": "",
"example": "1",
"possible": [
{
"name": lang['Original Choice'],
"value": "",
"selected": true
},
{
"name": lang.Yes,
"value": "1",
}
]
})
} catch (err) {
console.log(err);
console.log('Could not engage Email notifications.');
}
};

View File

@ -17,6 +17,10 @@ const {
} = require('./onvifDeviceManager/utils.js') } = require('./onvifDeviceManager/utils.js')
module.exports = function(s,config,lang,app,io){ module.exports = function(s,config,lang,app,io){
async function getOnvifDevice(groupKey,monitorId){
const onvifDevice = s.group[groupKey].activeMonitors[monitorId].onvifConnection || (await s.createOnvifDevice({id: monitorId, ke: groupKey})).device
return onvifDevice
}
/** /**
* API : Get ONVIF Data from Camera * API : Get ONVIF Data from Camera
*/ */
@ -26,7 +30,7 @@ module.exports = function(s,config,lang,app,io){
try{ try{
const groupKey = req.params.ke const groupKey = req.params.ke
const monitorId = req.params.id const monitorId = req.params.id
const onvifDevice = s.group[groupKey].activeMonitors[monitorId].onvifConnection const onvifDevice = await getOnvifDevice(groupKey,monitorId)
const cameraInfo = await getUIFieldValues(onvifDevice) const cameraInfo = await getUIFieldValues(onvifDevice)
endData.onvifData = cameraInfo endData.onvifData = cameraInfo
}catch(err){ }catch(err){
@ -47,7 +51,7 @@ module.exports = function(s,config,lang,app,io){
try{ try{
const groupKey = req.params.ke const groupKey = req.params.ke
const monitorId = req.params.id const monitorId = req.params.id
const onvifDevice = s.group[groupKey].activeMonitors[monitorId].onvifConnection const onvifDevice = await getOnvifDevice(groupKey,monitorId)
const form = s.getPostData(req) const form = s.getPostData(req)
const videoToken = form.VideoConfiguration && form.VideoConfiguration.videoToken ? form.VideoConfiguration.videoToken : null const videoToken = form.VideoConfiguration && form.VideoConfiguration.videoToken ? form.VideoConfiguration.videoToken : null
if(form.DateandTime){ if(form.DateandTime){
@ -100,7 +104,7 @@ module.exports = function(s,config,lang,app,io){
try{ try{
const groupKey = req.params.ke const groupKey = req.params.ke
const monitorId = req.params.id const monitorId = req.params.id
const onvifDevice = s.group[groupKey].activeMonitors[monitorId].onvifConnection const onvifDevice = await getOnvifDevice(groupKey,monitorId)
const cameraInfo = await rebootCamera(onvifDevice) const cameraInfo = await rebootCamera(onvifDevice)
endData.onvifData = cameraInfo endData.onvifData = cameraInfo
}catch(err){ }catch(err){

View File

@ -149,6 +149,7 @@ const getDeviceInformation = async (onvifDevice,options) => {
response.ok = false response.ok = false
response.error = err.stack.toString().toString() response.error = err.stack.toString().toString()
s.debugLog(err) s.debugLog(err)
s.debugLog(onvifDevice)
} }
return response return response
} }

View File

@ -119,6 +119,9 @@ module.exports = (s,config,lang) => {
ProfileToken : device.current_profile.token, ProfileToken : device.current_profile.token,
Protocol : 'RTSP' Protocol : 'RTSP'
}) })
const snapUri = (await device.services.media.getSnapshotUri({
ProfileToken : device.current_profile.token,
})).GetSnapshotUriResponse.MediaUri.Uri
var cameraResponse = { var cameraResponse = {
ip: camera.ip, ip: camera.ip,
port: camera.port, port: camera.port,
@ -147,7 +150,7 @@ module.exports = (s,config,lang) => {
imageSnap = (await s.getSnapshotFromOnvif({ imageSnap = (await s.getSnapshotFromOnvif({
username: onvifUsername, username: onvifUsername,
password: onvifPassword, password: onvifPassword,
uri: cameraResponse.uri, uri: snapUri,
})).toString('base64'); })).toString('base64');
}catch(err){ }catch(err){
s.debugLog(err) s.debugLog(err)
@ -185,7 +188,7 @@ module.exports = (s,config,lang) => {
error: errorMessage error: errorMessage
}) })
} }
s.debugLog(err) if(config.debugLogVerbose)s.debugLog(err);
} }
}) })
return responseList return responseList

View File

@ -955,7 +955,22 @@ module.exports = function(s,config,lang,io){
s.deleteFileBinEntry(d.file) s.deleteFileBinEntry(d.file)
break; break;
case's.setDiskUsedForGroup': case's.setDiskUsedForGroup':
s.setDiskUsedForGroup(d.ke,d.size,d.target || undefined) function doOnMain(){
s.setDiskUsedForGroup(d.ke,d.size,d.target || undefined)
}
if(d.videoRow){
let storageIndex = s.getVideoStorageIndex(d.videoRow);
if(storageIndex){
s.setDiskUsedForGroupAddStorage(d.ke,{
size: d.size,
storageIndex: storageIndex
})
}else{
doOnMain()
}
}else{
doOnMain()
}
break; break;
case'start':case'end': case'start':case'end':
d.mid='_cron';s.userLog(d,{type:'cron',msg:d.msg}) d.mid='_cron';s.userLog(d,{type:'cron',msg:d.msg})

View File

@ -176,6 +176,7 @@ module.exports = function(s,config,lang,app,io){
endDate: req.query.end, endDate: req.query.end,
startOperator: req.query.startOperator, startOperator: req.query.startOperator,
endOperator: req.query.endOperator, endOperator: req.query.endOperator,
noLimit: req.query.noLimit,
limit: req.query.limit, limit: req.query.limit,
archived: req.query.archived, archived: req.query.archived,
rowType: 'frames', rowType: 'frames',
@ -241,9 +242,9 @@ module.exports = function(s,config,lang,app,io){
const frames = [] const frames = []
var n = 0 var n = 0
framesPosted.forEach((frame) => { framesPosted.forEach((frame) => {
var firstParam = ['ke','=',req.params.ke] var firstParam = [['ke','=',req.params.ke],['mid','=',req.params.id],['filename','=',frame.filename]]
if(n !== 0)firstParam = (['or']).concat(firstParam) if(n !== 0)firstParam[0] = (['or']).concat(firstParam[0])
frames.push(firstParam,['mid','=',req.params.id],['filename','=',frame.filename]) frames.push(...firstParam)
++n ++n
}) })
s.knexQuery({ s.knexQuery({
@ -252,6 +253,7 @@ module.exports = function(s,config,lang,app,io){
table: "Timelapse Frames", table: "Timelapse Frames",
where: frames where: frames
},(err,r) => { },(err,r) => {
s.debugLog("Timelapse Frames Building Video",r.length)
if(r.length === 0){ if(r.length === 0){
s.closeJsonResponse(res,{ s.closeJsonResponse(res,{
ok: false ok: false
@ -330,6 +332,7 @@ module.exports = function(s,config,lang,app,io){
groupKey: req.params.ke, groupKey: req.params.ke,
archived: req.query.archived, archived: req.query.archived,
filename: req.params.filename, filename: req.params.filename,
limit: 1,
rowType: 'frames', rowType: 'frames',
endIsStartTo: true endIsStartTo: true
},(response) => { },(response) => {

View File

@ -220,7 +220,7 @@ module.exports = function(s,config,lang){
filename: filename, filename: filename,
mid: e.id, mid: e.id,
ke: e.ke, ke: e.ke,
time: s.nameToTime(filename), time: new Date(s.nameToTime(filename)),
end: s.formattedTime(new Date,'YYYY-MM-DD HH:mm:ss') end: s.formattedTime(new Date,'YYYY-MM-DD HH:mm:ss')
},'GRP_'+e.ke); },'GRP_'+e.ke);
var storageIndex = s.getVideoStorageIndex(e) var storageIndex = s.getVideoStorageIndex(e)
@ -471,13 +471,13 @@ module.exports = function(s,config,lang){
file.pipe(res) file.pipe(res)
return file return file
} }
s.createVideoFromTimelapse = function(timelapseFrames,framesPerSecond,callback){ s.createVideoFromTimelapse = async function(timelapseFrames,framesPerSecond,callback){
framesPerSecond = parseInt(framesPerSecond) framesPerSecond = parseInt(framesPerSecond)
if(!framesPerSecond || isNaN(framesPerSecond))framesPerSecond = 2 if(!framesPerSecond || isNaN(framesPerSecond))framesPerSecond = 2
var frames = timelapseFrames.reverse() var frames = timelapseFrames.reverse()
var ke = frames[0].ke var ke = frames[0].ke
var mid = frames[0].mid var mid = frames[0].mid
var finalFileName = frames[0].filename.split('.')[0] + '_' + frames[frames.length - 1].filename.split('.')[0] + `-${framesPerSecond}fps` var finalFileName = `${s.md5(JSON.stringify(frames))}-${framesPerSecond}fps`
var concatFiles = [] var concatFiles = []
var createLocation var createLocation
frames.forEach(function(frame,frameNumber){ frames.forEach(function(frame,frameNumber){
@ -515,7 +515,7 @@ module.exports = function(s,config,lang){
} }
},4000) },4000)
}) })
videoBuildProcess.on('exit',function(data){ videoBuildProcess.on('close',function(data){
var timeNow = new Date() var timeNow = new Date()
var fileStats = fs.statSync(finalMp4OutputLocation) var fileStats = fs.statSync(finalMp4OutputLocation)
var details = {} var details = {}
@ -547,12 +547,11 @@ module.exports = function(s,config,lang){
if(!err)videoBuildProcess.stdin.write(buffer) if(!err)videoBuildProcess.stdin.write(buffer)
if(currentFile === concatFiles.length - 1){ if(currentFile === concatFiles.length - 1){
//is last //is last
}else{ }else{
setTimeout(function(){ setTimeout(async function(){
++currentFile ++currentFile
readFile() readFile()
},1/framesPerSecond) },10/framesPerSecond)
} }
}) })
} }
@ -565,13 +564,22 @@ module.exports = function(s,config,lang){
msg: lang['Started Building'] msg: lang['Started Building']
}) })
}else{ }else{
callback({ if(s.group[ke].activeMonitors[mid].buildingTimelapseVideo){
ok: false, callback({
fileExists: true, ok: false,
filename: finalFileName + '.mp4', fileExists: false,
fileLocation: finalMp4OutputLocation, fileLocation: finalMp4OutputLocation,
msg: lang['Already exists'] msg: lang.Building
}) })
}else{
callback({
ok: false,
fileExists: true,
filename: finalFileName + '.mp4',
fileLocation: finalMp4OutputLocation,
msg: lang['Already exists']
})
}
} }
}else{ }else{
callback({ callback({
@ -591,9 +599,11 @@ module.exports = function(s,config,lang){
} }
s.getVideoStorageIndex = function(video){ s.getVideoStorageIndex = function(video){
try{ try{
var details = s.parseJSON(video.details) || {} const monitorId = video.id || video.mid
var storageId = details.storageId const details = s.parseJSON(video.details) || {}
if(s.group[video.ke] && s.group[video.ke].activeMonitors[video.id] && s.group[video.ke].activeMonitors[video.id].addStorageId)storageId = s.group[video.ke].activeMonitors[video.id].addStorageId let storageId = details.storageId
const activeMonitor = s.group[video.ke] && s.group[video.ke].activeMonitors[monitorId] ? s.group[video.ke].activeMonitors[monitorId] : null;
if(activeMonitor && activeMonitor.addStorageId)storageId = activeMonitor.addStorageId;
if(storageId){ if(storageId){
return s.group[video.ke].addStorageUse[storageId] return s.group[video.ke].addStorageUse[storageId]
} }

View File

@ -96,6 +96,7 @@ module.exports = function(s,config,lang,app){
*/ */
app.all(config.webPaths.adminApiPrefix+':auth/accounts/:ke/delete', function (req,res){ app.all(config.webPaths.adminApiPrefix+':auth/accounts/:ke/delete', function (req,res){
s.auth(req.params,function(user){ s.auth(req.params,function(user){
const groupKey = req.params.ke;
var endData = { var endData = {
ok : false ok : false
} }
@ -106,47 +107,60 @@ module.exports = function(s,config,lang,app){
} }
var form = s.getPostData(req) || {} var form = s.getPostData(req) || {}
var uid = form.uid || s.getPostData(req,'uid',false) var uid = form.uid || s.getPostData(req,'uid',false)
var mail = form.mail || s.getPostData(req,'mail',false)
s.knexQuery({
action: "delete",
table: "Users",
where: {
ke: req.params.ke,
uid: uid,
mail: mail,
}
})
s.knexQuery({ s.knexQuery({
action: "select", action: "select",
columns: "*", columns: "*",
table: "API", table: "Users",
where: [ where: [
['ke','=',req.params.ke], ['ke','=',groupKey],
['uid','=',uid], ['uid','=',uid],
] ]
},function(err,rows){ },function(err,usersFound){
if(rows && rows[0]){ const theUserUpForDeletion = usersFound[0]
rows.forEach(function(row){ if(theUserUpForDeletion){
delete(s.api[row.code])
})
s.knexQuery({ s.knexQuery({
action: "delete", action: "delete",
table: "API", table: "Users",
where: { where: {
ke: req.params.ke, ke: groupKey,
uid: uid, uid: uid,
} }
}) })
s.knexQuery({
action: "select",
columns: "*",
table: "API",
where: [
['ke','=',groupKey],
['uid','=',uid],
]
},function(err,rows){
if(rows && rows[0]){
rows.forEach(function(row){
delete(s.api[row.code])
})
s.knexQuery({
action: "delete",
table: "API",
where: {
ke: groupKey,
uid: uid,
}
})
}
})
s.tx({
f: 'delete_sub_account',
ke: groupKey,
uid: uid,
mail: theUserUpForDeletion.mail
},'ADM_'+groupKey)
endData.ok = true
}else{
endData.msg = user.lang['User Not Found']
} }
s.closeJsonResponse(res,endData)
}) })
s.tx({
f: 'delete_sub_account',
ke: req.params.ke,
uid: uid,
mail: mail
},'ADM_'+req.params.ke)
endData.ok = true
s.closeJsonResponse(res,endData)
},res,req) },res,req)
}) })
/** /**

View File

@ -839,13 +839,20 @@ module.exports = function(s,config,lang,app,io){
}else{ }else{
const monitorConfig = s.group[groupKey].rawMonitorConfigurations[monitorId] const monitorConfig = s.group[groupKey].rawMonitorConfigurations[monitorId]
const activeMonitor = s.group[groupKey].activeMonitors[monitorId] const activeMonitor = s.group[groupKey].activeMonitors[monitorId]
if(!activeMonitor.subStreamProcess){ const substreamConfig = monitorConfig.details.substream
response.ok = true if(
activeMonitor.allowDestroySubstream = false; substreamConfig.output
spawnSubstreamProcess(monitorConfig) ){
if(!activeMonitor.subStreamProcess){
response.ok = true
activeMonitor.allowDestroySubstream = false;
spawnSubstreamProcess(monitorConfig)
}else{
activeMonitor.allowDestroySubstream = true
await destroySubstreamProcess(activeMonitor)
}
}else{ }else{
activeMonitor.allowDestroySubstream = true response.msg = lang['Invalid Settings']
await destroySubstreamProcess(activeMonitor)
} }
} }
s.closeJsonResponse(res,response); s.closeJsonResponse(res,response);
@ -942,6 +949,7 @@ module.exports = function(s,config,lang,app,io){
endTime: req.query.end, endTime: req.query.end,
startTimeOperator: req.query.startOperator, startTimeOperator: req.query.startOperator,
endTimeOperator: req.query.endOperator, endTimeOperator: req.query.endOperator,
noLimit: req.query.noLimit,
limit: req.query.limit, limit: req.query.limit,
archived: req.query.archived, archived: req.query.archived,
endIsStartTo: !!req.query.endIsStartTo, endIsStartTo: !!req.query.endIsStartTo,
@ -1014,6 +1022,7 @@ module.exports = function(s,config,lang,app,io){
endTime: req.query.end, endTime: req.query.end,
startTimeOperator: req.query.startOperator, startTimeOperator: req.query.startOperator,
endTimeOperator: req.query.endOperator, endTimeOperator: req.query.endOperator,
noLimit: req.query.noLimit,
limit: req.query.limit, limit: req.query.limit,
endIsStartTo: true, endIsStartTo: true,
parseRowDetails: true, parseRowDetails: true,

2209
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -35,13 +35,14 @@
"mp4frag": "^0.2.0", "mp4frag": "^0.2.0",
"mysql": "^2.18.1", "mysql": "^2.18.1",
"node-fetch": "3.0.0-beta.9", "node-fetch": "3.0.0-beta.9",
"node-pushover": "^1.0.0",
"node-ssh": "^11.1.1", "node-ssh": "^11.1.1",
"node-telegram-bot-api": "^0.52.0", "node-telegram-bot-api": "^0.52.0",
"nodemailer": "^6.4.11", "nodemailer": "^6.4.11",
"node-pushover": "^1.0.0", "pam-diff": "^1.1.0",
"pam-diff": "^1.0.0",
"path": "^0.12.7", "path": "^0.12.7",
"pipe2pam": "^0.6.2", "pipe2pam": "^0.6.2",
"pixel-change": "^1.1.0",
"request": "^2.88.0", "request": "^2.88.0",
"sat": "^0.7.1", "sat": "^0.7.1",
"shinobi-onvif": "0.1.9", "shinobi-onvif": "0.1.9",
@ -69,9 +70,9 @@
"node12" "node12"
], ],
"scripts": [ "scripts": [
"libs/cameraThread/detector.js", "libs/cameraThread/detector.js",
"libs/cameraThread/singleCamera.js", "libs/cameraThread/singleCamera.js",
"libs/cameraThread/snapshot.js" "libs/cameraThread/snapshot.js"
], ],
"assets": [ "assets": [
"definitions/**/*", "definitions/**/*",

View File

@ -179,3 +179,7 @@
#powerVideo .vis-item.vis-box { #powerVideo .vis-item.vis-box {
border-radius: 5px; border-radius: 5px;
} }
#powerVideo .vis-labelset .vis-label {
color: #fff;
}

View File

@ -30,6 +30,7 @@
#tab-timelapseViewer .frameIcons{ #tab-timelapseViewer .frameIcons{
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
max-height: 400px;
} }
#tab-timelapseViewer .frameIcons .frame{ #tab-timelapseViewer .frameIcons .frame{
background-size: cover; background-size: cover;

View File

@ -29,3 +29,40 @@
.tab-videoPlayer:hover .tab-videoPlayer-event-objects { .tab-videoPlayer:hover .tab-videoPlayer-event-objects {
display: none; display: none;
} }
/* video-time-strip */
.video-time-img {
background-color: #000;
background-size: contain;
background-position: center;
min-height: 400px;
background-repeat: no-repeat;
}
.video-time-img:not(.video-time-no-img) .card-body:hover {
background: rgba(0,0,0,0.9);
color: #fff;
}
.video-time-strip {
min-height: 30px;
position: relative;
}
.video-time-needle {
position: absolute;
border-left: 3px solid #1f80f9;
height: 100%;
transition: none;
top: 0;
}
.video-time-needle-event {
border-left: 3px solid #f9f21f;
}
.video-day-slice {
height: 100%;
}
.video-time-header {
position: absolute;
left: 0;
top: 0;
width: 100%;
background: rgba(0,0,0,0.6)
}

View File

@ -383,3 +383,36 @@ ul.squeeze {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
/* Shake Animation */
.animate-shake {
animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both;
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
perspective: 1000px;
}
.animate-shake-hover:hover {
animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both;
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
perspective: 1000px;
}
@keyframes shake {
10%, 90% {
transform: translate3d(-1px, 0, 0);
}
20%, 80% {
transform: translate3d(2px, 0, 0);
}
30%, 50%, 70% {
transform: translate3d(-4px, 0, 0);
}
40%, 60% {
transform: translate3d(4px, 0, 0);
}
}

View File

@ -48,7 +48,7 @@ $(document).ready(function(e){
detailsElement.val(JSON.stringify(details)) detailsElement.val(JSON.stringify(details))
} }
var getApiKeys = function(callback){ var getApiKeys = function(callback){
$.get(getApiPrefix('api') + '/list',function(data){ $.getJSON(getApiPrefix('api') + '/list',function(data){
callback(data.keys) callback(data.keys)
}) })
} }

View File

@ -25,7 +25,7 @@ $(document).ready(function(e){
var form = el.serializeObject() var form = el.serializeObject()
var flags = 'default' var flags = 'default'
var url = form.url.trim() var url = form.url.trim()
$.get(`${getApiPrefix()}/probe/${$user.ke}?url=${url}`,function(data){ $.getJSON(`${getApiPrefix()}/probe/${$user.ke}?url=${url}`,function(data){
if(data.ok === true){ if(data.ok === true){
var html var html
try{ try{

View File

@ -25,7 +25,7 @@ $(document).ready(function(){
} }
var getConfigurationsFromHub = function(rowLimit,skipOver,explore,searchQuery,sortBy,sortDirection,callback){ var getConfigurationsFromHub = function(rowLimit,skipOver,explore,searchQuery,sortBy,sortDirection,callback){
// $.get(,callback) // $.get(,callback)
$.get(`https://hub.shinobi.video/searchConfiguration?skipOver=${skipOver}&rowLimit=${rowLimit}&sortBy=${sortBy}&sortDirection=${sortDirection}${searchQuery ? `&text=${searchQuery}` : ''}`,function(data){ $.getJSON(`https://hub.shinobi.video/searchConfiguration?skipOver=${skipOver}&rowLimit=${rowLimit}&sortBy=${sortBy}&sortDirection=${sortDirection}${searchQuery ? `&text=${searchQuery}` : ''}`,function(data){
callback(data) callback(data)
// $.get(getApiPrefix() + `/getShinobiHubConfigurations/${$user.ke}/cam?rowLimit=${rowLimit}&skipOver=${skipOver}&explore=${explore ? explore : "0"}&search=${searchQuery}&sortDirection=${sortDirection}&sortBy=${sortBy}`,function(privateData){ // $.get(getApiPrefix() + `/getShinobiHubConfigurations/${$user.ke}/cam?rowLimit=${rowLimit}&skipOver=${skipOver}&explore=${explore ? explore : "0"}&search=${searchQuery}&sortDirection=${sortDirection}&sortBy=${sortBy}`,function(privateData){
// callback(data.concat(privateData || [])) // callback(data.concat(privateData || []))

View File

@ -71,6 +71,9 @@ function base64ArrayBuffer(arrayBuffer) {
return base64 return base64
} }
function getLocationPathName(){
return location.pathname.endsWith('/') ? location.pathname : location.pathname
}
function debugLog(...args){ function debugLog(...args){
console.log(...args) console.log(...args)
} }
@ -198,7 +201,7 @@ function liveStamp(){
} }
function loadMonitorsIntoMemory(callback){ function loadMonitorsIntoMemory(callback){
$.get(`${getApiPrefix(`monitor`)}`,function(data){ $.getJSON(`${getApiPrefix(`monitor`)}`,function(data){
$.each(data,function(n,monitor){ $.each(data,function(n,monitor){
monitor.details = safeJsonParse(monitor.details) monitor.details = safeJsonParse(monitor.details)
loadedMonitors[monitor.mid] = monitor loadedMonitors[monitor.mid] = monitor
@ -261,6 +264,10 @@ function blipTo(xPageValue,yPageValue){
function openTab(theTab,loadData,backAction,haltTrigger,type){ function openTab(theTab,loadData,backAction,haltTrigger,type){
loadData = loadData ? loadData : {} loadData = loadData ? loadData : {}
if(tabTree && tabTree.back && tabTree.back.name === theTab){
goBackOneTab()
return;
}
saveTabBlipPosition(activeTabName) saveTabBlipPosition(activeTabName)
var allTabs = $('.page-tab'); var allTabs = $('.page-tab');
allTabs.hide().removeClass('tab-active'); allTabs.hide().removeClass('tab-active');
@ -578,7 +585,7 @@ function diffObject(obj1, obj2) {
function getAllSectionsFromDefinition(definitionsBase){ function getAllSectionsFromDefinition(definitionsBase){
var sections = {} var sections = {}
var addSection = function(section,parentName){ var addSection = function(section,parentName){
sections[section.name] = { sections[section.id + section.name] = {
name: section.name, name: section.name,
id: section.id, id: section.id,
color: section.color, color: section.color,
@ -593,7 +600,7 @@ function getAllSectionsFromDefinition(definitionsBase){
} }
if(section.blocks){ if(section.blocks){
$.each(section.blocks,function(m,block){ $.each(section.blocks,function(m,block){
addSection(block) addSection(block,section.name)
}) })
} }
} }
@ -677,14 +684,28 @@ function drawMonitorListToSelector(jqTarget,selectFirst,showId){
.change() .change()
} }
} }
var logWriterIconIndicator = $('#side-menu-link-logViewer i')
var logWriterIconIndicatorShaking = false
var logWriterIconIndicatorTimeout = null
function shakeLogWriterIcon(){
if(logWriterIconIndicatorShaking)return;
logWriterIconIndicatorShaking = true;
logWriterIconIndicator.addClass('animate-shake')
logWriterIconIndicatorTimeout = setTimeout(function(){
logWriterIconIndicatorShaking = false;
logWriterIconIndicator.removeClass('animate-shake')
},3000)
}
var logWriterFloodTimeout = null var logWriterFloodTimeout = null
var logWriterFloodCounter = 0 var logWriterFloodCounter = 0
var logWriterFloodLock = null var logWriterFloodLock = null
function buildLogRow(v){ function buildLogRow(v){
var monitor = loadedMonitors[v.mid]
var humanMonitorName = monitor ? monitor.name + ` (${monitor.mid}) : ` : ''
var html = '' var html = ''
html += `<div class="card shadow-lg mb-3 px-0 btn-default search-row"> html += `<div class="log-item card shadow-lg mb-3 px-0 btn-default search-row">
<div class="card-header"> <div class="card-header">
<small class="${definitions.Theme.isDark ? 'text-white' : ''}">${v.info && v.info.type ? v.info.type : v.mid}</small> <small class="${definitions.Theme.isDark ? 'text-white' : ''}">${humanMonitorName}${v.info && v.info.type ? v.info.type : v.mid}</small>
</div> </div>
<div class="card-body"> <div class="card-body">
<div>${jsonToHtmlBlock(v.info.msg)}</div> <div>${jsonToHtmlBlock(v.info.msg)}</div>
@ -718,10 +739,12 @@ function logWriterDraw(id,data){
info: data.log, info: data.log,
time: data.time, time: data.time,
}) })
shakeLogWriterIcon()
$(elementTags).prepend(html).each(function(n,v){ $(elementTags).prepend(html).each(function(n,v){
var el = $(v); var el = $(v);
if(el.find('.log-item').length > 10){ var theRows = el.find('.log-item')
v.find('.log-item:last').remove() if(theRows.length > 10){
theRows.last().remove()
} }
}) })
} }
@ -853,6 +876,14 @@ $(document).ready(function(){
deleteTab(tabName) deleteTab(tabName)
return false; return false;
}) })
.on('click','.delete-tab-dynamic',function(e){
e.preventDefault()
e.stopPropagation()
var tabName = $(this).parents('.page-tab').attr('id').replace('tab-','')
goBackOneTab()
deleteTab(tabName)
return false;
})
.on('click','[page-open]',function(){ .on('click','[page-open]',function(){
var el = $(this) var el = $(this)
var pageChoice = el.attr('page-open') var pageChoice = el.attr('page-open')

View File

@ -145,9 +145,9 @@ $(document).ready(function(){
eventEndTime = formattedTimeForFilename(options.endDate,false) eventEndTime = formattedTimeForFilename(options.endDate,false)
requestQueries.push(`end=${eventEndTime}`) requestQueries.push(`end=${eventEndTime}`)
} }
$.get(`${getApiPrefix(`videos`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.concat([`limit=${limit}`]).join('&')}`,function(data){ $.getJSON(`${getApiPrefix(`videos`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.concat([`limit=${limit}`]).join('&')}`,function(data){
var videos = data.videos var videos = data.videos
$.get(`${getApiPrefix(`events`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.join('&')}`,function(eventData){ $.getJSON(`${getApiPrefix(`events`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.join('&')}`,function(eventData){
var newEventList = applyVideosToEventsList(videos,eventData) var newEventList = applyVideosToEventsList(videos,eventData)
$.each(newEventList,function(n,event){ $.each(newEventList,function(n,event){
loadedEventsInMemory[`${event.mid}${event.time}`] = event loadedEventsInMemory[`${event.mid}${event.time}`] = event

74
web/assets/js/bs5.help.js Normal file
View File

@ -0,0 +1,74 @@
$(document).ready(function(){
var helpWindow = $('#help_window')
var openMessage = null
function lessThanOneWeekAgo(date){
const WEEK = 1000 * 60 * 60 * 24 * 7;
const aWeekAgo = Date.now() - WEEK;
return date < aWeekAgo;
}
function showHelpNotice(){
var buttonHtml = ``
$.each([
{
icon: 'share-square-o',
color: 'default',
text: 'ShinobiShop Subscriptions',
href: 'https://licenses.shinobi.video/subscribe',
class: ''
},
{
icon: 'paypal',
color: 'success',
text: 'Donate by PayPal',
href: 'https://www.paypal.me/ShinobiCCTV',
class: ''
},
{
icon: 'bank',
color: 'default',
text: 'University of Zurich (UZH)',
href: 'https://www.media.uzh.ch/en/Press-Releases/2017/Generosity.html',
class: ''
},
{
icon: 'cube',
color: 'danger',
text: lang[`Don't Show for 1 Week`],
href: '#',
class: 'hide_donate',
},
],function(n,button){
buttonHtml += `<a style="margin-bottom:4px" ${ button.href ? `href="${button.href}"` : '' } target="_blank" class="d-flex flex-row btn btn-sm btn-block btn-${ button.color } ${ button.class }">
<div><i class="fa fa-${ button.icon }" aria-hidden="true"></i></div>
<div class="text-center flex-grow-1">${ button.text }</div>
</a>`
})
openMessage = new PNotify({
title: `It's a proven fact`,
text: `
<div class="mb-3">Generosity makes you a happier person, please consider supporting the development.</div>
<div class="mb-3">If you are already supporting the development, please contact us or use your provided license key and we can get this popup to go away for you <i class="fa fa-smile-o"></i> Cheers!</div>
${buttonHtml}`,
hide: false,
})
}
function dontShowForOneWeek(){
if(openMessage){
openMessage.remove()
}
dashboardOptions('subscription_checked',new Date());
}
if(!userHasSubscribed && !dashboardOptions().subscription_checked || lessThanOneWeekAgo(new Date(dashboardOptions().subscription_checked))){
setTimeout(function(){
showHelpNotice()
},1000 * 60 * 0.2)
}
$('body').on('click','.hide_donate',function(e){
e.preventDefault()
dontShowForOneWeek()
return false;
})
console.log('Please support the Shinobi developement.')
console.log('https://licenses.shinobi.video/subscribe')
})

View File

@ -159,6 +159,9 @@ function buildLiveGridBlock(monitor){
<div class="mdl-overlay-menu-backdrop hidden"> <div class="mdl-overlay-menu-backdrop hidden">
<ul class="mdl-overlay-menu list-group">` <ul class="mdl-overlay-menu list-group">`
var buttons = streamBlockInfo.links var buttons = streamBlockInfo.links
if(!monitor.details.control === '1'){
delete(buttons["Control"])
}
if(!permissionCheck('video_view',monitor.mid)){ if(!permissionCheck('video_view',monitor.mid)){
delete(buttons["Videos List"]) delete(buttons["Videos List"])
delete(buttons["Time-lapse"]) delete(buttons["Time-lapse"])
@ -273,7 +276,6 @@ function drawLiveGridBlock(monitorConfig,subStreamChannel){
setLiveGridOpenCount(1) setLiveGridOpenCount(1)
} }
initiateLiveGridPlayer(loadedMonitors[monitorId],subStreamChannel) initiateLiveGridPlayer(loadedMonitors[monitorId],subStreamChannel)
loadVideoMiniList(monitorId)
} }
function initiateLiveGridPlayer(monitor,subStreamChannel){ function initiateLiveGridPlayer(monitor,subStreamChannel){
var livePlayerElement = loadedLiveGrids[monitor.mid] var livePlayerElement = loadedLiveGrids[monitor.mid]
@ -525,15 +527,17 @@ function initiateLiveGridPlayer(monitor,subStreamChannel){
}) })
} }
//initiate signal check //initiate signal check
var signalCheckInterval = (isNaN(loadedMonitor.details.signal_check) ? 10 : parseFloat(loadedMonitor.details.signal_check)) * 1000 * 60 if(streamType !== 'useSubstream'){
if(signalCheckInterval > 0){ var signalCheckInterval = (isNaN(loadedMonitor.details.signal_check) ? 10 : parseFloat(loadedMonitor.details.signal_check)) * 1000 * 60
clearInterval(loadedPlayer.signal) if(signalCheckInterval > 0){
loadedPlayer.signal = setInterval(function(){ clearInterval(loadedPlayer.signal)
signalCheckLiveStream({ loadedPlayer.signal = setInterval(function(){
mid: monitorId, signalCheckLiveStream({
checkSpeed: 1000, mid: monitorId,
}) checkSpeed: 1000,
},signalCheckInterval); })
},signalCheckInterval);
}
} }
} }
function revokeVideoPlayerUrl(monitorId){ function revokeVideoPlayerUrl(monitorId){
@ -602,11 +606,7 @@ function closeAllLiveGridPlayers(rememberClose){
$.each(watchedOn,function(n,groupOfMons){ $.each(watchedOn,function(n,groupOfMons){
$.each(groupOfMons,function(monitorId,monitor){ $.each(groupOfMons,function(monitorId,monitor){
if(monitor === 1){ if(monitor === 1){
if(rememberClose){ mainSocket.f({f:'monitor',ff:'watch_off',id: monitorId})
mainSocket.f({f:'monitor',ff:'watch_off',id: monitorId})
}else{
closeLiveGridPlayer(monitorId,true)
}
} }
}) })
}) })
@ -817,15 +817,6 @@ $(document).ready(function(e){
.resize(function(){ .resize(function(){
resetAllLiveGridDimensionsInMemory() resetAllLiveGridDimensionsInMemory()
}) })
.on('click','.toggle-substream',function(){
var monitorId = $(this).parents('[data-mid]').attr('data-mid')
var monitor = loadedMonitors[monitorId]
if(monitor.subStreamToggleLock)return false;
monitor.subStreamToggleLock = true
$.get(getApiPrefix(`toggleSubstream`) + '/' + monitor.mid,function(data){
monitor.subStreamToggleLock = false
})
})
.on('click','.launch-live-grid-monitor',function(){ .on('click','.launch-live-grid-monitor',function(){
var monitorId = $(this).parents('[data-mid]').attr('data-mid') var monitorId = $(this).parents('[data-mid]').attr('data-mid')
// if(isMobile){ // if(isMobile){
@ -868,9 +859,11 @@ $(document).ready(function(e){
}) })
.on('click','.toggle-live-grid-monitor-logs',function(){ .on('click','.toggle-live-grid-monitor-logs',function(){
var monitorItem = $(this).parents('[data-mid]') var monitorItem = $(this).parents('[data-mid]')
var monitorId = monitorItem.attr('data-mid')
monitorItem.toggleClass('show_data') monitorItem.toggleClass('show_data')
var dataBlocks = monitorItem.find('.stream-block,.mdl-data_window') var dataBlocks = monitorItem.find('.stream-block,.mdl-data_window')
if(monitorItem.hasClass('show_data')){ if(monitorItem.hasClass('show_data')){
loadVideoMiniList(monitorId)
dataBlocks.addClass('col-md-6').removeClass('col-md-12') dataBlocks.addClass('col-md-6').removeClass('col-md-12')
}else{ }else{
dataBlocks.addClass('col-md-12').removeClass('col-md-6') dataBlocks.addClass('col-md-12').removeClass('col-md-6')
@ -897,6 +890,10 @@ $(document).ready(function(e){
var monitorId = $(this).parents('[data-mid]').attr('data-mid') var monitorId = $(this).parents('[data-mid]').attr('data-mid')
popOutMonitor(monitorId) popOutMonitor(monitorId)
}) })
.on('click','.toggle-monitor-substream',function(){
var monitorId = $(this).parents('[data-mid]').attr('data-mid')
toggleSubStream(monitorId)
})
.on('click','.run-live-grid-monitor-ptz',function(){ .on('click','.run-live-grid-monitor-ptz',function(){
var el = $(this) var el = $(this)
var monitorId = el.parents('[data-mid]').attr('data-mid') var monitorId = el.parents('[data-mid]').attr('data-mid')
@ -925,6 +922,9 @@ $(document).ready(function(e){
ff: 'watch_off', ff: 'watch_off',
id: monitor.mid id: monitor.mid
}) })
setTimeout(function(){
saveLiveGridBlockOpenState(monitorId,$user.ke,0)
},1000)
}) })
}) })
liveGrid liveGrid
@ -990,9 +990,17 @@ $(document).ready(function(e){
break; break;
case'monitor_watch_on': case'monitor_watch_on':
var monitorId = d.mid || d.id var monitorId = d.mid || d.id
var loadedMonitor = loadedMonitors[monitorId]
var subStreamChannel = d.subStreamChannel var subStreamChannel = d.subStreamChannel
drawLiveGridBlock(loadedMonitors[monitorId],subStreamChannel) if(!loadedMonitor.subStreamChannel && loadedMonitor.details.stream_type === 'useSubstream'){
saveLiveGridBlockOpenState(monitorId,$user.ke,1) toggleSubStream(monitorId,function(){
drawLiveGridBlock(loadedMonitors[monitorId],subStreamChannel)
saveLiveGridBlockOpenState(monitorId,$user.ke,1)
})
}else{
drawLiveGridBlock(loadedMonitors[monitorId],subStreamChannel)
saveLiveGridBlockOpenState(monitorId,$user.ke,1)
}
break; break;
case'mode_jpeg_off': case'mode_jpeg_off':
window.jpegModeOn = false window.jpegModeOn = false

View File

@ -205,7 +205,7 @@ function initiateLivePlayer(monitor){
function createSteamNow(){ function createSteamNow(){
clearTimeout(loadedPlayer.m3uCheck) clearTimeout(loadedPlayer.m3uCheck)
var url = getApiPrefix(`hls`) + '/' + monitor.mid + '/s.m3u8' var url = getApiPrefix(`hls`) + '/' + monitor.mid + '/s.m3u8'
$.get(url,function(m3u){ $.getJSON(url,function(m3u){
if(m3u == 'File Not Found'){ if(m3u == 'File Not Found'){
loadedPlayer.m3uCheck = setTimeout(function(){ loadedPlayer.m3uCheck = setTimeout(function(){
createSteamNow() createSteamNow()

View File

@ -37,7 +37,7 @@ $(document).ready(function(e){
selectedLogType = selectedLogType === 'all' ? '' : selectedLogType selectedLogType = selectedLogType === 'all' ? '' : selectedLogType
var currentDateRange = dateRangeSelector.data('daterangepicker'); var currentDateRange = dateRangeSelector.data('daterangepicker');
var apiEndpoint = getApiPrefix(`logs`) + '/' + selectedLogType + '?start=' + formattedTimeForFilename(currentDateRange.startDate) + '&end=' + formattedTimeForFilename(currentDateRange.endDate) var apiEndpoint = getApiPrefix(`logs`) + '/' + selectedLogType + '?start=' + formattedTimeForFilename(currentDateRange.startDate) + '&end=' + formattedTimeForFilename(currentDateRange.endDate)
$.get(apiEndpoint,function(rows){ $.getJSON(apiEndpoint,function(rows){
logViewerDataInMemory = { logViewerDataInMemory = {
startDate: currentDateRange.startDate, startDate: currentDateRange.startDate,
endDate: currentDateRange.endDate, endDate: currentDateRange.endDate,

View File

@ -214,7 +214,43 @@ function generateDefaultMonitorSettings(){
"detector_cascades": "", "detector_cascades": "",
"stream_channels": "", "stream_channels": "",
"input_maps": "", "input_maps": "",
"input_map_choices": "" "input_map_choices": "",
"substream": {
"input": {
"type": "h264",
"fulladdress": "",
"sfps": "",
"aduration": "",
"probesize": "",
"stream_loop": null,
"rtsp_transport": "",
"accelerator": "0",
"hwaccel": null,
"hwaccel_vcodec": "",
"hwaccel_device": "",
"cust_input": ""
},
"output": {
"stream_type": "hls",
"rtmp_server_url": "",
"rtmp_stream_key": "",
"stream_mjpeg_clients": "",
"stream_vcodec": "copy",
"stream_acodec": "no",
"hls_time": "",
"hls_list_size": "",
"preset_stream": "",
"stream_quality": "",
"stream_v_br": "",
"stream_a_br": "",
"stream_fps": "",
"stream_scale_x": "640",
"stream_scale_y": "480",
"stream_rotate": null,
"svf": "",
"cust_stream": ""
}
}
}, },
"shto": "[]", "shto": "[]",
"shfr": "[]" "shfr": "[]"
@ -372,6 +408,7 @@ window.getMonitorEditFormFields = function(){
monitorConfig.details = safeJsonParse(monitorConfig.details) monitorConfig.details = safeJsonParse(monitorConfig.details)
monitorConfig.details.substream = getSubStreamChannelFields() monitorConfig.details.substream = getSubStreamChannelFields()
monitorConfig.details.groups = getMonitorGroupsSelected() monitorConfig.details.groups = getMonitorGroupsSelected()
monitorConfig.details.input_map_choices = monitorSectionInputMapsave()
// TODO : Input Maps and Stream Channels (does old way at the moment) // TODO : Input Maps and Stream Channels (does old way at the moment)
@ -434,20 +471,24 @@ function drawStreamChannelHtml(options){
monitorStreamChannels.find('.stream-channel').last().find('[channel-detail="stream_vcodec"]').change() monitorStreamChannels.find('.stream-channel').last().find('[channel-detail="stream_vcodec"]').change()
return tempID; return tempID;
} }
function replaceMap(string,mapNumber){
var newString = string.split(':')
newString[0] = `${mapNumber}`
return newString.join(':')
}
function replaceMapInName(string,mapNumber){
var newString = string.split('(')
newString[1] = replaceMap(newString[1],mapNumber)
var lastIndex = newString.length - 1
if(!newString[lastIndex].endsWith(')')){
newString[lastIndex] = newString + ')'
}
return newString.join('(')
}
function buildMapSelectorOptionsBasedOnAddedMaps(){ function buildMapSelectorOptionsBasedOnAddedMaps(){
var baseOptionSet = definitions['Monitor Settings'].blocks.Input.info.find((item) => {return item.name === 'detail=primary_input'}).possible var baseOptionSet = definitions['Monitor Settings'].blocks.Input.info.find((item) => {return item.name === 'detail=primary_input'}).possible
var newOptGroup = [baseOptionSet] var newOptGroup = [baseOptionSet]
var addedInputMaps = monitorEditorWindow.find('.input-map') var addedInputMaps = monitorEditorWindow.find('.input-map')
function replaceMap(string,mapNumber){
var newString = string.split(':')
newString[0] = `${mapNumber}`
return newString.join(':')
}
function replaceMapInName(string,mapNumber){
var newString = string.split('(')
newString[1] = replaceMap(newString[1],mapNumber)
return newString.join('(')
}
$.each(addedInputMaps,function(n){ $.each(addedInputMaps,function(n){
var mapNumber = n + 1 var mapNumber = n + 1
var newOptionSet = [] var newOptionSet = []
@ -464,7 +505,7 @@ function buildMapSelectorOptionsBasedOnAddedMaps(){
function drawInputMapSelectorHtml(options,parent){ function drawInputMapSelectorHtml(options,parent){
if(!options.map)options.map = ''; if(!options.map)options.map = '';
var availableInputMapSelections = buildMapSelectorOptionsBasedOnAddedMaps() var availableInputMapSelections = buildMapSelectorOptionsBasedOnAddedMaps()
var html = `<div class="form-group map-row d-flex flex-row"> var html = `<div class="map-row form-group map-row d-flex flex-row">
<div class="flex-grow-1"> <div class="flex-grow-1">
<select class="form-control form-control-sm" map-input="map">` <select class="form-control form-control-sm" map-input="map">`
$.each(availableInputMapSelections,function(n,optgroup){ $.each(availableInputMapSelections,function(n,optgroup){
@ -473,6 +514,7 @@ function drawInputMapSelectorHtml(options,parent){
html += createOptionHtml({ html += createOptionHtml({
label: option.name, label: option.name,
value: option.value, value: option.value,
selected: option.value === options.map,
}) })
}) })
html += `</optgroup>` html += `</optgroup>`
@ -499,12 +541,7 @@ function importIntoMonitorEditor(options){
//get maps //get maps
monitorSectionInputMaps.empty() monitorSectionInputMaps.empty()
if(monitorDetails.input_maps && monitorDetails.input_maps !== ''){ if(monitorDetails.input_maps && monitorDetails.input_maps !== ''){
var input_maps var input_maps = safeJsonParse(monitorDetails.input_maps)
try{
input_maps = safeJsonParse(monitorDetails.input_maps)
}catch(er){
input_maps = monitorDetails.input_maps;
}
if(input_maps.length > 0){ if(input_maps.length > 0){
showInputMappingFields() showInputMappingFields()
$.each(input_maps,function(n,v){ $.each(input_maps,function(n,v){
@ -737,7 +774,7 @@ var mapPlacementInit = function(){
}) })
} }
var monitorSectionInputMapsave = function(){ var monitorSectionInputMapsave = function(){
var mapContainers = $('[input-mapping]'); var mapContainers = monitorEditorWindow.find('[input-mapping]');
var stringForSave = {} var stringForSave = {}
mapContainers.each(function(q,t){ mapContainers.each(function(q,t){
var mapRowElement = $(t).find('.map-row'); var mapRowElement = $(t).find('.map-row');
@ -751,7 +788,7 @@ var monitorSectionInputMapsave = function(){
}); });
stringForSave[$(t).attr('input-mapping')] = mapRow; stringForSave[$(t).attr('input-mapping')] = mapRow;
}); });
monitorEditorWindow.find('[detail="input_map_choices"]').val(JSON.stringify(stringForSave)).change(); return stringForSave
} }
monitorSectionInputMaps.on('click','.delete',function(){ monitorSectionInputMaps.on('click','.delete',function(){
$(this).parents('.input-map').remove() $(this).parents('.input-map').remove()
@ -779,14 +816,9 @@ monitorEditorWindow.on('change','[map-detail]',function(){
}) })
monitorEditorWindow.on('click','[input-mapping] .add_map_row',function(){ monitorEditorWindow.on('click','[input-mapping] .add_map_row',function(){
drawInputMapSelectorHtml({},$(this).parents('[input-mapping]').find('.choices')) drawInputMapSelectorHtml({},$(this).parents('[input-mapping]').find('.choices'))
monitorSectionInputMapsave()
}) })
monitorEditorWindow.on('click','[input-mapping] .delete_map_row',function(){ monitorEditorWindow.on('click','[input-mapping] .delete_map_row',function(){
$(this).parents('.map-row').remove() $(this).parents('.map-row').remove()
monitorSectionInputMapsave()
})
monitorEditorWindow.on('change','[map-input]',function(){
monitorSectionInputMapsave()
}) })
////////////////// //////////////////
//Stream Channels //Stream Channels
@ -808,7 +840,6 @@ var channelPlacementInit = function(){
_this.attr('stream-channel',n) _this.attr('stream-channel',n)
_this.find('.place').text(n) _this.find('.place').text(n)
_this.find('[input-mapping]').attr('input-mapping','stream_channel-'+n) _this.find('[input-mapping]').attr('input-mapping','stream_channel-'+n)
monitorSectionInputMapsave()
}) })
} }
var getSubStreamChannelFields = function(){ var getSubStreamChannelFields = function(){
@ -1064,7 +1095,7 @@ editorForm.find('[name="type"]').change(function(e){
} }
// presets // presets
var loadPresets = function(callback){ var loadPresets = function(callback){
$.get(getApiPrefix() + '/monitorStates/' + $user.ke,function(d){ $.getJSON(getApiPrefix() + '/monitorStates/' + $user.ke,function(d){
var presets = d.presets var presets = d.presets
loadedPresets = {} loadedPresets = {}
$.each(presets,function(n,preset){ $.each(presets,function(n,preset){
@ -1461,5 +1492,17 @@ editorForm.find('[name="type"]').change(function(e){
break; break;
} }
}) })
function checkToOpenSideMenu(){
if(isSideBarMenuCollapsed()){
sideMenuCollapsePoint.collapse('show')
}
}
addOnTabOpen('monitorSettings', checkToOpenSideMenu)
addOnTabReopen('monitorSettings', checkToOpenSideMenu)
addOnTabAway('monitorSettings', function(){
if(isSideBarMenuCollapsed()){
sideMenuCollapsePoint.collapse('hide')
}
})
window.generateDefaultMonitorSettings = generateDefaultMonitorSettings window.generateDefaultMonitorSettings = generateDefaultMonitorSettings
}) })

View File

@ -5,7 +5,7 @@ $(document).ready(function(){
var monitorStatesMonitors = $('#monitorStatesMonitors') var monitorStatesMonitors = $('#monitorStatesMonitors')
var theForm = monitorStatesPage.find('form') var theForm = monitorStatesPage.find('form')
function loadMonitorStatesPresets(callback){ function loadMonitorStatesPresets(callback){
$.get(getApiPrefix(`monitorStates`),function(d){ $.getJSON(getApiPrefix(`monitorStates`),function(d){
var html = '' var html = ''
$.each(d.presets,function(n,v){ $.each(d.presets,function(n,v){
monitorStatesLoaded[v.name] = v monitorStatesLoaded[v.name] = v

View File

@ -131,7 +131,7 @@ $(document).ready(function(){
} }
} }
function drawMonitorsListApiKeyList(){ function drawMonitorsListApiKeyList(){
$.get(getApiPrefix(`api`) + '/list',function(d){ $.getJSON(getApiPrefix(`api`) + '/list',function(d){
var html = '' var html = ''
$.each(d.keys || [],function(n,key){ $.each(d.keys || [],function(n,key){
console.log(key) console.log(key)
@ -169,7 +169,7 @@ $(document).ready(function(){
var el = thisEl.parents('[data-mid]') var el = thisEl.parents('[data-mid]')
var monitorId = el.attr('data-mid') var monitorId = el.attr('data-mid')
var mode = thisEl.attr('set-mode') var mode = thisEl.attr('set-mode')
$.get(`${getApiPrefix('monitor')}/${monitorId}/${mode}`,function(data){ $.getJSON(`${getApiPrefix('monitor')}/${monitorId}/${mode}`,function(data){
console.log(data) console.log(data)
}) })
}) })

View File

@ -149,7 +149,7 @@ function getVideoSnapshot(videoElement,cb){
function runPtzCommand(monitorId,switchChosen){ function runPtzCommand(monitorId,switchChosen){
switch(switchChosen){ switch(switchChosen){
case'setHome': case'setHome':
$.get(getApiPrefix(`control`) + '/' + monitorId + '/setHome',function(data){ $.getJSON(getApiPrefix(`control`) + '/' + monitorId + '/setHome',function(data){
console.log(data) console.log(data)
}) })
break; break;
@ -170,7 +170,26 @@ function runTestDetectionTrigger(monitorId,callback){
if(callback)callback() if(callback)callback()
}) })
} }
function toggleSubStream(monitorId,callback){
var monitor = loadedMonitors[monitorId]
var substreamConfig = monitor.details.substream
var isSubStreamConfigured = !!substreamConfig.output;
if(!isSubStreamConfigured){
new PNotify({
type: 'warning',
title: lang['Invalid Settings'],
text: lang.SubstreamNotConfigured,
});
return;
}
if(monitor.subStreamToggleLock)return false;
monitor.subStreamToggleLock = true
$.getJSON(getApiPrefix() + '/toggleSubstream/'+$user.ke+'/'+monitorId,function(d){
monitor.subStreamToggleLock = false
debugLog(d)
if(callback)callback()
})
}
function playAudioAlert(){ function playAudioAlert(){
var fileName = $user.details.audio_alert var fileName = $user.details.audio_alert
if(fileName && window.soundAlarmed !== true){ if(fileName && window.soundAlarmed !== true){
@ -195,7 +214,7 @@ function playAudioAlert(){
function buildStreamUrl(monitorId){ function buildStreamUrl(monitorId){
var monitor = loadedMonitors[monitorId] var monitor = loadedMonitors[monitorId]
var streamURL var streamURL = ''
var streamType = safeJsonParse(monitor.details).stream_type var streamType = safeJsonParse(monitor.details).stream_type
switch(streamType){ switch(streamType){
case'jpeg': case'jpeg':
@ -216,6 +235,9 @@ function buildStreamUrl(monitorId){
case'b64': case'b64':
streamURL = 'Websocket' streamURL = 'Websocket'
break; break;
case'useSubstream':
streamURL = lang['Use Substream']
break;
} }
if(!streamURL){ if(!streamURL){
$.each(onBuildStreamUrlExtensions,function(n,extender){ $.each(onBuildStreamUrlExtensions,function(n,extender){
@ -285,7 +307,7 @@ function importM3u8Playlist(textData){
}) })
$.each(parsedList,function(name,url){ $.each(parsedList,function(name,url){
var link = getUrlPieces(url) var link = getUrlPieces(url)
var newMon = $.aM.generateDefaultMonitorSettings() var newMon = generateDefaultMonitorSettings()
newMon.details = JSON.parse(newMon.details) newMon.details = JSON.parse(newMon.details)
newMon.mid = 'HLS' + name.toLowerCase() newMon.mid = 'HLS' + name.toLowerCase()
newMon.name = name newMon.name = name
@ -388,7 +410,7 @@ function deleteMonitors(monitorsSelected,afterDelete){
class:'btn-danger', class:'btn-danger',
callback:function(){ callback:function(){
$.each(monitorsSelected,function(n,monitor){ $.each(monitorsSelected,function(n,monitor){
$.get(`${getApiPrefix(`configureMonitor`)}/${monitor.mid}/delete`,function(data){ $.getJSON(`${getApiPrefix(`configureMonitor`)}/${monitor.mid}/delete`,function(data){
console.log(data) console.log(data)
if(monitorsSelected.length === n + 1){ if(monitorsSelected.length === n + 1){
//last //last
@ -403,7 +425,7 @@ function deleteMonitors(monitorsSelected,afterDelete){
class:'btn-danger', class:'btn-danger',
callback:function(){ callback:function(){
$.each(monitorsSelected,function(n,monitor){ $.each(monitorsSelected,function(n,monitor){
$.get(`${getApiPrefix(`configureMonitor`)}/${monitor.mid}/delete?deleteFiles=true`,function(data){ $.getJSON(`${getApiPrefix(`configureMonitor`)}/${monitor.mid}/delete?deleteFiles=true`,function(data){
console.log(data) console.log(data)
if(monitorsSelected.length === n + 1){ if(monitorsSelected.length === n + 1){
//last //last

View File

@ -0,0 +1,310 @@
$(document).ready(function(){
var selectedMonitorId
var loadedVideoEncoders = {}
var blockWindow = $('#tab-onvifDeviceManager')
var blockWindowInfo = $('#onvifDeviceManagerInfo')
var blockForm = blockWindow.find('form')
var monitorsList = blockWindow.find('.monitors_list')
var dateRangePicker = blockForm.find('[name="utcDateTime"]')
var convertFormFieldNameToObjectKeys = function(formFields){
var newObject = {}
$.each(formFields,function(key,value){
var keyPieces = key.split(':')
var parent = null
$.each(keyPieces,function(n,piece){
if(!parent){
parent = newObject
}
if(keyPieces[n + 1]){
if(!parent[piece])parent[piece] = {}
parent = parent[piece]
}else{
parent[piece] = value
}
})
})
return newObject
}
var converObjectKeysToFormFieldName = (object,parentKey) => {
parentKey = parentKey ? parentKey : ''
var theList = {}
Object.keys(object).forEach((key) => {
var value = object[key]
var newKey = parentKey ? parentKey + ':' + key : key
if(typeof value === 'string'){
theList[newKey] = value
}else if(value instanceof Object || value instanceof Array){
theList = Object.assign(theList,converObjectKeysToFormFieldName(value,newKey))
}
})
return theList
}
var setIntegerGuider = function(fieldName,theRange){
blockForm.find(`[${fieldName}]`)
.attr('min',theRange.Min)
.attr('max',theRange.Max)
.attr('placeholder',`Mininum : ${theRange.Min}, Maximum: ${theRange.Max}`);
}
var setGuidersInFormFields = function(onvifData){
if(onvifData.videoEncoderOptions){
var encoderOptions = onvifData.videoEncoderOptions
//Encoding
var hasH264 = !!encoderOptions.H264;
var hasH265 = !!encoderOptions.H265;
// var hasJPEG = !!encoderOptions.JPEG;
var availableEncoders = []
if(hasH264)availableEncoders.push('H264')
if(hasH265)availableEncoders.push('H265')
// if(hasJPEG)availableEncoders.push('JPEG')
var html = ``
$.each(availableEncoders,function(n,encoder){
html += `<option value="${encoder}">${encoder}</option>`
})
blockForm.find('[name="Encoding"]').html(html)
//Resolutions
console.log(encoderOptions)
if(encoderOptions.H264){
var html = ``
$.each(encoderOptions.H264.ResolutionsAvailable,function(n,resolution){
html += `<option value="${resolution.Width}x${resolution.Height}">${resolution.Width}x${resolution.Height}</option>`
})
blockForm.find('[detail="Resolution"]').html(html)
//Profiles Supported
var html = ``
var profilesSupported = encoderOptions.H264.H264ProfilesSupported
profilesSupported = typeof profilesSupported === 'string' ? [profilesSupported] : profilesSupported
$.each(profilesSupported,function(n,profile){
html += `<option value="${profile}">${profile}</option>`
})
blockForm.find('[name="H264:H264Profile"]').html(html)
//GOV Length, Frame Rate, Encoding Interval Range
setIntegerGuider('name="H264:GovLength"',encoderOptions.H264.GovLengthRange)
setIntegerGuider('name="RateControl:FrameRateLimit"',encoderOptions.H264.FrameRateRange)
setIntegerGuider('name="RateControl:EncodingInterval"',encoderOptions.H264.EncodingIntervalRange)
}
setIntegerGuider('name="Quality"',encoderOptions.QualityRange)
}
if(onvifData.videoEncoders){
loadedVideoEncoders = {}
var html = ``
onvifData.videoEncoders.forEach((encoder) => {
html += `<option value="${encoder.$.token}">${encoder.Name}</option>`
loadedVideoEncoders[encoder.$.token] = encoder
})
blockForm.find('[name=videoToken]').html(html)
}
}
var writeOnvifDataToFormFields = function(onvifData){
var formFields = {}
console.log('write fields ',onvifData)
if(onvifData.date){
var utcDatePieces = onvifData.date.UTCDateTime
var dateString = `${utcDatePieces.Date.Year}-${utcDatePieces.Date.Month}-${utcDatePieces.Date.Day} ${utcDatePieces.Time.Hour}:${utcDatePieces.Time.Minute}:${utcDatePieces.Time.Second} UTC`
var parsedDate = new Date(dateString);
console.log(dateString,parsedDate)
formFields["utcDateTime"] = parsedDate
formFields["dateTimeType"] = onvifData.date.DateTimeType
formFields["daylightSavings"] = onvifData.date.DaylightSavings
formFields["timezone"] = onvifData.date.TimeZone.TZ
}
if(onvifData.networkInterface){
var eth0 = onvifData.networkInterface[0] || onvifData.networkInterface
var ipConfig = eth0.IPv4.Config
var linkLocal = ipConfig.LinkLocal || ipConfig.FromDHCP
var ipv4 = ipConfig.DHCP === 'true' ? linkLocal.Address : ipConfig.Manual.Address || linkLocal.Address
formFields["setNetworkInterface:ipv4"] = ipv4
}
if(onvifData.gateway){
formFields["setGateway:ipv4"] = onvifData.gateway
}
if(onvifData.hostname){
formFields["setHostname:name"] = onvifData.hostname
}
if(onvifData.dns && onvifData.dns.DNSManual){
var dnsList = onvifData.dns.DNSManual
if(dnsList.IPv4Address){
dnsList = dnsList.IPv4Address
}else if(dnsList[0]){
dnsList = dnsList.map((item) => {
return item.IPv4Address
}).join(',')
}else{
dnsList = ''
}
formFields["setDNS:dns"] = dnsList
}
if(onvifData.ntp && onvifData.ntp.NTPManual){
var ntpIp = onvifData.ntp.NTPManual.IPv4Address
formFields["setNTP:ipv4"] = ntpIp
}
if(onvifData.protocols){
onvifData.protocols.forEach((protocol) => {
//RTSP, HTTP
formFields[`setProtocols:${protocol.Name}`] = protocol.Port
})
}
if(onvifData.videoEncoders){
setFieldsFromOnvifKeys(onvifData.videoEncoders[0])
}
if(onvifData.imagingSettings && onvifData.imagingSettings.ok !== false){
$('#Imaging').find('.form-group').hide()
setFieldsFromOnvifKeys(onvifData.imagingSettings)
$.each(onvifData.imagingSettings,function(key){
$('#Imaging').find(`[name="${key}"]`).parents('.form-group').show()
})
$('#Imaging').show()
}else{
$('#Imaging').hide()
}
Object.keys(formFields).forEach((key) => {
var value = formFields[key]
blockForm.find(`[name="${key}"]`).val(value)
})
if(onvifData.date)dateRangePicker.data('daterangepicker').setStartDate(dateString)
}
var setFieldsFromOnvifKeys = function(encoder){
var formFields = converObjectKeysToFormFieldName(encoder)
Object.keys(formFields).forEach((key) => {
var value = formFields[key]
blockForm.find(`[name="${key}"]`).val(value).parents('.form-group')
})
}
var rebootCamera = function(monitorId){
$.confirm.create({
title: lang['Reboot Camera'],
body: lang.noUndoForAction,
clickOptions: {
title: lang['Reboot'],
class: 'btn-warning'
},
clickCallback: function(){
$.get(getApiPrefix('onvifDeviceManager') + '/' + monitorId + '/reboot',function(response){
new PNotify({
title: lang.rebootingCamera,
text: lang['Please Wait...'],
type: 'warning'
})
})
}
})
}
var getUIFieldValuesFromCamera = function(monitorId){
$.get(getApiPrefix('onvifDeviceManager') + '/' + monitorId,function(response){
var onvifData = response.onvifData
if(onvifData && onvifData.ok === true){
blockWindowInfo.html(JSON.stringify(onvifData,null,3))
setGuidersInFormFields(onvifData)
writeOnvifDataToFormFields(onvifData)
}else{
new PNotify({
title: lang.ONVIFEventsNotAvailable,
text: lang.ONVIFEventsNotAvailableText1,
type: 'warning'
})
}
})
}
var getUIFieldValuesFromForm = function(){
var newObject = {}
var formOptions = blockForm.serializeObject()
$.each(formOptions,function(key,value){
var enclosingObject = blockForm.find(`[name="${key}"]`).parents('.form-group-group').attr("id")
if(key === 'utcDateTime'){
//dateRangePicker
newObject[enclosingObject + ':' + key] = dateRangePicker.data('daterangepicker').startDate._d
console.log(`dateRangePicker.data('daterangepicker').startDate._d`,dateRangePicker.data('daterangepicker').startDate._d)
}else{
newObject[enclosingObject + ':' + key] = value
}
})
return newObject
}
function openOnvifDeviceManager(monitorId){
selectedMonitorId = `${monitorId}`
blockForm.find('input').val('')
getUIFieldValuesFromCamera(monitorId)
}
function submitTheForm(){
$.confirm.create({
title: lang.updateCamerasInternalSettings,
body: lang.noUndoForAction,
clickOptions: {
title: lang['Save'],
class:'btn-success'
},
clickCallback: function(){
var postData = convertFormFieldNameToObjectKeys(getUIFieldValuesFromForm())
console.log('postData',postData)
$.post(getApiPrefix('onvifDeviceManager') + '/' + selectedMonitorId + '/save',{
data: JSON.stringify(postData)
},function(response){
var notifyTitle = lang['Settings Changed']
var notifyText = lang.onvifdeviceSavedText
var notifyTextError = ''
var notifyType = 'success'
$.each(response.responses,function(key,response){
if(!response.ok){
notifyTextError = lang.onvifdeviceSavedFoundErrorText
notifyType = 'warning'
console.log(response)
}
})
notifyText = notifyTextError ? notifyText + ' ' + notifyTextError : notifyText;
new PNotify({
title: notifyTitle,
text: notifyText,
type: notifyType,
})
})
}
})
}
dateRangePicker.daterangepicker({
singleDatePicker: true,
timePicker: true,
timePicker24Hour: true,
timePickerSeconds: true,
// timePickerIncrement: 30,
locale: {
format: 'YYYY-MM-DD HH:mm:ss'
}
},function(start, end, label){
console.log(start)
});
$('body').on('click','.open-onvif-device-manager',function(){
var monitorId = $(this).attr('data-mid') || $(this).parents('[data-mid]').attr('data-mid');
openOnvifDeviceManager(monitorId)
})
monitorsList.change(function(){
var monitorId = $(this).val()
openOnvifDeviceManager(monitorId)
})
blockWindow.on('click','.onvif-device-reboot',function(){
rebootCamera(selectedMonitorId)
})
blockForm.on('change','[name="videoToken"]',function(){
var selectedEncoder = loadedVideoEncoders[$(this).val()]
setFieldsFromOnvifKeys(selectedEncoder)
blockForm.find('[detail="Resolution"]').val(`${selectedEncoder['Resolution:Width']}x${selectedEncoder['Resolution:Height']}`)
})
blockForm.on('change','[detail="Resolution"]',function(){
var resolution = $(this).val().split('x')
var width = resolution[0]
var height = resolution[1]
blockForm.find('[name="Resolution:Width"]').val(width)
blockForm.find('[name="Resolution:Height"]').val(height)
})
blockForm.submit(function(e){
e.preventDefault()
submitTheForm()
return false;
})
onWebSocketEvent(function(d){
switch(d.f){
case'init_success':
drawMonitorListToSelector(monitorsList)
break;
}
})
})

View File

@ -119,7 +119,7 @@ $(document).ready(function(e){
return matches; return matches;
} }
var filterOutMonitorsThatAreAlreadyAdded = function(listOfCameras,callback){ var filterOutMonitorsThatAreAlreadyAdded = function(listOfCameras,callback){
$.get(getApiPrefix(`monitor`),function(monitors){ $.getJSON(getApiPrefix(`monitor`),function(monitors){
var monitorsNotExisting = [] var monitorsNotExisting = []
$.each(listOfCameras,function(n,camera){ $.each(listOfCameras,function(n,camera){
var matches = false var matches = false

View File

@ -2,21 +2,56 @@ $(document).ready(function(){
var theBlock = $('#recentVideos') var theBlock = $('#recentVideos')
var theList = $('#recentVideosList') var theList = $('#recentVideosList')
var monitorList = theBlock.find('.monitors_list') var monitorList = theBlock.find('.monitors_list')
function drawRowToList(row,toBegin){ function drawRowToList(row,toBegin,returnLastChild){
theList[toBegin ? 'prepend' : 'append'](createVideoRow(row)) theList[toBegin ? 'prepend' : 'append'](createVideoRow(row))
if(returnLastChild){
var theChildren = theList.children()
return toBegin ? theChildren.first() : theChildren.last()
}
}
function drawDaysToList(videos,toBegin){
var videosSortedByDays = sortVideosByDays(videos)
$.each(videosSortedByDays,function(monitorId,days){
$.each(days,function(dayKey,videos){
var copyOfVideos = ([]).concat(videos).reverse()
theList.append(createDayCard(copyOfVideos,dayKey,monitorId))
var theChildren = theList.children()
var createdCardCarrier = toBegin ? theChildren.first() : theChildren.last()
bindFrameFindingByMouseMoveForDay(createdCardCarrier,dayKey,copyOfVideos)
preloadAllTimelapseFramesToMemoryFromVideoList(copyOfVideos)
})
})
} }
function loadVideos(options,callback){ function loadVideos(options,callback){
theList.empty(); theList.empty();
getVideos(options,function(data){ var currentDate = new Date()
options.startDate = moment(currentDate).subtract(72, 'hours')._d;
options.endDate = moment(currentDate)._d;
console.log(options)
function drawVideoData(data){
var html = `` var html = ``
var videos = data.videos || {} var videos = data.videos || []
$.each(videos,function(n,row){ // $.each(videos,function(n,row){
drawRowToList(row) // var createdCardCarrier = drawRowToList(row,false,true)
}) // bindFrameFindingByMouseMove(createdCardCarrier,row)
// })
drawDaysToList(videos,false)
getCountOfEvents({ getCountOfEvents({
startDate: options.startDate,
endDate: options.endDate,
monitorId: options.monitorId, monitorId: options.monitorId,
}) })
callback(data) callback(data)
}
getVideos(options,function(data){
if(data.videos.length === 0){
options.limit = 20
delete(options.startDate)
delete(options.endDate)
getVideos(options,drawVideoData)
}else{
drawVideoData(data)
}
}) })
} }
function getCountOfEvents(options){ function getCountOfEvents(options){
@ -43,12 +78,12 @@ $(document).ready(function(){
liveStamp() liveStamp()
}) })
}) })
addOnTabReopen('initial', function () { theBlock.find('.recent-videos-refresh').click(function(){
var theSelected = `${monitorList.val()}` var theSelected = `${monitorList.val()}`
drawMonitorListToSelector(monitorList.find('optgroup')) drawMonitorListToSelector(monitorList.find('optgroup'))
monitorList.val(theSelected) monitorList.val(theSelected)
loadVideos({ loadVideos({
limit: 10, limit: 20,
monitorId: theSelected || undefined, monitorId: theSelected || undefined,
},function(){ },function(){
liveStamp() liveStamp()
@ -59,15 +94,16 @@ $(document).ready(function(){
case'init_success': case'init_success':
drawMonitorListToSelector(monitorList.find('optgroup')) drawMonitorListToSelector(monitorList.find('optgroup'))
loadVideos({ loadVideos({
limit: 10, limit: 20,
},function(){ },function(){
liveStamp() liveStamp()
}) })
break; break;
case'video_build_success': // case'video_build_success':
loadVideoData(d) // loadVideoData(d)
drawRowToList(createVideoLinks(d),true) // var createdCardCarrier = drawRowToList(createVideoLinks(d),true)
break; // bindFrameFindingByMouseMove(createdCardCarrier,row)
// break;
} }
}) })
}) })

View File

@ -7,7 +7,7 @@ $(document).ready(function(){
var selectedStates = schedulerWindow.find('[name="monitorStates"]') var selectedStates = schedulerWindow.find('[name="monitorStates"]')
var selectedDays = schedulerWindow.find('[name="days"]') var selectedDays = schedulerWindow.find('[name="days"]')
var loadSchedules = function(callback){ var loadSchedules = function(callback){
$.get(getApiPrefix() + '/schedule/' + $user.ke,function(d){ $.getJSON(getApiPrefix() + '/schedule/' + $user.ke,function(d){
var html = '' var html = ''
$.each(d.schedules,function(n,v){ $.each(d.schedules,function(n,v){
loadedSchedules[v.name] = v loadedSchedules[v.name] = v
@ -21,7 +21,7 @@ $(document).ready(function(){
}) })
} }
var loadMonitorStates = function(){ var loadMonitorStates = function(){
$.get(getApiPrefix() + '/monitorStates/' + $user.ke,function(d){ $.getJSON(getApiPrefix() + '/monitorStates/' + $user.ke,function(d){
var html = '' var html = ''
$.each(d.presets,function(n,v){ $.each(d.presets,function(n,v){
loadedMonitorStates[v.name] = v loadedMonitorStates[v.name] = v

View File

@ -3,7 +3,7 @@ var sidebarMenuInner = $('#menu-side')
var pageTabContainer = $('#pageTabContainer') var pageTabContainer = $('#pageTabContainer')
var topMenu = $('#topMenu') var topMenu = $('#topMenu')
var monitorSideList = $('#monitorSideList') var monitorSideList = $('#monitorSideList')
var toggleSideBarMenu = $('#toggleSideBarMenu') var sideMenuCollapsePoint = $('#side-menu-collapse-point')
function buildTabHtml(tabName,tabLabel,tabIcon){ function buildTabHtml(tabName,tabLabel,tabIcon){
return `<li class="nav-item"> return `<li class="nav-item">
<a class="nav-link side-menu-link" page-open="${tabName}"> <a class="nav-link side-menu-link" page-open="${tabName}">
@ -97,6 +97,27 @@ function toggleSideMenuVisibility(){
pageTabContainer.addClass('col-md-9 col-lg-10') pageTabContainer.addClass('col-md-9 col-lg-10')
} }
} }
function toggleSideMenuCollapse(dontSaveChange){
var isVisible = sideMenuCollapsePoint.hasClass('show')
if(isVisible){
sideMenuCollapsePoint.collapse('hide')
}else{
sideMenuCollapsePoint.collapse('show')
}
if(!dontSaveChange)dashboardOptions('sideMenuCollapsed',!isVisible ? '0' : 1)
}
function loadSideMenuCollapseStatus(){
var isCollapsed = dashboardOptions().sideMenuCollapsed === 1;
if(isCollapsed){
sideMenuCollapsePoint.collapse('hide')
}else{
sideMenuCollapsePoint.collapse('show')
}
return isCollapsed
}
function isSideBarMenuCollapsed(){
return dashboardOptions().sideMenuCollapsed === 1
}
$('#monitors_list_search').keyup(function(){ $('#monitors_list_search').keyup(function(){
var monitorBlocks = monitorSideList.find('.monitor_block'); var monitorBlocks = monitorSideList.find('.monitor_block');
var searchTerms = $(this).val().toLowerCase().split(' ') var searchTerms = $(this).val().toLowerCase().split(' ')
@ -140,6 +161,10 @@ onDashboardReady(function(){
drawMonitors() drawMonitors()
fixSideMenuScroll() fixSideMenuScroll()
sortListMonitors() sortListMonitors()
loadSideMenuCollapseStatus()
$('.toggle-menu-collapse').click(function(){
toggleSideMenuCollapse()
})
}) })
onWebSocketEvent(function(d){ onWebSocketEvent(function(d){
switch(d.f){ switch(d.f){

View File

@ -134,12 +134,6 @@ $('body')
dropdownToggles[keyName] = value dropdownToggles[keyName] = value
dashboardOptions('dropdown_toggle',dropdownToggles) dashboardOptions('dropdown_toggle',dropdownToggles)
}) })
.on('click','.logout',function(e){
$.get(getApiPrefix() + '/logout/'+$user.ke+'/'+$user.uid,function(data){
localStorage.removeItem('ShinobiLogin_' + location.host);
location.href = location.href.split('#')[0];
});
})
.on('dblclick','[type="password"],.password_field',function(){ .on('dblclick','[type="password"],.password_field',function(){
var _this = $(this) var _this = $(this)
var type = 'password' var type = 'password'

View File

@ -40,7 +40,7 @@ $(document).ready(function(){
var notifyColor = 'info' var notifyColor = 'info'
if(data.ok){ if(data.ok){
loadedSubAccounts[uid] = null; loadedSubAccounts[uid] = null;
accountTable.find('tr[uid="' + uid + '"]').remove() accountTable.find('[uid="' + uid + '"]').remove()
}else{ }else{
notifyTitle = lang.accountActionFailed notifyTitle = lang.accountActionFailed
notifyText = lang.contactAdmin notifyText = lang.contactAdmin
@ -165,7 +165,7 @@ $(document).ready(function(){
} }
var setPermissionSelectionsToFields = function(uid){ var setPermissionSelectionsToFields = function(uid){
var account = loadedSubAccounts[uid] var account = loadedSubAccounts[uid]
var details = $.parseJSON(account.details) var details = safeJsonParse(account.details)
// load values to Account Information : email, password, etc. // load values to Account Information : email, password, etc.
$.each(account,function(n,v){ $.each(account,function(n,v){
theWindowForm.find('[name="'+n+'"]').val(v) theWindowForm.find('[name="'+n+'"]').val(v)
@ -325,6 +325,10 @@ $(document).ready(function(){
$(`#active-user-${user.uid}-${user.cnid}`).remove() $(`#active-user-${user.uid}-${user.cnid}`).remove()
} }
break; break;
case'delete_sub_account':
var user = d.user
accountTable.find(`[uid="${user.uid}"]`).remove()
break;
} }
}) })
initiateSubAccountPage() initiateSubAccountPage()

View File

@ -24,6 +24,7 @@ $(document).ready(function(e){
var downloaderIsChecking = false var downloaderIsChecking = false
var allowKeepChecking = true var allowKeepChecking = true
var selectedFps = 15 var selectedFps = 15
var currentPlaylistArray = []
var openTimelapseWindow = function(monitorId,startDate,endDate){ var openTimelapseWindow = function(monitorId,startDate,endDate){
drawTimelapseWindowElements(monitorId,startDate,endDate) drawTimelapseWindowElements(monitorId,startDate,endDate)
@ -70,7 +71,11 @@ $(document).ready(function(e){
if(!startDate)startDate = dateRange.startDate if(!startDate)startDate = dateRange.startDate
if(!endDate)endDate = dateRange.endDate if(!endDate)endDate = dateRange.endDate
if(!selectedMonitor)selectedMonitor = monitorsList.val() if(!selectedMonitor)selectedMonitor = monitorsList.val()
var queryString = ['start=' + startDate,'end=' + endDate] var queryString = [
'start=' + startDate,
'end=' + endDate,
'noLimit=1'
]
var frameIconsHtml = '' var frameIconsHtml = ''
var apiURL = apiBaseUrl + '/timelapse/' + $user.ke + '/' + selectedMonitor var apiURL = apiBaseUrl + '/timelapse/' + $user.ke + '/' + selectedMonitor
$.getJSON(apiURL + '?' + queryString.join('&'),function(data){ $.getJSON(apiURL + '?' + queryString.join('&'),function(data){
@ -82,7 +87,7 @@ $(document).ready(function(e){
$.each(data.reverse(),function(n,fileInfo){ $.each(data.reverse(),function(n,fileInfo){
fileInfo.href = apiURL + '/' + fileInfo.filename.split('T')[0] + '/' + fileInfo.filename fileInfo.href = apiURL + '/' + fileInfo.filename.split('T')[0] + '/' + fileInfo.filename
fileInfo.number = n fileInfo.number = n
frameIconsHtml += '<div class="col-md-4 frame-container"><div class="frame" data-filename="' + fileInfo.filename + '" style="background-image:url(\'' + fileInfo.href + '\')"><div class="button-strip"><button type="button" class="btn btn-sm btn-danger delete"><i class="fa fa-trash-o"></i></button></div><div class="shade">' + moment(fileInfo.time).format('YYYY-MM-DD HH:mm:ss') + '</div></div></div>' frameIconsHtml += '<div class="col-md-4 frame-container"><div class="frame" data-filename="' + fileInfo.filename + '" frame-container-unloaded="' + fileInfo.href + '"><div class="button-strip"><button type="button" class="btn btn-sm btn-danger delete"><i class="fa fa-trash-o"></i></button></div><div class="shade">' + moment(fileInfo.time).format('YYYY-MM-DD HH:mm:ss') + '</div></div></div>'
currentPlaylist[fileInfo.filename] = fileInfo currentPlaylist[fileInfo.filename] = fileInfo
}) })
currentPlaylistArray = data currentPlaylistArray = data
@ -90,6 +95,7 @@ $(document).ready(function(e){
frameIcons.find(`.frame:first`).click() frameIcons.find(`.frame:first`).click()
// getLiveStream() // getLiveStream()
resetFilmStripPositions() resetFilmStripPositions()
loadVisibleTimelapseFrames()
}else{ }else{
frameIconsHtml = lang['No Data'] frameIconsHtml = lang['No Data']
frameIcons.html(frameIconsHtml) frameIcons.html(frameIconsHtml)
@ -193,7 +199,7 @@ $(document).ready(function(e){
title: lang.Delete, title: lang.Delete,
}, },
clickCallback: function(){ clickCallback: function(){
$.get(frame.href + '/delete',function(response){ $.getJSON(frame.href + '/delete',function(response){
if(response.ok){ if(response.ok){
el.parent().remove() el.parent().remove()
} }
@ -231,7 +237,7 @@ $(document).ready(function(e){
setDownloadButtonLabel(lang['Automatic Checking Cancelled']) setDownloadButtonLabel(lang['Automatic Checking Cancelled'])
downloadRecheckTimers[timerId] = setTimeout(function(){ downloadRecheckTimers[timerId] = setTimeout(function(){
setDownloadButtonLabel(lang['Build Video'], 'database') setDownloadButtonLabel(lang['Build Video'], 'database')
},5000) },30000)
downloaderIsChecking = false downloaderIsChecking = false
allowKeepChecking = true allowKeepChecking = true
return return
@ -258,7 +264,7 @@ $(document).ready(function(e){
downloadRecheckTimers[timerId] = setTimeout(function(){ downloadRecheckTimers[timerId] = setTimeout(function(){
setDownloadButtonLabel(lang['Please Wait or Click to Stop Checking'], 'spinner fa-pulse') setDownloadButtonLabel(lang['Please Wait or Click to Stop Checking'], 'spinner fa-pulse')
runDownloader() runDownloader()
},5000) },30000)
} }
}) })
} }
@ -269,6 +275,25 @@ $(document).ready(function(e){
playInterval = 1000 / ev.value playInterval = 1000 / ev.value
selectedFps = ev.value + 0 selectedFps = ev.value + 0
}) })
function isElementVisible (el) {
const holder = frameIcons[0]
const { top, bottom, height } = el.getBoundingClientRect()
const holderRect = holder.getBoundingClientRect()
return top <= holderRect.top
? holderRect.top - top <= height
: bottom - holderRect.bottom <= height
}
function loadVisibleTimelapseFrames(){
frameIcons.find('[frame-container-unloaded]').each(function(n,v){
if(isElementVisible(v)){
var el = $(v)
var imgSrc = el.attr('frame-container-unloaded')
el.removeAttr('frame-container-unloaded').attr('style',`background-image:url(${imgSrc})`)
}
})
}
frameIcons.on('scroll',loadVisibleTimelapseFrames)
$('body').on('click','.open-timelapse-viewer',function(){ $('body').on('click','.open-timelapse-viewer',function(){
var el = $(this).parents('[data-mid]') var el = $(this).parents('[data-mid]')
var monitorId = el.attr('data-mid') var monitorId = el.attr('data-mid')

View File

@ -1,7 +1,10 @@
function getVideoPlayerTabId(video){
return `videoPlayer-${video.mid}-${moment(video.time).format('YYYY-MM-DD-HH-mm-ss')}`
}
$(document).ready(function(){ $(document).ready(function(){
var theBlock = $('#tab-videoPlayer') var theBlock = $('#tab-videoPlayer')
window.createVideoPlayerTab = function(video){ window.createVideoPlayerTab = function(video,timeStart){
var newTabId = `videoPlayer-${video.mid}-${moment(video.time).format('YYYY-MM-DD-HH-mm-ss')}` var newTabId = getVideoPlayerTabId(video)
var humanStartTime = formattedTime(video.time,true) var humanStartTime = formattedTime(video.time,true)
var humanEndTime = formattedTime(video.end,true) var humanEndTime = formattedTime(video.end,true)
var tabLabel = `<b>${lang['Video']}</b> : ${loadedMonitors[video.mid].name} : ${formattedTime(video.time,true)}` var tabLabel = `<b>${lang['Video']}</b> : ${loadedMonitors[video.mid].name} : ${formattedTime(video.time,true)}`
@ -32,40 +35,46 @@ $(document).ready(function(){
}) })
eventMatrixHtml += `</div>` eventMatrixHtml += `</div>`
} }
var baseHtml = `<main class="container page-tab tab-videoPlayer" id="tab-${newTabId}" video-id="${video.mid}${video.time}"> var baseHtml = `<main class="container page-tab tab-videoPlayer" id="tab-${newTabId}" video-id="${video.mid}${video.time}" data-mid="${video.mid}" data-time="${video.time}">
<div class="my-3 ${definitions.Theme.isDark ? 'bg-dark text-white' : 'bg-light text-dark'} rounded shadow-sm"> <div class="mt-3 ${definitions.Theme.isDark ? 'bg-dark text-white' : 'bg-light text-dark'} rounded shadow-sm" style="overflow:hidden">
<div class="p-3"> <div class="d-flex flex-row">
<h6 class="video-title border-bottom-dotted border-bottom-dark pb-2 mb-0">${tabLabel}</h6> <div class="flex-grow-1">
<h6 class="video-title border-bottom-dotted border-bottom-dark p-3 mb-0">${tabLabel}</h6>
</div>
<div>
<a class="btn btn-default btn-sm delete-tab-dynamic"><i class="fa fa-times"></i></a>
</div>
</div> </div>
<div style="position: relative"> <div style="position: relative">
<div class="tab-videoPlayer-event-objects" style="position: absolute;width: 100%; height: 100%; z-index: 10"></div> <div class="tab-videoPlayer-event-objects" style="position: absolute;width: 100%; height: 100%; z-index: 10"></div>
<video class="tab-videoPlayer-video-element" controls autoplay src="${videoUrl}"></video> <video class="tab-videoPlayer-video-element" controls autoplay src="${videoUrl}"></video>
</div> </div>
<div class="p-3"> <div class="d-flex flex-row">
<div class="d-block"> <div class="d-block p-3">
<a class="btn btn-sm btn-success" download href="${videoUrl}"><i class="fa fa-download"></i> ${lang.Download}</a> <b class="flex-grow-1">${lang.Started}</b>
</div> <div class="video-time">${humanStartTime}</div>
<div class="d-block"> </div>
<b class="flex-grow-1">${lang.Started}</b> <div class="d-block p-3">
<div class="video-time">${humanStartTime}</div> <b class="flex-grow-1">${lang.Ended}</b>
</div> <div class="video-end">${humanEndTime}</div>
<div class="d-block"> </div>
<b class="flex-grow-1">${lang.Ended}</b> </div>
<div class="video-end">${humanEndTime}</div> <div class="mb-3">
</div> ${eventMatrixHtml}
<div class="d-block"> </div>
${eventMatrixHtml} <div class="d-flex flex-row">
</div> <div class="flex-grow-1 bg-gradient-blue">
<small class="d-block text-end mt-3"> <a class="btn btn-block px-1 py-3 ${definitions.Theme.isDark ? 'text-white' : 'text-dark'}" download href="${videoUrl}"><i class="fa fa-download"></i> ${lang.Download}</a>
<a class="go-back btn badge">${lang['Back']}</a> </div>
</small> <div class="flex-grow-1 bg-gradient-orange">
<a class="btn btn-block px-1 py-3 delete-video ${definitions.Theme.isDark ? 'text-white' : 'text-dark'}"><i class="fa fa-trash-o"></i> ${lang.Delete}</a>
</div>
</div> </div>
</div> </div>
</main>` </main>`
var tabCreateResponse = createNewTab(newTabId,tabLabel,baseHtml,{},null,'videoPlayer') var tabCreateResponse = createNewTab(newTabId,tabLabel,baseHtml,{},null,'videoPlayer')
console.log(tabCreateResponse) var videoElement = tabCreateResponse.theTab.find('.tab-videoPlayer-video-element')[0]
if(!tabCreateResponse.existAlready){ if(!tabCreateResponse.existAlready){
var videoElement = tabCreateResponse.theTab.find('.tab-videoPlayer-video-element')[0]
var videoObjectContainer = tabCreateResponse.theTab.find('.tab-videoPlayer-event-objects') var videoObjectContainer = tabCreateResponse.theTab.find('.tab-videoPlayer-event-objects')
var videoHeight = videoObjectContainer.height() var videoHeight = videoObjectContainer.height()
var videoWidth = videoObjectContainer.width() var videoWidth = videoObjectContainer.width()
@ -83,6 +92,7 @@ $(document).ready(function(){
} }
} }
} }
if(timeStart)videoElement.currentTime = timeStart;
} }
window.closeVideoPlayer = function(tabId){ window.closeVideoPlayer = function(tabId){
console.log('closeVideoPlayer') console.log('closeVideoPlayer')

View File

@ -1,4 +1,38 @@
var loadedVideosInMemory = {} var loadedVideosInMemory = {}
var loadedFramesMemory = {}
var loadedFramesMemoryTimeout = {}
function getLocalTimelapseImageLink(imageUrl){
if(loadedFramesMemory[imageUrl]){
return loadedFramesMemory[imageUrl]
}else{
return new Promise((resolve,reject) => {
fetch(imageUrl)
.then(res => res.blob()) // Gets the response and returns it as a blob
.then(blob => {
var objectURL = URL.createObjectURL(blob);
loadedFramesMemory[imageUrl] = objectURL
clearTimeout(loadedFramesMemoryTimeout[imageUrl])
loadedFramesMemoryTimeout[imageUrl] = setTimeout(function(){
URL.revokeObjectURL(objectURL)
delete(loadedFramesMemory[imageUrl])
delete(loadedFramesMemoryTimeout[imageUrl])
},1000 * 60 * 10)
resolve(objectURL)
});
})
}
}
async function preloadAllTimelapseFramesToMemoryFromVideoList(videos){
for (let i = 0; i < videos.length; i++) {
var video = videos[i]
for (let ii = 0; ii < video.timelapseFrames.length; ii++) {
var frame = video.timelapseFrames[ii]
// console.log ('Loading... ',frame.href)
await getLocalTimelapseImageLink(frame.href)
// console.log ('Loaded! ',frame.href)
}
}
}
function createVideoLinks(video){ function createVideoLinks(video){
var details = safeJsonParse(video.details) var details = safeJsonParse(video.details)
var queryString = [] var queryString = []
@ -32,7 +66,7 @@ function createVideoLinks(video){
video.details = details video.details = details
return video return video
} }
function applyEventListToVideos(videos,events){ function applyDataListToVideos(videos,events,keyName,reverseList){
var updatedVideos = videos.concat([]) var updatedVideos = videos.concat([])
var currentEvents = events.concat([]) var currentEvents = events.concat([])
updatedVideos.forEach(function(video){ updatedVideos.forEach(function(video){
@ -41,48 +75,182 @@ function applyEventListToVideos(videos,events){
var startTime = new Date(video.time) var startTime = new Date(video.time)
var endTime = new Date(video.end) var endTime = new Date(video.end)
var eventTime = new Date(theEvent.time) var eventTime = new Date(theEvent.time)
if(eventTime >= startTime && eventTime <= endTime){ if(theEvent.mid === video.mid && eventTime >= startTime && eventTime <= endTime){
videoEvents.push(theEvent) videoEvents.push(theEvent)
currentEvents.splice(index, 1) currentEvents.splice(index, 1)
} }
}) })
video.events = videoEvents if(reverseList)videoEvents = videoEvents.reverse()
video[keyName || 'events'] = videoEvents
}) })
return updatedVideos return updatedVideos
} }
function createVideoRow(row,classOverride){ function applyTimelapseFramesListToVideos(videos,events,keyName,reverseList){
var possibleEventFrames = '' var thisApiPrefix = getApiPrefix() + '/timelapse/' + $user.ke + '/'
var hasRows = row.events && row.events.length > 0 var newVideos = applyDataListToVideos(videos,events,keyName,reverseList)
if(hasRows){ newVideos.forEach(function(video){
var eventMatrixHtml = `` video.timelapseFrames.forEach(function(row){
var objectsFound = {} var apiURL = thisApiPrefix + row.mid
eventMatrixHtml += ` row.href = libURL + apiURL + '/' + row.filename.split('T')[0] + '/' + row.filename
<table class="table table-striped mb-0"> })
<tr> })
<th scope="col" class="${definitions.Theme.isDark ? 'text-white' : ''} text-epic">${lang.Events}</th> return newVideos
<th scope="col" class="text-end"><span class="badge bg-light text-dark rounded-pill">${row.events.length}</span></th> }
</tr>` function getFrameOnVideoRow(percentageInward,video){
$.each(([]).concat(row.events).splice(0,11),function(n,theEvent){ var startTime = new Date(video.time)
var imagePath = `${formattedTimeForFilename(theEvent.time,false,'YYYY-MM-DD')}/${formattedTimeForFilename(theEvent.time,false,'YYYY-MM-DDTHH-mm-ss')}.jpg` var endTime = new Date(video.end)
possibleEventFrames += `<div class="col-4 mb-2"><img class="rounded pop-image cursor-pointer" style="max-width:100%;" src="${getApiPrefix('timelapse')}/${theEvent.mid}/${imagePath}" onerror="$(this).parent().remove()"></div>` var timeDifference = endTime - startTime
}) var timeInward = timeDifference / (100 / percentageInward)
$.each(row.events,function(n,theEvent){ var timeAdded = new Date(startTime.getTime() + timeInward) // ms
$.each(theEvent.details.matrices,function(n,matrix){ var foundFrame = video.timelapseFrames.find(function(row){
if(!objectsFound[matrix.tag])objectsFound[matrix.tag] = 1 return new Date(row.time) > timeAdded
++objectsFound[matrix.tag] });
return {
timeInward: timeInward,
foundFrame: foundFrame,
timeAdded: timeAdded,
}
}
function getVideoFromDay(percentageInward,videos){
var startTime = new Date(videos[0].time)
var endTime = new Date(videos[videos.length - 1].end)
var timeDifference = endTime - startTime
var timeInward = timeDifference / (100 / percentageInward)
var timeAdded = new Date(startTime.getTime() + timeInward) // ms
var foundVideo = ([]).concat(videos).reverse().find(function(row){
return new Date(timeAdded - 1000) >= new Date(row.time)
});
return foundVideo
}
function bindFrameFindingByMouseMove(createdCardCarrier,video){
var createdCardElement = createdCardCarrier.find('.card').first()
var timeImg = createdCardElement.find('.video-time-img')
var timeStrip = createdCardElement.find('.video-time-strip')
var timeNeedleSeeker = createdCardElement.find('.video-time-needle-seeker')
if(video.timelapseFrames.length > 0){
createdCardElement.on('mousemove',function(evt){
var offest = createdCardElement.offset()
var elementWidth = createdCardElement.width() + 2
var amountMoved = evt.pageX - offest.left
var percentMoved = amountMoved / elementWidth * 100
percentMoved = percentMoved > 100 ? 100 : percentMoved < 0 ? 0 : percentMoved
var frameFound = getFrameOnVideoRow(percentMoved,video).frameFound
if(frameFound){
timeImg.css('background-image',`url(${frameFound.href})`)
}
timeNeedleSeeker.css('left',`${amountMoved}px`)
})
timeImg.css('background-image',`url(${getFrameOnVideoRow(1,video).frameFound.href})`)
}else{
if(video.events.length === 0){
timeStrip.hide()
}else{
var eventMatrixHtml = ``
var objectsFound = {}
eventMatrixHtml += `
<table class="table table-striped mb-0">
<tr>
<th scope="col" class="${definitions.Theme.isDark ? 'text-white' : ''} text-epic">${lang.Events}</th>
<th scope="col" class="text-end"><span class="badge bg-light text-dark rounded-pill">${video.events.length}</span></th>
</tr>`
$.each(([]).concat(video.events).splice(0,11),function(n,theEvent){
var imagePath = `${formattedTimeForFilename(theEvent.time,false,'YYYY-MM-DD')}/${formattedTimeForFilename(theEvent.time,false,'YYYY-MM-DDTHH-mm-ss')}.jpg`
possibleEventFrames += `<div class="col-4 mb-2"><img class="rounded pop-image cursor-pointer" style="max-width:100%;" src="${getApiPrefix('timelapse')}/${theEvent.mid}/${imagePath}" onerror="$(this).parent().remove()"></div>`
}) })
}) $.each(video.events,function(n,theEvent){
$.each(objectsFound,function(tag,count){ $.each(theEvent.details.matrices,function(n,matrix){
eventMatrixHtml += `<tr> if(!objectsFound[matrix.tag])objectsFound[matrix.tag] = 1
<td class="${definitions.Theme.isDark ? 'text-white' : ''}" style="text-transform:capitalize">${tag}</td> ++objectsFound[matrix.tag]
<td class="text-end"><span class="badge ${definitions.Theme.isDark ? 'bg-light text-dark' : 'bg-dark text-white'} rounded-pill">${count}</span></td> })
</tr>` })
}) $.each(objectsFound,function(tag,count){
eventMatrixHtml += `</table>` eventMatrixHtml += `<tr>
<td class="${definitions.Theme.isDark ? 'text-white' : ''}" style="text-transform:capitalize">${tag}</td>
<td class="text-end"><span class="badge badge-dark text-white rounded-pill">${count}</span></td>
</tr>`
})
eventMatrixHtml += `</table>`
timeStrip.append(eventMatrixHtml)
}
timeImg.css('min-height',`auto`).addClass('video-time-no-img')
}
}
function bindFrameFindingByMouseMoveForDay(createdCardCarrier,dayKey,videos){
var createdCardElement = createdCardCarrier.find('.card').first()
var timeImg = createdCardElement.find('.video-time-img')
var timeStrip = createdCardElement.find('.video-time-strip')
var timeNeedleSeeker = createdCardElement.find('.video-time-needle-seeker')
var videoLabel = createdCardElement.find('.video-time-label-selected-video')
var dayStart = videos[0].time
var dayEnd = videos[videos.length - 1].end
var hasFrames = false
$.each(videos,function(day,video){
if(video.timelapseFrames.length > 0){
hasFrames = true
}
})
if(hasFrames){
var videoSlices = createdCardElement.find('.video-day-slice')
var videoTimeLabel = createdCardElement.find('.video-time-label')
var currentlySelected = null
var currentlySelectedFrame = null
createdCardElement.on('mousemove',function(evt){
var offest = createdCardElement.offset()
var elementWidth = createdCardElement.width() + 2
var amountMoved = evt.pageX - offest.left
var percentMoved = amountMoved / elementWidth * 100
percentMoved = percentMoved > 100 ? 100 : percentMoved < 0 ? 0 : percentMoved
var videoFound = getVideoFromDay(percentMoved,videos)
createdCardElement.find(`[data-time]`).css('background-color','')
if(videoFound){
var videoSlice = createdCardElement.find(`[data-time="${videoFound.time}"]`).css('background-color','rgba(0,0,0,0.3)')
var videoSliceOffest = videoSlice.offset()
var videoSliceElementWidth = videoSlice.width()
var videoSliceAmountMoved = evt.pageX - videoSliceOffest.left
var videoSlicePercentMoved = videoSliceAmountMoved / videoSliceElementWidth * 100
videoSlicePercentMoved = videoSlicePercentMoved > 100 ? 100 : videoSlicePercentMoved < 0 ? 0 : videoSlicePercentMoved
var result = getFrameOnVideoRow(videoSlicePercentMoved,videoFound)
var frameFound = result.foundFrame
videoTimeLabel.text(formattedTime(result.timeAdded))
if(frameFound){
currentlySelectedFrame = Object.assign({},frameFound)
setTimeout(async function(){
var frameUrl = await getLocalTimelapseImageLink(frameFound.href)
if(currentlySelectedFrame.time === frameFound.time)timeImg.css('background-image',`url(${frameUrl})`);
},1)
}
if(currentlySelected && currentlySelected.time !== videoFound.time){
timeNeedleSeeker.attr('video-time-seeked-video-position',videoFound.time)
videoLabel.text(videoFound.time)
}
currentlySelected = Object.assign({},videoFound)
timeNeedleSeeker.attr('video-slice-seeked',result.timeInward)
}
timeNeedleSeeker.css('left',`${percentMoved}%`)
})
createdCardElement.trigger('mousemove')
}
}
function getPercentOfTimePositionFromVideo(video,theEvent){
var startTime = new Date(video.time)
var endTime = new Date(video.end)
var eventTime = new Date(theEvent.time)
var rangeMax = endTime - startTime
var eventMs = eventTime - startTime
var percentChanged = eventMs / rangeMax * 100
return percentChanged
}
function createVideoRow(row,classOverride){
var eventMatrixHtml = ``
if(row.events && row.events.length > 0){
$.each(row.events,function(n,theEvent){
var leftPercent = getPercentOfTimePositionFromVideo(row,theEvent)
eventMatrixHtml += `<div class="video-time-needle video-time-needle-event" style="left:${leftPercent}%"></div>`
})
} }
var videoEndpoint = getLocation() + '/' + $user.auth_token + '/videos/' + $user.ke + '/' + row.mid + '/' + row.filename var videoEndpoint = getLocation() + '/' + $user.auth_token + '/videos/' + $user.ke + '/' + row.mid + '/' + row.filename
return ` return `
<div class="video-row ${classOverride ? classOverride : `col-md-12 col-lg-6 mb-3`} search-row" data-mid="${row.mid}" data-time="${row.time}"> <div class="video-row ${classOverride ? classOverride : `col-md-12 col-lg-6 mb-3`} search-row" data-mid="${row.mid}" data-time="${row.time}" data-time-formed="${new Date(row.time)}">
<div class="card shadow-lg px-0 btn-default"> <div class="card shadow-lg px-0 btn-default">
<div class="card-header d-flex flex-row"> <div class="card-header d-flex flex-row">
<div class="flex-grow-1 ${definitions.Theme.isDark ? 'text-white' : ''}"> <div class="flex-grow-1 ${definitions.Theme.isDark ? 'text-white' : ''}">
@ -94,29 +262,95 @@ function createVideoRow(row,classOverride){
<a class="badge btn btn-danger delete-video" title="${lang['Delete']}"><i class="fa fa-trash-o"></i></a> <a class="badge btn btn-danger delete-video" title="${lang['Delete']}"><i class="fa fa-trash-o"></i></a>
</div> </div>
</div> </div>
<div class="card-body"> <div class="video-time-img">
<div title="${row.time}" class="d-flex flex-row"> <div class="card-body">
<div class="flex-grow-1"> <div title="${row.time}" class="d-flex flex-row">
${moment(row.time).fromNow()} <div class="flex-grow-1">
${moment(row.time).fromNow()}
</div>
<div>
<small class="text-muted">~${durationBetweenTimes(row.time,row.end)} ${lang.Minutes}</small>
</div>
</div> </div>
<div> <div title="${row.time}" class="d-flex flex-row border-bottom-dotted border-bottom-dark mb-2">
<small class="text-muted">~${durationBetweenTimes(row.time,row.end)} ${lang.Minutes}</small> <div>
<div title="${row.time}"><small class="text-muted">${lang.Started} : ${formattedTime(row.time,true)}</small></div>
<div title="${row.end}"><small class="text-muted">${lang.Ended} : ${formattedTime(row.end,true)}</small></div>
</div>
</div> </div>
</div> </div>
<div title="${row.time}" class="d-flex flex-row border-bottom-dotted border-bottom-dark mb-2">
<div>
<div title="${row.time}"><small class="text-muted">${lang.Started} : ${formattedTime(row.time,true)}</small></div>
<div title="${row.end}"><small class="text-muted">${lang.Ended} : ${formattedTime(row.end,true)}</small></div>
</div>
</div>
<div class="row">${possibleEventFrames}</div>
</div> </div>
<div class="card-footer p-0"> <div class="video-time-strip card-footer p-0">
${hasRows ? eventMatrixHtml : `<div class="p-3"><small class="text-muted">${lang['No Events found for this video']}</small></div>`} ${eventMatrixHtml}
<div class="video-time-needle video-time-needle-seeker" style="z-index: 2"></div>
</div> </div>
</div> </div>
</div>` </div>`
} }
function sortVideosByDays(videos){
var days = {}
videos.forEach(function(video){
var videoTime = new Date(video.time)
var theDayKey = `${videoTime.getDate()}-${videoTime.getMonth()}-${videoTime.getFullYear()}`
if(!days[video.mid])days[video.mid] = {};
if(!days[video.mid][theDayKey])days[video.mid][theDayKey] = [];
days[video.mid][theDayKey].push(video)
})
return days
}
function getVideoPercentWidthForDay(row,videos){
var startTime = new Date(row.time)
var endTime = new Date(row.end)
var timeDifference = endTime - startTime
var stripStartTime = new Date(videos[0].time)
var stripEndTime = new Date(videos[videos.length - 1].end)
var stripTimeDifference = stripEndTime - stripStartTime
var percent = (timeDifference / stripTimeDifference) * 100
return percent
}
function createDayCard(videos,dayKey,monitorId,classOverride){
var html = ''
var eventMatrixHtml = ``
$.each(videos,function(n,row){
eventMatrixHtml += `<div class="video-day-slice" data-mid="${row.mid}" data-time="${row.time}" style="width:${getVideoPercentWidthForDay(row,videos)}%;position:relative">`
if(row.events && row.events.length > 0){
$.each(row.events,function(n,theEvent){
var leftPercent = getPercentOfTimePositionFromVideo(row,theEvent)
eventMatrixHtml += `<div class="video-time-needle video-time-needle-event" style="left:${leftPercent}%"></div>`
})
}
eventMatrixHtml += `</div>`
})
html += `
<div class="video-row ${classOverride ? classOverride : `col-md-12 col-lg-6 mb-3`} search-row">
<div class="card shadow-sm px-0 ${definitions.Theme.isDark ? 'bg-dark' : 'bg-light'}" style="border-radius: 10px;overflow: hidden;">
<div class="video-time-header p-3">
<div class="d-flex flex-row ${definitions.Theme.isDark ? 'text-white' : ''}">
<div class="flex-grow-1">
<b>${loadedMonitors[monitorId] ? loadedMonitors[monitorId].name : monitorId}</b>
</div>
<div class="text-right">
<span class="badge badge-sm badge-primary">${dayKey}</span>
</div>
</div>
<div class="${definitions.Theme.isDark ? 'text-white' : ''}">
<span class="video-time-label">${formattedTime(videos[0].time)} to ${formattedTime(videos[videos.length - 1].end)}</span>
</div>
<div class="${definitions.Theme.isDark ? 'text-white' : ''}">
<span class="video-time-label-selected-video"></span>
</div>
</div>
<div class="video-time-img" style="background-image: url(${videos[0].timelapseFrames[0] ? videos[0].timelapseFrames[0].href : ''})">
</div>
<div class="video-time-strip card-footer p-0">
<div class="flex-row d-flex" style="height:30px">${eventMatrixHtml}</div>
<div class="video-time-needle video-time-needle-seeker" data-mid="${videos[0].mid}" style="z-index: 2"></div>
</div>
</div>
</div>`
return html
}
function drawVideoRowsToList(targetElement,rows){ function drawVideoRowsToList(targetElement,rows){
var theVideoList = $(targetElement) var theVideoList = $(targetElement)
theVideoList.empty() theVideoList.empty()
@ -146,14 +380,17 @@ function getVideos(options,callback){
eventEndTime = formattedTimeForFilename(options.endDate,false) eventEndTime = formattedTimeForFilename(options.endDate,false)
requestQueries.push(`end=${eventEndTime}`) requestQueries.push(`end=${eventEndTime}`)
} }
$.get(`${getApiPrefix(`videos`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.concat([`limit=${limit}`]).join('&')}`,function(data){ $.getJSON(`${getApiPrefix(`videos`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.concat([`noLimit=1`]).join('&')}`,function(data){
var videos = data.videos var videos = data.videos
$.get(`${getApiPrefix(`events`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.join('&')}`,function(eventData){ $.getJSON(`${getApiPrefix(`timelapse`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.concat([`noLimit=1`]).join('&')}`,function(timelapseFrames){
var newVideos = applyEventListToVideos(videos,eventData) $.getJSON(`${getApiPrefix(`events`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.concat([`noLimit=1`]).join('&')}`,function(eventData){
$.each(newVideos,function(n,video){ var newVideos = applyDataListToVideos(videos,eventData)
loadVideoData(video) newVideos = applyTimelapseFramesListToVideos(newVideos,timelapseFrames,'timelapseFrames',true)
$.each(newVideos,function(n,video){
loadVideoData(video)
})
callback({videos: newVideos})
}) })
callback({videos: newVideos})
}) })
}) })
} }
@ -177,10 +414,30 @@ function getEvents(options,callback){
if(options.onlyCount){ if(options.onlyCount){
requestQueries.push(`onlyCount=1`) requestQueries.push(`onlyCount=1`)
} }
$.get(`${getApiPrefix(`events`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.join('&')}`,function(eventData){ $.getJSON(`${getApiPrefix(`events`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.join('&')}`,function(eventData){
callback(eventData) callback(eventData)
}) })
} }
onWebSocketEvent(function(d){
switch(d.f){
case'video_delete':
$('[file="'+d.filename+'"][mid="'+d.mid+'"]:not(.modal)').remove();
$('[data-file="'+d.filename+'"][data-mid="'+d.mid+'"]:not(.modal)').remove();
$('[data-time-formed="'+(new Date(d.time))+'"][data-mid="'+d.mid+'"]:not(.modal)').remove();
var videoPlayerId = getVideoPlayerTabId(d)
deleteTab(videoPlayerId)
// if($.powerVideoWindow.currentDataObject&&$.powerVideoWindow.currentDataObject[d.filename]){
// delete($.timelapse.currentVideos[$.powerVideoWindow.currentDataObject[d.filename].position])
// $.powerVideoWindow.drawTimeline(false)
// }
// if($.timelapse.currentVideos&&$.timelapse.currentVideos[d.filename]){
// delete($.timelapse.currentVideosArray.videos[$.timelapse.currentVideos[d.filename].position])
// $.timelapse.drawTimeline(false)
// }
// if($.vidview.loadedVideos && $.vidview.loadedVideos[d.filename])delete($.vidview.loadedVideos[d.filename])
break;
}
})
$(document).ready(function(){ $(document).ready(function(){
$('body') $('body')
.on('click','.open-video',function(){ .on('click','.open-video',function(){
@ -190,6 +447,15 @@ $(document).ready(function(){
var video = loadedVideosInMemory[`${monitorId}${videoTime}`] var video = loadedVideosInMemory[`${monitorId}${videoTime}`]
createVideoPlayerTab(video) createVideoPlayerTab(video)
}) })
.on('click','[video-time-seeked-video-position]',function(){
var el = $(this)
var monitorId = el.attr('data-mid')
var videoTime = el.attr('video-time-seeked-video-position')
var timeInward = (parseInt(el.attr('video-slice-seeked')) / 1000) - 2
var video = loadedVideosInMemory[`${monitorId}${videoTime}`]
timeInward = timeInward < 0 ? 0 : timeInward
createVideoPlayerTab(video,timeInward)
})
.on('click','.delete-video',function(){ .on('click','.delete-video',function(){
var el = $(this).parents('[data-mid]') var el = $(this).parents('[data-mid]')
var monitorId = el.attr('data-mid') var monitorId = el.attr('data-mid')
@ -206,7 +472,7 @@ $(document).ready(function(){
class: 'btn-danger btn-sm' class: 'btn-danger btn-sm'
}, },
clickCallback: function(){ clickCallback: function(){
$.get(videoEndpoint + '/delete',function(data){ $.getJSON(videoEndpoint + '/delete',function(data){
if(data.ok){ if(data.ok){
console.log('Video Deleted') console.log('Video Deleted')
}else{ }else{

View File

@ -2,6 +2,7 @@ var mainSocket = {}
var websocketPath = checkCorrectPathEnding(location.pathname) + 'socket.io' var websocketPath = checkCorrectPathEnding(location.pathname) + 'socket.io'
var websocketQuery = {} var websocketQuery = {}
if(location.search === '?p2p=1'){ if(location.search === '?p2p=1'){
window.machineId = location.pathname.split('/')[2]
websocketPath = '/socket.io' websocketPath = '/socket.io'
websocketQuery.machineId = machineId websocketQuery.machineId = machineId
} }

View File

@ -115,7 +115,7 @@ $(document).ready(function(){
var selectedServer = p2pServerList[currentlyRegisteredP2PServer] var selectedServer = p2pServerList[currentlyRegisteredP2PServer]
console.log(selectedServer,currentlySelectedP2PServerId,p2pServerList) console.log(selectedServer,currentlySelectedP2PServerId,p2pServerList)
if(selectedServer && selectedServer.host){ if(selectedServer && selectedServer.host){
var href = `http://${selectedServer.host}:${selectedServer.webPort}/s/${apiKey}?p2p=1` var href = `http://${selectedServer.host}:${selectedServer.webPort}/s/${apiKey}/?p2p=1`
var win = window.open(href, '_blank'); var win = window.open(href, '_blank');
win.focus(); win.focus();
}else{ }else{

View File

@ -86,7 +86,7 @@
</div> </div>
</div> </div>
<script> <script>
var adminApiPrefix = location.search === '?p2p=1' ? location.pathname + '/' : "<%=originalURL%><%=config.webPaths.adminApiPrefix%>" var adminApiPrefix = location.search === '?p2p=1' ? location.pathname : "<%=originalURL%><%=config.webPaths.adminApiPrefix%>"
</script> </script>
<% include blocks/confirm.ejs %> <% include blocks/confirm.ejs %>
<% include blocks/subpermissions.ejs %> <% include blocks/subpermissions.ejs %>

View File

@ -6,8 +6,8 @@
<script src="<%-window.libURL%>libs/js/livestamp.min.js"></script> <script src="<%-window.libURL%>libs/js/livestamp.min.js"></script>
<script src="<%-window.libURL%>libs/js/poseidon.js"></script> <script src="<%-window.libURL%>libs/js/poseidon.js"></script>
<script src="<%-window.libURL%>assets/vendor/bootstrap5/js/bootstrap.bundle.min.js"></script> <script src="<%-window.libURL%>assets/vendor/bootstrap5/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/feather-icons@4.28.0/dist/feather.min.js" integrity="sha384-uO3SXW5IuS1ZpFPKugNNWqTZRRglnUJK6UAZ/gxOX80nxEkN9NcGZTftn6RzhGWE" crossorigin="anonymous"></script> <!-- <script src="https://cdn.jsdelivr.net/npm/feather-icons@4.28.0/dist/feather.min.js" integrity="sha384-uO3SXW5IuS1ZpFPKugNNWqTZRRglnUJK6UAZ/gxOX80nxEkN9NcGZTftn6RzhGWE" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js" integrity="sha384-zNy6FEbO50N+Cg5wap8IKA4M/ZnLJgzc6w2NqACZaK0u0FXfOWRRJOnQtpZun8ha" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js" integrity="sha384-zNy6FEbO50N+Cg5wap8IKA4M/ZnLJgzc6w2NqACZaK0u0FXfOWRRJOnQtpZun8ha" crossorigin="anonymous"></script> -->
<script src="<%-window.libURL%>libs/js/daterangepicker.js"></script> <script src="<%-window.libURL%>libs/js/daterangepicker.js"></script>
<script src="<%-window.libURL%>libs/js/placeholder.js"></script> <script src="<%-window.libURL%>libs/js/placeholder.js"></script>
<script src="<%-window.libURL%>libs/js/lodash.min.js"></script> <script src="<%-window.libURL%>libs/js/lodash.min.js"></script>

View File

@ -2,7 +2,7 @@
<script>definitions = <%- JSON.stringify(define) %></script> <script>definitions = <%- JSON.stringify(define) %></script>
<script>addStorage = <%- JSON.stringify(addStorage) %>;</script> <script>addStorage = <%- JSON.stringify(addStorage) %>;</script>
<script>logoLocation196x196 = `<%- window.libURL + config.logoLocation196x196 %>`;</script> <script>logoLocation196x196 = `<%- window.libURL + config.logoLocation196x196 %>`;</script>
<script>libURL = location.search === '?p2p=1' ? location.pathname + '/' : '<%-window.libURL%>';</script> <script>libURL = location.search === '?p2p=1' ? location.pathname : '<%-window.libURL%>';</script>
<script>HWAccelChoices = [ <script>HWAccelChoices = [
<% if(config.availableHWAccels) { <% if(config.availableHWAccels) {
var methods = { var methods = {

View File

@ -1,11 +1,13 @@
<% window.additionalJsScripts = []; %> <% window.additionalJsScripts = []; %>
<% <%
if(config.baseURL)window.libURL = config.baseURL;
if(!window.libURL)window.libURL = originalURL; if(!window.libURL)window.libURL = originalURL;
%> %>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title><%-pageTitle%></title> <title><%-pageTitle%></title>
<div><%-window.libURL%></div>
<meta charset="utf-8"> <meta charset="utf-8">
<meta content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0, shrink-to-fit=no' name='viewport' /> <meta content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0, shrink-to-fit=no' name='viewport' />
<meta name="description" content="Shinobi"> <meta name="description" content="Shinobi">
@ -62,7 +64,7 @@
if(!details.sub){ %> if(!details.sub){ %>
<script> <script>
window.getAdminApiPrefix = function(piece){ window.getAdminApiPrefix = function(piece){
return location.search === '?p2p=1' ? location.pathname + '/' : "<%=originalURL%><%=config.webPaths.adminApiPrefix%><%- $user.auth_token %>/" return location.search === '?p2p=1' ? (location.pathname.endsWith('/') ? location.pathname : location.pathname) : "<%=originalURL%><%=config.webPaths.adminApiPrefix%><%- $user.auth_token %>/"
} }
</script> </script>
<% } <% }

View File

@ -1,353 +0,0 @@
<div class="modal fade" id="help_window" tabindex="-1" role="dialog" aria-labelledby="help_windowLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title" id="help_windowLabel"><i class="fa fa-question-circle"></i> &nbsp; <span><%-lang.Help%></span></h4>
</div>
<div class="modal-body">
<div class="row">
<% [
{
bigIcon: "smile-o",
title: "It's a proven fact",
info: `Generosity makes you a happier person, please consider supporting the development.`,
buttons: [
{
icon: 'share-square-o',
color: 'default',
text: 'ShinobiShop Subscriptions',
href: 'https://licenses.shinobi.video/subscribe',
class: ''
},
{
icon: 'paypal',
color: 'default',
text: 'Donate by PayPal',
href: 'https://www.paypal.me/ShinobiCCTV',
class: ''
},
{
icon: 'bank',
color: 'default',
text: 'University of Zurich (UZH)',
href: 'https://www.media.uzh.ch/en/Press-Releases/2017/Generosity.html',
class: ''
},
]
},
{
title: "New to Shinobi?",
info: `Try reading over some of these links to get yourself started.`,
buttons: [
{
icon: 'newspaper-o',
color: 'default',
text: 'After Installation Guides',
href: 'https://shinobi.video/docs/configure',
class: ''
},
{
icon: 'plus',
color: 'default',
text: 'Adding an H.264 Camera',
href: 'https://shinobi.video/docs/configure#content-adding-an-h264h265-camera',
class: ''
},
{
icon: 'plus',
color: 'default',
text: 'Adding an MJPEG Camera',
href: 'https://shinobi.video/articles/2018-09-19-how-to-add-an-mjpeg-camera',
class: ''
},
{
icon: 'gears',
color: 'default',
text: 'RTSP Camera Optimization',
href: 'https://shinobi.video/articles/2017-07-29-how-i-optimized-my-rtsp-camera',
class: ''
},
{
icon: 'comments-o',
color: 'info',
text: 'Community Chat',
href: 'https://discord.gg/ehRd8Zz',
class: ''
},
{
icon: 'reddit',
color: 'info',
text: 'Forum on Reddit',
href: 'https://www.reddit.com/r/ShinobiCCTV',
class: ''
},
{
icon: 'file-o',
color: 'primary',
text: 'Documentation',
href: 'http://shinobi.video/docs',
class: ''
}
]
},
{
title: "ShinobiHub",
info: `Explore and Contribute to the community of Shinobi all in one place.`,
buttons: [
{
icon: 'newspaper-o',
color: 'primary',
text: 'Read Articles',
href: 'https://hub.shinobi.video/articles',
class: ''
},
{
icon: 'search',
color: 'success',
text: 'Explore Configurations',
href: 'https://hub.shinobi.video/explore',
class: ''
},
{
icon: 'lightbulb-o',
color: 'info',
text: 'View Suggestions',
href: 'https://hub.shinobi.video/suggestions',
class: ''
},
]
},
{
title: "Shinobi Mobile",
info: `Your subscription key can unlock features for <a href="https://cdn.shinobi.video/installers/ShinobiMobile/" target="_blank"><b>Shinobi Mobile</b></a> running on iOS and Android today!`,
buttons: [
{
icon: 'star',
color: 'success',
text: 'Join Public Beta',
href: 'https://cdn.shinobi.video/installers/ShinobiMobile/',
class: ''
},
{
icon: 'comments-o',
color: 'primary',
text: '<b>#mobile-client</b> Chat',
href: 'https://discord.gg/ehRd8Zz',
class: ''
},
{
icon: 'cube',
color: 'default',
text: lang[`Don't Show for 1 Week`],
class: 'hide_donate',
},
]
},
{
title: "Shinobi Dashcam",
info: `<a href="https://cdn.shinobi.video/installers/Dashcam/" target="_blank"><b>Shinobi Dashcam</b></a> runs on iOS and Android, Subscribe to a Shinobi Mobile license to unlock extended features!`,
buttons: [
{
icon: 'star',
color: 'success',
text: 'Join Public Beta',
href: 'https://cdn.shinobi.video/installers/Dashcam/',
class: ''
},
{
icon: 'comments-o',
color: 'primary',
text: '<b>#dashcam</b> Chat',
href: 'https://discord.gg/D3cHZ3u',
class: ''
},
{
icon: 'cube',
color: 'default',
text: lang[`Don't Show for 1 Week`],
class: 'hide_donate',
},
]
},
{
title: "Byaku",
info: `Byaku is a Neural Net Training tool that allows you to build Object Detection sets. <a href="https://cdn.shinobi.video/installers/Byaku/" target="_blank"><b>Byaku</b></a> runs on Linux. Subscribe to use features like automatically downloading thousands of images already pre-annotated and ready for you to train.`,
buttons: [
{
icon: 'star',
color: 'success',
text: 'Join Public Beta',
href: 'https://cdn.shinobi.video/installers/Byaku/',
class: ''
},
{
icon: 'comments-o',
color: 'primary',
text: '<b>#byaku</b> Chat',
href: 'https://discord.gg/3XA9RNP',
class: ''
},
{
icon: 'cube',
color: 'default',
text: lang[`Don't Show for 1 Week`],
class: 'hide_donate',
},
]
},
{
title: "Support the Development",
info: `Subscribe to any of the following to boost development! Once subscribed put your Subscription ID in at the Super user panel, then restart Shinobi to Activate your installation, thanks! <i class="fa fa-smile-o"></i>`,
buttons: [
{
icon: 'share-square-o',
color: 'default',
text: 'Shinobi Mobile License ($5/m)',
href: 'https://licenses.shinobi.video/subscribe?planSubscribe=plan_G31AZ9mknNCa6z',
class: ''
},
{
icon: 'share-square-o',
color: 'default',
text: 'Tiny Support Subscription ($10/m)',
href: 'https://licenses.shinobi.video/subscribe?planSubscribe=plan_G42jNgIqXaWmIC',
class: ''
},
{
icon: 'share-square-o',
color: 'default',
text: 'Shinobi Pro License ($75/m)',
href: 'https://licenses.shinobi.video/subscribe?planSubscribe=plan_G3LGdNwA8lSmQy',
class: ''
},
]
},
{
title: "Donations, One-Time Boost",
info: `Sometimes a subscription isn't practical for people. In which case you may show support through a PayPal donation. And as a thank you for doing so your <b>PayPal Transaction ID</b> can be used as a <code>subscriptionId</code> in your Shinobi configuration file. <br><br>Each 5 USD/EUR or 7 CAD will provide one month of activated usage. <i>Meaning, a $20 USD donation today makes this popup go away (or activates the mobile app) for 4 months.</i>`,
buttons: [
{
icon: 'paypal',
color: 'default',
text: 'Donate by PayPal',
href: 'https://www.paypal.me/ShinobiCCTV',
class: ''
},
]
},
].forEach((promo) => { %>
<div class="col-md-<%- promo.width || '6' %>">
<div class="helpquote" style="margin-bottom:5px;font-size:10pt;padding: 10px 20px;border-left: 5px solid #eee;">
<%- promo.bigIcon ? `<div style="margin-bottom:15px;text-align:center"><i class="fa fa-${promo.bigIcon} fa-5x"></i></div>` : '' %>
<%- promo.title ? `<h4>${promo.title}</h4>` : '' %>
<p><%- promo.info %></p>
<% if(promo.buttons) { %>
<div style="margin-top:5px;">
<% promo.buttons.forEach((button) => { %>
<a style="margin-bottom:4px" <%- button.href ? `href="${button.href}"` : '' %> target="_blank" class="d-flex flex-row btn btn-sm btn-block btn-<%- button.color %> <%- button.class %>">
<div><i class="fa fa-<%- button.icon %>" aria-hidden="true"></i></div>
<div class="flex-grow-1 text-center"><%- button.text %></div>
</a>
<% }) %>
</div>
<% } %>
</div>
</div>
<% }) %>
</div>
<blockquote class="blockquoteInHelp" style="margin-bottom:10px">
<p>If you already are supporting the development in a different way, please contact us and we can get this popup to go away for you <i class="fa fa-smile-o"></i> Cheers!</p>
</blockquote>
<div>
<!-- <iframe src="https://shinobi.video/ads" style="width:100%;height:250px;border:none;"></iframe> -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger pull-left hide_donate"><i class="fa fa-cube"></i> <%-lang[`Don't Show for 1 Week`]%></button>
<a target="_blank" href="https://discord.gg/gCxefHA" class="btn btn-primary"><i class="fa fa-comments-o"></i> <%-lang['Chat on Discord']%></a>
<a target="_blank" href="http://shinobi.video/docs" class="btn btn-default"><i class="fa fa-file-o"></i> <%-lang.Documentation%></a>
</div>
</div>
</div>
</div>
<style>
.blockquoteInHelp:before,.blockquoteInHelp:after{
display:none;
}
</style>
<script>
$(document).ready(function(){
var helpWindow = $('#help_window')
var openMessage = null
function lessThanOneWeekAgo(date){
const WEEK = 1000 * 60 * 60 * 24 * 7;
const aWeekAgo = Date.now() - WEEK;
return date < aWeekAgo;
}
function showHelpNotice(){
var buttonHtml = ``
$.each([
{
icon: 'share-square-o',
color: 'default',
text: 'ShinobiShop Subscriptions',
href: 'https://licenses.shinobi.video/subscribe',
class: ''
},
{
icon: 'paypal',
color: 'success',
text: 'Donate by PayPal',
href: 'https://www.paypal.me/ShinobiCCTV',
class: ''
},
{
icon: 'bank',
color: 'default',
text: 'University of Zurich (UZH)',
href: 'https://www.media.uzh.ch/en/Press-Releases/2017/Generosity.html',
class: ''
},
{
icon: 'cube',
color: 'danger',
text: lang[`Don't Show for 1 Week`],
href: '#',
class: 'hide_donate',
},
],function(n,button){
buttonHtml += `<a style="margin-bottom:4px" ${ button.href ? `href="${button.href}"` : '' } target="_blank" class="d-flex flex-row btn btn-sm btn-block btn-${ button.color } ${ button.class }">
<div><i class="fa fa-${ button.icon }" aria-hidden="true"></i></div>
<div class="text-center flex-grow-1">${ button.text }</div>
</a>`
})
openMessage = new PNotify({
title: `It's a proven fact`,
text: `<div class="mb-3">Generosity makes you a happier person, please consider supporting the development.</div>${buttonHtml}`,
hide: false,
})
}
function dontShowForOneWeek(){
if(openMessage){
openMessage.remove()
}
dashboardOptions('subscription_checked',new Date());
}
if(<%- !config.userHasSubscribed %> && !dashboardOptions().subscription_checked || lessThanOneWeekAgo(new Date(dashboardOptions().subscription_checked))){
setTimeout(function(){
showHelpNotice()
},1000 * 60 * 0.2)
}
$('body').on('click','.hide_donate',function(e){
e.preventDefault()
dontShowForOneWeek()
return false;
})
console.log('Please support the Shinobi developement.')
console.log('https://licenses.shinobi.video/subscribe')
})
</script>

View File

@ -21,12 +21,13 @@
'home/cameraProbe', 'home/cameraProbe',
'home/powerVideo', 'home/powerVideo',
'home/onvifScanner', 'home/onvifScanner',
'home/onvifDeviceManager',
'home/configFinder', 'home/configFinder',
'home/logViewer', 'home/logViewer',
'home/calendar', 'home/calendar',
'home/eventListWithPics', 'home/eventListWithPics',
'confirm', 'confirm',
'help', 'home/help',
]).forEach(function(block){ %> ]).forEach(function(block){ %>
<%- include(__dirname + '/web/pages/blocks/' +block + '.ejs') %> <%- include(__dirname + '/web/pages/blocks/' +block + '.ejs') %>
<% }) %> <% }) %>

View File

@ -0,0 +1,266 @@
<main class="container page-tab pt-3" id="tab-helpWindow">
<div style="" class="card form-group-group p-3 bg-dark mb-3 shadow green">
<h4 class="form-section-header cursor-pointer mb-3 pb-3 text-white border-bottom-dotted border-color-green "><%- config.poweredByShinobi %></h4>
<div class="card-body text-white">
<div class="row">
<% [
{
title: "New to Shinobi?",
info: `Try reading over some of these links to get yourself started.`,
buttons: [
{
icon: 'newspaper-o',
color: 'default',
text: 'After Installation Guides',
href: 'https://shinobi.video/docs/configure',
class: ''
},
{
icon: 'plus',
color: 'default',
text: 'Adding an H.264 Camera',
href: 'https://shinobi.video/docs/configure#content-adding-an-h264h265-camera',
class: ''
},
{
icon: 'plus',
color: 'default',
text: 'Adding an MJPEG Camera',
href: 'https://shinobi.video/articles/2018-09-19-how-to-add-an-mjpeg-camera',
class: ''
},
{
icon: 'gears',
color: 'default',
text: 'RTSP Camera Optimization',
href: 'https://shinobi.video/articles/2017-07-29-how-i-optimized-my-rtsp-camera',
class: ''
},
{
icon: 'comments-o',
color: 'info',
text: 'Community Chat',
href: 'https://discord.gg/ehRd8Zz',
class: ''
},
{
icon: 'reddit',
color: 'info',
text: 'Forum on Reddit',
href: 'https://www.reddit.com/r/ShinobiCCTV',
class: ''
},
{
icon: 'file-o',
color: 'primary',
text: 'Documentation',
href: 'http://shinobi.video/docs',
class: ''
}
]
},
{
bigIcon: "smile-o",
title: "It's a proven fact",
info: `Generosity makes you a happier person, please consider supporting the development.`,
buttons: [
{
icon: 'share-square-o',
color: 'default',
text: 'ShinobiShop Subscriptions',
href: 'https://licenses.shinobi.video/subscribe',
class: ''
},
{
icon: 'paypal',
color: 'default',
text: 'Donate by PayPal',
href: 'https://www.paypal.me/ShinobiCCTV',
class: ''
},
{
icon: 'bank',
color: 'default',
text: 'University of Zurich (UZH)',
href: 'https://www.media.uzh.ch/en/Press-Releases/2017/Generosity.html',
class: ''
},
]
},
// {
// title: "ShinobiHub",
// info: `Explore and Contribute to the community of Shinobi all in one place.`,
// buttons: [
// {
// icon: 'newspaper-o',
// color: 'primary',
// text: 'Read Articles',
// href: 'https://hub.shinobi.video/articles',
// class: ''
// },
// {
// icon: 'search',
// color: 'success',
// text: 'Explore Configurations',
// href: 'https://hub.shinobi.video/explore',
// class: ''
// },
// {
// icon: 'lightbulb-o',
// color: 'info',
// text: 'View Suggestions',
// href: 'https://hub.shinobi.video/suggestions',
// class: ''
// },
// ]
// },
{
title: "Shinobi Mobile",
info: `Your subscription key can unlock features for <a href="https://cdn.shinobi.video/installers/ShinobiMobile/" target="_blank"><b>Shinobi Mobile</b></a> running on iOS and Android today!`,
buttons: [
{
icon: 'star',
color: 'success',
text: 'Join Public Beta',
href: 'https://shinobi.video/mobile',
class: ''
},
{
icon: 'comments-o',
color: 'primary',
text: '<b>#mobile-client</b> Chat',
href: 'https://discord.gg/ehRd8Zz',
class: ''
},
// {
// icon: 'cube',
// color: 'default',
// text: lang[`Don't Show for 1 Week`],
// class: 'hide_donate',
// },
]
},
// {
// title: "Shinobi Dashcam",
// info: `<a href="https://cdn.shinobi.video/installers/Dashcam/" target="_blank"><b>Shinobi Dashcam</b></a> runs on iOS and Android, Subscribe to a Shinobi Mobile license to unlock extended features!`,
// buttons: [
// {
// icon: 'star',
// color: 'success',
// text: 'Join Public Beta',
// href: 'https://cdn.shinobi.video/installers/Dashcam/',
// class: ''
// },
// {
// icon: 'comments-o',
// color: 'primary',
// text: '<b>#dashcam</b> Chat',
// href: 'https://discord.gg/D3cHZ3u',
// class: ''
// },
// // {
// // icon: 'cube',
// // color: 'default',
// // text: lang[`Don't Show for 1 Week`],
// // class: 'hide_donate',
// // },
// ]
// },
// {
// title: "Byaku",
// info: `Byaku is a Neural Net Training tool that allows you to build Object Detection sets. <a href="https://cdn.shinobi.video/installers/Byaku/" target="_blank"><b>Byaku</b></a> runs on Linux. Subscribe to use features like automatically downloading thousands of images already pre-annotated and ready for you to train.`,
// buttons: [
// {
// icon: 'star',
// color: 'success',
// text: 'Join Public Beta',
// href: 'https://cdn.shinobi.video/installers/Byaku/',
// class: ''
// },
// {
// icon: 'comments-o',
// color: 'primary',
// text: '<b>#byaku</b> Chat',
// href: 'https://discord.gg/3XA9RNP',
// class: ''
// },
// // {
// // icon: 'cube',
// // color: 'default',
// // text: lang[`Don't Show for 1 Week`],
// // class: 'hide_donate',
// // },
// ]
// },
{
title: "Support the Development",
info: `Subscribe to any of the following to boost development! Once subscribed put your Subscription ID in at the Super user panel, then restart Shinobi to Activate your installation, thanks! <i class="fa fa-smile-o"></i>`,
buttons: [
{
icon: 'share-square-o',
color: 'default',
text: 'Shinobi Mobile License ($5/m)',
href: 'https://licenses.shinobi.video/subscribe?planSubscribe=plan_G31AZ9mknNCa6z',
class: ''
},
{
icon: 'share-square-o',
color: 'default',
text: 'Tiny Support Subscription ($10/m)',
href: 'https://licenses.shinobi.video/subscribe?planSubscribe=plan_G42jNgIqXaWmIC',
class: ''
},
{
icon: 'share-square-o',
color: 'default',
text: 'Shinobi Pro License ($75/m)',
href: 'https://licenses.shinobi.video/subscribe?planSubscribe=plan_G3LGdNwA8lSmQy',
class: ''
},
]
},
{
title: "Donations, One-Time Boost",
info: `Sometimes a subscription isn't practical for people. In which case you may show support through a PayPal donation. And as a thank you for doing so your <b>PayPal Transaction ID</b> can be used as a <code>subscriptionId</code> in your Shinobi configuration file. <br><br>Each 5 USD/EUR or 7 CAD will provide one month of activated usage. <i>Meaning, a $20 USD donation today makes this popup go away (or activates the mobile app) for 4 months.</i>`,
width: 12,
buttons: [
{
icon: 'paypal',
color: 'default',
text: 'Donate by PayPal',
href: 'https://www.paypal.me/ShinobiCCTV',
class: ''
},
]
},
].forEach((promo) => { %>
<div class="col-md-<%- promo.width || '6' %> mb-3">
<div class="helpquote" style="font-size:10pt;padding: 10px 20px;border-left: 5px solid #eee;">
<%- promo.bigIcon ? `<div style="margin-bottom:15px;text-align:center"><i class="fa fa-${promo.bigIcon} fa-5x"></i></div>` : '' %>
<%- promo.title ? `<h4>${promo.title}</h4>` : '' %>
<p><%- promo.info %></p>
<% if(promo.buttons) { %>
<div style="margin-top:5px;">
<% promo.buttons.forEach((button) => { %>
<a style="margin-bottom:4px" <%- button.href ? `href="${button.href}"` : '' %> target="_blank" class="d-flex flex-row btn btn-sm btn-block btn-<%- button.color %> <%- button.class %>">
<div><i class="fa fa-<%- button.icon %>" aria-hidden="true"></i></div>
<div class="flex-grow-1 text-center"><%- button.text %></div>
</a>
<% }) %>
</div>
<% } %>
</div>
</div>
<% }) %>
</div>
</div>
</div>
<style>
.blockquoteInHelp:before,.blockquoteInHelp:after{
display:none;
}
</style>
<script>userHasSubscribed = <%- !!config.userHasSubscribed %>;</script>
<script src="<%-window.libURL%>assets/js/bs5.help.js"></script>
</main>

View File

@ -2,18 +2,18 @@
const showMonitors = define.SideMenu.showMonitors; %> const showMonitors = define.SideMenu.showMonitors; %>
<nav id="<%- menuInfo.id || 'sidebarMenu' %>" class="<%- menuInfo.class || 'col-md-3 col-lg-2 d-md-block sidebar collapse' %>"> <nav id="<%- menuInfo.id || 'sidebarMenu' %>" class="<%- menuInfo.class || 'col-md-3 col-lg-2 d-md-block sidebar collapse' %>">
<div id="menu-side" class="position-sticky"> <div id="menu-side" class="position-sticky">
<div class="align-items-center d-flex flex-row p-3 my-3 m-3 btn-default rounded shadow-sm cursor-pointer" style="overflow: hidden;"> <div class="align-items-center d-flex flex-row my-3 m-3 btn-default rounded shadow-sm cursor-pointer" style="overflow: hidden;">
<div class="align-items-center mr-3"> <div class="align-items-center mr-3 py-3 pl-3">
<div class="lh-1" <%- showMonitors ? `data-bs-toggle="collapse" data-bs-target=".home-collapse"` : '' %>> <div class="lh-1 <%- showMonitors ? `toggle-menu-collapse` : '' %>">
<img src="<%- window.libURL + config.logoLocation196x196 %>" width="42" style="<%- config.logoLocation76x76Style %>"> <img src="<%- window.libURL + config.logoLocation196x196 %>" width="42" style="<%- config.logoLocation76x76Style %>">
</div> </div>
</div> </div>
<div class="flex-grow-1 align-items-center mr-3" <%- showMonitors ? `data-bs-toggle="collapse" data-bs-target=".home-collapse"` : '' %>> <div class="flex-grow-1 align-items-center mr-3 py-3 <%- !showMonitors ? `pr-3` : '' %> <%- showMonitors ? `toggle-menu-collapse` : '' %>">
<h1 class="h6 mb-0 lh-1 text-ellipsis" style="overflow: hidden;"><%- $user.mail %></h1> <h1 class="h6 mb-0 lh-1 text-ellipsis" style="overflow: hidden;"><%- $user.mail %></h1>
<small><%- lang.Monitors %> : <span class="cameraCount"></span></small> <small><%- lang.Monitors %> : <span class="cameraCount"></span></small>
</div> </div>
<% if(showMonitors){ %> <% if(showMonitors){ %>
<div class="align-items-center mr-3 btn-default rounded shadow-sm cursor-pointer"> <div class="align-items-center mr-3 py-3 pr-3 btn-default rounded shadow-sm cursor-pointer">
<div class="lh-1"> <div class="lh-1">
<a class_toggle="compressed-monitor-icons" data-target="#monitorSideList" icon-toggle="fa-th fa-bars" icon-child="i"><i class="fa fa-th"></i></a> <a class_toggle="compressed-monitor-icons" data-target="#monitorSideList" icon-toggle="fa-th fa-bars" icon-child="i"><i class="fa fa-th"></i></a>
</div> </div>
@ -28,7 +28,7 @@
include fieldBuilders.ejs include fieldBuilders.ejs
%> %>
<% drawBlock(define.SideMenu.blocks.SideMenuBeforeList) %> <% drawBlock(define.SideMenu.blocks.SideMenuBeforeList) %>
<div class="collapse show home-collapse"> <div id="side-menu-collapse-point" class="collapse show home-collapse">
<ul id="pageTabLinks" class="nav flex-column nav-pills"> <ul id="pageTabLinks" class="nav flex-column nav-pills">
<div class="pb-2 px-3" id="home-collapse"> <div class="pb-2 px-3" id="home-collapse">
<% menuInfo.links.forEach((item) => { <% menuInfo.links.forEach((item) => {

View File

@ -0,0 +1,21 @@
<main class="container page-tab pt-3" id="tab-onvifDeviceManager">
<form class="dark row">
<%
var drawBlock
var buildOptions
%>
<%
include fieldBuilders.ejs
%>
<% Object.keys(define['ONVIF Device Manager'].blocks).forEach(function(blockKey){
var accountSettings = define['ONVIF Device Manager'].blocks[blockKey]
drawBlock(accountSettings)
}) %>
<div class="col-md-12">
<%
include stickySubmitBar.ejs
%>
</div>
</form>
</main>
<script src="<%-window.libURL%>assets/js/bs5.onvifDeviceManager.js"></script>

View File

@ -1,8 +1,11 @@
<div class="my-3 p-3 <%- define.Theme.isDark ? 'bg-dark text-white' : 'bg-light' %> rounded shadow-sm" id="recentVideos"> <div class="my-3 p-3 <%- define.Theme.isDark ? 'bg-dark text-white' : 'bg-light' %> rounded shadow-sm" id="recentVideos">
<h4 class="mb-3 pb-3 <%- define.Theme.isDark ? 'text-white' : '' %> border-bottom-dotted border-color-blue row"> <h4 class="mb-3 pb-3 <%- define.Theme.isDark ? 'text-white' : '' %> border-bottom-dotted border-color-blue row">
<div class="col-md-8"> <div class="col-md-7">
<%- lang['Recent Videos'] %> <%- lang['Recent Videos'] %>
</div> </div>
<div class="col-md-1 text-right">
<a class="btn btn-primary btn-sm recent-videos-refresh">&nbsp;<i class="fa fa-refresh">&nbsp;</i></a>
</div>
<div class="col-md-4"> <div class="col-md-4">
<select class="form-control form-control-sm <%- define.Theme.isDark ? 'btn-dark' : '' %> text-start monitors_list" type="text" id="recentVideosMonitorId"> <select class="form-control form-control-sm <%- define.Theme.isDark ? 'btn-dark' : '' %> text-start monitors_list" type="text" id="recentVideosMonitorId">
<option value=""><%- lang['All Monitors'] %></option> <option value=""><%- lang['All Monitors'] %></option>

View File

@ -1,7 +1,10 @@
<head> <head>
<!-- Powered by Shinobi, http://shinobi.video --> <!-- Powered by Shinobi, http://shinobi.video -->
<% include blocks/header-title.ejs %> <% include blocks/header-title.ejs %>
<% if(!window.libURL)window.libURL = originalURL + global.s.checkCorrectPathEnding(config.webPaths.home) %> <%
if(config.baseURL)window.libURL = config.baseURL;
if(!window.libURL)window.libURL = originalURL;
%>
<% include blocks/header-meta.ejs %> <% include blocks/header-meta.ejs %>
<meta http-equiv="content-type" content="text/html;charset=UTF-8"> <meta http-equiv="content-type" content="text/html;charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">

View File

@ -33,7 +33,7 @@
a {cursor:pointer} a {cursor:pointer}
</style> </style>
<script> <script>
var superApiPrefix = location.search === '?p2p=1' ? location.pathname + '/' : "<%=originalURL%><%=config.webPaths.superApiPrefix%>" var superApiPrefix = location.search === '?p2p=1' ? (location.pathname.endsWith('/') ? location.pathname : location.pathname) : "<%=originalURL%><%=config.webPaths.superApiPrefix%>"
</script> </script>
<% customAutoLoad.superLibsCss.forEach(function(lib){ %> <% customAutoLoad.superLibsCss.forEach(function(lib){ %>
<link rel="stylesheet" href="<%-window.libURL%>libs/css/<%-lib%>"> <link rel="stylesheet" href="<%-window.libURL%>libs/css/<%-lib%>">