import { readFile, unlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { BadRequestException, Injectable, Logger } from "@nestjs/common"; import ffmpeg from "fluent-ffmpeg"; import { v4 as uuidv4 } from "uuid"; import type { MediaProcessingResult } from "../../common/interfaces/media.interface"; import type { IMediaProcessorStrategy } from "./media-processor.strategy"; @Injectable() export class VideoProcessorStrategy implements IMediaProcessorStrategy { private readonly logger = new Logger(VideoProcessorStrategy.name); canHandle(mimeType: string): boolean { return mimeType.startsWith("video/") || mimeType === "image/gif"; } async process( buffer: Buffer, options: { format: "webm" | "av1" } = { format: "webm" }, ): Promise { const { format } = options; const tempInput = join(tmpdir(), `${uuidv4()}.tmp`); const tempOutput = join( tmpdir(), `${uuidv4()}.${format === "av1" ? "mp4" : "webm"}`, ); try { await writeFile(tempInput, buffer); await new Promise((resolve, reject) => { let command = ffmpeg(tempInput); if (format === "webm") { command = command .toFormat("webm") .videoCodec("libvpx-vp9") .audioCodec("libopus") .addOutputOptions("-crf", "30", "-b:v", "0"); } else { command = command .toFormat("mp4") .videoCodec("libaom-av1") .audioCodec("libopus") .addOutputOptions("-crf", "34", "-b:v", "0", "-strict", "experimental"); } command .on("end", () => resolve()) .on("error", (err) => reject(err)) .save(tempOutput); }); const processedBuffer = await readFile(tempOutput); return { buffer: processedBuffer, mimeType: format === "av1" ? "video/mp4" : "video/webm", extension: format === "av1" ? "mp4" : "webm", size: processedBuffer.length, }; } catch (error) { this.logger.error(`Error processing video: ${error.message}`); throw new BadRequestException("Failed to process video"); } finally { await unlink(tempInput).catch(() => {}); await unlink(tempOutput).catch(() => {}); } } }