bash
# 安装所需依赖
sudo apt-get update
sudo apt-get install -y ca-certificates curl python3 tar xz-utils coreutils
# 先检查下载、校验和实际运行是否正常
bash install-ffmpeg.sh --check-only
# 检查通过后安装
bash install-ffmpeg.sh
bash
#!/usr/bin/env bash
# FFmpeg 安装脚本:Linux x86_64,需要 Bash。
# 来源:BtbN/FFmpeg-Builds;默认选择其提供的最高稳定分支。
# 稳定分支构建可能包含正式版本之后的修复,不由 apt 管理。
set -Eeuo pipefail
umask 022
readonly RELEASE_API="https://api.github.com/repos/BtbN/FFmpeg-Builds/releases/latest"
readonly INSTALL_ROOT="/opt/ffmpeg"
readonly COMMAND_DIR="/usr/local/bin"
# 运行状态,分别由参数解析、下载选择和权限检查填充。
branch=""
check_only=false
work=""
archive=""
source_dir=""
selected=()
priv=()
# ── 通用工具 ────────────────────────────────────────────────────────
log() {
printf '[FFmpeg] %s\n' "$*"
}
fail() {
printf '[错误] %s\n' "$1" >&2
exit "${2:-1}"
}
show_help() {
cat <<'HELP'
用法:bash install-ffmpeg.sh [选项]
选项:
--branch MAJOR.MINOR 指定稳定分支,例如 9.0
--check-only 仅下载、校验和测试,不安装
-h, --help 显示帮助
示例:
bash install-ffmpeg.sh
bash install-ffmpeg.sh --branch 9.0
bash install-ffmpeg.sh --check-only
安装目录:/opt/ffmpeg/;命令入口:/usr/local/bin/
重新运行即可更新,旧版本目录会保留。请使用 bash,不要使用 sh。
HELP
}
cleanup() {
if [[ -n "$work" && -d "$work" ]]; then
rm -rf -- "$work"
fi
}
fetch() {
local url="$1"
local output_path="$2"
curl --fail --location --silent --show-error \
--retry 3 \
--connect-timeout 20 \
--max-time 600 \
"$url" --output "$output_path"
}
# ── 参数和环境检查 ──────────────────────────────────────────────────
parse_arguments() {
while (($#)); do
case "$1" in
--branch)
if [[ $# -lt 2 || ! "$2" =~ ^[0-9]+\.[0-9]+$ ]]; then
fail '--branch 需要 MAJOR.MINOR 格式的参数,例如 9.0' 2
fi
branch="$2"
shift 2
;;
--check-only)
check_only=true
shift
;;
-h|--help)
show_help
exit 0
;;
*)
fail "未知参数:$1(使用 --help 查看帮助)" 2
;;
esac
done
}
check_environment() {
local tool
if [[ $(uname -s) != Linux || $(uname -m) != x86_64 ]]; then
fail '目前仅支持 Linux x86_64。'
fi
for tool in curl python3 tar xz sha256sum mktemp; do
if ! command -v "$tool" >/dev/null; then
fail "缺少依赖:$tool"
fi
done
}
# ── 选择构建和下载 ──────────────────────────────────────────────────
select_build() {
log "查询稳定分支构建......"
fetch "$RELEASE_API" "$work/release.json"
python3 - "$work/release.json" "$branch" > "$work/selected" <<'PY'
import json
import re
import sys
release = json.load(open(sys.argv[1]))
choices = []
for asset in release['assets']:
match = re.fullmatch(r'ffmpeg-n(\d+)\.(\d+)-latest-linux64-gpl-\1\.\2.tar.xz', asset['name'])
if match and (not sys.argv[2] or '.'.join(match.groups()) == sys.argv[2]):
choices.append((tuple(map(int, match.groups())), asset))
if not choices:
sys.exit('No matching stable build found in BtbN release assets.')
asset = max(choices, key=lambda item: item[0])[1]
checksum = next(a for a in release['assets'] if a['name'] == 'checksums.sha256')
for item in (asset, checksum):
url = item['browser_download_url']
if not url.startswith('https://github.com/BtbN/FFmpeg-Builds/releases/download/'):
sys.exit('Unexpected download URL')
print(asset['name'])
print(asset['browser_download_url'])
print(checksum['browser_download_url'])
PY
mapfile -t selected < "$work/selected"
archive=${selected[0]}
log "已选择:$archive"
}
download_build() {
log "下载安装包及校验文件......"
fetch "${selected[2]}" "$work/checksums.sha256"
fetch "${selected[1]}" "$work/$archive"
}
# ── 完整性检查和安全解压 ──────────────────────────────────────────────
verify_checksum() {
log "校验 SHA-256......"
python3 - "$work/checksums.sha256" "$archive" > "$work/verify.sha256" <<'PY'
import re
import sys
matches = []
for line in open(sys.argv[1]):
parts = line.split()
if len(parts) == 2 and parts[1].lstrip('*') == sys.argv[2]:
if not re.fullmatch('[0-9a-fA-F]{64}', parts[0]):
sys.exit('Invalid SHA-256 checksum')
matches.append(parts[0] + ' ' + sys.argv[2])
if len(matches) != 1:
sys.exit('Expected exactly one archive checksum')
print(matches[0])
PY
(
cd "$work"
sha256sum --check --strict verify.sha256
)
}
extract_build() {
# 解压前拒绝路径穿越、链接和设备文件。
python3 - "$work/$archive" "${archive%.tar.xz}" <<'PY'
import pathlib
import sys
import tarfile
with tarfile.open(sys.argv[1]) as archive:
for member in archive:
path = pathlib.PurePosixPath(member.name)
if (path.is_absolute() or '..' in path.parts or not path.parts
or path.parts[0] != sys.argv[2]
or not (member.isfile() or member.isdir())):
sys.exit('Unsafe archive member: ' + member.name)
PY
tar --extract --xz \
--file "$work/$archive" \
--directory "$work" \
--no-same-owner \
--no-same-permissions
source_dir="$work/${archive%.tar.xz}"
}
# ── 实际功能测试 ────────────────────────────────────────────────────
verify_build() {
log "测试编码、媒体探测和解码......"
"$source_dir/bin/ffmpeg" -version > "$work/version.txt"
head -n 1 "$work/version.txt"
"$source_dir/bin/ffmpeg" -nostdin -hide_banner -loglevel error \
-f lavfi -i testsrc2=size=128x96:rate=10 \
-f lavfi -i sine=frequency=1000:sample_rate=48000 \
-t 1 -threads 1 \
-c:v libx264 -pix_fmt yuv420p \
-c:a aac "$work/test.mp4"
"$source_dir/bin/ffprobe" -v error \
-show_entries stream=codec_name \
-of csv=p=0 "$work/test.mp4"
"$source_dir/bin/ffmpeg" -nostdin -v error -i "$work/test.mp4" -f null -
log '校验、编码、媒体探测和解码测试通过。'
}
# ── 安装和命令入口 ──────────────────────────────────────────────────
prepare_privileges() {
if ((EUID != 0)); then
if ! command -v sudo >/dev/null; then
fail '安装需要 root 权限或 sudo。'
fi
sudo -v
priv=(sudo)
fi
}
check_existing_commands() {
local name target
# 只替换指向本安装目录的链接,保护其他方式安装的程序。
for name in ffmpeg ffprobe; do
target="$COMMAND_DIR/$name"
if [[ ! -e "$target" && ! -L "$target" ]]; then
continue
fi
if [[ ! -L "$target" || $(readlink "$target") != "$INSTALL_ROOT/"*/bin/"$name" ]]; then
fail "拒绝覆盖其他安装:$target"
fi
done
}
install_build() {
local destination name
prepare_privileges
check_existing_commands
# 独立目录保留旧版本;复制成功后再更新命令链接。
"${priv[@]}" install -d -m 755 "$INSTALL_ROOT" "$COMMAND_DIR"
destination=$("${priv[@]}" mktemp -d "$INSTALL_ROOT/build.XXXXXXXX")
"${priv[@]}" cp -a "$source_dir/." "$destination/"
"${priv[@]}" chown -R root:root "$destination"
"${priv[@]}" chmod 755 "$destination"
for name in ffmpeg ffprobe; do
"${priv[@]}" ln -sfnT "$destination/bin/$name" "$COMMAND_DIR/$name"
"$COMMAND_DIR/$name" -version > "$work/$name-version.txt"
head -n 1 "$work/$name-version.txt"
done
log "安装完成:$destination"
log "命令入口:$COMMAND_DIR/ffmpeg 和 $COMMAND_DIR/ffprobe"
log '再次运行本脚本即可更新;旧版本目录已保留。'
}
# ── 主流程 ──────────────────────────────────────────────────────────
main() {
parse_arguments "$@"
check_environment
work=$(mktemp -d -t ffmpeg-install.XXXXXXXX)
trap cleanup EXIT
select_build
download_build
verify_checksum
extract_build
verify_build
if [[ "$check_only" == true ]]; then
log '仅检查模式:未修改现有安装。'
return
fi
install_build
}
main "$@"