视频处理工具
需求
以前我是很少拍视频的,主要是拍照,但是自从买了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]