UNPKG

@jrmc/adonis-attachment

Version:

Turn any field on your Lucid model to an attachment data type

116 lines (115 loc) 4.08 kB
/** * @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; } }