UNPKG

homebridge-shinobi

Version:

A Homebridge plugin integrating Shinobi for motion detector cameras

269 lines 13.5 kB
"use strict"; /* eslint-disable no-case-declarations */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ShinobiStreamingDelegate = void 0; const ip_1 = __importDefault(require("ip")); const node_fetch_1 = __importDefault(require("node-fetch")); const child_process_1 = require("child_process"); /** * Shinobi Camera Streaming Delegate */ class ShinobiStreamingDelegate { constructor(platform, hap, monitor, config) { this.platform = platform; this.hap = hap; this.monitor = monitor; this.config = config; // keep track of sessions this.pendingSessions = {}; this.ongoingSessions = {}; let shinobiConfig = this.monitor.shinobiConfig; if (Array.isArray(shinobiConfig)) { shinobiConfig = this.monitor.shinobiConfig[0]; } this.platform.log.info(`creating ShinobiStreamingDelegate using shinobi config: ${JSON.stringify(shinobiConfig)}`); this.imageSource = `${this.platform.config.shinobi_api}${shinobiConfig.snapshot}`; // default to shinobi video source... this.videoSource = `${this.platform.config.shinobi_api}${shinobiConfig.streams[0]}`; const monitorDetails = JSON.parse(shinobiConfig.details); // ...but prefer to connect directly to stream if possible if (this.monitor.useSubStream) { if (monitorDetails.substream && monitorDetails.substream.input && monitorDetails.substream.input.fulladdress) { this.videoSource = monitorDetails.substream.input.fulladdress; this.platform.log.info(`ShinobiStreamingDelegate using shinobi ' + 'substream direct camera source: ${this.videoSource}`); } else { this.monitor.proxyStream = true; this.platform.log.info(`ShinobiStreamingDelegate using shinobi dynamic substream proxy source: ${this.videoSource}`); } } else { if (monitorDetails.auto_host) { this.videoSource = monitorDetails.auto_host; this.platform.log.info(`ShinobiStreamingDelegate using direct camera source: ${this.videoSource}`); } else { this.monitor.proxyStream = true; this.platform.log.info(`ShinobiStreamingDelegate using shinobi proxy source: ${this.videoSource}`); } } } handleSnapshotRequest(request, callback) { this.platform.log.debug('handleSnapshotRequest: ' + `${this.monitor.monitorConfig.monitor_id} => ${JSON.stringify(request)} from ${this.imageSource}`); (0, node_fetch_1.default)(this.imageSource) .then(res => res.buffer()) .then(buffer => { this.platform.log.debug('handleSnapshotRequest() success'); callback(undefined, buffer); }) .catch(err => { this.platform.log.error(`handleSnapshotRequest() error: ${err.message}`); callback(err); }); } // called when iOS requests rtp setup async prepareStream(request, callback) { this.platform.log.debug(`prepareStream: ${this.monitor.monitorConfig.monitor_id} => ${JSON.stringify(request)}`); const sessionId = request.sessionID; const targetAddress = request.targetAddress; const video = request.video; const videoPort = video.port; const videoSrtpKey = video.srtp_key; const videoSrtpSalt = video.srtp_salt; const videoSSRC = this.hap.CameraController.generateSynchronisationSource(); const sessionInfo = { address: targetAddress, videoPort: videoPort, videoSRTP: Buffer.concat([videoSrtpKey, videoSrtpSalt]), videoSSRC: videoSSRC }; const currentAddress = ip_1.default.address('public', request.addressVersion); const response = { address: currentAddress, video: { port: videoPort, ssrc: videoSSRC, srtp_key: videoSrtpKey, srtp_salt: videoSrtpSalt } }; this.pendingSessions[sessionId] = sessionInfo; await this.setRequiredSubStreamState(); this.platform.log.debug('prepareStream() success'); callback(undefined, response); } // called when iOS device asks stream to start/stop/reconfigure async handleStreamRequest(request, callback) { this.platform.log.debug(`handleStreamRequest: ${JSON.stringify(request)}`); const sessionId = request.sessionID; switch (request.type) { case "start" /* StreamRequestTypes.START */: const sessionInfo = this.pendingSessions[sessionId]; if (!sessionInfo) { const message = 'unknown sessionIdentifier: ' + `${this.monitor.monitorConfig.monitor_id} => ${sessionId} for start request!`; this.platform.log.warn(message); callback(new Error(message)); return; } const video = request.video; const width = video.width; const height = video.height; const fps = video.fps; const payloadType = video.pt; const maxBitrate = video.max_bit_rate; const mtu = video.mtu; const address = sessionInfo.address; const videoPort = sessionInfo.videoPort; const ssrc = sessionInfo.videoSSRC; const videoSRTP = sessionInfo.videoSRTP.toString('base64'); this.platform.log.debug(`requested video stream: ${width}x${height}, ${fps} fps, ${maxBitrate} kbps, ${mtu} mtu`); const ffmpegInputArgs = this.config.ffmpeg_input_args || '-fflags +genpts'; const ffmpegProcessArgs = this.config.ffmpeg_process_args || '-vsync drop -vcodec copy -an'; let ffmpegCommand = `${ffmpegInputArgs} -i ${this.videoSource} ${ffmpegProcessArgs} ` + `-f rtp -payload_type ${payloadType} -ssrc ${ssrc}`; ffmpegCommand += ` -srtp_out_suite AES_CM_128_HMAC_SHA1_80 -srtp_out_params ${videoSRTP}`; ffmpegCommand += ` srtp://${address}:${videoPort}` + `?rtcpport=${videoPort}&localrtcpport=${videoPort}&pkt_size=${mtu}`; this.platform.log.debug(ffmpegCommand); let started = false; const ffmpegProcess = (0, child_process_1.spawn)('ffmpeg', ffmpegCommand.split(' '), { env: process.env }); ffmpegProcess.stderr.on('data', () => { if (!started) { started = true; this.platform.log.debug('ffmpeg received first frame'); // do not forget to execute callback once set up callback(); } }); ffmpegProcess.on('error', error => { this.platform.log.error(`failed to start video stream: ${error.message}`); callback(new Error('ffmpeg process creation failed!')); }); ffmpegProcess.on('exit', (code, signal) => { const message = `ffmpeg exited with code: ${code} and signal: ${signal}`; if (code === null || code === 255) { this.platform.log.debug(`${message} (Video stream stopped!)`); } else { this.platform.log.error(`${message} (error)`); if (!started) { callback(new Error(message)); } else { this.controller.forceStopStreamingSession(sessionId); } } }); this.ongoingSessions[sessionId] = ffmpegProcess; delete this.pendingSessions[sessionId]; this.platform.log.debug('handleStreamRequest() START success'); break; case "reconfigure" /* StreamRequestTypes.RECONFIGURE */: // not supported this.platform.log.warn(`received (unsupported) request to reconfigure to: ${JSON.stringify(request.video)}`); callback(); break; case "stop" /* StreamRequestTypes.STOP */: const existingFfmpegProcess = this.ongoingSessions[sessionId]; if (!existingFfmpegProcess) { const message = `unknown sessionIdentifier: ${this.monitor.monitorConfig.monitor_id} => ${sessionId} for stop request!`; this.platform.log.warn(message); callback(new Error(message)); return; } this.platform.log.debug(`killing: ${this.monitor.monitorConfig.monitor_id} ` + `=> ${sessionId} => PID: ${existingFfmpegProcess.pid}`); try { if (existingFfmpegProcess) { existingFfmpegProcess.kill('SIGKILL'); } } catch (e) { this.platform.log.error('error occurred terminating the video process! ' + e); } delete this.ongoingSessions[sessionId]; this.platform.log.debug('stopped streaming session!'); await this.setRequiredSubStreamState(); callback(); break; } } // called when Homebridge is shutting down async shutdown() { Object.keys(this.ongoingSessions).forEach((sessionId) => { const ffmpegProcess = this.ongoingSessions[sessionId]; this.platform.log.debug(`killing: ${this.monitor.monitorConfig.monitor_id} => ${sessionId} => PID: ${ffmpegProcess.pid}`); try { if (ffmpegProcess) { ffmpegProcess.kill('SIGKILL'); } } catch (e) { this.platform.log.error('error occurred terminating the video process! ' + e); } }); this.ongoingSessions = {}; this.pendingSessions = {}; await this.setRequiredSubStreamState(); } shouldSubStreamBeActive() { const ongoingSessionsSessionsExist = (Object.keys(this.ongoingSessions).length > 0); const pendingSessionsExist = (Object.keys(this.pendingSessions).length > 0); return ongoingSessionsSessionsExist || pendingSessionsExist; } getSubStreamIsActive() { const url = `${this.platform.config.shinobi_api}/${this.platform.config.api_key}/monitor/` + `${this.platform.config.group_key}/${this.monitor.monitorConfig.monitor_id}`; this.platform.log.debug(`fetching from Shinobi API: ${url}`); return (0, node_fetch_1.default)(url) .then(res => res.json()) .then(shinobiConfig => { if (Array.isArray(shinobiConfig)) { shinobiConfig = shinobiConfig[0]; } return shinobiConfig.subStreamActive; }) .catch(err => { this.platform.log.error(`getSubStreamIsActive() error: ${err.message}`); }); } toggleSubStreamActive(subStreamShouldBeActive) { const url = `${this.platform.config.shinobi_api}/${this.platform.config.api_key}/toggleSubstream/` + `${this.platform.config.group_key}/${this.monitor.monitorConfig.monitor_id}`; this.platform.log.debug(`fetching from Shinobi API: ${url}`); return (0, node_fetch_1.default)(url) .then(res => res.json()) .then(shinobiResponse => { this.platform.log.debug(`substream toggle response: ${JSON.stringify(shinobiResponse)}`); if (shinobiResponse.ok !== subStreamShouldBeActive) { throw Error(`Unable to set substream active state: ${subStreamShouldBeActive}, ` + `response: ${JSON.stringify(shinobiResponse)}`); } }) .catch(err => { this.platform.log.error(`toggleSubStreamActive() error: ${err.message}`); }); } // do nothing if proxy dynamic sub-stream usage is not in use, otherwise if at least one pending or ongoing // session exists then ensure the sub-stream state in shinobi is active, otherwise ensure it is not active async setRequiredSubStreamState() { if (!(this.monitor.useSubStream && this.monitor.proxyStream)) { return; } const subStreamShouldBeActive = this.shouldSubStreamBeActive(); this.platform.log.debug(`subStreamShouldBeActive: ${subStreamShouldBeActive}`); const subStreamIsActive = await this.getSubStreamIsActive(); this.platform.log.debug(`subStreamIsActive: ${subStreamIsActive}`); if (subStreamIsActive !== subStreamShouldBeActive) { await this.toggleSubStreamActive(subStreamShouldBeActive); } } } exports.ShinobiStreamingDelegate = ShinobiStreamingDelegate; //# sourceMappingURL=shinobiStreamingDelegate.js.map