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

ffmpeg_video_hvc1_all

脚本

HEVC‑QSV 批量目录转码

补充说明

基于 Intel QSV(Quick Sync Video)硬件加速的「一级子目录批量转码」脚本,把源根目录下所有一级子文件夹内的 mp4 / mkv 视频逐个串行转码为 HEVC/H.265,写入 hvc1 标签。核心特性:前台一行命令启动,脚本自动 nohup 转后台静默执行(__BG_RUN__ 哨兵自重启),并通过锁文件 /tmp/videnc_batch.lock 防止重复启动;所有输出同时写入全局 batch_main.log。

目标目录结构:源根目录/专辑子文件夹/*.{mp4,mkv} → 目标根目录/同名子文件夹/*.mp4;mkv 统一转码为 mp4,mp4 保持原名与原后缀。每个文件转码前通过 ffprobe 读取时长并按约 1.91 倍速预估完成时间;转码成功后按需删除源文件并清理源目录下的空目录(深度优先,含嵌套),单文件日志转码成功即删除、失败则保留。

不传参数时进入交互式模式(前台执行):依次输入源/目标目录、删除开关,确认后开始转码。脚本启动时自动检测并安装 ffmpeg,校验 hevc_qsv 硬件编码器与 VA‑API 环境。逐个串行处理,避免多个 ffmpeg 同时抢占 QSV 硬件队列导致花屏/卡死。

执行流程:

# 前台执行(立即返回,任务在后台跑)
./ffmpeg_video_hvc1_all.sh 源根目录 目标根目录 [true/false]

# 后台进程实际执行(自动重启,带锁、写全局日志)
./ffmpeg_video_hvc1_all.sh __BG_RUN__ 源根目录 目标根目录 [true/false]

输出结构示例:

目标根目录/
├── batch_main.log        ← 全局总日志(去色写入,含预估耗时/成功/失败汇总)
├── 专辑A/
│   ├── 01.mp4            ← mkv 转码为 mp4
│   ├── 01.log            ← 成功即删除,失败保留
│   └── 02.mp4            ← mp4 保持原名
└── 专辑B/
    └── 03.mp4

一键脚本

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

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

chmod +x ffmpeg_video_hvc1_all.sh

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

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

# 不传参数 → 交互式(前台执行,输入源/目标、删除开关、确认)
./ffmpeg_video_hvc1_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 已装且支持 hevc_qsv 即直接通过;未装或不受支持则按发行版自动安装(apt/dnf/yum/pacman/zypper/apk/brew,自动安装需要 sudo 权限)。也可手动安装:

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

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

# Arch Linux
sudo pacman -S --noconfirm ffmpeg vainfo

# Alpine
sudo apk add ffmpeg vainfo

# macOS
brew install ffmpeg

QSV 硬件转码前提:FFmpeg 编译启用 QSV、系统 VA‑API 环境正常(需加载 Intel 核显驱动)。脚本启动时会校验 ffmpeg -h encoder=hevc_qsv,如果装了 vainfo 还会校验 VA‑API 环境。

功能特点

参数说明

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

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

遍历与后缀逻辑

  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;全部处理完后再执行一次兜底清理

转码命令详解

脚本对每个视频实际执行的 FFmpeg 命令:

ffmpeg -threads auto -probesize 32M -avioflags direct \
    -i "源目录/xxx.mkv" -y \
    -c:v hevc_qsv -preset fast -b:v 2600k -maxrate 5200k -bufsize 10400k -tag:v hvc1 \
    -c:a copy \
    "目标目录/xxx.mp4" > "目标目录/xxx.log" 2>&1

参数说明:

参数 作用
-threads auto 自动线程数,QSV 硬件通路下避免 CPU 抢占
-probesize 32M 加大探测缓冲,提高流信息识别成功率
-avioflags direct 直读模式,减少 IO 缓冲开销
-c:v hevc_qsv 使用 Intel QSV 硬件 HEVC 编码器
-preset fast QSV preset,平衡速度与画质
-b:v 2600k 目标视频码率
-maxrate 5200k 峰值码率上限(两倍目标)
-bufsize 10400k VBV 缓冲大小(两倍 maxrate)
-tag:v hvc1 写入 hvc1 封装标签
-c:a copy 音频流无损复制
> xxx.log 2>&1 单文件日志落盘,排查用

预估耗时

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

依赖 ffprobe 与 bc;时长无法读取时仅记录「[警告] 无法读取视频时长」并继续。

配套维护命令:

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

# 查看某个文件实时日志
tail -f 目标目录/xxx.log

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

注意事项

脚本源码

#!/bin/bash
# ==============================================================================
# HEVC hvc1 QSV 批量目录转码脚本
# 参数:源目录 目标目录 是否删除源文件(true/false)
# 特性:前台启动自动nohup后台,加锁防重复运行,全局batch_main.log
# ==============================================================================
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
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.91"
    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
}

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=hevc_qsv >/dev/null 2>&1; then
            if command -v vainfo &>/dev/null; then
                vainfo >/dev/null 2>&1
            fi
            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 vainfo
    elif command -v dnf &>/dev/null; then
        sudo dnf install -y ffmpeg vainfo
    elif command -v yum &>/dev/null; then
        sudo yum install -y epel-release && sudo yum install -y ffmpeg vainfo
    elif command -v pacman &>/dev/null; then
        sudo pacman -S --noconfirm ffmpeg vainfo
    elif command -v zypper &>/dev/null; then
        sudo zypper install -y ffmpeg vainfo
    elif command -v apk &>/dev/null; then
        sudo apk add ffmpeg vainfo
    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}] ====== 开始本轮批量任务 ======"
    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}"
            log_print "文件名称: ${FILENAME}"
            log_print "开始转码: ${src_file}"
            log_print "输出文件: ${DST_FILE}"
            log_print "日志文件: ${LOG_FILE}"
            log_print "查看日志:tail -f ${LOG_FILE}"
            ffmpeg -threads auto -probesize 32M -avioflags direct \
                -i "${src_file}" -y \
                -c:v hevc_qsv -preset fast -b:v 2600k -maxrate 5200k -bufsize 10400k -tag:v hvc1 \
                -c:a copy \
                "${DST_FILE}" > "${LOG_FILE}" 2>&1
            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 ">>> 交互式 一级子目录批量 转码(mp4/mkv → mp4)"
    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 "————————————————————————————————————————————————"
    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_hvc1_all.sh"

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

# 粘贴上方脚本源码

EOF

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

相关命令