homebridge-nest-cam-ft
Version:
Nest cam plugin for homebridge: https://homebridge.io/
465 lines • 20.1 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.NexusStreamer = void 0;
const ws_1 = __importDefault(require("ws"));
const endpoints_1 = require("./endpoints");
const pbf_1 = __importDefault(require("pbf"));
const PlaybackPacket_1 = require("./protos/PlaybackPacket");
const Redirect_1 = require("./protos/Redirect");
const Hello_1 = require("./protos/Hello");
const AuthorizeRequest_1 = require("./protos/AuthorizeRequest");
const AudioPayload_1 = require("./protos/AudioPayload");
const StartPlayback_1 = require("./protos/StartPlayback");
const StopPlayback_1 = require("./protos/StopPlayback");
const PlaybackBegin_1 = require("./protos/PlaybackBegin");
const PlaybackEnd_1 = require("./protos/PlaybackEnd");
const Error_1 = require("./protos/Error");
var StreamQuality;
(function (StreamQuality) {
StreamQuality[StreamQuality["LOW"] = 1] = "LOW";
StreamQuality[StreamQuality["MEDIUM"] = 2] = "MEDIUM";
StreamQuality[StreamQuality["HIGH"] = 3] = "HIGH";
})(StreamQuality || (StreamQuality = {}));
const generateDeviceId = () => {
return 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'.replace('x', () => {
return Math.floor(Math.random() * 16).toString(16);
});
};
class NexusStreamer {
constructor(cameraInfo, accessToken, streamQuality, ffmpegVideo, ffmpegAudio, ffmpegReturnAudio, log, nestAuth) {
this.authorized = false;
this.videoStarted = false;
this.returnAudioStarted = false;
this.sessionID = Math.floor(Math.random() * 100);
this.pendingMessages = [];
this.pendingPlaybackPackets = [];
this.videoChannelID = -1;
this.audioChannelID = -1;
this.nestAuth = false;
this.log = log;
this.streamQuality = streamQuality;
this.ffmpegVideo = ffmpegVideo;
this.ffmpegAudio = ffmpegAudio;
this.ffmpegReturnAudio = ffmpegReturnAudio;
this.cameraInfo = cameraInfo;
this.accessToken = accessToken;
this.nestAuth = nestAuth || false;
this.setupConnection(cameraInfo.websocket_nexustalk_host);
}
stopPlayback() {
this.videoStarted = false;
if (this.socket) {
this.sendStopPlayback();
this.socket.close();
}
}
redirect(host) {
var _a;
const self = this;
(_a = this.socket) === null || _a === void 0 ? void 0 : _a.on('close', () => {
self.setupConnection(host);
this.startPlayback();
});
this.stopPlayback();
}
createReturnAudioServer() {
var _a;
const self = this;
const stdout = (_a = this.ffmpegReturnAudio) === null || _a === void 0 ? void 0 : _a.getStdout();
if (stdout) {
stdout.on('data', (chunk) => {
this.sendAudioPayload(Buffer.from(chunk));
if (this.returnAudioTimeout) {
clearTimeout(this.returnAudioTimeout);
}
this.returnAudioTimeout = setTimeout(() => {
self.sendAudioPayload(Buffer.from([]));
}, 500);
});
this.returnAudioStarted = true;
}
}
setupConnection(host) {
const self = this;
let pingInterval;
this.stopPlayback();
if (!this.returnAudioStarted) {
this.createReturnAudioServer();
}
this.socket = new ws_1.default(`wss://${host}/nexustalk`);
this.socket.on('open', () => {
var _a;
(_a = self.log) === null || _a === void 0 ? void 0 : _a.info('[NexusStreamer] Connected');
if (this.nestAuth) {
self.requestHello_nestAuth();
}
else {
self.requestHello();
}
pingInterval = setInterval(() => {
self.sendMessage(1, Buffer.alloc(0));
}, 15000);
});
this.socket.on('message', (data) => {
self.handleNexusData(data);
});
this.socket.on('close', () => {
var _a;
clearInterval(pingInterval);
(_a = self.log) === null || _a === void 0 ? void 0 : _a.info('[NexusStreamer] Disconnected');
});
this.socket.on('error', (error) => {
var _a;
(_a = self.log) === null || _a === void 0 ? void 0 : _a.error(`[NexusStreamer] Websocket error: ${error.message}`);
self.stopPlayback();
});
this.socket.on('unexpected-response', (request, response) => {
var _a;
(_a = self.log) === null || _a === void 0 ? void 0 : _a.error(`[NexusStreamer] Websocket unexpected response: ${response.statusCode}`);
self.stopPlayback();
});
}
processPendingMessages() {
if (this.pendingMessages) {
const messages = this.pendingMessages;
this.pendingMessages = [];
messages.forEach((message) => {
this.sendMessage(message.type, message.buffer);
});
}
}
sendMessage(type, buffer) {
var _a, _b, _c, _d, _e;
if (((_a = this.socket) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.CONNECTING) {
(_b = this.log) === null || _b === void 0 ? void 0 : _b.debug('waiting for socket to connect');
if (!this.pendingMessages) {
this.pendingMessages = [];
}
this.pendingMessages.push({
type,
buffer,
});
return;
}
if (type !== PlaybackPacket_1.PacketType.HELLO && !this.authorized) {
(_c = this.log) === null || _c === void 0 ? void 0 : _c.debug('waiting for authorization');
if (!this.pendingMessages) {
this.pendingMessages = [];
}
this.pendingMessages.push({
type,
buffer,
});
return;
}
let requestBuffer;
if (type === 0xcd) {
requestBuffer = Buffer.alloc(5);
requestBuffer[0] = type;
requestBuffer.writeUInt32BE(buffer.length, 1);
requestBuffer = Buffer.concat([requestBuffer, Buffer.from(buffer)]);
}
else {
requestBuffer = Buffer.alloc(3);
requestBuffer[0] = type;
requestBuffer.writeUInt16BE(buffer.length, 1);
requestBuffer = Buffer.concat([requestBuffer, Buffer.from(buffer)]);
}
if (((_d = this.socket) === null || _d === void 0 ? void 0 : _d.readyState) === ws_1.default.OPEN) {
(_e = this.socket) === null || _e === void 0 ? void 0 : _e.send(requestBuffer, () => {
});
}
}
requestHello() {
const token = {
olive_token: this.accessToken,
};
const tokenContainer = new pbf_1.default();
AuthorizeRequest_1.AuthorizeRequest.write(token, tokenContainer);
const tokenBuffer = tokenContainer.finish();
const request = {
protocol_version: Hello_1.Hello.ProtocolVersion.VERSION_3,
uuid: this.cameraInfo.uuid,
device_id: generateDeviceId(),
require_connected_camera: false,
user_agent: endpoints_1.NestEndpoints.USER_AGENT_STRING,
client_type: Hello_1.Hello.ClientType.WEB,
authorize_request: tokenBuffer,
};
const pbfContainer = new pbf_1.default();
Hello_1.Hello.write(request, pbfContainer);
const buffer = pbfContainer.finish();
this.sendMessage(PlaybackPacket_1.PacketType.HELLO, buffer);
}
requestHello_nestAuth() {
const request = {
protocol_version: Hello_1.Hello.ProtocolVersion.VERSION_3,
uuid: this.cameraInfo.uuid,
device_id: generateDeviceId(),
require_connected_camera: false,
user_agent: endpoints_1.NestEndpoints.USER_AGENT_STRING,
client_type: Hello_1.Hello.ClientType.WEB,
session_token: this.accessToken,
};
const pbfContainer = new pbf_1.default();
Hello_1.Hello.write(request, pbfContainer);
const buffer = pbfContainer.finish();
this.sendMessage(PlaybackPacket_1.PacketType.HELLO, buffer);
}
updateAuthentication() {
const token = {
olive_token: this.accessToken,
};
const tokenContainer = new pbf_1.default();
AuthorizeRequest_1.AuthorizeRequest.write(token, tokenContainer);
const tokenBuffer = tokenContainer.finish();
this.sendMessage(PlaybackPacket_1.PacketType.AUTHORIZE_REQUEST, tokenBuffer);
}
startPlayback() {
var _a, _b, _c, _d;
this.videoStarted = true;
let primaryProfile = PlaybackBegin_1.StreamProfile.VIDEO_H264_2MBIT_L40;
const otherProfiles = [];
this.cameraInfo.capabilities.forEach((element) => {
if (element.startsWith('streaming.cameraprofile')) {
const profile = element.replace('streaming.cameraprofile.', '');
otherProfiles.push(PlaybackBegin_1.StreamProfile[profile]);
}
});
let index = -1;
switch (this.streamQuality) {
case StreamQuality.LOW:
(_a = this.log) === null || _a === void 0 ? void 0 : _a.debug('Using LOW quality stream.');
primaryProfile = PlaybackBegin_1.StreamProfile.VIDEO_H264_100KBIT_L30;
index = otherProfiles.indexOf(PlaybackBegin_1.StreamProfile.VIDEO_H264_2MBIT_L40, 0);
if (index > -1) {
otherProfiles.splice(index, 1);
}
index = otherProfiles.indexOf(PlaybackBegin_1.StreamProfile.VIDEO_H264_530KBIT_L31, 0);
if (index > -1) {
otherProfiles.splice(index, 1);
}
break;
case StreamQuality.MEDIUM:
(_b = this.log) === null || _b === void 0 ? void 0 : _b.debug('Using MEDIUM quality stream.');
primaryProfile = PlaybackBegin_1.StreamProfile.VIDEO_H264_530KBIT_L31;
index = otherProfiles.indexOf(PlaybackBegin_1.StreamProfile.VIDEO_H264_2MBIT_L40, 0);
if (index > -1) {
otherProfiles.splice(index, 1);
}
break;
case StreamQuality.HIGH:
(_c = this.log) === null || _c === void 0 ? void 0 : _c.debug('Using HIGH quality stream.');
break;
default:
(_d = this.log) === null || _d === void 0 ? void 0 : _d.debug('Using HIGH quality stream.');
break;
}
this.cameraInfo.properties['audio.enabled'] && this.ffmpegAudio && otherProfiles.push(PlaybackBegin_1.StreamProfile.AUDIO_AAC);
const request = {
session_id: this.sessionID,
profile: primaryProfile,
other_profiles: otherProfiles,
};
const pbfContainer = new pbf_1.default();
StartPlayback_1.StartPlayback.write(request, pbfContainer);
const buffer = pbfContainer.finish();
this.sendMessage(PlaybackPacket_1.PacketType.START_PLAYBACK, buffer);
}
sendStopPlayback() {
const request = {
session_id: this.sessionID,
};
const pbfContainer = new pbf_1.default();
StopPlayback_1.StopPlayback.write(request, pbfContainer);
const buffer = pbfContainer.finish();
this.sendMessage(PlaybackPacket_1.PacketType.STOP_PLAYBACK, buffer);
}
sendAudioPayload(payload) {
const request = {
payload: payload,
session_id: this.sessionID,
codec: PlaybackBegin_1.CodecType.SPEEX,
sample_rate: 16e3,
};
const pbfContainer = new pbf_1.default();
AudioPayload_1.AudioPayload.write(request, pbfContainer);
const buffer = pbfContainer.finish();
this.sendMessage(PlaybackPacket_1.PacketType.AUDIO_PAYLOAD, buffer);
}
handleRedirect(payload) {
var _a;
const packet = Redirect_1.Redirect.read(payload);
if (packet.new_host) {
(_a = this.log) === null || _a === void 0 ? void 0 : _a.info('[NexusStreamer] Redirecting...');
this.redirect(packet.new_host);
}
}
handlePlaybackBegin(payload) {
const packet = PlaybackBegin_1.PlaybackBegin.read(payload);
if (packet.session_id !== this.sessionID) {
return;
}
for (let i = 0; i < packet.channels.length; i++) {
const stream = packet.channels[`${i}`];
if (stream.codec_type === PlaybackBegin_1.CodecType.H264) {
this.videoChannelID = stream.channel_id;
}
else if (stream.codec_type === PlaybackBegin_1.CodecType.AAC) {
this.audioChannelID = stream.channel_id;
}
}
}
handlePlaybackPacket(payload) {
const packet = PlaybackPacket_1.PlaybackPacket.read(payload);
if (!this.videoStarted) {
this.pendingPlaybackPackets.push(packet);
}
else if (this.pendingPlaybackPackets.length > 0) {
this.pendingPlaybackPackets.forEach((pkt) => {
this.processPlaybackPacket(pkt);
});
this.pendingPlaybackPackets = [];
}
else {
this.processPlaybackPacket(packet);
}
}
processPlaybackPacket(packet) {
var _a;
if (packet.channel_id === this.videoChannelID) {
const startCode = Buffer.from([0x00, 0x00, 0x00, 0x01]);
const stdin = this.ffmpegVideo.getStdin();
if (!(stdin === null || stdin === void 0 ? void 0 : stdin.writableEnded)) {
stdin === null || stdin === void 0 ? void 0 : stdin.write(Buffer.concat([startCode, Buffer.from(packet.payload)]), () => {
});
}
}
if (packet.channel_id === this.audioChannelID) {
const stdin = (_a = this.ffmpegAudio) === null || _a === void 0 ? void 0 : _a.getStdin();
if (!(stdin === null || stdin === void 0 ? void 0 : stdin.writableEnded)) {
stdin === null || stdin === void 0 ? void 0 : stdin.write(Buffer.from(packet.payload), () => {
});
}
}
}
handlePlaybackEnd(payload) {
var _a, _b, _c;
const packet = PlaybackEnd_1.PlaybackEnd.read(payload);
switch (packet.reason) {
case PlaybackEnd_1.PlaybackEnd.Reason.ERROR_PROFILE_NOT_AVAILABLE:
(_a = this.log) === null || _a === void 0 ? void 0 : _a.debug('[NexusStreamer] Playback Error: Profile not available');
break;
case PlaybackEnd_1.PlaybackEnd.Reason.ERROR_TIME_NOT_AVAILABLE:
(_b = this.log) === null || _b === void 0 ? void 0 : _b.error('[NexusStreamer] Playback Error: Time not available');
break;
case PlaybackEnd_1.PlaybackEnd.Reason.ERROR_TRANSCODE_NOT_AVAILABLE:
(_c = this.log) === null || _c === void 0 ? void 0 : _c.debug('[NexusStreamer] Playback Error: Transcode not available');
break;
default:
break;
}
}
handleNexusError(payload) {
var _a, _b;
const packet = Error_1.Error.read(payload);
if (packet.code === Error_1.ErrorCode.ERROR_AUTHORIZATION_FAILED) {
(_a = this.log) === null || _a === void 0 ? void 0 : _a.debug('[NexusStreamer] Updating authentication');
this.updateAuthentication();
}
else {
(_b = this.log) === null || _b === void 0 ? void 0 : _b.error(`[NexusStreamer] Error: ${packet.message}`);
this.stopPlayback();
}
}
handleNexusPacket(type, payload) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
switch (type) {
case PlaybackPacket_1.PacketType.PING:
(_a = this.log) === null || _a === void 0 ? void 0 : _a.debug('[NexusStreamer] Ping');
break;
case PlaybackPacket_1.PacketType.OK:
(_b = this.log) === null || _b === void 0 ? void 0 : _b.debug('[NexusStreamer] OK');
this.authorized = true;
this.processPendingMessages();
break;
case PlaybackPacket_1.PacketType.ERROR:
this.handleNexusError(payload);
break;
case PlaybackPacket_1.PacketType.PLAYBACK_BEGIN:
(_c = this.log) === null || _c === void 0 ? void 0 : _c.debug('[NexusStreamer] Playback Begin');
this.handlePlaybackBegin(payload);
break;
case PlaybackPacket_1.PacketType.PLAYBACK_END:
(_d = this.log) === null || _d === void 0 ? void 0 : _d.debug('[NexusStreamer] Playback End');
this.handlePlaybackEnd(payload);
break;
case PlaybackPacket_1.PacketType.PLAYBACK_PACKET:
this.handlePlaybackPacket(payload);
break;
case PlaybackPacket_1.PacketType.LONG_PLAYBACK_PACKET:
this.handlePlaybackPacket(payload);
break;
case PlaybackPacket_1.PacketType.CLOCK_SYNC:
(_e = this.log) === null || _e === void 0 ? void 0 : _e.debug('[NexusStreamer] Clock Sync');
break;
case PlaybackPacket_1.PacketType.REDIRECT:
(_f = this.log) === null || _f === void 0 ? void 0 : _f.debug('[NexusStreamer] Redirect');
this.handleRedirect(payload);
break;
case PlaybackPacket_1.PacketType.TALKBACK_BEGIN:
(_g = this.log) === null || _g === void 0 ? void 0 : _g.info('[NexusStreamer] Talkback Begin');
break;
case PlaybackPacket_1.PacketType.TALKBACK_END:
(_h = this.log) === null || _h === void 0 ? void 0 : _h.info('[NexusStreamer] Talkback End');
break;
default:
(_j = this.log) === null || _j === void 0 ? void 0 : _j.debug('[NexusStreamer] Unhandled Type: ' + type);
}
}
handleNexusData(data) {
var _a, _b;
if (this.pendingBuffer === undefined) {
this.pendingBuffer = data;
}
else {
this.pendingBuffer = Buffer.concat([this.pendingBuffer, data]);
}
let headerLength = 0;
let length = 0;
let type = 0;
try {
type = this.pendingBuffer.readUInt8();
if (type === PlaybackPacket_1.PacketType.LONG_PLAYBACK_PACKET) {
headerLength = 5;
length = this.pendingBuffer.readUInt32BE(1);
}
else {
headerLength = 3;
length = this.pendingBuffer.readUInt16BE(1);
}
}
catch (error) {
(_a = this.log) === null || _a === void 0 ? void 0 : _a.debug(`Buffer only had ${this.pendingBuffer.length} bytes. Skipping...`);
(_b = this.log) === null || _b === void 0 ? void 0 : _b.debug(error);
this.pendingBuffer = undefined;
return;
}
const payloadEndPosition = length + headerLength;
if (this.pendingBuffer.length >= payloadEndPosition) {
const rawPayload = this.pendingBuffer.slice(headerLength, payloadEndPosition);
const payload = new pbf_1.default(rawPayload);
this.handleNexusPacket(type, payload);
const remainingData = this.pendingBuffer.slice(payloadEndPosition);
this.pendingBuffer = undefined;
if (remainingData.length !== 0) {
this.handleNexusData(remainingData);
}
}
}
}
exports.NexusStreamer = NexusStreamer;
//# sourceMappingURL=streamer.js.map