homebridge-loxone-proxy
Version:
Homebridge Dynamic Platform Plugin which exposes a Loxone System to Homekit.
201 lines • 7.44 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.RecordingDelegate = exports.parseFragmentedMP4 = exports.readLength = exports.listenServer = exports.FRAGMENTS_LENGTH = exports.PREBUFFER_LENGTH = void 0;
const child_process_1 = require("child_process");
const net_1 = require("net");
const events_1 = require("events");
const prebuffer_1 = require("./prebuffer");
exports.PREBUFFER_LENGTH = 4000;
exports.FRAGMENTS_LENGTH = 4000;
async function listenServer(server) {
while (true) {
const port = 10000 + Math.round(Math.random() * 30000);
server.listen(port);
try {
await (0, events_1.once)(server, 'listening');
return server.address().port;
}
catch (e) {
}
}
}
exports.listenServer = listenServer;
async function readLength(readable, length) {
if (!length) {
return Buffer.alloc(0);
}
{
const ret = readable.read(length);
if (ret) {
return ret;
}
}
return new Promise((resolve, reject) => {
const r = () => {
const ret = readable.read(length);
if (ret) {
cleanup();
resolve(ret);
}
};
const e = () => {
cleanup();
reject(new Error(`stream ended during read for minimum ${length} bytes`));
};
const cleanup = () => {
readable.removeListener('readable', r);
readable.removeListener('end', e);
};
readable.on('readable', r);
readable.on('end', e);
});
}
exports.readLength = readLength;
async function* parseFragmentedMP4(readable) {
while (true) {
const header = await readLength(readable, 8);
const length = header.readInt32BE(0) - 8;
const type = header.slice(4).toString();
const data = await readLength(readable, length);
yield {
header,
length,
type,
data,
};
}
}
exports.parseFragmentedMP4 = parseFragmentedMP4;
class RecordingDelegate {
constructor(platform, ip) {
this.platform = platform;
this.log = platform.log;
this.hap = platform.api.hap;
this.cameraName = ip;
this.videoProcessor = 'ffmpeg';
platform.api.on("shutdown", () => {
var _a, _b;
if (this.preBufferSession) {
(_a = this.preBufferSession.process) === null || _a === void 0 ? void 0 : _a.kill();
(_b = this.preBufferSession.server) === null || _b === void 0 ? void 0 : _b.close();
}
});
}
updateRecordingActive(active) {
}
updateRecordingConfiguration(configuration) {
}
handleRecordingStreamRequest(streamId) {
throw new Error('Method not implemented.');
}
acknowledgeStream(streamId) {
throw new Error('Method not implemented.');
}
closeRecordingStream(streamId, reason) {
throw new Error('Method not implemented.');
}
async startPreBuffer() {
if (!this.preBuffer) {
this.preBuffer = new prebuffer_1.PreBuffer(this.cameraName, this.cameraName, this.videoProcessor);
if (!this.preBufferSession) {
this.preBufferSession = await this.preBuffer.startPreBuffer();
}
}
}
async *handleFragmentsRequests(configuration) {
this.log.debug('video fragments requested', this.cameraName);
const iframeIntervalSeconds = 4;
const audioArgs = [
'-acodec', 'libfdk_aac',
...(configuration.audioCodec.type === 0 ?
['-profile:a', 'aac_low'] :
['-profile:a', 'aac_eld']),
'-b:a', `${configuration.audioCodec.bitrate}k`,
'-ac', `${configuration.audioCodec.audioChannels}`,
];
const profile = 'main';
const level = '4.0';
const videoArgs = [
'-an',
'-sn',
'-dn',
'-codec:v',
'libx264',
'-pix_fmt',
'yuv420p',
'-profile:v', profile,
'-level:v', level,
'-force_key_frames', `expr:eq(t,n_forced*${iframeIntervalSeconds})`,
'-r', configuration.videoCodec.resolution[2].toString(),
];
const ffmpegInput = [];
const input = await this.preBuffer.getVideo(4000);
ffmpegInput.push(...input);
this.log.debug('Start recording...', this.cameraName);
const session = await this.startFFMPegFragmetedMP4Session(this.videoProcessor, ffmpegInput, audioArgs, videoArgs);
this.log.info('Recording started', this.cameraName);
const { socket, cp, generator } = session;
let pending = [];
let filebuffer = Buffer.alloc(0);
try {
for await (const box of generator) {
const { header, type, length, data } = box;
pending.push(header, data);
if (type === 'moov' || type === 'mdat') {
const fragment = Buffer.concat(pending);
filebuffer = Buffer.concat([filebuffer, Buffer.concat(pending)]);
pending = [];
yield fragment;
}
this.log.debug('mp4 box type ' + type + ' and lenght: ' + length, this.cameraName);
}
}
catch (e) {
this.log.info('Recoding completed. ' + e, this.cameraName);
}
finally {
socket.destroy();
cp.kill();
}
}
async startFFMPegFragmetedMP4Session(ffmpegPath, ffmpegInput, audioOutputArgs, videoOutputArgs) {
return new Promise(async (resolve) => {
const server = (0, net_1.createServer)(socket => {
server.close();
async function* generator() {
while (true) {
const header = await readLength(socket, 8);
const length = header.readInt32BE(0) - 8;
const type = header.slice(4).toString();
const data = await readLength(socket, length);
yield {
header,
length,
type,
data,
};
}
}
resolve({
socket,
cp,
generator: generator(),
});
});
const serverPort = await listenServer(server);
const args = [];
args.push(...ffmpegInput);
args.push('-f', 'mp4');
args.push(...videoOutputArgs);
args.push('-fflags', '+genpts', '-reset_timestamps', '1');
args.push('-movflags', 'frag_keyframe+empty_moov+default_base_moof', 'tcp://127.0.0.1:' + serverPort);
this.log.debug(ffmpegPath + ' ' + args.join(' '), this.cameraName);
const debug = false;
const stdioValue = debug ? 'pipe' : 'ignore';
this.process = (0, child_process_1.spawn)(ffmpegPath, args, { env: process.env, stdio: stdioValue });
const cp = this.process;
});
}
}
exports.RecordingDelegate = RecordingDelegate;
//# sourceMappingURL=RecordingDelegate.js.map