Procesamiento de video para plataformas UGC: guía de ingeniería completa
Construye un pipeline de procesamiento de video robusto para contenido generado por usuarios. Cubre normalización de formatos, generación de miniaturas, salidas multiresolución y arquitectura escalable.

Los usuarios suben de todo. Un archivo 4K ProRes de una cámara DSLR. Una grabación de pantalla vertical en WebM. Un AVI de hace 10 años de un teléfono antiguo. Un MOV con HEVC que solo reproduce Safari. Tu plataforma necesita aceptar todo eso y ofrecer una experiencia de reproducción consistente en cualquier dispositivo y condición de red.
Esta guía cubre el pipeline completo de procesamiento de video para plataformas UGC (contenido generado por usuarios) — desde la carga hasta la entrega — con comandos FFmpeg prácticos y patrones de arquitectura escalables.
El pipeline de procesamiento de video UGC
Toda plataforma que acepte video subido por usuarios necesita alguna versión de este pipeline:
┌─────────┐ ┌──────────┐ ┌────────────┐ ┌────────────┐ ┌──────────┐
│ Carga │──>│ Validar │──>│ Normalizar │──>│ Generar │──>│ Entregar │
│ │ │ y guardar│ │ formato │ │ variantes │ │ via CDN │
└─────────┘ └──────────┘ └────────────┘ └────────────┘ └──────────┘
│ │
┌─────┴─────┐ ┌────┴─────┐
│ Transco- │ │Miniaturas│
│ dificar a │ │Previews │
│ H.264 MP4 │ │Multi-res │
└───────────┘ └──────────┘
Cada etapa tiene requisitos técnicos específicos. Veamos cada una.
Etapa 1: Carga y validación
Antes de gastar recursos computacionales en transcodificación, valida la carga:
Verificaciones a nivel de archivo
// 上传验证中间件
function validateUpload(req, res, next) {
const file = req.file;
// 文件大小限制
const MAX_SIZE = 2 * 1024 * 1024 * 1024; // 2GB
if (file.size > MAX_SIZE) {
return res.status(413).json({ error: "File exceeds 2GB limit" });
}
// MIME 类型白名单
const ALLOWED_TYPES = [
"video/mp4", "video/quicktime", "video/x-msvideo",
"video/webm", "video/x-matroska", "video/x-flv",
];
if (!ALLOWED_TYPES.includes(file.mimetype)) {
return res.status(415).json({ error: "Unsupported video format" });
}
next();
}
Analizar el archivo
Tras la carga, usa FFprobe para extraer metadatos antes de procesarlo:
ffprobe -v quiet -print_format json -show_format -show_streams input.mp4
Esto te proporciona duración, resolución, códec, tasa de bits y frames por segundo — datos que orientarán tus decisiones de transcodificación.
// 使用 FFprobe 获取视频元数据
async function probeVideo(fileUrl) {
const response = await fetch("https://api.ffhub.io/v1/tasks", {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
command: `ffprobe -v quiet -print_format json -show_format -show_streams ${fileUrl}`,
}),
});
const { task_id } = await response.json();
// 等待完成后解析 JSON 输出
return await waitAndGetResult(task_id);
}
Etapa 2: Normalización de formato
El objetivo: tomar cualquier formato de entrada y producir una salida consistente. Para la gran mayoría de plataformas UGC, eso significa H.264 MP4 con audio AAC.
¿Por qué H.264 MP4?
| Factor | H.264 MP4 | H.265 | VP9/WebM | AV1 |
|---|---|---|---|---|
| Soporte de navegadores | 99 %+ | ~80 % | ~90 % | ~70 % |
| Soporte móvil | Universal | Solo iOS | Mayormente Android | Limitado |
| Velocidad de codificación | Rápida | 2-3x más lenta | 3-5x más lenta | 10x más lenta |
| Decodificación por hardware | Universal | Dispositivos recientes | Chrome/Android | Solo los más nuevos |
| Tamaño de archivo (referencia) | 1x | 0,5x | 0,6x | 0,4x |
H.264 gana en compatibilidad. Si tu audiencia es global e incluye dispositivos más antiguos, es el único valor predeterminado seguro. Puedes ofrecer H.265 o AV1 como mejora progresiva para clientes compatibles. Para un análisis a fondo de la configuración de compresión, consulta nuestras mejores prácticas de compresión de video con FFmpeg.
El comando base de transcodificación
ffmpeg -i input.mov \
-c:v libx264 \
-crf 23 \
-preset medium \
-profile:v high \
-level 4.1 \
-pix_fmt yuv420p \
-c:a aac \
-b:a 128k \
-ar 44100 \
-movflags +faststart \
output.mp4
Por qué importa cada parámetro:
-crf 23: buen equilibrio entre calidad y tamaño para contenido UGC-profile:v high -level 4.1: máxima compatibilidad con dispositivos modernos-pix_fmt yuv420p: necesario para reproducción en muchos dispositivos (algunas cámaras producen yuv422p o yuv444p)-movflags +faststart: mueve los metadatos al inicio del archivo para que la reproducción pueda comenzar antes de que se complete la descarga — crítico para la entrega web
Manejo de rotación
Los videos grabados con teléfono suelen tener metadatos de rotación en lugar de píxeles físicamente rotados. FFmpeg lo gestiona automáticamente con -c:v libx264, pero si necesitas forzarlo:
ffmpeg -i input.mp4 -c:v libx264 -crf 23 -vf "transpose=1" output.mp4
Limitar la resolución máxima
Los usuarios suben en 4K, pero tu reproductor tiene un máximo de 1080p. Limita la resolución sin escalar hacia arriba los videos más pequeños:
ffmpeg -i input.mp4 \
-vf "scale='min(1920,iw)':'min(1080,ih)':force_original_aspect_ratio=decrease,pad=ceil(iw/2)*2:ceil(ih/2)*2" \
-c:v libx264 -crf 23 -preset medium \
-c:a aac -b:a 128k \
output.mp4
Esto reduce cualquier video mayor que 1920x1080 sin tocar los más pequeños, y garantiza dimensiones pares (requerido por H.264).
Etapa 3: Generación de miniaturas y previews
Todo video necesita al menos una miniatura. La mayoría de plataformas también generan previews animados para el estado hover.
Miniatura estática
Extrae un fotograma en la marca de 2 segundos (evita fotogramas negros de las introducciones):
ffmpeg -i input.mp4 -ss 2 -frames:v 1 -q:v 2 thumbnail.jpg
Múltiples candidatos de miniatura
Extrae 5 fotogramas distribuidos uniformemente y deja que el usuario elija (o usa un evaluador de calidad de imagen para seleccionar automáticamente):
ffmpeg -i input.mp4 \
-vf "select='not(mod(n\,floor($(ffprobe -v error -count_frames -select_streams v:0 -show_entries stream=nb_read_frames -of csv=p=0 input.mp4)/5)))',scale=640:-2" \
-frames:v 5 -vsync vfr \
thumb_%02d.jpg
Un enfoque más simple — extrae en marcas de tiempo específicas:
# 在 1s, 25%, 50%, 75% 处分别截取缩略图
ffmpeg -i input.mp4 -ss 1 -frames:v 1 thumb_01.jpg
ffmpeg -i input.mp4 -ss 25% -frames:v 1 thumb_02.jpg
ffmpeg -i input.mp4 -ss 50% -frames:v 1 thumb_03.jpg
ffmpeg -i input.mp4 -ss 75% -frames:v 1 thumb_04.jpg
Preview animado (GIF o WebP)
Un preview en bucle de 3 a 5 segundos para el estado hover:
# 从第 5 秒开始截取 4 秒,生成 10fps 320px 宽的 GIF
ffmpeg -i input.mp4 -ss 5 -t 4 \
-vf "fps=10,scale=320:-2:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" \
preview.gif
Para mayor calidad y menor tamaño, usa WebP:
ffmpeg -i input.mp4 -ss 5 -t 4 \
-vf "fps=10,scale=320:-2" \
-c:v libwebp -lossless 0 -quality 50 -loop 0 \
preview.webp
Etapa 4: Salidas multiresolución
Para streaming adaptativo, genera múltiples resoluciones desde la fuente:
# 1080p
ffmpeg -i input.mp4 -vf "scale=-2:1080" -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 128k output_1080p.mp4
# 720p
ffmpeg -i input.mp4 -vf "scale=-2:720" -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 96k output_720p.mp4
# 480p
ffmpeg -i input.mp4 -vf "scale=-2:480" -c:v libx264 -crf 26 -preset medium -c:a aac -b:a 64k output_480p.mp4
# 360p
ffmpeg -i input.mp4 -vf "scale=-2:360" -c:v libx264 -crf 28 -preset medium -c:a aac -b:a 48k output_360p.mp4
Observa cómo el CRF aumenta y la tasa de bits de audio disminuye para resoluciones menores — a menor resolución hay menos información visual, por lo que puedes comprimir de forma más agresiva.
Tabla de resoluciones
| Resolución | CRF | Tasa de bits de video (típica) | Tasa de bits de audio | Uso objetivo |
|---|---|---|---|---|
| 1080p | 23 | 3-5 Mbps | 128k | WiFi / banda ancha |
| 720p | 23 | 1,5-3 Mbps | 96k | Móvil con buena cobertura |
| 480p | 26 | 0,5-1,5 Mbps | 64k | Móvil lento |
| 360p | 28 | 0,3-0,7 Mbps | 48k | Conexión muy lenta |
Pipeline de procesamiento completo
A continuación, una implementación en Node.js que orquesta todo el pipeline — desde la carga hasta todas las salidas:
// ugc-pipeline.js
const API_BASE = "https://api.ffhub.io/v1";
const API_KEY = process.env.FFHUB_API_KEY;
// 提交 FFmpeg 任务并等待完成
async function runFFmpeg(command) {
const res = await fetch(`${API_BASE}/tasks`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ command }),
});
const { task_id } = await res.json();
return waitForTask(task_id);
}
async function waitForTask(taskId) {
while (true) {
const res = await fetch(`${API_BASE}/tasks/${taskId}`, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
const task = await res.json();
if (task.status === "completed") return task;
if (task.status === "failed") throw new Error(task.error);
await new Promise((r) => setTimeout(r, 3000));
}
}
// UGC 视频处理主流程
async function processUGCVideo(sourceUrl, videoId) {
console.log(`[${videoId}] 开始处理: ${sourceUrl}`);
// 步骤 1: 探测源文件
const probe = await runFFmpeg(
`ffprobe -v quiet -print_format json -show_format -show_streams ${sourceUrl}`
);
const metadata = JSON.parse(probe.output);
console.log(`[${videoId}] 源文件: ${metadata.format.duration}s, ${metadata.streams[0].width}x${metadata.streams[0].height}`);
// 步骤 2: 标准化转码 + 缩略图(并行执行)
const baseCommand = `-c:v libx264 -crf 23 -preset medium -profile:v high -pix_fmt yuv420p -c:a aac -b:a 128k -movflags +faststart`;
const [normalized, thumbnail, preview] = await Promise.all([
// 标准化为 1080p H.264 MP4
runFFmpeg(
`ffmpeg -i ${sourceUrl} -vf "scale='min(1920,iw)':'min(1080,ih)':force_original_aspect_ratio=decrease,pad=ceil(iw/2)*2:ceil(ih/2)*2" ${baseCommand} output.mp4`
),
// 缩略图
runFFmpeg(
`ffmpeg -i ${sourceUrl} -ss 2 -frames:v 1 -q:v 2 thumbnail.jpg`
),
// 动画预览
runFFmpeg(
`ffmpeg -i ${sourceUrl} -ss 5 -t 4 -vf "fps=10,scale=320:-2" -c:v libwebp -lossless 0 -quality 50 -loop 0 preview.webp`
),
]);
// 步骤 3: 生成低分辨率版本(并行)
const [res720, res480] = await Promise.all([
runFFmpeg(
`ffmpeg -i ${sourceUrl} -vf "scale=-2:720" -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 96k -movflags +faststart output_720p.mp4`
),
runFFmpeg(
`ffmpeg -i ${sourceUrl} -vf "scale=-2:480" -c:v libx264 -crf 26 -preset medium -c:a aac -b:a 64k -movflags +faststart output_480p.mp4`
),
]);
return {
videoId,
outputs: {
"1080p": normalized.output_url,
"720p": res720.output_url,
"480p": res480.output_url,
thumbnail: thumbnail.output_url,
preview: preview.output_url,
},
};
}
// 使用示例
processUGCVideo("https://your-bucket.s3.amazonaws.com/uploads/raw-video.mov", "vid_abc123")
.then((result) => console.log("处理完成:", result))
.catch((err) => console.error("处理失败:", err));
Arquitectura: de la carga a la entrega
Esta es la arquitectura de producción completa para una plataforma UGC:
┌──────────┐ ┌──────────────┐ ┌──────────────────┐
│ Cliente │────>│ Tu servidor │────>│ Almacenamiento │
│ (carga) │ │ API │ │ de objetos │
│ │ │ │ │ (S3 / R2) │
└──────────┘ └──────┬───────┘ └────────┬─────────┘
│ │
│ POST /tasks │ URL de origen
v │
┌──────────────┐ │
│ API de │<─────────────┘
│ transco- │
│ dificación │
└──────┬───────┘
│
│ webhook callback
v
┌──────────────┐ ┌──────────────────┐
│ Tu servidor │────>│ CDN │
│ (webhook) │ │ (CloudFront/CF) │
└──────────────┘ └──────────────────┘
Flujo
- El cliente sube el video sin procesar a tu servidor API (o directamente al almacenamiento de objetos mediante una URL prefirmada)
- Tu API guarda el archivo sin procesar y crea una tarea de procesamiento
- La API de transcodificación recibe el comando FFmpeg con la URL de origen y procesa el video
- El webhook notifica a tu API cuando el procesamiento se completa, con las URLs de salida
- Tu API actualiza la base de datos y pone el video disponible mediante CDN
Esquema de base de datos
CREATE TABLE videos (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
title TEXT,
status TEXT DEFAULT 'uploading', -- uploading, processing, ready, failed
source_url TEXT,
duration REAL,
width INTEGER,
height INTEGER,
created_at TIMESTAMPTZ DEFAULT NOW(),
processed_at TIMESTAMPTZ
);
CREATE TABLE video_variants (
id TEXT PRIMARY KEY,
video_id TEXT NOT NULL,
resolution TEXT NOT NULL, -- '1080p', '720p', '480p', 'thumbnail', 'preview'
url TEXT NOT NULL,
file_size BIGINT,
codec TEXT,
bitrate INTEGER,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_videos_user ON videos(user_id);
CREATE INDEX idx_variants_video ON video_variants(video_id);
Rendimiento a escala
Benchmarks de tiempo de procesamiento
| Origen | Duración | Salida 1080p | Miniatura | Pipeline total |
|---|---|---|---|---|
| Clip 1080p de 30s | 30s | ~15s | <1s | ~25s |
| Video 1080p de 5min | 5min | ~90s | <1s | ~120s |
| Video 4K de 10min | 10min | ~300s | <1s | ~360s |
| Clip 720p de 1min | 1min | ~20s | <1s | ~30s |
Estos son valores aproximados — los tiempos reales dependen del códec de origen, la tasa de bits y la complejidad del contenido.
Estrategias de escalado
Para 100 cargas/día: procesamiento en un solo hilo con una cola de tareas simple. Incluso una cola básica (respaldada por base de datos) es suficiente.
Para 1.000 cargas/día: procesamiento concurrente con 5-10 tareas paralelas. Agrega monitoreo y alertas para tareas bloqueadas.
Para más de 10.000 cargas/día: arquitectura completamente asíncrona con webhooks, base de datos dedicada para seguimiento de tareas, cola de mensajes fallidos y workers con escalado automático o una API en la nube que gestione el escalado por ti. Nuestra guía de transcodificación de video en lote cubre esta arquitectura en detalle.
Consideraciones de costo
Para una plataforma social que procesa 5.000 cargas de video por día (promedio 2 minutos cada una):
- Autoalojado (servidores GPU dedicados): ~$2.000-5.000/mes en servidores + tiempo de ingeniería
- API de transcodificación en la nube: ~$500-1.500/mes según las variantes de salida
- Híbrido: carga base en hardware dedicado, exceso en la API en la nube
Errores comunes
1. No manejar videos sin audio o con silencio
Algunas cargas no tienen pista de audio. FFmpeg dará error si intentas codificar audio inexistente:
# 安全处理:如果没有音轨则跳过音频编码
ffmpeg -i input.mp4 -c:v libx264 -crf 23 -c:a aac -b:a 128k output.mp4 2>&1 || \
ffmpeg -i input.mp4 -c:v libx264 -crf 23 -an output.mp4
2. Ignorar la relación de aspecto
Escalar a una resolución fija sin preservar la relación de aspecto produce video distorsionado. Usa siempre -2 para la dimensión libre o force_original_aspect_ratio=decrease.
3. Sin tiempo límite de procesamiento
Un archivo corrupto puede hacer que FFmpeg quede bloqueado indefinidamente. Establece siempre un tiempo límite:
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 600000); // 10 分钟超时
try {
await fetch(url, { signal: controller.signal });
} finally {
clearTimeout(timeout);
}
4. Almacenar solo una resolución
Si solo guardas el original, cada reproducción en una conexión lenta se cortará. Si solo guardas una versión transcodificada, no puedes adaptarte a las condiciones de red. Guarda al menos 2-3 resoluciones.
Conclusión
Un pipeline de procesamiento de video UGC no tiene que ser complejo, pero sí tiene que ser confiable. La receta central es:
- Valida las cargas desde el inicio (formato, tamaño, duración)
- Normaliza a H.264 MP4 con configuraciones consistentes
- Genera miniaturas y previews en paralelo con la transcodificación
- Produce múltiples resoluciones para entrega adaptativa
- Entrega mediante CDN con los headers de caché correctos
El código del pipeline mostrado en este artículo cubre todos estos pasos. Para el cómputo de transcodificación, puedes ejecutar FFmpeg en tus propios servidores o usar una API en la nube como FFHub.io para evitar gestionar infraestructura FFmpeg por completo.
Empieza de forma simple — una sola resolución con una miniatura — y agrega complejidad (multiresolución, previews animados, empaquetado HLS) conforme tu plataforma crezca. Si tu plataforma es un producto SaaS, nuestra guía de procesamiento de video para SaaS cubre la decisión de construir vs. comprar y los patrones de integración.
Artículos relacionados
- Transcodificación de video en lote mediante API — Guía de arquitectura e implementación para procesar miles de videos de forma confiable
- Procesamiento de video para SaaS — Marco de decisión para construir vs. comprar cuando el video es una función de apoyo en tu producto
- Cómo convertir formato de video con FFmpeg — Guía para principiantes sobre los comandos de conversión de formato usados en pipelines UGC