yuv-rx
Version:
Reading videos as Rx Observables of decoded frames
123 lines (122 loc) • 5.15 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.YuvParser = void 0;
const reader_1 = require("./reader");
const rxjs_1 = require("rxjs");
const frame_1 = require("./frame");
const y4m_1 = require("./y4m");
function parseFrame(data, header, frameHeader, fn) {
if (!frameHeader.startsWith('FRAME')) {
throw 'Invalid frame header';
}
const luma = header.lumaSize();
const chroma = header.chromaSize();
return {
header: {
fn: fn,
width: header.width,
height: header.height
},
colorPlanes: {
y: new frame_1.Gray(header.width, header.height, data.subarray(0, luma)),
u: new frame_1.Gray(header.chromaWidth(), header.chromaHeight(), data.subarray(luma, luma + chroma)),
v: new frame_1.Gray(header.chromaWidth(), header.chromaHeight(), data.subarray(luma + chroma))
}
};
}
class YuvParser {
constructor(options) {
this.options = options !== null && options !== void 0 ? options : {};
}
parseHeader(chunk) {
const headerString = chunk.toString('ascii');
if (this.options.verbose) {
console.log(`Video header: ${headerString} (${chunk})`);
}
return y4m_1.Y4MHeader.fromString(headerString);
}
nextChunk(buffer, state, frameSize) {
switch (state) {
case 'reading header':
case 'reading frame header':
const idx = buffer.indexOf('\n', 0, 'ascii');
return idx != -1 ? buffer.subarray(0, idx + 1) : null;
case 'reading frame':
if (frameSize == null) {
throw new Error('Illegal Y4M parser state: frame size should be known by now');
}
else {
return buffer.length >= frameSize ? buffer.subarray(0, frameSize) : null;
}
}
}
nextState(state) {
switch (state) {
case 'reading header':
return 'reading frame header';
case 'reading frame header':
return 'reading frame';
case 'reading frame':
return 'reading frame header';
}
}
readCustom(ffmpegArgs, options) {
return this.readFromStream(() => {
var _a;
return (0, reader_1.yuv4mpegStreamForCustomInput)((_a = this.options.ffmpeg) !== null && _a !== void 0 ? _a : 'ffmpeg', ffmpegArgs, Object.assign(Object.assign({}, this.options), (options !== null && options !== void 0 ? options : {})));
});
}
read(path, options) {
return this.readFromStream(() => {
var _a;
return (0, reader_1.yuv4mpegStreamForPath)((_a = this.options.ffmpeg) !== null && _a !== void 0 ? _a : 'ffmpeg', path, Object.assign(Object.assign({}, this.options), (options !== null && options !== void 0 ? options : {})));
});
}
readFromStream(createStream) {
return new rxjs_1.Observable(subscriber => {
let state = 'reading header';
let buffer = Buffer.allocUnsafe(0);
let header = null;
let frameHeader = null;
let fn = 0;
createStream().subscribe({
next: data => {
buffer = Buffer.concat([buffer, data]);
let chunk = null;
while ((chunk = this.nextChunk(buffer, state, header === null || header === void 0 ? void 0 : header.getFrameSize())) != null) {
buffer = buffer.subarray(chunk.length);
switch (state) {
case 'reading header':
header = this.parseHeader(chunk.subarray(0, -1));
break;
case 'reading frame header':
frameHeader = chunk.subarray(0, -1).toString('ascii');
if (this.options.verbose) {
console.log(`Frame ${fn} header: ${frameHeader}`);
}
break;
case 'reading frame':
subscriber.next(parseFrame(chunk, header, frameHeader, fn));
fn += 1;
break;
}
state = this.nextState(state);
}
},
error: subscriber.error,
complete: () => {
if (buffer.length != 0) {
subscriber.error(new Error(`Unparsed ${buffer.length} bytes in the end of the stream`));
}
else if (state == 'reading frame') {
subscriber.error(new Error('Parser finished while expecting frame data'));
}
else {
subscriber.complete();
}
}
});
});
}
}
exports.YuvParser = YuvParser;