Concatenação de MP4 sem re-encode
Esta API recebe um ou mais arquivos .mp4 e os concatena em um único vídeo. Pode aplicar máscaras PNG sobrepostas em momentos específicos da timeline ou em todo o projeto.
O processamento é seletivo: trechos sem máscara são copiados sem re-encode (rápido); trechos com máscara são re-encodados em H.264. Os jobs são assíncronos: você posta os arquivos, recebe um jobId, faz polling do progresso e baixa o resultado quando concluído.
Envia os vídeos e (opcionalmente) máscaras via multipart/form-data. Retorna imediatamente um jobId. O processamento ocorre em background.
| Campo | Tipo | Descrição |
|---|---|---|
| videosobrigatório | file (mp4) ×N | 1 a 10 arquivos MP4. A ordem do upload define a ordem da concatenação. |
| masksopcional | file (png) ×N | Até 10 arquivos PNG com transparência. A escala é definida em mask_meta. |
| mask_metaopcional | string (JSON) | Array JSON com os parâmetros de cada máscara — mesma quantidade de itens que masks, na mesma ordem. |
{
"jobId": "abc123def456",
"totalDuration": 45.5,
"hasMasks": true,
"progressUrl": "/merge/progress/abc123def456",
"resultUrl": "/merge/result/abc123def456"
}
Snapshot do estado atual. Faça polling enquanto status for queued ou running.
{
"id": "abc123def456",
"status": "running",
"stage": "re-encode seletivo (somente segmentos com máscara)",
"progress": 0.42,
"elapsedSec": 12.3,
"etaSec": 17.8,
"currentTime": 19.1,
"totalDuration": 45.5,
"speed": "1.5x",
"fps": 28.4,
"frame": 458,
"bitrate": "2400kbits/s",
"hasMasks": true,
"inputCount": 3,
"maskCount": 1,
"error": ""
}
Status possíveis: queued, running, done, error.
Retorna o arquivo merged.mp4 quando status === "done".
Content-Type: video/mp4, Content-Disposition: attachment)mask_metaArray JSON com um objeto por máscara, na mesma ordem dos arquivos enviados em masks.
| Campo | Tipo | Descrição |
|---|---|---|
| positionobrig. | string | Onde a máscara é ancorada no frame: top-left, top, top-right, left, center, right, bottom-left, bottom, bottom-right. |
| scaleobrig. | number | Largura da máscara como fração da largura do vídeo (0.05 a 1.0). Altura mantida proporcional ao PNG. |
| fullTimelineopc. | boolean | Se true, aplica em todos os clipes do início ao fim do vídeo final — independente das durações reais. Ignora start / end. |
| start | number | Segundos onde a máscara começa a aparecer (na timeline do projeto concatenado). Ignorado se fullTimeline=true. |
| end | number | Segundos onde a máscara para de aparecer. Deve ser maior que start. Ignorado se fullTimeline=true. |
Caso mais rápido: apenas concat com -c copy, segundos para finalizar.
curl -X POST https://videos.dunortetecnologia.com.br/merge \ -F "videos=@v1.mp4" \ -F "videos=@v2.mp4"
Logo permanente no canto superior direito ocupando 20% da largura do vídeo, do início ao fim — funciona com qualquer duração de input.
curl -X POST https://videos.dunortetecnologia.com.br/merge \
-F "videos=@video.mp4" \
-F "masks=@logo.png" \
-F 'mask_meta=[{"position":"top-right","scale":0.2,"fullTimeline":true}]'
Logo permanente no canto + frame decorativo central de 0:10 a 0:25 cobrindo metade do vídeo.
curl -X POST https://videos.dunortetecnologia.com.br/merge \
-F "videos=@intro.mp4" \
-F "videos=@conteudo.mp4" \
-F "videos=@outro.mp4" \
-F "masks=@logo.png" \
-F "masks=@frame.png" \
-F 'mask_meta=[
{"position":"top-right","scale":0.15,"fullTimeline":true},
{"position":"center","scale":0.5,"fullTimeline":false,"start":10,"end":25}
]'
Use o jobId da resposta anterior. Recomenda-se intervalo de ~600ms.
JOB_ID="abc123def456"
while true; do
SNAP=$(curl -s https://videos.dunortetecnologia.com.br/merge/progress/$JOB_ID)
STATUS=$(echo $SNAP | jq -r .status)
PCT=$(echo $SNAP | jq -r '.progress * 100 | floor')
echo "[$STATUS] $PCT%"
case "$STATUS" in
done|error) break ;;
esac
sleep 1
done
curl -o merged.mp4 \ https://videos.dunortetecnologia.com.br/merge/result/$JOB_ID
#!/bin/bash
set -e
URL="https://videos.dunortetecnologia.com.br"
# 1) Submete o job
RESPONSE=$(curl -s -X POST "$URL/merge" \
-F "videos=@v1.mp4" \
-F "videos=@v2.mp4" \
-F "masks=@logo.png" \
-F 'mask_meta=[{"position":"top-right","scale":0.2,"fullTimeline":true}]')
JOB_ID=$(echo $RESPONSE | jq -r .jobId)
echo "Job iniciado: $JOB_ID"
# 2) Polling até finalizar
while true; do
SNAP=$(curl -s "$URL/merge/progress/$JOB_ID")
STATUS=$(echo $SNAP | jq -r .status)
PCT=$(echo $SNAP | jq -r '.progress * 100 | floor')
echo -ne "\r[$STATUS] $PCT% "
case "$STATUS" in
done|error) echo; break ;;
esac
sleep 1
done
# 3) Baixa o resultado ou imprime erro
if [ "$STATUS" = "done" ]; then
curl -o merged.mp4 "$URL/merge/result/$JOB_ID"
echo "Salvo em merged.mp4"
else
echo "Erro: $(echo $SNAP | jq -r .error)"
exit 1
fi
O FFmpeg atualiza o progresso a aproximadamente 1Hz. Polling mais frequente que isso não traz benefícios — recomenda-se ~600ms. Sempre passe Cache-Control: no-cache ou similar para evitar respostas em cache de proxies.
O campo progress vai de 0 a 1; multiplique por 100 para porcentagem. etaSec só fica disponível durante running e após uma pequena janela inicial.
.mp4.pngmasksend deve ser maior que start (quando fullTimeline=false)POST /mergeX-API-Key válida são rejeitadas com 401. Admins via Basic Auth (UI) sempre podem submeter.