From 948c9dbfeab01ab160b5dcf5d67fb26e26a85bbf Mon Sep 17 00:00:00 2001 From: SeydX Date: Sat, 23 Apr 2022 18:32:51 +0200 Subject: [PATCH] v1.1.14 --- CHANGELOG.md | 14 +++++++++- package-lock.json | 4 +-- package.json | 2 +- src/common/ffmpeg.js | 26 +++++++++---------- src/controller/camera/camera.controller.js | 12 ++++----- .../camera/services/media.service.js | 5 ++-- .../camera/services/prebuffer.service.js | 4 ++- .../camera/services/stream.service.js | 3 +++ .../camera/services/videoanalysis.service.js | 13 +++++++--- src/controller/camera/utils/camera.utils.js | 21 +++++++++++++++ src/controller/event/event.controller.js | 22 +++++++--------- src/controller/motion/motion.controller.js | 23 +++++++++++++--- 12 files changed, 103 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a464a4e7..d95769bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,19 @@ # Changelog All notable changes to this project will be documented in this file. -# v1.1.12 - 2022-04-23 +# v1.1.14 - 2022-04-23 + +## Other Changes +- **MQTT:** When motion is detected, two MQTT messages are now published on following topics: + 1. **camera.ui/notifications**: Contains all notifications AFTER motion has been detected AND recorded. + 2. **camera.ui/motion** _(can be changed in the interface):_ Contains motion event (before something is recorded). +- Deprecated FFmpeg arguments will be auto replaced now +- Minor improvements to probe stream + +## Bugfixes +- Fixed an issue where changing camera settings via the interface did not work + +# v1.1.12 / v1.1.13 - 2022-04-23 ## Other Changes - Improved probe stream diff --git a/package-lock.json b/package-lock.json index 452ac9a0..ed963f56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "camera.ui", - "version": "1.1.13", + "version": "1.1.14", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "camera.ui", - "version": "1.1.13", + "version": "1.1.14", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 437133b5..20739eae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "camera.ui", - "version": "1.1.13", + "version": "1.1.14", "description": "NVR like user interface for RTSP capable cameras.", "author": "SeydX (https://github.com/SeydX/camera.ui)", "scripts": { diff --git a/src/common/ffmpeg.js b/src/common/ffmpeg.js index 9950e9f7..2667ebe1 100644 --- a/src/common/ffmpeg.js +++ b/src/common/ffmpeg.js @@ -150,18 +150,18 @@ export const getAndStoreSnapshot = ( } const videoProcessor = ConfigService.ui.options.videoProcessor; - const videoConfig = cameraUtils.generateVideoConfig(camera.videoConfig); + const videoWidth = videoConfig.maxWidth; + const videoHeight = videoConfig.maxHeight; + const destination = storeSnapshot ? `${recordingPath}/${fileName}${isPlaceholder ? '@2' : ''}.jpeg` : '-'; + const controller = CameraController.cameras.get(camera.name); + let ffmpegInput = [ ...cameraUtils.generateInputSource(videoConfig, fromSubSource ? videoConfig.subSource : false).split(/\s+/), ]; - const videoWidth = videoConfig.maxWidth; - const videoHeight = videoConfig.maxHeight; - - const destination = storeSnapshot ? `${recordingPath}/${fileName}${isPlaceholder ? '@2' : ''}.jpeg` : '-'; + ffmpegInput = cameraUtils.checkDeprecatedFFmpegArguments(controller?.media?.codecs?.ffmpegVersion, ffmpegInput); - const controller = CameraController.cameras.get(camera.name); if (!fromSubSource && camera.prebuffering && controller?.prebuffer) { try { ffmpegInput = await controller.prebuffer.getVideo(); @@ -296,16 +296,16 @@ export const storeVideo = (camera, recordingPath, fileName, recordingTimer) => { // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { const videoProcessor = ConfigService.ui.options.videoProcessor; - const videoConfig = cameraUtils.generateVideoConfig(camera.videoConfig); - let ffmpegInput = [...cameraUtils.generateInputSource(videoConfig).split(/\s+/)]; - const videoName = `${recordingPath}/${fileName}.mp4`; const videoWidth = videoConfig.maxWidth; const videoHeight = videoConfig.maxHeight; const vcodec = videoConfig.vcodec; - const controller = CameraController.cameras.get(camera.name); + + let ffmpegInput = [...cameraUtils.generateInputSource(videoConfig).split(/\s+/)]; + ffmpegInput = cameraUtils.checkDeprecatedFFmpegArguments(controller?.media?.codecs?.ffmpegVersion, ffmpegInput); + if (camera.prebuffering && controller?.prebuffer) { try { ffmpegInput = await controller.prebuffer.getVideo(); @@ -400,13 +400,13 @@ export const handleFragmentsRequests = async function* (camera) { log.debug('Video fragments requested from interface', camera.name); const videoConfig = cameraUtils.generateVideoConfig(camera.videoConfig); - let ffmpegInput = [...cameraUtils.generateInputSource(videoConfig).split(/\s+/)]; - const audioArguments = ['-acodec', 'copy']; const videoArguments = ['-vcodec', 'copy']; - const controller = CameraController.cameras.get(camera.name); + let ffmpegInput = [...cameraUtils.generateInputSource(videoConfig).split(/\s+/)]; + ffmpegInput = cameraUtils.checkDeprecatedFFmpegArguments(controller?.media?.codecs?.ffmpegVersion, ffmpegInput); + if (camera.prebuffering && controller?.prebuffer) { try { log.debug('Setting prebuffer stream as input', camera.name); diff --git a/src/controller/camera/camera.controller.js b/src/controller/camera/camera.controller.js index 32dee060..876f9fbb 100644 --- a/src/controller/camera/camera.controller.js +++ b/src/controller/camera/camera.controller.js @@ -31,6 +31,7 @@ export default class CameraController { const videoanalysisService = new VideoAnalysisService( camera, prebufferService, + mediaService, CameraController.#controller, CameraController.#socket ); @@ -96,16 +97,15 @@ export default class CameraController { static async reconfigureController(cameraName) { const controller = CameraController.cameras.get(cameraName); + const camera = ConfigService.ui.cameras.find((camera) => camera.name === cameraName); - if (!controller) { - throw new Error(`Can not reconfigure controller, controller for ${cameraName} not found!`); - } - - if (camera.disable) { + if (camera?.disable) { return; } - const camera = ConfigService.ui.cameras.find((camera) => camera.name === cameraName); + if (!controller) { + throw new Error(`Can not reconfigure controller, controller for ${cameraName} not found!`); + } if (!camera) { throw new Error(`Unexpected error occured during reconfiguring controller for ${cameraName}`); diff --git a/src/controller/camera/services/media.service.js b/src/controller/camera/services/media.service.js index 1532f69f..e3b81a5e 100644 --- a/src/controller/camera/services/media.service.js +++ b/src/controller/camera/services/media.service.js @@ -18,6 +18,7 @@ export default class MediaService { this.cameraName = camera.name; this.codecs = { + ffmpegVersion: null, probe: false, timedout: false, audio: [], @@ -67,7 +68,7 @@ export default class MediaService { stderr.on('line', (line) => { if (lines === 0) { - ConfigService.ffmpegVersion = line.split(' ')[2]; + this.codecs.ffmpegVersion = ConfigService.ffmpegVersion = line.split(' ')[2]; } const bitrateLine = line.includes('start: ') && line.includes('bitrate: ') ? line.split('bitrate: ')[1] : false; @@ -109,7 +110,7 @@ export default class MediaService { this.codecs.timedout = true; cp.kill('SIGKILL'); } - }, 5000); + }, 10000); }); } } diff --git a/src/controller/camera/services/prebuffer.service.js b/src/controller/camera/services/prebuffer.service.js index 708ef8fa..be46729f 100644 --- a/src/controller/camera/services/prebuffer.service.js +++ b/src/controller/camera/services/prebuffer.service.js @@ -168,7 +168,7 @@ export default class PrebufferService { let incompatibleAudio = audioSourceFound && !probeAudio.some((codec) => compatibleAudio.test(codec)); let probeTimedOut = this.#mediaService.codecs.timedout; - const ffmpegInput = [ + let ffmpegInput = [ '-hide_banner', '-loglevel', 'error', @@ -177,6 +177,8 @@ export default class PrebufferService { ...cameraUtils.generateInputSource(videoConfig).split(/\s+/), ]; + ffmpegInput = cameraUtils.checkDeprecatedFFmpegArguments(this.#mediaService.codecs.ffmpegVersion, ffmpegInput); + const audioArguments = []; /*if (audioEnabled && incompatibleAudio) { diff --git a/src/controller/camera/services/stream.service.js b/src/controller/camera/services/stream.service.js index ffcb482f..ed10a81f 100644 --- a/src/controller/camera/services/stream.service.js +++ b/src/controller/camera/services/stream.service.js @@ -52,7 +52,10 @@ export default class StreamService { const cameraSetting = Settings.find((camera) => camera && camera.name === this.cameraName); const videoConfig = cameraUtils.generateVideoConfig(this.#camera.videoConfig); + let ffmpegInput = [...cameraUtils.generateInputSource(videoConfig).split(/\s+/)]; + ffmpegInput = cameraUtils.checkDeprecatedFFmpegArguments(this.#mediaService.codecs.ffmpegVersion, ffmpegInput); + let prebuffer = null; if (this.#camera.prebuffering && this.#prebufferService) { diff --git a/src/controller/camera/services/videoanalysis.service.js b/src/controller/camera/services/videoanalysis.service.js index 8a939676..83fbff3a 100644 --- a/src/controller/camera/services/videoanalysis.service.js +++ b/src/controller/camera/services/videoanalysis.service.js @@ -47,6 +47,7 @@ export default class VideoAnalysisService { #controller; #socket; #prebufferService; + #mediaService; videoanalysisSession = null; killed = false; @@ -58,13 +59,14 @@ export default class VideoAnalysisService { finishLaunching = false; - constructor(camera, prebufferService, controller, socket) { + constructor(camera, prebufferService, mediaService, controller, socket) { //log.debug('Initializing video analysis', camera.name); this.#camera = camera; this.#controller = controller; this.#socket = socket; this.#prebufferService = prebufferService; + this.#mediaService = mediaService; this.cameraName = camera.name; @@ -112,12 +114,15 @@ export default class VideoAnalysisService { this.resetVideoAnalysis(); const videoConfig = cameraUtils.generateVideoConfig(this.#camera.videoConfig); - let input = cameraUtils.generateInputSource(videoConfig, videoConfig.subSource).split(/\s+/); + + let ffmpegInput = cameraUtils.generateInputSource(videoConfig, videoConfig.subSource).split(/\s+/); + ffmpegInput = cameraUtils.checkDeprecatedFFmpegArguments(this.#mediaService.codecs.ffmpegVersion, ffmpegInput); + let withPrebuffer = false; if (this.#camera.prebuffering && videoConfig.subSource === videoConfig.source) { try { - input = withPrebuffer = await this.#prebufferService.getVideo(); + ffmpegInput = withPrebuffer = await this.#prebufferService.getVideo(); } catch { // retry log.debug( @@ -153,7 +158,7 @@ export default class VideoAnalysisService { } } - this.videoanalysisSession = await this.#startVideoAnalysis(input, videoConfig); + this.videoanalysisSession = await this.#startVideoAnalysis(ffmpegInput, videoConfig); if (!withPrebuffer) { const timer = this.#millisUntilTime('04:00'); diff --git a/src/controller/camera/utils/camera.utils.js b/src/controller/camera/utils/camera.utils.js index 01bca9c9..b045d3b4 100644 --- a/src/controller/camera/utils/camera.utils.js +++ b/src/controller/camera/utils/camera.utils.js @@ -1,6 +1,7 @@ /* eslint-disable unicorn/number-literal-case */ 'use-strict'; +import compareVersions from 'compare-versions'; import { createServer } from 'net'; import { once } from 'events'; import readline from 'readline'; @@ -354,3 +355,23 @@ export const generateVideoConfig = (videoConfig) => { return config; }; + +export const checkDeprecatedFFmpegArguments = (ffmpegVersion, ffmpegArguments) => { + if (!ffmpegVersion) { + return ffmpegArguments; + } + + let ffmpegArgumentsArray = !Array.isArray(ffmpegArguments) ? ffmpegArguments.split(' ') : [...ffmpegArguments]; + + if (compareVersions.compare(ffmpegVersion, '5.0.0', '>=')) { + ffmpegArgumentsArray = ffmpegArgumentsArray.map((argument) => { + if (argument === '-stimeout') { + argument = '-timeout'; + } + + return argument; + }); + } + + return ffmpegArgumentsArray; +}; diff --git a/src/controller/event/event.controller.js b/src/controller/event/event.controller.js index f52c4379..ffe10ebe 100644 --- a/src/controller/event/event.controller.js +++ b/src/controller/event/event.controller.js @@ -106,10 +106,6 @@ export default class EventController { endpoint: CameraSettings.webhookUrl, }; - const mqttPublishSettings = { - topic: CameraSettings.mqttTopic, - }; - const webpushSettings = { publicKey: SettingsDB.webpush.publicKey, privateKey: SettingsDB.webpush.privateKey, @@ -165,7 +161,7 @@ export default class EventController { const { notification, notify } = await EventController.#handleNotification(motionInfo); // 1) - await EventController.#publishMqtt(cameraName, notification, mqttPublishSettings); + await EventController.#publishMqtt(cameraName, notification, notificationsSettings.active); // 2) await EventController.#sendWebhook( @@ -598,18 +594,18 @@ export default class EventController { } } - static async #publishMqtt(cameraName, notification, mqttPublishSettings) { + static async #publishMqtt(cameraName, notification, notificationActive) { + if (!notificationActive) { + return log.debug('Notifications not enabled, skip MQTT (notification)..', cameraName); + } + try { const mqttClient = EventController.#controller.motionController?.mqttClient; - if (mqttClient && mqttClient.connected) { - if (!mqttPublishSettings.topic) { - return log.debug('No MQTT Publish Topic defined, skip MQTT..'); - } - - mqttClient.publish(mqttPublishSettings.topic, JSON.stringify(notification)); + if (mqttClient?.connected) { + mqttClient.publish('camera.ui/notifications', JSON.stringify(notification)); } else { - return log.debug('MQTT client not connected, skip MQTT..'); + return log.debug('MQTT client not connected, skip MQTT (notification)..'); } } catch (error) { log.info('An error occured during publishing mqtt message', cameraName, 'events'); diff --git a/src/controller/motion/motion.controller.js b/src/controller/motion/motion.controller.js index 33a866ea..51845e24 100644 --- a/src/controller/motion/motion.controller.js +++ b/src/controller/motion/motion.controller.js @@ -690,9 +690,10 @@ export default class MotionController { } if (camera) { - const generalSettings = await Database.interfaceDB.chain.get('settings').get('general').cloneDeep().value(); - const atHome = generalSettings?.atHome || false; - const cameraExcluded = (generalSettings?.exclude || []).includes(camera.name); + const settingsDatabase = await Database.interfaceDB.chain.get('settings').cloneDeep().value(); + const cameraSettings = settingsDatabase?.cameras.find((cam) => cam.name === cameraName); + const atHome = settingsDatabase?.general?.atHome || false; + const cameraExcluded = (settingsDatabase?.general?.exclude || []).includes(camera.name); if (atHome && !cameraExcluded) { const message = `Skip motion trigger. At Home is active and ${camera.name} is not excluded!`; @@ -714,6 +715,22 @@ export default class MotionController { MotionController.#controller.emit('motion', camera.name, triggerType, state, event); // used for extern controller, like Homebridge if (camera.recordOnMovement) { + const mqttClient = MotionController.mqttClient; + + if (mqttClient?.connected && cameraSettings?.mqttTopic) { + mqttClient.publish( + cameraSettings.mqttTopic, + JSON.stringify({ + camera: camera.name, + state: state, + type: triggerType, + event: event, + }) + ); + } else { + log.debug('No MQTT Publish Topic defined, skip MQTT (motion)..'); + } + const recordingSettings = await Database.interfaceDB.chain .get('settings') .get('recordings')