视频处理工具

需求

以前我是很少拍视频的,主要是拍照,但是自从买了DJI的Action4和Neo2以后,视频内容就多了,转换视频的需求也多了。

GUI工具固然好用,但是操作麻烦效率低下,我需要的功能也没那么复杂,通常就是转个码率分辨率(便于网上发),或者裁剪一下什么的,ffmpeg足矣。

但是用过ffmpeg的人都知道,这玩意儿功能是强大,但是命令行参数也是复杂得一笔,为了省事,我把常用的几个操作都写成shell脚本,时间一长就积累了一堆脚本,用起来也还是不方便。

解决

这几天一怒之下拿AI把这些脚本整合成一个,于是有这个工具脚本:

#!/bin/bash

# mp4tool.sh - 视频处理多功能工具
# 用法: mp4tool.sh [选项] 输入文件
# 选项:
#   -b 码率      指定视频码率 (如 1500K, 3M)
#   -s 分辨率    缩放分辨率 (720p/1080p/2160p/4k),仅缩小不放大
#   -f           修复模式,使用copy方式不重新编码
#   -cpu         强制使用CPU编码,默认使用GPU
#   -dji         DJI模式,使用轨道映射,默认码率*4
#   -t           竖屏转横屏(可选p参数,如-tp则填充16:9)
#   -cs 时间     开头剪辑位置(秒数或HH:MM:SS格式)
#   -ce 时间     结尾剪辑位置(秒数或HH:MM:SS格式,时间格式表示结束点位置)
#   -p 后缀      输出文件后缀,默认为 "out"
#   -af 音频文件  指定外部音频文件作为视频音轨

# 默认值
BITRATE=""
SCALE=""
FIX_MODE=false
USE_CPU=false
DJI_MODE=false
TRANSPOSE_MODE=""
CUT_START=0
CUT_END=0
SUFFIX="out"
INPUT=""
CUT_END_ARG=""
AUDIO_FILE=""

# 将时间字符串转换为秒数
# 支持格式:纯数字(秒)、SS、MM:SS、HH:MM:SS
parse_time() {
    local time_str="$1"
    
    # 检查是否包含冒号
    if [[ "$time_str" == *":"* ]]; then
        # 时间格式,解析 HH:MM:SS 或 MM:SS
        local IFS=':'
        read -ra parts <<< "$time_str"
        local seconds=0
        local multiplier=1
        
        # 从后往前处理
        for (( i=${#parts[@]}-1; i>=0; i-- )); do
            seconds=$(( seconds + ${parts[i]} * multiplier ))
            multiplier=$(( multiplier * 60 ))
        done
        echo "$seconds"
    else
        # 纯数字,直接返回秒数
        echo "$time_str"
    fi
}

# 解析参数
while [[ $# -gt 0 ]]; do
    case $1 in
        -b)
            BITRATE="$2"
            BITRATE_SPECIFIED=true
            shift 2
            ;;
        -s)
            SCALE="$2"
            shift 2
            ;;
        -f)
            FIX_MODE=true
            shift
            ;;
        -cpu)
            USE_CPU=true
            shift
            ;;
        -dji)
            DJI_MODE=true
            shift
            ;;
        -tp)
            TRANSPOSE_MODE="pad"
            shift
            ;;
        -t)
            TRANSPOSE_MODE="rotate"
            shift
            ;;
        -cs)
            CUT_START=$(parse_time "$2")
            shift 2
            ;;
        -ce)
            CUT_END_ARG="$2"
            shift 2
            ;;
        -af)
            AUDIO_FILE="$2"
            shift 2
            ;;
        -p)
            SUFFIX="$2"
            shift 2
            ;;
        *)
            INPUT="$1"
            shift
            ;;
    esac
done

# 检查输入文件
if [ -z "$INPUT" ]; then
    echo "用法: mp4tool.sh [选项] 输入文件"
    echo "选项:"
    echo "  -b 码率      指定视频码率 (如 1500K, 3M)"
    echo "  -s 分辨率    缩放分辨率 (720p/1080p/2160p/4k),仅缩小不放大"
    echo "  -f           修复模式,使用copy方式不重新编码"
    echo "  -cpu         强制使用CPU编码,默认使用GPU"
    echo "  -dji         DJI模式,使用轨道映射,默认码率*4"
    echo "  -t           竖屏转横屏(仅旋转)"
    echo "  -tp          竖屏转横屏(旋转并填充16:9)"
    echo "  -cs 时间     开头剪辑位置(秒数或HH:MM:SS格式)"
    echo "  -ce 时间     结尾剪辑位置(秒数或HH:MM:SS格式,时间格式表示结束点位置)"
    echo "  -af 音频文件  指定外部音频文件作为视频音轨"
    echo "  -p 后缀      输出文件后缀,默认为 'out'"
    exit 1
fi

if [ ! -f "$INPUT" ]; then
    echo "错误: 文件 '$INPUT' 不存在"
    exit 1
fi

if [ -n "$AUDIO_FILE" ] && [ ! -f "$AUDIO_FILE" ]; then
    echo "错误: 音频文件 '$AUDIO_FILE' 不存在"
    exit 1
fi

# 输出文件名
OUTPUT="${INPUT%.*}.${SUFFIX}.mp4"

# 获取视频信息
get_video_info() {
    ffprobe -v error -select_streams v:0 -show_entries stream=width,height,duration -of csv=p=0 "$1" 2>/dev/null
}

# 获取原视频码率(返回bps)
get_video_bitrate() {
    ffprobe -v error -select_streams v:0 -show_entries stream=bit_rate -of default=noprint_wrappers=1:nokey=1 "$1" 2>/dev/null
}

# 将码率字符串转换为bps数值
parse_bitrate() {
    local br="$1"
    local num=$(echo "$br" | sed 's/[^0-9.]//g')
    local unit=$(echo "$br" | sed 's/[0-9.]//g' | tr '[:lower:]' '[:upper:]')
    
    case $unit in
        K|KB|KBIT) echo "$(echo "$num * 1000" | bc)" ;;
        M|MB|MBIT) echo "$(echo "$num * 1000000" | bc)" ;;
        G|GB|GBIT) echo "$(echo "$num * 1000000000" | bc)" ;;
        *) echo "$num" ;;
    esac
}

# 分辨率高度映射
get_target_height() {
    case $1 in
        720p)  echo 720 ;;
        1080p) echo 1080 ;;
        2160p|4k) echo 2160 ;;
        *) echo 0 ;;
    esac
}

# 根据分辨率获取默认码率
get_default_bitrate() {
    case $1 in
        720p)  echo "1500K" ;;
        1080p) echo "3000K" ;;
        2160p|4k) echo "10000K" ;;
        *) echo "3000K" ;;
    esac
}

# 处理 -ce 参数(需要在获取视频时长之后)
# 纯数字表示结尾剪掉的秒数,时间格式表示结束点位置
if [ -n "$CUT_END_ARG" ]; then
    if [[ "$CUT_END_ARG" == *":"* ]]; then
        # 时间格式,计算从结尾剪掉的秒数
        CUT_END_TIME=$(parse_time "$CUT_END_ARG")
        # 需要获取视频时长来计算
        DURATION_FOR_CUT=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$INPUT" 2>/dev/null)
        if [ -n "$DURATION_FOR_CUT" ]; then
            CUT_END=$(echo "$DURATION_FOR_CUT - $CUT_END_TIME" | bc)
            CUT_END=$(printf "%.0f" "$CUT_END")
            if [ "$(echo "$CUT_END < 0" | bc)" -eq 1 ]; then
                CUT_END=0
            fi
        fi
    else
        # 纯数字,直接作为剪掉的秒数
        CUT_END="$CUT_END_ARG"
    fi
fi

# 获取原视频分辨率(先获取视频信息用于判断)
VIDEO_INFO=$(get_video_info "$INPUT")
if [ -z "$VIDEO_INFO" ]; then
    echo "错误: 无法获取视频信息"
    exit 1
fi

ORIGINAL_WIDTH=$(echo "$VIDEO_INFO" | cut -d',' -f1)
ORIGINAL_HEIGHT=$(echo "$VIDEO_INFO" | cut -d',' -f2)
VIDEO_DURATION=$(echo "$VIDEO_INFO" | cut -d',' -f3)

echo "原视频: ${ORIGINAL_WIDTH}x${ORIGINAL_HEIGHT}, 时长: ${VIDEO_DURATION}s"

# 确定是否需要缩放
NEED_SCALE=false
TARGET_WIDTH=""
TARGET_HEIGHT=""

if [ -n "$SCALE" ]; then
    TARGET_HEIGHT=$(get_target_height "$SCALE")
    
    if [ "$TARGET_HEIGHT" -eq 0 ]; then
        echo "错误: 不支持的分辨率 '$SCALE',支持: 720p, 1080p, 2160p, 4k"
        exit 1
    fi
    
    # 转置模式下,宽高互换
    if [ -n "$TRANSPOSE_MODE" ]; then
        # 转置后:宽度=原高度,高度=原宽度
        TRANSPOSED_WIDTH=$ORIGINAL_HEIGHT
        TRANSPOSED_HEIGHT=$ORIGINAL_WIDTH
        
        # 仅缩小,不放大(基于转置后的高度)
        if [ "$TRANSPOSED_HEIGHT" -gt "$TARGET_HEIGHT" ]; then
            NEED_SCALE=true
            # 计算目标宽度(基于转置后的比例)
            TARGET_WIDTH=$(echo "scale=0; $TRANSPOSED_WIDTH * $TARGET_HEIGHT / $TRANSPOSED_HEIGHT" | bc)
            # 确保宽度为偶数
            TARGET_WIDTH=$(echo "scale=0; $TARGET_WIDTH / 2 * 2" | bc)
            echo "转置后将缩放到: ${TARGET_WIDTH}x${TARGET_HEIGHT}"
        else
            echo "转置后高度 ($TRANSPOSED_HEIGHT) <= 目标高度 ($TARGET_HEIGHT),不缩放"
        fi
    else
        # 仅缩小,不放大
        if [ "$ORIGINAL_HEIGHT" -gt "$TARGET_HEIGHT" ]; then
            NEED_SCALE=true
            # 计算目标宽度(保持宽高比)
            TARGET_WIDTH=$(echo "scale=0; $ORIGINAL_WIDTH * $TARGET_HEIGHT / $ORIGINAL_HEIGHT" | bc)
            # 确保宽度为偶数
            TARGET_WIDTH=$(echo "scale=0; $TARGET_WIDTH / 2 * 2" | bc)
            echo "将缩放到: ${TARGET_WIDTH}x${TARGET_HEIGHT}"
        else
            echo "原视频高度 ($ORIGINAL_HEIGHT) <= 目标高度 ($TARGET_HEIGHT),不缩放"
        fi
    fi
fi

# 竖屏转横屏模式
if [ -n "$TRANSPOSE_MODE" ]; then
    echo "竖屏转横屏模式: $TRANSPOSE_MODE"
    # 转置操作需要CPU处理,强制使用CPU
    USE_CPU=true
fi

# DJI模式:禁用自动copy模式
if [ "$DJI_MODE" = true ]; then
    echo "DJI模式: 启用轨道映射..."
fi

# 获取原视频码率
ORIGINAL_BITRATE=$(get_video_bitrate "$INPUT")
if [ -n "$ORIGINAL_BITRATE" ] && [ "$ORIGINAL_BITRATE" != "N/A" ]; then
    echo "原视频码率: ${ORIGINAL_BITRATE} bps"
fi

# 判断是否需要处理
NEED_PROCESS=true

# 如果指定了分辨率但不需要缩放,检查码率
if [ -n "$SCALE" ] && [ "$NEED_SCALE" = false ]; then
    TARGET_BITRATE=$(parse_bitrate "$(get_default_bitrate "$SCALE")")
    if [ -n "$ORIGINAL_BITRATE" ] && [ "$ORIGINAL_BITRATE" != "N/A" ]; then
        if [ "$ORIGINAL_BITRATE" -le "$TARGET_BITRATE" ]; then
            echo "原视频码率 ($ORIGINAL_BITRATE) <= 目标码率 ($TARGET_BITRATE),无需处理"
            NEED_PROCESS=false
        else
            echo "原视频码率 ($ORIGINAL_BITRATE) > 目标码率 ($TARGET_BITRATE),将转码"
            BITRATE=$(get_default_bitrate "$SCALE")
        fi
    fi
fi

# 如果指定了码率,检查是否低于原视频码率
if [ -n "$BITRATE" ] && [ "$BITRATE_SPECIFIED" = true ] && [ "$NEED_SCALE" = false ]; then
    TARGET_BITRATE=$(parse_bitrate "$BITRATE")
    if [ -n "$ORIGINAL_BITRATE" ] && [ "$ORIGINAL_BITRATE" != "N/A" ]; then
        if [ "$ORIGINAL_BITRATE" -le "$TARGET_BITRATE" ]; then
            echo "原视频码率 ($ORIGINAL_BITRATE) <= 指定码率 ($TARGET_BITRATE),无需处理"
            NEED_PROCESS=false
        fi
    fi
fi

# 如果不需要处理,直接退出
if [ "$NEED_PROCESS" = false ]; then
    echo "视频无需处理,退出"
    exit 0
fi

# 如果没有指定码率和分辨率,自动使用copy模式(DJI模式除外)
if [ -z "$BITRATE" ] && [ "$NEED_SCALE" = false ] && [ "$DJI_MODE" = false ] && [ -z "$TRANSPOSE_MODE" ]; then
    echo "未指定码率和分辨率,自动使用copy模式..."
    FIX_MODE=true
fi

# 修复模式:使用copy方式
if [ "$FIX_MODE" = true ]; then
    echo "修复模式: 使用copy方式..."
    
    CMD="ffmpeg -i \"$INPUT\""
    
    # 外部音频文件
    if [ -n "$AUDIO_FILE" ]; then
        CMD="$CMD -i \"$AUDIO_FILE\""
        echo "外部音频: $AUDIO_FILE"
    fi
    
    # 添加剪辑参数
    if [ "$CUT_START" -gt 0 ] || [ "$CUT_END" -gt 0 ]; then
        if [ "$CUT_START" -gt 0 ]; then
            CMD="$CMD -ss $CUT_START"
        fi
        if [ "$CUT_END" -gt 0 ]; then
            # 获取视频时长
            DURATION=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$INPUT" 2>/dev/null)
            if [ -n "$DURATION" ]; then
                END_TIME=$(echo "$DURATION - $CUT_END" | bc)
                CMD="$CMD -to $END_TIME"
            fi
        fi
    fi
    
    if [ -n "$AUDIO_FILE" ]; then
        CMD="$CMD -map 0:v:0 -map 1:a:0 -c:v copy -c:a copy -shortest -f mp4 -y \"$OUTPUT\""
    else
        CMD="$CMD -c copy -f mp4 -y \"$OUTPUT\""
    fi
    
    echo "执行: $CMD"
    eval "$CMD"
    exit $?
fi

# 确定码率
if [ -z "$BITRATE" ]; then
    if [ "$NEED_SCALE" = true ]; then
        BITRATE=$(get_default_bitrate "$SCALE")
    else
        BITRATE="3000K"
    fi
fi

# DJI模式:如果未指定码率,默认码率*4
if [ "$DJI_MODE" = true ] && [ "$BITRATE_SPECIFIED" != true ]; then
    # 将码率转换为数字并乘以4
    BITRATE_NUM=$(echo "$BITRATE" | sed 's/[^0-9]//g')
    BITRATE_UNIT=$(echo "$BITRATE" | sed 's/[0-9]//g')
    BITRATE=$((BITRATE_NUM * 4))${BITRATE_UNIT}
fi

echo "码率: $BITRATE"

# 构建ffmpeg命令
build_ffmpeg_cmd() {
    local USE_GPU=$1
    local CMD="ffmpeg"
    
    # GPU加速输入
    if [ "$USE_GPU" = true ]; then
        CMD="$CMD -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format vaapi"
    fi
    
    # 剪辑参数(放在 -i 前面实现快速定位)
    if [ "$CUT_START" -gt 0 ]; then
        CMD="$CMD -ss $CUT_START"
    fi
    
    CMD="$CMD -i \"$INPUT\""
    
    # 外部音频文件
    if [ -n "$AUDIO_FILE" ]; then
        CMD="$CMD -i \"$AUDIO_FILE\""
    fi
    
    # DJI模式:轨道映射 / 音频映射
    if [ -n "$AUDIO_FILE" ]; then
        CMD="$CMD -map 0:v:0 -map 1:a:0"
    elif [ "$DJI_MODE" = true ]; then
        CMD="$CMD -map 0:v:0 -map 0:a:0"
    fi
    
    # 结尾剪辑
    if [ "$CUT_END" -gt 0 ] && [ -n "$VIDEO_DURATION" ]; then
        END_TIME=$(echo "$VIDEO_DURATION - $CUT_END - $CUT_START" | bc)
        CMD="$CMD -t $END_TIME"
    fi
    
    if [ "$USE_GPU" = true ]; then
        # GPU编码
        if [ "$NEED_SCALE" = true ]; then
            CMD="$CMD -vf 'hwupload,scale_vaapi=w=${TARGET_WIDTH}:h=${TARGET_HEIGHT}:format=nv12'"
        else
            CMD="$CMD -vf 'hwupload,scale_vaapi=w=-1:h=-1:format=nv12'"
        fi
        CMD="$CMD -c:v hevc_vaapi -b:v $BITRATE"
    else
        # CPU编码
        VF_FILTER=""
        
        # 竖屏转横屏处理
        if [ -n "$TRANSPOSE_MODE" ]; then
            if [ "$TRANSPOSE_MODE" = "pad" ]; then
                # 旋转并填充为16:9
                VF_FILTER="transpose=2,pad=ih*16/9:ih:(ow-iw)/2:(oh-ih)/2"
            else
                # 仅旋转
                VF_FILTER="transpose=2"
            fi
            
            # 如果还需要缩放(转置后的分辨率)
            if [ "$NEED_SCALE" = true ]; then
                VF_FILTER="${VF_FILTER},scale=${TARGET_WIDTH}:${TARGET_HEIGHT}"
            fi
        elif [ "$NEED_SCALE" = true ]; then
            VF_FILTER="scale=${TARGET_WIDTH}:${TARGET_HEIGHT}"
        fi
        
        if [ -n "$VF_FILTER" ]; then
            CMD="$CMD -vf '$VF_FILTER'"
        fi
        
        CMD="$CMD -c:v libx265 -b:v $BITRATE"
    fi
    
    # 音频和其他流处理
    if [ -n "$AUDIO_FILE" ]; then
        CMD="$CMD -c:a copy -shortest -f mp4 -y \"$OUTPUT\""
    else
        CMD="$CMD -c:a copy -c:d copy -f mp4 -y \"$OUTPUT\""
    fi
    
    echo "$CMD"
}

# 执行编码
if [ "$USE_CPU" = true ]; then
    echo "使用CPU编码..."
    CMD=$(build_ffmpeg_cmd false)
else
    echo "使用GPU编码..."
    CMD=$(build_ffmpeg_cmd true)
fi

echo "执行: $CMD"
eval "$CMD"

if [ $? -eq 0 ]; then
    echo "编码成功: $OUTPUT"
else
    echo "编码失败"
    exit 1
fi

需要注意的是,因为我是用AMD的,所以GPU加速用的是vaapi,如果你是用nVidia,则需要改一下。

推送到[go4pro.org]