From 6e354886e7727fc49152189ae800df5cc31ac011 Mon Sep 17 00:00:00 2001 From: Moe Date: Sun, 2 Jul 2023 21:02:06 +0000 Subject: [PATCH 1/4] Helios 685 --- definitions/base.js | 20 +++++--- languages/en_CA.json | 3 ++ libs/basic.js | 30 ++++++------ libs/events/utils.js | 2 +- libs/monitor/utils.js | 2 + libs/timelapse.js | 8 ++-- package-lock.json | 14 +++--- package.json | 2 +- web/assets/js/bs5.dashboard-base.js | 4 ++ web/assets/js/bs5.liveGrid.js | 64 ++++++++++++++++++++++---- web/assets/js/bs5.liveGrid.keyboard.js | 41 +++++++++++++++++ web/assets/js/bs5.monitorSettings.js | 8 +--- web/assets/js/bs5.monitorStates.js | 3 +- web/assets/js/bs5.monitorsUtils.js | 3 ++ web/assets/js/bs5.regionEditor.js | 14 ++++-- web/assets/js/bs5.videos.js | 4 +- web/pages/blocks/footer.ejs | 1 + 17 files changed, 165 insertions(+), 58 deletions(-) create mode 100644 web/assets/js/bs5.liveGrid.keyboard.js diff --git a/definitions/base.js b/definitions/base.js index ef70b982..630b6a83 100644 --- a/definitions/base.js +++ b/definitions/base.js @@ -4822,8 +4822,6 @@ module.exports = function(s,config,lang){ "name": "detail=audio_note", "field": lang["Notification Sound"], "description": lang["fieldTextAudioNote"], - "default": "", - "example": "", "fieldType": "select", "possible": s.listOfAudioFiles }, @@ -4831,8 +4829,6 @@ module.exports = function(s,config,lang){ "name": "detail=audio_alert", "field": lang["Alert Sound"], "description": lang["fieldTextAudioAlert"], - "default": "", - "example": "", "fieldType": "select", "possible": s.listOfAudioFiles }, @@ -4841,15 +4837,12 @@ module.exports = function(s,config,lang){ "field": lang["Alert Sound Delay"], "description": lang["fieldTextAudioDelay"], "default": "1", - "example": "", - "possible": "" }, { "name": "detail=event_mon_pop", "field": lang["Popout Monitor on Event"], "description": lang["fieldTextEventMonPop"], "default": "en_CA", - "example": "", "fieldType": "select", "possible": [ { @@ -7449,6 +7442,7 @@ module.exports = function(s,config,lang){ streamBlockHudControlsHtml: ` + ${lang['Add Marker']} ${lang['Test Object Event']} ${lang['Test Motion Event']} `, @@ -7689,6 +7683,18 @@ module.exports = function(s,config,lang){ attributes: 'shinobi-switch="dontShowDetection" ui-change-target=".dot" on-class="dot-green" off-class="dot-grey"', color: 'grey', }, + { + label: lang[`Alert on Event`], + class: 'cursor-pointer', + attributes: 'shinobi-switch="alertOnEvent" ui-change-target=".dot" on-class="dot-green" off-class="dot-grey"', + color: 'grey', + }, + { + label: lang[`Popout on Event`], + class: 'cursor-pointer', + attributes: 'shinobi-switch="popOnEvent" ui-change-target=".dot" on-class="dot-green" off-class="dot-grey"', + color: 'grey', + }, ] }, { diff --git a/languages/en_CA.json b/languages/en_CA.json index fd791e66..5f1b6966 100644 --- a/languages/en_CA.json +++ b/languages/en_CA.json @@ -748,6 +748,9 @@ "Rotate": "Rotate", "Trigger Event": "Trigger Event", "Test": "Test", + "Popout on Event": "Popout on Event", + "Alert on Event": "Alert on Event", + "Add Marker": "Add Marker", "Test Object Event": "Test Object Event", "Test Motion Event": "Test Motion Event", "Primary Engine": "Primary Engine", diff --git a/libs/basic.js b/libs/basic.js index a12d8a69..4b63887f 100644 --- a/libs/basic.js +++ b/libs/basic.js @@ -207,28 +207,28 @@ module.exports = function(s,config){ } return url } - s.file = function(x,e,callback){ + s.file = async function(x,e,callback){ if(!e){e={}}; switch(x){ case'size': return fs.statSync(e.filename)["size"]; break; case'delete': - if(!e){return false;} - fs.rm(e,(err)=>{ - if(err){ - s.debugLog(err) - if(s.isWin){ - exec('rd /s /q "' + e + '"',{detached: true},function(err){ - if(callback)callback(err) - }) - }else{ - exec('rm -rf '+e,{detached: true},function(err){ - if(callback)callback(err) - }) - } + if (!e) { return false; } + try{ + return await fs.promises.rm(e, { force: true }) + }catch(err){ + s.debugLog(err) + if(s.isWin){ + exec('rd /s /q "' + e + '"', { detached: true }, function (err) { + if (callback) callback(err) + }) + }else{ + exec('rm -rf ' + e, { detached: true }, function (err) { + if (callback) callback(err) + }) } - }) + } break; case'deleteFolder': if(!e){return false;} diff --git a/libs/events/utils.js b/libs/events/utils.js index 03325bad..55c25bab 100644 --- a/libs/events/utils.js +++ b/libs/events/utils.js @@ -38,7 +38,7 @@ module.exports = (s,config,lang) => { async function saveImageFromEvent(options,frameBuffer){ const monitorId = options.mid || options.id const groupKey = options.ke - if(imageSaveEventLock[groupKey + monitorId])return; + if(!frameBuffer || imageSaveEventLock[groupKey + monitorId])return; const eventTime = options.time const objectsFound = options.matrices const monitorConfig = Object.assign({id: monitorId},s.group[groupKey].rawMonitorConfigurations[monitorId]) diff --git a/libs/monitor/utils.js b/libs/monitor/utils.js index b2219791..80412166 100644 --- a/libs/monitor/utils.js +++ b/libs/monitor/utils.js @@ -748,6 +748,8 @@ module.exports = (s,config,lang) => { }) } function monitorIdle(e){ + const monitorId = e.mid || e.id + const groupKey = e.ke s.tx({f:'monitor_idle',mid:monitorId,ke:groupKey,time:s.formattedTime()},'GRP_'+groupKey); s.userLog(e,{type:lang['Monitor Idling'],msg:lang.MonitorIdlingText}); s.sendMonitorStatus({ diff --git a/libs/timelapse.js b/libs/timelapse.js index f5cd1f82..4a543e89 100644 --- a/libs/timelapse.js +++ b/libs/timelapse.js @@ -163,11 +163,11 @@ module.exports = function(s,config,lang,app,io){ limit: 1 },async function(){ s.setDiskUsedForGroup(e.ke,-(r.size / 1048576),'timelapseFrames') - s.file('delete',e.fileLocation) - const fileDirectory = getFileDirectory(folderPath); - const folderIsEmpty = (await fs.promises.readdir(folderPath)).filter(file => file.indexOf('.jpg') > -1).length === 0; + await s.file('delete', e.fileLocation); + const fileDirectory = getFileDirectory(e.fileLocation); + const folderIsEmpty = (await fs.promises.readdir(fileDirectory)).filter(file => file.indexOf('.jpg') > -1).length === 0; if(folderIsEmpty){ - await fs.rm(folderPath, { recursive: true }) + await fs.rm(fileDirectory, { recursive: true }) } }) }else{ diff --git a/package-lock.json b/package-lock.json index 1aa65864..c7f2f079 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ "ldapauth-fork": "^5.0.2", "marked": "^4.3.0", "moment": "^2.29.4", - "mp4frag": "^0.6.0", + "mp4frag": "^0.6.1", "mqtt": "^4.3.7", "mysql": "^2.18.1", "mysql2": "^2.1.0", @@ -5037,9 +5037,9 @@ } }, "node_modules/mp4frag": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/mp4frag/-/mp4frag-0.6.0.tgz", - "integrity": "sha512-MvBAaWkW94SSpam/QsCmbMi7+ZY2YHzAjj6Uno7AZ6qxH7gZstN+L3jFopdN5F3/5mRK25gvA4k0DVpCbDe7+g==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/mp4frag/-/mp4frag-0.6.1.tgz", + "integrity": "sha512-x2xVwVZqT+P1dmGLpBStS6Op6oownx2Q/6qh5ov3hOIH4rTNab0p6Gi/l+EliG0FKW9R6jA2eGr0vnHvJVMT9w==", "engines": { "node": ">=10" } @@ -11781,9 +11781,9 @@ "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" }, "mp4frag": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/mp4frag/-/mp4frag-0.6.0.tgz", - "integrity": "sha512-MvBAaWkW94SSpam/QsCmbMi7+ZY2YHzAjj6Uno7AZ6qxH7gZstN+L3jFopdN5F3/5mRK25gvA4k0DVpCbDe7+g==" + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/mp4frag/-/mp4frag-0.6.1.tgz", + "integrity": "sha512-x2xVwVZqT+P1dmGLpBStS6Op6oownx2Q/6qh5ov3hOIH4rTNab0p6Gi/l+EliG0FKW9R6jA2eGr0vnHvJVMT9w==" }, "mqtt": { "version": "4.3.7", diff --git a/package.json b/package.json index 69a54202..ee579d19 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "ldapauth-fork": "^5.0.2", "marked": "^4.3.0", "moment": "^2.29.4", - "mp4frag": "^0.6.0", + "mp4frag": "^0.6.1", "mqtt": "^4.3.7", "mysql": "^2.18.1", "mysql2": "^2.1.0", diff --git a/web/assets/js/bs5.dashboard-base.js b/web/assets/js/bs5.dashboard-base.js index 1e5f3077..d791a54a 100644 --- a/web/assets/js/bs5.dashboard-base.js +++ b/web/assets/js/bs5.dashboard-base.js @@ -1017,6 +1017,10 @@ function onDashboardReadyExecute(theAction){ function popImage(imageSrc){ $('body').append(`
`) } +function setSubmitButton(editorForm,text,icon,toggle){ + var submitButtons = editorForm.find('[type="submit"]').prop('disabled',toggle) + submitButtons.html(` ${text}`) +} $(document).ready(function(){ onInitWebsocket(function(){ loadMonitorsIntoMemory(function(data){ diff --git a/web/assets/js/bs5.liveGrid.js b/web/assets/js/bs5.liveGrid.js index 06c4a681..b0847009 100644 --- a/web/assets/js/bs5.liveGrid.js +++ b/web/assets/js/bs5.liveGrid.js @@ -963,6 +963,30 @@ function openAllLiveGridPlayers(){ openLiveGrid() }) } +function addMarkAsEvent(monitorId){ + runTestDetectionTrigger(monitorId,{ + "name":"Marker", + "reason":"marker", + "matrices": [ + { + x: 0, + y: 0, + width: 1, + height: 1, + tag: 'Marked', + confidence: 100, + } + ] + }); +} +function addMarkAsEventToAllOpenMonitors(){ + $.each(loadedMonitors,function(n,monitor){ + var monitorId = monitor.mid + if(liveGridPlayingNow[monitorId]){ + addMarkAsEvent(monitorId) + } + }) +} $(document).ready(function(e){ liveGrid .on('dblclick','.stream-block',function(){ @@ -976,15 +1000,14 @@ $(document).ready(function(e){ .on('click','.launch-live-grid-monitor',function(){ var monitorId = $(this).parents('[data-mid]').attr('data-mid') if(isMobile){ - createLivePlayerTab(loadedMonitors[monitorId]) - }else{ - mainSocket.f({ - f: 'monitor', - ff: 'watch_on', - id: monitorId - }) - openLiveGrid() + closeAllLiveGridPlayers() } + mainSocket.f({ + f: 'monitor', + ff: 'watch_on', + id: monitorId + }) + openLiveGrid() }) .on('click','.monitor-live-group-open',function(){ var monitorIds = $(this).attr('monitor-ids').split(',') @@ -1086,6 +1109,11 @@ $(document).ready(function(e){ var monitorId = el.parents('[data-mid]').attr('data-mid') runTestDetectionTrigger(monitorId) }) + .on('click','.run-monitor-detection-trigger-marker',function(){ + var el = $(this) + var monitorId = el.parents('[data-mid]').attr('data-mid') + addMarkAsEvent(monitorId) + }) .on('click','.run-monitor-detection-trigger-test-motion',function(){ var el = $(this) var monitorId = el.parents('[data-mid]').attr('data-mid') @@ -1244,7 +1272,7 @@ $(document).ready(function(e){ } playAudioAlert() var monitorPop = monitorPops[monitorId] - if($user.details.event_mon_pop === '1' && (!monitorPop || monitorPop.closed === true)){ + if(window.popLiveOnEvent && (!monitorPop || monitorPop.closed === true)){ popOutMonitor(monitorId) } // console.log({ @@ -1292,12 +1320,28 @@ $(document).ready(function(e){ window.dontShowDetection = true } } + dashboardSwitchCallbacks.alertOnEvent = function(toggleState){ + // audio_alert + if(toggleState !== 1){ + window.audioAlertOnEvent = false + }else{ + window.audioAlertOnEvent = true + } + } + dashboardSwitchCallbacks.popOnEvent = function(toggleState){ + if($user.details.event_mon_pop === '1'){ + window.popLiveOnEvent = true + }else if(toggleState !== 1){ + window.popLiveOnEvent = false + }else{ + window.popLiveOnEvent = true + } + } dashboardSwitchCallbacks.monitorMuteAudio = function(toggleState){ var monitorMutes = dashboardOptions().monitorMutes || {} $('.monitor_item video').each(function(n,vidEl){ var el = $(this) var monitorId = el.parents('[data-mid]').attr('data-mid') - console.log(monitorId,monitorMutes[monitorId]) if(toggleState === 1){ vidEl.muted = true }else{ diff --git a/web/assets/js/bs5.liveGrid.keyboard.js b/web/assets/js/bs5.liveGrid.keyboard.js new file mode 100644 index 00000000..6c32ee6d --- /dev/null +++ b/web/assets/js/bs5.liveGrid.keyboard.js @@ -0,0 +1,41 @@ +function keyShortcutsForLiveGridUtils(enable) { + function cleanup(){ + document.removeEventListener('keydown', keyShortcuts['liveGridUtils'].keydown); + document.removeEventListener('keyup', keyShortcuts['liveGridUtils'].keyup); + delete(keyShortcuts['liveGridUtils']) + } + if(enable){ + let isKeyPressed = false; + function handleKeyboard(event){ + if (isKeyPressed) { + return; + } + event.preventDefault(); + switch(event.code){ + case 'Enter': + addMarkAsEventToAllOpenMonitors() + break; + } + } + function handleKeyup(event) { + isKeyPressed = false; + } + keyShortcuts['liveGridUtils'] = { + keydown: handleKeyboard, + keyup: handleKeyup, + } + document.addEventListener('keydown', keyShortcuts['liveGridUtils'].keydown); + document.addEventListener('keyup', keyShortcuts['liveGridUtils'].keyup); + }else{ + cleanup() + } +} +addOnTabOpen('liveGrid', function () { + keyShortcutsForLiveGridUtils(true) +}) +addOnTabReopen('liveGrid', function () { + keyShortcutsForLiveGridUtils(true) +}) +addOnTabAway('liveGrid', function () { + keyShortcutsForLiveGridUtils(false) +}) diff --git a/web/assets/js/bs5.monitorSettings.js b/web/assets/js/bs5.monitorSettings.js index 3460b46f..6f4f1ba4 100644 --- a/web/assets/js/bs5.monitorSettings.js +++ b/web/assets/js/bs5.monitorSettings.js @@ -758,10 +758,6 @@ monitorEditorWindow.on('change','[detail="auto_host"]',function(e){ } }) editorForm.submit(function(e){ - function setSubmitButton(text,icon,toggle){ - var submitButtons = editorForm.find('[type="submit"]').prop('disabled',toggle) - submitButtons.html(` ${text}`) - } e.preventDefault(); var validation = getMonitorEditFormFields() if(!validation.ok){ @@ -770,7 +766,7 @@ editorForm.submit(function(e){ new PNotify({title:'Configuration Invalid',text:errorsFound.join('
'),type:'error'}); } var monitorConfig = validation.monitorConfig - setSubmitButton(lang[`Please Wait...`], `spinner fa-pulse`, true) + setSubmitButton(editorForm, lang[`Please Wait...`], `spinner fa-pulse`, true) $.post(getApiPrefix()+'/configureMonitor/'+$user.ke+'/'+monitorConfig.mid,{data:JSON.stringify(monitorConfig)},function(d){ if(d.ok === false){ new PNotify({ @@ -780,7 +776,7 @@ editorForm.submit(function(e){ }) } debugLog(d) - setSubmitButton(lang.Save, `check`, false) + setSubmitButton(editorForm, lang.Save, `check`, false) }) // if(copySettingsSelector.val() === '1'){ diff --git a/web/assets/js/bs5.monitorStates.js b/web/assets/js/bs5.monitorStates.js index acd15dfd..e5e59e4a 100644 --- a/web/assets/js/bs5.monitorStates.js +++ b/web/assets/js/bs5.monitorStates.js @@ -143,13 +143,14 @@ $(document).ready(function(){ var drawMonitor = function(preloadedData){ var MonitorSettings = definitions['Monitor Settings'] var html = '' + var monitorId = preloadedData ? preloadedData.mid : '' Object.keys(MonitorSettings.blocks).forEach(function(blockKey){ var block = MonitorSettings.blocks[blockKey] html += drawBlock(block,preloadedData) }) var monitorSelect = `` var fullHtml = `
diff --git a/web/assets/js/bs5.monitorsUtils.js b/web/assets/js/bs5.monitorsUtils.js index fef182e1..f3257162 100644 --- a/web/assets/js/bs5.monitorsUtils.js +++ b/web/assets/js/bs5.monitorsUtils.js @@ -230,6 +230,9 @@ function toggleSubStream(monitorId,callback){ } function playAudioAlert(){ var fileName = $user.details.audio_alert + if(window.audioAlertOnEvent && !fileName){ + fileName = `alert.mp3` + } if(fileName && window.soundAlarmed !== true){ window.soundAlarmed = true var audio = new Audio(`libs/audio/${fileName}`) diff --git a/web/assets/js/bs5.regionEditor.js b/web/assets/js/bs5.regionEditor.js index edca3d2c..a420300c 100644 --- a/web/assets/js/bs5.regionEditor.js +++ b/web/assets/js/bs5.regionEditor.js @@ -113,13 +113,19 @@ $(document).ready(function(e){ }); monitorConfig.details.cords = JSON.stringify(regionCoordinates) monitorConfig.details = JSON.stringify(monitorConfig.details) + setSubmitButton(regionEditorForm, lang[`Please Wait...`], `spinner fa-pulse`, true) $.post(getApiPrefix(`configureMonitor`)+ '/' + monitorId,{ data: JSON.stringify(monitorConfig) },function(d){ - debugLog(d) - if(d.ok){ - + if(d.ok === false){ + new PNotify({ + title: lang['Action Failed'], + text: d.msg, + type: 'danger' + }) } + debugLog(d) + setSubmitButton(regionEditorForm, lang.Save, `check`, false) }) } var initiateRegionList = function(presetVal){ @@ -142,7 +148,7 @@ $(document).ready(function(e){ } function setGridDisplayBasedOnFields(){ var isOn = accuracyModeToggle.val() === '1' - var tileSize = tileSizeField.val() + var tileSize = tileSizeField.val() || 20 displayGridOverCanvas(isOn,tileSize) } function initLiveStream(monitorId){ diff --git a/web/assets/js/bs5.videos.js b/web/assets/js/bs5.videos.js index 76b97d1b..b2eb9162 100644 --- a/web/assets/js/bs5.videos.js +++ b/web/assets/js/bs5.videos.js @@ -125,11 +125,11 @@ function getFrameOnVideoRow(percentageInward, video) { }; } - var closestFrame = frames.reduce(function(prev, curr) { + var closestFrame = frames.length > 0 ? frames.reduce(function(prev, curr) { var prevDiff = Math.abs(timeAdded - new Date(prev.time)); var currDiff = Math.abs(timeAdded - new Date(curr.time)); return (prevDiff < currDiff) ? prev : curr; - }); + }) : null; return { timeInward: timeInward, diff --git a/web/pages/blocks/footer.ejs b/web/pages/blocks/footer.ejs index 8f24d096..a3f700b4 100644 --- a/web/pages/blocks/footer.ejs +++ b/web/pages/blocks/footer.ejs @@ -33,6 +33,7 @@ + From a9653d6517419935def478f8934cd38b635a7646 Mon Sep 17 00:00:00 2001 From: Moe Date: Mon, 28 Aug 2023 12:36:19 -0700 Subject: [PATCH 2/4] 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 --- INSTALL/cuda-10-2.sh | 2 +- INSTALL/cuda-10.sh | 2 +- INSTALL/rocky9-touchless.sh | 1 + README.md | 4 +- definitions/base.js | 322 ++++-- definitions/glyphs.js | 84 ++ languages/en_CA.json | 108 +- languages/zh.json | 404 +++++++- libs/childNode/childUtils.js | 18 +- libs/commander/workerv2.js | 109 +- libs/control.js | 5 +- libs/control/onvif.js | 141 +-- libs/control/ptz.js | 186 ++-- libs/database/utils.js | 7 +- libs/events/onvif.js | 25 +- libs/events/utils.js | 31 +- libs/monitor/utils.js | 1 + libs/notifications/discordBot.js | 22 +- libs/notifications/matrix.js | 9 +- libs/notifications/pushover.js | 12 +- libs/notifications/telegram.js | 108 +- libs/plugins/superUser.js | 2 +- libs/startup.js | 8 +- libs/videoBrowser.js | 10 +- libs/videos.js | 2 +- libs/webServer.js | 3 +- package-lock.json | 22 +- package.json | 2 +- tools/onvifGetStreamUri.js | 164 +++ web/assets/css/bs5.monitorMap.css | 37 + web/assets/css/bs5.powerVideo.css | 191 ---- web/assets/css/bs5.sideMenu.css | 3 +- web/assets/css/bs5.timeline.css | 127 +++ web/assets/css/dashboard.css | 23 +- web/assets/js/bs5.dashboard-base.js | 67 +- web/assets/js/bs5.liveGrid.js | 11 + web/assets/js/bs5.monitorMap.js | 155 +++ web/assets/js/bs5.monitorMap.utils.js | 89 ++ web/assets/js/bs5.monitorSettings.js | 29 +- .../js/bs5.monitorSettings.monitorMap.js | 150 +++ web/assets/js/bs5.monitorsUtils.js | 8 +- web/assets/js/bs5.powerVideo.js | 798 -------------- web/assets/js/bs5.sideMenu.js | 20 +- web/assets/js/bs5.timeline.js | 970 ++++++++++++++++++ web/assets/js/bs5.videoPlayer.js | 15 +- web/assets/js/bs5.videos.js | 54 +- web/assets/js/bs5.videosTable.js | 19 +- web/assets/js/super.configEditor.js | 82 +- web/assets/js/super.customAutoLoad.js | 48 +- web/assets/js/super.pluginManager.js | 5 +- web/pages/blocks/easyRemoteAccess.ejs | 4 +- web/pages/blocks/footer.ejs | 2 + web/pages/blocks/header.ejs | 2 + web/pages/blocks/home/menuSide.ejs | 2 +- web/pages/blocks/home/monitorMap.ejs | 23 + web/pages/blocks/home/powerVideo.ejs | 19 - web/pages/blocks/home/timeline.ejs | 21 + .../blocks/superCustomAutoLoadManager.ejs | 44 +- web/pages/factor.ejs | 7 +- web/pages/super.ejs | 2 +- 60 files changed, 3350 insertions(+), 1491 deletions(-) create mode 100644 definitions/glyphs.js create mode 100644 tools/onvifGetStreamUri.js create mode 100644 web/assets/css/bs5.monitorMap.css delete mode 100644 web/assets/css/bs5.powerVideo.css create mode 100644 web/assets/css/bs5.timeline.css create mode 100644 web/assets/js/bs5.monitorMap.js create mode 100644 web/assets/js/bs5.monitorMap.utils.js create mode 100644 web/assets/js/bs5.monitorSettings.monitorMap.js delete mode 100644 web/assets/js/bs5.powerVideo.js create mode 100644 web/assets/js/bs5.timeline.js create mode 100644 web/pages/blocks/home/monitorMap.ejs delete mode 100644 web/pages/blocks/home/powerVideo.ejs create mode 100644 web/pages/blocks/home/timeline.ejs diff --git a/INSTALL/cuda-10-2.sh b/INSTALL/cuda-10-2.sh index 2fb4144f..364be7ea 100644 --- a/INSTALL/cuda-10-2.sh +++ b/INSTALL/cuda-10-2.sh @@ -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 diff --git a/INSTALL/cuda-10.sh b/INSTALL/cuda-10.sh index 192265fb..2b5f7a04 100644 --- a/INSTALL/cuda-10.sh +++ b/INSTALL/cuda-10.sh @@ -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 diff --git a/INSTALL/rocky9-touchless.sh b/INSTALL/rocky9-touchless.sh index 5a13a0e7..3eb8201e 100644 --- a/INSTALL/rocky9-touchless.sh +++ b/INSTALL/rocky9-touchless.sh @@ -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 "=========================================================" diff --git a/README.md b/README.md index dc6b388c..09754a1e 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/definitions/base.js b/definitions/base.js index 630b6a83..80f6498b 100644 --- a/definitions/base.js +++ b/definitions/base.js @@ -24,7 +24,7 @@ module.exports = function(s,config,lang){ "blocks": { "Page Control": { name: lang.Monitor, - headerTitle: `
Monitor Settings : Add New
`, + headerTitle: `
${lang['Monitor Settings']} : Add New
`, "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: ` +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
`, + }, ] }, "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: ` + ${lang['Add Marker']} ${lang['Test Object Event']} ${lang['Test Motion Event']} @@ -7703,6 +7769,17 @@ module.exports = function(s,config,lang){ `, 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 Shinobi Mobile 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: '#mobile-client 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! `, + 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 PayPal Transaction ID can be used as a subscriptionId in your Shinobi configuration file.

Each 5 USD/EUR or 7 CAD will provide one month of activated usage. Meaning, a $20 USD donation today makes this popup go away (or activates the mobile app) for 4 months.`, + 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": ` +
+ +
` + }, + { + "fieldType": "div", + "class": "p-2", + "id": "timeline-controls", + "divContent": ` +
+
+ + + + + +
+
+ x1 + x2 + x5 + x7 + x10 +
+
+ 1 + 2 + 3 +
+
+ +
+
+ + + +
+
+ +
+
+ + +
+
+ +
+
+ `, + }, + { + "fieldType": "div", + "id": "timeline-bottom-strip", + }, + { + "fieldType": "div", + "id": "timeline-pre-buffers", + "class": "hidden", + } + ] + }, + } + }, }) } diff --git a/definitions/glyphs.js b/definitions/glyphs.js new file mode 100644 index 00000000..1a3db023 --- /dev/null +++ b/definitions/glyphs.js @@ -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": "🏃‍♂ïļ" +} diff --git a/languages/en_CA.json b/languages/en_CA.json index 5f1b6966..f4b4e075 100644 --- a/languages/en_CA.json +++ b/languages/en_CA.json @@ -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 PayPal Transaction ID can be used as a subscriptionId in your Shinobi configuration file.

Each 5 USD/EUR or 7 CAD will provide one month of activated usage. Meaning, a $20 USD donation today makes this popup go away (or activates the mobile app) for 4 months.", "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 installing plugins in the Documentation.", + "moduleDownloadText": "Learn about Custom Auto Load Modules 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 here on ShinobiHub.", "IdentityText1": "This is how the system will identify the data for this stream. You cannot change the Monitor ID once you have pressed save. If you want you can make the Monitor ID more human readable before you continue.", "IdentityText2": "You can duplicate a monitor by modifying the Monitor ID then pressing save. You cannot 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 optimization guide 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 8000. This can be 80 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 -strict 2 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! ", + "Shinobi Mobile": "Shinobi Mobile", + "yourSubscriptionText": "Your subscription key can unlock features for Shinobi Mobile 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": "This feature is available to Mobile License subscribers. To get an API Key please login to your ShinobiShop account and create a key associated to any active Subscription ID. Learn More.", + "HowToConnectDes2": "If you would like to get access to a private (dedicated) P2P server please create an account at the ShinobiShop and contact us via the Live Chat widget", + "User": "User", + "Current Version": "Current Version", + "Default is Global value": "Default is Global value" } diff --git a/languages/zh.json b/languages/zh.json index 49a3e34e..a69a4c5a 100644 --- a/languages/zh.json +++ b/languages/zh.json @@ -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 įš„éĢŽæ žä― įš„äŧŠčĄĻæŋ。", + "CSS": "CSS äļšä― įš„äŧŠčĄĻį›˜čŪūį―Ū样垏", "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": "

å―“åŪ―åšĶ和éŦ˜åšĶæĄ†æ˜ūįĪšïžŒä― åš”čŊĨ将åŪƒäŧŽčŪūį―Ūäļš640x480或äŧĨäļ‹ã€‚ čŋ™å°†äž˜åŒ–čŊĨ阅čŊŧįš„速åšĶæĄ†æžķ。

", "Disable Night Vision": "įĶæ­ĒåĪœč§† įš„URL地址", "Disable Nightvision": "įĶæ­ĒåĪœč§†", - "Disabled": "æŪ‹į–ūäšš", + "Disabled": "įĶį”Ļ", "Documentation": "文æĄĢ", "Don't show this anymore": "äļå†čŋ™æ ·äļ‹åŽŧ乆", "Double Quote Directory": "双垕į›Ūå―•äļ­įš„ äļ€äš›į›Ūå―•å…·æœ‰įĐšé—ī。 ä―ŋį”Ļčŋ™äļŠåŊčƒ―äžšåīĐ暃äļ€äš›æ‘„像朚。", @@ -176,8 +176,8 @@ "Font Path": "å­—ä―“č·Ŋåū„", "Font Size": "å­—ä―“åĪ§å°", "Force Port": "éƒĻ队åĢ", - "Found Devices": "čŪūåĪ‡æ‰ū到䚆", - "Frame Rate": "æĄ†įŽ‡ (》)", + "Found Devices": "发įŽ°čŪūåĪ‡", + "Frame Rate": "åļ§įŽ‡", "Full Frame Detection": "åŪŒå…ĻæĢ€æĩ‹æĄ†æžķ", "Fullscreen": "å…Ļåą", "Greater Than": "åĪ§äšŽ", @@ -220,11 +220,11 @@ "Input": "čū“å…Ĩ", "Input Flags": "čū“å…Ĩįš„æ ‡åŋ—", "Input Type": "čū“å…Ĩįąŧ型", - "InputText1": "čŋ™éƒĻ分告čŊ‰åŋåĶ‚ä―•æķˆč€—æĩã€‚ 最ä―ģæ€§čƒ―čŊ•å›ū调æ•īä― įš„摄像朚įš„内į―Ū。 æ‰ū到äŧĨäļ‹é€‰éĄđåđķčŪūåŪšäŧ–äŧŽåĶ‚å›ū所įĪšã€‚ æ‰ūåˆ°ä― įš„摄像朚åŊäŧĨä―ŋį”Ļ åŧšįŦ‹åœĻ升æ‰Ŧ描äŧŠ įš„åŋč€…. äļ€äš›å‡æ‘„像朚čĶæą‚采į”Ļäļ€äļŠįŪĄį†å·Ĩ具äŧĨäŋŪæ”đå…ķ内éƒĻčŪūį―Ū。 åĶ‚æžœä― æ‰ūäļåˆ°ä― įš„æ‘„åƒæœšä― åŊäŧĨ尝čŊ• 提升čŪūåĪ‡Manager for Windows.", + "InputText1": "čŋ™éƒĻ分告čŊ‰ShinobiåĶ‚ä―•ä―ŋį”Ļæĩã€‚ 最ä―ģæ€§čƒ―čŊ•å›ū调æ•īä― įš„摄像朚įš„内į―Ū。 æ‰ū到äŧĨäļ‹é€‰éĄđåđķčŪūåŪšäŧ–äŧŽåĶ‚å›ū所įĪšã€‚ æ‰ūåˆ°ä― įš„摄像朚åŊäŧĨä―ŋį”Ļ åŧšįŦ‹åœĻ升æ‰Ŧ描äŧŠ įš„Shinobi. äļ€äš›å‡æ‘„像朚čĶæą‚采į”Ļäļ€äļŠįŪĄį†å·Ĩ具äŧĨäŋŪæ”đå…ķ内éƒĻčŪūį―Ū。 åĶ‚æžœä― æ‰ūäļåˆ°ä― įš„æ‘„åƒæœšä― åŊäŧĨ尝čŊ• 提升čŪūåĪ‡Manager for Windows.", "InputText2": "
  • Framerate(》): éŦ˜ïžš10-15ã€‹ïžŒä―Žïžš2-5》
  • æˆ‘æĄ†æžķįš„æ—ķé—īé—ī隔 80
  • æŊ”į‰đįŽ‡įąŧåž‹ïžš įĪū匚嚷åĪ(恒åŪšįš„æŊ”įŽ‡)
  • æŊ”į‰đįŽ‡ïžš é—ī256kbps-500kbps
", - "InputText3": "åĶ‚æžœä― éœ€čĶåļŪåŋ™æ‰ū凚æĨæ˜Ŋäŧ€äđˆæ ·įš„čū“å…Ĩåž‹ä― įš„摄像朚æ˜Ŋä― åŊäŧĨįœ‹įœ‹ 摄像朚įš„Urlæļ…单 äļŠįš„åŋč€…įš„į―‘įŦ™ã€‚", + "InputText3": "åĶ‚æžœä― éœ€čĶåļŪåŋ™æ‰ū凚æĨæ˜Ŋäŧ€äđˆæ ·įš„čū“å…Ĩåž‹ä― įš„摄像朚æ˜Ŋä― åŊäŧĨįœ‹įœ‹ 摄像朚įš„Urlæļ…单 äļŠįš„Shinobiįš„į―‘įŦ™ã€‚", "Invalid JSON": "无效JSON", - "InvalidJSONText": "čŊ·įĄŪäŋčŋ™æ˜Ŋäļ€äļŠæœ‰æ•ˆįš„JSONäļēåŋį›‘控配į―Ū。", + "InvalidJSONText": "čŊ·įĄŪäŋčŋ™æ˜Ŋäļ€äļŠæœ‰æ•ˆįš„Shinobi į›‘控配į―ŪJSONäļē。", "JPEG": "JPEG", "JPEG (Auto Enables JPEG API)": "JPEG(臩åŠĻä―ŋJPEG API)", "JPEG API": "JPEG API åŋŦį…§(cgi-bin)", @@ -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": "į™ŧå―•äŋĄå·įš„äš‹äŧķ åŪĒ户及有äļ€äū§", @@ -254,7 +255,7 @@ "MPEG-4 (.mp4 / .ts)": "MPEG-4(äļ­ã€‚mp4/.ts)", "MailError": "é‚Ūäŧķ错čŊŊïžšäļčƒ―发送į”ĩ子é‚Ūäŧķ、æĢ€æŸĨconf.手朚äļ­ã€‚ č·ģčŋ‡įš„äŧŧä―•åŠŸčƒ―įš„äūčĩ–é‚ŪåŊ„。", "Matches": "æŊ”čĩ›", - "Max Storage Amount": "最åĪ§å‚Ļ存量 åœĻ兆", + "Max Storage Amount": "最åĪ§å‚Ļ存量 兆", "Mode": "æĻĄåž", "Monitor": "į›‘控", "Monitor Added by user": "į›‘控äļ­åŠ å…Ĩįš„į”Ļ户。", @@ -272,7 +273,7 @@ "MonitorIdlingText": "į›‘控䞚čŪŪå·ēčŪĒ䚎įĐšé—ē。", "MonitorStoppedText": "į›‘控䞚čŪŪå·ēčĒŦäļ‹äŧĪ停æ­Ē。", "Monitors": "į›‘控", - "Monitors per row": "į›‘控æŊčĄŒ äļšįš„č’™åĪŠåĨ‡", + "Monitors per row": "į›‘控æŊčĄŒ 镜åĪīįŧ„æŽĨ", "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": "发įŽ°æå‡čŪūåĪ‡į―‘įŧœäđ‹åĪ–č‡Šå·ąæˆ–į•™äļ‹įš„įĐšį™―äŧĨæ‰Ŧæä― įš„前įš„į―‘įŧœã€‚
į”Ļ户名和åŊ†į åŊäŧĨį•™įĐšã€‚", + "ONVIF Scanner": "ONVIFčŪūåĪ‡æ‰Ŧ描äŧŠ", + "ONVIFnote": "åœĻæ‚Ļč‡Šå·ąįš„į―‘įŧœäđ‹åĪ–įš„į―‘įŧœäļŠå‘įŽ°ONVIFčŪūåĪ‡æˆ–å°†å…ķäŋį•™įĐšį™―äŧĨæ‰Ŧæå―“å‰į―‘įŧœã€‚
į”Ļ户名和åŊ†į åŊäŧĨį•™įĐšã€‚", "OpenCV Cascades": "čŊĨį‰ˆæœŽįš„į€‘åļƒ", "Order Streams": "饚æĩ", "Output Method": "čū“凚æ–đæģ•", "Password": "åŊ†į ", - "Password Again": "å†æŽĄåŊ†į ", + "Password Again": "å†æŽĄčū“å…ĨåŊ†į ", "Passwords don't match": "åŊ†į äļåŒđ配", "Paste JSON here.": "čīīJSONåœĻčŋ™é‡Œã€‚", "Path": "č·Ŋåū„", "Permissions": "权限", "Points": "į‚đ åœĻæ·ŧ加į‚đå‡ŧčūđįž˜äļŠįš„åĪščūđå―Ē。", - "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": "čīĻ量 1éŦ˜ïžŒ23ä―Ž", + "Quality": "éŸģčīĻ 1éŦ˜ïžŒ23ä―Ž", "Query": "æŸĨčŊĒ", "RAM": "RAM", + "Recent Videos": "čŋ‘æœŸč§†éĒ‘", "RTSP": "RTSP", "RTSP Transport": "RTSPčŋčū“", - "Range or Single": "范å›ī内或单", + "Range or Single": "范å›ī或单äļŠ", + "fieldTextIp": "范å›ī或单äļŠ", "Rate": "įŽ‡ (》)", "Record": "čŪ°å―•", "Record File Type": "čŪ°å―•æ–‡äŧķįš„įąŧ型", @@ -347,7 +350,7 @@ "Region Name": "地匚名į§°", "RegionNote": "į‚đ及äŋå­˜ïžŒå―“ä― æŒ‰äļ‹ äŋå­˜ åœĻ į›‘控čŪūį―Ū įŠ—åĢ。", "Regions": "匚域", - "Remember Me": "čŋ˜čŪ°åū—我", + "Remember Me": "čŪ°ä―æˆ‘åœĻæ­ĪčŪĄįŪ—朚įš„į™ŧå―•", "Reset Timer": "重į―ŪčŪĄæ—ķå™Ļ", "Restarting Process": "重新åŊåŠĻčŋ›įĻ‹", "Retry Connection": "čŊ•čŋžæŽĨ įš„æŽĄæ•°ïžŒå…čŪļåĪąčīĨ", @@ -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": "å―•åˆķ文äŧķäŋå­˜įš„ä―į―Ū。æ‚ĻåŊäŧĨä―ŋį”ĻaddStorage变量配į―Ūæ›īåĪšä―į―Ū。", + "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": "æ‚ĻåŊäŧĨåœĻShinobiHubäļŠäš†č§ĢåĶ‚ä―•ä―ŋį”ĻåŪƒã€‚", + "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": "æ­Īå·Ĩ具将åļŪåŠĐæ‚Ļ搜įīĒįĪū匚发åļƒįš„摄像朚配į―Ūã€‚éƒ―åœĻ ShinobiHubäļŠã€‚ä― äđŸåŊäŧĨčīīå‡šä― įš„čŋ™äžšåŊđįĪū匚有åūˆåĪ§įš„åļŪåŠĐ。", + "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": "æ­ĪåŠŸčƒ―é€‚į”Ļ䚎į§ŧåŠĻčŪļåŊčŊį”Ļ户。čĶčŽ·åū—APIåŊ†é’ĨčŊ·į™ŧå―•åˆ°æ‚Ļįš„Shinobi商嚗åļæˆ·åđķ创åŧšäļ€äļŠäļŽäŧŧä―•æīŧ跃įš„čŪĒ阅IDį›ļå…ģ联įš„åŊ†é’Ĩ。å­Ķäđ æ›īåĪšã€‚", + "HowToConnectDes2": "åĶ‚æžœæ‚ĻæƒģčŪŋé—Ūį§äšš(äļ“į”Ļ)P2PæœåŠĄå™ĻčŊ·åœĻShinobi商嚗åđķ通čŋ‡Live Chat小éƒĻäŧķäļŽæˆ‘äŧŽč”įģŧ", + "Download URL for Module": "æĻĄå—äļ‹č――į―‘址", + "Subdirectory for Module": "æĻĄå—子į›Ūå―•", + "Inside the downloaded package": "åœĻäļ‹č――įš„包äļ­", + "Download Plugins": "äļ‹č――插äŧķ", + "pluginDownloadText": "åœĻ文æĄĢäļ­äš†č§ĢåĶ‚ä―•åŪ‰čĢ…插äŧķ。", + "User": "į”Ļ户", + "Database": "数æŪåš“", + "Current Version": "å―“å‰į‰ˆæœŽ", + "Reset Form": "重į―ŪčĄĻ单", + "Default is Global value": "éŧ˜čŪĪäļšå…Ļåą€å€ž", + "fieldTextEventDays": "æļ…é™Ī前äŋį•™äš‹äŧķįš„åĪĐ数。", + "EncodingInterval": "å…ģé”Ūåļ§", + "ShinobiHub": "ShinobiHub" } diff --git a/libs/childNode/childUtils.js b/libs/childNode/childUtils.js index a2e70178..e1a9b7a5 100644 --- a/libs/childNode/childUtils.js +++ b/libs/childNode/childUtils.js @@ -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; } } diff --git a/libs/commander/workerv2.js b/libs/commander/workerv2.js index e4631b13..c0e49207 100644 --- a/libs/commander/workerv2.js +++ b/libs/commander/workerv2.js @@ -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() }) diff --git a/libs/control.js b/libs/control.js index dda466c9..fd1e1439 100644 --- a/libs/control.js +++ b/libs/control.js @@ -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 diff --git a/libs/control/onvif.js b/libs/control/onvif.js index a7953496..4b9d0980 100644 --- a/libs/control/onvif.js +++ b/libs/control/onvif.js @@ -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; diff --git a/libs/control/ptz.js b/libs/control/ptz.js index abdf0aec..8c2508d0 100644 --- a/libs/control/ptz.js +++ b/libs/control/ptz.js @@ -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, diff --git a/libs/database/utils.js b/libs/database/utils.js index 36f2903e..ea33e257 100644 --- a/libs/database/utils.js +++ b/libs/database/utils.js @@ -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; diff --git a/libs/events/onvif.js b/libs/events/onvif.js index 6f177fe8..90d524e6 100644 --- a/libs/events/onvif.js +++ b/libs/events/onvif.js @@ -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); } } } diff --git a/libs/events/utils.js b/libs/events/utils.js index 55c25bab..98e50cdb 100644 --- a/libs/events/utils.js +++ b/libs/events/utils.js @@ -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, diff --git a/libs/monitor/utils.js b/libs/monitor/utils.js index 80412166..5db663da 100644 --- a/libs/monitor/utils.js +++ b/libs/monitor/utils.js @@ -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 diff --git a/libs/notifications/discordBot.js b/libs/notifications/discordBot.js index 0f39e60a..d88f7e5b 100644 --- a/libs/notifications/discordBot.js +++ b/libs/notifications/discordBot.js @@ -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) } diff --git a/libs/notifications/matrix.js b/libs/notifications/matrix.js index 81e23cd7..9222c6d0 100644 --- a/libs/notifications/matrix.js +++ b/libs/notifications/matrix.js @@ -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, diff --git a/libs/notifications/pushover.js b/libs/notifications/pushover.js index e2ac79af..590752f3 100644 --- a/libs/notifications/pushover.js +++ b/libs/notifications/pushover.js @@ -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, }, [ { diff --git a/libs/notifications/telegram.js b/libs/notifications/telegram.js index c84629d0..466dc5f7 100644 --- a/libs/notifications/telegram.js +++ b/libs/notifications/telegram.js @@ -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) } diff --git a/libs/plugins/superUser.js b/libs/plugins/superUser.js index 434c420a..b6e9d333 100644 --- a/libs/plugins/superUser.js +++ b/libs/plugins/superUser.js @@ -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(); }) diff --git a/libs/startup.js b/libs/startup.js index 660ec1c1..0599309d 100644 --- a/libs/startup.js +++ b/libs/startup.js @@ -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({ diff --git a/libs/videoBrowser.js b/libs/videoBrowser.js index 5d0ea634..6b292446 100644 --- a/libs/videoBrowser.js +++ b/libs/videoBrowser.js @@ -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; } }); diff --git a/libs/videos.js b/libs/videos.js index 8bb9fede..af766161 100644 --- a/libs/videos.js +++ b/libs/videos.js @@ -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) diff --git a/libs/webServer.js b/libs/webServer.js index 3f611fa7..32dd7fe3 100644 --- a/libs/webServer.js +++ b/libs/webServer.js @@ -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', ] diff --git a/package-lock.json b/package-lock.json index c7f2f079..f3daef70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index ee579d19..b88c3718 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/tools/onvifGetStreamUri.js b/tools/onvifGetStreamUri.js new file mode 100644 index 00000000..cebcce0e --- /dev/null +++ b/tools/onvifGetStreamUri.js @@ -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 = ` + + + + + + `; + + 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 = ` + + + + + RTP-Unicast + + RTSP + + + ${firstProfileToken} + + + + `; + + 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(` + + + http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe + uuid:1000-3000-5000-70000000000000 + + http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous + + urn:schemas-xmlsoap-org:ws:2005:04:discovery + + + + dn:NetworkVideoTransmitter + + + +`); + +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)); + }); +}); diff --git a/web/assets/css/bs5.monitorMap.css b/web/assets/css/bs5.monitorMap.css new file mode 100644 index 00000000..9347547d --- /dev/null +++ b/web/assets/css/bs5.monitorMap.css @@ -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; +} diff --git a/web/assets/css/bs5.powerVideo.css b/web/assets/css/bs5.powerVideo.css deleted file mode 100644 index c3596f2a..00000000 --- a/web/assets/css/bs5.powerVideo.css +++ /dev/null @@ -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; -} diff --git a/web/assets/css/bs5.sideMenu.css b/web/assets/css/bs5.sideMenu.css index 8e9d2839..b3e86cdf 100644 --- a/web/assets/css/bs5.sideMenu.css +++ b/web/assets/css/bs5.sideMenu.css @@ -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 { diff --git a/web/assets/css/bs5.timeline.css b/web/assets/css/bs5.timeline.css new file mode 100644 index 00000000..d996d146 --- /dev/null +++ b/web/assets/css/bs5.timeline.css @@ -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% +} diff --git a/web/assets/css/dashboard.css b/web/assets/css/dashboard.css index 65f90949..e007b649 100644 --- a/web/assets/css/dashboard.css +++ b/web/assets/css/dashboard.css @@ -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; +} diff --git a/web/assets/js/bs5.dashboard-base.js b/web/assets/js/bs5.dashboard-base.js index d791a54a..0edb4b92 100644 --- a/web/assets/js/bs5.dashboard-base.js +++ b/web/assets/js/bs5.dashboard-base.js @@ -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(` ${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){ diff --git a/web/assets/js/bs5.liveGrid.js b/web/assets/js/bs5.liveGrid.js index b0847009..29bd4b60 100644 --- a/web/assets/js/bs5.liveGrid.js +++ b/web/assets/js/bs5.liveGrid.js @@ -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 diff --git a/web/assets/js/bs5.monitorMap.js b/web/assets/js/bs5.monitorMap.js new file mode 100644 index 00000000..df857eb5 --- /dev/null +++ b/web/assets/js/bs5.monitorMap.js @@ -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 = `
${lang['No Videos Found']}
` + } + theVideoList.html(html) + },1000) + }) + } + function buildPinPopupHtml(monitor){ + var embedUrl = buildEmbedUrl(monitor) + var html = ` +
+
${userHasSubscribed ? `` : `
${lang.activateRequiredLiveStream}
`}
+
+
+
+
+ ` + 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; + } + }) +}) diff --git a/web/assets/js/bs5.monitorMap.utils.js b/web/assets/js/bs5.monitorMap.utils.js new file mode 100644 index 00000000..3b696ab6 --- /dev/null +++ b/web/assets/js/bs5.monitorMap.utils.js @@ -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'; + } +} diff --git a/web/assets/js/bs5.monitorSettings.js b/web/assets/js/bs5.monitorSettings.js index 6f4f1ba4..cd1a5c8f 100644 --- a/web/assets/js/bs5.monitorSettings.js +++ b/web/assets/js/bs5.monitorSettings.js @@ -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) diff --git a/web/assets/js/bs5.monitorSettings.monitorMap.js b/web/assets/js/bs5.monitorSettings.monitorMap.js new file mode 100644 index 00000000..3733b8dd --- /dev/null +++ b/web/assets/js/bs5.monitorSettings.monitorMap.js @@ -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) + }) +}) diff --git a/web/assets/js/bs5.monitorsUtils.js b/web/assets/js/bs5.monitorsUtils.js index f3257162..c8b5fd08 100644 --- a/web/assets/js/bs5.monitorsUtils.js +++ b/web/assets/js/bs5.monitorsUtils.js @@ -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
${infoHtml}
- diff --git a/web/assets/js/bs5.powerVideo.js b/web/assets/js/bs5.powerVideo.js deleted file mode 100644 index 3bbe57ac..00000000 --- a/web/assets/js/bs5.powerVideo.js +++ /dev/null @@ -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 += `
-
${monitor.name}
-
${monitor.host}
-
-
` - }) - 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: `
- ${formattedTime(video.time, 'hh:mm:ss AA, DD-MM-YYYY')} -
-
-
-
`, - 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: `
${eventSlotTag}
`, - 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(`
${lang['Please Wait...']}
`) - 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(`
${lang['No Data']}
`) - } - },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 += `
` - if(matrix.tag)html += `${matrix.tag}${!isNaN(matrix.id) ? ` ${matrix.id}`: ''}` - html += '
' - }) - 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 = `
${lang['Region']} : ${event.details.name}
-
${lang['Confidence']} : ${event.details.confidence}
-
${lang['Plugin']} : ${event.details.plug}
` - 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(`
-
-
- - -
- -
-
-
-
-
-
-
`) - 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('') - } - } - 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 - } -}) diff --git a/web/assets/js/bs5.sideMenu.js b/web/assets/js/bs5.sideMenu.js index 5d808b47..5de79fb3 100644 --- a/web/assets/js/bs5.sideMenu.js +++ b/web/assets/js/bs5.sideMenu.js @@ -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 `` } function drawMonitorIconToMenu(item){ diff --git a/web/assets/js/bs5.timeline.js b/web/assets/js/bs5.timeline.js new file mode 100644 index 00000000..d4b2cf5f --- /dev/null +++ b/web/assets/js/bs5.timeline.js @@ -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 = `
` + 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 += `
+
+
+
` + preBufferHtml += `
` + }) + 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(``) + 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 ? `` : '') + } + 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(``) + } + 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: `${lang.addTagText}`, + }) + } + 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() + } +}) diff --git a/web/assets/js/bs5.videoPlayer.js b/web/assets/js/bs5.videoPlayer.js index 53532c2a..fe3cfebc 100644 --- a/web/assets/js/bs5.videoPlayer.js +++ b/web/assets/js/bs5.videoPlayer.js @@ -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 = `${lang['Video']} : ${loadedMonitors[video.mid] ? loadedMonitors[video.mid].name : lang['Monitor or Key does not exist.']} : ${formattedTime(video.time,true)}` + var tabLabel = `${lang['Video']} : ${loadedMonitors[video.mid] ? loadedMonitors[video.mid].name : lang['Monitor or Key does not exist.']} : ${timeAgo(video.time)}` var videoUrl = getLocation() + video.href var hasRows = video.events && video.events.length > 0 var loadedEvents = {} @@ -47,19 +47,24 @@ $(document).ready(function(){
-
- ${lang.Started} +
+
${lang.Started} ${timeAgo(video.time)}
${humanStartTime}
-
- ${lang.Ended} +
+
${lang.Ended} ${timeAgo(video.end)}
${humanEndTime}
+
+
${lang['Objects Found']}
+
${video.objects}
+
${lang.Download} ${permissionCheck('video_delete',video.mid) ? ` ${lang.Delete}` : ''} + ${permissionCheck('video_delete',video.mid) ? ` ${lang.Archive}` : ''}

ShinobiShop account and create a key associated to any active Subscription ID. Learn More.

-

If you would like to get access to a private (dedicated) P2P server please create an account at the ShinobiShop and contact us via the Live Chat widget.

+

<%- lang['HowToConnectDes1'] %>

+

<%- lang['HowToConnectDes2'] %>

<%- lang['How to Connect'] %>

diff --git a/web/pages/blocks/footer.ejs b/web/pages/blocks/footer.ejs index a3f700b4..ae6b1857 100644 --- a/web/pages/blocks/footer.ejs +++ b/web/pages/blocks/footer.ejs @@ -1,4 +1,5 @@ + @@ -18,6 +19,7 @@ + diff --git a/web/pages/blocks/header.ejs b/web/pages/blocks/header.ejs index d18d3614..4757d5e1 100644 --- a/web/pages/blocks/header.ejs +++ b/web/pages/blocks/header.ejs @@ -31,6 +31,8 @@ + + diff --git a/web/pages/blocks/home/menuSide.ejs b/web/pages/blocks/home/menuSide.ejs index b64f8017..4e822626 100644 --- a/web/pages/blocks/home/menuSide.ejs +++ b/web/pages/blocks/home/menuSide.ejs @@ -45,6 +45,7 @@
<% drawBlock(define.SideMenu.blocks.SideMenuAfterList) %> diff --git a/web/pages/blocks/home/monitorMap.ejs b/web/pages/blocks/home/monitorMap.ejs new file mode 100644 index 00000000..74a26d0c --- /dev/null +++ b/web/pages/blocks/home/monitorMap.ejs @@ -0,0 +1,23 @@ +
+
+ <% + 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) + }) + %> + +
+ + + + +
diff --git a/web/pages/blocks/home/powerVideo.ejs b/web/pages/blocks/home/powerVideo.ejs deleted file mode 100644 index 1ceffa75..00000000 --- a/web/pages/blocks/home/powerVideo.ejs +++ /dev/null @@ -1,19 +0,0 @@ -
-
- <% - 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) - }) %> -
- - - - -
diff --git a/web/pages/blocks/home/timeline.ejs b/web/pages/blocks/home/timeline.ejs new file mode 100644 index 00000000..19b023b3 --- /dev/null +++ b/web/pages/blocks/home/timeline.ejs @@ -0,0 +1,21 @@ +
+
+ <% + 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) + }) + %> + +
+ + +
diff --git a/web/pages/blocks/superCustomAutoLoadManager.ejs b/web/pages/blocks/superCustomAutoLoadManager.ejs index b2f90b25..61999c0a 100644 --- a/web/pages/blocks/superCustomAutoLoadManager.ejs +++ b/web/pages/blocks/superCustomAutoLoadManager.ejs @@ -1,16 +1,40 @@ -
-
-
- +
+
+
+
+ <%- lang['Download Modules'] %> +
+
+

<%- lang.moduleDownloadText %>

+
+ +
+
+
-
- -
-
- -
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
diff --git a/web/pages/factor.ejs b/web/pages/factor.ejs index cf032819..7f42d27b 100644 --- a/web/pages/factor.ejs +++ b/web/pages/factor.ejs @@ -2,7 +2,10 @@ <% 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 %> @@ -51,7 +54,7 @@
-
+
diff --git a/web/pages/super.ejs b/web/pages/super.ejs index 55233f13..3593ae59 100644 --- a/web/pages/super.ejs +++ b/web/pages/super.ejs @@ -137,7 +137,7 @@ From cb6be8eb7f65323a03f0c247dc71b20266dcfe0e Mon Sep 17 00:00:00 2001 From: Moe Date: Tue, 5 Sep 2023 11:01:22 -0700 Subject: [PATCH 3/4] Dindai Hollow + : Critical Fixes commit ba42ac2cd8feeca4d6c39a2314ab920aec2c48ce Author: Moe Date: Tue Sep 5 11:00:47 2023 -0700 update node.js installation method commit 00a387a80397e9f4218e83760ffbec4404387695 Author: Moe Date: Sun Sep 3 16:44:24 2023 -0700 fix segment faststart commit c1cdbb011ef436cf8040da1b4d533926e050a774 Author: Moe Date: Mon Aug 28 12:27:36 2023 -0700 fix doFatalErrorCatch in monitors/utils.js commit 29dcec2810c30bf6335b1508a234d7570c111291 Author: Moe Date: Fri Aug 25 21:27:59 2023 -0700 better notifications for telegram, matrix, and pushover commit 54f51557e34534a101cf08bdb13429a056338276 Author: Moe Date: Fri Aug 25 06:45:36 2023 -0700 discord notifications more verbose commit 1684ebe8ea0b42e3b8c41fb5d223f423d483de9a Author: Moe Alam Date: Sun Aug 20 09:53:20 2023 -0700 typo fix commit 60ec6c5f4d768a75d45940530e10c61dd03e0fe5 Author: Moe Alam Date: Sun Aug 20 09:46:51 2023 -0700 update cuda installers for 10.0 and 10.2 commit b33d3c98434df99ca8c0ba84822c9fa69a9731ee Author: Moe Alam Date: Sun Aug 20 09:46:23 2023 -0700 fix child node s.camera execution commit 7d04972596abaa4e45c8b4911cd431c90923ed2f Author: Moe Date: Sun Aug 13 15:15:33 2023 -0700 Show Detection on Timeline + add toggle to stop on most recent video end + default to hiding non-playing commit ff8f2c7c83ce037363c3b2fff067849a1c40c631 Author: Moe Date: Sun Aug 13 10:32:53 2023 -0700 Update README.md commit a9e8fe70d6bdf679ac1817999f094c91cb930713 Author: Moe Date: Sun Aug 13 10:32:48 2023 -0700 Add Play Until End of Video Option commit 602b9b1c4e25e157bf04042d8bf13a045c7fb1ae Author: Moe Date: Fri Aug 11 14:24:02 2023 -0700 update remote shell control and allow custom host/port These features are off by default and should only be enabled with the discretion of the server administrator. commit cf2283c709898479676d6b9e57e4dd45b91b5352 Merge: 8fff0722 e34c68fe Author: Moe Date: Wed Aug 9 08:18:29 2023 -0700 Merge branch 'dev' of https://gitlab.com/Shinobi-Systems/Shinobi into dev commit 8fff07227047a20636111a5312911233d0ed4368 Author: Moe Date: Wed Aug 9 08:17:30 2023 -0700 Add NVMPI to HW Accel Encoders commit e34c68fe30b7ce5d842d1c5a7dd0f42855e1dbc2 Merge: 743599bb 4b2fa2cd Author: Shinobi Systems Date: Wed Aug 9 04:22:13 2023 +0000 Merge branch 'mr_zh_language_20230808' into 'dev' ADD zh Language translate See merge request Shinobi-Systems/Shinobi!479 commit 4b2fa2cdb0a1eddb4d6a36695f6b6333d1a8a4d1 Author: qt10 Date: Tue Aug 8 10:45:02 2023 +0800 add zh language commit 743599bb5091d01f24b2712780c1da4c9060a41a Author: Moe Date: Tue Aug 8 09:38:31 2023 -0700 Auto Next Video on empty canvas, better auto scroll commit 3f08165fb892f827f0ec47f5c9f38f4d0b62ab29 Author: Moe Date: Mon Aug 7 17:02:25 2023 -0700 Jump to Next/Previous Video for Timeline commit c13215f05c5a01efff528bbf219d5500513d1f72 Merge: 4b8c06a7 7d585522 Author: Moe Date: Sun Aug 6 12:58:57 2023 -0700 Merge branch 'remove-powerVideo' into dev commit 4b8c06a74ef1d47161cf033ec2929c25007a1a48 Author: Moe Date: Sun Aug 6 12:58:36 2023 -0700 fix plugin "Enable" button after new download commit 393db4c8114994ceed06264d312f0d9d2c29b4ee Author: Moe Date: Sun Aug 6 12:58:08 2023 -0700 lightly update Custom Auto Load tab in Superuser commit 7d585522e0f077e646e9d7d16ac9451be5881c11 Author: Moe Date: Sun Aug 6 10:13:00 2023 -0700 remove power viewer files commit 4579a5ba18237bbb192cbc66481fc941b7c1909e Author: Moe Date: Sun Aug 6 08:01:16 2023 -0700 Revert "make Power Viewer optional by config parameter+" This reverts commit 2b8709756c6f24b365648ae959c43b5678ad986c. commit 2b8709756c6f24b365648ae959c43b5678ad986c Author: Moe Date: Sun Aug 6 07:56:46 2023 -0700 make Power Viewer optional by config parameter+ commit eac50a6bc00064708f1feb654f65a2b8d2e5d0d4 Author: Moe Date: Sun Aug 6 07:42:20 2023 -0700 minor ui update to videoPlayer and videosTable commit d21076f5b4dd087bf472eada7633a4cbed8956ba Author: Moe Date: Sat Aug 5 20:15:47 2023 -0700 dynamic tab links better appearance and position commit 6ab21955812d08385acea60da7984856033454d3 Author: Moe Date: Fri Aug 4 22:02:16 2023 -0700 timeline colors more blue commit f70d195ea2e455fb5c28eb57ae5f290bd76321f8 Author: Moe Date: Fri Aug 4 21:57:30 2023 -0700 make Power Viewer optional by config parameter commit bf277675f31e16f048c63c12f009c86a76955107 Author: Moe Date: Fri Aug 4 21:51:10 2023 -0700 timeline colors stand out more commit 46b14dba11f936ac2e9f9a927586ac9b860dcc90 Author: Moe Date: Fri Aug 4 21:43:00 2023 -0700 fix open-video class commit 493ee9678f7530f430aefca76042fa236f7bb13a Author: Moe Date: Fri Aug 4 16:49:26 2023 -0700 performance upgrade to timeline loader + make double click open the video in regular video player commit 3981dd003c295ef625a58e1220857bd46d3139e2 Author: Moe Date: Fri Aug 4 16:48:40 2023 -0700 move monitor map up in side menu commit df62adf09b14e7b0294bea7caac636146adcdc34 Author: Moe Date: Fri Aug 4 15:07:53 2023 -0700 fix Videos tab open from Monitors tab commit 9466b540d3cd440877d24b498df67449031a8d53 Author: Moe Date: Fri Aug 4 13:00:53 2023 -0700 fix timeline color references commit 49ed119244d779c06598baa48de623fba9caf693 Author: Moe Date: Fri Aug 4 13:00:39 2023 -0700 add Substream indicator in Live Grid commit 441c3242f20a436583c9e87f2b149a30989d4bf2 Merge: dea84ca4 02a7fec4 Author: Moe Date: Fri Aug 4 10:12:50 2023 -0700 Merge branch 'dev' of https://gitlab.com/Shinobi-Systems/Shinobi into dev commit dea84ca4dc56a92fe6768cb9e487b62e43292a25 Author: Moe Date: Fri Aug 4 10:12:47 2023 -0700 Fix #487 commit 02a7fec48f54f0de1548a1664ffa618e8607d885 Merge: 17f535b9 b4566ca8 Author: Moe Date: Fri Aug 4 17:00:58 2023 +0000 Merge branch 'super-timeline' into 'dev' (Super) Timeline, Power Viewer v10 See merge request Shinobi-Systems/Shinobi!477 commit b4566ca886169c554bae94e2384f3acd93c3ce67 Author: Moe Date: Fri Aug 4 17:00:58 2023 +0000 (Super) Timeline, Power Viewer v10 commit 17f535b9ec6287346dd051c6118fdcf49d7d09cf Merge: d56b92ad 152ab4f0 Author: Moe Date: Tue Aug 1 15:35:42 2023 +0000 Merge branch 'telegram-fix' into 'dev' Fix telegram notifications See merge request Shinobi-Systems/Shinobi!475 commit 152ab4f0d697aa8828c0a229899d298d8e08c368 Author: skvalex Date: Sun Jul 30 23:19:02 2023 +0700 Support multiple chat ids commit ea5f8ed0c8455b02bbd17915f94929dc9f54ff1c Author: skvalex Date: Sun Jul 30 03:30:35 2023 +0700 Fixed telegram videos that exceed 10 megabytes commit d56b92ada797d9c81a47af15465e367eecfb5d37 Merge: d00fe997 4f5b8fd1 Author: Moe Date: Sun Jul 30 17:53:03 2023 +0000 Merge branch 'timezone-fix' into 'dev' Fix video browser time zone used by HA plugin See merge request Shinobi-Systems/Shinobi!476 commit 4f5b8fd117d5dcfee94f131881e1507b66cf47c2 Author: skvalex Date: Sun Jul 30 11:52:48 2023 +0700 Fix video browser time zone used by HA plugin commit 6dd3024693d3fd5d4451312a7c796b074dee832a Author: Moe Date: Fri Jul 28 10:27:29 2023 -0700 fix an on start error commit de6bc75c69fc71273ecf57412e351df144189278 Author: Moe Date: Fri Jul 28 10:27:03 2023 -0700 double check onvif device on ptz commit 69957b832fc5e8fafc86d0adee20cc5ef4158841 Author: Moe Date: Tue Jul 25 15:54:19 2023 -0700 Create onvifGetStreamUri.js commit d00fe997cf0605ad6eb84a54450741c204c574dc Author: Moe Date: Sat Jul 22 12:14:35 2023 -0700 Update utils.js commit 21bb3e4f996b7ab037be15a799aaede51192d473 Author: Moe Date: Sat Jul 22 12:14:07 2023 -0700 fix log output for ptz and minor cleanup commit d9d14065b598b02ef9a8ab1cfff32dc0a8a78c76 Author: Moe Date: Sat Jul 22 12:11:58 2023 -0700 add catch for failed eventBasedRecording commit 70b17a36f4115aab3ea48c50dff5426fb8bf9599 Merge: e2235d28 da83dfce Author: Moe Date: Mon Jul 17 15:50:08 2023 +0000 Merge branch 'axis-lock' into 'dev' PTZ Control Adjustments See merge request Shinobi-Systems/Shinobi!474 commit da83dfce8e6a618f9b36cdc63d36d3d17d99e892 Author: Moe Date: Mon Jul 17 15:50:08 2023 +0000 PTZ Control Adjustments commit e2235d286a7562ef02c6f9942b1928bd614a0c53 Author: Moe Date: Fri Jul 14 22:31:33 2023 -0700 make plugins enabled by super start before monitors commit 8e066ed01691c2d52689c9bcb15c8c12cd1e2611 Author: Moe Date: Fri Jul 14 17:15:21 2023 -0700 symlink mysql to mariadb commit e1dd3e5be3003e7c3c691ed07ec099dcb468b386 Author: Moe Date: Fri Jul 14 17:14:26 2023 -0700 symlink mysql to mariadb commit 4a5478e016ca963a2cd2da8280c15dbe6ea8ea69 Author: Moe Date: Wed Jul 12 23:23:37 2023 -0700 minor cleanup commit 5f96223eeff30181f946dcf82f00cfdd54e2d147 Merge: fa8804a3 0abae0d9 Author: Moe Date: Mon Jul 10 05:08:39 2023 +0000 Merge branch 'camera-map' into 'dev' Monitor Map (Camera Map) See merge request Shinobi-Systems/Shinobi!473 commit 0abae0d95bd00e402a0b5fb6b101c3f1e2c5d511 Author: Moe Date: Mon Jul 10 05:08:39 2023 +0000 Monitor Map (Camera Map) commit fa8804a30da6d65703846d8d4f064fc9176b20d6 Author: Moe Date: Fri Jun 30 10:49:11 2023 -0700 fix monitor state loading into UI for edit commit 5d50f0d407479fa8c969d922b89717c8b82d33d0 Author: Moe Date: Sun Jun 25 23:24:12 2023 -0700 fix error on missing frameBuffer when saving snap commit 86af8676d374628ed61858f50ca97506ea7fbe94 Author: Moe Date: Sat Jun 24 22:56:40 2023 -0700 fix Idle mode status display commit 5eded56a6f2f06889b7dd140ce57bf4a5df4ab22 Author: Moe Date: Sat Jun 24 18:48:09 2023 -0700 fix accuracy mode draw if Tile Size empty commit a8501334775108876077130c751e92d3b53c327e Author: Moe Date: Sat Jun 24 18:45:56 2023 -0700 clean up region editor status indicator commit 99cfb0dc6181fb4b043198ff01ef5e152e44b422 Author: Moe Date: Tue Jun 20 11:06:58 2023 -0700 alter live stream on mobile launch (web) commit 8cc4210207ed7b608e0c7b6feb2a3bbf9404be74 Author: Moe Date: Tue Jun 20 11:06:35 2023 -0700 fix Recent Videos seek when no frames present commit 913d8e15a75adbb16166d519db17f3300c0a6d63 Author: Moe Date: Thu Jun 8 10:54:40 2023 -0700 Make Alert and Pop on Event into localStorage options commit df907208e759712f6e8f754c2c8c537e4cf3531f Author: Moe Date: Tue Jun 6 23:09:01 2023 -0700 Add "Mark" button to quickly label a video Add button and Keyboard command in LiveGrid. Press "Enter" in liveGrid to trigger all monitors that are in view. commit e4b3c3d7c649f4ff20e14376db9e5385bf82ed14 Author: Moe Date: Sat May 20 18:29:33 2023 -0700 update mp4frag to fix Poseidon over HTTP commit ffb511bd153592caffaaca99514e66181584cf2e Author: Moe Date: Sat May 20 18:25:28 2023 -0700 Manually merge !465 with some changes --- INSTALL/CentOS - Quick Install.sh | 3 +-- INSTALL/nodejs-redhat.sh | 2 ++ INSTALL/nodejs-ubuntu.sh | 8 ++++++++ INSTALL/now.sh | 2 +- INSTALL/rocky9-touchless.sh | 4 ++-- INSTALL/ubuntu-easyinstall.sh | 6 +----- INSTALL/ubuntu-touchless-iso.sh | 7 +------ INSTALL/ubuntu-touchless.sh | 7 +------ INSTALL/ubuntu.sh | 6 +----- libs/ffmpeg/builders.js | 2 +- 10 files changed, 19 insertions(+), 28 deletions(-) create mode 100644 INSTALL/nodejs-redhat.sh create mode 100644 INSTALL/nodejs-ubuntu.sh diff --git a/INSTALL/CentOS - Quick Install.sh b/INSTALL/CentOS - Quick Install.sh index b64aec48..1fb92339 100644 --- a/INSTALL/CentOS - Quick Install.sh +++ b/INSTALL/CentOS - Quick Install.sh @@ -85,8 +85,7 @@ echo "=========================================================" #Check if Node.js is installed if ! [ -x "$(command -v node)" ]; then echo "Node.js not found, installing..." - sudo curl --silent --location https://rpm.nodesource.com/setup_16.x | sudo bash - - sudo "$pkgmgr" install nodejs -y -q -e 0 + sh nodejs-redhat.sh else echo "Node.js is already installed..." echo "Version: $(node -v)" diff --git a/INSTALL/nodejs-redhat.sh b/INSTALL/nodejs-redhat.sh new file mode 100644 index 00000000..6d020624 --- /dev/null +++ b/INSTALL/nodejs-redhat.sh @@ -0,0 +1,2 @@ +sudo yum install --nogpgcheck https://rpm.nodesource.com/pub_16.x/nodistro/repo/nodesource-release-nodistro-1.noarch.rpm -y +sudo yum install --nogpgcheck nodejs -y diff --git a/INSTALL/nodejs-ubuntu.sh b/INSTALL/nodejs-ubuntu.sh new file mode 100644 index 00000000..0794bab6 --- /dev/null +++ b/INSTALL/nodejs-ubuntu.sh @@ -0,0 +1,8 @@ +sudo apt-get update +sudo apt-get install -y ca-certificates curl gnupg +sudo mkdir -p /etc/apt/keyrings +curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg +NODE_MAJOR=16 +echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list +sudo apt-get update +sudo apt-get install nodejs -y diff --git a/INSTALL/now.sh b/INSTALL/now.sh index 7da11795..9667a268 100644 --- a/INSTALL/now.sh +++ b/INSTALL/now.sh @@ -6,7 +6,7 @@ echo "If your OS is not on the list please refer to the docs." echo "========" echo "1. Ubuntu - Fast and Touchless" echo "2. Ubuntu - Advanced" -echo "3. Rocky 9 / CentOS 8 - Fast and Touchless" +echo "3. Alma Linux 9.2 / Rocky 9 / CentOS 8 - Fast and Touchless (Red Hat)" echo "4. CentOS" echo "5. MacOS" echo "6. FreeBSD" diff --git a/INSTALL/rocky9-touchless.sh b/INSTALL/rocky9-touchless.sh index 3eb8201e..33cf2246 100644 --- a/INSTALL/rocky9-touchless.sh +++ b/INSTALL/rocky9-touchless.sh @@ -44,8 +44,7 @@ echo "=========================================================" #Check if Node.js is installed if ! [ -x "$(command -v node)" ]; then echo "Node.js not found, installing..." - sudo curl --silent --location https://rpm.nodesource.com/setup_16.x | sudo bash - - sudo yum install nodejs -y -q -e 0 + sh nodejs-redhat.sh else echo "Node.js is already installed..." echo "Version: $(node -v)" @@ -93,6 +92,7 @@ sudo npm install pm2@latest -g sudo chmod -R 755 . touch INSTALL/installed.txt dos2unix INSTALL/shinobi +chmod +x INSTALL/shinobi ln -s INSTALL/shinobi /usr/bin/shinobi echo "=========================================================" diff --git a/INSTALL/ubuntu-easyinstall.sh b/INSTALL/ubuntu-easyinstall.sh index 5c5c72e2..24923370 100644 --- a/INSTALL/ubuntu-easyinstall.sh +++ b/INSTALL/ubuntu-easyinstall.sh @@ -3,11 +3,7 @@ echo "Shinobi - Do you want to Install Node.js?" echo "(y)es or (N)o" read -r nodejsinstall if [ "$nodejsinstall" = "y" ]; then - wget https://deb.nodesource.com/setup_12.x - chmod +x setup_12.x - ./setup_12.x - sudo apt install nodejs -y - rm setup_12.x + sh nodejs-ubuntu.sh fi #Detect Ubuntu Version diff --git a/INSTALL/ubuntu-touchless-iso.sh b/INSTALL/ubuntu-touchless-iso.sh index 8e43f4e8..3984e0b3 100644 --- a/INSTALL/ubuntu-touchless-iso.sh +++ b/INSTALL/ubuntu-touchless-iso.sh @@ -51,12 +51,7 @@ fi if ! [ -x "$(command -v node)" ]; then echo "=============" echo "Shinobi - Installing Node.js" - wget https://deb.nodesource.com/setup_12.x - chmod +x setup_12.x - ./setup_12.x - sudo apt install nodejs -y - sudo apt install node-pre-gyp -y - rm setup_12.x + sh nodejs-ubuntu.sh else echo "Node.js Found..." echo "Version : $(node -v)" diff --git a/INSTALL/ubuntu-touchless.sh b/INSTALL/ubuntu-touchless.sh index fef7b987..32435895 100644 --- a/INSTALL/ubuntu-touchless.sh +++ b/INSTALL/ubuntu-touchless.sh @@ -55,12 +55,7 @@ if ! [ -x "$(command -v ifconfig)" ]; then fi echo "=============" echo "Shinobi - Installing Node.js" -wget https://deb.nodesource.com/setup_16.x -chmod +x setup_16.x -./setup_16.x -sudo apt install nodejs -y -sudo apt install node-pre-gyp -y -rm setup_16.x +sh nodejs-ubuntu.sh if ! [ -x "$(command -v npm)" ]; then sudo apt install npm -y fi diff --git a/INSTALL/ubuntu.sh b/INSTALL/ubuntu.sh index df715eb8..74217841 100644 --- a/INSTALL/ubuntu.sh +++ b/INSTALL/ubuntu.sh @@ -47,11 +47,7 @@ fi if ! [ -x "$(command -v node)" ]; then echo "=============" echo "Shinobi - Installing Node.js" - wget https://deb.nodesource.com/setup_16.x - chmod +x setup_16.x - ./setup_16.x - sudo apt install nodejs -y - rm setup_16.x + sh nodejs-ubuntu.sh else echo "Node.js Found..." echo "Version : $(node -v)" diff --git a/libs/ffmpeg/builders.js b/libs/ffmpeg/builders.js index a7e4b0fe..727ee359 100644 --- a/libs/ffmpeg/builders.js +++ b/libs/ffmpeg/builders.js @@ -549,7 +549,7 @@ module.exports = (s,config,lang) => { } } if(videoExtIsMp4){ - customRecordingFlags.push(`-segment_format_options movflags=faststart+frag_keyframe+empty_moov`) + customRecordingFlags.push(`-segment_format_options movflags=faststart`) } if(videoCodec === 'h264_vaapi'){ recordingFilters.push('format=nv12,hwupload') From d60e5c5d4184d339a7ddfaf6b54b4d962aa3c1b0 Mon Sep 17 00:00:00 2001 From: Moe Date: Tue, 5 Sep 2023 11:13:27 -0700 Subject: [PATCH 4/4] Dindai Hollow + : Critical Fixes+ commit 411b88aa54812d7b7eb5a2e7bd723b5e2cf0936b Author: Moe Date: Tue Sep 5 11:11:59 2023 -0700 Dindai Hollow + : Critical Fixes+ commit ba42ac2cd8feeca4d6c39a2314ab920aec2c48ce Author: Moe Date: Tue Sep 5 11:00:47 2023 -0700 update node.js installation method commit 00a387a80397e9f4218e83760ffbec4404387695 Author: Moe Date: Sun Sep 3 16:44:24 2023 -0700 fix segment faststart commit c1cdbb011ef436cf8040da1b4d533926e050a774 Author: Moe Date: Mon Aug 28 12:27:36 2023 -0700 fix doFatalErrorCatch in monitors/utils.js commit 29dcec2810c30bf6335b1508a234d7570c111291 Author: Moe Date: Fri Aug 25 21:27:59 2023 -0700 better notifications for telegram, matrix, and pushover commit 54f51557e34534a101cf08bdb13429a056338276 Author: Moe Date: Fri Aug 25 06:45:36 2023 -0700 discord notifications more verbose commit 1684ebe8ea0b42e3b8c41fb5d223f423d483de9a Author: Moe Alam Date: Sun Aug 20 09:53:20 2023 -0700 typo fix commit 60ec6c5f4d768a75d45940530e10c61dd03e0fe5 Author: Moe Alam Date: Sun Aug 20 09:46:51 2023 -0700 update cuda installers for 10.0 and 10.2 commit b33d3c98434df99ca8c0ba84822c9fa69a9731ee Author: Moe Alam Date: Sun Aug 20 09:46:23 2023 -0700 fix child node s.camera execution commit 7d04972596abaa4e45c8b4911cd431c90923ed2f Author: Moe Date: Sun Aug 13 15:15:33 2023 -0700 Show Detection on Timeline + add toggle to stop on most recent video end + default to hiding non-playing commit ff8f2c7c83ce037363c3b2fff067849a1c40c631 Author: Moe Date: Sun Aug 13 10:32:53 2023 -0700 Update README.md commit a9e8fe70d6bdf679ac1817999f094c91cb930713 Author: Moe Date: Sun Aug 13 10:32:48 2023 -0700 Add Play Until End of Video Option commit 602b9b1c4e25e157bf04042d8bf13a045c7fb1ae Author: Moe Date: Fri Aug 11 14:24:02 2023 -0700 update remote shell control and allow custom host/port These features are off by default and should only be enabled with the discretion of the server administrator. commit cf2283c709898479676d6b9e57e4dd45b91b5352 Merge: 8fff0722 e34c68fe Author: Moe Date: Wed Aug 9 08:18:29 2023 -0700 Merge branch 'dev' of https://gitlab.com/Shinobi-Systems/Shinobi into dev commit 8fff07227047a20636111a5312911233d0ed4368 Author: Moe Date: Wed Aug 9 08:17:30 2023 -0700 Add NVMPI to HW Accel Encoders commit e34c68fe30b7ce5d842d1c5a7dd0f42855e1dbc2 Merge: 743599bb 4b2fa2cd Author: Shinobi Systems Date: Wed Aug 9 04:22:13 2023 +0000 Merge branch 'mr_zh_language_20230808' into 'dev' ADD zh Language translate See merge request Shinobi-Systems/Shinobi!479 commit 4b2fa2cdb0a1eddb4d6a36695f6b6333d1a8a4d1 Author: qt10 Date: Tue Aug 8 10:45:02 2023 +0800 add zh language commit 743599bb5091d01f24b2712780c1da4c9060a41a Author: Moe Date: Tue Aug 8 09:38:31 2023 -0700 Auto Next Video on empty canvas, better auto scroll commit 3f08165fb892f827f0ec47f5c9f38f4d0b62ab29 Author: Moe Date: Mon Aug 7 17:02:25 2023 -0700 Jump to Next/Previous Video for Timeline commit c13215f05c5a01efff528bbf219d5500513d1f72 Merge: 4b8c06a7 7d585522 Author: Moe Date: Sun Aug 6 12:58:57 2023 -0700 Merge branch 'remove-powerVideo' into dev commit 4b8c06a74ef1d47161cf033ec2929c25007a1a48 Author: Moe Date: Sun Aug 6 12:58:36 2023 -0700 fix plugin "Enable" button after new download commit 393db4c8114994ceed06264d312f0d9d2c29b4ee Author: Moe Date: Sun Aug 6 12:58:08 2023 -0700 lightly update Custom Auto Load tab in Superuser commit 7d585522e0f077e646e9d7d16ac9451be5881c11 Author: Moe Date: Sun Aug 6 10:13:00 2023 -0700 remove power viewer files commit 4579a5ba18237bbb192cbc66481fc941b7c1909e Author: Moe Date: Sun Aug 6 08:01:16 2023 -0700 Revert "make Power Viewer optional by config parameter+" This reverts commit 2b8709756c6f24b365648ae959c43b5678ad986c. commit 2b8709756c6f24b365648ae959c43b5678ad986c Author: Moe Date: Sun Aug 6 07:56:46 2023 -0700 make Power Viewer optional by config parameter+ commit eac50a6bc00064708f1feb654f65a2b8d2e5d0d4 Author: Moe Date: Sun Aug 6 07:42:20 2023 -0700 minor ui update to videoPlayer and videosTable commit d21076f5b4dd087bf472eada7633a4cbed8956ba Author: Moe Date: Sat Aug 5 20:15:47 2023 -0700 dynamic tab links better appearance and position commit 6ab21955812d08385acea60da7984856033454d3 Author: Moe Date: Fri Aug 4 22:02:16 2023 -0700 timeline colors more blue commit f70d195ea2e455fb5c28eb57ae5f290bd76321f8 Author: Moe Date: Fri Aug 4 21:57:30 2023 -0700 make Power Viewer optional by config parameter commit bf277675f31e16f048c63c12f009c86a76955107 Author: Moe Date: Fri Aug 4 21:51:10 2023 -0700 timeline colors stand out more commit 46b14dba11f936ac2e9f9a927586ac9b860dcc90 Author: Moe Date: Fri Aug 4 21:43:00 2023 -0700 fix open-video class commit 493ee9678f7530f430aefca76042fa236f7bb13a Author: Moe Date: Fri Aug 4 16:49:26 2023 -0700 performance upgrade to timeline loader + make double click open the video in regular video player commit 3981dd003c295ef625a58e1220857bd46d3139e2 Author: Moe Date: Fri Aug 4 16:48:40 2023 -0700 move monitor map up in side menu commit df62adf09b14e7b0294bea7caac636146adcdc34 Author: Moe Date: Fri Aug 4 15:07:53 2023 -0700 fix Videos tab open from Monitors tab commit 9466b540d3cd440877d24b498df67449031a8d53 Author: Moe Date: Fri Aug 4 13:00:53 2023 -0700 fix timeline color references commit 49ed119244d779c06598baa48de623fba9caf693 Author: Moe Date: Fri Aug 4 13:00:39 2023 -0700 add Substream indicator in Live Grid commit 441c3242f20a436583c9e87f2b149a30989d4bf2 Merge: dea84ca4 02a7fec4 Author: Moe Date: Fri Aug 4 10:12:50 2023 -0700 Merge branch 'dev' of https://gitlab.com/Shinobi-Systems/Shinobi into dev commit dea84ca4dc56a92fe6768cb9e487b62e43292a25 Author: Moe Date: Fri Aug 4 10:12:47 2023 -0700 Fix #487 commit 02a7fec48f54f0de1548a1664ffa618e8607d885 Merge: 17f535b9 b4566ca8 Author: Moe Date: Fri Aug 4 17:00:58 2023 +0000 Merge branch 'super-timeline' into 'dev' (Super) Timeline, Power Viewer v10 See merge request Shinobi-Systems/Shinobi!477 commit b4566ca886169c554bae94e2384f3acd93c3ce67 Author: Moe Date: Fri Aug 4 17:00:58 2023 +0000 (Super) Timeline, Power Viewer v10 commit 17f535b9ec6287346dd051c6118fdcf49d7d09cf Merge: d56b92ad 152ab4f0 Author: Moe Date: Tue Aug 1 15:35:42 2023 +0000 Merge branch 'telegram-fix' into 'dev' Fix telegram notifications See merge request Shinobi-Systems/Shinobi!475 commit 152ab4f0d697aa8828c0a229899d298d8e08c368 Author: skvalex Date: Sun Jul 30 23:19:02 2023 +0700 Support multiple chat ids commit ea5f8ed0c8455b02bbd17915f94929dc9f54ff1c Author: skvalex Date: Sun Jul 30 03:30:35 2023 +0700 Fixed telegram videos that exceed 10 megabytes commit d56b92ada797d9c81a47af15465e367eecfb5d37 Merge: d00fe997 4f5b8fd1 Author: Moe Date: Sun Jul 30 17:53:03 2023 +0000 Merge branch 'timezone-fix' into 'dev' Fix video browser time zone used by HA plugin See merge request Shinobi-Systems/Shinobi!476 commit 4f5b8fd117d5dcfee94f131881e1507b66cf47c2 Author: skvalex Date: Sun Jul 30 11:52:48 2023 +0700 Fix video browser time zone used by HA plugin commit 6dd3024693d3fd5d4451312a7c796b074dee832a Author: Moe Date: Fri Jul 28 10:27:29 2023 -0700 fix an on start error commit de6bc75c69fc71273ecf57412e351df144189278 Author: Moe Date: Fri Jul 28 10:27:03 2023 -0700 double check onvif device on ptz commit 69957b832fc5e8fafc86d0adee20cc5ef4158841 Author: Moe Date: Tue Jul 25 15:54:19 2023 -0700 Create onvifGetStreamUri.js commit d00fe997cf0605ad6eb84a54450741c204c574dc Author: Moe Date: Sat Jul 22 12:14:35 2023 -0700 Update utils.js commit 21bb3e4f996b7ab037be15a799aaede51192d473 Author: Moe Date: Sat Jul 22 12:14:07 2023 -0700 fix log output for ptz and minor cleanup commit d9d14065b598b02ef9a8ab1cfff32dc0a8a78c76 Author: Moe Date: Sat Jul 22 12:11:58 2023 -0700 add catch for failed eventBasedRecording commit 70b17a36f4115aab3ea48c50dff5426fb8bf9599 Merge: e2235d28 da83dfce Author: Moe Date: Mon Jul 17 15:50:08 2023 +0000 Merge branch 'axis-lock' into 'dev' PTZ Control Adjustments See merge request Shinobi-Systems/Shinobi!474 commit da83dfce8e6a618f9b36cdc63d36d3d17d99e892 Author: Moe Date: Mon Jul 17 15:50:08 2023 +0000 PTZ Control Adjustments commit e2235d286a7562ef02c6f9942b1928bd614a0c53 Author: Moe Date: Fri Jul 14 22:31:33 2023 -0700 make plugins enabled by super start before monitors commit 8e066ed01691c2d52689c9bcb15c8c12cd1e2611 Author: Moe Date: Fri Jul 14 17:15:21 2023 -0700 symlink mysql to mariadb commit e1dd3e5be3003e7c3c691ed07ec099dcb468b386 Author: Moe Date: Fri Jul 14 17:14:26 2023 -0700 symlink mysql to mariadb commit 4a5478e016ca963a2cd2da8280c15dbe6ea8ea69 Author: Moe Date: Wed Jul 12 23:23:37 2023 -0700 minor cleanup commit 5f96223eeff30181f946dcf82f00cfdd54e2d147 Merge: fa8804a3 0abae0d9 Author: Moe Date: Mon Jul 10 05:08:39 2023 +0000 Merge branch 'camera-map' into 'dev' Monitor Map (Camera Map) See merge request Shinobi-Systems/Shinobi!473 commit 0abae0d95bd00e402a0b5fb6b101c3f1e2c5d511 Author: Moe Date: Mon Jul 10 05:08:39 2023 +0000 Monitor Map (Camera Map) commit fa8804a30da6d65703846d8d4f064fc9176b20d6 Author: Moe Date: Fri Jun 30 10:49:11 2023 -0700 fix monitor state loading into UI for edit commit 5d50f0d407479fa8c969d922b89717c8b82d33d0 Author: Moe Date: Sun Jun 25 23:24:12 2023 -0700 fix error on missing frameBuffer when saving snap commit 86af8676d374628ed61858f50ca97506ea7fbe94 Author: Moe Date: Sat Jun 24 22:56:40 2023 -0700 fix Idle mode status display commit 5eded56a6f2f06889b7dd140ce57bf4a5df4ab22 Author: Moe Date: Sat Jun 24 18:48:09 2023 -0700 fix accuracy mode draw if Tile Size empty commit a8501334775108876077130c751e92d3b53c327e Author: Moe Date: Sat Jun 24 18:45:56 2023 -0700 clean up region editor status indicator commit 99cfb0dc6181fb4b043198ff01ef5e152e44b422 Author: Moe Date: Tue Jun 20 11:06:58 2023 -0700 alter live stream on mobile launch (web) commit 8cc4210207ed7b608e0c7b6feb2a3bbf9404be74 Author: Moe Date: Tue Jun 20 11:06:35 2023 -0700 fix Recent Videos seek when no frames present commit 913d8e15a75adbb16166d519db17f3300c0a6d63 Author: Moe Date: Thu Jun 8 10:54:40 2023 -0700 Make Alert and Pop on Event into localStorage options commit df907208e759712f6e8f754c2c8c537e4cf3531f Author: Moe Date: Tue Jun 6 23:09:01 2023 -0700 Add "Mark" button to quickly label a video Add button and Keyboard command in LiveGrid. Press "Enter" in liveGrid to trigger all monitors that are in view. commit e4b3c3d7c649f4ff20e14376db9e5385bf82ed14 Author: Moe Date: Sat May 20 18:29:33 2023 -0700 update mp4frag to fix Poseidon over HTTP commit ffb511bd153592caffaaca99514e66181584cf2e Author: Moe Date: Sat May 20 18:25:28 2023 -0700 Manually merge !465 with some changes --- INSTALL/CentOS - Quick Install.sh | 3 ++- INSTALL/rocky9-touchless.sh | 3 ++- INSTALL/ubuntu-easyinstall.sh | 3 ++- INSTALL/ubuntu-touchless-iso.sh | 3 ++- INSTALL/ubuntu-touchless.sh | 4 ++-- INSTALL/ubuntu.sh | 3 ++- 6 files changed, 12 insertions(+), 7 deletions(-) diff --git a/INSTALL/CentOS - Quick Install.sh b/INSTALL/CentOS - Quick Install.sh index 1fb92339..7bed5f90 100644 --- a/INSTALL/CentOS - Quick Install.sh +++ b/INSTALL/CentOS - Quick Install.sh @@ -2,6 +2,7 @@ #Identify version of CentOS version=$(rpm --eval %{centos_ver}) +DIR=$(dirname $0) #Set script to use dnf or yum if [ "$version" = 7 ]; then @@ -85,7 +86,7 @@ echo "=========================================================" #Check if Node.js is installed if ! [ -x "$(command -v node)" ]; then echo "Node.js not found, installing..." - sh nodejs-redhat.sh + sh $DIR/nodejs-redhat.sh else echo "Node.js is already installed..." echo "Version: $(node -v)" diff --git a/INSTALL/rocky9-touchless.sh b/INSTALL/rocky9-touchless.sh index 33cf2246..c9a9a68b 100644 --- a/INSTALL/rocky9-touchless.sh +++ b/INSTALL/rocky9-touchless.sh @@ -1,4 +1,5 @@ #!/bin/bash +DIR=$(dirname $0) if ! [ -x "$(command -v dnf)" ]; then echo 'This installer does not run on systems without "dnf" package manager. Try "CentOS - Quick Install"' exit 1 @@ -44,7 +45,7 @@ echo "=========================================================" #Check if Node.js is installed if ! [ -x "$(command -v node)" ]; then echo "Node.js not found, installing..." - sh nodejs-redhat.sh + sh $DIR/nodejs-redhat.sh else echo "Node.js is already installed..." echo "Version: $(node -v)" diff --git a/INSTALL/ubuntu-easyinstall.sh b/INSTALL/ubuntu-easyinstall.sh index 24923370..4b4568b3 100644 --- a/INSTALL/ubuntu-easyinstall.sh +++ b/INSTALL/ubuntu-easyinstall.sh @@ -1,9 +1,10 @@ #!/bin/bash +DIR=$(dirname $0) echo "Shinobi - Do you want to Install Node.js?" echo "(y)es or (N)o" read -r nodejsinstall if [ "$nodejsinstall" = "y" ]; then - sh nodejs-ubuntu.sh + sh $DIR/nodejs-ubuntu.sh fi #Detect Ubuntu Version diff --git a/INSTALL/ubuntu-touchless-iso.sh b/INSTALL/ubuntu-touchless-iso.sh index 3984e0b3..1d24977f 100644 --- a/INSTALL/ubuntu-touchless-iso.sh +++ b/INSTALL/ubuntu-touchless-iso.sh @@ -1,4 +1,5 @@ #!/bin/bash +DIR=$(dirname $0) echo "=========================================================" echo "==!! Shinobi : The Open Source CCTV and NVR Solution !!==" echo "=========================================================" @@ -51,7 +52,7 @@ fi if ! [ -x "$(command -v node)" ]; then echo "=============" echo "Shinobi - Installing Node.js" - sh nodejs-ubuntu.sh + sh $DIR/nodejs-ubuntu.sh else echo "Node.js Found..." echo "Version : $(node -v)" diff --git a/INSTALL/ubuntu-touchless.sh b/INSTALL/ubuntu-touchless.sh index 32435895..d60a9fbc 100644 --- a/INSTALL/ubuntu-touchless.sh +++ b/INSTALL/ubuntu-touchless.sh @@ -1,4 +1,5 @@ #!/bin/bash +DIR=$(dirname $0) echo "=========================================================" echo "==!! Shinobi : The Open Source CCTV and NVR Solution !!==" echo "=========================================================" @@ -55,7 +56,7 @@ if ! [ -x "$(command -v ifconfig)" ]; then fi echo "=============" echo "Shinobi - Installing Node.js" -sh nodejs-ubuntu.sh +sh $DIR/nodejs-ubuntu.sh if ! [ -x "$(command -v npm)" ]; then sudo apt install npm -y fi @@ -89,7 +90,6 @@ sqluser="root" sudo mysql -e "source sql/user.sql" || true echo "=============" echo "Shinobi - Install NPM Libraries" -sudo npm i npm -g sudo npm install --unsafe-perm # sudo npm audit fix --force echo "=============" diff --git a/INSTALL/ubuntu.sh b/INSTALL/ubuntu.sh index 74217841..1570574d 100644 --- a/INSTALL/ubuntu.sh +++ b/INSTALL/ubuntu.sh @@ -1,4 +1,5 @@ #!/bin/bash +DIR=$(dirname $0) echo "=========================================================" echo "==!! Shinobi : The Open Source CCTV and NVR Solution !!==" echo "=========================================================" @@ -47,7 +48,7 @@ fi if ! [ -x "$(command -v node)" ]; then echo "=============" echo "Shinobi - Installing Node.js" - sh nodejs-ubuntu.sh + sh $DIR/nodejs-ubuntu.sh else echo "Node.js Found..." echo "Version : $(node -v)"