diff --git a/Nest_accfactory.js b/Nest_accfactory.js index 9b0f51e..6103491 100644 --- a/Nest_accfactory.js +++ b/Nest_accfactory.js @@ -112,13 +112,15 @@ // -- Config option for which H264 encoder to use. default is to just copy the stream // -- Better framerate output for HKSV recordings // -- re-code cam event snapshots +// -- simplified HKSV recording code loop +// -- can specify seperate h264 encoders for live view vs recording. Defaults now "copy" for live view and "libx264" for recording // // bugs // -- Sarting Jan 2020, google has enabled reCAPTCHA for Nest Accounts. Modfied code to no longer use user/name password login, but access token // Access token can be view by logging in to https//home.nest.com on webbrowser then in going to https://home.nest.com/session Seems access token expires every 30days // so needs manually updating (haven't seen it expire yet.....) // -// Version 23/7/2022 +// Version 3/8/2022 // Mark Hulskamp module.exports = accessories = []; @@ -171,7 +173,7 @@ var NexusStreamer = require("./nexusstreamer"); // Define constants const AccessoryName = "Nest"; const AccessoryPincode = "031-45-154"; -const USERAGENT = "iPhone iOS 15.4 Dropcam/5.67.0.6 com.nestlabs.jasper.release Darwin"; +const USERAGENT = "Nest/5.69.0 (iOScom.nestlabs.jasper.release) os=15.6"; const REFERER = "https://home.nest.com" const CAMERAAPIHOST = "https://webapi.camera.home.nest.com"; const NESTAPITIMEOUT = 10000; // Calls to Nest API timeout @@ -999,11 +1001,11 @@ class CameraClass { this.updateHomeKit(HomeKitAccessory, deviceData); // Do initial HomeKit update console.log("Setup %s '%s' on '%s'", HomeKitAccessory.displayName, thisServiceName, HomeKitAccessory.username, deviceData.HKSV == true ? "with HomeKit Secure Video" : this.MotionServices.length >= 1 ? "with motion sensor(s)" : ""); + console.log("Nest Aware subscription for '%s' is", HomeKitAccessory.username, (deviceData.nest_aware == true ? "active" : "not active")) } // Taken and adapted from https://github.com/hjdhjd/homebridge-unifi-protect/blob/eee6a4e379272b659baa6c19986d51f5bf2cbbbc/src/protect-ffmpeg-record.ts async *handleRecordingStreamRequest(streamId) { - var lastSegment = false; var header = Buffer.alloc(0); var bufferRemaining = Buffer.alloc(0); var dataLength = 0; @@ -1014,8 +1016,10 @@ class CameraClass { if (this.MotionServices[0].service.getCharacteristic(Characteristic.MotionDetected).value == true) { // Audio if enabled on doorbell/camera && audio recording configured for HKSV var includeAudio = (this.nestObject.nestDevices[this.deviceID].audio_enabled == true && this.controller.recordingManagement.recordingManagementService.getCharacteristic(Characteristic.RecordingAudioActive).value == Characteristic.RecordingAudioActive.ENABLE); - var recordCodec = this.nestObject.nestDevices[this.deviceID].H264Encoder; // Codec to use for H264 encoding + var recordCodec = this.nestObject.nestDevices[this.deviceID].H264EncoderRecord; // Codec to use for H264 encoding when recording + var recordCodec = VideoCodecs.COPY; + // Build our ffmpeg command string for the video stream var ffmpeg = "-hide_banner" + " -fflags +discardcorrupt" @@ -1143,56 +1147,37 @@ class CameraClass { this.NexusStreamer.startRecordStream("HKSV" + streamId, this.HKSVRecorder.ffmpeg, this.HKSVRecorder.video, this.HKSVRecorder.audio, true, 0); this.nestObject.config.debug.includes(Debugging.HKSV) && console.debug(getTimestamp() + " [HKSV] Recording started on '%s' %s %s", this.nestObject.nestDevices[this.deviceID].mac_address, (includeAudio == true ? "with audio" : "without audio"), (recordCodec != VideoCodecs.COPY ? "using H264 encoder " + recordCodec : "")); - for await (const mp4box of this.segmentGenerator()) { - // We'll process segments while motion is still active or ffmpeg process has exited - lastSegment = (this.MotionServices[0].service.getCharacteristic(Characteristic.MotionDetected).value == false || this.HKSVRecorder.ffmpeg == null); - - yield { - data: mp4box, - isLast: lastSegment, - }; - - if (lastSegment == true) { - // Active motion ended, so end recording - return; + // Loop generating either FTYP/MOOV box pairs or MOOF/MDAT box pairs for HomeKit Secure Video. + // Exit when the recorder process is nolonger running + // HAP-NodeJS can cancel this async generator function when recording completes also + var segment = []; + for(;;) { + if (this.HKSVRecorder.ffmpeg == null) { + // ffmpeg recorder process isnt running, so finish up the loop + break; + } + + if (this.HKSVRecorder.buffer == null || this.HKSVRecorder.buffer.length == 0) { + // since the ffmpeg recorder process hasn't notified us of any mp4 fragment boxes, so wait until there are some + await EventEmitter.once(this.events, MP4BOX); + } + + var mp4box = this.HKSVRecorder.buffer && this.HKSVRecorder.buffer.shift(); + if (mp4box == null || typeof mp4box != "object") { + // Not an mp4 fragment box, so try again + continue; } - } - } - - if (lastSegment == false && this.HKSVRecorder.ffmpeg == null) { - // Seems we have haven't sent last segment notification to HKSV (likely some failure?), so do so now. Will still generate a HDS error in log - yield { data: Buffer.alloc(0), isLast: true }; - return; - } - } - - // Taken from https://github.com/hjdhjd/homebridge-unifi-protect/blob/eee6a4e379272b659baa6c19986d51f5bf2cbbbc/src/protect-ffmpeg-record.ts - async *segmentGenerator() { - var segment = []; - - // Loop forever, generating either FTYP/MOOV box pairs or MOOF/MDAT box pairs for HomeKit Secure Video. - for(;;) { - if (this.HKSVRecorder.ffmpeg == null) { - // ffmpeg recorder process isnt running, so finish up - return; - } - if (this.HKSVRecorder.buffer == null || this.HKSVRecorder.buffer.length == 0) { - // since the ffmpeg recorder process hasn't notified us of any mp4 fragment boxes, so wait until there are some - await EventEmitter.once(this.events, MP4BOX); - } - - var mp4box = this.HKSVRecorder.buffer && this.HKSVRecorder.buffer.shift(); - if (mp4box == null || typeof mp4box != "object") { - // Not an mp4 fragment box, so try again - continue; - } - // Queue up this fragment mp4 box to send back to HomeKit. - segment.push(mp4box.header, mp4box.data); + // Queue up this fragment mp4 box to send back to HomeKit. + segment.push(mp4box.header, mp4box.data); - if (mp4box.type === "moov" || mp4box.type === "mdat") { - yield Buffer.concat(segment); - segment = []; + if (mp4box.type === "moov" || mp4box.type === "mdat") { + yield { + data: Buffer.concat(segment), + isLast: (this.MotionServices[0].service.getCharacteristic(Characteristic.MotionDetected).value == false || this.HKSVRecorder.ffmpeg == null) + }; + segment = []; + } } } } @@ -1351,11 +1336,11 @@ class CameraClass { delete this.pendingSessions[request.sessionID]; // remove this pending session information var includeAudio = (this.nestObject.nestDevices[this.deviceID].audio_enabled == true); - var streamCodec = this.nestObject.nestDevices[this.deviceID].H264Encoder; // Codec to use for H264 encoding + var streamCodec = this.nestObject.nestDevices[this.deviceID].H264EncoderLive; // Codec to use for H264 streaming encoding // Build our ffmpeg command string for the video stream var ffmpeg = "-hide_banner" - + " -use_wallclock_as_timestamps 1" + // + " -use_wallclock_as_timestamps 1" + " -fflags +discardcorrupt" + " -f h264 -an -thread_queue_size 1024 -i pipe:0" // Video data only on stdin + (includeAudio == true ? " -f aac -vn -thread_queue_size 1024 -i pipe:3" : ""); // Audio data only on extra pipe created in spawn command @@ -1416,8 +1401,9 @@ class CameraClass { } if (data.toString().includes("frame=") == false) { // Monitor ffmpeg output while testing. Use "ffmpeg as a debug option" - this.nestObject.config.debug.includes(Debugging.FFMPEG) && console.debug(getTimestamp() + " [FFMPEG]", data.toString()); + // this.nestObject.config.debug.includes(Debugging.FFMPEG) && console.debug(getTimestamp() + " [FFMPEG]", data.toString()); } + console.debug(data.toString()); }); ffmpegStreaming.on("exit", (code, signal) => { @@ -1810,7 +1796,8 @@ class NestClass extends EventEmitter { doorbellCooldown : 60000, // Default cooldown period for doorbell button press (1min/60secs) motionCooldown : 60000, // Default cooldown period for motion detected (1min/60secs) personCooldown : 120000, // Default cooldown person for person detected (2mins/120secs) - H264Encoder : VideoCodecs.COPY, // Default H264 Encoder for HKSV recording and streaming + H264EncoderRecord : VideoCodecs.LIBX264, // Default H264 Encoder for HKSV recording + H264EncoderLive : VideoCodecs.COPY, // Default H264 Encoder for HomeKit/HKSV live video mDNS : MDNSAdvertiser.BONJOUR, // Default mDNS advertiser for HAP-NodeJS library EveApp : true // Intergration with evehome app }; @@ -1850,24 +1837,38 @@ class NestClass extends EventEmitter { if (value.toUpperCase() == "AVAHI") this.config.mDNS = MDNSAdvertiser.AVAHI; // Use avahi as the mDNS advertiser } if (key == "H264ENCODER" && typeof value == "string") { - if (value.toUpperCase() == "LIBX264") this.config.H264Encoder = VideoCodecs.LIBX264; // Use libx264, software encoder - if (value.toUpperCase() == "H264_OMX") this.config.H264Encoder = VideoCodecs.H264_OMX; // Use the older RPI hardware h264 encoder + if (value.toUpperCase() == "LIBX264") { + this.config.H264EncoderRecord = VideoCodecs.LIBX264; // Use libx264, software encoder + this.config.H264EncoderLive = VideoCodecs.LIBX264; // Use libx264, software encoder + } + if (value.toUpperCase() == "H264_OMX") { + this.config.H264EncoderRecord = VideoCodecs.H264_OMX; // Use the older RPI hardware h264 encoder + this.config.H264EncoderLive = VideoCodecs.H264_OMX; // Use the older RPI hardware h264 encoder + } + } + if (key == "H264RECORDENCODER" && typeof value == "string") { + if (value.toUpperCase() == "LIBX264") this.config.H264EncoderRecord = VideoCodecs.LIBX264; // Use libx264, software encoder + if (value.toUpperCase() == "H264_OMX") this.config.H264EncoderRecord = VideoCodecs.H264_OMX; // Use the older RPI hardware h264 encoder + } + if (key == "H264STREAMENCODER" && typeof value == "string") { + if (value.toUpperCase() == "LIBX264") this.config.H264EncoderLive = VideoCodecs.LIBX264; // Use libx264, software encoder + if (value.toUpperCase() == "H264_OMX") this.config.H264EncoderLive = VideoCodecs.H264_OMX; // Use the older RPI hardware h264 encoder } if (key == "HKSVPREBUFFER" && typeof value == "number") { - if (value < 1000) value = value * 1000; // If less 1000, assume seconds value passed in, so convert to milliseconds + if (value < 1000) value = value * 1000; // If less than 1000, assume seconds value passed in, so convert to milliseconds this.config.HKSVPreBuffer = value; // Global HKSV pre-buffer sizing } if (key == "DOORBELLCOOLDOWN" && typeof value == "number") { - if (value < 1000) value = value * 1000; // If less 1000, assume seconds value passed in, so convert to milliseconds + if (value < 1000) value = value * 1000; // If less than 1000, assume seconds value passed in, so convert to milliseconds this.config.doorbellCooldown = value; // Global doorbell press cooldown time } if (key == "EVEAPP" && typeof value == "boolean") this.config.EveApp = value; // Evehome app integration if (key == "MOTIONCOOLDOWN" && typeof value == "number") { - if (value < 1000) value = value * 1000; // If less 1000, assume seconds value passed in, so convert to milliseconds + if (value < 1000) value = value * 1000; // If less than 1000, assume seconds value passed in, so convert to milliseconds this.config.motionCooldown = value; // Global motion detected cooldown time } if (key == "PERSONCOOLDOWN" && typeof value == "number") { - if (value < 1000) value = value * 1000; // If less 1000, assume seconds value passed in, so convert to milliseconds + if (value < 1000) value = value * 1000; // If less than 1000, assume seconds value passed in, so convert to milliseconds this.config.personCooldown = value; // Global person detected cooldown time } if (typeof value == "object") { @@ -1878,23 +1879,38 @@ class NestClass extends EventEmitter { if (subKey.toUpperCase() == "HKSV" && typeof value == "boolean") this.extraOptions[key]["HKSV"] = value; // HomeKit Secure Video for this device? if (subKey.toUpperCase() == "EVEAPP" && typeof value == "boolean") this.extraOptions[key]["EveApp"] = value; // Evehome app integration if (subKey.toUpperCase() == "H264ENCODER" && typeof value == "string") { - if (value.toUpperCase() == "LIBX264") this.extraOptions[key]["H264Encoder"] = VideoCodecs.LIBX264; // Use libx264, software encoder - if (value.toUpperCase() == "H264_OMX") this.extraOptions[key]["H264Encoder"] = VideoCodecs.H264_OMX; // Use the older RPI hardware h264 encoder + // Legacy option. Replaced by H264EncoderRecord and H264EncoderLive + if (value.toUpperCase() == "LIBX264") { + this.extraOptions[key]["H264EncoderRecord"] = VideoCodecs.LIBX264; // Use libx264, software encoder + this.extraOptions[key]["H264EncoderLive"] = VideoCodecs.LIBX264; // Use libx264, software encoder + } + if (value.toUpperCase() == "H264_OMX") { + this.extraOptions[key]["H264EncoderRecord"] = VideoCodecs.H264_OMX; // Use the older RPI hardware h264 encoder + this.extraOptions[key]["H264EncoderLive"] = VideoCodecs.H264_OMX; // Use the older RPI hardware h264 encoder + } + } + if (subKey.toUpperCase() == "H264RECORDENCODER" && typeof value == "string") { + if (value.toUpperCase() == "LIBX264") this.extraOptions[key]["H264EncoderRecord"] = VideoCodecs.LIBX264; // Use libx264, software encoder + if (value.toUpperCase() == "H264_OMX") this.extraOptions[key]["H264EncoderRecord"] = VideoCodecs.H264_OMX; // Use the older RPI hardware h264 encoder + } + if (subKey.toUpperCase() == "H264STREAMENCODER" && typeof value == "string") { + if (value.toUpperCase() == "LIBX264") this.extraOptions[key]["H264EncoderLive"] = VideoCodecs.LIBX264; // Use libx264, software encoder + if (value.toUpperCase() == "H264_OMX") this.extraOptions[key]["H264EncoderLive"] = VideoCodecs.H264_OMX; // Use the older RPI hardware h264 encoder } if (subKey.toUpperCase() == "HKSVPREBUFFER" && typeof value == "number") { - if (value < 1000) value = value * 1000; // If less 1000, assume seconds value passed in, so convert to milliseconds + if (value < 1000) value = value * 1000; // If less than 1000, assume seconds value passed in, so convert to milliseconds this.extraOptions[key]["HKSVPreBuffer"] = value; // HKSV pre-buffer sizing for this device } if (subKey.toUpperCase() == "DOORBELLCOOLDOWN" && typeof value == "number") { - if (value < 1000) value = value * 1000; // If less 1000, assume seconds value passed in, so convert to milliseconds + if (value < 1000) value = value * 1000; // If less than 1000, assume seconds value passed in, so convert to milliseconds this.extraOptions[key]["doorbellCooldown"] = value; // Doorbell press cooldown time for this device } if (subKey.toUpperCase() == "MOTIONCOOLDOWN" && typeof value == "number") { - if (value < 1000) value = value * 1000; // If less 1000, assume seconds value passed in, so convert to milliseconds + if (value < 1000) value = value * 1000; // If less than 1000, assume seconds value passed in, so convert to milliseconds this.extraOptions[key]["motionCooldown"] = value; // Motion detected cooldown time for this device } if (subKey.toUpperCase() == "PERSONCOOLDOWN" && typeof value == "number") { - if (value < 1000) value = value * 1000; // If less 1000, assume seconds value passed in, so convert to milliseconds + if (value < 1000) value = value * 1000; // If less than 1000, assume seconds value passed in, so convert to milliseconds this.extraOptions[key]["personCooldown"] = value; // Person detected cooldown time for this device } if (subKey.toUpperCase() == "HUMIDITYSENSOR" && typeof value == "boolean") this.extraOptions[key]["humiditySensor"] = value; // Seperate humidity sensor for this device. Only valid for thermostats @@ -2444,6 +2460,7 @@ class NestClass extends EventEmitter { this.nestDevices[camera.serial_number].mac_address = tempMACAddress; // Our created MAC address; this.nestDevices[camera.serial_number].description = camera.hasOwnProperty("description") ? __makeValidHomeKitName(camera.description) : ""; this.nestDevices[camera.serial_number].camera_uuid = deviceID; // Can generate from .nest_device_structure anyway + this.nestDevices[camera.serial_number].nest_aware = (camera.cvr_enrolled.toUpperCase() != "NONE"); // Does user have an active Nest aware subscription this.nestDevices[camera.serial_number].direct_nexustalk_host = camera.direct_nexustalk_host; this.nestDevices[camera.serial_number].websocket_nexustalk_host = camera.websocket_nexustalk_host; this.nestDevices[camera.serial_number].streaming_enabled = (camera.streaming_state.includes("enabled") ? true : false); @@ -2471,7 +2488,8 @@ class NestClass extends EventEmitter { // Insert any extra options we've read in from configuration file for this device this.nestDevices[camera.serial_number].EveApp = this.config.EveApp; // Global config option for EveHome App integration. Gets overriden below for specific doorbell/camera this.nestDevices[camera.serial_number].HKSV = this.config.HKSV; // Global config option for HomeKit Secure Video. Gets overriden below for specific doorbell/camera - this.nestDevices[camera.serial_number].H264Encoder = this.config.H264Encoder; // Global config option for using H264Encoder. Gets overriden below for specific doorbell/camera + this.nestDevices[camera.serial_number].H264EncoderRecord = this.config.H264EncoderRecord; // Global config option for using H264EncoderRecord. Gets overriden below for specific doorbell/camera + this.nestDevices[camera.serial_number].H264EncoderLive = this.config.H264EncoderLive; // Global config option for using H264EncoderLive. Gets overriden below for specific doorbell/camera this.nestDevices[camera.serial_number].HKSVPreBuffer = this.config.HKSVPreBuffer; // Global config option for HKSV pre buffering size. Gets overriden below for specific doorbell/camera this.nestDevices[camera.serial_number].doorbellCooldown = this.config.doorbellCooldown; // Global default for doorbell press cooldown. Gets overriden below for specific doorbell/camera this.nestDevices[camera.serial_number].motionCooldown = this.config.motionCooldown; // Global default for motion detected cooldown. Gets overriden below for specific doorbell/camera diff --git a/nexusstreamer.js b/nexusstreamer.js index bd945c6..472ee0d 100644 --- a/nexusstreamer.js +++ b/nexusstreamer.js @@ -5,7 +5,7 @@ // Cleaned up/recoded // // Mark Hulskamp -// 1/7/2022 +// 2/8/2022 // // done // -- switching camera stream on/off - going from off to on doesn't restart stream from Nest @@ -19,6 +19,8 @@ // -- fixes in buffering code. Will now correctly output requested buffer to multiple streams // -- refactor class definition // -- Changes to buffering timestamping +// -- get snapshot image from buffer if active +// -- re-connection fixes when playback has ended with error // // todo // -- When camera goes offline, we don't get notified straight away and video stream stops. Perhaps timer to go to camera off image if no data receieve in past 15 seconds? @@ -27,7 +29,6 @@ // -- audio echo with return audio // -- speed up live image stream starting when have a buffer active. Should almost start straight away // -- dynamic audio switching on/off from camera -// -- get snapshot image from buffer if active "use strict"; @@ -58,10 +59,11 @@ const CodecType = { }; const StreamProfile = { + AVPROFILE_MOBILE_1 : 1, + AVPROFILE_HD_MAIN_1 : 2, AUDIO_AAC : 3, AUDIO_SPEEX : 4, AUDIO_OPUS : 5, - AUDIO_OPUS_LIVE : 13, VIDEO_H264_50KBIT_L12 : 6, VIDEO_H264_530KBIT_L31 : 7, VIDEO_H264_100KBIT_L30 : 8, @@ -69,10 +71,9 @@ const StreamProfile = { VIDEO_H264_50KBIT_L12_THUMBNAIL : 10, META : 11, DIRECTORS_CUT : 12, + AUDIO_OPUS_LIVE : 13, VIDEO_H264_L31 : 14, - VIDEO_H264_L40 : 15, - AVPROFILE_MOBILE_1 : 1, - AVPROFILE_HD_MAIN_1 : 2, + VIDEO_H264_L40 : 15 }; const ErrorCode = { @@ -208,6 +209,7 @@ class NexusStreamer { this.pendingMessages = []; this.pendingBuffer = null; this.authorised = false; + this.pendingHostChange = false; this.streamQuality = StreamProfile.VIDEO_H264_2MBIT_L40; // Default streaming quaility this.timer = null; // Internal timer handle @@ -285,7 +287,7 @@ class NexusStreamer { if (this.buffer.active == false && this.socket == null) { // We not doing any buffering and there isnt an active socket connection, so startup connection to nexus this.debug && console.debug(getTimestamp() + " [NEXUS] Starting connection to '%s'", this.camera.direct_nexustalk_host); - this.#connect(this.camera.direct_nexustalk_host);; + this.#connect(this.camera.direct_nexustalk_host); } // Should have an active connection here now, so can add video/audio/talkback stream handles for our ffmpeg router to handle @@ -323,7 +325,7 @@ class NexusStreamer { if (this.buffer.active == false && this.socket == null) { // We not doing any buffering and/or there isnt an active socket connection, so startup connection to nexus - this.debug && console.debug(getTimestamp() + " [NEXUS] Starting connection to '%s''", this.camera.direct_nexustalk_host); + this.debug && console.debug(getTimestamp() + " [NEXUS] Starting connection to '%s'", this.camera.direct_nexustalk_host); this.#connect(this.camera.direct_nexustalk_host); } @@ -395,6 +397,7 @@ class NexusStreamer { this.camera.online = cameraData.online; this.camera.streaming_enabled = cameraData.streaming_enabled; if ((this.camera.online == false || this.camera.streaming_enabled == false) && this.socket != null) { + this.debug && console.debug(getTimestamp() + " [NEXUS] Camera went offline"); this.#close(true); // as offline or streaming not enabled, close socket } if ((this.camera.online == true && this.camera.streaming_enabled == true) && (this.socket == null && (this.buffer.active == true || this.buffer.streams.length > 0))) { @@ -402,6 +405,10 @@ class NexusStreamer { } } + if (this.camera.direct_nexustalk_host != cameraData.direct_nexustalk_host) { + this.debug && console.debug(getTimestamp() + " [NEXUS] Updated Nexusstreamer host '%s'", cameraData.direct_nexustalk_host); + } + this.camera = cameraData; // Update our internally stored copy of the camera details } } @@ -439,23 +446,10 @@ class NexusStreamer { return image; } - status() { - // Returns what we're currenting doing ie: - // Buffering, recording, livestreaming, idle - var statusArray = []; - if (this.buffer.active == true) statusArray.push("buffering"); - this.buffer.streams.forEach(stream => { - if (stream.type == "record") statusArray.push("recording"); - if (stream.type == "live") statusArray.push("livestream"); - }); - if (statusArray.length == 0) statusArray.push("idle"); - return statusArray; - } - #connect(host) { clearInterval(this.pingtimer); // Clear ping timer if was running - if (this.sessionID == null) this.sessionID = Math.floor(Math.random() * 100); // Random session ID + if (this.sessionID == null) this.sessionID = Math.floor(Math.random() * (100 - 1) + 1); // Random session ID bwteen 1 and 100 if (this.camera.streaming_enabled == true && this.camera.online == true) { if (typeof host == "undefined") { @@ -466,7 +460,7 @@ class NexusStreamer { this.socket = tls.connect({host: host, port: 1443}, () => { // Opened connection to Nexus server, so now need to authenticate ourselves this.host = host; // update internal host name since we've connected - this.debug && console.debug(getTimestamp() + " [NEXUS] Connection establised to '%s' with session ID '%s'", host, this.sessionID); + this.debug && console.debug(getTimestamp() + " [NEXUS] Connection established to '%s'", host); this.socket.setKeepAlive(true); // Keep socket connection alive this.#Authenticate(false); this.#startNexusData(); // start processing data @@ -481,7 +475,11 @@ class NexusStreamer { this.socket.on("error", (error) => { // Catch any socket errors to avoid code quitting // Our "close" handler will try reconnecting if needed - //this.debug && console.debug(getTimestamp() + " [NEXUS] Stocket error", error); + this.debug && console.debug(getTimestamp() + " [NEXUS] Stocket error", error); + }); + + this.socket.on("end", (error) => { + this.debug && console.debug(getTimestamp() + " [NEXUS] Stocket ended"); }); this.socket.on("data", (data) => { @@ -502,7 +500,7 @@ class NexusStreamer { this.debug && console.debug(getTimestamp() + " [NEXUS] Connection closed to '%s'", host); } if (hadError == false && this.playingBack == true && (this.buffer.active == true || this.buffer.streams.length > 0)) { - // No error, but the conenction closed without gracefully ending playback. + // No error, but the connection closed without gracefully ending playback. // We still have either active buffering occuring or output streams running // so attempt to restart connection to existing host this.debug && console.debug(getTimestamp() + " [NEXUS] Connection closed to '%s'. Attempting reconnection", host); @@ -516,7 +514,8 @@ class NexusStreamer { if (reconnect == true) { // Restart connection - this.#connect(this.camera.direct_nexustalk_host); // Connect back to main host to start process again + //this.#connect(this.camera.direct_nexustalk_host); // Connect back to main host to start process again + this.#connect(host); } }); } @@ -558,7 +557,8 @@ class NexusStreamer { stopBuffer.writeVarintField(1, this.sessionID); // session ID} this.#sendMessage(PacketType.STOP_PLAYBACK, stopBuffer.finish()); } - this.socket.end(); + //this.socket.end(); + this.socket.destroy(); } this.socket = null; this.sessionID = null; // Not an active session anymore @@ -663,7 +663,7 @@ class NexusStreamer { #sendMessage(type, buffer) { if (this.socket != null) { - if ((this.socket.connecting == true || this.socket.encrypted == false) || (type !== PacketType.HELLO && this.authorised == false)) { + if ((this.socket.readyState != "open") || (type !== PacketType.HELLO && this.authorised == false)) { this.pendingMessages.push({type, buffer}); return; } @@ -713,9 +713,9 @@ class NexusStreamer { helloBuffer.writeStringField(2, this.camera.camera_uuid); helloBuffer.writeBooleanField(3, false); // Doesnt required a connect camera helloBuffer.writeStringField(6, this.deviceID); // Random UUID v4 device ID - //helloBuffer.writeStringField(7, "Nest/5.67.0.6 (iOScom.nestlabs.jasper.release) os=15.4"); + //helloBuffer.writeStringField(7, "Nest/5.69.0 (iOScom.nestlabs.jasper.release) os=15.6"); //helloBuffer.writeVarintField(9, ClientType.IOS); - helloBuffer.writeStringField(7, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15"); + helloBuffer.writeStringField(7, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Safari/605.1.15"); helloBuffer.writeVarintField(9, ClientType.WEB); this.#sendMessage(PacketType.HELLO, helloBuffer.finish()); } @@ -763,7 +763,7 @@ class NexusStreamer { #handlePlaybackBegin(payload) { // Decode playback begin packet - this.debug && console.debug(getTimestamp() + " [NEXUS] Playback started from '%s'", this.host); + this.debug && console.debug(getTimestamp() + " [NEXUS] Playback started from '%s' with session ID '%s'", this.host, this.sessionID); var packet = payload.readFields(function(tag, obj, protoBuf) { if (tag === 1) obj.session_id = protoBuf.readVarint(); else if (tag === 2) obj.channels.push(protoBuf.readFields(function(tag, obj, protoBuf) { @@ -842,16 +842,18 @@ class NexusStreamer { // Normal playback ended ie: when we stopped playback this.debug && console.debug(getTimestamp() + " [NEXUS] Playback ended on '%s'", this.host); } - if (this.playingBack = true && (packet.reason == Reason.ERROR_TRANSCODE_NOT_AVAILABLE || packet.reason == Reason.PLAY_END_SESSION_COMPLETE)) { + + if (packet.reason != 0) { // Error during playback, so we'll attempt to restart by reconnection to host this.debug && console.debug(getTimestamp() + " [NEXUS] Playback ended on '%s' with error '%s'. Attempting reconnection", this.host, packet.reason); - + // Setup listener for socket close event. Once socket is closed, we'll perform the re-connection this.socket && this.socket.on("close", (hasError) => { - this.#connect(); // Re-connect to existing host + this.#connect(this.host); // try reconnection to existing host }); this.#close(false); // Close existing socket } + this.playingBack = false; // Playback ended } @@ -959,6 +961,7 @@ class NexusStreamer { } default: { + this.debug && console.debug(getTimestamp() + " [NEXUS] Data packet type '%s'", type); break } } diff --git a/package.json b/package.json index f9f12de..858896a 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "hap-nodejs": ">=0.10.3-beta", "axios": ">=0.22.0", "pbf": ">=3.2.1", + "ws": ">=7.5.6", "ffmpeg-for-homebridge" : ">=0.1.0" } -} +} \ No newline at end of file