homebridge-loxone-proxy
Version:
Homebridge Dynamic Platform Plugin which exposes a Loxone System to Homekit.
172 lines • 7.24 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.PreBuffer = void 0;
const child_process_1 = require("child_process");
const events_1 = __importDefault(require("events"));
const net_1 = require("net");
const RecordingDelegate_1 = require("./RecordingDelegate");
const defaultPrebufferDuration = 15000;
class PreBuffer {
constructor(ffmpegInput, cameraName, videoProcessor, log) {
this.prebufferFmp4 = [];
this.events = new events_1.default();
this.released = false;
this.idrInterval = 0;
this.prevIdr = 0;
this.ffmpegInput = ffmpegInput;
this.cameraName = cameraName;
this.ffmpegPath = videoProcessor;
this.log = log;
}
async startPreBuffer() {
if (this.prebufferSession) {
return this.prebufferSession;
}
const vcodec = [
'-vcodec', 'libx264',
'-color_range', 'pc',
'-colorspace', 'bt470bg',
'-color_primaries', 'smpte170m',
'-color_trc', 'smpte170m',
'-preset', 'veryfast',
'-tune', 'zerolatency',
'-crf', '22',
'-g', '25',
'-keyint_min', '25',
'-sc_threshold', '0',
'-bf', '0',
'-force_key_frames', 'expr:gte(t,n_forced*1)',
'-vf', 'fps=25:round=down,scale=\'min(1280,iw)\':\'min(720,ih)\':force_original_aspect_ratio=decrease,scale=trunc(iw/2)*2:trunc(ih/2)*2',
'-an',
];
const fmp4OutputServer = (0, net_1.createServer)(async (socket) => {
fmp4OutputServer.close();
const parser = (0, RecordingDelegate_1.parseFragmentedMP4)(socket);
for await (const atom of parser) {
const now = Date.now();
if (!this.ftyp) {
this.ftyp = atom;
this.log.debug(`[${this.cameraName}] [PreBuffer] Received ftyp atom`);
}
else if (!this.moov) {
this.moov = atom;
this.log.info(`[${this.cameraName}] [PreBuffer] ✅ Received moov atom - ready for recording`);
}
else {
if (atom.type === 'mdat') {
if (this.prevIdr) {
this.idrInterval = now - this.prevIdr;
}
this.prevIdr = now;
}
this.prebufferFmp4.push({ atom, time: now });
}
while (this.prebufferFmp4.length && this.prebufferFmp4[0].time < now - defaultPrebufferDuration) {
this.prebufferFmp4.shift();
}
this.events.emit('atom', atom);
}
});
const fmp4Port = await (0, RecordingDelegate_1.listenServer)(fmp4OutputServer, this.log);
const ffmpegOutput = [
'-f', 'mp4',
...vcodec,
'-movflags', 'frag_keyframe+empty_moov+default_base_moof',
`tcp://127.0.0.1:${fmp4Port}`,
];
const debug = false;
const stdioValue = debug ? 'pipe' : 'ignore';
const ffmpegArgs = [...this.ffmpegInput, ...ffmpegOutput];
this.log.debug(`[${this.cameraName}] [PreBuffer] Spawn FFmpeg: ${this.ffmpegPath} ${ffmpegArgs.join(' ')}`);
const cp = (0, child_process_1.spawn)(this.ffmpegPath, ffmpegArgs, {
env: process.env,
stdio: ['ignore', stdioValue, 'pipe'],
});
cp.on('exit', (code, signal) => {
if (code === 0 || code === null) {
this.log.info(`[${this.cameraName}] [PreBuffer] FFmpeg exited successfully with code ${code}, signal ${signal}`);
}
else if (signal === 'SIGTERM' || signal === 'SIGKILL') {
this.log.debug(`[${this.cameraName}] [PreBuffer] FFmpeg exited with code ${code}, signal ${signal} (intentional kill)`);
}
else {
this.log.error(`[${this.cameraName}] [PreBuffer] FFmpeg exited with code ${code}, signal ${signal}`);
}
});
const terminate = () => {
if (!this.released) {
this.released = true;
this.events.emit('killed');
try {
cp.kill('SIGKILL');
}
catch (e) {
this.log.error(`[${this.cameraName}] [PreBuffer] Failed to kill FFmpeg: ${e}`);
}
}
};
process.once('SIGTERM', terminate);
process.once('SIGINT', terminate);
if (cp.stderr) {
cp.stderr.on('data', data => {
const output = data.toString();
if (output.toLowerCase().includes('error') && !output.toLowerCase().includes('deprecated')) {
this.log.error(`[${this.cameraName}] [PreBuffer] FFmpeg error: ${output.trim()}`);
}
});
}
this.prebufferSession = { server: fmp4OutputServer, process: cp };
return this.prebufferSession;
}
async getVideo(requestedPrebuffer) {
const waitUntil = Date.now() + 15000;
while (!this.moov && Date.now() < waitUntil) {
await new Promise(resolve => setTimeout(resolve, 50));
}
if (!this.moov) {
this.log.error(`[${this.cameraName}] [PreBuffer] Timeout: moov atom not available`);
throw new Error('moov atom not yet available');
}
const server = new net_1.Server(socket => {
server.close();
const writeAtom = (atom) => socket.write(Buffer.concat([atom.header, atom.data]));
if (this.ftyp) {
writeAtom(this.ftyp);
}
if (this.moov) {
writeAtom(this.moov);
}
const now = Date.now();
let needMoof = true;
for (const prebuffer of this.prebufferFmp4) {
if (prebuffer.time < now - requestedPrebuffer) {
continue;
}
if (needMoof && prebuffer.atom.type !== 'moof') {
continue;
}
needMoof = false;
writeAtom(prebuffer.atom);
}
this.events.on('atom', writeAtom);
const cleanup = () => {
this.events.removeListener('atom', writeAtom);
this.events.removeListener('killed', cleanup);
socket.removeAllListeners();
socket.destroy();
};
this.events.once('killed', cleanup);
socket.once('end', cleanup);
socket.once('close', cleanup);
socket.once('error', cleanup);
});
setTimeout(() => server.close(), 30000);
const port = await (0, RecordingDelegate_1.listenServer)(server, this.log);
return ['-f', 'mp4', '-i', `tcp://127.0.0.1:${port}`];
}
}
exports.PreBuffer = PreBuffer;
//# sourceMappingURL=Prebuffer.js.map