// npm import * as fs from 'fs'; import * as path from 'path'; import { v4 as uuid } from 'uuid'; import * as readline from 'readline'; import { execSync, spawn } from 'child_process'; // @ownzones import { MxfFileReader } from '@ownzones/imf'; // app import { Profiler } from './profiler'; import { Honeycomb } from './tracing'; export interface IDecodeOptions { width: number; height: number; yuvSuffix: string; bitDepth: string; chroma: string; } interface IJ2kDecoderOptions { height: number, width: number, editRate: number, pixFmt: string, bitDepth: string, startFrameIndex: number, lastFrameIndex: number, mxfFileReader: MxfFileReader, segmentId: string, } export class J2kDecoder { public readonly frameRate: number; public yuvPathWithSuffix?: string; public mjcPath?: string; public decodeOptions?: IDecodeOptions; private readonly mxfFileReader: MxfFileReader; private readonly segmentId; private readonly width: number; private readonly height: number; private readonly bitDepth: string; private readonly pixFmt: string; private yuvPath?: string; private reduce?: number; private readonly startFrameIndex: number; private readonly lastFrameIndex: number; public constructor(options: IJ2kDecoderOptions) { this.mxfFileReader = options.mxfFileReader; this.pixFmt = options.pixFmt; this.frameRate = options.editRate; this.segmentId = options.segmentId; this.width = options.width; this.height = options.height; this.bitDepth = options.bitDepth; this.startFrameIndex = options.startFrameIndex; this.lastFrameIndex = options.lastFrameIndex; } // ToDo Don't expose this. Just construct the object via a static async method. public async init(): Promise { this.mjcPath = await this.extractFrames(); this.yuvPath = path.join(__dirname, `../media/${uuid()}`); this.reduce = this.width >= 3840 ? 2 : (this.width >= 1920 ? 1 : 0); const width = this.width / 2 ** this.reduce; const height = this.height / 2 ** this.reduce; let chroma = ''; let { bitDepth } = this; const match = /(yuv|rgb)(422|444)?p?(\d*)(le|be)?/.exec(this.pixFmt); if (match) { if (match[1] === 'rgb') { chroma = 'RGB'; } else { chroma = match[2]; if (match[3]) { bitDepth = match[3]; } } } // the frame rate in the MJC header is hardcoded to 24fps const yuvSuffix = `_${width}x${height}_24_${bitDepth}b_${chroma}.yuv`; execSync(`mkfifo ${this.yuvPath}${yuvSuffix}`); this.yuvPathWithSuffix = `${this.yuvPath}${yuvSuffix}`; this.decodeOptions = { width, height, yuvSuffix, bitDepth, chroma, }; } public startKduExpand(): void { const params: string[] = [ '-disjoint_frames', '-i', `${this.mjcPath as string}`, '-o', `${this.yuvPath as string}.yuv`, ]; if (this.reduce) { params.push('-reduce'); params.push(this.reduce.toString(10)); } const child = spawn('kdu_v_expand', params, { stdio: ['ignore', 'pipe', 'pipe'] }); const output: string[] = []; readline.createInterface({ input: child.stdout }) .on('line', (line) => { // save the first lines, so we can extract the complete yuv file name. // If the path is long, it can appear on multiple lines. if (output.length < 10) { output.push(line); } if (output.length === 10) { const filePath = (/"([^"]+)"/.exec(output.join('')) as string[])[1]; if (filePath !== this.yuvPathWithSuffix) { child.kill(); // close the named pipe, so ffmpeg will fail fs.closeSync(fs.openSync(this.yuvPathWithSuffix as string, 'a')); } output.push(line); } }); } private async extractFrames(): Promise { const spanExtractMxf = Honeycomb.startSpan(path.basename(__filename), 'extractMxfFrames'); Profiler.tag(this.segmentId, 'extractMXFFramesStart'); const frames = await this.mxfFileReader.extractFrames( this.startFrameIndex, this.lastFrameIndex, ) as Buffer[]; Honeycomb.endSpan(spanExtractMxf); Profiler.tag(this.segmentId, 'extractMXFFramesEnd'); const mjcPath = path.join(__dirname, `../media/${uuid()}.mjc`); const spanMjcWrite = Honeycomb.startSpan('J2kDecoder', 'spanMjcWrite'); Profiler.tag(this.segmentId, 'mjcWriteStart'); const fileDescriptor = fs.openSync(mjcPath, 'w'); let colorSpaceFlag = 0x01; if (this.pixFmt.includes('rgb')) { colorSpaceFlag = 0x00; } fs.writeSync(fileDescriptor, Buffer.from([0x4D, 0x4A, 0x43, 0x32, 0x00, 0x00, 0x5D, 0xC0, 0x00, 0x00, 0x03, 0xE9, 0x00, 0x00, 0x00, colorSpaceFlag])); frames.forEach((frame) => { const lengthBuffer = Buffer.alloc(4); lengthBuffer.writeInt32BE(frame.length, 0); fs.writeSync(fileDescriptor, lengthBuffer); fs.writeSync(fileDescriptor, frame); }); fs.closeSync(fileDescriptor); Profiler.tag(this.segmentId, 'mjcWriteEnd'); Honeycomb.endSpan(spanMjcWrite); return mjcPath; } }