homebridge-eufy-security-mikebcbc
Version:
Control Eufy Security from homebridge.
157 lines • 7.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FFmpegRecord = void 0;
const child_process_1 = require("child_process");
const ffmpeg_progress_1 = require("./ffmpeg-progress");
const ffmpeg_params_1 = require("./ffmpeg-params");
const ffmpeg_1 = require("./ffmpeg");
const node_events_1 = require("node:events");
class FFmpegRecord extends ffmpeg_1.FFmpeg {
constructor(name, parameters, log) {
super(name, parameters, log);
this.recordingBuffer = [];
this.bufferRemaining = Buffer.alloc(0);
this.currentHeader = Buffer.alloc(0);
this.currentType = '';
this.currentDataLength = 0;
}
start() {
this.starttime = Date.now();
this.progress = new ffmpeg_progress_1.FFmpegProgress(this.parameters[0].progressPort);
this.progress.on('progress started', this.onProgressStarted.bind(this));
const processArgs = ffmpeg_params_1.FFmpegParameters.getRecordingArguments(this.parameters);
this.log.debug(`${this.name} Stream command: ${this.ffmpegExec} ${processArgs.join(' ')}`);
this.process = (0, child_process_1.spawn)(this.ffmpegExec, processArgs.join(' ').split(/\s+/), {
env: process.env,
stdio: ['inherit', 'pipe', 'inherit', 'pipe', 'pipe'],
});
// Set up the stderr, stdout and exit handlers
this.setupStderrHandler();
this.setupStdoutHandler();
this.setupExitHandler();
}
setupStderrHandler() {
var _a;
(_a = this.stderr) === null || _a === void 0 ? void 0 : _a.on('data', (chunk) => {
if (this.parameters[0].debug) {
this.log.debug(`${this.name} ffmpeg log message:\n ${chunk.toString()}`);
}
});
}
setupStdoutHandler() {
if (this.stdout) {
this.stdout.on('data', (data) => {
this.handleFFmpegOutput(data);
});
}
}
setupExitHandler() {
var _a;
(_a = this.process) === null || _a === void 0 ? void 0 : _a.on('exit', () => {
this.cleanupOnExit();
});
}
cleanupOnExit() {
var _a, _b, _c, _d;
this.log.debug(`${this.name} cleanupOnExit`);
this.emit('mp4box');
this.isEnded = true;
// Remove listeners
(_b = (_a = this.process) === null || _a === void 0 ? void 0 : _a.stderr) === null || _b === void 0 ? void 0 : _b.removeAllListeners('data');
(_d = (_c = this.process) === null || _c === void 0 ? void 0 : _c.stdout) === null || _d === void 0 ? void 0 : _d.removeAllListeners('data');
}
handleFFmpegOutput(buffer) {
// Initialize our variables that we need to process incoming FFmpeg packets.
let offset = 0;
// If we have anything left from the last buffer we processed, prepend it to this buffer.
if (this.bufferRemaining.length > 0) {
buffer = Buffer.concat([this.bufferRemaining, buffer]);
this.bufferRemaining = Buffer.alloc(0);
}
// FFmpeg is outputting an fMP4 stream that's suitable for HomeKit Secure Video. However, we can't just
// pass this stream directly back to HomeKit since we're using a generator-based API to send packets back to
// HKSV. Here, we take on the task of parsing the fMP4 stream that's being generated and split it up into the
// MP4 boxes that HAP-NodeJS is ultimately expecting.
for (;;) {
let data;
// The MP4 container format is well-documented and designed around the concept of boxes. A box (or atom as they
// used to be called), is at the center of an MP4 container. It's composed of an 8-byte header, followed by the data payload
// it carries.
// No existing header, let's start a new box.
if (!this.currentHeader.length) {
// Grab the header. The first four bytes represents the length of the entire box. Second four bytes represent the box type.
this.currentHeader = buffer.slice(0, 8);
// Now we retrieve the length of the box and subtract the length of the header to get the length of the data portion of the box.
this.currentDataLength = this.currentHeader.readUInt32BE(0) - 8;
// Get the type of the box. This is always a string and has a funky history to it that makes for an interesting read!
this.currentType = this.currentHeader.slice(4).toString();
// Finally, we get the data portion of the box.
data = buffer.slice(8, this.currentDataLength + 8);
offset = 8;
}
else {
// Grab the data from our buffer.
data = buffer.slice(0, this.currentDataLength);
offset = 0;
}
// If we don't have enough data in this buffer, save what we have for the next buffer we see and append it there.
if (data.length < this.currentDataLength) {
this.bufferRemaining = data;
break;
}
// Add it to our queue to be eventually pushed out through our generator function.
this.recordingBuffer.push({ data: data, header: this.currentHeader, length: this.currentDataLength, type: this.currentType });
this.emit('mp4box');
// Prepare to start a new box for the next buffer that we will be processing.
data = Buffer.alloc(0);
this.currentHeader = Buffer.alloc(0);
this.currentType = '';
// We've parsed an entire box, and there's no more data in this buffer to parse.
if (buffer.length === (offset + this.currentDataLength)) {
this.currentDataLength = 0;
break;
}
// If there's anything left in the buffer, move us to the new box and let's keep iterating.
buffer = buffer.slice(offset + this.currentDataLength);
this.currentDataLength = 0;
}
}
async *segmentGenerator() {
let segment = [];
for (;;) {
// FFmpeg has finished it's output - we're done.
if (this.isEnded) {
return;
}
// If the buffer is empty, wait for our FFmpeg process to produce more boxes.
if (!this.recordingBuffer.length) {
// eslint-disable-next-line no-await-in-loop
await (0, node_events_1.once)(this, 'mp4box');
}
// Grab the next fMP4 box from our buffer.
const box = this.recordingBuffer.shift();
// No fMP4 box, let's keep trying.
if (!box) {
continue;
}
this.log.debug(`${this.name} Yeah a Segment!`);
segment.push(box.header, box.data);
// What we want to send are two types of complete segments, made up of multiple MP4 boxes:
//
// - a complete MOOV box, usually with an accompanying FTYP box, that's sent at the very
// beginning of any valid fMP4 stream. HomeKit Secure Video looks for this before anything
// else.
//
// - a complete MOOF/MDAT pair. MOOF describes the sample locations and their sizes and MDAT contains the actual audio and video
// data related to that segment. This of MOOF as the audio/video data "header", and MDAT as the "payload".
//
// Once we see these, we combine all the segments in our queue to send back to HomeKit.
if ((box.type === 'moov') || (box.type === 'mdat')) {
yield Buffer.concat(segment);
segment = [];
}
}
}
}
exports.FFmpegRecord = FFmpegRecord;
//# sourceMappingURL=ffmpeg-record.js.map