homebridge-eufy-security-mikebcbc
Version:
Control Eufy Security from homebridge.
276 lines • 14.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.StreamingDelegate = void 0;
const ffmpeg_1 = require("../utils/ffmpeg");
const ffmpeg_params_1 = require("../utils/ffmpeg-params");
const eufy_security_client_1 = require("eufy-security-client");
const SnapshotManager_1 = require("./SnapshotManager");
const Talkback_1 = require("../utils/Talkback");
const utils_1 = require("../utils/utils");
const LocalLivestreamManager_1 = require("./LocalLivestreamManager");
class StreamingDelegate {
// eslint-disable-next-line max-len
constructor(camera) {
this.camera = camera;
this.platform = this.camera.platform;
this.device = this.camera.device;
this.cameraConfig = this.camera.cameraConfig;
this.hap = this.platform.api.hap;
this.api = this.platform.api;
this.log = this.platform.log;
this.cameraName = this.device.getName();
this.videoConfig = this.cameraConfig.videoConfig;
this.localLivestreamManager = new LocalLivestreamManager_1.LocalLivestreamManager(this);
this.snapshotManager = new SnapshotManager_1.SnapshotManager(this);
// keep track of sessions
this.pendingSessions = new Map();
this.ongoingSessions = new Map();
this.timeouts = new Map();
this.api.on("shutdown" /* APIEvent.SHUTDOWN */, () => {
for (const session in this.ongoingSessions) {
this.stopStream(session);
}
this.log.debug(this.cameraName, 'Streaming STOP! API shutdown!');
this.localLivestreamManager.stopLocalLiveStream();
});
}
setController(controller) {
this.controller = controller;
}
getLivestreamManager() {
return this.localLivestreamManager;
}
async handleSnapshotRequest(request, callback) {
this.log.debug(this.cameraName, 'handleSnapshotRequest');
try {
this.log.debug(this.cameraName, 'Snapshot requested: ' + request.width + ' x ' + request.height, this.videoConfig.debug);
const snapshot = await this.snapshotManager.getSnapshotBuffer(request);
this.log.debug(this.cameraName, 'snapshot byte lenght: ' + (snapshot === null || snapshot === void 0 ? void 0 : snapshot.byteLength));
callback(undefined, snapshot);
}
catch (err) {
this.log.error(this.cameraName, err);
callback();
}
}
async prepareStream(request, callback) {
const videoReturnPort = await ffmpeg_params_1.FFmpegParameters.allocateUDPPort();
const videoSSRC = this.hap.CameraController.generateSynchronisationSource();
const audioReturnPort = await ffmpeg_params_1.FFmpegParameters.allocateUDPPort();
const audioSSRC = this.hap.CameraController.generateSynchronisationSource();
const sessionInfo = {
address: request.targetAddress,
ipv6: request.addressVersion === '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) {
var _a;
this.log.debug(this.cameraName, 'Starting session with id: ' + request.sessionID);
const sessionInfo = this.pendingSessions.get(request.sessionID);
if (!sessionInfo) {
this.log.error(this.cameraName, 'Error finding session information.');
callback(new Error('Error finding session information'));
}
this.log.debug(this.cameraName, 'VIDEOCONFIG: ' + JSON.stringify(this.videoConfig));
try {
const activeSession = {};
// get streams
const videoParams = await ffmpeg_params_1.FFmpegParameters.create({ type: 'video', debug: this.videoConfig.debug });
videoParams.setup(this.cameraConfig, request);
videoParams.setRTPTarget(sessionInfo, request);
const useAudio = (request.audio.codec === "OPUS" /* AudioStreamingCodecType.OPUS */
|| request.audio.codec === "AAC-eld" /* AudioStreamingCodecType.AAC_ELD */)
&& this.videoConfig.audio;
if (!useAudio && this.videoConfig.audio) {
this.log.warn(this.cameraName, `An unsupported audio codec (type: ${request.audio.codec}) was requested. Audio streaming will be omitted.`);
}
let audioParams = undefined;
if (useAudio) {
audioParams = await ffmpeg_params_1.FFmpegParameters.create({ type: 'audio', debug: this.videoConfig.debug });
audioParams.setup(this.cameraConfig, request);
audioParams.setRTPTarget(sessionInfo, request);
}
const rtsp = (0, utils_1.is_rtsp_ready)(this.device, this.cameraConfig, this.log);
let streamData = null;
if (rtsp) {
const url = this.device.getPropertyValue(eufy_security_client_1.PropertyName.DeviceRTSPStreamUrl);
this.platform.log.debug(this.cameraName, 'RTSP URL: ' + url);
videoParams.setInputSource(url);
audioParams === null || audioParams === void 0 ? void 0 : audioParams.setInputSource(url);
}
else {
const value = await this.localLivestreamManager.getLocalLivestream()
.catch((err) => {
throw (this.cameraName + ' Unable to start the livestream: ' + err);
});
streamData = value;
videoParams.setInputSource('pipe:3');
audioParams === null || audioParams === void 0 ? void 0 : audioParams.setInputSource('pipe:4');
}
const videoProcess = new ffmpeg_1.FFmpeg(`[${this.cameraName}] [Video Process]`, audioParams ? [videoParams, audioParams] : [videoParams], this.platform.ffmpegLogger);
videoProcess.on('started', () => {
callback();
});
videoProcess.on('error', (err) => {
this.log.error(this.cameraName, 'Video process ended with error: ' + err);
this.stopStream(request.sessionID);
});
videoProcess.on('exit', () => {
this.log.info(this.cameraName, 'Video process ended');
this.stopStream(request.sessionID);
});
activeSession.videoProcess = videoProcess;
activeSession.videoProcess.start();
if (activeSession.videoProcess && activeSession.videoProcess.stdio) {
// stdio is defined and can be used
if (streamData !== null) {
streamData.videostream.pipe(activeSession.videoProcess.stdio[3]);
streamData.audiostream.pipe(activeSession.videoProcess.stdio[4]);
}
}
if (this.cameraConfig.talkback) {
const talkbackParameters = await ffmpeg_params_1.FFmpegParameters.create({ type: 'audio', debug: this.videoConfig.debug });
await talkbackParameters.setTalkbackInput(sessionInfo);
activeSession.talkbackStream = new Talkback_1.TalkbackStream(this.platform, this.device);
activeSession.returnProcess = new ffmpeg_1.FFmpeg(`[${this.cameraName}] [Talkback Process]`, [talkbackParameters], this.platform.ffmpegLogger);
activeSession.returnProcess.on('error', (err) => {
this.log.error(this.cameraName, 'Talkback process ended with error: ' + err);
this.stopStream(request.sessionID);
});
activeSession.returnProcess.on('exit', () => {
this.log.info(this.cameraName, 'Talkback process ended');
this.stopStream(request.sessionID);
});
activeSession.returnProcess.start();
(_a = activeSession.returnProcess.stdout) === null || _a === void 0 ? void 0 : _a.pipe(activeSession.talkbackStream);
}
// Check if the pendingSession has been stopped before it was successfully started.
const pendingSession = this.pendingSessions.get(request.sessionID);
// pendingSession has not been deleted. Transfer it to ongoingSessions.
if (pendingSession) {
this.ongoingSessions.set(request.sessionID, activeSession);
this.pendingSessions.delete(request.sessionID);
}
else { // pendingSession has been deleted. Add it to ongoingSession and end it immediately.
this.ongoingSessions.set(request.sessionID, activeSession);
this.log.info(this.cameraName, 'pendingSession has been deleted. Add it to ongoingSession and end it immediately.');
this.stopStream(request.sessionID);
}
}
catch (err) {
this.log.error(this.cameraName, 'Stream could not be started: ' + err);
callback(err);
this.pendingSessions.delete(request.sessionID);
}
}
/**
* Handles various types of streaming requests.
*
* This method delegates different types of streaming requests (like starting, reconfiguring, or stopping a stream)
* to their respective handlers. It enhances code organization by segregating different request handling logics into
* separate methods. This makes the code easier to read and maintain.
*
* @param {StreamingRequest} request - The streaming request to be handled.
* @param {StreamRequestCallback} callback - The callback to be invoked after handling the request.
*/
handleStreamRequest(request, callback) {
this.log.debug(this.cameraName, 'Receive Apple HK stream request' + JSON.stringify(request));
switch (request.type) {
case "start" /* StreamRequestTypes.START */:
this.startStream(request, callback);
break;
case "reconfigure" /* StreamRequestTypes.RECONFIGURE */:
callback();
break;
case "stop" /* StreamRequestTypes.STOP */:
this.stopStream(request.sessionID);
callback();
break;
}
}
/**
* Safely stops a specific resource associated with the streaming session.
*
* This method attempts to stop or clean up a given resource (like a stream or a socket) and handles any errors that
* may occur in the process. It's a general-purpose utility method to ensure that all resources are closed properly
* without causing unhandled exceptions. This improves the robustness of the resource management in streaming sessions.
*
* @param {string} resourceName - A descriptive name of the resource for logging purposes.
* @param {() => void} stopAction - A function encapsulating the logic to stop or clean up the resource.
*/
safelyStopResource(resourceName, stopAction) {
try {
stopAction();
}
catch (err) {
this.log.error(this.cameraName, `Error occurred terminating ${resourceName}: ${err}`);
}
}
/**
* Stops an ongoing streaming session.
*
* This method is responsible for stopping all processes and resources associated with a given streaming session.
* It first checks for any pending sessions with the provided session ID and removes them if found.
* Then, it proceeds to stop all active processes (video, audio, return processes) and resources (talkback stream, socket)
* associated with the ongoing session. Each resource is stopped safely using the `safelyStopResource` method, which handles
* any errors that may occur during the stopping process. If no session is found with the given ID, it logs that no session
* needs to be stopped.
*
* @param {string} sessionId - The unique identifier of the streaming session to be stopped.
*/
stopStream(sessionId) {
this.log.debug('Stopping session with id: ' + sessionId);
const pendingSession = this.pendingSessions.get(sessionId);
if (pendingSession) {
this.pendingSessions.delete(sessionId);
}
const session = this.ongoingSessions.get(sessionId);
if (session) {
this.safelyStopResource('talkbackStream', () => { var _a; return (_a = session.talkbackStream) === null || _a === void 0 ? void 0 : _a.stopTalkbackStream(); });
this.safelyStopResource('returnAudio FFmpeg process', () => {
var _a, _b, _c;
(_b = (_a = session.returnProcess) === null || _a === void 0 ? void 0 : _a.stdout) === null || _b === void 0 ? void 0 : _b.unpipe();
(_c = session.returnProcess) === null || _c === void 0 ? void 0 : _c.stop();
});
this.safelyStopResource('video FFmpeg process', () => { var _a; return (_a = session.videoProcess) === null || _a === void 0 ? void 0 : _a.stop(); });
this.safelyStopResource('audio FFmpeg process', () => { var _a; return (_a = session.audioProcess) === null || _a === void 0 ? void 0 : _a.stop(); });
if (!(0, utils_1.is_rtsp_ready)(this.device, this.cameraConfig, this.log)) {
this.log.debug(this.cameraName, 'Streaming STOP! Stream!');
this.safelyStopResource('Eufy Station livestream', () => this.localLivestreamManager.stopLocalLiveStream());
}
this.ongoingSessions.delete(sessionId);
this.log.info(this.cameraName, 'Stopped video stream.');
}
else {
this.log.debug(this.cameraName, 'No session to stop.');
}
}
}
exports.StreamingDelegate = StreamingDelegate;
//# sourceMappingURL=streamingDelegate.js.map