From b98b5106d98cbe856c5b6a11bc725061722c215b Mon Sep 17 00:00:00 2001 From: Dmitri Date: Mon, 13 Apr 2026 19:17:18 +0400 Subject: [PATCH] updated docker for ffmpeg --- Dockerfile | 2 + Dockerfile.dev | 2 + backend/Dockerfile | 4 +- backend/src/services/assets.js | 102 ++++++++++++++++++++++++++++++++- 4 files changed, 106 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 970d54f..10f49e9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,8 @@ RUN yarn build FROM node:20.15.1-alpine +# Install FFmpeg for video processing (reversed video generation) +RUN apk add --no-cache ffmpeg WORKDIR /app COPY backend/package.json backend/yarn.lock ./ RUN yarn install --pure-lockfile diff --git a/Dockerfile.dev b/Dockerfile.dev index a8353d5..14caf13 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -21,6 +21,8 @@ RUN yarn install --pure-lockfile FROM node:20.15.1-alpine AS build RUN apk add --no-cache git nginx curl RUN apk add --no-cache lsof procps +# Install FFmpeg for video processing (reversed video generation) +RUN apk add --no-cache ffmpeg RUN yarn global add concurrently RUN apk add --no-cache \ diff --git a/backend/Dockerfile b/backend/Dockerfile index 581cb98..be94ae1 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,6 +1,8 @@ FROM node:20.15.1-alpine -RUN apk update && apk add bash +# Install bash and FFmpeg for video processing (reversed video generation) +RUN apk update && apk add --no-cache bash ffmpeg + # Create app directory WORKDIR /usr/src/app diff --git a/backend/src/services/assets.js b/backend/src/services/assets.js index fc02a5c..205bf51 100644 --- a/backend/src/services/assets.js +++ b/backend/src/services/assets.js @@ -1,6 +1,10 @@ const AssetsDBApi = require('../db/api/assets'); +const Asset_variantsDBApi = require('../db/api/asset_variants'); const { createEntityService } = require('../factories/service.factory'); const ValidationError = require('./notifications/errors/validation'); +const { downloadToBuffer, uploadBuffer } = require('./file'); +const videoProcessing = require('./videoProcessing'); +const { logger } = require('../utils/logger'); /** * Valid MIME type patterns for each asset type @@ -68,11 +72,11 @@ const BaseService = createEntityService(AssetsDBApi, { }); /** - * Assets Service with validation + * Assets Service with validation and video pre-processing */ class AssetsService extends BaseService { /** - * Create asset with MIME type validation + * Create asset with MIME type validation and video pre-processing */ static async create(data, currentUser) { // Validate asset_type and mime_type match @@ -85,7 +89,99 @@ class AssetsService extends BaseService { } // Call parent create - return super.create(data, currentUser); + const asset = await super.create(data, currentUser); + + // Pre-generate reversed video for video assets (async, doesn't block response) + if (assetType === 'video' && data.storage_key) { + AssetsService.preGenerateReversedVideo(asset, currentUser).catch((err) => { + logger.error( + { err, assetId: asset.id }, + 'Failed to pre-generate reversed video (non-blocking)', + ); + }); + } + + return asset; + } + + /** + * Pre-generate reversed video variant for a video asset. + * Runs asynchronously after asset creation - doesn't block the upload response. + * This ensures reversed videos are ready for instant use in transitions. + * + * @param {Object} asset - Created asset record + * @param {Object} currentUser - Current user context + */ + static async preGenerateReversedVideo(asset, currentUser) { + const log = logger.child({ + assetId: asset.id, + operation: 'preGenerateReversed', + }); + + log.info('Starting pre-generation of reversed video'); + + try { + // Check if FFmpeg is available + const ffmpegAvailable = await videoProcessing.isFFmpegAvailable(); + if (!ffmpegAvailable) { + log.warn('FFmpeg not available, skipping pre-generation'); + return null; + } + + // Check if reversed variant already exists (shouldn't happen on create, but safety check) + const existingAsset = await AssetsDBApi.findBy({ id: asset.id }); + const variants = existingAsset?.asset_variants_asset || []; + const existingReversed = variants.find((v) => v.variant_type === 'reversed'); + + if (existingReversed) { + log.debug('Reversed variant already exists'); + return existingReversed.storage_key; + } + + // Download original video to buffer + const storageKey = asset.storage_key; + log.info({ storageKey }, 'Downloading original video'); + + const originalBuffer = await downloadToBuffer(storageKey); + + // Generate reversed video + log.info('Generating reversed video with FFmpeg'); + const reversedBuffer = await videoProcessing.reverseVideo( + originalBuffer, + asset.original_file_name || 'video.mp4', + ); + + // Upload reversed video to storage + const reversedKey = `assets/${asset.id}/reversed.mp4`; + log.info({ reversedKey }, 'Uploading reversed video'); + + const result = await uploadBuffer(reversedKey, reversedBuffer, { + contentType: 'video/mp4', + }); + + // Create variant record + await Asset_variantsDBApi.create( + { + assetId: asset.id, + variant_type: 'reversed', + cdn_url: result.url, + storage_key: reversedKey, + size_mb: reversedBuffer.length / (1024 * 1024), + }, + { currentUser }, + ); + + log.info( + { reversedKey, sizeMb: (reversedBuffer.length / (1024 * 1024)).toFixed(2) }, + 'Pre-generated reversed video successfully', + ); + + return reversedKey; + } catch (err) { + log.error({ err }, 'Failed to pre-generate reversed video'); + // Don't throw - this is a background operation + return null; + } } /**