UNPKG

homebridge-nest-cam-ft

Version:

Nest cam plugin for homebridge: https://homebridge.io/

375 lines 15.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.StreamingDelegate = void 0; const streamer_1 = require("./nest/streamer"); const endpoints_1 = require("./nest/endpoints"); const rtp_1 = require("./util/rtp"); const ffmpeg_1 = require("./ffmpeg"); const fs_1 = require("fs"); const path_1 = require("path"); const ffmpeg_for_homebridge_1 = __importDefault(require("ffmpeg-for-homebridge")); class StreamingDelegate { constructor(hap, camera, config, log) { var _a; this.ffmpegCodec = 'libx264'; this.ffmpegInstalled = true; this.ffmpegSupportsLibfdk_acc = true; this.ffmpegSupportsLibspeex = true; this.pendingSessions = {}; this.ongoingSessions = {}; this.ongoingStreams = {}; this.hap = hap; this.log = log; this.config = config; this.camera = camera; this.customFfmpeg = (_a = config.options) === null || _a === void 0 ? void 0 : _a.pathToFfmpeg; this.videoProcessor = this.customFfmpeg || ffmpeg_for_homebridge_1.default || 'ffmpeg'; (0, ffmpeg_1.getCodecsOutput)(this.videoProcessor) .then((output) => { var _a; const codec = (_a = config.options) === null || _a === void 0 ? void 0 : _a.ffmpegCodec; if (codec === 'copy' || (codec && output.includes(codec))) { this.ffmpegCodec = codec; } else { this.log.error(`Unknown video codec ${codec}. Defaulting to libx264.`); } this.ffmpegSupportsLibfdk_acc = output.includes('libfdk_aac'); this.ffmpegSupportsLibspeex = output.includes('libspeex'); }) .catch(() => { }); (0, ffmpeg_1.isFfmpegInstalled)(this.videoProcessor) .then((installed) => { this.ffmpegInstalled = installed; }) .catch(() => { }); } getOfflineImage(callback) { const log = this.log; (0, fs_1.readFile)((0, path_1.join)(__dirname, `../images/offline.jpg`), (err, data) => { if (err) { log.error(err.message); callback(err); } else { callback(undefined, data); } }); } handleSnapshotRequest(request, callback) { if (this.camera.info.properties['streaming.enabled']) { this.camera .getSnapshot(request.height) .then((snapshot) => { callback(undefined, snapshot); }) .catch((error) => { (0, endpoints_1.handleError)(this.log, error, `Error fetching snapshot for ${this.camera.info.name}`); callback(error); }); } else { this.getOfflineImage(callback); } } async prepareStream(request, callback) { const sessionId = request.sessionID; const targetAddress = request.targetAddress; const video = request.video; const videoPort = video.port; const returnVideoPort = (await (0, rtp_1.reservePorts)())[0]; const videoCryptoSuite = video.srtpCryptoSuite; const videoSrtpKey = video.srtp_key; const videoSrtpSalt = video.srtp_salt; const videoSSRC = this.hap.CameraController.generateSynchronisationSource(); const audio = request.audio; const audioPort = audio.port; const returnAudioPort = (await (0, rtp_1.reservePorts)())[0]; const twoWayAudioPort = (await (0, rtp_1.reservePorts)(2))[0]; const audioServerPort = (await (0, rtp_1.reservePorts)())[0]; const audioCryptoSuite = video.srtpCryptoSuite; const audioSrtpKey = audio.srtp_key; const audioSrtpSalt = audio.srtp_salt; const audioSSRC = this.hap.CameraController.generateSynchronisationSource(); const sessionInfo = { address: targetAddress, videoPort: videoPort, returnVideoPort: returnVideoPort, videoCryptoSuite: videoCryptoSuite, videoSRTP: Buffer.concat([videoSrtpKey, videoSrtpSalt]), videoSSRC: videoSSRC, audioPort: audioPort, returnAudioPort: returnAudioPort, twoWayAudioPort: twoWayAudioPort, rtpSplitter: new rtp_1.RtpSplitter(audioServerPort, returnAudioPort, twoWayAudioPort), audioCryptoSuite: audioCryptoSuite, audioSRTP: Buffer.concat([audioSrtpKey, audioSrtpSalt]), audioSSRC: audioSSRC, }; const response = { video: { port: returnVideoPort, ssrc: videoSSRC, srtp_key: videoSrtpKey, srtp_salt: videoSrtpSalt, }, audio: { port: audioServerPort, ssrc: audioSSRC, srtp_key: audioSrtpKey, srtp_salt: audioSrtpSalt, }, }; this.pendingSessions[sessionId] = sessionInfo; callback(undefined, response); } getVideoCommand(info, sessionId) { const sessionInfo = this.pendingSessions[sessionId]; const videoPort = sessionInfo.videoPort; const returnVideoPort = sessionInfo.returnVideoPort; const videoSsrc = sessionInfo.videoSSRC; const videoSRTP = sessionInfo.videoSRTP.toString('base64'); const address = sessionInfo.address; const bitrate = info.max_bit_rate * 4; const videoPayloadType = info.pt; const mtu = info.mtu; const output = [ '-payload_type', videoPayloadType.toString(), '-ssrc', videoSsrc.toString(), '-f', 'rtp', '-srtp_out_suite', 'AES_CM_128_HMAC_SHA1_80', '-srtp_out_params', videoSRTP, `srtp://${address}:${videoPort}?rtcpport=${videoPort}&localrtcpport=${returnVideoPort}&pkt_size=${mtu}`, ]; if (!this.camera.info.properties['streaming.enabled']) { return [ '-loop', '1', '-i', (0, path_1.join)(__dirname, `../images/offline.jpg`), '-c:v', this.ffmpegCodec, ...(this.ffmpegCodec === 'libx264' ? ['-preset', 'ultrafast', '-tune', 'zerolatency'] : []), '-pix_fmt', 'yuv420p', '-an', ...output, ]; } return [ '-f', 'h264', '-use_wallclock_as_timestamps', '1', '-r', '15', '-i', 'pipe:', '-c:v', this.ffmpegCodec, ...(this.ffmpegCodec === 'libx264' ? ['-preset', 'ultrafast', '-tune', 'zerolatency'] : []), '-bf', '0', '-b:v', `${bitrate}k`, '-bufsize', `${bitrate}k`, '-maxrate', `${2 * bitrate}k`, '-pix_fmt', 'yuv420p', '-an', ...output, ]; } getAudioCommand(info, sessionId) { const sessionInfo = this.pendingSessions[sessionId]; if (!sessionInfo) { return; } const address = sessionInfo.address; const audioPort = sessionInfo.audioPort; const returnAudioPort = sessionInfo.returnAudioPort; const audioSsrc = sessionInfo.audioSSRC; const audioSRTP = sessionInfo.audioSRTP.toString('base64'); const audioPayloadType = info.pt; const audioMaxBitrate = info.max_bit_rate; const sampleRate = info.sample_rate; return [ '-c:a', 'libfdk_aac', '-i', 'pipe:', '-c:a', 'libfdk_aac', '-profile:a', 'aac_eld', '-ac', '1', '-vn', '-ar', `${sampleRate}k`, '-b:a', `${audioMaxBitrate}k`, '-flags', '+global_header', '-payload_type', audioPayloadType.toString(), '-ssrc', audioSsrc.toString(), '-f', 'rtp', '-srtp_out_suite', 'AES_CM_128_HMAC_SHA1_80', '-srtp_out_params', audioSRTP, `srtp://${address}:${audioPort}?rtcpport=${audioPort}&localrtcpport=${returnAudioPort}&pkt_size=188`, ]; } getReturnAudioCommand(info, sessionId) { const sessionInfo = this.pendingSessions[sessionId]; if (!sessionInfo) { return; } return [ '-hide_banner', '-protocol_whitelist', 'pipe,udp,rtp,file,crypto', '-f', 'sdp', '-c:a', 'libfdk_aac', '-i', 'pipe:0', '-map', '0:0', '-c:a', 'libspeex', '-frames_per_packet', '4', '-ac', '1', '-vn', '-ar', `16k`, '-f', 'data', 'pipe:1', ]; } handleStreamRequest(request, callback) { var _a, _b, _c; const sessionId = request.sessionID; switch (request.type) { case "start": const sessionInfo = this.pendingSessions[sessionId]; const video = request.video; const audio = request.audio; const address = sessionInfo.address; const audioSRTP = sessionInfo.audioSRTP.toString('base64'); const twoWayAudioPort = sessionInfo.twoWayAudioPort; if (!this.ffmpegInstalled) { this.log.error('FFMPEG is not installed. Please install it and restart homebridge.'); callback(new Error('FFmpeg not installed')); break; } const videoffmpegCommand = this.getVideoCommand(video, sessionId); const ffmpegVideo = new ffmpeg_1.FfmpegProcess('VIDEO', videoffmpegCommand, this.log, this, sessionId, false, this.customFfmpeg, (error) => { callback(error); }); let ffmpegAudio; let ffmpegReturnAudio; if (this.camera.info.properties['audio.enabled'] && this.camera.info.properties['streaming.enabled']) { if (this.ffmpegSupportsLibfdk_acc) { const audioffmpegCommand = this.getAudioCommand(audio, sessionId); if (audioffmpegCommand) { ffmpegAudio = new ffmpeg_1.FfmpegProcess('AUDIO', audioffmpegCommand, this.log, this, sessionId, false, this.customFfmpeg); } if (this.ffmpegSupportsLibspeex) { const returnAudioffmpegCommand = this.getReturnAudioCommand(audio, sessionId); if (returnAudioffmpegCommand) { ffmpegReturnAudio = new ffmpeg_1.FfmpegProcess('RETURN AUDIO', returnAudioffmpegCommand, this.log, this, sessionId, false, this.customFfmpeg); const sdpReturnAudio = [ 'v=0', 'o=- 0 0 IN IP4 127.0.0.1', 's=Talk', `c=IN IP4 ${address}`, 't=0 0', 'a=tool:libavformat 58.38.100', `m=audio ${twoWayAudioPort} RTP/AVP 110`, 'b=AS:24', 'a=rtpmap:110 MPEG4-GENERIC/16000/1', 'a=fmtp:110 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3; config=F8F0212C00BC00', `a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:${audioSRTP}`, ].join('\n'); (_a = ffmpegReturnAudio.getStdin()) === null || _a === void 0 ? void 0 : _a.write(sdpReturnAudio); (_b = ffmpegReturnAudio.getStdin()) === null || _b === void 0 ? void 0 : _b.end(); } } else { this.log.error("This version of FFMPEG does not support the audio codec 'libspeex'. You may need to recompile FFMPEG using '--enable-libspeex' and restart homebridge."); } } else { this.log.error("This version of FFMPEG does not support the audio codec 'libfdk_aac'. You may need to recompile FFMPEG using '--enable-libfdk_aac' and restart homebridge."); } } if (this.camera.info.properties['streaming.enabled'] && this.pendingSessions[sessionId]) { const streamer = new streamer_1.NexusStreamer(this.camera.info, this.config.access_token, ((_c = this.config.options) === null || _c === void 0 ? void 0 : _c.streamQuality) || 3, ffmpegVideo, ffmpegAudio, ffmpegReturnAudio, this.log, this.config.nest_token !== undefined); streamer.startPlayback(); this.ongoingStreams[sessionId] = streamer; } this.ongoingSessions[sessionId] = [ffmpegVideo, ffmpegAudio, ffmpegReturnAudio]; break; case "reconfigure": this.log.debug('(Not implemented) Received request to reconfigure to: ' + JSON.stringify(request.video)); callback(); break; case "stop": this.stopStream(sessionId); callback(); break; } } stopStream(sessionId) { try { if (this.ongoingStreams[sessionId]) { const streamer = this.ongoingStreams[sessionId]; streamer.stopPlayback(); } if (this.ongoingSessions[sessionId]) { const ffmpegVideoProcess = this.ongoingSessions[sessionId][0]; ffmpegVideoProcess === null || ffmpegVideoProcess === void 0 ? void 0 : ffmpegVideoProcess.stop(); if (this.ongoingSessions[sessionId].length > 1) { const ffmpegAudioProcess = this.ongoingSessions[sessionId][1]; const ffmpegReturnAudioProcess = this.ongoingSessions[sessionId][2]; ffmpegAudioProcess === null || ffmpegAudioProcess === void 0 ? void 0 : ffmpegAudioProcess.stop(); ffmpegReturnAudioProcess === null || ffmpegReturnAudioProcess === void 0 ? void 0 : ffmpegReturnAudioProcess.stop(); } } const sessionInfo = this.pendingSessions[sessionId]; if (sessionInfo) { sessionInfo.rtpSplitter.close(); } delete this.pendingSessions[sessionId]; delete this.ongoingSessions[sessionId]; this.log.debug('Stopped streaming session!'); } catch (e) { this.log.error('Error occurred terminating the video process!'); this.log.error(e); } } } exports.StreamingDelegate = StreamingDelegate; //# sourceMappingURL=streaming-delegate.js.map