@jrmc/adonis-attachment
Version:
Turn any field on your Lucid model to an attachment data type
116 lines (115 loc) • 4.08 kB
JavaScript
/**
* @jrmc/adonis-attachment
*
* @license MIT
* @copyright Jeremy Chaufourier <jeremy@chaufourier.fr>
*/
import os from 'node:os';
import path from 'node:path';
import { $, ExecaError } from 'execa';
import { cuid } from '@adonisjs/core/helpers';
import logger from '@adonisjs/core/services/logger';
import { attachmentManager } from '@jrmc/adonis-attachment';
import { secondsToTimeFormat } from '../utils/helpers.js';
export default class FFmpeg {
input;
#ffmpegPath;
#ffprobePath;
#timeout;
#TIMEOUT;
constructor(input) {
this.input = input;
this.#ffmpegPath = 'ffmpeg';
this.#ffprobePath = 'ffprobe';
this.#timeout = null;
this.#TIMEOUT = attachmentManager.getConfig().timeout || 30_000;
}
#createAbortController() {
this.#cleanup();
const controller = new AbortController();
this.#timeout = setTimeout(() => {
controller.abort();
}, this.#TIMEOUT);
return controller;
}
#cleanup() {
if (this.#timeout) {
clearTimeout(this.#timeout);
}
}
async screenshots(options) {
const folder = os.tmpdir();
const filename = `${cuid()}.jpg`;
const { time } = options;
const output = path.join(folder, filename);
const timestamp = secondsToTimeFormat(time);
try {
const { stderr } = await $({
cancelSignal: this.#createAbortController().signal,
gracefulCancel: true,
timeout: this.#TIMEOUT
}) `${this.#ffmpegPath} -y -i ${this.input} -ss ${timestamp} -vframes 1 -q:v 2 ${output}`;
if (stderr.includes('Output file is empty, nothing was encoded')) {
const durationMatch = stderr.match(/Duration: (\d{2}:\d{2}:\d{2}\.\d{2})/);
if (durationMatch) {
const videoDuration = durationMatch[1];
logger.error(`Video is not long enough. Duration: ${videoDuration}, Requested timestamp: ${timestamp}`);
throw new Error(`Video is not long enough. Duration: ${videoDuration}, Requested timestamp: ${timestamp}`);
}
}
return output;
}
catch (error) {
if (error instanceof ExecaError) {
if (error.failed) {
const stderr = error.stderr;
if (stderr) {
logger.error(stderr.split('\n').pop());
}
throw error;
}
}
else {
logger.error(error);
throw error;
}
}
finally {
this.#cleanup();
}
}
async exif() {
try {
const { stdout } = await $({
cancelSignal: this.#createAbortController().signal,
gracefulCancel: true,
timeout: this.#TIMEOUT
}) `${this.#ffprobePath} -v quiet -print_format json -show_format -show_streams ${this.input}`;
const metadata = JSON.parse(stdout);
const videoStream = metadata.streams.find((stream) => stream.codec_type === 'video');
const audioStream = metadata.streams.find((stream) => stream.codec_type === 'audio');
return {
types: metadata.streams.map((stream) => stream.codec_type),
width: videoStream?.width,
height: videoStream?.height,
videoCodec: videoStream?.codec_name,
audioCodec: audioStream?.codec_name,
duration: +metadata.format.duration,
size: +metadata.format.size
};
}
catch (error) {
logger.error(error);
throw error;
}
finally {
this.#cleanup();
}
}
async setFfmpegPath(ffmpegPath) {
this.#ffmpegPath = ffmpegPath;
}
async setFfprobePath(ffprobePath) {
this.#ffprobePath = ffprobePath;
}
}