← All posts

Processamento de vídeo para plataformas UGC: o guia completo de engenharia

Construa um pipeline robusto de processamento de vídeo para conteúdo gerado por usuário. Normalização de formato, thumbnails, multi-resolução e arquitetura escalável.

FFHub·2026-05-11
Processamento de vídeo para plataformas UGC: o guia completo de engenharia

Usuário envia qualquer coisa. Um arquivo 4K ProRes de uma câmera DSLR. Uma gravação de tela vertical em WebM. Um AVI de 10 anos atrás de um celular com tampa. Um MOV com HEVC que só toca no Safari. Sua plataforma precisa aceitar tudo isso e entregar uma experiência de playback consistente em todo dispositivo e condição de rede.

Este guia cobre o pipeline completo de processamento de vídeo para plataformas UGC — do upload à entrega — com comandos FFmpeg práticos e padrões de arquitetura que escalam.

O pipeline de processamento de vídeo UGC

Toda plataforma que aceita vídeo enviado por usuário precisa de alguma versão deste pipeline:

┌─────────┐   ┌──────────┐   ┌────────────┐   ┌────────────┐   ┌──────────┐
│  Upload  │──>│ Validate │──>│ Normalize  │──>│ Generate   │──>│ Deliver  │
│          │   │ & Store  │   │ Format     │   │ Variants   │   │ via CDN  │
└─────────┘   └──────────┘   └────────────┘   └────────────┘   └──────────┘
                                    │                │
                              ┌─────┴─────┐    ┌────┴─────┐
                              │ Transcode │    │ Thumbnails│
                              │ to H.264  │    │ Previews  │
                              │ MP4       │    │ Multi-res  │
                              └───────────┘    └──────────┘

Cada estágio tem requisitos técnicos específicos. Vamos passar por eles.

Estágio 1: Upload e validação

Antes de gastar compute em transcodificação, valide o upload:

Checagens em nível de arquivo

// 上传验证中间件
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();
}

Probe do arquivo

Após o upload, use FFprobe para extrair metadados antes do processamento:

ffprobe -v quiet -print_format json -show_format -show_streams input.mp4

Isso te dá duração, resolução, codec, bitrate, e frame rate — tudo isso informa suas decisões de transcodificação.

// 使用 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);
}

Estágio 2: Normalização de formato

O objetivo: pegar qualquer formato de entrada e produzir uma saída consistente. Para a grande maioria das plataformas UGC, isso significa H.264 MP4 com áudio AAC.

Por que H.264 MP4?

FatorH.264 MP4H.265VP9/WebMAV1
Suporte em browser99%+~80%~90%~70%
Suporte mobileUniversalSó iOSAndroid maioriaLimitado
Velocidade de encodingRápida2-3x mais lento3-5x mais lento10x mais lento
Decode em hardwareUniversalDispositivos mais novosChrome/AndroidSó os mais novos
Tamanho de arquivo (baseline)1x0.5x0.6x0.4x

H.264 ganha em compatibilidade. Se sua audiência é global e inclui dispositivos mais antigos, é a única escolha segura como padrão. Você pode oferecer H.265 ou AV1 como progressive enhancement para clients capazes. Para um mergulho profundo nas configurações de compressão, veja nossas boas práticas de compressão de vídeo com FFmpeg.

O comando base de transcodificação

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 que cada flag importa:

  • -crf 23: Bom equilíbrio de qualidade e tamanho para conteúdo UGC
  • -profile:v high -level 4.1: Compatibilidade máxima com dispositivos modernos
  • -pix_fmt yuv420p: Necessário para playback em muitos dispositivos (algumas câmeras produzem yuv422p ou yuv444p)
  • -movflags +faststart: Move os metadados para o início do arquivo para que o playback comece antes do download completo terminar — crítico para entrega web

Lidando com rotação

Vídeos de celular geralmente têm metadados de rotação em vez de pixels rotacionados fisicamente. O FFmpeg lida com isso automaticamente com -c:v libx264, mas se você precisa forçar:

ffmpeg -i input.mp4 -c:v libx264 -crf 23 -vf "transpose=1" output.mp4

Limitando a resolução

Usuários enviam 4K, mas seu player vai no máximo até 1080p. Limite a resolução sem fazer upscale em vídeos menores:

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

Isso faz downscale de qualquer coisa maior que 1920x1080 deixando vídeos menores intocados, e garante que as dimensões sejam números pares (exigência do H.264).

Estágio 3: Geração de thumbnail e preview

Todo vídeo precisa pelo menos de uma thumbnail. A maioria das plataformas também gera previews animados para hover.

Thumbnail estática

Extrai um frame na marca dos 2 segundos (evita frames pretos de intros):

ffmpeg -i input.mp4 -ss 2 -frames:v 1 -q:v 2 thumbnail.jpg

Múltiplos candidatos a thumbnail

Extrai 5 frames igualmente espaçados e deixa o usuário escolher (ou use um scorer de qualidade de imagem para auto-selecionar):

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

Uma abordagem mais simples — extrair em timestamps específicos:

# 在 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 ou WebP)

Um preview de 3-5 segundos em loop para 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 melhor qualidade e tamanho menor, use 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

Estágio 4: Saídas multi-resolução

Para streaming adaptativo, gere múltiplas resoluções a partir da fonte:

# 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

Note como o CRF aumenta e o bitrate de áudio diminui para resoluções mais baixas — resolução menor significa menos informação visual, então você pode comprimir mais agressivamente.

Ladder de resolução

ResoluçãoCRFBitrate de vídeo (típico)Bitrate de áudioUso alvo
1080p233-5 Mbps128kWiFi / banda larga
720p231.5-3 Mbps96kMobile bom
480p260.5-1.5 Mbps64kMobile lento
360p280.3-0.7 Mbps48kConexão muito lenta

Pipeline de processamento completo

Aqui vai uma implementação Node.js que orquestra o pipeline inteiro — do upload a todas as saídas:

// 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));

Arquitetura: do upload à entrega

A arquitetura completa de produção para uma plataforma UGC:

┌──────────┐     ┌──────────────┐     ┌──────────────────┐
│  Client  │────>│  Your API    │────>│  Object Storage  │
│  Upload  │     │  Server      │     │  (S3 / R2)       │
└──────────┘     └──────┬───────┘     └────────┬─────────┘
                        │                      │
                        │ POST /tasks          │ source URL
                        v                      │
                 ┌──────────────┐              │
                 │  Transcoding │<─────────────┘
                 │  API         │
                 └──────┬───────┘
                        │
                        │ webhook callback
                        v
                 ┌──────────────┐     ┌──────────────────┐
                 │  Your API    │────>│  CDN              │
                 │  (webhook)   │     │  (CloudFront/CF) │
                 └──────────────┘     └──────────────────┘

Fluxo

  1. Client faz upload do vídeo cru para seu API server (ou direto para object storage via presigned URL)
  2. Sua API armazena o arquivo cru e cria um job de processamento
  3. Transcoding API recebe o comando FFmpeg com a URL da fonte, processa o vídeo
  4. Webhook notifica sua API quando o processamento termina, com URLs de saída
  5. Sua API atualiza o banco e disponibiliza o vídeo via CDN

Schema de banco

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);

Performance em escala

Benchmarks de tempo de processamento

FonteDuraçãoSaída 1080pThumbnailPipeline total
Clip 1080p 30s30s~15s<1s~25s
Vídeo 1080p 5min5min~90s<1s~120s
Vídeo 4K 10min10min~300s<1s~360s
Clip 720p 1min1min~20s<1s~30s

Esses são aproximados — tempos reais dependem do codec da fonte, bitrate, e complexidade.

Estratégias de scaling

Para 100 uploads/dia: processamento single-thread com uma fila simples. Até uma fila básica (em DB) é suficiente.

Para 1.000 uploads/dia: processamento concorrente com 5-10 jobs em paralelo. Adicione monitoramento e alerta para jobs travados.

Para 10.000+ uploads/dia: arquitetura async completa com webhooks, banco dedicado de tracking, dead letter queue para falhas, e workers com auto-scaling ou uma cloud API que cuida do scaling por você. Nosso guia de transcodificação em lote cobre essa arquitetura em detalhes.

Considerações de custo

Para uma plataforma social processando 5.000 uploads de vídeo por dia (média de 2 minutos cada):

  • Self-hosted (servidores GPU dedicados): ~$2.000-5.000/mês de servidores + tempo de engenharia
  • Cloud transcoding API: ~$500-1.500/mês dependendo dos variants de saída
  • Híbrido: carga base em hardware dedicado, picos para cloud API

Pegadinhas que pegamos

1. Não tratar vídeos sem áudio ou silenciosos

Alguns uploads não têm stream de áudio. O FFmpeg vai dar erro se você tentar codificar áudio 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 aspect ratio

Fazer scale para uma resolução fixa sem preservar aspect ratio produz vídeo esticado. Sempre use -2 para a dimensão livre ou force_original_aspect_ratio=decrease.

3. Sem timeout no processamento

Um arquivo corrompido pode fazer o FFmpeg travar indefinidamente. Sempre defina um timeout:

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 600000); // 10 分钟超时

try {
  await fetch(url, { signal: controller.signal });
} finally {
  clearTimeout(timeout);
}

4. Armazenar só uma resolução

Se você só guarda o original, todo playback em conexão lenta vai bufferar. Se só guarda uma versão transcodificada, não consegue se adaptar às condições de rede. Guarde pelo menos 2-3 resoluções.

Conclusão

Um pipeline de processamento de vídeo UGC não precisa ser complicado, mas precisa ser confiável. A receita central é:

  1. Validar uploads cedo (formato, tamanho, duração)
  2. Normalizar para H.264 MP4 com configurações consistentes
  3. Gerar thumbnails e previews em paralelo com a transcodificação
  4. Sair em múltiplas resoluções para entrega adaptativa
  5. Entregar via CDN com cache headers apropriados

O código de pipeline mostrado neste artigo cobre todos esses passos. Para o compute de transcodificação, você pode rodar FFmpeg nos seus próprios servidores ou usar uma cloud API como o FFHub.io para evitar gerenciar a infra do FFmpeg.

Comece simples — uma única resolução com uma thumbnail — e adicione complexidade (multi-resolução, previews animados, packaging HLS) conforme sua plataforma cresce. Se sua plataforma é um produto SaaS, nosso guia de processamento de vídeo para SaaS cobre a decisão build-vs-buy e padrões de integração.

Artigos relacionados

Processamento de vídeo para plataformas UGC: o guia completo de engenharia | FFHub