UNPKG

homebridge-loxone-proxy

Version:

Homebridge Dynamic Platform Plugin which exposes a Loxone System to Homekit.

201 lines 7.44 kB
"use strict"; 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