UNPKG

homebridge-eufy-security-mikebcbc

Version:
276 lines 14.8 kB
"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