Merge branch 'dev' into 'master'

Fragrant Foliage

See merge request Shinobi-Systems/Shinobi!503
alpha
Moe 2024-08-09 18:03:36 +00:00
commit 947de85a10
54 changed files with 3496 additions and 4457 deletions

View File

@ -1,239 +0,0 @@
docker-latest-build:
image: docker:latest
stage: build
variables:
EXCLUDE_DB: "false"
BASE_IMAGE: "node:18-buster-slim"
services:
- docker:dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
- |
BRANCH_PREFIX="$(if [ $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH ]; then echo ${CI_COMMIT_REF_SLUG} ; else echo 'latest'; fi)"
NO_DB_SUFIX="$(${EXCLUDE_DB} && echo '-no-db' || echo '')"
BASE_IMAGE_ARCH="$(echo ${BASE_IMAGE} | cut -d'/' -f1)"
BASE_IMAGE_TYPE="$(echo ${BASE_IMAGE} | cut -d'/' -f2)"
ARCH_TYPE="$(if [ $BASE_IMAGE_ARCH != $BASE_IMAGE_TYPE ]; then echo '-'${BASE_IMAGE_ARCH}; else echo ''; fi )"
export IMAGE_NAME="$CI_REGISTRY_IMAGE:${BRANCH_PREFIX}${ARCH_TYPE}${NO_DB_SUFIX}"
echo "Running on branch '${CI_COMMIT_BRANCH}', Image: ${IMAGE_NAME}" ;
- docker build --pull --build-arg EXCLUDE_DB="${EXCLUDE_DB}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -t "${IMAGE_NAME}" . -f "Dockerfile"
- docker push "${IMAGE_NAME}"
rules:
- if: $CI_COMMIT_BRANCH != "master" && $CI_COMMIT_BRANCH != "main" && $CI_COMMIT_BRANCH != "dev"
when: never
- if: $CI_COMMIT_BRANCH
exists:
- Dockerfile
docker-latest-no-db-build:
image: docker:latest
stage: build
variables:
EXCLUDE_DB: "true"
BASE_IMAGE: "node:18-buster-slim"
services:
- docker:dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
- |
BRANCH_PREFIX="$(if [ $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH ]; then echo ${CI_COMMIT_REF_SLUG} ; else echo 'latest'; fi)"
NO_DB_SUFIX="$(${EXCLUDE_DB} && echo '-no-db' || echo '')"
BASE_IMAGE_ARCH="$(echo ${BASE_IMAGE} | cut -d'/' -f1)"
BASE_IMAGE_TYPE="$(echo ${BASE_IMAGE} | cut -d'/' -f2)"
ARCH_TYPE="$(if [ $BASE_IMAGE_ARCH != $BASE_IMAGE_TYPE ]; then echo '-'${BASE_IMAGE_ARCH}; else echo ''; fi )"
export IMAGE_NAME="$CI_REGISTRY_IMAGE:${BRANCH_PREFIX}${ARCH_TYPE}${NO_DB_SUFIX}"
echo "Running on branch '${CI_COMMIT_BRANCH}', Image: ${IMAGE_NAME}" ;
- docker build --pull --build-arg EXCLUDE_DB="${EXCLUDE_DB}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -t "${IMAGE_NAME}" . -f "Dockerfile"
- docker push "${IMAGE_NAME}"
rules:
- if: $CI_COMMIT_BRANCH != "master" && $CI_COMMIT_BRANCH != "main" && $CI_COMMIT_BRANCH != "dev"
when: never
- if: $CI_COMMIT_BRANCH
exists:
- Dockerfile
docker-arm32v7-build:
image: docker:latest
stage: build
variables:
EXCLUDE_DB: "false"
BASE_IMAGE: "arm32v7/node:18-buster-slim"
services:
- docker:dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
- |
BRANCH_PREFIX="$(if [ $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH ]; then echo ${CI_COMMIT_REF_SLUG} ; else echo 'latest'; fi)"
NO_DB_SUFIX="$(${EXCLUDE_DB} && echo '-no-db' || echo '')"
BASE_IMAGE_ARCH="$(echo ${BASE_IMAGE} | cut -d'/' -f1)"
BASE_IMAGE_TYPE="$(echo ${BASE_IMAGE} | cut -d'/' -f2)"
ARCH_TYPE="$(if [ $BASE_IMAGE_ARCH != $BASE_IMAGE_TYPE ]; then echo '-'${BASE_IMAGE_ARCH}; else echo ''; fi )"
export IMAGE_NAME="$CI_REGISTRY_IMAGE:${BRANCH_PREFIX}${ARCH_TYPE}${NO_DB_SUFIX}"
echo "Running on branch '${CI_COMMIT_BRANCH}', Image: ${IMAGE_NAME}" ;
- docker build --pull --build-arg EXCLUDE_DB="${EXCLUDE_DB}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -t "${IMAGE_NAME}" . -f "Dockerfile"
- docker push "${IMAGE_NAME}"
rules:
- if: $CI_COMMIT_BRANCH != "master" && $CI_COMMIT_BRANCH != "main" && $CI_COMMIT_BRANCH != "dev"
when: never
- if: $CI_COMMIT_BRANCH
exists:
- Dockerfile
docker-arm32v7-no-db-build:
image: docker:latest
stage: build
variables:
EXCLUDE_DB: "true"
BASE_IMAGE: "arm32v7/node:18-buster-slim"
services:
- docker:dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
- |
BRANCH_PREFIX="$(if [ $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH ]; then echo ${CI_COMMIT_REF_SLUG} ; else echo 'latest'; fi)"
NO_DB_SUFIX="$(${EXCLUDE_DB} && echo '-no-db' || echo '')"
BASE_IMAGE_ARCH="$(echo ${BASE_IMAGE} | cut -d'/' -f1)"
BASE_IMAGE_TYPE="$(echo ${BASE_IMAGE} | cut -d'/' -f2)"
ARCH_TYPE="$(if [ $BASE_IMAGE_ARCH != $BASE_IMAGE_TYPE ]; then echo '-'${BASE_IMAGE_ARCH}; else echo ''; fi )"
export IMAGE_NAME="$CI_REGISTRY_IMAGE:${BRANCH_PREFIX}${ARCH_TYPE}${NO_DB_SUFIX}"
echo "Running on branch '${CI_COMMIT_BRANCH}', Image: ${IMAGE_NAME}" ;
- docker build --pull --build-arg EXCLUDE_DB="${EXCLUDE_DB}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -t "${IMAGE_NAME}" . -f "Dockerfile"
- docker push "${IMAGE_NAME}"
rules:
- if: $CI_COMMIT_BRANCH != "master" && $CI_COMMIT_BRANCH != "main" && $CI_COMMIT_BRANCH != "dev"
when: never
- if: $CI_COMMIT_BRANCH
exists:
- Dockerfile
docker-arm64v8-build:
image: docker:latest
stage: build
variables:
EXCLUDE_DB: "false"
BASE_IMAGE: "arm64v8/node:18-buster-slim"
services:
- docker:dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
- |
BRANCH_PREFIX="$(if [ $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH ]; then echo ${CI_COMMIT_REF_SLUG} ; else echo 'latest'; fi)"
NO_DB_SUFIX="$(${EXCLUDE_DB} && echo '-no-db' || echo '')"
BASE_IMAGE_ARCH="$(echo ${BASE_IMAGE} | cut -d'/' -f1)"
BASE_IMAGE_TYPE="$(echo ${BASE_IMAGE} | cut -d'/' -f2)"
ARCH_TYPE="$(if [ $BASE_IMAGE_ARCH != $BASE_IMAGE_TYPE ]; then echo '-'${BASE_IMAGE_ARCH}; else echo ''; fi )"
export IMAGE_NAME="$CI_REGISTRY_IMAGE:${BRANCH_PREFIX}${ARCH_TYPE}${NO_DB_SUFIX}"
echo "Running on branch '${CI_COMMIT_BRANCH}', Image: ${IMAGE_NAME}" ;
- docker build --pull --build-arg EXCLUDE_DB="${EXCLUDE_DB}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -t "${IMAGE_NAME}" . -f "Dockerfile"
- docker push "${IMAGE_NAME}"
rules:
- if: $CI_COMMIT_BRANCH != "master" && $CI_COMMIT_BRANCH != "main" && $CI_COMMIT_BRANCH != "dev"
when: never
- if: $CI_COMMIT_BRANCH
exists:
- Dockerfile
docker-arm64v8-no-db-build:
image: docker:latest
stage: build
variables:
EXCLUDE_DB: "true"
BASE_IMAGE: "arm64v8/node:18-buster-slim"
services:
- docker:dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
- |
BRANCH_PREFIX="$(if [ $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH ]; then echo ${CI_COMMIT_REF_SLUG} ; else echo 'latest'; fi)"
NO_DB_SUFIX="$(${EXCLUDE_DB} && echo '-no-db' || echo '')"
BASE_IMAGE_ARCH="$(echo ${BASE_IMAGE} | cut -d'/' -f1)"
BASE_IMAGE_TYPE="$(echo ${BASE_IMAGE} | cut -d'/' -f2)"
ARCH_TYPE="$(if [ $BASE_IMAGE_ARCH != $BASE_IMAGE_TYPE ]; then echo '-'${BASE_IMAGE_ARCH}; else echo ''; fi )"
export IMAGE_NAME="$CI_REGISTRY_IMAGE:${BRANCH_PREFIX}${ARCH_TYPE}${NO_DB_SUFIX}"
echo "Running on branch '${CI_COMMIT_BRANCH}', Image: ${IMAGE_NAME}" ;
- docker build --pull --build-arg EXCLUDE_DB="${EXCLUDE_DB}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -t "${IMAGE_NAME}" . -f "Dockerfile"
- docker push "${IMAGE_NAME}"
rules:
- if: $CI_COMMIT_BRANCH != "master" && $CI_COMMIT_BRANCH != "main" && $CI_COMMIT_BRANCH != "dev"
when: never
- if: $CI_COMMIT_BRANCH
exists:
- Dockerfile
docker-nvidia-build:
image: docker:latest
stage: build
variables:
EXCLUDE_DB: "false"
BASE_IMAGE: "nvidia/cuda:11.7.1-cudnn8-runtime-ubuntu22.04"
services:
- docker:dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
- |
BRANCH_PREFIX="$(if [ $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH ]; then echo ${CI_COMMIT_REF_SLUG} ; else echo 'latest'; fi)"
NO_DB_SUFIX="$(${EXCLUDE_DB} && echo '-no-db' || echo '')"
BASE_IMAGE_ARCH="$(echo ${BASE_IMAGE} | cut -d'/' -f1)"
BASE_IMAGE_TYPE="$(echo ${BASE_IMAGE} | cut -d'/' -f2)"
ARCH_TYPE="$(if [ $BASE_IMAGE_ARCH != $BASE_IMAGE_TYPE ]; then echo '-'${BASE_IMAGE_ARCH}; else echo ''; fi )"
export IMAGE_NAME="$CI_REGISTRY_IMAGE:${BRANCH_PREFIX}${ARCH_TYPE}${NO_DB_SUFIX}"
echo "Running on branch '${CI_COMMIT_BRANCH}', Image: ${IMAGE_NAME}" ;
- docker build --pull --build-arg EXCLUDE_DB="${EXCLUDE_DB}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -t "${IMAGE_NAME}" . -f "Dockerfile"
- docker push "${IMAGE_NAME}"
rules:
- if: $CI_COMMIT_BRANCH != "master" && $CI_COMMIT_BRANCH != "main" && $CI_COMMIT_BRANCH != "dev"
when: never
- if: $CI_COMMIT_BRANCH
exists:
- Dockerfile
docker-nvidia-no-db-build:
image: docker:latest
stage: build
variables:
EXCLUDE_DB: "true"
BASE_IMAGE: "nvidia/cuda:11.7.1-cudnn8-runtime-ubuntu22.04"
services:
- docker:dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
- |
BRANCH_PREFIX="$(if [ $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH ]; then echo ${CI_COMMIT_REF_SLUG} ; else echo 'latest'; fi)"
NO_DB_SUFIX="$(${EXCLUDE_DB} && echo '-no-db' || echo '')"
BASE_IMAGE_ARCH="$(echo ${BASE_IMAGE} | cut -d'/' -f1)"
BASE_IMAGE_TYPE="$(echo ${BASE_IMAGE} | cut -d'/' -f2)"
ARCH_TYPE="$(if [ $BASE_IMAGE_ARCH != $BASE_IMAGE_TYPE ]; then echo '-'${BASE_IMAGE_ARCH}; else echo ''; fi )"
export IMAGE_NAME="$CI_REGISTRY_IMAGE:${BRANCH_PREFIX}${ARCH_TYPE}${NO_DB_SUFIX}"
echo "Running on branch '${CI_COMMIT_BRANCH}', Image: ${IMAGE_NAME}" ;
- docker build --pull --build-arg EXCLUDE_DB="${EXCLUDE_DB}" --build-arg BASE_IMAGE="${BASE_IMAGE}" -t "${IMAGE_NAME}" . -f "Dockerfile"
- docker push "${IMAGE_NAME}"
rules:
- if: $CI_COMMIT_BRANCH != "master" && $CI_COMMIT_BRANCH != "main" && $CI_COMMIT_BRANCH != "dev"
when: never
- if: $CI_COMMIT_BRANCH
exists:
- Dockerfile

View File

@ -1,3 +1,9 @@
⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
# OUTDATED, Newest Docker method here :
https://gitlab.com/Shinobi-Systems/ShinobiDocker
⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
# Install Shinobi with Docker
**Warning :** It is recommended that you have a dedicated machine for Shinobi even if you intend to use Docker. If you are willing to install directly on the operating system please consider installing Ubuntu 22.04 and using the Ninja Way.

View File

@ -5750,6 +5750,10 @@ module.exports = function(s,config,lang){
name: lang['Can Get Monitors'],
value: 'get_monitors',
},
{
name: lang['Can Edit Monitors'],
value: 'edit_monitors',
},
{
name: lang['Can Control Monitors'],
value: 'control_monitors',
@ -6606,7 +6610,7 @@ module.exports = function(s,config,lang){
{
"name": "ip",
"field": lang['IP Address'],
"description": lang[lang["fieldTextIp"]],
"description": lang.fieldTextIp,
"example": "10.1.100.1-10.1.100.254",
},
{
@ -6618,11 +6622,13 @@ module.exports = function(s,config,lang){
{
"name": "user",
"field": lang['Camera Username'],
"description": lang.fieldTextOnvifScanCameraUsername,
"placeholder": "Can be left blank.",
},
{
"name": "pass",
"field": lang['Camera Password'],
"description": lang.fieldTextOnvifScanCameraPassword,
"fieldType": "password",
},
{
@ -6631,8 +6637,13 @@ module.exports = function(s,config,lang){
{
"fieldType": "btn",
"forForm": true,
"class": `btn-block btn-success`,
"btnContent": `${lang['Search']}<span class="_loading" style="display:none"> &nbsp; <i class="fa fa-pulse fa-spinner"></i></span>`,
"class": `btn-success start-scan`,
"btnContent": `${lang['Search']}`,
},
{
"fieldType": "btn",
"class": `btn-danger stop-scan d-none`,
"btnContent": `${lang['Stop']}`,
},
{
"fieldType": "btn",
@ -6653,18 +6664,7 @@ module.exports = function(s,config,lang){
"class": "onvif_result row",
}
]
},
"Other Devices": {
"name": lang['Other Devices'],
"color": "danger",
"section-pre-class": "col-md-12",
"info": [
{
"fieldType": "div",
"class": "onvif_result_error row",
}
]
},
}
}
},
"Camera Probe": {
@ -7696,6 +7696,11 @@ module.exports = function(s,config,lang){
pageOpen: 'liveGrid',
addUl: true,
ulItems: [
{
label: lang['Open Wall Display'],
class: 'open-wallview cursor-pointer',
color: 'blue',
},
{
label: lang['Open All Monitors'],
class: 'open-all-monitors cursor-pointer',
@ -9191,6 +9196,12 @@ module.exports = function(s,config,lang){
<div class="btn-group">
<a class="btn btn-sm btn-primary" id="timeline-date-selector" title="${lang.Date}"><i class="fa fa-calendar"></i></a>
</div>
<div class="btn-group">
<button type="button" class="btn btn-primary btn-sm dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa fa-video-camera"></i>
</button>
<ul class="dropdown-menu px-3 bg-dark" id="timeline-monitor-list"></ul>
</div>
</div>
`,
},

View File

@ -20,6 +20,8 @@
"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",
"Monitor Saved": "Monitor Saved",
"Auto Placement": "Auto Placement",
"Unmute": "Unmute",
"byUser": "by user",
"accountDeleted": "Account Deleted",
@ -135,7 +137,12 @@
"Viewing Server Stats": "Viewing Server Stats",
"Connected Users": "Connected Users",
"Registered Servers": "Registered Servers",
"Open All": "Open All",
"Close All": "Close All",
"Open All Monitors": "Open All Monitors",
"Open Wall Display": "Open Wall Display",
"New Wall Display": "New Wall Display",
"openWallViewInfo": "Open Monitors in the top right of this window.",
"Accounts": "Accounts",
"Settings": "Settings",
"Count Objects only inside Regions": "Count Objects only inside Regions",
@ -298,6 +305,7 @@
"Can Get Logs": "Can Get Logs",
"Can Authenticate Websocket": "Can Authenticate Websocket",
"Can Control Monitors": "Can Control Monitors",
"Can Edit Monitors": "Can Edit Monitors",
"Can View Snapshots": "Can View Snapshots",
"Can View Streams": "Can View Streams",
"Can View Videos": "Can View Videos",
@ -417,6 +425,8 @@
"Audio streams only": "Audio streams only",
"Audio stream only from first feed": "Audio stream only from first feed",
"sorryNothingWasFound": "Sorry, nothing was found.",
"fieldTextOnvifScanCameraUsername": "Commas are used to separate potential usernames to attempt while scanning. If you have multiple usernames and passwords then all variations will be attempted.",
"fieldTextOnvifScanCameraPassword": "Commas and @ are not allowed in passwords. Commas are used to separate potential passwords to attempt while scanning. Double click this field to reveal the currently set value.",
"Event Occurred": "Event Occurred",
"ONVIF Port": "ONVIF Port",
"ONVIF Scanner": "ONVIF Scanner",
@ -424,9 +434,10 @@
"ONVIFEventsNotAvailable": "ONVIF Events not Available",
"ONVIFEventsNotAvailableText1": "This service may not be available for this camera or ONVIF has not initialized yet.",
"ONVIFnotCompliantProfileT": "Camera is not ONVIF Profile T Compliant",
"ONVIFErr400": "Found ONVIF port but authorization failed when retrieving the Stream URL. Check username and password used for scan. Make sure your camera time and server time are synced.",
"ONVIFErr400": "Bad Request. Found ONVIF port but failed when retrieving the Stream URL. Check username and password used for scan. Make sure your camera time and server time are synced.",
"ONVIFErr405": "Method Not Allowed. Check username and password used for scan.",
"ONVIFErr404": "Not Found. This may just be the web panel for a network device.",
"ONVIFErr401": "Not Authorized. Found ONVIF port but authorization failed when retrieving the Stream URL. Check username and password used for scan. Make sure your camera time and server time are synced.",
"Scan Settings": "Scan Settings",
"ONVIFnote": "Discover ONVIF devices on networks outside your own or leave it blank to scan your current network. <br>Username and Password can be left blank.",
"Range or Single": "Range or Single",
@ -1426,6 +1437,7 @@
"Select atleast one monitor to delete": "Select atleast one monitor to delete.",
"Use Built-In": "Use Built-In",
"Add Cameras": "Add Cameras",
"addAllCamerasText": "You are about to add 9001 cameras to your system.",
"Add Camera": "Add Camera",
"Delete Camera": "Delete Camera",
"Event Rules": "Event Rules",
@ -1448,7 +1460,7 @@
"Original Choice": "Original Choice",
"Legacy Webhook": "Legacy Webhook",
"eventFilterActionText": "These are the actions that occur from the filter conditions that have succeeded. \"Original Choice\" refers to the option you had chosen in your Monitor's Settings.",
"eventFilterEnableNoticeText": "This is an advnaced feature. Ensure you have enabled \"Event Filters\" in your Detector Settings section. If you cannot see this option toggle Simple to Advanced in the bottom right corner of the Monitor Settings.",
"eventFilterEnableNoticeText": "This is an advanced feature. Ensure you have enabled \"Event Filters\" in your Detector Settings section. If you cannot see this option toggle Simple to Advanced in the bottom right corner of the Monitor Settings.",
"Telegram": "Telegram",
"Before": "Before",
"After": "After",

View File

@ -106,6 +106,8 @@ module.exports = function(s,config,lang){
permissions: {}
})
callback(err,user,true)
}else{
callback(lang['Not Authorized'],null,false)
}
})
}

View File

@ -150,8 +150,8 @@ module.exports = (s,config,lang,app) => {
alternateLoginsFieldList[alternateLoginsFieldList.length - 1].btns.push({
"class": `btn-info ldap-sign-in`,
"btnContent": `<i class="fa fa-group"></i> &nbsp; ${lang['Link LDAP Account']}`,
})
s.customAutoLoadTree['LibsJs'].push(`dash2.ldapSignIn.js`)
});
s.customAutoLoadTree['AssetsJs'].push(`bs5.ldapSignIn.js`)
})
/**
* API : Add Token Window (Sign-In to LDAP) (GET)

View File

@ -179,7 +179,7 @@ module.exports = function(s,config,lang,app,io){
}
s.insertDatabaseRow(monitorConfig,insert)
s.insertCompletedVideoExtensions.forEach(function(extender){
extender(monitorConfig,insert)
extender(activeMonitor, monitorConfig, insert)
})
//purge over max
s.purgeDiskForGroup(data.ke)

View File

@ -8,6 +8,9 @@ let config = workerData.config
let lang = workerData.lang
let sslInfo = config.ssl || {}
let remoteConnectionPort = config.easyRemotePort || (sslInfo && sslInfo.port && (sslInfo.enabled !== false) ? sslInfo.port : config.port || 8080)
const multipleSelected = config.p2pHostMultiSelected instanceof Array && config.p2pHostMultiSelected.length > 0;
const p2pApiKey = config.p2pApiKey;
const p2pServerList = config.p2pServerList;
const net = require("net")
const bson = require('bson')
const WebSocket = require('cws')
@ -28,7 +31,16 @@ const s = {
parentPort.on('message',(data) => {
switch(data.f){
case'init':
initialize()
if(multipleSelected){
for(aSelection of config.p2pHostMultiSelected){
clearAllTimeouts(aSelection)
initialize(aSelection)
}
}else{
const singleSelection = config.p2pHostSelected;
clearAllTimeouts(singleSelection)
initialize(singleSelection)
}
break;
case'exit':
s.debugLog('Closing P2P Connection...')
@ -36,11 +48,11 @@ parentPort.on('message',(data) => {
break;
}
})
var socketCheckTimer = null
var heartbeatTimer = null
var heartBeatCheckTimout = null
var onClosedTimeout = null
let stayDisconnected = false
var socketCheckTimer = {}
var heartbeatTimer = {}
var heartBeatCheckTimout = {}
var onClosedTimeout = {}
let stayDisconnected = {}
const requestConnections = {}
const requestConnectionsData = {}
function getRequestConnection(requestId){
@ -48,29 +60,28 @@ function getRequestConnection(requestId){
write: () => {}
}
}
function clearAllTimeouts(){
clearInterval(heartbeatTimer)
clearTimeout(heartBeatCheckTimout)
clearTimeout(onClosedTimeout)
function clearAllTimeouts(p2pServerAddress){
clearInterval(heartbeatTimer[p2pServerAddress])
clearTimeout(heartBeatCheckTimout[p2pServerAddress])
clearTimeout(onClosedTimeout[p2pServerAddress])
}
function startConnection(p2pServerAddress,subscriptionId){
let tunnelToP2P
stayDisconnected = false
stayDisconnected[p2pServerAddress] = false
const allMessageHandlers = []
async function startWebsocketConnection(key,callback){
s.debugLog(`startWebsocketConnection EXECUTE`,new Error())
console.log('P2P : Connecting to Konekta P2P Server...')
function createWebsocketConnection(){
clearAllTimeouts()
return new Promise((resolve,reject) => {
try{
stayDisconnected = true
stayDisconnected[p2pServerAddress] = true
if(tunnelToP2P)tunnelToP2P.close()
}catch(err){
console.log(err)
}
tunnelToP2P = new WebSocket(p2pServerAddress);
stayDisconnected = false;
console.log('P2P : Connecting to Konekta P2P Server :', p2pServerAddress)
stayDisconnected[p2pServerAddress] = false;
tunnelToP2P.on('open', function(){
resolve(tunnelToP2P)
})
@ -81,7 +92,7 @@ function startConnection(p2pServerAddress,subscriptionId){
})
tunnelToP2P.on('close', () => {
console.log(`P2P Connection Closed!`)
clearAllTimeouts()
clearAllTimeouts(p2pServerAddress)
// onClosedTimeout = setTimeout(() => {
// disconnectedConnection();
// },5000)
@ -95,8 +106,8 @@ function startConnection(p2pServerAddress,subscriptionId){
})
}
clearInterval(socketCheckTimer)
socketCheckTimer = setInterval(() => {
clearInterval(socketCheckTimer[p2pServerAddress])
socketCheckTimer[p2pServerAddress] = setInterval(() => {
// s.debugLog('Tunnel Ready State :',tunnelToP2P.readyState)
if(tunnelToP2P.readyState !== 1){
s.debugLog('Tunnel NOT Ready! Reconnecting...')
@ -106,10 +117,10 @@ function startConnection(p2pServerAddress,subscriptionId){
})
}
function disconnectedConnection(code,reason){
s.debugLog('stayDisconnected',stayDisconnected)
s.debugLog('stayDisconnected',stayDisconnected[p2pServerAddress])
clearAllTimeouts()
s.debugLog('DISCONNECTED!')
if(stayDisconnected)return;
if(stayDisconnected[p2pServerAddress])return;
s.debugLog('RESTARTING!')
setTimeout(() => {
if(tunnelToP2P && tunnelToP2P.readyState !== 1)startWebsocketConnection()
@ -122,8 +133,8 @@ function startConnection(p2pServerAddress,subscriptionId){
subscriptionId: subscriptionId,
restrictedTo: config.p2pRestrictedTo || [],
})
clearInterval(heartbeatTimer)
heartbeatTimer = setInterval(() => {
clearInterval(heartbeatTimer[p2pServerAddress])
heartbeatTimer[p2pServerAddress] = setInterval(() => {
sendDataToTunnel({
f: 'ping',
})
@ -158,7 +169,7 @@ function startConnection(p2pServerAddress,subscriptionId){
// remotesocket.off('close')
// requestConnections[requestId].end()
// }
const responseTunnel = await getResponseTunnel(requestId)
const responseTunnel = await getResponseTunnel(requestId, p2pServerAddress)
let remotesocket = new net.Socket();
remotesocket.on('ready',() => {
remotesocket.write(initData.buffer)
@ -196,8 +207,8 @@ function startConnection(p2pServerAddress,subscriptionId){
}
}
function refreshHeartBeatCheck(){
clearTimeout(heartBeatCheckTimout)
heartBeatCheckTimout = setTimeout(() => {
clearTimeout(heartBeatCheckTimout[p2pServerAddress])
heartBeatCheckTimout[p2pServerAddress] = setTimeout(() => {
startWebsocketConnection()
},1000 * 10 * 1.5)
}
@ -304,15 +315,16 @@ function startConnection(p2pServerAddress,subscriptionId){
onIncomingMessage('disconnect',function(data,requestId){
console.log(`FAILED LICENSE CHECK ON P2P`)
const retryLater = data && data.retryLater;
stayDisconnected = !retryLater
stayDisconnected[p2pServerAddress] = !retryLater
if(retryLater)console.log(`Retrying P2P Later...`)
})
return tunnelToP2P;
}
const responseTunnels = {}
async function getResponseTunnel(originalRequestId){
return responseTunnels[originalRequestId] || await createResponseTunnel(originalRequestId)
async function getResponseTunnel(originalRequestId, p2pServerAddress){
return responseTunnels[originalRequestId] || await createResponseTunnel(originalRequestId, p2pServerAddress)
}
function createResponseTunnel(originalRequestId){
function createResponseTunnel(originalRequestId, p2pServerAddress){
const responseTunnelMessageHandlers = []
function onMessage(key,callback){
responseTunnelMessageHandlers.push({
@ -321,7 +333,7 @@ function createResponseTunnel(originalRequestId){
})
}
return new Promise((resolve,reject) => {
const responseTunnel = new WebSocket(config.selectedHost);
const responseTunnel = new WebSocket(p2pServerAddress);
function sendToResponseTunnel(data){
responseTunnel.send(
bson.serialize(data)
@ -374,10 +386,9 @@ function closeResponseTunnel(originalRequestId){
s.debugLog('closeResponseTunnel',err)
}
}
function initialize(){
const selectedP2PServerId = config.p2pServerList[config.p2pHostSelected] ? config.p2pHostSelected : Object.keys(config.p2pServerList)[0]
const p2pServerDetails = config.p2pServerList[selectedP2PServerId]
function initialize(p2pHostSelected){
const selectedP2PServerId = p2pServerList[p2pHostSelected] ? p2pHostSelected : Object.keys(p2pServerList)[0]
const p2pServerDetails = p2pServerList[selectedP2PServerId]
const selectedHost = `${p2pServerDetails.secure ? `wss` : 'ws'}://` + p2pServerDetails.host + ':' + p2pServerDetails.p2pPort
config.selectedHost = selectedHost
startConnection(selectedHost,config.p2pApiKey)
startConnection(selectedHost,p2pApiKey)
}

View File

@ -389,12 +389,14 @@ module.exports = function(s,config,lang){
},callback)
}
const moveToHomePosition = (options,callback) => {
const nonStandardOnvif = s.group[options.ke].rawMonitorConfigurations[options.id].details.onvif_non_standard === '1'
const groupKey = options.ke
const monitorId = options.mid || options.id
const nonStandardOnvif = s.group[groupKey].rawMonitorConfigurations[monitorId].details.onvif_non_standard === '1'
const profileToken = options.ProfileToken || "__CURRENT_TOKEN"
return s.runOnvifMethod({
auth: {
ke: options.ke,
id: options.id,
ke: groupKey,
id: monitorId,
service: 'ptz',
action: 'gotoHomePosition',
},
@ -537,5 +539,6 @@ module.exports = function(s,config,lang){
moveToPresetPosition,
moveCameraPtzToMatrix,
setHomePositionPreset,
moveToHomePosition,
}
}

View File

@ -50,7 +50,7 @@ module.exports = (s,config,lang) => {
}
break;
default:
s.systemLog('CRON.js MESSAGE : ',data)
s.debugLog('CRON.js MESSAGE : ',data)
break;
}
})

View File

@ -1,116 +1,106 @@
module.exports = function(s,config,lang){
function hasOnvifEventsEnabled(monitorConfig) {
return monitorConfig.details.is_onvif === '1' && monitorConfig.details.onvif_events === '1';
}
module.exports = function (s, config, lang) {
const {Cam} = require("onvif");
const {
triggerEvent,
} = require('./utils.js')(s,config,lang)
const onvifEvents = require("node-onvif-events");
const onvifEventIds = []
const onvifEventControllers = {}
const startMotion = async (onvifId,monitorConfig) => {
const groupKey = monitorConfig.ke
const monitorId = monitorConfig.mid
const onvifIdKey = `${monitorConfig.mid}${monitorConfig.ke}`
const controlBaseUrl = monitorConfig.details.control_base_url || s.buildMonitorUrl(monitorConfig, true)
const controlURLOptions = s.cameraControlOptionsFromUrl(controlBaseUrl,monitorConfig)
const onvifPort = parseInt(monitorConfig.details.onvif_port) || 8000
let options = {
id: onvifId,
hostname: controlURLOptions.host,
username: controlURLOptions.username,
password: controlURLOptions.password,
port: onvifPort,
};
const detector = onvifEventControllers[onvifIdKey] || await onvifEvents.MotionDetector.create(options.id, options);
function onvifEventLog(type,data){
s.userLog({
ke: groupKey,
mid: monitorId
},{
type: type,
msg: data
})
}
onvifEventLog(`ONVIF Event Detection Listening!`)
try {
detector.listen((motion) => {
if (motion) {
onvifEventLog(`ONVIF Event Detected!`)
triggerEvent({
f: 'trigger',
id: monitorId,
ke: groupKey,
details:{
plug: 'onvifEvent',
name: 'onvifEvent',
reason: 'motion',
confidence: 100,
// reason: 'object',
// matrices: [matrix],
// imgHeight: img.height,
// imgWidth: img.width,
}
})
} else {
onvifEventLog(`ONVIF Event Stopped`)
}
});
} catch(e) {
console.error(e)
onvifEventLog(`ONVIF Event Error`,e)
}
return detector
}
async function initializeOnvifEvents(monitorConfig){
const monitorMode = monitorConfig.mode
const groupKey = monitorConfig.ke
const monitorId = monitorConfig.mid
const hasOnvifEventsEnabled = monitorConfig.details.is_onvif === '1' && monitorConfig.details.onvif_events === '1';
if(hasOnvifEventsEnabled){
const onvifIdKey = `${monitorConfig.mid}${monitorConfig.ke}`
let onvifId = onvifEventIds.indexOf(onvifIdKey)
if(onvifEventIds.indexOf(onvifIdKey) === -1){
onvifId = onvifEventIds.length;
onvifEventIds.push(onvifIdKey);
}
try{
onvifEventControllers[onvifIdKey].close()
s.debugLog('ONVIF Event Module Warning : This could cause a memory leak?')
}catch(err){
s.debugLog('ONVIF Event Module Error', err.stack);
}
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);
}
} = require('./utils.js')(s, config, lang)
function handleEvent(event, monitorConfig, onvifEventLog) {
const eventValue = event.message?.message?.data?.simpleItem?.$?.Value;
if (eventValue === false) {
onvifEventLog(`ONVIF Event Stopped`, `topic ${event.topic?._}`)
return
}
onvifEventLog(`ONVIF Event Detected!`, `topic ${event.topic?._}`)
triggerEvent({
f: 'trigger',
id: monitorConfig.mid,
ke: monitorConfig.ke,
details: {
plug: 'onvifEvent',
name: 'onvifEvent',
reason: event.topic?._,
confidence: 100,
[event.message?.message?.data?.simpleItem?.$?.Name]: eventValue
}
})
}
function configureOnvif(monitorConfig, onvifEventLog) {
const controlBaseUrl = monitorConfig.details.control_base_url || s.buildMonitorUrl(monitorConfig, true)
const controlURLOptions = s.cameraControlOptionsFromUrl(controlBaseUrl, monitorConfig)
const onvifPort = parseInt(monitorConfig.details.onvif_port) || 8000
const options = {
hostname: controlURLOptions.host,
username: controlURLOptions.username,
password: controlURLOptions.password,
port: onvifPort,
};
return new Cam(options, function (error) {
if (error) {
onvifEventLog(`ONVIF Event Error`,e)
return
}
this.on('event', function (event) {
handleEvent(event, monitorConfig, onvifEventLog);
})
})
}
const cams = {};
function initializeOnvifEvents(monitorConfig) {
monitorConfig.key = `${monitorConfig.mid}${monitorConfig.ke}`
const onvifEventLog = function onvifEventLog(type, data) {
s.userLog({
ke: monitorConfig.key,
mid: monitorConfig.mid
}, {
type: type,
msg: data
})
}
if (!hasOnvifEventsEnabled(monitorConfig)) {
cams[monitorConfig.key]?.removeAllListeners('event')
return
}
if (cams[monitorConfig.key]) {
onvifEventLog("ONVIF already listening to events")
return;
}
cams[monitorConfig.key] = configureOnvif(monitorConfig,onvifEventLog);
}
s.onMonitorStart((monitorConfig) => {
initializeOnvifEvents(monitorConfig)
})
const connectionInfoArray = s.definitions["Monitor Settings"].blocks["Detector"].info
connectionInfoArray.splice(2, 0, {
"name": "detail=onvif_events",
"field": lang['ONVIF Events'],
"default": "0",
"form-group-class": "h_onvif_input h_onvif_1",
"form-group-class-pre-layer": "h_det_input h_det_1",
"fieldType": "select",
"possible": [
{
"name": lang.No,
"value": "0"
},
{
"name": lang.Yes,
"value": "1"
}
]
"name": "detail=onvif_events",
"field": lang['ONVIF Events'],
"default": "0",
"form-group-class": "h_onvif_input h_onvif_1",
"form-group-class-pre-layer": "h_det_input h_det_1",
"fieldType": "select",
"possible": [
{
"name": lang.No,
"value": "0"
},
{
"name": lang.Yes,
"value": "1"
}
]
});
}

View File

@ -83,6 +83,7 @@ module.exports = (s,config,lang) => {
&& detailString.matrices[0]
&& detailString.matrices[0].tag;
newString = newString
.replace(/{{CONFIDENCE}}/g,d.details.confidence)
.replace(/{{TIME}}/g,d.currentTimestamp)
.replace(/{{REGION_NAME}}/g,d.details.name)
.replace(/{{SNAP_PATH}}/g,s.dir.streams+d.ke+'/'+d.id+'/s.jpg')

View File

@ -428,7 +428,7 @@ module.exports = (s,config,lang) => {
}
if(!videoCodecisCopy || outputRequiresEncoding){
if(videoWidth && videoHeight)streamFlags.push(`-s ${videoWidth}x${videoHeight}`)
if(videoFps && streamType === 'mjpeg' || streamType === 'b64'){
if(videoFps && streamType === 'mjpeg' || streamType === 'b64' || videoFps && !videoCodecisCopy){
streamFilters.push(`fps=${videoFps}`)
}
}
@ -495,7 +495,7 @@ module.exports = (s,config,lang) => {
const isCudaEnabled = hasCudaEnabled(e)
const videoFlags = []
const videoFilters = []
const inputMap = buildInputMap(e,e.details.input_map_choices.stream)
const inputMap = buildInputMap(e,e.details.input_map_choices.snap)
const { videoWidth, videoHeight } = validateDimensions(e.details.snap_scale_x,e.details.snap_scale_y)
if(inputMap)videoFlags.push(inputMap)
if(e.details.snap_vf)videoFilters.push(e.details.snap_vf)

View File

@ -196,7 +196,7 @@ module.exports = function(s,config,lang){
var iconImageFile = streamDir + 'icon.jpg'
const snapRawFilters = monitor.details.cust_snap_raw
if(snapRawFilters)outputOptions.push(snapRawFilters);
var ffmpegCmd = splitForFFMPEG(`-y -loglevel warning ${isDetectorStream ? '-live_start_index 2' : ''} -re ${inputOptions.join(' ')} -stimeout 4000000 -i "${url}" ${outputOptions.join(' ')} -f image2 -an -frames:v 1 "${temporaryImageFile}"`)
var ffmpegCmd = splitForFFMPEG(`-y -loglevel warning ${isDetectorStream ? '-live_start_index 2' : ''} -re ${inputOptions.join(' ')} -timeout 4000000 -i "${url}" ${outputOptions.join(' ')} -f image2 -an -frames:v 1 "${temporaryImageFile}"`)
try{
await fs.promises.mkdir(streamDir, {recursive: true}, (err) => {s.debugLog(err)})
}catch(err){
@ -796,6 +796,7 @@ module.exports = function(s,config,lang){
[
'auth_socket',
'get_monitors',
'edit_monitors',
'control_monitors',
'get_logs',
'watch_stream',

View File

@ -1,15 +1,15 @@
const fs = require('fs');
const URL = require('url');
const events = require('events');
const Mp4Frag = require('mp4frag');
const treekill = require('tree-kill');
const exec = require('child_process').exec;
const spawn = require('child_process').spawn;
const connectionTester = require('connection-tester')
const SoundDetection = require('shinobi-sound-detection')
const streamViewerCountTimeouts = {}
const { createQueueAwaited } = require('../common.js')
module.exports = (s,config,lang) => {
const fs = require('fs');
const URL = require('url');
const events = require('events');
const Mp4Frag = require('mp4frag');
const treekill = require('tree-kill');
const exec = require('child_process').exec;
const spawn = require('child_process').spawn;
const connectionTester = require('connection-tester')
const SoundDetection = require('shinobi-sound-detection')
const streamViewerCountTimeouts = {}
const { createQueueAwaited } = require('../common.js')
const {
applyPartialToConfiguration,
getWarningChangesForMonitor,
@ -29,6 +29,7 @@ module.exports = (s,config,lang) => {
} = require('../events/utils.js')(s,config,lang)
const {
setHomePositionPreset,
moveToHomePosition,
} = require('../control/ptz.js')(s,config,lang)
const {
scanForOrphanedVideos,
@ -498,7 +499,20 @@ module.exports = (s,config,lang) => {
const groupKey = e.ke
const monitorId = e.mid || e.id
const activeMonitor = getActiveMonitor(groupKey,monitorId);
const monitorConfig = copyMonitorConfiguration(groupKey,monitorId);
const streamType = monitorConfig.details.stream_type;
const analyzeDuration = (parseInt(monitorConfig.details.aduration) / 1000) || 10000;
let initialHeartBeat = null
if(streamType !== 'useSubstream'){
initialHeartBeat = setTimeout(() => {
resetStreamCheck({
ke: groupKey,
mid: monitorId,
})
}, analyzeDuration);
}
activeMonitor.spawn_exit = async function(){
clearTimeout(initialHeartBeat)
if(activeMonitor.isStarted === true){
if(e.details.loglevel !== 'quiet'){
s.userLog(e,{type:lang['Process Unexpected Exit'],msg:{msg:lang.unexpectedExitText,cmd:activeMonitor.ffmpeg}});
@ -508,7 +522,6 @@ module.exports = (s,config,lang) => {
forceCheck: true,
checkMax: 2
})
const monitorConfig = copyMonitorConfiguration(groupKey,monitorId);
s.onMonitorUnexpectedExitExtensions.forEach(function(extender){
extender(monitorConfig,e)
})
@ -1707,7 +1720,8 @@ module.exports = (s,config,lang) => {
activeMonitor.errorFatalCount = 0;
delete(activeMonitor.childNode)
if(e.details.detector_ptz_follow === '1'){
setHomePositionPreset(e)
// setHomePositionPreset(e)
moveToHomePosition(e)
}
try{
await launchMonitorProcesses(e)
@ -1778,6 +1792,36 @@ module.exports = (s,config,lang) => {
const streamDir = s.dir.streams + options.ke + '/' + options.mid + '/'
return streamDir
}
function removeSenstiveInfoFromMonitorConfig(monitorConfig){
monitorConfig.protocol = ''
monitorConfig.host = ''
monitorConfig.path = ''
monitorConfig.port = ''
monitorConfig.details.muser = ''
monitorConfig.details.mpass = ''
monitorConfig.details.auto_host = ''
monitorConfig.details.rtmp_key = ''
monitorConfig.details.notes = ''
monitorConfig.details.tv_channel_id = ''
monitorConfig.details.tv_channel_group_title = ''
monitorConfig.details.control_base_url = ''
monitorConfig.details.cust_input = ''
monitorConfig.details.cust_stream = ''
monitorConfig.details.cust_snap = ''
monitorConfig.details.cust_snap_raw = ''
monitorConfig.details.cust_record = ''
monitorConfig.details.cust_record = ''
monitorConfig.details.cust_detect = ''
monitorConfig.details.cust_detect_object = ''
monitorConfig.details.cust_sip_record = ''
monitorConfig.details.custom_output = ''
monitorConfig.details.detector_cascades = ''
monitorConfig.details.stream_channels = ''
monitorConfig.details.input_maps = ''
delete(monitorConfig.details.input_map_choices)
delete(monitorConfig.details.substream)
return monitorConfig
}
return {
monitorStop,
monitorIdle,
@ -1809,5 +1853,6 @@ module.exports = (s,config,lang) => {
getActiveViewerCount: getActiveViewerCount,
setTimedActiveViewerForHttp: setTimedActiveViewerForHttp,
attachMainProcessHandlers: attachMainProcessHandlers,
removeSenstiveInfoFromMonitorConfig,
}
}

View File

@ -18,11 +18,11 @@ module.exports = function(s,config,lang){
){
require('./notifications/email.js')(s,config,lang,getSnapshot)
}
require('./notifications/webhook.js')(s,config,lang,getSnapshot)
require('./notifications/emailByUser.js')(s,config,lang,getSnapshot)
require('./notifications/discordBot.js')(s,config,lang,getSnapshot)
require('./notifications/telegram.js')(s,config,lang,getSnapshot)
require('./notifications/pushover.js')(s,config,lang,getSnapshot)
require('./notifications/webhook.js')(s,config,lang,getSnapshot)
require('./notifications/mqtt.js')(s,config,lang,getSnapshot)
require('./notifications/matrix.js')(s,config,lang,getSnapshot)
}

View File

@ -25,41 +25,33 @@ module.exports = function(s,config,lang,getSnapshot){
})
})
}
const doPostMethod = s.group[groupKey].init.global_webhook_method === 'post';
const doPostMethod = s.group[groupKey].init.global_webhook_method === 'POST';
// const includeSnapshot = s.group[groupKey].init.global_webhook_include_image === '1';
const webhookInfoData = {
info: sendBody,
files: [],
}
if(files){
const formData = new FormData();
const formData = new FormData();
if(files && doPostMethod){
files.forEach(async (file,n) => {
const fileName = file.name
switch(file.type){
case'video':
// video cannot be sent this way unless POST
if(doPostMethod){
const fileName = file.name
formData.append(`file${n + 1}`, file.attachment, {
contentType: 'video/mp4',
name: fileName,
filename: fileName,
});
webhookInfoData.files.push(fileName)
}
formData.append(`file${n + 1}`, file.attachment, {
contentType: 'video/mp4',
name: fileName,
filename: fileName,
});
webhookInfoData.files.push(fileName)
break;
case'photo':
if(doPostMethod){
const fileName = file.name
formData.append(`file${n + 1}`, file.attachment, {
contentType: 'image/jpeg',
name: fileName,
filename: fileName,
});
webhookInfoData.files.push(fileName)
}else{
const base64StringofImage = file.attachment.toString('base64')
webhookInfoData.files.push(base64StringofImage)
}
formData.append(`file${n + 1}`, file.attachment, {
contentType: 'image/jpeg',
name: fileName,
filename: fileName,
});
webhookInfoData.files.push(fileName)
break;
}
})

View File

@ -1,16 +1,34 @@
module.exports = function(s,config,lang,app,io){
const {
scanStatus,
runOnvifScanner,
stopOnvifScanner,
} = require('./scanners/utils.js')(s,config,lang)
const {
ffprobe,
} = require('./ffmpeg/utils.js')(s,config,lang)
const {
runOnvifScanner,
} = require('./scanners/utils.js')(s,config,lang)
const onWebSocketConnection = async (cn) => {
const tx = function(z){if(!z.ke){z.ke=cn.ke;};cn.emit('f',z);}
cn.on('f',(d) => {
switch(d.f){
case'onvif_scan_reconnect':
tx({f: 'onvif_scan_current', devices: Object.values(scanStatus.allSuccessful), isScanning: scanStatus.isActive})
break;
case'onvif_stop':
stopOnvifScanner()
tx({f: 'onvif_scan_stopped'})
break;
case'onvif':
runOnvifScanner(d,tx)
const groupKey = `${cn.ke}`
runOnvifScanner(d, (data) => {
const response = { f: 'onvif', ...data }
s.tx(response, 'GRP_' + cn.ke)
}, (data) => {
const response = { f: 'onvif', ff: 'failed_capture', ...data }
s.tx(response, 'GRP_' + cn.ke)
}).then((responseList) => {
s.tx({ f: 'onvif_scan_complete' }, 'GRP_' + cn.ke)
})
break;
}
})
@ -40,4 +58,60 @@ module.exports = function(s,config,lang,app,io){
})
},res,req);
})
/**
* API : ONVIF Scanner RUN
*/
app.get(config.webPaths.apiPrefix+':auth/onvifScanner/:ke/scan',function (req,res){
s.auth(req.params,function(user){
const {
isRestricted,
isRestrictedApiKey,
apiKeyPermissions,
} = s.checkPermission(user);
if(
isRestrictedApiKey && apiKeyPermissions.control_monitors_disallowed
){
s.closeJsonResponse(res,{
ok: false,
msg: lang['Not Authorized']
});
return
}
const groupKey = req.params.ke;
stopOnvifScanner()
s.closeJsonResponse(res, { ok: true });
},res,req);
})
/**
* API : ONVIF Scanner STOP
*/
app.get(config.webPaths.apiPrefix+':auth/onvifScanner/:ke/scan/stop',function (req,res){
s.auth(req.params,function(user){
const {
isRestricted,
isRestrictedApiKey,
apiKeyPermissions,
} = s.checkPermission(user);
if(
isRestrictedApiKey && apiKeyPermissions.control_monitors_disallowed
){
s.closeJsonResponse(res,{
ok: false,
msg: lang['Not Authorized']
});
return
}
const groupKey = req.params.ke;
runOnvifScanner(d, (data) => {
const response = { f: 'onvif', ...data }
s.tx(response, 'GRP_' + groupKey)
}, (data) => {
const response = { f: 'onvif', ff: 'failed_capture', ...data }
s.tx(response, 'GRP_' + groupKey)
}).then((responseList) => {
s.tx({ f: 'onvif_scan_complete' }, 'GRP_' + groupKey)
s.closeJsonResponse(res, responseList)
})
},res,req);
})
}

View File

@ -1,202 +1,377 @@
var os = require('os');
const async = require('async');
const onvif = require("shinobi-onvif");
const {
addCredentialsToUrl,
stringContains,
getBuffer,
} = require('../common.js')
const scanStatus = {
current: [],
allSuccessful: {},
cancelPromises: null,
abortController: null
};
module.exports = (s,config,lang) => {
const ipRange = (start_ip, end_ip) => {
var startLong = toLong(start_ip);
var endLong = toLong(end_ip);
if (startLong > endLong) {
var tmp = startLong;
startLong = endLong
endLong = tmp;
}
var rangeArray = [];
var i;
for (i = startLong; i <= endLong; i++) {
rangeArray.push(fromLong(i));
}
return rangeArray;
}
const portRange = (lowEnd,highEnd) => {
var list = [];
for (var i = lowEnd; i <= highEnd; i++) {
const startLong = toLong(start_ip);
const endLong = toLong(end_ip);
if (startLong > endLong) {
const tmp = startLong;
startLong = endLong;
endLong = tmp;
}
const rangeArray = [];
for (let i = startLong; i <= endLong; i++) {
rangeArray.push(fromLong(i));
}
return rangeArray;
};
const portRange = (lowEnd, highEnd) => {
const list = [];
for (let i = lowEnd; i <= highEnd; i++) {
list.push(i);
}
return list;
}
//toLong taken from NPM package 'ip'
const toLong = (ip) => {
var ipl = 0;
ip.split('.').forEach(function(octet) {
ipl <<= 8;
ipl += parseInt(octet);
});
return(ipl >>> 0);
}
//fromLong taken from NPM package 'ip'
const fromLong = (ipl) => {
return ((ipl >>> 24) + '.' +
(ipl >> 16 & 255) + '.' +
(ipl >> 8 & 255) + '.' +
(ipl & 255) );
}
const runOnvifScanner = (options,foundCameraCallback) => {
var ip = options.ip.replace(/ /g,'')
var ports = options.port.replace(/ /g,'')
if(options.ip === ''){
var interfaces = os.networkInterfaces()
var addresses = []
for (var k in interfaces) {
for (var k2 in interfaces[k]) {
var address = interfaces[k][k2]
if (address.family === 'IPv4' && !address.internal) {
addresses.push(address.address)
}
}
}
const addressRange = []
addresses.forEach(function(address){
if(address.indexOf('0.0.0')>-1){return false}
var addressPrefix = address.split('.')
delete(addressPrefix[3]);
addressPrefix = addressPrefix.join('.')
addressRange.push(`${addressPrefix}1-${addressPrefix}254`)
})
ip = addressRange.join(',')
}
if(ports === ''){
ports = '80,8080,8000,7575,8081,9080,8090,8999,8899'
}
if(ports.indexOf('-') > -1){
ports = ports.split('-')
var portRangeStart = ports[0]
var portRangeEnd = ports[1]
ports = portRange(portRangeStart,portRangeEnd);
}else{
ports = ports.split(',')
}
var ipList = options.ipList
var onvifUsername = options.user || ''
var onvifPassword = options.pass || ''
ip.split(',').forEach(function(addressRange){
var ipRangeStart = addressRange[0]
var ipRangeEnd = addressRange[1]
if(addressRange.indexOf('-')>-1){
addressRange = addressRange.split('-');
ipRangeStart = addressRange[0]
ipRangeEnd = addressRange[1]
}else{
ipRangeStart = addressRange
ipRangeEnd = addressRange
}
if(!ipList){
ipList = ipRange(ipRangeStart,ipRangeEnd);
}else{
ipList = ipList.concat(ipRange(ipRangeStart,ipRangeEnd))
}
})
var hitList = []
ipList.forEach((ipEntry,n) => {
ports.forEach((portEntry,nn) => {
hitList.push({
xaddr : 'http://' + ipEntry + ':' + portEntry + '/onvif/device_service',
user : onvifUsername,
pass : onvifPassword,
ip: ipEntry,
port: portEntry,
})
})
})
var responseList = []
hitList.forEach(async (camera) => {
try{
var device = new onvif.OnvifDevice(camera)
var info = await device.init()
var date = await device.services.device.getSystemDateAndTime()
var stream = await device.services.media.getStreamUri({
ProfileToken : device.current_profile.token,
Protocol : 'RTSP'
})
};
var cameraResponse = {
ip: camera.ip,
port: camera.port,
info: info,
date: date,
uri: stream.data.GetStreamUriResponse.MediaUri.Uri
const toLong = (ip) => {
let ipl = 0;
ip.split('.').forEach(function(octet) {
ipl <<= 8;
ipl += parseInt(octet);
});
return (ipl >>> 0);
};
const fromLong = (ipl) => {
return ((ipl >>> 24) + '.' +
(ipl >> 16 & 255) + '.' +
(ipl >> 8 & 255) + '.' +
(ipl & 255));
};
const getNetworkAddresses = () => {
const interfaces = os.networkInterfaces();
const addresses = [];
for (const k in interfaces) {
for (const k2 in interfaces[k]) {
const address = interfaces[k][k2];
if (address.family === 'IPv4' && !address.internal) {
addresses.push(address.address);
}
try{
const camPtzConfigs = (await device.services.ptz.getConfigurations()).data.GetConfigurationsResponse
if(
camPtzConfigs.PTZConfiguration &&
(
camPtzConfigs.PTZConfiguration.PanTiltLimits ||
camPtzConfigs.PTZConfiguration.ZoomLimits
)
){
cameraResponse.isPTZ = true
}
}catch(err){
s.debugLog(err)
}
responseList.push(cameraResponse)
var imageSnap
try{
const snapUri = addCredentialsToUrl({
username: onvifUsername,
password: onvifPassword,
url: (await device.services.media.getSnapshotUri({
ProfileToken : device.current_profile.token,
})).data.GetSnapshotUriResponse.MediaUri.Uri,
});
imageSnap = (await getBuffer(snapUri)).toString('base64');
}catch(err){
s.debugLog(err)
}
if(foundCameraCallback)foundCameraCallback(Object.assign(cameraResponse,{f: 'onvif', snapShot: imageSnap}))
}catch(err){
const searchError = (find) => {
return stringContains(find,err.message,true)
}
var foundDevice = false
var errorMessage = ''
switch(true){
//ONVIF camera found but denied access
case searchError('400'): //Bad Request - Sender not Authorized
foundDevice = true
errorMessage = lang.ONVIFErr400
break;
case searchError('405'): //Method Not Allowed
foundDevice = true
errorMessage = lang.ONVIFErr405
break;
//Webserver exists but undetermined if IP Camera
case searchError('404'): //Not Found
foundDevice = true
errorMessage = lang.ONVIFErr404
break;
}
if(foundDevice && foundCameraCallback){
foundCameraCallback({
f: 'onvif',
ff: 'failed_capture',
ip: camera.ip,
port: camera.port,
error: errorMessage
})
}
if(config.debugLogVerbose)s.debugLog(err);
}
})
return responseList
}
return addresses;
};
const getAddressRange = (addresses) => {
const addressRange = [];
addresses.forEach((address) => {
if (address.indexOf('0.0.0') > -1) return;
const addressPrefix = address.split('.').slice(0, 3).join('.');
addressRange.push(`${addressPrefix}.1-${addressPrefix}.254`);
});
return addressRange.join(',');
};
const getPorts = (ports) => {
if (ports === '') {
return '80,8080,8000,7575,8081,9080,8090,8999,8899'.split(',');
}
if (ports.indexOf('-') > -1) {
const [start, end] = ports.split('-');
return portRange(start, end);
}
return ports.split(',');
};
const getIpList = (ip) => {
const ipList = [];
ip.split(',').forEach((range) => {
const [start, end] = range.indexOf('-') > -1 ? range.split('-') : [range, range];
ipList.push(...ipRange(start, end));
});
return ipList;
};
const createHitList = (ipList, ports, onvifUsername = '', onvifPassword = '') => {
const hitList = [];
const usernameVariants = onvifUsername.split(',');
const passwordVariants = onvifPassword.split(',');
for (const username of usernameVariants) {
for (const password of passwordVariants) {
hitList.push(...ipList.flatMap((ipEntry) =>
ports.map((portEntry) => ({
xaddr: `http://${ipEntry}:${portEntry}/onvif/device_service`,
user: username,
pass: password,
ip: ipEntry,
port: portEntry,
}))
));
}
}
return hitList;
};
const takeSnapshot = async (cameraResponse, device) => {
try {
const snapUri = addCredentialsToUrl({
username: cameraResponse.user,
password: cameraResponse.pass,
url: (await device.services.media.getSnapshotUri({ ProfileToken: device.current_profile.token })).data.GetSnapshotUriResponse.MediaUri.Uri,
});
const imgBuffer = await getBuffer(snapUri);
cameraResponse.snapShot = imgBuffer.toString('base64');
} catch (err) {
console.error('Failed to get snapshot via ONVIF:', err);
}
return cameraResponse;
}
const fetchCameraDetails = async (camera, onvifUsername, onvifPassword, foundCameraCallback, failedCameraCallback) => {
const previousSuccess = scanStatus.allSuccessful[camera.ip];
if (previousSuccess) {
// console.log('FOUND PREVIOUS', camera.ip);
foundCameraCallback(previousSuccess);
return;
}
try {
const device = new onvif.OnvifDevice(camera);
const info = await device.init();
const stream = await device.services.media.getStreamUri({
ProfileToken: device.current_profile.token,
Protocol: 'RTSP'
});
const cameraResponse = {
ip: camera.ip,
port: camera.port,
user: camera.user,
pass: camera.pass,
info: info,
uri: stream.data.GetStreamUriResponse.MediaUri.Uri
};
try {
const camPtzConfigs = (await device.services.ptz.getConfigurations()).data.GetConfigurationsResponse;
if (camPtzConfigs.PTZConfiguration && (camPtzConfigs.PTZConfiguration.PanTiltLimits || camPtzConfigs.PTZConfiguration.ZoomLimits)) {
cameraResponse.isPTZ = true;
}
} catch (err) {
console.error(er)
// s.debugLog(err);
}
await takeSnapshot(cameraResponse, device)
scanStatus.allSuccessful[camera.ip] = cameraResponse;
foundCameraCallback(cameraResponse);
return cameraResponse;
} catch (err) {
return handleCameraError(camera, err, failedCameraCallback);
}
};
const handleCameraError = (camera, err, failedCameraCallback) => {
const previousSuccess = scanStatus.allSuccessful[camera.ip];
if (previousSuccess) {
// console.log('FOUND PREVIOUS AFTER ERROR', camera.ip);
return previousSuccess;
}
const searchError = (find) => stringContains(find, err.message, true);
const commonIgnoredErrors = ['ECONNREFUSED', 'socket hang up'];
let foundDevice = false;
let errorMessage = '';
switch (true) {
case searchError('ECONNREFUSED'):
errorMessage = `ECONNREFUSED`;
return {refused: true}
break;
case searchError('TIMEDOUT'):
foundDevice = true;
errorMessage = lang.ONVIFErr401;
break;
case searchError('401'):
foundDevice = true;
errorMessage = lang.ONVIFErr401;
break;
case searchError('400'):
foundDevice = true;
errorMessage = lang.ONVIFErr400;
break;
case searchError('405'):
foundDevice = true;
errorMessage = lang.ONVIFErr405;
break;
case searchError('404'):
foundDevice = true;
errorMessage = lang.ONVIFErr404;
break;
default:
break;
}
if (foundDevice) {
const cameraResponse = {
ip: camera.ip,
port: camera.port,
error: errorMessage,
failedConnection: true,
};
failedCameraCallback(cameraResponse);
return cameraResponse;
}
return null;
};
function isValidOnvifResult(result) {
return result.info || result.uri;
}
function detectAndReplaceReolinkRTSP(camera, url){
const possibilities = [`/h264Preview_01_main`, `/h265Preview_01_main`]
for(possible of possibilities){
// console.log(url, possible, url.indexOf(possible) > -1)
if(url.indexOf(possible) > -1){
return `rtmp://${camera.user}:${camera.pass}@${camera.ip}:1935/bcs/channel0_main.bcs?token=sdasdasd&channel=0&stream=0&user=${camera.user}&password=${camera.pass}`
}
}
return url
}
const runOnvifScanner = async (options, foundCameraCallback, failedCameraCallback) => {
if (scanStatus.isActive) return scanStatus.current;
scanStatus.isActive = true;
scanStatus.abortController = new AbortController();
const { signal } = scanStatus.abortController;
const cancelPromises = [];
scanStatus.cancelPromises = cancelPromises;
let ip = options.ip.replace(/ /g, '');
let ports = options.port.replace(/ /g, '');
const onvifUsername = options.user || 'admin';
const onvifPassword = options.pass || '';
if (ip === '') {
const addresses = getNetworkAddresses();
ip = getAddressRange(addresses);
}
ports = getPorts(ports);
const ipList = getIpList(ip);
const hitList = createHitList(ipList, ports, onvifUsername, onvifPassword);
const ipQueues = {};
const responseList = [];
const allPingSuccess = {};
const fetchWithTimeout = async (camera, onvifUsername, onvifPassword, foundCameraCallback, failedCameraCallback, signal) => {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Timeout')), 2500); // Adjust the timeout as needed
fetchCameraDetails(camera, onvifUsername, onvifPassword, foundCameraCallback, failedCameraCallback, signal)
.then(result => {
clearTimeout(timeout);
resolve(result);
})
.catch(error => {
clearTimeout(timeout);
reject(error);
});
});
};
try {
for (const camera of hitList) {
if (!ipQueues[camera.ip]) {
ipQueues[camera.ip] = async.queue(async (task) => {
if (signal.aborted) {
throw new Error('Aborted');
}
if(!scanStatus.allSuccessful[camera.ip]){
const cameraIp = task.camera.ip;
const hasPingSuccess = allPingSuccess[cameraIp];
if (hasPingSuccess !== false) {
const fetchPromise = fetchWithTimeout(task.camera, task.onvifUsername, task.onvifPassword, task.foundCameraCallback, task.failedCameraCallback, signal);
cancelPromises.push(fetchPromise);
const result = await fetchPromise;
if (result.refused) allPingSuccess[cameraIp] = !result.refused;
if (result.uri){
result.uri = detectAndReplaceReolinkRTSP(task.camera, addCredentialsToUrl({ url: result.uri, username: task.camera.user, password: task.camera.pass }));
}
responseList.push({...result});
}
}
}, 1);
}
ipQueues[camera.ip].push({
camera,
onvifUsername: camera.user,
onvifPassword: camera.pass,
foundCameraCallback,
failedCameraCallback
});
}
await Promise.all(Object.values(ipQueues).map(queue => new Promise((resolve) => queue.drain(resolve))));
} catch (err) {
if (err.message === 'Aborted') {
console.log('Scan aborted');
} else {
console.error('big error', err);
}
}
scanStatus.isActive = false;
scanStatus.abortController = null;
scanStatus.cancelPromises = null;
s.debugLog('Done Scan');
return responseList;
};
const stopOnvifScanner = () => {
if (scanStatus.isActive && scanStatus.abortController) {
scanStatus.abortController.abort();
scanStatus.cancelPromises.forEach(promise => promise.catch(() => {}));
scanStatus.isActive = false;
s.debugLog('Scan stopped');
}
};
function expandIPRange(rangeStr) {
const ipRangeToArray = (start, end) => {
const startParts = start.split('.').map(Number);
const endParts = end.split('.').map(Number);
const ips = [];
for (let a = startParts[0]; a <= endParts[0]; a++) {
for (let b = startParts[1]; b <= endParts[1]; b++) {
for (let c = startParts[2]; c <= endParts[2]; c++) {
for (let d = startParts[3]; d <= endParts[3]; d++) {
ips.push([a, b, c, d].join('.'));
}
}
}
}
return ips;
};
return rangeStr.split(',')
.flatMap(range => {
const [start, end] = range.split('-');
return ipRangeToArray(start.trim(), end.trim());
});
}
return {
ipRange: ipRange,
portRange: portRange,
runOnvifScanner: runOnvifScanner,
}
expandIPRange,
ipRange,
portRange,
scanStatus,
runOnvifScanner,
stopOnvifScanner,
};
}

View File

@ -25,7 +25,7 @@ module.exports = (config) => {
getConfiguration: () => {
return new Promise((resolve,reject) => {
const configPath = config.thisIsDocker ? "/config/conf.json" : s.location.config;
fs.readFile(configPath, 'utf8', (err, data) => {
resolve(JSON.parse(data))
});
@ -33,11 +33,11 @@ module.exports = (config) => {
},
modifyConfiguration: (postBody) => {
return new Promise((resolve, reject) => {
console.log(s.location.config)
console.log(config)
const configPath = config.thisIsDocker ? "/config/conf.json" : s.location.config;
const configData = JSON.stringify(postBody,null,3);
fs.writeFile(configPath, configData, resolve);
});
},

View File

@ -66,10 +66,17 @@ module.exports = (s,config,lang) => {
const videosDirectory = s.getVideoDirectory(monitor)
const tempDirectory = s.getStreamsDirectory(monitor)
// const findCmd = [videosDirectory].concat(options.flags || ['-maxdepth','1'])
fs.writeFileSync(
tempDirectory + 'orphanCheck.sh',
`find "${s.checkCorrectPathEnding(videosDirectory,true)}" -maxdepth 1 -type f -exec stat -c "%n" {} + | sort -r | head -n ${options.checkMax}`
);
try{
fs.writeFileSync(
tempDirectory + 'orphanCheck.sh',
`find "${s.checkCorrectPathEnding(videosDirectory,true)}" -maxdepth 1 -type f -exec stat -c "%n" {} + | sort -r | head -n ${options.checkMax}`
);
}catch(err){
console.log('Failed scanForOrphanedVideos', monitor.ke, monitor.mid)
response.err = err.toString()
resolve(response)
return
}
let listing = spawn('sh',[tempDirectory + 'orphanCheck.sh'])
// const onData = options.onData ? options.onData : () => {}
const onError = options.onError ? options.onError : s.systemLog

View File

@ -54,6 +54,10 @@ module.exports = function(s,config,lang,io){
if(config.renderPaths.grid === undefined){config.renderPaths.grid='pages/grid'}
//slick.js (cycle) page
if(config.renderPaths.cycle === undefined){config.renderPaths.cycle='pages/cycle'}
//WallView page
if(config.renderPaths.wallview === undefined){config.renderPaths.wallview='pages/wallview'}
//WallVideoView page
if(config.renderPaths.wallvideoview === undefined){config.renderPaths.wallvideoview='pages/wallvideoview'}
// Use uws/cws
if(config.useUWebsocketJs === undefined){config.useUWebsocketJs=true}
if(config.webBlocksPreloaded === undefined){

View File

@ -311,7 +311,7 @@ module.exports = function(s,config,lang,app){
} = s.checkPermission(user)
if(
userPermissions.monitor_create_disallowed ||
isRestrictedApiKey && apiKeyPermissions.control_monitors_disallowed ||
isRestrictedApiKey && apiKeyPermissions.edit_monitors_disallowed ||
isRestricted && !monitorPermissions[`${monitorId}_monitor_edit`]
){
s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized']});

View File

@ -31,6 +31,7 @@ module.exports = function(s,config,lang,app,io){
const {
spawnSubstreamProcess,
destroySubstreamProcess,
removeSenstiveInfoFromMonitorConfig,
} = require('./monitor/utils.js')(s,config,lang)
const {
sliceVideo,
@ -771,6 +772,7 @@ module.exports = function(s,config,lang,app,io){
} = s.getMonitorsPermitted(user.details,monitorId)
const {
isRestricted,
userPermissions,
isRestrictedApiKey,
apiKeyPermissions,
} = s.checkPermission(user)
@ -784,6 +786,7 @@ module.exports = function(s,config,lang,app,io){
s.closeJsonResponse(res,[]);
return
}
const cannotSeeImportantSettings = (isRestrictedApiKey && apiKeyPermissions.edit_monitors_disallowed) || userPermissions.monitor_create_disallowed;
s.knexQuery({
action: "select",
columns: "*",
@ -794,6 +797,12 @@ module.exports = function(s,config,lang,app,io){
]
},(err,r) => {
r.forEach(function(v,n){
const monitorId = v.mid;
v.details = JSON.parse(v.details)
var details = v.details;
if(isRestricted && !monitorPermissions[`${monitorId}_monitor_edit`] || cannotSeeImportantSettings){
r[n] = removeSenstiveInfoFromMonitorConfig(v);
}
if(s.group[v.ke] && s.group[v.ke].activeMonitors[v.mid]){
const activeMonitor = s.group[v.ke].activeMonitors[v.mid]
r[n].currentlyWatching = Object.keys(activeMonitor.watch).length
@ -847,7 +856,6 @@ module.exports = function(s,config,lang,app,io){
}
return streamURL
}
var details = JSON.parse(r[n].details);
if(!details.tv_channel_id||details.tv_channel_id==='')details.tv_channel_id = 'temp_'+s.gid(5)
if(details.snap==='1'){
r[n].snapshot = '/'+req.params.auth+'/jpeg/'+v.ke+'/'+v.mid+'/s.jpg'
@ -1094,6 +1102,35 @@ module.exports = function(s,config,lang,app,io){
},res,req);
});
/**
* Page : Get Wall Video View (Wall Timeline)
*/
app.get(config.webPaths.apiPrefix+':auth/wallvideoview/:ke', function (req,res){
s.auth(req.params,function(user){
const authKey = req.params.auth
const groupKey = req.params.ke
if(
user.permissions.watch_videos === "0"
&& user.details.allmonitors !== '1'
){
res.end(user.lang['Not Permitted'])
return
}
s.renderPage(req,res,config.renderPaths.wallvideoview,{
forceUrlPrefix: req.query.host || '',
data: req.params,
protocol: req.protocol,
baseUrl: req.protocol + '://' + req.hostname,
config: s.getConfigWithBranding(req.hostname),
define: s.getDefinitonFile(user.details ? user.details.lang : config.lang),
lang: lang,
$user: user,
authKey: authKey,
groupKey: groupKey,
originalURL: s.getOriginalUrl(req)
});
},res,req);
});
/**
* API : Get Events
*/
app.get([

View File

@ -415,4 +415,33 @@ module.exports = function(s,config,lang,app){
},res,req);
},res,req);
});
/**
* Page : Get WallView
*/
app.get(config.webPaths.apiPrefix+':auth/wallview/:ke', function (req,res){
s.auth(req.params,function(user){
const authKey = req.params.auth
const groupKey = req.params.ke
if(
user.permissions.watch_stream === "0"
&& user.details.allmonitors !== '1'
){
res.end(user.lang['Not Permitted'])
return
}
s.renderPage(req,res,config.renderPaths.wallview,{
forceUrlPrefix: req.query.host || '',
data: req.params,
protocol: req.protocol,
baseUrl: req.protocol + '://' + req.hostname,
config: s.getConfigWithBranding(req.hostname),
define: s.getDefinitonFile(user.details ? user.details.lang : config.lang),
lang: lang,
$user: user,
authKey: authKey,
groupKey: groupKey,
originalURL: s.getOriginalUrl(req)
});
},res,req);
});
}

3952
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -33,20 +33,20 @@
"googleapis": "^100.0.0",
"http-proxy": "^1.18.1",
"jsonfile": "^3.0.1",
"knex": "^0.21.21",
"knex": "^3.1.0",
"ldapauth-fork": "^5.0.2",
"marked": "^4.3.0",
"moment": "^2.29.4",
"mp4frag": "^0.6.1",
"mqtt": "^4.3.7",
"mysql": "^2.18.1",
"mysql2": "^2.1.0",
"mysql2": "^3.9.7",
"node-abort-controller": "^3.0.1",
"node-fetch": "^2.6.7",
"node-onvif-events": "^2.0.5",
"node-ssh": "^12.0.4",
"node-telegram-bot-api": "^0.61.0",
"node-telegram-bot-api": "^0.65.1",
"nodemailer": "^6.7.1",
"onvif": "^0.7.1",
"pam-diff": "^1.1.0",
"path": "^0.12.7",
"pipe2pam": "^0.6.2",
@ -54,7 +54,7 @@
"pushover-notifications": "^1.2.2",
"sat": "^0.7.1",
"shinobi-node-moving-things-tracker": "^0.9.3",
"shinobi-onvif": "0.1.9",
"shinobi-onvif": "0.2.2",
"shinobi-sound-detection": "^0.1.13",
"shinobi-zwave": "^1.0.11",
"smtp-server": "^3.13.0",

68
tools/downloadPlugins.js Normal file
View File

@ -0,0 +1,68 @@
const fetch = require('node-fetch');
const AdmZip = require('adm-zip');
const fs = require('fs').promises;
const path = require('path');
const GITLAB_API_URL = 'https://gitlab.com/api/v4';
const PROJECT_ID = 'Shinobi-Systems/shinobi-plugins';
const BRANCH = 'master';
async function fetchAndDownloadFolder(folderName) {
const url = `${GITLAB_API_URL}/projects/${encodeURIComponent(PROJECT_ID)}/repository/archive.zip?path=plugins/${folderName}&ref=${BRANCH}`;
try {
const response = await fetch(url, {
headers: {
'Accept': 'application/zip',
},
});
if (!response.ok) {
throw new Error(`Failed to download folder: ${response.statusText}`);
}
const buffer = await response.buffer();
const zip = new AdmZip(buffer);
// Extract the ZIP to a temporary location
const tempExtractPath = path.join(process.cwd(), 'temp_extracted');
await fs.mkdir(tempExtractPath, { recursive: true });
zip.extractAllTo(tempExtractPath, true);
// Find the folder ending with the target name
const extractedFolder = (await fs.readdir(tempExtractPath)).find(dir => dir.endsWith(folderName));
if (!extractedFolder) {
throw new Error(`Folder ${folderName} not found in the extracted ZIP`);
}
// Move the contents to the specified output path
const sourcePath = path.join(tempExtractPath, extractedFolder, 'plugins', folderName);
const targetPath = path.join(process.cwd(), 'plugins', `dl_${folderName}`);
// Ensure target path exists
await fs.mkdir(targetPath, { recursive: true });
// Copy files from source to target
const files = await fs.readdir(sourcePath);
await Promise.all(files.map(async file => {
const srcFile = path.join(sourcePath, file);
const destFile = path.join(targetPath, file);
await fs.rename(srcFile, destFile);
}));
// Clean up temporary extraction directory
await fs.rm(tempExtractPath, { recursive: true });
console.log(`Folder ${folderName} extracted to ${targetPath}`);
} catch (error) {
console.error('Error fetching and extracting folder:', error);
}
}
// Parse command-line arguments
const [folderName] = process.argv.slice(2);
if (!folderName) {
console.error('Usage: node script.js <folderName>');
process.exit(1);
}

View File

@ -0,0 +1,90 @@
const onvif = require('shinobi-onvif');
const fs = require('fs');
const { getDeviceInformation, mergeDeep, getFunctionParamNames } = require('./onvifUtilsForTest.js');
// Fetch command line arguments
const args = process.argv.slice(2);
const jsonFilePath = args[0];
const streamType = args[1]; // 'main' or 'sub'
const newBitrate = parseInt(args[2]); // Bitrate in kbps
if(!jsonFilePath || !streamType || !newBitrate){
console.log(`node massBitrateUpdateOnvif.js ./exportedShinobiMonitors.json main 1500`)
return
}
// Read camera configurations from a JSON file
const cameras = JSON.parse(fs.readFileSync(jsonFilePath, 'utf8'));
function tokenCheck(token){
return streamType === 'sub' ?
token.includes('sub')
|| token.includes('Sub')
|| token.includes('1')
:
token.includes('main')
|| token.includes('Main')
|| token.includes('0')
}
// Function to update the bitrate
const updateBitrate = async (camera, newBitrate, streamType) => {
try {
const device = new onvif.OnvifDevice({
xaddr: `http://${camera.host}:${camera.details.onvif_port}/onvif/device_service`,
user: camera.details.muser,
pass: camera.details.mpass
});
await device.init();
// Determine which stream type to update
const configs = (await device.services.media.getVideoEncoderConfigurations()).data.GetVideoEncoderConfigurationsResponse.Configurations;
const config = configs.find(c => tokenCheck(c.$.token));
if (!config) {
console.log(`No ${streamType} stream configuration found for camera ${camera.host}`);
return;
}
const chosenToken = config.$.token;
const {
videoEncoders,
videoEncoderOptions
} = await getDeviceInformation(device,{
videoEncoders: true,
videoEncoderOptions: true
});
const videoEncoderIndex = {};
videoEncoders.forEach((encoder) => {videoEncoderIndex[encoder.$.token] = encoder});
const videoEncoder = videoEncoderIndex[chosenToken] || {};
const theChanges = {
RateControl: {
BitrateLimit: newBitrate
}
};
const onvifParams = mergeDeep(videoEncoder,theChanges);
const newConfig = {
ConfigurationToken: config.$.token,
Configuration: onvifParams
};
if(newBitrate != videoEncoder.RateControl.BitrateLimit){
console.log(videoEncoder)
const updateResponse = await device.services.media.setVideoEncoderConfiguration(newConfig);
console.log('updateResponse',updateResponse.data)
console.log(`Updated ${streamType} stream bitrate for camera ${camera.host} to ${newBitrate} kbps`);
}else{
console.log(`Skipped ${camera.host}`)
}
} catch (err) {
console.error(`Error updating bitrate for camera ${camera.host}:`, err);
}
};
// Iterate over cameras sequentially and update bitrate for the specified stream type
const updateAllCameras = async () => {
for (const camera of cameras) {
await updateBitrate(camera, newBitrate, streamType);
}
};
updateAllCameras();

546
tools/onvifUtilsForTest.js Normal file
View File

@ -0,0 +1,546 @@
// relies on https://gitlab.com/Shinobi-Systems/shinobi-onvif
const onvif = require('shinobi-onvif');
const getFunctionParamNames = function(func) {
var fnStr = func.toString().replace(/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg, '');
var result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(/([^\s,]+)/g);
if(result === null)
result = [];
return result;
}
const mergeDeep = function(...objects) {
const isObject = obj => obj && typeof obj === 'object';
return objects.reduce((prev, obj) => {
Object.keys(obj).forEach(key => {
const pVal = prev[key];
const oVal = obj[key];
if (Array.isArray(pVal) && Array.isArray(oVal)) {
prev[key] = pVal.concat(...oVal);
}
else if (isObject(pVal) && isObject(oVal)) {
prev[key] = mergeDeep(pVal, oVal);
}
else {
prev[key] = oVal;
}
});
return prev;
}, {});
}
const replaceDynamicInOptions = (Camera,options) => {
const newOptions = {}
Object.keys(options).forEach((key) => {
const value = options[key]
if(typeof value === 'string'){
newOptions[key] = value.replace(/__CURRENT_TOKEN/g,Camera.current_profile.token)
}else if(value !== undefined && value !== null){
newOptions[key] = value
}
})
return newOptions
}
const runOnvifMethod = async (onvifOptions) => {
return new Promise((resolve,reject) => {
const onvifDevice = onvifOptions.device
var response = {ok: false}
var errorMessage = function(msg,error){
response.ok = false
response.msg = msg
response.error = error
resolve(response)
}
var actionCallback = function(onvifActionResponse){
response.ok = true
if(onvifActionResponse.data){
response.responseFromDevice = onvifActionResponse.data
}else{
response.responseFromDevice = onvifActionResponse
}
if(onvifActionResponse.soap)response.soap = onvifActionResponse.soap
resolve(response)
}
var isEmpty = function(obj) {
for(var key in obj) {
if(obj.hasOwnProperty(key))
return false;
}
return true;
}
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
resolve(response)
}else{
response.error = 'Big Errors, Please report it to Shinobi Development'
resolve(response)
}
}
var action
if(onvifOptions.service){
if(onvifDevice.services[onvifOptions.service] === undefined){
return errorMessage('This is not an available service. Please use one of the following : '+Object.keys(onvifDevice.services).join(', '))
}
if(onvifDevice.services[onvifOptions.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 = onvifDevice.services[onvifOptions.service][onvifOptions.action]
}else{
action = onvifDevice[onvifOptions.action]
}
if(!action || typeof action !== 'function'){
errorMessage(onvifOptions.action+' is not an available ONVIF function. See https://github.com/futomi/node-onvif for functions.')
}else{
var argNames = getFunctionParamNames(action)
var options
var command
if(argNames[0] === 'options' || argNames[0] === 'params'){
options = replaceDynamicInOptions(onvifDevice,onvifOptions.options || {})
response.options = options
}
if(onvifOptions.service){
command = onvifDevice.services[onvifOptions.service][onvifOptions.action](options)
}else{
command = onvifDevice[onvifOptions.action](options)
}
completeAction(command)
}
})
}
const getDeviceInformation = async (onvifDevice,options) => {
// const options = {
// protocols: true,
// networkInterface: true,
// gateway: true,
// dns: true,
// ntp: true,
// }
const response = {
ok: true,
}
try{
if(options.all || options.protocols){
response.protocols = (await onvifDevice.services.device.getNetworkProtocols()).data.GetNetworkProtocolsResponse.NetworkProtocols
}
if(options.all || options.networkInterface){
response.networkInterface = (await onvifDevice.services.device.getNetworkInterfaces()).data.GetNetworkInterfacesResponse.NetworkInterfaces
}
if(options.all || options.gatewayAddress){
response.gateway = (await onvifDevice.services.device.getNetworkDefaultGateway()).data.GetNetworkDefaultGatewayResponse.NetworkGateway.IPv4Address
}
if(options.all || options.dns){
response.dns = (await onvifDevice.services.device.getDNS()).data.GetDNSResponse.DNSInformation
}
if(options.all || options.ntp){
response.ntp = (await onvifDevice.services.device.getNTP()).data.GetNTPResponse.NTPInformation
}
if(options.all || options.date){
response.date = (await onvifDevice.services.device.getSystemDateAndTime()).data.GetSystemDateAndTimeResponse.SystemDateAndTime
}
if(options.all || options.users){
response.users = (await onvifDevice.services.device.getUsers()).data.GetUsersResponse.User
}
if(options.all || options.hostname){
response.hostname = (await onvifDevice.services.device.getHostname()).data.GetHostnameResponse.HostnameInformation.Name
}
if(options.all || options.discoveryMode){
response.discoveryMode = (await onvifDevice.services.device.getDiscoveryMode()).data.GetDiscoveryModeResponse.DiscoveryMode
}
if(options.all || options.videoEncoders){
response.videoEncoders = (await onvifDevice.services.media.getVideoEncoderConfigurations()).data.GetVideoEncoderConfigurationsResponse.Configurations
}
if(options.all || options.videoEncoderOptions){
response.videoEncoderOptions = (await runOnvifMethod({
device: onvifDevice,
action: 'getVideoEncoderConfigurationOptions',
service: 'media',
options: {}
})).responseFromDevice.GetVideoEncoderConfigurationOptionsResponse.Options
}
if(options.all || options.imagingSettings){
const imagingSettings = (await runOnvifMethod({
device: onvifDevice,
action: 'getImagingSettings',
service: 'imaging',
options: {"ConfigurationToken":"__CURRENT_TOKEN"}
}));
response.imagingSettings = imagingSettings.responseFromDevice ? imagingSettings.responseFromDevice.GetImagingSettingsResponse.ImagingSettings : imagingSettings
}
}catch(err){
response.ok = false
response.error = err.stack.toString()
console.log(err)
console.log(onvifDevice)
}
return response
}
const setProtocols = async (onvifDevice,saveSet) => {
// const saveSet = {
// "HTTP": "80",
// "RTSP": "554",
// }
const response = {
ok: false
}
try{
const saveKeys = Object.keys(saveSet)
const protocols = (await getDeviceInformation(onvifDevice,{protocols: true})).protocols
protocols.forEach((item) => {
saveKeys.forEach((key) => {
if(item.Name === key.toUpperCase()){
const port = saveSet[key] && !isNaN(saveSet[key]) ? parseInt(saveSet[key]) : item.Name === 'RTSP' ? 554 : 80
console.log('PORT',port)
item.Port = port
item.Enabled = true
}
})
})
console.log('protocols',protocols)
const onvifResponse = await onvifDevice.services.device.setNetworkProtocols({
NetworkProtocols: protocols
})
response.ok = true
response.onvifResponse = onvifResponse
}catch(err){
response.error = err.stack.toString()
}
return response
}
const setNetworkInterface = async (onvifDevice,options) => {
// const options = {
// interface: 'eth0',
// ipv4: '10.1.103.158',
// dhcp: false,
// }
const response = {
ok: false
}
try{
const { networkInterface } = await getDeviceInformation(onvifDevice,{networkInterface: true})
const onvifParams = {
SetNetworkInterfaces: {
InterfaceToken: options.interface || networkInterface.$.token,
NetworkInterface: {
Enabled: true,
IPv4: {
Enabled: true,
Manual: {
Address: options.ipv4 || networkInterface.IPv4.Config.Manual.Address,
PrefixLength: '24',
},
'DHCP': options.dhcp === undefined || options.dhcp === null ? networkInterface.IPv4.Config.DHCP === 'true' ? true : false : options.dhcp,
}
}
}
}
const onvifResponse = await onvifDevice.services.device.setNetworkInterfaces(onvifParams)
response.ok = true
response.onvifResponse = onvifResponse
}catch(err){
response.error = err.stack.toString()
}
return response
}
const setGateway = async (onvifDevice,options) => {
// const options = {
// ipv4: '1.1.1.1,8.8.8.8',
// }
const response = {
ok: false
}
try{
const gatewayAddress = (await getDeviceInformation(onvifDevice,{gateway: true})).gateway
const onvifResponse = await onvifDevice.services.device.setNetworkDefaultGateway({
'NetworkGateway': [
{'IPv4Address': options.ipv4 || gatewayAddress}
]
})
response.ok = true
response.onvifResponse = onvifResponse
}catch(err){
response.error = err.stack.toString()
}
return response
}
const setDNS = async (onvifDevice,options) => {
options = Object.assign({
dns: '1.1.1.1,8.8.8.8',
searchDomain: 'localhost',
dhcp: false,
},options)
const response = {
ok: false
}
try{
const dnsArray = []
const searchDomain = (options.searchDomain || '').split(',')
const dnsList = (options.dns || '1.1.1.1').split(',')
dnsList.forEach((item) => {
dnsArray.push({
Type: 'IPv4',
IPv4Address: item,
})
})
const dnsInfo = (await getDeviceInformation(onvifDevice,{dns: true})).dns
const onvifResponse = await onvifDevice.services.device.setDNS({
'FromDHCP' : !options.dhcp ? false : true,
'SearchDomain': searchDomain,
'DNSManual' : dnsArray
})
response.ok = true
response.onvifResponse = onvifResponse
}catch(err){
response.error = err.stack.toString()
}
return response
}
const setNTP = async (onvifDevice,options) => {
// const options = {
// ipv4: '1.1.1.1,8.8.8.8',
// dhcp: false,
// }
const response = {
ok: false
}
try{
const ntpInfo = (await getDeviceInformation(onvifDevice,{ntp: true})).ntp
const ipv4 = options.ipv4 || ntpInfo.NTPManual.IPv4Address
const onvifResponse = await onvifDevice.services.device.setNTP({
FromDHCP: !options.dhcp ? false : true,
NTPManual: {'Type': "IPv4", 'IPv4Address': ipv4}
})
response.ok = true
response.onvifResponse = onvifResponse
}catch(err){
response.error = err.stack.toString()
}
return response
}
const setHostname = async (onvifDevice,options) => {
// const options = {
// name: 'hostname',
// }
const response = {
ok: false
}
try{
const hostname = options.name || (await getDeviceInformation(onvifDevice,{hostname: true})).hostname
const onvifResponse = await onvifDevice.services.device.setHostname({
Name: hostname
})
response.ok = true
response.onvifResponse = onvifResponse
}catch(err){
response.error = err.stack.toString()
}
return response
}
const rebootCamera = async (onvifDevice,options) => {
const response = {
ok: false
}
try{
const onvifResponse = await onvifDevice.services.device.reboot()
response.ok = true
response.onvifResponse = onvifResponse
}catch(err){
response.error = err.stack.toString()
}
return response
}
const setDateAndTime = async (onvifDevice,options) => {
// const options = {
// dateTimeType: 'ntp',
// daylightSavings: false,
// timezone: 'XXXX',
// utcDateTime: 'XXXX',
// }
const response = {
ok: false
}
try{
// const dateInfo = await onvifDevice.services.device.getSystemDateAndTime().GetSystemDateAndTimeResponse.SystemDateAndTime
const onvifResponse = await onvifDevice.services.device.setSystemDateAndTime ({
DateTimeType: options.dateTimeType,
DaylightSavings: !options.daylightSavings ? false : true,
// TimeZone: options.timezone,
UTCDateTime: new Date(options.utcDateTime),
})
response.ok = true
response.onvifResponse = onvifResponse
}catch(err){
response.error = err.stack.toString()
}
return response
}
const createUser = async (onvifDevice,options) => {
// const options = {
// name: 'user1',
// password: '123',
// level: 'Administrator' || 'Operator' || 'User',
// }
const response = {
ok: false
}
try{
const onvifResponse = await onvifDevice.services.device.createUsers({
'User' : [
{'Username': options.name, 'Password' : options.password, 'UserLevel': options.level}
]
})
response.ok = true
response.onvifResponse = onvifResponse
}catch(err){
response.error = err.stack.toString()
}
return response
}
const deleteUser = async (onvifDevice,options) => {
// const options = {
// name: 'user1',
// }
const response = {
ok: false
}
try{
const onvifResponse = await onvifDevice.services.device.deleteUsers({
'User' : [
{'Username': options.name}
]
})
response.ok = true
response.onvifResponse = onvifResponse
}catch(err){
response.error = err.stack.toString()
}
return response
}
const validateEncoderOptions = (chosenOptions, videoEncoderOptions) => {
const resolutions = []
const minQuality = parseInt(videoEncoderOptions.QualityRange.Min) || 1
const maxQuality = parseInt(videoEncoderOptions.QualityRange.Max) || 6
const quality = !isNaN(chosenOptions.Quality) ? parseInt(chosenOptions.Quality) : 1
const minGovLength = parseInt(videoEncoderOptions.H264.GovLengthRange.Min) || 0
const maxGovLength = parseInt(videoEncoderOptions.H264.GovLengthRange.Max) || 100
const govLength = !isNaN(chosenOptions.H264.GovLength) ? parseInt(chosenOptions.H264.GovLength) : 10
const minFrameRateLimit = parseInt(videoEncoderOptions.H264.FrameRateRange.Min) || 1
const maxFrameRateLimit = parseInt(videoEncoderOptions.H264.FrameRateRange.Max) || 30
const frameRateLimit = !isNaN(chosenOptions.RateControl.FrameRateLimit) ? parseInt(chosenOptions.RateControl.FrameRateLimit) : 15
const minEncodingInterval = parseInt(videoEncoderOptions.H264.EncodingIntervalRange.Min) || 1
const maxEncodingInterval = parseInt(videoEncoderOptions.H264.EncodingIntervalRange.Max) || 30
const encodingInterval = !isNaN(chosenOptions.RateControl.EncodingInterval) ? parseInt(chosenOptions.RateControl.EncodingInterval) : 1
videoEncoderOptions.H264.ResolutionsAvailable.forEach((resolution) => {
resolutions.push(`${resolution.Width}x${resolution.Height}`)
})
if(resolutions.indexOf(`${chosenOptions.Resolution.Width}x${chosenOptions.Resolution.Height}`) === -1){
chosenOptions.Resolution.Width = resolutions[0].Width
chosenOptions.Resolution.Height = resolutions[0].Height
}
if(quality < minQuality || quality > maxQuality){
chosenOptions.Quality = 1
}
if(govLength < minGovLength || govLength > maxGovLength){
chosenOptions.H264.GovLength = 10
}
if(frameRateLimit < minFrameRateLimit || frameRateLimit > maxFrameRateLimit){
chosenOptions.RateControl.FrameRateLimit = 15
}
if(encodingInterval < minEncodingInterval || encodingInterval > maxEncodingInterval){
chosenOptions.RateControl.EncodingInterval = 1
}
}
const setVideoConfiguration = async (onvifDevice,options) => {
const response = {
ok: false
}
try{
const {
videoEncoders,
videoEncoderOptions
} = await getDeviceInformation(onvifDevice,{
videoEncoders: true,
videoEncoderOptions: true
})
const videoEncoderIndex = {}
videoEncoders.forEach((encoder) => {videoEncoderIndex[encoder.$.token] = encoder})
const chosenToken = `${options.videoToken || videoEncoders[0].$.token}`
const videoEncoder = videoEncoderIndex[chosenToken] || {}
const onvifParams = mergeDeep(videoEncoder,options,{
Multicast: {
AutoStart: !options.Multicast || !options.Multicast.AutoStart ? 'false' : 'true'
},
})
const validatedEncoderOptions = validateEncoderOptions(onvifParams, videoEncoderOptions)
const onvifResponse = await onvifDevice.services.media.setVideoEncoderConfiguration({
Configuration: onvifParams,
ConfigurationToken: chosenToken
})
response.ok = true
response.onvifResponse = onvifResponse
}catch(err){
response.error = err.stack.toString()
}
return response
}
const setImagingSettings = async (onvifDevice,options) => {
const response = {
ok: false
}
try{
const { imagingSettings } = await getDeviceInformation(onvifDevice,{imagingSettings: true})
const onvifParams = mergeDeep(imagingSettings,options)
const onvifResponse = await onvifDevice.services.imaging.setImagingSettings({
ConfigurationToken: options.videoToken || onvifDevice.current_profile.token,
ImagingSettings: onvifParams
})
response.ok = true
response.onvifResponse = onvifResponse
}catch(err){
response.error = err.stack.toString()
}
return response
}
const setDiscoveryMode = async (onvifDevice,options) => {
const response = {
ok: false
}
try{
const onvifResponse = await onvifDevice.services.device.setDiscoveryMode(options.mode === 'Discoverable' || options.mode === 'NonDiscoverable' ? options.mode : 'Discoverable')
response.ok = true
response.onvifResponse = onvifResponse
}catch(err){
response.error = err.stack.toString()
}
return response
}
const getUIFieldValues = async (onvifDevice) => {
return await getDeviceInformation(onvifDevice,{
all: true,
})
}
module.exports = {
mergeDeep,
getFunctionParamNames,
getDeviceInformation: getDeviceInformation,
setHostname: setHostname,
setProtocols: setProtocols,
setGateway: setGateway,
setDNS: setDNS,
setNTP: setNTP,
rebootCamera: rebootCamera,
setDateAndTime: setDateAndTime,
createUser: createUser,
deleteUser: deleteUser,
setVideoConfiguration: setVideoConfiguration,
setImagingSettings: setImagingSettings,
setDiscoveryMode: setDiscoveryMode,
setNetworkInterface: setNetworkInterface,
getUIFieldValues: getUIFieldValues,
}

View File

@ -3,12 +3,22 @@
background-size: cover;
height: 100px;
}
#tab-onvifScanner .onvif_result .card {
/* #tab-onvifScanner .onvif_result .card {
cursor: pointer;
}
} */
#tab-onvifScanner .onvif_result .card-body {
min-height: auto;
}
#tab-onvifScanner .onvif_result {
padding-top:2rem;
padding-top: 0.5rem;
}
#tab-onvifScanner .onvif_result .uri{
word-break: break-word;
}
#tab-onvifScanner .scan-item-img {
width: 100px;
border-radius: 7px;
height: 100px;
background-size: cover;
background-color: #1f80f9;
}

View File

@ -0,0 +1,89 @@
#wallview-container {
position: absolute;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.9);
}
#wallview-canvas,body {
position: relative;
width: 100vw;
height: 100vh;
flex-grow: 1;
overflow: hidden;
flex: 1;
}
#wallview-controls{
position: absolute;
width: 100%;
top: 0;
padding: 5px;
opacity: 0;
transition: 0.2s;
z-index: 11;
}
#wallview-controls:hover{
opacity: 100%;
background: rgba(0,0,0,0.2)
}
#wallview-info-screen{
z-index: 10;
background-color: rgba(0,0,0,0.6);
position: fixed;
width: 100vw;
height: 100vh;
}
#wallview-monitorList i{
display: none;
}
#wallview-monitorList .active i{
display: inlin-block;
}
#wallview-canvas .wallview-video {
position: absolute;
}
#wallview-canvas .wallview-video .overlay {
position: absolute;
height: 100%;
width: 100%;
border: 0;
}
#wallview-canvas .wallview-video iframe {
height: 100%;
width: 100%;
border: 0;
}
#wallview-canvas .wallview-video.col-md-4 {
height:30vh;
}
#wallview-canvas .wallview-video.col-md-6 {
height: 40vh;
}
#wallview-canvas .wallview-video.col-md-12 {
height: 80vh;
margin-bottom: 0.5rem !important;
}
#wallview-canvas .wallview-video:not(.no-video){
background-color: #000!important;
}
#wallview-canvas .wallview-video.no-video{
display: none;
}
#wallview-canvas.show-non-playing .wallview-video.no-video{
display: flex;
}
#wallview-canvas .wallview-video .wallview-item-controls{
width: 100%;
padding: 5px;
opacity: 0;
}
#wallview-canvas .wallview-video:hover .wallview-item-controls{
opacity: 0.5;
}

View File

@ -35,6 +35,12 @@
#easyRemoteAccess .card.active .fa {
color: #6ee068;
}
#easyRemoteAccess .card .remote-dashboard-link {
display: none!important;
}
#easyRemoteAccess .card.active .remote-dashboard-link {
display: block!important;
}
#easyRemoteAccess .card .selected-badge {
display: none;
color: #fff!important;
@ -56,3 +62,7 @@
#p2pServerList .card .d-flex.flex-row:nth-of-type(odd) {
background: rgba(0,0,0,0.1)
}
#easyRemoteAccess .active .activate-remote-selection .fa-check:before {
content: "\f00d"!important;
}

View File

@ -0,0 +1,59 @@
$(document).ready(function(){
var alternateLoginsBox = $('#alternate-logins')
function getAlternateLogins(){
$.get(getApiPrefix('loginTokens'),function(data){
var rows = data.rows
alternateLoginsBox.empty()
if(rows.length > 0){
$.each(rows,function(n,row){
alternateLoginsBox.append(`<div class="row" login-id="${row.loginId}" style="border-bottom: 1px solid #333;padding-top:1rem;padding-bottom:1rem;display:flex">
<div class="col-md-3" style="flex: 3">
<i class="fa fa-address-card"></i> &nbsp;
<span class="epic-text text-white">${row.type}</span>
</div>
<div class="col-md-3" style="flex: 3">
<div>${row.name}</div>
</div>
<div class="col-md-3" style="flex: 3">
<div title="${lang.lastLogin}">${moment(row.lastLogin).format('YYYY-MM-DD hh:mm:ss A')}</div>
</div>
<div class="col-md-3 text-right" style="flex: 3">
<a class="btn badge-sm badge btn-danger unlink-account"><i class="fa fa-unlink"></i> ${lang.Unlink}</a>
</div>
</div>`)
})
}else{
alternateLoginsBox.append(`<div class="row">
<div class="col-md-12 text-center epic-text" style="margin: 0">
${lang.noLoginTokensAdded}
</div>
</div>`)
}
})
}
getAlternateLogins()
alternateLoginsBox.on('click','.unlink-account',function(){
var loginId = $(this).parents('[login-id]').attr('login-id')
$.confirm.create({
title: lang['Unlink Login'],
body: lang.noUndoForAction,
clickOptions: {
title: lang['Unlink'],
class: 'btn-danger'
},
clickCallback: function(){
$.get(getApiPrefix('loginTokens') + '/' + loginId + '/delete',function(data){
if(data.ok){
new PNotify({
title: lang.Unlinked,
text: lang.loginHandleUnbound,
type: 'success'
})
alternateLoginsBox.find(`[login-id="${loginId}"]`).remove()
}
})
}
})
})
window.drawAlternateLoginsToSettings = getAlternateLogins
})

View File

@ -0,0 +1,180 @@
$(document).ready(function(){
onInitWebsocket(function(){
loadMonitorsIntoMemory(function(data){
setInterfaceCounts(data)
openTab('initial')
onDashboardReadyExecute()
})
});
$('body')
// .on('tab-away',function(){
//
// })
// .on('tab-close',function(){
//
// })
.on('click','.toggle-accordion-list',function(e){
e.preventDefault();
var el = $(this)
var iconEl = el.find('i')
var targetEl = el.parent().find('.accordion-list').first()
targetEl.toggle()
el.toggleClass('btn-primary btn-success')
iconEl.toggleClass('fa-plus fa-minus')
return false;
})
.on('click','.toggle-display-of-el',function(e){
e.preventDefault();
var el = $(this)
var target = el.attr('data-target')
var targetFirstOnly = el.attr('data-get') === 'first'
var startIsParent = el.attr('data-start') === 'parent'
var targetEl = startIsParent ? el.parent().find(target) : $(target)
if(targetFirstOnly){
targetEl = targetEl.first()
}
targetEl.toggle()
return false;
})
.on('click','.pop-image',function(){
var imageSrc = $(this).attr('src')
if(!imageSrc){
new PNotify({
title: lang['Action Failed'],
text: lang['No Image'],
type: 'warning'
})
return;
};
popImage(imageSrc)
})
.on('click','.popped-image',function(){
$(this).remove()
})
.on('click','.popped-image img',function(e){
e.stopPropagation()
return false;
})
.on('click','.go-home',goBackHome)
.on('click','.go-back',goBackOneTab)
.on('click','.delete-tab',function(e){
e.preventDefault()
e.stopPropagation()
var tabName = $(this).parents(`[page-open]`).attr(`page-open`)
if(activeTabName === tabName){
goBackOneTab()
}
deleteTab(tabName)
return false;
})
.on('click','.delete-tab-dynamic',function(e){
e.preventDefault()
e.stopPropagation()
var tabName = $(this).parents('.page-tab').attr('id').replace('tab-','')
goBackOneTab()
deleteTab(tabName)
return false;
})
.on('click','[page-open]',function(){
var el = $(this)
var pageChoice = el.attr('page-open')
var pageOptions = JSON.parse(el.attr('page-options') || '{}')
if(tabTree.name === pageChoice)return;
openTab(pageChoice,pageOptions)
})
.on('click','[class_toggle]',function(){
var el = $(this)
var targetElement = el.attr('data-target')
var classToToggle = el.attr('class_toggle')
var iconClassesToToggle = el.attr('icon-toggle').split(' ')
var iconTarget = el.attr('icon-child')
var iconTargetElement = el.find(el.attr('icon-child'))
var togglPosition = $(targetElement).hasClass(classToToggle) ? 0 : 1
var classToggles = dashboardOptions().class_toggle || {}
classToggles[targetElement] = [classToToggle,togglPosition,iconClassesToToggle,iconTarget];
dashboardOptions('class_toggle',classToggles)
$(targetElement).toggleClass(classToToggle)
iconTargetElement
.removeClass(iconClassesToToggle[togglPosition === 1 ? 0 : 1])
.addClass(iconClassesToToggle[togglPosition])
})
.on('keyup','.search-parent .search-controller',function(){
var _this = this;
var parent = $(this).parents('.search-parent')
$.each(parent.find(".search-body .search-row"), function() {
if($(this).text().toLowerCase().indexOf($(_this).val().toLowerCase()) === -1)
$(this).hide();
else
$(this).show();
});
})
.on('click','[tab-chooser]',function(){
var el = $(this)
var parent = el.parents('[tab-chooser-parent]')
var tabName = el.attr('tab-chooser')
var allTabChoosersInParent = parent.find('[tab-chooser]')
var allTabsInParent = parent.find('[tab-section]')
allTabsInParent.hide()
allTabChoosersInParent.removeClass('active')
el.addClass('active')
parent.find(`[tab-section="${tabName}"]`).show()
});
if(!isMobile){
$('body').on('mousedown',"select[multiple]",function(e){
e.preventDefault();
var select = this;
var scroll = select .scrollTop;
e.target.selected = !e.target.selected;
setTimeout(function(){select.scrollTop = scroll;}, 0);
$(select).focus().change();
}).on('mousemove',"select[multiple]",function(e){
e.preventDefault()
});
}
$('.logout').click(function(e){
$.get(getApiPrefix() + '/logout/' + $user.ke + '/' + $user.uid,function(data){
localStorage.removeItem('ShinobiLogin_'+location.host);
location.href = location.href.split('#')[0];
})
})
// only binded on load
$('.form-section-header:not(.no-toggle-header)').click(function(e){
var parent = $(this).parent('.form-group-group')
var boxWrapper = parent.attr('id')
parent.toggleClass('hide-box-wrapper')
var hideBoxWrapper = parent.hasClass('hide-box-wrapper')
boxWrappersHidden[boxWrapper] = hideBoxWrapper
dashboardOptions('boxWrappersHidden',boxWrappersHidden)
})
$('[data-bs-target="#sidebarMenu"]').click(function(e){
resizeMonitorIcons()
})
if(!isMobile){
var clicked = false, clickX, oldClickX;
var htmlBody = $('html')
pageTabLinks.on({
'mousemove': function(e) {
clicked && updateScrollPos(e);
},
'mousedown': function(e) {
e.preventDefault();
clicked = true;
oldClickX = clickX + 0;
clickX = e.pageX;
},
'mouseup': function(e) {
if(oldClickX !== clickX){
e.preventDefault()
}
clicked = false;
htmlBody.css('cursor', 'auto');
}
});
var updateScrollPos = function(e) {
htmlBody.css('cursor', 'grabbing');
pageTabLinks.scrollLeft(pageTabLinks.scrollLeft() + (clickX - e.pageX));
}
}
})

View File

@ -239,7 +239,7 @@ function getApiHost(path,isAdmin){
return getLocation() + (window.adminApiPrefix && isAdmin ? `${window.adminApiPrefix}` : '');
}
function getApiPrefix(path,isAdmin){
var mainPart = getApiHost(path,isAdmin) + $user.auth_token
var mainPart = getApiHost(path,isAdmin) + ($user.auth_token || $user.auth)
return path ? mainPart + '/' + path + '/' + $user.ke : mainPart
}
@ -1096,183 +1096,9 @@ function featureIsActivated(showNotice){
return false
}
}
$(document).ready(function(){
onInitWebsocket(function(){
loadMonitorsIntoMemory(function(data){
setInterfaceCounts(data)
openTab('initial')
onDashboardReadyExecute()
})
});
$('body')
// .on('tab-away',function(){
//
// })
// .on('tab-close',function(){
//
// })
.on('click','.toggle-accordion-list',function(e){
e.preventDefault();
var el = $(this)
var iconEl = el.find('i')
var targetEl = el.parent().find('.accordion-list').first()
targetEl.toggle()
el.toggleClass('btn-primary btn-success')
iconEl.toggleClass('fa-plus fa-minus')
return false;
})
.on('click','.toggle-display-of-el',function(e){
e.preventDefault();
var el = $(this)
var target = el.attr('data-target')
var targetFirstOnly = el.attr('data-get') === 'first'
var startIsParent = el.attr('data-start') === 'parent'
var targetEl = startIsParent ? el.parent().find(target) : $(target)
if(targetFirstOnly){
targetEl = targetEl.first()
}
targetEl.toggle()
return false;
})
.on('click','.pop-image',function(){
var imageSrc = $(this).attr('src')
if(!imageSrc){
new PNotify({
title: lang['Action Failed'],
text: lang['No Image'],
type: 'warning'
})
return;
};
popImage(imageSrc)
})
.on('click','.popped-image',function(){
$(this).remove()
})
.on('click','.popped-image img',function(e){
e.stopPropagation()
return false;
})
.on('click','.go-home',goBackHome)
.on('click','.go-back',goBackOneTab)
.on('click','.delete-tab',function(e){
e.preventDefault()
e.stopPropagation()
var tabName = $(this).parents(`[page-open]`).attr(`page-open`)
if(activeTabName === tabName){
goBackOneTab()
}
deleteTab(tabName)
return false;
})
.on('click','.delete-tab-dynamic',function(e){
e.preventDefault()
e.stopPropagation()
var tabName = $(this).parents('.page-tab').attr('id').replace('tab-','')
goBackOneTab()
deleteTab(tabName)
return false;
})
.on('click','[page-open]',function(){
var el = $(this)
var pageChoice = el.attr('page-open')
var pageOptions = JSON.parse(el.attr('page-options') || '{}')
if(tabTree.name === pageChoice)return;
openTab(pageChoice,pageOptions)
})
.on('click','[class_toggle]',function(){
var el = $(this)
var targetElement = el.attr('data-target')
var classToToggle = el.attr('class_toggle')
var iconClassesToToggle = el.attr('icon-toggle').split(' ')
var iconTarget = el.attr('icon-child')
var iconTargetElement = el.find(el.attr('icon-child'))
var togglPosition = $(targetElement).hasClass(classToToggle) ? 0 : 1
var classToggles = dashboardOptions().class_toggle || {}
classToggles[targetElement] = [classToToggle,togglPosition,iconClassesToToggle,iconTarget];
dashboardOptions('class_toggle',classToggles)
$(targetElement).toggleClass(classToToggle)
iconTargetElement
.removeClass(iconClassesToToggle[togglPosition === 1 ? 0 : 1])
.addClass(iconClassesToToggle[togglPosition])
})
.on('keyup','.search-parent .search-controller',function(){
var _this = this;
var parent = $(this).parents('.search-parent')
$.each(parent.find(".search-body .search-row"), function() {
if($(this).text().toLowerCase().indexOf($(_this).val().toLowerCase()) === -1)
$(this).hide();
else
$(this).show();
});
})
.on('click','[tab-chooser]',function(){
var el = $(this)
var parent = el.parents('[tab-chooser-parent]')
var tabName = el.attr('tab-chooser')
var allTabChoosersInParent = parent.find('[tab-chooser]')
var allTabsInParent = parent.find('[tab-section]')
allTabsInParent.hide()
allTabChoosersInParent.removeClass('active')
el.addClass('active')
parent.find(`[tab-section="${tabName}"]`).show()
});
if(!isMobile){
$('body').on('mousedown',"select[multiple]",function(e){
e.preventDefault();
var select = this;
var scroll = select .scrollTop;
e.target.selected = !e.target.selected;
setTimeout(function(){select.scrollTop = scroll;}, 0);
$(select).focus().change();
}).on('mousemove',"select[multiple]",function(e){
e.preventDefault()
});
}
$('.logout').click(function(e){
$.get(getApiPrefix() + '/logout/' + $user.ke + '/' + $user.uid,function(data){
localStorage.removeItem('ShinobiLogin_'+location.host);
location.href = location.href.split('#')[0];
})
})
// only binded on load
$('.form-section-header:not(.no-toggle-header)').click(function(e){
var parent = $(this).parent('.form-group-group')
var boxWrapper = parent.attr('id')
parent.toggleClass('hide-box-wrapper')
var hideBoxWrapper = parent.hasClass('hide-box-wrapper')
boxWrappersHidden[boxWrapper] = hideBoxWrapper
dashboardOptions('boxWrappersHidden',boxWrappersHidden)
})
$('[data-bs-target="#sidebarMenu"]').click(function(e){
resizeMonitorIcons()
})
if(!isMobile){
var clicked = false, clickX, oldClickX;
var htmlBody = $('html')
pageTabLinks.on({
'mousemove': function(e) {
clicked && updateScrollPos(e);
},
'mousedown': function(e) {
e.preventDefault();
clicked = true;
oldClickX = clickX + 0;
clickX = e.pageX;
},
'mouseup': function(e) {
if(oldClickX !== clickX){
e.preventDefault()
}
clicked = false;
htmlBody.css('cursor', 'auto');
}
});
var updateScrollPos = function(e) {
htmlBody.css('cursor', 'grabbing');
pageTabLinks.scrollLeft(pageTabLinks.scrollLeft() + (clickX - e.pageX));
}
}
})
function makeButton({ color, link, text, class: classes}){
return `<a class="btn btn-sm d-block btn-${color} ${classes}" ${link ? `href="${link}" target="_blank"` : ''}>${text}</a>`
}
function replaceBrokenImage(_this){
$(_this).attr('src', `${libURL}/libs/img/bg.jpg`)
}

View File

@ -195,14 +195,39 @@ function initiateLiveGridPlayer(monitor,subStreamChannel){
var stream = containerElement.find('.stream-element');
var onPoseidonError = function(){
// setTimeout(function(){
// mainSocket.f({f:'monitor',ff:'watch_on',id:monitor.mid})
// },5000)
// mainSocket.f({f:'monitor',ff:'watch_on',id:monitorId})
// },2000)
}
if(!loadedPlayer.PoseidonErrorCount)loadedPlayer.PoseidonErrorCount = 0
if(loadedPlayer.PoseidonErrorCount >= 5)return
stream.attr('src',getApiPrefix(`mp4`)+'/'+monitor.mid + (subStreamChannel ? `/${subStreamChannel}` : '')+'/s.mp4?time=' + (new Date()).getTime())
stream[0].onerror = function(err){
console.error(err)
if(subStreamChannel ? details.substream.output.stream_flv_type === 'ws' : monitor.details.stream_flv_type === 'ws'){
if(loadedPlayer.Poseidon){
loadedPlayer.Poseidon.stop()
revokeVideoPlayerUrl(monitorId)
}
try{
loadedPlayer.Poseidon = new Poseidon({
video: stream[0],
auth_token: $user.auth_token,
ke: monitor.ke,
uid: $user.uid,
id: monitor.mid,
url: location.origin,
path: websocketPath,
query: websocketQuery,
onError : onPoseidonError,
channel : subStreamChannel
})
loadedPlayer.Poseidon.start();
}catch(err){
// onPoseidonError()
console.log('onTryPoseidonError',err)
}
}else{
stream.attr('src',getApiPrefix(`mp4`)+'/'+monitor.mid + (subStreamChannel ? `/${subStreamChannel}` : '')+'/s.mp4?time=' + (new Date()).getTime())
stream[0].onerror = function(err){
console.error(err)
}
}
},1000)
break;

View File

@ -0,0 +1,14 @@
$(document).ready(function(){
$('#settings').on('click','.ldap-sign-in',function(e){
e.preventDefault()
var signInWindow = window.open(getApiPrefix('loginTokenAddLDAP'),'popup','width=500,height=700,scrollbars=no,resizable=no');
if(!signInWindow || signInWindow.closed || typeof signInWindow.closed=='undefined'){
alert(`Your Popup Blocker is disabling this feature.`)
}else{
signInWindow.onbeforeunload = function(){
drawAlternateLoginsToSettings()
}
}
return false;
})
})

View File

@ -136,6 +136,7 @@ function resetMonitorCanvas(monitorId,initiateAfter,subStreamChannel){
streamBlock.append(buildStreamElementHtml(streamType))
attachVideoElementErrorHandler(monitorId)
if(initiateAfter)initiateLiveGridPlayer(monitor,subStreamChannel)
resetLiveGridDimensionsInMemory(monitorId)
}
function replaceMonitorInfoInHtml(htmlString,monitor){
var monitorMutes = dashboardOptions().monitorMutes || {}
@ -261,6 +262,7 @@ function loadVideoMiniList(monitorId){
}
function updateLiveGridElementHeightWidth(monitorId){
var liveGridElement = liveGridElements[monitorId]
liveGridElement.streamElement = liveGridElement.monitorItem.find('.stream-element')
var streamElement = liveGridElement.streamElement
liveGridElement.width = streamElement.width()
liveGridElement.height = streamElement.height()
@ -712,12 +714,19 @@ function openLiveGrid(){
}
}
function popOutMonitor(monitorId){
var monitorPop = monitorPops[monitorId]
var monitorPop = monitorPops[monitorId] || {}
if(monitorPop.isOpen){
return
}
function finish(img){
if(monitorPop){
monitorPop.close()
monitorPops[monitorId] = window.open(getApiPrefix() + '/embed/' + $user.ke + '/' + monitorId + '/fullscreen|jquery|relative|gui' + `?host=${location.pathname}`,'pop_' + monitorId + $user.auth_token,'height='+img.height+',width='+img.width);
monitorPop = monitorPops[monitorId]
monitorPop.isOpen = true
monitorPop.onload = function(){
this.onbeforeunload = function(){
monitorPop.isOpen = false
}
}
monitorPop = window.open(getApiPrefix() + '/embed/' + $user.ke + '/' + monitorId + '/fullscreen|jquery|relative|gui' + `?host=${location.pathname}`,'pop_' + monitorId + $user.auth_token,'height='+img.height+',width='+img.width);
}
if(loadedLiveGrids[monitorId]){
getSnapshot(loadedMonitors[monitorId],function(url){
@ -736,6 +745,12 @@ function popOutMonitor(monitorId){
finish(img)
}
}
function createWallViewWindow(windowName){
var el = $(document)
var width = el.width()
var height = el.height()
window.open(getApiPrefix() + '/wallview/' + $user.ke + (windowName ? 'window=' + windowName : ''), 'wallview_'+windowName, 'height='+height+',width='+width)
}
function fullScreenLiveGridStream(monitorItem){
var videoElement = monitorItem.find('.stream-element')
monitorItem.addClass('fullscreen')
@ -1092,6 +1107,9 @@ $(document).ready(function(e){
var monitorId = $(this).parents('[data-mid]').attr('data-mid')
popOutMonitor(monitorId)
})
.on('click','.open-wallview',function(){
createWallViewWindow()
})
.on('click','.toggle-monitor-substream',function(){
var monitorId = $(this).parents('[data-mid]').attr('data-mid')
toggleSubStream(monitorId)
@ -1290,6 +1308,7 @@ $(document).ready(function(e){
// break;
case'detector_trigger':
var monitorId = d.id
var matrices = d.details.matrices
var liveGridElement = liveGridElements[monitorId]
if(!window.dontShowDetection && liveGridElement){
var monitorElement = liveGridElement.monitorItem
@ -1303,12 +1322,12 @@ $(document).ready(function(e){
}else{
monitorElement.removeClass('doObjectDetection')
}
if(d.details.matrices&&d.details.matrices.length>0){
if(matrices && matrices.length > 0){
drawMatrices(d,{
theContainer: liveGridElement.eventObjects,
height: liveGridElement.height,
width: liveGridElement.width,
})
}, null, true)
}
if(d.details.confidence){
var eventConfidence = d.details.confidence
@ -1329,7 +1348,7 @@ $(document).ready(function(e){
}
playAudioAlert()
var monitorPop = monitorPops[monitorId]
if(window.popLiveOnEvent && (!monitorPop || monitorPop.closed === true)){
if(window.popLiveOnEvent && (!monitorPop || !monitorPop.isOpen)){
popOutMonitor(monitorId)
}
// console.log({

View File

@ -1150,8 +1150,8 @@ editorForm.find('[name="type"]').change(function(e){
setCosmeticMonitorInfo(newMonitorData)
drawMonitorGroupList()
if(!d.silenceNote){
new PNotify({
title: 'Monitor Saved',
redAlertNotify({
title: lang['Monitor Saved'],
text: '<b>'+newMonitorData.name+'</b> <small>'+newMonitorData.mid+'</small> has been saved.',
type: 'success'
})

View File

@ -669,6 +669,25 @@ function readAlertNotice(title, text, type) {
});
}
}
function redAlertNotify(options) {
var title = options.title;
var redAlertNotice = redAlertNotices[title];
var notifyOptions = {
title: title,
text: options.text,
type: options.type,
hide: options.hide === undefined ? false : options.hide,
delay: options.delay || 30000
};
if (redAlertNotice) {
redAlertNotice.update(notifyOptions);
} else {
redAlertNotices[title] = new PNotify(notifyOptions);
redAlertNotices[title].on('close', function() {
redAlertNotices[title] = null;
});
}
}
function buildPosePoints(bodyParts, x, y){
let theArray = []
for(const point of bodyParts){
@ -683,7 +702,7 @@ function buildPosePoints(bodyParts, x, y){
}
return theArray;
}
function drawMatrices(event,options){
function drawMatrices(event, options, autoRemoveTimeout, drawTrails){
var theContainer = options.theContainer
var height = options.height
var width = options.width
@ -695,8 +714,11 @@ function drawMatrices(event,options){
let moreMatrices = []
var monitorId = event.id;
function processMatrix(n,matrix){
html += `<div class="stream-detected-object" name="${objectTagGroup}" style="height:${heightRatio * matrix.height}px;width:${widthRatio * matrix.width}px;top:${heightRatio * matrix.y}px;left:${widthRatio * matrix.x}px;border-color: ${matrix.color};">`
if(matrix.tag)html += `<span class="tag">${matrix.tag}${!isNaN(matrix.id) ? ` <small class="label label-default">${matrix.id}</small>`: ''}</span>`
const newWidth = widthRatio * matrix.width;
const newHeight = heightRatio * matrix.height;
if(drawTrails)html += `<div class="stream-detected-object fresh-detected-trail" style="height:2px;width:2px;top:${heightRatio * matrix.y + (newHeight / 2)}px;left:${widthRatio * matrix.x + (newWidth / 2)}px;border-color: green;"></div>`
html += `<div class="stream-detected-object fresh-detected-object" name="${objectTagGroup}" style="height:${newHeight}px;width:${newWidth}px;top:${heightRatio * matrix.y}px;left:${widthRatio * matrix.x}px;border-color: ${matrix.color};">`
if(matrix.tag)html += `<span class="tag">${matrix.tag}${!isNaN(matrix.id) ? ` <small class="label label-default">${matrix.id}</small>`: ''} (${matrix.confidence.toFixed(2) || 0})</span>`
if(matrix.notice)html += `<div class="matrix-info" style="color:yellow">${matrix.notice}</div>`;
if(matrix.missingNear && matrix.missingNear.length > 0){
html += `<div class="matrix-info yellow"><small>Missing Near</small><br>${matrix.missingRecently.map(item => `${item.tag} (${item.id}) by ${item.missedNear.tag} (${item.missedNear.id})`).join(', ')}</div>`;
@ -728,7 +750,7 @@ function drawMatrices(event,options){
if(matrix.nearBy){
html += `<div class="matrix-info">`
matrix.nearBy.forEach((nearMatrix) => {
html += `<div class="mb-1">${nearMatrix.tag} <small class="label label-default">${nearMatrix.id}</small> (${nearMatrix.overlapPercent}%)</div>`
html += `<div class="mb-1">${nearMatrix.tag} <small class="label label-default">${nearMatrix.id}</small> (${nearMatrix.overlapPercent.toFixed(2)}%)</div>`
});
html += `</div>`
}
@ -740,7 +762,19 @@ function drawMatrices(event,options){
}
$.each(event.details.matrices, processMatrix);
$.each(moreMatrices, processMatrix);
theContainer.append(html)
var addedEls = theContainer.append(html)
if(autoRemoveTimeout){
addedEls = addedEls.find('.fresh-detected-object').removeClass('fresh-detected-object')
setTimeout(function(){
addedEls.remove()
}, autoRemoveTimeout);
}
if(drawTrails){
var addedTrails = theContainer.find('.fresh-detected-trail').removeClass('fresh-detected-trail')
setTimeout(function(){
addedTrails.remove()
}, 5000);
}
}
function setMonitorCountOnUI(){
$('.cameraCount').text(Object.keys(loadedMonitors).length)

View File

@ -4,6 +4,8 @@ $(document).ready(function(e){
var loadedResultsByIp = {}
var monitorEditorWindow = $('#tab-monitorSettings')
var onvifScannerWindow = $('#tab-onvifScanner')
var onvifScannerStartButton = onvifScannerWindow.find('.start-scan')
var onvifScannerStopButton = onvifScannerWindow.find('.stop-scan')
var onvifScannerResultPane = onvifScannerWindow.find('.onvif_result')
var onvifScannerErrorResultPane = onvifScannerWindow.find('.onvif_result_error')
var scanForm = onvifScannerWindow.find('form');
@ -30,86 +32,97 @@ $(document).ready(function(e){
var html = buildSubMenuItems(allFound)
sideMenuList.html(html)
}
var setAsLoading = function(appearance){
var showStopButton = function(appearance){
if(appearance){
onvifScannerWindow.find('._loading').show()
onvifScannerWindow.find('[type="submit"]').prop('disabled',true)
onvifScannerStartButton.addClass('d-none')
onvifScannerStopButton.removeClass('d-none')
}else{
onvifScannerWindow.find('._loading').hide()
onvifScannerWindow.find('[type="submit"]').prop('disabled',false)
onvifScannerStartButton.removeClass('d-none')
onvifScannerStopButton.addClass('d-none')
}
}
function drawProbeResult(options){
if(!options.error){
var currentUsername = onvifScannerWindow.find('[name="user"]').val()
var currentPassword = onvifScannerWindow.find('[name="pass"]').val()
var tempID = generateId()
var info = options.info ? jsonToHtmlBlock(options.info) : ''
var streamUrl = ''
var launchWebPage = `target="_blank" href="http${options.port == 443 ? 's' : ''}://${options.ip}:${options.port}"`
if(options.uri){
streamUrl = options.uri
}
var theLocation = getLocationFromUri(options.uri)
var pathLocation = theLocation.location
var monitorConfigPartial = {
name: pathLocation.hostname,
mid: tempID + `${options.port}`,
host: pathLocation.hostname,
port: pathLocation.port,
path: pathLocation.pathname + (pathLocation.search && pathLocation.search !== '?' ? pathLocation.search : ''),
protocol: theLocation.protocol,
details: {
auto_host: addCredentialsToUri(streamUrl,currentUsername,currentPassword),
muser: currentUsername,
mpass: currentPassword,
is_onvif: '1',
onvif_port: options.port,
},
}
if(options.isPTZ){
monitorConfigPartial.details = Object.assign(monitorConfigPartial.details,{
control: '1',
control_url_method: 'ONVIF',
control_stop: '1',
})
}
var monitorAlreadyAdded = isOnvifRowAlreadyALoadedMonitor(monitorConfigPartial)
if(monitorAlreadyAdded){
monitorConfigPartial.mid = monitorAlreadyAdded.mid;
}
var monitorId = monitorConfigPartial.mid
loadedResults[monitorId] = monitorConfigPartial;
loadedResultsByIp[monitorConfigPartial.host] = monitorConfigPartial;
onvifScannerResultPane.append(`
<div class="col-md-4 mb-3" onvif_row="${monitorId}" id="onvif-result-${monitorId}">
<div style="display:block" class="card shadow btn-default copy">
<div class="preview-image card-header" style="background-image:url(${options.snapShot ? 'data:image/png;base64,' + options.snapShot : placeholder.getData(placeholder.plcimg({text: ' ', fsize: 25, bgcolor:'#1f80f9'}))})"></div>
<div class="card-body" style="min-height:190px">
<div>${info}</div>
<div class="url">${streamUrl}</div>
</div>
<div class="card-footer">${options.ip}:${options.port}</div>
</div>
</div>
`)
onvifScannerWindow.find('._notfound').remove()
setAsLoading(false)
drawFoundCamerasSubMenu()
function drawDeviceTableRow(device, gotAccess){
var ip = device.ip;
var el = onvifScannerResultPane.find(`[scan-item="${ip}"]`)
var hasError = !!device.error;
var uriText = !hasError ? device.uri ? device.uri.split('?')[0] : '' : device.error;
var statusColor = hasError ? 'red' : 'green';
var snapShot = device.snapShot;
// console.log(ip, device.error, hasError)
if(gotAccess)loadMonitorConfigFromResult(device)
if(el.length === 0){
var html = `<div scan-item="${ip}" class="col-md-6">
<div class="card btn-default d-flex flex-row align-items-center p-2 mb-3 mx-2" style="border:none;border-left: 3px solid;border-color: ${statusColor}">
<div class="pr-2"><i class="fa fa-square" style="color:${statusColor}"></i></div>
<div class="pr-2"><div class="scan-item-img copy ${snapShot ? `cursor-pointer` : ''}" style="${snapShot ? `background-image:url(data:image/jpeg;base64,${snapShot})` : 'background-color:${statusColor};'}"></div></div>
<div class="pr-2 flex-grow-1">${ip}<br><small class="uri">${uriText}</small></div>
<div class="text-center copy-button pr-2">${!hasError ? makeButton({text: lang.Copy, class:'copy', color: 'primary'}) : ''}</div>
</div>
</div>`
onvifScannerResultPane.append(html)
}else{
if(!loadedResultsByIp[options.ip]){
onvifScannerErrorResultPane.append(`
<div onvif_error_row="${options.ip}" class="d-flex flex-row">
<div class="py-2 px-1" style="min-width:170px"><b>${options.ip}:${options.port}</b></div>
<div class="py-2 px-1 flex-grow-1">${options.error}</div>
<div class="py-2 px-1 text-right">
<a target="_blank" class="btn btn-sm btn-secondary" href="http://${options.ip}:${options.port}"><i class="fa fa-external-link"></i></a>
</div>
</div>
`)
var copyButton = el.find('.copy-button');
var imgEl = el.find('.scan-item-img');
if(hasError){
copyButton.empty()
imgEl.removeClass('copy cursor-pointer')
}else{
copyButton.html(makeButton({text: lang.Copy, class:'copy', color: 'primary'}))
imgEl.addClass('copy cursor-pointer')
}
if(snapShot){
imgEl.css('background-image', `url("data:image/jpeg;base64,${snapShot}")`)
}else{
imgEl.css('background-image', '')
}
imgEl.css('background-color', statusColor)
el.find('.uri').text(uriText)
el.find('.card').css('border-color', statusColor)
el.find('.fa-circle').css('color', statusColor)
}
}
function loadMonitorConfigFromResult(options){
var monitorId = removeSpecialCharacters(options.ip)
var currentUsername = options.user
var currentPassword = options.pass
var streamUrl = ''
var launchWebPage = `target="_blank" href="http${options.port == 443 ? 's' : ''}://${options.ip}:${options.port}"`
if(options.uri){
streamUrl = options.uri
}
var theLocation = getLocationFromUri(options.uri)
var pathLocation = theLocation.location
var monitorConfigPartial = {
name: pathLocation.hostname,
mid: monitorId,
host: pathLocation.hostname,
port: pathLocation.port,
path: pathLocation.pathname + (pathLocation.search && pathLocation.search !== '?' ? pathLocation.search : ''),
protocol: theLocation.protocol,
details: {
auto_host: addCredentialsToUri(streamUrl,currentUsername,currentPassword),
muser: currentUsername,
mpass: currentPassword,
is_onvif: '1',
onvif_port: options.port,
},
}
if(options.isPTZ){
monitorConfigPartial.details = Object.assign(monitorConfigPartial.details,{
control: '1',
control_url_method: 'ONVIF',
control_stop: '1',
})
}
var monitorAlreadyAdded = isOnvifRowAlreadyALoadedMonitor(monitorConfigPartial)
if(monitorAlreadyAdded){
monitorConfigPartial.mid = monitorAlreadyAdded.mid;
}
loadedResults[monitorId] = monitorConfigPartial;
loadedResultsByIp[monitorConfigPartial.host] = monitorConfigPartial;
return monitorConfigPartial
}
function isOnvifRowAlreadyALoadedMonitor(onvifRow){
var matches = null;
$.each(loadedMonitors,function(n,monitor){
@ -174,7 +187,7 @@ $(document).ready(function(e){
var form = el.serializeObject();
onvifScannerResultPane.empty();
onvifScannerErrorResultPane.empty();
setAsLoading(true)
showStopButton(true)
mainSocket.f({
f: 'onvif',
ip: form.ip,
@ -184,34 +197,80 @@ $(document).ready(function(e){
});
clearTimeout(checkTimeout)
checkTimeout = setTimeout(function(){
if(onvifScannerResultPane.find('.card').length === 0){
setAsLoading(false)
if(onvifScannerResultPane.find('[scan-item]').length === 0){
showStopButton(false)
onvifScannerResultPane.append(`<div class="p-2 text-center ${definitions.Theme.isDark ? 'text-white' : ''} _notfound text-white epic-text">${lang.sorryNothingWasFound}</div>`)
}
},5000)
return false;
});
onvifScannerWindow.on('click','.copy',function(){
onvifScannerWindow.on('click','.copy',function(e){
e.preventDefault()
openMonitorEditorPage()
var el = $(this).parents('[onvif_row]');
var id = el.attr('onvif_row');
var onvifRecord = loadedResults[id];
var el = $(this).parents('[scan-item]');
var id = el.attr('scan-item');
var onvifRecord = loadedResultsByIp[id];
var streamURL = onvifRecord.details.auto_host
writeToMonitorSettingsWindow(onvifRecord)
})
onvifScannerWindow.on('click','.add-all',function(){
filterOutMonitorsThatAreAlreadyAdded(loadedResults,function(importableCameras){
$.each(importableCameras,function(n,camera){
// console.log(camera)
postMonitor(camera)
})
const numberOfCameras = importableCameras.length
if(numberOfCameras === 0){
new PNotify({
title: lang["ONVIF Scanner"],
text: lang.sorryNothingWasFound,
type: 'danger',
})
}else{
$.confirm.create({
title: lang['Add Cameras'],
body: `<p>${lang.addAllCamerasText.replace('9001', numberOfCameras)}</p><ul>${importableCameras.map(item => `<li>${item.host}</li>`).join('')}</ul>`,
clickOptions: {
class: 'btn-success',
title: lang.Add,
},
clickCallback: function(){
$.each(importableCameras,function(n,camera){
// console.log(camera)
postMonitor(camera)
})
}
})
}
})
})
onvifScannerWindow.on('click','.stop-scan',function(){
mainSocket.f({ f: 'onvif_stop' });
})
loadLocalOptions()
onInitWebsocket(function (){
mainSocket.f({ f: 'onvif_scan_reconnect' });
})
onWebSocketEvent(function (d){
switch(d.f){
case'onvif':
drawProbeResult(d)
try{
drawDeviceTableRow(d, d.ff !== 'failed_capture' && !d.failedConnection);
}catch(err){
console.error(err)
}
break;
case'onvif_scan_current':
console.log(d)
if(d.isScanning){
showStopButton(true)
}else{
showStopButton(false)
}
d.devices.forEach(device => {
console.log('onvif_scan_current', device)
drawDeviceTableRow(device, !device.error && !d.failedConnection)
});
break;
case'onvif_scan_complete':
showStopButton(false)
break;
}
})

View File

@ -8,6 +8,7 @@ $(document).ready(function(){
var timeStripObjectSearchInput = $('#timeline-video-object-search');
var dateSelector = $('#timeline-date-selector');
var sideMenuList = $(`#side-menu-link-timeline ul`)
var monitorList = $(`#timeline-monitor-list`)
var playToggles = timeStripControls.find('[timeline-action="playpause"]')
var speedButtons = timeStripControls.find('[timeline-action="speed"]')
var gridSizeButtons = timeStripControls.find('[timeline-action="gridSize"]')
@ -50,6 +51,8 @@ $(document).ready(function(){
var dateRangeChanging = false
var lastDateChecked = new Date(0)
var monitorSelectionElements = []
var sideMenuListMissing = sideMenuList.length === 0;
var selectingMonitorList = (sideMenuListMissing ? monitorList : sideMenuList);
function setLoadingMask(turnOn){
if(turnOn){
if(theWindow.find('.loading-mask').length === 0){
@ -441,7 +444,7 @@ $(document).ready(function(){
function setVideoInCanvas(newVideo){
var monitorId = newVideo.mid
var container = getVideoContainerInCanvas(newVideo)
.removeClass('no-video').find('.film').html(`<video muted src="${newVideo.href}"></video>`)
.removeClass('no-video').find('.film').html(`<video muted src="${getApiPrefix('videos')}/${monitorId}/${newVideo.filename}"></video>`)
var vidEl = getVideoElInCanvas(newVideo)
var objectContainer = getObjectContainerInCanvas(newVideo)
vidEl.playbackRate = timelineSpeed
@ -821,15 +824,16 @@ $(document).ready(function(){
})
})
var html = buildSubMenuItems(allFound)
sideMenuList.html(html)
monitorSelectionElements = sideMenuList.find('.timeline-selectMonitor')
if(!sideMenuListMissing)sideMenuList.html(html)
monitorList.html(html)
monitorSelectionElements = selectingMonitorList.find('.timeline-selectMonitor')
}
async function setSideMenuMonitorVisualSelection(){
var getForAllMonitors = timeStripSelectedMonitors.length === 0;
monitorSelectionElements.find('.dot').removeClass('dot-green')
if(!getForAllMonitors){
timeStripSelectedMonitors.forEach((monitorId) => {
sideMenuList.find(`[data-mid="${monitorId}"] .dot`).addClass('dot-green')
selectingMonitorList.find(`[data-mid="${monitorId}"] .dot`).addClass('dot-green')
})
}
}
@ -928,7 +932,7 @@ $(document).ready(function(){
refreshTimeline()
}
}
sideMenuList.on('click','[timeline-menu-action]',function(){
function monitorSelectorController(){
var el = $(this)
var type = el.attr('timeline-menu-action')
switch(type){
@ -957,7 +961,9 @@ $(document).ready(function(){
}
break;
}
})
}
monitorList.on('click','[timeline-menu-action]', monitorSelectorController)
sideMenuList.on('click','[timeline-menu-action]', monitorSelectorController)
timelineActionButtons.click(function(){
var el = $(this)
var type = el.attr('timeline-action')

View File

@ -0,0 +1,155 @@
onWebSocketEvent(function(d){
switch(d.f){
case'video_edit':case'video_archive':
var video = loadedVideosInMemory[`${d.mid}${d.time}${d.type}`]
if(video){
let filename = `${formattedTimeForFilename(convertTZ(d.time),false,`YYYY-MM-DDTHH-mm-ss`)}.${video.ext || 'mp4'}`
loadedVideosInMemory[`${d.mid}${d.time}${d.type}`].status = d.status
$(`[data-mid="${d.mid}"][data-filename="${filename}"]`).attr('data-status',d.status);
}
break;
case'video_delete':
$('[file="'+d.filename+'"][mid="'+d.mid+'"]:not(.modal)').remove();
$('[data-file="'+d.filename+'"][data-mid="'+d.mid+'"]:not(.modal)').remove();
$('[data-time-formed="'+(new Date(d.time))+'"][data-mid="'+d.mid+'"]:not(.modal)').remove();
var videoPlayerId = getVideoPlayerTabId(d)
if(tabTree.name === videoPlayerId){
goBackOneTab()
}
deleteTab(videoPlayerId)
break;
}
})
$(document).ready(function(){
$('body')
.on('click','.open-video',function(e){
e.preventDefault()
var _this = this;
var {
monitorId,
videoTime,
video,
} = getVideoInfoFromEl(_this)
createVideoPlayerTab(video)
setVideoStatus(video)
return false;
})
.on('click','[video-time-seeked-video-position]',function(){
var el = $(this)
var monitorId = el.attr('data-mid')
var videoTime = el.attr('video-time-seeked-video-position')
var timeInward = (parseInt(el.attr('video-slice-seeked')) / 1000) - 2
var video = loadedVideosInMemory[`${monitorId}${videoTime}${undefined}`]
timeInward = timeInward < 0 ? 0 : timeInward
createVideoPlayerTab(video,timeInward)
})
.on('click','.delete-video',function(e){
e.preventDefault()
var el = $(this).parents('[data-mid]')
var monitorId = el.attr('data-mid')
var videoTime = el.attr('data-time')
var type = el.attr('data-type')
var video = loadedVideosInMemory[`${monitorId}${videoTime}${type}`]
var videoSet = video.videoSet
var ext = video.filename.split('.')
ext = ext[ext.length - 1]
var isCloudVideo = videoSet === 'cloudVideos'
var videoEndpoint = getApiPrefix(videoSet || 'videos') + '/' + video.mid + '/' + video.filename
var endpointType = isCloudVideo ? `?type=${video.type}` : ''
$.confirm.create({
title: lang["Delete Video"] + ' : ' + video.filename,
body: `${lang.DeleteVideoMsg}<br><br><div class="row"><video class="video_video" autoplay loop controls><source src="${videoEndpoint}${endpointType}" type="video/${ext}"></video></div>`,
clickOptions: {
title: '<i class="fa fa-trash-o"></i> ' + lang.Delete,
class: 'btn-danger btn-sm'
},
clickCallback: function(){
$.getJSON(videoEndpoint + '/delete' + endpointType,function(data){
if(data.ok){
console.log('Video Deleted')
}else{
console.log('Video Not Deleted',data,videoEndpoint + endpointType)
}
})
}
});
return false;
})
.on('click','.compress-video',function(e){
e.preventDefault()
var el = $(this).parents('[data-mid]')
var monitorId = el.attr('data-mid')
var videoTime = el.attr('data-time')
var video = loadedVideosInMemory[`${monitorId}${videoTime}${undefined}`]
var ext = video.filename.split('.')
ext = ext[ext.length - 1]
var videoEndpoint = getApiPrefix(`videos`) + '/' + video.mid + '/' + video.filename
$.confirm.create({
title: lang["Compress"] + ' : ' + video.filename,
body: `${lang.CompressVideoMsg}<br><br><div class="row"><video class="video_video" autoplay loop controls><source src="${videoEndpoint}" type="video/${ext}"></video></div>`,
clickOptions: {
title: '<i class="fa fa-compress"></i> ' + lang.Compress,
class: 'btn-primary btn-sm'
},
clickCallback: function(){
compressVideo(video)
}
});
return false;
})
.on('click','.archive-video',function(e){
e.preventDefault()
var el = $(this).parents('[data-mid]')
var monitorId = el.attr('data-mid')
var videoTime = el.attr('data-time')
var unarchive = $(this).hasClass('status-archived')
var video = loadedVideosInMemory[`${monitorId}${videoTime}${undefined}`]
var ext = video.filename.split('.')
ext = ext[ext.length - 1]
var videoEndpoint = getApiPrefix(`videos`) + '/' + video.mid + '/' + video.filename
if(unarchive){
unarchiveVideo(video)
}else{
// $.confirm.create({
// title: lang["Archive"] + ' : ' + video.filename,
// body: `${lang.ArchiveVideoMsg}<br><br><div class="row"><video class="video_video" autoplay loop controls><source src="${videoEndpoint}" type="video/${ext}"></video></div>`,
// clickOptions: {
// title: '<i class="fa fa-lock"></i> ' + lang.Archive,
// class: 'btn-primary btn-sm'
// },
// clickCallback: function(){
archiveVideo(video)
// }
// });
}
return false;
})
.on('click','.fix-video',function(e){
e.preventDefault()
var el = $(this).parents('[data-mid]')
var monitorId = el.attr('data-mid')
var videoTime = el.attr('data-time')
var video = loadedVideosInMemory[`${monitorId}${videoTime}${undefined}`]
var ext = video.filename.split('.')
ext = ext[ext.length - 1]
var videoEndpoint = getApiPrefix(`videos`) + '/' + video.mid + '/' + video.filename
$.confirm.create({
title: lang["Fix Video"] + ' : ' + video.filename,
body: `${lang.FixVideoMsg}<br><br><div class="row"><video class="video_video" autoplay loop controls><source src="${videoEndpoint}" type="video/${ext}"></video></div>`,
clickOptions: {
title: '<i class="fa fa-wrench"></i> ' + lang.Fix,
class: 'btn-danger btn-sm'
},
clickCallback: function(){
$.getJSON(videoEndpoint + '/fix',function(data){
if(data.ok){
console.log('Video Fixed')
}else{
console.log('Video Not Fixed',data,videoEndpoint)
}
})
}
});
return false;
})
})

View File

@ -658,158 +658,3 @@ function getDisplayDimensions(videoElement) {
videoHeight: displayHeight,
};
}
onWebSocketEvent(function(d){
switch(d.f){
case'video_edit':case'video_archive':
var video = loadedVideosInMemory[`${d.mid}${d.time}${d.type}`]
if(video){
let filename = `${formattedTimeForFilename(convertTZ(d.time),false,`YYYY-MM-DDTHH-mm-ss`)}.${video.ext || 'mp4'}`
loadedVideosInMemory[`${d.mid}${d.time}${d.type}`].status = d.status
$(`[data-mid="${d.mid}"][data-filename="${filename}"]`).attr('data-status',d.status);
}
break;
case'video_delete':
$('[file="'+d.filename+'"][mid="'+d.mid+'"]:not(.modal)').remove();
$('[data-file="'+d.filename+'"][data-mid="'+d.mid+'"]:not(.modal)').remove();
$('[data-time-formed="'+(new Date(d.time))+'"][data-mid="'+d.mid+'"]:not(.modal)').remove();
var videoPlayerId = getVideoPlayerTabId(d)
if(tabTree.name === videoPlayerId){
goBackOneTab()
}
deleteTab(videoPlayerId)
break;
}
})
$(document).ready(function(){
$('body')
.on('click','.open-video',function(e){
e.preventDefault()
var _this = this;
var {
monitorId,
videoTime,
video,
} = getVideoInfoFromEl(_this)
createVideoPlayerTab(video)
setVideoStatus(video)
return false;
})
.on('click','[video-time-seeked-video-position]',function(){
var el = $(this)
var monitorId = el.attr('data-mid')
var videoTime = el.attr('video-time-seeked-video-position')
var timeInward = (parseInt(el.attr('video-slice-seeked')) / 1000) - 2
var video = loadedVideosInMemory[`${monitorId}${videoTime}${undefined}`]
timeInward = timeInward < 0 ? 0 : timeInward
createVideoPlayerTab(video,timeInward)
})
.on('click','.delete-video',function(e){
e.preventDefault()
var el = $(this).parents('[data-mid]')
var monitorId = el.attr('data-mid')
var videoTime = el.attr('data-time')
var type = el.attr('data-type')
var video = loadedVideosInMemory[`${monitorId}${videoTime}${type}`]
var videoSet = video.videoSet
var ext = video.filename.split('.')
ext = ext[ext.length - 1]
var isCloudVideo = videoSet === 'cloudVideos'
var videoEndpoint = getApiPrefix(videoSet || 'videos') + '/' + video.mid + '/' + video.filename
var endpointType = isCloudVideo ? `?type=${video.type}` : ''
$.confirm.create({
title: lang["Delete Video"] + ' : ' + video.filename,
body: `${lang.DeleteVideoMsg}<br><br><div class="row"><video class="video_video" autoplay loop controls><source src="${videoEndpoint}${endpointType}" type="video/${ext}"></video></div>`,
clickOptions: {
title: '<i class="fa fa-trash-o"></i> ' + lang.Delete,
class: 'btn-danger btn-sm'
},
clickCallback: function(){
$.getJSON(videoEndpoint + '/delete' + endpointType,function(data){
if(data.ok){
console.log('Video Deleted')
}else{
console.log('Video Not Deleted',data,videoEndpoint + endpointType)
}
})
}
});
return false;
})
.on('click','.compress-video',function(e){
e.preventDefault()
var el = $(this).parents('[data-mid]')
var monitorId = el.attr('data-mid')
var videoTime = el.attr('data-time')
var video = loadedVideosInMemory[`${monitorId}${videoTime}${undefined}`]
var ext = video.filename.split('.')
ext = ext[ext.length - 1]
var videoEndpoint = getApiPrefix(`videos`) + '/' + video.mid + '/' + video.filename
$.confirm.create({
title: lang["Compress"] + ' : ' + video.filename,
body: `${lang.CompressVideoMsg}<br><br><div class="row"><video class="video_video" autoplay loop controls><source src="${videoEndpoint}" type="video/${ext}"></video></div>`,
clickOptions: {
title: '<i class="fa fa-compress"></i> ' + lang.Compress,
class: 'btn-primary btn-sm'
},
clickCallback: function(){
compressVideo(video)
}
});
return false;
})
.on('click','.archive-video',function(e){
e.preventDefault()
var el = $(this).parents('[data-mid]')
var monitorId = el.attr('data-mid')
var videoTime = el.attr('data-time')
var unarchive = $(this).hasClass('status-archived')
var video = loadedVideosInMemory[`${monitorId}${videoTime}${undefined}`]
var ext = video.filename.split('.')
ext = ext[ext.length - 1]
var videoEndpoint = getApiPrefix(`videos`) + '/' + video.mid + '/' + video.filename
if(unarchive){
unarchiveVideo(video)
}else{
// $.confirm.create({
// title: lang["Archive"] + ' : ' + video.filename,
// body: `${lang.ArchiveVideoMsg}<br><br><div class="row"><video class="video_video" autoplay loop controls><source src="${videoEndpoint}" type="video/${ext}"></video></div>`,
// clickOptions: {
// title: '<i class="fa fa-lock"></i> ' + lang.Archive,
// class: 'btn-primary btn-sm'
// },
// clickCallback: function(){
archiveVideo(video)
// }
// });
}
return false;
})
.on('click','.fix-video',function(e){
e.preventDefault()
var el = $(this).parents('[data-mid]')
var monitorId = el.attr('data-mid')
var videoTime = el.attr('data-time')
var video = loadedVideosInMemory[`${monitorId}${videoTime}${undefined}`]
var ext = video.filename.split('.')
ext = ext[ext.length - 1]
var videoEndpoint = getApiPrefix(`videos`) + '/' + video.mid + '/' + video.filename
$.confirm.create({
title: lang["Fix Video"] + ' : ' + video.filename,
body: `${lang.FixVideoMsg}<br><br><div class="row"><video class="video_video" autoplay loop controls><source src="${videoEndpoint}" type="video/${ext}"></video></div>`,
clickOptions: {
title: '<i class="fa fa-wrench"></i> ' + lang.Fix,
class: 'btn-danger btn-sm'
},
clickCallback: function(){
$.getJSON(videoEndpoint + '/fix',function(data){
if(data.ok){
console.log('Video Fixed')
}else{
console.log('Video Not Fixed',data,videoEndpoint)
}
})
}
});
return false;
})
})

View File

@ -0,0 +1,21 @@
function createVideoPlayerTab(video){
// not real one, dummy for wallvideoview.
var videoEndpoint = getApiPrefix(`videos`) + '/' + video.mid + '/' + video.filename
$.confirm.create({
title: lang["Download"],
body: `<div class="text-center mb-2"><h6 class="mb-0">${loadedMonitors[video.mid].name}</h6><small>${video.time}</small></div><video style="border-radius:10px;max-width:100%;" autoplay controls playsinline src="${videoEndpoint}"></video>`,
clickOptions: {
title: '<i class="fa fa-download"></i> ' + lang.Download,
class: 'btn-success btn-sm'
},
clickCallback: function(){
downloadFile(videoEndpoint, video.filename)
}
});
}
$(document).ready(function(){
loadMonitorsIntoMemory(function(data){
openTab('timeline')
onDashboardReadyExecute()
})
})

View File

@ -0,0 +1,299 @@
var loadedMonitors = {}
var selectedMonitors = {}
var selectedMonitorsCount = 0
$(document).ready(function(){
PNotify.prototype.options.styling = "fontawesome";
var wallViewMonitorList = $('#wallview-monitorList')
var wallViewControls = $('#wallview-controls')
var wallViewCanvas = $('#wallview-canvas')
var wallViewInfoScreen = $('#wallview-info-screen')
var theWindow = $(window);
var lastWindowWidth = theWindow.width()
var lastWindowHeight = theWindow.height()
function featureIsActivated(showNotice){
if(userHasSubscribed){
return true
}else{
if(showNotice){
new PNotify({
title: lang.activationRequired,
text: lang.featureRequiresActivationText,
type: 'warning'
})
}
return false
}
}
function createWallViewWindow(windowName){
var el = $(document)
var width = el.width()
var height = el.height()
window.open(getApiPrefix() + '/wallview/' + groupKey + (windowName ? '?window=' + windowName : ''), 'wallview_'+windowName, 'height='+height+',width='+width)
}
function getApiPrefix(innerPart){
return `${urlPrefix}${authKey}${innerPart ? `/${innerPart}/${groupKey}` : ''}`
}
function getWindowName(){
const urlParams = new URLSearchParams(window.location.search);
const theWindowChoice = urlParams.get('window');
return theWindowChoice || '1'
}
function drawMonitorListItem(monitor){
wallViewMonitorList.append(`<li><a class="dropdown-item" select-monitor="${monitor.mid}" href="#"><i class="fa fa-check"></i> ${monitor.name}</a></li>`)
}
function drawMonitorList(){
return new Promise((resolve) => {
$.get(getApiPrefix('monitor'),function(monitors){
$.each(monitors, function(n,monitor){
if(monitor.mode !== 'stop' && monitor.mode !== 'idle'){
loadedMonitors[monitor.mid] = monitor;
drawMonitorListItem(monitor)
}
})
resolve(monitors)
})
})
}
function getMonitorListItem(monitorId){
return wallViewMonitorList.find(`[select-monitor="${monitorId}"]`)
}
function selectMonitor(monitorId, css){
css = css || {};
var isSelected = selectedMonitors[monitorId]
if(isSelected)return;
var numberOfSelected = Object.keys(selectedMonitors)
if(numberOfSelected > 3 && !featureIsActivated(true)){
return
}
++selectedMonitorsCount
selectedMonitors[monitorId] = Object.assign({}, loadedMonitors[monitorId]);
wallViewCanvas.append(`<div class="wallview-video p-0 m-0" live-stream="${monitorId}" style="left:${css.left || 0}px;top:${css.top || 0}px;width:${css.width ? css.width + 'px' : '50vw'};height:${css.height ? css.height + 'px' : '50vh'};"><div class="overlay"><div class="wallview-item-controls text-end"><a class="btn btn-sm btn-outline-danger wallview-item-close"><i class="fa fa-times"></i></a></div></div><iframe src="${getApiPrefix('embed')}/${monitorId}/fullscreen%7Cjquery%7Crelative?host=/"></iframe></div>`)
wallViewCanvas.find(`[live-stream="${monitorId}"]`)
.draggable({
grid: [40, 40],
snap: '#wallview-canvas',
containment: "window",
stop: function(){
saveLayout()
}
})
.resizable({
grid: [40, 40],
snap: '#wallview-container',
stop: function(){
saveLayout()
}
});
getMonitorListItem(monitorId).addClass('active')
}
function deselectMonitor(monitorId){
--selectedMonitorsCount
delete(selectedMonitors[monitorId])
var monitorItem = wallViewCanvas.find(`[live-stream="${monitorId}"]`);
monitorItem.find('iframe').attr('src','about:blank')
monitorItem.remove()
getMonitorListItem(monitorId).removeClass('active')
}
function getCurrentLayout(){
var layout = []
wallViewCanvas.find('.wallview-video').each(function(n,v){
var el = $(v)
var monitorId = el.attr('live-stream')
var position = el.position()
layout.push({
monitorId,
css: {
left: position.left,
top: position.top,
width: el.width(),
height: el.height(),
}
})
})
return layout
}
function saveLayout(){
var windowName = getWindowName();
var layouts = getAllLayouts();
var layout = getCurrentLayout();
var saveContainer = {
layout,
windowInnerWidth: window.innerWidth,
windowInnerHeight: window.innerHeight,
}
layouts[windowName] = saveContainer;
localStorage.setItem('windowLayouts', JSON.stringify(layouts));
}
function getAllLayouts(){
return JSON.parse(localStorage.getItem(`windowLayouts`) || '{}');
}
function getLayout(full){
var windowName = getWindowName();
var saveContainer = getAllLayouts()[windowName]
if(full)return saveContainer || { layout: [] };
var layout = saveContainer.layout || []
return layout;
}
function resetWindowDimensions(){
var saveContainer = getLayout(true);
if(saveContainer.windowInnerWidth && saveContainer.windowInnerHeight){
var widthDiff = window.outerWidth - window.innerWidth;
var heightDiff = window.outerHeight - window.innerHeight;
lastWindowWidth = saveContainer.windowInnerWidth
lastWindowHeight = saveContainer.windowInnerHeight
window.resizeTo(saveContainer.windowInnerWidth + widthDiff, saveContainer.windowInnerHeight + heightDiff);
}
}
function loadSavedLayout(){
var saveContainer = getLayout(true);
resetWindowDimensions()
saveContainer.layout.forEach(function({ monitorId, css }, n){
selectMonitor(monitorId, css);
});
displayInfoScreen();
}
function displayInfoScreen(){
if(selectedMonitorsCount === 0){
wallViewInfoScreen.css('display','flex')
}else{
wallViewInfoScreen.hide()
}
}
function resizeMonitorItem({ monitorId, css }, oldWidth, oldHeight, newWidth, newHeight){
var monitorItem = wallViewCanvas.find(`[live-stream="${monitorId}"]`);
var newCss = rescaleMatrix(css, oldWidth, oldHeight, newWidth, newHeight)
monitorItem.css(newCss)
}
function rescaleMatrix(matrix, oldWidth, oldHeight, newWidth, newHeight) {
const scaleX = newWidth / oldWidth;
const scaleY = newHeight / oldHeight;
return {
left: matrix.left * scaleX,
top: matrix.top * scaleY,
width: matrix.width * scaleX,
height: matrix.height * scaleY
};
}
function onWindowResize(){
var currentWindowWidth = theWindow.width()
var currentWindowHeight = theWindow.height()
var layout = getCurrentLayout();
for(item of layout){
resizeMonitorItem(item,lastWindowWidth,lastWindowHeight,currentWindowWidth,currentWindowHeight)
}
lastWindowWidth = currentWindowWidth
lastWindowHeight = currentWindowHeight
}
function autoPlaceCurrentMonitorItems() {
const wallviewVideos = wallViewCanvas.find('.wallview-video');
const totalItems = wallviewVideos.length;
let numRows, numCols;
if (totalItems === 6 || totalItems === 5) {
numCols = 3;
numRows = 2;
} else {
numRows = Math.ceil(Math.sqrt(totalItems));
numCols = Math.ceil(totalItems / numRows);
}
const containerWidth = wallViewCanvas.width();
const containerHeight = wallViewCanvas.height();
const itemWidth = containerWidth / numCols;
const itemHeight = containerHeight / numRows;
wallviewVideos.each(function(index, element) {
const row = Math.floor(index / numCols);
const col = index % numCols;
$(element).css({
left: col * itemWidth,
top: row * itemHeight,
width: itemWidth,
height: itemHeight
});
});
}
function openAllMonitors(){
$.each(loadedMonitors,function(monitorId, monitor){
selectMonitor(monitorId)
})
autoPlaceCurrentMonitorItems()
displayInfoScreen()
saveLayout()
}
function closeAllMonitors(){
$.each(loadedMonitors,function(monitorId, monitor){
deselectMonitor(monitorId)
})
displayInfoScreen()
saveLayout()
}
drawMonitorList().then(() => {
loadSavedLayout()
setTimeout(() => {
theWindow.resize(() => {
onWindowResize()
saveLayout()
})
},500)
})
$('body')
.on('click', '[select-monitor]', function(e){
e.preventDefault()
var el = $(this);
var monitorId = el.attr('select-monitor')
var isSelected = selectedMonitors[monitorId]
if(isSelected){
deselectMonitor(monitorId)
}else{
selectMonitor(monitorId)
}
displayInfoScreen()
saveLayout()
})
.on('click', '.open-wallview', function(e){
e.preventDefault()
var windowName = getWindowName();
if(isNaN(windowName)){
windowName = windowName + '2'
}else{
windowName = `${parseInt(windowName) + 1}`
}
createWallViewWindow(windowName)
})
.on('click', '.wallview-autoplace', function(e){
e.preventDefault()
autoPlaceCurrentMonitorItems()
saveLayout()
})
.on('click', '.wallview-item-close', function(e){
e.preventDefault()
var monitorId = $(this).parents('[live-stream]').attr('live-stream')
deselectMonitor(monitorId)
})
.on('click', '.wallview-open-all', function(e){
e.preventDefault()
openAllMonitors()
})
.on('click', '.wallview-close-all', function(e){
e.preventDefault()
closeAllMonitors()
})
})

View File

@ -7,7 +7,6 @@ $(document).ready(function(){
var remoteDashboardLinkButton = easyRemoteAccessTab.find('.remote-dashboard-link')
var loadingRegistration = false
var statusConnections = {}
var currentlyRegisteredP2PServer = currentlySelectedP2PServerId ? currentlySelectedP2PServerId + '' : undefined
function copyToClipboard(str) {
const el = document.createElement('textarea');
el.value = str;
@ -72,37 +71,11 @@ $(document).ready(function(){
loadingRegistration = false
easyRemoteAccessTab.find('.remote-dashboard-link').html(`<i class="fa fa-external-link"></i> ` + lang['Open Remote Dashboard'])
easyRemoteAccessTab.find('.remote-dashboard-link-copy').html(`<i class="fa fa-copy"></i> ` + lang['Copy Remote Link'])
displayCurrentlySelectedInternally()
}
function displayCurrentlySelectedInternally(){
var selectedServer = p2pServerList[currentlyRegisteredP2PServer]
if(selectedServer){
var key = selectedServer.key
var cardEl = easyRemoteAccessTab.find(`[drawn-id="${key}"]`)
easyRemoteAccessTab.find(`[drawn-id].selected`).removeClass('selected')
cardEl.addClass('selected')
setCurrentRemoteLink()
}
}
function makeHostLink(selectedServer,apiKey){
var href = `https://${selectedServer.host}:${selectedServer.webPort == 80 ? 443 : selectedServer.webPort}/s/${apiKey}/`
return href
}
function setCurrentRemoteLink(){
var apiKey = easyRemoteAccessForm.find('[name="p2pApiKey"]').val()
var selectedServer = p2pServerList[currentlyRegisteredP2PServer]
console.log(selectedServer,currentlySelectedP2PServerId,p2pServerList)
if(selectedServer && selectedServer.host){
var href = makeHostLink(selectedServer,apiKey)
remoteDashboardLinkButton.attr('href',href)
}else{
new PNotify({
type: 'warning',
title: lang['P2P Server Not Selected'],
text: lang.p2pServerNotSelectedText,
})
}
}
function beginAllStatusConnections(){
$.each(p2pServerList,function(key,server){
server.key = key
@ -127,6 +100,14 @@ $(document).ready(function(){
toggleAffected.hide()
}
}
function getSelectedServers(){
var theArray = [];
$(`[drawn-id].active`).each(function(){
var theKey = $(this).attr('drawn-id')
theArray.push(theKey)
})
return theArray
}
p2pEnabledSwitch.change(setVisibilityForList)
easyRemoteAccessTab.find('.submit').click(function(){
easyRemoteAccessForm.submit()
@ -135,39 +116,51 @@ $(document).ready(function(){
e.preventDefault()
var formValues = $(this).serializeObject()
disableForm()
formValues.p2pHostSelected = currentlySelectedP2PServerId
// formValues.p2pHostSelected = currentlySelectedP2PServerId
formValues.p2pHostMultiSelected = getSelectedServers()
console.log(formValues)
$.post(superApiPrefix + $user.sessionKey + '/p2p/save',{
data: JSON.stringify(formValues)
},function(data){
console.log(data)
if(data.ok){
currentlyRegisteredP2PServer = currentlySelectedP2PServerId + ''
new PNotify({
type: 'success',
title: lang['P2P Settings Applied'],
text: lang.p2pSettingsText1,
})
setCurrentRemoteLink()
setTimeout(enableForm,5000)
}
})
return false
})
easyRemoteAccessForm.on('click','[drawn-id]',function(){
easyRemoteAccessTab.on('click','.activate-remote-selection',function(e){
e.preventDefault()
var el = $(this)
var p2pServerId = el.attr('drawn-id')
easyRemoteAccessForm.find('[drawn-id]').removeClass('active')
el.addClass('active')
currentlySelectedP2PServerId = p2pServerId
var parent = el.parents('[drawn-id]')
var drawnId = parent.attr('drawn-id')
var alreadyActive = parent.hasClass('active')
var selectedServer = p2pServerList[drawnId]
var isWss = selectedServer.p2pPort == 443
console.log(selectedServer)
if(alreadyActive){
parent.removeClass('active')
}else{
parent.addClass('active')
const drawnIdToDisable = isWss ? drawnId.replace('-ssl','') : `${drawnId}-ssl`
console.log(drawnIdToDisable)
easyRemoteAccessTab.find(`[drawn-id="${drawnIdToDisable}"]`).removeClass('active')
}
return false;
})
easyRemoteAccessTab.on('click','.remote-dashboard-link-copy',function(e){
e.preventDefault()
if(!loadingRegistration){
var parent = $(this).parents('[drawn-id]')
var drawnId = parent.attr('drawn-id')
var apiKey = easyRemoteAccessForm.find('[name="p2pApiKey"]').val()
var selectedServer = p2pServerList[currentlyRegisteredP2PServer]
console.log(selectedServer,currentlySelectedP2PServerId,p2pServerList)
if(selectedServer && selectedServer.host){
var selectedServer = p2pServerList[drawnId]
if(parent.hasClass('active') && selectedServer && selectedServer.host){
var href = makeHostLink(selectedServer,apiKey)
copyToClipboard(href)
new PNotify({
@ -183,8 +176,24 @@ $(document).ready(function(){
})
}
}
e.stopPropagation()
return false;
})
easyRemoteAccessTab.on('click','.remote-dashboard-link',function(e){
e.preventDefault()
if(!loadingRegistration){
var parent = $(this).parents('[drawn-id]')
var drawnId = parent.attr('drawn-id')
var apiKey = easyRemoteAccessForm.find('[name="p2pApiKey"]').val()
var selectedServer = p2pServerList[drawnId]
console.log(selectedServer,parent.hasClass('active'),parent.length)
if(parent.hasClass('active') && selectedServer && selectedServer.host){
var href = makeHostLink(selectedServer,apiKey)
window.open(href, '_blank').focus();
}
}
e.stopPropagation()
return false;
})
setVisibilityForList()
displayCurrentlySelectedInternally()
})

View File

@ -1,5 +1,7 @@
<% const p2pServerList = config.p2pServerList || {}
const selectedServer = p2pServerList[config.p2pHostSelected]
<%
const p2pServerList = config.p2pServerList || {}
const selectedServers = config.p2pHostMultiSelected
const multipleSelected = selectedServers instanceof Array && selectedServers.length > 0;
%>
<style>
.epic-text-filter {
@ -19,8 +21,9 @@
</style>
<script>
window.p2pServerList = <%- JSON.stringify(p2pServerList) %>
window.multipleSelected = <%- multipleSelected %>
window.selectedServers = <%- JSON.stringify(selectedServers) %>
window.useBetterP2P = <%- !!config.useBetterP2P %>
window.currentlySelectedP2PServerId = `<%- config.p2pHostSelected %>`
</script>
<link rel="stylesheet" href="<%-window.libURL%>assets/css/super.easyRemoteAccess.css">
<form>
@ -40,12 +43,6 @@
<div class="form-group p2p-toggle-affected">
<input placeholder="<%-lang['P2P API Key']%>" class="form-control btn btn-dark text-left text-white" type="text" name="p2pApiKey" value="<%- config.p2pApiKey %>">
</div>
<div class="form-group p2p-toggle-affected">
<div class="btn-group d-flex flex-row">
<a target="_blank" href="#" class="flex-grow-1 btn btn-default remote-dashboard-link-copy"><i class="fa fa-copy"></i> <%- lang['Copy Remote Link'] %></a>
<a target="_blank" href="#" class="flex-grow-1 btn btn-info remote-dashboard-link" target="_blank"><i class="fa fa-external-link"></i> <%- lang['Open Remote Dashboard'] %></a>
</div>
</div>
<div class="btn-group d-flex flex-row">
<a href="#" class="submit flex-grow-1 btn btn-success"><i class="fa fa-check"></i> <%- lang.Save %></a>
</div>
@ -72,7 +69,7 @@
if(!config.useBetterP2P && details.v2)return;
%>
<div class="col-md-4">
<div class="card bg-dark cursor-pointer text-white mb-4 <% if(config.p2pHostSelected === key){ %>active<% } %>" drawn-id="<%- key %>">
<div class="card bg-dark cursor-pointer text-white mb-4 <% if((!multipleSelected && config.p2pHostSelected === key) || multipleSelected && selectedServers.indexOf(key) > -1){ %>active<% } %>" drawn-id="<%- key %>">
<div class="card-header" style="min-height:auto">
<span class="badge badge-sm badge-danger pull-right selected-badge"><%- lang.Selected %></span>
<span class="badge badge-sm badge-info name-badge"><%- details.name.toUpperCase() %></span>
@ -107,7 +104,11 @@
</div>
</div>
<div class="card-footer" style="min-height:auto">
<a class="btn badge badge-sm badge-warning geo-badge" target="_blank" href="<%- `https://www.google.ca/maps/@${details.location.lat},${details.location.lon},15z` %>"><i class="fa fa-map-marker"></i> <%- details.location.lat %>, <%- details.location.lon %></a>
<div>
<a target="_blank" href="#" class="d-block btn btn-default activate-remote-selection"><i class="fa fa-check"></i> <%- lang['Activate'] %></a>
<a target="_blank" href="#" class="d-block btn btn-default remote-dashboard-link-copy"><i class="fa fa-copy"></i> <%- lang['Copy Remote Link'] %></a>
<a target="_blank" href="#" class="d-block btn btn-info remote-dashboard-link" target="_blank"><i class="fa fa-external-link"></i> <%- lang['Open Remote Dashboard'] %></a>
</div>
</div>
</div>
</div>
@ -115,7 +116,6 @@
</div>
<div class="form-group p2p-toggle-affected">
<div class="btn-group d-flex flex-row">
<a target="_blank" href="#" class="flex-grow-1 btn btn-info remote-dashboard-link" target="_blank"><i class="fa fa-external-link"></i> <%- lang['Open Remote Dashboard'] %></a>
<a href="#" class="submit flex-grow-1 btn btn-success"><i class="fa fa-check"></i> <%- lang.Save %></a>
</div>
</div>

View File

@ -21,12 +21,15 @@
<script src="<%-window.libURL%>assets/vendor/js/clock.js"></script>
<script src="<%-window.libURL%>assets/vendor/js/vis.min.js" async></script>
<script src="<%-window.libURL%>assets/js/bs5.dashboard-base.js"></script>
<script src="<%-window.libURL%>assets/js/bs5.dashboard-base.buttons.js"></script>
<script src="<%-window.libURL%>assets/js/bs5.extenders.js"></script>
<script src="<%-window.libURL%>assets/js/bs5.websocket.js"></script>
<script src="<%-window.libURL%>assets/js/bs5.videos.js"></script>
<script src="<%-window.libURL%>assets/js/bs5.videos.buttons.js"></script>
<script src="<%-window.libURL%>assets/js/bs5.videoPlayer.js"></script>
<script src="<%-window.libURL%>assets/js/bs5.livePlayer.js"></script>
<script src="<%-window.libURL%>assets/js/bs5.accountSettings.js"></script>
<script src="<%-window.libURL%>assets/js/bs5.alternateLogins.js"></script>
<script src="<%-window.libURL%>assets/js/bs5.subAccountManager.js"></script>
<script src="<%-window.libURL%>assets/js/bs5.apiKeys.js"></script>
<script src="<%-window.libURL%>assets/js/bs5.monitorSettings.js"></script>

View File

@ -42,12 +42,16 @@
</main>
<script>
var monitorSettingsAdditionalInputMapFieldHtml = `<% Object.keys(define["Monitor Settings Additional Input Map"].blocks).forEach(function(blockKey){
drawBlock(define["Monitor Settings Additional Input Map"].blocks[blockKey])
}) %>`
var monitorSettingsAdditionalInputMapFieldHtml = `<% Object.keys(define["Monitor Settings Additional Input Map"].blocks).forEach(function(blockKey){ %>
<%- include('drawBlock', {
theBlock: define["Monitor Settings Additional Input Map"].blocks[blockKey]
}) %>
<% }) %>`
</script>
<script>
var monitorSettingsAdditionalStreamChannelFieldHtml = `<% Object.keys(define["Monitor Settings Additional Stream Channel"].blocks).forEach(function(blockKey){
drawBlock(define["Monitor Settings Additional Stream Channel"].blocks[blockKey])
}) %>`
var monitorSettingsAdditionalStreamChannelFieldHtml = `<% Object.keys(define["Monitor Settings Additional Stream Channel"].blocks).forEach(function(blockKey){ %>
<%- include('drawBlock', {
theBlock: define["Monitor Settings Additional Stream Channel"].blocks[blockKey]
}) %>
<% }) %>`
</script>

View File

@ -17,10 +17,7 @@
var buildOptions
%>
<form class="dark">
<%- include('home/fieldBuilders'); %>
<%
drawBlock(define['LDAP'].blocks.LDAP)
%>
<%- include('home/drawBlock', { theBlock: define['LDAP'].blocks.LDAP }) %>
</form>
<script>
$(document).ready(function() {

View File

@ -0,0 +1,82 @@
<title><%- lang.Shinobi %></title>
<%
var forceUrlPrefix
var urlPrefix = ``
var targetPort = config.ssl && config.ssl.port && protocol === 'https' ? config.ssl.port : config.port
var addonsEnabled = {}
var rawAddonList = decodeURI(data.addon || '').split('|');
rawAddonList.forEach(function(piece){
var pieceParts = piece.split('=');
var key = pieceParts[0];
var value = pieceParts[1] || true;
addonsEnabled[key] = value;
});
function getAddon(addonTag){
return addonsEnabled[addonTag];
}
var streamWidth = parseInt(getAddon('width')) || 640
var streamHeight = parseInt(getAddon('height')) || 480
var hasGUI = getAddon('gui')
var isFullscreen = getAddon('fullscreen')
var isRelativeUrl = getAddon('relative')
if(forceUrlPrefix){
urlPrefix = forceUrlPrefix
}else if(isRelativeUrl){
urlPrefix = ''
}else if(config.baseURL){
urlPrefix = config.baseURL
}else if(!targetPort || targetPort === '' || targetPort == 80 || targetPort == 443){
urlPrefix = baseUrl
}else{
urlPrefix = `${baseUrl}:${targetPort}`
}
if(urlPrefix.endsWith('/') === false){
urlPrefix += '/'
}
var originalURL = `${urlPrefix}`
var libURL = config.baseURL;
if(!libURL)libURL = originalURL;
%>
<%- include('blocks/header-favicon') %>
<script>window.libURL="<%- libURL %>";</script>
<script>window.$user=<%- JSON.stringify($user) %>;</script>
<script>window.lang=<%- JSON.stringify(lang) %>;</script>
<script>window.definitions=<%- JSON.stringify(define) %>;</script>
<script>window.urlPrefix = "<%- urlPrefix || '' %>";</script>
<script>window.groupKey = "<%- groupKey || '' %>";</script>
<script>window.authKey = "<%- authKey || '' %>";</script>
<script>window.userHasSubscribed = <%- config.userHasSubscribed ? 'true' : 'false' %>;</script>
<script src="<%- urlPrefix %>assets/vendor/js/socket.io.min.js"></script>
<script src="<%- urlPrefix %>assets/vendor/js/jquery.min.js"></script>
<link rel="stylesheet" href="<%- urlPrefix %>assets/vendor/font-awesome/css/font-awesome.min.css">
<link rel="stylesheet" href="<%- urlPrefix %>assets/vendor/bootstrap5/css/bootstrap.min.css">
<link rel="stylesheet" href="<%- urlPrefix %>assets/vendor/daterangepicker.css">
<link rel="stylesheet" href="<%- urlPrefix %>assets/vendor/vis.min.css">
<link rel="stylesheet" href="<%- urlPrefix %>assets/css/bs5.timeline.css">
<link rel="stylesheet" href="<%- urlPrefix %>assets/css/dashboard.css">
<link rel="stylesheet" href="<%- urlPrefix %>assets/vendor/jquery-ui.min.css">
<link rel="stylesheet" href="<%- urlPrefix %>assets/vendor/pnotify.custom.min.css">
<body class="bg-darker">
<div class="container-fluid">
<div class="row">
<main id="pageTabContainer" class="col-md-12 ms-sm-auto col-lg-12 px-md-4">
<%- include('blocks/home/timeline.ejs') %>
<%- include('blocks/confirm.ejs') %>
</main>
</div>
</div>
</body>
<div style="display:none" id="temp"></div>
<a id="floating-back-button" class="go-back" style="display:none"><i class="fa fa-arrow-left"></i></a>
<script src="<%- urlPrefix %>assets/vendor/moment-with-locales.js"></script>
<script src="<%- urlPrefix %>assets/vendor/js/daterangepicker.js"></script>
<script src="<%- urlPrefix %>assets/vendor/bootstrap5/js/bootstrap.bundle.min.js"></script>
<script src="<%- urlPrefix %>assets/vendor/js/pnotify.custom.min.js"></script>
<script src="<%- urlPrefix %>assets/vendor/js/jquery-ui.min.js"></script>
<script src="<%- urlPrefix %>assets/vendor/js/vis.min.js" async></script>
<script src="<%- urlPrefix %>assets/js/bs5.dashboard-base.js"></script>
<script src="<%- urlPrefix %>assets/js/bs5.zipAndDownload.js"></script>
<script src="<%- urlPrefix %>assets/js/bs5.monitorsUtils.js"></script>
<script src="<%- urlPrefix %>assets/js/bs5.videos.js"></script>
<script src="<%- urlPrefix %>assets/js/bs5.confirm.js"></script>
<script src="<%- urlPrefix %>assets/js/bs5.wallvideoview.js"></script>

78
web/pages/wallview.ejs Normal file
View File

@ -0,0 +1,78 @@
<title><%- lang.Shinobi %></title>
<%
var forceUrlPrefix
var urlPrefix = ``
var targetPort = config.ssl && config.ssl.port && protocol === 'https' ? config.ssl.port : config.port
var addonsEnabled = {}
var rawAddonList = decodeURI(data.addon || '').split('|');
rawAddonList.forEach(function(piece){
var pieceParts = piece.split('=');
var key = pieceParts[0];
var value = pieceParts[1] || true;
addonsEnabled[key] = value;
});
function getAddon(addonTag){
return addonsEnabled[addonTag];
}
var streamWidth = parseInt(getAddon('width')) || 640
var streamHeight = parseInt(getAddon('height')) || 480
var hasGUI = getAddon('gui')
var isFullscreen = getAddon('fullscreen')
var isRelativeUrl = getAddon('relative')
if(forceUrlPrefix){
urlPrefix = forceUrlPrefix
}else if(isRelativeUrl){
urlPrefix = ''
}else if(config.baseURL){
urlPrefix = config.baseURL
}else if(!targetPort || targetPort === '' || targetPort == 80 || targetPort == 443){
urlPrefix = baseUrl
}else{
urlPrefix = `${baseUrl}:${targetPort}`
}
if(urlPrefix.endsWith('/') === false){
urlPrefix += '/'
}
var originalURL = `${urlPrefix}`
%>
<%- include('blocks/header-favicon') %>
<script>window.$user=<%- JSON.stringify($user) %>;</script>
<script>window.urlPrefix = "<%- urlPrefix || '' %>";</script>
<script>window.groupKey = "<%- groupKey || '' %>";</script>
<script>window.authKey = "<%- authKey || '' %>";</script>
<script>window.userHasSubscribed = <%- config.userHasSubscribed ? 'true' : 'false' %>;</script>
<script src="<%- urlPrefix %>assets/vendor/js/socket.io.min.js"></script>
<script src="<%- urlPrefix %>assets/vendor/js/jquery.min.js"></script>
<link rel="stylesheet" href="<%- urlPrefix %>assets/vendor/font-awesome/css/font-awesome.min.css">
<link rel="stylesheet" href="<%- urlPrefix %>assets/vendor/bootstrap5/css/bootstrap.min.css">
<link rel="stylesheet" href="<%- urlPrefix %>assets/css/bs5.wallview.css">
<link rel="stylesheet" href="<%- urlPrefix %>assets/vendor/jquery-ui.min.css">
<link rel="stylesheet" href="<%- urlPrefix %>assets/vendor/pnotify.custom.min.css">
<div id="wallview-container">
<div id="wallview-info-screen" class="justify-content-center align-self-center" style="display:none">
<div class="justify-content-center align-self-center text-center col-3 text-white">
<i class="fa fa-info-circle fa-5x mb-3"></i>
<h4><%- lang.openWallViewInfo %></h4>
</div>
</div>
<div id="wallview-canvas">
</div>
<div id="wallview-controls" class="text-end">
<a class="btn btn-primary wallview-open-all" href="#"><%- lang['Open All'] %></a>
<a class="btn btn-danger wallview-close-all" href="#"><%- lang['Close All'] %></a>
<a class="btn btn-success wallview-autoplace" href="#"><%- lang['Auto Placement'] %></a>
<a class="btn btn-primary open-wallview" href="#"><%- lang['New Wall Display'] %></a>
<div class="dropdown d-inline-block">
<a class="btn btn-secondary dropdown-toggle" href="#" role="button" id="monitorListButton" data-bs-toggle="dropdown" aria-expanded="false">
<%- lang.Monitors %>
</a>
<ul id="wallview-monitorList" class="dropdown-menu" aria-labelledby="monitorListButton"></ul>
</div>
</div>
</div>
<!-- <script src="<%- urlPrefix %>assets/js/bs5.embed.utils.js"></script> -->
<script src="<%- urlPrefix %>assets/vendor/bootstrap5/js/bootstrap.bundle.min.js"></script>
<script src="<%- urlPrefix %>assets/vendor/js/pnotify.custom.min.js"></script>
<script src="<%- urlPrefix %>assets/vendor/js/jquery-ui.min.js"></script>
<script src="<%- urlPrefix %>assets/js/bs5.wallview.js"></script>