Home » Создание API для обработки медиа в Node.js с Express и FFmpeg WASM
Создание API для обработки медиа в Node.js с Express и FFmpeg WASM

Создание API для обработки медиа в Node.js с Express и FFmpeg WASM

Если вы хотите создать собственный API для обработки видео и аудио прямо в браузере без необходимости установки тяжелых зависимостей на сервере, то этот пост для вас. Сегодня поговорим о том, как поднять мощный медиа-сервер с использованием Node.js, Express и FFmpeg WASM. Эта штука поможет вам автоматизировать обработку пользовательских файлов, создать систему для конвертации видео или даже собрать собственный сервис для стриминга с минимальными затратами ресурсов.

Главный кайф в том, что FFmpeg WASM работает в изолированной среде и не требует установки нативных библиотек. Это особенно актуально для тех, кто работает с shared hosting или хочет развернуть API на VPS без головной боли с зависимостями.

Как это работает под капотом

FFmpeg WASM — это порт легендарного FFmpeg, скомпилированный в WebAssembly. Он работает в браузере или Node.js окружении и предоставляет почти все возможности классического FFmpeg, но с некоторыми ограничениями по производительности.

Основные компоненты системы:

  • Express.js — веб-фреймворк для создания API endpoints
  • FFmpeg WASM — движок для обработки медиа
  • Multer — middleware для загрузки файлов
  • Stream API — для работы с большими файлами

Пошаговая настройка проекта

Начнем с создания базовой структуры проекта:

mkdir media-api
cd media-api
npm init -y
npm install express multer @ffmpeg/ffmpeg @ffmpeg/util cors
npm install --save-dev nodemon

Создаем основной файл сервера:

// server.js
const express = require('express');
const multer = require('multer');
const cors = require('cors');
const { FFmpeg } = require('@ffmpeg/ffmpeg');
const { fetchFile, toBlobURL } = require('@ffmpeg/util');
const fs = require('fs');
const path = require('path');

const app = express();
const PORT = process.env.PORT || 3000;

// Настройка CORS и middleware
app.use(cors());
app.use(express.json());

// Конфигурация Multer для загрузки файлов
const storage = multer.memoryStorage();
const upload = multer({ 
  storage: storage,
  limits: { fileSize: 100 * 1024 * 1024 } // 100MB лимит
});

// Инициализация FFmpeg
let ffmpeg = null;

const initFFmpeg = async () => {
  if (!ffmpeg) {
    ffmpeg = new FFmpeg();
    const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm';
    await ffmpeg.load({
      coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
      wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
    });
  }
  return ffmpeg;
};

// Конвертация видео
app.post('/convert', upload.single('video'), async (req, res) => {
  try {
    const { format = 'mp4', quality = 'medium' } = req.body;
    
    if (!req.file) {
      return res.status(400).json({ error: 'No video file provided' });
    }

    const ffmpeg = await initFFmpeg();
    const inputFileName = 'input.' + req.file.originalname.split('.').pop();
    const outputFileName = `output.${format}`;

    // Загружаем файл в файловую систему FFmpeg
    await ffmpeg.writeFile(inputFileName, await fetchFile(req.file.buffer));

    // Определяем параметры качества
    const qualityParams = {
      'low': ['-crf', '28'],
      'medium': ['-crf', '23'],
      'high': ['-crf', '18']
    };

    // Выполняем конвертацию
    await ffmpeg.exec([
      '-i', inputFileName,
      '-c:v', 'libx264',
      ...qualityParams[quality],
      '-preset', 'medium',
      outputFileName
    ]);

    // Читаем результат
    const data = await ffmpeg.readFile(outputFileName);
    
    // Очищаем временные файлы
    await ffmpeg.deleteFile(inputFileName);
    await ffmpeg.deleteFile(outputFileName);

    res.set({
      'Content-Type': `video/${format}`,
      'Content-Disposition': `attachment; filename="converted.${format}"`
    });
    
    res.send(Buffer.from(data));
  } catch (error) {
    console.error('Conversion error:', error);
    res.status(500).json({ error: 'Conversion failed' });
  }
});

// Получение информации о видео
app.post('/info', upload.single('video'), async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: 'No video file provided' });
    }

    const ffmpeg = await initFFmpeg();
    const inputFileName = 'input.' + req.file.originalname.split('.').pop();
    
    await ffmpeg.writeFile(inputFileName, await fetchFile(req.file.buffer));
    
    // Получаем информацию о файле
    await ffmpeg.exec(['-i', inputFileName, '-f', 'null', '-']);
    
    // В реальном проекте здесь нужно парсить вывод ffmpeg
    // Для примера возвращаем базовую информацию
    const info = {
      filename: req.file.originalname,
      size: req.file.size,
      mimetype: req.file.mimetype
    };
    
    await ffmpeg.deleteFile(inputFileName);
    res.json(info);
  } catch (error) {
    res.status(500).json({ error: 'Failed to get video info' });
  }
});

app.listen(PORT, () => {
  console.log(`Media API server running on port ${PORT}`);
});

Практические примеры использования

Создадим несколько полезных endpoint’ов для разных задач:

// Создание превью изображения
app.post('/thumbnail', upload.single('video'), async (req, res) => {
  try {
    const { timestamp = '00:00:01' } = req.body;
    const ffmpeg = await initFFmpeg();
    
    const inputFileName = 'input.' + req.file.originalname.split('.').pop();
    const outputFileName = 'thumbnail.jpg';
    
    await ffmpeg.writeFile(inputFileName, await fetchFile(req.file.buffer));
    
    await ffmpeg.exec([
      '-i', inputFileName,
      '-ss', timestamp,
      '-vframes', '1',
      '-q:v', '2',
      outputFileName
    ]);
    
    const data = await ffmpeg.readFile(outputFileName);
    
    await ffmpeg.deleteFile(inputFileName);
    await ffmpeg.deleteFile(outputFileName);
    
    res.set('Content-Type', 'image/jpeg');
    res.send(Buffer.from(data));
  } catch (error) {
    res.status(500).json({ error: 'Thumbnail generation failed' });
  }
});

// Извлечение аудио из видео
app.post('/extract-audio', upload.single('video'), async (req, res) => {
  try {
    const { format = 'mp3' } = req.body;
    const ffmpeg = await initFFmpeg();
    
    const inputFileName = 'input.' + req.file.originalname.split('.').pop();
    const outputFileName = `audio.${format}`;
    
    await ffmpeg.writeFile(inputFileName, await fetchFile(req.file.buffer));
    
    await ffmpeg.exec([
      '-i', inputFileName,
      '-vn',
      '-acodec', format === 'mp3' ? 'libmp3lame' : 'aac',
      '-ab', '128k',
      outputFileName
    ]);
    
    const data = await ffmpeg.readFile(outputFileName);
    
    await ffmpeg.deleteFile(inputFileName);
    await ffmpeg.deleteFile(outputFileName);
    
    res.set({
      'Content-Type': `audio/${format}`,
      'Content-Disposition': `attachment; filename="extracted.${format}"`
    });
    res.send(Buffer.from(data));
  } catch (error) {
    res.status(500).json({ error: 'Audio extraction failed' });
  }
});

Сравнение с альтернативными решениями

Решение Преимущества Недостатки Производительность
FFmpeg WASM Не требует установки, изолированность, безопасность Медленнее нативного FFmpeg, ограничения по памяти Средняя
Native FFmpeg Максимальная производительность, все возможности Требует установки, сложности с зависимостями Высокая
Cloud API (AWS MediaConvert) Масштабируемость, не нагружает сервер Дороже, зависимость от провайдера Высокая
ImageMagick + Sox Легковесность для простых задач Ограниченные возможности Высокая

Оптимизация и масштабирование

Для production-окружения добавим несколько важных улучшений:

// Добавляем в server.js
const cluster = require('cluster');
const os = require('os');

// Кластеризация для использования всех ядер
if (cluster.isMaster) {
  const numCPUs = os.cpus().length;
  console.log(`Master ${process.pid} is running`);
  
  for (let i = 0; i < numCPUs; i++) { cluster.fork(); } cluster.on('exit', (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} died`);
    cluster.fork();
  });
} else {
  // Основной код сервера здесь
}

// Middleware для логирования
const morgan = require('morgan');
app.use(morgan('combined'));

// Ограничение rate limit
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 минут
  max: 10 // максимум 10 запросов с IP
});
app.use('/convert', limiter);

// Мониторинг памяти
const monitorMemory = (req, res, next) => {
  const usage = process.memoryUsage();
  console.log(`Memory usage: ${Math.round(usage.heapUsed / 1024 / 1024)} MB`);
  next();
};
app.use(monitorMemory);

Обработка ошибок и безопасность

Создадим надежную систему обработки ошибок:

// Валидация файлов
const fileFilter = (req, file, cb) => {
  const allowedTypes = ['video/mp4', 'video/avi', 'video/mov', 'video/mkv'];
  if (allowedTypes.includes(file.mimetype)) {
    cb(null, true);
  } else {
    cb(new Error('Invalid file type'), false);
  }
};

const upload = multer({ 
  storage: storage,
  limits: { fileSize: 100 * 1024 * 1024 },
  fileFilter: fileFilter
});

// Глобальный обработчик ошибок
app.use((error, req, res, next) => {
  if (error instanceof multer.MulterError) {
    if (error.code === 'LIMIT_FILE_SIZE') {
      return res.status(400).json({ error: 'File too large' });
    }
  }
  
  console.error(error);
  res.status(500).json({ error: 'Internal server error' });
});

// Graceful shutdown
process.on('SIGTERM', () => {
  console.log('SIGTERM received, shutting down gracefully');
  server.close(() => {
    console.log('Process terminated');
  });
});

Интеграция с другими сервисами

Для более продвинутого использования можно интегрировать наш API с облачными хранилищами:

// Интеграция с AWS S3
const AWS = require('aws-sdk');
const s3 = new AWS.S3();

app.post('/convert-and-upload', upload.single('video'), async (req, res) => {
  try {
    // Конвертация (код выше)
    const convertedData = await convertVideo(req.file);
    
    // Загрузка в S3
    const uploadParams = {
      Bucket: 'your-bucket-name',
      Key: `converted/${Date.now()}.mp4`,
      Body: convertedData,
      ContentType: 'video/mp4'
    };
    
    const result = await s3.upload(uploadParams).promise();
    
    res.json({ 
      message: 'Video converted and uploaded',
      url: result.Location 
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

Автоматизация и CI/CD

Создадим Docker-контейнер для легкого развертывания:

# Dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 3000

USER node

CMD ["node", "server.js"]
# docker-compose.yml
version: '3.8'
services:
  media-api:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
    restart: unless-stopped
    
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - media-api

Мониторинг и метрики

Добавим базовые метрики для мониторинга:

// Простой healthcheck endpoint
app.get('/health', (req, res) => {
  const health = {
    uptime: process.uptime(),
    message: 'OK',
    timestamp: new Date().toISOString(),
    memory: process.memoryUsage()
  };
  res.json(health);
});

// Метрики обработки
let processingStats = {
  totalRequests: 0,
  successfulConversions: 0,
  failedConversions: 0,
  averageProcessingTime: 0
};

app.get('/metrics', (req, res) => {
  res.json(processingStats);
});

Интересные кейсы использования

Вот несколько нестандартных способов применения такого API:

  • Автоматическое создание превью для видео-плеера — генерация кадров через равные интервалы
  • Система модерации контента — извлечение кадров для анализа ИИ
  • Оптимизация для мобильных устройств — автоматическое сжатие загружаемых видео
  • Создание GIF из видео — популярная фича для соцсетей
  • Подготовка контента для стриминга — нарезка на сегменты для HLS

Производительность и бенчмарки

Для VPS с 4 ядрами и 8GB RAM типичные показатели:

  • Конвертация 1-минутного HD видео: ~30-60 секунд
  • Создание превью: ~2-5 секунд
  • Извлечение аудио: ~10-20 секунд
  • Максимальная нагрузка: ~3-5 одновременных конвертаций

Для серьезных нагрузок рекомендую использовать выделенный сервер с более мощными характеристиками.

Альтернативные решения

Стоит также рассмотреть эти инструменты:

  • fluent-ffmpeg — Node.js обертка для нативного FFmpeg
  • Transloadit — облачный сервис для обработки медиа
  • imageio-ffmpeg — Python-решение для простых задач
  • Howler.js — для работы только с аудио

Заключение и рекомендации

FFmpeg WASM + Node.js — это отличное решение для средних проектов, где нужна гибкость и простота развертывания. Используйте его, если:

  • Вам нужно быстро прототипировать медиа-сервис
  • Работаете с shared hosting или контейнерами
  • Требуется изолированная среда выполнения
  • Нагрузка не критична к производительности

Не рекомендую для high-load проектов или real-time обработки — там лучше использовать нативный FFmpeg или облачные решения. Для production обязательно добавьте очереди задач (Redis + Bull), логирование и мониторинг.

Помните про ограничения WASM по памяти и производительности. Для файлов больше 500MB лучше использовать streaming или разбивать на чанки. И не забудьте про CORS настройки, если планируете использовать API из браузера.


В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.

Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.

Leave a reply

Your email address will not be published. Required fields are marked