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/plugins"]
VOLUME ["/home/Shinobi/libs/customAutoLoad"]
VOLUME ["/config"]
VOLUME ["/customAutoLoad"]
VOLUME ["/var/lib/mysql"]
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/plugins"]
VOLUME ["/home/Shinobi/libs/customAutoLoad"]
VOLUME ["/config"]
VOLUME ["/customAutoLoad"]
VOLUME ["/var/lib/mysql"]
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/plugins"]
VOLUME ["/home/Shinobi/libs/customAutoLoad"]
VOLUME ["/config"]
VOLUME ["/customAutoLoad"]
VOLUME ["/var/lib/mysql"]
EXPOSE 8080 443 21 25

View File

@ -125,7 +125,7 @@ if [ "${ffmpeginstall^}" = "Y" ]; then
elif [ "$version" = 8 ]; then
#Enable Negativo17 repo for FFMPEG (CentOS 8)
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 dnf install ffmpeg ffmpeg-devel -y -q -e 0
fi
@ -148,7 +148,7 @@ if [ "${installdbserver^}" = "Y" ] || [ "${installdbserver^}" = "" ]; then
if [ "${securedbserver^}" = "Y" ]; then
#Configure basic security for MariaDB
sudo mysql_secure_installation
sudo mariadb-secure-installation
else
echo "========================================================="
echo "Skipping database server security configuration..."

View File

@ -76,7 +76,7 @@ if [ "$mysqlagree" = "y" ] || [ "$mysqlagree" = "Y" ]; then
sudo systemctl start mariadb
sudo systemctl enable mariadb
#Run mysql install
sudo mysql_secure_installation
sudo mariadb-secure-installation
fi
echo "============="
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) => {
postMessage({f:'s.deleteFileBinEntry',file:x})
}
const setDiskUsedForGroup = (groupKey,size,target) => {
postMessage({f:'s.setDiskUsedForGroup', ke: groupKey, size: size, target: target})
const setDiskUsedForGroup = (groupKey,size,target,videoRow) => {
postMessage({f:'s.setDiskUsedForGroup', ke: groupKey, size: size, target: target, videoRow: videoRow})
}
const getVideoDirectory = function(e){
if(e.mid&&!e.id){e.id=e.mid};
@ -185,6 +185,7 @@ const checkFilterRules = function(v){
})
}
const deleteVideosByDays = async (v,days,addedQueries) => {
const groupKey = v.ke;
const whereQuery = [
['ke','=',v.ke],
['time','<', sqlDate(days+' DAY')],
@ -207,7 +208,8 @@ const deleteVideosByDays = async (v,days,addedQueries) => {
const filename = formattedTime(row.time) + '.' + row.ext
try{
await fs.promises.unlink(dir + filename)
row.size += clearSize
const fileSizeMB = row.size / 1048576;
setDiskUsedForGroup(groupKey,-fileSizeMB,null,row)
sendToWebSocket({
f: 'video_delete',
filename: filename + '.' + row.ext,
@ -227,7 +229,6 @@ const deleteVideosByDays = async (v,days,addedQueries) => {
where: whereQuery
})
affectedRows = deleteResponse.rows || 0
setDiskUsedForGroup(v.ke,-clearSize)
}
return {
ok: true,

View File

@ -120,6 +120,7 @@ module.exports = function(s,config,lang){
{
"fieldType": 'div',
"id": "monitorPresetsSelection",
"style": "max-height:400px;overflow:auto;",
"class": "mdl-list"
},
{
@ -497,23 +498,23 @@ module.exports = function(s,config,lang){
"form-group-class": "input-mapping",
"possible": [
{
"name": lang['All streams in first feed'] + '(0, ' + lang.Default + ')',
"name": lang['All streams in first feed'] + ' (0, ' + lang.Default + ')',
"value": "0"
},
{
"name": lang['First stream in feed'] + '(0:0)',
"name": lang['First stream in feed'] + ' (0:0)',
"value": "0:0"
},
{
"name": lang['Second stream in feed'] + "(0:1)",
"name": lang['Second stream in feed'] + " (0:1)",
"value": "0:1"
},
{
"name": lang['Video streams only'] + "(0:v)",
"name": lang['Video streams only'] + " (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"
}
]
@ -764,6 +765,10 @@ module.exports = function(s,config,lang){
"name": lang['HLS (includes Audio)'],
"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."
},
{
"name": lang.useSubStreamOnlyWhenWatching,
"value": "useSubstream",
}
]
},
@ -1285,16 +1290,20 @@ module.exports = function(s,config,lang){
isAdvanced: true,
"isSection": true,
"id": "monSectionSubstream",
"blockquote": lang.substreamText,
"blockquoteClass": 'global_tip',
"info": [
{
"name": lang['Connection'],
"color": "orange",
id: "monSectionSubstreamInput",
"blockquote": lang.substreamConnectionText,
"blockquoteClass": 'global_tip',
isSection: true,
isFormGroupGroup: true,
"info": [
{
name:'map-detail=type',
name:'detail-substream-input=type',
field:lang['Input Type'],
default:'h264',
attribute:'selector="h_i_SUBSTREAM_FIELDS"',
@ -1303,7 +1312,8 @@ module.exports = function(s,config,lang){
possible:[
{
"name": "H.264 / H.265 / H.265+",
"value": "h264"
"value": "h264",
selected: true,
},
{
"name": "JPEG",
@ -1371,6 +1381,7 @@ module.exports = function(s,config,lang){
{
"name": lang.Yes,
"value": "1",
selected: true,
}
]
},
@ -1385,7 +1396,8 @@ module.exports = function(s,config,lang){
{
"name": lang.Auto,
"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",
@ -1410,6 +1422,7 @@ module.exports = function(s,config,lang){
{
"name": lang.No,
"value": "0",
selected: true,
},
{
"name": lang.Yes,
@ -1438,7 +1451,8 @@ module.exports = function(s,config,lang){
possible:[
{
"name": lang.Auto + '('+lang.Recommended+')',
"value": ""
"value": "",
selected: true,
},
{
"name": lang.NVIDIA,
@ -1516,6 +1530,8 @@ module.exports = function(s,config,lang){
"name": lang['Output'],
"color": "blue",
id: "monSectionSubstreamOutput",
"blockquote": lang.substreamOutputText,
"blockquoteClass": 'global_tip',
isSection: true,
isFormGroupGroup: true,
"info": [
@ -1531,41 +1547,22 @@ module.exports = function(s,config,lang){
{
"name": lang.Poseidon,
"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'],
"value": "mjpeg",
"info": "Standard Motion JPEG image. No audio."
},
{
"name": lang['FLV'],
"value": "flv",
"info": "Sending FLV encoded frames over WebSocket."
},
{
"name": lang['HLS (includes Audio)'],
"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'],
"name": `detail-substream-output="stream_mjpeg_clients"`,
@ -1577,14 +1574,15 @@ module.exports = function(s,config,lang){
"name": `detail-substream-output="stream_vcodec"`,
"description": "Video codec for streaming.",
"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",
"selector": "h_hls_v_channel_SUBSTREAM_FIELDS",
"possible": [
{
"name": lang.Auto,
"value": "no",
"info": "Let FFMPEG choose."
"info": "Let FFMPEG choose.",
selected: true,
},
{
"name": "libx264",
@ -1659,12 +1657,13 @@ module.exports = function(s,config,lang){
"default": "",
"example": "",
"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": [
{
"name": lang.Auto,
"info": "Let FFMPEG choose.",
"value": ""
"value": "",
selected: true,
},
{
"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.",
"default": "15",
"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",
"possible": "1-23"
},
@ -1752,7 +1751,7 @@ module.exports = function(s,config,lang){
"name": "detail-substream-output=stream_fps",
"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.",
"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",
},
{
@ -1762,7 +1761,7 @@ module.exports = function(s,config,lang){
"fieldType": "number",
"numberMin": "1",
"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",
},
{
@ -1772,7 +1771,7 @@ module.exports = function(s,config,lang){
"fieldType": "number",
"numberMin": "1",
"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",
},
{
@ -1780,7 +1779,7 @@ module.exports = function(s,config,lang){
"field": lang["Rotate"],
"description": "Change the viewing angle of the video stream.",
"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",
"possible": [
{
@ -1813,7 +1812,7 @@ module.exports = function(s,config,lang){
"name": "detail-substream-output=svf",
"field": lang["Video Filter"],
"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",
},
{
@ -2425,6 +2424,10 @@ module.exports = function(s,config,lang){
"name": `.5 ${lang.minutes}`,
"value": "30"
},
{
"name": `1 ${lang.minute}`,
"value": "60"
},
{
"name": `5 ${lang.minutes}`,
"value": "300"
@ -5103,11 +5106,22 @@ module.exports = function(s,config,lang){
"blockquoteClass": "global_tip",
"blockquote": lang.onvifdeviceManagerGlobalTip,
"info": [
{
"field": lang["Monitor"],
"fieldType": "select",
"class": "monitors_list",
"possible": []
},
{
"fieldType": "btn",
"class": `btn-warning onvif-device-reboot`,
"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": {
@ -5987,6 +6001,21 @@ module.exports = function(s,config,lang){
"Schedules": {
"section": "Schedules",
"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": {
"name": lang["Schedules"],
"color": "orange",
@ -6118,8 +6147,23 @@ module.exports = function(s,config,lang){
"Monitor States": {
"section": "Monitor States",
"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": {
"name": lang["Monitor States"],
noHeader: true,
"color": "green",
"section-pre-class": "col-md-6",
"info": [
@ -6758,7 +6802,7 @@ module.exports = function(s,config,lang){
"section": "Monitor Settings Additional Input Map",
"blocks": {
"Connection" : {
"id": `monSectionMap$[NUMBER]`,
"id": `monSectionMap$[TEMP_ID]`,
"name": `${lang['Input Map']} $[NUMBER]`,
"section-class": "input-map",
"color": "orange",
@ -7350,8 +7394,11 @@ module.exports = function(s,config,lang){
<div class="monitor_details">
<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['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['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>
</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",
"icon": "exclamation-triangle"
},
"Show Logs": {
"label": lang['Toggle Substream'],
"class": "warning toggle-monitor-substream",
"icon": "eye"
},
"Control": {
"label": lang['Control'],
"class": "default toggle-live-grid-monitor-ptz-controls",
@ -7523,12 +7575,48 @@ module.exports = function(s,config,lang){
label: `${lang['Calendar']}`,
pageOpen: 'calendarView',
},
{
icon: 'fast-forward',
label: `${lang['Time-lapse']}`,
pageOpen: 'timelapseViewer',
},
{
divider: true,
},
{
icon: 'wrench',
label: `${lang['Monitor Settings']}`,
pageOpen: 'monitorSettings',
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',
label: `${lang['Account Settings']}`,
@ -7541,43 +7629,13 @@ module.exports = function(s,config,lang){
pageOpen: 'subAccountManager',
addUl: true,
},
{
icon: 'compass',
label: `${lang['ShinobiHub']}`,
pageOpen: 'configFinder',
addUl: true,
},
{
icon: 'grav',
label: `${lang['Region Editor']}`,
pageOpen: 'regionEditor',
addUl:true
},
{
icon: 'key',
label: `${lang['API Keys']}`,
pageOpen: 'apiKeys',
},
{
icon: 'align-right',
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
divider: true,
},
{
icon: 'search',
@ -7585,15 +7643,29 @@ module.exports = function(s,config,lang){
pageOpen: 'onvifScanner',
addUl:true
},
{
icon: 'opera',
label: `${lang['ONVIF Device Manager']}`,
pageOpen: 'onvifDeviceManager',
},
{
icon: 'eyedropper',
label: `${lang['FFprobe']}`,
pageOpen: 'cameraProbe',
},
{
icon: 'exclamation-triangle',
label: `${lang['Logs']}`,
pageOpen: 'logViewer',
icon: 'compass',
label: `${lang['ShinobiHub']}`,
pageOpen: 'configFinder',
addUl: true,
},
{
divider: true,
},
{
icon: 'info-circle',
label: `${lang['Help']}`,
pageOpen: 'helpWindow',
},
// {
// icon: 'exclamation-circle',
@ -7710,61 +7782,6 @@ module.exports = function(s,config,lang){
"Power Viewer": {
"section": lang["Power Viewer"],
"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": {
id: "powerVideoVideoPlayback",
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": {
id: "powerVideoTimelineStripsContainer",
noHeader: true,
"color": "bg-gradient-blue text-white",
"section-pre-class": "col-md-12 mt-3",
"section-pre-class": "col-md-4",
"info": [
{
"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>`,
},
]
}
},
}
},
"Calendar": {

View File

@ -23,8 +23,14 @@
"Use Raw Snapshot": "Use Raw Snapshot",
"Login": "Login",
"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",
"Output": "Output",
"SubstreamNotConfigured": "Substream not configured. Open your Monitor Settings and configure it.",
"Substream Process": "Substream Process",
"Welcome": "Welcome!",
"API Key Action Failed": "API Key Action Failed",
@ -508,6 +514,7 @@
"ago": "ago",
"a few seconds": "a few seconds",
"a minute": "a minute",
"minute": "minute",
"minutes": "minutes",
"an hour": "an hour",
"hours": "hours",
@ -577,6 +584,7 @@
"Creation Interval": "Creation Interval",
"Plugin": "Plugin",
"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.",
"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>.",
@ -727,6 +735,7 @@
"NotifyErrorText": "Sending Notification caused an Error",
"Check the Channel ID": "Check the Channel 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.",
"Account Settings": "Account Settings",
"How to Record": "How to Record",
@ -1247,6 +1256,7 @@
"Close All Monitors": "Close All Monitors",
"Daily Events": "Daily Events",
"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.",
"Save Events": "Save Events",
"Original Choice": "Original Choice",

View File

@ -51,7 +51,7 @@
"Enable Nightvision": "Ativar Visão Noturna",
"Disable Nightvision": "Desativar Visão Noturna",
"Current": "Atual",
"Monitors": "Monitors",
"Monitors": "Monitores",
"Video": "Video",
"Videos": "Videos",
"Events": "Eventos",
@ -122,12 +122,12 @@
"Execute Command": "Executar Comando",
"for Global Access": "para Acesso Global",
"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",
"Documentation": "Documentação",
"All Monitors": "Todos Monitores",
"Motion Meter": "Medidos de Movimento",
"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.",
"Motion Meter": "Medição de movimentos",
"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",
"ONVIF Scanner": "ONVIF Scanner",
"Scan Settings": "Configurações de digitalização",
@ -162,7 +162,7 @@
"Browser Console Log": "Navegador de logs",
"All Monitors and Privileges": "Todos monitores e privilégios",
"Permissions": "Permissões",
"Time-lapse Tool": "Ferramenta Time-laps",
"Time-lapse Tool": "Time-lapse",
"total": "total",
"MB": "MB",
"Calendar": "Calendário",
@ -187,11 +187,11 @@
"Add New": "Adicionar novo",
"Delete Selected Videos": "Excluir vídeos selecionados",
"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",
"confirmDeleteFilter": "Deseja excluir este filtro? Você não poderá recuperá-lo.",
"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.",
"dropBoxSuccess": "Sucesso! Arquivos salvos em seu Dropbox.",
"API Key Deleted": "Chave da API excluída",
@ -246,13 +246,13 @@
"Connected": "Conectado",
"Not Connected": "Não conectado",
"Lisence Plate Detector": "Detector de placas",
"OpenCV Cascades": "OpenCV Cascatas",
"OpenCV Cascades": "Cascatas OpenCV ",
"Refresh List of Cascades": "Atualizar Lista de Cascatas",
"\"No Motion\" Detector": "\"Sem movimento\" Detector",
"Control": "Controle",
"Grouping": "Agrupando &nbsp; <small>Adicione grupos em <b>Configurações</b></small>",
"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.",
"noSpecialCharacters": "Sem espaços ou caracteres especiais.",
"NotesPlacholder": "Comentários que você quer deixar para as configurações desta câmera.",
@ -273,7 +273,7 @@
"Path": "Caminho",
"Monitor Capture Rate": "Taxa de captura do monitor <small>(FPS)</small>",
"Analyzation Duration": "Duração da análise",
"Probe Size": "Probe Size",
"Probe Size": "Tamanho da sonda",
"Stream Type": "Tipo de transmissão",
"# of Allow MJPEG Clients": "# para permitir clientes MJPEG <small>0 para infinito</small>",
"HLS Video Encoder": "Codificador de vídeo HLS",
@ -281,9 +281,9 @@
"HLS Segment Length": "Comprimento do segmento HLS <small>em segundos</small>",
"HLS Preset": "Pré-definição 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>",
"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>",
"Width": "Largura",
"Height": "Altura",
@ -444,13 +444,13 @@
"Process Unexpected Exit": "Saída inesperado do processo",
"Process Crashed for Monitor": "Processo de Monitor quebrado",
"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",
"JPEGErrorText": "Houve um problema ao obter dados da sua 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",
"Can't Connect": "Não pode conectar",
"Can't Connect": "Não foi possível conectar",
"Video Finished": "Vídeo finalizado",
"No Monitor Found, Ignoring Request": "Monitor não encontrado, ignorando requisição",
"Event": "Evento",

View File

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

View File

@ -123,13 +123,24 @@ module.exports = function(s,config,lang,app,io){
}
}
async function getSnapshotFromOnvif(onvifOptions){
return await createSnapshot({
output: ['-s 400x400'],
url: addCredentialsToStreamLink({
let theUrl;
if(onvifOptions.mid && onvifOptions.ke){
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,
password: onvifOptions.password,
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
var frameLimit = options.limit
const noLimit = options.noLimit === '1'
const endIsStartTo = options.endIsStartTo
const chosenDate = options.date
const startDate = options.startDate ? stringToSqlTime(options.startDate) : null
@ -217,6 +218,7 @@ module.exports = function(s,config){
whereQuery.push(['filename','=',options.filename])
frameLimit = "1";
}
if(noLimit)frameLimit = '0';
options.orderBy = options.orderBy ? options.orderBy : ['time','desc']
if(options.count)options.groupBy = options.groupBy ? options.groupBy : options.orderBy[0]
knexQuery({
@ -337,7 +339,7 @@ module.exports = function(s,config){
endDate: endTime,
startOperator: startTimeOperator,
endOperator: endTimeOperator,
limit: options.limit,
limit: options.noLimit === '1' ? '0' : options.limit,
archived: archived,
rowType: rowName,
endIsStartTo: endIsStartTo

View File

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

View File

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

View File

@ -185,7 +185,7 @@ module.exports = (s,config,lang) => {
const createStreamChannel = function(e,number,channel){
//`e` is the monitor object
//`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)){
try{
fs.mkdirSync(channelStreamDirectory)
@ -377,7 +377,7 @@ module.exports = (s,config,lang) => {
//x = temporary values
const streamFlags = []
const streamType = e.details.stream_type ? e.details.stream_type : 'hls'
if(streamType !== 'jpeg'){
if(streamType !== 'jpeg' && streamType !== 'useSubstream'){
const isCudaEnabled = hasCudaEnabled(e)
const streamFilters = []
const videoCodecisCopy = e.details.stream_vcodec === 'copy'

View File

@ -35,6 +35,10 @@ module.exports = function(s,config,lang){
cameraDestroy,
monitorConfigurationMigrator,
attachStreamChannelHandlers,
setActiveViewer,
getActiveViewerCount,
destroySubstreamProcess,
attachMainProcessHandlers,
} = require('./monitor/utils.js')(s,config,lang)
const {
addEventDetailsToString,
@ -66,7 +70,7 @@ module.exports = function(s,config,lang){
if(!activeMonitor.contentWriter){activeMonitor.contentWriter={}};
if(!activeMonitor.childNodeStreamWriters){activeMonitor.childNodeStreamWriters={}};
if(!activeMonitor.eventBasedRecording){activeMonitor.eventBasedRecording={}};
if(!activeMonitor.watch){activeMonitor.watch={}};
if(!activeMonitor.watch){activeMonitor.watch = []};
if(!activeMonitor.fixingVideos){activeMonitor.fixingVideos={}};
// if(!activeMonitor.viewerConnection){activeMonitor.viewerConnection={}};
// if(!activeMonitor.viewerConnectionCount){activeMonitor.viewerConnectionCount=0};
@ -151,7 +155,7 @@ module.exports = function(s,config,lang){
return x.ar;
}
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){
return new Promise((resolve,reject) => {
@ -513,20 +517,7 @@ module.exports = function(s,config,lang){
s.checkDetails(e)
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].activeMonitors[e.mid].onvifConnection){
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{
async function getRaw(){
var pathDir = s.dir.streams+e.ke+'/'+e.mid+'/'
const {screenShot, isStaticFile} = await s.getRawSnapshotFromMonitor(s.group[e.ke].rawMonitorConfigurations[e.mid],options)
if(screenShot){
@ -540,7 +531,27 @@ module.exports = function(s,config,lang){
}else{
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)
}
}
}
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{
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
});
//on unexpected exit restart
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)
}
if(s.group[e.ke].activeMonitors[e.id].spawn)attachMainProcessHandlers(e,fatalError)
return s.group[e.ke].activeMonitors[e.id].spawn
}
const createEventCounter = function(monitor){
if(monitor.details.detector_obj_count === '1'){
@ -1235,27 +1199,29 @@ module.exports = function(s,config,lang){
if(pingResponse.success === true){
activeMonitor.isRecording = true
try{
createCameraFfmpegProcess(e)
createCameraStreamHandlers(e)
var mainProcess = createCameraFfmpegProcess(e)
createEventCounter(e)
if(e.type === 'dashcam' || e.type === 'socket'){
setTimeout(function(){
activeMonitor.allowStdinWrite = true
s.txToDashcamUsers({
f : 'enable_stream',
ke : e.ke,
mid : e.id
},e.ke)
},30000)
}
if(
e.functionMode === 'record' ||
e.type === 'mjpeg' ||
e.type === 'h264' ||
e.type === 'local'
){
catchNewSegmentNames(e)
cameraFilterFfmpegLog(e)
if(mainProcess){
createCameraStreamHandlers(e)
if(e.type === 'dashcam' || e.type === 'socket'){
setTimeout(function(){
activeMonitor.allowStdinWrite = true
s.txToDashcamUsers({
f : 'enable_stream',
ke : e.ke,
mid : e.id
},e.ke)
},30000)
}
if(
e.functionMode === 'record' ||
e.type === 'mjpeg' ||
e.type === 'h264' ||
e.type === 'local'
){
catchNewSegmentNames(e)
cameraFilterFfmpegLog(e)
}
}
clearTimeout(activeMonitor.onMonitorStartTimer)
activeMonitor.onMonitorStartTimer = setTimeout(() => {
@ -1511,26 +1477,23 @@ module.exports = function(s,config,lang){
s.initiateMonitorObject({ke:e.ke,mid:e.id})
switch(e.functionMode){
case'watch_on'://live streamers - join
if(!cn.monitorsCurrentlyWatching){cn.monitorsCurrentlyWatching = {}}
if(!cn.monitorsCurrentlyWatching[e.id]){cn.monitorsCurrentlyWatching[e.id]={ke:e.ke}}
s.group[e.ke].activeMonitors[e.id].watch[cn.id]={};
var numberOfViewers = Object.keys(s.group[e.ke].activeMonitors[e.id].watch).length
s.tx({
viewers: numberOfViewers,
ke: e.ke,
id: e.id
},'MON_'+e.ke+e.id)
if(!cn.monitorsCurrentlyWatching){cn.monitorsCurrentlyWatching = {}}
if(!cn.monitorsCurrentlyWatching[e.id]){cn.monitorsCurrentlyWatching[e.id]={ke:e.ke}}
setActiveViewer(e.ke,e.id,cn.id,true)
s.group[e.ke].activeMonitors[e.id].allowDestroySubstream = false
clearTimeout(s.group[e.ke].activeMonitors[e.id].noViewerCountDisableSubstream)
break;
case'watch_off'://live streamers - leave
if(cn.monitorsCurrentlyWatching){delete(cn.monitorsCurrentlyWatching[e.id])}
var numberOfViewers = 0
delete(s.group[e.ke].activeMonitors[e.id].watch[cn.id]);
numberOfViewers = Object.keys(s.group[e.ke].activeMonitors[e.id].watch).length
s.tx({
viewers: numberOfViewers,
ke: e.ke,
id: e.id
},'MON_'+e.ke+e.id)
setActiveViewer(e.ke,e.id,cn.id,false)
clearTimeout(s.group[e.ke].activeMonitors[e.id].noViewerCountDisableSubstream)
s.group[e.ke].activeMonitors[e.id].noViewerCountDisableSubstream = setTimeout(async () => {
let currentCount = getActiveViewerCount(e.ke,e.id)
if(currentCount === 0 && s.group[e.ke].activeMonitors[e.id].subStreamProcess){
s.group[e.ke].activeMonitors[e.id].allowDestroySubstream = true
await destroySubstreamProcess(s.group[e.ke].activeMonitors[e.id])
}
},10000)
break;
case'restart'://restart monitor
s.sendMonitorStatus({

View File

@ -3,7 +3,11 @@ const treekill = require('tree-kill');
const spawn = require('child_process').spawn;
const events = require('events');
const Mp4Frag = require('mp4frag');
const streamViewerCountTimeouts = {}
module.exports = (s,config,lang) => {
const {
scanForOrphanedVideos
} = require('../video/utils.js')(s,config,lang)
const {
createPipeArray,
splitForFFPMEG,
@ -17,6 +21,10 @@ module.exports = (s,config,lang) => {
const processKill = (proc) => {
const response = {ok: true}
return new Promise((resolve,reject) => {
if(!proc){
resolve(response)
return
}
function sendError(err){
response.ok = false
response.err = err
@ -94,13 +102,17 @@ module.exports = (s,config,lang) => {
if(activeMonitor.onChildNodeExit){
activeMonitor.onChildNodeExit()
}
activeMonitor.spawn.stdio.forEach(function(stdio){
try{
stdio.unpipe()
}catch(err){
console.log(err)
}
})
try{
activeMonitor.spawn.stdio.forEach(function(stdio){
try{
stdio.unpipe()
}catch(err){
console.log(err)
}
})
}catch(err){
// s.debugLog(err)
}
if(activeMonitor.mp4frag){
var mp4FragChannels = Object.keys(activeMonitor.mp4frag)
mp4FragChannels.forEach(function(channel){
@ -116,6 +128,7 @@ module.exports = (s,config,lang) => {
}else{
processKill(proc).then((response) => {
s.debugLog(`cameraDestroy`,response)
activeMonitor.allowDestroySubstream = true
destroySubstreamProcess(activeMonitor).then((response) => {
if(response.hadSubStream)s.debugLog(`cameraDestroy`,response.closeResponse)
})
@ -209,13 +222,19 @@ module.exports = (s,config,lang) => {
const spawnSubstreamProcess = function(e){
// e = monitorConfig
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 activeMonitor = s.group[e.ke].activeMonitors[e.mid]
const channelNumber = 1 + (monitorDetails.stream_channels || []).length
const ffmpegCommand = [`-progress pipe:5`];
const logLevel = monitorDetails.loglevel ? e.details.loglevel : 'warning'
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 {
inputAndConnectionFields,
outputFields,
@ -259,8 +278,7 @@ module.exports = (s,config,lang) => {
s.userLog({
ke: e.ke,
mid: e.mid,
},
{
},{
type: lang["Substream Process"],
msg: data.toString()
})
@ -302,21 +320,25 @@ module.exports = (s,config,lang) => {
hadSubStream: false,
alreadyClosing: false
}
if(activeMonitor.subStreamProcessClosing){
response.alreadyClosing = true
}else if(activeMonitor.subStreamProcess){
activeMonitor.subStreamProcessClosing = true
activeMonitor.subStreamChannel = null;
const closeResponse = await processKill(activeMonitor.subStreamProcess)
response.hadSubStream = true
response.closeResponse = closeResponse
delete(activeMonitor.subStreamProcess)
s.tx({
f: 'substream_end',
mid: activeMonitor.mid,
ke: activeMonitor.ke
},'GRP_'+activeMonitor.ke);
activeMonitor.subStreamProcessClosing = false
try{
if(activeMonitor.subStreamProcessClosing){
response.alreadyClosing = true
}else if(activeMonitor.subStreamProcess){
activeMonitor.subStreamProcessClosing = true
activeMonitor.subStreamChannel = null;
const closeResponse = await processKill(activeMonitor.subStreamProcess)
response.hadSubStream = true
response.closeResponse = closeResponse
delete(activeMonitor.subStreamProcess)
s.tx({
f: 'substream_end',
mid: activeMonitor.mid,
ke: activeMonitor.ke
},'GRP_'+activeMonitor.ke);
activeMonitor.subStreamProcessClosing = false
}
}catch(err){
s.debugLog('destroySubstreamProcess',err)
}
return response
}
@ -360,6 +382,97 @@ module.exports = (s,config,lang) => {
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 {
cameraDestroy: cameraDestroy,
createSnapshot: createSnapshot,
@ -369,5 +482,9 @@ module.exports = (s,config,lang) => {
spawnSubstreamProcess: spawnSubstreamProcess,
destroySubstreamProcess: destroySubstreamProcess,
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')
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
*/
@ -26,7 +30,7 @@ module.exports = function(s,config,lang,app,io){
try{
const groupKey = req.params.ke
const monitorId = req.params.id
const onvifDevice = s.group[groupKey].activeMonitors[monitorId].onvifConnection
const onvifDevice = await getOnvifDevice(groupKey,monitorId)
const cameraInfo = await getUIFieldValues(onvifDevice)
endData.onvifData = cameraInfo
}catch(err){
@ -47,7 +51,7 @@ module.exports = function(s,config,lang,app,io){
try{
const groupKey = req.params.ke
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 videoToken = form.VideoConfiguration && form.VideoConfiguration.videoToken ? form.VideoConfiguration.videoToken : null
if(form.DateandTime){
@ -100,7 +104,7 @@ module.exports = function(s,config,lang,app,io){
try{
const groupKey = req.params.ke
const monitorId = req.params.id
const onvifDevice = s.group[groupKey].activeMonitors[monitorId].onvifConnection
const onvifDevice = await getOnvifDevice(groupKey,monitorId)
const cameraInfo = await rebootCamera(onvifDevice)
endData.onvifData = cameraInfo
}catch(err){

View File

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

View File

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

View File

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

View File

@ -220,7 +220,7 @@ module.exports = function(s,config,lang){
filename: filename,
mid: e.id,
ke: e.ke,
time: s.nameToTime(filename),
time: new Date(s.nameToTime(filename)),
end: s.formattedTime(new Date,'YYYY-MM-DD HH:mm:ss')
},'GRP_'+e.ke);
var storageIndex = s.getVideoStorageIndex(e)
@ -471,13 +471,13 @@ module.exports = function(s,config,lang){
file.pipe(res)
return file
}
s.createVideoFromTimelapse = function(timelapseFrames,framesPerSecond,callback){
s.createVideoFromTimelapse = async function(timelapseFrames,framesPerSecond,callback){
framesPerSecond = parseInt(framesPerSecond)
if(!framesPerSecond || isNaN(framesPerSecond))framesPerSecond = 2
var frames = timelapseFrames.reverse()
var ke = frames[0].ke
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 createLocation
frames.forEach(function(frame,frameNumber){
@ -515,7 +515,7 @@ module.exports = function(s,config,lang){
}
},4000)
})
videoBuildProcess.on('exit',function(data){
videoBuildProcess.on('close',function(data){
var timeNow = new Date()
var fileStats = fs.statSync(finalMp4OutputLocation)
var details = {}
@ -547,12 +547,11 @@ module.exports = function(s,config,lang){
if(!err)videoBuildProcess.stdin.write(buffer)
if(currentFile === concatFiles.length - 1){
//is last
}else{
setTimeout(function(){
setTimeout(async function(){
++currentFile
readFile()
},1/framesPerSecond)
},10/framesPerSecond)
}
})
}
@ -565,13 +564,22 @@ module.exports = function(s,config,lang){
msg: lang['Started Building']
})
}else{
callback({
ok: false,
fileExists: true,
filename: finalFileName + '.mp4',
fileLocation: finalMp4OutputLocation,
msg: lang['Already exists']
})
if(s.group[ke].activeMonitors[mid].buildingTimelapseVideo){
callback({
ok: false,
fileExists: false,
fileLocation: finalMp4OutputLocation,
msg: lang.Building
})
}else{
callback({
ok: false,
fileExists: true,
filename: finalFileName + '.mp4',
fileLocation: finalMp4OutputLocation,
msg: lang['Already exists']
})
}
}
}else{
callback({
@ -591,9 +599,11 @@ module.exports = function(s,config,lang){
}
s.getVideoStorageIndex = function(video){
try{
var details = s.parseJSON(video.details) || {}
var storageId = details.storageId
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
const monitorId = video.id || video.mid
const details = s.parseJSON(video.details) || {}
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){
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){
s.auth(req.params,function(user){
const groupKey = req.params.ke;
var endData = {
ok : false
}
@ -106,47 +107,60 @@ module.exports = function(s,config,lang,app){
}
var form = s.getPostData(req) || {}
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({
action: "select",
columns: "*",
table: "API",
table: "Users",
where: [
['ke','=',req.params.ke],
['ke','=',groupKey],
['uid','=',uid],
]
},function(err,rows){
if(rows && rows[0]){
rows.forEach(function(row){
delete(s.api[row.code])
})
},function(err,usersFound){
const theUserUpForDeletion = usersFound[0]
if(theUserUpForDeletion){
s.knexQuery({
action: "delete",
table: "API",
table: "Users",
where: {
ke: req.params.ke,
ke: groupKey,
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)
})
/**

View File

@ -839,13 +839,20 @@ module.exports = function(s,config,lang,app,io){
}else{
const monitorConfig = s.group[groupKey].rawMonitorConfigurations[monitorId]
const activeMonitor = s.group[groupKey].activeMonitors[monitorId]
if(!activeMonitor.subStreamProcess){
response.ok = true
activeMonitor.allowDestroySubstream = false;
spawnSubstreamProcess(monitorConfig)
const substreamConfig = monitorConfig.details.substream
if(
substreamConfig.output
){
if(!activeMonitor.subStreamProcess){
response.ok = true
activeMonitor.allowDestroySubstream = false;
spawnSubstreamProcess(monitorConfig)
}else{
activeMonitor.allowDestroySubstream = true
await destroySubstreamProcess(activeMonitor)
}
}else{
activeMonitor.allowDestroySubstream = true
await destroySubstreamProcess(activeMonitor)
response.msg = lang['Invalid Settings']
}
}
s.closeJsonResponse(res,response);
@ -942,6 +949,7 @@ module.exports = function(s,config,lang,app,io){
endTime: req.query.end,
startTimeOperator: req.query.startOperator,
endTimeOperator: req.query.endOperator,
noLimit: req.query.noLimit,
limit: req.query.limit,
archived: req.query.archived,
endIsStartTo: !!req.query.endIsStartTo,
@ -1014,6 +1022,7 @@ module.exports = function(s,config,lang,app,io){
endTime: req.query.end,
startTimeOperator: req.query.startOperator,
endTimeOperator: req.query.endOperator,
noLimit: req.query.noLimit,
limit: req.query.limit,
endIsStartTo: 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",
"mysql": "^2.18.1",
"node-fetch": "3.0.0-beta.9",
"node-pushover": "^1.0.0",
"node-ssh": "^11.1.1",
"node-telegram-bot-api": "^0.52.0",
"nodemailer": "^6.4.11",
"node-pushover": "^1.0.0",
"pam-diff": "^1.0.0",
"pam-diff": "^1.1.0",
"path": "^0.12.7",
"pipe2pam": "^0.6.2",
"pixel-change": "^1.1.0",
"request": "^2.88.0",
"sat": "^0.7.1",
"shinobi-onvif": "0.1.9",
@ -69,9 +70,9 @@
"node12"
],
"scripts": [
"libs/cameraThread/detector.js",
"libs/cameraThread/singleCamera.js",
"libs/cameraThread/snapshot.js"
"libs/cameraThread/detector.js",
"libs/cameraThread/singleCamera.js",
"libs/cameraThread/snapshot.js"
],
"assets": [
"definitions/**/*",

View File

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

View File

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

View File

@ -29,3 +29,40 @@
.tab-videoPlayer:hover .tab-videoPlayer-event-objects {
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;
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))
}
var getApiKeys = function(callback){
$.get(getApiPrefix('api') + '/list',function(data){
$.getJSON(getApiPrefix('api') + '/list',function(data){
callback(data.keys)
})
}

View File

@ -25,7 +25,7 @@ $(document).ready(function(e){
var form = el.serializeObject()
var flags = 'default'
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){
var html
try{

View File

@ -25,7 +25,7 @@ $(document).ready(function(){
}
var getConfigurationsFromHub = function(rowLimit,skipOver,explore,searchQuery,sortBy,sortDirection,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)
// $.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 || []))

View File

@ -71,6 +71,9 @@ function base64ArrayBuffer(arrayBuffer) {
return base64
}
function getLocationPathName(){
return location.pathname.endsWith('/') ? location.pathname : location.pathname
}
function debugLog(...args){
console.log(...args)
}
@ -198,7 +201,7 @@ function liveStamp(){
}
function loadMonitorsIntoMemory(callback){
$.get(`${getApiPrefix(`monitor`)}`,function(data){
$.getJSON(`${getApiPrefix(`monitor`)}`,function(data){
$.each(data,function(n,monitor){
monitor.details = safeJsonParse(monitor.details)
loadedMonitors[monitor.mid] = monitor
@ -261,6 +264,10 @@ function blipTo(xPageValue,yPageValue){
function openTab(theTab,loadData,backAction,haltTrigger,type){
loadData = loadData ? loadData : {}
if(tabTree && tabTree.back && tabTree.back.name === theTab){
goBackOneTab()
return;
}
saveTabBlipPosition(activeTabName)
var allTabs = $('.page-tab');
allTabs.hide().removeClass('tab-active');
@ -578,7 +585,7 @@ function diffObject(obj1, obj2) {
function getAllSectionsFromDefinition(definitionsBase){
var sections = {}
var addSection = function(section,parentName){
sections[section.name] = {
sections[section.id + section.name] = {
name: section.name,
id: section.id,
color: section.color,
@ -593,7 +600,7 @@ function getAllSectionsFromDefinition(definitionsBase){
}
if(section.blocks){
$.each(section.blocks,function(m,block){
addSection(block)
addSection(block,section.name)
})
}
}
@ -677,14 +684,28 @@ function drawMonitorListToSelector(jqTarget,selectFirst,showId){
.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 logWriterFloodCounter = 0
var logWriterFloodLock = null
function buildLogRow(v){
var monitor = loadedMonitors[v.mid]
var humanMonitorName = monitor ? monitor.name + ` (${monitor.mid}) : ` : ''
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">
<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 class="card-body">
<div>${jsonToHtmlBlock(v.info.msg)}</div>
@ -718,10 +739,12 @@ function logWriterDraw(id,data){
info: data.log,
time: data.time,
})
shakeLogWriterIcon()
$(elementTags).prepend(html).each(function(n,v){
var el = $(v);
if(el.find('.log-item').length > 10){
v.find('.log-item:last').remove()
var theRows = el.find('.log-item')
if(theRows.length > 10){
theRows.last().remove()
}
})
}
@ -853,6 +876,14 @@ $(document).ready(function(){
deleteTab(tabName)
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(){
var el = $(this)
var pageChoice = el.attr('page-open')

View File

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

View File

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

View File

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

View File

@ -214,7 +214,43 @@ function generateDefaultMonitorSettings(){
"detector_cascades": "",
"stream_channels": "",
"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": "[]",
"shfr": "[]"
@ -372,6 +408,7 @@ window.getMonitorEditFormFields = function(){
monitorConfig.details = safeJsonParse(monitorConfig.details)
monitorConfig.details.substream = getSubStreamChannelFields()
monitorConfig.details.groups = getMonitorGroupsSelected()
monitorConfig.details.input_map_choices = monitorSectionInputMapsave()
// 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()
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(){
var baseOptionSet = definitions['Monitor Settings'].blocks.Input.info.find((item) => {return item.name === 'detail=primary_input'}).possible
var newOptGroup = [baseOptionSet]
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){
var mapNumber = n + 1
var newOptionSet = []
@ -464,7 +505,7 @@ function buildMapSelectorOptionsBasedOnAddedMaps(){
function drawInputMapSelectorHtml(options,parent){
if(!options.map)options.map = '';
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">
<select class="form-control form-control-sm" map-input="map">`
$.each(availableInputMapSelections,function(n,optgroup){
@ -473,6 +514,7 @@ function drawInputMapSelectorHtml(options,parent){
html += createOptionHtml({
label: option.name,
value: option.value,
selected: option.value === options.map,
})
})
html += `</optgroup>`
@ -499,12 +541,7 @@ function importIntoMonitorEditor(options){
//get maps
monitorSectionInputMaps.empty()
if(monitorDetails.input_maps && monitorDetails.input_maps !== ''){
var input_maps
try{
input_maps = safeJsonParse(monitorDetails.input_maps)
}catch(er){
input_maps = monitorDetails.input_maps;
}
var input_maps = safeJsonParse(monitorDetails.input_maps)
if(input_maps.length > 0){
showInputMappingFields()
$.each(input_maps,function(n,v){
@ -737,7 +774,7 @@ var mapPlacementInit = function(){
})
}
var monitorSectionInputMapsave = function(){
var mapContainers = $('[input-mapping]');
var mapContainers = monitorEditorWindow.find('[input-mapping]');
var stringForSave = {}
mapContainers.each(function(q,t){
var mapRowElement = $(t).find('.map-row');
@ -751,7 +788,7 @@ var monitorSectionInputMapsave = function(){
});
stringForSave[$(t).attr('input-mapping')] = mapRow;
});
monitorEditorWindow.find('[detail="input_map_choices"]').val(JSON.stringify(stringForSave)).change();
return stringForSave
}
monitorSectionInputMaps.on('click','.delete',function(){
$(this).parents('.input-map').remove()
@ -779,14 +816,9 @@ monitorEditorWindow.on('change','[map-detail]',function(){
})
monitorEditorWindow.on('click','[input-mapping] .add_map_row',function(){
drawInputMapSelectorHtml({},$(this).parents('[input-mapping]').find('.choices'))
monitorSectionInputMapsave()
})
monitorEditorWindow.on('click','[input-mapping] .delete_map_row',function(){
$(this).parents('.map-row').remove()
monitorSectionInputMapsave()
})
monitorEditorWindow.on('change','[map-input]',function(){
monitorSectionInputMapsave()
})
//////////////////
//Stream Channels
@ -808,7 +840,6 @@ var channelPlacementInit = function(){
_this.attr('stream-channel',n)
_this.find('.place').text(n)
_this.find('[input-mapping]').attr('input-mapping','stream_channel-'+n)
monitorSectionInputMapsave()
})
}
var getSubStreamChannelFields = function(){
@ -1064,7 +1095,7 @@ editorForm.find('[name="type"]').change(function(e){
}
// presets
var loadPresets = function(callback){
$.get(getApiPrefix() + '/monitorStates/' + $user.ke,function(d){
$.getJSON(getApiPrefix() + '/monitorStates/' + $user.ke,function(d){
var presets = d.presets
loadedPresets = {}
$.each(presets,function(n,preset){
@ -1461,5 +1492,17 @@ editorForm.find('[name="type"]').change(function(e){
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
})

View File

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

View File

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

View File

@ -149,7 +149,7 @@ function getVideoSnapshot(videoElement,cb){
function runPtzCommand(monitorId,switchChosen){
switch(switchChosen){
case'setHome':
$.get(getApiPrefix(`control`) + '/' + monitorId + '/setHome',function(data){
$.getJSON(getApiPrefix(`control`) + '/' + monitorId + '/setHome',function(data){
console.log(data)
})
break;
@ -170,7 +170,26 @@ function runTestDetectionTrigger(monitorId,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(){
var fileName = $user.details.audio_alert
if(fileName && window.soundAlarmed !== true){
@ -195,7 +214,7 @@ function playAudioAlert(){
function buildStreamUrl(monitorId){
var monitor = loadedMonitors[monitorId]
var streamURL
var streamURL = ''
var streamType = safeJsonParse(monitor.details).stream_type
switch(streamType){
case'jpeg':
@ -216,6 +235,9 @@ function buildStreamUrl(monitorId){
case'b64':
streamURL = 'Websocket'
break;
case'useSubstream':
streamURL = lang['Use Substream']
break;
}
if(!streamURL){
$.each(onBuildStreamUrlExtensions,function(n,extender){
@ -285,7 +307,7 @@ function importM3u8Playlist(textData){
})
$.each(parsedList,function(name,url){
var link = getUrlPieces(url)
var newMon = $.aM.generateDefaultMonitorSettings()
var newMon = generateDefaultMonitorSettings()
newMon.details = JSON.parse(newMon.details)
newMon.mid = 'HLS' + name.toLowerCase()
newMon.name = name
@ -388,7 +410,7 @@ function deleteMonitors(monitorsSelected,afterDelete){
class:'btn-danger',
callback:function(){
$.each(monitorsSelected,function(n,monitor){
$.get(`${getApiPrefix(`configureMonitor`)}/${monitor.mid}/delete`,function(data){
$.getJSON(`${getApiPrefix(`configureMonitor`)}/${monitor.mid}/delete`,function(data){
console.log(data)
if(monitorsSelected.length === n + 1){
//last
@ -403,7 +425,7 @@ function deleteMonitors(monitorsSelected,afterDelete){
class:'btn-danger',
callback:function(){
$.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)
if(monitorsSelected.length === n + 1){
//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;
}
var filterOutMonitorsThatAreAlreadyAdded = function(listOfCameras,callback){
$.get(getApiPrefix(`monitor`),function(monitors){
$.getJSON(getApiPrefix(`monitor`),function(monitors){
var monitorsNotExisting = []
$.each(listOfCameras,function(n,camera){
var matches = false

View File

@ -2,21 +2,56 @@ $(document).ready(function(){
var theBlock = $('#recentVideos')
var theList = $('#recentVideosList')
var monitorList = theBlock.find('.monitors_list')
function drawRowToList(row,toBegin){
function drawRowToList(row,toBegin,returnLastChild){
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){
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 videos = data.videos || {}
$.each(videos,function(n,row){
drawRowToList(row)
})
var videos = data.videos || []
// $.each(videos,function(n,row){
// var createdCardCarrier = drawRowToList(row,false,true)
// bindFrameFindingByMouseMove(createdCardCarrier,row)
// })
drawDaysToList(videos,false)
getCountOfEvents({
startDate: options.startDate,
endDate: options.endDate,
monitorId: options.monitorId,
})
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){
@ -43,12 +78,12 @@ $(document).ready(function(){
liveStamp()
})
})
addOnTabReopen('initial', function () {
theBlock.find('.recent-videos-refresh').click(function(){
var theSelected = `${monitorList.val()}`
drawMonitorListToSelector(monitorList.find('optgroup'))
monitorList.val(theSelected)
loadVideos({
limit: 10,
limit: 20,
monitorId: theSelected || undefined,
},function(){
liveStamp()
@ -59,15 +94,16 @@ $(document).ready(function(){
case'init_success':
drawMonitorListToSelector(monitorList.find('optgroup'))
loadVideos({
limit: 10,
limit: 20,
},function(){
liveStamp()
})
break;
case'video_build_success':
loadVideoData(d)
drawRowToList(createVideoLinks(d),true)
break;
// case'video_build_success':
// loadVideoData(d)
// var createdCardCarrier = drawRowToList(createVideoLinks(d),true)
// bindFrameFindingByMouseMove(createdCardCarrier,row)
// break;
}
})
})

View File

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

View File

@ -3,7 +3,7 @@ var sidebarMenuInner = $('#menu-side')
var pageTabContainer = $('#pageTabContainer')
var topMenu = $('#topMenu')
var monitorSideList = $('#monitorSideList')
var toggleSideBarMenu = $('#toggleSideBarMenu')
var sideMenuCollapsePoint = $('#side-menu-collapse-point')
function buildTabHtml(tabName,tabLabel,tabIcon){
return `<li class="nav-item">
<a class="nav-link side-menu-link" page-open="${tabName}">
@ -97,6 +97,27 @@ function toggleSideMenuVisibility(){
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(){
var monitorBlocks = monitorSideList.find('.monitor_block');
var searchTerms = $(this).val().toLowerCase().split(' ')
@ -140,6 +161,10 @@ onDashboardReady(function(){
drawMonitors()
fixSideMenuScroll()
sortListMonitors()
loadSideMenuCollapseStatus()
$('.toggle-menu-collapse').click(function(){
toggleSideMenuCollapse()
})
})
onWebSocketEvent(function(d){
switch(d.f){

View File

@ -134,12 +134,6 @@ $('body')
dropdownToggles[keyName] = value
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(){
var _this = $(this)
var type = 'password'

View File

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

View File

@ -24,6 +24,7 @@ $(document).ready(function(e){
var downloaderIsChecking = false
var allowKeepChecking = true
var selectedFps = 15
var currentPlaylistArray = []
var openTimelapseWindow = function(monitorId,startDate,endDate){
drawTimelapseWindowElements(monitorId,startDate,endDate)
@ -70,7 +71,11 @@ $(document).ready(function(e){
if(!startDate)startDate = dateRange.startDate
if(!endDate)endDate = dateRange.endDate
if(!selectedMonitor)selectedMonitor = monitorsList.val()
var queryString = ['start=' + startDate,'end=' + endDate]
var queryString = [
'start=' + startDate,
'end=' + endDate,
'noLimit=1'
]
var frameIconsHtml = ''
var apiURL = apiBaseUrl + '/timelapse/' + $user.ke + '/' + selectedMonitor
$.getJSON(apiURL + '?' + queryString.join('&'),function(data){
@ -82,7 +87,7 @@ $(document).ready(function(e){
$.each(data.reverse(),function(n,fileInfo){
fileInfo.href = apiURL + '/' + fileInfo.filename.split('T')[0] + '/' + fileInfo.filename
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
})
currentPlaylistArray = data
@ -90,6 +95,7 @@ $(document).ready(function(e){
frameIcons.find(`.frame:first`).click()
// getLiveStream()
resetFilmStripPositions()
loadVisibleTimelapseFrames()
}else{
frameIconsHtml = lang['No Data']
frameIcons.html(frameIconsHtml)
@ -193,7 +199,7 @@ $(document).ready(function(e){
title: lang.Delete,
},
clickCallback: function(){
$.get(frame.href + '/delete',function(response){
$.getJSON(frame.href + '/delete',function(response){
if(response.ok){
el.parent().remove()
}
@ -231,7 +237,7 @@ $(document).ready(function(e){
setDownloadButtonLabel(lang['Automatic Checking Cancelled'])
downloadRecheckTimers[timerId] = setTimeout(function(){
setDownloadButtonLabel(lang['Build Video'], 'database')
},5000)
},30000)
downloaderIsChecking = false
allowKeepChecking = true
return
@ -258,7 +264,7 @@ $(document).ready(function(e){
downloadRecheckTimers[timerId] = setTimeout(function(){
setDownloadButtonLabel(lang['Please Wait or Click to Stop Checking'], 'spinner fa-pulse')
runDownloader()
},5000)
},30000)
}
})
}
@ -269,6 +275,25 @@ $(document).ready(function(e){
playInterval = 1000 / ev.value
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(){
var el = $(this).parents('[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(){
var theBlock = $('#tab-videoPlayer')
window.createVideoPlayerTab = function(video){
var newTabId = `videoPlayer-${video.mid}-${moment(video.time).format('YYYY-MM-DD-HH-mm-ss')}`
window.createVideoPlayerTab = function(video,timeStart){
var newTabId = getVideoPlayerTabId(video)
var humanStartTime = formattedTime(video.time,true)
var humanEndTime = formattedTime(video.end,true)
var tabLabel = `<b>${lang['Video']}</b> : ${loadedMonitors[video.mid].name} : ${formattedTime(video.time,true)}`
@ -32,40 +35,46 @@ $(document).ready(function(){
})
eventMatrixHtml += `</div>`
}
var baseHtml = `<main class="container page-tab tab-videoPlayer" id="tab-${newTabId}" video-id="${video.mid}${video.time}">
<div class="my-3 ${definitions.Theme.isDark ? 'bg-dark text-white' : 'bg-light text-dark'} rounded shadow-sm">
<div class="p-3">
<h6 class="video-title border-bottom-dotted border-bottom-dark pb-2 mb-0">${tabLabel}</h6>
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="mt-3 ${definitions.Theme.isDark ? 'bg-dark text-white' : 'bg-light text-dark'} rounded shadow-sm" style="overflow:hidden">
<div class="d-flex flex-row">
<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 style="position: relative">
<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>
</div>
<div class="p-3">
<div class="d-block">
<a class="btn btn-sm btn-success" download href="${videoUrl}"><i class="fa fa-download"></i> ${lang.Download}</a>
</div>
<div class="d-block">
<b class="flex-grow-1">${lang.Started}</b>
<div class="video-time">${humanStartTime}</div>
</div>
<div class="d-block">
<b class="flex-grow-1">${lang.Ended}</b>
<div class="video-end">${humanEndTime}</div>
</div>
<div class="d-block">
${eventMatrixHtml}
</div>
<small class="d-block text-end mt-3">
<a class="go-back btn badge">${lang['Back']}</a>
</small>
<div class="d-flex flex-row">
<div class="d-block p-3">
<b class="flex-grow-1">${lang.Started}</b>
<div class="video-time">${humanStartTime}</div>
</div>
<div class="d-block p-3">
<b class="flex-grow-1">${lang.Ended}</b>
<div class="video-end">${humanEndTime}</div>
</div>
</div>
<div class="mb-3">
${eventMatrixHtml}
</div>
<div class="d-flex flex-row">
<div class="flex-grow-1 bg-gradient-blue">
<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>
</div>
<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>
</main>`
var tabCreateResponse = createNewTab(newTabId,tabLabel,baseHtml,{},null,'videoPlayer')
console.log(tabCreateResponse)
var videoElement = tabCreateResponse.theTab.find('.tab-videoPlayer-video-element')[0]
if(!tabCreateResponse.existAlready){
var videoElement = tabCreateResponse.theTab.find('.tab-videoPlayer-video-element')[0]
var videoObjectContainer = tabCreateResponse.theTab.find('.tab-videoPlayer-event-objects')
var videoHeight = videoObjectContainer.height()
var videoWidth = videoObjectContainer.width()
@ -83,6 +92,7 @@ $(document).ready(function(){
}
}
}
if(timeStart)videoElement.currentTime = timeStart;
}
window.closeVideoPlayer = function(tabId){
console.log('closeVideoPlayer')

View File

@ -1,4 +1,38 @@
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){
var details = safeJsonParse(video.details)
var queryString = []
@ -32,7 +66,7 @@ function createVideoLinks(video){
video.details = details
return video
}
function applyEventListToVideos(videos,events){
function applyDataListToVideos(videos,events,keyName,reverseList){
var updatedVideos = videos.concat([])
var currentEvents = events.concat([])
updatedVideos.forEach(function(video){
@ -41,48 +75,182 @@ function applyEventListToVideos(videos,events){
var startTime = new Date(video.time)
var endTime = new Date(video.end)
var eventTime = new Date(theEvent.time)
if(eventTime >= startTime && eventTime <= endTime){
if(theEvent.mid === video.mid && eventTime >= startTime && eventTime <= endTime){
videoEvents.push(theEvent)
currentEvents.splice(index, 1)
}
})
video.events = videoEvents
if(reverseList)videoEvents = videoEvents.reverse()
video[keyName || 'events'] = videoEvents
})
return updatedVideos
}
function createVideoRow(row,classOverride){
var possibleEventFrames = ''
var hasRows = row.events && row.events.length > 0
if(hasRows){
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">${row.events.length}</span></th>
</tr>`
$.each(([]).concat(row.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(row.events,function(n,theEvent){
$.each(theEvent.details.matrices,function(n,matrix){
if(!objectsFound[matrix.tag])objectsFound[matrix.tag] = 1
++objectsFound[matrix.tag]
function applyTimelapseFramesListToVideos(videos,events,keyName,reverseList){
var thisApiPrefix = getApiPrefix() + '/timelapse/' + $user.ke + '/'
var newVideos = applyDataListToVideos(videos,events,keyName,reverseList)
newVideos.forEach(function(video){
video.timelapseFrames.forEach(function(row){
var apiURL = thisApiPrefix + row.mid
row.href = libURL + apiURL + '/' + row.filename.split('T')[0] + '/' + row.filename
})
})
return newVideos
}
function getFrameOnVideoRow(percentageInward,video){
var startTime = new Date(video.time)
var endTime = new Date(video.end)
var timeDifference = endTime - startTime
var timeInward = timeDifference / (100 / percentageInward)
var timeAdded = new Date(startTime.getTime() + timeInward) // ms
var foundFrame = video.timelapseFrames.find(function(row){
return new Date(row.time) > timeAdded
});
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(objectsFound,function(tag,count){
eventMatrixHtml += `<tr>
<td class="${definitions.Theme.isDark ? 'text-white' : ''}" style="text-transform:capitalize">${tag}</td>
<td class="text-end"><span class="badge ${definitions.Theme.isDark ? 'bg-light text-dark' : 'bg-dark text-white'} rounded-pill">${count}</span></td>
</tr>`
})
eventMatrixHtml += `</table>`
$.each(video.events,function(n,theEvent){
$.each(theEvent.details.matrices,function(n,matrix){
if(!objectsFound[matrix.tag])objectsFound[matrix.tag] = 1
++objectsFound[matrix.tag]
})
})
$.each(objectsFound,function(tag,count){
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
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-header d-flex flex-row">
<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>
</div>
</div>
<div class="card-body">
<div title="${row.time}" class="d-flex flex-row">
<div class="flex-grow-1">
${moment(row.time).fromNow()}
<div class="video-time-img">
<div class="card-body">
<div title="${row.time}" class="d-flex flex-row">
<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>
<small class="text-muted">~${durationBetweenTimes(row.time,row.end)} ${lang.Minutes}</small>
<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>
<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 class="card-footer p-0">
${hasRows ? eventMatrixHtml : `<div class="p-3"><small class="text-muted">${lang['No Events found for this video']}</small></div>`}
<div class="video-time-strip card-footer p-0">
${eventMatrixHtml}
<div class="video-time-needle video-time-needle-seeker" style="z-index: 2"></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){
var theVideoList = $(targetElement)
theVideoList.empty()
@ -146,14 +380,17 @@ function getVideos(options,callback){
eventEndTime = formattedTimeForFilename(options.endDate,false)
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
$.get(`${getApiPrefix(`events`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.join('&')}`,function(eventData){
var newVideos = applyEventListToVideos(videos,eventData)
$.each(newVideos,function(n,video){
loadVideoData(video)
$.getJSON(`${getApiPrefix(`timelapse`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.concat([`noLimit=1`]).join('&')}`,function(timelapseFrames){
$.getJSON(`${getApiPrefix(`events`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.concat([`noLimit=1`]).join('&')}`,function(eventData){
var newVideos = applyDataListToVideos(videos,eventData)
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){
requestQueries.push(`onlyCount=1`)
}
$.get(`${getApiPrefix(`events`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.join('&')}`,function(eventData){
$.getJSON(`${getApiPrefix(`events`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.join('&')}`,function(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(){
$('body')
.on('click','.open-video',function(){
@ -190,6 +447,15 @@ $(document).ready(function(){
var video = loadedVideosInMemory[`${monitorId}${videoTime}`]
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(){
var el = $(this).parents('[data-mid]')
var monitorId = el.attr('data-mid')
@ -206,7 +472,7 @@ $(document).ready(function(){
class: 'btn-danger btn-sm'
},
clickCallback: function(){
$.get(videoEndpoint + '/delete',function(data){
$.getJSON(videoEndpoint + '/delete',function(data){
if(data.ok){
console.log('Video Deleted')
}else{

View File

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

View File

@ -115,7 +115,7 @@ $(document).ready(function(){
var selectedServer = p2pServerList[currentlyRegisteredP2PServer]
console.log(selectedServer,currentlySelectedP2PServerId,p2pServerList)
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');
win.focus();
}else{

View File

@ -86,7 +86,7 @@
</div>
</div>
<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>
<% include blocks/confirm.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/poseidon.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/chart.js@2.9.4/dist/Chart.min.js" integrity="sha384-zNy6FEbO50N+Cg5wap8IKA4M/ZnLJgzc6w2NqACZaK0u0FXfOWRRJOnQtpZun8ha" 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="<%-window.libURL%>libs/js/daterangepicker.js"></script>
<script src="<%-window.libURL%>libs/js/placeholder.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>addStorage = <%- JSON.stringify(addStorage) %>;</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 = [
<% if(config.availableHWAccels) {
var methods = {

View File

@ -1,11 +1,13 @@
<% window.additionalJsScripts = []; %>
<%
if(config.baseURL)window.libURL = config.baseURL;
if(!window.libURL)window.libURL = originalURL;
%>
<!DOCTYPE html>
<html lang="en">
<head>
<title><%-pageTitle%></title>
<div><%-window.libURL%></div>
<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 name="description" content="Shinobi">
@ -62,7 +64,7 @@
if(!details.sub){ %>
<script>
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>
<% }

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/powerVideo',
'home/onvifScanner',
'home/onvifDeviceManager',
'home/configFinder',
'home/logViewer',
'home/calendar',
'home/eventListWithPics',
'confirm',
'help',
'home/help',
]).forEach(function(block){ %>
<%- 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; %>
<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 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 mr-3">
<div class="lh-1" <%- showMonitors ? `data-bs-toggle="collapse" data-bs-target=".home-collapse"` : '' %>>
<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 py-3 pl-3">
<div class="lh-1 <%- showMonitors ? `toggle-menu-collapse` : '' %>">
<img src="<%- window.libURL + config.logoLocation196x196 %>" width="42" style="<%- config.logoLocation76x76Style %>">
</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>
<small><%- lang.Monitors %> : <span class="cameraCount"></span></small>
</div>
<% 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">
<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>
@ -28,7 +28,7 @@
include fieldBuilders.ejs
%>
<% 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">
<div class="pb-2 px-3" id="home-collapse">
<% 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">
<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'] %>
</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">
<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>

View File

@ -1,7 +1,10 @@
<head>
<!-- Powered by Shinobi, http://shinobi.video -->
<% 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 %>
<meta http-equiv="content-type" content="text/html;charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">

View File

@ -33,7 +33,7 @@
a {cursor:pointer}
</style>
<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>
<% customAutoLoad.superLibsCss.forEach(function(lib){ %>
<link rel="stylesheet" href="<%-window.libURL%>libs/css/<%-lib%>">