homebridge-nest-cam-ft
Version:
Nest cam plugin for homebridge: https://homebridge.io/
375 lines • 15.8 kB
JavaScript
"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