import { execa } from 'execa' import { tmpdir } from 'os' import path from 'node:path' import fs from 'node:fs' import os from 'node:os' export class VideoConcatError extends Error { constructor (msg) { super(msg || 'Failed to concatenate video') this.name = 'VideoConcatError' } } export default class Video { constructor (opts) { if (typeof opts.filePaths === 'undefined') throw new Error('Video must be called with opts.filePaths'); if (typeof opts.cwd === 'undefined') throw new Error('Video must be called with opts.cwd'); this.filePaths = opts.filePaths this.cwd = opts.cwd this.room = opts.room || 'projektmelody' this.execa = opts.execa || execa } getFilesTxt () { return this.filePaths .sort((a, b) => a.timestamp - b.timestamp) .map((d) => `file '${d.file}'`) .join('\n') .concat('\n') } getFilesFile () { const p = path.join(this.cwd, 'files.txt') fs.writeFileSync( p, this.getFilesTxt(this.filePaths), { encoding: 'utf-8' } ) return p } async concat () { const target = path.join(this.cwd, `${this.room}-chaturbate-${new Date().valueOf()}.mp4`) const { exitCode, killed, stdout, stderr } = await this.execa('ffmpeg', [ '-y', '-f', 'concat', '-safe', '0', '-i', this.getFilesFile(this.filePaths), '-c', 'copy', target ], { cwd: this.cwd }); if (exitCode !== 0 || killed !== false) { throw new VideoConcatError(`exitCode:${exitCode}, killed:${killed}, stdout:${stdout}, stderr:${stderr}`); } return target } }