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 options
node-20
Moe 2023-08-28 12:36:19 -07:00
parent 9844de231d
commit a9653d6517
60 changed files with 3350 additions and 1491 deletions

View File

@ -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

View File

@ -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

View File

@ -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 "========================================================="

View File

@ -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

View File

@ -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",
}
]
},
}
},
})
}

84
definitions/glyphs.js Normal file
View File

@ -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": "🏃‍♂️"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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;
}
}

View File

@ -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()
})

View File

@ -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

View File

@ -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;

View File

@ -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,

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -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,

View File

@ -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

View File

@ -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)
}

View File

@ -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,

View File

@ -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,
},
[
{

View File

@ -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)
}

View File

@ -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();
})

View File

@ -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({

View File

@ -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;
}
});

View File

@ -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)

View 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',
]

22
package-lock.json generated
View File

@ -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",

View File

@ -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",

164
tools/onvifGetStreamUri.js Normal file
View File

@ -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));
});
});

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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 {

View File

@ -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%
}

View File

@ -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;
}

View File

@ -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){

View File

@ -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

View File

@ -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;
}
})
})

View File

@ -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';
}
}

View File

@ -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)

View File

@ -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)
})
})

View File

@ -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>

View File

@ -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
}
})

View File

@ -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> &nbsp; ${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){

View File

@ -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()
}
})

View File

@ -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>

View File

@ -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;

View File

@ -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

View File

@ -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"]
}
}
};

View File

@ -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()
})

View File

@ -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){

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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) %>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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%>">

View File

@ -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>