diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index 562d2038e3..4eb7b1a0e4 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -429,9 +429,12 @@ "testConnectionButton": "Test connection", "saveButton": "Save", "deleteButton": "Delete", - "rotate180Label": "180° Rotation", "saveError": "There was an error saving the camera.", - "testConnectionError": "There was an error while getting the RTSP Flux. Are you sure the provided URL is right and accessible from Gladys instance?" + "testConnectionError": "There was an error while getting the RTSP Flux. Are you sure the provided URL is right and accessible from Gladys instance?", + "rotationO": "No rotation", + "rotation90": "90°", + "rotation18O": "180°", + "rotation27O": "270°" }, "tasmota": { "title": "Tasmota", diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json index 58cf3eddf7..e0b06f5031 100644 --- a/front/src/config/i18n/fr.json +++ b/front/src/config/i18n/fr.json @@ -555,9 +555,12 @@ "testConnectionButton": "Tester", "saveButton": "Sauvegarder", "deleteButton": "Supprimer", - "rotate180Label": "Rotation 180°", "saveError": "Une erreur s'est produite lors de l'enregistrement de la caméra.", - "testConnectionError": "Une erreur s'est produite lors de l'obtention du flux RTSP. Êtes-vous sûr que l'URL fournie est correcte et accessible à partir de l'instance Gladys ?" + "testConnectionError": "Une erreur s'est produite lors de l'obtention du flux RTSP. Êtes-vous sûr que l'URL fournie est correcte et accessible à partir de l'instance Gladys ?", + "rotationO": "Pas de rotation", + "rotation90": "90°", + "rotation18O": "180°", + "rotation27O": "270°" }, "tasmota": { "title": "Tasmota", diff --git a/front/src/routes/integration/all/rtsp-camera/RtspCameraBox.jsx b/front/src/routes/integration/all/rtsp-camera/RtspCameraBox.jsx index 6ef0c7a036..0067e90cde 100644 --- a/front/src/routes/integration/all/rtsp-camera/RtspCameraBox.jsx +++ b/front/src/routes/integration/all/rtsp-camera/RtspCameraBox.jsx @@ -3,7 +3,7 @@ import { Component } from 'preact'; import get from 'get-value'; import cx from 'classnames'; import { RequestStatus } from '../../../../utils/consts'; -import { DEVICE_POLL_FREQUENCIES } from '../../../../../../server/utils/constants'; +import { DEVICE_POLL_FREQUENCIES, DEVICE_ROTATION } from '../../../../../../server/utils/constants'; class RtspCameraBox extends Component { saveCamera = async () => { @@ -69,8 +69,7 @@ class RtspCameraBox extends Component { this.props.updateCameraUrl(this.props.cameraIndex, e.target.value); }; updateCameraRotation = e => { - const newValue = e.target.checked ? '1' : '0'; - this.props.updateCameraRotation(this.props.cameraIndex, newValue); + this.props.updateCameraRotation(this.props.cameraIndex, e.target.value); }; updateCameraRoom = e => { const newRoom = e.target.value === '' ? null : e.target.value; @@ -180,20 +179,24 @@ class RtspCameraBox extends Component {

- +
-
diff --git a/server/migrations/20230628144609-change-rotation-camera.js b/server/migrations/20230628144609-change-rotation-camera.js new file mode 100644 index 0000000000..4369f71b84 --- /dev/null +++ b/server/migrations/20230628144609-change-rotation-camera.js @@ -0,0 +1,42 @@ +const Promise = require('bluebird'); +const db = require('../models'); +const logger = require('../utils/logger'); + +module.exports = { + up: async (queryInterface, Sequelize) => { + const service = await db.Service.findOne({ + where: { + name: 'rtsp-camera', + }, + }); + if (service === null) { + return; + } + logger.info(`RstpCamera migration: Found service rtsp-camera = ${service.id}`); + const cameraDevices = await db.Device.findAll({ + where: { + service_id: service.id, + }, + }); + logger.info(`RstpCamera migration: Found ${cameraDevices.length} devices`); + await Promise.each(cameraDevices, async (enedisDevice) => { + const deviceParam = await db.DeviceParam.findOne({ + where: { + device_id: enedisDevice.id, + name: 'CAMERA_ROTATION', + }, + }); + if (deviceParam === null) { + return; + } + if (deviceParam.value === '1') { + logger.info(`RstpCamera migration: Updating device_params ${deviceParam.id} with 180°`); + deviceParam.set({ + value: '180', + }); + await deviceParam.save(); + } + }); + }, + down: async (queryInterface, Sequelize) => {}, +}; diff --git a/server/services/rtsp-camera/lib/getImage.js b/server/services/rtsp-camera/lib/getImage.js index 135a203fcb..45419c1665 100644 --- a/server/services/rtsp-camera/lib/getImage.js +++ b/server/services/rtsp-camera/lib/getImage.js @@ -2,6 +2,7 @@ const fse = require('fs-extra'); const path = require('path'); const logger = require('../../../utils/logger'); const { NotFoundError } = require('../../../utils/coreErrors'); +const { DEVICE_ROTATION } = require('../../../utils/constants'); const DEVICE_PARAM_CAMERA_URL = 'CAMERA_URL'; const DEVICE_PARAM_CAMERA_ROTATION = 'CAMERA_ROTATION'; @@ -41,12 +42,23 @@ async function getImage(device) { const writeStream = fse.createWriteStream(filePath); const outputOptions = [ '-vframes 1', - '-vf scale=640:-1', // resize the image with max width = 640 '-qscale:v 15', // Effective range for JPEG is 2-31 with 31 being the worst quality. ]; - if (cameraRotationParam.value === '1') { - outputOptions.push('-vf hflip,vflip'); // Rotate 180 + switch (cameraRotationParam.value) { + case DEVICE_ROTATION.DEGREES_90: + outputOptions.push('-vf scale=640:-1,transpose=1'); // Rotate 90 + break; + case DEVICE_ROTATION.DEGREES_180: + outputOptions.push('-vf scale=640:-1,transpose=1,transpose=1'); // Rotate 180 + break; + case DEVICE_ROTATION.DEGREES_270: + outputOptions.push('-vf scale=640:-1,transpose=2'); // Rotate 270 + break; + default: + outputOptions.push('-vf scale=640:-1'); // Rotate 0 + break; } + // Send a camera thumbnail to this stream // Add a timeout to prevent ffmpeg from running forever this.ffmpeg(cameraUrlParam.value, { timeout: 10 }) diff --git a/server/services/rtsp-camera/lib/startStreaming.js b/server/services/rtsp-camera/lib/startStreaming.js index 8470c79efc..960e3e5d57 100644 --- a/server/services/rtsp-camera/lib/startStreaming.js +++ b/server/services/rtsp-camera/lib/startStreaming.js @@ -8,6 +8,7 @@ const util = require('util'); const randomBytes = util.promisify(require('crypto').randomBytes); const logger = require('../../../utils/logger'); const { NotFoundError } = require('../../../utils/coreErrors'); +const { DEVICE_ROTATION } = require('../../../utils/constants'); const DEVICE_PARAM_CAMERA_URL = 'CAMERA_URL'; const DEVICE_PARAM_CAMERA_ROTATION = 'CAMERA_ROTATION'; @@ -113,8 +114,6 @@ async function startStreaming(cameraSelector, isGladysGateway, segmentDuration = 'veryfast', // Encoding presets '-flags', '+cgop', - '-vf', - 'scale=1920:-1', // Full HD resolution '-r', '25', // Frames (rate) per second '-g', @@ -150,11 +149,24 @@ async function startStreaming(cameraSelector, isGladysGateway, segmentDuration = indexFilePath, ]; - if (cameraRotationParam.value === '1') { - args.push('-vf'); // Rotate 180 - args.push('hflip,vflip'); + let cameraRotationArgs = ''; + switch (cameraRotationParam.value) { + case DEVICE_ROTATION.DEGREES_90: + cameraRotationArgs = 'scale=1920:-1,transpose=1'; // Full HD resolution & Rotate 90 + break; + case DEVICE_ROTATION.DEGREES_180: + cameraRotationArgs = 'scale=1920:-1,transpose=1,transpose=1'; // Full HD resolution & Rotate 180 + break; + case DEVICE_ROTATION.DEGREES_270: + cameraRotationArgs = 'scale=1920:-1,transpose=2'; // Full HD resolution & Rotate 270 + break; + default: + cameraRotationArgs = 'scale=1920:-1'; // Full HD resolution & Rotate 0 + break; } + args.splice(8, 0, '-vf', cameraRotationArgs); + const options = { timeout: 5 * 60 * 1000, // 5 minutes }; diff --git a/server/test/services/rtsp-camera/rtspCamera.streaming.test.js b/server/test/services/rtsp-camera/rtspCamera.streaming.test.js index 6028236862..75602bcec5 100644 --- a/server/test/services/rtsp-camera/rtspCamera.streaming.test.js +++ b/server/test/services/rtsp-camera/rtspCamera.streaming.test.js @@ -6,6 +6,7 @@ const { fake, assert: fakeAssert } = require('sinon'); const FfmpegMock = require('./FfmpegMock.test'); const RtspCameraManager = require('../../../services/rtsp-camera/lib'); const { NotFoundError } = require('../../../utils/coreErrors'); +const { DEVICE_ROTATION } = require('../../../utils/constants'); const device = { id: 'a6fb4cb8-ccc2-4234-a752-b25d1eb5ab6b', @@ -133,7 +134,7 @@ describe('Camera.streaming', () => { await rtspCameraManager.stopStreaming('my-camera'); fakeAssert.called(rtspCameraManager.sendCameraFileToGatewayLimited); }); - it('should star with rotation & stop streaming', async () => { + it('should star with 90 rotation & stop streaming', async () => { const gladysDeviceWithRotation = { config: { tempFolder: '/tmp/gladys', @@ -149,7 +150,79 @@ describe('Camera.streaming', () => { }, { name: 'CAMERA_ROTATION', - value: '1', + value: DEVICE_ROTATION.DEGREES_90, + }, + ], + }), + }, + }; + rtspCameraManager = new RtspCameraManager( + gladysDeviceWithRotation, + FfmpegMock, + childProcessMock, + 'de051f90-f34a-4fd5-be2e-e502339ec9bc', + ); + rtspCameraManager.onNewCameraFile = fake.resolves(null); + const liveStreamingProcess = await rtspCameraManager.startStreaming('my-camera', false, 1); + expect(liveStreamingProcess).to.have.property('camera_folder'); + expect(liveStreamingProcess).to.have.property('encryption_key'); + await rtspCameraManager.liveActivePing('my-camera'); + await rtspCameraManager.stopStreaming('my-camera'); + fakeAssert.called(rtspCameraManager.onNewCameraFile); + }); + it('should star with 180 rotation & stop streaming', async () => { + const gladysDeviceWithRotation = { + config: { + tempFolder: '/tmp/gladys', + }, + device: { + getBySelector: fake.resolves({ + id: 'a6fb4cb8-ccc2-4234-a752-b25d1eb5ab6b', + selector: 'my-camera', + params: [ + { + name: 'CAMERA_URL', + value: 'test', + }, + { + name: 'CAMERA_ROTATION', + value: DEVICE_ROTATION.DEGREES_180, + }, + ], + }), + }, + }; + rtspCameraManager = new RtspCameraManager( + gladysDeviceWithRotation, + FfmpegMock, + childProcessMock, + 'de051f90-f34a-4fd5-be2e-e502339ec9bc', + ); + rtspCameraManager.onNewCameraFile = fake.resolves(null); + const liveStreamingProcess = await rtspCameraManager.startStreaming('my-camera', false, 1); + expect(liveStreamingProcess).to.have.property('camera_folder'); + expect(liveStreamingProcess).to.have.property('encryption_key'); + await rtspCameraManager.liveActivePing('my-camera'); + await rtspCameraManager.stopStreaming('my-camera'); + fakeAssert.called(rtspCameraManager.onNewCameraFile); + }); + it('should star with 270 rotation & stop streaming', async () => { + const gladysDeviceWithRotation = { + config: { + tempFolder: '/tmp/gladys', + }, + device: { + getBySelector: fake.resolves({ + id: 'a6fb4cb8-ccc2-4234-a752-b25d1eb5ab6b', + selector: 'my-camera', + params: [ + { + name: 'CAMERA_URL', + value: 'test', + }, + { + name: 'CAMERA_ROTATION', + value: DEVICE_ROTATION.DEGREES_270, }, ], }), diff --git a/server/test/services/rtsp-camera/rtspCamera.test.js b/server/test/services/rtsp-camera/rtspCamera.test.js index 779f76b265..20fc277df3 100644 --- a/server/test/services/rtsp-camera/rtspCamera.test.js +++ b/server/test/services/rtsp-camera/rtspCamera.test.js @@ -33,7 +33,7 @@ const gladys = { }, }; -const deviceFlipped = { +const deviceRotation90 = { id: 'a6fb4cb8-ccc2-4234-a752-b25d1eb5ab6c', selector: 'my-camera', params: [ @@ -43,7 +43,37 @@ const deviceFlipped = { }, { name: 'CAMERA_ROTATION', - value: '1', + value: '90', + }, + ], +}; + +const deviceRotation180 = { + id: 'a6fb4cb8-ccc2-4234-a752-b25d1eb5ab6c', + selector: 'my-camera', + params: [ + { + name: 'CAMERA_URL', + value: 'test', + }, + { + name: 'CAMERA_ROTATION', + value: '180', + }, + ], +}; + +const deviceRotation270 = { + id: 'a6fb4cb8-ccc2-4234-a752-b25d1eb5ab6c', + selector: 'my-camera', + params: [ + { + name: 'CAMERA_URL', + value: 'test', + }, + { + name: 'CAMERA_ROTATION', + value: '270', }, ], }; @@ -107,8 +137,16 @@ describe('RtspCameraManager commands', () => { const image = await rtspCameraManager.getImage(device); expect(image).to.equal('image/png;base64,aW1hZ2U='); }); + it('should getImage 90°', async () => { + const image = await rtspCameraManager.getImage(deviceRotation90); + expect(image).to.equal('image/png;base64,aW1hZ2U='); + }); it('should getImage 180°', async () => { - const image = await rtspCameraManager.getImage(deviceFlipped); + const image = await rtspCameraManager.getImage(deviceRotation180); + expect(image).to.equal('image/png;base64,aW1hZ2U='); + }); + it('should getImage 270°', async () => { + const image = await rtspCameraManager.getImage(deviceRotation270); expect(image).to.equal('image/png;base64,aW1hZ2U='); }); it('should return error', async () => { diff --git a/server/utils/constants.js b/server/utils/constants.js index 0b19e3ad64..962d7c003c 100644 --- a/server/utils/constants.js +++ b/server/utils/constants.js @@ -762,6 +762,13 @@ const DEVICE_POLL_FREQUENCIES = { EVERY_SECONDS: 1 * 1000, }; +const DEVICE_ROTATION = { + DEGREES_0: '0', + DEGREES_90: '90', + DEGREES_180: '180', + DEGREES_270: '270', +}; + const WEBSOCKET_MESSAGE_TYPES = { BACKUP: { DOWNLOADED: 'backup.downloaded', @@ -962,6 +969,8 @@ module.exports.SESSION_TOKEN_TYPE_LIST = SESSION_TOKEN_TYPE_LIST; module.exports.DEVICE_POLL_FREQUENCIES = DEVICE_POLL_FREQUENCIES; module.exports.DEVICE_POLL_FREQUENCIES_LIST = createList(DEVICE_POLL_FREQUENCIES); +module.exports.DEVICE_ROTATION = DEVICE_ROTATION; + module.exports.WEBSOCKET_MESSAGE_TYPES = WEBSOCKET_MESSAGE_TYPES; module.exports.DEVICE_FEATURE_UNITS = DEVICE_FEATURE_UNITS;