FFmpeg on Cloud Run: AI Video Gotchas
2026-02-26 · Jazz Lien
The Setup
My video editor runs on Cloud Run. Users combine AI-generated clips (from Veo, BytePlus, Kling) with music, trim points, and titles. The backend uses FFmpeg to concatenate and export.
Worked perfectly in development. Broke mysteriously in production.
The Bug: "Stream map matches no streams"
Stream map '0:a:0' matches no streams.
Users reported "Failed to concatenate clips" with no other context. The generic error message hid the real problem.
The Root Cause
AI-generated videos from photo animation providers often have no audio track.
When you give Veo a photo and ask it to animate, you get video only. No audio. Not even silence.
My FFmpeg command assumed audio:
ffmpeg -i clip1.mp4 -i clip2.mp4 -filter_complex "[0:v][0:a][1:v][1:a]concat=n=2:v=1:a=1" output.mp4
The [0:a] references audio stream 0 of input 0. If there's no audio stream, FFmpeg fails.
The Fix: Detect Before Processing
Step 1: Probe for Audio
async function hasAudioTrack(filePath: string): Promise<boolean> {
const result = await exec(
`ffprobe -v error -select_streams a -show_entries stream=codec_type -of csv=p=0 "${filePath}"`
);
return result.stdout.trim() === 'audio';
}
Step 2: Add Silent Audio If Missing
async function ensureAudioTrack(inputPath: string, outputPath: string): Promise<void> {
if (await hasAudioTrack(inputPath)) {
// Already has audio, just copy
await fs.copyFile(inputPath, outputPath);
return;
}
// Get video duration
const duration = await getVideoDuration(inputPath);
// Add silent audio track
await exec(`
ffmpeg -i "${inputPath}" \
-f lavfi -i anullsrc=r=44100:cl=stereo \
-c:v copy -c:a aac -shortest \
-t ${duration} \
"${outputPath}"
`);
}
The anullsrc filter generates silence. -shortest stops when the video ends.
Step 3: Pre-process All Inputs
Before concatenation:
async function prepareClipsForConcat(clips: string[]): Promise<string[]> {
const prepared: string[] = [];
for (const clip of clips) {
const tempPath = `/tmp/${uuid()}.mp4`;
await ensureAudioTrack(clip, tempPath);
prepared.push(tempPath);
}
return prepared;
}
Safety Net: Optional Stream Mapping
Even with pre-processing, add a safety net. FFmpeg 4.1+ supports optional mapping:
ffmpeg -i input.mp4 -map 0:v:0 -map 0:a:0? output.mp4
# ^ question mark = optional
The ? makes the audio stream optional. If it doesn't exist, FFmpeg continues without it instead of failing.
For concat:
ffmpeg -i clip1.mp4 -i clip2.mp4 \
-filter_complex "[0:v][1:v]concat=n=2:v=1:a=0[outv]" \
-map "[outv]" \
output.mp4
This concatenates video only. Add audio in a separate pass if needed.
Debugging Cloud Run FFmpeg
Check the Actual Error
Client sees: "Failed to concatenate clips"
Server logs have the real error:
gcloud run logs read --service=my-service --limit=50
Look for FFmpeg stderr. The stream mapping error points to the -map flag, but the actual problem is the missing stream.
Common Culprits
| Source |
Audio? |
| AI photo animation (Veo, BytePlus, Kling) |
Usually no |
| Screen recordings |
Sometimes no |
| Re-encoded clips |
Might be stripped |
| User uploads |
Check before assuming |
FFprobe Cheat Sheet
# Check all streams
ffprobe -show_streams input.mp4
# Check just audio
ffprobe -select_streams a -show_entries stream=codec_type input.mp4
# Get duration
ffprobe -v error -show_entries format=duration -of csv=p=0 input.mp4
Cloud Run Specifics
Memory for Video Processing
Video processing is memory-hungry. Default 512MB isn't enough:
gcloud run deploy my-service --memory=2Gi
Timeout for Long Videos
Default 5-minute timeout may not be enough for long exports:
gcloud run deploy my-service --timeout=15m
Concurrency
FFmpeg is CPU-intensive. Lower concurrency to avoid resource contention:
gcloud run deploy my-service --concurrency=4
The Pattern
1. Receive clip URLs
2. Download to /tmp
3. Probe each for audio
4. Add silent audio where missing
5. Concatenate with optional mapping as safety net
6. Clean up temp files
7. Return result
Never assume AI-generated video has audio. Always check.
FFmpeg is infinitely capable and infinitely confusing. The -map documentation is your friend.