视频相关的命令和脚本

2023-11-22 • 更新于 2023-11-27

FFmpeg 实用命令

剪切视频而不重新转码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 视频兼容,解决方式是将音频也转码:

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

旋转视频而不转码3

逆时针旋转 90 度:

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

裁切视频4

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

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

循环重复5

循环重复 3 次:

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

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

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

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

然后:

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

去隔行扫描7

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

视频加速/减速8

加速两倍:

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

减速一半:

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

视频反转9

仅反转视频:

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

同时反转视频和音频:

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

hev1 转 hvc110

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

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

转换为 hvc1 即可:

$ ffmpeg -i input.mp4 -tag:v hvc1 -c copy output.mp4
$ 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
#!/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.io/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

示例:

$ 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
#!/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.io/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

示例:

$ 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
#!/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.io/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

已知问题

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

[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 丢弃数据流:

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

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

示例

批量转码

支持通配符:

$ 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 下所有文件进行转码,结果保存在当前目录:

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

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

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

$ 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 -v 来检查文件是否不包含特定字符串

在 Linux 上使用 Exim4 发送邮件