Saat burn-in caption ke video 1080×1920 untuk TikTok atau Reels, rencananya biasanya masuk akal: UI feed menutupi kira-kira seperlima bagian bawah frame, jadi taruh caption di atas area itu. Dengan filter subtitles di ffmpeg, Anda akan memakai force_style dan mengatur MarginV. Lalu Anda render, dan caption-nya muncul di tempat yang sama sekali lain, dan jauh lebih besar dari perkiraan.
Penyebabnya, file .srt sama sekali tidak tahu seberapa besar video Anda. ffmpeg mengubahnya menjadi script ASS dengan canvas yang tetap dan sangat kecil, dan setiap angka di force_style dibaca dalam satuan canvas itu. Di bawah ini ada hasil pengukuran pada render 1080×1920 sungguhan, plus force_style yang menaruh caption tepat di posisi yang sesuai hitungan.
Canvas 384×288 di balik setiap .srt
Konversi .srt apa saja dengan ffmpeg lalu lihat header-nya:
ffmpeg -i one.srt one.ass && head -12 one.ass
[Script Info]
; Script generated by FFmpeg/Lavc61.19.100
ScriptType: v4.00+
PlayResX: 384
PlayResY: 288
ScaledBorderAndShadow: yes
...
Style: Default,Arial,16,&Hffffff,&Hffffff,&H0,&H0,0,0,0,0,100,100,0,0,1,1,0,2,10,10,10,1
PlayResX: 384 dan PlayResY: 288 adalah sistem koordinat script, sebuah canvas 4:3 yang kecil. libass menskalakannya ke frame tempat ia me-render: nilai vertikal dikali 1920 / 288 = 6.67, nilai horizontal dikali 1080 / 384 = 2.81. MarginV, Fontsize, Outline, MarginL, dan MarginR di force_style semuanya dalam satuan piksel script tersebut.
Jadi MarginV=28 bukan "28 piksel" dan bukan "28% dari tinggi". Di frame 1920 piksel, nilainya sekitar 187 piksel. Fontsize=22 sekitar 147 piksel. Yang kedua saya cek langsung: huruf kapital H dengan Fontsize=22 dan dengan PlayResY=1920,Fontsize=146.67 menempati piksel yang persis sama (baris 1258 sampai 1351).
Mengukur di mana teks benar-benar muncul
Supaya dapat angka, bukan sekadar kesan, render satu caption di atas frame abu-abu polos 1080×1920, baca kembali frame itu sebagai byte grey mentah, lalu cari piksel putihnya. Standard library Python sudah cukup:
import subprocess, sys
FF = sys.argv[1] # path to an ffmpeg built with libass
W, H = 1080, 1920
def text_box(style, srt="one.srt"):
"""Burn one caption onto a grey 1080x1920 frame and return where the white text landed."""
vf = f"subtitles={srt}:force_style='{style}',format=gray"
raw = subprocess.run(
[FF, "-v", "error", "-f", "lavfi", "-i", f"color=c=0x303030:s={W}x{H}:d=2",
"-vf", vf, "-ss", "1", "-frames:v", "1", "-f", "rawvideo", "-"],
capture_output=True, check=True).stdout
pts = [(i // W, i % W) for i, v in enumerate(raw) if v > 200]
rows = sorted({y for y, _ in pts})
lines = 1 + sum(1 for a, b in zip(rows, rows[1:]) if b - a > 20)
xs = [x for _, x in pts]
return rows[0], rows[-1], min(xs), max(xs), lines
Caption di one.srt adalah satu baris 38 karakter dari transkrip sungguhan, "the Saharan dust cloud that we've seen", tampil dari detik 0 sampai 2. Threshold 200 menangkap isi teks yang putih dan mengabaikan outline hitam; jeda lebih dari 20 baris kosong dihitung sebagai baris teks baru.
Hasilnya
Targetnya adalah yang dipakai editor video osFoundry untuk 9:16: 20% bagian bawah frame tertutup feed, dan caption duduk di 72% dari atas, yang pada 1920 piksel berarti teks seharusnya berakhir di sekitar baris 1382.
| force_style | Atas teks | Bawah teks | Baris |
|---|---|---|---|
MarginV=10,Fontsize=22 |
1437 (74.8%) | 1826 (95.1%) | 3 |
MarginV=28,Fontsize=22 |
1317 (68.6%) | 1706 (88.9%) | 3 |
MarginV=81,Fontsize=22 |
964 (50.2%) | 1352 (70.4%) | 3 |
PlayResX=1080,PlayResY=1920,MarginV=538,Fontsize=80 |
1235 (64.3%) | 1366 (71.1%) | 2 |
Ada tiga hal yang terlihat.
MarginV=28, angka yang Anda dapat kalau menganggap margin sebagai persentase (100 − 72), membuat baris terakhir berada di 89% tinggi frame, jauh di dalam area yang tertutup. MarginV=81 adalah 28% yang sama dalam piksel script (0.28 × 288), dan tepi bawahnya memang jatuh di 70%.
Tapi membetulkan margin saja belum cukup. Pada Fontsize=22, baris 38 karakter tadi berubah jadi tiga baris teks setinggi 147 piksel, dan karena caption ditambatkan di bagian bawah, baris tambahan tumbuh ke atas. Bagian atas caption sampai ke 50% frame, yang pada klip talking-head adalah dada atau wajah pembicara.
Sisi kiri dan kanan punya masalah yang sama, hanya arahnya terbalik. Default MarginL=10 dan MarginR=10 hanya jadi sekitar 28 piksel di frame selebar 1080 (mengubah MarginL dari 10 ke 23 menggeser teks 37 piksel, 2.85 per unit), jadi teks yang di-wrap bisa masuk ke strip 6% di sisi tempat tombol like dan share berada.
Pakai piksel asli saja
Solusi paling sederhana adalah memberi tahu libass bahwa canvas script itu adalah videonya. PlayResX dan PlayResY adalah field script info, tapi libass juga menerimanya di force_style, dan setelah itu setiap margin dan ukuran dalam satuan piksel frame:
def safe_style(top=0.08, bottom=0.20, sides=0.06, baseline=0.72, size=80):
"""force_style that works in real pixels, with the caption sitting on `baseline`."""
return ",".join([
f"PlayResX={W}", f"PlayResY={H}",
"Alignment=2",
f"MarginV={round((1 - baseline) * H)}",
f"MarginL={round(sides * W) + 15}", f"MarginR={round(sides * W) + 15}",
f"Fontsize={size}", "Outline=5",
])
Fungsi ini menghasilkan PlayResX=1080,PlayResY=1920,Alignment=2,MarginV=538,MarginL=80,MarginR=80,Fontsize=80,Outline=5, yaitu baris terakhir di tabel: dua baris, tepi bawah di 71.1%, dan teks di antara x = 200 dan 877, jauh dari kedua strip samping selebar 65 piksel. Smart wrapping bawaan libass membagi baris menjadi dua dengan lebar yang mirip, bukan satu panjang dan satu pendek.
Di command line:
ffmpeg -i in.mp4 -vf "subtitles=captions.srt:force_style='PlayResX=1080,PlayResY=1920,Alignment=2,MarginV=538,MarginL=80,MarginR=80,Fontsize=80,Outline=5'" -c:a copy out.mp4
Satu detail saat berhitung: MarginV mengukur sampai bagian bawah kotak baris, yang sudah termasuk ruang untuk descender, bukan sampai baseline. Dengan MarginV=540, bagian bawah huruf kapital H ada di baris 1351, sekitar 29 piksel di atas 1920 − 540 = 1380. Kalau beberapa piksel itu penting, ukur sendiri dengan font Anda daripada percaya pada angkanya.
Kalau Anda tidak mau bergantung pada force_style, menulis file .ass sendiri dengan PlayResX: 1080 dan PlayResY: 1920 di header memberi hasil yang sama, dan Anda bisa memberi style berbeda untuk tiap baris.
Berapa biaya burn-in-nya
Pada klip 1080×1920 berdurasi 32 detik dengan 13 caption, burn-in dengan style di atas memakan 13.2 detik di Mac M1 dengan ffmpeg 7.1 dan setting default libx264. Re-encode klip yang sama tanpa filter subtitle memakan 13.3 detik. Waktu untuk menggambar teks tenggelam di dalam noise; seluruh biayanya ada di encoding.
Area mana yang dijadikan target
Angka safe area tidak punya satu standar. Angka-angka di atas adalah milik editor osFoundry sendiri (SAFE_AREAS di kode canvas-nya: 9:16 adalah atas 8%, bawah 20%, samping 6%), dan menggambarkan apa yang ditimpa feed di atas postingan biasa. Platform-platform memublikasikan panduan terutama untuk iklan:
| Penempatan | Area yang dikosongkan | Sumber |
|---|---|---|
| Iklan Instagram Reels | atas 14%, bawah 35%, samping 6% | Meta Ads Guide, spesifikasi video Reels |
| Iklan in-feed TikTok | tergantung panjang caption iklan; ada file safe zone yang bisa diunduh, dengan versi terpisah untuk wilayah right-to-left dan untuk iklan dengan anchor | Pusat bantuan TikTok Ads |
| Iklan YouTube Shorts | template safe zone 1080×1920; overlay berbeda-beda per format | Bantuan Google Ads |
| Editor osFoundry, 9:16 | atas 8%, bawah 20%, samping 6% | SAFE_AREAS |
Untuk iklan di Reels, fungsi yang sama dengan bottom=0.35 dan baseline sekitar 0.62 menjaga caption tetap di atas area Meta. Inti dari menghitung style alih-alih mengetiknya manual adalah: ganti target cukup dengan satu argumen.
Aplikasinya menurunkan tinggi caption dari safe area di satu fungsi kecil, dan alasannya ditulis di komentarnya:
/// Where captions sit, as a fraction of frame height from the top.
///
/// 🔴 Derived from the platform safe area, not from a fixed margin. On a vertical feed the
/// bottom fifth of the frame is under the platform's own caption and buttons, so a caption
/// placed at the conventional 90% is guaranteed to be covered — which is the whole reason C1
/// shipped the safe-area overlay.
pub fn caption_baseline(aspect: &str) -> f64 {
match aspect {
"9:16" => 0.72,
"1:1" => 0.84,
_ => 0.88,
}
}
Aplikasi desktop dengan editor tersebut bisa diunduh gratis dari halaman download osFoundry, dan penjelasan lebih lanjut tentang bagaimana osFoundry menjaga pekerjaan seperti ini tetap di komputer Anda sendiri ada di halaman local-first.

Top comments (0)