FFmpeg ile fotoğraflardan fade geçişli bir slayt video yapmak istedim. Elimde farklı çözünürlüklerde, dikey ve yatay karışık bir sürü fotoğraf vardı. Üzerine bir de uzun bir müzik ekleyip, fotoğrafların yumuşak fade in ve fade out geçişlerle aktığı bir video klip üretmek istiyordum. Bunu herhangi bir grafik arayüzlü programa bulaşmadan, tamamen FFmpeg ve script kullanarak yapmayı hedefledim.
Temel problem şuydu: Fotoğraflar birbirinden tamamen farklı boyutlardaydı. Bazıları dikey, bazıları yataydı. Ayrıca müzik süresi slaytların toplam süresinden uzundu ve yeni foto eklemek istemiyordum. Video süresi müzik süresi kadar olmalıydı.
İlk adımda tüm fotoğrafları ortak bir video formatına zorladım. Bunun için her fotoğrafa aynı FFmpeg filtresini uyguladım. Bu filtre fotoğrafı 1920x1080 çözünürlüğe, oranı bozmadan sığdırıyor, boş kalan yerleri pad ile dolduruyor, fade işlemleri için gerekli olan rgba formatına çeviriyor ve fps değerini sabitliyordu. Böylece fotoğrafın dikey ya da yatay olması tamamen önemsiz hale geldi.
Asıl iş fade geçişlerinde başladı. FFmpeg’in xfade filtresini kullanarak fotoğrafları zincir halinde birbirine bağladım. Her fotoğraf ekranda dört saniye kalıyor, bunun bir saniyesi bir sonraki fotoğrafa fade ile geçiyordu. Offset değerlerini fotoğraf sırasına göre manuel olarak hesaplayıp zincirledim. Bu işlemin sonunda tek bir video çıkışı elde ettim.
Bu noktada bir hatayla karşılaştım. FFmpeg sürekli olarak “No such filter: ''” hatasını veriyordu. İlk başta insan otomatik olarak fotoğraflarda, xfade kullanımında ya da müzik dosyasında bir problem olduğunu düşünüyor. Oysa sorun bunların hiçbiri değildi.
Hatanın gerçek sebebi filter.txt dosyasının sonunda kalan tek bir noktalı virgüldü. Filtergraph satırı noktalı virgül ile bittiğinde FFmpeg bir filter daha geleceğini varsayıyor. Ama devamında bir şey olmadığı için boş bir filter algılıyor ve bu da “No such filter: ''” hatasına yol açıyor. Yani tek bir fazla karakter tüm işi bozabiliyor. Çözüm, filter dosyasının son satırının kesinlikle noktalı virgül ile bitmemesi.
Fade’li slayt sorunsuz çalışmaya başladıktan sonra bu sefer müzik süresi problemi ortaya çıktı. Slaytlar kısa, müzik uzundu. Yeni foto eklemek istemediğim için slaytı loop etmek gerekiyordu. Bunu doğrudan fotoğraflar üzerinde yapmak FFmpeg açısından oldukça kırılgan bir yöntem. En stabil çözüm iki aşamalı çalışmak oldu.
Önce slayt zincirini bir kere render edip tek bir video dosyası ürettim. Ardından bu video dosyasını stream_loop parametresi ile sonsuz döndürdüm ve üzerine müziği ekledim. shortest parametresi sayesinde müzik bittiği anda video otomatik olarak kesildi. Bu yöntemle video süresi birebir müzik süresi kadar oldu ve fade geçişleri bozulmadı.
Son aşamada videonun başından tam olarak iki saniye kesmek istedim. İlk refleks olarak re-encode yapmadan, kopyalama ile kesmeyi denedim. FFmpeg komutu hatasız çalıştı ve dosya oluştu ama video fiilen kesilmemiş gibiydi. Bunun sebebi FFmpeg’in keyframe mantığıydı. c copy kullanıldığında FFmpeg keyframe olmayan bir noktadan net kesme yapamıyor. Dosya oluşsa bile görüntü başı çoğu zaman aynı kalıyor.
Bu durumda kesin çözüm re-encode ederek kesmek oldu. Video yeniden encode edildiğinde FFmpeg tam olarak ikinci saniyeden başlatıyor ve sonuç birebir istenen gibi oluyor. Küçük bir kalite kaybı olsa da pratikte fark edilmiyor.
Bu süreçte FFmpeg’in inanılmaz güçlü ama aynı zamanda aşırı hassas bir araç olduğunu tekrara zevkle gözlemledim. Özellikle filtergraph söz dizimi tek bir karakter yüzünden tamamen çökmeye çok müsait. Sondaki bir noktalı virgül uzunca süre debug yapmak zorunda bırakabiliyor. Ayrıca loop işlemlerinin mümkün olduğunca final videoda yapılması, c copy kullanımının ise keyframe bağımlılığı nedeniyle dikkatli ele alınması gerekiyor.
FFmpeg bence GUI olmadan piyasadaki son kullanıcı programlarından daha pratik, esnek ve güçlü bir video üretim aracı. En iyisi de nerd işi olması :)
# make_slides.ps1
# Aynı klasördeki jpg/png'leri alır, 1920x1080 yapar, fade geçişli slayt video üretir.
# Çıktı: out_slides.mp4
$ErrorActionPreference = "Stop"
$ffmpeg = "ffmpeg"
$here = (Get-Location).Path
# Ayarlar
$w = 1920
$h = 1080
$fps = 24
$still = 4 # her foto toplam kaç saniye görünsün
$xfade = 1 # geçiş süresi
$transition = "fade"
$outFile = Join-Path $here "out_slides.mp4"
# Görselleri topla (music.mp3 vs hariç)
$imgs = Get-ChildItem -LiteralPath $here -File |
Where-Object { $_.Extension -match '^\.(jpg|jpeg|png)$' } |
Sort-Object Name
if ($imgs.Count -lt 2) { throw "En az 2 foto lazım." }
Write-Host "Images:" $imgs.Count
Write-Host "Output:" $outFile
# Filtergraph üret
# Her input: scale+pad+format+setsar+fps -> [v0]..[vN]
$parts = New-Object System.Collections.Generic.List[string]
for ($i=0; $i -lt $imgs.Count; $i++) {
$parts.Add("[$i:v]scale=$w`:$h`:force_original_aspect_ratio=decrease,pad=$w`:$h`:(ow-iw)/2`:(oh-ih)/2,format=rgba,setsar=1,fps=$fps[v$i]")
}
# xfade zinciri
# Offset mantığı: her yeni foto bir öncekinin (still - xfade) kadar ilerisine oturur
# i=0->1: offset = still - xfade
# i=1->2: offset = 2*(still - xfade)
$step = ($still - $xfade)
$offset = $step
if ($imgs.Count -ge 2) {
$parts.Add("[v0][v1]xfade=transition=$transition:duration=$xfade:offset=$offset[x1]")
for ($i=2; $i -lt $imgs.Count; $i++) {
$offset = $step * $i
$parts.Add("[x" + ($i-1) + "][v$i]xfade=transition=$transition:duration=$xfade:offset=$offset[x$i]")
}
$last = "x" + ($imgs.Count - 1)
$parts.Add("[$last]format=yuv420p[vout]")
} else {
# teoride buraya düşmez çünkü 2 foto şart dedik
$parts.Add("[v0]format=yuv420p[vout]")
}
$filter = ($parts -join ";")
# Filter dosyasını temiz ASCII olarak yaz (BOM yok, sonda boş filter yok)
$filterPath = Join-Path $here "filter_slides.txt"
[System.IO.File]::WriteAllText($filterPath, $filter, (New-Object System.Text.ASCIIEncoding($false)))
# ffmpeg args
$args = New-Object System.Collections.Generic.List[string]
foreach ($img in $imgs) {
$args.AddRange(@("-loop","1","-t",$still.ToString(),"-i",$img.FullName))
}
$args.AddRange(@(
"-filter_complex_script", $filterPath,
"-map","[vout]",
"-c:v","libx264",
"-preset","medium",
"-crf","18",
"-pix_fmt","yuv420p",
"-y", $outFile
))
Write-Host "Running..."
& $ffmpeg @args
if ($LASTEXITCODE -ne 0) { throw "ffmpeg hata verdi." }
Write-Host "OK -> $outFile"
# loop_audio.ps1
# out_slides.mp4'ü müzik bitene kadar loop eder, çıktı: out_loop.mp4
# stream_loop -1 ile video sonsuz döner, shortest ile audio bitince durur.
$ErrorActionPreference = "Stop"
$ffmpeg = "ffmpeg"
$here = (Get-Location).Path
$slides = Join-Path $here "out_slides.mp4"
$music = Join-Path $here "music.mp3"
$out = Join-Path $here "out_loop.mp4"
if (!(Test-Path $slides)) { throw "out_slides.mp4 yok. Önce make_slides.ps1 çalıştır." }
if (!(Test-Path $music)) { throw "music.mp3 yok." }
Write-Host "Slides:" $slides
Write-Host "Music :" $music
Write-Host "Out :" $out
# NOT: Burada filter yok. En sağlamı bu.
& $ffmpeg `
-stream_loop -1 -i $slides `
-i $music `
-map 0:v:0 -map 1:a:0 `
-c:v libx264 -preset medium -crf 18 -pix_fmt yuv420p `
-c:a aac -b:a 192k `
-shortest -y $out
if ($LASTEXITCODE -ne 0) { throw "ffmpeg hata verdi." }
Write-Host "OK -> $out"
# cut2s.ps1
# out_loop.mp4 başından 2 saniye keser.
# c copy bazen keyframe yüzünden kesmiyor gibi görünür, o yüzden re-encode garanti.
$ErrorActionPreference = "Stop"
$ffmpeg = "ffmpeg"
$here = (Get-Location).Path
$in = Join-Path $here "out_loop.mp4"
$out = Join-Path $here "out_loop_cut.mp4"
if (!(Test-Path $in)) { throw "out_loop.mp4 yok. Önce loop_audio.ps1 çalıştır." }
Write-Host "Input :" $in
Write-Host "Output:" $out
# Garanti kesim: re-encode
& $ffmpeg -ss 2 -i $in `
-c:v libx264 -preset medium -crf 18 -pix_fmt yuv420p `
-c:a aac -b:a 192k `
-movflags +faststart `
-y $out
if ($LASTEXITCODE -ne 0) { throw "ffmpeg hata verdi." }
Write-Host "OK -> $out"






