随机
Enter 搜索 ↑↓ 切换 Esc 清空

ffmpeg_video_h264_all

脚本

libx264 CRF 智能 H.264 批量目录转码 · 瘦身/无损自动判断(自动后台+锁防重跑)

补充说明

基于 libx264(CPU 软编码)的「一级子目录批量 H.264 智能处理」脚本,把源根目录下所有一级子文件夹内的 mp4 / mkv 视频逐个处理为统一 mp4 容器,并按「智能判断」决定**CRF 恒定质量重编码(瘦身)**还是 -c copy 无损流复制,避免不需要重编码的视频浪费算力与损失画质。

智能判断规则:

视频情况 处理方式
非 H264 编码 或 非 yuv420p libx264 CRF23 重编码(统一为 H264+yuv420p 标准格式)
H264 + yuv420p,码率 > 3000 kbps libx264 CRF23 重编码瘦身(高码率压缩体积)
H264 + yuv420p,码率 ≤ 3000 kbps -c copy -movflags +faststart 流复制,秒级完成、画质零损失

码率阈值顶部 BITRATE_THRESHOLD=3000000(3000 kbps)可自行调整。重编码模式输出 -crf 23 -profile:v main -movflags +faststart,兼容主流播放器与网页端。

核心特性与 HEVC 版一致:前台一行命令启动自动 nohup 转后台静默执行(__BG_RUN__ 哨兵自重启),锁文件 /tmp/videnc_batch.lock 防重复启动,全局 batch_main.log 汇总所有输出;每个文件转码前按约 1.5 倍速预估耗时;转码成功即删除单文件 .log,del_src=true 时删除源文件并清理空目录。

输出结构示例:

目标根目录/
├── batch_main.log        ← 全局总日志(去色写入,含阈值/预估耗时/成败汇总)
├── 专辑A/
│   ├── 01.mp4            ← 智能判断:copy 或 重编码
│   ├── 01.log            ← 成功即删除,失败保留
│   └── 02.mp4
└── 专辑B/
    └── 03.mp4

不传参数时进入交互式模式(前台执行):依次输入源/目标目录、删除开关,确认后开始处理。脚本启动时自动检测并安装 ffmpeg,校验 libx264 编码器。逐个串行处理,避免并发抢占 CPU/QSV 资源。

一键脚本

bash <(curl -sL gitee.com/meimolihan/cmdbox/raw/master/sh/ffmpeg_video_h264_all.sh)
bash <(curl -sL ../sh/ffmpeg_video_h264_all.sh)

命令行模式示例(脚本名以实际部署路径为准,以下为本地脚本名):

chmod +x ffmpeg_video_h264_all.sh

# 一键启动:源根目录 → 目标根目录(不删除源文件),自动后台执行
./ffmpeg_video_h264_all.sh /vol2/1000/mydisk/Video/Video-Album /vol2/1000/mydisk/Video/Japanese

# 处理成功后删除源文件 + 清理空目录
./ffmpeg_video_h264_all.sh /vol2/1000/mydisk/Video/Video-Album /vol2/1000/mydisk/Video/Japanese true

# 不传参数 → 交互式(前台执行,输入源/目标、删除开关、确认)
./ffmpeg_video_h264_all.sh

命令行启动后脚本打印提示并立即返回,后台任务开始执行:

>>> 命令行批量转码模式
————————————————————————————————————————————————
源 目 录:/vol2/.../Video-Album
目标目录:/vol2/.../Japanese
转码成功后删除源文件:false
✅ 任务已自动进入后台静默执行
📖 查看全局日志:tail -f /vol2/.../Japanese/batch_main.log
❌ 强制结束进程:pkill -9 -f ffmpeg && rm -f /tmp/videnc_batch.lock
————————————————————————————————————————————————

任务运行中再次启动会被锁文件拒绝(锁文件 /tmp/videnc_batch.lock 存在,禁止重复启动)。

手动安装 ffmpeg

脚本 install_deps 启动时自动检测:ffmpeg 已装且支持 libx264 即直接通过;未装则按发行版自动安装(apt/dnf/yum/pacman/zypper/apk/brew,自动安装需要 sudo 权限)。也可手动安装:

# Debian/Ubuntu
sudo apt update && sudo apt install -y ffmpeg

# CentOS/RHEL
sudo yum install -y epel-release && sudo yum install -y ffmpeg

# Arch Linux
sudo pacman -S --noconfirm ffmpeg

# Alpine
sudo apk add ffmpeg

# macOS
brew install ffmpeg

libx264 为 FFmpeg 自带的软件 H.264 编码器,编译 FFmpeg 时启用即可,无需硬件驱动;本脚本为 CPU 软编码,不依赖 GPU。

功能特点

参数说明

参数 含义 默认值 示例
$1 源根目录 必填(交互模式无需) /vol/.../Video/Video-Album
$2 目标根目录 必填(交互模式无需) /vol/.../Video/Japanese
$3 处理成功后是否删除源文件 false true / false

可调参数:

配置 说明 默认值
BITRATE_THRESHOLD 流复制 / 瘦身的码率阈值(bps) 3000000(3000 kbps)
speed_x 预估耗时换算倍速 1.5

不传任何参数时进入交互式(前台执行):依次询问源根目录、目标根目录、是否删除源文件(y/N)、确认开始(y/N),确认后开始处理。命令行模式带参数才触发自动后台,交互模式不会转入后台。

智能判断逻辑

对每个视频调用 get_video_info(ffprobe 读取 codec_name / pix_fmt / bit_rate)后按规则决策:

NEED_ENCODE=1
if codec == h264 && pixfmt == yuv420p:
    if bitrate <= 3000k:  → NEED_ENCODE=0(流复制,无损)
    else:                 → CRF23 瘦身重编码
else:                     → CRF23 重编码
判定 动作 日志提示
h264 + yuv420p + 码率 ≤ 3000k -c copy 流复制 ✅ H264+yuv420p,码率低于阈值,采用流复制封装(无损)
h264 + yuv420p + 码率 > 3000k libx264 -crf 23 瘦身 🔄 ...码率超过阈值,执行CRF23重编码瘦身
其他(hevc/av1/mpeg2 等,或 pix_fmt 非 yuv420p) libx264 -crf 23 重编码 🔄 非标准H264/yuv420p,执行CRF23重编码
码率读取为 N/A 视为超过阈值 → CRF23 重编码 走 🔄 分支

遍历与后缀逻辑

  1. 目录遍历:for SRC_DIR in "${SRC_ROOT}"/*/,遍历源根目录下所有一级子文件夹(无子文件夹则跳过)
  2. 目标级联:每个子文件夹在目标根目录下创建同名目录 DST_ROOT/DIR_NAME
  3. 文件遍历:"${SRC_DIR}"*.{mp4,mkv} 自动匹配两种后缀,不存在时跳过
  4. 后缀逻辑:
    • 源文件 xxx.mkv → 输出 xxx.mp4
    • 源文件 xxx.mp4 → 输出 xxx.mp4(保持原名)
  5. 日志:全局 batch_main.log 记录全部流程;单文件 xxx.log 成功即删、失败保留
  6. del_src=true 时每个文件成功后删除源文件,并对源根目录执行 find -depth -type d -empty -delete;全部处理完后再执行一次兜底清理

转码命令详解

分支一:重编码(CRF23 瘦身 / 标准化)

ffmpeg -threads auto -probesize 32M -avioflags direct \
    -i "源目录/xxx.mkv" -y \
    -c:v libx264 -preset fast -profile:v main -crf 23 \
    -c:a copy \
    -movflags +faststart \
    "目标目录/xxx.mp4" > "目标目录/xxx.log" 2>&1

分支二:流复制(H264+yuv420p 且码率 ≤ 3000k)

ffmpeg -threads auto -probesize 32M -avioflags direct \
    -i "源目录/xxx.mkv" -y \
    -c copy -movflags +faststart \
    "目标目录/xxx.mp4" > "目标目录/xxx.log" 2>&1

参数说明:

参数 作用
-threads auto 自动线程数,充分利用多核 CPU
-probesize 32M 加大探测缓冲,提高流信息识别成功率
-avioflags direct 直读模式,减少 IO 缓冲开销
-c:v libx264 使用 libx264 软件 H.264 编码器
-preset fast 编码速度预设(越慢压缩率越高)
-profile:v main 输出 Main Profile,兼容性最佳
-crf 23 恒定质量档位,值越小质量越好、文件越大
-c copy 视频/音频全程流复制,无损、秒级完成
-movflags +faststart moov 移到文件头,网页/在线播放秒开
-c:a copy 音频流无损直通
> xxx.log 2>&1 单文件日志落盘,排查用

预估耗时

calc_est_time <源文件>
# 读取视频时长(ffprobe)→ ÷ 1.5 倍速 → 日志打印:
# 视频时长:300 秒(≈ 0小时5分0秒)
# 预估耗时:0小时3分20秒 (转码速度=1.5倍)

依赖 ffprobe 与 bc;时长无法读取时仅记录「[警告] 无法读取视频时长」并继续。流复制分支实际耗时远小于预估(无需重编码)。

配套维护命令:

# 查看全局进度
tail -f 目标根目录/batch_main.log

# 强制结束任务并释放锁
pkill -9 -f ffmpeg && rm -f /tmp/videnc_batch.lock

注意事项

脚本源码






#!/bin/bash
# ==============================================================================
# libx264 CRF恒定质量 H.264 批量目录转码脚本
# 参数:源目录 目标目录 是否删除源文件(true/false)
# 特性:前台启动自动nohup后台,加锁防重复运行,全局batch_main.log
# 智能判断:
#   非H264/yuv420p → CRF23重编码
#   H264+yuv420p,视频码率>3000kbps → CRF23瘦身重编码
#   H264+yuv420p,视频码率≤3000kbps → -c copy 无损流复制
# ==============================================================================
set -uo pipefail

list_color_init() {
    export gl_hui=$'\033[38;5;59m'
    export gl_hong=$'\033[38;5;9m'
    export gl_lv=$'\033[38;5;10m'
    export gl_huang=$'\033[38;5;11m'
    export gl_lan=$'\033[38;5;32m'
    export gl_bai=$'\033[38;5;15m'
    export gl_zi=$'\033[38;5;13m'
    export gl_bufan=$'\033[38;5;14m'
}
list_color_init

# 码率阈值:3000000 bps = 3000 kbps,可自行修改
BITRATE_THRESHOLD=3000000

LOCK="/tmp/videnc_batch.lock"

if [[ "${1:-}" != "__BG_RUN__" ]]; then
    if [[ -f "${LOCK}" ]]; then
        echo "任务正在运行,锁文件 ${LOCK} 存在,禁止重复启动"
        exit 1
    fi
    SRC_DIR_PARAM="${1:-}"
    DST_DIR_PARAM="${2:-}"
    DEL_SRC_PARAM="${3:-false}"
    nohup bash "$0" __BG_RUN__ "$@" >/dev/null 2>&1 &
    echo ">>> 命令行批量转码模式"
    echo "————————————————————————————————————————————————"
    echo "源 目 录:${SRC_DIR_PARAM}"
    echo "目标目录:${DST_DIR_PARAM}"
    echo "转码成功后删除源文件:${DEL_SRC_PARAM}"
    echo "✅ 任务已自动进入后台静默执行"
    echo "📖 查看全局日志:tail -f ${DST_DIR_PARAM}/batch_main.log"
    echo "❌ 强制结束进程:pkill -9 -f ffmpeg && rm -f /tmp/videnc_batch.lock"
    echo "————————————————————————————————————————————————"
    exit 0
fi

shift
SRC_ROOT="$1"
DST_ROOT="$2"
DEL_SRC="$3"
GLOBAL_LOG="${DST_ROOT}/batch_main.log"
mkdir -p "${DST_ROOT}"

log_print() {
    echo -e "$1"
    echo -e "$1" | sed -r 's/\x1B\[[0-9;]*[a-zA-Z]//g' >> "${GLOBAL_LOG}"
}

echo $$ > "${LOCK}"
trap 'rm -f "${LOCK}"' EXIT

calc_est_time() {
    local src_file="$1"
    local speed_x="1.5"
    local dur
    dur=$(ffprobe -v error -select_streams v:0 \
        -show_entries stream=duration \
        -of default=noprint_wrappers=1:nokey=1 "${src_file}" 2>/dev/null)
    if [[ -z "${dur}" || "${dur}" == "N/A" ]]; then
        log_print "[警告] 无法读取视频时长"
        return 1
    fi
    local dur_int
    dur_int=$(echo "scale=0; ${dur}/1" | bc)
    local dur_h=$(( dur_int / 3600 ))
    local dur_rem=$(( dur_int % 3600 ))
    local dur_m=$(( dur_rem / 60 ))
    local dur_s=$(( dur_rem % 60 ))
    local est_sec
    est_sec=$(echo "scale=0; ${dur} / ${speed_x}" | bc -l)
    local total_int=${est_sec%%.*}
    local h=$(( total_int / 3600 ))
    local rem=$(( total_int % 3600 ))
    local m=$(( rem / 60 ))
    local s=$(( rem % 60 ))
    log_print "视频时长:${dur} 秒(≈ ${dur_h}小时${dur_m}分${dur_s}秒)"
    log_print "预估耗时:${h}小时${m}分${s}秒 (转码速度=${speed_x}倍)"
    return 0
}

# 智能检测函数:输出三个变量 codec / pixfmt / vbitrate
get_video_info(){
    local f="$1"
    local out
    out=$(ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,pix_fmt,bit_rate -of default=noprint_wrappers=1:nokey=1 "$f" 2>/dev/null)
    read -r codec pixfmt vbitrate <<< $(echo $out | tr '\n' ' ')
    echo "$codec $pixfmt $vbitrate"
}

break_end() {
    echo -e "${gl_lv}操作完成${gl_bai}"
    echo -e "${gl_bai}按任意键继续 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}\c"
    read -r -n 1 -s -r -p ""
    echo ""
    clear
}

abspath() {
    local p="$1"
    if command -v realpath &>/dev/null; then
        realpath "$p" 2>/dev/null || readlink -f "$p" 2>/dev/null || echo "$p"
    else
        case "$p" in
            /*) echo "$p" ;;
            *)  echo "$PWD/$p" ;;
        esac
    fi
}

install_deps() {
    if command -v ffmpeg &>/dev/null; then
        if ffmpeg -h encoder=libx264 >/dev/null 2>&1; then
            return 0
        fi
    fi
    echo -e "${gl_huang}ffmpeg 未找到,尝试自动安装${gl_bai}"
    if command -v apt &>/dev/null; then
        sudo apt update && sudo apt install -y ffmpeg
    elif command -v dnf &>/dev/null; then
        sudo dnf install -y ffmpeg
    elif command -v yum &>/dev/null; then
        sudo yum install -y epel-release && sudo yum install -y ffmpeg
    elif command -v pacman &>/dev/null; then
        sudo pacman -S --noconfirm ffmpeg
    elif command -v zypper &>/dev/null; then
        sudo zypper install -y ffmpeg
    elif command -v apk &>/dev/null; then
        sudo apk add ffmpeg
    elif command -v brew &>/dev/null; then
        brew install ffmpeg
    else
        echo -e "${gl_hong}无法自动安装 ffmpeg${gl_bai}"
    fi
    if ! command -v ffmpeg &>/dev/null; then
        echo -e "${gl_hong}ffmpeg 安装失败,请手动安装${gl_bai}"
        return 1
    fi
    return 0
}

do_batch_encode() {
    local SRC_ROOT="$1"
    local DST_ROOT="$2"
    local DEL_SRC="$3"
    local GLOBAL_LOG="${DST_ROOT}/batch_main.log"
    log_print "====================================="
    local now
    now=$(date +"%Y-%m-%d %H:%M:%S")
    log_print "[${now}] ====== 开始本轮批量任务 ======"
    log_print "阈值设置:视频码率大于 $((BITRATE_THRESHOLD/1000)) kbps 将进行CRF23重编码瘦身"

    for SRC_DIR in "${SRC_ROOT}"/*/; do
        [ -d "${SRC_DIR}" ] || continue
        DIR_NAME=$(basename "${SRC_DIR%/}")
        DST_DIR="${DST_ROOT}/${DIR_NAME}"
        log_print ""
        now=$(date +"%Y-%m-%d %H:%M:%S")
        log_print "[${now}] 正在处理目录:${DIR_NAME}"
        log_print "====================================="
        log_print "源 目 录: ${SRC_DIR}"
        log_print "目标目录: ${DST_DIR}"
        mkdir -p "${DST_DIR}"

        for src_file in "${SRC_DIR}"*.{mp4,mkv}; do
            [ -f "${src_file}" ] || continue
            FILENAME=$(basename "${src_file}")
            NAME_NO_EXT="${FILENAME%.*}"
            EXT="${FILENAME##*.}"
            if [[ "${EXT,,}" == "mkv" ]]; then
                DST_FILE="${DST_DIR}/${NAME_NO_EXT}.mp4"
            else
                DST_FILE="${DST_DIR}/${FILENAME}"
            fi
            LOG_FILE="${DST_DIR}/${NAME_NO_EXT}.log"
            log_print "正在处理:${src_file}"
            calc_est_time "${src_file}"

            # 获取视频信息
            read codec pixfmt vbitrate <<< $(get_video_info "${src_file}")
            log_print "文件名称: ${FILENAME}"
            log_print "源视频编码: $codec , pixfmt: $pixfmt , bitrate: $vbitrate bps"
            log_print "输出文件: ${DST_FILE}"
            log_print "日志文件: ${LOG_FILE}"

            NEED_ENCODE=1
            if [[ "$codec" == "h264" && "$pixfmt" == "yuv420p" ]];then
                if [[ "$vbitrate" != "N/A" && "$vbitrate" -le "${BITRATE_THRESHOLD}" ]];then
                    NEED_ENCODE=0
                    log_print "✅ H264+yuv420p,码率低于阈值,采用流复制封装(无损)"
                else
                    log_print "🔄 H264+yuv420p,但码率超过阈值,执行CRF23重编码瘦身"
                fi
            else
                log_print "🔄 非标准H264/yuv420p,执行CRF23重编码"
            fi

            if [[ $NEED_ENCODE -eq 1 ]];then
                ffmpeg -threads auto -probesize 32M -avioflags direct \
                    -i "${src_file}" -y \
                    -c:v libx264 -preset veryfast -profile:v main -crf 23 \
                    -c:a copy \
                    -movflags +faststart \
                    "${DST_FILE}" > "${LOG_FILE}" 2>&1
            else
                ffmpeg -threads auto -probesize 32M -avioflags direct \
                    -i "${src_file}" -y \
                    -c copy -movflags +faststart \
                    "${DST_FILE}" > "${LOG_FILE}" 2>&1
            fi

            if [ $? -eq 0 ]; then
                log_print "✅ ${FILENAME} 处理成功"
                rm -f "${LOG_FILE}"
                if [[ "${DEL_SRC}" == "true" ]]; then
                    log_print "✅ 删除源文件"
                    rm -f "${src_file}"
                    find "${SRC_ROOT}" -depth -type d -empty -delete
                fi
            else
                log_print "❌ ${FILENAME} 处理失败!保留源文件和日志,继续下一个文件"
            fi
            log_print ""
        done
    done

    now=$(date +"%Y-%m-%d %H:%M:%S")
    log_print "[${now}] ====== 本轮所有文件处理完成 ======"
    if [[ "${DEL_SRC}" == "true" ]]; then
        log_print "\n🎉 全部目录处理完毕,执行最后一次空目录清理"
        find "${SRC_ROOT}" -depth -type d -empty -delete
    else
        log_print "\n🎉 全部目录处理完毕(不删除源文件,跳过空目录清理)"
    fi
}

cli_mode() {
    local src_dir="$1"
    local dst_dir="$2"
    local del_src="${3:-false}"
    src_dir=$(abspath "${src_dir}")
    dst_dir=$(abspath "${dst_dir}")
    if [[ ! -d "${src_dir}" ]]; then
        echo "错误:源目录不存在 -> ${src_dir}"
        return 1
    fi
    mkdir -p "${dst_dir}" || { echo "无法创建目标目录"; break_end; return 1; }
    echo ">>> 命令行批量转码模式"
    echo "————————————————————————————————————————————————"
    echo "源 目 录:${src_dir}"
    echo "目标目录:${dst_dir}"
    echo "转码成功后删除源文件:${del_src}"
    echo "————————————————————————————————————————————————"
    do_batch_encode "${src_dir}" "${dst_dir}" "${del_src}"
    return $?
}

interactive_mode() {
    clear
    echo ">>> 交互式 一级子目录批量 libx264 CRF H.264智能处理(mp4/mkv → mp4)"
    echo "规则:H264+yuv420p且码率>3000kbps自动瘦身;低于阈值直接无损复制"
    echo "————————————————————————————————————————————————"
    read -r -e -p "输入源根目录: " src_in
    src_in=$(abspath "${src_in}")
    if [[ ! -d "${src_in}" ]]; then
        echo "源目录不存在"
        break_end
        return 1
    fi
    read -r -e -p "输入目标根目录: " dst_in
    dst_in=$(abspath "${dst_in}")
    mkdir -p "${dst_in}" || { echo "无法创建目标目录"; break_end; return 1; }
    read -r -e -p "$(echo -e "${gl_bai}处理成功是否删除源文件? (${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}): ")" del_choice
    local del_src="false"
    if [[ "${del_choice}" =~ ^[Yy]$ ]]; then
        del_src="true"
    fi
    echo "————————————————————————————————————————————————"
    echo "源 目 录:${src_in}"
    echo "目标目录:${dst_in}"
    echo "删除源文件:${del_src}"
    echo "码率阈值:$((BITRATE_THRESHOLD/1000)) kbps"
    echo "————————————————————————————————————————————————"
    read -r -e -p "$(echo -e "${gl_bai}确认开始? (${gl_lv}y${gl_bai}/${gl_hong}N${gl_bai}): ")" confirm
    if [[ ! "${confirm}" =~ ^[Yy]$ ]]; then
        echo "已取消任务"
        break_end
        return 0
    fi
    do_batch_encode "${src_in}" "${dst_in}" "${del_src}"
    break_end
}

main() {
    install_deps || exit 1
    if [[ $# -ge 1 ]]; then
        cli_mode "$@"
    else
        interactive_mode
    fi
}

main "$@"

创建本地脚本

new_script="ffmpeg_video_h264_all.sh"

cat > "$new_script" << 'EOF'
#!/bin/bash

# 粘贴上方脚本源码

EOF

# 保留本地脚本,去掉 rm -f "$new_script"
chmod +x "$new_script" && ./"$new_script" && rm -f "$new_script"

相关命令