homebridge-eufy-security
Version:
Control Eufy Security from homebridge.
116 lines • 5.09 kB
JavaScript
import { Deferred } from '../utils/utils.js';
const P2P_TIMEOUT_MS = 15_000;
const DUPLICATE_STREAM_GUARD_S = 5;
export class LocalLivestreamManager {
stationStream = null;
pending = null;
eufyClient;
log;
serialNumber;
constructor(camera) {
this.eufyClient = camera.platform.eufyClient;
this.serialNumber = camera.device.getSerial();
this.log = camera.log;
this.log.debug(`LocalLivestreamManager initialized for ${camera.device.getName()} (serial: ${this.serialNumber})`);
this.eufyClient.on('station livestream start', this.onStationLivestreamStart);
this.eufyClient.on('station livestream stop', this.onStationLivestreamStop);
}
/** Destroy active streams and reset state. */
destroyStreams() {
if (this.stationStream) {
this.stationStream.audiostream.unpipe();
this.stationStream.audiostream.destroy();
this.stationStream.videostream.unpipe();
this.stationStream.videostream.destroy();
this.stationStream = null;
}
}
/** Return the active livestream, or start a new one. Concurrent callers share the same pending request. */
async getLocalLiveStream() {
if (this.stationStream) {
const runtime = ((Date.now() - this.stationStream.createdAt) / 1000).toFixed(1);
this.log.debug(`Reusing livestream started ${runtime}s ago.`);
return this.stationStream;
}
return this.startLocalLiveStream();
}
/** True when a P2P livestream is currently active. */
isStreamActive() {
return this.stationStream !== null;
}
/**
* Requests a P2P livestream from the eufy station and waits for the
* 'station livestream start' event. If a start is already in progress,
* the caller piggy-backs on the existing promise instead of issuing a
* duplicate request.
*/
startLocalLiveStream() {
if (this.pending) {
this.log.debug('Livestream already starting — waiting on existing request.');
return this.pending.deferred.promise;
}
this.log.debug(`Starting station livestream for serial: ${this.serialNumber}...`);
const deferred = new Deferred();
const timer = setTimeout(() => {
this.log.error(`Livestream timeout: no P2P stream event received within ${P2P_TIMEOUT_MS / 1000}s for serial ${this.serialNumber}.`);
this.log.warn('If using a recent Node.js version, try enabling "Embedded PKCS1 Support" in the plugin settings.');
this.settlePending('reject', 'Livestream timeout — try enabling Embedded PKCS1 Support in settings.');
}, P2P_TIMEOUT_MS);
this.pending = { deferred, timer };
this.eufyClient.startStationLivestream(this.serialNumber).catch((err) => {
this.log.error(`startStationLivestream failed: ${err}`);
this.settlePending('reject', err);
});
return deferred.promise;
}
settlePending(action, payload) {
const p = this.pending;
if (!p)
return;
clearTimeout(p.timer);
this.pending = null;
if (action === 'resolve') {
p.deferred.resolve(payload);
}
else {
p.deferred.reject(payload instanceof Error ? payload : new Error(String(payload)));
this.stopLocalLiveStream();
}
}
stopLocalLiveStream() {
this.log.debug('Stopping station livestream.');
this.eufyClient.stopStationLivestream(this.serialNumber).catch((err) => {
this.log.warn(`stopStationLivestream failed: ${err}`);
});
this.destroyStreams();
}
/** True when the event belongs to this camera instance. */
isOwnDevice(device) {
return device.getSerial() === this.serialNumber;
}
onStationLivestreamStop = (_station, device) => {
if (!this.isOwnDevice(device))
return;
this.log.debug(`Station livestream for ${device.getName()} has stopped.`);
this.destroyStreams();
};
onStationLivestreamStart = (station, device, metadata, videostream, audiostream) => {
if (!this.isOwnDevice(device))
return;
// Guard against duplicate events fired in quick succession.
if (this.stationStream) {
const elapsed = (Date.now() - this.stationStream.createdAt) / 1000;
if (elapsed < DUPLICATE_STREAM_GUARD_S) {
this.log.warn('Duplicate livestream event received — ignoring.');
return;
}
}
// Tear down any prior stream before storing the new one.
this.destroyStreams();
this.log.debug(`${station.getName()} P2P livestream for ${device.getName()} started.`);
this.log.debug('Stream metadata:', JSON.stringify(metadata));
this.stationStream = { videostream, audiostream, createdAt: Date.now() };
this.settlePending('resolve', this.stationStream);
};
}
//# sourceMappingURL=LocalLivestreamManager.js.map