// npm import * as fs from 'fs'; import * as path from 'path'; import * as _ from 'lodash'; import bluebird from 'bluebird'; import ffmpeg from '@ownzones/fluent-ffmpeg'; import { spawn } from 'child_process'; import { v4 as uuid } from 'uuid'; // @ownzones import { AcesFrameWrappedPictureElement, MxfFileReader } from '@ownzones/imf'; // app import { config, log } from '../config'; import { Nullable } from '../utils/types'; /** * Extract the ACES frames from a MXF container, transform them to REC709 color space and encode the stream to h264 */ export class AcesDecoder { public static async transform(input: string, output: string, ctlTransforms: string[]): Promise { // create an array with -ctl options const ctlOptions: string[] = []; for (const transform of ctlTransforms) { ctlOptions.push('-ctl'); ctlOptions.push(path.join(config.acesTransformsPath, transform)); } // run the crlrender command const ctlProcess = spawn( 'ctlrender', [...ctlOptions, input, output], { env: { ...process.env, CTL_MODULE_PATH: path.join(config.acesTransformsPath, 'lib') } }, ); return new Promise((resolve, reject) => { let error = ''; ctlProcess.stderr.on('data', (data) => { error += data; }); ctlProcess.on('close', (code) => { if (code !== 0) { reject(new Error(`CTLRender failed: ${error}`)); } resolve(); }); }); } public constructor(private mxfFileReader: MxfFileReader) {} public async decode(startIndex: number, endIndex: number): Promise { const tifList: string[] = []; const fileListPath = path.join(config.mediaPath, `${uuid()}.txt`); const frameIndexes = _.range(startIndex, endIndex + 1); await bluebird.map(frameIndexes, async (index) => { const frame = (await this.mxfFileReader.extractFrames(index, index, AcesFrameWrappedPictureElement.regex) as Buffer[])[0]; log.debug(`ACES frame #${index} was extracted from the mxf`); // save the frame to local disk const inputPath = path.join(config.mediaPath, `${uuid()}.exr`); try { fs.writeFileSync(inputPath, frame); // transform the aces frame to tif, using the rec709 CTL const tifPath = path.join(config.mediaPath, `${uuid()}.tif`); await AcesDecoder.transform(inputPath, tifPath, ['RRT.ctl', 'ODT.Academy.Rec709_100nits_dim.ctl']); tifList[index - startIndex] = `file ${tifPath}`; } catch (err) { log.error(`acesDecoder failed: ${JSON.stringify(err)}`); } finally { if (fs.existsSync(inputPath)) { fs.unlinkSync(inputPath); } } }, { concurrency: 10 }); if (tifList) { log.debug(`tiflist exists: ${JSON.stringify(tifList)}`); if (tifList.length) { log.debug(`tiflist exists and has content: ${JSON.stringify(tifList)}`); fs.writeFileSync(fileListPath, tifList.join('\n')); log.debug(`tiflist content: ${JSON.stringify(tifList)} wrote on :${fileListPath}`); } } const segmentPath = path.join(config.mediaPath, `${uuid()}.ts`); return new Promise((resolve, reject) => { const onFFmpegEnd = (tsSegmentPath: Nullable, error?: Error) => { // remove tiff files for (const line of tifList) { const tifPath = line.replace('file ', ''); if (fs.existsSync(tifPath)) { fs.unlinkSync(tifPath); } } if (fs.existsSync(fileListPath)) { fs.unlinkSync(fileListPath); } if (error) { return reject(error); } return resolve(tsSegmentPath as string); }; const outputOptions = [ '-muxdelay 0', // i see that muxing to mpegts adds delay, but found no reason for it '-copyts', '-c:v libx264', '-maxrate 5M', '-bufsize 3M', // why don't we specify target bitrate "-b:v" https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate '-bf', '0', '-f', 'mpegts', '-profile:v', 'main', '-level', '4.0', // this means at most 2MB/s, so why is maxrate 5MB/s https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels '-pix_fmt', 'yuv420p', ]; // const ffmpegCommand = ffmpeg({ logger: log }) .input(fileListPath) .inputOptions('-hide_banner') .outputOptions(outputOptions); ffmpegCommand .inputOptions('-safe 0') .inputOptions('-protocol_whitelist pipe,file,http,https,tcp,tls') .inputOptions('-r 24') .outputOptions('-r 24') .inputFormat('concat'); return ffmpegCommand .on('start', (cmdline: string) => { log.info(`command line: ${cmdline}`); }) .on('progress', (info) => { log.debug(info); }) .on('error', (error) => { log.error(error); onFFmpegEnd(null, error); }) .on('end', () => { onFFmpegEnd(segmentPath); }) .save(segmentPath); }); } }