UNPKG

homebridge-nest-cam-ft

Version:

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

465 lines 20.1 kB
"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