Videodateien verkleinern mit ffmpeg – aufteilen oder neu encodieren

Manche Dienste akzeptieren nur Dateien bis zu einer bestimmten Größe – sei es ein E-Mail-Anhang, ein Messenger oder ein Upload-Portal. Ist ein Video zu groß, bleibt oft nur das Aufteilen in mehrere Teile.

In diesem Beitrag zeigen wir, wie sich Videodateien mit ffmpeg in kleinere Stücke zerlegen lassen – ohne zeitaufwendiges und qualitätsminderndes Neuencodieren. Neben dem manuellen Weg stellen wir ein Bash-Script vor, das die Aufteilung anhand einer gewünschten Maximalgröße vollständig automatisiert. Für Fälle, in denen das Video als eine Datei erhalten bleiben soll, zeigen wir am Ende des Beitrags außerdem, wie sich die Größe durch ein Neuencodieren reduzieren lässt.

Voraussetzungen

Vorausgesetzt wird ein Linux-System mit installiertem ffmpeg beziehungsweise ffprobe. Unter Debian und Ubuntu genügt dafür folgendes Paket:

Installation von ffmpeg unter Debian/Ubuntu
1
sudo apt install ffmpeg

Die weiteren vom Script genutzten Werkzeuge awk und stat sind auf üblichen Linux-Systemen bereits vorhanden.

Der manuelle Weg

ffmpeg kann ein Video an einer beliebigen Zeitposition teilen. Die beiden wichtigen Optionen dafür sind -ss für den Startpunkt und -t für die Dauer des Ausschnitts. Entscheidend ist der Parameter -c copy: Dabei werden die vorhandenen Video- und Audiostreams unverändert kopiert, anstatt sie neu zu encodieren. Das ist um ein Vielfaches schneller und geht ohne Qualitätsverlust vonstatten.

Ein Video, das an der Position von 50 Minuten geteilt werden soll, lässt sich zum Beispiel so in zwei Teile zerlegen:

Video an einer festen Position teilen
1
2
ffmpeg -ss 0 -i grosses-video.mp4 -t 00:50:00 -map 0 -c copy teil1.mp4
ffmpeg -ss 00:50:00 -i grosses-video.mp4 -map 0 -c copy teil2.mp4

Der erste Befehl erzeugt teil1.mp4 vom Anfang bis zur Marke von 50 Minuten, der zweite Befehl teil2.mp4 mit dem Rest. Mit -map 0 werden alle Streams der Quelldatei übernommen – also auch zusätzliche Tonspuren oder Untertitel. Der Wert für -ss und -t kann wahlweise im Format HH:MM:SS oder als Sekunden angegeben werden.

Beide Teile lassen sich alternativ auch mit einem einzigen Befehl erzeugen:

Beide Teile mit einem Befehl erstellen
1
2
ffmpeg -i grosses-video.mp4 -t 00:50:00 -map 0 -c copy teil1.mp4 \
-ss 00:50:00 -map 0 -c copy teil2.mp4

Hinweis

Durch -c copy kann ffmpeg nur an sogenannten Keyframes schneiden. Liegt an der gewünschten Schnittstelle kein Keyframe, verschiebt sich der tatsächliche Schnitt zum nächsten Keyframe. Die Teile können dadurch minimal länger oder kürzer ausfallen als angegeben.

Das Problem bei der Aufteilung nach Größe

Das manuelle Vorgehen funktioniert gut, wenn die Schnittpositionen in etwa bekannt sind. Soll dagegen eine maximale Dateigröße pro Teil eingehalten werden, wird es aufwendig: Die Dateigröße hängt von der Bitrate ab, und diese schwankt über die Laufzeit des Videos. Aus der gewünschten Größe lässt sich daher nur eine grobe Schätzung für die Schnittdauer ableiten – ob ein Teil am Ende wirklich unter dem Limit bleibt, weiß man erst nach dem Erzeugen.

Genau dieses Problem löst das folgende Script.

Der automatisierte Ansatz

Das folgende Script split-video.sh teilt eine Videodatei automatisiert in möglichst gleich große Teile, die jeweils unter einer vorgegebenen Maximalgröße bleiben:

Bash-Script zum Aufteilen von Videodateien nach Größe
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
#!/bin/bash
# Usage: ./split-video.sh <video-datei>

set -o pipefail

if [[ $# -lt 1 ]]; then
echo "Verwendung: $0 <video-datei>"
exit 1
fi

input=$1

if [[ ! -f "$input" ]]; then
echo "Fehler: Datei nicht gefunden: $input"
exit 1
fi

for cmd in ffmpeg ffprobe awk stat; do
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "Fehler: '$cmd' ist nicht installiert."
exit 1
fi
done

# Maximale Dateigröße abfragen
read -rp "Maximale Dateigröße pro Teil in MB: " max_mb

if ! [[ "$max_mb" =~ ^[0-9]+$ ]] || (( max_mb <= 10 )); then
echo "Fehler: Bitte eine ganze Zahl größer als 10 eingeben."
exit 1
fi

# Dezimale MB: 1 MB = 1.000.000 Bytes; Zielgröße 10 MB unter dem Maximum
target_bytes=$(((max_mb - 10) * 1000000))

# Dateiinformationen
input_dir=$(dirname -- "$input")
filename=$(basename -- "$input")
extension="${filename##*.}"

if [[ "$filename" == "$extension" ]]; then
name="$filename"
extension="mp4"
else
name="${filename%.*}"
fi

input_size=$(stat -c%s -- "$input")

duration=$(ffprobe -v error \
-show_entries format=duration \
-of default=noprint_wrappers=1:nokey=1 \
"$input")

if [[ -z "$duration" ]]; then
echo "Fehler: Videodauer konnte nicht ermittelt werden."
exit 1
fi

echo
echo "Eingabedatei: $input"
echo "Dateigröße: $((input_size / 1000000)) MB"
echo "Gesamtdauer: ${duration} Sekunden"
echo "Maximale Größe: ${max_mb} MB"
echo "Zielgröße: $((target_bytes / 1000000)) MB"
echo

# Temporäres Arbeitsverzeichnis
tmpdir=$(mktemp -d) || exit 1
trap 'rm -rf -- "$tmpdir"' EXIT

# Initiale Schätzung anhand der durchschnittlichen Bitrate
chunk_duration=$(awk \
-v d="$duration" \
-v s="$input_size" \
-v t="$target_bytes" \
'BEGIN { printf "%.6f", d * t / s }')

start=0
part=1
total_part_duration=0

echo "Starte Aufteilung ..."
echo

while awk -v s="$start" -v d="$duration" \
'BEGIN { exit !(s < d - 0.05) }'; do

remaining=$(awk -v d="$duration" -v s="$start" \
'BEGIN { printf "%.6f", d - s }')

# Nicht über das Ende der Quelldatei hinausgehen
if awk -v c="$chunk_duration" -v r="$remaining" \
'BEGIN { exit !(c > r) }'; then
chunk_duration="$remaining"
fi

output="${tmpdir}/Teil${part}.${extension}"
accepted=false

echo "Erstelle Teil $part ab ${start}s ..."

# Mehrere Versuche, falls die Datei zu groß wird
for attempt in {1..20}; do

rm -f -- "$output"

if ! ffmpeg -hide_banner -loglevel error -y \
-ss "$start" \
-i "$input" \
-t "$chunk_duration" \
-map 0 \
-c copy \
-avoid_negative_ts make_zero \
"$output"; then

echo "Fehler beim Erstellen von Teil $part."
exit 1
fi

if [[ ! -s "$output" ]]; then
echo "Fehler: Teil $part ist leer."
exit 1
fi

actual_size=$(stat -c%s -- "$output")

if (( actual_size <= target_bytes )); then
accepted=true
break
fi

echo " Zu groß: $((actual_size / 1000000)) MB – verkleinere Schnittdauer."

# Schnittdauer proportional reduzieren, mit zusätzlichem Puffer
chunk_duration=$(awk \
-v c="$chunk_duration" \
-v s="$actual_size" \
-v t="$target_bytes" \
'BEGIN { printf "%.6f", c * t / s * 0.95 }')

if ! awk -v c="$chunk_duration" \
'BEGIN { exit !(c > 0.1) }'; then
echo "Fehler: Schnittdauer ist zu klein. Abbruch."
exit 1
fi
done

if [[ "$accepted" != true ]]; then
echo "Fehler: Teil $part konnte nicht auf die Zielgröße reduziert werden."
echo "Möglicherweise ist bereits ein einzelner Keyframe-Bereich zu groß."
exit 1
fi

# Tatsächliche Dauer des erzeugten Teils ermitteln
part_duration=$(ffprobe -v error \
-show_entries format=duration \
-of default=noprint_wrappers=1:nokey=1 \
"$output")

if [[ -z "$part_duration" ]]; then
echo "Fehler: Dauer von Teil $part konnte nicht ermittelt werden."
exit 1
fi

total_part_duration=$(awk \
-v total="$total_part_duration" \
-v d="$part_duration" \
'BEGIN { printf "%.6f", total + d }')

echo " Fertig: $((actual_size / 1000000)) MB, ${part_duration}s"

# Nächster Start basiert auf der tatsächlichen Dauer des Teils
start=$(awk \
-v s="$start" \
-v d="$part_duration" \
'BEGIN { printf "%.6f", s + d }')

# Neue Schätzung für den nächsten Teil
chunk_duration=$(awk \
-v d="$part_duration" \
-v s="$actual_size" \
-v t="$target_bytes" \
'BEGIN { printf "%.6f", d * t / s }')

((part++))
done

echo
echo "Alle Teile wurden erstellt."
echo

# Endkontrolle: Gesamtdauer
difference=$(awk \
-v total="$total_part_duration" \
-v original="$duration" \
'BEGIN { printf "%.6f", total - original }')

echo "Originaldauer: ${duration}s"
echo "Summe der Teile: ${total_part_duration}s"
echo "Differenz: ${difference}s"

if awk -v d="$difference" \
'BEGIN { exit !(d > 1 || d < -1) }'; then
echo "WARNUNG: Die Gesamtdauer weicht um mehr als eine Sekunde ab."
else
echo "Dauerprüfung: OK"
fi

echo

# Ausgabedateien erst nach erfolgreicher Verarbeitung verschieben
for ((i=1; i<part; i++)); do
destination="${input_dir}/${name}_Teil${i}.${extension}"
if [[ -e "$destination" ]]; then
echo "Fehler: Zieldatei existiert bereits: $destination"
exit 1
fi
done

for ((i=1; i<part; i++)); do
src="${tmpdir}/Teil${i}.${extension}"
destination="${input_dir}/${name}_Teil${i}.${extension}"

mv -- "$src" "$destination" || exit 1
echo "Erstellt: $destination"
done

echo
echo "Aufteilung abgeschlossen: $((part - 1)) Teile."

Funktionsweise

Das Script geht dabei folgendermaßen vor:

  1. Maximalgröße abfragen: Nach dem Start wird interaktiv die gewünschte Maximalgröße pro Teil in MB abgefragt (muss größer als 10 sein). Als Zielgröße wird bewusst konservativ 10 MB unter diesem Maximum angesetzt, um Puffer für schwankende Bitraten zu haben.
  2. Schätzung der Schnittdauer: Über ffprobe und stat werden Dauer und Größe der Quelldatei ermittelt. Aus der durchschnittlichen Bitrate wird eine initiale Schnittdauer berechnet.
  3. Teile erzeugen: Für jeden Teil wird ffmpeg mit -ss, -t und -c copy aufgerufen. Die Ausgabe erfolgt zunächst in ein temporäres Verzeichnis.
  4. Größenprüfung mit Wiederholung: Wird ein Teil zu groß, wird die Schnittdauer proportional zum Überschuss reduziert – inklusive eines zusätzlichen Puffers von fünf Prozent – und der Versuch wiederholt. Nach 20 erfolglosen Versuchen bricht das Script ab.
  5. Fortschreibung anhand der tatsächlichen Dauer: Da bei -c copy nur an Keyframes geschnitten werden kann, kann die erzeugte Datei von der angeforderten Dauer abweichen. Der Startpunkt des nächsten Teils wird deshalb aus der per ffprobe gemessenen tatsächlichen Dauer des erzeugten Teils berechnet – nicht aus der angeforderten.
  6. Abschlusskontrolle: Am Ende wird die Summe der Teildauern mit der Originaldauer verglichen und bei einer Abweichung von mehr als einer Sekunde eine Warnung ausgegeben.
  7. Verschieben der Ergebnisse: Erst wenn alle Teile erfolgreich erstellt wurden, werden sie als <name>_Teil<N>.<endung> neben der Quelldatei abgelegt. Existiert eine Zieldatei bereits, bricht das Script ab, statt sie zu überschreiben.

Anwendung

Das Script wird ausführbar gemacht und mit der Videodatei als Argument aufgerufen:

Script aufrufen
1
2
chmod +x split-video.sh
./split-video.sh grosses-video.mp4

Eine beispielhafte Ausgabe sieht wie folgt aus:

Beispielhafte Ausgabe des Scripts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Maximale Dateigröße pro Teil in MB: 500

Eingabedatei: grosses-video.mp4
Dateigröße: 1420 MB
Gesamtdauer: 5423.123456 Sekunden
Maximale Größe: 500 MB
Zielgröße: 490 MB

Starte Aufteilung ...

Erstelle Teil 1 ab 0s ...
Fertig: 488 MB, 1872.341000s
Erstelle Teil 2 ab 1872.341000s ...
Zu groß: 512 MB – verkleinere Schnittdauer.
Fertig: 485 MB, 1814.009000s
Erstelle Teil 3 ab 3686.350000s ...
Fertig: 456 MB, 1736.773000s

Alle Teile wurden erstellt.

Originaldauer: 5423.123456s
Summe der Teile: 5423.123000s
Differenz: -0.000456s
Dauerprüfung: OK

Erstellt: ./grosses-video_Teil1.mp4
Erstellt: ./grosses-video_Teil2.mp4
Erstellt: ./grosses-video_Teil3.mp4

Aufteilung abgeschlossen: 3 Teile.

Einschränkungen

Da das Script weiterhin -c copy verwendet, kann ffmpeg nur an geeigneten Keyframes schneiden. Die gemessene Dauer eines Teils ist daher nicht unter allen Umständen identisch mit dem exakten Zeitbereich in der Quelldatei. Trotz der Fortschreibung anhand der tatsächlichen Dauer kann es zu kleinen Überlappungen oder Lücken zwischen den Teilen kommen.

Die Größenbegrenzung ist dagegen bewusst konservativ ausgelegt: Geprüft wird gegen die Zielgröße, also 10 MB unter dem eingestellten Maximum. Sollte sich ein Teil wegen der Keyframe-Struktur nicht klein genug schneiden lassen – etwa wenn bereits ein einzelner Keyframe-Bereich die Zielgröße übersteigt –, bricht das Script ab, statt eine zu große Datei als erfolgreich auszugeben. In diesem Fall ließe sich das Video nur durch ein Neuencodieren mit verkürztem Keyframe-Intervall oder eine geringere Bitrate weiter verkleinern.

Alternative: Komprimieren durch Neuencodieren

Das Aufteilen ist nicht immer gewünscht – etwa wenn das Video als eine zusammenhängende Datei weitergegeben oder abgespielt werden soll. In diesem Fall lässt sich die Dateigröße auch durch ein Neuencodieren reduzieren. Im Gegensatz zum Kopieren der Streams wird das Video dabei komplett neu kodiert, was deutlich länger dauert und je nach Einstellung mit einem sichtbaren Qualitätsverlust einhergeht.

Für die Kodierung mit dem weit verbreiteten Encoder libx264 steuert der Parameter -crf (CRF) die Qualität beziehungsweise die resultierende Dateigröße. Der CRF-Wert liegt zwischen 0 und 51, wobei kleinere Werte für eine bessere Qualität bei größeren Dateien stehen. Werte um 18 gelten als nahezu verlustfrei wahrnehmbar, Werte ab etwa 28 zeigen je nach Inhalt bereits deutliche Kompressionsartefakte. Der Standardwert von libx264 liegt bei 23.

Ein einmaliger Aufruf zum Komprimieren einer Datei sieht zum Beispiel so aus:

Video mit CRF 25 neu encodieren
1
ffmpeg -i grosses-video.mp4 -vcodec libx264 -crf 25 grosses-video_komprimiert.mp4

Auch hierfür gibt es ein Script, das den Aufruf vereinfacht:

Bash-Script zum Komprimieren einer Videodatei
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#!/bin/bash
# Usage: ./compress-video.sh <video-datei>

set -o pipefail

if [[ $# -lt 1 ]]; then
echo "Verwendung: $0 <video-datei>"
exit 1
fi

input=$1

if [[ ! -f "$input" ]]; then
echo "Fehler: Datei nicht gefunden: $input"
exit 1
fi

for cmd in ffmpeg ffprobe awk stat; do
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "Fehler: '$cmd' ist nicht installiert."
exit 1
fi
done

# CRF-Wert abfragen
read -rp "CRF-Wert (Standard 25, kleiner = bessere Qualität): " crf
crf="${crf:-25}"

# Eingabe validieren: CRF zwischen 0 und 51
if ! [[ "$crf" =~ ^[0-9]+$ ]] || (( crf > 51 )); then
echo "Fehler: Ungültiger CRF-Wert. Erlaubt sind ganze Zahlen von 0 bis 51."
exit 1
fi

# Dateiinformationen
input_dir=$(dirname -- "$input")
filename=$(basename -- "$input")
name="${filename%.*}"
output="${input_dir}/${name}_komprimiert.mp4"

if [[ -e "$output" ]]; then
echo "Fehler: Zieldatei existiert bereits: $output"
exit 1
fi

input_size=$(stat -c%s -- "$input")

duration=$(ffprobe -v error \
-show_entries format=duration \
-of default=noprint_wrappers=1:nokey=1 \
"$input" 2>/dev/null || true)

echo
echo "Eingabedatei: $input"
echo "Dateigröße: $((input_size / 1000000)) MB"

if [[ -n "$duration" ]]; then
echo "Gesamtdauer: ${duration} Sekunden"
fi

echo "CRF-Wert: $crf"
echo "Ausgabedatei: $output"
echo
echo "Starte Komprimierung ..."
echo

if ! ffmpeg -hide_banner -n -i "$input" \
-vcodec libx264 -crf "$crf" \
"$output"; then
echo "Fehler beim Komprimieren."
rm -f -- "$output"
exit 1
fi

if [[ ! -s "$output" ]]; then
echo "Fehler: Ausgabedatei ist leer."
rm -f -- "$output"
exit 1
fi

output_size=$(stat -c%s -- "$output")
saved=$(awk -v i="$input_size" -v o="$output_size" \
'BEGIN { printf "%.1f", (1 - o / i) * 100 }')

echo
echo "Originalgröße: $((input_size / 1000000)) MB"
echo "Komprimiert: $((output_size / 1000000)) MB"
echo "Ersparnis: ${saved} %"
echo
echo "Erstellt: $output"

Das Script compress-video.sh wird wie das Aufteilungs-Script mit der Videodatei als einzigem Argument aufgerufen und fragt interaktiv den gewünschten CRF-Wert ab (Standard: 25). Das Ergebnis wird als <name>_komprimiert.mp4 neben der Quelldatei abgelegt; eine bereits vorhandene Zieldatei wird nicht überschrieben. Nach der Kodierung zeigt das Script die Größen von Original und Ergebnis sowie die prozentuale Ersparnis an:

Script aufrufen
1
2
chmod +x compress-video.sh
./compress-video.sh grosses-video.mp4
Beispielhafte Ausgabe des Scripts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CRF-Wert (Standard 25, kleiner = bessere Qualität): 25

Eingabedatei: grosses-video.mp4
Dateigröße: 1420 MB
Gesamtdauer: 5423.123456 Sekunden
CRF-Wert: 25
Ausgabedatei: ./grosses-video_komprimiert.mp4

Starte Komprimierung ...

...

Originalgröße: 1420 MB
Komprimiert: 418 MB
Ersparnis: 70.6 %

Erstellt: ./grosses-video_komprimiert.mp4

Da die tatsächliche Ersparnis stark vom Videoinhalt und den vorhandenen Codecs abhängt, empfiehlt sich gegebenenfalls ein Testlauf mit einem kurzen Ausschnitt, bevor eine große Datei komplett neu kodiert wird.

Fazit

Für das schnelle Aufteilen großer Videodateien ist ffmpeg mit -c copy das Werkzeug der Wahl: kein Neuencodieren, kein Qualitätsverlust und in Sekundenschnelle fertig. Wer die Teile nach einer festen Uhrzeit schneiden möchte, kommt mit einzelnen ffmpeg-Befehlen aus. Soll dagegen eine Maximalgröße eingehalten werden, nimmt split-video.sh die iterative Anpassung der Schnittdauer automatisch vor – inklusive Größenprüfung und Abschlusskontrolle der Gesamtdauer.

Muss das Video dagegen als eine Datei erhalten bleiben, führt kein Weg am Neuencodieren vorbei. Hier hilft compress-video.sh, das die Kodierung mit libx264 und einem wählbaren CRF-Wert übernimmt – mit dem unvermeidlichen Nebeneffekt längerer Laufzeit und möglichem Qualitätsverlust.