视频相关的命令和脚本

2023-11-22 • 更新于 2026-02-18

FFmpeg 实用命令

剪切视频而不重新转码1

1
ffmpeg -ss 00:00:30.0 -to 00:00:40.0 -i input.mp4 -c copy output.mp4

-ss-to 分别指定起始时间。

这种方式输出的视频有一定问题,最好还是转码(不要用 -c copy)。

codec not currently supported in container2

一些视频流和音频流无法放到同一容器中,例如 WMA 音频不能与 H.264 视频兼容,解决方式是将音频也转码:

1
ffmpeg -i input.wmv -c:v libx264 -c:a aac output.mp4

旋转视频而不转码3

逆时针旋转 90 度:

1
ffmpeg -display_rotation 90 -i input.mp4 -c copy output.mp4

裁切视频4

1
ffmpeg -i input.mp4 -vf "crop=out_w:out_h:x:y" -c:a copy output.mp4

crop= 后以 : 分隔的四个参数分别表示:输出宽度、输出高度、左上角横坐标、左上角纵坐标。

调整分辨率5

将高度调整为 1080,比例不变:

1
ffmpeg -i input.mp4 -vf scale=-1:1080 -c:a copy output.mp4

循环重复6

循环重复 3 次:

1
ffmpeg -stream_loop 3 -i input.mp4 -c copy output.mp4

合并多个同类文件而不转码7

使用一个文本保存需合并的文件路径,如 input.txt

file 'path/to/part1.mp4'
file 'path/to/part2.mp4'
file 'path/to/part3.mp4'

然后:

1
ffmpeg -f concat -i input.txt -c copy output.mp4

去隔行扫描8

1
ffmpeg -i input.vob -vf yadif -c:a copy output.mp4

视频加速/减速9

加速两倍:

1
ffmpeg -i input.mp4 -vf "setpts=0.5*PTS" output.mp4

减速一半:

1
ffmpeg -i input.mp4 -vf "setpts=2*PTS" output.mp4

视频反转10

仅反转视频:

1
ffmpeg -i input.mp4 -vf reverse output.mp4

同时反转视频和音频:

1
ffmpeg -i input.mp4 -vf reverse -af areverse output.mp4

hev1 转 hvc111

一些 HEVC 在 macOS 上无法显示缩略图,也无法用原生 APP 打开,这是因为使用了 hev1:

1
2
$ ffprobe -v error -select_streams v -show_entries stream=codec_tag_string -of default=noprint_wrappers=1:nokey=1 input.mp4
hev1

转换为 hvc1 即可:

1
ffmpeg -i input.mp4 -tag:v hvc1 -c copy output.mp4
1
2
$ ffprobe -v error -select_streams v -show_entries stream=codec_tag_string -of default=noprint_wrappers=1:nokey=1 output.mp4
hvc1

脚本

列出码率最高的视频

默认列出前 10 个,不支持含换行符的文件名。

使用 -v 选项以首个视频流码率列出(而不是总体码率),但不支持 MKV、TS、WebM 等容器。

top-videos-by-bitrate.sh
 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
#!/usr/bin/env sh

BRIEF=false
VIDEO_EXT="3gp avi flv m4v mkv mov mp4 mpeg mpg ts vob webm wmv"
NUM=10
VIDEO_BITRATE=false

SCRIPT=$(basename "$0")

USAGE=$(
  cat <<-END
Usage: $SCRIPT [<options>] <path>...
List top videos by bitrate.

  -b            do not prepend bitrate to output lines
  -e <ext>      video file extensions, separated by spaces, case-insensitive
                defaults to '$VIDEO_EXT'
  -n <num>      list top num video files
                defaults to '$NUM'
  -v            list by the first video stream bitrate instead of the overall bitrate
                not applicable for containers like MKV, TS, WebM, etc
  -h            display this help and exit

Home page: <https://binac.org/posts/video-related-commands-and-scripts/>
END
)

error() { printf "%s\n" "$@" >&2; }

_exit() {
  error "$USAGE"
  exit 2
}

while getopts "bhve:n:" c; do
  case $c in
  b) BRIEF=true ;;
  e) VIDEO_EXT=$OPTARG ;;
  n) NUM=$OPTARG ;;
  v) VIDEO_BITRATE=true ;;
  h) error "$USAGE" && exit ;;
  *) _exit ;;
  esac
done

is_integer() {
  [ -n "$1" ] && [ "$1" -eq "$1" ] 2>/dev/null
}

is_integer "$NUM" || _exit

shift $((OPTIND - 1))
[ $# -eq 0 ] && _exit

min() {
  [ "$1" -le "$2" ] && echo "$1" || echo "$2"
}

get_bitrate() {
  if [ "$VIDEO_BITRATE" = false ]; then
    ffprobe -v error -show_entries format=bit_rate -of default=noprint_wrappers=1:nokey=1 "$1"
  else
    ffprobe -v error -select_streams v:0 -show_entries stream=bit_rate -of default=noprint_wrappers=1:nokey=1 "$1" | head -n 1
  fi
}

TMP_FILE=$(mktemp) || exit 1
trap 'rm -f "$TMP_FILE"' EXIT

count=0
grep_expression="$(echo " $VIDEO_EXT" | sed 's/ /\$|\\./g' | cut -c3-)$"
find "$@" -type f | grep -i -E "$grep_expression" | while IFS= read -r f; do
  : $((count += 1))
  printf "Detecting videos: %s\r" "$count" >&2
  br=$(get_bitrate "$f")
  if is_integer "$br"; then
    echo "$(numfmt --to iec "$br")|$f" >>"$TMP_FILE"
  else
    printf "\n%s\n" "Cannot get bitrate: $f" >&2
  fi
done
count=$(wc -l <"$TMP_FILE" | xargs)
printf "\n" >&2

if [ "$count" -eq 0 ]; then
  error "No videos found"
  exit
else
  error "Detected $count videos"
fi

error "Top $(min "$NUM" "$count") videos by bitrate:"
[ "$BRIEF" = true ] || printf "%-16s%s\n" "Bitrate" "File"
sort -t '|' -k1 -hr "$TMP_FILE" | head -n "$NUM" | while IFS= read -r line; do
  br=$(echo "${line%%|*}" | xargs)
  f=${line#*|}
  [ "$BRIEF" = true ] && echo "$f" || printf "%-16s%s\n" "$br" "$f"
done

示例:

1
2
3
4
5
6
7
8
$ top-videos-by-bitrate.sh -n 3 /path/to/videos
Detecting videos: 100
Detected 100 videos
Top 3 videos by bitrate:
Bitrate         File
8.8M            /path/to/videos/1.mp4
4.2M            /path/to/videos/2/3.avi
3.8M            /path/to/videos/4/5/6.mkv

列出占用空间最大的视频

使用 du 命令检测磁盘占用空间(而不是实际大小)。

默认列出前 10 个,不支持含换行符的文件名。

top-videos-by-size.sh
 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
#!/usr/bin/env sh

BRIEF=false
VIDEO_EXT="3gp avi flv m4v mkv mov mp4 mpeg mpg ts vob webm wmv"
NUM=10

SCRIPT=$(basename "$0")

USAGE=$(
  cat <<-END
Usage: $SCRIPT [<options>] <path>...
List top videos by size.

  -b            do not prepend size to output lines
  -e <ext>      video file extensions, separated by spaces, case-insensitive
                defaults to '$VIDEO_EXT'
  -n <num>      list top num video files
                defaults to '$NUM'
  -h            display this help and exit

Home page: <https://binac.org/posts/video-related-commands-and-scripts/>
END
)

error() { printf "%s\n" "$@" >&2; }

_exit() {
  error "$USAGE"
  exit 2
}

while getopts "bhe:n:" c; do
  case $c in
  b) BRIEF=true ;;
  e) VIDEO_EXT=$OPTARG ;;
  n) NUM=$OPTARG ;;
  h) error "$USAGE" && exit ;;
  *) _exit ;;
  esac
done

is_integer() {
  [ -n "$1" ] && [ "$1" -eq "$1" ] 2>/dev/null
}

is_integer "$NUM" || _exit

shift $((OPTIND - 1))
[ $# -eq 0 ] && _exit

min() {
  [ "$1" -le "$2" ] && echo "$1" || echo "$2"
}

TMP_FILE=$(mktemp) || exit 1
trap 'rm -f "$TMP_FILE"' EXIT

grep_expression="$(echo " $VIDEO_EXT" | sed 's/ /\$|\\./g' | cut -c3-)$"
find "$@" -type f -exec du -h {} + | grep -i -E "$grep_expression" | sort -h -r >"$TMP_FILE"
count=$(wc -l <"$TMP_FILE" | xargs)

if [ "$count" -eq 0 ]; then
  error "No videos found"
  exit
else
  error "Detected $count videos"
fi

error "Top $(min "$NUM" "$count") videos by size:"
[ "$BRIEF" = true ] || printf "%-16s%s\n" "Size" "File"
head -n "$NUM" "$TMP_FILE" | while IFS= read -r line; do
  sz=$(echo "${line%%	*}" | xargs)
  f=${line#*	}
  [ "$BRIEF" = true ] && echo "$f" || printf "%-16s%s\n" "$sz" "$f"
done

示例:

1
2
3
4
5
6
7
$ top-videos-by-size.sh -n 3 /path/to/videos
Detected 100 videos
Top 3 videos by size:
Size            File
4.2G            /path/to/videos/1.mp4
420M            /path/to/videos/2/3.avi
42M             /path/to/videos/4/5/6.mkv

HEVC 转码

将文件批量转为 HEVC 编码,只转码首条视频流,其他视频流、音频流、字幕流原封不动。

默认会忽略本身已经是 hevc/av1/vp9 编码的文件,因为这些编码已经效率很高了。使用 -c <codecs> 指定需要忽略的编码。

hevcize.sh
  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
#!/usr/bin/env sh

ARGUMENTS=
CODEC_WHITELIST="hevc av1 vp9"
AVAILABLE_ENCODERS=$(ffmpeg -codecs 2>/dev/null | grep '^.\{8\}hevc' | sed -n 's/.*(encoders: \([^)]*\)).*/\1/p' | xargs)
ENCODER=libx265
OUTPUT_DIR=

SCRIPT=$(basename "$0")

USAGE=$(
  cat <<-END
Usage: $SCRIPT [<options>] <file>...
List top videos by size.

  -a <args>     pass extra arguments to ffmpeg
  -c <codecs>   videos with these codecs will NOT be encoded
                defaults to '$CODEC_WHITELIST'
  -e <encoder>  set HEVC encoder, this could accelerate transcoding but reduce the quality
                you may need to use '-a <args>'
                available encoders: $AVAILABLE_ENCODERS
                defaults to '$ENCODER'
  -o <dir>      set output directory
                by default, the output file will be placed in the same directory as the original video
  -h            display this help and exit

Home page: <https://binac.org/posts/video-related-commands-and-scripts/>
END
)

THIN_LINE=$(printf '%.s-' $(seq 1 80))
BOLD_LINE=$(printf '%.s=' $(seq 1 80))

error() { printf "%s\n" "$@" >&2; }

_exit() {
  error "$USAGE"
  exit 2
}

while getopts "ha:c:e:o:" c; do
  case $c in
  a) ARGUMENTS=$OPTARG ;;
  c) CODEC_WHITELIST=$OPTARG ;;
  e) ENCODER=$OPTARG ;;
  o) OUTPUT_DIR=$OPTARG ;;
  h) error "$USAGE" && exit ;;
  *) _exit ;;
  esac
done

contains() {
  echo "$1" | grep -qs "\<$2\>"
}

if ! contains "$AVAILABLE_ENCODERS" "$ENCODER"; then
  error "Invalid encoder: '$ENCODER'"
  _exit
fi

if [ -z "$ARGUMENTS" ] && [ "$ENCODER" = libx265 ]; then
  ARGUMENTS="-x265-params log-level=error"
fi

if [ -n "$OUTPUT_DIR" ]; then
  if [ ! -d "$OUTPUT_DIR" ] || [ ! -w "$OUTPUT_DIR" ]; then
    error "Cannot access: '$OUTPUT_DIR'"
    exit 1
  fi
  OUTPUT_DIR=$(realpath "$OUTPUT_DIR")
fi

shift $((OPTIND - 1))
[ $# -eq 0 ] && _exit

do_ffmpeg() {
  if [ -f "$2" ]; then
    error "File already exists: '$2'"
    return 0
  fi
  if ffmpeg -nostdin -v error -hide_banner -stats -i "$1" -map 0 -c copy -c:v:0 "$ENCODER" $ARGUMENTS -tag:v:0 hvc1 "$2"; then
    error "==> '$2'"
  else
    rm -f "$2"
    return 1
  fi
}

FAIL_LIST=$(mktemp) || exit 1
trap 'rm -f "$FAIL_LIST"' EXIT

count=1
total="$#"
for f in "$@"; do
  error "$THIN_LINE"
  error "==> $count/$total: '$f'"
  : $((count += 1))
  vc=$(ffprobe "$f" -v error -select_streams v:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 | head -n 1)
  if [ -z "$vc" ]; then
    error "Unknown input: '$f'"
    echo "$f" >>"$FAIL_LIST"
    continue
  fi
  if contains "$CODEC_WHITELIST" "$vc"; then
    error "Skipping codec: '$vc'"
    continue
  fi

  if [ -n "$OUTPUT_DIR" ]; then
    output="$OUTPUT_DIR/$(basename "$f").HEVC.mp4"
  else
    output="$f.HEVC.mp4"
  fi
  do_ffmpeg "$f" "$output" && continue

  error "Unable to use MP4 container, retrying with MKV"
  output="${output%.*}.mkv"
  do_ffmpeg "$f" "$output" && continue

  error "Unable to use MKV container"
  error "Skipping file: '$f'"
  echo "$f" >>"$FAIL_LIST"
done

if [ -s "$FAIL_LIST" ]; then
  error "$BOLD_LINE"
  error "Unable to encode the following $(wc -l <"$FAIL_LIST" | xargs) files:"
  cat "$FAIL_LIST" >&2
  exit 1
fi

已知问题

一些视频除了视频流、音频流、字幕流外,还带有数据流、附件流,这些流可能无法在转码后的容器中支持(即使转码前后容器相同)12。以下是一个容器无法支持数据流的报错:

[mp4 @ 0x55af0c59d0c0] Could not find tag for codec none in stream #3, codec not currently supported in container
Could not write header for output file #0 (incorrect codec parameters ?): Invalid argument
Error initializing output stream 0:0 --
Unable to use MP4 container, retrying with MKV
[matroska @ 0x55ceccef70c0] Only audio, video, and subtitles are supported for Matroska.
Could not write header for output file #0 (incorrect codec parameters ?): Invalid argument
Error initializing output stream 0:0 --

此时可以手动转码并使用 -map -0:d 丢弃数据流:

1
ffmpeg -i input.mp4 -map 0 -map -0:d -c copy -c:v libx265 output.mp4

另外,由于脚本只使用 MP4 和 MKV 尝试,也可能存在不支持的音频流和字幕流等13,此时也可以按需转码或丢弃,例如音频转码 -c:a acc,丢弃字幕 -map -0:s

示例

批量转码

支持通配符:

1
2
hevcize.sh /path/to/videos/1.mp4 /path/to/videos/2/3.avi /path/to/videos/4/5/6.mkv
hevcize.sh /path/to/videos/*.mp4

/path/to/videos 下所有文件进行转码,结果保存在当前目录:

1
find /path/to/videos -type f -exec hevcize.sh -o . {} +
显卡加速

比如 NVIDIA 显卡可以使用 hevc_nvenc 编码器进行加速,Intel 显卡使用 hevc_qsv,这些都需要硬件和驱动支持。

macOS 使用 hevc_videotoolbox 编码器进行显卡加速(hevcize.sh -h 查看有哪些可用编码器):

1
2
3
4
5
$ hevecize.sh -e hevc_videotoolbox /path/to/videos/1.mp4
--------------------------------------------------------------------------------
==> 1/1: '/path/to/videos/1.mp4'
frame= 8000 fps= 80 q=-0.0 Lsize=   15000kB time=00:05:00.00 bitrate= 500.0kbits/s dup=33 drop=23 speed=3.00x    
==> '/path/to/videos/1.mp4.HEVC.mp4'

显卡加速转码很快,CPU 占用率低,但比起默认的 libx265(CPU 软编码)质量较差。

通过 -a <args> 可以传递更多参数给 ffmpeg。

FFmpegShellLinuxmacOS

本作品根据 署名-非商业性使用-相同方式共享 4.0 国际许可 进行授权。

grep 使用中的一些坑

在 Linux 上使用 Exim4 发送邮件