diff --git a/CHANGELOG.md b/CHANGELOG.md index f8e39211..a0acc5e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,18 @@ # Changelog All notable changes to this project will be documented in this file. -# Next +# v1.0.6 - 2022-01-14 + +## Other Changes +- Videoanalysis: Reduced dwell time from 120s to 90s +- Videoanalysis: Minor improvements +- Refactored log output for better understanding of the flow of events + +## Bugfixes +- Fixed an issue where prebuffering/videoanalysis didnt work on cameras with mapping video/audio +- Minor bugfixes + +# v1.0.5 - 2022-01-13 ## Other Changes - Reduced default videoanalysis sensitivity to 25 diff --git a/package-lock.json b/package-lock.json index 5ddacf9b..1cccbd7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "camera.ui", - "version": "1.0.5", + "version": "1.0.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "camera.ui", - "version": "1.0.5", + "version": "1.0.6", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 25ccffb5..3dbde1e7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "camera.ui", - "version": "1.0.5", + "version": "1.0.6", "description": "User Interface for RTSP capable cameras.", "author": "SeydX (https://github.com/SeydX/camera.ui)", "scripts": { diff --git a/src/controller/camera/services/prebuffer.service.js b/src/controller/camera/services/prebuffer.service.js index 30dd5a08..ad970756 100644 --- a/src/controller/camera/services/prebuffer.service.js +++ b/src/controller/camera/services/prebuffer.service.js @@ -231,17 +231,12 @@ class PrebufferService { } else { audioArguments.push('-bsf:a', 'aac_adtstoasc', '-acodec', 'copy'); } - - if (videoConfig.mapaudio) { - audioArguments.unshift('-map', videoConfig.mapaudio); - } } else { audioArguments.push('-an'); } let vcodec = 'copy'; - //todo change const probeTimedout = this.#mediaService.codecs.timedout; const videoCodecProbe = this.#mediaService.codecs.video[0]; const forcePrebuffering = this.#camera.forcePrebuffering; @@ -265,20 +260,12 @@ class PrebufferService { const videoArguments = ['-vcodec', vcodec]; - if (videoConfig.mapvideo) { - videoArguments.unshift('-map', videoConfig.mapvideo); - } - - /*if (videoConfig.videoFilter) { - videoArguments.push('-filter:v', videoConfig.videoFilter); - }*/ - this.parsers = { mp4: cameraUtils.createFragmentedMp4Parser(), mpegts: cameraUtils.createMpegTsParser(), }; - const session = await this.#startRebroadcastSession(ffmpegInput, videoArguments, audioArguments, { + const session = await this.#startRebroadcastSession(ffmpegInput, videoConfig, videoArguments, audioArguments, { parsers: this.parsers, }); @@ -470,7 +457,7 @@ class PrebufferService { }; } - async #startRebroadcastSession(ffmpegInput, videoArguments, audioArguments, options) { + async #startRebroadcastSession(ffmpegInput, videoConfig, videoArguments, audioArguments, options) { const events = new EventEmitter(); let isActive = true; @@ -540,6 +527,9 @@ class PrebufferService { } } + let audioMap = `${videoConfig.mapaudio ? videoConfig.mapaudio : '0:a'}?`; + let videoMap = `${videoConfig.mapvideo ? videoConfig.mapvideo : '0:v'}?`; + const arguments_ = [ ...ffmpegInput, ...videoArguments, @@ -547,9 +537,9 @@ class PrebufferService { '-f', 'tee', '-map', - '0:v?', + videoMap, '-map', - '0:a?', + audioMap, `${mp4Arguments}|${mpegtsArguments}`, ]; diff --git a/src/controller/camera/services/stream.service.js b/src/controller/camera/services/stream.service.js index 3d9226a7..efb1dca6 100644 --- a/src/controller/camera/services/stream.service.js +++ b/src/controller/camera/services/stream.service.js @@ -78,6 +78,8 @@ class StreamService { if (cameraSetting?.audio && this.#mediaService.codecs.audio.length > 0) { audioArguments.push('-acodec', 'mp2', '-ac', '1', '-b:a', '128k'); + } else { + audioArguments.push('-an'); } if (videoConfig.mapaudio && !prebuffer) { @@ -114,6 +116,7 @@ class StreamService { async start() { if (!this.streamSession) { + // eslint-disable-next-line no-unused-vars let { ffmpegInput, prebuffer, audioArguments, videoArguments, additionalFlags } = await this.configureStreamOptions(); @@ -163,9 +166,9 @@ class StreamService { this.streamSession = null; - if (!prebuffer) { + /*if (!prebuffer) { this.#sessionService.closeSession(); - } + }*/ }); } } diff --git a/src/controller/camera/services/videoanalysis.service.js b/src/controller/camera/services/videoanalysis.service.js index 0a3560f6..b1c0d2e3 100644 --- a/src/controller/camera/services/videoanalysis.service.js +++ b/src/controller/camera/services/videoanalysis.service.js @@ -19,12 +19,12 @@ const { log } = LoggerService; const isUINT = (value) => Number.isInteger(value) && value >= 0; -const FFMPEG_MODE = 'rgba'; // gray, rgba, rgb24 +const FFMPEG_MODE = 'rgb24'; // gray, rgba, rgb24 const FFMPEG_RESOLUTION = '640:360'; const FFMPEG_FPS = '2'; const DIFFERENCE = 9; //const GRAYSCALE = 'luminosity'; -const DWELL_TIME = 2 * 60 * 1000; +const DWELL_TIME = 90 * 1000; class VideoAnalysisService { #camera; @@ -137,18 +137,10 @@ class VideoAnalysisService { clearTimeout(this.watchdog); } - if (this.motionEventTimeout) { - clearTimeout(this.motionEventTimeout); - this.motionEventTimeout = null; - + if (this.motionEventTimeout || this.forceCloseTimeout) { this.#triggerMotion(false); } - if (this.forceCloseTimeout) { - clearTimeout(this.forceCloseTimeout); - this.forceCloseTimeout = null; - } - if (this.restartTimer) { clearTimeout(this.restartTimer); this.restartTimer = null; @@ -156,7 +148,7 @@ class VideoAnalysisService { this.videoanalysisSession.cp?.kill('SIGKILL'); this.videoanalysisSession.pamDiff?.resetCache(); - this.videoanalysisSession = undefined; + this.videoanalysisSession = null; } } @@ -191,7 +183,7 @@ class VideoAnalysisService { const videoArguments = ['-an', '-vcodec', 'pam']; if (!prebufferInput && videoConfig.mapvideo) { - videoArguments.push('-map', videoConfig.mapvideo); + videoArguments.unshift('-map', videoConfig.mapvideo); } const ffmpegArguments = [ @@ -223,7 +215,6 @@ class VideoAnalysisService { const p2p = new P2P(); const pamDiff = new PamDiff({ - //difference: settings?.videoanalysis?.difference || 9, //grayscale: GRAYSCALE, difference: DIFFERENCE, percent: 100 - (settings?.videoanalysis?.sensitivity || 25), @@ -257,25 +248,22 @@ class VideoAnalysisService { // eslint-disable-next-line no-unused-vars pamDiff.on('diff', async (data) => { if (!this.motionEventTimeout) { - if (this.forceCloseTimeout) { - clearTimeout(this.forceCloseTimeout); - this.forceCloseTimeout = null; - } - log.debug(`Motion detected via Videoanalysis: ${JSON.stringify(data.trigger[0])}`, this.cameraName); this.#triggerMotion(true); + // forceClose after 3min this.forceCloseTimeout = setTimeout(() => { - // forceClose after 3min this.#triggerMotion(false); }, 3 * 60 * 1000); } - clearTimeout(this.motionEventTimeout); + if (this.motionEventTimeout) { + clearTimeout(this.motionEventTimeout); + this.motionEventTimeout = null; + } this.motionEventTimeout = setTimeout(async () => { this.#triggerMotion(false); - this.motionEventTimeout = null; }, DWELL_TIME); }); @@ -326,12 +314,18 @@ class VideoAnalysisService { } async #triggerMotion(state) { - const result = await MotionController.handleMotion('motion', this.cameraName, state, 'videoanalysis', {}); - log.debug(`Received a new VIDEOANALYSIS message ${JSON.stringify(result)} (${this.cameraName})`); + await MotionController.handleMotion('motion', this.cameraName, state, 'videoanalysis'); + + if (!state) { + if (this.forceCloseTimeout) { + clearTimeout(this.forceCloseTimeout); + this.forceCloseTimeout = null; + } - if (!state && this.forceCloseTimeout) { - clearTimeout(this.forceCloseTimeout); - this.forceCloseTimeout = null; + if (this.motionEventTimeout) { + clearTimeout(this.motionEventTimeout); + this.motionEventTimeout = null; + } } } @@ -358,7 +352,8 @@ class VideoAnalysisService { } #createRegions(regions = [], sensitivity) { - const percent = 100 - (sensitivity >= 0 && sensitivity <= 100 ? sensitivity : 25); + const sens = sensitivity >= 0 && sensitivity <= 100 ? sensitivity : 25; + const percent = 100 - sens; const zones = regions ?.map((region, index) => { @@ -386,7 +381,7 @@ class VideoAnalysisService { }) .filter((zone) => zone?.polygon?.length > 2); - log.debug(`Videoanalysis: Currently active zones: ${JSON.stringify(zones)}`, this.cameraName); + log.debug(`Videoanalysis: Sensitivity: ${sens} - Active zones: ${JSON.stringify(zones)}`, this.cameraName); return zones; } diff --git a/src/controller/event/event.controller.js b/src/controller/event/event.controller.js index 53651c77..1f49fa50 100644 --- a/src/controller/event/event.controller.js +++ b/src/controller/event/event.controller.js @@ -13,7 +13,7 @@ const { Telegram } = require('../../common/telegram'); const { LoggerService } = require('../../services/logger/logger.service'); -const { CameraController } = require('../camera/camera.controller'); +//const { CameraController } = require('../camera/camera.controller'); const CamerasModel = require('../../api/components/cameras/cameras.model'); const NotificationsModel = require('../../api/components/notifications/notifications.model'); @@ -43,7 +43,7 @@ class EventController { // eslint-disable-next-line no-unused-vars static async handle(trigger, cameraName, active, fileBuffer, type) { if (active) { - const controller = CameraController.cameras.get(cameraName); + //const controller = CameraController.cameras.get(cameraName); try { let Camera, CameraSettings; @@ -265,9 +265,9 @@ class EventController { EventController.#movementHandler[cameraName] = false; - if (controller) { + /*if (controller) { controller.session.closeSession(); - } + }*/ } else { log.debug(`Skip event, motion state: ${active}`, cameraName); } diff --git a/src/controller/motion/motion.controller.js b/src/controller/motion/motion.controller.js index fac2b557..31695422 100644 --- a/src/controller/motion/motion.controller.js +++ b/src/controller/motion/motion.controller.js @@ -65,14 +65,7 @@ class MotionController { //used for external events this.triggerMotion = MotionController.triggerMotion = async (cameraName, state) => { - let result = { - error: true, - message: 'Custom event could not be handled', - }; - - result = await MotionController.handleMotion('custom', cameraName, state, 'extern', result); - - log.debug(`Received a new EXTERN message ${JSON.stringify(result)} (${cameraName})`); + await MotionController.handleMotion('custom', cameraName, state, 'extern'); }; this.httpServer = MotionController.httpServer; @@ -171,8 +164,6 @@ class MotionController { } } - log.debug(`Received a new HTTP message ${JSON.stringify(result)} (${cameraName})`); - response.writeHead(result.error ? 500 : 200); response.write(JSON.stringify(result)); response.end(); @@ -223,11 +214,6 @@ class MotionController { }); MotionController.mqttClient.on('message', async (topic, data) => { - let result = { - error: true, - message: `Malformed MQTT message ${data.toString()} (${topic})`, - }; - log.debug(`${data.toString()} (${topic})`, 'MQTT'); let cameraName; @@ -312,21 +298,16 @@ class MotionController { } } - result = - state !== undefined - ? await MotionController.handleMotion(triggerType, cameraName, state, 'mqtt', result) - : { - error: true, - message: `The incoming MQTT message (${data.toString()}) for the topic (${topic}) was not the same as set in config.json (${message.toString()}). Skip...`, - }; + if (state !== undefined) { + await MotionController.handleMotion(triggerType, cameraName, state, 'mqtt'); + } else { + log.warn( + `The incoming MQTT message (${data.toString()}) for the topic (${topic}) was not the same as set in config.json (${message.toString()}). Skip...` + ); + } } else { - result = { - error: true, - message: `Can not assign the MQTT topic (${topic}) to a camera!`, - }; + log.warn(`Can not assign the MQTT topic (${topic}) to a camera!`, 'MQTT'); } - - log.debug(`Received a new message ${JSON.stringify(result)} (${cameraName})`, 'MQTT'); }); MotionController.mqttClient.on('end', () => { @@ -388,8 +369,7 @@ class MotionController { const name = rcptTo.address.split('@')[0].replace(regex, ' '); log.debug(`Email received (${name}).`, 'SMTP'); - const result = await MotionController.handleMotion('motion', name, true, 'smtp', {}); - log.debug(`Received a new SMTP message ${JSON.stringify(result)} (${name})`); + await MotionController.handleMotion('motion', name, true, 'smtp'); } }, }); @@ -512,8 +492,7 @@ class MotionController { const name = pathSplit[0]; log.debug(`Receiving file. (${name}).`, 'FTP'); - const result = await MotionController.handleMotion('motion', name, true, 'ftp', {}); - log.debug(`Received a new FTP message ${JSON.stringify(result)} (${name})`); + await MotionController.handleMotion('motion', name, true, 'ftp'); } else { this.connection.reply(550, 'Permission denied.'); } @@ -609,7 +588,8 @@ class MotionController { return ConfigService.ui.cameras.find((camera) => camera && camera.name === cameraName); } - static async handleMotion(triggerType, cameraName, state, event, result) { + static async handleMotion(triggerType, cameraName, state, event, result = {}) { + // result = {} is used as http response const camera = MotionController.#getCamera(cameraName); if (camera) { @@ -618,32 +598,37 @@ class MotionController { const cameraExcluded = (generalSettings?.exclude || []).includes(cameraName); if (atHome && !cameraExcluded) { + const message = `Skip motion trigger. At Home is active and ${cameraName} is not excluded!`; + + log.info(message, cameraName); + result = { error: false, - message: `Skip motion trigger. At Home is active and ${cameraName} is not excluded!`, + message: message, }; } else { - result = { - error: false, - message: 'Handled through extern controller', - }; - - //if handled through EXTERN controller, motionTimeout should also be handled through EXTERN controller MotionController.#controller.emit('motion', cameraName, triggerType, state, event); if (camera.recordOnMovement) { - //if handled through INTERN controller, motionTimeout should also be handled through INTERN controller - result.message = 'Handled through intern controller'; + const message = 'Handling through intern controller..'; + + log.debug(message, cameraName); - const timeout = MotionController.#motionTimers.get(camera.name); + result = { + error: false, + message: message, + }; + + const timeout = MotionController.#motionTimers.get(cameraName); const timeoutConfig = camera.motionTimeout >= 0 ? camera.motionTimeout : 1; if (timeout) { if (state) { + log.info('Skip motion event, motion timeout active!', cameraName); result.message += ' - Skip motion event, timeout active!'; } else { clearTimeout(timeout); - MotionController.#motionTimers.delete(camera.name); + MotionController.#motionTimers.delete(cameraName); MotionController.#controller.emit('uiMotion', { triggerType: triggerType, @@ -652,13 +637,13 @@ class MotionController { }); } } else { - if (state && timeoutConfig > 0) { + if (state) { const timer = setTimeout(() => { - log.info('Motion handler timeout. (ui)', camera.name); - MotionController.#motionTimers.delete(camera.name); + log.debug('Motion handler timeout. (ui)', cameraName); + MotionController.#motionTimers.delete(cameraName); }, timeoutConfig * 1000); - MotionController.#motionTimers.set(camera.name, timer); + MotionController.#motionTimers.set(cameraName, timer); } MotionController.#controller.emit('uiMotion', { @@ -667,12 +652,25 @@ class MotionController { state: state, }); } + } else { + const message = 'Handling through extern controller..'; + + log.debug(message, cameraName); + + result = { + error: false, + message: message, + }; } } } else { + const message = `Camera '${cameraName}' not found`; + + log.warn(message, cameraName); + result = { error: true, - message: `Camera '${cameraName}' not found`, + message: message, }; } diff --git a/ui/src/i18n/locale/en.json b/ui/src/i18n/locale/en.json index 0380c62a..93aed5a2 100644 --- a/ui/src/i18n/locale/en.json +++ b/ui/src/i18n/locale/en.json @@ -12,8 +12,8 @@ "admin": "Administrator", "alarm": "Alarm", "alarm_ftp_info": "FTP Server must be enabled to trigger a movement through file upload!", - "alarm_http_info": "HTTP Server mmust be enabled to trigger a movement through HTTP call!", - "alarm_http_reset_info": "HTTP Server mmust be enabled to reset a movement through HTTP call!", + "alarm_http_info": "HTTP Server must be enabled to trigger a movement through HTTP call!", + "alarm_http_reset_info": "HTTP Server must be enabled to reset a movement through HTTP call!", "alarm_smtp_info": "SMTP Server must be enabled to trigger a movement through Email!", "alexa": "Alexa", "all": "All", diff --git a/ui/src/views/Settings/subpages/cameras.vue b/ui/src/views/Settings/subpages/cameras.vue index c8c85543..fa30bad8 100644 --- a/ui/src/views/Settings/subpages/cameras.vue +++ b/ui/src/views/Settings/subpages/cameras.vue @@ -163,12 +163,12 @@ h4.tw-my-3 {{ $t('http') }} label.form-input-label {{ $t('motion') }} - v-text-field(:value="`http://${hostname}:${config.http.port}/motion?${encodeURIComponent(camera.name)}`" persistent-hint :hint="$t('alarm_http_info')" prepend-inner-icon="mdi-alphabetical" background-color="var(--cui-bg-card)" color="var(--cui-text-default)" solo readonly) + v-text-field(:value="`http://${hostname}:${config.http.port || 7272}/motion?${encodeURIComponent(camera.name)}`" persistent-hint :hint="$t('alarm_http_info')" prepend-inner-icon="mdi-alphabetical" background-color="var(--cui-bg-card)" color="var(--cui-text-default)" solo readonly) template(v-slot:prepend-inner) v-icon.text-muted {{ icons['mdiAlphabetical'] }} label.form-input-label {{ $t('motion_reset') }} - v-text-field(:value="`http://${hostname}:${config.http.port}/reset?${encodeURIComponent(camera.name)}`" persistent-hint :hint="$t('alarm_http_reset_info')" prepend-inner-icon="mdi-alphabetical" background-color="var(--cui-bg-card)" color="var(--cui-text-default)" solo readonly) + v-text-field(:value="`http://${hostname}:${config.http.port || 7272}/reset?${encodeURIComponent(camera.name)}`" persistent-hint :hint="$t('alarm_http_reset_info')" prepend-inner-icon="mdi-alphabetical" background-color="var(--cui-bg-card)" color="var(--cui-text-default)" solo readonly) template(v-slot:prepend-inner) v-icon.text-muted {{ icons['mdiAlphabetical'] }} @@ -771,7 +771,7 @@ export default { http: config.data.http || { active: false, localhttp: false, - port: 7575, + port: 7272, }, mqtt: config.data.mqtt || { active: false, diff --git a/ui/src/views/Settings/subpages/system.vue b/ui/src/views/Settings/subpages/system.vue index 9c92933f..d7f0e171 100644 --- a/ui/src/views/Settings/subpages/system.vue +++ b/ui/src/views/Settings/subpages/system.vue @@ -361,7 +361,7 @@ export default { http: config.data.http || { active: false, localhttp: false, - port: 7575, + port: 7272, }, mqtt: config.data.mqtt || { active: false,