最新 FFmpeg 安装脚本:Linux x86_64

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

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 "$@"
相关推荐
唐墨1232 小时前
linux kernel 比较数据大小的宏定义
linux·算法
程序开发阿元2 小时前
密码不进工具:一个独立开发者的服务器凭据管理办法
linux·ssh
分布式存储与RustFS4 小时前
群晖 NAS 被 C3RB3R 锁了:本地备份一起被加密,RustFS 对象锁为什么删不掉
运维·云原生·开源·对象存储·分布式存储·s3·性能基准
真好啊又活了一天5 小时前
服务器和虚机配置IPV6
运维·服务器·github
zixu_star5 小时前
定时任务是什么?从日常自动化到 AI 应用,软件如何自动执行任务?
运维·人工智能·自动化
AR-26710-5 小时前
Linux Day33——定时任务+lvm
linux·运维
墨痕诉清风6 小时前
SSRF漏洞实战记录+OSS桶命令执行+MCP漏洞
linux·服务器·数据库
byte轻骑兵6 小时前
实战 4:BlueZ 源码中添加自定义设备过滤规则
linux·物联网·bluez·电脑蓝牙·嵌入式蓝牙
润滑卫士6 小时前
液压系统工作粘度窗口等级选型:泵允许粘度范围与油温-粘度匹配计算
运维
瓦瓦gogo7 小时前
从一个 TypeScript 函数开始理解 MCP 协议
linux·ubuntu·typescript