shell脚本开发技巧

  这里只介绍如何在ubuntu下进行shell脚本的开发与检查

0.参考资料

0【亲测免费】 ShellCheck 安装和配置指南

1.如何安装shellcheck

bash 复制代码
sudo apt-get install shellcheck

2.案例

  我用kimi写了一个gst的数字变焦测试案例,shell脚本如下:

sh 复制代码
#!/bin/bash
# 多相机变焦录制脚本 - QCS8550 平台 (支持SAT分辨率切换)
# 超过6.3x时sensor分辨率变为4624x2608
# 支持: 固定zoom录制 / 连续zoom变化录制 / 自动档位遍历 / RTSP实时观察

# ==================== 配置参数 ====================
CAMERA_ID=3
FOCUS_MODE=3
WIDTH=1920
HEIGHT=1080
FRAMERATE=30
OUTPUT_FILE="/data/test3.mp4"

# SAT切换阈值
SAT_THRESHOLD=6.9

# sensor-ov50h40分辨率 (Wide)
LOW_RES_W=4096
LOW_RES_H=3072

# sensor-imx682分辨率 (Tele/Periscope)
HIGH_RES_W=4624
HIGH_RES_H=2608

# 预定义zoom档位 (基于对应sensor分辨率计算)
# 1.0x~6.3x: 基于4096x3072
ZOOM_1_0X="<0,0,4096,3072>"
ZOOM_2_0X="<1024,768,2048,1536>"
ZOOM_4_0X="<1536,1152,1024,768>"
ZOOM_6_0X="<1706,1280,682,512>"

# 6.3x~24.0x: 基于4624x2608 (中心点2312,1304)
ZOOM_8_0X_HIGH="<2024,1139,578,326>"
ZOOM_12_0X_HIGH="<2119,1188,385,217>"
ZOOM_24_0X_HIGH="<2215,1235,193,109>"

# 默认zoom (1.0x)
DEFAULT_ZOOM="${ZOOM_1_0X}"

# ==================== 函数定义 ====================

show_help() {
    echo "========================================"
    echo "  QMMF SAT变焦录制脚本"
    echo "========================================"
    echo "用法: $0 [模式] [选项]"
    echo ""
    echo "模式:"
    echo "  record     固定zoom录制 (默认)"
    echo "  sweep      zoom从1.0x连续变化到24.0x录制"
    echo "  step       自动遍历zoom档位录制"
    echo "  rtsp       RTSP实时观察"
    echo ""
    echo "选项:"
    echo "  -z, --zoom <倍数>     设置zoom倍数 (record模式, 默认1.0)"
    echo "  -s, --speed <秒>      sweep模式每档停留时间 (默认3)"
    echo "  -d, --delay <秒>      step模式每档停留时间 (默认5)"
    echo "  -o, --output <路径>    输出文件路径"
    echo "  -t, --time <秒>       录制总时长 (record模式)"
    echo "  -p, --port <端口>     RTSP端口 (默认8554)"
    echo "  -h, --help            显示帮助"
    echo ""
    echo "示例:"
    echo "  $0 record -z 8.0 -t 60         # 固定8倍录制60秒"
    echo "  $0 sweep -s 2 -o /data/zoom.mp4 # 连续zoom变化录制"
    echo "  $0 step -d 5                   # 遍历档位每档5秒"
    echo "  $0 rtsp -z 4.0                 # RTSP实时观察4倍变焦"
    echo ""
    echo "注意: >6.3x时sensor自动切换到4624x2608高分辨率"
    echo "========================================"
}

# 根据倍数获取zoom参数 (自动判断SAT切换)
get_zoom_by_factor() {
    local factor="$1"
    local is_high_res
    is_high_res=$(echo "$factor > $SAT_THRESHOLD" | bc -l)

    if [ "$is_high_res" -eq 1 ]; then
        # 高倍率模式 (4624x2608)
        case "${factor}" in
            8.0|8)   echo "${ZOOM_8_0X_HIGH}" ;;
            12.0|12) echo "${ZOOM_12_0X_HIGH}" ;;
            16.0|16) echo "<2167,1213,289,163>" ;;
            20.0|20) echo "<2204,1240,231,130>" ;;
            24.0|24) echo "${ZOOM_24_0X_HIGH}" ;;
            *)       
                # 动态计算高分辨率zoom
                local w h x y
                w=$(echo "scale=0; $HIGH_RES_W / $factor" | bc)
                h=$(echo "scale=0; $HIGH_RES_H / $factor" | bc)
                x=$(echo "scale=0; ($HIGH_RES_W - $w) / 2" | bc)
                y=$(echo "scale=0; ($HIGH_RES_H - $h) / 2" | bc)
                echo "<${x},${y},${w},${h}>"
                ;;
        esac
    else
        # 低倍率模式 (4096x3072)
        case "${factor}" in
            1.0|1)   echo "${ZOOM_1_0X}" ;;
            2.0|2)   echo "${ZOOM_2_0X}" ;;
            3.0|3)   echo "<1024,768,1365,1024>" ;;
            4.0|4)   echo "${ZOOM_4_0X}" ;;
            5.0|5)   echo "<1638,1228,819,614>" ;;
            6.0|6)   echo "${ZOOM_6_0X}" ;;
            *)       
                # 动态计算低分辨率zoom
                local w h x y
                w=$(echo "scale=0; $LOW_RES_W / $factor" | bc)
                h=$(echo "scale=0; $LOW_RES_H / $factor" | bc)
                x=$(echo "scale=0; ($LOW_RES_W - $w) / 2" | bc)
                y=$(echo "scale=0; ($LOW_RES_H - $h) / 2" | bc)
                echo "<${x},${y},${w},${h}>"
                ;;
        esac
    fi
}

# 判断当前分辨率模式
get_sensor_mode() {
    local factor="$1"
    local is_high
    is_high=$(echo "$factor > $SAT_THRESHOLD" | bc -l)
    if [ "$is_high" -eq 1 ]; then
        echo "HIGH(4624x2608)"
    else
        echo "LOW(4096x3072)"
    fi
}

# 构建GStreamer管道
build_pipeline() {
    local zoom="$1"
    local mode="$2"
    local port="$3"

    local src="qtiqmmfsrc focus-mode=${FOCUS_MODE} camera=${CAMERA_ID} zoom=${zoom} name=camsrc"
    local caps="video/x-raw,format=NV12,width=${WIDTH},height=${HEIGHT},framerate=${FRAMERATE}/1"
    local encode="qtic2venc ! h264parse"

    case "$mode" in
        rtsp)
            echo "${src} ! ${caps} ! ${encode} ! rtspclientsink location=rtsp://0.0.0.0:${port}/live"
            ;;
        *)
            echo "${src} ! ${caps} ! ${encode} ! mp4mux ! queue ! filesink location=${OUTPUT_FILE}"
            ;;
    esac
}

# 固定zoom录制
start_recording() {
    local zoom_param="$1"
    local duration="$2"
    local output="$3"

    echo "========================================"
    echo "  固定Zoom录制"
    echo "========================================"
    echo "相机ID: ${CAMERA_ID} (逻辑相机/SAT)"
    echo "输出分辨率: ${WIDTH}x${HEIGHT}@${FRAMERATE}fps"
    echo "Zoom: ${zoom_param}"
    echo "SAT阈值: ${SAT_THRESHOLD}x"
    echo "输出文件: ${output}"
    if [ -n "${duration}" ]; then
        echo "录制时长: ${duration}秒"
    else
        echo "录制时长: 无限 (按Ctrl+C停止)"
    fi
    echo "========================================"

    if [ ! -d "$(dirname "${output}")" ]; then
        echo "错误: 输出目录不存在"
        exit 1
    fi

    local pipeline
    pipeline=$(build_pipeline "$zoom_param" "record")

    echo "执行命令:"
    echo "${pipeline}"
    echo ""

    if [ -n "${duration}" ]; then
        timeout "${duration}" bash -c "${pipeline}"
    else
        eval "${pipeline}"
    fi

    local ret=$?
    if [ $ret -eq 0 ] || [ $ret -eq 124 ]; then
        echo ""
        echo "录制完成: ${output}"
        if [ -f "${output}" ]; then
            ls -lh "${output}"
        fi
    else
        echo ""
        echo "录制失败"
        exit 1
    fi
}

# zoom连续变化录制 (sweep模式)
sweep_zoom() {
    local speed="$1"
    local output="$2"

    echo "========================================"
    echo "  Zoom连续变化录制 (1.0x -> 24.0x)"
    echo "========================================"
    echo "变化范围: 1.0x ~ 24.0x"
    echo "每档停留: ${speed}秒"
    echo "输出文件: ${output}"
    echo "========================================"

    if [ ! -d "$(dirname "${output}")" ]; then
        echo "错误: 输出目录不存在"
        exit 1
    fi

    # 定义zoom变化序列 (从1.0x到24.0x)
    local zoom_factors=(1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 5.5 6.0 6.3 6.5 7.0 7.5 8.0 9.0 10.0 11.0 12.0 14.0 16.0 18.0 20.0 22.0 24.0)

    # 分段录制,每段一个zoom值,最后合并
    local temp_dir
    temp_dir="/data/zoom_sweep_$$"
    mkdir -p "${temp_dir}"

    local segment_idx=0
    for factor in "${zoom_factors[@]}"; do
        local zoom_param sensor_mode segment_file
        zoom_param=$(get_zoom_by_factor "$factor")
        sensor_mode=$(get_sensor_mode "$factor")
        segment_file="${temp_dir}/seg_${segment_idx}.mp4"

        echo ""
        echo "[$(date '+%H:%M:%S')] Zoom: ${factor}x | Sensor: ${sensor_mode} | 录制${speed}秒"

        local pipeline
        pipeline="gst-launch-1.0 -e \
            qtiqmmfsrc focus-mode=${FOCUS_MODE} camera=${CAMERA_ID} zoom=${zoom_param} name=camsrc \
            ! video/x-raw,format=NV12,width=${WIDTH},height=${HEIGHT},framerate=${FRAMERATE}/1 \
            ! qtic2venc \
            ! h264parse \
            ! mp4mux \
            ! queue \
            ! filesink location=${segment_file}"

        timeout "${speed}" bash -c "${pipeline}" 2>/dev/null

        if [ -f "${segment_file}" ] && [ -s "${segment_file}" ]; then
            echo "  片段${segment_idx}完成: ${factor}x"
        else
            echo "  片段${segment_idx}失败或为空: ${factor}x"
        fi

        segment_idx=$((segment_idx + 1))
        sleep 0.5
    done

    # 合并所有片段
    echo ""
    echo "正在合并${segment_idx}个片段..."

    # 创建concat列表文件
    local concat_list
    concat_list="${temp_dir}/list.txt"
    true > "${concat_list}"
    for i in $(seq 0 $((segment_idx - 1))); do
        local seg
        seg="${temp_dir}/seg_${i}.mp4"
        if [ -f "$seg" ] && [ -s "$seg" ]; then
            echo "file '${seg}'" >> "${concat_list}"
        fi
    done

    if [ -s "${concat_list}" ]; then
        if ffmpeg -f concat -safe 0 -i "${concat_list}" -c copy "${output}" -y 2>/dev/null; then
            if [ -f "${output}" ]; then
                echo "合并完成: ${output}"
                ls -lh "${output}"
            fi
        else
            echo "合并失败,保留分段文件在: ${temp_dir}"
        fi
    else
        echo "没有有效的片段文件"
    fi

    # 清理临时文件
    rm -rf "${temp_dir}"
    echo "Zoom连续变化录制完成"
}

# 自动档位遍历录制 (step模式)
step_zoom() {
    local delay="$1"
    local output="$2"

    echo "========================================"
    echo "  Zoom档位遍历录制"
    echo "========================================"
    echo "遍历档位: 1.0x, 2.0x, 4.0x, 6.0x, 6.3x, 8.0x, 12.0x, 16.0x, 24.0x"
    echo "每档停留: ${delay}秒"
    echo "输出文件: ${output}"
    echo "========================================"

    if [ ! -d "$(dirname "${output}")" ]; then
        echo "错误: 输出目录不存在"
        exit 1
    fi

    local zoom_factors=(1.0 2.0 4.0 6.0 6.3 8.0 12.0 16.0 24.0)
    local temp_dir
    temp_dir="/data/zoom_step_$$"
    mkdir -p "${temp_dir}"

    local segment_idx=0
    for factor in "${zoom_factors[@]}"; do
        local zoom_param sensor_mode segment_file
        zoom_param=$(get_zoom_by_factor "$factor")
        sensor_mode=$(get_sensor_mode "$factor")
        segment_file="${temp_dir}/seg_${segment_idx}.mp4"

        echo ""
        echo "[$(date '+%H:%M:%S')] 档位 ${factor}x | Sensor: ${sensor_mode} | 录制${delay}秒"

        local pipeline
        pipeline="gst-launch-1.0 -e \
            qtiqmmfsrc focus-mode=${FOCUS_MODE} camera=${CAMERA_ID} zoom=${zoom_param} name=camsrc \
            ! video/x-raw,format=NV12,width=${WIDTH},height=${HEIGHT},framerate=${FRAMERATE}/1 \
            ! qtic2venc \
            ! h264parse \
            ! mp4mux \
            ! queue \
            ! filesink location=${segment_file}"

        timeout "${delay}" bash -c "${pipeline}" 2>/dev/null

        if [ -f "${segment_file}" ] && [ -s "${segment_file}" ]; then
            echo "  片段完成"
        fi

        segment_idx=$((segment_idx + 1))
        sleep 0.5
    done

    # 合并片段
    echo ""
    echo "正在合并${segment_idx}个片段..."

    local concat_list
    concat_list="${temp_dir}/list.txt"
    true > "${concat_list}"
    for i in $(seq 0 $((segment_idx - 1))); do
        local seg
        seg="${temp_dir}/seg_${i}.mp4"
        if [ -f "$seg" ] && [ -s "$seg" ]; then
            echo "file '${seg}'" >> "${concat_list}"
        fi
    done

    if [ -s "${concat_list}" ]; then
        if ffmpeg -f concat -safe 0 -i "${concat_list}" -c copy "${output}" -y 2>/dev/null; then
            if [ -f "${output}" ]; then
                echo "合并完成: ${output}"
                ls -lh "${output}"
            fi
        else
            echo "合并失败,保留分段文件在: ${temp_dir}"
        fi
    fi

    rm -rf "${temp_dir}"
    echo "Zoom档位遍历录制完成"
}

# RTSP实时观察
rtsp_preview() {
    local zoom_param="$1"
    local port="$2"

    echo "========================================"
    echo "  RTSP实时观察"
    echo "========================================"
    echo "Zoom: ${zoom_param}"
    echo "RTSP端口: ${port}"
    echo "========================================"

    local ip pipeline
    ip=$(hostname -I | awk '{print $1}')
    pipeline=$(build_pipeline "$zoom_param" "rtsp" "$port")

    echo "RTSP地址: rtsp://${ip}:${port}/live"
    echo "VLC播放: vlc rtsp://${ip}:${port}/live"
    echo "ffplay:  ffplay rtsp://${ip}:${port}/live"
    echo ""
    echo "按Ctrl+C停止"
    echo "========================================"

    eval "${pipeline}"
}

# ==================== 主程序 ====================

# 默认值
MODE="record"
ZOOM_PARAM="${DEFAULT_ZOOM}"
DURATION=""
SWEEP_SPEED=3
STEP_DELAY=5
RTSP_PORT=8554

# 解析模式参数
if [ $# -gt 0 ] && [[ ! "$1" =~ ^- ]]; then
    MODE="$1"
    shift
fi

while [[ $# -gt 0 ]]; do
    case "$1" in
        -z|--zoom)
            ZOOM_PARAM=$(get_zoom_by_factor "$2")
            shift 2
            ;;
        -s|--speed)
            SWEEP_SPEED="$2"
            shift 2
            ;;
        -d|--delay)
            STEP_DELAY="$2"
            shift 2
            ;;
        -o|--output)
            OUTPUT_FILE="$2"
            shift 2
            ;;
        -t|--time)
            DURATION="$2"
            shift 2
            ;;
        -p|--port)
            RTSP_PORT="$2"
            shift 2
            ;;
        -h|--help)
            show_help
            exit 0
            ;;
        *)
            echo "未知选项: $1"
            show_help
            exit 1
            ;;
    esac
done

# 检查bc命令
if ! command -v bc &> /dev/null; then
    echo "错误: 需要安装bc (基本计算器)"
    echo "安装: apt-get install bc 或 opkg install bc"
    exit 1
fi

# 检查ffmpeg (sweep/step模式需要)
if [ "$MODE" == "sweep" ] || [ "$MODE" == "step" ]; then
    if ! command -v ffmpeg &> /dev/null; then
        echo "警告: 未找到ffmpeg,合并功能将不可用"
        echo "建议安装: apt-get install ffmpeg"
    fi
fi

# 执行对应模式
case "$MODE" in
    record)
        start_recording "${ZOOM_PARAM}" "${DURATION}" "${OUTPUT_FILE}"
        ;;
    sweep)
        sweep_zoom "${SWEEP_SPEED}" "${OUTPUT_FILE}"
        ;;
    step)
        step_zoom "${STEP_DELAY}" "${OUTPUT_FILE}"
        ;;
    rtsp)
        rtsp_preview "${ZOOM_PARAM}" "${RTSP_PORT}"
        ;;
    *)
        echo "未知模式: $MODE"
        show_help
        exit 1
        ;;
esac

  通过shellcheck可以轻松检查出脚本问题,将执行结果告诉kimi后,让kimi重新生成脚本,这样可以快速解决shell脚本中的语法错误,检查结果如下:

txt 复制代码
In test_logicalcamera.sh line 78:
    local is_high_res=$(echo "$factor > $SAT_THRESHOLD" | bc -l)
          ^---------^ SC2155: Declare and assign separately to avoid masking return values.


In test_logicalcamera.sh line 90:
                local w=$(echo "scale=0; $HIGH_RES_W / $factor" | bc)
                      ^-- SC2155: Declare and assign separately to avoid masking return values.


In test_logicalcamera.sh line 91:
                local h=$(echo "scale=0; $HIGH_RES_H / $factor" | bc)
                      ^-- SC2155: Declare and assign separately to avoid masking return values.


In test_logicalcamera.sh line 92:
                local x=$(echo "scale=0; ($HIGH_RES_W - $w) / 2" | bc)
                      ^-- SC2155: Declare and assign separately to avoid masking return values.


In test_logicalcamera.sh line 93:
                local y=$(echo "scale=0; ($HIGH_RES_H - $h) / 2" | bc)
                      ^-- SC2155: Declare and assign separately to avoid masking return values.


In test_logicalcamera.sh line 108:
                local w=$(echo "scale=0; $LOW_RES_W / $factor" | bc)
                      ^-- SC2155: Declare and assign separately to avoid masking return values.


In test_logicalcamera.sh line 109:
                local h=$(echo "scale=0; $LOW_RES_H / $factor" | bc)
                      ^-- SC2155: Declare and assign separately to avoid masking return values.


In test_logicalcamera.sh line 110:
                local x=$(echo "scale=0; ($LOW_RES_W - $w) / 2" | bc)
                      ^-- SC2155: Declare and assign separately to avoid masking return values.


In test_logicalcamera.sh line 111:
                local y=$(echo "scale=0; ($LOW_RES_H - $h) / 2" | bc)
                      ^-- SC2155: Declare and assign separately to avoid masking return values.


In test_logicalcamera.sh line 121:
    local is_high=$(echo "$factor > $SAT_THRESHOLD" | bc -l)
          ^-----^ SC2155: Declare and assign separately to avoid masking return values.


In test_logicalcamera.sh line 170:
    if [ ! -d "$(dirname ${output})" ]; then
                         ^-------^ SC2086: Double quote to prevent globbing and word splitting.

Did you mean: 
    if [ ! -d "$(dirname "${output}")" ]; then


In test_logicalcamera.sh line 175:
    local pipeline=$(build_pipeline "$zoom_param" "record")
          ^------^ SC2155: Declare and assign separately to avoid masking return values.


In test_logicalcamera.sh line 182:
        timeout ${duration} bash -c "${pipeline}"
                ^---------^ SC2086: Double quote to prevent globbing and word splitting.

Did you mean: 
        timeout "${duration}" bash -c "${pipeline}"


In test_logicalcamera.sh line 187:
    if [ $? -eq 0 ] || [ $? -eq 124 ]; then
         ^-- SC2181: Check exit code directly with e.g. 'if mycmd;', not indirectly with $?.


In test_logicalcamera.sh line 190:
        [ -f "${output}" ] && ls -lh ${output}
                                     ^-------^ SC2086: Double quote to prevent globbing and word splitting.

Did you mean: 
        [ -f "${output}" ] && ls -lh "${output}"


In test_logicalcamera.sh line 212:
    if [ ! -d "$(dirname ${output})" ]; then
                         ^-------^ SC2086: Double quote to prevent globbing and word splitting.

Did you mean: 
    if [ ! -d "$(dirname "${output}")" ]; then


In test_logicalcamera.sh line 226:
        local zoom_param=$(get_zoom_by_factor "$factor")
              ^--------^ SC2155: Declare and assign separately to avoid masking return values.


In test_logicalcamera.sh line 227:
        local sensor_mode=$(get_sensor_mode "$factor")
              ^---------^ SC2155: Declare and assign separately to avoid masking return values.


In test_logicalcamera.sh line 242:
        timeout ${speed} bash -c "${pipeline}" 2>/dev/null
                ^------^ SC2086: Double quote to prevent globbing and word splitting.

Did you mean: 
        timeout "${speed}" bash -c "${pipeline}" 2>/dev/null


In test_logicalcamera.sh line 260:
    > ${concat_list}
    ^--------------^ SC2188: This redirection doesn't have a command. Move to its command (or use 'true' as no-op).


In test_logicalcamera.sh line 269:
        ffmpeg -f concat -safe 0 -i ${concat_list} -c copy ${output} -y 2>/dev/null
                                                           ^-------^ SC2086: Double quote to prevent globbing and word splitting.

Did you mean: 
        ffmpeg -f concat -safe 0 -i ${concat_list} -c copy "${output}" -y 2>/dev/null


In test_logicalcamera.sh line 270:
        if [ $? -eq 0 ] && [ -f "${output}" ]; then
             ^-- SC2181: Check exit code directly with e.g. 'if mycmd;', not indirectly with $?.


In test_logicalcamera.sh line 272:
            ls -lh ${output}
                   ^-------^ SC2086: Double quote to prevent globbing and word splitting.

Did you mean: 
            ls -lh "${output}"


In test_logicalcamera.sh line 298:
    if [ ! -d "$(dirname ${output})" ]; then
                         ^-------^ SC2086: Double quote to prevent globbing and word splitting.

Did you mean: 
    if [ ! -d "$(dirname "${output}")" ]; then


In test_logicalcamera.sh line 309:
        local zoom_param=$(get_zoom_by_factor "$factor")
              ^--------^ SC2155: Declare and assign separately to avoid masking return values.


In test_logicalcamera.sh line 310:
        local sensor_mode=$(get_sensor_mode "$factor")
              ^---------^ SC2155: Declare and assign separately to avoid masking return values.


In test_logicalcamera.sh line 325:
        timeout ${delay} bash -c "${pipeline}" 2>/dev/null
                ^------^ SC2086: Double quote to prevent globbing and word splitting.

Did you mean: 
        timeout "${delay}" bash -c "${pipeline}" 2>/dev/null


In test_logicalcamera.sh line 340:
    > ${concat_list}
    ^--------------^ SC2188: This redirection doesn't have a command. Move to its command (or use 'true' as no-op).


In test_logicalcamera.sh line 349:
        ffmpeg -f concat -safe 0 -i ${concat_list} -c copy ${output} -y 2>/dev/null
                                                           ^-------^ SC2086: Double quote to prevent globbing and word splitting.

Did you mean: 
        ffmpeg -f concat -safe 0 -i ${concat_list} -c copy "${output}" -y 2>/dev/null


In test_logicalcamera.sh line 350:
        if [ $? -eq 0 ] && [ -f "${output}" ]; then
             ^-- SC2181: Check exit code directly with e.g. 'if mycmd;', not indirectly with $?.


In test_logicalcamera.sh line 352:
            ls -lh ${output}
                   ^-------^ SC2086: Double quote to prevent globbing and word splitting.

Did you mean: 
            ls -lh "${output}"


In test_logicalcamera.sh line 374:
    local ip=$(hostname -I | awk '{print $1}')
          ^-- SC2155: Declare and assign separately to avoid masking return values.


In test_logicalcamera.sh line 375:
    local pipeline=$(build_pipeline "$zoom_param" "rtsp" "$port")
          ^------^ SC2155: Declare and assign separately to avoid masking return values.

  kimi重新修改后输出结果如下:

sh 复制代码
#!/bin/bash
# 多相机变焦录制脚本 - QCS8550 平台 (支持SAT分辨率切换)
# 超过6.3x时sensor分辨率变为4624x2608
# 支持: 固定zoom录制 / 连续zoom变化录制 / 自动档位遍历 / RTSP实时观察

# ==================== 配置参数 ====================
CAMERA_ID=3
FOCUS_MODE=3
WIDTH=1920
HEIGHT=1080
FRAMERATE=30
OUTPUT_FILE="/data/test3.mp4"

# SAT切换阈值
SAT_THRESHOLD=6.9

# sensor-ov50h40分辨率 (Wide)
LOW_RES_W=4096
LOW_RES_H=3072

# sensor-imx682分辨率 (Tele/Periscope)
HIGH_RES_W=4624
HIGH_RES_H=2608

# 预定义zoom档位 (基于对应sensor分辨率计算)
# 1.0x~6.3x: 基于4096x3072
ZOOM_1_0X="<0,0,4096,3072>"
ZOOM_2_0X="<1024,768,2048,1536>"
ZOOM_4_0X="<1536,1152,1024,768>"
ZOOM_6_0X="<1706,1280,682,512>"

# 6.3x~24.0x: 基于4624x2608 (中心点2312,1304)
ZOOM_8_0X_HIGH="<2024,1139,578,326>"
ZOOM_12_0X_HIGH="<2119,1188,385,217>"
ZOOM_24_0X_HIGH="<2215,1235,193,109>"

# 默认zoom (1.0x)
DEFAULT_ZOOM="${ZOOM_1_0X}"

# ==================== 函数定义 ====================

show_help() {
    echo "========================================"
    echo "  QMMF SAT变焦录制脚本"
    echo "========================================"
    echo "用法: $0 [模式] [选项]"
    echo ""
    echo "模式:"
    echo "  record     固定zoom录制 (默认)"
    echo "  sweep      zoom从1.0x连续变化到24.0x录制"
    echo "  step       自动遍历zoom档位录制"
    echo "  rtsp       RTSP实时观察"
    echo ""
    echo "选项:"
    echo "  -z, --zoom <倍数>     设置zoom倍数 (record模式, 默认1.0)"
    echo "  -s, --speed <秒>      sweep模式每档停留时间 (默认3)"
    echo "  -d, --delay <秒>      step模式每档停留时间 (默认5)"
    echo "  -o, --output <路径>    输出文件路径"
    echo "  -t, --time <秒>       录制总时长 (record模式)"
    echo "  -p, --port <端口>     RTSP端口 (默认8554)"
    echo "  -h, --help            显示帮助"
    echo ""
    echo "示例:"
    echo "  $0 record -z 8.0 -t 60         # 固定8倍录制60秒"
    echo "  $0 sweep -s 2 -o /data/zoom.mp4 # 连续zoom变化录制"
    echo "  $0 step -d 5                   # 遍历档位每档5秒"
    echo "  $0 rtsp -z 4.0                 # RTSP实时观察4倍变焦"
    echo ""
    echo "注意: >6.3x时sensor自动切换到4624x2608高分辨率"
    echo "========================================"
}

# 根据倍数获取zoom参数 (自动判断SAT切换)
get_zoom_by_factor() {
    local factor="$1"
    local is_high_res
    is_high_res=$(echo "$factor > $SAT_THRESHOLD" | bc -l)

    if [ "$is_high_res" -eq 1 ]; then
        # 高倍率模式 (4624x2608)
        case "${factor}" in
            8.0|8)   echo "${ZOOM_8_0X_HIGH}" ;;
            12.0|12) echo "${ZOOM_12_0X_HIGH}" ;;
            16.0|16) echo "<2167,1213,289,163>" ;;
            20.0|20) echo "<2204,1240,231,130>" ;;
            24.0|24) echo "${ZOOM_24_0X_HIGH}" ;;
            *)       
                # 动态计算高分辨率zoom
                local w h x y
                w=$(echo "scale=0; $HIGH_RES_W / $factor" | bc)
                h=$(echo "scale=0; $HIGH_RES_H / $factor" | bc)
                x=$(echo "scale=0; ($HIGH_RES_W - $w) / 2" | bc)
                y=$(echo "scale=0; ($HIGH_RES_H - $h) / 2" | bc)
                echo "<${x},${y},${w},${h}>"
                ;;
        esac
    else
        # 低倍率模式 (4096x3072)
        case "${factor}" in
            1.0|1)   echo "${ZOOM_1_0X}" ;;
            2.0|2)   echo "${ZOOM_2_0X}" ;;
            3.0|3)   echo "<1024,768,1365,1024>" ;;
            4.0|4)   echo "${ZOOM_4_0X}" ;;
            5.0|5)   echo "<1638,1228,819,614>" ;;
            6.0|6)   echo "${ZOOM_6_0X}" ;;
            *)       
                # 动态计算低分辨率zoom
                local w h x y
                w=$(echo "scale=0; $LOW_RES_W / $factor" | bc)
                h=$(echo "scale=0; $LOW_RES_H / $factor" | bc)
                x=$(echo "scale=0; ($LOW_RES_W - $w) / 2" | bc)
                y=$(echo "scale=0; ($LOW_RES_H - $h) / 2" | bc)
                echo "<${x},${y},${w},${h}>"
                ;;
        esac
    fi
}

# 判断当前分辨率模式
get_sensor_mode() {
    local factor="$1"
    local is_high
    is_high=$(echo "$factor > $SAT_THRESHOLD" | bc -l)
    if [ "$is_high" -eq 1 ]; then
        echo "HIGH(4624x2608)"
    else
        echo "LOW(4096x3072)"
    fi
}

# 构建GStreamer管道
build_pipeline() {
    local zoom="$1"
    local mode="$2"
    local port="$3"

    local src="qtiqmmfsrc focus-mode=${FOCUS_MODE} camera=${CAMERA_ID} zoom=${zoom} name=camsrc"
    local caps="video/x-raw,format=NV12,width=${WIDTH},height=${HEIGHT},framerate=${FRAMERATE}/1"
    local encode="qtic2venc ! h264parse"

    case "$mode" in
        rtsp)
            echo "${src} ! ${caps} ! ${encode} ! rtspclientsink location=rtsp://0.0.0.0:${port}/live"
            ;;
        *)
            echo "${src} ! ${caps} ! ${encode} ! mp4mux ! queue ! filesink location=${OUTPUT_FILE}"
            ;;
    esac
}

# 固定zoom录制
start_recording() {
    local zoom_param="$1"
    local duration="$2"
    local output="$3"

    echo "========================================"
    echo "  固定Zoom录制"
    echo "========================================"
    echo "相机ID: ${CAMERA_ID} (逻辑相机/SAT)"
    echo "输出分辨率: ${WIDTH}x${HEIGHT}@${FRAMERATE}fps"
    echo "Zoom: ${zoom_param}"
    echo "SAT阈值: ${SAT_THRESHOLD}x"
    echo "输出文件: ${output}"
    if [ -n "${duration}" ]; then
        echo "录制时长: ${duration}秒"
    else
        echo "录制时长: 无限 (按Ctrl+C停止)"
    fi
    echo "========================================"

    if [ ! -d "$(dirname "${output}")" ]; then
        echo "错误: 输出目录不存在"
        exit 1
    fi

    local pipeline
    pipeline=$(build_pipeline "$zoom_param" "record")

    echo "执行命令:"
    echo "${pipeline}"
    echo ""

    if [ -n "${duration}" ]; then
        timeout "${duration}" bash -c "${pipeline}"
    else
        eval "${pipeline}"
    fi

    local ret=$?
    if [ $ret -eq 0 ] || [ $ret -eq 124 ]; then
        echo ""
        echo "录制完成: ${output}"
        if [ -f "${output}" ]; then
            ls -lh "${output}"
        fi
    else
        echo ""
        echo "录制失败"
        exit 1
    fi
}

# zoom连续变化录制 (sweep模式)
sweep_zoom() {
    local speed="$1"
    local output="$2"

    echo "========================================"
    echo "  Zoom连续变化录制 (1.0x -> 24.0x)"
    echo "========================================"
    echo "变化范围: 1.0x ~ 24.0x"
    echo "每档停留: ${speed}秒"
    echo "输出文件: ${output}"
    echo "========================================"

    if [ ! -d "$(dirname "${output}")" ]; then
        echo "错误: 输出目录不存在"
        exit 1
    fi

    # 定义zoom变化序列 (从1.0x到24.0x)
    local zoom_factors=(1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 5.5 6.0 6.3 6.5 7.0 7.5 8.0 9.0 10.0 11.0 12.0 14.0 16.0 18.0 20.0 22.0 24.0)

    # 分段录制,每段一个zoom值,最后合并
    local temp_dir
    temp_dir="/data/zoom_sweep_$$"
    mkdir -p "${temp_dir}"

    local segment_idx=0
    for factor in "${zoom_factors[@]}"; do
        local zoom_param sensor_mode segment_file
        zoom_param=$(get_zoom_by_factor "$factor")
        sensor_mode=$(get_sensor_mode "$factor")
        segment_file="${temp_dir}/seg_${segment_idx}.mp4"

        echo ""
        echo "[$(date '+%H:%M:%S')] Zoom: ${factor}x | Sensor: ${sensor_mode} | 录制${speed}秒"

        local pipeline
        pipeline="gst-launch-1.0 -e \
            qtiqmmfsrc focus-mode=${FOCUS_MODE} camera=${CAMERA_ID} zoom=${zoom_param} name=camsrc \
            ! video/x-raw,format=NV12,width=${WIDTH},height=${HEIGHT},framerate=${FRAMERATE}/1 \
            ! qtic2venc \
            ! h264parse \
            ! mp4mux \
            ! queue \
            ! filesink location=${segment_file}"

        timeout "${speed}" bash -c "${pipeline}" 2>/dev/null

        if [ -f "${segment_file}" ] && [ -s "${segment_file}" ]; then
            echo "  片段${segment_idx}完成: ${factor}x"
        else
            echo "  片段${segment_idx}失败或为空: ${factor}x"
        fi

        segment_idx=$((segment_idx + 1))
        sleep 0.5
    done

    # 合并所有片段
    echo ""
    echo "正在合并${segment_idx}个片段..."

    # 创建concat列表文件
    local concat_list
    concat_list="${temp_dir}/list.txt"
    true > "${concat_list}"
    for i in $(seq 0 $((segment_idx - 1))); do
        local seg
        seg="${temp_dir}/seg_${i}.mp4"
        if [ -f "$seg" ] && [ -s "$seg" ]; then
            echo "file '${seg}'" >> "${concat_list}"
        fi
    done

    if [ -s "${concat_list}" ]; then
        if ffmpeg -f concat -safe 0 -i "${concat_list}" -c copy "${output}" -y 2>/dev/null; then
            if [ -f "${output}" ]; then
                echo "合并完成: ${output}"
                ls -lh "${output}"
            fi
        else
            echo "合并失败,保留分段文件在: ${temp_dir}"
        fi
    else
        echo "没有有效的片段文件"
    fi

    # 清理临时文件
    rm -rf "${temp_dir}"
    echo "Zoom连续变化录制完成"
}

# 自动档位遍历录制 (step模式)
step_zoom() {
    local delay="$1"
    local output="$2"

    echo "========================================"
    echo "  Zoom档位遍历录制"
    echo "========================================"
    echo "遍历档位: 1.0x, 2.0x, 4.0x, 6.0x, 6.3x, 8.0x, 12.0x, 16.0x, 24.0x"
    echo "每档停留: ${delay}秒"
    echo "输出文件: ${output}"
    echo "========================================"

    if [ ! -d "$(dirname "${output}")" ]; then
        echo "错误: 输出目录不存在"
        exit 1
    fi

    local zoom_factors=(1.0 2.0 4.0 6.0 6.3 8.0 12.0 16.0 24.0)
    local temp_dir
    temp_dir="/data/zoom_step_$$"
    mkdir -p "${temp_dir}"

    local segment_idx=0
    for factor in "${zoom_factors[@]}"; do
        local zoom_param sensor_mode segment_file
        zoom_param=$(get_zoom_by_factor "$factor")
        sensor_mode=$(get_sensor_mode "$factor")
        segment_file="${temp_dir}/seg_${segment_idx}.mp4"

        echo ""
        echo "[$(date '+%H:%M:%S')] 档位 ${factor}x | Sensor: ${sensor_mode} | 录制${delay}秒"

        local pipeline
        pipeline="gst-launch-1.0 -e \
            qtiqmmfsrc focus-mode=${FOCUS_MODE} camera=${CAMERA_ID} zoom=${zoom_param} name=camsrc \
            ! video/x-raw,format=NV12,width=${WIDTH},height=${HEIGHT},framerate=${FRAMERATE}/1 \
            ! qtic2venc \
            ! h264parse \
            ! mp4mux \
            ! queue \
            ! filesink location=${segment_file}"

        timeout "${delay}" bash -c "${pipeline}" 2>/dev/null

        if [ -f "${segment_file}" ] && [ -s "${segment_file}" ]; then
            echo "  片段完成"
        fi

        segment_idx=$((segment_idx + 1))
        sleep 0.5
    done

    # 合并片段
    echo ""
    echo "正在合并${segment_idx}个片段..."

    local concat_list
    concat_list="${temp_dir}/list.txt"
    true > "${concat_list}"
    for i in $(seq 0 $((segment_idx - 1))); do
        local seg
        seg="${temp_dir}/seg_${i}.mp4"
        if [ -f "$seg" ] && [ -s "$seg" ]; then
            echo "file '${seg}'" >> "${concat_list}"
        fi
    done

    if [ -s "${concat_list}" ]; then
        if ffmpeg -f concat -safe 0 -i "${concat_list}" -c copy "${output}" -y 2>/dev/null; then
            if [ -f "${output}" ]; then
                echo "合并完成: ${output}"
                ls -lh "${output}"
            fi
        else
            echo "合并失败,保留分段文件在: ${temp_dir}"
        fi
    fi

    rm -rf "${temp_dir}"
    echo "Zoom档位遍历录制完成"
}

# RTSP实时观察
rtsp_preview() {
    local zoom_param="$1"
    local port="$2"

    echo "========================================"
    echo "  RTSP实时观察"
    echo "========================================"
    echo "Zoom: ${zoom_param}"
    echo "RTSP端口: ${port}"
    echo "========================================"

    local ip pipeline
    ip=$(hostname -I | awk '{print $1}')
    pipeline=$(build_pipeline "$zoom_param" "rtsp" "$port")

    echo "RTSP地址: rtsp://${ip}:${port}/live"
    echo "VLC播放: vlc rtsp://${ip}:${port}/live"
    echo "ffplay:  ffplay rtsp://${ip}:${port}/live"
    echo ""
    echo "按Ctrl+C停止"
    echo "========================================"

    eval "${pipeline}"
}

# ==================== 主程序 ====================

# 默认值
MODE="record"
ZOOM_PARAM="${DEFAULT_ZOOM}"
DURATION=""
SWEEP_SPEED=3
STEP_DELAY=5
RTSP_PORT=8554

# 解析模式参数
if [ $# -gt 0 ] && [[ ! "$1" =~ ^- ]]; then
    MODE="$1"
    shift
fi

while [[ $# -gt 0 ]]; do
    case "$1" in
        -z|--zoom)
            ZOOM_PARAM=$(get_zoom_by_factor "$2")
            shift 2
            ;;
        -s|--speed)
            SWEEP_SPEED="$2"
            shift 2
            ;;
        -d|--delay)
            STEP_DELAY="$2"
            shift 2
            ;;
        -o|--output)
            OUTPUT_FILE="$2"
            shift 2
            ;;
        -t|--time)
            DURATION="$2"
            shift 2
            ;;
        -p|--port)
            RTSP_PORT="$2"
            shift 2
            ;;
        -h|--help)
            show_help
            exit 0
            ;;
        *)
            echo "未知选项: $1"
            show_help
            exit 1
            ;;
    esac
done

# 检查bc命令
if ! command -v bc &> /dev/null; then
    echo "错误: 需要安装bc (基本计算器)"
    echo "安装: apt-get install bc 或 opkg install bc"
    exit 1
fi

# 检查ffmpeg (sweep/step模式需要)
if [ "$MODE" == "sweep" ] || [ "$MODE" == "step" ]; then
    if ! command -v ffmpeg &> /dev/null; then
        echo "警告: 未找到ffmpeg,合并功能将不可用"
        echo "建议安装: apt-get install ffmpeg"
    fi
fi

# 执行对应模式
case "$MODE" in
    record)
        start_recording "${ZOOM_PARAM}" "${DURATION}" "${OUTPUT_FILE}"
        ;;
    sweep)
        sweep_zoom "${SWEEP_SPEED}" "${OUTPUT_FILE}"
        ;;
    step)
        step_zoom "${STEP_DELAY}" "${OUTPUT_FILE}"
        ;;
    rtsp)
        rtsp_preview "${ZOOM_PARAM}" "${RTSP_PORT}"
        ;;
    *)
        echo "未知模式: $MODE"
        show_help
        exit 1
        ;;
esac
相关推荐
丘山望岳8 小时前
藤萝垂序——二叉搜索树
开发语言·数据结构·c++
惊鸿一博8 小时前
统计_滚动标准差:局部波动性衡量
开发语言·python
这个DBA有点耶9 小时前
数据库管理工具+开发工具的融合:AI如何重塑DBA工作流?
开发语言·数据库·人工智能·sql·云计算·dba
lynnlovemin9 小时前
【信息学竞赛专题】滑动窗口(尺取法)超全详解|C++模板+经典例题+避坑指南
开发语言·c++·算法·滑动窗口·信息学竞赛
wjs20249 小时前
JavaScript 类型转换
开发语言
似水এ᭄往昔9 小时前
【Qt】--Qt概述
开发语言·c++·qt
星秀日9 小时前
rust学习入门
开发语言·学习·rust
2501_916007479 小时前
iOS应用性能优化全面指南:从内存管理到工具使用
android·ios·性能优化·小程序·uni-app·iphone·webview
星越华夏9 小时前
python办公自动化,csv文件/excel文件差集合并
开发语言·python·excel