homebridge-camera-ui
Version:
User Interface for RTSP capable cameras with HSV support.
909 lines (751 loc) • 28.7 kB
JavaScript
'use-strict';
import { createSocket } from 'dgram';
import { fileURLToPath } from 'url';
import fs from 'fs-extra';
import path from 'path';
import pickPort from 'pick-port';
import { spawn } from 'child_process';
import * as cameraUtils from 'camera.ui/src/controller/camera/utils/camera.utils.js';
import Logger from '../../services/logger/logger.service.js';
import FfmpegProcess from '../services/ffmpeg.service.js';
import RecordingDelegate from '../services/recording.service.js';
import Ping from 'camera.ui/src/common/ping.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
//const maxstreamsImage = path.resolve(__dirname, '..', 'thumbs', 'maxstreams_cameraui.png');
const offlineImage = path.resolve(__dirname, '..', 'thumbs', 'offline_cameraui.png');
const privacyImage = path.resolve(__dirname, '..', 'thumbs', 'privacy_cameraui.png');
//const maxstreamsImageInBytes = fs.readFileSync(maxstreamsImage);
const offlineImageInBytes = fs.readFileSync(offlineImage);
const privacyImageInBytes = fs.readFileSync(privacyImage);
export default class CameraDelegate {
constructor(api, accessory, config, cameraUi, handler) {
this.api = api;
this.log = Logger.log;
this.config = config;
this.accessory = accessory;
this.handler = handler;
this.cameraUi = cameraUi;
this.services = [];
this.streamControllers = [];
this.recordingDelegate = null;
this.pendingSessions = new Map();
this.ongoingSessions = new Map();
this.timeouts = new Map();
const recordingCodecs = [];
if (this.accessory.context.config.hsv && !this.api.versionGreaterOrEqual('1.4.0')) {
this.log.warn(
'HSV cannot be activated. Not compatible Homebridge version detected! You must have at least v1.4.0-beta.4 installed!',
this.accessory.displayName,
'Homebridge'
);
this.accessory.context.config.hsv = false;
}
if (this.accessory.context.config.hsv) {
this.log.debug('Initializing HomeKit Secure Video', this.accessory.displayName);
const samplerate = [];
for (const sr of [this.api.hap.AudioRecordingSamplerate.KHZ_32]) {
samplerate.push(sr);
}
for (const type of [this.api.hap.AudioRecordingCodecType.AAC_LC]) {
const entry = {
type,
bitrateMode: 0,
samplerate,
audioChannels: 1,
};
recordingCodecs.push(entry);
}
this.recordingDelegate = new RecordingDelegate(this.api, this.accessory, config, cameraUi, handler);
}
this.controller = new this.api.hap.CameraController({
cameraStreamCount: this.accessory.context.config.videoConfig.maxStreams || 2,
delegate: this,
streamingOptions: {
supportedCryptoSuites: [this.api.hap.SRTPCryptoSuites.AES_CM_128_HMAC_SHA1_80],
video: {
resolutions: [
[320, 180, 30],
[320, 240, 15],
[320, 240, 30],
[480, 270, 30],
[480, 360, 30],
[640, 360, 30],
[640, 480, 30],
[1280, 720, 30],
[1280, 960, 30],
[1920, 1080, 30],
[1600, 1200, 30],
],
codec: {
profiles: [this.api.hap.H264Profile.BASELINE, this.api.hap.H264Profile.MAIN, this.api.hap.H264Profile.HIGH],
levels: [this.api.hap.H264Level.LEVEL3_1, this.api.hap.H264Level.LEVEL3_2, this.api.hap.H264Level.LEVEL4_0],
},
},
audio: {
twoWayAudio: !!this.accessory.context.config.videoConfig.returnAudioTarget,
codecs: [
{
type: this.api.hap.AudioStreamingCodecType.AAC_ELD,
samplerate: this.api.hap.AudioStreamingSamplerate.KHZ_16,
/*type: AudioStreamingCodecType.OPUS,
samplerate: AudioStreamingSamplerate.KHZ_24*/
},
],
},
},
recording: this.accessory.context.config.hsv
? {
options: {
overrideEventTriggerOptions: [
this.api.hap.EventTriggerOption.MOTION,
this.api.hap.EventTriggerOption.DOORBELL,
],
prebufferLength: this.accessory.context.config.prebufferLength * 1000, // prebufferLength always remains 4s ?
mediaContainerConfiguration: [
{
type: this.api.hap.MediaContainerType.FRAGMENTED_MP4,
fragmentLength: 4000,
},
],
video: {
type: this.api.hap.VideoCodecType.H264,
parameters: {
profiles: [
this.api.hap.H264Profile.BASELINE,
this.api.hap.H264Profile.MAIN,
this.api.hap.H264Profile.HIGH,
],
levels: [
this.api.hap.H264Level.LEVEL3_1,
this.api.hap.H264Level.LEVEL3_2,
this.api.hap.H264Level.LEVEL4_0,
],
},
resolutions: [
[320, 180, 30],
[320, 240, 15],
[320, 240, 30],
[480, 270, 30],
[480, 360, 30],
[640, 360, 30],
[640, 480, 30],
[1280, 720, 30],
[1280, 960, 30],
[1920, 1080, 30],
[1600, 1200, 30],
],
},
audio: {
codecs: recordingCodecs,
},
},
delegate: this.recordingDelegate,
}
: undefined,
sensors: this.accessory.context.config.hsv
? {
motion: this.accessory.getServiceById(this.api.hap.Service.MotionSensor, 'motion') || true,
occupancy: this.accessory.getServiceById(this.api.hap.Service.OccupancySensor, 'occupancy') || false, //not implemented yet
}
: undefined,
});
this.api.on('shutdown', () => {
for (const session in this.ongoingSessions) {
this.stopStream(session);
}
});
}
//https://github.com/homebridge/camera-utils/blob/master/src/ports.ts
async reservePorts(count = 1, type = 'udp', attemptNumber) {
if (attemptNumber > 100) {
throw new Error('Failed to reserve ports after 100 tries');
}
const pickPortOptions = {
type,
reserveTimeout: 15, // 15 seconds is max setup time for HomeKit streams, so the port should be in use by then
};
const port = await pickPort(pickPortOptions);
const ports = [port];
const tryAgain = () => {
return this.reservePorts({
count,
type,
attemptNumber: attemptNumber + 1,
});
};
// eslint-disable-next-line unicorn/prevent-abbreviations
for (let i = 1; i < count; i++) {
try {
const targetConsecutivePort = port + i,
openPort = await pickPort({
...pickPortOptions,
minPort: targetConsecutivePort,
maxPort: targetConsecutivePort,
});
ports.push(openPort);
} catch {
// can't reserve next port, bail and get another set
return tryAgain();
}
}
return ports;
}
determineResolution(request, isSnapshot) {
const resultInfo = {
width: request.width,
height: request.height,
};
const videoConfig = cameraUtils.generateVideoConfig(this.accessory.context.config.videoConfig);
if (!isSnapshot) {
if (videoConfig.maxWidth && videoConfig.forceMax) {
resultInfo.width = videoConfig.maxWidth;
}
if (videoConfig.maxHeight && videoConfig.forceMax) {
resultInfo.height = videoConfig.maxHeight;
}
}
let filters = videoConfig.videoFilter ? videoConfig.videoFilter.split(',') : [];
const noneFilter = filters.indexOf('none');
if (noneFilter >= 0) {
filters.splice(noneFilter, 1);
}
resultInfo.snapFilter = filters.join(',');
if (noneFilter < 0 && (resultInfo.width > 0 || resultInfo.height > 0)) {
resultInfo.resizeFilter =
'scale=' +
// eslint-disable-next-line quotes
(resultInfo.width > 0 ? "'min(" + resultInfo.width + ",iw)'" : 'iw') +
':' +
// eslint-disable-next-line quotes
(resultInfo.height > 0 ? "'min(" + resultInfo.height + ",ih)'" : 'ih') +
':force_original_aspect_ratio=decrease';
filters.push(resultInfo.resizeFilter, 'scale=trunc(iw/2)*2:trunc(ih/2)*2'); // Force to fit encoder restrictions
}
if (filters.length > 0) {
resultInfo.videoFilter = filters.join(',');
}
return resultInfo;
}
fetchSnapshot(snapFilter) {
// eslint-disable-next-line no-async-promise-executor, no-unused-vars
this.snapshotPromise = new Promise(async (resolve, reject) => {
const atHome = await this.getPrivacyState();
if (atHome) {
this.snapshotPromise = undefined;
return resolve(privacyImageInBytes);
}
let input = this.accessory.context.config.videoConfig.stillImageSource.split(/\s+/);
const startTime = Date.now();
const controller = this.cameraUi.cameraController.get(this.accessory.displayName);
if (this.accessory.context.config.prebuffering && controller?.prebuffer) {
try {
input = await controller.prebuffer.getVideo();
} catch {
// ignore
}
}
const ffmpegArguments = ['-hide_banner', '-loglevel', 'error', ...input, '-frames:v', '1'];
if (snapFilter) {
ffmpegArguments.push('-filter:v', ...snapFilter.split(/\s+/));
}
ffmpegArguments.push('-f', 'image2', '-');
this.log.debug(
`Snapshot command: ${this.config.options.videoProcessor} ${ffmpegArguments.join(' ')}`,
this.accessory.displayName
);
const ffmpeg = spawn(this.config.options.videoProcessor, ffmpegArguments, {
env: process.env,
});
let errors = [];
let snapshotBuffer = Buffer.alloc(0);
ffmpeg.stdout.on('data', (data) => {
snapshotBuffer = Buffer.concat([snapshotBuffer, data]);
});
ffmpeg.on('error', (error) => {
this.log.error(
`FFmpeg process creation failed: ${error.message} - Showing "offline" image instead.`,
this.accessory.displayName
);
resolve(offlineImageInBytes);
this.snapshotPromise = undefined;
});
ffmpeg.stderr.on('data', (data) => {
errors = errors.slice(-5);
errors.push(data.toString().replace(/(\r\n|\n|\r)/gm, ' '));
});
ffmpeg.on('close', () => {
if (snapshotBuffer.length > 0) {
resolve(snapshotBuffer);
} else {
this.log.error('Failed to fetch snapshot. Showing "offline" image instead.', this.accessory.displayName);
if (errors.length > 0) {
this.log.error(errors.join(' - '), this.accessory.displayName, 'Homebridge');
}
this.snapshotPromise = undefined;
return resolve(offlineImageInBytes);
}
setTimeout(() => {
this.snapshotPromise = undefined;
}, 5 * 1000); // Expire cached snapshot after 5 seconds
const runtime = (Date.now() - startTime) / 1000;
let message = `Fetching snapshot took ${runtime} seconds.`;
if (runtime < 5) {
this.log.debug(message, this.accessory.displayName);
} else {
if (!this.accessory.context.config.unbridge) {
message += ' It is highly recommended you switch to unbridge mode.';
}
if (runtime < 22) {
this.log.info(message, this.accessory.displayName, 'Homebridge');
} else {
message += ' The request has timed out and the snapshot has not been refreshed in HomeKit.';
this.log.error(message, this.accessory.displayName, 'Homebridge');
}
}
});
});
return this.snapshotPromise;
}
resizeSnapshot(snapshot, resizeFilter) {
return new Promise((resolve, reject) => {
const ffmpegArguments = ['-i', 'pipe:', '-frames:v', '1'];
if (resizeFilter) {
ffmpegArguments.push('-filter:v', ...resizeFilter.split(/\s+/));
}
ffmpegArguments.push('-f', 'image2', '-');
this.log.debug(
`Resize command: ${this.config.options.videoProcessor} ${ffmpegArguments.join(' ')}`,
this.accessory.displayName
);
const ffmpeg = spawn(this.config.options.videoProcessor, ffmpegArguments, {
env: process.env,
});
let resizeBuffer = Buffer.alloc(0);
ffmpeg.stdout.on('data', (data) => {
resizeBuffer = Buffer.concat([resizeBuffer, data]);
});
ffmpeg.on('error', (error) => {
reject(`FFmpeg process creation failed: ${error.message}`);
});
ffmpeg.on('close', () => {
resolve(resizeBuffer);
});
ffmpeg.stdin.end(snapshot);
});
}
async handleSnapshotRequest(request, callback) {
const resolution = this.determineResolution(request, true);
try {
const cachedSnapshot = !!this.snapshotPromise;
this.log.debug(`Snapshot requested: ${request.width} x ${request.height}`, this.accessory.displayName);
const snapshot = await (this.snapshotPromise || this.fetchSnapshot(resolution.snapFilter));
const resolutionText =
resolution.width > 0 && resolution.height > 0 ? `${resolution.width}x${resolution.height}` : 'native';
this.log.debug(
`Sending snapshot: ${resolutionText}${cachedSnapshot ? ' (cached)' : ''}`,
this.accessory.displayName
);
const resized = await this.resizeSnapshot(snapshot, resolution.resizeFilter);
callback(undefined, resized);
} catch (error) {
this.log.error(error, this.accessory.displayName, 'Homebridge');
callback(error);
}
}
async prepareStream(request, callback) {
const videoReturnPort = await this.reservePorts(1);
const videoSSRC = this.api.hap.CameraController.generateSynchronisationSource();
const audioReturnPort = await this.reservePorts(1);
const audioSSRC = this.api.hap.CameraController.generateSynchronisationSource();
const ipv6 = request.addressVersion === 'ipv6';
const sessionInfo = {
address: request.targetAddress,
ipv6: ipv6,
videoPort: request.video.port,
videoReturnPort: videoReturnPort,
videoCryptoSuite: request.video.srtpCryptoSuite,
videoSRTP: Buffer.concat([request.video.srtp_key, request.video.srtp_salt]),
videoSSRC: videoSSRC,
audioPort: request.audio.port,
audioReturnPort: audioReturnPort,
audioCryptoSuite: request.audio.srtpCryptoSuite,
audioSRTP: Buffer.concat([request.audio.srtp_key, request.audio.srtp_salt]),
audioSSRC: audioSSRC,
};
const response = {
video: {
port: videoReturnPort,
ssrc: videoSSRC,
srtp_key: request.video.srtp_key,
srtp_salt: request.video.srtp_salt,
},
audio: {
port: audioReturnPort,
ssrc: audioSSRC,
srtp_key: request.audio.srtp_key,
srtp_salt: request.audio.srtp_salt,
},
};
this.pendingSessions.set(request.sessionID, sessionInfo);
callback(undefined, response);
}
async startStream(request, callback) {
const controller = this.cameraUi.cameraController.get(this.accessory.displayName);
const sessionInfo = this.pendingSessions.get(request.sessionID);
if (sessionInfo) {
let inputChanged = false;
let prebufferInput = false;
const videoConfig = cameraUtils.generateVideoConfig(this.accessory.context.config.videoConfig);
let ffmpegInput = cameraUtils.generateInputSource(videoConfig).split(/\s+/);
ffmpegInput = cameraUtils.checkDeprecatedFFmpegArguments(controller?.media?.codecs?.ffmpegVersion, ffmpegInput);
if (!(await this.pingCamera())) {
// camera offline
ffmpegInput = ['-re', '-loop', '1', '-i', offlineImage];
inputChanged = true;
} else if (await this.getPrivacyState()) {
// privacy mode enabled
ffmpegInput = ['-re', '-loop', '1', '-i', privacyImage];
inputChanged = true;
} else {
// prebuffer
if (this.accessory.context.config.prebuffering && controller?.prebuffer) {
try {
this.log.debug('Setting prebuffer stream as input', this.accessory.displayName);
ffmpegInput = await controller.prebuffer.getVideo({
container: 'mpegts',
});
prebufferInput = true;
} catch (error) {
this.log.warn(
`Can not access prebuffer stream, skipping: ${error}`,
this.accessory.displayName,
'Homebridge'
);
}
}
}
/*if (!prebufferInput) {
const allowStream = controller ? controller.session.requestSession() : true;
if (!allowStream) {
// maxStream reached
ffmpegInput = ['-re', '-loop', '1', '-i', maxstreamsImage];
inputChanged = true;
}
}*/
let audioSourceFound = controller?.media.codecs.audio.length;
if (!audioSourceFound) {
if (videoConfig.audio) {
this.log.warn(
'Replacing audio with a dummy track, audio source not found or timed out during probe stream (stream). Disable "audio" to mute this warning.',
this.accessory.displayName,
'Homebridge'
);
}
ffmpegInput.push('-f', 'lavfi', '-i', 'anullsrc=cl=1', '-shortest');
}
const resolution = this.determineResolution(request.video, false);
const vcodec = videoConfig.vcodec;
const mtu = videoConfig.packetSize || 1316; // request.video.mtu is not used
let fps = videoConfig.maxFPS && videoConfig.forceMax ? videoConfig.maxFPS : request.video.fps;
let videoBitrate =
videoConfig.maxBitrate && videoConfig.forceMax ? videoConfig.maxBitrate : request.video.max_bit_rate;
let bufsize = request.video.max_bit_rate * 2;
let maxrate = request.video.max_bit_rate;
let encoderOptions = videoConfig.encoderOptions || '-preset ultrafast -tune zerolatency';
if (vcodec === 'copy') {
resolution.width = 0;
resolution.height = 0;
resolution.videoFilter = undefined;
fps = 0;
videoBitrate = 0;
bufsize = 0;
maxrate = 0;
encoderOptions = undefined;
}
const resolutionText =
vcodec === 'copy'
? 'native'
: `${resolution.width}x${resolution.height}, ${fps} fps, ${videoBitrate} kbps ${
videoConfig.audio ? ' (' + request.audio.codec + ')' : ''
}`;
this.log.info(`Starting video stream: ${resolutionText}`, this.accessory.displayName);
const ffmpegArguments = ['-hide_banner'];
if (videoConfig.debug) {
ffmpegArguments.push('-loglevel', 'verbose');
}
ffmpegArguments.push(...ffmpegInput);
if (!inputChanged && !prebufferInput && videoConfig.mapvideo) {
ffmpegArguments.push('-map', videoConfig.mapvideo);
} else {
ffmpegArguments.push('-an', '-sn', '-dn');
}
if (fps) {
ffmpegArguments.push('-r', fps);
}
ffmpegArguments.push(
'-vcodec',
inputChanged ? (vcodec === 'copy' ? 'libx264' : vcodec) : vcodec,
'-pix_fmt',
'yuv420p',
'-color_range',
'mpeg',
'-f',
'rawvideo'
);
if (encoderOptions) {
ffmpegArguments.push(...encoderOptions.split(/\s+/));
}
if (resolution.videoFilter) {
ffmpegArguments.push('-filter:v', ...resolution.videoFilter.split(/\s+/));
}
if (videoBitrate > 0) {
ffmpegArguments.push('-b:v', `${videoBitrate}k`);
}
if (bufsize > 0) {
ffmpegArguments.push('-bufsize', `${bufsize}k`);
}
if (maxrate > 0) {
ffmpegArguments.push('-maxrate', `${maxrate}k`);
}
ffmpegArguments.push(
'-payload_type',
request.video.pt,
'-ssrc',
sessionInfo.videoSSRC,
'-f',
'rtp',
'-srtp_out_suite',
'AES_CM_128_HMAC_SHA1_80',
'-srtp_out_params',
sessionInfo.videoSRTP.toString('base64'),
`srtp://${sessionInfo.address}:${sessionInfo.videoPort}?rtcpport=${sessionInfo.videoPort}&pkt_size=${mtu}`
);
if (videoConfig.audio && !inputChanged) {
if (
request.audio.codec === this.api.hap.AudioStreamingCodecType.OPUS ||
request.audio.codec === this.api.hap.AudioStreamingCodecType.AAC_ELD
) {
if (videoConfig.mapaudio && !prebufferInput) {
ffmpegArguments.push('-map', videoConfig.mapaudio.split(/\s+/));
} else {
ffmpegArguments.push('-vn', '-sn', '-dn');
}
if (request.audio.codec === this.api.hap.AudioStreamingCodecType.OPUS) {
ffmpegArguments.push('-acodec', 'libopus', '-application', 'lowdelay');
} else {
ffmpegArguments.push('-acodec', 'libfdk_aac', '-profile:a', 'aac_eld');
}
ffmpegArguments.push(
'-flags',
'+global_header',
'-f',
'null',
'-ar',
`${request.audio.sample_rate}k`,
'-b:a',
`${request.audio.max_bit_rate}k`,
'-ac',
request.audio.channel,
'-payload_type',
request.audio.pt,
'-ssrc',
sessionInfo.audioSSRC,
'-f',
'rtp',
'-srtp_out_suite',
'AES_CM_128_HMAC_SHA1_80',
'-srtp_out_params',
sessionInfo.audioSRTP.toString('base64'),
`srtp://${sessionInfo.address}:${sessionInfo.audioPort}?rtcpport=${sessionInfo.audioPort}&pkt_size=188`
);
} else {
this.log.error(
`Unsupported audio codec requested: ${request.audio.codec}`,
this.accessory.displayName,
'Homebridge'
);
}
}
ffmpegArguments.push('-progress', 'pipe:1');
const activeSession = {};
activeSession.socket = createSocket(sessionInfo.ipv6 ? 'udp6' : 'udp4');
activeSession.socket.on('error', (error) => {
this.log.error(`Socket error: ${error.message}`, this.accessory.displayName, 'Homebridge');
this.stopStream(request.sessionID);
});
activeSession.socket.on('message', () => {
if (activeSession.timeout) {
clearTimeout(activeSession.timeout);
}
activeSession.timeout = setTimeout(() => {
this.log.info('Device appears to be inactive. Stopping stream.', this.accessory.displayName);
this.controller.forceStopStreamingSession(request.sessionID);
this.stopStream(request.sessionID);
}, request.video.rtcp_interval * 5 * 1000);
});
activeSession.socket.bind(sessionInfo.videoReturnPort);
activeSession.mainProcess = new FfmpegProcess(
this.accessory.displayName,
videoConfig.debug,
request.sessionID,
this.config.options.videoProcessor,
ffmpegArguments,
this,
callback
);
if (videoConfig.audio && videoConfig.returnAudioTarget && !inputChanged) {
const ffmpegReturnArguments = ['-hide_banner'];
if (videoConfig.debug) {
ffmpegReturnArguments.push('-loglevel', 'verbose');
}
ffmpegReturnArguments.push(
'-protocol_whitelist',
'pipe,udp,rtp,file,crypto',
'-f',
'sdp',
'-c:a',
'libfdk_aac',
'-i',
'pipe:',
...videoConfig.returnAudioTarget.split(/\s+/)
);
const ipVersion = sessionInfo.ipv6 ? 'IP6' : 'IP4';
const sdpReturnAudio =
'v=0\r\n' +
'o=- 0 0 IN ' +
ipVersion +
' ' +
sessionInfo.address +
'\r\n' +
's=Talk\r\n' +
'c=IN ' +
ipVersion +
' ' +
sessionInfo.address +
'\r\n' +
't=0 0\r\n' +
'm=audio ' +
sessionInfo.audioReturnPort +
' RTP/AVP 110\r\n' +
'b=AS:24\r\n' +
'a=rtpmap:110 MPEG4-GENERIC/16000/1\r\n' +
'a=rtcp-mux\r\n' + // FFmpeg ignores this, but might as well
'a=fmtp:110 ' +
'profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3; ' +
'config=F8F0212C00BC00\r\n' +
'a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:' +
sessionInfo.audioSRTP.toString('base64') +
'\r\n';
activeSession.returnProcess = new FfmpegProcess(
this.accessory.displayName,
videoConfig.debug,
request.sessionID,
this.config.options.videoProcessor,
ffmpegReturnArguments,
this
);
activeSession.returnProcess.getStdin().end(sdpReturnAudio);
}
this.ongoingSessions.set(request.sessionID, activeSession);
this.pendingSessions.delete(request.sessionID);
} else {
this.log.error('Error finding session information.', this.accessory.displayName, 'Homebridge');
callback(new Error('Error finding session information'));
}
}
stopStream(sessionId) {
const session = this.ongoingSessions.get(sessionId);
if (session) {
if (session.timeout) {
clearTimeout(session.timeout);
}
try {
session.socket?.close();
} catch (error) {
this.log.error(`Error occurred closing socket: ${error}`, this.accessory.displayName, 'Homebridge');
}
try {
session.mainProcess?.stop();
} catch (error) {
this.log.error(
`Error occurred terminating main FFmpeg process: ${error}`,
this.accessory.displayName,
'Homebridge'
);
}
try {
session.returnProcess?.stop();
} catch (error) {
this.log.error(
`Error occurred terminating two-way FFmpeg process: ${error}`,
this.accessory.displayName,
'Homebridge'
);
}
this.ongoingSessions.delete(sessionId);
//const controller = this.cameraUi.cameraController.get(this.accessory.displayName);
//controller?.session.closeSession();
this.log.info('Stopped video stream.', this.accessory.displayName);
}
}
handleStreamRequest(request, callback) {
switch (request.type) {
case 'start': {
this.log.debug(
`Start stream requested: ${request.video.width}x${request.video.height}, ${request.video.fps} fps, ${request.video.max_bit_rate} kbps`,
this.accessory.displayName
);
this.startStream(request, callback);
break;
}
case 'reconfigure': {
this.log.debug(
`Reconfigure stream requested: ${request.video.width}x${request.video.height}, ${request.video.fps} fps, ${request.video.max_bit_rate} kbps (Ignored)`,
this.accessory.displayName
);
callback();
break;
}
case 'stop': {
this.log.debug('Stop stream requested', this.accessory.displayName);
this.stopStream(request.sessionID);
callback();
break;
}
}
}
async getPrivacyState() {
let privacy = false;
try {
const generalSettings = await this.cameraUi?.database?.interface.chain
.get('settings')
.get('general')
.cloneDeep()
.value();
const atHome = generalSettings?.atHome || false;
const excluded = generalSettings?.exclude || [];
if (atHome && !excluded.includes(this.accessory.displayName)) {
const camerasSettings = await this.cameraUi?.database?.interface.chain
.get('settings')
.get('cameras')
.find({ name: this.accessory.displayName })
.cloneDeep()
.value();
privacy = camerasSettings?.privacyMode || false;
}
} catch (error) {
this.log.info('An error occurred during getting atHome state, skipping..', this.accessory.displayName);
this.log.error(error, this.accessory.displayName, 'Homebridge');
}
return privacy;
}
async pingCamera() {
let state = true;
try {
state = await Ping.status(this.accessory.context.config, 1);
} catch {
// ignore
}
return state;
}
}