- Home »

Создание 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 из браузера.
В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.
Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.