Dindai Hollow
- Timeline (Power Viewer v10) - Monitor Map - PTZ Control Adjustments - Critical Fixes and QOL changes - Update CUDA 10 and 10.2 installers - Add "Mark" button to quickly label a video - Add ZH language - Add NVMPI to HW Accel Encoder optionsnode-20
parent
9844de231d
commit
a9653d6517
|
@ -29,7 +29,7 @@ fi
|
|||
if [ -x "$(command -v yum)" ]; then
|
||||
sudo yum-config-manager --add-repo http://developer.download.nvidia.com/compute/cuda/repos/rhel7/x86_64/cuda-rhel7.repo
|
||||
sudo yum clean all
|
||||
sudo yum -y install nvidia-driver-latest-dkms cuda
|
||||
sudo yum -y install nvidia-driver-latest-dkms cuda-toolkit-10-2
|
||||
sudo yum -y install cuda-drivers
|
||||
wget https://cdn.shinobi.video/installers/libcudnn7-7.6.5.33-1.cuda10.2.x86_64.rpm -O cuda-dnn.rpm
|
||||
sudo yum -y localinstall cuda-dnn.rpm
|
||||
|
|
|
@ -29,7 +29,7 @@ if [ -x "$(command -v yum)" ]; then
|
|||
wget https://developer.download.nvidia.com/compute/cuda/repos/rhel7/x86_64/cuda-repo-rhel7-10.0.130-1.x86_64.rpm
|
||||
sudo rpm -i cuda-repo-rhel7-10.0.130-1.x86_64.rpm
|
||||
sudo yum clean all
|
||||
sudo yum install cuda
|
||||
sudo yum install cuda-toolkit-10-0 -y
|
||||
wget https://cdn.shinobi.video/installers/libcudnn7-7.6.5.32-1.cuda10.0.x86_64.rpm -O cuda-dnn.rpm
|
||||
sudo yum -y localinstall cuda-dnn.rpm
|
||||
wget https://cdn.shinobi.video/installers/libcudnn7-devel-7.6.5.32-1.cuda10.0.x86_64.rpm -O cuda-dnn-dev.rpm
|
||||
|
|
|
@ -73,6 +73,7 @@ if ! [ -x "$(command -v mysql)" ]; then
|
|||
#Start mysql and enable on boot
|
||||
sudo systemctl start mariadb
|
||||
sudo systemctl enable mariadb
|
||||
ln -s /usr/bin/mariadb /usr/bin/mysql
|
||||
fi
|
||||
|
||||
echo "========================================================="
|
||||
|
|
|
@ -51,8 +51,8 @@ http://shinobi.video/why
|
|||
|
||||
Moe Alam, Shinobi Systems
|
||||
|
||||
Shinobi is developed by many contributors. Please have a look at the commits to see some of who they are :)
|
||||
https://gitlab.com/Shinobi-Systems/Shinobi/-/commits/dev
|
||||
Shinobi is developed by many contributors. See here
|
||||
https://gitlab.com/Shinobi-Systems/Shinobi/-/graphs/dev
|
||||
|
||||
## Support the Development
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ module.exports = function(s,config,lang){
|
|||
"blocks": {
|
||||
"Page Control": {
|
||||
name: lang.Monitor,
|
||||
headerTitle: `<div id="tab-monitorSettings-title">Monitor Settings : <span>Add New</span></div>`,
|
||||
headerTitle: `<div id="tab-monitorSettings-title">${lang['Monitor Settings']} : <span>Add New</span></div>`,
|
||||
"color": "blue",
|
||||
isSection: false,
|
||||
"info": [
|
||||
|
@ -140,7 +140,34 @@ module.exports = function(s,config,lang){
|
|||
"value": "1"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "detail=geolocation",
|
||||
"field": lang["Geolocation"],
|
||||
"example": "49.2578298,-123.2634732",
|
||||
"description": lang["fieldTextGeolocation"],
|
||||
},
|
||||
{
|
||||
"id": "monitor-settings-monitor-map-container",
|
||||
"style": "position: relative",
|
||||
"fieldType": "div",
|
||||
divContent: `
|
||||
<div id="monitor-settings-geolocation-options" class="p-2" style="background: rgba(0,0,0,0.4);position: absolute; top: 0; right: 0; border-radius: 0 0 0 15px; z-index: 405;">
|
||||
<label for="direction">${lang.Direction} <span class="badge" map-option-value="direction"></span></label>
|
||||
<div class="slider-container">
|
||||
<input type="range" map-option="direction" class="slider" min="0" max="360" value="90">
|
||||
</div>
|
||||
<label for="fov">${lang['Field of View']} <span class="badge"><span map-option-value="fov"></span>°</span></label>
|
||||
<div class="slider-container">
|
||||
<input type="range" map-option="fov" class="slider" min="0" max="180" value="60">
|
||||
</div>
|
||||
<label for="range">${lang.Range} <span class="badge"><span map-option-value="range"></span> km</span></label>
|
||||
<div class="slider-container">
|
||||
<input type="range" map-option="range" class="slider" min="0" max="10" value="1" step="0.1">
|
||||
</div>
|
||||
</div>
|
||||
<div id="monitor-settings-monitor-map" style="width: 100%;height: 300px;border-radius:15px;"></div>`,
|
||||
},
|
||||
]
|
||||
},
|
||||
"Connection": {
|
||||
|
@ -448,25 +475,6 @@ module.exports = function(s,config,lang){
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "detail=onvif_non_standard",
|
||||
"field": lang['Non-Standard ONVIF'],
|
||||
"description": lang["fieldTextOnvifNonStandard"],
|
||||
"default": "0",
|
||||
"example": "",
|
||||
"form-group-class": "h_onvif_input h_onvif_1",
|
||||
"fieldType": "select",
|
||||
"possible": [
|
||||
{
|
||||
"name": lang.No,
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"name": lang.Yes,
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
hidden: true,
|
||||
"name": "detail=onvif_port",
|
||||
|
@ -870,6 +878,10 @@ module.exports = function(s,config,lang){
|
|||
"name": "H.264 NVENC (NVIDIA HW Accel)",
|
||||
"value": "h264_nvenc"
|
||||
},
|
||||
{
|
||||
"name": "H.264 NVENC Jetson (NVIDIA HW Accel NVMPI)",
|
||||
"value": "h264_nvmpi"
|
||||
},
|
||||
{
|
||||
"name": "H.265 NVENC (NVIDIA HW Accel)",
|
||||
"value": "hevc_nvenc"
|
||||
|
@ -1648,6 +1660,10 @@ module.exports = function(s,config,lang){
|
|||
"name": "H.264 NVENC (NVIDIA HW Accel)",
|
||||
"value": "h264_nvenc"
|
||||
},
|
||||
{
|
||||
"name": "H.264 NVENC Jetson (NVIDIA HW Accel NVMPI)",
|
||||
"value": "h264_nvmpi"
|
||||
},
|
||||
{
|
||||
"name": "H.265 NVENC (NVIDIA HW Accel)",
|
||||
"value": "hevc_nvenc"
|
||||
|
@ -2037,6 +2053,10 @@ module.exports = function(s,config,lang){
|
|||
"name": "H.264 NVENC (NVIDIA HW Accel)",
|
||||
"value": "h264_nvenc"
|
||||
},
|
||||
{
|
||||
"name": "H.264 NVENC Jetson (NVIDIA HW Accel NVMPI)",
|
||||
"value": "h264_nvmpi"
|
||||
},
|
||||
{
|
||||
"name": "H.265 NVENC (NVIDIA HW Accel)",
|
||||
"value": "hevc_nvenc"
|
||||
|
@ -3767,13 +3787,33 @@ module.exports = function(s,config,lang){
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "detail=onvif_non_standard",
|
||||
"field": lang['ONVIF Home Control'],
|
||||
"description": lang.fieldTextOnvifHomeControl,
|
||||
"default": "0",
|
||||
"form-group-class": "h_control_call_input h_control_call_ONVIF",
|
||||
"fieldType": "select",
|
||||
"possible": [
|
||||
{
|
||||
"name": lang.usingPreset1,
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"name": lang.usingPreset1HikvisionClone,
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"name": lang.usingHomePreset,
|
||||
"value": "2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
isAdvanced: true,
|
||||
"name": "detail=control_digest_auth",
|
||||
"field": lang['Digest Authentication'],
|
||||
"description": "",
|
||||
"default": "0",
|
||||
"example": "",
|
||||
"fieldType": "select",
|
||||
"form-group-class": "h_control_call_input h_control_call_GET h_control_call_PUT h_control_call_POST",
|
||||
"possible": [
|
||||
|
@ -3787,6 +3827,27 @@ module.exports = function(s,config,lang){
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
isAdvanced: true,
|
||||
"name": "detail=control_axis_lock",
|
||||
"field": lang['Pan/Tilt Only'],
|
||||
"default": "",
|
||||
"fieldType": "select",
|
||||
"possible": [
|
||||
{
|
||||
"name": lang['Pan and Tilt'],
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"name": lang['Pan Only'],
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"name": lang['Tilt Only'],
|
||||
"value": "2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "detail=control_stop",
|
||||
"field": lang['Stop Command'],
|
||||
|
@ -6430,7 +6491,7 @@ module.exports = function(s,config,lang){
|
|||
"info": [
|
||||
{
|
||||
"name": "actions=halt",
|
||||
"field": "Drop Event",
|
||||
"field": lang["Drop Event"],
|
||||
"fieldType": "select",
|
||||
"form-group-class": "actions-row",
|
||||
"description": lang["fieldTextActionsHalt"],
|
||||
|
@ -6467,7 +6528,7 @@ module.exports = function(s,config,lang){
|
|||
},
|
||||
{
|
||||
"name": "actions=indifference",
|
||||
"field": "Modify Indifference",
|
||||
"field": lang["Modify Indifference"],
|
||||
"description": lang["fieldTextActionsIndifference"],
|
||||
"form-group-class": "actions-row",
|
||||
},
|
||||
|
@ -6492,7 +6553,7 @@ module.exports = function(s,config,lang){
|
|||
},
|
||||
{
|
||||
"name": "actions=command",
|
||||
"field": "Detector Command",
|
||||
"field": lang["Detector Command"],
|
||||
"fieldType": "select",
|
||||
"form-group-class": "actions-row",
|
||||
"description": lang["fieldTextActionsCommand"],
|
||||
|
@ -6512,7 +6573,7 @@ module.exports = function(s,config,lang){
|
|||
},
|
||||
{
|
||||
"name": "actions=record",
|
||||
"field": "Use Record Method",
|
||||
"field": lang["Use Record Method"],
|
||||
"fieldType": "select",
|
||||
"description": lang["fieldTextActionsRecord"],
|
||||
"default": "",
|
||||
|
@ -7215,6 +7276,10 @@ module.exports = function(s,config,lang){
|
|||
"name": "H.264 NVENC (NVIDIA HW Accel)",
|
||||
"value": "h264_nvenc"
|
||||
},
|
||||
{
|
||||
"name": "H.264 NVENC Jetson (NVIDIA HW Accel NVMPI)",
|
||||
"value": "h264_nvmpi"
|
||||
},
|
||||
{
|
||||
"name": "H.265 NVENC (NVIDIA HW Accel)",
|
||||
"value": "hevc_nvenc"
|
||||
|
@ -7442,6 +7507,7 @@ module.exports = function(s,config,lang){
|
|||
streamBlockHudControlsHtml: `<span title="${lang['Currently viewing']}" class="label label-default">
|
||||
<span class="viewers"></span>
|
||||
</span>
|
||||
<span class="badge btn-success text-white substream-is-on" style="display:none">${lang['Substream']}</span>
|
||||
<a class="btn btn-sm badge btn-primary run-monitor-detection-trigger-marker">${lang['Add Marker']}</a>
|
||||
<a class="btn btn-sm badge btn-warning run-monitor-detection-trigger-test">${lang['Test Object Event']}</a>
|
||||
<a class="btn btn-sm badge btn-warning run-monitor-detection-trigger-test-motion">${lang['Test Motion Event']}</a>
|
||||
|
@ -7703,6 +7769,17 @@ module.exports = function(s,config,lang){
|
|||
<span class="badge bg-light text-dark rounded-pill align-text-bottom cameraCount"><i class="fa fa-spinner fa-pulse"></i></span>`,
|
||||
pageOpen: 'monitorsList',
|
||||
},
|
||||
{
|
||||
icon: 'barcode',
|
||||
label: `${lang['Timeline']}`,
|
||||
pageOpen: 'timeline',
|
||||
addUl: true,
|
||||
},
|
||||
{
|
||||
icon: 'map-marker',
|
||||
label: `${lang['Monitor Map']}`,
|
||||
pageOpen: 'monitorMap',
|
||||
},
|
||||
{
|
||||
icon: 'film',
|
||||
label: `${lang['Videos']}`,
|
||||
|
@ -7717,11 +7794,6 @@ module.exports = function(s,config,lang){
|
|||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
icon: 'map-marker',
|
||||
label: `${lang['Power Viewer']}`,
|
||||
pageOpen: 'powerVideo',
|
||||
},
|
||||
{
|
||||
icon: 'calendar',
|
||||
label: `${lang['Calendar']}`,
|
||||
|
@ -8877,146 +8949,130 @@ module.exports = function(s,config,lang){
|
|||
"box-wrapper-class": "row",
|
||||
"info": [
|
||||
{
|
||||
title: "New to Shinobi?",
|
||||
title: lang["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: ''
|
||||
text: lang.afterInstallationGuides,
|
||||
href: 'https://shinobi.video/docs/configure'
|
||||
},
|
||||
{
|
||||
icon: 'plus',
|
||||
color: 'default',
|
||||
text: 'Adding an H.264 Camera',
|
||||
href: 'https://shinobi.video/docs/configure#content-adding-an-h264h265-camera',
|
||||
class: ''
|
||||
text: lang.addingAnH264Camera,
|
||||
href: 'https://shinobi.video/docs/configure#content-adding-an-h264h265-camera'
|
||||
},
|
||||
{
|
||||
icon: 'plus',
|
||||
color: 'default',
|
||||
text: 'Adding an MJPEG Camera',
|
||||
href: 'https://shinobi.video/articles/2018-09-19-how-to-add-an-mjpeg-camera',
|
||||
class: ''
|
||||
text: lang.addingAnMJPEGCamera,
|
||||
href: 'https://shinobi.video/articles/2018-09-19-how-to-add-an-mjpeg-camera'
|
||||
},
|
||||
{
|
||||
icon: 'gears',
|
||||
color: 'default',
|
||||
text: 'RTSP Camera Optimization',
|
||||
href: 'https://shinobi.video/articles/2017-07-29-how-i-optimized-my-rtsp-camera',
|
||||
class: ''
|
||||
text: lang.rtspCameraOptimization,
|
||||
href: 'https://shinobi.video/articles/2017-07-29-how-i-optimized-my-rtsp-camera'
|
||||
},
|
||||
{
|
||||
icon: 'comments-o',
|
||||
color: 'info',
|
||||
text: 'Community Chat',
|
||||
href: 'https://discord.gg/ehRd8Zz',
|
||||
class: ''
|
||||
text: lang.communityChat,
|
||||
href: 'https://discord.gg/ehRd8Zz'
|
||||
},
|
||||
{
|
||||
icon: 'reddit',
|
||||
color: 'info',
|
||||
text: 'Forum on Reddit',
|
||||
href: 'https://www.reddit.com/r/ShinobiCCTV',
|
||||
class: ''
|
||||
text: lang.forumOnReddit,
|
||||
href: 'https://www.reddit.com/r/ShinobiCCTV'
|
||||
},
|
||||
{
|
||||
icon: 'file-o',
|
||||
color: 'primary',
|
||||
text: 'Documentation',
|
||||
href: 'http://shinobi.video/docs',
|
||||
class: ''
|
||||
text: lang.Documentation,
|
||||
href: 'http://shinobi.video/docs'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
bigIcon: "smile-o",
|
||||
title: "It's a proven fact",
|
||||
info: `Generosity makes you a happier person, please consider supporting the development.`,
|
||||
title: lang.itsAProvenFact,
|
||||
info: lang.generosityHappierPerson,
|
||||
buttons: [
|
||||
{
|
||||
icon: 'share-square-o',
|
||||
color: 'default',
|
||||
text: 'ShinobiShop Subscriptions',
|
||||
href: 'https://licenses.shinobi.video/subscribe',
|
||||
class: ''
|
||||
text: lang['ShinobiShop Subscriptions'],
|
||||
href: 'https://licenses.shinobi.video/subscribe'
|
||||
},
|
||||
{
|
||||
icon: 'paypal',
|
||||
color: 'default',
|
||||
text: 'Donate by PayPal',
|
||||
href: 'https://www.paypal.me/ShinobiCCTV',
|
||||
class: ''
|
||||
text: lang['Donate by PayPal'],
|
||||
href: 'https://www.paypal.me/ShinobiCCTV'
|
||||
},
|
||||
{
|
||||
icon: 'bank',
|
||||
color: 'default',
|
||||
text: 'University of Zurich (UZH)',
|
||||
href: 'https://www.zora.uzh.ch/id/eprint/139275/',
|
||||
class: ''
|
||||
href: 'https://www.zora.uzh.ch/id/eprint/139275/'
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
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!`,
|
||||
title: lang["Shinobi Mobile"],
|
||||
info: lang.yourSubscriptionText,
|
||||
buttons: [
|
||||
{
|
||||
icon: 'star',
|
||||
color: 'success',
|
||||
text: 'Join Public Beta',
|
||||
href: 'https://shinobi.video/mobile',
|
||||
class: ''
|
||||
text: lang['Get the Mobile App'],
|
||||
href: 'https://shinobi.video/mobile'
|
||||
},
|
||||
{
|
||||
icon: 'comments-o',
|
||||
color: 'primary',
|
||||
text: '<b>#mobile-client</b> Chat',
|
||||
href: 'https://discord.gg/ehRd8Zz',
|
||||
class: ''
|
||||
text: lang['#mobile-client Chat'],
|
||||
href: 'https://discord.gg/ehRd8Zz'
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
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>`,
|
||||
title: lang.activateShinobi,
|
||||
info: lang.howToActivate,
|
||||
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)',
|
||||
text: '100 Camera 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>`,
|
||||
title: lang['Donations, One-Time Boost'],
|
||||
info: lang.donationOneTimeText,
|
||||
width: 12,
|
||||
buttons: [
|
||||
{
|
||||
icon: 'paypal',
|
||||
color: 'default',
|
||||
text: 'Donate by PayPal',
|
||||
href: 'https://www.paypal.me/ShinobiCCTV',
|
||||
class: ''
|
||||
text: lang['Donate by PayPal'],
|
||||
href: 'https://www.paypal.me/ShinobiCCTV'
|
||||
},
|
||||
]
|
||||
},
|
||||
|
@ -9051,5 +9107,105 @@ module.exports = function(s,config,lang){
|
|||
},
|
||||
}
|
||||
},
|
||||
"Monitor Map": {
|
||||
"section": "Monitor Map",
|
||||
"blocks": {
|
||||
"Search Settings": {
|
||||
"name": lang["Monitor Map"],
|
||||
"color": "blue",
|
||||
"noHeader": true,
|
||||
"noDefaultSectionClasses": true,
|
||||
"info": [
|
||||
{
|
||||
"fieldType": "div",
|
||||
"id": "monitor-map-canvas",
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
"Timeline": {
|
||||
"section": "Timeline",
|
||||
"blocks": {
|
||||
"Search Settings": {
|
||||
"name": lang["Timeline"],
|
||||
"color": "blue",
|
||||
"noHeader": true,
|
||||
"noDefaultSectionClasses": true,
|
||||
"box-wrapper-class": "flex-direction-column",
|
||||
"info": [
|
||||
{
|
||||
"fieldType": "div",
|
||||
"class": "row m-0",
|
||||
"id": "timeline-video-canvas",
|
||||
},
|
||||
{
|
||||
"fieldType": "div",
|
||||
"class": "row p-1 m-0",
|
||||
"id": "timeline-info",
|
||||
"divContent": `
|
||||
<div class="text-center">
|
||||
<span class="current-time font-monospace ${textWhiteOnBgDark}"></span>
|
||||
</div>`
|
||||
},
|
||||
{
|
||||
"fieldType": "div",
|
||||
"class": "p-2",
|
||||
"id": "timeline-controls",
|
||||
"divContent": `
|
||||
<div class="text-center">
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-sm btn-default" timeline-action="jumpPrev" title="${lang.jumptoPreviousVideo}"><i class="fa fa-angle-double-left"></i></a>
|
||||
<a class="btn btn-sm btn-default" timeline-action="jumpLeft" title="${lang.jumpFiveSeconds}"><i class="fa fa-arrow-circle-left"></i></a>
|
||||
<a class="btn btn-sm btn-primary" timeline-action="playpause" title="${lang.Play}/${lang.Pause}"><i class="fa fa-play-circle-o"></i></a>
|
||||
<a class="btn btn-sm btn-default" timeline-action="jumpRight" title="${lang.jumpFiveSeconds}"><i class="fa fa-arrow-circle-right"></i></a>
|
||||
<a class="btn btn-sm btn-default" timeline-action="jumpNext" title="${lang.jumptoNextVideo}"><i class="fa fa-angle-double-right"></i></a>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-sm btn-default btn-success" timeline-action="speed" speed="1" title="${lang.Speed} x1">x1</a>
|
||||
<a class="btn btn-sm btn-default" timeline-action="speed" speed="2" title="${lang.Speed} x2">x2</a>
|
||||
<a class="btn btn-sm btn-default" timeline-action="speed" speed="5" title="${lang.Speed} x5">x5</a>
|
||||
<a class="btn btn-sm btn-default" timeline-action="speed" speed="7" title="${lang.Speed} x7">x7</a>
|
||||
<a class="btn btn-sm btn-default" timeline-action="speed" speed="10" title="${lang.Speed} x10">x10</a>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-sm btn-default" timeline-action="gridSize" size="md-12">1</a>
|
||||
<a class="btn btn-sm btn-default btn-success" timeline-action="gridSize" size="md-6">2</a>
|
||||
<a class="btn btn-sm btn-default" timeline-action="gridSize" size="md-4">3</a>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<input class="form-control form-control-sm" id="timeline-video-object-search" placeholder="${lang['Search Object Tags']}">
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-sm btn-primary" timeline-action="autoGridSizer" title="${lang.autoResizeGrid}"><i class="fa fa-expand"></i></a>
|
||||
<a class="btn btn-sm btn-primary" timeline-action="playUntilVideoEnd" title="${lang.playUntilVideoEnd}"><i class="fa fa-step-forward"></i></a>
|
||||
<a class="btn btn-sm btn-primary" timeline-action="dontShowDetection" title="${lang['Hide Detection on Stream']}"><i class="fa fa-grav"></i></a>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-sm btn-success" timeline-action="downloadAll" title="${lang.Download}"><i class="fa fa-download"></i></a>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-sm btn-default" class_toggle="show-non-playing" data-target="#timeline-video-canvas" icon-toggle="fa-eye fa-eye-slash" icon-child="i" title="${lang['Show Only Playing']}"><i class="fa fa-eye-slash"></i></a>
|
||||
<a class="btn btn-sm btn-default" timeline-action="refresh" title="${lang.Refresh}"><i class="fa fa-refresh"></i></a>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-sm btn-primary" id="timeline-date-selector" title="${lang.Date}"><i class="fa fa-calendar"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
},
|
||||
{
|
||||
"fieldType": "div",
|
||||
"id": "timeline-bottom-strip",
|
||||
},
|
||||
{
|
||||
"fieldType": "div",
|
||||
"id": "timeline-pre-buffers",
|
||||
"class": "hidden",
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
module.exports = {
|
||||
"person": "🙅♂️",
|
||||
"bicycle": "🚲",
|
||||
"car": "🚗",
|
||||
"motorcycle": "🏍",
|
||||
"airplane": "✈️",
|
||||
"bus": "🚌",
|
||||
"train": "🚂",
|
||||
"truck": "🚚",
|
||||
"boat": "⛵",
|
||||
"traffic light": "🚦",
|
||||
"fire hydrant": "🚒",
|
||||
"stop sign": "🛑",
|
||||
"parking meter": "🅿️",
|
||||
"bench": "🪑",
|
||||
"bird": "🐦",
|
||||
"cat": "🐈",
|
||||
"dog": "🐕",
|
||||
"horse": "🐎",
|
||||
"sheep": "🐏",
|
||||
"cow": "🐄",
|
||||
"elephant": "🐘",
|
||||
"bear": "🐻",
|
||||
"zebra": "🦓",
|
||||
"giraffe": "🦒",
|
||||
"backpack": "🎒",
|
||||
"umbrella": "☂️",
|
||||
"handbag": "👜",
|
||||
"tie": "👔",
|
||||
"suitcase": "🧳",
|
||||
"frisbee": "🥏",
|
||||
"skis": "🎿",
|
||||
"snowboard": "🏂",
|
||||
"sports ball": "⚽",
|
||||
"kite": "🪁",
|
||||
"skateboard": "🛹",
|
||||
"surfboard": "🏄",
|
||||
"tennis racket": "🎾",
|
||||
"bottle": "🍼",
|
||||
"wine glass": "🍷",
|
||||
"cup": "☕",
|
||||
"fork": "🍴",
|
||||
"knife": "🔪",
|
||||
"spoon": "🥄",
|
||||
"bowl": "🍲",
|
||||
"banana": "🍌",
|
||||
"apple": "🍏",
|
||||
"sandwich": "🥪",
|
||||
"orange": "🍊",
|
||||
"broccoli": "🥦",
|
||||
"carrot": "🥕",
|
||||
"hot dog": "🌭",
|
||||
"pizza": "🍕",
|
||||
"donut": "🍩",
|
||||
"cake": "🍰",
|
||||
"chair": "🪑",
|
||||
"couch": "🛋",
|
||||
"potted plant": "🪴",
|
||||
"bed": "🛏",
|
||||
"toilet": "🚽",
|
||||
"tv": "📺",
|
||||
"laptop": "💻",
|
||||
"mouse": "🖱",
|
||||
"remote": "📱",
|
||||
"keyboard": "⌨️",
|
||||
"cell phone": "📱",
|
||||
"microwave": "🌊",
|
||||
"toaster": "🍞",
|
||||
"refrigerator": "🍽",
|
||||
"book": "📚",
|
||||
"clock": "⏰",
|
||||
"vase": "🏺",
|
||||
"scissors": "✂️",
|
||||
"teddy bear": "🧸",
|
||||
"hair drier": "💨",
|
||||
"toothbrush": "🪥",
|
||||
"baseball bat": "🏏",
|
||||
"baseball glove": "🥅",
|
||||
"dining table": "🍽",
|
||||
"oven": "🔥",
|
||||
"sink": "🚰",
|
||||
"Clock Format": "⏲",
|
||||
"_default": "🏃♂️"
|
||||
}
|
|
@ -14,6 +14,10 @@
|
|||
"monitorDeleted": "Monitor Deleted",
|
||||
"accountCreationError": "Account Creation Error",
|
||||
"accountEditError": "Account Edit Error",
|
||||
"Monitor Map": "Monitor Map",
|
||||
"Geolocation": "Geolocation",
|
||||
"fieldTextGeolocation": "The map coordinates of this camera in the real world. This will plot a point for your camera on the Monitor Map.",
|
||||
"playUntilVideoEnd": "Play until video end",
|
||||
"Unmute": "Unmute",
|
||||
"byUser": "by user",
|
||||
"accountDeleted": "Account Deleted",
|
||||
|
@ -24,6 +28,7 @@
|
|||
"Accuracy Mode": "Accuracy Mode",
|
||||
"Client ID": "Client ID",
|
||||
"Tags": "Tags",
|
||||
"addTagText": "Add Tags to your monitors to get more choices here.",
|
||||
"tagsCannotAddText": "Cannot add Tag",
|
||||
"tagsTriggerCannotAddText": "Trigger tags must match an existing tag that was added to a Monitor previously or is pending to be added to this monitor that is currently being edited.",
|
||||
"tagsFieldText": "Automatically group Monitors based on a common identifier.",
|
||||
|
@ -34,6 +39,7 @@
|
|||
"Tile Size": "Tile Size",
|
||||
"fieldTextTileSize": "When in Accuracy Mode this is the size of each tile in pixels squared. A lower number will have higher accuracy but more resource use.",
|
||||
"Turn Speed": "Turn Speed",
|
||||
"Speed": "Speed",
|
||||
"Session Key": "Session Key",
|
||||
"Active Monitors": "Active Monitors",
|
||||
"Storage Use": "Storage Use",
|
||||
|
@ -118,8 +124,10 @@
|
|||
"Slice": "Slice",
|
||||
"Stitch": "Stitch",
|
||||
"Studio": "Studio",
|
||||
"Show Only Playing": "Show Only Playing",
|
||||
"Power Video Viewer": "Power Video Viewer",
|
||||
"Time-lapse": "Time-lapse",
|
||||
"Timeline": "Timeline",
|
||||
"Montage": "Montage",
|
||||
"Registered": "Registered",
|
||||
"Viewing Server Stats": "Viewing Server Stats",
|
||||
|
@ -371,9 +379,28 @@
|
|||
"Execute Command": "Execute Command",
|
||||
"for Global Access": "for Global Access",
|
||||
"Help": "Help",
|
||||
"Range": "Range",
|
||||
"ONVIF Home Control": "ONVIF Home Control",
|
||||
"usingPreset1": "Using Preset 1",
|
||||
"usingPreset1HikvisionClone": "Using Preset 1, Non-Standard, Hikvision Clone",
|
||||
"usingHomePreset": "Using \"Home\" Preset",
|
||||
"Pan and Tilt": "Pan and Tilt",
|
||||
"Tilt Only": "Tilt Only",
|
||||
"Pan Only": "Pan Only",
|
||||
"Pan/Tilt Only": "Pan/Tilt Only",
|
||||
"Direction": "Direction",
|
||||
"Field of View": "Field of View",
|
||||
"Don't show this anymore": "Don't show this anymore",
|
||||
"New to Shinobi?": "New to Shinobi?",
|
||||
"Get the Mobile App": "Get the Mobile App",
|
||||
"#mobile-client Chat": "#mobile-client Chat",
|
||||
"Donations, One-Time Boost": "Donations, One-Time Boost",
|
||||
"donationOneTimeText": "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>",
|
||||
"Chat on Discord": "Chat on Discord",
|
||||
"Documentation": "Documentation",
|
||||
"Donate by PayPal": "Donate by PayPal",
|
||||
"Join Public Beta": "Join Public Beta",
|
||||
"ShinobiShop Subscriptions": "ShinobiShop Subscriptions",
|
||||
"All Monitors": "All Monitors",
|
||||
"Motion Meter": "Motion Meter",
|
||||
"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.",
|
||||
|
@ -687,8 +714,10 @@
|
|||
"Plugin": "Plugin",
|
||||
"Plugins": "Plugins",
|
||||
"pluginDownloadText": "Learn about <a href='https://docs.shinobi.video/plugin/install' target='_blank'>installing plugins</a> in the Documentation.",
|
||||
"moduleDownloadText": "Learn about <a href='https://docs.shinobi.video/developer#link--custom-auto-load-modules-' target='_blank'>Custom Auto Load Modules</a> in the Documentation. We've listed some samples and complete modules below for you to try.",
|
||||
"Plugin Manager": "Plugin Manager",
|
||||
"Download Plugins": "Download Plugins",
|
||||
"Download Modules": "Download Modules",
|
||||
"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.",
|
||||
|
@ -990,6 +1019,13 @@
|
|||
"WebdavErrorTextCreatingDir": "Cannot create directory.",
|
||||
"File Not Exist": "File Not Exist",
|
||||
"No Videos Found": "No Videos Found",
|
||||
"activateRequiredLiveStream": "Live Stream is only available here with an Activated installation.",
|
||||
"activationRequired": "Activation Required",
|
||||
"autoResizeGrid": "Auto Resize Grid",
|
||||
"jumpFiveSeconds": "Jump 5 Seconds",
|
||||
"jumptoNextVideo": "Jump to Next Video",
|
||||
"jumptoPreviousVideo": "Jump to Previous Video",
|
||||
"featureRequiresActivationText": "The feature you are trying to use requires that your installation be activated. Please see the Help tab for more information.",
|
||||
"FileNotExistText": "Cannot save non existant file. Something went wrong.",
|
||||
"CameraNotRecordingText": "Settings may be incompatible. Check encoders. Restarting...",
|
||||
"Camera is not running": "Camera is not running",
|
||||
|
@ -1516,6 +1552,7 @@
|
|||
"MQTT Outbound": "MQTT Outbound",
|
||||
"MQTT Client": "MQTT Client",
|
||||
"Buffer Time from Event": "Buffer Time from Event",
|
||||
"detected": "detected",
|
||||
"fieldTextEventFilters": "Enable to have all Events honor your Event Filter rules.",
|
||||
"fieldTextBufferTimeFromEvent": "The amount of seconds to record before the trigger happened. If this is consistently inaccurate you will need to look at the <a target='_blank' href='https://hub.shinobi.video/articles/view/DmWIID78VtvEfnf'>optimization guide</a> or force encoding on the server.",
|
||||
"fieldTextMode": "This is the primary task of the monitor.",
|
||||
|
@ -1556,7 +1593,7 @@
|
|||
"fieldTextFatalMax": "The number of times to retry for network connection between the server and camera before setting the monitor to Disabled. No decimals. Set to 0 to retry forever.",
|
||||
"fieldTextSkipPing": "Choose if a successful ping is required before a monitor process is started.",
|
||||
"fieldTextIsOnvif": "Is this an ONVIF compliant camera?",
|
||||
"fieldTextOnvifNonStandard": "Is this a Non-Standard ONVIF camera?",
|
||||
"fieldTextOnvifHomeControl": "Some ONVIF cameras seem to follow different standards on how to PTZ to Home position. Try each of the following if your PTZ Auto Tracking doesn't automatically return to home after 7 seconds of inactivity.",
|
||||
"fieldTextOnvifPort": "ONVIF is usually run on port <code>8000</code>. This can be <code>80</code> as well depending on your camera model.",
|
||||
"fieldTextAduration": "Specify how many microseconds are analyzed to probe the input. Set to 100000 if you are using RTSP and having stream issues.",
|
||||
"fieldTextProbesize": "Specify how big to make the analyzation probe for the input. Set to 100000 if you are using RTSP and having stream issues.",
|
||||
|
@ -1631,7 +1668,7 @@
|
|||
"fieldTextDetailSubstreamOutputStreamAcodecAac": "Used for MP4 video.",
|
||||
"fieldTextDetailSubstreamOutputStreamAcodecAc3": "Used for MP4 video.",
|
||||
"fieldTextDetailSubstreamOutputStreamAcodecCopy": "Used for MP4 video. Has very low CPU usage but some audio codecs need custom flags like <code>-strict 2</code> for aac.",
|
||||
"fieldTextDetailSubstreamOutputHlsTime": "How long each video segment should be, in minutes. Each segment will be drawn by the client through an m3u8 file. Shorter segments take less space.",
|
||||
"fieldTextDetailSubstreamOutputHlsTime": "How long each video segment should be, in seconds. Each segment will be drawn by the client through an m3u8 file. Shorter segments take less space.",
|
||||
"fieldTextDetailSubstreamOutputHlsListSize": "The number of segments maximum before deleting old segments automatically.",
|
||||
"fieldTextDetailSubstreamOutputPresetStream": "Preset flag for certain video encoders. If you find your camera is crashing every few seconds : try leaving it blank.",
|
||||
"fieldTextDetailSubstreamOutputStreamQuality": "Low number means higher quality. Higher number means less quality.",
|
||||
|
@ -1786,10 +1823,20 @@
|
|||
"fieldTextChannelStreamScaleY": "Height of the stream image that is output after processing.",
|
||||
"fieldTextChannelStreamRotate": "Change the viewing angle of the video stream.",
|
||||
"fieldTextChannelSvf": "Place FFMPEG video filters in this box to affect the streaming portion. No spaces.",
|
||||
"afterInstallationGuides": "After Installation Guides",
|
||||
"addingAnH264Camera": "Adding an H.264 Camera",
|
||||
"addingAnMJPEGCamera": "Adding an MJPEG Camera",
|
||||
"rtspCameraOptimization": "RTSP Camera Optimization",
|
||||
"communityChat": "Community Chat",
|
||||
"forumOnReddit": "Forum on Reddit",
|
||||
"activateShinobi": "Activate Shinobi",
|
||||
"howToActivate": "Subscribe to any of the following to support the development and activate your Shinobi! 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>",
|
||||
"Shinobi Mobile": "Shinobi Mobile",
|
||||
"yourSubscriptionText": "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!",
|
||||
"Last Updated": "Last Updated",
|
||||
"Z-Wave Manager": "Z-Wave Manager",
|
||||
"Z-Wave": "Z-Wave",
|
||||
"Primary":"Primary",
|
||||
"Primary": "Primary",
|
||||
"Upload Images": "Upload Images",
|
||||
"Images Sent": "Images Sent",
|
||||
"Click to Upload Images": "Click to Upload Images",
|
||||
|
@ -1798,5 +1845,58 @@
|
|||
"deleteFace": "Delete Face",
|
||||
"deleteFaceText": "Are you sure you want to delete ALL the images for this face? they will not be recoverable.",
|
||||
"deleteImage": "Delete Image",
|
||||
"deleteImageText": "Are you sure you want to delete this image? it will not be recoverable."
|
||||
"deleteImageText": "Are you sure you want to delete this image? it will not be recoverable.",
|
||||
"Drop Event": "Drop Event",
|
||||
"Detector Command": "Detector Command",
|
||||
"Use Record Method": "Use Record Method",
|
||||
"Main Configuration": "Main Configuration",
|
||||
"Enable Debug Log": "Enable Debug Log",
|
||||
"Fill in subscription ID": "Fill in subscription ID",
|
||||
"Server port": "Server port",
|
||||
"Password type": "Password type",
|
||||
"Additional Storage": "Additional Storage",
|
||||
"AdditionalStorageDes": "Separate storage locations that can be set for different monitors.",
|
||||
"Storage Array": "Storage Array",
|
||||
"Plugin Keys": "Plugin Keys",
|
||||
"PluginKeysDes": "Quick client connection setup for plugins. Just add the plugin key to make it ready for incoming connections.",
|
||||
"Database Options": "Database Options",
|
||||
"DatabaseOptionDes": "Credentials to connect to where detailed information is stored.",
|
||||
"Hostname / IP": "Hostname / IP",
|
||||
"CRON Options": "CRON Options",
|
||||
"deleteOld": "deleteOld",
|
||||
"deleteOldDes": "cron will delete videos older than Max Number of Days per account.",
|
||||
"deleteNoVideo": "deleteNoVideo",
|
||||
"deleteNoVideoDes": "cron will delete SQL rows that it thinks have no video files.",
|
||||
"deleteOverMax": "deleteOverMax",
|
||||
"deleteOverMaxDes": "cron will delete files that are over the set maximum storage per account.",
|
||||
"Email Options": "Email Options",
|
||||
"service": "service",
|
||||
"auth": "auth",
|
||||
"secure": "secure",
|
||||
"itsAProvenFact": "It's a proven fact",
|
||||
"generosityHappierPerson": "Generosity makes you a happier person, please consider supporting the development.",
|
||||
"ignoreTLS": "ignoreTLS",
|
||||
"requireTLS": "requireTLS",
|
||||
"detectorMergePamRegionTriggers": "detectorMergePamRegionTriggers",
|
||||
"doSnapshot": "doSnapshot",
|
||||
"discordBot": "discordBot",
|
||||
"dropInEventServer": "dropInEventServer",
|
||||
"ftpServer": "ftpServer",
|
||||
"oldPowerVideo": "oldPowerVideo",
|
||||
"wallClockTimestampAsDefault": "wallClockTimestampAsDefault",
|
||||
"defaultMjpeg": "defaultMjpeg",
|
||||
"streamDir": "streamDir",
|
||||
"videosDir": "videosDir",
|
||||
"windowsTempDir": "windowsTempDir",
|
||||
"Enable Face Manager": "Enable Face Manager",
|
||||
"enableFaceManagerDes": "Enable / Disable face manager for face recognition plugins in the dashboard.",
|
||||
"PluginsDes": "Elaborate Plugin connection settings.",
|
||||
"Https": "Https",
|
||||
"Key": "Key",
|
||||
"Plug": "Plug",
|
||||
"HowToConnectDes1": "<b>This feature is available to Mobile License subscribers.</b> To get an API Key please login to your <a href='https: //licenses.shinobi.video/login' target='_blank'>Shinobi<b>Shop</b></a> account and create a key associated to <b>any active Subscription ID</b>. <a href='https://hub.shinobi.video/articles/view/3Yhivc6djTtuBPE' target='_blank'>Learn More.</a>",
|
||||
"HowToConnectDes2": "If you would like to get access to a private (dedicated) P2P server please create an account at the <a href='https: //licenses.shinobi.video/login' target='_blank'>Shinobi<b>Shop</b></a> and contact us via the Live Chat widget",
|
||||
"User": "User",
|
||||
"Current Version": "Current Version",
|
||||
"Default is Global value": "Default is Global value"
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
"Archive": "存档",
|
||||
"Audio Codec": "音频编",
|
||||
"Authenticate": "进行身份验证",
|
||||
"Auto": "汽车",
|
||||
"Auto": "自动",
|
||||
"Autosave": "自动保存",
|
||||
"Base64 over Websocket": "Base64过Websocket",
|
||||
"Bottom Left": "左下",
|
||||
|
@ -42,20 +42,20 @@
|
|||
"Browser Console Log": "浏览器控制台的记录",
|
||||
"CPU": "CPU",
|
||||
"CPU indicator will not work. Continuing...": "CPU指标将不会的工作。 继续...",
|
||||
"CSS": "CSS <small>的风格你的仪表板。</small>",
|
||||
"CSS": "CSS <small>为你的仪表盘设置样式</small>",
|
||||
"Calendar": "日历",
|
||||
"Camera Password": "摄像机的密码",
|
||||
"Camera Username": "摄像机的用户名",
|
||||
"Camera is not recording": "摄像机是不是记录",
|
||||
"CameraNotRecordingText": "设置可能不相容的。 检查编码器。 重新启动...",
|
||||
"Can Control Monitors": "可以控制的监控",
|
||||
"Can Control Monitors": "可以控制监视器",
|
||||
"Can Delete Videos": "可以删除的视频",
|
||||
"Can Delete Videos and Events": "可以删除的视频和活动",
|
||||
"Can Edit Monitor": "可以编辑监控",
|
||||
"Can Get Logs": "可以得到日志",
|
||||
"Can Delete Videos and Events": "可以删除视频和事件",
|
||||
"Can Edit Monitor": "可以编辑监视器",
|
||||
"Can Get Logs": "可以获取日志",
|
||||
"Can Get Monitors": "可以得到监控",
|
||||
"Can View Monitor": "可以查看监控",
|
||||
"Can View Snapshots": "可以查看的快照",
|
||||
"Can View Snapshots": "可以查看快照",
|
||||
"Can View Streams": "可以看流",
|
||||
"Can View Videos": "可以观看视频",
|
||||
"Can View Videos and Events": "可以观看视频和活动",
|
||||
|
@ -112,7 +112,7 @@
|
|||
"DetectorText": "<p>当宽度和高度框显示,你应该将它们设置为640x480或以下。 这将优化该阅读的速度框架。</p>",
|
||||
"Disable Night Vision": "禁止夜视 <small>的URL地址</small>",
|
||||
"Disable Nightvision": "禁止夜视",
|
||||
"Disabled": "残疾人",
|
||||
"Disabled": "禁用",
|
||||
"Documentation": "文档",
|
||||
"Don't show this anymore": "不再这样下去了",
|
||||
"Double Quote Directory": "双引目录中的 <small>一些目录具有空间。 使用这个可能会崩溃,一些摄像机。</small>",
|
||||
|
@ -176,8 +176,8 @@
|
|||
"Font Path": "字体路径",
|
||||
"Font Size": "字体大小",
|
||||
"Force Port": "部队口",
|
||||
"Found Devices": "设备找到了",
|
||||
"Frame Rate": "框率 <small>(》)</small>",
|
||||
"Found Devices": "发现设备",
|
||||
"Frame Rate": "帧率",
|
||||
"Full Frame Detection": "完全检测框架",
|
||||
"Fullscreen": "全屏",
|
||||
"Greater Than": "大于",
|
||||
|
@ -220,11 +220,11 @@
|
|||
"Input": "输入",
|
||||
"Input Flags": "输入的标志",
|
||||
"Input Type": "输入类型",
|
||||
"InputText1": "这部分告诉忍如何消耗流。 最佳性能试图调整你的摄像机的内置。 找到以下选项,并设定他们如图所示。 找到你的摄像机可以使用 <b>建立在升扫描仪</b> 的忍者. 一些升摄像机要求采用一个管理工具,以修改其内部设置。 如果你找不到你的摄像机你可以尝试 <a href=\"https://s3.amazonaws.com/cloudcamio/odm-v2.2.250.msi\">提升设备Manager for Windows</a>.",
|
||||
"InputText1": "这部分告诉Shinobi如何使用流。 最佳性能试图调整你的摄像机的内置。 找到以下选项,并设定他们如图所示。 找到你的摄像机可以使用 <b>建立在升扫描仪</b> 的Shinobi. 一些升摄像机要求采用一个管理工具,以修改其内部设置。 如果你找不到你的摄像机你可以尝试 <a href=\"https://s3.amazonaws.com/cloudcamio/odm-v2.2.250.msi\">提升设备Manager for Windows</a>.",
|
||||
"InputText2": "<ul><li><b>Framerate(》):</b> 高:10-15》,低:2-5》</li><li><b>我框架的时间间隔:</b> 80</li><li><b>比特率类型:</b> 社区康复(恒定的比率)</li><li><b>比特率:</b> 间256kbps-500kbps</li></ul>",
|
||||
"InputText3": "如果你需要帮忙找出来是什么样的输入型你的摄像机是你可以看看 <a href=\"http://shinobi.video/docs/cameras\" target=\"_blank\">摄像机的Url清单</a> 上的忍者的网站。",
|
||||
"InputText3": "如果你需要帮忙找出来是什么样的输入型你的摄像机是你可以看看 <a href=\"http://shinobi.video/docs/cameras\" target=\"_blank\">摄像机的Url清单</a> 上的Shinobi的网站。",
|
||||
"Invalid JSON": "无效JSON",
|
||||
"InvalidJSONText": "请确保这是一个有效的JSON串忍监控配置。",
|
||||
"InvalidJSONText": "请确保这是一个有效的Shinobi 监控配置JSON串。",
|
||||
"JPEG": "JPEG",
|
||||
"JPEG (Auto Enables JPEG API)": "JPEG(自动使JPEG API)",
|
||||
"JPEG API": "JPEG API <small>快照(cgi-bin)</small>",
|
||||
|
@ -239,8 +239,9 @@
|
|||
"Like": "喜欢",
|
||||
"Lisence Plate Detector": "图编辑功能板检测器",
|
||||
"List Toggle": "列表中切换",
|
||||
"Live Stream Toggle": "现场流肘",
|
||||
"Live Stream Toggle": "实时流切换",
|
||||
"Live View": "现场查看",
|
||||
"Live Grid": "现场网格",
|
||||
"Local": "本地",
|
||||
"Log Level": "日志的水平",
|
||||
"Log Signal Event": "登录信号的事件, <small>客户只有一侧</small>",
|
||||
|
@ -254,7 +255,7 @@
|
|||
"MPEG-4 (.mp4 / .ts)": "MPEG-4(中。mp4/.ts)",
|
||||
"MailError": "邮件错误:不能发送电子邮件、检查conf.手机中。 跳过的任何功能的依赖邮寄。",
|
||||
"Matches": "比赛",
|
||||
"Max Storage Amount": "最大储存量 <small>在兆</small>",
|
||||
"Max Storage Amount": "最大储存量 <small>兆</small>",
|
||||
"Mode": "模式",
|
||||
"Monitor": "监控",
|
||||
"Monitor Added by user": "监控中加入的用户。",
|
||||
|
@ -272,7 +273,7 @@
|
|||
"MonitorIdlingText": "监控会议已订于空闲。",
|
||||
"MonitorStoppedText": "监控会议已被下令停止。",
|
||||
"Monitors": "监控",
|
||||
"Monitors per row": "监控,每行 <small>为的蒙太奇</small>",
|
||||
"Monitors per row": "监控,每行 <small>镜头组接</small>",
|
||||
"Montage": "蒙太奇",
|
||||
"Motion GUI": "运动GUI",
|
||||
"Motion Meter": "运动米",
|
||||
|
@ -296,39 +297,41 @@
|
|||
"Not Permitted": "不允许",
|
||||
"Not an Administrator Account": "不是管理员的帐户",
|
||||
"NotAuthorizedText1": "没有授权,提交init命令与\"授权\",\"科\",并\"uid\"",
|
||||
"Notes": "注意到",
|
||||
"Notes": "说明",
|
||||
"NotesPlacholder": "评论你想离开这个相机设置。",
|
||||
"Number of Days to keep": "保留的天数",
|
||||
"ONVIF": "ONVIF",
|
||||
"ONVIF Scanner": "ONVIF设备描仪",
|
||||
"ONVIFnote": "发现提升设备网络之外自己或留下的空白以扫描你的前的网络。 <br>用户名和密码可以留空。",
|
||||
"ONVIF Scanner": "ONVIF设备扫描仪",
|
||||
"ONVIFnote": "在您自己的网络之外的网络上发现ONVIF设备或将其保留空白以扫描当前网络。 <br>用户名和密码可以留空。",
|
||||
"OpenCV Cascades": "该版本的瀑布",
|
||||
"Order Streams": "顺流",
|
||||
"Output Method": "输出方法",
|
||||
"Password": "密码",
|
||||
"Password Again": "再次密码",
|
||||
"Password Again": "再次输入密码",
|
||||
"Passwords don't match": "密码不匹配",
|
||||
"Paste JSON here.": "贴JSON在这里。",
|
||||
"Path": "路径",
|
||||
"Permissions": "权限",
|
||||
"Points": "点 <small>在添加点击边缘上的多边形。</small>",
|
||||
"Port": "口",
|
||||
"Port": "端口",
|
||||
"Position X": "X位置",
|
||||
"Position Y": "Y位置",
|
||||
"Power Video Viewer": "的电视观众",
|
||||
"Power Viewer": "电观众",
|
||||
"Power Viewer": "功率查看器",
|
||||
"Preferences": "喜好",
|
||||
"Preset": "预先设定",
|
||||
"Probe Size": "探头大小",
|
||||
"Process Crashed for Monitor": "进程崩溃的监控",
|
||||
"Process Unexpected Exit": "处理意想不到的退出",
|
||||
"Profile": "配置文件",
|
||||
"Quality": "质量 <small>1高,23低</small>",
|
||||
"Quality": "音质 <small>1高,23低</small>",
|
||||
"Query": "查询",
|
||||
"RAM": "RAM",
|
||||
"Recent Videos": "近期视频",
|
||||
"RTSP": "RTSP",
|
||||
"RTSP Transport": "RTSP运输",
|
||||
"Range or Single": "范围内或单",
|
||||
"Range or Single": "范围或单个",
|
||||
"fieldTextIp": "范围或单个",
|
||||
"Rate": "率 <small>(》)</small>",
|
||||
"Record": "记录",
|
||||
"Record File Type": "记录文件的类型",
|
||||
|
@ -347,7 +350,7 @@
|
|||
"Region Name": "地区名称",
|
||||
"RegionNote": "点只保存,当你按下 <b>保存</b> 在 <b>监控设置</b> 窗口。",
|
||||
"Regions": "区域",
|
||||
"Remember Me": "还记得我",
|
||||
"Remember Me": "记住我在此计算机的登录",
|
||||
"Reset Timer": "重置计时器",
|
||||
"Restarting Process": "重新启动进程",
|
||||
"Retry Connection": "试连接 <small>的次数,允许失败</small>",
|
||||
|
@ -369,8 +372,8 @@
|
|||
"Settings": "设置",
|
||||
"Settings Changed": "设置改变",
|
||||
"SettingsChangedText": "你的设置已保存和应用。",
|
||||
"Shinobi": "忍者",
|
||||
"Shinobi Streamer": "忍流光",
|
||||
"Shinobi": "Shinobi",
|
||||
"Shinobi Streamer": "Shinobi 流",
|
||||
"Show Logs": "日志显示",
|
||||
"Silent": "沉默",
|
||||
"Simple": "简单的",
|
||||
|
@ -486,9 +489,9 @@
|
|||
"startUpText2": "所有用户的检查,等待关闭打开文件,并删除文件的用户限制",
|
||||
"startUpText3": "等着给未完成的视频检查一些时间。 3秒钟。",
|
||||
"startUpText4": "开始的所有监控组,以观察和记录",
|
||||
"startUpText5": "忍者已准备就绪。",
|
||||
"startUpText5": "Shinobi 已准备就绪。",
|
||||
"superAdminText": "\"超级。json\"不存在。 请重新命名\"超级。样品。json\"到\"超级。json\"。",
|
||||
"superAdminTitle": "忍:超级管理员",
|
||||
"superAdminTitle": "Shinobi:超级管理员",
|
||||
"total": "总",
|
||||
"updateKeyText1": "\"updateKey\"缺失\"conf.json\",无法做更新这样直到你加入它。",
|
||||
"updateKeyText2": "\"updateKey\"是不正确的。",
|
||||
|
@ -496,5 +499,346 @@
|
|||
"Open All Monitors": "打开所有监控",
|
||||
"Close All Monitors": "关闭所有监控",
|
||||
"Home": "首页",
|
||||
"Event Filters": "事件搜索"
|
||||
"Event Filters": "事件搜索",
|
||||
"Remember Positions": "记住定位",
|
||||
"Mute Audio": "静音",
|
||||
"Cycle Monitors": "周期监测",
|
||||
"Stream in Background": "后台流",
|
||||
"Original Aspect Ratio": "原始宽高比",
|
||||
"Hide Detection on Stream": "隐藏流检测",
|
||||
"Alert on Event": "事件警报",
|
||||
"Popout on Event": "事件弹出",
|
||||
"Save Compressed Video on Completion": "完成后保存压缩视频",
|
||||
"Search Settings": "搜索设置",
|
||||
"Search Object Tags": "搜索对象标签",
|
||||
"Date": "日期",
|
||||
"Video Set": "视频设置",
|
||||
"Time": "时间",
|
||||
"Objects Found": "发现的对象",
|
||||
"No matching records found": "没有找到匹配的记录",
|
||||
"Select a Monitor": "选择监视器",
|
||||
"Video Limit": "视频限制",
|
||||
"Per Monitor": "每个监控",
|
||||
"Refresh": "刷新",
|
||||
"Play": "播放",
|
||||
"Build Video": "构建视频",
|
||||
"Zip and Download": "压缩及下载",
|
||||
"Save Built Video on Completion": "完成后保存构建的视频",
|
||||
"FileBin": "文件箱",
|
||||
"Time Created": "创建时间",
|
||||
"fieldTextMode": "这是监视器的主要任务",
|
||||
"Watch-Only": "只看",
|
||||
"fieldTextMid": "这是监视器的不可更改标识符,您可以通过双击监视器ID并更改它来复制监视器。",
|
||||
"fieldTextName": "这是可读的监视器的显示名称。",
|
||||
"tagsFieldText": "根据公共标识符自动对监视器进行分组",
|
||||
"fieldTextMaxKeepDays": "在清除此监视器之前保留视频的天数。",
|
||||
"fieldTextNotes": "你想给这台相机留下的评论。",
|
||||
"Storage Location": "存储位置",
|
||||
"fieldTextDir": "录制文件保存的位置。您可以使用<code>addStorage</code>变量配置更多位置。",
|
||||
"Tags": "标签",
|
||||
"Compress Completed Videos": "压缩完成的视频",
|
||||
"compressCompletedVideosFieldText": "自动压缩视频到WebM一旦录制。这样做需要一个强大的CPU,或者您必须允许大量的时间来进行压缩。视频添加到数据库的速率不能快于压缩速率。",
|
||||
"Connection": "连接",
|
||||
"fieldTextType": "将用于消费视频流的方法。",
|
||||
"Automatic": "自动",
|
||||
"fieldTextAutoHostEnable": "提供构建流URL所需的各个部分,或者提供完整的URL,并允许Shinobi为您解析它。",
|
||||
"Full URL Path": "完整URL路径",
|
||||
"fieldTextAutoHost": "完整的流URL。",
|
||||
"fieldTextFatalMax": "在将监视器设置为禁用之前,服务器和摄像机之间的网络连接需要重试的次数。没有小数。设置为0将永远重试。",
|
||||
"Skip Ping": "跳过Ping",
|
||||
"fieldTextSkipPing": "选择在启动监视器进程之前是否需要一个成功的ping。",
|
||||
"fieldTextIsOnvif": "这是一个兼容ONVIF的相机吗?",
|
||||
"fieldTextAduration": "如果您正在使用RTSP并且有流问题,则设置为100000。",
|
||||
"fieldTextProbesize": "如果您正在使用RTSP并且有流问题,则设置为100000。",
|
||||
"fieldTextSfps": "指定摄像机提供流的帧率(FPS)。",
|
||||
"Use Camera Timestamps": "使用相机时间戳",
|
||||
"fieldTextWallClockTimestampIgnore": "将所有传入的摄像机数据以摄像机时间而不是服务器时间为基础",
|
||||
"Accelerator": "加速器",
|
||||
"fieldTextAccelerator": "硬件加速(HWAccel)解码流。",
|
||||
"fieldTextStreamType": "将用于消费视频流的方法。",
|
||||
"fieldTextChannelStreamVcodec": "流媒体视频编解码器。",
|
||||
"fieldTextStreamVcodec": "流媒体视频编解码器。",
|
||||
"fieldTextDetailSubstreamOutputStreamVcodec": "流媒体视频编解码器。",
|
||||
"fieldTextStreamAcodec": "只有在你所在地区的法律允许录制音频的情况下才打开这个功能。",
|
||||
"fieldTextDetailSubstreamOutputStreamAcodec": "只有在你所在地区的法律允许录制音频的情况下才打开这个功能。",
|
||||
"fieldTextChannelStreamAcodec": "只有在你所在地区的法律允许录制音频的情况下才打开这个功能。",
|
||||
"Substream": "子流",
|
||||
"substreamText": "这是一种按需观看直播的方式。你可以让它只在有人观看时才能观看,或者用于在低分辨率和高分辨率之间切换。",
|
||||
"Output": "输出",
|
||||
"substreamOutputText": "你可以在这里设置按需流的配置。在这里了解流类型的延迟。",
|
||||
"fieldTextDetailSubstreamOutputHlsTime": "每个视频片段应该有多长,单位为分钟。每个段将由客户端通过m3u8文件绘制。更短的片段占用更少的空间",
|
||||
"fieldTextChannelHlsTime": "每个视频片段应该有多长,单位为分钟。每个段将由客户端通过m3u8文件绘制。更短的片段占用更少的空间",
|
||||
"fieldTextHlsListSize": "自动删除旧段前的最大段数。",
|
||||
"fieldTextDetailSubstreamOutputHlsListSize": "自动删除旧段前的最大段数。",
|
||||
"fieldTextDetectorBufferHlsListSize": "自动删除旧段前的最大段数。",
|
||||
"fieldTextChannelHlsListSize": "自动删除旧段前的最大段数。",
|
||||
"fieldTextChannelPresetStream": "如果你发现你的相机每隔几秒钟就死机一次:试着让它保持空白。",
|
||||
"fieldTextPresetStream": "如果你发现你的相机每隔几秒钟就死机一次:试着让它保持空白。",
|
||||
"fieldTextDetailSubstreamOutputPresetStream": "如果你发现你的相机每隔几秒钟就死机一次:试着让它保持空白。",
|
||||
"fieldTextPresetRecord": "如果你发现你的相机每隔几秒钟就死机一次:试着让它保持空白。",
|
||||
"fieldTextStreamQuality": "数字越大,质量越差。",
|
||||
"fieldTextDetailSubstreamOutputStreamQuality": "数字越大,质量越差。",
|
||||
"fieldTextCrf": "数字越大,质量越差。",
|
||||
"fieldTextChannelStreamQuality": "数字越大,质量越差。",
|
||||
"fieldTextStreamFps": "向客户端显示帧的速度,单位为每秒帧数。请注意,没有默认值。这可能导致高带宽占用。",
|
||||
"fieldTextDetailSubstreamOutputStreamFps": "向客户端显示帧的速度,单位为每秒帧数。请注意,没有默认值。这可能导致高带宽占用。",
|
||||
"fieldTextChannelStreamFps": "向客户端显示帧的速度,单位为每秒帧数。请注意,没有默认值。这可能导致高带宽占用。",
|
||||
"fieldTextStreamScaleX": "处理后输出的流图像的宽度。",
|
||||
"fieldTextDetailSubstreamOutputStreamScaleX": "处理后输出的流图像的宽度。",
|
||||
"fieldTextChannelStreamScaleX": "处理后输出的流图像的宽度。",
|
||||
"fieldTextStreamScaleY": "处理后输出的流图像的高度。",
|
||||
"fieldTextDetailSubstreamOutputStreamScaleY": "处理后输出的流图像的高度。",
|
||||
"fieldTextChannelStreamScaleY": "处理后输出的流图像的高度。",
|
||||
"fieldTextStreamRotate": "改变视频流的观看角度。",
|
||||
"fieldTextDetailSubstreamOutputStreamRotate": "改变视频流的观看角度。",
|
||||
"fieldTextChannelStreamRotate": "改变视频流的观看角度。",
|
||||
"fieldTextSnap": "在JPEG中获取最新的帧。",
|
||||
"Timelapse": "间隔拍摄",
|
||||
"fieldTextRecordTimelapse": "创建一个基于JPEG的时间推移。",
|
||||
"Detector Settings": "探测器的设置",
|
||||
"Primary Engine": "主引擎",
|
||||
"fieldTextDetector": "这将在FFMPEG命令中为运动检测器添加另一个输出。",
|
||||
"Call Method": "调用方法",
|
||||
"PTZ Tracking": "PTZ跟踪",
|
||||
"fieldTextDetectorPtzFollow": "用PTZ跟踪检测到的最大物体?需要运行对象检测器或提供事件的矩阵。",
|
||||
"Copy Settings": "复制设置",
|
||||
"Copy to Selected Monitor(s)": "复制到选定的监视器",
|
||||
"Notifications": "通知",
|
||||
"Methods": "方法",
|
||||
"On Unexpected Exit": "意外退出",
|
||||
"Use Raw Snapshot": "使用原始快照",
|
||||
"fieldTextLoglevel": "完成工作时要提供的数据量。",
|
||||
"fieldTextSqllog": "请谨慎使用,因为FFMPEG有时喜欢抛出多余的数据,这可能导致大量的数据库行。",
|
||||
"Log Stream": "日志流",
|
||||
"Minimum Change": "最小的变化",
|
||||
"Maximum Change": "最大的变化",
|
||||
"Trigger Threshold": "触发阈值",
|
||||
"Color Threshold": "颜色阈值",
|
||||
"Primary": "最基本的",
|
||||
"fieldTextDetectorSensitivity": "运动置信度必须超过这个值才能被视为触发。这个数字与运动检测器返回的置信度直接相关。这个选项以前被命名为“无差异的”。",
|
||||
"fieldTextDetectorMaxSensitivity": "动作置信度必须低于这个值才能被视为触发。留空表示没有最大值。这个选项以前被命名为“最大无差异”。",
|
||||
"fieldTextDetectorThreshold": "触发运动事件的最小检测数。检测必须在检测器阈值内除以检测器fps秒。例如,如果检测器fps为2,触发阈值为3,则必须在1.5秒内发生三次检测才能触发一个运动事件。该阈值针对每个检测区域。",
|
||||
"fieldTextDetectorColorThreshold": "",
|
||||
"fieldTextDetectorNoiseFilterRange": "在一个像素被视为是移动之前允许的差异量。",
|
||||
"fieldTextDetectorFrame": "这将读取整个帧的像素差异。这与创建覆盖整个屏幕的区域是一样的。如果没有区域添加到此监视器,此选项将默认为“是”。",
|
||||
"Accuracy Mode": "精度模式",
|
||||
"Tile Size": "平铺大小",
|
||||
"fieldTextTileSize": "在精度模式下,这是每个贴图的像素平方。数值越低,准确性越高,但会占用更多的资源。",
|
||||
"fieldTextEventFilters": "启用使所有事件遵守您的事件筛选规则。",
|
||||
"Filter for Objects only": "仅针对对象的过滤器",
|
||||
"Conditions": "条件",
|
||||
"eventFilterActionText": "这些是从成功的筛选条件中发生的操作。“原始选择”是指您在监视器设置中选择的选项。",
|
||||
"Drop Event": "剔除事件",
|
||||
"fieldTextActionsHalt": "让事件什么都不做,就好像它从未发生过。",
|
||||
"Save Events": "保存事件",
|
||||
"Original Choice": "原始选项",
|
||||
"Modify Indifference": "无差异修改",
|
||||
"fieldTextActionsIndifference": "修改事件所需的最小无差异。",
|
||||
"Legacy Webhook": "遗留Webhook",
|
||||
"Detector Command": "探测器命令",
|
||||
"fieldTextActionsCommand": "你可以使用它来触发一个脚本命令。",
|
||||
"Use Record Method": "使用记录方法",
|
||||
"fieldTextActionsRecord": "在“全局检测设置”部分中,使用基于事件的记录、热交换或不动地删除与其当前设置的选项。",
|
||||
"Monitor States and Schedules": "监视状态和进度",
|
||||
"Monitor States": "监控状态",
|
||||
"MonitorStatesText": "您可以在<a href='https://hub.shinobi.video/articles/view/6ylYHj9MemlZwrM' target='_blank'>ShinobiHub</a>上了解如何使用它。",
|
||||
"Schedules": "日程安排",
|
||||
"Schedule": "时间表",
|
||||
"Timezone Offset": "时区偏移",
|
||||
"Days": "天数",
|
||||
"January": "1月",
|
||||
"February": "2月",
|
||||
"March": "3月",
|
||||
"April": "4月",
|
||||
"May": "5月",
|
||||
"June": "6月",
|
||||
"July": "7月",
|
||||
"August": "8月",
|
||||
"September": "9月",
|
||||
"October": "10月",
|
||||
"November": "11月",
|
||||
"December": "12月",
|
||||
"Sunday": "周日",
|
||||
"Monday": "周一",
|
||||
"Tuesday": "周二",
|
||||
"Wednesday": "周三",
|
||||
"Thursday": "周四",
|
||||
"Friday": "周五",
|
||||
"Saturday": "周六",
|
||||
"Today": "今天",
|
||||
"Saved Logs": "保存日志",
|
||||
"Type": "类型",
|
||||
"Websocket Connected": "Websocket 连接",
|
||||
"New Authentication Token": "新的身份验证令牌",
|
||||
"Websocket Disconnected": "Websocket 断开连接",
|
||||
"Streamed Logs": "流日志",
|
||||
"Account Settings": "帐户设置",
|
||||
"Alternate Logins": "备用登录",
|
||||
"fieldTextFactorAuth": "通过已启用的方法之一启用登录的次要需求。",
|
||||
"fieldTextMail": "登录账户。主帐户持有人的电子邮件地址将收到通知。",
|
||||
"fieldTextPass": "修改设置时请留空,以保持相同的密码。",
|
||||
"fieldTextPasswordAgain": "如果要更改密码,必须匹配密码字段。",
|
||||
"fieldTextSize": "在清理之前,Shinobi允许消耗的磁盘空间大小。该值以兆字节为单位读取。",
|
||||
"Video Share": "视频分享",
|
||||
"fieldTextSizeVideoPercent": "视频可以记录的最大存储容量的百分比。",
|
||||
"Timelapse Frames Share": "延时帧共享",
|
||||
"fieldTextSizeTimelapsePercent": "延时帧可以记录的最大存储空间的百分比。",
|
||||
"FileBin Share": "文件箱文件分享",
|
||||
"fieldTextSizeFilebinPercent": "文件箱归档文件可以使用的最大存储量的百分比。",
|
||||
"fieldTextDays": "清除前保存视频的天数。",
|
||||
"fieldTextLang": "文本元素的主要语言。要完成翻译,请在conf.json中添加您的语言,例如:'language': 'en_CA'.",
|
||||
"Notification Sound": "通知提示声音",
|
||||
"fieldTextAudioNote": "当信息框出现时发出声音。",
|
||||
"No Sound": "静音",
|
||||
"Alert Sound": "警报提示音",
|
||||
"fieldTextAudioAlert": "事件发生时的声音。",
|
||||
"Alert Sound Delay": "警报声音延迟",
|
||||
"fieldTextAudioDelay": "延迟到下一次事件触发警报时。以秒为单位。",
|
||||
"Popout Monitor on Event": "事件上的弹出监视器",
|
||||
"fieldTextEventMonPop": "当事件发生时弹出监视器流。",
|
||||
"Uploaders": "上传工具",
|
||||
"Cycle Monitors per row": "每行周期监视器",
|
||||
"Number of Cycle Monitors": "周期监控器数量",
|
||||
"Cycle Monitor Height": "周期监测仪高度",
|
||||
"Cycle Interval": "周期时间间隔",
|
||||
"Clock Format": "时钟格式",
|
||||
"hlsOptions": "HLS选项",
|
||||
"Force Monitors Per Row": "每行强制监视器",
|
||||
"Get Logs to Client": "将日志发送到客户端",
|
||||
"Themes": "主题",
|
||||
"subAccountManager": "子帐户管理",
|
||||
"Sub-Accounts": "子帐户",
|
||||
"Currently Active": "当前活动",
|
||||
"Account Information": "账户信息",
|
||||
"Account Privileges": "账户权限",
|
||||
"Can Create and Delete Monitors": "可以创建和删除监视器吗?",
|
||||
"Can Change User Settings": "可以更改用户设置吗?",
|
||||
"Can View Logs": "可以查看日志吗? ",
|
||||
"Landing Page": "着陆页",
|
||||
"Clear": "清除",
|
||||
"Authenticated": "已认证",
|
||||
"Can Authenticate Websocket": "可以验证Websocket",
|
||||
"separateByCommasOrRange": "用逗号或一个范围分隔",
|
||||
"fieldTextPort": "用逗号或一个范围分隔",
|
||||
"Add All": "添加全部",
|
||||
"Other Devices": "其他设备",
|
||||
"ONVIFErr404": "没有找到。这可能只是网络设备的web面板。",
|
||||
"ONVIFErr400": "找到ONVIF端口,但检索流URL时授权失败。检查扫描使用的用户名和密码。确保您的相机时间和服务器时间同步。",
|
||||
"ONVIFErr405": "方法不允许。检查扫描使用的用户名和密码。",
|
||||
"sorryNothingWasFound": "对不起,什么也没找到。",
|
||||
"ONVIF Device Manager": "设备管理器",
|
||||
"Notice": "注意事项",
|
||||
"onvifdeviceManagerGlobalTip": "ONVIF允许修改相机的内部设置。ONVIF是一个总括性的术语,不幸的是它可以意味着很多东西。在这种情况下,您可能会在此工具中看到一个选项,但它可能无法编辑。这通常是因为相机供应商没有添加此方法或偏离了其预期用途。在这种情况下,您需要通过相机供应商规定的方法进入相机的配置,这通常是在您的web浏览器中打开相机的IP地址。",
|
||||
"Reboot Camera": "重新启动相机",
|
||||
"Gateway": "网关",
|
||||
"Hostname": "主机名",
|
||||
"Date and Time": "日期和时间",
|
||||
"UTCDateTime": "日期",
|
||||
"NTP Servers": "NTP 服务器",
|
||||
"DateTimeType": "日期管理",
|
||||
"Manual": "手动",
|
||||
"DaylightSavings": "夏令时",
|
||||
"Imaging": "成像",
|
||||
"IrCutFilter": "夜视",
|
||||
"On": "开",
|
||||
"Off": "关",
|
||||
"Brightness": "亮度",
|
||||
"ColorSaturation": "色彩饱和度",
|
||||
"Contrast": "对比",
|
||||
"BacklightCompensation": "背光补偿",
|
||||
"Exposure": "曝光",
|
||||
"MinExposureTime": "最小曝光时间",
|
||||
"MaxExposureTime": "最大曝光时间",
|
||||
"MinGain": "最小增益",
|
||||
"MaxGain": "最大增益",
|
||||
"Sharpness": "锐度",
|
||||
"WideDynamicRange": "动态宽度范围",
|
||||
"WhiteBalance": "白平衡",
|
||||
"Video Configuration": "视频配置",
|
||||
"Resolution": "分辨率",
|
||||
"FrameRateLimit": "帧率限制(FPS)",
|
||||
"BitrateLimit": "比特率限制",
|
||||
"Encoding": "编码",
|
||||
"H264Profile": "H264概要",
|
||||
"monitorConfigFinderDescription": "此工具将帮助您搜索社区发布的摄像机配置。都在 <a href='https://hub.shinobi.video/explore' target='_blank'>ShinobiHub</a>上。你也可以贴出你的,这会对社区有很大的帮助。",
|
||||
"Date Updated": "更新日期",
|
||||
"Date Added": "添加日期",
|
||||
"Title": "标题",
|
||||
"Subtitle": "副标题",
|
||||
"Newest": "最新的",
|
||||
"Oldest": "最旧的",
|
||||
"Helping Hand": "援助之手",
|
||||
"helpFinderDescription": "这个工具将帮助你学习使用Shinobi或者只是为你做一些事情。",
|
||||
"Active Tutorial": "活跃的教程",
|
||||
"Welcome": "欢迎!",
|
||||
"failedLoginText2": "请检查您的登录凭据。",
|
||||
"Configuration": "配置",
|
||||
"Controls and Logs": "控制和日志",
|
||||
"Easy Remote Access (P2P)": "轻松远程访问(P2P)",
|
||||
"Custom Auto Load": "自定义自动加载",
|
||||
"Plugin Manager": "插件管理器",
|
||||
"Not Activated": "未激活",
|
||||
"Activated": "已激活",
|
||||
"Main Configuration": "主要配置",
|
||||
"Enable Debug Log": "开启调试日志",
|
||||
"Fill in subscription ID": "填写订阅ID",
|
||||
"Server port": "服务器端口",
|
||||
"Password type": "密码类型",
|
||||
"Additional Storage": "额外的存储",
|
||||
"AdditionalStorageDes": "可以为不同的监视器设置单独的存储位置。",
|
||||
"Storage Array": "存储阵列",
|
||||
"Plugin Keys": "插件的Key",
|
||||
"PluginKeysDes": "插件的快速客户端连接设置。只需添加插件Key,使其为传入连接做好准备。",
|
||||
"Database Options": "数据库的选择",
|
||||
"DatabaseOptionDes": "连接到存储详细信息位置的凭据。",
|
||||
"Hostname / IP": "主机名/ IP",
|
||||
"CRON Options": "CRON选项",
|
||||
"deleteOld": "删除旧的视频",
|
||||
"deleteOldDes": "cron将删除超过每个帐户最大天数的视频。",
|
||||
"deleteNoVideo": "删除无视频文件",
|
||||
"deleteNoVideoDes": "cron将删除它认为没有视频文件的SQL行。",
|
||||
"deleteOverMax": "删除超出最大存储的视频",
|
||||
"deleteOverMaxDes": "Cron将删除超过每个帐户最大存储空间的文件。",
|
||||
"Email Options": "电子邮件选项",
|
||||
"service": "服务",
|
||||
"auth": "身份验证",
|
||||
"secure": "安全",
|
||||
"ignoreTLS": "忽略TLS",
|
||||
"requireTLS": "需要TLS",
|
||||
"detectorMergePamRegionTriggers": "检测器合并pam区域触发器",
|
||||
"doSnapshot": "做快照",
|
||||
"discordBot": "与机器不一致",
|
||||
"dropInEventServer": "插入事件服务器",
|
||||
"ftpServer": "ftp服务器",
|
||||
"oldPowerVideo": "旧的电源视频",
|
||||
"wallClockTimestampAsDefault": "以挂钟时间戳为默认值",
|
||||
"defaultMjpeg": "默认Mjpeg",
|
||||
"streamDir": "流目录",
|
||||
"videosDir": "视频目录",
|
||||
"windowsTempDir": "windows 临时目录",
|
||||
"Enable Face Manager": "启用面部识别管理器",
|
||||
"enableFaceManagerDes": "在仪表板中启用/禁用面部识别插件的面部管理器。",
|
||||
"PluginsDes": "精心制作插件连接设置。",
|
||||
"Https": "Https",
|
||||
"Key": "Key",
|
||||
"Plug": "插件",
|
||||
"Restart Core": "重新启动核心",
|
||||
"Update": "更新",
|
||||
"Flush PM2 Logs": "刷新PM2日志",
|
||||
"How to Connect": "如何连接",
|
||||
"HowToConnectDes1": "<b>此功能适用于移动许可证用户。</b>要获得API密钥,请登录到您的<a href='https: //licenses.shinobi.video/login' target='_blank'>Shinobi</a><b>商店</b>帐户并创建一个与任何活跃的订阅ID相关联的密钥。<a href='https://hub.shinobi.video/articles/view/3Yhivc6djTtuBPE' target='_blank'>学习更多</a>。",
|
||||
"HowToConnectDes2": "如果您想访问私人(专用)P2P服务器,请在<a href='https: //licenses.shinobi.video/login' target='_blank'>Shinobi<b>商店</b></a>并通过Live Chat小部件与我们联系",
|
||||
"Download URL for Module": "模块下载网址",
|
||||
"Subdirectory for Module": "模块子目录",
|
||||
"Inside the downloaded package": "在下载的包中",
|
||||
"Download Plugins": "下载插件",
|
||||
"pluginDownloadText": "在文档中了解如何<a href='https://docs.shinobi.video/plugin/install' target='_blank'>安装插件</a>。",
|
||||
"User": "用户",
|
||||
"Database": "数据库",
|
||||
"Current Version": "当前版本",
|
||||
"Reset Form": "重置表单",
|
||||
"Default is Global value": "默认为全局值",
|
||||
"fieldTextEventDays": "清除前保留事件的天数。",
|
||||
"EncodingInterval": "关键帧",
|
||||
"ShinobiHub": "ShinobiHub"
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ const { createWebSocketClient } = require('../basic/websocketTools.js')
|
|||
module.exports = function(s,config,lang,app,io){
|
||||
const { cameraDestroy } = require('../monitor/utils.js')(s,config,lang)
|
||||
var checkHwInterval = null;
|
||||
function onDataFromMasterNode(d) {
|
||||
async function onDataFromMasterNode(d) {
|
||||
switch(d.f){
|
||||
case'sqlCallback':
|
||||
const callbackId = d.callbackId;
|
||||
|
@ -37,14 +37,18 @@ module.exports = function(s,config,lang,app,io){
|
|||
break;
|
||||
case'cameraStop'://stop camera
|
||||
// s.group[d.d.ke].activeMonitors[d.d.mid].masterSaysToStop = true
|
||||
s.camera('stop',d.d)
|
||||
await s.camera('stop',d.d)
|
||||
break;
|
||||
case'cameraStart'://start or record camera
|
||||
s.camera(d.mode,d.d)
|
||||
let activeMonitor = s.group[d.d.ke].activeMonitors[d.d.mid]
|
||||
// activeMonitor.masterSaysToStop = false
|
||||
clearTimeout(activeMonitor.recordingChecker);
|
||||
clearTimeout(activeMonitor.streamChecker);
|
||||
try{
|
||||
await s.camera(d.mode,d.d)
|
||||
let activeMonitor = s.group[d.d.ke].activeMonitors[d.d.mid]
|
||||
// activeMonitor.masterSaysToStop = false
|
||||
clearTimeout(activeMonitor.recordingChecker);
|
||||
clearTimeout(activeMonitor.streamChecker);
|
||||
}catch(err){
|
||||
s.debugLog(err)
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
const { spawn } = require('child_process');
|
||||
const { parentPort, workerData } = require('worker_threads');
|
||||
process.on("uncaughtException", function(error) {
|
||||
console.error(error);
|
||||
});
|
||||
const activeTerminalCommands = {}
|
||||
let config = workerData.config
|
||||
let lang = workerData.lang
|
||||
let sslInfo = config.ssl || {}
|
||||
|
@ -117,7 +119,8 @@ function startConnection(p2pServerAddress,subscriptionId){
|
|||
await createWebsocketConnection(p2pServerAddress,allMessageHandlers)
|
||||
console.log('P2P : Connected! Authenticating...')
|
||||
sendDataToTunnel({
|
||||
subscriptionId: subscriptionId
|
||||
subscriptionId: subscriptionId,
|
||||
restrictedTo: config.p2pRestrictedTo || [],
|
||||
})
|
||||
clearInterval(heartbeatTimer)
|
||||
heartbeatTimer = setInterval(() => {
|
||||
|
@ -198,38 +201,82 @@ function startConnection(p2pServerAddress,subscriptionId){
|
|||
startWebsocketConnection()
|
||||
},1000 * 10 * 1.5)
|
||||
}
|
||||
// onIncomingMessage('connect',(data,requestId) => {
|
||||
// console.log('New Request Incoming',requestId)
|
||||
// await createRemoteSocket('172.16.101.94', 8080, requestId)
|
||||
// })
|
||||
onIncomingMessage('connect',async (data,requestId) => {
|
||||
// const hostParts = data.host.split(':')
|
||||
// const host = hostParts[0]
|
||||
// const port = parseInt(hostParts[1]) || 80
|
||||
s.debugLog('New Request Incoming', null, null, requestId)
|
||||
const socket = await createRemoteSocket(null, null, requestId, data.init)
|
||||
})
|
||||
if(config.p2pAllowNetworkAccess === true){
|
||||
onIncomingMessage('connect',async (data,requestId) => {
|
||||
const host = data.host || null;
|
||||
const port = data.port || null;
|
||||
s.debugLog('New Request Incoming', host, port, requestId);
|
||||
const socket = await createRemoteSocket(host, port, requestId, data.init)
|
||||
})
|
||||
}else{
|
||||
onIncomingMessage('connect',async (data,requestId) => {
|
||||
s.debugLog('New Request Incoming', requestId);
|
||||
const socket = await createRemoteSocket(null, null, requestId, data.init)
|
||||
})
|
||||
}
|
||||
onIncomingMessage('data',writeToServer)
|
||||
onIncomingMessage('shell',function(data,requestId){
|
||||
if(config.p2pShellAccess === true){
|
||||
const execCommand = data.exec
|
||||
exec(execCommand,function(err,response){
|
||||
sendDataToTunnel({
|
||||
f: 'exec',
|
||||
requestId,
|
||||
err,
|
||||
response,
|
||||
if(config.p2pShellAccess === true){
|
||||
onIncomingMessage('shell_exit',function(data,requestId){
|
||||
const shellId = data.shellId;
|
||||
if(activeTerminalCommands[shellId]){
|
||||
const theCommand = activeTerminalCommands[shellId]
|
||||
theCommand.stdin.pause();
|
||||
theCommand.kill();
|
||||
}
|
||||
})
|
||||
onIncomingMessage('shell',function(data,requestId){
|
||||
const shellId = data.shellId;
|
||||
if(activeTerminalCommands[shellId]){
|
||||
const theCommand = activeTerminalCommands[shellId]
|
||||
const commandRunAfterProcessStart = data.exec
|
||||
switch(commandRunAfterProcessStart){
|
||||
case'^C':
|
||||
theCommand.stdin.pause();
|
||||
theCommand.kill();
|
||||
break;
|
||||
default:
|
||||
theCommand.stdin.write(`${commandRunAfterProcessStart}\n`)
|
||||
break;
|
||||
}
|
||||
}else{
|
||||
if(data.exec.startsWith('^')){
|
||||
outboundMessage('shell_err',{
|
||||
shellId,
|
||||
err: "No process running to use this command."
|
||||
},requestId)
|
||||
return;
|
||||
}
|
||||
const newCommand = spawn('/bin/sh')
|
||||
activeTerminalCommands[shellId] = newCommand;
|
||||
newCommand.stdout.on('data',function(d){
|
||||
const data = d.toString();
|
||||
outboundMessage('shell_stdout',{
|
||||
shellId,
|
||||
data,
|
||||
},requestId)
|
||||
})
|
||||
})
|
||||
}else{
|
||||
sendDataToTunnel({
|
||||
f: 'exec',
|
||||
requestId,
|
||||
err: lang['Not Authorized'],
|
||||
response: '',
|
||||
})
|
||||
}
|
||||
})
|
||||
newCommand.stderr.on('data',function(d){
|
||||
const data = d.toString();
|
||||
outboundMessage('shell_stderr',{
|
||||
shellId,
|
||||
data,
|
||||
},requestId)
|
||||
})
|
||||
newCommand.on('exit',function(){
|
||||
outboundMessage('shell_exit',{
|
||||
shellId,
|
||||
},requestId)
|
||||
})
|
||||
newCommand.on('close',function(){
|
||||
outboundMessage('shell_close',{
|
||||
shellId,
|
||||
},requestId)
|
||||
delete(activeTerminalCommands[shellId])
|
||||
})
|
||||
newCommand.stdin.write(`${data.exec}\n`)
|
||||
}
|
||||
})
|
||||
}
|
||||
onIncomingMessage('resume',function(data,requestId){
|
||||
requestConnections[requestId].resume()
|
||||
})
|
||||
|
|
|
@ -23,7 +23,10 @@ module.exports = function(s,config,lang,app,io){
|
|||
break;
|
||||
case'control':
|
||||
ptzControl(data,function(msg){
|
||||
s.userLog(data,msg);
|
||||
s.userLog(data,{
|
||||
type: lang['Control'],
|
||||
msg: msg
|
||||
});
|
||||
connection.emit('f',{
|
||||
f: 'control',
|
||||
response: msg
|
||||
|
|
|
@ -43,87 +43,94 @@ module.exports = function(s,config,lang,app,io){
|
|||
})
|
||||
return newOptions
|
||||
}
|
||||
const runOnvifMethod = async (onvifOptions,callback) => {
|
||||
var onvifAuth = onvifOptions.auth
|
||||
var response = {ok: false}
|
||||
var errorMessage = function(msg,error){
|
||||
response.ok = false
|
||||
response.msg = msg
|
||||
response.error = error
|
||||
callback(response)
|
||||
}
|
||||
var actionCallback = function(onvifActionResponse){
|
||||
response.ok = true
|
||||
if(onvifActionResponse.data){
|
||||
response.responseFromDevice = onvifActionResponse.data
|
||||
}else{
|
||||
response.responseFromDevice = onvifActionResponse
|
||||
const runOnvifMethod = (onvifOptions,callback) => {
|
||||
return new Promise((resolve) => {
|
||||
var onvifAuth = onvifOptions.auth
|
||||
var response = {ok: false}
|
||||
function doCallback(response){
|
||||
if(callback)callback(response)
|
||||
resolve(response)
|
||||
}
|
||||
if(onvifActionResponse.soap)response.soap = onvifActionResponse.soap
|
||||
callback(response)
|
||||
}
|
||||
var isEmpty = function(obj) {
|
||||
for(var key in obj) {
|
||||
if(obj.hasOwnProperty(key))
|
||||
return false;
|
||||
var errorMessage = function(msg,error){
|
||||
response.ok = false
|
||||
response.msg = msg
|
||||
response.error = error
|
||||
doCallback(response)
|
||||
}
|
||||
return true;
|
||||
}
|
||||
var doAction = function(Camera){
|
||||
var completeAction = function(command){
|
||||
if(command && command.then){
|
||||
command.then(actionCallback).catch(function(error){
|
||||
errorMessage('Device Action responded with an error',error)
|
||||
})
|
||||
}else if(command){
|
||||
response.ok = true
|
||||
response.repsonseFromDevice = command
|
||||
callback(response)
|
||||
var actionCallback = function(onvifActionResponse){
|
||||
response.ok = true
|
||||
if(onvifActionResponse.data){
|
||||
response.responseFromDevice = onvifActionResponse.data
|
||||
}else{
|
||||
response.error = 'Big Errors, Please report it to Shinobi Development'
|
||||
callback(response)
|
||||
response.responseFromDevice = onvifActionResponse
|
||||
}
|
||||
if(onvifActionResponse.soap)response.soap = onvifActionResponse.soap
|
||||
doCallback(response)
|
||||
}
|
||||
var action
|
||||
if(onvifAuth.service){
|
||||
if(Camera.services[onvifAuth.service] === undefined){
|
||||
return errorMessage('This is not an available service. Please use one of the following : '+Object.keys(Camera.services).join(', '))
|
||||
var isEmpty = function(obj) {
|
||||
for(var key in obj) {
|
||||
if(obj.hasOwnProperty(key))
|
||||
return false;
|
||||
}
|
||||
if(Camera.services[onvifAuth.service] === null){
|
||||
return errorMessage('This service is not activated. Maybe you are not connected through ONVIF. You can test by attempting to use the "Control" feature with ONVIF in Shinobi.')
|
||||
}
|
||||
action = Camera.services[onvifAuth.service][onvifAuth.action]
|
||||
}else{
|
||||
action = Camera[onvifAuth.action]
|
||||
return true;
|
||||
}
|
||||
if(!action || typeof action !== 'function'){
|
||||
errorMessage(onvifAuth.action+' is not an available ONVIF function. See https://github.com/futomi/node-onvif for functions.')
|
||||
}else{
|
||||
var argNames = s.getFunctionParamNames(action)
|
||||
var options
|
||||
var command
|
||||
if(argNames[0] === 'options' || argNames[0] === 'params'){
|
||||
options = replaceDynamicInOptions(Camera,onvifOptions.options || {})
|
||||
response.options = options
|
||||
var doAction = function(Camera){
|
||||
var completeAction = function(command){
|
||||
if(command && command.then){
|
||||
command.then(actionCallback).catch(function(error){
|
||||
errorMessage('Device Action responded with an error',error)
|
||||
})
|
||||
}else if(command){
|
||||
response.ok = true
|
||||
response.repsonseFromDevice = command
|
||||
doCallback(response)
|
||||
}else{
|
||||
response.error = 'Big Errors, Please report it to Shinobi Development'
|
||||
doCallback(response)
|
||||
}
|
||||
}
|
||||
var action
|
||||
if(onvifAuth.service){
|
||||
command = Camera.services[onvifAuth.service][onvifAuth.action](options)
|
||||
if(Camera.services[onvifAuth.service] === undefined){
|
||||
return errorMessage('This is not an available service. Please use one of the following : '+Object.keys(Camera.services).join(', '))
|
||||
}
|
||||
if(Camera.services[onvifAuth.service] === null){
|
||||
return errorMessage('This service is not activated. Maybe you are not connected through ONVIF. You can test by attempting to use the "Control" feature with ONVIF in Shinobi.')
|
||||
}
|
||||
action = Camera.services[onvifAuth.service][onvifAuth.action]
|
||||
}else{
|
||||
command = Camera[onvifAuth.action](options)
|
||||
action = Camera[onvifAuth.action]
|
||||
}
|
||||
if(!action || typeof action !== 'function'){
|
||||
errorMessage(onvifAuth.action+' is not an available ONVIF function. See https://github.com/futomi/node-onvif for functions.')
|
||||
}else{
|
||||
var argNames = s.getFunctionParamNames(action)
|
||||
var options
|
||||
var command
|
||||
if(argNames[0] === 'options' || argNames[0] === 'params'){
|
||||
options = replaceDynamicInOptions(Camera,onvifOptions.options || {})
|
||||
response.options = options
|
||||
}
|
||||
if(onvifAuth.service){
|
||||
command = Camera.services[onvifAuth.service][onvifAuth.action](options)
|
||||
}else{
|
||||
command = Camera[onvifAuth.action](options)
|
||||
}
|
||||
completeAction(command)
|
||||
}
|
||||
completeAction(command)
|
||||
}
|
||||
}
|
||||
if(!s.group[onvifAuth.ke].activeMonitors[onvifAuth.id].onvifConnection){
|
||||
const response = await createOnvifDevice(onvifAuth)
|
||||
if(response.ok){
|
||||
doAction(response.device)
|
||||
if(!s.group[onvifAuth.ke].activeMonitors[onvifAuth.id].onvifConnection){
|
||||
createOnvifDevice(onvifAuth).then((response) => {
|
||||
if(response.ok){
|
||||
doAction(response.device)
|
||||
}else{
|
||||
errorMessage(response.msg,response.error)
|
||||
}
|
||||
})
|
||||
}else{
|
||||
errorMessage(response.msg,response.error)
|
||||
doAction(s.group[onvifAuth.ke].activeMonitors[onvifAuth.id].onvifConnection)
|
||||
}
|
||||
}else{
|
||||
doAction(s.group[onvifAuth.ke].activeMonitors[onvifAuth.id].onvifConnection)
|
||||
}
|
||||
})
|
||||
}
|
||||
async function getSnapshotFromOnvif(onvifOptions){
|
||||
let theUrl;
|
||||
|
|
|
@ -132,6 +132,14 @@ module.exports = function(s,config,lang){
|
|||
}
|
||||
function returnResponse(){
|
||||
return new Promise((resolve,reject) => {
|
||||
if(
|
||||
!device ||
|
||||
!device.current_profile ||
|
||||
!device.current_profile.token
|
||||
){
|
||||
resolve({ok: false, msg: lang.ONVIFEventsNotAvailableText1})
|
||||
return
|
||||
}
|
||||
controlOptions.ProfileToken = device.current_profile.token
|
||||
s.runOnvifMethod({
|
||||
auth: {
|
||||
|
@ -200,22 +208,24 @@ module.exports = function(s,config,lang){
|
|||
options.direction = doMove ? options.direction : 'stopMove';
|
||||
switch(options.direction){
|
||||
case'center':
|
||||
const onvifHomeControlMethod = monitorConfig.details.onvif_non_standard
|
||||
const actionFunction = onvifHomeControlMethod === '2' ? moveToHomePosition : moveToPresetPosition
|
||||
moveLock[options.ke + options.id] = true
|
||||
moveToPresetPosition({
|
||||
actionFunction({
|
||||
ke: options.ke,
|
||||
id: options.id,
|
||||
},(endData) => {
|
||||
moveLock[options.ke + options.id] = false
|
||||
resolve({ type: lang['Moving to Home Preset'], response: endData })
|
||||
resolve({ type: lang['Moving to Home Preset'], msg: endData })
|
||||
})
|
||||
break;
|
||||
case'stopMove':
|
||||
resolve({ type: lang['Control Trigger Ended'] })
|
||||
stopMoveOnvif({
|
||||
ke: options.ke,
|
||||
id: options.id,
|
||||
}).then((response) => {
|
||||
moveLock[options.ke + options.id] = false
|
||||
resolve({ type: lang['Control Trigger Ended'], msg: response })
|
||||
})
|
||||
break;
|
||||
default:
|
||||
|
@ -225,7 +235,7 @@ module.exports = function(s,config,lang){
|
|||
if(!moveResponse.ok){
|
||||
s.debugLog('ONVIF Move Error',moveResponse)
|
||||
}
|
||||
resolve(moveResponse)
|
||||
resolve({ type: lang['Control Triggered'], msg: moveResponse })
|
||||
})
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
|
@ -259,6 +269,8 @@ module.exports = function(s,config,lang){
|
|||
const controlUrlMethod = monitorConfig.details.control_url_method || 'GET'
|
||||
const controlUrlStopTimeout = options.moveTimeout || parseInt(monitorConfig.details.control_url_stop_timeout) || 1000
|
||||
const stopCommandEnabled = monitorConfig.details.control_stop === '1' || monitorConfig.details.control_stop === '2';
|
||||
const axisLock = monitorConfig.details.control_axis_lock
|
||||
const direction = options.direction
|
||||
if(monitorConfig.details.control !== "1"){
|
||||
s.userLog(monitorConfig,{
|
||||
type: lang['Control Error'],
|
||||
|
@ -270,14 +282,28 @@ module.exports = function(s,config,lang){
|
|||
}
|
||||
}
|
||||
let response = {
|
||||
direction: options.direction,
|
||||
direction,
|
||||
}
|
||||
if(axisLock){
|
||||
const isHorizontalOnly = axisLock === '1'
|
||||
const isVerticalOnly = axisLock === '2'
|
||||
const moveIsHorizontal = direction === 'left' || direction === 'right'
|
||||
const moveIsVertical = direction === 'up' || direction === 'down'
|
||||
if(
|
||||
isHorizontalOnly && moveIsVertical ||
|
||||
isVerticalOnly && moveIsHorizontal
|
||||
){
|
||||
response.ok = false
|
||||
response.msg = isHorizontalOnly ? lang['Pan Only'] : lang['Tilt Only']
|
||||
return response
|
||||
}
|
||||
}
|
||||
if(controlUrlMethod === 'ONVIF'){
|
||||
if(options.direction === 'center'){
|
||||
if(direction === 'center'){
|
||||
response.moveResponse = await moveOnvifCamera(options,true)
|
||||
}else if(stopCommandEnabled){
|
||||
response.moveResponse = await moveOnvifCamera(options,true)
|
||||
if(options.direction !== 'stopMove' && options.direction !== 'center'){
|
||||
if(direction !== 'stopMove' && direction !== 'center'){
|
||||
await asyncSetTimeout(controlUrlStopTimeout)
|
||||
response.stopMoveResponse = await moveOnvifCamera(options,false)
|
||||
response.ok = response.moveResponse.ok && response.stopMoveResponse.ok;
|
||||
|
@ -288,7 +314,7 @@ module.exports = function(s,config,lang){
|
|||
response = await relativeMoveOnvif(options);
|
||||
}
|
||||
}else{
|
||||
if(options.direction === 'stopMove'){
|
||||
if(direction === 'stopMove'){
|
||||
response = await moveGeneric(options,false)
|
||||
}else{
|
||||
// left, right, up, down, center
|
||||
|
@ -320,22 +346,25 @@ module.exports = function(s,config,lang){
|
|||
},callback)
|
||||
}
|
||||
const setPresetForCurrentPosition = (options,callback) => {
|
||||
const nonStandardOnvif = s.group[options.ke].rawMonitorConfigurations[options.id].details.onvif_non_standard === '1'
|
||||
const profileToken = options.ProfileToken || "__CURRENT_TOKEN"
|
||||
s.runOnvifMethod({
|
||||
auth: {
|
||||
ke: options.ke,
|
||||
id: options.id,
|
||||
service: 'ptz',
|
||||
action: 'setPreset',
|
||||
},
|
||||
options: {
|
||||
ProfileToken: profileToken,
|
||||
PresetToken: nonStandardOnvif ? '1' : options.PresetToken || profileToken,
|
||||
PresetName: options.PresetName || nonStandardOnvif ? '1' : profileToken
|
||||
},
|
||||
},(endData) => {
|
||||
callback(endData)
|
||||
return new Promise((resolve) => {
|
||||
const nonStandardOnvif = s.group[options.ke].rawMonitorConfigurations[options.id].details.onvif_non_standard === '1'
|
||||
const profileToken = options.ProfileToken || "__CURRENT_TOKEN"
|
||||
s.runOnvifMethod({
|
||||
auth: {
|
||||
ke: options.ke,
|
||||
id: options.id,
|
||||
service: 'ptz',
|
||||
action: 'setPreset',
|
||||
},
|
||||
options: {
|
||||
ProfileToken: profileToken,
|
||||
PresetToken: nonStandardOnvif ? '1' : options.PresetToken || profileToken,
|
||||
PresetName: options.PresetName || nonStandardOnvif ? '1' : profileToken
|
||||
},
|
||||
},(response) => {
|
||||
if(callback)callback(response)
|
||||
resolve(response)
|
||||
})
|
||||
})
|
||||
}
|
||||
const moveToPresetPosition = (options,callback) => {
|
||||
|
@ -359,10 +388,50 @@ module.exports = function(s,config,lang){
|
|||
},
|
||||
},callback)
|
||||
}
|
||||
const setHomePositionTimeout = (event) => {
|
||||
const moveToHomePosition = (options,callback) => {
|
||||
const nonStandardOnvif = s.group[options.ke].rawMonitorConfigurations[options.id].details.onvif_non_standard === '1'
|
||||
const profileToken = options.ProfileToken || "__CURRENT_TOKEN"
|
||||
return s.runOnvifMethod({
|
||||
auth: {
|
||||
ke: options.ke,
|
||||
id: options.id,
|
||||
service: 'ptz',
|
||||
action: 'gotoHomePosition',
|
||||
},
|
||||
options: {
|
||||
ProfileToken: profileToken,
|
||||
},
|
||||
},callback)
|
||||
}
|
||||
const setHomePosition = (options,callback) => {
|
||||
return new Promise((resolve) => {
|
||||
const nonStandardOnvif = s.group[options.ke].rawMonitorConfigurations[options.id].details.onvif_non_standard === '1'
|
||||
const profileToken = options.ProfileToken || "__CURRENT_TOKEN"
|
||||
s.runOnvifMethod({
|
||||
auth: {
|
||||
ke: options.ke,
|
||||
id: options.id,
|
||||
service: 'ptz',
|
||||
action: 'setHomePosition',
|
||||
},
|
||||
options: {
|
||||
ProfileToken: profileToken,
|
||||
},
|
||||
},(response) => {
|
||||
if(callback)callback(response)
|
||||
resolve(response)
|
||||
})
|
||||
})
|
||||
}
|
||||
const moveToHomePositionTimeout = (event) => {
|
||||
const groupKey = event.ke
|
||||
const monitorId = event.id
|
||||
const monitorConfig = s.group[groupKey].rawMonitorConfigurations[monitorId]
|
||||
const onvifHomeControlMethod = monitorConfig.details.onvif_non_standard
|
||||
const actionFunction = onvifHomeControlMethod === '2' ? moveToHomePosition : moveToPresetPosition
|
||||
clearTimeout(ptzTimeoutsUntilResetToHome[event.ke + event.id])
|
||||
ptzTimeoutsUntilResetToHome[event.ke + event.id] = setTimeout(() => {
|
||||
moveToPresetPosition({
|
||||
actionFunction({
|
||||
ke: event.ke,
|
||||
id: event.id,
|
||||
},(endData) => {
|
||||
|
@ -424,49 +493,40 @@ module.exports = function(s,config,lang){
|
|||
id: event.id,
|
||||
ke: event.ke
|
||||
},(msg) => {
|
||||
s.userLog(event,msg)
|
||||
s.userLog(event,{
|
||||
type: lang['Control'],
|
||||
msg: msg
|
||||
});
|
||||
// console.log(msg)
|
||||
setHomePositionTimeout(event)
|
||||
moveToHomePositionTimeout(event)
|
||||
})
|
||||
}else{
|
||||
setHomePositionTimeout(event)
|
||||
moveToHomePositionTimeout(event)
|
||||
}
|
||||
}
|
||||
function setHomePositionPreset(e){
|
||||
async function setHomePositionPreset(e){
|
||||
const groupKey = e.ke
|
||||
const monitorId = e.mid || e.id
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
setPresetForCurrentPosition({
|
||||
ke: e.ke,
|
||||
id: monitorId
|
||||
},(endData) => {
|
||||
if(endData.ok === false){
|
||||
setTimeout(() => {
|
||||
setPresetForCurrentPosition({
|
||||
ke: e.ke,
|
||||
id: monitorId
|
||||
},(endData) => {
|
||||
if(endData.ok === false){
|
||||
setTimeout(() => {
|
||||
setPresetForCurrentPosition({
|
||||
ke: e.ke,
|
||||
id: monitorId
|
||||
},(endData) => {
|
||||
console.log(endData)
|
||||
resolve()
|
||||
})
|
||||
},5000)
|
||||
}else{
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
},5000)
|
||||
}else{
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
},5000)
|
||||
})
|
||||
const controlOptions = {
|
||||
ke: groupKey,
|
||||
id: monitorId
|
||||
}
|
||||
const waitTime = 5000
|
||||
let response = {ok: false}
|
||||
const monitorConfig = s.group[groupKey].rawMonitorConfigurations[monitorId]
|
||||
const onvifHomeControlMethod = monitorConfig.details.onvif_non_standard
|
||||
const actionFunction = onvifHomeControlMethod === '2' ? setHomePosition : setPresetForCurrentPosition
|
||||
await asyncSetTimeout(waitTime)
|
||||
response = await actionFunction(controlOptions)
|
||||
if(response.ok === false){
|
||||
await asyncSetTimeout(waitTime)
|
||||
response = await actionFunction(controlOptions)
|
||||
if(response.ok === false){
|
||||
await asyncSetTimeout(waitTime)
|
||||
response = await actionFunction(controlOptions)
|
||||
}
|
||||
}
|
||||
return response
|
||||
}
|
||||
return {
|
||||
startMove,
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
const async = require("async");
|
||||
const {
|
||||
stringToSqlTime,
|
||||
} = require('../common.js')
|
||||
module.exports = function(s,config){
|
||||
const isMySQL = config.databaseType === 'mysql';
|
||||
const runQuery = async.queue(function(data, callback) {
|
||||
|
@ -9,6 +6,10 @@ module.exports = function(s,config){
|
|||
.raw(data.query,data.values)
|
||||
.asCallback(callback)
|
||||
}, 4);
|
||||
function stringToSqlTime(value){
|
||||
newValue = new Date(value.replace('T',' '))
|
||||
return newValue
|
||||
}
|
||||
const mergeQueryValues = function(query,values){
|
||||
if(!values){values=[]}
|
||||
var valuesNotFunction = true;
|
||||
|
|
|
@ -33,7 +33,7 @@ module.exports = function(s,config,lang){
|
|||
try {
|
||||
detector.listen((motion) => {
|
||||
if (motion) {
|
||||
// onvifEventLog(`ONVIF Event Detected!`)
|
||||
onvifEventLog(`ONVIF Event Detected!`)
|
||||
triggerEvent({
|
||||
f: 'trigger',
|
||||
id: monitorId,
|
||||
|
@ -49,16 +49,17 @@ module.exports = function(s,config,lang){
|
|||
// imgWidth: img.width,
|
||||
}
|
||||
})
|
||||
// } else {
|
||||
// onvifEventLog(`ONVIF Event Stopped`)
|
||||
} else {
|
||||
onvifEventLog(`ONVIF Event Stopped`)
|
||||
}
|
||||
});
|
||||
} catch(e) {
|
||||
console.error(e)
|
||||
onvifEventLog(`ONVIF Event Error`,e)
|
||||
}
|
||||
return detector
|
||||
}
|
||||
function initializeOnvifEvents(monitorConfig){
|
||||
async function initializeOnvifEvents(monitorConfig){
|
||||
const monitorMode = monitorConfig.mode
|
||||
const groupKey = monitorConfig.ke
|
||||
const monitorId = monitorConfig.mid
|
||||
|
@ -74,13 +75,19 @@ module.exports = function(s,config,lang){
|
|||
onvifEventControllers[onvifIdKey].close()
|
||||
s.debugLog('ONVIF Event Module Warning : This could cause a memory leak?')
|
||||
}catch(err){
|
||||
s.debugLog('ONVIF Event Module Error', err);
|
||||
s.debugLog('ONVIF Event Module Error', err.stack);
|
||||
}
|
||||
delete(onvifEventControllers[onvifIdKey])
|
||||
if(monitorMode !== 'stop'){
|
||||
startMotion(onvifId,monitorConfig).then((detector) => {
|
||||
try{
|
||||
delete(onvifEventControllers[onvifIdKey])
|
||||
s.debugLog('Can ',monitorConfig.name, 'read ONVIF Events?',monitorMode !== 'stop')
|
||||
if(monitorMode !== 'stop'){
|
||||
s.debugLog('Starting ONVIF Event Reader on ',monitorConfig.name)
|
||||
const detector = await startMotion(onvifId,monitorConfig)
|
||||
onvifEventControllers[onvifIdKey] = detector;
|
||||
})
|
||||
}
|
||||
}catch(err){
|
||||
console.error(err)
|
||||
s.debugLog('ONVIF Event Module Start Error', err.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ module.exports = (s,config,lang) => {
|
|||
isEven,
|
||||
fetchTimeout,
|
||||
} = require('../basic/utils.js')(process.cwd(),config)
|
||||
const glyphs = require('../../definitions/glyphs.js')
|
||||
async function saveImageFromEvent(options,frameBuffer){
|
||||
const monitorId = options.mid || options.id
|
||||
const groupKey = options.ke
|
||||
|
@ -351,7 +352,7 @@ module.exports = (s,config,lang) => {
|
|||
function bindTagLegendForMonitors(groupKey){
|
||||
const newTagLegend = {}
|
||||
const theGroup = s.group[groupKey]
|
||||
const monitorIds = Object.keys(theGroup.rawMonitorConfigurations)
|
||||
const monitorIds = Object.keys(theGroup.rawMonitorConfigurations || {})
|
||||
monitorIds.forEach((monitorId) => {
|
||||
const monitorConfig = theGroup.rawMonitorConfigurations[monitorId]
|
||||
const theTags = (monitorConfig.tags || '').split(',')
|
||||
|
@ -546,8 +547,12 @@ module.exports = (s,config,lang) => {
|
|||
clearTimeout(activeMonitor.eventBasedRecording.timeout)
|
||||
activeMonitor.eventBasedRecording.timeout = setTimeout(function(){
|
||||
activeMonitor.eventBasedRecording.allowEnd = true
|
||||
activeMonitor.eventBasedRecording.process.stdin.setEncoding('utf8')
|
||||
activeMonitor.eventBasedRecording.process.stdin.write('q')
|
||||
try{
|
||||
activeMonitor.eventBasedRecording.process.stdin.setEncoding('utf8')
|
||||
activeMonitor.eventBasedRecording.process.stdin.write('q')
|
||||
}catch(err){
|
||||
s.debugLog(err)
|
||||
}
|
||||
activeMonitor.eventBasedRecording.process.kill('SIGINT')
|
||||
delete(activeMonitor.eventBasedRecording.timeout)
|
||||
},detector_timeout * 1000 * 60)
|
||||
|
@ -824,7 +829,27 @@ module.exports = (s,config,lang) => {
|
|||
|
||||
return newRegions;
|
||||
}
|
||||
function getTagWithIcon(tag){
|
||||
var icon = glyphs[tag.toLowerCase()] || glyphs._default
|
||||
return `${icon} ${tag}`;
|
||||
}
|
||||
function getObjectTagsFromMatrices(d){
|
||||
if(d.details.reason === 'motion'){
|
||||
return [getTagWithIcon(lang.Motion)]
|
||||
}else{
|
||||
const matrices = d.details.matrices
|
||||
return [...new Set(matrices.map(matrix => getTagWithIcon(matrix.tag)))];
|
||||
}
|
||||
}
|
||||
function getObjectTagNotifyText(d){
|
||||
const monitorId = d.mid || d.id
|
||||
const monitorName = s.group[d.ke].rawMonitorConfigurations[monitorId].name
|
||||
const tags = getObjectTagsFromMatrices(d)
|
||||
return `${tags.join(', ')} ${lang.detected} in ${monitorName}`
|
||||
}
|
||||
return {
|
||||
getObjectTagNotifyText,
|
||||
getObjectTagsFromMatrices,
|
||||
countObjects: countObjects,
|
||||
isAtleastOneMatrixInRegion,
|
||||
convertRegionPointsToNewDimensions,
|
||||
|
|
|
@ -1295,6 +1295,7 @@ module.exports = (s,config,lang) => {
|
|||
async function doFatalErrorCatch(e,d){
|
||||
const groupKey = e.ke
|
||||
const monitorId = e.mid || e.id
|
||||
const activeMonitor = getActiveMonitor(groupKey,monitorId)
|
||||
if(activeMonitor.isStarted === true){
|
||||
const activeMonitor = getActiveMonitor(groupKey,monitorId)
|
||||
activeMonitor.isStarted = false
|
||||
|
|
|
@ -2,6 +2,7 @@ var fs = require("fs")
|
|||
var Discord = require("discord.js")
|
||||
module.exports = function(s,config,lang,getSnapshot){
|
||||
const {
|
||||
getObjectTagNotifyText,
|
||||
getEventBasedRecordingUponCompletion,
|
||||
} = require('../events/utils.js')(s,config,lang)
|
||||
//discord bot
|
||||
|
@ -56,6 +57,8 @@ module.exports = function(s,config,lang,getSnapshot){
|
|||
//discord bot
|
||||
const isEnabled = filter.discord || monitorConfig.details.detector_discordbot === '1' || monitorConfig.details.notify_discord === '1'
|
||||
if(s.group[d.ke].discordBot && isEnabled && !s.group[d.ke].activeMonitors[d.id].detector_discordbot){
|
||||
const monitorName = s.group[d.ke].rawMonitorConfigurations[d.id].name
|
||||
const notifyText = getObjectTagNotifyText(d)
|
||||
var detector_discordbot_timeout
|
||||
if(!monitorConfig.details.detector_discordbot_timeout||monitorConfig.details.detector_discordbot_timeout===''){
|
||||
detector_discordbot_timeout = 1000 * 60 * 10;
|
||||
|
@ -70,11 +73,11 @@ module.exports = function(s,config,lang,getSnapshot){
|
|||
if(d.screenshotBuffer){
|
||||
sendMessage({
|
||||
author: {
|
||||
name: s.group[d.ke].rawMonitorConfigurations[d.id].name,
|
||||
name: monitorName,
|
||||
icon_url: config.iconURL
|
||||
},
|
||||
title: lang.Event+' - '+d.screenshotName,
|
||||
description: lang.EventText1+' '+d.currentTimestamp,
|
||||
title: notifyText,
|
||||
description: notifyText+' '+d.currentTimestamp,
|
||||
fields: [],
|
||||
timestamp: d.currentTime,
|
||||
footer: {
|
||||
|
@ -84,7 +87,7 @@ module.exports = function(s,config,lang,getSnapshot){
|
|||
},[
|
||||
{
|
||||
attachment: d.screenshotBuffer,
|
||||
name: d.screenshotName+'.jpg'
|
||||
name: notifyText + '.jpg'
|
||||
}
|
||||
],d.ke)
|
||||
}
|
||||
|
@ -106,20 +109,21 @@ module.exports = function(s,config,lang,getSnapshot){
|
|||
if(videoPath){
|
||||
sendMessage({
|
||||
author: {
|
||||
name: s.group[d.ke].rawMonitorConfigurations[d.id].name,
|
||||
name: monitorName,
|
||||
icon_url: config.iconURL
|
||||
},
|
||||
title: videoName,
|
||||
title: `${notifyText}`,
|
||||
description: notifyText,
|
||||
fields: [],
|
||||
timestamp: d.currentTime,
|
||||
footer: {
|
||||
icon_url: config.iconURL,
|
||||
text: "Shinobi Systems"
|
||||
icon_url: config.iconURL,
|
||||
text: "Shinobi Systems"
|
||||
}
|
||||
},[
|
||||
{
|
||||
attachment: videoPath,
|
||||
name: videoName
|
||||
name: notifyText + '.mp4'
|
||||
}
|
||||
],d.ke)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ const fs = require("fs")
|
|||
const fetch = require("node-fetch")
|
||||
module.exports = function(s,config,lang,getSnapshot){
|
||||
const {
|
||||
getObjectTagNotifyText,
|
||||
getEventBasedRecordingUponCompletion,
|
||||
} = require('../events/utils.js')(s,config,lang)
|
||||
//matrix bot
|
||||
|
@ -76,6 +77,7 @@ module.exports = function(s,config,lang,getSnapshot){
|
|||
// d = event object
|
||||
const isEnabled = filter.matrixBot || monitorConfig.details.detector_matrixbot === '1' || monitorConfig.details.notify_matrix === '1'
|
||||
if(s.group[d.ke].matrixBot && isEnabled && !s.group[d.ke].activeMonitors[d.id].detector_matrixbot){
|
||||
const notifyText = getObjectTagNotifyText(d)
|
||||
var detector_matrixbot_timeout
|
||||
if(!monitorConfig.details.detector_matrixbot_timeout||monitorConfig.details.detector_matrixbot_timeout===''){
|
||||
detector_matrixbot_timeout = 1000 * 60 * 10;
|
||||
|
@ -88,9 +90,8 @@ module.exports = function(s,config,lang,getSnapshot){
|
|||
},detector_matrixbot_timeout)
|
||||
await getSnapshot(d,monitorConfig)
|
||||
if(d.screenshotBuffer){
|
||||
const imageEventText = `${lang.Event} ${d.screenshotName} ${d.currentTimestamp}`
|
||||
sendMessage({
|
||||
text: imageEventText,
|
||||
text: notifyText,
|
||||
},[
|
||||
{
|
||||
buffer: d.screenshotBuffer,
|
||||
|
@ -119,7 +120,9 @@ module.exports = function(s,config,lang,getSnapshot){
|
|||
videoName = siftedVideoFileFromRam.filename
|
||||
}
|
||||
if(videoPath){
|
||||
sendMessage({},[
|
||||
sendMessage({
|
||||
text: notifyText,
|
||||
},[
|
||||
{
|
||||
buffer: await fs.promises.readFile(videoPath),
|
||||
name: videoName,
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
var fs = require('fs');
|
||||
module.exports = function (s, config, lang, getSnapshot) {
|
||||
const { getEventBasedRecordingUponCompletion } =
|
||||
require('../events/utils.js')(s, config, lang);
|
||||
const {
|
||||
getObjectTagNotifyText,
|
||||
getEventBasedRecordingUponCompletion,
|
||||
} = require('../events/utils.js')(s,config,lang)
|
||||
|
||||
if (config.pushover === true) {
|
||||
const Pushover = require('pushover-notifications');
|
||||
|
@ -116,6 +118,7 @@ module.exports = function (s, config, lang, getSnapshot) {
|
|||
(filter.pushover || monitorConfig.details.notify_pushover === '1') &&
|
||||
!s.group[d.ke].activeMonitors[d.id].detector_pushover
|
||||
) {
|
||||
const notifyText = getObjectTagNotifyText(d)
|
||||
var detector_pushover_timeout;
|
||||
if (
|
||||
!monitorConfig.details.detector_pushover_timeout ||
|
||||
|
@ -144,9 +147,8 @@ module.exports = function (s, config, lang, getSnapshot) {
|
|||
if (d.screenshotBuffer) {
|
||||
sendMessage(
|
||||
{
|
||||
title: lang.Event + ' - ' + d.screenshotName,
|
||||
description:
|
||||
lang.EventText1 + ' ' + d.currentTimestamp,
|
||||
title: notifyText,
|
||||
description: lang.EventText1 + ' ' + d.currentTimestamp,
|
||||
},
|
||||
[
|
||||
{
|
||||
|
|
|
@ -8,47 +8,89 @@ var fs = require("fs")
|
|||
// }
|
||||
module.exports = function(s,config,lang,getSnapshot){
|
||||
const {
|
||||
getObjectTagNotifyText,
|
||||
getEventBasedRecordingUponCompletion,
|
||||
} = require('../events/utils.js')(s,config,lang)
|
||||
const {
|
||||
getStreamDirectory
|
||||
} = require('../monitor/utils.js')(s,config,lang)
|
||||
const {
|
||||
ffprobe
|
||||
} = require('../ffmpeg/utils.js')(s,config,lang)
|
||||
|
||||
//telegram bot
|
||||
if(config.telegramBot === true){
|
||||
const TelegramBot = require('node-telegram-bot-api');
|
||||
try{
|
||||
const sendMessage = async function(sendBody,files,groupKey){
|
||||
const sendMessage = async function(sendBody,attachments,groupKey){
|
||||
var bot = s.group[groupKey].telegramBot
|
||||
if(!bot){
|
||||
s.userLog({ke:groupKey,mid:'$USER'},{type:lang.NotifyErrorText,msg:lang.DiscordNotEnabledText})
|
||||
return
|
||||
}
|
||||
const chatId = s.group[groupKey].init.telegrambot_channel
|
||||
if(bot && bot.sendMessage){
|
||||
try{
|
||||
await bot.sendMessage(chatId, `${sendBody.title}${sendBody.description ? '\n' + sendBody.description : ''}`)
|
||||
if(files){
|
||||
files.forEach(async (file) => {
|
||||
switch(file.type){
|
||||
case'video':
|
||||
await bot.sendVideo(chatId, file.attachment)
|
||||
break;
|
||||
case'photo':
|
||||
await bot.sendPhoto(chatId, file.attachment)
|
||||
break;
|
||||
}
|
||||
})
|
||||
|
||||
const sendMessageToChat = async function(chatId, files) {
|
||||
if(bot && bot.sendMessage){
|
||||
try{
|
||||
await bot.sendMessage(chatId, `${sendBody.title}${sendBody.description ? '\n' + sendBody.description : ''}`)
|
||||
if(files){
|
||||
await Promise.all(files.map(async (file) => {
|
||||
switch(file.type){
|
||||
case'video':
|
||||
if(file.hasOwnProperty("file_id") === false) {
|
||||
const videoFileInfo = (await ffprobe(file.attachment,file.attachment)).result
|
||||
const duration = Math.floor(videoFileInfo.streams[0].duration)
|
||||
const width = videoFileInfo.streams[0].width
|
||||
const height = videoFileInfo.streams[0].height
|
||||
|
||||
const options = {
|
||||
thumb: file.thumb,
|
||||
width: width,
|
||||
height: height,
|
||||
duration: duration,
|
||||
supports_streaming: true
|
||||
}
|
||||
file.file_id = (await bot.sendVideo(chatId, file.attachment, options)).video.file_id
|
||||
delete file.attachment
|
||||
} else {
|
||||
await bot.sendVideo(chatId, file.file_id)
|
||||
}
|
||||
break;
|
||||
case'photo':
|
||||
if(file.hasOwnProperty("file_id") === false) {
|
||||
file.file_id = (await bot.sendPhoto(chatId, file.attachment)).photo[0].file_id
|
||||
delete file.attachment
|
||||
} else {
|
||||
await bot.sendPhoto(chatId, file.file_id)
|
||||
}
|
||||
break;
|
||||
}
|
||||
return file
|
||||
}))
|
||||
}
|
||||
return files
|
||||
}catch(err){
|
||||
s.debugLog('Telegram Error',err)
|
||||
s.userLog({ke:groupKey,mid:'$USER'},{type:lang.NotifyErrorText,msg:err})
|
||||
}
|
||||
}catch(err){
|
||||
s.debugLog('Telegram Error',err)
|
||||
s.userLog({ke:groupKey,mid:'$USER'},{type:lang.NotifyErrorText,msg:err})
|
||||
}else{
|
||||
s.userLog({
|
||||
ke: groupKey,
|
||||
mid: '$USER'
|
||||
},{
|
||||
type: lang.NotifyErrorText,
|
||||
msg: lang["Check the Recipient ID"]
|
||||
})
|
||||
}
|
||||
}else{
|
||||
s.userLog({
|
||||
ke: groupKey,
|
||||
mid: '$USER'
|
||||
},{
|
||||
type: lang.NotifyErrorText,
|
||||
msg: lang["Check the Recipient ID"]
|
||||
})
|
||||
}
|
||||
|
||||
const chatIds = s.group[groupKey].init.telegrambot_channel.split(",")
|
||||
const resolvedFiles = await sendMessageToChat(chatIds[0], attachments)
|
||||
|
||||
chatIds.forEach((chatId, index) => {
|
||||
if(index < 1) return
|
||||
sendMessageToChat(chatId, resolvedFiles)
|
||||
});
|
||||
}
|
||||
const onEventTriggerBeforeFilterForTelegram = function(d,filter){
|
||||
filter.telegram = false
|
||||
|
@ -58,6 +100,7 @@ module.exports = function(s,config,lang,getSnapshot){
|
|||
// d = event object
|
||||
//telegram bot
|
||||
if(s.group[d.ke].telegramBot && (filter.telegram || monitorConfig.details.notify_telegram === '1') && !s.group[d.ke].activeMonitors[d.id].detector_telegrambot){
|
||||
const notifyText = getObjectTagNotifyText(d)
|
||||
var detector_telegrambot_timeout
|
||||
if(!monitorConfig.details.detector_telegrambot_timeout||monitorConfig.details.detector_telegrambot_timeout===''){
|
||||
detector_telegrambot_timeout = 1000 * 60 * 10;
|
||||
|
@ -71,13 +114,13 @@ module.exports = function(s,config,lang,getSnapshot){
|
|||
await getSnapshot(d,monitorConfig)
|
||||
if(d.screenshotBuffer){
|
||||
sendMessage({
|
||||
title: lang.Event+' - '+d.screenshotName,
|
||||
title: notifyText,
|
||||
description: lang.EventText1+' '+d.currentTimestamp,
|
||||
},[
|
||||
{
|
||||
type: 'photo',
|
||||
attachment: d.screenshotBuffer,
|
||||
name: d.screenshotName+'.jpg'
|
||||
name: notifyText + '.jpg'
|
||||
}
|
||||
],d.ke)
|
||||
}
|
||||
|
@ -98,13 +141,16 @@ module.exports = function(s,config,lang,getSnapshot){
|
|||
videoName = siftedVideoFileFromRam.filename
|
||||
}
|
||||
if(videoPath){
|
||||
const thumbFile = getStreamDirectory(d) + 'thumb.jpg';
|
||||
fs.writeFileSync(thumbFile, d.screenshotBuffer)
|
||||
sendMessage({
|
||||
title: videoName,
|
||||
title: notifyText,
|
||||
},[
|
||||
{
|
||||
type: 'video',
|
||||
attachment: videoPath,
|
||||
name: videoName
|
||||
name: notifyText + '.mp4',
|
||||
thumb: thumbFile
|
||||
}
|
||||
],d.ke)
|
||||
}
|
||||
|
|
|
@ -574,7 +574,7 @@ module.exports = async (s,config,lang,app,io,currentUse) => {
|
|||
s.closeJsonResponse(res,{ok: true, readme: readme})
|
||||
},res,req)
|
||||
})
|
||||
s.onProcessReady(async () => {
|
||||
s.beforeMonitorsLoadedOnStartup(async () => {
|
||||
// Initialize Modules on Start
|
||||
await initializeAllModules();
|
||||
})
|
||||
|
|
|
@ -48,10 +48,10 @@ module.exports = function(s,config,lang,io){
|
|||
}
|
||||
var loadedAccounts = []
|
||||
var foundMonitors = []
|
||||
var loadMonitors = function(callback){
|
||||
s.beforeMonitorsLoadedOnStartupExtensions.forEach(function(extender){
|
||||
extender()
|
||||
})
|
||||
var loadMonitors = async function(callback){
|
||||
for (let i = 0; i < s.beforeMonitorsLoadedOnStartupExtensions.length; i++) {
|
||||
await s.beforeMonitorsLoadedOnStartupExtensions[i]()
|
||||
}
|
||||
s.systemLog(lang.startUpText4)
|
||||
//preliminary monitor start
|
||||
s.knexQuery({
|
||||
|
|
|
@ -158,13 +158,11 @@ module.exports = (s, shinobiConfig, lang, app, io) => {
|
|||
videoItems.forEach(v => {
|
||||
const imagesOfVideo = imageItems.filter(i => i.time >= v.time && i.time <= v.end);
|
||||
|
||||
if (imagesOfVideo.length > 0) {
|
||||
const chosenImage = imagesOfVideo[0];
|
||||
|
||||
v.time = getISODateTime(v.time);
|
||||
v.filename = chosenImage.filename;
|
||||
v.time = getISODateTime(v.time);
|
||||
v.end = getISODateTime(v.end);
|
||||
|
||||
delete v.end;
|
||||
if (imagesOfVideo.length > 0) {
|
||||
v.filename = imagesOfVideo[0].filename;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -124,7 +124,7 @@ module.exports = function(s,config,lang){
|
|||
k.details = k.details && k.details instanceof Object ? k.details : {}
|
||||
var listOEvents = activeMonitor.detector_motion_count || []
|
||||
var listOTags = listOEvents.filter(row => row.details.reason === 'object').map(row => row.details.matrices.map(matrix => matrix.tag).join(',')).join(',').split(',')
|
||||
if(listOTags && !k.objects)k.objects = [...new Set(listOTags)].join(',');
|
||||
if(listOTags && !k.objects)k.objects = [...new Set(listOTags)].filter(item => !!item).join(',');
|
||||
k.filename = k.filename || k.file
|
||||
k.ext = k.ext || e.ext || k.filename.split('.')[1]
|
||||
k.stat = fs.statSync(k.dir+k.file)
|
||||
|
|
|
@ -72,7 +72,6 @@ module.exports = function(s,config,lang,io){
|
|||
'home/timelapseViewer',
|
||||
'home/eventFilters',
|
||||
'home/cameraProbe',
|
||||
'home/powerVideo',
|
||||
'home/onvifScanner',
|
||||
'home/onvifDeviceManager',
|
||||
'home/configFinder',
|
||||
|
@ -82,6 +81,8 @@ module.exports = function(s,config,lang,io){
|
|||
'home/fileBin',
|
||||
'home/videosTable',
|
||||
'home/studio',
|
||||
'home/monitorMap',
|
||||
'home/timeline',
|
||||
'confirm',
|
||||
'home/help',
|
||||
]
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
"node-fetch": "^2.6.7",
|
||||
"node-onvif-events": "^2.0.5",
|
||||
"node-ssh": "^12.0.4",
|
||||
"node-telegram-bot-api": "^0.58.0",
|
||||
"node-telegram-bot-api": "^0.61.0",
|
||||
"nodemailer": "^6.7.1",
|
||||
"pam-diff": "^1.1.0",
|
||||
"path": "^0.12.7",
|
||||
|
@ -5384,15 +5384,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/node-telegram-bot-api": {
|
||||
"version": "0.58.0",
|
||||
"resolved": "https://registry.npmjs.org/node-telegram-bot-api/-/node-telegram-bot-api-0.58.0.tgz",
|
||||
"integrity": "sha512-DmP5wBON9stOiunvUw/NvTb1clMYvj+c3NnSqbPZdVd6hNkNRnM97eqPZIH4UsBJ+4n+XFGpU33dCzjqD1sv3A==",
|
||||
"version": "0.61.0",
|
||||
"resolved": "https://registry.npmjs.org/node-telegram-bot-api/-/node-telegram-bot-api-0.61.0.tgz",
|
||||
"integrity": "sha512-BZXd8Bh2C5+uBEQuuI3FD7TFJF3alV+6oFQt8CNLx3ldX/hsd+NYyllTX+Y+5X0tG+xtcRQQjbfLgz/4sRvmBQ==",
|
||||
"dependencies": {
|
||||
"array.prototype.findindex": "^2.0.2",
|
||||
"bl": "^1.2.3",
|
||||
"bluebird": "^3.5.1",
|
||||
"debug": "^3.1.0",
|
||||
"depd": "^1.1.1",
|
||||
"debug": "^3.2.7",
|
||||
"eventemitter3": "^3.0.0",
|
||||
"file-type": "^3.9.0",
|
||||
"mime": "^1.6.0",
|
||||
|
@ -12050,15 +12048,13 @@
|
|||
}
|
||||
},
|
||||
"node-telegram-bot-api": {
|
||||
"version": "0.58.0",
|
||||
"resolved": "https://registry.npmjs.org/node-telegram-bot-api/-/node-telegram-bot-api-0.58.0.tgz",
|
||||
"integrity": "sha512-DmP5wBON9stOiunvUw/NvTb1clMYvj+c3NnSqbPZdVd6hNkNRnM97eqPZIH4UsBJ+4n+XFGpU33dCzjqD1sv3A==",
|
||||
"version": "0.61.0",
|
||||
"resolved": "https://registry.npmjs.org/node-telegram-bot-api/-/node-telegram-bot-api-0.61.0.tgz",
|
||||
"integrity": "sha512-BZXd8Bh2C5+uBEQuuI3FD7TFJF3alV+6oFQt8CNLx3ldX/hsd+NYyllTX+Y+5X0tG+xtcRQQjbfLgz/4sRvmBQ==",
|
||||
"requires": {
|
||||
"array.prototype.findindex": "^2.0.2",
|
||||
"bl": "^1.2.3",
|
||||
"bluebird": "^3.5.1",
|
||||
"debug": "^3.1.0",
|
||||
"depd": "^1.1.1",
|
||||
"debug": "^3.2.7",
|
||||
"eventemitter3": "^3.0.0",
|
||||
"file-type": "^3.9.0",
|
||||
"mime": "^1.6.0",
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
"node-fetch": "^2.6.7",
|
||||
"node-onvif-events": "^2.0.5",
|
||||
"node-ssh": "^12.0.4",
|
||||
"node-telegram-bot-api": "^0.58.0",
|
||||
"node-telegram-bot-api": "^0.61.0",
|
||||
"nodemailer": "^6.7.1",
|
||||
"pam-diff": "^1.1.0",
|
||||
"path": "^0.12.7",
|
||||
|
|
|
@ -0,0 +1,164 @@
|
|||
const fetch = require('node-fetch');
|
||||
const dgram = require('dgram');
|
||||
const parseString = require('xml2js').parseString;
|
||||
const base64 = require('base-64');
|
||||
|
||||
const USERNAME = process.argv[2]
|
||||
const PASSWORD = process.argv[3]
|
||||
|
||||
if(!USERNAME || !PASSWORD){
|
||||
console.log(`Missing Username and/or Password!`)
|
||||
console.log(`Example : node ./onvifGetStreamUri.js "USERNAME" "PASSWORD"`)
|
||||
console.log(`Put the quotations seen in the example!`)
|
||||
return
|
||||
}
|
||||
|
||||
function cleanKeys(obj) {
|
||||
const cleanKey = (key) => key.includes(':') ? key.split(':').pop() : key;
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(cleanKeys);
|
||||
} else if (obj !== null && typeof obj === 'object') {
|
||||
return Object.keys(obj).reduce((newObj, key) => {
|
||||
newObj[cleanKey(key)] = cleanKeys(obj[key]);
|
||||
return newObj;
|
||||
}, {});
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
async function getStreamUri(deviceXAddr, username, password) {
|
||||
let envelope = `
|
||||
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
|
||||
<s:Body>
|
||||
<GetProfiles xmlns="http://www.onvif.org/ver10/media/wsdl" />
|
||||
</s:Body>
|
||||
</s:Envelope>
|
||||
`;
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/soap+xml',
|
||||
'Authorization': 'Basic ' + base64.encode(username + ':' + password),
|
||||
},
|
||||
body: envelope,
|
||||
};
|
||||
|
||||
let response = await fetch(deviceXAddr, options);
|
||||
let body = await response.text();
|
||||
|
||||
parseString(body, async (err, result) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
const soapBody = cleanKeys(result).Envelope.Body[0]
|
||||
if (soapBody.Fault) {
|
||||
console.log(deviceXAddr,'Not Authorized');
|
||||
return;
|
||||
}
|
||||
try{
|
||||
var profiles = soapBody.GetProfilesResponse[0].Profiles;
|
||||
}catch(err){
|
||||
console.log(err.stack)
|
||||
console.error(deviceXAddr,`getStreamUri soapBody on ERROR`,JSON.stringify(soapBody,null,3))
|
||||
return
|
||||
}
|
||||
if (!profiles || !profiles.length) {
|
||||
console.log(deviceXAddr,'No profiles found');
|
||||
return;
|
||||
}
|
||||
|
||||
const firstProfileToken = profiles[0]['$']['token'];
|
||||
|
||||
envelope = `
|
||||
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
|
||||
<s:Body>
|
||||
<GetStreamUri xmlns="http://www.onvif.org/ver10/media/wsdl">
|
||||
<StreamSetup>
|
||||
<Stream xmlns="http://www.onvif.org/ver10/schema">RTP-Unicast</Stream>
|
||||
<Transport xmlns="http://www.onvif.org/ver10/schema">
|
||||
<Protocol>RTSP</Protocol>
|
||||
</Transport>
|
||||
</StreamSetup>
|
||||
<ProfileToken>${firstProfileToken}</ProfileToken>
|
||||
</GetStreamUri>
|
||||
</s:Body>
|
||||
</s:Envelope>
|
||||
`;
|
||||
|
||||
options.body = envelope;
|
||||
response = await fetch(deviceXAddr, options);
|
||||
body = await response.text();
|
||||
|
||||
parseString(body, (err, result) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
const uri = result['SOAP-ENV:Envelope']['SOAP-ENV:Body'][0]['trt:GetStreamUriResponse'][0]['trt:MediaUri'][0]['tt:Uri'][0];
|
||||
console.log('Stream URI:', uri);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const DISCOVER_MSG = Buffer.from(`
|
||||
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
|
||||
xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing"
|
||||
xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery">
|
||||
<s:Header>
|
||||
<a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</a:Action>
|
||||
<a:MessageID>uuid:1000-3000-5000-70000000000000</a:MessageID>
|
||||
<a:ReplyTo>
|
||||
<a:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address>
|
||||
</a:ReplyTo>
|
||||
<a:To s:mustUnderstand="1">urn:schemas-xmlsoap-org:ws:2005:04:discovery</a:To>
|
||||
</s:Header>
|
||||
<s:Body>
|
||||
<d:Probe>
|
||||
<d:Types>dn:NetworkVideoTransmitter</d:Types>
|
||||
</d:Probe>
|
||||
</s:Body>
|
||||
</s:Envelope>
|
||||
`);
|
||||
|
||||
const socket = dgram.createSocket('udp4');
|
||||
socket.bind(() => {
|
||||
socket.setBroadcast(true);
|
||||
socket.setMulticastTTL(128);
|
||||
socket.addMembership('239.255.255.250');
|
||||
socket.send(DISCOVER_MSG, 0, DISCOVER_MSG.length, 3702, '239.255.255.250');
|
||||
});
|
||||
|
||||
socket.on('message', function (message, rinfo) {
|
||||
parseString(message, (err, result) => {
|
||||
if (err) {
|
||||
console.error('Failed to parse XML', err);
|
||||
return;
|
||||
}
|
||||
const cleanJson = cleanKeys(result)
|
||||
if (!cleanJson.Envelope || !cleanJson.Envelope.Body) {
|
||||
console.error('Unexpected message format', result);
|
||||
console.error('cleanJson', cleanJson);
|
||||
return;
|
||||
}
|
||||
|
||||
const soapBody = cleanJson.Envelope.Body[0];
|
||||
const probeMatches = soapBody.ProbeMatches;
|
||||
if (!probeMatches) {
|
||||
console.error('No probe matches in message', soapBody);
|
||||
return;
|
||||
}
|
||||
|
||||
const probeMatch = probeMatches[0].ProbeMatch[0];
|
||||
if (!probeMatch) {
|
||||
console.error('No probe match in probe matches', probeMatches);
|
||||
return;
|
||||
}
|
||||
let xAddrs = probeMatch.XAddrs[0];
|
||||
console.log('Found ONVIF device', xAddrs);
|
||||
getStreamUri(xAddrs, USERNAME, PASSWORD)
|
||||
.catch(err => console.error('Failed to get stream URI', err));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
#monitor-map-canvas {
|
||||
height: calc(100vh - 2rem);
|
||||
border-radius: 20px;
|
||||
}
|
||||
#monitor-map-canvas .leaflet-map-pane {
|
||||
transition: initial;
|
||||
}
|
||||
#monitor-map-canvas .leaflet-popup-content {
|
||||
margin: 0px;
|
||||
width: 400px!important;
|
||||
}
|
||||
#monitor-map-canvas .leaflet-popup-content-wrapper {
|
||||
padding: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
#monitor-map-canvas iframe {
|
||||
width: 100%;
|
||||
height: 300px
|
||||
}
|
||||
#leaflet-monitor-block {
|
||||
width:100%;
|
||||
}
|
||||
#leaflet-monitor-videos {
|
||||
overflow:auto;
|
||||
height: 250px;
|
||||
padding: 0.4rem 0.5rem;
|
||||
}
|
||||
.leaflet-popup-content-wrapper, .leaflet-popup-tip {
|
||||
background-color: #121417;
|
||||
}
|
||||
.leaflet-container a.btn {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.leaflet-marker-icon {
|
||||
transition: 0s;
|
||||
}
|
|
@ -1,191 +0,0 @@
|
|||
#powerVideo .videoPlayer {
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
#powerVideo .videoPlayer video{
|
||||
max-width: 100%;
|
||||
height: 300px;
|
||||
object-fit: fill;
|
||||
}
|
||||
|
||||
#powerVideo .videoPlayer:fullscreen video{
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
#powerVideoMonitorControls{
|
||||
border-radius: 0 0 5px 5px;
|
||||
padding: 5px;
|
||||
background: #222;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#powerVideoMonitorsList{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#powerVideoMonitorsList .list-item{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#powerVideoMonitorViews {
|
||||
text-align: center;
|
||||
min-height: 300px;
|
||||
background: #444;
|
||||
border-radius: 5px 5px 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#powerVideo .videoPlayer .videoPlayer-detection-info {
|
||||
position: absolute;
|
||||
padding: 20px 10px 20px 10px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin: auto;
|
||||
z-index: 11;
|
||||
opacity: 0;
|
||||
background: rgba(0,0,0,0.7);
|
||||
color: #fff;
|
||||
font-family: monospace;
|
||||
overflow: auto;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#powerVideo .videoPlayer:hover .videoPlayer-detection-info,
|
||||
#powerVideo .videoPlayer.show-detection-info .videoPlayer-detection-info {
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
#powerVideo .videoPlayer .videoPlayer-stream-objects {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#powerVideo .videoPlayer .videoPlayer-detection-info-object div {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
#powerVideo .videoPlayer .videoPlayer-detection-info-object {
|
||||
text-align: left
|
||||
}
|
||||
|
||||
.videoPlayer-stream-objects .tag {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
background: red;
|
||||
color: #fff;
|
||||
font-family: monospace;
|
||||
font-size: 80%;
|
||||
border-radius: 5px 5px 0 0;
|
||||
padding: 3px 5px;
|
||||
}
|
||||
|
||||
.videoPlayer-stream-objects .stream-detected-object {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border: 3px solid red;
|
||||
background: transparent;
|
||||
border-radius: 5px
|
||||
}
|
||||
|
||||
.videoPlayer-stream-objects .stream-detected-point {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border: 3px solid yellow;
|
||||
background: transparent;
|
||||
border-radius: 5px
|
||||
}
|
||||
|
||||
.videoPlayer-stream-objects .point {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border: 3px solid red;
|
||||
border-radius: 50%
|
||||
}
|
||||
|
||||
/* loading */
|
||||
#powerVideo .loading {
|
||||
font-size: 20pt;
|
||||
text-align: center;
|
||||
}
|
||||
#powerVideo .loading > div {
|
||||
margin-top: 5px
|
||||
}
|
||||
|
||||
/* VIS.js */
|
||||
|
||||
#powerVideo video {
|
||||
width: 100%;
|
||||
padding: 6px 0 0 0
|
||||
}
|
||||
|
||||
#powerVideo .videoAfter,
|
||||
#powerVideo .videoBefore {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#powerVideo .vis-timeline {
|
||||
font-family: monospace;
|
||||
border-radius: 5px;
|
||||
border-color: #172b4d;
|
||||
}
|
||||
#powerVideo .vis-item {
|
||||
border-color: #347af1;
|
||||
background-color: #4d87d0;
|
||||
color: #fff;
|
||||
}
|
||||
#powerVideo .vis-item.vis-selected {
|
||||
border-color: #f5365c;
|
||||
background-color: #f5365c;
|
||||
color: #fff;
|
||||
}
|
||||
#powerVideo .vis-panel.vis-bottom {
|
||||
border: 1px #17294b;
|
||||
}
|
||||
#powerVideo .vis-time-axis .vis-grid.vis-minor{
|
||||
border-color: #1a539a;
|
||||
}
|
||||
#powerVideo .vis-time-axis .vis-grid.vis-major {
|
||||
border-color: #4d87d0;
|
||||
}
|
||||
#powerVideo .vis-time-axis .vis-text {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
[timeline-video-file] .progress{
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
opacity: 0;
|
||||
}
|
||||
.vis-selected [timeline-video-file] .progress{
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#powerVideo .vis-item.vis-box {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#powerVideo .vis-labelset .vis-label {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#powerVideo .videoPlayer-detection-info-buttons {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
}
|
|
@ -57,12 +57,13 @@ ul:not(.compressed-monitor-icons) .monitor-icon img{
|
|||
right: 5px;
|
||||
}
|
||||
|
||||
#createdTabLinks .nav-item:not(:last-child){
|
||||
#createdTabLinks .nav-link:not(:last-child){
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
#createdTabLinks .side-menu-link:not(.active){
|
||||
border: 1px solid rgb(31 128 249 / 20%);
|
||||
background: #2d3f55;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
#tab-timeline .loading-mask {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.7);
|
||||
color: #1f80f9;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
#tab-timeline .loading-mask i {
|
||||
|
||||
}
|
||||
#tab-timeline .event-objects {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left:0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#timeline-video-canvas {
|
||||
background: rgba(0,0,0,0.9);
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
}
|
||||
#timeline-video-canvas video,
|
||||
#timeline-video-canvas .film {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
#timeline-video-canvas .timeline-video.col-md-4 {
|
||||
height:30vh;
|
||||
}
|
||||
#timeline-video-canvas .timeline-video.col-md-6 {
|
||||
height: 40vh;
|
||||
}
|
||||
#timeline-video-canvas .timeline-video.col-md-12 {
|
||||
height: 80vh;
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
#timeline-video-canvas .timeline-video:not(.no-video){
|
||||
background-color: #000!important;
|
||||
}
|
||||
#timeline-video-canvas .timeline-video.no-video{
|
||||
display: none;
|
||||
}
|
||||
#timeline-video-canvas.show-non-playing .timeline-video.no-video{
|
||||
display: flex;
|
||||
}
|
||||
#timeline-video-canvas .timeline-video {
|
||||
position: relative;
|
||||
}
|
||||
#timeline-info {
|
||||
background: #004e8e;
|
||||
}
|
||||
#timeline-bottom-strip {
|
||||
background: rgba(0,0,0,0.6);
|
||||
height: 93px;
|
||||
overflow: hidden;
|
||||
}
|
||||
#timeline-bottom-strip * {
|
||||
transition: none;
|
||||
}
|
||||
#timeline-bottom-strip .vis-timeline {
|
||||
visibility: visible!important;
|
||||
border: 1px solid #2a4cb5;
|
||||
border-radius: 5px;
|
||||
}
|
||||
#timeline-bottom-strip .vis-item {
|
||||
opacity:0.4;
|
||||
}
|
||||
#timeline-bottom-strip .vis-time-axis .vis-grid.vis-minor {
|
||||
border-color: rgb(31 128 249 / 40%);
|
||||
}
|
||||
|
||||
#timeline-bottom-strip .vis-panel.vis-bottom,
|
||||
#timeline-bottom-strip .vis-panel.vis-center,
|
||||
#timeline-bottom-strip .vis-panel.vis-left,
|
||||
#timeline-bottom-strip .vis-panel.vis-right,
|
||||
#timeline-bottom-strip .vis-panel.vis-top {
|
||||
border-color: rgb(31 128 249 / 40%)!important;
|
||||
}
|
||||
|
||||
/* event object, detected */
|
||||
#tab-timeline .event-objects .tag {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
background: red;
|
||||
color: #fff;
|
||||
font-family: monospace;
|
||||
font-size: 80%;
|
||||
border-radius: 5px 5px 0 0;
|
||||
padding: 3px 5px;
|
||||
}
|
||||
|
||||
#tab-timeline .event-objects .stream-detected-object {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border: 3px solid red;
|
||||
background: transparent;
|
||||
border-radius: 5px
|
||||
}
|
||||
|
||||
#tab-timeline .event-objects .stream-detected-point {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border: 3px solid yellow;
|
||||
background: transparent;
|
||||
border-radius: 5px
|
||||
}
|
||||
|
||||
#tab-timeline .event-objects .point {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border: 3px solid red;
|
||||
border-radius: 50%
|
||||
}
|
|
@ -296,22 +296,6 @@ body {
|
|||
background-size: cover;
|
||||
}
|
||||
|
||||
/* */
|
||||
.side-menu-link {
|
||||
position: relative;
|
||||
}
|
||||
.side-menu-link .delete-tab {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
}
|
||||
.side-menu-link .delete-tab:hover {
|
||||
text-shadow: 0 0 15px #333;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
/* */
|
||||
|
||||
ul.squeeze {
|
||||
flex-direction: row!important;
|
||||
align-items: center;
|
||||
|
@ -440,3 +424,10 @@ ul.squeeze {
|
|||
.fixed-table-body {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
|
||||
.flex-direction-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
}
|
||||
|
|
|
@ -18,9 +18,11 @@ var chartColors = {
|
|||
purple: 'rgb(153, 102, 255)',
|
||||
grey: 'rgb(201, 203, 207)'
|
||||
};
|
||||
var isAppleDevice = navigator.userAgent.match(/(iPod|iPhone|iPad)/)||(navigator.userAgent.match(/(Safari)/)&&!navigator.userAgent.match('Chrome'));
|
||||
var userAgent = navigator.userAgent;
|
||||
var isAppleDevice = userAgent.match(/(iPod|iPhone|iPad)/)||(navigator.userAgent.match(/(Safari)/)&&!navigator.userAgent.match('Chrome'));
|
||||
var isMobile = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent)
|
||||
|| /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0,4))
|
||||
var isChromiumBased = userAgent.includes('Chrome') && !userAgent.includes('Edg') && !userAgent.includes('OPR') || userAgent.includes('Brave');
|
||||
var keyShortcuts = {}
|
||||
function base64ArrayBuffer(arrayBuffer) {
|
||||
var base64 = ''
|
||||
|
@ -73,6 +75,55 @@ function base64ArrayBuffer(arrayBuffer) {
|
|||
|
||||
return base64
|
||||
}
|
||||
function timeAgo(date) {
|
||||
const now = new Date();
|
||||
const secondsPast = (now.getTime() - (new Date(date)).getTime()) / 1000;
|
||||
if(secondsPast < 60) {
|
||||
return parseInt(secondsPast) + ' seconds ago';
|
||||
}
|
||||
if(secondsPast < 3600) {
|
||||
return parseInt(secondsPast / 60) + ' minutes ago';
|
||||
}
|
||||
if(secondsPast < 86400) {
|
||||
return parseInt(secondsPast / 3600) + ' hours ago';
|
||||
}
|
||||
return parseInt(secondsPast / 86400) + ' days ago';
|
||||
}
|
||||
function getDayOfWeek(date) {
|
||||
const days = [lang.Sunday, lang.Monday, lang.Tuesday, lang.Wednesday, lang.Thursday, lang.Friday, lang.Saturday];
|
||||
return days[date.getDay()];
|
||||
}
|
||||
function stringToColor(str) {
|
||||
let blueColors = [
|
||||
'#00BFFF', // Deep Sky Blue
|
||||
'#1E90FF', // Dodger Blue
|
||||
'#00F5FF', // Turquoise Blue
|
||||
'#00D7FF', // Electric Blue
|
||||
'#00C2FF', // Bright Cerulean
|
||||
'#00A9FF', // Vivid Sky Blue
|
||||
'#0099FF', // Blue Ribbon
|
||||
'#007FFF', // Azure Radiance
|
||||
'#0066FF', // Blue Dianne
|
||||
'#0055FF', // Electric Ultramarine
|
||||
'#0044FF', // Rich Electric Blue
|
||||
'#0033FF', // Medium Electric Blue
|
||||
'#0022FF', // Bright Blue
|
||||
'#0011FF', // Vivid Blue
|
||||
'#0000FF' // Pure Blue
|
||||
];
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
return blueColors[Math.abs(hash) % blueColors.length];
|
||||
}
|
||||
function getTimeBetween(start, end, percent) {
|
||||
const startDate = new Date(start);
|
||||
const endDate = new Date(end);
|
||||
const difference = endDate - startDate;
|
||||
const time = new Date(startDate.getTime() + difference * percent / 100);
|
||||
return time;
|
||||
}
|
||||
function stringContains(find,string,toLowerCase){
|
||||
var newString = string + ''
|
||||
if(toLowerCase)newString = newString.toLowerCase()
|
||||
|
@ -1021,6 +1072,20 @@ function setSubmitButton(editorForm,text,icon,toggle){
|
|||
var submitButtons = editorForm.find('[type="submit"]').prop('disabled',toggle)
|
||||
submitButtons.html(`<i class="fa fa-${icon}"></i> ${text}`)
|
||||
}
|
||||
function featureIsActivated(showNotice){
|
||||
if(userHasSubscribed){
|
||||
return true
|
||||
}else{
|
||||
if(showNotice){
|
||||
new PNotify({
|
||||
title: lang.activationRequired,
|
||||
text: lang.featureRequiresActivationText,
|
||||
type: 'warning'
|
||||
})
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
$(document).ready(function(){
|
||||
onInitWebsocket(function(){
|
||||
loadMonitorsIntoMemory(function(data){
|
||||
|
|
|
@ -987,6 +987,14 @@ function addMarkAsEventToAllOpenMonitors(){
|
|||
}
|
||||
})
|
||||
}
|
||||
function showHideSubstreamActiveIcon(monitorId, show){
|
||||
try{
|
||||
var liveBlock = liveGridElements[monitorId].monitorItem
|
||||
liveBlock.find('.substream-is-on')[show ? 'show' : 'hide']()
|
||||
}catch(err){
|
||||
|
||||
}
|
||||
}
|
||||
$(document).ready(function(e){
|
||||
liveGrid
|
||||
.on('dblclick','.stream-block',function(){
|
||||
|
@ -1193,6 +1201,7 @@ $(document).ready(function(e){
|
|||
break;
|
||||
case'substream_start':
|
||||
loadedMonitors[d.mid].subStreamChannel = d.channel
|
||||
showHideSubstreamActiveIcon(d.mid,true)
|
||||
setTimeout(() => {
|
||||
resetMonitorCanvas(d.mid,true,d.channel)
|
||||
},3000)
|
||||
|
@ -1200,6 +1209,7 @@ $(document).ready(function(e){
|
|||
case'substream_end':
|
||||
loadedMonitors[d.mid].subStreamChannel = null
|
||||
resetMonitorCanvas(d.mid,true,null)
|
||||
showHideSubstreamActiveIcon(d.mid,false)
|
||||
break;
|
||||
case'monitor_watch_on':
|
||||
var monitorId = d.mid || d.id
|
||||
|
@ -1216,6 +1226,7 @@ $(document).ready(function(e){
|
|||
drawLiveGridBlock(loadedMonitors[monitorId],subStreamChannel,monitorsPerRow,monitorHeight)
|
||||
saveLiveGridBlockOpenState(monitorId,$user.ke,1)
|
||||
}
|
||||
showHideSubstreamActiveIcon(monitorId,!!subStreamChannel)
|
||||
break;
|
||||
case'mode_jpeg_off':
|
||||
window.jpegModeOn = false
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
$(document).ready(function(){
|
||||
var theBlock = $('#tab-monitorMap')
|
||||
var theMap = $('#monitor-map-canvas')
|
||||
var loadedMap;
|
||||
function loadPopupVideoList(monitor){
|
||||
var groupKey = monitor.ke
|
||||
var monitorId = monitor.mid
|
||||
getVideos({
|
||||
monitorId: monitorId,
|
||||
limit: 10,
|
||||
},function(data){
|
||||
var videos = data.videos
|
||||
var html = ''
|
||||
setTimeout(function(){
|
||||
var theVideoList = $(`#leaflet-monitor-videos`)
|
||||
if(videos.length > 0){
|
||||
theVideoList.css('height','')
|
||||
console.log(2,videos,videos.length)
|
||||
$.each(videos,function(n,video){
|
||||
html += createVideoRow(video,`col-12 mb-2`)
|
||||
})
|
||||
}else{
|
||||
theVideoList.css('height','initial')
|
||||
html = `<div class="text-center text-light mb-2">${lang['No Videos Found']}</div>`
|
||||
}
|
||||
theVideoList.html(html)
|
||||
},1000)
|
||||
})
|
||||
}
|
||||
function buildPinPopupHtml(monitor){
|
||||
var embedUrl = buildEmbedUrl(monitor)
|
||||
var html = `
|
||||
<div id="leaflet-monitor-block" data-mid="${monitor.mid}" data-ke="${monitor.ke}">
|
||||
<div>${userHasSubscribed ? `<iframe src="${embedUrl}"></iframe>` : `<div class="text-center p-3 text-light cursor-pointer" onclick="openTab('helpWindow')">${lang.activateRequiredLiveStream}</div>`}</div>
|
||||
<div id="leaflet-monitor-videos">
|
||||
<div class="text-center text-light" style="padding-top: 75px"><i class="fa fa-3x fa-spinner fa-pulse"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
return html
|
||||
}
|
||||
function getPinsFromMonitors(){
|
||||
var points = []
|
||||
var n = 0;
|
||||
$.each(loadedMonitors,function(monitorId,monitor){
|
||||
var geolocation = monitor.details.geolocation
|
||||
var modeIsOn = monitor.mode === 'record' || monitor.mode === 'start'
|
||||
var point = {
|
||||
coords: [49.2578298 + n,-123.2634732 + n],
|
||||
direction: 90,
|
||||
fov: 60,
|
||||
range: 1,
|
||||
title: `${monitor.name} (${monitor.host})`,
|
||||
html: buildPinPopupHtml(monitor)
|
||||
};
|
||||
if(!modeIsOn){
|
||||
|
||||
}else if(geolocation){
|
||||
var {
|
||||
lat,
|
||||
lng,
|
||||
zoom,
|
||||
direction,
|
||||
fov,
|
||||
range,
|
||||
} = getGeolocationParts(monitor.details.geolocation);
|
||||
point.direction = direction
|
||||
point.fov = fov
|
||||
point.range = range
|
||||
point.coords = [lat, lng]
|
||||
points.push(point)
|
||||
}else{
|
||||
n += 0.0001105;
|
||||
points.push(point)
|
||||
}
|
||||
})
|
||||
return points
|
||||
}
|
||||
function plotPinsToMap(pins){
|
||||
for (var i = 0; i < pins.length; i++) {
|
||||
var pin = pins[i];
|
||||
var lat = pin.coords[0];
|
||||
var lng = pin.coords[1];
|
||||
var html = pin.html;
|
||||
L.marker([lat, lng], { title: pin.title }).bindPopup(html).addTo(loadedMap);
|
||||
console.log(pin)
|
||||
drawMapMarkerFov(loadedMap,{
|
||||
lat,
|
||||
lng,
|
||||
direction: pin.direction,
|
||||
fov: pin.fov,
|
||||
range: pin.range,
|
||||
});
|
||||
}
|
||||
}
|
||||
function loadMap(){
|
||||
console.log('Load Map')
|
||||
var monitorMapInfo = dashboardOptions().monitorMap || {
|
||||
center: { lat:49.2578298, lng:-123.2634732 },
|
||||
zoom: 13
|
||||
};
|
||||
var center = monitorMapInfo.center
|
||||
var lat = center.lat
|
||||
var lng = center.lng
|
||||
var zoom = monitorMapInfo.zoom
|
||||
var monitorPins = getPinsFromMonitors()
|
||||
loadedMap = L.map('monitor-map-canvas').setView([lat, lng], zoom);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
}).addTo(loadedMap);
|
||||
loadedMap.on('moveend', function() {
|
||||
saveCurrentPosition()
|
||||
});
|
||||
loadedMap.on('popupopen', function(e) {
|
||||
var popup = $('#leaflet-monitor-block')
|
||||
var groupKey = popup.attr('data-ke')
|
||||
var monitorId = popup.attr('data-mid')
|
||||
var monitor = loadedMonitors[monitorId]
|
||||
console.log(`loading `,monitorId,!!monitor)
|
||||
loadPopupVideoList(monitor)
|
||||
});
|
||||
plotPinsToMap(monitorPins)
|
||||
}
|
||||
function unloadMap(){
|
||||
loadedMap.remove();
|
||||
loadedMap = null;
|
||||
}
|
||||
function saveCurrentPosition(){
|
||||
var center = loadedMap.getCenter();
|
||||
var zoom = loadedMap.getZoom();
|
||||
dashboardOptions('monitorMap',{
|
||||
center,
|
||||
zoom,
|
||||
})
|
||||
}
|
||||
addOnTabOpen('monitorMap', function () {
|
||||
loadMap()
|
||||
})
|
||||
addOnTabReopen('monitorMap', function () {
|
||||
loadMap()
|
||||
})
|
||||
addOnTabAway('monitorMap', function () {
|
||||
unloadMap()
|
||||
})
|
||||
onInitWebsocket(function (d){
|
||||
})
|
||||
onWebSocketEvent(function (d){
|
||||
switch(d.f){
|
||||
case'monitor_edit':
|
||||
break;
|
||||
case'monitor_status':
|
||||
break;
|
||||
}
|
||||
})
|
||||
})
|
|
@ -0,0 +1,89 @@
|
|||
function calculateFOV(camera, direction, fieldOfView, range) {
|
||||
var startAngle = (direction - fieldOfView / 2) * Math.PI / 180;
|
||||
var endAngle = (direction + fieldOfView / 2) * Math.PI / 180;
|
||||
var sectorPoints = [camera];
|
||||
for (var angle = startAngle; angle <= endAngle; angle += Math.PI / 180) {
|
||||
var dx = range * Math.cos(angle);
|
||||
var dy = range * Math.sin(angle);
|
||||
var lat = camera[0] + dy / 111.325; // Rough conversion from kilometers to degrees
|
||||
var lng = camera[1] + dx / (111.325 * Math.cos(camera[0] * Math.PI / 180)); // Rough conversion from kilometers to degrees
|
||||
sectorPoints.push([lat, lng]);
|
||||
}
|
||||
sectorPoints.push(camera);
|
||||
return sectorPoints;
|
||||
}
|
||||
function drawMapMarkerFov(map, {
|
||||
lat,
|
||||
lng,
|
||||
direction,
|
||||
fov,
|
||||
range
|
||||
}){
|
||||
var fovEl = L.polygon(calculateFOV([lat, lng], direction, fov, range), {color: 'red'}).addTo(map);
|
||||
return fovEl
|
||||
}
|
||||
function setMapMarkerFov(fovEl,{
|
||||
lat,
|
||||
lng,
|
||||
direction,
|
||||
fov,
|
||||
range,
|
||||
}){
|
||||
fovEl.setLatLngs(calculateFOV([lat, lng], direction, fov, range));
|
||||
}
|
||||
|
||||
function getGeolocationParts(geolocation){
|
||||
var defaultLat = 49.2578298
|
||||
var defaultLng = -123.2634732
|
||||
var defaultZoom = 13
|
||||
var defaultDirection = 90
|
||||
var defaultFov = 60
|
||||
var defaultRange = 1
|
||||
try{
|
||||
var parts = geolocation.split(',')
|
||||
var lat = !parts[0] ? defaultLat : parseFloat(parts[0].trim().replace('@','')) || defaultLat
|
||||
var lng = !parts[1] ? defaultLng : parseFloat(parts[1].trim()) || defaultLng
|
||||
var zoom = !parts[2] ? defaultZoom : parseFloat(parts[2].trim().replace('v','')) || defaultZoom
|
||||
var direction = !parts[3] ? defaultDirection : parseFloat(parts[3].trim()) || defaultDirection
|
||||
var fov = !parts[4] ? defaultFov : parseFloat(parts[4].trim()) || defaultFov
|
||||
var range = !parts[5] ? defaultRange : parseFloat(parts[5].trim()) || defaultRange
|
||||
}catch(err){
|
||||
console.error(err)
|
||||
var lat = defaultLat
|
||||
var lng = defaultLng
|
||||
var zoom = defaultZoom
|
||||
var direction = defaultDirection
|
||||
var fov = defaultFov
|
||||
var range = defaultRange
|
||||
}
|
||||
return {
|
||||
lat,
|
||||
lng,
|
||||
zoom,
|
||||
direction,
|
||||
fov,
|
||||
range,
|
||||
}
|
||||
}
|
||||
|
||||
function getCardinalDirection(degree) {
|
||||
if (degree >= 337.5 || degree < 22.5) {
|
||||
return 'N';
|
||||
} else if (degree >= 22.5 && degree < 67.5) {
|
||||
return 'NE';
|
||||
} else if (degree >= 67.5 && degree < 112.5) {
|
||||
return 'E';
|
||||
} else if (degree >= 112.5 && degree < 157.5) {
|
||||
return 'SE';
|
||||
} else if (degree >= 157.5 && degree < 202.5) {
|
||||
return 'S';
|
||||
} else if (degree >= 202.5 && degree < 247.5) {
|
||||
return 'SW';
|
||||
} else if (degree >= 247.5 && degree < 292.5) {
|
||||
return 'W';
|
||||
} else if (degree >= 292.5 && degree < 337.5) {
|
||||
return 'NW';
|
||||
} else {
|
||||
return 'Invalid degree';
|
||||
}
|
||||
}
|
|
@ -1,4 +1,8 @@
|
|||
var monitorEditorSelectedMonitor = null
|
||||
onMonitorSettingsLoadedExtensions = []
|
||||
function onMonitorSettingsLoaded(theAction){
|
||||
onMonitorSettingsLoadedExtensions.push(theAction)
|
||||
}
|
||||
$(document).ready(function(e){
|
||||
|
||||
//Monitor Editor
|
||||
|
@ -48,6 +52,7 @@ function generateDefaultMonitorSettings(){
|
|||
"skip_ping": null,
|
||||
"is_onvif": null,
|
||||
"onvif_port": "",
|
||||
"onvif_events": "0",
|
||||
"primary_input": "0",
|
||||
"aduration": "1000000000",
|
||||
"probesize": "1000000000",
|
||||
|
@ -225,12 +230,16 @@ function generateDefaultMonitorSettings(){
|
|||
"detector_buffer_hls_list_size": "",
|
||||
"detector_buffer_start_number": "",
|
||||
"detector_buffer_live_start_index": "",
|
||||
"detector_ptz_follow": "0",
|
||||
"control": "0",
|
||||
"control_base_url": "",
|
||||
"control_url_method": null,
|
||||
"onvif_non_standard":"1",
|
||||
"control_url_method":"ONVIF",
|
||||
"control_turn_speed":"0.01",
|
||||
"control_digest_auth": null,
|
||||
"control_stop": "0",
|
||||
"control_url_stop_timeout": "",
|
||||
"control_axis_lock": "",
|
||||
"control_stop": "1",
|
||||
"control_url_stop_timeout": "500",
|
||||
"control_url_center": "",
|
||||
"control_url_left": "",
|
||||
"control_url_left_stop": "",
|
||||
|
@ -434,7 +443,8 @@ window.getMonitorEditFormFields = function(){
|
|||
}
|
||||
if(monitorConfig.name == ''){errorsFound.push('Monitor Name cannot be blank')}
|
||||
//edit details
|
||||
monitorConfig.details = safeJsonParse(monitorConfig.details)
|
||||
monitorConfig.details = getDetailValues(editorForm)
|
||||
// monitorConfig.details = safeJsonParse(monitorConfig.details)
|
||||
monitorConfig.details.substream = getSubStreamChannelFields()
|
||||
monitorConfig.details.input_map_choices = monitorSectionInputMapsave()
|
||||
// TODO : Input Maps and Stream Channels (does old way at the moment)
|
||||
|
@ -463,12 +473,10 @@ function getAdditionalStreamChannelFields(tempID,channelId){
|
|||
var fieldInfo = monitorSettingsAdditionalStreamChannelFieldHtml.replaceAll('$[TEMP_ID]',tempID).replaceAll('$[NUMBER]',channelId)
|
||||
return fieldInfo
|
||||
}
|
||||
|
||||
addOnTabOpen('monitorSettings', function () {
|
||||
setFieldVisibility()
|
||||
drawMonitorSettingsSubMenu()
|
||||
})
|
||||
|
||||
addOnTabReopen('monitorSettings', function () {
|
||||
setFieldVisibility()
|
||||
drawMonitorSettingsSubMenu()
|
||||
|
@ -688,6 +696,9 @@ function importIntoMonitorEditor(options){
|
|||
monitorsForCopy.find('optgroup').html(tmp)
|
||||
setFieldVisibility()
|
||||
drawMonitorSettingsSubMenu()
|
||||
onMonitorSettingsLoadedExtensions.forEach(function(theAction){
|
||||
theAction(monitorConfig)
|
||||
})
|
||||
}
|
||||
//parse "Automatic" field in "Input" Section
|
||||
monitorEditorWindow.on('change','.auto_host_fill input,.auto_host_fill select',function(e){
|
||||
|
@ -1014,9 +1025,9 @@ editorForm.find('[name="type"]').change(function(e){
|
|||
var el = $(this);
|
||||
if(el.val()==='h264')editorForm.find('[name="protocol"]').val('rtsp').change()
|
||||
})
|
||||
editorForm.find('[detail]').change(function(){
|
||||
onDetailFieldChange(this)
|
||||
})
|
||||
// editorForm.find('[detail]').change(function(){
|
||||
// onDetailFieldChange(this)
|
||||
// })
|
||||
editorForm.on('change','[selector]',function(){
|
||||
var el = $(this);
|
||||
onSelectorChange(el,editorForm)
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
$(document).ready(function(e){
|
||||
var monitorEditorWindow = $('#tab-monitorSettings')
|
||||
var monitorSettingsMonitorMap = $('#monitor-settings-monitor-map')
|
||||
var monitorSettingsMonitorMapContainer = $('#monitor-settings-monitor-map-container')
|
||||
var monitorSettingsMapOptionsEl = $('#monitor-settings-geolocation-options')
|
||||
var monitorSettingsMapOptionsElOptions = monitorSettingsMapOptionsEl.find('[map-option]')
|
||||
var editorForm = monitorEditorWindow.find('form')
|
||||
var loadedMap;
|
||||
var monitorMapMarker;
|
||||
var monitorMapMarkerFov;
|
||||
function setAdditionalControls(options){
|
||||
options = options || {}
|
||||
monitorSettingsMapOptionsElOptions.each(function(n,v){
|
||||
var el = $(v)
|
||||
var key = el.attr('map-option')
|
||||
if(options[key])el.val(options[key]);
|
||||
})
|
||||
}
|
||||
function setGeolocationFieldValue(markerDetails) {
|
||||
editorForm.find(`[detail="geolocation"]`).val(getMapMarkerPosition(markerDetails))
|
||||
}
|
||||
function loadMap(monitor, geoString){
|
||||
try{
|
||||
unloadMap()
|
||||
}catch(err){
|
||||
|
||||
}
|
||||
console.log('MAP LOAD!!!',monitor)
|
||||
var {
|
||||
lat,
|
||||
lng,
|
||||
zoom,
|
||||
direction,
|
||||
fov,
|
||||
range,
|
||||
} = getGeolocationParts(geoString || monitor.details.geolocation);
|
||||
loadedMap = L.map('monitor-settings-monitor-map').setView([lat, lng], zoom);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
}).addTo(loadedMap);
|
||||
monitorMapMarker = L.marker([lat, lng], {
|
||||
title: monitor ? `${monitor.name} (${monitor.host})` : null,
|
||||
draggable: true,
|
||||
}).addTo(loadedMap);
|
||||
monitorMapMarker.on('dragend', function(){
|
||||
setGeolocationFieldValue()
|
||||
});
|
||||
monitorMapMarker.on('drag', function(){
|
||||
var markerDetails = getMapMarkerDetails();
|
||||
setMapMarkerFov(monitorMapMarkerFov,markerDetails)
|
||||
});
|
||||
loadedMap.on('zoomend', function(){
|
||||
setGeolocationFieldValue()
|
||||
});
|
||||
setAdditionalControls({
|
||||
direction,
|
||||
fov,
|
||||
range,
|
||||
})
|
||||
monitorMapMarkerFov = drawMapMarkerFov(loadedMap,{
|
||||
lat,
|
||||
lng,
|
||||
direction,
|
||||
fov,
|
||||
range,
|
||||
})
|
||||
setAdditionalControlsUI({
|
||||
direction,
|
||||
fov,
|
||||
range,
|
||||
})
|
||||
}
|
||||
function unloadMap(){
|
||||
loadedMap.remove();
|
||||
loadedMap = null;
|
||||
}
|
||||
function getMapOptions(){
|
||||
var options = {}
|
||||
monitorSettingsMapOptionsElOptions.each(function(n,v){
|
||||
var el = $(v)
|
||||
var key = el.attr('map-option')
|
||||
var value = el.val()
|
||||
options[key] = parseFloat(value) || value
|
||||
})
|
||||
return options
|
||||
}
|
||||
function getMapMarkerDetails(){
|
||||
var pos = monitorMapMarker.getLatLng()
|
||||
var zoom = loadedMap.getZoom();
|
||||
var {
|
||||
direction,
|
||||
fov,
|
||||
range,
|
||||
} = getMapOptions();
|
||||
return {
|
||||
lat: pos.lat,
|
||||
lng: pos.lng,
|
||||
zoom,
|
||||
direction,
|
||||
fov,
|
||||
range,
|
||||
}
|
||||
}
|
||||
function getMapMarkerPosition(markerDetails){
|
||||
var {
|
||||
lat,
|
||||
lng,
|
||||
zoom,
|
||||
direction,
|
||||
fov,
|
||||
range,
|
||||
} = (markerDetails || getMapMarkerDetails());
|
||||
return `${lat},${lng},${zoom},${direction},${fov},${range}`
|
||||
}
|
||||
function setAdditionalControlsUI(markerDetails){
|
||||
$.each(markerDetails,function(key,value){
|
||||
var setValue = `${value}`
|
||||
if(key === 'direction'){
|
||||
setValue = `${value} (${getCardinalDirection(value)})`
|
||||
}
|
||||
monitorSettingsMapOptionsEl.find(`[map-option-value="${key}"]`).text(setValue)
|
||||
})
|
||||
}
|
||||
editorForm.find(`[detail="geolocation"]`).change(function(){
|
||||
var geoString = $(this).val();
|
||||
var currentGeoString = monitorEditorSelectedMonitor.details.geolocation
|
||||
if(!geoString && currentGeoString){
|
||||
editorForm.find(`[detail="geolocation"]`).val(currentGeoString)
|
||||
}
|
||||
loadMap(monitorEditorSelectedMonitor, geoString)
|
||||
})
|
||||
addOnTabOpen('monitorSettings', function () {
|
||||
loadMap(monitorEditorSelectedMonitor)
|
||||
})
|
||||
addOnTabReopen('monitorSettings', function () {
|
||||
loadMap(monitorEditorSelectedMonitor)
|
||||
})
|
||||
addOnTabAway('monitorSettings', function () {
|
||||
unloadMap()
|
||||
})
|
||||
onMonitorSettingsLoaded(function(monitorConfig){
|
||||
loadMap(monitorConfig)
|
||||
})
|
||||
monitorSettingsMapOptionsElOptions.on('input',function(){
|
||||
var markerDetails = getMapMarkerDetails();
|
||||
setGeolocationFieldValue(markerDetails)
|
||||
setMapMarkerFov(monitorMapMarkerFov,markerDetails)
|
||||
setAdditionalControlsUI(markerDetails)
|
||||
})
|
||||
})
|
|
@ -289,6 +289,12 @@ function buildStreamUrl(monitorId){
|
|||
return streamURL
|
||||
}
|
||||
|
||||
function buildEmbedUrl(monitor){
|
||||
var monitorId = monitor.mid;
|
||||
var streamURL = `${getApiPrefix(`embed`)}/${monitorId}/fullscreen|jquery|gui|relative?host=${location.pathname}`
|
||||
return streamURL;
|
||||
}
|
||||
|
||||
function getDbColumnsForMonitor(monitor){
|
||||
var acceptedFields = [
|
||||
'mid',
|
||||
|
@ -998,7 +1004,7 @@ function buildMiniMonitorCardBody(monitorAlreadyAdded,monitorConfigPartial,addit
|
|||
<div class="card-body p-2">
|
||||
<div>${infoHtml}</div>
|
||||
</div>
|
||||
<div class="card-footer text-center">
|
||||
<div class="card-footer text-center" data-mid="${monitorId}">
|
||||
<a class="btn btn-sm btn-block btn-${monitorAlreadyAdded ? doOpenVideosInsteadOfDelete ? 'primary open-videosTable' : 'danger delete-monitor' : 'success add-monitor'}">${monitorAlreadyAdded ? doOpenVideosInsteadOfDelete ? lang['Videos'] : lang['Delete Camera'] : lang['Add Camera']}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,798 +0,0 @@
|
|||
$(document).ready(function(e){
|
||||
var powerVideoWindow = $('#powerVideo')
|
||||
var powerVideoMonitorsListElement = $('#powerVideoMonitorsList')
|
||||
var powerVideoMonitorViewsElement = $('#powerVideoMonitorViews')
|
||||
var powerVideoTimelineStripsContainer = $('#powerVideoTimelineStrips')
|
||||
var dateSelector = $('#powerVideoDateRange')
|
||||
var powerVideoVideoLimitElement = $('#powerVideoVideoLimit')
|
||||
var powerVideoEventLimitElement = $('#powerVideoEventLimit')
|
||||
var powerVideoSet = $('#powerVideoSet')
|
||||
var powerVideoMuteIcon = powerVideoWindow.find('[powerVideo-control="toggleMute"] i')
|
||||
var objectTagSearchField = $('#powerVideo_tag_search')
|
||||
var powerVideoLoadedVideos = {}
|
||||
var powerVideoLoadedEvents = {}
|
||||
var powerVideoLoadedChartData = {}
|
||||
var loadedTableGroupIds = {}
|
||||
var eventsLabeledByTime = {}
|
||||
var monitorSlotPlaySpeeds = {}
|
||||
var currentlyPlayingVideos = {}
|
||||
var powerVideoMute = true
|
||||
var powerVideoCanAutoPlay = true
|
||||
var lastPowerVideoSelectedMonitors = []
|
||||
var extenders = {
|
||||
onVideoPlayerTimeUpdateExtensions: [],
|
||||
onVideoPlayerTimeUpdate: function(extender){
|
||||
extenders.onVideoPlayerTimeUpdateExtensions.push(extender)
|
||||
},
|
||||
onVideoPlayerCreateExtensions: [],
|
||||
onVideoPlayerCreate: function(extender){
|
||||
extenders.onVideoPlayerCreateExtensions.push(extender)
|
||||
},
|
||||
}
|
||||
var activeTimeline = null
|
||||
// fix utc/localtime translation (use timelapseJpeg as guide, it works as expected) >
|
||||
loadDateRangePicker(dateSelector,{
|
||||
startDate: moment().subtract(moment.duration("24:00:00")),
|
||||
endDate: moment().add(moment.duration("24:00:00")),
|
||||
timePicker24Hour: true,
|
||||
timePickerSeconds: true,
|
||||
onChange: function(start, end, label) {
|
||||
dateSelector.focus()
|
||||
$.each(lastPowerVideoSelectedMonitors,async function(n,monitorId){
|
||||
await requestTableData(monitorId)
|
||||
})
|
||||
}
|
||||
})
|
||||
// fix utc/localtime translation (use timelapseJpeg as guide, it works as expected) />
|
||||
function loadVideosToTimeLineMemory(monitorId,videos,events){
|
||||
videos.forEach((video) => {
|
||||
createVideoLinks(video,{
|
||||
hideRemote: true
|
||||
})
|
||||
})
|
||||
powerVideoLoadedVideos[monitorId] = videos
|
||||
powerVideoLoadedEvents[monitorId] = events
|
||||
}
|
||||
function drawMonitorsList(){
|
||||
// var monitorList = Object.values(loadedMonitors).map(function(item){
|
||||
// return {
|
||||
// value: item.mid,
|
||||
// label: item.name,
|
||||
// }
|
||||
// });
|
||||
var html = ``
|
||||
$.each(getLoadedMonitorsAlphabetically(),function(n,monitor){
|
||||
html += `<div class="flex-row d-flex">
|
||||
<div class="flex-grow-1 p-2">${monitor.name}</div>
|
||||
<div class="p-2 text-right"><small>${monitor.host}</small></div>
|
||||
<div class="p-2"><input name="${monitor.mid}" value="1" type="checkbox" class="form-check-input position-initial ml-0"></div>
|
||||
</div>`
|
||||
})
|
||||
powerVideoMonitorsListElement.html(html)
|
||||
}
|
||||
function getVideoSetSelected(){
|
||||
return powerVideoSet.val()
|
||||
}
|
||||
async function requestTableData(monitorId){
|
||||
var dateRange = getSelectedTime(dateSelector)
|
||||
var searchQuery = objectTagSearchField.val() || null
|
||||
var startDate = dateRange.startDate
|
||||
var endDate = dateRange.endDate
|
||||
var wantsCloudVideo = getVideoSetSelected() === 'cloud'
|
||||
var wantsArchivedVideo = getVideoSetSelected() === 'archive'
|
||||
var videos = (await getVideos({
|
||||
monitorId,
|
||||
startDate,
|
||||
endDate,
|
||||
searchQuery,
|
||||
archived: wantsArchivedVideo,
|
||||
customVideoSet: wantsCloudVideo ? 'cloudVideos' : 'videos',
|
||||
})).videos;
|
||||
var events = ([]).concat(...videos.map(row => row.events || []));
|
||||
loadVideosToTimeLineMemory(monitorId,videos,events)
|
||||
drawLoadedTableData()
|
||||
}
|
||||
function unloadTableData(monitorId,user){
|
||||
if(!user)user = $user
|
||||
delete(powerVideoLoadedVideos[monitorId])
|
||||
delete(powerVideoLoadedEvents[monitorId])
|
||||
delete(loadedTableGroupIds[monitorId])
|
||||
delete(loadedTableGroupIds[monitorId + '_events'])
|
||||
powerVideoMonitorViewsElement.find(`.videoPlayer[data-mid="${monitorId}"]`).remove()
|
||||
drawLoadedTableData()
|
||||
}
|
||||
function checkEventsAgainstVideo(video,events){
|
||||
var videoStartTime = new Date(video.time)
|
||||
var videoEndTime = new Date(video.end)
|
||||
var eventsToCheck = events
|
||||
video.detections = {}
|
||||
var newSetOfEventsWithoutChecked = {}
|
||||
$.each(eventsToCheck,function(n,event){
|
||||
var eventTime = new Date(event.time)
|
||||
var seekPosition = (eventTime - videoStartTime) / 1000
|
||||
if (videoStartTime <= eventTime && eventTime <= videoEndTime) {
|
||||
if(!video.details.confidence)video.details.confidence = 0
|
||||
video.detections[seekPosition] = event
|
||||
eventsLabeledByTime[video.mid][video.time][seekPosition] = event
|
||||
}else{
|
||||
newSetOfEventsWithoutChecked[n] = video
|
||||
}
|
||||
})
|
||||
eventsToCheck = newSetOfEventsWithoutChecked
|
||||
}
|
||||
function prepareVideosAndEventsForTable(monitorId,videos,events){
|
||||
var chartData = []
|
||||
eventsLabeledByTime[monitorId] = {}
|
||||
$.each(videos,function(n,video){
|
||||
eventsLabeledByTime[monitorId][video.time] = {}
|
||||
if(videos[n - 1])video.videoAfter = videos[n - 1]
|
||||
if(videos[n + 1])video.videoBefore = videos[n + 1]
|
||||
checkEventsAgainstVideo(video,events)
|
||||
chartData.push({
|
||||
group: loadedTableGroupIds[monitorId],
|
||||
content: `<div timeline-video-file="${video.mid}${video.time}">
|
||||
${formattedTime(video.time, 'hh:mm:ss AA, DD-MM-YYYY')}
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-danger" role="progressbar" style="width:0%;"><span></span></div>
|
||||
</div>
|
||||
</div>`,
|
||||
start: video.time,
|
||||
end: video.end,
|
||||
videoInfo: video
|
||||
})
|
||||
})
|
||||
$.each(events,function(n,event){
|
||||
var eventReason = event.details && event.details.reason ? event.details.reason.toUpperCase() : "UNKNOWN"
|
||||
var eventSlotTag = eventReason
|
||||
if(eventReason === 'OBJECT' && event.details.matrices && event.details.matrices[0]){
|
||||
eventSlotTag = []
|
||||
event.details.matrices.forEach(function(matrix){
|
||||
eventSlotTag.push(matrix.tag)
|
||||
})
|
||||
eventSlotTag = eventSlotTag.join(', ')
|
||||
}
|
||||
chartData.push({
|
||||
group: loadedTableGroupIds[monitorId + '_events'],
|
||||
content: `<div timeline-event="${event.time}">${eventSlotTag}</div>`,
|
||||
start: event.time,
|
||||
eventInfo: event
|
||||
})
|
||||
})
|
||||
return chartData
|
||||
}
|
||||
function getMiniEventsChartConfig(video){
|
||||
var monitorId = video.mid
|
||||
var labels = []
|
||||
var chartData = []
|
||||
var events = video.detections || video.events
|
||||
$.each(events,function(n,v){
|
||||
if(!v.details.confidence){v.details.confidence=0}
|
||||
var time = moment(v.time).format('MM/DD/YYYY HH:mm:ss')
|
||||
labels.push(time)
|
||||
chartData.push(v.details.confidence)
|
||||
})
|
||||
var timeFormat = 'MM/DD/YYYY HH:mm:ss';
|
||||
Chart.defaults.global.defaultFontColor = '#fff';
|
||||
var config = {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
type: 'line',
|
||||
label: 'Motion Confidence',
|
||||
backgroundColor: window.chartColors.blue,
|
||||
borderColor: window.chartColors.red,
|
||||
data: chartData,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
title: {
|
||||
fontColor: "white",
|
||||
text:"Events in this video"
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
type: "time",
|
||||
display: true,
|
||||
time: {
|
||||
format: timeFormat,
|
||||
}
|
||||
}],
|
||||
},
|
||||
}
|
||||
};
|
||||
return config
|
||||
}
|
||||
function drawMiniEventsChart(video,chartConfig){
|
||||
var videoContainer = powerVideoMonitorViewsElement.find(`.videoPlayer[data-mid=${video.mid}]`)
|
||||
var canvas = videoContainer.find('canvas')
|
||||
var ctx = canvas[0].getContext("2d")
|
||||
var miniChart = new Chart(ctx, chartConfig)
|
||||
canvas.click(function(f) {
|
||||
var target = miniChart.getElementsAtEvent(f)[0];
|
||||
if(!target){return false}
|
||||
var event = video.detections[target._index]
|
||||
var video1 = videoContainer.find('video')[0]
|
||||
video1.currentTime = moment(event.time).diff(moment(video.time),'seconds')
|
||||
video1.play()
|
||||
})
|
||||
}
|
||||
function getAllChartDataForLoadedVideos(){
|
||||
var chartData = []
|
||||
Object.keys(powerVideoLoadedVideos).forEach(function(monitorId,n){
|
||||
var videos = powerVideoLoadedVideos[monitorId]
|
||||
var events = powerVideoLoadedEvents[monitorId]
|
||||
var parsedVideos = prepareVideosAndEventsForTable(monitorId,videos,events)
|
||||
powerVideoLoadedChartData[monitorId] = parsedVideos
|
||||
chartData = chartData.concat(parsedVideos)
|
||||
})
|
||||
return chartData
|
||||
}
|
||||
function visuallySelectItemInRow(video){
|
||||
powerVideoTimelineStripsContainer.find(`[timeline-video-file="${video.mid}${video.time}"]`).parents('.vis-item').addClass('vis-selected')
|
||||
}
|
||||
function visuallyDeselectItemInRow(video){
|
||||
powerVideoTimelineStripsContainer.find(`[timeline-video-file="${video.mid}${video.time}"]`).parents('.vis-item').removeClass('vis-selected')
|
||||
}
|
||||
var drawTableTimeout = null
|
||||
function drawLoadedTableData(){
|
||||
// destroy old
|
||||
try{
|
||||
if(activeTimeline && activeTimeline.destroy){
|
||||
activeTimeline.destroy()
|
||||
}
|
||||
}catch(err){
|
||||
|
||||
}
|
||||
//
|
||||
powerVideoTimelineStripsContainer.html(`<div class="loading"><i class="fa fa-spinner fa-pulse"></i><div class="epic-text">${lang['Please Wait...']}</div></div>`)
|
||||
clearTimeout(drawTableTimeout)
|
||||
drawTableTimeout = setTimeout(function(){
|
||||
var container = powerVideoTimelineStripsContainer[0]
|
||||
var groupsDataSet = new vis.DataSet()
|
||||
var groups = []
|
||||
var groupId = 1
|
||||
Object.keys(powerVideoLoadedVideos).forEach(function(monitorId,n){
|
||||
var mon = Object.values(loadedMonitors).find(m => { return m.mid === monitorId });
|
||||
var name = mon.name;
|
||||
groups.push({
|
||||
id: groupId,
|
||||
content: name + " | " + lang.Videos
|
||||
})
|
||||
groupId += 1
|
||||
groups.push({
|
||||
id: groupId,
|
||||
content: name + " | " + lang.Events
|
||||
})
|
||||
groupId += 1
|
||||
loadedTableGroupIds[monitorId] = groupId - 2
|
||||
loadedTableGroupIds[monitorId + '_events'] = groupId - 1
|
||||
})
|
||||
groupsDataSet.add(groups)
|
||||
var chartData = getAllChartDataForLoadedVideos()
|
||||
if(chartData.length > 0){
|
||||
var items = new vis.DataSet(chartData)
|
||||
var options = {
|
||||
selectable: false,
|
||||
stack: false,
|
||||
showCurrentTime: false,
|
||||
}
|
||||
// Create a Timeline
|
||||
var timeline = new vis.Timeline(container, items, groupsDataSet, options)
|
||||
powerVideoTimelineStripsContainer.find('.loading').remove()
|
||||
var timeChanging = false
|
||||
timeline.on('rangechange', function(properties){
|
||||
timeChanging = true
|
||||
})
|
||||
timeline.on('rangechanged', function(properties){
|
||||
setTimeout(function(){
|
||||
timeChanging = false
|
||||
},300)
|
||||
})
|
||||
timeline.on('click', function(properties){
|
||||
if(!timeChanging){
|
||||
var selectedTime = properties.time
|
||||
var videosAtSameTime = findAllVideosAtTime(selectedTime)
|
||||
powerVideoTimelineStripsContainer.find('.vis-item').removeClass('vis-selected')
|
||||
$.each(videosAtSameTime,function(monitorId,videos){
|
||||
var selectedVideo = videos[0]
|
||||
if(selectedVideo){
|
||||
loadVideoIntoMonitorSlot(selectedVideo,selectedTime)
|
||||
visuallySelectItemInRow(selectedVideo)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
activeTimeline = timeline
|
||||
}else{
|
||||
powerVideoTimelineStripsContainer.html(`<div class="loading"><i class="fa fa-exclamation-circle"></i><div class="epic-text">${lang['No Data']}</div></div>`)
|
||||
}
|
||||
},1000)
|
||||
}
|
||||
function drawMatrices(event,options){
|
||||
var streamObjectsContainer = options.streamObjectsContainer
|
||||
var height = options.height
|
||||
var width = options.width
|
||||
var monitorId = options.mid
|
||||
var widthRatio = width / event.details.imgWidth
|
||||
var heightRatio = height / event.details.imgHeight
|
||||
|
||||
streamObjectsContainer.find('.stream-detected-object[name="'+event.details.name+'"]').remove()
|
||||
var html = ''
|
||||
$.each(event.details.matrices,function(n,matrix){
|
||||
html += `<div class="stream-detected-object" name="${event.details.name}" style="height:${heightRatio * matrix.height}px;width:${widthRatio * matrix.width}px;top:${heightRatio * matrix.y}px;left:${widthRatio * matrix.x}px;">`
|
||||
if(matrix.tag)html += `<span class="tag">${matrix.tag}${!isNaN(matrix.id) ? ` <small class="label label-default">${matrix.id}</small>`: ''}</span>`
|
||||
html += '</div>'
|
||||
})
|
||||
streamObjectsContainer.append(html)
|
||||
}
|
||||
function attachEventsToVideoActiveElement(video){
|
||||
var monitorId = video.mid
|
||||
var videoPlayerContainer = powerVideoMonitorViewsElement.find(`.videoPlayer[data-mid=${monitorId}]`)
|
||||
var videoElement = videoPlayerContainer.find(`video.videoNow`)
|
||||
var streamObjectsContainer = videoPlayerContainer.find(`.videoPlayer-stream-objects`)
|
||||
var detectionInfoContainerMotion = videoPlayerContainer.find(`.videoPlayer-detection-info-motion`)
|
||||
var detectionInfoContainerObject = videoPlayerContainer.find(`.videoPlayer-detection-info-object`)
|
||||
var detectionInfoContainerRaw = videoPlayerContainer.find(`.videoPlayer-detection-info-raw`)
|
||||
var motionMeterProgressBar = videoPlayerContainer.find(`.videoPlayer-motion-meter .progress-bar`)
|
||||
var motionMeterProgressBarTextBox = videoPlayerContainer.find(`.videoPlayer-motion-meter .progress-bar span`)
|
||||
var videoCurrentTimeProgressBar = powerVideoTimelineStripsContainer.find(`[timeline-video-file="${video.mid}${video.time}"] .progress-bar`)[0]
|
||||
var preloadedNext = false
|
||||
var reinitializeStreamObjectsContainer = function(){
|
||||
height = videoElement.height()
|
||||
width = videoElement.width()
|
||||
}
|
||||
reinitializeStreamObjectsContainer()
|
||||
$(videoElement)
|
||||
.resize(reinitializeStreamObjectsContainer)
|
||||
// .off('loadeddata').on('loadeddata', function() {
|
||||
// reinitializeStreamObjectsContainer()
|
||||
// var allLoaded = true
|
||||
// getAllActiveVideosInSlots().each(function(n,videoElement){
|
||||
// if(!videoElement.readyState === 4)allLoaded = false
|
||||
// })
|
||||
// setTimeout(function(){
|
||||
// if(allLoaded){
|
||||
// playAllSlots()
|
||||
// }
|
||||
// },1500)
|
||||
// })
|
||||
// .off("pause").on("pause",function(){
|
||||
// console.log(monitorId,'pause')
|
||||
// })
|
||||
// .off("play").on("play",function(){
|
||||
// console.log(monitorId,'play')
|
||||
// })
|
||||
.off("loadedmetadata").on("loadedmetadata",function(){
|
||||
resetWidthForActiveVideoPlayers()
|
||||
})
|
||||
.off("pause").on("pause",function(){
|
||||
resetWidthForActiveVideoPlayers()
|
||||
})
|
||||
.off("play").on("play",function(){
|
||||
resetWidthForActiveVideoPlayers()
|
||||
})
|
||||
.off("timeupdate").on("timeupdate",function(){
|
||||
try{
|
||||
var event = eventsLabeledByTime[monitorId][video.time][parseInt(this.currentTime)]
|
||||
if(event){
|
||||
if(event.details.matrices){
|
||||
drawMatrices(event,{
|
||||
streamObjectsContainer: streamObjectsContainer,
|
||||
monitorId: monitorId,
|
||||
height: height,
|
||||
width: width,
|
||||
})
|
||||
detectionInfoContainerObject.html(jsonToHtmlBlock(event.details.matrices))
|
||||
}
|
||||
if(event.details.confidence){
|
||||
motionMeterProgressBar.css('width',event.details.confidence+'%')
|
||||
motionMeterProgressBarTextBox.text(event.details.confidence)
|
||||
var html = `<div>${lang['Region']} : ${event.details.name}</div>
|
||||
<div>${lang['Confidence']} : ${event.details.confidence}</div>
|
||||
<div>${lang['Plugin']} : ${event.details.plug}</div>`
|
||||
detectionInfoContainerMotion.html(html)
|
||||
// detectionInfoContainerRaw.html(jsonToHtmlBlock({`${lang['Plug']}`:event.details.plug}))
|
||||
}
|
||||
}
|
||||
var currentTime = this.currentTime;
|
||||
var watchPoint = Math.floor((currentTime/this.duration) * 100)
|
||||
if(!preloadedNext && watchPoint >= 75){
|
||||
preloadedNext = true
|
||||
var videoAfter = videoPlayerContainer.find(`video.videoAfter`)[0]
|
||||
videoAfter.setAttribute('preload',true)
|
||||
}
|
||||
if(videoCurrentTimeProgressBar)videoCurrentTimeProgressBar.style.width = `${watchPoint}%`
|
||||
extenders.onVideoPlayerTimeUpdateExtensions.forEach(function(extender){
|
||||
extender(videoElement,watchPoint)
|
||||
})
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
var onEnded = function() {
|
||||
visuallyDeselectItemInRow(video)
|
||||
if(video.videoAfter){
|
||||
visuallySelectItemInRow(video.videoAfter)
|
||||
loadVideoIntoMonitorSlot(video.videoAfter)
|
||||
}
|
||||
}
|
||||
videoElement[0].onended = onEnded
|
||||
videoElement[0].onerror = onEnded
|
||||
}
|
||||
function dettachEventsToVideoActiveElement(monitorId){
|
||||
var videoElement = powerVideoMonitorViewsElement.find(`.videoPlayer[data-mid=${monitorId}] video.videoNow`)
|
||||
$(videoElement)
|
||||
// .off('loadeddata')
|
||||
.off("pause")
|
||||
.off("play")
|
||||
.off("timeupdate")
|
||||
}
|
||||
function findAllVideosAtTime(selectedTime){
|
||||
var time = new Date(selectedTime)
|
||||
var parsedVideos = {}
|
||||
$.each(powerVideoLoadedVideos,function(monitorId,videos){
|
||||
var videosFilteredByTime = videos.filter(function(video){
|
||||
return (
|
||||
(new Date(video.time)) <= time && time < (new Date(video.end))
|
||||
)
|
||||
});
|
||||
parsedVideos[monitorId] = videosFilteredByTime
|
||||
})
|
||||
return parsedVideos
|
||||
}
|
||||
function resetVisualDetectionDataForMonitorSlot(monitorId){
|
||||
var videoPlayerContainer = powerVideoMonitorViewsElement.find(`.videoPlayer[data-mid=${monitorId}]`)
|
||||
var streamObjectsContainer = videoPlayerContainer.find(`.videoPlayer-stream-objects`)
|
||||
var detectionInfoContainerObject = videoPlayerContainer.find(`.videoPlayer-detection-info-object`)
|
||||
var detectionInfoContainerMotion = videoPlayerContainer.find(`.videoPlayer-detection-info-motion`)
|
||||
var motionMeterProgressBar = videoPlayerContainer.find(`.videoPlayer-motion-meter .progress-bar`)
|
||||
var motionMeterProgressBarTextBox = videoPlayerContainer.find(`.videoPlayer-motion-meter .progress-bar span`)
|
||||
detectionInfoContainerObject.empty()
|
||||
detectionInfoContainerMotion.empty()
|
||||
streamObjectsContainer.empty()
|
||||
motionMeterProgressBar.css('width','0')
|
||||
motionMeterProgressBarTextBox.text('0')
|
||||
}
|
||||
function resetWidthForActiveVideoPlayers(){
|
||||
powerVideoMonitorViewsElement.find('.videoPlayer').css('width',`49.8%`)
|
||||
}
|
||||
function loadVideoIntoMonitorSlot(video,selectedTime){
|
||||
if(!video)return
|
||||
resetVisualDetectionDataForMonitorSlot(video.mid)
|
||||
currentlyPlayingVideos[video.mid] = video
|
||||
var timeToStartAt = selectedTime - new Date(video.time)
|
||||
var numberOfMonitors = Object.keys(powerVideoLoadedVideos).length
|
||||
// if(numberOfMonitors > 3)numberOfMonitors = 3 //start new row after 3
|
||||
if(numberOfMonitors == 1)numberOfMonitors = 2 //make single monitor not look like a doofus
|
||||
if(timeToStartAt < 0)timeToStartAt = 0
|
||||
var videoContainer = powerVideoMonitorViewsElement.find(`.videoPlayer[data-mid=${video.mid}] .videoPlayer-buffers`)
|
||||
if(videoContainer.length === 0){
|
||||
if(!monitorSlotPlaySpeeds)monitorSlotPlaySpeeds[video.mid] = {}
|
||||
powerVideoMonitorViewsElement.append(`<div class="videoPlayer" style="width:49.8%;max-width:500px;min-width:250px;" data-mid="${video.mid}">
|
||||
<div class="videoPlayer-detection-info">
|
||||
<div class="videoPlayer-detection-info-buttons btn-group">
|
||||
<a powerVideo-control="downloadVideo" class="btn btn-sm btn-success"><i class="fa fa-download"></i></a>
|
||||
<a powerVideo-control="openVideoPlayer" class="btn btn-sm btn-default"><i class="fa fa-external-link"></i></a>
|
||||
</div>
|
||||
<canvas style="height:400px"></canvas>
|
||||
</div>
|
||||
<div class="videoPlayer-stream-objects"></div>
|
||||
<div class="videoPlayer-buffers"></div>
|
||||
<div class="videoPlayer-motion-meter progress" title="${lang['Motion Meter']}">
|
||||
<div class="progress-bar progress-bar-danger" role="progressbar" style="width:0%;"><span></span></div>
|
||||
</div>
|
||||
</div>`)
|
||||
videoContainer = powerVideoMonitorViewsElement.find(`.videoPlayer[data-mid=${video.mid}] .videoPlayer-buffers`)
|
||||
}else{
|
||||
powerVideoMonitorViewsElement.find('.videoPlayer').css('width',`49.8%`)
|
||||
}
|
||||
var videoCurrentNow = videoContainer.find('.videoNow')
|
||||
var videoCurrentAfter = videoContainer.find('.videoAfter')
|
||||
// var videoCurrentBefore = videoContainer.find('.videoBefore')
|
||||
dettachEventsToVideoActiveElement(video.mid)
|
||||
videoContainer.find('video').each(function(n,v){
|
||||
v.pause()
|
||||
})
|
||||
var videoIsSame = (video.href == videoCurrentNow.attr('video'))
|
||||
var videoIsAfter = (video.href == videoCurrentAfter.attr('video'))
|
||||
// var videoIsBefore = (video.href == videoCurrentBefore.attr('video'))
|
||||
var drawVideoHTML = function(position){
|
||||
var videoData
|
||||
var exisitingElement = videoContainer.find('.' + position)
|
||||
if(position){
|
||||
videoData = video[position]
|
||||
}else{
|
||||
position = 'videoNow'
|
||||
videoData = video
|
||||
}
|
||||
if(videoData){
|
||||
videoContainer.append('<video class="video_video '+position+'" video="'+videoData.href+'" playsinline><source src="'+videoData.href+'" type="video/'+videoData.ext+'"></video>')
|
||||
}
|
||||
}
|
||||
if(
|
||||
videoIsSame ||
|
||||
videoIsAfter
|
||||
// || videoIsBefore
|
||||
){
|
||||
switch(true){
|
||||
case videoIsSame:
|
||||
var videoNow = videoContainer.find('video.videoNow')[0]
|
||||
if(!videoNow.paused)videoNow.pause()
|
||||
videoNow.currentTime = timeToStartAt / 1000
|
||||
if(videoNow.paused)videoNow.play()
|
||||
attachEventsToVideoActiveElement(video)
|
||||
return
|
||||
break;
|
||||
case videoIsAfter:
|
||||
// videoCurrentBefore.remove()
|
||||
videoCurrentNow.remove()
|
||||
videoCurrentAfter.removeClass('videoAfter').addClass('videoNow')
|
||||
// videoCurrentNow.removeClass('videoNow').addClass('videoBefore')
|
||||
drawVideoHTML('videoAfter')
|
||||
break;
|
||||
// case videoIsBefore:
|
||||
// videoCurrentAfter.remove()
|
||||
// videoCurrentBefore.removeClass('videoBefore').addClass('videoNow')
|
||||
// videoCurrentNow.removeClass('videoNow').addClass('videoAfter')
|
||||
// drawVideoHTML('videoBefore')
|
||||
// break;
|
||||
}
|
||||
}else{
|
||||
videoContainer.empty()
|
||||
drawVideoHTML()//videoNow
|
||||
// drawVideoHTML('videoBefore')
|
||||
drawVideoHTML('videoAfter')
|
||||
}
|
||||
var videoNow = videoContainer.find('video.videoNow')[0]
|
||||
attachEventsToVideoActiveElement(video)
|
||||
//
|
||||
videoNow.setAttribute('preload',true)
|
||||
videoNow.muted = true
|
||||
videoNow.playbackRate = monitorSlotPlaySpeeds[video.mid] || 1
|
||||
try{
|
||||
videoNow.currentTime = timeToStartAt / 1000
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
}
|
||||
videoNow.play()
|
||||
setTimeout(function(){
|
||||
resetWidthForActiveVideoPlayers()
|
||||
},1400)
|
||||
extenders.onVideoPlayerCreateExtensions.forEach(function(extender){
|
||||
extender(videoElement,watchPoint)
|
||||
})
|
||||
drawMiniEventsChart(video,getMiniEventsChartConfig(video))
|
||||
}
|
||||
function getSelectedMonitors(){
|
||||
var form = powerVideoMonitorsListElement.serializeObject()
|
||||
var selectedMonitors = Object.keys(form).filter(key => form[key] == '1')
|
||||
return selectedMonitors
|
||||
}
|
||||
function getActiveVideoInSlot(monitorId){
|
||||
return powerVideoMonitorViewsElement.find(`.videoPlayer[data-mid="${monitorId}"] video.videoNow`)[0]
|
||||
}
|
||||
function getAllActiveVideosInSlots(){
|
||||
return powerVideoMonitorViewsElement.find('video.videoNow')
|
||||
}
|
||||
function getActiveVideoRow(monitorId){
|
||||
return currentlyPlayingVideos[monitorId]
|
||||
}
|
||||
function pauseAllSlots(){
|
||||
getAllActiveVideosInSlots().each(function(n,video){
|
||||
if(!video.paused)video.pause()
|
||||
})
|
||||
}
|
||||
function toggleZoomAllSlots(){
|
||||
powerVideoMonitorViewsElement.find(`.videoPlayer`).each(function(n,videoContainer){
|
||||
var streamWindow = $(videoContainer)
|
||||
var monitorId = streamWindow.attr('data-mid')
|
||||
var enabled = streamWindow.attr('zoomEnabled')
|
||||
if(enabled === '1'){
|
||||
streamWindow
|
||||
.attr('zoomEnabled','0')
|
||||
.off('mouseover')
|
||||
.off('mouseout')
|
||||
.off('mousemove')
|
||||
.off('touchmove')
|
||||
.find('.zoomGlass').remove()
|
||||
}else{
|
||||
const magnifyStream = function(e){
|
||||
var videoElement = streamWindow.find('video.videoNow')
|
||||
console.log(videoElement[0].currentTime)
|
||||
magnifyStream({
|
||||
p: streamWindow,
|
||||
videoUrl: streamWindow.find('video.videoNow').find('source').attr('src'),
|
||||
setTime: videoElement[0].currentTime,
|
||||
monitor: loadedMonitors[monitorId],
|
||||
targetForZoom: 'video.videoNow',
|
||||
magnifyOffsetElement: '.videoPlayer-buffers',
|
||||
zoomAmount: 1,
|
||||
auto: false,
|
||||
animate: false,
|
||||
pageX: e.pageX,
|
||||
pageY: e.pageY
|
||||
},$user)
|
||||
}
|
||||
streamWindow
|
||||
.attr('zoomEnabled','1')
|
||||
.on('mouseover', function(){
|
||||
streamWindow.find(".zoomGlass").show()
|
||||
})
|
||||
.on('mouseout', function(){
|
||||
streamWindow.find(".zoomGlass").hide()
|
||||
})
|
||||
.on('mousemove', magnifyStream)
|
||||
.on('touchmove', magnifyStream)
|
||||
}
|
||||
})
|
||||
}
|
||||
function playAllSlots(){
|
||||
getAllActiveVideosInSlots().each(function(n,video){
|
||||
if(video.paused)video.play()
|
||||
})
|
||||
}
|
||||
function toggleMute(){
|
||||
powerVideoMute = !powerVideoMute
|
||||
getAllActiveVideosInSlots().each(function(n,video){
|
||||
if(powerVideoMute){
|
||||
powerVideoMuteIcon.removeClass('fa-volume-up').addClass('fa-volume-off')
|
||||
video.muted = true
|
||||
}else{
|
||||
powerVideoMuteIcon.removeClass('fa-volume-off').addClass('fa-volume-up')
|
||||
video.muted = false
|
||||
}
|
||||
})
|
||||
}
|
||||
function setPlaySpeedOnAllSlots(playSpeed){
|
||||
Object.keys(powerVideoLoadedVideos).forEach(function(monitorId){
|
||||
monitorSlotPlaySpeeds[monitorId] = playSpeed
|
||||
})
|
||||
getAllActiveVideosInSlots().each(function(n,video){
|
||||
video.playbackRate = playSpeed
|
||||
})
|
||||
}
|
||||
function nextVideoAllSlots(){
|
||||
Object.keys(currentlyPlayingVideos).forEach(function(monitorId){
|
||||
var video = currentlyPlayingVideos[monitorId]
|
||||
visuallyDeselectItemInRow(video)
|
||||
visuallySelectItemInRow(video.videoAfter)
|
||||
loadVideoIntoMonitorSlot(video.videoAfter,0)
|
||||
})
|
||||
}
|
||||
function previousVideoAllSlots(){
|
||||
Object.keys(currentlyPlayingVideos).forEach(function(monitorId){
|
||||
var video = currentlyPlayingVideos[monitorId]
|
||||
visuallyDeselectItemInRow(video)
|
||||
visuallySelectItemInRow(video.videoBefore)
|
||||
loadVideoIntoMonitorSlot(video.videoBefore,0)
|
||||
})
|
||||
}
|
||||
function onPowerVideoSettingsChange(){
|
||||
var monitorIdsSelectedNow = getSelectedMonitors()
|
||||
lastPowerVideoSelectedMonitors.forEach((monitorId) => {
|
||||
unloadTableData(monitorId)
|
||||
})
|
||||
monitorIdsSelectedNow.forEach(async (monitorId) => {
|
||||
await requestTableData(monitorId)
|
||||
})
|
||||
lastPowerVideoSelectedMonitors = ([]).concat(monitorIdsSelectedNow || [])
|
||||
}
|
||||
function downloadPlayingVideo(video){
|
||||
if(video.currentSrc){
|
||||
var filename = getFilenameFromUrl(video.currentSrc)
|
||||
downloadFile(video.currentSrc,filename)
|
||||
}
|
||||
}
|
||||
function downloadAllPlayingVideos(){
|
||||
getAllActiveVideosInSlots().each(function(n,video){
|
||||
downloadPlayingVideo(video)
|
||||
})
|
||||
}
|
||||
function openVideoPlayerTabFromViewer(el){
|
||||
var monitorId = el.attr('data-mid') || el.parents('[data-mid]').attr('data-mid')
|
||||
var video = getActiveVideoRow(monitorId)
|
||||
createVideoPlayerTab(video)
|
||||
}
|
||||
function downloadPlayingVideoTabFromViewer(el){
|
||||
var monitorId = el.attr('data-mid') || el.parents('[data-mid]').attr('data-mid')
|
||||
var video = getActiveVideoInSlot(monitorId)
|
||||
downloadPlayingVideo(video)
|
||||
}
|
||||
powerVideoMonitorsListElement.on('change','input',onPowerVideoSettingsChange);
|
||||
powerVideoVideoLimitElement.change(onPowerVideoSettingsChange);
|
||||
powerVideoEventLimitElement.change(onPowerVideoSettingsChange);
|
||||
powerVideoSet.change(onPowerVideoSettingsChange);
|
||||
powerVideoWindow
|
||||
.on('dblclick','.videoPlayer',function(){
|
||||
var el = $(this)
|
||||
$('.videoPlayer-detection-info').addClass('hide')
|
||||
fullScreenInit(this)
|
||||
})
|
||||
.on('click','[powerVideo-control]',function(){
|
||||
var el = $(this)
|
||||
var controlType = el.attr('powerVideo-control')
|
||||
switch(controlType){
|
||||
// single video affected
|
||||
case'downloadVideo':
|
||||
downloadPlayingVideoTabFromViewer(el)
|
||||
break;
|
||||
case'openVideoPlayer':
|
||||
openVideoPlayerTabFromViewer(el)
|
||||
break;
|
||||
// all videos affected
|
||||
case'downloadPlaying':
|
||||
downloadAllPlayingVideos()
|
||||
break;
|
||||
case'toggleMute':
|
||||
toggleMute()
|
||||
break;
|
||||
case'toggleZoom':
|
||||
toggleZoomAllSlots()
|
||||
break;
|
||||
case'playAll':
|
||||
powerVideoCanAutoPlay = true
|
||||
playAllSlots()
|
||||
break;
|
||||
case'pauseAll':
|
||||
powerVideoCanAutoPlay = false
|
||||
pauseAllSlots()
|
||||
break;
|
||||
case'playSpeedAll':
|
||||
var playSpeed = el.attr('data-speed')
|
||||
setPlaySpeedOnAllSlots(playSpeed)
|
||||
break;
|
||||
case'previousVideoAll':
|
||||
playAllSlots()
|
||||
previousVideoAllSlots()
|
||||
break;
|
||||
case'nextVideoAll':
|
||||
playAllSlots()
|
||||
nextVideoAllSlots()
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
addOnTabOpen('powerVideo', function () {
|
||||
drawMonitorsList()
|
||||
})
|
||||
addOnTabReopen('powerVideo', function () {
|
||||
if(powerVideoCanAutoPlay){
|
||||
powerVideoWindow.find('video').each(function(n,video){
|
||||
try{
|
||||
video.play()
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
addOnTabAway('powerVideo', function () {
|
||||
powerVideoWindow.find('video').each(function(n,video){
|
||||
console.log(video)
|
||||
try{
|
||||
video.pause()
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
// addOnTabReopen('powerVideo', function () {
|
||||
// drawMonitorsList()
|
||||
// })
|
||||
$.powerVideoViewer = {
|
||||
window: powerVideoWindow,
|
||||
drawMonitorsList: drawMonitorsList,
|
||||
activeTimeline: activeTimeline,
|
||||
monitorListElement: powerVideoMonitorsListElement,
|
||||
monitorViewsElement: powerVideoMonitorViewsElement,
|
||||
timelineStripsElement: powerVideoTimelineStripsContainer,
|
||||
dateRangeElement: dateSelector,
|
||||
loadedVideos: powerVideoLoadedVideos,
|
||||
loadedEvents: powerVideoLoadedEvents,
|
||||
loadedChartData: powerVideoLoadedChartData,
|
||||
loadedTableGroupIds: loadedTableGroupIds,
|
||||
extenders: extenders
|
||||
}
|
||||
})
|
|
@ -7,10 +7,22 @@ var sideMenuCollapsePoint = $('#side-menu-collapse-point')
|
|||
var floatingHideButton = $('#floating-hide-button')
|
||||
var floatingBackButton = $('#floating-back-button')
|
||||
function buildTabHtml(tabName,tabLabel,tabIcon){
|
||||
return `<li class="nav-item">
|
||||
<a class="nav-link side-menu-link" page-open="${tabName}">
|
||||
<i class="fa fa-${tabIcon ? tabIcon : 'file-o'}"></i> ${tabLabel} <span class="delete-tab align-text-bottom"><i class="fa fa-times"></i></span>
|
||||
</a>
|
||||
return `<li class="nav-link side-menu-link cursor-pointer" page-open="${tabName}">
|
||||
<div class="d-flex flex-row">
|
||||
<div class="d-flex pr-2">
|
||||
<span>
|
||||
<i class="fa fa-${tabIcon ? tabIcon : 'file-o'}"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-grow-1 pr-2">
|
||||
${tabLabel}
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<span class="delete-tab">
|
||||
<i class="fa fa-times-circle-o"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>`
|
||||
}
|
||||
function drawMonitorIconToMenu(item){
|
||||
|
|
|
@ -0,0 +1,970 @@
|
|||
$(document).ready(function(){
|
||||
var theWindow = $('#tab-timeline')
|
||||
var timeStripVideoCanvas = $('#timeline-video-canvas');
|
||||
var timeStripEl = $('#timeline-bottom-strip');
|
||||
var timeStripControls = $('#timeline-controls');
|
||||
var timeStripInfo = $('#timeline-info');
|
||||
var timeStripPreBuffers = $('#timeline-pre-buffers');
|
||||
var timeStripObjectSearchInput = $('#timeline-video-object-search');
|
||||
var dateSelector = $('#timeline-date-selector');
|
||||
var sideMenuList = $(`#side-menu-link-timeline ul`)
|
||||
var playToggles = timeStripControls.find('[timeline-action="playpause"]')
|
||||
var speedButtons = timeStripControls.find('[timeline-action="speed"]')
|
||||
var gridSizeButtons = timeStripControls.find('[timeline-action="gridSize"]')
|
||||
var autoGridSizerButtons = timeStripControls.find('[timeline-action="autoGridSizer"]')
|
||||
var playUntilVideoEndButtons = timeStripControls.find('[timeline-action="playUntilVideoEnd"]')
|
||||
var currentTimeLabel = timeStripInfo.find('.current-time')
|
||||
var timelineActionButtons = timeStripControls.find('[timeline-action]')
|
||||
var timelineSpeed = 1;
|
||||
var timelineGridSizing = `md-6`;
|
||||
var timeStripVis = null;
|
||||
var timeStripVisTick = null;
|
||||
var timeStripVisItems = null;
|
||||
var timeStripCurrentStart = null;
|
||||
var timeStripCurrentEnd = null;
|
||||
var timeStripVisTickMovementInterval = null;
|
||||
var timeStripVisTickMovementIntervalSecond = null;
|
||||
var timeStripHollowClickQueue = {}
|
||||
var timeStripTickPosition = new Date()
|
||||
var timeStripPreBuffersEls = {}
|
||||
var timeStripItemColors = {}
|
||||
var timeStripAutoGridSizer = false
|
||||
var timeStripListOfQueries = []
|
||||
var timeStripSelectedMonitors = []
|
||||
var timeStripAutoScrollTimeout = null;
|
||||
var timeStripAutoScrollPositionStart = null;
|
||||
var timeStripAutoScrollPositionEnd = null;
|
||||
var timeStripAutoScrollAmount = null;
|
||||
var timeStripItemIncrement = 0;
|
||||
var loadedVideosOnTimeStrip = []
|
||||
var loadedVideosOnCanvas = {}
|
||||
var loadedVideoElsOnCanvas = {}
|
||||
var loadedVideoElsOnCanvasNextVideoTimeout = {}
|
||||
var loadedVideoEndingTimeouts = {}
|
||||
var playUntilVideoEnd = false
|
||||
var dontShowDetectionOnTimeline = false
|
||||
var isPlaying = false
|
||||
var earliestStart = null
|
||||
var latestEnd = null
|
||||
var timeChanging = false
|
||||
var dateRangeChanging = false
|
||||
function setLoadingMask(turnOn){
|
||||
if(turnOn){
|
||||
if(theWindow.find('.loading-mask').length === 0){
|
||||
var html = `<div class="loading-mask"><i class="fa fa-spinner fa-pulse fa-5x"></i></div>`
|
||||
theWindow.prepend(html)
|
||||
}
|
||||
}else{
|
||||
theWindow.find('.loading-mask').remove()
|
||||
}
|
||||
|
||||
}
|
||||
function addVideoBeforeAndAfter(videos) {
|
||||
videos.sort((a, b) => {
|
||||
if (a.mid === b.mid) {
|
||||
return new Date(a.time) - new Date(b.time);
|
||||
}
|
||||
return a.mid.localeCompare(b.mid);
|
||||
});
|
||||
for (let i = 0; i < videos.length; i++) {
|
||||
if (i > 0 && videos[i].mid === videos[i - 1].mid) {
|
||||
videos[i].videoBefore = videos[i - 1];
|
||||
} else {
|
||||
videos[i].videoBefore = null;
|
||||
}
|
||||
if (i < videos.length - 1 && videos[i].mid === videos[i + 1].mid) {
|
||||
videos[i].videoAfter = videos[i + 1];
|
||||
} else {
|
||||
videos[i].videoAfter = null;
|
||||
}
|
||||
}
|
||||
return videos;
|
||||
}
|
||||
function findGapsInSearchRanges(timeRanges, range) {
|
||||
timeRanges.sort((a, b) => a[0] - b[0]);
|
||||
let gaps = [];
|
||||
let currentEnd = new Date(range[0]);
|
||||
for (let i = 0; i < timeRanges.length; i++) {
|
||||
let [start, end] = timeRanges[i];
|
||||
if (start > currentEnd) {
|
||||
gaps.push([currentEnd, start]);
|
||||
}
|
||||
if (end > currentEnd) {
|
||||
currentEnd = end;
|
||||
}
|
||||
}
|
||||
if (currentEnd < range[1]) {
|
||||
gaps.push([currentEnd, range[1]]);
|
||||
}
|
||||
return gaps;
|
||||
}
|
||||
async function getVideosInGaps(gaps,monitorIds){
|
||||
var searchQuery = timeStripObjectSearchInput.val()
|
||||
var videos = []
|
||||
var eventLimit = Object.values(loadedMonitors).length * 300
|
||||
async function loopOnGaps(monitorId){
|
||||
for (let i = 0; i < gaps.length; i++) {
|
||||
var range = gaps[i]
|
||||
videos.push(...(await getVideos({
|
||||
monitorId,
|
||||
startDate: range[0],
|
||||
endDate: range[1],
|
||||
eventLimit,
|
||||
searchQuery,
|
||||
// archived: false,
|
||||
// customVideoSet: wantCloudVideo ? 'cloudVideos' : null,
|
||||
},null,dontShowDetectionOnTimeline)).videos);
|
||||
}
|
||||
}
|
||||
if(monitorIds && monitorIds.length > 0){
|
||||
for (let ii = 0; ii < monitorIds.length; ii++) {
|
||||
var monitorId = monitorIds[ii]
|
||||
await loopOnGaps(monitorId)
|
||||
}
|
||||
}else{
|
||||
await loopOnGaps('')
|
||||
}
|
||||
return videos;
|
||||
}
|
||||
async function getVideosByTimeStripRange(addOrOverWrite){
|
||||
// timeStripSelectedMonitors = selected monitors
|
||||
var currentVideosLength = parseInt(loadedVideosOnTimeStrip.length)
|
||||
var stripDate = getTimestripDate()
|
||||
var startDate = stripDate.start
|
||||
var endDate = stripDate.end
|
||||
var gaps = findGapsInSearchRanges(timeStripListOfQueries, [startDate,endDate])
|
||||
// console.error([startDate,endDate])
|
||||
// console.log('range : ',JSON.stringify([startDate,endDate]))
|
||||
// console.log('timeRanges : ',JSON.stringify(timeStripListOfQueries))
|
||||
// console.log('gaps : ',JSON.stringify(gaps))
|
||||
if(gaps.length > 0){
|
||||
setLoadingMask(true)
|
||||
timeStripListOfQueries.push(...gaps)
|
||||
var videos = await getVideosInGaps(gaps,timeStripSelectedMonitors)
|
||||
videos = addVideoBeforeAndAfter(videos)
|
||||
loadedVideosOnTimeStrip.push(...videos)
|
||||
if(currentVideosLength !== loadedVideosOnTimeStrip.length)addTimelineItems(loadedVideosOnTimeStrip);
|
||||
setLoadingMask(false)
|
||||
}
|
||||
return loadedVideosOnTimeStrip
|
||||
}
|
||||
function selectVideosForCanvas(time, videos){
|
||||
var selectedVideosByMonitorId = {}
|
||||
$.each(loadedMonitors,function(n,monitor){
|
||||
selectedVideosByMonitorId[monitor.mid] = null
|
||||
})
|
||||
var filteredVideos = videos.filter(video => {
|
||||
var startTime = new Date(video.time);
|
||||
var endTime = new Date(video.end);
|
||||
return time >= startTime && time <= endTime;
|
||||
});
|
||||
$.each(filteredVideos,function(n,video){
|
||||
selectedVideosByMonitorId[video.mid] = video;
|
||||
})
|
||||
return selectedVideosByMonitorId;
|
||||
}
|
||||
function drawVideosToCanvas(selectedVideosByMonitorId){
|
||||
var html = ''
|
||||
var preBufferHtml = ''
|
||||
$.each(loadedMonitors,function(monitorId,monitor){
|
||||
var itemColor = timeStripItemColors[monitorId];
|
||||
html += `<div class="timeline-video open-video col-${timelineGridSizing} p-0 m-0 no-video" data-mid="${monitorId}" data-ke="${monitor.ke}" style="background-color:${itemColor}">
|
||||
<div class="film"></div>
|
||||
<div class="event-objects"></div>
|
||||
</div>`
|
||||
preBufferHtml += `<div class="timeline-video-buffer" data-mid="${monitorId}" data-ke="${monitor.ke}"></div>`
|
||||
})
|
||||
timeStripVideoCanvas.html(html)
|
||||
timeStripPreBuffers.html(preBufferHtml)
|
||||
$.each(selectedVideosByMonitorId,function(monitorId,video){
|
||||
if(!video)return;
|
||||
setVideoInCanvas(video)
|
||||
})
|
||||
}
|
||||
function destroyTimeline(){
|
||||
try{
|
||||
timeStripVis.destroy()
|
||||
}catch(err){
|
||||
// console.log(err)
|
||||
}
|
||||
}
|
||||
function formatVideosForTimeline(videos){
|
||||
var formattedVideos = (videos || []).map((item) => {
|
||||
var blockColor = timeStripItemColors[item.mid];
|
||||
++timeStripItemIncrement;
|
||||
return {
|
||||
id: timeStripItemIncrement,
|
||||
content: ``,
|
||||
style: `background-color: ${blockColor};border-color: ${blockColor}`,
|
||||
start: item.time,
|
||||
end: item.end,
|
||||
group: 1
|
||||
}
|
||||
});
|
||||
return formattedVideos
|
||||
}
|
||||
function createTimelineItems(){
|
||||
var items = new vis.DataSet([]);
|
||||
var groups = new vis.DataSet([
|
||||
{id: 1, content: ''}
|
||||
]);
|
||||
timeStripVisItems = items
|
||||
return {
|
||||
items,
|
||||
groups,
|
||||
}
|
||||
}
|
||||
function resetTimelineItems(videos){
|
||||
var newVideos = formatVideosForTimeline(videos)
|
||||
timeStripVisItems.clear();
|
||||
timeStripVisItems.add(newVideos);
|
||||
}
|
||||
function addTimelineItems(videos){
|
||||
var newVideos = formatVideosForTimeline(videos)
|
||||
timeStripVisItems.add(newVideos);
|
||||
}
|
||||
async function resetTimeline(clickTime){
|
||||
await getAndDrawVideosToTimeline(clickTime,true)
|
||||
setTickDate(clickTime)
|
||||
setTimeLabel(clickTime)
|
||||
setTimeOfCanvasVideos(clickTime)
|
||||
setHollowClickQueue()
|
||||
}
|
||||
function timeStripActionWithPausePlay(restartPlaySpeed){
|
||||
return new Promise((resolve,reject) => {
|
||||
var currentlyPlaying = !!isPlaying;
|
||||
timeStripPlay(true)
|
||||
resolve(timeChanging)
|
||||
if(currentlyPlaying){
|
||||
setTimeout(() => {
|
||||
timeStripPlay()
|
||||
},restartPlaySpeed || 500)
|
||||
}
|
||||
})
|
||||
}
|
||||
function createTimeline(){
|
||||
timeStripItemIncrement = 0;
|
||||
var timeChangingTimeout = null
|
||||
var dateNow = new Date()
|
||||
var hour = 1000 * 60 * 60
|
||||
var startTimeForLoad = new Date(dateNow.getTime() - (hour * 24 + hour))
|
||||
destroyTimeline()
|
||||
var {
|
||||
items,
|
||||
groups,
|
||||
} = createTimelineItems()
|
||||
// make chart
|
||||
timeStripVis = new vis.Timeline(timeStripEl[0], items, groups, {
|
||||
showCurrentTime: false,
|
||||
stack: false,
|
||||
start: timeStripCurrentStart || startTimeForLoad,
|
||||
end: timeStripCurrentEnd || dateNow,
|
||||
});
|
||||
// make tick
|
||||
timeStripVisTick = timeStripVis.addCustomTime(dateNow, `${lang.Time}`);
|
||||
timeStripVis.on('click', async function(properties) {
|
||||
var clickTime = properties.time;
|
||||
timeStripActionWithPausePlay().then((timeChanging) => {
|
||||
if(!timeChanging){
|
||||
resetTimeline(clickTime)
|
||||
}
|
||||
})
|
||||
});
|
||||
timeStripVis.on('rangechange', function(properties){
|
||||
timeChanging = true
|
||||
})
|
||||
timeStripVis.on('rangechanged', function(properties){
|
||||
clearTimeout(timeChangingTimeout)
|
||||
timeStripCurrentStart = properties.start;
|
||||
timeStripCurrentEnd = properties.end;
|
||||
timeStripAutoScrollPositionStart = getTimeBetween(timeStripCurrentStart,timeStripCurrentEnd,10);
|
||||
timeStripAutoScrollPositionEnd = getTimeBetween(timeStripCurrentStart,timeStripCurrentEnd,90);
|
||||
timeStripAutoScrollAmount = getTimelineScrollAmount(timeStripCurrentStart,timeStripCurrentEnd);
|
||||
if(dateRangeChanging)return;
|
||||
timeChangingTimeout = setTimeout(function(){
|
||||
var clickTime = properties.time;
|
||||
resetDateRangePicker()
|
||||
setTimeout(() => {
|
||||
timeChanging = false
|
||||
getAndDrawVideosToTimeline(clickTime)
|
||||
},500)
|
||||
},300)
|
||||
})
|
||||
setTimeout(function(){
|
||||
timeStripEl.find('.vis-timeline').resize()
|
||||
},2000)
|
||||
}
|
||||
function getTimelineScrollAmount(start,end){
|
||||
var startTime = start.getTime()
|
||||
var endTime = end.getTime()
|
||||
var difference = (endTime - startTime) / 1000;
|
||||
var minute = 60
|
||||
var hour = 60 * 60
|
||||
var day = 60 * 60 * 24
|
||||
// returns hours
|
||||
if(difference <= 60){
|
||||
return 0.1
|
||||
}else if(difference > minute && difference <= hour){
|
||||
return 0.1
|
||||
}else if(difference > hour && difference < day){
|
||||
return 0.3
|
||||
}else if(difference >= day){
|
||||
return 0.9
|
||||
}
|
||||
}
|
||||
function scrollTimeline(addHours){
|
||||
if(timeStripAutoScrollTimeout)return;
|
||||
timeStripAutoScrollTimeout = setTimeout(() => {
|
||||
delete(timeStripAutoScrollTimeout)
|
||||
timeStripAutoScrollTimeout = null
|
||||
},2000)
|
||||
var stripTime = getTimestripDate()
|
||||
var timeToAdd = addHours * 1000 * 60 * 60
|
||||
var start = new Date(stripTime.start.getTime() + timeToAdd)
|
||||
var end = new Date(stripTime.end.getTime() + timeToAdd)
|
||||
setTimestripDate(start,end)
|
||||
}
|
||||
function scrollTimelineToTime(tickTime) {
|
||||
var stripTime = getTimestripDate();
|
||||
var halfRange = (stripTime.end.getTime() - stripTime.start.getTime()) / 2;
|
||||
var start = new Date(tickTime.getTime() - halfRange);
|
||||
var end = new Date(tickTime.getTime() + halfRange);
|
||||
setTimestripDate(start, end);
|
||||
}
|
||||
function setTickDate(newDate){
|
||||
if(isPlaying){
|
||||
if(newDate >= timeStripAutoScrollPositionEnd){
|
||||
scrollTimeline(timeStripAutoScrollAmount)
|
||||
}else if(newDate >= new Date()){
|
||||
timeStripPlay(true)
|
||||
}
|
||||
}
|
||||
timeStripTickPosition = new Date(newDate)
|
||||
return timeStripVis.setCustomTime(newDate, timeStripVisTick);
|
||||
}
|
||||
function setTimeLabel(newDate){
|
||||
return currentTimeLabel.text(`${timeAgo(newDate)}, ${getDayOfWeek(newDate)}, ${formattedTime(newDate)}`)
|
||||
}
|
||||
function getTickDate() {
|
||||
return timeStripTickPosition;
|
||||
}
|
||||
function getTimestripDate() {
|
||||
var visibleWindow = timeStripVis.getWindow();
|
||||
var start = visibleWindow.start;
|
||||
var end = visibleWindow.end;
|
||||
return {
|
||||
start,
|
||||
end
|
||||
};
|
||||
}
|
||||
function setTimestripDate(newStart, newEnd) {
|
||||
return timeStripVis.setWindow(newStart, newEnd);
|
||||
}
|
||||
function selectAndDrawVideosToCanvas(theTime,redrawVideos){
|
||||
var selectedVideosForTime = selectVideosForCanvas(theTime,loadedVideosOnTimeStrip)
|
||||
loadedVideosOnCanvas = selectedVideosForTime;
|
||||
if(redrawVideos){
|
||||
drawVideosToCanvas(selectedVideosForTime)
|
||||
}
|
||||
}
|
||||
async function getAndDrawVideosToTimeline(theTime,redrawVideos){
|
||||
await getVideosByTimeStripRange()
|
||||
selectAndDrawVideosToCanvas(theTime,redrawVideos)
|
||||
}
|
||||
function getVideoContainerInCanvas(video){
|
||||
return timeStripVideoCanvas.find(`[data-mid="${video.mid}"][data-ke="${video.ke}"]`)
|
||||
}
|
||||
function getVideoFilmInCanvas(video){
|
||||
return getVideoContainerInCanvas(video).find('.film')
|
||||
}
|
||||
function getVideoElInCanvas(video){
|
||||
return getVideoContainerInCanvas(video).find('video')[0]
|
||||
}
|
||||
function getObjectContainerInCanvas(video){
|
||||
return getVideoContainerInCanvas(video).find('.event-objects')
|
||||
}
|
||||
function getVideoContainerPreBufferEl(video){
|
||||
return timeStripPreBuffers.find(`[data-mid="${video.mid}"][data-ke="${video.ke}"]`)
|
||||
}
|
||||
function getWaitTimeUntilNextVideo(endTimeOfFirstVideo,startTimeOfNextVideo){
|
||||
return (new Date(startTimeOfNextVideo).getTime() - new Date(endTimeOfFirstVideo).getTime()) / timelineSpeed
|
||||
}
|
||||
function jumpNextVideoIfEmptyCanvas(){
|
||||
if(isPlaying && hasNoCanvasVideos()){
|
||||
jumpToNextVideo()
|
||||
}
|
||||
}
|
||||
function clearVideoInCanvas(oldVideo){
|
||||
var monitorId = oldVideo.mid
|
||||
loadedVideosOnCanvas[monitorId] = null
|
||||
loadedVideoElsOnCanvas[monitorId] = null
|
||||
clearTimeout(loadedVideoEndingTimeouts[monitorId])
|
||||
var container = getVideoContainerInCanvas(oldVideo).addClass('no-video').find('.film')
|
||||
var videoEl = container.find('video')
|
||||
videoEl.attr('src','')
|
||||
try{
|
||||
videoEl[0].pause()
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
}
|
||||
container.empty()
|
||||
timeStripAutoGridResize()
|
||||
if(playUntilVideoEnd){
|
||||
timeStripPlay(true)
|
||||
}else{
|
||||
jumpNextVideoIfEmptyCanvas()
|
||||
}
|
||||
}
|
||||
function setVideoInCanvas(newVideo){
|
||||
var monitorId = newVideo.mid
|
||||
var container = getVideoContainerInCanvas(newVideo)
|
||||
.removeClass('no-video').find('.film').html(`<video muted src="${newVideo.href}"></video>`)
|
||||
var vidEl = getVideoElInCanvas(newVideo)
|
||||
var objectContainer = getObjectContainerInCanvas(newVideo)
|
||||
vidEl.playbackRate = timelineSpeed
|
||||
if(isPlaying)playVideo(vidEl)
|
||||
loadedVideoElsOnCanvas[monitorId] = {
|
||||
vidEl,
|
||||
container,
|
||||
objectContainer,
|
||||
}
|
||||
loadedVideosOnCanvas[monitorId] = newVideo
|
||||
timeStripPreBuffersEls[monitorId] = getVideoContainerPreBufferEl(newVideo)
|
||||
queueNextVideo(newVideo)
|
||||
timeStripAutoGridResize()
|
||||
}
|
||||
function setTimeOfCanvasVideos(newTime){
|
||||
$.each(loadedVideosOnCanvas,function(n,video){
|
||||
if(!video)return;
|
||||
var monitorId = video.mid
|
||||
var timeAfterStart = (newTime - new Date(video.time)) / 1000;
|
||||
var videoEl = loadedVideoElsOnCanvas[monitorId].vidEl
|
||||
videoEl.currentTime = timeAfterStart
|
||||
// playVideo(videoEl)
|
||||
// pauseVideo(videoEl)
|
||||
})
|
||||
}
|
||||
function hasNoCanvasVideos(){
|
||||
return getLoadedVideosOnCanvas().length === 0;
|
||||
}
|
||||
function prepareEventsList(events){
|
||||
var newEvents = {}
|
||||
events.forEach((item) => {
|
||||
newEvents[new Date(item.time)] = item
|
||||
})
|
||||
return newEvents
|
||||
}
|
||||
function respaceObjectContainer(parentConatiner,objectContainer,videoWidth,videoHeight){
|
||||
var parentWidth = parentConatiner.width()
|
||||
var spaceWidth = (parentWidth - videoWidth) / 2
|
||||
objectContainer.width(videoWidth).css('left',`${spaceWidth}px`)
|
||||
}
|
||||
function queueNextVideo(video){
|
||||
if(!video)return;
|
||||
var monitorId = video.mid
|
||||
var onCanvas = loadedVideoElsOnCanvas[monitorId]
|
||||
var videoEl = onCanvas.vidEl
|
||||
var videoAfter = video.videoAfter
|
||||
var endingTimeout = null;
|
||||
var alreadyDone = false;
|
||||
var videoStartTime = (new Date(video.time).getTime() / 1000)
|
||||
var container = onCanvas.container
|
||||
var objectContainer = onCanvas.objectContainer
|
||||
var videoHeight = 0
|
||||
var videoWidth = 0
|
||||
var videoEvents = prepareEventsList(video.events)
|
||||
function currentVideoIsOver(){
|
||||
if(alreadyDone)return;
|
||||
alreadyDone = true;
|
||||
clearVideoInCanvas(video)
|
||||
if(videoAfter){
|
||||
var waitTimeTimeTillNext = getWaitTimeUntilNextVideo(video.end,videoAfter.time)
|
||||
// console.log('End of Video',video)
|
||||
// console.log('Video After',videoAfter)
|
||||
// console.log('Starting in ',waitTimeTimeTillNext / 1000, 'seconds')
|
||||
loadedVideoElsOnCanvasNextVideoTimeout[monitorId] = setTimeout(() => {
|
||||
setVideoInCanvas(videoAfter)
|
||||
},waitTimeTimeTillNext)
|
||||
// }else{
|
||||
// console.log('End of Timeline for Monitor',loadedMonitors[monitorId].name)
|
||||
}
|
||||
}
|
||||
function drawMatricesOnVideoTimeUpdate(){
|
||||
var eventTime = new Date((videoStartTime + videoEl.currentTime) * 1000)
|
||||
var theEvent = videoEvents[eventTime]
|
||||
if(theEvent){
|
||||
drawMatrices(theEvent,{
|
||||
theContainer: objectContainer,
|
||||
height: videoHeight,
|
||||
width: videoWidth,
|
||||
})
|
||||
}else{
|
||||
objectContainer.find('.stream-detected-object').remove()
|
||||
}
|
||||
}
|
||||
videoEl.onerror = function(err){
|
||||
err.preventDefault()
|
||||
console.error(`video error`)
|
||||
console.error(err)
|
||||
}
|
||||
videoEl.ontimeupdate = function(){
|
||||
if(videoEl.currentTime >= videoEl.duration){
|
||||
clearTimeout(loadedVideoEndingTimeouts[monitorId])
|
||||
currentVideoIsOver()
|
||||
}else if(isPlaying){
|
||||
var theTime = getTickDate()
|
||||
var waitTimeTimeTillNext = getWaitTimeUntilNextVideo(theTime,video.end)
|
||||
clearTimeout(loadedVideoEndingTimeouts[monitorId])
|
||||
loadedVideoEndingTimeouts[monitorId] = setTimeout(() => {
|
||||
currentVideoIsOver()
|
||||
},waitTimeTimeTillNext)
|
||||
}
|
||||
drawMatricesOnVideoTimeUpdate()
|
||||
}
|
||||
videoEl.oncanplay = function() {
|
||||
var dims = getDisplayDimensions(videoEl);
|
||||
videoWidth = dims.videoWidth
|
||||
videoHeight = dims.videoHeight
|
||||
respaceObjectContainer(container,objectContainer,videoWidth,videoHeight)
|
||||
}
|
||||
// pre-buffer it
|
||||
timeStripPreBuffersEls[monitorId].html(videoAfter ? `<video preload="auto" muted src="${videoAfter.href}"></video>` : '')
|
||||
}
|
||||
function findVideoAfterTime(time, monitorId) {
|
||||
let inputTime = new Date(time);
|
||||
let matchingVideos = loadedVideosOnTimeStrip.filter(video => {
|
||||
let videoTime = new Date(video.time);
|
||||
return video.mid === monitorId && videoTime > inputTime;
|
||||
});
|
||||
matchingVideos.sort((a, b) => new Date(a.time) - new Date(b.time));
|
||||
return matchingVideos.length > 0 ? matchingVideos[0] : null;
|
||||
}
|
||||
function setHollowClickQueue(){
|
||||
$.each(loadedVideosOnCanvas,function(monitorId,video){
|
||||
if(!video){
|
||||
// console.log(`Add Hollow Action`, loadedMonitors[monitorId].name)
|
||||
var tickTime = getTickDate()
|
||||
var foundVideo = findVideoAfterTime(tickTime,monitorId)
|
||||
clearTimeout(loadedVideoElsOnCanvasNextVideoTimeout[monitorId])
|
||||
if(foundVideo){
|
||||
var waitTimeTimeTillNext = getWaitTimeUntilNextVideo(tickTime,foundVideo.time)
|
||||
// console.log('Found Video',foundVideo)
|
||||
// console.log('Video Starts in ',waitTimeTimeTillNext / 1000, 'seconds after Play')
|
||||
timeStripHollowClickQueue[monitorId] = () => {
|
||||
// console.log('Hollow Start Point for',loadedMonitors[monitorId].name)
|
||||
loadedVideoElsOnCanvasNextVideoTimeout[monitorId] = setTimeout(() => {
|
||||
// console.log('Hollow Replace')
|
||||
setVideoInCanvas(foundVideo)
|
||||
},waitTimeTimeTillNext)
|
||||
}
|
||||
}else{
|
||||
// console.log('End of Timeline for Monitor',loadedMonitors[monitorId].name)
|
||||
timeStripHollowClickQueue[monitorId] = () => {}
|
||||
}
|
||||
}else{
|
||||
timeStripHollowClickQueue[monitorId] = () => {}
|
||||
}
|
||||
})
|
||||
}
|
||||
function runHollowClickQueues(){
|
||||
$.each(timeStripHollowClickQueue,function(monitorId,theAction){
|
||||
theAction()
|
||||
})
|
||||
}
|
||||
function getAllActiveVideosInSlots(){
|
||||
return timeStripVideoCanvas.find('video')
|
||||
}
|
||||
function playVideo(videoEl){
|
||||
try{
|
||||
videoEl.playbackRate = timelineSpeed
|
||||
videoEl.play()
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
function pauseVideo(videoEl){
|
||||
try{
|
||||
videoEl.pause()
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
function playAllVideos(){
|
||||
getAllActiveVideosInSlots().each(function(n,video){
|
||||
playVideo(video)
|
||||
})
|
||||
}
|
||||
function pauseAllVideos(){
|
||||
getAllActiveVideosInSlots().each(function(n,video){
|
||||
pauseVideo(video)
|
||||
})
|
||||
}
|
||||
function setPlayToggleUI(icon){
|
||||
playToggles.html(`<i class="fa fa-${icon}"></i>`)
|
||||
}
|
||||
function timeStripPlay(forcePause){
|
||||
if(!forcePause && !isPlaying){
|
||||
isPlaying = true
|
||||
var currentDate = getTickDate().getTime();
|
||||
var msSpeed = 50
|
||||
var addition = 0
|
||||
var newTime
|
||||
runHollowClickQueues()
|
||||
playAllVideos()
|
||||
timeStripVisTickMovementInterval = setInterval(function() {
|
||||
addition += (msSpeed * timelineSpeed);
|
||||
newTime = new Date(currentDate + addition)
|
||||
setTickDate(newTime);
|
||||
// setTimeOfCanvasVideos(newTime)
|
||||
}, msSpeed)
|
||||
timeStripVisTickMovementIntervalSecond = setInterval(function() {
|
||||
setTimeLabel(newTime);
|
||||
}, 1000)
|
||||
setPlayToggleUI(`pause-circle-o`)
|
||||
jumpNextVideoIfEmptyCanvas()
|
||||
}else{
|
||||
isPlaying = false
|
||||
pauseAllVideos()
|
||||
clearInterval(timeStripVisTickMovementInterval)
|
||||
clearInterval(timeStripVisTickMovementIntervalSecond)
|
||||
$.each(loadedVideoElsOnCanvasNextVideoTimeout,function(n,timeout){
|
||||
clearTimeout(timeout)
|
||||
})
|
||||
$.each(loadedVideoEndingTimeouts,function(n,timeout){
|
||||
clearTimeout(timeout)
|
||||
})
|
||||
setPlayToggleUI(`play-circle-o`)
|
||||
}
|
||||
}
|
||||
// function downloadPlayingVideo(video){
|
||||
// if(video.currentSrc){
|
||||
// var filename = getFilenameFromUrl(video.currentSrc)
|
||||
// downloadFile(video.currentSrc,filename)
|
||||
// }
|
||||
// }
|
||||
function getLoadedVideosOnCanvas(){
|
||||
return Object.values(loadedVideosOnCanvas).filter(item => !!item)
|
||||
}
|
||||
function downloadAllPlayingVideos(){
|
||||
zipVideosAndDownloadWithConfirm(getLoadedVideosOnCanvas())
|
||||
}
|
||||
async function jumpTimeline(amountInMs,direction){
|
||||
timeStripPlay(true)
|
||||
var tickTime = getTickDate().getTime()
|
||||
var newTime = 0;
|
||||
if(direction === 'right'){
|
||||
newTime = tickTime + amountInMs
|
||||
}else{
|
||||
newTime = tickTime - amountInMs
|
||||
}
|
||||
newTime = new Date(newTime)
|
||||
await resetTimeline(newTime)
|
||||
checkScroll(tickTime)
|
||||
}
|
||||
function checkScroll(tickTime,scrollToTick){
|
||||
if(tickTime <= timeStripAutoScrollPositionStart){
|
||||
if(scrollToTick){
|
||||
scrollTimelineToTime(tickTime)
|
||||
}else{
|
||||
scrollTimeline(-timeStripAutoScrollAmount)
|
||||
}
|
||||
}else if(tickTime >= timeStripAutoScrollPositionEnd){
|
||||
if(scrollToTick){
|
||||
scrollTimelineToTime(tickTime)
|
||||
}else{
|
||||
scrollTimeline(timeStripAutoScrollAmount)
|
||||
}
|
||||
}
|
||||
}
|
||||
function adjustTimelineSpeed(newSpeed){
|
||||
var currentlyPlaying = !!isPlaying;
|
||||
if(currentlyPlaying)timeStripPlay(true);
|
||||
timelineSpeed = newSpeed + 0
|
||||
setHollowClickQueue()
|
||||
if(currentlyPlaying)timeStripPlay();
|
||||
}
|
||||
function adjustTimelineGridSize(newCol){
|
||||
timelineGridSizing = `${newCol}`
|
||||
var containerEls = timeStripVideoCanvas.find('.timeline-video')
|
||||
containerEls.removeClass (function (index, className) {
|
||||
return (className.match (/(^|\s)col-\S+/g) || []).join(' ');
|
||||
}).addClass(`col-${newCol}`)
|
||||
gridSizeButtons.removeClass('btn-success')
|
||||
timeStripControls.find(`[timeline-action="gridSize"][size="${newCol}"]`).addClass('btn-success')
|
||||
}
|
||||
async function refreshTimeline(){
|
||||
var currentlyPlaying = !!isPlaying;
|
||||
timeStripPlay(true)
|
||||
timeStripListOfQueries = []
|
||||
loadedVideosOnTimeStrip = []
|
||||
createTimeline()
|
||||
await resetTimeline(getTickDate())
|
||||
if(currentlyPlaying)timeStripPlay();
|
||||
}
|
||||
function timeStripAutoGridSizerToggle(){
|
||||
if(timeStripAutoGridSizer){
|
||||
timeStripAutoGridSizer = false
|
||||
autoGridSizerButtons.removeClass('btn-success')
|
||||
dashboardOptions('timeStripAutoGridSizer','2')
|
||||
}else{
|
||||
timeStripAutoGridSizer = true
|
||||
autoGridSizerButtons.addClass('btn-success')
|
||||
timeStripAutoGridResize()
|
||||
dashboardOptions('timeStripAutoGridSizer','1')
|
||||
}
|
||||
}
|
||||
function timeStripPlayUntilVideoEndToggle(){
|
||||
if(playUntilVideoEnd){
|
||||
playUntilVideoEnd = false
|
||||
playUntilVideoEndButtons.removeClass('btn-success')
|
||||
dashboardOptions('timeStripPlayUntilVideoEnd','2')
|
||||
}else{
|
||||
playUntilVideoEnd = true
|
||||
playUntilVideoEndButtons.addClass('btn-success')
|
||||
dashboardOptions('timeStripPlayUntilVideoEnd','1')
|
||||
}
|
||||
}
|
||||
function timeStripDontShowDetectionToggle(){
|
||||
var theButtons = timeStripControls.find('[timeline-action="dontShowDetection"]')
|
||||
if(dontShowDetectionOnTimeline){
|
||||
dontShowDetectionOnTimeline = false
|
||||
theButtons.removeClass('btn-warning')
|
||||
dashboardOptions('dontShowDetectionOnTimeline','2')
|
||||
}else{
|
||||
dontShowDetectionOnTimeline = true
|
||||
theButtons.addClass('btn-warning')
|
||||
dashboardOptions('dontShowDetectionOnTimeline','1')
|
||||
}
|
||||
refreshTimeline()
|
||||
}
|
||||
function timeStripAutoGridResize(){
|
||||
if(!timeStripAutoGridSizer)return;
|
||||
var numberOfBlocks = timeStripVideoCanvas.find('.timeline-video:visible').length
|
||||
if(numberOfBlocks <= 1){
|
||||
adjustTimelineGridSize(`md-12`)
|
||||
}else if(numberOfBlocks >= 2 && numberOfBlocks < 5){
|
||||
adjustTimelineGridSize(`md-6`)
|
||||
}else if(numberOfBlocks >= 5){
|
||||
adjustTimelineGridSize(`md-4`)
|
||||
}
|
||||
}
|
||||
function resetDateRangePicker(){
|
||||
var stripDate = getTimestripDate()
|
||||
var startDate = stripDate.start
|
||||
var endDate = stripDate.end
|
||||
var picker = dateSelector.data('daterangepicker')
|
||||
picker.setStartDate(startDate);
|
||||
picker.setEndDate(endDate);
|
||||
}
|
||||
function drawFoundCamerasSubMenu(){
|
||||
var tags = getListOfTagsFromMonitors()
|
||||
var allFound = [
|
||||
{
|
||||
attributes: `timeline-menu-action="selectMonitorGroup" tag=""`,
|
||||
class: `cursor-pointer`,
|
||||
color: 'green',
|
||||
label: lang['All Monitors'],
|
||||
}
|
||||
]
|
||||
$.each(tags,function(tag,monitors){
|
||||
allFound.push({
|
||||
attributes: `timeline-menu-action="selectMonitorGroup" tag="${tag}"`,
|
||||
class: `cursor-pointer`,
|
||||
color: 'blue',
|
||||
label: tag,
|
||||
})
|
||||
})
|
||||
if(allFound.length === 1){
|
||||
allFound.push({
|
||||
attributes: ``,
|
||||
class: ``,
|
||||
color: ' d-none',
|
||||
label: `<small class="mt-1">${lang.addTagText}</small>`,
|
||||
})
|
||||
}
|
||||
var html = buildSubMenuItems(allFound)
|
||||
sideMenuList.html(html)
|
||||
}
|
||||
function setColorReferences(){
|
||||
$.each(loadedMonitors,function(monitorId,monitor){
|
||||
var itemColor = stringToColor(monitorId)
|
||||
timeStripItemColors[monitorId] = itemColor
|
||||
})
|
||||
}
|
||||
function findEndingLatestInCanvas(){
|
||||
var foundVideo = {time: 0};
|
||||
$.each(loadedVideosOnCanvas,function(monitorId,video){
|
||||
if(!video)return;
|
||||
var videoTime = new Date(video.time).getTime()
|
||||
if(new Date(foundVideo.time).getTime() < videoTime){
|
||||
foundVideo = video;
|
||||
}
|
||||
})
|
||||
if(!foundVideo.mid)return null;
|
||||
return foundVideo
|
||||
}
|
||||
function findCurrentVideoIndex(video){
|
||||
var currentVideoIndex = loadedVideosOnTimeStrip.findIndex((item) => {
|
||||
return video.mid === item.mid && video.time == item.time
|
||||
});
|
||||
return currentVideoIndex
|
||||
}
|
||||
function findNextVideo(){
|
||||
var tickTime = getTickDate()
|
||||
let closestObject = [...loadedVideosOnTimeStrip]
|
||||
.sort((a, b) => new Date(a.time) - new Date(b.time))
|
||||
.find(obj => new Date(obj.time) > tickTime);
|
||||
return closestObject;
|
||||
}
|
||||
function findPreviousVideo(){
|
||||
var tickTime = getTickDate()
|
||||
let closestObject = [...loadedVideosOnTimeStrip]
|
||||
.sort((a, b) => new Date(b.time) - new Date(a.time))
|
||||
.find(obj => new Date(obj.time) < tickTime);
|
||||
return closestObject;
|
||||
}
|
||||
async function jumpToVideo(video){
|
||||
var clickTime = new Date(video.time)
|
||||
timeStripActionWithPausePlay().then(async (timeChanging) => {
|
||||
if(!timeChanging){
|
||||
await resetTimeline(clickTime)
|
||||
checkScroll(clickTime, true)
|
||||
}
|
||||
})
|
||||
}
|
||||
async function jumpToNextVideo(){
|
||||
var video = findNextVideo()
|
||||
if(!video)timeStripPlay(true);
|
||||
await jumpToVideo(video)
|
||||
}
|
||||
async function jumpToPreviousVideo(){
|
||||
var video = findPreviousVideo()
|
||||
if(!video)return console.log('No More!')
|
||||
await jumpToVideo(video)
|
||||
}
|
||||
sideMenuList.on('click','[timeline-menu-action]',function(){
|
||||
var el = $(this)
|
||||
var type = el.attr('timeline-menu-action')
|
||||
switch(type){
|
||||
case'selectMonitorGroup':
|
||||
var tag = el.attr('tag')
|
||||
if(!tag){
|
||||
timeStripSelectedMonitors = []
|
||||
}else{
|
||||
var tags = getListOfTagsFromMonitors()
|
||||
var monitorIds = tags[tag]
|
||||
timeStripSelectedMonitors = [...monitorIds];
|
||||
}
|
||||
refreshTimeline()
|
||||
break;
|
||||
}
|
||||
})
|
||||
timelineActionButtons.click(function(){
|
||||
var el = $(this)
|
||||
var type = el.attr('timeline-action')
|
||||
switch(type){
|
||||
case'playpause':
|
||||
timeStripPlay()
|
||||
break;
|
||||
case'downloadAll':
|
||||
if(featureIsActivated(true)){
|
||||
downloadAllPlayingVideos()
|
||||
}
|
||||
break;
|
||||
case'jumpLeft':
|
||||
jumpTimeline(5000,'left')
|
||||
break;
|
||||
case'jumpRight':
|
||||
jumpTimeline(5000,'right')
|
||||
break;
|
||||
case'jumpNext':
|
||||
jumpToNextVideo()
|
||||
break;
|
||||
case'jumpPrev':
|
||||
jumpToPreviousVideo()
|
||||
break;
|
||||
case'speed':
|
||||
var speed = parseInt(el.attr('speed'))
|
||||
if(featureIsActivated(true)){
|
||||
adjustTimelineSpeed(speed)
|
||||
speedButtons.removeClass('btn-success')
|
||||
el.addClass('btn-success')
|
||||
}
|
||||
break;
|
||||
case'gridSize':
|
||||
var size = el.attr('size')
|
||||
adjustTimelineGridSize(size)
|
||||
break;
|
||||
case'refresh':
|
||||
refreshTimeline()
|
||||
break;
|
||||
case'autoGridSizer':
|
||||
timeStripAutoGridSizerToggle()
|
||||
break;
|
||||
case'playUntilVideoEnd':
|
||||
timeStripPlayUntilVideoEndToggle()
|
||||
break;
|
||||
case'dontShowDetection':
|
||||
timeStripDontShowDetectionToggle()
|
||||
break;
|
||||
}
|
||||
})
|
||||
timeStripObjectSearchInput.change(function(){
|
||||
refreshTimeline()
|
||||
})
|
||||
timeStripVideoCanvas.on('dblclick','.timeline-video',function(){
|
||||
var monitorId = $(this).attr('data-mid')
|
||||
var video = loadedVideosOnCanvas[monitorId];
|
||||
createVideoPlayerTab(video)
|
||||
setVideoStatus(video)
|
||||
})
|
||||
addOnTabOpen('timeline', async function () {
|
||||
setColorReferences()
|
||||
refreshTimeline()
|
||||
drawFoundCamerasSubMenu()
|
||||
})
|
||||
addOnTabReopen('timeline', function () {
|
||||
drawFoundCamerasSubMenu()
|
||||
})
|
||||
addOnTabAway('timeline', function () {
|
||||
timeStripPlay(true)
|
||||
})
|
||||
loadDateRangePicker(dateSelector,{
|
||||
timePicker: true,
|
||||
timePicker24Hour: true,
|
||||
timePickerSeconds: true,
|
||||
timePickerIncrement: 30,
|
||||
autoApply: true,
|
||||
buttonClasses: 'hidden',
|
||||
drops: 'up',
|
||||
maxDate: new Date(),
|
||||
onChange: function(start, end, label) {
|
||||
if(!timeChanging){
|
||||
setLoadingMask(true)
|
||||
dateRangeChanging = true
|
||||
setTimestripDate(start, end)
|
||||
var newTickPosition = getTimeBetween(start,end,50);
|
||||
setTickDate(newTickPosition)
|
||||
setTimeout(() => {
|
||||
dateRangeChanging = false
|
||||
refreshTimeline()
|
||||
},2000)
|
||||
}
|
||||
}
|
||||
})
|
||||
var currentOptions = dashboardOptions()
|
||||
if(isChromiumBased){
|
||||
[ 7, 10 ].forEach((speed) => {
|
||||
timeStripControls.find(`[timeline-action="speed"][speed="${speed}"]`).remove()
|
||||
});
|
||||
}
|
||||
if(!currentOptions.timeStripAutoGridSizer || currentOptions.timeStripAutoGridSizer === '1'){
|
||||
timeStripAutoGridSizerToggle()
|
||||
}
|
||||
if(currentOptions.timeStripPlayUntilVideoEnd === '1'){
|
||||
timeStripPlayUntilVideoEndToggle()
|
||||
}
|
||||
if(currentOptions.dontShowDetectionOnTimeline === '1'){
|
||||
timeStripDontShowDetectionToggle()
|
||||
}
|
||||
})
|
|
@ -7,7 +7,7 @@ $(document).ready(function(){
|
|||
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] ? loadedMonitors[video.mid].name : lang['Monitor or Key does not exist.']} : ${formattedTime(video.time,true)}`
|
||||
var tabLabel = `<b>${lang['Video']}</b> : ${loadedMonitors[video.mid] ? loadedMonitors[video.mid].name : lang['Monitor or Key does not exist.']} : <span title="${formattedTime(video.time)}">${timeAgo(video.time)}</span>`
|
||||
var videoUrl = getLocation() + video.href
|
||||
var hasRows = video.events && video.events.length > 0
|
||||
var loadedEvents = {}
|
||||
|
@ -47,19 +47,24 @@ $(document).ready(function(){
|
|||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<b class="flex-grow-1">${lang.Started}</b>
|
||||
<div class="col-md-4">
|
||||
<div class="flex-grow-1"><b>${lang.Started}</b> ${timeAgo(video.time)}</div>
|
||||
<div class="video-time">${humanStartTime}</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<b class="flex-grow-1">${lang.Ended}</b>
|
||||
<div class="col-md-4">
|
||||
<div class="flex-grow-1"><b>${lang.Ended}</b> ${timeAgo(video.end)}</div>
|
||||
<div class="video-end">${humanEndTime}</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="flex-grow-1"><b>${lang['Objects Found']}</b></div>
|
||||
<div>${video.objects}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-block pt-4">
|
||||
<div class="btn-group btn-group-justified">
|
||||
<a class="btn btn-sm btn-success" download href="${videoUrl}"><i class="fa fa-download"></i> ${lang.Download}</a>
|
||||
${permissionCheck('video_delete',video.mid) ? `<a class="btn btn-sm btn-danger delete-video"><i class="fa fa-trash-o"></i> ${lang.Delete}</a>` : ''}
|
||||
${permissionCheck('video_delete',video.mid) ? `<a class="btn btn-sm btn-${video.archive === 1 ? `success status-archived` : `default`} archive-video" title="${lang.Archive}"><i class="fa fa-${video.archive === 1 ? `lock` : `unlock-alt`}"></i> ${lang.Archive}</a>` : ''}
|
||||
<div class="dropdown d-inline-block">
|
||||
<a class="btn btn-sm btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false" data-bs-reference="parent">
|
||||
<i class="fa fa-ellipsis-v" aria-hidden="true"></i>
|
||||
|
|
|
@ -408,7 +408,7 @@ function loadEventsData(videoEvents){
|
|||
loadedEventsInMemory[`${anEvent.mid}${anEvent.time}`] = anEvent
|
||||
})
|
||||
}
|
||||
function getVideos(options,callback){
|
||||
function getVideos(options,callback,noEvents){
|
||||
return new Promise((resolve,reject) => {
|
||||
options = options ? options : {}
|
||||
var searchQuery = options.searchQuery
|
||||
|
@ -444,7 +444,7 @@ function getVideos(options,callback){
|
|||
})
|
||||
})
|
||||
$.getJSON(`${getApiPrefix(`timelapse`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.concat([`noLimit=1`]).join('&')}`,function(timelapseFrames){
|
||||
$.getJSON(`${getApiPrefix(`events`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.concat([`limit=${eventLimit}`]).join('&')}`,function(eventData){
|
||||
function completeRequest(eventData){
|
||||
var theEvents = eventData.events || eventData;
|
||||
var newVideos = applyDataListToVideos(videos,theEvents)
|
||||
newVideos = applyTimelapseFramesListToVideos(newVideos,timelapseFrames.frames || timelapseFrames,'timelapseFrames',true).map((video) => {
|
||||
|
@ -455,7 +455,14 @@ function getVideos(options,callback){
|
|||
loadVideosData(newVideos)
|
||||
if(callback)callback({videos: newVideos, frames: timelapseFrames});
|
||||
resolve({videos: newVideos, frames: timelapseFrames})
|
||||
})
|
||||
}
|
||||
if(noEvents){
|
||||
completeRequest([])
|
||||
}else{
|
||||
$.getJSON(`${getApiPrefix(`events`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.concat([`limit=${eventLimit}`]).join('&')}`,function(eventData){
|
||||
completeRequest(eventData)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -620,6 +627,37 @@ function setVideoStatus(video,toStatus){
|
|||
}
|
||||
})
|
||||
}
|
||||
function getVideoInfoFromEl(_this){
|
||||
var el = $(_this).parents('[data-mid]')
|
||||
var monitorId = el.attr('data-mid')
|
||||
var videoTime = el.attr('data-time')
|
||||
var video = loadedVideosInMemory[`${monitorId}${videoTime}${undefined}`]
|
||||
return {
|
||||
monitorId,
|
||||
videoTime,
|
||||
video,
|
||||
}
|
||||
}
|
||||
function getDisplayDimensions(videoElement) {
|
||||
var actualVideoWidth = videoElement.videoWidth;
|
||||
var actualVideoHeight = videoElement.videoHeight;
|
||||
var elementWidth = videoElement.offsetWidth;
|
||||
var elementHeight = videoElement.offsetHeight;
|
||||
var actualVideoAspect = actualVideoWidth / actualVideoHeight;
|
||||
var elementAspect = elementWidth / elementHeight;
|
||||
var displayWidth, displayHeight;
|
||||
if (actualVideoAspect > elementAspect) {
|
||||
displayWidth = elementWidth;
|
||||
displayHeight = elementWidth / actualVideoAspect;
|
||||
} else {
|
||||
displayHeight = elementHeight;
|
||||
displayWidth = elementHeight * actualVideoAspect;
|
||||
}
|
||||
return {
|
||||
videoWidth: displayWidth,
|
||||
videoHeight: displayHeight,
|
||||
};
|
||||
}
|
||||
onWebSocketEvent(function(d){
|
||||
switch(d.f){
|
||||
case'video_edit':case'video_archive':
|
||||
|
@ -646,10 +684,12 @@ $(document).ready(function(){
|
|||
$('body')
|
||||
.on('click','.open-video',function(e){
|
||||
e.preventDefault()
|
||||
var el = $(this).parents('[data-mid]')
|
||||
var monitorId = el.attr('data-mid')
|
||||
var videoTime = el.attr('data-time')
|
||||
var video = loadedVideosInMemory[`${monitorId}${videoTime}${undefined}`]
|
||||
var _this = this;
|
||||
var {
|
||||
monitorId,
|
||||
videoTime,
|
||||
video,
|
||||
} = getVideoInfoFromEl(_this)
|
||||
createVideoPlayerTab(video)
|
||||
setVideoStatus(video)
|
||||
return false;
|
||||
|
|
|
@ -37,8 +37,11 @@ $(document).ready(function(e){
|
|||
imgBlock.find('img').attr('src',href)
|
||||
})
|
||||
}
|
||||
function openVideosTableView(monitorId,startDate,endDate){
|
||||
drawVideosTableViewElements(monitorId,startDate,endDate)
|
||||
window.openVideosTableView = function(monitorId){
|
||||
drawMonitorListToSelector(monitorsList,null,null,true)
|
||||
monitorsList.val(monitorId)
|
||||
drawVideosTableViewElements()
|
||||
openTab(`videosTableView`,{})
|
||||
}
|
||||
loadDateRangePicker(dateSelector,{
|
||||
onChange: function(start, end, label) {
|
||||
|
@ -151,8 +154,10 @@ $(document).ready(function(e){
|
|||
</div>`,
|
||||
Monitor: loadedMonitor && loadedMonitor.name ? loadedMonitor.name : file.mid,
|
||||
mid: file.mid,
|
||||
time: `<div><b>${lang.Start}</b> ${formattedTime(file.time, 'DD-MM-YYYY hh:mm:ss AA')}</div>
|
||||
<div><b>${lang.End}</b> ${formattedTime(file.end, 'DD-MM-YYYY hh:mm:ss AA')}</div>`,
|
||||
time: `
|
||||
<div>${timeAgo(file.time)}</div>
|
||||
<div><small><b>${lang.Start} :</b> ${formattedTime(file.time, 'DD-MM-YYYY hh:mm:ss AA')}</small></div>
|
||||
<div><small><b>${lang.End} :</b> ${formattedTime(file.end, 'DD-MM-YYYY hh:mm:ss AA')}</small></div>`,
|
||||
objects: file.objects,
|
||||
tags: `
|
||||
${file.ext ? `<span class="badge badge-${file.ext ==='webm' ? `primary` : 'danger'}">${file.ext}</span>` : ''}
|
||||
|
@ -242,11 +247,7 @@ $(document).ready(function(e){
|
|||
.on('click','.open-videosTable',function(e){
|
||||
e.preventDefault()
|
||||
var monitorId = getRowsMonitorId(this)
|
||||
openTab(`videosTableView`,{},null,null,null,() => {
|
||||
drawMonitorListToSelector(monitorsList,null,null,true)
|
||||
monitorsList.val(monitorId)
|
||||
drawVideosTableViewElements()
|
||||
})
|
||||
openVideosTableView(monitorId)
|
||||
return false;
|
||||
});
|
||||
sideLinkListBox
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
$(document).ready(function () {
|
||||
var schema = {
|
||||
"title": "Main Configuration",
|
||||
"title": lang["Main Configuration"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"debugLog": {
|
||||
"title": "Enable Debug Log",
|
||||
"title": lang["Enable Debug Log"],
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"subscriptionId": {
|
||||
"title": "Fill in subscription ID",
|
||||
"title": lang["Fill in subscription ID"],
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"port": {
|
||||
"title": "Server port",
|
||||
"title": lang["Server port"],
|
||||
"type": "integer",
|
||||
"default": 8080
|
||||
},
|
||||
"passwordType": {
|
||||
"title": "Password type",
|
||||
"title": lang["Password type"],
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"sha256",
|
||||
|
@ -31,12 +31,12 @@ $(document).ready(function () {
|
|||
"addStorage": {
|
||||
"type": "array",
|
||||
"format": "table",
|
||||
"title": "Additional Storage",
|
||||
"description": "Separate storage locations that can be set for different monitors.",
|
||||
"title": lang["Additional Storage"],
|
||||
"description": lang["AdditionalStorageDes"],
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"title": "Storage Array",
|
||||
"title": lang["Storage Array"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
|
@ -57,21 +57,24 @@ $(document).ready(function () {
|
|||
"plugins": {
|
||||
"type": "array",
|
||||
"format": "table",
|
||||
"title": "Plugins",
|
||||
"descripton": "Elaborate Plugin connection settings.",
|
||||
"title": lang["Plugins"],
|
||||
"descripton": lang["PluginsDes"],
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"title": "Plugin",
|
||||
"title": lang["Plugin"],
|
||||
"properties": {
|
||||
"plug": {
|
||||
"title": lang["Plug"],
|
||||
"type": "string",
|
||||
"default": "pluginName"
|
||||
},
|
||||
"key": {
|
||||
"title": lang["Key"],
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"title": lang["Mode"],
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"host",
|
||||
|
@ -80,21 +83,25 @@ $(document).ready(function () {
|
|||
"default": "client"
|
||||
},
|
||||
"https": {
|
||||
"title": lang["Https"],
|
||||
"type": "boolean",
|
||||
"descripton": "Only for Host mode.",
|
||||
"default": false
|
||||
},
|
||||
"host": {
|
||||
"title": lang["Host"],
|
||||
"type": "string",
|
||||
"descripton": "Only for Host mode.",
|
||||
"default": "localhost"
|
||||
},
|
||||
"port": {
|
||||
"title": lang["Port"],
|
||||
"type": "integer",
|
||||
"descripton": "Only for Host mode.",
|
||||
"default": 8082
|
||||
},
|
||||
"type": {
|
||||
"title": lang["Type"],
|
||||
"type": "string",
|
||||
"default": "detector"
|
||||
}
|
||||
|
@ -104,8 +111,8 @@ $(document).ready(function () {
|
|||
"pluginKeys": {
|
||||
"type": "object",
|
||||
"format": "table",
|
||||
"title": "Plugin Keys",
|
||||
"description": "Quick client connection setup for plugins. Just add the plugin key to make it ready for incoming connections.",
|
||||
"title": lang["Plugin Keys"],
|
||||
"description": lang["PluginKeysDes"],
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "object",
|
||||
|
@ -116,29 +123,31 @@ $(document).ready(function () {
|
|||
"db": {
|
||||
"type": "object",
|
||||
"format": "table",
|
||||
"title": "Database Options",
|
||||
"description": "Credentials to connect to where detailed information is stored.",
|
||||
"title": lang["Database Options"],
|
||||
"description": lang["DatabaseOptionDes"],
|
||||
"properties": {
|
||||
"host": {
|
||||
"title": "Hostname / IP",
|
||||
"title": lang["Hostname / IP"],
|
||||
"type": "string",
|
||||
"default": "127.0.0.1"
|
||||
},
|
||||
"user": {
|
||||
"title": "Username",
|
||||
"title": lang["User"],
|
||||
"type": "string",
|
||||
"default": "majesticflame"
|
||||
},
|
||||
"password": {
|
||||
"title": "Password",
|
||||
"title": lang["Password"],
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"database": {
|
||||
"title": lang["Database"],
|
||||
"type": "string",
|
||||
"default": "ccio"
|
||||
},
|
||||
"port": {
|
||||
"title": lang["Port"],
|
||||
"type": "integer",
|
||||
"default": 3306
|
||||
}
|
||||
|
@ -154,24 +163,27 @@ $(document).ready(function () {
|
|||
"cron": {
|
||||
"type": "object",
|
||||
"format": "table",
|
||||
"title": "CRON Options",
|
||||
"title": lang["CRON Options"],
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
},
|
||||
"deleteOld": {
|
||||
"title": lang["deleteOld"],
|
||||
"type": "boolean",
|
||||
"description": "cron will delete videos older than Max Number of Days per account.",
|
||||
"description": lang["deleteOldDes"],
|
||||
"default": true
|
||||
},
|
||||
"deleteNoVideo": {
|
||||
"title": lang["deleteNoVideo"],
|
||||
"type": "boolean",
|
||||
"description": "cron will delete SQL rows that it thinks have no video files.",
|
||||
"description": lang["deleteNoVideoDes"],
|
||||
"default": true
|
||||
},
|
||||
"deleteOverMax": {
|
||||
"title": lang["deleteOverMax"],
|
||||
"type": "boolean",
|
||||
"description": "cron will delete files that are over the set maximum storage per account.",
|
||||
"description": lang["deleteOverMaxDes"],
|
||||
"default": true
|
||||
},
|
||||
}
|
||||
|
@ -179,85 +191,105 @@ $(document).ready(function () {
|
|||
"mail": {
|
||||
"type": "object",
|
||||
"format": "table",
|
||||
"title": "Email Options",
|
||||
"title": lang["Email Options"],
|
||||
"properties": {
|
||||
"service": {
|
||||
"title": lang["service"],
|
||||
"type": "string",
|
||||
},
|
||||
"host": {
|
||||
"title": lang["Host"],
|
||||
"type": "string",
|
||||
},
|
||||
"auth": {
|
||||
"title": lang["auth"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {
|
||||
"title": lang["User"],
|
||||
"type": "string",
|
||||
},
|
||||
"pass": {
|
||||
"title": lang["Password"],
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"secure": {
|
||||
"title": lang["secure"],
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"ignoreTLS": {
|
||||
"title": lang["ignoreTLS"],
|
||||
"type": "boolean",
|
||||
},
|
||||
"requireTLS": {
|
||||
"title": lang["requireTLS"],
|
||||
"type": "boolean",
|
||||
},
|
||||
"port": {
|
||||
"title": lang["Port"],
|
||||
"type": "integer",
|
||||
}
|
||||
}
|
||||
},
|
||||
"detectorMergePamRegionTriggers": {
|
||||
"title": lang["detectorMergePamRegionTriggers"],
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"doSnapshot": {
|
||||
"title": lang["doSnapshot"],
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"discordBot": {
|
||||
"title": lang["discordBot"],
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"dropInEventServer": {
|
||||
"title": lang["dropInEventServer"],
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"ftpServer": {
|
||||
"title": lang["ftpServer"],
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"oldPowerVideo": {
|
||||
"title": lang["oldPowerVideo"],
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"wallClockTimestampAsDefault": {
|
||||
"title": lang["wallClockTimestampAsDefault"],
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"defaultMjpeg": {
|
||||
"title": lang["defaultMjpeg"],
|
||||
"type": "string",
|
||||
},
|
||||
"streamDir": {
|
||||
"title": lang["streamDir"],
|
||||
"type": "string",
|
||||
},
|
||||
"videosDir": {
|
||||
"title": lang["videosDir"],
|
||||
"type": "string",
|
||||
},
|
||||
"windowsTempDir": {
|
||||
"title": lang["windowsTempDir"],
|
||||
"type": "string",
|
||||
},
|
||||
"enableFaceManager": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"title": "Enable Face Manager",
|
||||
"description": "Enable / Disable face manager for face recognition plugins in the dashboard."
|
||||
"title": lang["Enable Face Manager"],
|
||||
"description": lang["enableFaceManagerDes"]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,9 +1,31 @@
|
|||
$(document).ready(function(){
|
||||
var loadedModules = {}
|
||||
var listElement = $('#customAutoLoadList')
|
||||
var quickSelectEl = $('#moduleQuickSelect')
|
||||
var downloadForm = $('#downloadNewModule')
|
||||
var getModules = function(callback) {
|
||||
$.get(superApiPrefix + $user.sessionKey + '/package/list',callback)
|
||||
}
|
||||
function getDownloadableModules(callback) {
|
||||
return new Promise((resolve,reject) => {
|
||||
const pluginListUrl = `https://gitlab.com/api/v4/projects/Shinobi-Systems%2FcustomAutoLoad-samples/repository/tree?path=samples`
|
||||
const filePrefix = `https://gitlab.com/Shinobi-Systems/customAutoLoad-samples/-/`
|
||||
$.getJSON(pluginListUrl,function(data){
|
||||
var html = ''
|
||||
data.forEach(item => {
|
||||
let downloadLink;
|
||||
if (item.type === 'blob') {
|
||||
downloadLink = `${filePrefix}raw/main/samples/${item.name}`;
|
||||
return;
|
||||
} else if (item.type === 'tree') {
|
||||
downloadLink = `${filePrefix}archive/master/customautoload-samples-master.zip,${item.path}`;
|
||||
}
|
||||
html += `<option value="${downloadLink}">${item.name}</option>`
|
||||
});
|
||||
quickSelectEl.html(html);
|
||||
})
|
||||
})
|
||||
}
|
||||
var loadedBlocks = {}
|
||||
var drawModuleBlock = function(module){
|
||||
var humanName = module.properties.name ? module.properties.name : module.name
|
||||
|
@ -12,9 +34,9 @@ $(document).ready(function(){
|
|||
existingElement.find('.title').text(humanName)
|
||||
existingElement.find('[calm-action="status"]').text(module.properties.disabled ? lang.Enable : lang.Disable)
|
||||
}else{
|
||||
listElement.append(`
|
||||
listElement.prepend(`
|
||||
<div class="col-md-12">
|
||||
<div class="card bg-dark" package-name="${module.name}">
|
||||
<div class="card bg-dark mb-3" package-name="${module.name}">
|
||||
<div class="card-body">
|
||||
<div><h4 class="title mt-0">${humanName}</h4></div>
|
||||
<div class="pb-2"><b>${lang['Time Created']} :</b> ${module.created}</div>
|
||||
|
@ -30,19 +52,20 @@ $(document).ready(function(){
|
|||
</div>
|
||||
<div class="pl-2 pr-2">
|
||||
<div class="install-output row">
|
||||
<div class="col-md-6 pr-2"><pre class="install-output-stdout"></pre></div>
|
||||
<div class="col-md-6 pl-2"><pre class="install-output-stderr"></pre></div>
|
||||
<div class="col-md-6 pr-2 mb-2"><pre class="install-output-stdout text-white mb-0"></pre></div>
|
||||
<div class="col-md-6 pl-2 mb-2"><pre class="install-output-stderr text-white mb-0"></pre></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`)
|
||||
var newBlock = $(`.card[package-name="${module.name}"]`)
|
||||
loadedBlocks[module.name] = {
|
||||
loadedBlocks[module.name] = Object.assign({
|
||||
block: newBlock,
|
||||
stdout: newBlock.find('.install-output-stdout'),
|
||||
stderr: newBlock.find('.install-output-stderr'),
|
||||
}
|
||||
},module)
|
||||
loadedModules[module.name] = module;
|
||||
}
|
||||
}
|
||||
var downloadModule = function(url,packageRoot,callback){
|
||||
|
@ -129,7 +152,7 @@ $(document).ready(function(){
|
|||
break;
|
||||
}
|
||||
})
|
||||
$('#downloadNewModule').submit(function(e){
|
||||
downloadForm.submit(function(e){
|
||||
e.preventDefault();
|
||||
var el = $(this)
|
||||
var form = el.serializeObject()
|
||||
|
@ -142,6 +165,16 @@ $(document).ready(function(){
|
|||
})
|
||||
return false
|
||||
})
|
||||
$('#moduleQuickSelectExec').click(function(){
|
||||
var currentVal = quickSelectEl.val()
|
||||
var valParts = currentVal.split(',')
|
||||
var packageUrl = `${valParts[0]}`
|
||||
var packageRoot = valParts[1]
|
||||
downloadForm.find(`[name="downloadUrl"]`).val(packageUrl)
|
||||
downloadForm.find(`[name="packageRoot"]`).val(packageRoot)
|
||||
downloadForm.find(`[name="downloadUrl"]`).val(packageUrl)
|
||||
downloadForm.submit()
|
||||
})
|
||||
setTimeout(function(){
|
||||
getModules(function(data){
|
||||
loadedModules = data.modules
|
||||
|
@ -172,4 +205,5 @@ $(document).ready(function(){
|
|||
break;
|
||||
}
|
||||
})
|
||||
getDownloadableModules()
|
||||
})
|
||||
|
|
|
@ -79,11 +79,12 @@ $(document).ready(function(){
|
|||
</div>
|
||||
</div>`)
|
||||
var newBlock = $(`.card[package-name="${module.name}"]`)
|
||||
loadedBlocks[module.name] = {
|
||||
loadedBlocks[module.name] = Object.assign({
|
||||
block: newBlock,
|
||||
stdout: newBlock.find('.install-output-stdout'),
|
||||
stderr: newBlock.find('.install-output-stderr'),
|
||||
}
|
||||
},module)
|
||||
loadedModules[module.name] = module;
|
||||
}
|
||||
}
|
||||
var downloadModule = function(url,packageRoot,callback){
|
||||
|
|
|
@ -58,8 +58,8 @@
|
|||
<%- lang['How to Connect'] %>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><b>This feature is available to Mobile License subscribers.</b> To get an API Key please login to your <a href="https://licenses.shinobi.video/login" target="_blank">Shinobi<b>Shop</b></a> account and create a key associated to <b>any active Subscription ID</b>. <a href="https://hub.shinobi.video/articles/view/3Yhivc6djTtuBPE" target="_blank">Learn More.</a></p>
|
||||
<p>If you would like to get access to a private (dedicated) P2P server please create an account at the <a href="https://licenses.shinobi.video/login" target="_blank">Shinobi<b>Shop</b></a> and contact us via the Live Chat widget.</p>
|
||||
<p><%- lang['HowToConnectDes1'] %></p>
|
||||
<p><%- lang['HowToConnectDes2'] %></p>
|
||||
<p class="mb-0"><a class="btn btn-sm btn-info" href="https://hub.shinobi.video/articles/view/3Yhivc6djTtuBPE" target="_blank"><%- lang['How to Connect'] %></a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<!-- Core JS Files -->
|
||||
<script src="<%-window.libURL%>assets/vendor/leaflet/leaflet.js"></script>
|
||||
<script src="<%-window.libURL%>assets/vendor/js/jquery-ui.min.js"></script>
|
||||
<script src="<%-window.libURL%>assets/vendor/js/pnotify.custom.min.js"></script>
|
||||
<script src="<%-window.libURL%>assets/vendor/js/socket.io.min.js"></script>
|
||||
|
@ -18,6 +19,7 @@
|
|||
<script src="<%-window.libURL%>assets/vendor/js/jquery.canvasAreaDraw.js"></script>
|
||||
<script src="<%-window.libURL%>assets/vendor/js/Chart.js"></script>
|
||||
<script src="<%-window.libURL%>assets/vendor/js/clock.js"></script>
|
||||
<script src="<%-window.libURL%>assets/vendor/js/vis.min.js" async></script>
|
||||
<script src="<%-window.libURL%>assets/js/bs5.dashboard-base.js"></script>
|
||||
<script src="<%-window.libURL%>assets/js/bs5.extenders.js"></script>
|
||||
<script src="<%-window.libURL%>assets/js/bs5.websocket.js"></script>
|
||||
|
|
|
@ -31,6 +31,8 @@
|
|||
<link rel="stylesheet" href="<%-window.libURL%>assets/vendor/daterangepicker.css">
|
||||
<link rel="stylesheet" href="<%-window.libURL%>assets/vendor/gridstack.min.css">
|
||||
<link rel="stylesheet" href="<%-window.libURL%>assets/vendor/jquery-ui.min.css">
|
||||
<link rel="stylesheet" href="<%-window.libURL%>assets/vendor/leaflet/leaflet.css" />
|
||||
<link rel="stylesheet" href="<%-window.libURL%>assets/vendor/vis.min.css">
|
||||
<link rel="stylesheet" href="<%-window.libURL%>assets/css/clock.css">
|
||||
<link rel="stylesheet" href="<%-window.libURL%>assets/css/bs5.liveGrid.css">
|
||||
<link rel="stylesheet" href="<%-window.libURL%>assets/css/bs5.regionEditor.css">
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
</div>
|
||||
<div id="side-menu-collapse-point" class="collapse show home-collapse">
|
||||
<ul id="pageTabLinks" class="nav flex-column nav-pills">
|
||||
<div id="createdTabLinks" class="pb-3 px-3 hidden-empty"></div>
|
||||
<div class="pb-2 px-3" id="home-collapse">
|
||||
<% menuInfo.links.forEach((item) => {
|
||||
if(!item.eval || eval(item.eval)){
|
||||
|
@ -69,7 +70,6 @@
|
|||
<% } %>
|
||||
<% }) %>
|
||||
</div>
|
||||
<div id="createdTabLinks" class="pb-3 px-3 hidden-empty"></div>
|
||||
</ul>
|
||||
</div>
|
||||
<% drawBlock(define.SideMenu.blocks.SideMenuAfterList) %>
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<main class="page-tab pt-3" id="tab-monitorMap">
|
||||
<form class="dark row">
|
||||
<%
|
||||
var drawBlock
|
||||
var buildOptions
|
||||
%>
|
||||
<%
|
||||
include fieldBuilders.ejs
|
||||
%>
|
||||
<%
|
||||
var pageName = 'Monitor Map';
|
||||
Object.keys(define[pageName].blocks).forEach(function(blockKey){
|
||||
var pageLayout = define[pageName].blocks[blockKey]
|
||||
drawBlock(pageLayout)
|
||||
})
|
||||
%>
|
||||
|
||||
</form>
|
||||
<link rel="stylesheet" href="<%-window.libURL%>assets/css/bs5.monitorMap.css" />
|
||||
<script src="<%-window.libURL%>assets/js/bs5.monitorMap.utils.js"></script>
|
||||
<script src="<%-window.libURL%>assets/js/bs5.monitorMap.js"></script>
|
||||
<script src="<%-window.libURL%>assets/js/bs5.monitorSettings.monitorMap.js"></script>
|
||||
</main>
|
|
@ -1,19 +0,0 @@
|
|||
<main class="page-tab pt-3" id="tab-powerVideo">
|
||||
<div class="row <%- define.Theme.isDark ? `dark` : '' %>" id="powerVideo">
|
||||
<%
|
||||
var drawBlock
|
||||
var buildOptions
|
||||
%>
|
||||
<%
|
||||
include fieldBuilders.ejs
|
||||
%>
|
||||
<% Object.keys(define['Power Viewer'].blocks).forEach(function(blockKey){
|
||||
var theBlock = define['Power Viewer'].blocks[blockKey]
|
||||
drawBlock(theBlock)
|
||||
}) %>
|
||||
</div>
|
||||
<link rel="stylesheet" href="<%-window.libURL%>assets/vendor/vis.min.css">
|
||||
<link rel="stylesheet" href="<%-window.libURL%>assets/css/bs5.powerVideo.css">
|
||||
<script src="<%-window.libURL%>assets/js/bs5.powerVideo.js"></script>
|
||||
<script src="<%-window.libURL%>assets/vendor/js/vis.min.js" async></script>
|
||||
</main>
|
|
@ -0,0 +1,21 @@
|
|||
<main class="page-tab" id="tab-timeline" style="position:relative;margin-right:-1.5rem;margin-left:-1.5rem;">
|
||||
<div class="dark">
|
||||
<%
|
||||
var drawBlock
|
||||
var buildOptions
|
||||
%>
|
||||
<%
|
||||
include fieldBuilders.ejs
|
||||
%>
|
||||
<%
|
||||
var pageName = 'Timeline';
|
||||
Object.keys(define[pageName].blocks).forEach(function(blockKey){
|
||||
var pageLayout = define[pageName].blocks[blockKey]
|
||||
drawBlock(pageLayout)
|
||||
})
|
||||
%>
|
||||
|
||||
</div>
|
||||
<link rel="stylesheet" href="<%-window.libURL%>assets/css/bs5.timeline.css" />
|
||||
<script src="<%-window.libURL%>assets/js/bs5.timeline.js"></script>
|
||||
</main>
|
|
@ -1,16 +1,40 @@
|
|||
<link rel="stylesheet" href="<%-window.libURL%>assets/css/super.customAutoLoad.css">
|
||||
<form class="form-group-group card bg-dark red mt-1" id="downloadNewModule">
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<input type="text" placeholder="Download URL for Module" class="form-control" name="downloadUrl" />
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card bg-dark grey mt-1">
|
||||
<div class="card-header">
|
||||
<%- lang['Download Modules'] %>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><%- lang.moduleDownloadText %></p>
|
||||
<div class="form-group">
|
||||
<select class="form-control" id="moduleQuickSelect">
|
||||
<% [
|
||||
|
||||
].forEach((option) => { %>
|
||||
<option value="<%- option.value %>"><%- option.label %></option>
|
||||
<% }) %>
|
||||
</select>
|
||||
</div>
|
||||
<div><button id="moduleQuickSelectExec" class="btn btn-round btn-block btn-default mb-0"><i class="fa fa-download"></i> <%- lang.Download %></button></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="text" placeholder="Subdirectory for Module (Inside the downloaded package)" class="form-control" name="packageRoot" />
|
||||
</div>
|
||||
<div><button type="submit" class="btn btn-round btn-block btn-default mb-0"><i class="fa fa-download"></i> <%- lang.Download %></button></div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="form-group-group red pt-4 pb-0 row m-0" id="customAutoLoadList">
|
||||
<div class="col-md-6">
|
||||
<form class="card bg-dark grey mt-1" id="downloadNewModule">
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<input type="text" placeholder="<%- lang['Download URL for Module'] %> (.zip)" class="form-control" name="downloadUrl" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="text" placeholder="<%- lang['Subdirectory for Module'] %> (<%- lang['Inside the downloaded package'] %>)" class="form-control" name="packageRoot" />
|
||||
</div>
|
||||
<div><button type="submit" class="btn btn-round btn-block btn-default mb-0"><i class="fa fa-download"></i> <%- lang.Download %></button></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-4 pb-0 m-0" id="customAutoLoadList">
|
||||
|
||||
</div>
|
||||
<script src="<%-window.libURL%>assets/js/super.customAutoLoad.js" type="text/javascript"></script>
|
||||
|
|
|
@ -2,7 +2,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">
|
||||
|
@ -51,7 +54,7 @@
|
|||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row dark">
|
||||
<form id="auth-form" action="/" method="post" style="display: block;">
|
||||
<form id="auth-form" method="post" style="display: block;">
|
||||
<div class="form-group">
|
||||
<input type="hidden" name="ke" id="ke" value="<%-$user.ke%>">
|
||||
<input type="hidden" name="id" id="uid" value="<%-$user.uid%>">
|
||||
|
|
|
@ -137,7 +137,7 @@
|
|||
<div class="col">
|
||||
<a href="https://gitlab.com/Shinobi-Systems/Shinobi/commit/<%- currentVersion %>" target="_blank">
|
||||
<small class="epic-text" style="font-weight: 700;color: #fff;letter-spacing: 2pt;">
|
||||
Current Version : <%- currentVersion %>
|
||||
<%- lang['Current Version'] %> : <%- currentVersion %>
|
||||
</small>
|
||||
</a>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue