Progressive MP4 is not adaptive streaming. Transcode off the HTTP request; serve .mpd + CMAF segments. Resumable upload: tus-golang-nextjs.
Stack and scope
| Part | Role |
|---|---|
| FFmpeg | Decode, encode, muxer dash |
| ffprobe | Duration, codecs, resolution |
| Job/worker | Post-upload queue (golang-goroutines) |
| Object storage / disk | DASH artifacts |
| Gin | Static/range for segments (if the app serves them) |
VOD (complete file) is the default. Live DASH/HLS is out of scope unless the project is already live.
Pipeline
MP4 (TUS complete) → probe → transcode DASH → store → publicar URL do .mpd
- Upload finishes (TUS hook
PreFinish/PostFinish). - Enqueue job with
videoID; respond202— do not block the handler. - Worker: probe → FFmpeg with timeout/context → write
manifest.mpd+init-*.m4s+chunk-*.m4s. - Mark status
ready/failed; point the player only to the.mpdwhen ready.
Non-negotiable rules
- FFmpeg via
exec.CommandContext— cancel on shutdown and on job timeout. - Never transcode in the Gin handler goroutine without a queue + concurrency limit.
- Paths: opaque IDs; never concatenate the client filename into
exec. - Validate codec/container in the probe; reject unsupported formats before encoding.
- Atomic segments + manifest: publish the
.mpdonly when the job completes (staging dir → rename/upload). - Correct MIME types:
application/dash+xml(.mpd),video/iso.segmentorvideo/mp4(.m4s).
FFmpeg — MP4 → MPEG-DASH
Minimal ladder (adjust to the product): 360p / 720p / 1080p, stereo AAC, H.264 +faststart equivalent in CMAF (separate init).
ffmpeg -y -i "$IN" -hide_banner -loglevel error \
-filter_complex "[0:v]split=3[v360][v720][v1080]; \
[v360]scale=w=640:h=360:force_original_aspect_ratio=decrease:force_divisible_by=2[v360o]; \
[v720]scale=w=1280:h=720:force_original_aspect_ratio=decrease:force_divisible_by=2[v720o]; \
[v1080]scale=w=1920:h=1080:force_original_aspect_ratio=decrease:force_divisible_by=2[v1080o]" \
-map "[v360o]" -map "[v720o]" -map "[v1080o]" -map 0:a:0? \
-c:v libx264 -preset veryfast -profile:v high -pix_fmt yuv420p \
-c:a aac -b:a 128k -ac 2 \
-b:v:0 400k -maxrate:v:0 600k -bufsize:v:0 800k \
-b:v:1 1500k -maxrate:v:1 2000k -bufsize:v:1 3000k \
-b:v:2 4000k -maxrate:v:2 5000k -bufsize:v:2 8000k \
-g 48 -keyint_min 48 -sc_threshold 0 \
-adaptation_sets "id=0,streams=v id=1,streams=a" \
-use_timeline 1 -use_template 1 -seg_duration 4 \
-init_seg_name "init-\$RepresentationID\$.m4s" \
-media_seg_name "chunk-\$RepresentationID\$-\$Number%05d\$.m4s" \
-f dash "$OUT/manifest.mpd"
- GOP aligned to
seg_duration(e.g., 4s @ 24fps →-g 96if fps is 24; derive from the probe). - No audio: omit
-map 0:abased on the probe — do not let FFmpeg fail opaquely. libx265/av1only if the player and CPU cost are acceptable; H.264 is the compatibility default.- Keep the command in versioned config/templates; log the exit code and truncated stderr.
Job in Go
cmd := exec.CommandContext(ctx, "ffmpeg", args...)
cmd.Dir = workDir
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("ffmpeg: %w: %s", err, truncate(stderr.String(), 4<<10))
}
SetLimitin the worker pool (1–N according to CPU/GPU).- Disk: quota per job; clean up
workDirin thedeferafter uploading the artifacts. - Idempotency: the same
videoIDdoes not trigger two encodes (lock/statusprocessing). - Optional progress: parse
-progress pipe:1if the UX asks for a percentage.
Serving
- CDN / object storage in front of the
.m4s; Go origin only if the volume is low. - CORS if the player (Next.js) is on another host:
GET/HEADon the artifacts. - Cache-Control:
manifest.mpdshort if the job can republish; immutable segmentsimmutable. - Do not use
http.ServeFilefrom a directory with a user path without an allowlist ofvideoID. - Player: dash.js / Shaka on the frontend; public URL only from
.mpd(+ token if the VOD is private).
Anti-patterns
- Synchronous transcode in the
POSTupload ffmpegwith args interpolated from the original filename- A single “1080p” bitrate sold as DASH
- Serving the original MP4 in the player “while DASH isn't ready” without an explicit product
- Misaligned GOP/segments (broken seeking)
- Unbounded FFmpeg queue
Definition of done
- Asynchronous job with context, limit and status
.mpd+ init/media segments published atomically- ABR ladder and documented codecs
- Correct MIME, CORS and cache
- No path traversal / injection in FFmpeg
- Player can do ABR and seek on the generated artifact