From 2c243b67408e3c11315bfada335746b10bee6020 Mon Sep 17 00:00:00 2001 From: Moe Date: Mon, 13 Mar 2023 01:31:11 +0000 Subject: [PATCH] Paladium : Critical Fixes --- Docker/README.md | 8 +- Docker/init.sh | 7 +- Dockerfile | 1 + Dockerfile.arm32v7 | 1 + Dockerfile.nodb | 97 +++++ Dockerfile.nvidia | 1 + definitions/base.js | 43 +- languages/en_CA.json | 5 +- libs/basic/utils.js | 18 +- libs/commander/workerv2.js | 4 +- libs/events/tracking.js | 157 +++++++ libs/events/utils.js | 123 +++--- libs/monitor/utils.js | 16 +- libs/uploaders/s3based.js | 6 +- libs/webServerStreamPaths.js | 4 + package-lock.json | 212 ++++++--- package.json | 3 +- tools/installAllCustomAutoLoadPackages.js | 46 ++ web/assets/js/bs5.accountSettings.js | 3 +- web/assets/js/bs5.liveGrid.js | 508 ++++++++++++---------- web/assets/js/bs5.monitorSettings.js | 2 +- web/assets/js/bs5.monitorsUtils.js | 2 +- web/assets/js/bs5.powerVideo.js | 2 +- 23 files changed, 874 insertions(+), 395 deletions(-) create mode 100644 Dockerfile.nodb create mode 100644 libs/events/tracking.js create mode 100644 tools/installAllCustomAutoLoadPackages.js diff --git a/Docker/README.md b/Docker/README.md index 111bd0a3..815ebd8f 100644 --- a/Docker/README.md +++ b/Docker/README.md @@ -20,7 +20,7 @@ bash <(curl -s https://gitlab.com/Shinobi-Systems/Shinobi-Installer/raw/master/s Once complete open port `8080` of your Docker host in a web browser. -## Run Way +## "Run" Way **Installing Shinobi** @@ -85,6 +85,12 @@ docker run -d --name='Shinobi' -p '8080:8080/tcp' -v "/dev/shm/Shinobi/streams": > Host mount paths have been updated in this document. + ### Running without Included Database (NoDB) + + For information about this please see this Merge Request done by @thtmnisamnstr. It thoroughly documents how to use the NoDB installation method. + + https://gitlab.com/Shinobi-Systems/Shinobi/-/merge_requests/443 + ### Volumes | Volumes | Description | diff --git a/Docker/init.sh b/Docker/init.sh index 0bd57db2..e94374b3 100644 --- a/Docker/init.sh +++ b/Docker/init.sh @@ -70,13 +70,18 @@ cronKey="$(head -c 1024 < /dev/urandom | sha256sum | awk '{print substr($1,1,29) cd /home/Shinobi mkdir -p libs/customAutoLoad + if [ -e "/config/conf.json" ]; then cp /config/conf.json conf.json elif [ ! -e "./conf.json" ]; then cp conf.sample.json conf.json fi +# Create /config/conf.json if it doesn't exist +if [ ! -e "/config/conf.json" ]; then + node tools/modifyConfiguration.js cpuUsageMarker=CPU subscriptionId=$SUBSCRIPTION_ID thisIsDocker=true pluginKeys="$PLUGIN_KEYS" databaseType="$DB_TYPE" db="$DATABASE_CONFIG" ssl="$SSL_CONFIG" + cp /config/conf.json conf.json +fi sudo sed -i -e 's/change_this_to_something_very_random__just_anything_other_than_this/'"$cronKey"'/g' conf.json -node tools/modifyConfiguration.js cpuUsageMarker=CPU subscriptionId=$SUBSCRIPTION_ID thisIsDocker=true pluginKeys="$PLUGIN_KEYS" db="$DATABASE_CONFIG" ssl="$SSL_CONFIG" echo "=============" diff --git a/Dockerfile b/Dockerfile index 4d2d1e41..6f4f1072 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,7 @@ ENV DB_USER=majesticflame \ DB_HOST='localhost' \ DB_DATABASE=ccio \ DB_PORT=3306 \ + DB_TYPE='mysql' \ SUBSCRIPTION_ID=sub_XXXXXXXXXXXX \ PLUGIN_KEYS='{}' \ SSL_ENABLED='false' \ diff --git a/Dockerfile.arm32v7 b/Dockerfile.arm32v7 index eed0ba2a..2e724f77 100644 --- a/Dockerfile.arm32v7 +++ b/Dockerfile.arm32v7 @@ -5,6 +5,7 @@ ENV DB_USER=majesticflame \ DB_HOST='localhost' \ DB_DATABASE=ccio \ DB_PORT=3306 \ + DB_TYPE='mysql' \ SUBSCRIPTION_ID=sub_XXXXXXXXXXXX \ PLUGIN_KEYS='{}' \ SSL_ENABLED='false' \ diff --git a/Dockerfile.nodb b/Dockerfile.nodb new file mode 100644 index 00000000..8648484f --- /dev/null +++ b/Dockerfile.nodb @@ -0,0 +1,97 @@ +FROM node:16-buster-slim + +ENV DB_USER=majesticflame \ + DB_PASSWORD='' \ + DB_HOST='localhost' \ + DB_DATABASE=ccio \ + DB_PORT=3306 \ + DB_TYPE='mysql' \ + SUBSCRIPTION_ID=sub_XXXXXXXXXXXX \ + PLUGIN_KEYS='{}' \ + SSL_ENABLED='false' \ + SSL_COUNTRY='CA' \ + SSL_STATE='BC' \ + SSL_LOCATION='Vancouver' \ + SSL_ORGANIZATION='Shinobi Systems' \ + SSL_ORGANIZATION_UNIT='IT Department' \ + SSL_COMMON_NAME='nvr.ninja' \ + DB_DISABLE_INCLUDED=true +ARG DEBIAN_FRONTEND=noninteractive + +RUN mkdir -p /home/Shinobi /config /var/lib/mysql + +RUN apt update -y +RUN apt install wget curl net-tools -y + +# Install FFmpeg + +RUN apt update --fix-missing +RUN apt install -y software-properties-common \ + libfreetype6-dev \ + libgnutls28-dev \ + libmp3lame-dev \ + libass-dev \ + libogg-dev \ + libtheora-dev \ + libvorbis-dev \ + libvpx-dev \ + libwebp-dev \ + libssh2-1-dev \ + libopus-dev \ + librtmp-dev \ + libx264-dev \ + libx265-dev \ + yasm +RUN apt install -y \ + build-essential \ + bzip2 \ + coreutils \ + procps \ + gnutls-bin \ + nasm \ + tar \ + x264 + +RUN apt install -y zip + +RUN apt install -y \ + ffmpeg \ + git \ + make \ + g++ \ + gcc \ + pkg-config \ + python3 \ + wget \ + tar \ + sudo \ + xz-utils + + +WORKDIR /home/Shinobi +COPY . . +#RUN rm -rf /home/Shinobi/plugins +COPY ./plugins /home/Shinobi/plugins +RUN chmod -R 777 /home/Shinobi/plugins +RUN npm i npm@latest -g && \ + npm install --unsafe-perm && \ + npm install pm2 -g +COPY ./Docker/pm2.yml ./ + +RUN npm i pg --save + +# Copy default configuration files +# COPY ./config/conf.json ./config/super.json /home/Shinobi/ +RUN chmod -f +x /home/Shinobi/Docker/init.sh +RUN sed -i -e 's/\r//g' /home/Shinobi/Docker/init.sh +# RUN chmod -f +x /home/Shinobi/shinobi + +VOLUME ["/home/Shinobi/videos"] +VOLUME ["/home/Shinobi/libs/customAutoLoad"] +VOLUME ["/config"] + +EXPOSE 8080 443 21 25 + +ENTRYPOINT ["sh","/home/Shinobi/Docker/init.sh"] + +CMD [ "pm2-docker", "/home/Shinobi/Docker/pm2.yml" ] diff --git a/Dockerfile.nvidia b/Dockerfile.nvidia index 529d41b4..d6221971 100644 --- a/Dockerfile.nvidia +++ b/Dockerfile.nvidia @@ -5,6 +5,7 @@ ENV DB_USER=majesticflame \ DB_HOST='localhost' \ DB_DATABASE=ccio \ DB_PORT=3306 \ + DB_TYPE='mysql' \ SUBSCRIPTION_ID=sub_XXXXXXXXXXXX \ PLUGIN_KEYS='{}' \ SSL_ENABLED='false' \ diff --git a/definitions/base.js b/definitions/base.js index a2245ee0..48f021cd 100644 --- a/definitions/base.js +++ b/definitions/base.js @@ -3105,6 +3105,31 @@ module.exports = function(s,config,lang){ } ] }, + { + "name": "detail=detector_object_ignore_not_move", + "field": lang["Ignore Non-Moving"], + "default": "0", + "fieldType": "select", + "selector": "h_obj_ignore_move", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + { + hidden: true, + "name": "detail=detector_object_move_percent", + "field": lang['Minimum Movement'], + "description": lang.inPercent, + "default": "5", + "form-group-class": "h_obj_ignore_move_input h_obj_ignore_move_1" + }, { isAdvanced: true, "name": "detail=detector_send_frames_object", @@ -4935,24 +4960,6 @@ module.exports = function(s,config,lang){ } ] }, - { - "field": lang['Show Thumbnails in Video List'], - attribute:'localStorage="showThumbnail"', - "description": "", - "default": "0", - "example": "", - "fieldType": "select", - "possible": [ - { - "name": lang.No, - "value": "0" - }, - { - "name": lang.Yes, - "value": "1" - } - ] - }, { "field": lang.Themes, "name": "detail=theme", diff --git a/languages/en_CA.json b/languages/en_CA.json index 54047166..8daab026 100644 --- a/languages/en_CA.json +++ b/languages/en_CA.json @@ -196,6 +196,8 @@ "Trigger Camera Groups": "Trigger Camera Groups", "Motion Detection": "Motion Detection", "Object Detection": "Object Detection", + "Minimum Movement": "Minimum Movement", + "inPercent": "In Percent", "Hide Detection on Stream": "Hide Detection on Stream", "JPEG Mode": "JPEG Mode", "Reconnect Stream": "Reconnect Stream", @@ -394,7 +396,7 @@ "Found Devices": "Found Devices", "Switch on for Still Image": "Switch on for Still Image", "Live Stream Toggle": "Live Stream Toggle", - "RegionNote": "When adding points click on the edge of the polygon. Right click a point to remove.", + "RegionNote": "When adding points click on the edge of the polygon. Right click a point to remove it. The dimensions here are based on the Detector Settings of your Monitor. Regions will automatically scale to match Object Detection dimensions.", "Points": "Points", "Minimum Change": "Minimum Change", "Maximum Change": "Maximum Change", @@ -841,6 +843,7 @@ "Delete Motionless Video": "Delete Motionless Video", "Send Frames": "Send Frames Push frames to be analyzed", "Detector Rate": "Detector Rate (FPS)", + "Ignore Non-Moving": "Ignore Non-Moving", "Feed-in Image Width": "Feed-in Image Width", "Feed-in Image Height": "Feed-in Image Height", "Check for Motion First": "Check for Motion First", diff --git a/libs/basic/utils.js b/libs/basic/utils.js index 8bbb9c0d..be6e92fd 100644 --- a/libs/basic/utils.js +++ b/libs/basic/utils.js @@ -114,14 +114,16 @@ module.exports = (processCwd,config) => { method : options.method || 'GET', headers: {'Content-Type': 'application/json'} } - if(typeof options.postData === 'object'){ - requestOptions.body = JSON.stringify(options.postData) - } else if(typeof options.postData === 'string'){ - try{ - JSON.parse(options.postData) - requestOptions.body = options.postData - }catch(err){ - + if(requestOptions.method !== 'GET'){ + if(typeof options.postData === 'object'){ + requestOptions.body = JSON.stringify(options.postData) + }else if(options.postData && typeof options.postData === 'string'){ + try{ + JSON.parse(options.postData) + requestOptions.body = options.postData + }catch(err){ + + } } } if(hasUsernameAndPassword && hasDigestAuthEnabled){ diff --git a/libs/commander/workerv2.js b/libs/commander/workerv2.js index dfa7a950..fdffd758 100644 --- a/libs/commander/workerv2.js +++ b/libs/commander/workerv2.js @@ -27,7 +27,7 @@ parentPort.on('message',(data) => { case'init': config = Object.assign({},data.config) lang = Object.assign({},data.lang) - remoteConnectionPort = config.ssl ? config.ssl.port || 443 : config.port || 8080 + remoteConnectionPort = config.ssl && JSON.stringify(config.ssl) !== '{}' ? config.ssl.port || 443 : config.port || 8080 initialize() break; case'exit': @@ -332,7 +332,7 @@ function closeResponseTunnel(originalRequestId){ function initialize(){ const selectedP2PServerId = config.p2pServerList[config.p2pHostSelected] ? config.p2pHostSelected : Object.keys(config.p2pServerList)[0] const p2pServerDetails = config.p2pServerList[selectedP2PServerId] - const selectedHost = 'ws://' + p2pServerDetails.host + ':' + p2pServerDetails.p2pPort + const selectedHost = `${p2pServerDetails.secure ? `wss` : 'ws'}://` + p2pServerDetails.host + ':' + p2pServerDetails.p2pPort config.selectedHost = selectedHost startConnection(selectedHost,config.p2pApiKey) } diff --git a/libs/events/tracking.js b/libs/events/tracking.js new file mode 100644 index 00000000..b0bdff2c --- /dev/null +++ b/libs/events/tracking.js @@ -0,0 +1,157 @@ +const movingThings = require('shinobi-node-moving-things-tracker').Tracker +module.exports = (s,config,lang,app,io) => { + const objectTrackers = {} + const objectTrackerTimeouts = {} + function resetObjectTracker(trackerId,matrices){ + const Tracker = movingThings.newTracker(); + objectTrackers[trackerId] = { + frameCount: 1, + tracker: Tracker, + lastPositions: [] + } + return objectTrackers[trackerId] + } + function setLastTracked(trackerId, trackedMatrices){ + const theTracker = objectTrackers[trackerId] + theTracker.lastPositions = trackedMatrices + } + function getTracked(trackerId){ + const theTracker = objectTrackers[trackerId] + const frameCount = theTracker.frameCount + const trackedObjects = theTracker.tracker.getJSONOfTrackedItems().map((matrix) => { + return { + id: matrix.id, + tag: matrix.name, + x: matrix.x, + y: matrix.y, + width: matrix.w, + height: matrix.h, + confidence: matrix.confidence, + isZombie: matrix.isZombie, + } + }) + return trackedObjects; + } + function trackObject(trackerId,matrices){ + if(!objectTrackers[trackerId]){ + resetObjectTracker(trackerId) + } + const mappedMatrices = matrices.map((matrix) => { + return { + x: matrix.x, + y: matrix.y, + w: matrix.width, + h: matrix.height, + confidence: matrix.confidence, + name: matrix.tag, + } + }); + const theTracker = objectTrackers[trackerId] + theTracker.tracker.updateTrackedItemsWithNewFrame(mappedMatrices, theTracker.frameCount); + ++theTracker.frameCount + } + function trackObjectWithTimeout(trackerId,matrices){ + clearTimeout(objectTrackerTimeouts[trackerId]); + objectTrackerTimeouts[trackerId] = setTimeout(() => { + objectTrackers[trackerId].tracker.reset() + delete(objectTrackers[trackerId]) + delete(objectTrackerTimeouts[trackerId]) + },1000 * 60); + trackObject(trackerId,matrices); + } + function objectHasMoved(matrices, options = {}) { + const { imgHeight = 1, imgWidth = 1, threshold = 0 } = options; + for (let i = 0; i < matrices.length; i++) { + const current = matrices[i]; + if (i < matrices.length - 1) { + const next = matrices[i + 1]; + let totalDistanceMoved = 0; + let numPointsCompared = 0; + if (next) { + // Compare each corner of the matrices + const currentCorners = [ + { x: current.x, y: current.y }, + { x: current.x + current.width, y: current.y }, + { x: current.x, y: current.y + current.height }, + { x: current.x + current.width, y: current.y + current.height } + ]; + const nextCorners = [ + { x: next.x, y: next.y }, + { x: next.x + next.width, y: next.y }, + { x: next.x, y: next.y + next.height }, + { x: next.x + next.width, y: next.y + next.height } + ]; + for (let j = 0; j < currentCorners.length; j++) { + const currentCorner = currentCorners[j]; + const nextCorner = nextCorners[j]; + const dx = nextCorner.x - currentCorner.x; + const dy = nextCorner.y - currentCorner.y; + const distanceMoved = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)); + const distanceMovedPercent = + (100 * distanceMoved) / Math.max(current.width, current.height); + totalDistanceMoved += distanceMovedPercent; + numPointsCompared++; + } + const averageDistanceMoved = totalDistanceMoved / numPointsCompared; + if (averageDistanceMoved < threshold) { + continue; + } else { + return true; + } + } + } + } + return false; + } + function groupMatricesById(matrices) { + const matrixById = {}; + const matrixTags = {}; + + matrices.forEach(matrix => { + const id = matrix.id; + const tag = matrix.tag; + if (!matrixById[id]) { + matrixById[id] = []; + } + matrixTags[tag] = id; + matrixById[id].push(matrix); + }); + + return matrixById + } + function getAllMatricesThatMoved(monitorConfig,matrices){ + const monitorDetails = monitorConfig.details + const imgWidth = parseInt(monitorDetails.detector_scale_x_object) || 1280 + const imgHeight = parseInt(monitorDetails.detector_scale_y_object) || 720 + const objectMovePercent = parseInt(monitorDetails.detector_object_move_percent) || 5 + const groupKey = monitorConfig.ke + const monitorId = monitorConfig.mid + const trackerId = `${groupKey}${monitorId}` + const theTracker = objectTrackers[trackerId] + const lastPositions = theTracker.lastPositions + const sortedById = groupMatricesById([...lastPositions,...matrices]) + const movedMatrices = [] + for (const objectId in sortedById) { + const sortedList = sortedById[objectId] + if(sortedList[1]){ + const matrixHasMoved = objectHasMoved(sortedList,{ + threshold: objectMovePercent, + imgWidth: imgWidth, + imgHeight: imgHeight, + }); + if(matrixHasMoved){ + movedMatrices.push(sortedList[1]) + } + } + } + return movedMatrices + } + return { + trackObjectWithTimeout, + resetObjectTracker, + trackObject, + getTracked, + setLastTracked, + getAllMatricesThatMoved, + } +} diff --git a/libs/events/utils.js b/libs/events/utils.js index 679824b5..16994645 100644 --- a/libs/events/utils.js +++ b/libs/events/utils.js @@ -24,6 +24,12 @@ module.exports = (s,config,lang,app,io) => { cutVideoLength, reEncodeVideoAndBinOriginalAddToQueue } = require('../video/utils.js')(s,config,lang) + const { + getTracked, + setLastTracked, + trackObjectWithTimeout, + getAllMatricesThatMoved, + } = require('./tracking.js')(s,config,lang,app,io) const { isEven, fetchTimeout, @@ -97,7 +103,7 @@ module.exports = (s,config,lang,app,io) => { } return newString } - const isAtleastOneMatrixInRegion = function(regions,matrices,callback){ + const isAtleastOneMatrixInRegion = function(regions,matrices){ var regionPolys = [] var matrixPoints = [] regions.forEach(function(region,n){ @@ -108,46 +114,20 @@ module.exports = (s,config,lang,app,io) => { regionPolys[n] = new P(new V(0,0), polyPoints) }) var collisions = [] - var foundInRegion = false matrices.forEach(function(matrix){ var matrixPoly = new B(new V(matrix.x, matrix.y), matrix.width, matrix.height).toPolygon() + var foundInRegion = false regionPolys.forEach(function(region,n){ - var response = new SAT.Response() - var collided = SAT.testPolygonPolygon(matrixPoly, region, response) - if(collided === true){ - collisions.push({ - matrix: matrix, - region: regions[n] - }) - foundInRegion = true + if(!foundInRegion){ + var response = new SAT.Response() + var collided = SAT.testPolygonPolygon(matrixPoly, region, response) + if(collided === true){ + foundInRegion = true + collisions.push(matrix) + } } }) }) - if(callback)callback(foundInRegion,collisions) - return foundInRegion - } - const scanMatricesforCollisions = function(region,matrices){ - var matrixPoints = [] - var collisions = [] - if (!region || !matrices){ - if(callback)callback(collisions) - return collisions - } - var polyPoints = [] - region.points.forEach(function(point){ - polyPoints.push(new V(parseInt(point[0]),parseInt(point[1]))) - }) - var regionPoly = new P(new V(0,0), polyPoints) - matrices.forEach(function(matrix){ - if (matrix){ - var matrixPoly = new B(new V(matrix.x, matrix.y), matrix.width, matrix.height).toPolygon() - var response = new SAT.Response() - var collided = SAT.testPolygonPolygon(matrixPoly, regionPoly, response) - if(collided === true){ - collisions.push(matrix) - } - } - }) return collisions } const getLargestMatrix = (matrices) => { @@ -367,22 +347,6 @@ module.exports = (s,config,lang,app,io) => { } }) } - const checkForObjectsInRegions = (monitorConfig,eventDetails,filter,d,didCountingAlready) => { - const monitorDetails = monitorConfig.details - if(hasMatrices(eventDetails) && monitorDetails.detector_obj_region === '1'){ - var regions = s.group[monitorConfig.ke].activeMonitors[monitorConfig.mid].parsedObjects.cords - var isMatrixInRegions = isAtleastOneMatrixInRegion(regions,eventDetails.matrices) - if(isMatrixInRegions){ - s.debugLog('Matrix in region!') - if(filter.countObjects && monitorDetails.detector_obj_count === '1' && monitorDetails.detector_obj_count_in_region === '1' && !didCountingAlready){ - countObjects(d) - } - }else{ - return false - } - } - return true - } const runEventExecutions = async (eventTime,monitorConfig,eventDetails,forceSave,filter,d, triggerEvent) => { const monitorDetails = monitorConfig.details const detailString = JSON.stringify(eventDetails) @@ -674,6 +638,8 @@ module.exports = (s,config,lang,app,io) => { } const triggerEvent = async (d,forceSave) => { var didCountingAlready = false + const groupKey = d.ke + const monitorId = d.mid || d.id const filter = { halt : false, addToMotionCounter : true, @@ -698,7 +664,6 @@ module.exports = (s,config,lang,app,io) => { s.onEventTriggerBeforeFilterExtensions.forEach(function(extender){ extender(d,filter) }) - const eventDetails = d.details const passedEventFilters = checkEventFilters(d,activeMonitor.details,filter) if(!passedEventFilters)return; const eventTime = new Date() @@ -722,20 +687,37 @@ module.exports = (s,config,lang,app,io) => { ){ addToEventCounter(d) } + const eventDetails = d.details if( (filter.countObjects || monitorDetails.detector_obj_count === '1') && monitorDetails.detector_obj_count_in_region !== '1' ){ didCountingAlready = true - countObjects(d) + countObjects(eventDetails.matrices) } if(filter.useLock){ const passedMotionLock = checkMotionLock(d,monitorDetails) if(!passedMotionLock)return } - const passedObjectInRegionCheck = checkForObjectsInRegions(monitorConfig,eventDetails,filter,d,didCountingAlready) - if(!passedObjectInRegionCheck)return - + const thisHasMatrices = hasMatrices(eventDetails) + if(thisHasMatrices && monitorDetails.detector_obj_region === '1'){ + var regions = s.group[monitorConfig.ke].activeMonitors[monitorConfig.mid].parsedObjects.cordsForObjectDetection + var matricesInRegions = isAtleastOneMatrixInRegion(regions,eventDetails.matrices) + eventDetails.matrices = matricesInRegions + if(matricesInRegions.length === 0)return; + if(filter.countObjects && monitorDetails.detector_obj_count === '1' && monitorDetails.detector_obj_count_in_region === '1' && !didCountingAlready){ + countObjects(eventDetails.matrices) + } + } + if(thisHasMatrices && monitorDetails.detector_object_ignore_not_move === '1'){ + const trackerId = `${groupKey}${monitorId}` + trackObjectWithTimeout(trackerId,eventDetails.matrices) + const trackedObjects = getTracked(trackerId) + const objectsThatMoved = getAllMatricesThatMoved(monitorConfig,trackedObjects) + setLastTracked(trackerId, trackedObjects) + if(objectsThatMoved.length === 0)return; + eventDetails.matrices = objectsThatMoved + } // d.doObjectDetection = ( eventDetails.reason !== 'object' && @@ -762,10 +744,34 @@ module.exports = (s,config,lang,app,io) => { doObjectDetection: d.doObjectDetection },`DETECTOR_${monitorConfig.ke}${monitorConfig.mid}`); } + function convertRegionPointsToNewDimensions(regions, options) { + const { fromWidth, fromHeight, toWidth, toHeight } = options; + + // Compute the conversion factors for x and y coordinates + const xFactor = toWidth / fromWidth; + const yFactor = toHeight / fromHeight; + + // Clone the regions array and update the points for each region + const newRegions = regions.map(region => { + const { points } = region; + + // Clone the points array and update the coordinates + const newPoints = points.map(([x, y]) => { + const newX = Math.round(x * xFactor); + const newY = Math.round(y * yFactor); + return [newX.toString(), newY.toString()]; + }); + + // Clone the region object and update the points + return { ...region, points: newPoints }; + }); + + return newRegions; + } return { countObjects: countObjects, - isAtleastOneMatrixInRegion: isAtleastOneMatrixInRegion, - scanMatricesforCollisions: scanMatricesforCollisions, + isAtleastOneMatrixInRegion, + convertRegionPointsToNewDimensions, getLargestMatrix: getLargestMatrix, addToEventCounter: addToEventCounter, clearEventCounter: clearEventCounter, @@ -774,7 +780,6 @@ module.exports = (s,config,lang,app,io) => { checkEventFilters: checkEventFilters, checkMotionLock: checkMotionLock, runMultiEventBasedRecord: runMultiEventBasedRecord, - checkForObjectsInRegions: checkForObjectsInRegions, runEventExecutions: runEventExecutions, createEventBasedRecording: createEventBasedRecording, closeEventBasedRecording: closeEventBasedRecording, diff --git a/libs/monitor/utils.js b/libs/monitor/utils.js index 1b3147b7..01ce2840 100644 --- a/libs/monitor/utils.js +++ b/libs/monitor/utils.js @@ -26,6 +26,7 @@ module.exports = (s,config,lang) => { const { addEventDetailsToString, closeEventBasedRecording, + convertRegionPointsToNewDimensions, triggerEvent, } = require('../events/utils.js')(s,config,lang) const { @@ -49,7 +50,7 @@ module.exports = (s,config,lang) => { const getUpdateableFields = require('./updatedFields.js') const processKill = (proc) => { const response = {ok: true} - const processPID = parseInt(`${proc.pid}`) + const processPID = proc && proc.pid ? parseInt(`${proc.pid}`) : null return new Promise((resolve,reject) => { let alreadyResolved = false function doResolve(response){ @@ -1703,7 +1704,18 @@ module.exports = (s,config,lang) => { activeMonitor.details = e.details switch(v){ case'cords': - activeMonitor.parsedObjects[v] = Object.values(s.parseJSON(e.details[v])) + const fromWidth = parseInt(e.details.detector_scale_x) || 640 + const fromHeight = parseInt(e.details.detector_scale_y) || 480 + const toWidth = parseInt(e.details.detector_scale_x_object) || 1280 + const toHeight = parseInt(e.details.detector_scale_y_object) || 720 + const theCords = Object.values(s.parseJSON(e.details[v])) || []; + activeMonitor.parsedObjects.cordsForObjectDetection = convertRegionPointsToNewDimensions(theCords,{ + fromWidth, + fromHeight, + toWidth, + toHeight, + }); + activeMonitor.parsedObjects.cords = theCords break; default: activeMonitor.parsedObjects[v] = s.parseJSON(e.details[v]) diff --git a/libs/uploaders/s3based.js b/libs/uploaders/s3based.js index e15bc8e2..c800bd7a 100644 --- a/libs/uploaders/s3based.js +++ b/libs/uploaders/s3based.js @@ -331,16 +331,16 @@ module.exports = function(s,config,lang){ "value": "us-west-1" }, { - "name": "US West 2)", + "name": "US West 2", "value": "us-west-2" }, { "name": "US East 1", - "value": "us-east-2" + "value": "us-east-1" }, { "name": "US East 2", - "value": "us-east-1" + "value": "us-east-2" }, { "name": "Asia Pacific 1", diff --git a/libs/webServerStreamPaths.js b/libs/webServerStreamPaths.js index 6750f54c..cd2a85f5 100644 --- a/libs/webServerStreamPaths.js +++ b/libs/webServerStreamPaths.js @@ -186,6 +186,10 @@ module.exports = function(s,config,lang,app){ res.write("\r\n"); res.write(content,'binary'); res.write("\r\n"); + if(!Emitter){ + res.end(); + return; + } var ip = s.getClientIp(req) s.camera('watch_on',{ id : req.params.id, diff --git a/package-lock.json b/package-lock.json index bf2b493b..5e9c67d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.226.0", "async": "^3.2.2", - "backblaze-b2": "^0.9.12", + "backblaze-b2": "^1.7.0", "body-parser": "^1.19.0", "bson": "^4.6.1", "connection-tester": "^0.2.0", @@ -35,6 +35,7 @@ "mysql": "^2.18.1", "node-abort-controller": "^3.0.1", "node-fetch": "^2.6.7", + "shinobi-node-moving-things-tracker": "^0.9.1", "node-onvif-events": "^2.0.5", "node-ssh": "^12.0.4", "node-telegram-bot-api": "^0.58.0", @@ -1384,6 +1385,17 @@ "node": ">=14.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", + "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@discordjs/collection": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.1.6.tgz", @@ -1694,15 +1706,34 @@ "follow-redirects": "^1.14.4" } }, - "node_modules/backblaze-b2": { - "version": "0.9.12", - "resolved": "https://registry.npmjs.org/backblaze-b2/-/backblaze-b2-0.9.12.tgz", - "integrity": "sha1-b0IHJs7G0L787ohqS7bfUOnAGco=", + "node_modules/axios-retry": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-3.4.0.tgz", + "integrity": "sha512-VdgaP+gHH4iQYCCNUWF2pcqeciVOdGrBBAYUfTY+wPcO5Ltvp/37MLFNCmJKo7Gj3SHvCSdL8ouI1qLYJN3liA==", "dependencies": { - "node-sha1": "^1.0.1", - "q": "^1.4.1", - "request": "^2.67.0", - "request-progress": "^3.0.0" + "@babel/runtime": "^7.15.4", + "is-retry-allowed": "^2.2.0" + } + }, + "node_modules/backblaze-b2": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/backblaze-b2/-/backblaze-b2-1.7.0.tgz", + "integrity": "sha512-8cVsKkXspuM1UeLI8WWSWw2JHfB7/IvqTtzvwhHqqhNyqcYl8iZ2lFpeuXGKcFA1TiSRlgALXWFJ9eKG6+3ZPg==", + "dependencies": { + "axios": "^0.21.1", + "axios-retry": "^3.1.9", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/backblaze-b2/node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dependencies": { + "follow-redirects": "^1.14.0" } }, "node_modules/backoff": { @@ -4365,6 +4396,17 @@ "node": ">=0.10.0" } }, + "node_modules/is-retry-allowed": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", + "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-shared-array-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", @@ -4735,6 +4777,11 @@ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -5035,6 +5082,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "node_modules/munkres-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/munkres-js/-/munkres-js-1.2.2.tgz", + "integrity": "sha512-0oF4tBDvzx20CYzQ44tTJMfwTBJWXe7cE73Sa/u7Mz7X8jRtyOXOGE9kJBhCfX7Akku3Iy/WHa0sRgqLRq2xaQ==" + }, "node_modules/mv": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", @@ -5174,6 +5226,20 @@ "node": ">= 6.13.0" } }, + "node_modules/node-moving-things-tracker": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/node-moving-things-tracker/-/node-moving-things-tracker-0.9.1.tgz", + "integrity": "sha512-JVa+DbQRgOsOcfIIxhw3kTUfO407RdXnVqNgPvAlhUHu7PWziUah+MuTcaN4rRktBicj/l2scI64a2crBgrzKw==", + "dependencies": { + "lodash.isequal": "^4.5.0", + "minimist": "^1.2.0", + "munkres-js": "^1.2.2", + "uuid": "^3.2.1" + }, + "bin": { + "shinobi-node-moving-things-tracker": "main.js" + } + }, "node_modules/node-onvif-events": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/node-onvif-events/-/node-onvif-events-2.0.5.tgz", @@ -5182,11 +5248,6 @@ "onvif": "git+https://github.com/agsh/onvif.git" } }, - "node_modules/node-sha1": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/node-sha1/-/node-sha1-1.0.1.tgz", - "integrity": "sha1-Mu2EfYUTFXuW3sa3noxHvK63jhw=" - }, "node_modules/node-ssh": { "version": "12.0.4", "resolved": "https://registry.npmjs.org/node-ssh/-/node-ssh-12.0.4.tgz", @@ -5839,15 +5900,6 @@ "node": "*" } }, - "node_modules/q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", - "engines": { - "node": ">=0.6.0", - "teleport": ">=0.2.0" - } - }, "node_modules/qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", @@ -5907,6 +5959,11 @@ "node": ">= 0.10" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, "node_modules/regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", @@ -5976,14 +6033,6 @@ "node": ">= 6" } }, - "node_modules/request-progress": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", - "integrity": "sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=", - "dependencies": { - "throttleit": "^1.0.0" - } - }, "node_modules/request-promise": { "version": "4.2.6", "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.6.tgz", @@ -6946,11 +6995,6 @@ "node": ">=8.0.0" } }, - "node_modules/throttleit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", - "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=" - }, "node_modules/tildify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", @@ -8846,6 +8890,14 @@ "tslib": "^2.3.1" } }, + "@babel/runtime": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", + "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, "@discordjs/collection": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.1.6.tgz", @@ -9078,15 +9130,33 @@ "follow-redirects": "^1.14.4" } }, - "backblaze-b2": { - "version": "0.9.12", - "resolved": "https://registry.npmjs.org/backblaze-b2/-/backblaze-b2-0.9.12.tgz", - "integrity": "sha1-b0IHJs7G0L787ohqS7bfUOnAGco=", + "axios-retry": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-3.4.0.tgz", + "integrity": "sha512-VdgaP+gHH4iQYCCNUWF2pcqeciVOdGrBBAYUfTY+wPcO5Ltvp/37MLFNCmJKo7Gj3SHvCSdL8ouI1qLYJN3liA==", "requires": { - "node-sha1": "^1.0.1", - "q": "^1.4.1", - "request": "^2.67.0", - "request-progress": "^3.0.0" + "@babel/runtime": "^7.15.4", + "is-retry-allowed": "^2.2.0" + } + }, + "backblaze-b2": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/backblaze-b2/-/backblaze-b2-1.7.0.tgz", + "integrity": "sha512-8cVsKkXspuM1UeLI8WWSWw2JHfB7/IvqTtzvwhHqqhNyqcYl8iZ2lFpeuXGKcFA1TiSRlgALXWFJ9eKG6+3ZPg==", + "requires": { + "axios": "^0.21.1", + "axios-retry": "^3.1.9", + "lodash": "^4.17.21" + }, + "dependencies": { + "axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "requires": { + "follow-redirects": "^1.14.0" + } + } } }, "backoff": { @@ -11097,6 +11167,11 @@ "is-unc-path": "^1.0.0" } }, + "is-retry-allowed": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", + "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==" + }, "is-shared-array-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", @@ -11373,6 +11448,11 @@ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -11588,6 +11668,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "munkres-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/munkres-js/-/munkres-js-1.2.2.tgz", + "integrity": "sha512-0oF4tBDvzx20CYzQ44tTJMfwTBJWXe7cE73Sa/u7Mz7X8jRtyOXOGE9kJBhCfX7Akku3Iy/WHa0sRgqLRq2xaQ==" + }, "mv": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", @@ -11697,6 +11782,17 @@ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" }, + "shinobi-node-moving-things-tracker": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/node-moving-things-tracker/-/node-moving-things-tracker-0.9.1.tgz", + "integrity": "sha512-JVa+DbQRgOsOcfIIxhw3kTUfO407RdXnVqNgPvAlhUHu7PWziUah+MuTcaN4rRktBicj/l2scI64a2crBgrzKw==", + "requires": { + "lodash.isequal": "^4.5.0", + "minimist": "^1.2.0", + "munkres-js": "^1.2.2", + "uuid": "^3.2.1" + } + }, "node-onvif-events": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/node-onvif-events/-/node-onvif-events-2.0.5.tgz", @@ -11705,11 +11801,6 @@ "onvif": "git+https://github.com/agsh/onvif.git" } }, - "node-sha1": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/node-sha1/-/node-sha1-1.0.1.tgz", - "integrity": "sha1-Mu2EfYUTFXuW3sa3noxHvK63jhw=" - }, "node-ssh": { "version": "12.0.4", "resolved": "https://registry.npmjs.org/node-ssh/-/node-ssh-12.0.4.tgz", @@ -12205,11 +12296,6 @@ "resolved": "https://registry.npmjs.org/pushover-notifications/-/pushover-notifications-1.2.2.tgz", "integrity": "sha512-+3Xcj+kiMiouZK1Ws8yGBTyl8WMPZZdELgl/iVxYqNwDdlaObBHMhEGPRC6Zb9t0BE27ikOoOqSIO1cKZOtsDA==" }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" - }, "qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", @@ -12254,6 +12340,11 @@ "resolve": "^1.1.6" } }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, "regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", @@ -12327,14 +12418,6 @@ } } }, - "request-progress": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", - "integrity": "sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=", - "requires": { - "throttleit": "^1.0.0" - } - }, "request-promise": { "version": "4.2.6", "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.6.tgz", @@ -13071,11 +13154,6 @@ "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.1.tgz", "integrity": "sha512-6usSlV9KyHsspvwu2duKH+FMUhqJnAh6J5J/4MITl8s94iSUQTLkJggdiewKv4RyARQccnigV48Z+khiuVZDJw==" }, - "throttleit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", - "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=" - }, "tildify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", diff --git a/package.json b/package.json index 9c654a03..1358974a 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.226.0", "async": "^3.2.2", - "backblaze-b2": "^0.9.12", + "backblaze-b2": "^1.7.0", "body-parser": "^1.19.0", "bson": "^4.6.1", "connection-tester": "^0.2.0", @@ -41,6 +41,7 @@ "mysql": "^2.18.1", "node-abort-controller": "^3.0.1", "node-fetch": "^2.6.7", + "shinobi-node-moving-things-tracker": "^0.9.1", "node-onvif-events": "^2.0.5", "node-ssh": "^12.0.4", "node-telegram-bot-api": "^0.58.0", diff --git a/tools/installAllCustomAutoLoadPackages.js b/tools/installAllCustomAutoLoadPackages.js new file mode 100644 index 00000000..1c1bbb90 --- /dev/null +++ b/tools/installAllCustomAutoLoadPackages.js @@ -0,0 +1,46 @@ +const fsOld = require('fs') +const fs = fsOld.promises +const spawn = require('child_process').spawn +const configLocation = process.argv[2] ? process.argv[2] : `${__dirname}/../conf.json` +const config = require(configLocation) +const customAutoLoadPath = `${__dirname}/../libs/customAutoLoad/` + +async function getAllCustomAutoLoadModuleFolders(){ + return (await fs.readdir(customAutoLoadPath)).filter((filename) => { + return fsOld.lstatSync(`${customAutoLoadPath}${filename}`).isDirectory() + }) +} +function installModule(filename){ + return new Promise((resolve) => { + console.log(`Installing Module : ${filename}`) + const folderPath = `${customAutoLoadPath}${filename}` + const tempSh = `${folderPath}/tempSh.sh` + fsOld.writeFileSync(tempSh,`cd "${folderPath}" && npm install`) + const installProcess = spawn('sh',[tempSh]) + installProcess.stdout.on('data',function(data){ + const text = data.toString() + console.log(text) + }) + installProcess.stderr.on('data',function(data){ + const text = data.toString() + console.error(text) + }) + installProcess.on('close',function(){ + resolve() + fs.rm(tempSh) + }) + }) +} +async function run(){ + const folderList = await getAllCustomAutoLoadModuleFolders() + for (let i = 0; i < folderList.length; i++) { + const folderName = folderList[i] + await installModule(folderName) + } +} +run().then(() => { + console.log('Done!') +}) +.catch((err) => { + console.error('RUN ERROR',err) +}) diff --git a/web/assets/js/bs5.accountSettings.js b/web/assets/js/bs5.accountSettings.js index 0afc888c..5d0e601d 100644 --- a/web/assets/js/bs5.accountSettings.js +++ b/web/assets/js/bs5.accountSettings.js @@ -7,7 +7,7 @@ $(document).ready(function(){ var addStorageMaxAmountsField = theForm.find('[detail="addStorage"]') function drawAddStorageFields(){ try{ - var addStorageData = JSON.parse($user.details.addStorage || '{}') + var addStorageData = safeJsonParse($user.details.addStorage || '{}') var html = '' $.each(addStorage,function(n,storage){ var theStorage = addStorageData[storage.path] @@ -62,7 +62,6 @@ $(document).ready(function(){ }) return json } - $('body') theForm.find('[detail]').change(onDetailFieldChange) theForm.find('[detail]').change(function(){ onDetailFieldChange(this) diff --git a/web/assets/js/bs5.liveGrid.js b/web/assets/js/bs5.liveGrid.js index 0b478c9f..11f91d58 100644 --- a/web/assets/js/bs5.liveGrid.js +++ b/web/assets/js/bs5.liveGrid.js @@ -6,6 +6,8 @@ var liveGrid = $('#monitors_live') var liveGridData = null var liveGridOpenCountElements = $('.liveGridOpenCount') var liveGridOpenCount = 0 +var liveGridPauseScrollTimeout = null; +var liveGridPlayingNow = {}; // var onLiveStreamInitiateExtensions = [] function onLiveStreamInitiate(callback){ @@ -337,222 +339,225 @@ function drawLiveGridBlock(monitorConfig,subStreamChannel){ } } function initiateLiveGridPlayer(monitor,subStreamChannel){ - var livePlayerElement = loadedLiveGrids[monitor.mid] + var monitorId = monitor.mid var details = monitor.details var groupKey = monitor.ke var monitorId = monitor.mid + var livePlayerBlocks = liveGridElements[monitorId] + var monitorItem = livePlayerBlocks.monitorItem var loadedMonitor = loadedMonitors[monitorId] - var loadedPlayer = loadedLiveGrids[monitor.mid] + var loadedPlayer = loadedLiveGrids[monitorId] var websocketPath = checkCorrectPathEnding(location.pathname) + 'socket.io' var containerElement = $(`#monitor_live_${monitor.mid}`) var streamType = subStreamChannel ? details.substream ? details.substream.output.stream_type : 'hls' : details.stream_type - if(location.search === '?p2p=1'){ - websocketPath = '/socket.io' - // websocketQuery.machineId = machineId + var isInView = isScrolledIntoView(monitorItem[0]) + if(!isInView){ + return; } + liveGridPlayingNow[monitorId] = true switch(streamType){ - case'jpeg': - startJpegStream(monitorId) - break; - case'b64': - if(loadedPlayer.Base64 && loadedPlayer.Base64.connected){ - loadedPlayer.Base64.disconnect() - } - loadedPlayer.Base64 = io(location.origin,{ path: websocketPath, query: websocketQuery, transports: ['websocket'], forceNew: false}) - var ws = loadedPlayer.Base64 - var buffer - ws.on('diconnect',function(){ - console.log('Base64 Stream Disconnected') - }) - ws.on('connect',function(){ - ws.emit('Base64',{ - auth: $user.auth_token, - uid: $user.uid, - ke: monitor.ke, - id: monitor.mid, - channel: subStreamChannel + case'jpeg': + startJpegStream(monitorId) + break; + case'b64': + if(loadedPlayer.Base64 && loadedPlayer.Base64.connected){ + loadedPlayer.Base64.disconnect() + } + loadedPlayer.Base64 = io(location.origin,{ path: websocketPath, query: websocketQuery, transports: ['websocket'], forceNew: false}) + var ws = loadedPlayer.Base64 + var buffer + ws.on('diconnect',function(){ + console.log('Base64 Stream Disconnected') }) - if(!loadedPlayer.ctx || loadedPlayer.ctx.length === 0){ - loadedPlayer.ctx = containerElement.find('canvas'); - } - var ctx = loadedPlayer.ctx[0] - var ctx2d = ctx.getContext("2d") - loadedPlayer.image = new Image() - var image = loadedPlayer.image - image.onload = function() { - loadedPlayer.imageLoading = false - var x = 0 - var y = 0 - ctx.getContext("2d").drawImage(image,x,y,ctx.width,ctx.height) - URL.revokeObjectURL(loadedPlayer.imageUrl) - } - ws.on('data',function(imageData){ - try{ - if(loadedPlayer.imageLoading === true)return console.log('drop'); - loadedPlayer.imageLoading = true - var arrayBufferView = new Uint8Array(imageData); - var blob = new Blob( [ arrayBufferView ], { type: "image/jpeg" } ); - loadedPlayer.imageUrl = URL.createObjectURL( blob ); - loadedPlayer.image.src = loadedPlayer.imageUrl - loadedPlayer.last_frame = 'data:image/jpeg;base64,'+base64ArrayBuffer(imageData) - }catch(er){ - debugLog('base64 frame') + ws.on('connect',function(){ + ws.emit('Base64',{ + auth: $user.auth_token, + uid: $user.uid, + ke: monitor.ke, + id: monitor.mid, + channel: subStreamChannel + }) + if(!loadedPlayer.ctx || loadedPlayer.ctx.length === 0){ + loadedPlayer.ctx = containerElement.find('canvas'); } - // $.ccio.init('signal',d); + var ctx = loadedPlayer.ctx[0] + var ctx2d = ctx.getContext("2d") + loadedPlayer.image = new Image() + var image = loadedPlayer.image + image.onload = function() { + loadedPlayer.imageLoading = false + var x = 0 + var y = 0 + ctx.getContext("2d").drawImage(image,x,y,ctx.width,ctx.height) + URL.revokeObjectURL(loadedPlayer.imageUrl) + } + ws.on('data',function(imageData){ + try{ + if(loadedPlayer.imageLoading === true)return console.log('drop'); + loadedPlayer.imageLoading = true + var arrayBufferView = new Uint8Array(imageData); + var blob = new Blob( [ arrayBufferView ], { type: "image/jpeg" } ); + loadedPlayer.imageUrl = URL.createObjectURL( blob ); + loadedPlayer.image.src = loadedPlayer.imageUrl + loadedPlayer.last_frame = 'data:image/jpeg;base64,'+base64ArrayBuffer(imageData) + }catch(er){ + debugLog('base64 frame') + } + // $.ccio.init('signal',d); + }) }) - }) - break; - case'mp4': - setTimeout(function(){ - var stream = containerElement.find('.stream-element'); - var onPoseidonError = function(){ - // setTimeout(function(){ - // mainSocket.f({f:'monitor',ff:'watch_on',id:monitorId}) - // },2000) - } - if(!loadedPlayer.PoseidonErrorCount)loadedPlayer.PoseidonErrorCount = 0 - if(loadedPlayer.PoseidonErrorCount >= 5)return - if(subStreamChannel ? details.substream.output.stream_flv_type === 'ws' : monitor.details.stream_flv_type === 'ws'){ - if(loadedPlayer.Poseidon){ - loadedPlayer.Poseidon.stop() + break; + case'mp4': + setTimeout(function(){ + var stream = containerElement.find('.stream-element'); + var onPoseidonError = function(){ + // setTimeout(function(){ + // mainSocket.f({f:'monitor',ff:'watch_on',id:monitorId}) + // },2000) + } + if(!loadedPlayer.PoseidonErrorCount)loadedPlayer.PoseidonErrorCount = 0 + if(loadedPlayer.PoseidonErrorCount >= 5)return + 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; + case'flv': + if (flvjs.isSupported()) { + if(loadedPlayer.flv){ + loadedPlayer.flv.destroy() revokeVideoPlayerUrl(monitorId) } - try{ - loadedPlayer.Poseidon = new Poseidon({ - video: stream[0], + var options = {}; + if(monitor.details.stream_flv_type==='ws'){ + if(monitor.details.stream_flv_maxLatency&&monitor.details.stream_flv_maxLatency!==''){ + monitor.details.stream_flv_maxLatency = parseInt(monitor.details.stream_flv_maxLatency) + }else{ + monitor.details.stream_flv_maxLatency = 20000; + } + options = { + type: 'flv', + isLive: true, auth_token: $user.auth_token, ke: monitor.ke, uid: $user.uid, id: monitor.mid, + maxLatency: monitor.details.stream_flv_maxLatency, + hasAudio:false, 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; - case'flv': - if (flvjs.isSupported()) { - if(loadedPlayer.flv){ - loadedPlayer.flv.destroy() - revokeVideoPlayerUrl(monitorId) - } - var options = {}; - if(monitor.details.stream_flv_type==='ws'){ - if(monitor.details.stream_flv_maxLatency&&monitor.details.stream_flv_maxLatency!==''){ - monitor.details.stream_flv_maxLatency = parseInt(monitor.details.stream_flv_maxLatency) + channel : subStreamChannel, + query: websocketQuery + } }else{ - monitor.details.stream_flv_maxLatency = 20000; - } - options = { - type: 'flv', - isLive: true, - auth_token: $user.auth_token, - ke: monitor.ke, - uid: $user.uid, - id: monitor.mid, - maxLatency: monitor.details.stream_flv_maxLatency, - hasAudio:false, - url: location.origin, - path: websocketPath, - channel : subStreamChannel, - query: websocketQuery - } - }else{ - options = { - type: 'flv', - isLive: true, - url: getApiPrefix(`flv`)+'/'+monitor.mid + (subStreamChannel ? `/${subStreamChannel}` : '')+'/s.flv' - } - } - loadedPlayer.flv = flvjs.createPlayer(options); - loadedPlayer.flv.attachMediaElement(containerElement.find('.stream-element')[0]); - loadedPlayer.flv.on('error',function(err){ - console.log(err) - }); - loadedPlayer.flv.load(); - loadedPlayer.flv.play(); - }else{ - new PNotify({title:'Stream cannot be started',text:'FLV.js is not supported on this browser. Try another stream type.',type:'error'}); - } - break; - case'hls': - function createSteamNow(){ - clearTimeout(loadedPlayer.m3uCheck) - var url = getApiPrefix(`hls`) + '/' + monitor.mid + (subStreamChannel ? `/${subStreamChannel}` : '') + '/s.m3u8' - $.get(url,function(m3u){ - if(m3u == 'File Not Found'){ - loadedPlayer.m3uCheck = setTimeout(function(){ - createSteamNow() - },2000) - }else{ - var video = containerElement.find('.stream-element')[0] - if (isAppleDevice) { - video.src = url; - video.addEventListener('loadedmetadata', function() { - setTimeout(function(){ - video.play(); - },3000) - }, false); - }else{ - var hlsOptions = safeJsonParse(dashboardOptions().hlsOptions) || {} - if(hlsOptions instanceof String){ - hlsOptions = {} - new PNotify({ - title: lang['Invalid JSON'], - text: lang.hlsOptionsInvalid, - type: `warning`, - }) - } - if(loadedPlayer.hls){ - loadedPlayer.hls.destroy() - revokeVideoPlayerUrl(monitorId) - } - loadedPlayer.hls = new Hls(hlsOptions) - loadedPlayer.hls.loadSource(url) - loadedPlayer.hls.attachMedia(video) - loadedPlayer.hls.on(Hls.Events.MANIFEST_PARSED,function() { - if (video.paused) { - video.play(); - } - }); + options = { + type: 'flv', + isLive: true, + url: getApiPrefix(`flv`)+'/'+monitor.mid + (subStreamChannel ? `/${subStreamChannel}` : '')+'/s.flv' } } - }) - } - createSteamNow() - break; - case'mjpeg': - var liveStreamElement = containerElement.find('.stream-element') - var setSource = function(){ - liveStreamElement.attr('src',getApiPrefix(`mjpeg`)+'/'+monitorId + (subStreamChannel ? `/${subStreamChannel}` : '')) - liveStreamElement.unbind('ready') - liveStreamElement.ready(function(){ + loadedPlayer.flv = flvjs.createPlayer(options); + loadedPlayer.flv.attachMediaElement(containerElement.find('.stream-element')[0]); + loadedPlayer.flv.on('error',function(err){ + console.log(err) + }); + loadedPlayer.flv.load(); + loadedPlayer.flv.play(); + }else{ + new PNotify({title:'Stream cannot be started',text:'FLV.js is not supported on this browser. Try another stream type.',type:'error'}); + } + break; + case'hls': + function createSteamNow(){ + clearTimeout(loadedPlayer.m3uCheck) + var url = getApiPrefix(`hls`) + '/' + monitor.mid + (subStreamChannel ? `/${subStreamChannel}` : '') + '/s.m3u8' + $.get(url,function(m3u){ + if(m3u == 'File Not Found'){ + loadedPlayer.m3uCheck = setTimeout(function(){ + createSteamNow() + },2000) + }else{ + var video = containerElement.find('.stream-element')[0] + if (isAppleDevice) { + video.src = url; + video.addEventListener('loadedmetadata', function() { + setTimeout(function(){ + video.play(); + },3000) + }, false); + }else{ + var hlsOptions = safeJsonParse(dashboardOptions().hlsOptions) || {} + if(hlsOptions instanceof String){ + hlsOptions = {} + new PNotify({ + title: lang['Invalid JSON'], + text: lang.hlsOptionsInvalid, + type: `warning`, + }) + } + if(loadedPlayer.hls){ + loadedPlayer.hls.destroy() + revokeVideoPlayerUrl(monitorId) + } + loadedPlayer.hls = new Hls(hlsOptions) + loadedPlayer.hls.loadSource(url) + loadedPlayer.hls.attachMedia(video) + loadedPlayer.hls.on(Hls.Events.MANIFEST_PARSED,function() { + if (video.paused) { + video.play(); + } + }); + } + } + }) + } + createSteamNow() + break; + case'mjpeg': + var liveStreamElement = containerElement.find('.stream-element') + var setSource = function(){ + liveStreamElement.attr('src',getApiPrefix(`mjpeg`)+'/'+monitorId + (subStreamChannel ? `/${subStreamChannel}` : '')) + liveStreamElement.unbind('ready') + liveStreamElement.ready(function(){ + setTimeout(function(){ + liveStreamElement.contents().find("body").append('') + },1000) + }) + } + setSource() + liveStreamElement.on('error',function(err){ setTimeout(function(){ - liveStreamElement.contents().find("body").append('') - },1000) + setSource() + },4000) }) - } - setSource() - liveStreamElement.on('error',function(err){ - setTimeout(function(){ - setSource() - },4000) - }) - break; - } + break; + } $.each(onLiveStreamInitiateExtensions,function(n,extender){ extender(streamType,monitor,loadedPlayer,subStreamChannel) }) @@ -662,13 +667,15 @@ function loadPreviouslyOpenedLiveGridBlocks(){ }) } function closeAllLiveGridPlayers(rememberClose){ - var watchedOn = dashboardOptions().watch_on || {} - $.each(watchedOn,function(n,groupOfMons){ - $.each(groupOfMons,function(monitorId,monitor){ - if(monitor === 1){ - mainSocket.f({f:'monitor',ff:'watch_off',id: monitorId}) - } + $.each(loadedMonitors,function(monitorId,monitor){ + mainSocket.f({ + f: 'monitor', + ff: 'watch_off', + id: monitor.mid }) + setTimeout(function(){ + saveLiveGridBlockOpenState(monitorId,$user.ke,0) + },1000) }) } function saveLiveGridBlockOpenState(monitorId,groupKey,state){ @@ -870,6 +877,70 @@ function signalCheckLiveStream(options){ delete(liveGridData.signal); } } + +function pauseMonitorItem(monitorId){ + liveGridPlayingNow[monitorId] = false + closeLiveGridPlayer(monitorId,false) +} +function resumeMonitorItem(monitorId){ + // needs to know about substream + liveGridPlayingNow[monitorId] = true + resetMonitorCanvas(monitorId,true,null) +} +function isScrolledIntoView(elem){ + var el = $(elem) + var theWindow = $(window) + var docViewTop = theWindow.scrollTop(); + var docViewBottom = docViewTop + theWindow.height(); + + var elemTop = el.offset().top; + var elemBottom = elemTop + el.height(); + + return ( + elemTop >= docViewTop && elemTop <= docViewBottom || + elemBottom >= docViewTop && elemBottom <= docViewBottom + ); +} +function pauseAllLiveGridPlayers(unpause){ + $('.monitor_item').each(function(n,el){ + var monitorId = $(el).attr('data-mid') + if(!unpause){ + pauseMonitorItem(monitorId) + }else{ + resumeMonitorItem(monitorId) + } + }) +} +function setPauseStatusForMonitorItems(){ + $('.monitor_item').each(function(n,el){ + var monitorId = $(el).attr('data-mid') + var isVisible = isScrolledIntoView(el) + console.log(monitorId,isVisible) + if(isVisible){ + if(!liveGridPlayingNow[monitorId])resumeMonitorItem(monitorId); + }else{ + pauseMonitorItem(monitorId) + } + }) +} +function setPauseScrollTimeout(){ + clearTimeout(liveGridPauseScrollTimeout) + if(tabTree.name === 'liveGrid'){ + liveGridPauseScrollTimeout = setTimeout(function(){ + setPauseStatusForMonitorItems() + },700) + } +} +function openAllLiveGridPlayers(){ + $.each(loadedMonitors,function(monitorId,monitor){ + mainSocket.f({ + f: 'monitor', + ff: 'watch_on', + id: monitor.mid + }) + openLiveGrid() + }) +} $(document).ready(function(e){ liveGrid .on('dblclick','.stream-block',function(){ @@ -882,16 +953,16 @@ $(document).ready(function(e){ }) .on('click','.launch-live-grid-monitor',function(){ var monitorId = $(this).parents('[data-mid]').attr('data-mid') - // if(isMobile){ - // createLivePlayerTab(loadedMonitors[monitorId]) - // }else{ + if(isMobile){ + createLivePlayerTab(loadedMonitors[monitorId]) + }else{ mainSocket.f({ f: 'monitor', ff: 'watch_on', id: monitorId }) openLiveGrid() - // } + } }) .on('click','.monitor-live-group-open',function(){ var monitorIds = $(this).attr('monitor-ids').split(',') @@ -1012,26 +1083,10 @@ $(document).ready(function(e){ }); }) $('.open-all-monitors').click(function(){ - $.each(loadedMonitors,function(monitorId,monitor){ - mainSocket.f({ - f: 'monitor', - ff: 'watch_on', - id: monitor.mid - }) - openLiveGrid() - }) + openAllLiveGridPlayers() }) $('.close-all-monitors').click(function(){ - $.each(loadedMonitors,function(monitorId,monitor){ - mainSocket.f({ - f: 'monitor', - ff: 'watch_off', - id: monitor.mid - }) - setTimeout(function(){ - saveLiveGridBlockOpenState(monitorId,$user.ke,0) - },1000) - }) + closeAllLiveGridPlayers() }); var dontShowDetectionSelectionOnStart = dashboardOptions().dontShowDetection != 1 liveGridData = GridStack.init(); @@ -1042,14 +1097,14 @@ $(document).ready(function(e){ },700) }) .on('resizestop', function(){ - // resetAllLiveGridDimensionsInMemory() + resetAllLiveGridDimensionsInMemory() saveLiveGridBlockPositions() }); addOnTabReopen('liveGrid', function () { - loadPreviouslyOpenedLiveGridBlocks() + pauseAllLiveGridPlayers(true) }) addOnTabAway('liveGrid', function () { - closeAllLiveGridPlayers() + pauseAllLiveGridPlayers(false) }) onInitWebsocket(function (d){ loadPreviouslyOpenedLiveGridBlocks() @@ -1059,9 +1114,6 @@ $(document).ready(function(e){ }) onWebSocketEvent(function (d){ switch(d.f){ - case'init_success': - // loadPreviouslyOpenedLiveGridBlocks() - break; case'video_build_success': d.status = 1 d.mid = d.id || d.mid @@ -1185,12 +1237,14 @@ $(document).ready(function(e){ }) $(window).focus(function(){ if(canBackgroundStream()){ - loadPreviouslyOpenedLiveGridBlocks() + pauseAllLiveGridPlayers(true) } }).blur(function(){ if(canBackgroundStream()){ - closeAllLiveGridPlayers() + pauseAllLiveGridPlayers(false) } + }).scroll(function(){ + setPauseScrollTimeout() }) dashboardSwitchCallbacks.monitorOrder = function(toggleState){ if(toggleState !== 1){ diff --git a/web/assets/js/bs5.monitorSettings.js b/web/assets/js/bs5.monitorSettings.js index 91bcab67..8ceb2315 100644 --- a/web/assets/js/bs5.monitorSettings.js +++ b/web/assets/js/bs5.monitorSettings.js @@ -618,7 +618,7 @@ function importIntoMonitorEditor(options){ input_map_choices = monitorDetails.input_map_choices; } $.each(input_map_choices,function(n,v){ - $.each(v,function(m,b){ + $.each(safeJsonParse(v),function(m,b){ var parent = $('[input-mapping="'+n+'"] .choices') drawInputMapSelectorHtml(b,parent) }) diff --git a/web/assets/js/bs5.monitorsUtils.js b/web/assets/js/bs5.monitorsUtils.js index 706a9e00..8c473a18 100644 --- a/web/assets/js/bs5.monitorsUtils.js +++ b/web/assets/js/bs5.monitorsUtils.js @@ -647,7 +647,7 @@ function drawMatrices(event,options){ var html = '' $.each(event.details.matrices,function(n,matrix){ html += `
` - if(matrix.tag)html += `${matrix.tag}` + if(matrix.tag)html += `${matrix.tag}${!isNaN(matrix.id) ? ` ${matrix.id}`: ''}` html += '
' }) theContainer.append(html) diff --git a/web/assets/js/bs5.powerVideo.js b/web/assets/js/bs5.powerVideo.js index c2d34829..71ee360f 100644 --- a/web/assets/js/bs5.powerVideo.js +++ b/web/assets/js/bs5.powerVideo.js @@ -322,7 +322,7 @@ $(document).ready(function(e){ var html = '' $.each(event.details.matrices,function(n,matrix){ html += `
` - if(matrix.tag)html += `${matrix.tag}` + if(matrix.tag)html += `${matrix.tag}${!isNaN(matrix.id) ? ` ${matrix.id}`: ''}` html += '
' }) streamObjectsContainer.append(html)