VariaReEncoder / garmin_varia_transcode.sh
1 contributor
2118 lines | 68.296kb
#!/usr/bin/env bash
set -Eeuo pipefail

# Garmin Varia batch transcoder (macOS/Linux)
#
# Apple compatibility note:
# For HEVC outputs we explicitly set -tag:v hvc1 (not hev1), which is required
# for reliable QuickTime / Photos compatibility.
#
# Encoding strategy note:
# Do not derive output bitrate mechanically from fixed Garmin source sizes.
# Prefer CRF (software) or constrained VBR (hardware), as used below.

TOOL_NAME="ffmpeg"

DEFAULT_SOURCE="."
DEFAULT_MODE="hardware"
DEFAULT_EXTENSIONS="mp4,mov,avi,m4v"
DEFAULT_CRF_HEVC=20
DEFAULT_CRF_H264=19
DEFAULT_STAGING_RAMDISK_MB=8192
DEFAULT_PREFLIGHT_TIMEOUT_SEC=10

# Garmin publicly declares a 140-degree field of view.
# We infer practical focal metadata from this single spec.
# 35mm-equivalent focal length is integer-only in EXIF, so we round to 7 mm.
GARMIN_DECLARED_FOV_DEG=140
GARMIN_INFERRED_FOCAL_MM="6.5"
GARMIN_INFERRED_FOCAL_35MM_MM="7"

SOURCE_DIR="$DEFAULT_SOURCE"
DEST_DIR="$DEFAULT_SOURCE"
STAGING_DIR=""
MODE="$DEFAULT_MODE"
EXTENSIONS_CSV="$DEFAULT_EXTENSIONS"
CRF_OVERRIDE=""
OVERWRITE=true
DRY_RUN=false
RECURSIVE=true
SINGLE_FILE=""
VERBOSE=false
MOVE_SOURCE=false
UNATTENDED=false
SOURCE_PROVIDED=false
DEST_PROVIDED=false
STAGING_PROVIDED=false
STAGE_INPUT=false
STAGING_RAMDISK_MB="$DEFAULT_STAGING_RAMDISK_MB"
AUTO_CREATED_STAGING_RAMDISK=false
AUTO_CREATED_STAGING_PATH=""
AUTO_CREATED_STAGING_DEV=""
STAGING_RAMDISK_CREATED_AT=0
STAGING_RAMDISK_CREATE_ERROR=""
AUTO_STAGING_CLEANED_UP=false
RUN_STARTED_AT=0
FIRST_ENCODE_STARTED_AT=0
DEBUG_TIMING_LIMIT=0
DEBUG_TIMING_FILES=0
DEBUG_TIMING_STOPPED=false
FAIL_FAST=true
SOURCE_READABLE_MODE="normal"
APPLE_REPACK_FALLBACK=true
REPACKED_SOURCE_PATH=""
STAGED_INPUT_PATH=""
STAGED_INPUT_FAILURE_RC=0

HAS_VIDEOTOOLBOX=false
HAS_LIBX265=false
HAS_LIBX264=false
HAS_AVCONVERT=false

ENCODER_KIND=""
VIDEO_CODEC=""
VIDEO_CRF=""
VIDEO_ARGS=()
FIND_EXT_EXPR=()
EXT_LIST=()
VIDEO_FILES=()

VIDEOS_PROCESSED=0
VIDEOS_SKIPPED=0
ERRORS=0
INVALID_SOURCES_SKIPPED=0
DESTINATION_FAILURES=0
TOTAL_VIDEO_TIME_SEC=0
TOTAL_FILE_REAL_TIME_SEC=0
INPUT_BYTES_PROCESSED=0
OUTPUT_BYTES_PROCESSED=0

DURATION_TOLERANCE_SEC=1.0
PREFLIGHT_TIMEOUT_SEC="$DEFAULT_PREFLIGHT_TIMEOUT_SEC"
STOP_AFTER_CURRENT=false
INTERRUPT_COUNT=0
CURRENT_FFMPEG_PID=""
PROGRESS_LINE_OPEN=false
FINAL_REPORT_PRINTED=false
MAIN_FLOW_STARTED=false

usage() {
  cat <<'EOF'
Usage:
  garmin_varia_transcode.sh [options]

Options:
  -s, --source, --input DIR       Source directory or single file (default: current directory)
  -d, --destination, --output DIR Destination directory (default: current directory)
  --staging-dir DIR               Temporary staging directory for intermediate output files
                                  (falls back to destination if staging cannot be used)
  --stage-input                   Copy each source video to the staging directory before
                                  encoding; useful for flaky NAS/autofs source reads
  --staging-ramdisk-mb N          RAM disk size in MB when auto-creating missing /Volumes staging
                                  directory on macOS (default: 8192)
  --debug-timing N                Process at most N video files, then stop and print timing stats
  --mode MODE                     Encoding mode (default: hardware); see Encoding Modes below
  --crf N                         CRF value for quality/compat modes (lower = better; default: 20/19)
  --no-overwrite                  Skip files that already exist at destination (default: overwrite)
  --dry-run                       Print actions without writing files
  --no-recursive                  Process only the top-level source directory (default: recursive)
  --extensions LIST               Comma-separated video extensions (default: mp4,mov,avi,m4v)
  --verbose                       Log each operation with timestamp; show ffmpeg/ffprobe output
  --delete-source                 Delete source file only after strict post-encode validation
  --keep-going                    Continue after source-file failures (default: stop)
  --unattended                    Preset for long runs: --delete-source + --keep-going
  --no-apple-repack-fallback      Disable macOS avconvert fallback for unreadable MP4/MOV sources
  --apple-repack-fallback         Enable macOS avconvert fallback (default)
  -h, --help                      Show this help

Run Controls:
  q / Q      Request graceful stop before the next file (interactive runs only)
  Ctrl+C     Request stop after the current encode finishes
  Ctrl+C x2  Force-stop the current encode

  Note: --unattended disables q/Q terminal polling so long runs cannot be
  stopped accidentally by buffered terminal input. Use Ctrl+C for unattended.

Encoding Modes:
  hardware   Uses hevc_videotoolbox (Apple Silicon / Intel Mac GPU). macOS only.
             ~4-5s per 30s clip, ~35W (measured on Apple Silicon MacBook Pro).
             Best choice when running on battery or transcoding large libraries.
             Output quality is good for dashcam/action footage.
             Falls back to an error on Linux or if videotoolbox is absent.

  auto       Like hardware on macOS with videotoolbox, otherwise falls back to quality,
             then compat. Safe cross-platform default when the machine is unknown.

  quality    Uses libx265 (software HEVC, CRF 20). Platform-independent.
             ~50s per 30s clip, ~80W (measured on Apple Silicon MacBook Pro).
             Best compression ratio and quality for archival, high-resolution, or visually
             complex sources (e.g. cinema, screen recordings).
             Overkill for dashcam footage; prefer hardware or auto for those.

  compat     Uses libx264 (software H.264, CRF 19). Maximum player compatibility.
             Use when the destination player cannot decode HEVC (older TVs, Android 4.x,
             web browsers without HEVC support). Larger files than HEVC modes.

Behavior:
  - Output is always .mp4 (H.264 or HEVC depending on mode, always Apple-compatible)
  - HEVC outputs are tagged hvc1 for QuickTime / Apple Photos compatibility
  - Metadata is restored from source to output with exiftool
  - Garmin camera model is also mapped to standard Make/Model tags for compatibility
  - Inferred lens metadata is written from Garmin's declared 140-degree FOV
  - Transcoding encoder/mode is written in metadata (Software)
  - Original directory structure is preserved under destination
  - When --source points to a file, only that file is processed

Examples:
  ./garmin_varia_transcode.sh -s SampleFootage -d /Volumes/Archive
  ./garmin_varia_transcode.sh -s SampleFootage -d encoded --mode auto
  ./garmin_varia_transcode.sh -s clip.mp4 -d encoded --dry-run --verbose
  ./garmin_varia_transcode.sh -s SampleFootage -d encoded --mode quality --crf 18
  ./garmin_varia_transcode.sh -s SampleFootage -d encoded --mode compat
  ./garmin_varia_transcode.sh -s SampleFootage -d encoded --delete-source
  ./garmin_varia_transcode.sh -s SampleFootage -d encoded --unattended
EOF
}

log_msg() {
  local level="$1"
  shift
  local ts
  ts="$(date '+%Y-%m-%d %H:%M:%S')"
  echo "[$ts] [$level] $*"
}

handle_unexpected_error() {
  local rc=$?
  local line_no="${1:-unknown}"
  local command_text="${2:-unknown}"

  if [[ "$rc" -eq 0 ]]; then
    return 0
  fi

  if [[ "$PROGRESS_LINE_OPEN" == true ]]; then
    printf '\n'
    PROGRESS_LINE_OPEN=false
  fi

  log_msg "ERROR" "Unexpected script failure at line $line_no (rc=$rc): $command_text"
  return "$rc"
}

# Verbose-only log: suppressed in default quiet mode
vlog_msg() {
  [[ "$VERBOSE" == true ]] && log_msg "$@"
  return 0
}

# Quiet-mode per-file progress lines
log_progress_start() {
  local input_file="$1"
  local ts
  ts="$(date '+%Y-%m-%d %H:%M:%S')"
  printf '[%s] [INFO] Transcoding %s ...' "$ts" "$input_file"
  PROGRESS_LINE_OPEN=true
}

log_progress_done() {
  local real_elapsed_sec="$2"
  local encode_elapsed_sec="$3"
  if [[ "$PROGRESS_LINE_OPEN" == true ]]; then
    printf ' done in %ss (encode %ss)\n' "$real_elapsed_sec" "$encode_elapsed_sec"
    PROGRESS_LINE_OPEN=false
    return
  fi

  local input_file="$1"
  local ts
  ts="$(date '+%Y-%m-%d %H:%M:%S')"
  echo "[$ts] [INFO] Transcoding $input_file ... done in ${real_elapsed_sec}s (encode ${encode_elapsed_sec}s)"
}

log_progress_failed() {
  local input_file="$1"
  local real_elapsed_sec="$2"
  local encode_elapsed_sec="$3"
  local ffmpeg_rc="$4"
  local ffmpeg_log="$5"

  if [[ "$PROGRESS_LINE_OPEN" == true ]]; then
    printf ' FAILED after %ss (encode %ss, rc=%s, log=%s)\n' "$real_elapsed_sec" "$encode_elapsed_sec" "$ffmpeg_rc" "${ffmpeg_log:-n/a}"
    PROGRESS_LINE_OPEN=false
    return
  fi

  local ts
  ts="$(date '+%Y-%m-%d %H:%M:%S')"
  echo "[$ts] [ERROR] Transcoding $input_file ... FAILED after ${real_elapsed_sec}s (encode ${encode_elapsed_sec}s, rc=$ffmpeg_rc, log=${ffmpeg_log:-n/a})"
}

log_progress_skipped_unreadable() {
  local input_file="$1"

  if [[ "$PROGRESS_LINE_OPEN" == true ]]; then
    printf ' SKIPPED (unreadable/corrupted source)\n'
    PROGRESS_LINE_OPEN=false
    return
  fi

  local ts
  ts="$(date '+%Y-%m-%d %H:%M:%S')"
  echo "[$ts] [INFO] Transcoding $input_file ... SKIPPED (unreadable/corrupted source)"
}

die() {
  log_msg "ERROR" "$*"
  exit 1
}

handle_interrupt() {
  INTERRUPT_COUNT=$((INTERRUPT_COUNT + 1))
  STOP_AFTER_CURRENT=true

  # If quiet-mode progress is mid-line, break it before interrupt logs.
  if [[ "$PROGRESS_LINE_OPEN" == true ]]; then
    printf '\n'
    PROGRESS_LINE_OPEN=false
  fi

  if [[ "$INTERRUPT_COUNT" -eq 1 ]]; then
    # ffmpeg is in the same process group and already received SIGINT; it will
    # stop gracefully on its own.  A second Ctrl+C force-kills it if needed.
    log_msg "WARN" "Interrupted; stopping after current encode. Ctrl+C again to force-kill."
  elif [[ -n "$CURRENT_FFMPEG_PID" ]]; then
    log_msg "WARN" "Force-stopping encode (pid=$CURRENT_FFMPEG_PID)."
    kill -9 "$CURRENT_FFMPEG_PID" 2>/dev/null || true
  fi
}

# Drain all pending keystrokes from /dev/tty (non-blocking) and set
# STOP_AFTER_CURRENT if any of them is 'q'/'Q'.  Self-contained: saves/restores
# stty settings so it works regardless of the terminal's current mode.
# Uses min=0 time=0 so the terminal's read() returns immediately when the
# buffer is empty (bash treats a 0-byte read as EOF and exits the loop).
# -t 1 is a safety net for platforms where select() on a VMIN=0 fd does not
# report readable-when-empty; in that case we block at most 1s then exit.
check_for_quit_key() {
  [[ "$UNATTENDED" == true ]] && return 0
  [[ -t 0 ]] || return 0
  [[ -e /dev/tty ]] || return 0
  local key old_stty found=false tty_fd
  if ! exec {tty_fd}</dev/tty 2>/dev/null; then
    return 0
  fi
  old_stty=$(stty -g <&"$tty_fd" 2>/dev/null) || { exec {tty_fd}<&-; return 0; }
  stty -echo -icanon min 0 time 0 <&"$tty_fd" 2>/dev/null \
    || { stty "$old_stty" <&"$tty_fd" 2>/dev/null || true; exec {tty_fd}<&-; return 0; }
  while IFS= read -r -s -n 1 -t 1 key <&"$tty_fd" 2>/dev/null; do
    if [[ "$key" == "q" || "$key" == "Q" ]]; then
      found=true
      break
    fi
  done
  stty "$old_stty" <&"$tty_fd" 2>/dev/null || true
  exec {tty_fd}<&-
  if [[ "$found" == true ]]; then
    STOP_AFTER_CURRENT=true
    log_msg "INFO" "Quit key pressed; stopping before next file"
  fi
  return 0
}

run_ffmpeg_with_signal_guard() {
  # ffmpeg runs in the same process group as the shell so Ctrl+C (SIGINT)
  # reaches it directly and triggers its own graceful shutdown.  stdin is
  # /dev/null so ffmpeg disables keyboard control and avoids SIGTTIN.
  # CURRENT_FFMPEG_PID is tracked for the force-kill second-Ctrl+C path.
  "$@" </dev/null &

  CURRENT_FFMPEG_PID=$!
  if wait "$CURRENT_FFMPEG_PID"; then
    CURRENT_FFMPEG_PID=""
    return 0
  fi

  local rc=$?
  CURRENT_FFMPEG_PID=""
  return "$rc"
}

format_seconds() {
  local sec="$1"
  local h m s
  h=$((sec / 3600))
  m=$(((sec % 3600) / 60))
  s=$((sec % 60))
  printf '%02d:%02d:%02d' "$h" "$m" "$s"
}

format_bytes_human() {
  local bytes="$1"
  awk -v b="$bytes" 'BEGIN {
    split("B KiB MiB GiB TiB PiB", u, " ");
    i = 1;
    while (b >= 1024 && i < 6) {
      b = b / 1024;
      i++;
    }
    if (i == 1) {
      printf "%d %s", b, u[i];
    } else {
      printf "%.2f %s", b, u[i];
    }
  }'
}

print_final_report() {
  local total_elapsed_sec="$1"
  local total_elapsed_fmt="$2"
  local file_post_total_sec="$3"
  local file_post_total_fmt="$4"
  local run_non_file_overhead_sec="$5"
  local run_non_file_overhead_fmt="$6"
  local avg_file_real_time_sec="$7"
  local avg_file_real_time_fmt="$8"
  local avg_video_time_sec="$9"
  local avg_video_time_fmt="${10}"
  local startup_warmup_sec="${11}"
  local startup_warmup_fmt="${12}"
  local staging_warmup_sec="${13}"
  local staging_warmup_fmt="${14}"
  local input_bytes_processed="${15}"
  local output_bytes_processed="${16}"

  printf '\n'
  printf 'Run Summary\n'
  printf '+---------------------------+-------+\n'
  printf '| %-25s | %-5s |\n' "Metric" "Value"
  printf '+---------------------------+-------+\n'
  printf '| %-25s | %5s |\n' "Videos processed" "$VIDEOS_PROCESSED"
  printf '| %-25s | %5s |\n' "Videos skipped" "$VIDEOS_SKIPPED"
  printf '| %-25s | %5s |\n' "Invalid sources skipped" "$INVALID_SOURCES_SKIPPED"
  printf '| %-25s | %5s |\n' "Destination failures" "$DESTINATION_FAILURES"
  printf '| %-25s | %5s |\n' "Errors" "$ERRORS"
  printf '+---------------------------+-------+\n'

  printf '\nTimings\n'
  printf '+---------------------------+---------+----------+\n'
  printf '| %-25s | %-7s | %-8s |\n' "Metric" "Seconds" "Elapsed"
  printf '+---------------------------+---------+----------+\n'
  printf '| %-25s | %7s | %-8s |\n' "Total run time" "${total_elapsed_sec}s" "$total_elapsed_fmt"
  printf '| %-25s | %7s | %-8s |\n' "File wall time total" "${TOTAL_FILE_REAL_TIME_SEC}s" "$(format_seconds "$TOTAL_FILE_REAL_TIME_SEC")"
  printf '| %-25s | %7s | %-8s |\n' "Encode time total" "${TOTAL_VIDEO_TIME_SEC}s" "$(format_seconds "$TOTAL_VIDEO_TIME_SEC")"
  printf '| %-25s | %7s | %-8s |\n' "Post-processing total" "${file_post_total_sec}s" "$file_post_total_fmt"
  printf '| %-25s | %7s | %-8s |\n' "Non-file overhead" "${run_non_file_overhead_sec}s" "$run_non_file_overhead_fmt"
  if [[ "$startup_warmup_sec" -ge 0 ]]; then
    printf '| %-25s | %7s | %-8s |\n' "Startup warm-up" "${startup_warmup_sec}s" "$startup_warmup_fmt"
  fi
  if [[ "$staging_warmup_sec" -ge 0 ]]; then
    printf '| %-25s | %7s | %-8s |\n' "Staging warm-up" "${staging_warmup_sec}s" "$staging_warmup_fmt"
  fi
  printf '+---------------------------+---------+----------+\n'

  printf '\nPer-File Averages\n'
  printf '+---------------------------+---------+----------+\n'
  printf '| %-25s | %-7s | %-8s |\n' "Metric" "Seconds" "Elapsed"
  printf '+---------------------------+---------+----------+\n'
  printf '| %-25s | %7s | %-8s |\n' "Average file wall time" "${avg_file_real_time_sec}s" "$avg_file_real_time_fmt"
  printf '| %-25s | %7s | %-8s |\n' "Average encode time" "${avg_video_time_sec}s" "$avg_video_time_fmt"
  printf '+---------------------------+---------+----------+\n'
}

make_temp_log_file() {
  local temp_path
  local base_tmp="${TMPDIR:-/tmp}"
  base_tmp="${base_tmp%/}"

  temp_path="$(mktemp "$base_tmp/varia_ffmpeg.XXXXXX.log" 2>/dev/null || true)"
  if [[ -z "$temp_path" ]]; then
    temp_path="$(mktemp -t varia_ffmpeg 2>/dev/null || true)"
  fi

  printf '%s\n' "$temp_path"
}

make_temp_output_file() {
  local target_file="$1"
  local preferred_dir="${2:-}"
  local target_dir target_base target_noext temp_path seed_path work_dir

  target_dir="$(dirname "$target_file")"
  target_base="$(basename "$target_file")"
  target_noext="${target_base%.*}"
  work_dir="$target_dir"

  if [[ -n "$preferred_dir" ]]; then
    work_dir="$preferred_dir"
  fi

  seed_path="$(mktemp "$work_dir/.varia_tmp.${target_noext}.XXXXXX" 2>/dev/null || true)"
  if [[ -z "$seed_path" && "$work_dir" != "$target_dir" ]]; then
    seed_path="$(mktemp "$target_dir/.varia_tmp.${target_noext}.XXXXXX" 2>/dev/null || true)"
  fi

  if [[ -n "$seed_path" ]]; then
    rm -f -- "$seed_path" 2>/dev/null || true
    temp_path="${seed_path}.mp4"
  else
    temp_path="$work_dir/.varia_tmp.${target_noext}.$$.$RANDOM.mp4"
    if ! touch "$temp_path" >/dev/null 2>&1; then
      if [[ "$work_dir" != "$target_dir" ]]; then
        temp_path="$target_dir/.varia_tmp.${target_noext}.$$.$RANDOM.mp4"
        if ! touch "$temp_path" >/dev/null 2>&1; then
          temp_path=""
        else
          rm -f -- "$temp_path" 2>/dev/null || true
        fi
      else
        temp_path=""
      fi
    else
      rm -f -- "$temp_path" 2>/dev/null || true
    fi
  fi

  printf '%s\n' "$temp_path"
}

make_temp_staged_input_file() {
  local source_file="$1"
  local staging_dir="$2"
  local source_base source_noext source_ext seed_path temp_path

  source_base="$(basename "$source_file")"
  source_noext="${source_base%.*}"
  source_ext="$(file_extension_or_tmp "$source_file")"

  seed_path="$(mktemp "$staging_dir/.varia_input.${source_noext}.XXXXXX" 2>/dev/null || true)"
  if [[ -n "$seed_path" ]]; then
    rm -f -- "$seed_path" 2>/dev/null || true
    temp_path="${seed_path}.${source_ext}"
  else
    temp_path="$staging_dir/.varia_input.${source_noext}.$$.$RANDOM.${source_ext}"
    if ! touch "$temp_path" >/dev/null 2>&1; then
      temp_path=""
    else
      rm -f -- "$temp_path" 2>/dev/null || true
    fi
  fi

  printf '%s\n' "$temp_path"
}

cleanup_transcode_artifacts() {
  local temp_output="$1"
  local final_output="$2"
  local exif_backup_default="${temp_output}_original"
  local exif_backup_alt="${temp_output}.exif_original"

  rm -f -- "$temp_output" 2>/dev/null || true
  rm -f -- "$exif_backup_default" "$exif_backup_alt" 2>/dev/null || true

  # Defensive cleanup for previously failed non-atomic runs.
  if [[ -f "$final_output" && ! -s "$final_output" ]]; then
    rm -f -- "$final_output" 2>/dev/null || true
  fi
}

cleanup_staged_input() {
  local staged_input="$1"
  [[ -n "$staged_input" ]] || return 0
  rm -f -- "$staged_input" 2>/dev/null || true
}

cleanup_process_inputs() {
  local staged_input="$1"
  local repacked_input="$2"
  cleanup_staged_input "$staged_input"
  cleanup_repacked_input "$repacked_input"
}

is_apple_noise_file() {
  local file_path="$1"
  local base
  base="$(basename "$file_path")"

  case "$base" in
    ._*|.DS_Store|.AppleDouble|._.DS_Store)
      return 0
      ;;
  esac

  return 1
}

require_value() {
  local flag="$1"
  local value="${2:-}"
  if [[ -z "$value" ]]; then
    die "Missing value for $flag"
  fi
}

to_abs_path() {
  local p="$1"
  if [[ "$p" = /* ]]; then
    printf '%s\n' "$p"
  else
    printf '%s\n' "$PWD/$p"
  fi
}

path_is_within() {
  local child="$1"
  local parent="$2"
  [[ "$child" == "$parent" || "$child" == "$parent"/* ]]
}

run_with_timeout() {
  local timeout_sec="$1"
  shift
  local pid elapsed=0 rc=0

  "$@" &
  pid=$!

  while kill -0 "$pid" 2>/dev/null; do
    if [[ "$elapsed" -ge "$timeout_sec" ]]; then
      kill -TERM "$pid" 2>/dev/null || true
      sleep 1
      kill -KILL "$pid" 2>/dev/null || true
      return 124
    fi
    sleep 1
    elapsed=$((elapsed + 1))
  done

  wait "$pid" 2>/dev/null
  rc=$?
  return "$rc"
}

preflight_source_readable() {
  local source_dir="$1"
  local rc=0

  log_msg "INFO" "Preflight: checking source is readable: $source_dir"
  trap - ERR
  set +e
  run_with_timeout "$PREFLIGHT_TIMEOUT_SEC" bash -c '
    source_dir="$1"
    [[ -d "$source_dir" && -r "$source_dir" && -x "$source_dir" ]] || exit 10
    find "$source_dir" -maxdepth 1 -print -quit >/dev/null 2>&1
  ' _ "$source_dir"
  rc=$?
  set -e
  trap 'handle_unexpected_error "$LINENO" "$BASH_COMMAND"' ERR
  if [[ "$rc" -ne 0 ]]; then
    if [[ "$rc" -eq 124 ]]; then
      die "Source is not readable within ${PREFLIGHT_TIMEOUT_SEC}s, likely stale/inaccessible NFS/autofs mount: $source_dir"
    fi
    die "Source is not readable or searchable: $source_dir"
  fi
}

preflight_destination_writable() {
  local dest_dir="$1"
  local rc=0

  log_msg "INFO" "Preflight: checking destination is writable: $dest_dir"
  trap - ERR
  set +e
  run_with_timeout "$PREFLIGHT_TIMEOUT_SEC" bash -c '
    dest_dir="$1"
    mkdir -p "$dest_dir" >/dev/null 2>&1 || exit 10
    probe="$(mktemp "$dest_dir/.varia_write_probe.XXXXXX" 2>/dev/null)" || exit 11
    rm -f -- "$probe" >/dev/null 2>&1 || exit 12
  ' _ "$dest_dir"
  rc=$?
  set -e
  trap 'handle_unexpected_error "$LINENO" "$BASH_COMMAND"' ERR
  if [[ "$rc" -ne 0 ]]; then
    if [[ "$rc" -eq 124 ]]; then
      die "Destination is not writable within ${PREFLIGHT_TIMEOUT_SEC}s, likely stale/inaccessible NFS/autofs mount: $dest_dir"
    fi
    die "Destination is not writable: $dest_dir"
  fi
}

file_size_bytes_or_zero() {
  local file_path="$1"
  local file_size="0"

  file_size="$(stat -f%z "$file_path" 2>/dev/null || true)"
  if [[ ! "$file_size" =~ ^[0-9]+$ ]]; then
    file_size="$(stat -c%s "$file_path" 2>/dev/null || true)"
  fi

  if [[ "$file_size" =~ ^[0-9]+$ ]]; then
    printf '%s\n' "$file_size"
  else
    printf '0\n'
  fi
}

file_extension_or_tmp() {
  local file_path="$1"
  local base ext
  base="$(basename "$file_path")"
  ext="${base##*.}"
  if [[ "$base" == "$ext" || -z "$ext" ]]; then
    printf 'tmp\n'
  else
    printf '%s\n' "$ext"
  fi
}

dir_available_bytes_or_zero() {
  local dir_path="$1"
  local avail_kb=""

  avail_kb="$(df -Pk "$dir_path" 2>/dev/null | awk 'NR==2 {print $4}' || true)"
  if [[ "$avail_kb" =~ ^[0-9]+$ ]]; then
    printf '%s\n' $((avail_kb * 1024))
  else
    printf '0\n'
  fi
}

staging_has_space_for_input() {
  local staging_dir="$1"
  local input_file="$2"
  local input_size_bytes=0
  local needed_bytes=0
  local available_bytes=0

  [[ -n "$staging_dir" && -d "$staging_dir" && -w "$staging_dir" ]] || return 1

  input_size_bytes="$(file_size_bytes_or_zero "$input_file")"
  if [[ "$input_size_bytes" -le 0 ]]; then
    return 1
  fi

  # Keep one simple rule: need roughly 2x input size in staging.
  # This covers temp output plus metadata rewrite overhead.
  needed_bytes=$((input_size_bytes * 2))
  available_bytes="$(dir_available_bytes_or_zero "$staging_dir")"
  [[ "$available_bytes" -ge "$needed_bytes" ]]
}

staging_has_space_for_input_copy() {
  local staging_dir="$1"
  local input_file="$2"
  local input_size_bytes=0
  local needed_bytes=0
  local available_bytes=0

  [[ -n "$staging_dir" && -d "$staging_dir" && -w "$staging_dir" ]] || return 1

  input_size_bytes="$(file_size_bytes_or_zero "$input_file")"
  if [[ "$input_size_bytes" -le 0 ]]; then
    return 1
  fi

  # Input staging needs room for the source copy plus the output and metadata
  # rewrite temp files. Garmin 30s clips are usually fixed-size, so 3x is a
  # conservative and easy-to-explain guard.
  needed_bytes=$((input_size_bytes * 3))
  available_bytes="$(dir_available_bytes_or_zero "$staging_dir")"
  [[ "$available_bytes" -ge "$needed_bytes" ]]
}

join_cmd_for_log() {
  local out=""
  local arg
  for arg in "$@"; do
    out+="$(printf '%q' "$arg") "
  done
  printf '%s\n' "${out% }"
}

parse_args() {
  while [[ $# -gt 0 ]]; do
    case "$1" in
      -s|--source|--input)
        require_value "$1" "${2:-}"
        SOURCE_DIR="$2"
        SOURCE_PROVIDED=true
        shift 2
        ;;
      -d|--destination|--output)
        require_value "$1" "${2:-}"
        DEST_DIR="$2"
        DEST_PROVIDED=true
        shift 2
        ;;
      --staging-dir)
        require_value "$1" "${2:-}"
        STAGING_DIR="$2"
        STAGING_PROVIDED=true
        shift 2
        ;;
      --stage-input)
        STAGE_INPUT=true
        shift
        ;;
      --staging-ramdisk-mb)
        require_value "$1" "${2:-}"
        STAGING_RAMDISK_MB="$2"
        shift 2
        ;;
      --debug-timing)
        require_value "$1" "${2:-}"
        DEBUG_TIMING_LIMIT="$2"
        shift 2
        ;;
      --mode)
        require_value "$1" "${2:-}"
        MODE="$2"
        shift 2
        ;;
      --crf)
        require_value "$1" "${2:-}"
        CRF_OVERRIDE="$2"
        shift 2
        ;;
      --overwrite)
        OVERWRITE=true
        shift
        ;;
      --no-overwrite)
        OVERWRITE=false
        shift
        ;;
      --dry-run)
        DRY_RUN=true
        shift
        ;;
      --recursive)
        RECURSIVE=true
        shift
        ;;
      --no-recursive)
        RECURSIVE=false
        shift
        ;;
      --extensions)
        require_value "$1" "${2:-}"
        EXTENSIONS_CSV="$2"
        shift 2
        ;;
      --single)
        require_value "$1" "${2:-}"
        SINGLE_FILE="$2"
        shift 2
        ;;
      --verbose)
        VERBOSE=true
        shift
        ;;
      --delete-source)
        MOVE_SOURCE=true
        shift
        ;;
      --keep-going)
        FAIL_FAST=false
        shift
        ;;
      --unattended)
        UNATTENDED=true
        MOVE_SOURCE=true
        FAIL_FAST=false
        shift
        ;;
      --no-apple-repack-fallback)
        APPLE_REPACK_FALLBACK=false
        shift
        ;;
      --apple-repack-fallback)
        APPLE_REPACK_FALLBACK=true
        shift
        ;;
      --move-source)
        MOVE_SOURCE=true
        shift
        ;;
      --continue-on-error)
        FAIL_FAST=false
        shift
        ;;
      -h|--help)
        usage
        exit 0
        ;;
      *)
        die "Unknown argument: $1"
        ;;
    esac
  done

  case "$MODE" in
    auto|hardware|quality|compat) ;;
    *) die "Invalid --mode '$MODE'. Use auto|hardware|quality|compat." ;;
  esac

  if [[ "$SOURCE_PROVIDED" != true && "$DEST_PROVIDED" != true ]]; then
    die "At least one of --source or --destination must be provided"
  fi

  if [[ -n "$CRF_OVERRIDE" && ! "$CRF_OVERRIDE" =~ ^[0-9]+$ ]]; then
    die "--crf must be an integer"
  fi

  if [[ "$DEBUG_TIMING_LIMIT" != "0" ]]; then
    if ! [[ "$DEBUG_TIMING_LIMIT" =~ ^[0-9]+$ ]]; then
      die "--debug-timing must be a positive integer"
    fi
    if [[ "$DEBUG_TIMING_LIMIT" -le 0 ]]; then
      die "--debug-timing must be greater than 0"
    fi
  fi

  if ! [[ "$STAGING_RAMDISK_MB" =~ ^[0-9]+$ ]]; then
    die "--staging-ramdisk-mb must be a positive integer"
  fi
  if [[ "$STAGING_RAMDISK_MB" -le 0 ]]; then
    die "--staging-ramdisk-mb must be greater than 0"
  fi

  if [[ "$STAGE_INPUT" != true && "$STAGING_PROVIDED" == true && "$MOVE_SOURCE" == true && "$FAIL_FAST" == false ]]; then
    STAGE_INPUT=true
  fi
}

check_tools() {
  command -v ffmpeg >/dev/null 2>&1 || die "ffmpeg not found in PATH"
  command -v ffprobe >/dev/null 2>&1 || die "ffprobe not found in PATH"
  command -v exiftool >/dev/null 2>&1 || die "exiftool not found in PATH"

  HAS_AVCONVERT=false
  if [[ "$(uname -s)" == "Darwin" ]] && command -v avconvert >/dev/null 2>&1; then
    HAS_AVCONVERT=true
  fi
}

make_temp_repack_file() {
  local base_tmp="${TMPDIR:-/tmp}"
  local seed_path temp_path

  base_tmp="${base_tmp%/}"
  seed_path="$(mktemp "$base_tmp/varia_repack.XXXXXX" 2>/dev/null || true)"
  if [[ -n "$seed_path" ]]; then
    rm -f -- "$seed_path" 2>/dev/null || true
    temp_path="${seed_path}.mp4"
    printf '%s\n' "$temp_path"
    return
  fi

  temp_path="$base_tmp/varia_repack.$$.$RANDOM.mp4"
  printf '%s\n' "$temp_path"
}

cleanup_repacked_input() {
  local repacked_file="$1"
  if [[ -n "$repacked_file" ]]; then
    rm -f -- "$repacked_file" 2>/dev/null || true
  fi
}

try_apple_repack_for_unreadable() {
  local source_file="$1"
  local repacked_file=""

  REPACKED_SOURCE_PATH=""

  if [[ "$APPLE_REPACK_FALLBACK" != true || "$HAS_AVCONVERT" != true || "$(uname -s)" != "Darwin" ]]; then
    return 1
  fi

  repacked_file="$(make_temp_repack_file)"
  if [[ -z "$repacked_file" ]]; then
    return 1
  fi

  if [[ "$VERBOSE" == true ]]; then
    log_msg "WARN" "Trying avconvert passthrough repack fallback: $source_file"
    if ! avconvert --source "$source_file" --preset PresetPassthrough --output "$repacked_file" --replace; then
      cleanup_repacked_input "$repacked_file"
      return 1
    fi
  else
    if ! avconvert --source "$source_file" --preset PresetPassthrough --output "$repacked_file" --replace >/dev/null 2>&1; then
      cleanup_repacked_input "$repacked_file"
      return 1
    fi
  fi

  if ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$repacked_file" >/dev/null 2>&1; then
    REPACKED_SOURCE_PATH="$repacked_file"
    return 0
  fi

  cleanup_repacked_input "$repacked_file"
  return 1
}

restore_metadata_with_exiftool() {
  local input_file="$1"
  local output_file="$2"

  vlog_msg "CHECKPOINT" "metadata_start: $output_file"

  if [[ "$VERBOSE" == true ]]; then
    if exiftool -overwrite_original -m -TagsFromFile "$input_file" -all:all -unsafe "$output_file"; then
      vlog_msg "CHECKPOINT" "metadata_end: $output_file"
      return 0
    fi
  else
    if exiftool -overwrite_original -m -q -q -TagsFromFile "$input_file" -all:all -unsafe "$output_file" >/dev/null 2>&1; then
      vlog_msg "CHECKPOINT" "metadata_end: $output_file"
      return 0
    fi
  fi

  if [[ "$STOP_AFTER_CURRENT" == true && "$INTERRUPT_COUNT" -gt 0 ]]; then
    log_msg "INFO" "Metadata restore interrupted by user while stopping: $output_file"
    return 1
  fi

  log_msg "ERROR" "Failed to restore metadata with exiftool: $output_file"
  return 1
}

map_garmin_model_to_standard_tags() {
  local input_file="$1"
  local output_file="$2"
  local garmin_model

  garmin_model="$(exiftool -s3 -UserData:GarminModel "$input_file" 2>/dev/null | head -n1 || true)"
  if [[ -z "$garmin_model" ]]; then
    vlog_msg "CHECKPOINT" "model_map_skip: no GarminModel in source: $input_file"
    return 0
  fi

  # garmin_model confirms this is a Garmin Varia source; fixed metadata values are
  # used below because Garmin does not embed a parseable model string we can map.
  vlog_msg "CHECKPOINT" "model_map_start: $output_file (GarminModel=$garmin_model)"

  if [[ "$VERBOSE" == true ]]; then
    if exiftool -overwrite_original -m \
      -UserData:Make="Garmin" -UserData:Model="Varia RCT715" \
      -Keys:Make="Garmin" -Keys:Model="Varia RCT715" \
      -Keys:CompatibleBrands="isom, iso2, mp41" \
      -Keys:MajorBrand="isom" \
      -Make="Garmin" -Model="Varia RCT715" \
      -VideoKeys:LensModel="Fixed Wide Angle ${GARMIN_DECLARED_FOV_DEG} deg" \
      -VideoKeys:FocalLengthIn35mmFormat="$GARMIN_INFERRED_FOCAL_35MM_MM" \
      -XMP-exif:FocalLength="${GARMIN_INFERRED_FOCAL_MM} mm" \
      -XMP-exif:FocalLengthIn35mmFormat="$GARMIN_INFERRED_FOCAL_35MM_MM" \
      -XMP-exifEX:LensModel="Fixed Wide Angle ${GARMIN_DECLARED_FOV_DEG} deg" \
      "$output_file"; then
      vlog_msg "CHECKPOINT" "model_map_end: $output_file"
      return 0
    fi
  else
    if exiftool -overwrite_original -m -q -q \
      -UserData:Make="Garmin" -UserData:Model="Varia RCT715" \
      -Keys:Make="Garmin" -Keys:Model="Varia RCT715" \
      -Keys:CompatibleBrands="isom, iso2, mp41" \
      -Keys:MajorBrand="isom" \
      -Make="Garmin" -Model="Varia RCT715" \
      -VideoKeys:LensModel="Fixed Wide Angle ${GARMIN_DECLARED_FOV_DEG} deg" \
      -VideoKeys:FocalLengthIn35mmFormat="$GARMIN_INFERRED_FOCAL_35MM_MM" \
      -XMP-exif:FocalLength="${GARMIN_INFERRED_FOCAL_MM} mm" \
      -XMP-exif:FocalLengthIn35mmFormat="$GARMIN_INFERRED_FOCAL_35MM_MM" \
      -XMP-exifEX:LensModel="Fixed Wide Angle ${GARMIN_DECLARED_FOV_DEG} deg" \
      "$output_file" >/dev/null 2>&1; then
      vlog_msg "CHECKPOINT" "model_map_end: $output_file"
      return 0
    fi
  fi

  log_msg "ERROR" "Failed to map GarminModel to standard Make/Model tags: $output_file"
  return 1
}

write_transcode_encoder_metadata() {
  local output_file="$1"
  local encoder_meta

  encoder_meta="$TOOL_NAME mode=$ENCODER_KIND codec=$VIDEO_CODEC"

  vlog_msg "CHECKPOINT" "encoder_meta_start: $output_file ($encoder_meta)"

  if [[ "$VERBOSE" == true ]]; then
    if exiftool -overwrite_original -m \
      -Software="$encoder_meta" \
      -UserData:Software="$encoder_meta" \
      "$output_file"; then
      vlog_msg "CHECKPOINT" "encoder_meta_end: $output_file"
      return 0
    fi
  else
    if exiftool -overwrite_original -m -q -q \
      -Software="$encoder_meta" \
      -UserData:Software="$encoder_meta" \
      "$output_file" >/dev/null 2>&1; then
      vlog_msg "CHECKPOINT" "encoder_meta_end: $output_file"
      return 0
    fi
  fi

  log_msg "ERROR" "Failed to write encoder metadata: $output_file"
  return 1
}

detect_encoders() {
  local encoders
  encoders="$(ffmpeg -hide_banner -encoders 2>&1 || true)"

  if echo "$encoders" | grep -Eq '(^|[[:space:]])hevc_videotoolbox([[:space:]]|$)'; then
    HAS_VIDEOTOOLBOX=true
  fi
  if echo "$encoders" | grep -Eq '(^|[[:space:]])libx265([[:space:]]|$)'; then
    HAS_LIBX265=true
  fi
  if echo "$encoders" | grep -Eq '(^|[[:space:]])libx264([[:space:]]|$)'; then
    HAS_LIBX264=true
  fi

  if [[ "$VERBOSE" == true ]]; then
    log_msg "INFO" "Encoders: hevc_videotoolbox=$HAS_VIDEOTOOLBOX libx265=$HAS_LIBX265 libx264=$HAS_LIBX264"
  fi
}

resolve_encoder() {
  local os_name
  os_name="$(uname -s)"

  case "$MODE" in
    auto)
      if [[ "$os_name" == "Darwin" && "$HAS_VIDEOTOOLBOX" == true ]]; then
        ENCODER_KIND="hardware"
      elif [[ "$HAS_LIBX265" == true ]]; then
        ENCODER_KIND="quality"
      elif [[ "$HAS_LIBX264" == true ]]; then
        ENCODER_KIND="compat"
      else
        die "No suitable encoder found. Need one of hevc_videotoolbox, libx265, libx264."
      fi
      ;;
    hardware)
      [[ "$os_name" == "Darwin" ]] || die "--mode hardware is only supported on macOS (hevc_videotoolbox)"
      [[ "$HAS_VIDEOTOOLBOX" == true ]] || die "hevc_videotoolbox not available in ffmpeg"
      ENCODER_KIND="hardware"
      ;;
    quality)
      [[ "$HAS_LIBX265" == true ]] || die "libx265 not available in ffmpeg"
      ENCODER_KIND="quality"
      ;;
    compat)
      [[ "$HAS_LIBX264" == true ]] || die "libx264 not available in ffmpeg"
      ENCODER_KIND="compat"
      ;;
  esac

  case "$ENCODER_KIND" in
    hardware)
      VIDEO_CODEC="hevc_videotoolbox"
      VIDEO_CRF=""
      ;;
    quality)
      VIDEO_CODEC="libx265"
      VIDEO_CRF="${CRF_OVERRIDE:-$DEFAULT_CRF_HEVC}"
      ;;
    compat)
      VIDEO_CODEC="libx264"
      VIDEO_CRF="${CRF_OVERRIDE:-$DEFAULT_CRF_H264}"
      ;;
  esac

  vlog_msg "INFO" "Selected mode: $MODE (effective: $ENCODER_KIND, codec: $VIDEO_CODEC${VIDEO_CRF:+, crf: $VIDEO_CRF})"
}

probe_has_audio() {
  local input_file="$1"
  local out
  out="$(ffprobe -v error -select_streams a -show_entries stream=codec_type -of csv=p=0 "$input_file" || true)"
  [[ -n "$out" ]]
}

print_verbose_probe() {
  local input_file="$1"
  ffprobe -v error \
    -show_entries format=filename,format_name,duration,bit_rate,start_time:stream=index,codec_type,codec_name,profile,pix_fmt,width,height,avg_frame_rate,r_frame_rate,bit_rate \
    -of default=noprint_wrappers=1:nokey=0 "$input_file" || true
}

ffprobe_duration_or_empty() {
  local file_path="$1"
  ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file_path" 2>/dev/null | head -n1
}

ffprobe_video_codec_or_empty() {
  local file_path="$1"
  ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 "$file_path" 2>/dev/null | head -n1
}

source_video_is_readable() {
  local file_path="$1"
  REPACKED_SOURCE_PATH=""

  if ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file_path" >/dev/null 2>&1; then
    SOURCE_READABLE_MODE="normal"
    return 0
  fi

  if [[ "$VERBOSE" == true ]]; then
    log_msg "WARN" "Source probe failed, trying tolerant mode: $file_path"
  fi

  if ffprobe -v error -fflags +genpts -err_detect ignore_err -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file_path" >/dev/null 2>&1; then
    SOURCE_READABLE_MODE="tolerant"
    vlog_msg "INFO" "Source readable in tolerant mode: $file_path"
    return 0
  fi

  if try_apple_repack_for_unreadable "$file_path"; then
    SOURCE_READABLE_MODE="repacked"
    log_msg "WARN" "Using avconvert repack fallback for unreadable source: $file_path"
    return 0
  fi

  SOURCE_READABLE_MODE="normal"
  return 1
}

stage_input_for_encode() {
  local source_file="$1"
  local source_size staged_size
  local staged_duration
  local available_bytes=0 needed_bytes=0

  STAGED_INPUT_PATH=""
  STAGED_INPUT_FAILURE_RC=0

  if [[ "$STAGE_INPUT" != true ]]; then
    return 0
  fi

  if [[ "$DRY_RUN" == true ]]; then
    vlog_msg "DRY-RUN" "Would copy source to staging before encode: $source_file"
    return 0
  fi

  if [[ "$STAGING_PROVIDED" != true || -z "$STAGING_DIR" ]]; then
    STAGED_INPUT_FAILURE_RC=3
    log_msg "ERROR" "--stage-input requires --staging-dir"
    return 1
  fi

  if [[ ! -d "$STAGING_DIR" || ! -w "$STAGING_DIR" ]]; then
    STAGED_INPUT_FAILURE_RC=3
    log_msg "ERROR" "Input staging directory is not writable: $STAGING_DIR"
    return 1
  fi

  source_size="$(file_size_bytes_or_zero "$source_file")"
  available_bytes="$(dir_available_bytes_or_zero "$STAGING_DIR")"
  needed_bytes=$((source_size * 3))
  if [[ "$source_size" -le 0 || "$available_bytes" -lt "$needed_bytes" ]]; then
    STAGED_INPUT_FAILURE_RC=3
    log_msg "ERROR" "Insufficient staging space for input copy and output temp files: $source_file (needed=${needed_bytes}B available=${available_bytes}B source=${source_size}B staging=$STAGING_DIR)"
    return 1
  fi

  STAGED_INPUT_PATH="$(make_temp_staged_input_file "$source_file" "$STAGING_DIR")"
  if [[ -z "$STAGED_INPUT_PATH" ]]; then
    STAGED_INPUT_FAILURE_RC=3
    log_msg "ERROR" "Could not create staged input temp file: $source_file"
    return 1
  fi

  vlog_msg "INFO" "Copying source to staging before encode: $source_file -> $STAGED_INPUT_PATH"
  if ! dd if="$source_file" of="$STAGED_INPUT_PATH" bs=4m status=none; then
    cleanup_staged_input "$STAGED_INPUT_PATH"
    STAGED_INPUT_PATH=""
    STAGED_INPUT_FAILURE_RC=2
    log_msg "ERROR" "Failed to copy source to staging: $source_file"
    return 1
  fi

  staged_size="$(file_size_bytes_or_zero "$STAGED_INPUT_PATH")"
  if [[ "$source_size" -le 0 || "$source_size" != "$staged_size" ]]; then
    cleanup_staged_input "$STAGED_INPUT_PATH"
    STAGED_INPUT_PATH=""
    STAGED_INPUT_FAILURE_RC=2
    log_msg "ERROR" "Staged source copy failed size validation: $source_file (source=${source_size}B staged=${staged_size}B)"
    return 1
  fi

  staged_duration="$(ffprobe_duration_or_empty "$STAGED_INPUT_PATH")"
  if [[ -z "$staged_duration" ]]; then
    cleanup_staged_input "$STAGED_INPUT_PATH"
    STAGED_INPUT_PATH=""
    STAGED_INPUT_FAILURE_RC=2
    log_msg "ERROR" "Staged source copy failed duration probe: $STAGED_INPUT_PATH"
    return 1
  fi

  vlog_msg "INFO" "Staged input OK: $STAGED_INPUT_PATH"
  return 0
}

source_error_from_ffmpeg_log() {
  local ffmpeg_log="$1"
  [[ -n "$ffmpeg_log" && -f "$ffmpeg_log" ]] || return 1
  grep -Eiq 'invalid data found when processing input|error reading header|moov atom not found|corrupt|truncated|end of file|contradictionary STSC and STCO' "$ffmpeg_log"
}

destination_cannot_accept_file() {
  local input_file="$1"
  local out_dir="$2"
  local ffmpeg_log="$3"
  local probe_path=""
  local avail_kb=""
  local avail_bytes=0

  if [[ -n "$ffmpeg_log" && -f "$ffmpeg_log" ]]; then
    if grep -Eiq 'no space left on device|disk quota exceeded|read-only file system|permission denied|file too large|input/output error|stale file handle' "$ffmpeg_log"; then
      return 0
    fi
  fi

  probe_path="$(mktemp "$out_dir/.varia_write_probe.XXXXXX" 2>/dev/null || true)"
  if [[ -z "$probe_path" ]]; then
    return 0
  fi
  rm -f -- "$probe_path" 2>/dev/null || true

  avail_kb="$(df -Pk "$out_dir" 2>/dev/null | awk 'NR==2 {print $4}' || true)"
  if [[ "$avail_kb" =~ ^[0-9]+$ ]]; then
    avail_bytes=$((avail_kb * 1024))
    if [[ "$avail_bytes" -lt 67108864 ]]; then
      return 0
    fi
  fi

  return 1
}

validate_transcoded_output() {
  local input_file="$1"
  local output_file="$2"

  if [[ ! -f "$output_file" ]]; then
    log_msg "ERROR" "Validation failed: destination file missing: $output_file"
    return 1
  fi

  local expected_codec actual_codec
  case "$ENCODER_KIND" in
    hardware|quality) expected_codec="hevc" ;;
    compat) expected_codec="h264" ;;
    *)
      log_msg "ERROR" "Validation failed: unknown encoder kind '$ENCODER_KIND'"
      return 1
      ;;
  esac

  actual_codec="$(ffprobe_video_codec_or_empty "$output_file")"
  if [[ -z "$actual_codec" ]]; then
    log_msg "ERROR" "Validation failed: ffprobe could not read output codec: $output_file"
    return 1
  fi
  if [[ "$actual_codec" != "$expected_codec" ]]; then
    log_msg "ERROR" "Validation failed: codec mismatch for $output_file (expected=$expected_codec actual=$actual_codec)"
    return 1
  fi

  local in_duration out_duration duration_delta
  in_duration="$(ffprobe_duration_or_empty "$input_file")"
  out_duration="$(ffprobe_duration_or_empty "$output_file")"

  if [[ -z "$in_duration" || -z "$out_duration" ]]; then
    log_msg "ERROR" "Validation failed: missing duration probe data (input='$in_duration' output='$out_duration')"
    return 1
  fi

  duration_delta="$(awk -v a="$in_duration" -v b="$out_duration" 'BEGIN{d=a-b; if (d<0) d=-d; printf "%.3f", d}')"
  if ! awk -v d="$duration_delta" -v t="$DURATION_TOLERANCE_SEC" 'BEGIN{exit !(d<=t)}'; then
    log_msg "ERROR" "Validation failed: duration delta too large for $output_file (input=${in_duration}s output=${out_duration}s delta=${duration_delta}s tolerance=${DURATION_TOLERANCE_SEC}s)"
    return 1
  fi

  vlog_msg "INFO" "Validation OK: $output_file (codec=$actual_codec input_dur=${in_duration}s output_dur=${out_duration}s delta=${duration_delta}s)"
  return 0
}

build_video_args() {
  VIDEO_ARGS=()

  case "$ENCODER_KIND" in
    hardware)
      VIDEO_ARGS+=( -c:v "$VIDEO_CODEC" -b:v 8M -maxrate 16M -bufsize 24M -tag:v hvc1 )
      ;;
    quality)
      VIDEO_ARGS+=( -c:v "$VIDEO_CODEC" -crf "$VIDEO_CRF" -preset slow -tag:v hvc1 )
      ;;
    compat)
      VIDEO_ARGS+=( -c:v "$VIDEO_CODEC" -crf "$VIDEO_CRF" -preset slow -pix_fmt yuv420p )
      ;;
  esac
}

normalize_source_dir() {
  SOURCE_DIR="$(to_abs_path "$SOURCE_DIR")"
  [[ -d "$SOURCE_DIR" ]] || die "Source directory not found: $SOURCE_DIR"
  preflight_source_readable "$SOURCE_DIR"
  SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd)"
}

normalize_dest_dir() {
  DEST_DIR="$(to_abs_path "$DEST_DIR")"
}

# Sets globals: AUTO_CREATED_STAGING_RAMDISK, AUTO_CREATED_STAGING_PATH,
# AUTO_CREATED_STAGING_DEV, STAGING_RAMDISK_CREATED_AT
# Must be called directly (NOT inside $(...)) to preserve global assignments.
create_missing_staging_ramdisk_if_needed() {
  local staging_path="$1"
  local ramdisk_name=""
  local sectors=0
  local dev=""
  local mount_point=""

  STAGING_RAMDISK_CREATE_ERROR=""
  staging_path="${staging_path%/}"

  if [[ -d "$staging_path" ]]; then
    # Already exists; nothing to create.
    return
  fi

  if [[ "$(uname -s)" != "Darwin" ]]; then
    STAGING_RAMDISK_CREATE_ERROR="auto-create is only supported on macOS"
    return
  fi

  case "$staging_path" in
    /Volumes/*)
      ;;
    *)
      STAGING_RAMDISK_CREATE_ERROR="auto-create only supports top-level /Volumes/<Name> paths"
      return
      ;;
  esac

  ramdisk_name="$(basename "$staging_path")"
  if [[ -z "$ramdisk_name" || "$ramdisk_name" == "." || "$ramdisk_name" == ".." ]]; then
    STAGING_RAMDISK_CREATE_ERROR="invalid RAM disk volume name derived from staging path"
    return
  fi

  if [[ "$staging_path" != "/Volumes/$ramdisk_name" ]]; then
    # Only auto-create for top-level /Volumes/<Name>, not nested paths.
    STAGING_RAMDISK_CREATE_ERROR="auto-create only supports top-level /Volumes/<Name> paths"
    return
  fi

  [[ -x "/usr/bin/hdiutil" ]] || die "hdiutil not found; cannot auto-create RAM disk"
  [[ -x "/usr/sbin/diskutil" ]] || die "diskutil not found; cannot auto-create RAM disk"

  sectors=$((STAGING_RAMDISK_MB * 2048))
  vlog_msg "INFO" "Creating RAM disk for staging: /Volumes/$ramdisk_name (${STAGING_RAMDISK_MB}MB)"
  dev="$(/usr/bin/hdiutil attach -nomount "ram://$sectors" 2>/dev/null | /usr/bin/awk 'NR==1 {print $1}' || true)"
  if [[ -z "$dev" ]]; then
    STAGING_RAMDISK_CREATE_ERROR="hdiutil attach failed for ${STAGING_RAMDISK_MB}MB RAM disk"
    return
  fi

  if ! /usr/sbin/diskutil eraseVolume APFS "$ramdisk_name" "$dev" >/dev/null 2>&1; then
    STAGING_RAMDISK_CREATE_ERROR="diskutil eraseVolume failed for RAM disk device $dev"
    /usr/bin/hdiutil detach "$dev" >/dev/null 2>&1 || true
    return
  fi

  mount_point="$(/usr/sbin/diskutil info "$dev" 2>/dev/null | /usr/bin/awk -F': *' '/Mount Point/ {print $2; exit}')"
  if [[ -z "$mount_point" ]]; then
    mount_point="$staging_path"
  fi

  if [[ -d "$mount_point" ]]; then
    AUTO_CREATED_STAGING_RAMDISK=true
    AUTO_CREATED_STAGING_PATH="$mount_point"
    AUTO_CREATED_STAGING_DEV="$dev"
    STAGING_RAMDISK_CREATED_AT="$(date +%s)"
  else
    STAGING_RAMDISK_CREATE_ERROR="RAM disk device $dev was created but mount point was not found (expected $mount_point)"
  fi
}

attempt_unmount_auto_staging_ramdisk() {
  if [[ "$AUTO_CREATED_STAGING_RAMDISK" != true || -z "$AUTO_CREATED_STAGING_PATH" ]]; then
    return
  fi

  if [[ "$AUTO_STAGING_CLEANED_UP" == true ]]; then
    return
  fi
  AUTO_STAGING_CLEANED_UP=true

  if [[ "$(uname -s)" != "Darwin" ]]; then
    return
  fi

  local target="${AUTO_CREATED_STAGING_DEV:-$AUTO_CREATED_STAGING_PATH}"
  if /usr/bin/hdiutil detach "$target" >/dev/null 2>&1; then
    log_msg "INFO" "Auto-created staging RAM disk unmounted: $AUTO_CREATED_STAGING_PATH"
    return
  fi

  if [[ -d "$AUTO_CREATED_STAGING_PATH" ]]; then
    log_msg "WARN" "Could not unmount auto-created staging RAM disk; it remains mounted: $AUTO_CREATED_STAGING_PATH"
  else
    log_msg "INFO" "Auto-created staging RAM disk already gone: $AUTO_CREATED_STAGING_PATH"
  fi
}

cleanup_on_exit() {
  local rc=$?
  trap - ERR
  if [[ "$MAIN_FLOW_STARTED" == true && "$FINAL_REPORT_PRINTED" != true ]]; then
    if [[ "$rc" -eq 0 ]]; then
      log_msg "WARN" "Run exited before final report despite exit=0; this indicates an early-return path"
    else
      log_msg "ERROR" "Run exited before final report (exit=$rc)"
    fi
  fi
  attempt_unmount_auto_staging_ramdisk
  return "$rc"
}

normalize_staging_dir() {
  if [[ "$STAGING_PROVIDED" != true ]]; then
    STAGING_DIR=""
    return
  fi

  STAGING_DIR="$(to_abs_path "$STAGING_DIR")"
  if [[ ! -d "$STAGING_DIR" ]]; then
    create_missing_staging_ramdisk_if_needed "$STAGING_DIR"
    if [[ "$AUTO_CREATED_STAGING_RAMDISK" == true && -d "$AUTO_CREATED_STAGING_PATH" ]]; then
      STAGING_DIR="$AUTO_CREATED_STAGING_PATH"
      log_msg "INFO" "Created staging RAM disk: $STAGING_DIR"
    else
      if [[ -n "$STAGING_RAMDISK_CREATE_ERROR" ]]; then
        die "Staging directory not found and RAM disk auto-create failed: $STAGING_DIR ($STAGING_RAMDISK_CREATE_ERROR)"
      fi
      die "Staging directory not found: $STAGING_DIR"
    fi
  fi
  [[ -d "$STAGING_DIR" ]] || die "Staging directory not found: $STAGING_DIR"
  STAGING_DIR="$(cd "$STAGING_DIR" && pwd)"
  [[ -w "$STAGING_DIR" ]] || die "Staging directory not writable: $STAGING_DIR"

  if path_is_within "$STAGING_DIR" "$SOURCE_DIR"; then
    die "Staging directory must not be inside source: staging=$STAGING_DIR source=$SOURCE_DIR"
  fi
}

collect_extensions() {
  local raw="$EXTENSIONS_CSV"
  local token
  EXT_LIST=()

  IFS=',' read -r -a tokens <<< "$raw"
  for token in "${tokens[@]}"; do
    token="$(printf '%s' "$token" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
    token="${token#.}"
    token="$(printf '%s' "$token" | tr '[:upper:]' '[:lower:]')"
    [[ -n "$token" ]] && EXT_LIST+=("$token")
  done

  if [[ ${#EXT_LIST[@]} -eq 0 ]]; then
    die "No valid extensions after parsing --extensions"
  fi
}

build_find_expr_for_extensions() {
  FIND_EXT_EXPR=()
  local ext
  for ext in "${EXT_LIST[@]}"; do
    FIND_EXT_EXPR+=( -iname "*.${ext}" -o )
  done
  if [[ ${#FIND_EXT_EXPR[@]} -gt 0 ]]; then
    unset 'FIND_EXT_EXPR[${#FIND_EXT_EXPR[@]}-1]'
  fi
}

rel_path_from_source() {
  local abs_file="$1"
  if path_is_within "$abs_file" "$SOURCE_DIR"; then
    printf '%s\n' "${abs_file#$SOURCE_DIR/}"
  else
    printf '%s\n' "$(basename "$abs_file")"
  fi
}

collect_video_files() {
  VIDEO_FILES=()

  if [[ -n "$SINGLE_FILE" ]]; then
    local single_abs
    single_abs="$(to_abs_path "$SINGLE_FILE")"
    [[ -f "$single_abs" ]] || die "--single file not found: $single_abs"
    VIDEO_FILES+=("$single_abs")
    return
  fi

  build_find_expr_for_extensions

  while IFS= read -r -d '' file; do
    if is_apple_noise_file "$file"; then
      vlog_msg "SKIP" "Ignoring Apple artifact: $file"
      continue
    fi
    VIDEO_FILES+=("$file")
  done < <(
    if [[ "$RECURSIVE" == true ]]; then
      find "$SOURCE_DIR" \
        -path "$DEST_DIR" -prune -o \
        -type f \( "${FIND_EXT_EXPR[@]}" \) -print0
    else
      find "$SOURCE_DIR" \
        -maxdepth 1 -type f \( "${FIND_EXT_EXPR[@]}" \) -print0
    fi
  )
}

process_video_file() {
  local input_file="$1"
  local rel_path output_file temp_output_file out_dir
  local display_path
  local encode_input_file
  local staged_input_file=""
  local preferred_temp_dir=""
  local repacked_input_file=""
  local input_size_bytes=0 output_size_bytes=0
  local file_started_at file_ended_at file_real_elapsed_sec
  local encode_started_at encode_ended_at encode_elapsed_sec=0
  local post_elapsed_sec

  file_started_at="$(date +%s)"
  vlog_msg "CHECKPOINT" "file_start: $input_file"

  rel_path="$(rel_path_from_source "$input_file")"
  output_file="$DEST_DIR/${rel_path%.*}.mp4"
  out_dir="$(dirname "$output_file")"
  display_path="${input_file#$PWD/}"
  encode_input_file="$input_file"

  vlog_msg "INFO" "Preparing $rel_path"

  if [[ "$OVERWRITE" != true && -f "$output_file" ]]; then
    vlog_msg "SKIP" "Video exists: $output_file"
    VIDEOS_SKIPPED=$((VIDEOS_SKIPPED + 1))
    return
  fi

  if ! stage_input_for_encode "$encode_input_file"; then
    ERRORS=$((ERRORS + 1))
    cleanup_process_inputs "$STAGED_INPUT_PATH" "$repacked_input_file"
    if [[ "$STAGED_INPUT_FAILURE_RC" -eq 3 ]]; then
      DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1))
      return 3
    fi
    INVALID_SOURCES_SKIPPED=$((INVALID_SOURCES_SKIPPED + 1))
    return 2
  fi
  staged_input_file="$STAGED_INPUT_PATH"
  if [[ -n "$staged_input_file" ]]; then
    encode_input_file="$staged_input_file"
  fi

  if [[ "$DRY_RUN" == true ]]; then
    log_msg "DRY-RUN" "Would transcode: $input_file -> $output_file"
    cleanup_process_inputs "$staged_input_file" "$repacked_input_file"
    return 0
  fi

  if ! source_video_is_readable "$encode_input_file"; then
    ERRORS=$((ERRORS + 1))
    INVALID_SOURCES_SKIPPED=$((INVALID_SOURCES_SKIPPED + 1))
    if [[ "$VERBOSE" == true ]]; then
      log_msg "ERROR" "Skipping unreadable/corrupted source video: $input_file"
    else
      log_progress_start "$display_path"
      log_progress_skipped_unreadable "$display_path"
    fi
    cleanup_process_inputs "$staged_input_file" "$repacked_input_file"
    return 2
  fi

  if [[ "$SOURCE_READABLE_MODE" == "repacked" && -n "$REPACKED_SOURCE_PATH" ]]; then
    repacked_input_file="$REPACKED_SOURCE_PATH"
    encode_input_file="$REPACKED_SOURCE_PATH"
    vlog_msg "INFO" "Encoding from avconvert repacked source: $repacked_input_file"
  fi

  local has_audio=false
  if probe_has_audio "$encode_input_file"; then
    has_audio=true
  fi

  if [[ "$VERBOSE" == true ]]; then
    log_msg "INFO" "ffprobe summary: $input_file"
    print_verbose_probe "$input_file"
    log_msg "INFO" "Audio detected: $has_audio"
  fi

  build_video_args

  preferred_temp_dir="$STAGING_DIR"
  if [[ "$STAGING_PROVIDED" == true && -n "$STAGING_DIR" ]]; then
    if ! staging_has_space_for_input "$STAGING_DIR" "$encode_input_file"; then
      preferred_temp_dir=""
      log_msg "WARN" "Insufficient staging space; using destination temp path for: $input_file"
    fi
  fi

  temp_output_file="$(make_temp_output_file "$output_file" "$preferred_temp_dir")"
  if [[ -z "$temp_output_file" ]]; then
    ERRORS=$((ERRORS + 1))
    DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1))
    log_msg "ERROR" "Could not create temporary output file: $output_file"
    cleanup_process_inputs "$staged_input_file" "$repacked_input_file"
    return 3
  fi
  if [[ "$STAGING_PROVIDED" == true && -n "$STAGING_DIR" ]] && ! path_is_within "$temp_output_file" "$STAGING_DIR"; then
    log_msg "WARN" "Staging unavailable for temp output; using destination directory: $temp_output_file"
  fi

  local cmd=(ffmpeg -hide_banner)
  if [[ "$SOURCE_READABLE_MODE" == "tolerant" ]]; then
    cmd+=( -fflags +genpts -err_detect ignore_err )
  fi
  if [[ "$OVERWRITE" == true ]]; then
    cmd+=( -y )
  else
    cmd+=( -n )
  fi

  cmd+=( -i "$encode_input_file" -map 0:v:0 )

  if [[ "$has_audio" == true ]]; then
    cmd+=( -map 0:a? -c:a aac -b:a 128k )
  fi

  cmd+=( "${VIDEO_ARGS[@]}" -map_metadata 0 -movflags +faststart "$temp_output_file" )

  if [[ "$VERBOSE" == true ]]; then
    log_msg "INFO" "Command: $(join_cmd_for_log "${cmd[@]}")"
  fi

  if [[ "$VERBOSE" != true ]]; then
    log_progress_start "$display_path"
  fi

  vlog_msg "INFO" "Encoding: $input_file -> $output_file (temp: $temp_output_file)"
  if [[ "$FIRST_ENCODE_STARTED_AT" -eq 0 ]]; then
    local startup_warmup_sec=0
    local warmup_elapsed_sec=0
    FIRST_ENCODE_STARTED_AT="$(date +%s)"
    if [[ "$AUTO_CREATED_STAGING_RAMDISK" == true && "$STAGING_RAMDISK_CREATED_AT" -gt 0 ]]; then
      warmup_elapsed_sec=$((FIRST_ENCODE_STARTED_AT - STAGING_RAMDISK_CREATED_AT))
      if [[ "$warmup_elapsed_sec" -lt 0 ]]; then
        warmup_elapsed_sec=0
      fi
    fi
  fi
  encode_started_at="$(date +%s)"
  vlog_msg "CHECKPOINT" "encode_start: $input_file"

  local ffmpeg_rc=0
  local ffmpeg_log=""
  if [[ "$VERBOSE" == true ]]; then
    # Verbose: show ffmpeg output directly
    if run_ffmpeg_with_signal_guard "${cmd[@]}"; then
      :
    else
      ffmpeg_rc=$?
    fi
  else
    # Quiet (default): redirect ffmpeg output; keep log on failure
    ffmpeg_log="$(make_temp_log_file)"
    if [[ -z "$ffmpeg_log" ]]; then
      ERRORS=$((ERRORS + 1))
      DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1))
      log_msg "ERROR" "Could not create temporary ffmpeg log file"
      cleanup_process_inputs "$staged_input_file" "$repacked_input_file"
      return 3
    fi
    if run_ffmpeg_with_signal_guard "${cmd[@]}" >"$ffmpeg_log" 2>&1; then
      rm -f "$ffmpeg_log"
    else
      ffmpeg_rc=$?
    fi
  fi

  encode_ended_at="$(date +%s)"
  encode_elapsed_sec=$((encode_ended_at - encode_started_at))
  vlog_msg "CHECKPOINT" "encode_end: $input_file (encode_elapsed=${encode_elapsed_sec}s)"

  if [[ "$ffmpeg_rc" -ne 0 ]]; then
    # Ctrl+C reached ffmpeg directly (same process group); it stopped mid-encode.
    # Treat any user-triggered abort as a clean stop — no error counters, no noise.
    if [[ "$STOP_AFTER_CURRENT" == true && "$INTERRUPT_COUNT" -gt 0 ]]; then
      cleanup_transcode_artifacts "$temp_output_file" "$output_file"
      cleanup_process_inputs "$staged_input_file" "$repacked_input_file"
      return 4
    fi

    local failure_rc=1
    if destination_cannot_accept_file "$input_file" "$out_dir" "$ffmpeg_log"; then
      failure_rc=3
    elif source_error_from_ffmpeg_log "$ffmpeg_log"; then
      failure_rc=2
    fi

    cleanup_transcode_artifacts "$temp_output_file" "$output_file"
    file_ended_at="$(date +%s)"
    file_real_elapsed_sec=$((file_ended_at - file_started_at))
    local encode_elapsed_fmt
    local real_elapsed_fmt
    encode_elapsed_fmt="$(format_seconds "$encode_elapsed_sec")"
    real_elapsed_fmt="$(format_seconds "$file_real_elapsed_sec")"

    ERRORS=$((ERRORS + 1))
    if [[ "$failure_rc" -eq 2 ]]; then
      INVALID_SOURCES_SKIPPED=$((INVALID_SOURCES_SKIPPED + 1))
    elif [[ "$failure_rc" -eq 3 ]]; then
      DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1))
    fi
    if [[ "$VERBOSE" == true ]]; then
      log_msg "ERROR" "ffmpeg failed: $input_file (real=${file_real_elapsed_sec}s / $real_elapsed_fmt, encode=${encode_elapsed_sec}s / $encode_elapsed_fmt, rc=$ffmpeg_rc)"
    else
      log_progress_failed "$display_path" "$file_real_elapsed_sec" "$encode_elapsed_sec" "$ffmpeg_rc" "$ffmpeg_log"
    fi
    if [[ "$failure_rc" -eq 3 ]]; then
      log_msg "ERROR" "Destination cannot accept more output data: $out_dir"
    fi
    cleanup_process_inputs "$staged_input_file" "$repacked_input_file"
    return "$failure_rc"
  fi

  TOTAL_VIDEO_TIME_SEC=$((TOTAL_VIDEO_TIME_SEC + encode_elapsed_sec))

  if ! restore_metadata_with_exiftool "$encode_input_file" "$temp_output_file"; then
    cleanup_transcode_artifacts "$temp_output_file" "$output_file"
    if [[ "$STOP_AFTER_CURRENT" == true && "$INTERRUPT_COUNT" -gt 0 ]]; then
      cleanup_process_inputs "$staged_input_file" "$repacked_input_file"
      return 4
    fi
    ERRORS=$((ERRORS + 1))
    DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1))
    cleanup_process_inputs "$staged_input_file" "$repacked_input_file"
    return 3
  fi

  if ! map_garmin_model_to_standard_tags "$encode_input_file" "$temp_output_file"; then
    cleanup_transcode_artifacts "$temp_output_file" "$output_file"
    if [[ "$STOP_AFTER_CURRENT" == true && "$INTERRUPT_COUNT" -gt 0 ]]; then
      cleanup_process_inputs "$staged_input_file" "$repacked_input_file"
      return 4
    fi
    ERRORS=$((ERRORS + 1))
    DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1))
    cleanup_process_inputs "$staged_input_file" "$repacked_input_file"
    return 3
  fi

  if ! write_transcode_encoder_metadata "$temp_output_file"; then
    cleanup_transcode_artifacts "$temp_output_file" "$output_file"
    if [[ "$STOP_AFTER_CURRENT" == true && "$INTERRUPT_COUNT" -gt 0 ]]; then
      cleanup_process_inputs "$staged_input_file" "$repacked_input_file"
      return 4
    fi
    ERRORS=$((ERRORS + 1))
    DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1))
    cleanup_process_inputs "$staged_input_file" "$repacked_input_file"
    return 3
  fi

  if ! validate_transcoded_output "$encode_input_file" "$temp_output_file"; then
    cleanup_transcode_artifacts "$temp_output_file" "$output_file"
    ERRORS=$((ERRORS + 1))
    if destination_cannot_accept_file "$input_file" "$out_dir" ""; then
      DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1))
      cleanup_process_inputs "$staged_input_file" "$repacked_input_file"
      return 3
    fi
    # Validation failed but destination is healthy: the source or encoder produced
    # a corrupt/truncated output. Treat as source error so --keep-going can skip it.
    INVALID_SOURCES_SKIPPED=$((INVALID_SOURCES_SKIPPED + 1))
    log_msg "ERROR" "Encode produced invalid output (source may be corrupt): $input_file"
    cleanup_process_inputs "$staged_input_file" "$repacked_input_file"
    return 2
  fi

  touch -r "$input_file" "$temp_output_file" || true

  if ! mkdir -p "$out_dir"; then
    cleanup_transcode_artifacts "$temp_output_file" "$output_file"
    ERRORS=$((ERRORS + 1))
    DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1))
    log_msg "ERROR" "Failed to create destination directory: $out_dir"
    cleanup_process_inputs "$staged_input_file" "$repacked_input_file"
    return 3
  fi

  if [[ "$OVERWRITE" == true ]]; then
    if ! mv -f "$temp_output_file" "$output_file"; then
      cleanup_transcode_artifacts "$temp_output_file" "$output_file"
      ERRORS=$((ERRORS + 1))
      DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1))
      log_msg "ERROR" "Failed to move completed output into place: $output_file"
      cleanup_process_inputs "$staged_input_file" "$repacked_input_file"
      return 3
    fi
  else
    if ! mv -n "$temp_output_file" "$output_file"; then
      cleanup_transcode_artifacts "$temp_output_file" "$output_file"
      ERRORS=$((ERRORS + 1))
      DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1))
      log_msg "ERROR" "Failed to move completed output into place: $output_file"
      cleanup_process_inputs "$staged_input_file" "$repacked_input_file"
      return 3
    fi
  fi

  input_size_bytes="$(file_size_bytes_or_zero "$input_file")"
  output_size_bytes="$(file_size_bytes_or_zero "$output_file")"
  INPUT_BYTES_PROCESSED=$((INPUT_BYTES_PROCESSED + input_size_bytes))
  OUTPUT_BYTES_PROCESSED=$((OUTPUT_BYTES_PROCESSED + output_size_bytes))

  if [[ "$MOVE_SOURCE" == true ]]; then
    if timeout 5s rm -f "$input_file" 2>/dev/null; then
      vlog_msg "INFO" "Removed source after successful validation: $input_file"
    else
      local rm_rc=$?
      if [[ $rm_rc -eq 124 ]]; then
        log_msg "WARN" "Source file removal timed out (NFS unresponsive?): $input_file"
      else
        log_msg "WARN" "Could not remove source file: $input_file"
      fi
    fi
  fi

  file_ended_at="$(date +%s)"
  file_real_elapsed_sec=$((file_ended_at - file_started_at))
  post_elapsed_sec=$((file_real_elapsed_sec - encode_elapsed_sec))
  if [[ "$post_elapsed_sec" -lt 0 ]]; then
    post_elapsed_sec=0
  fi
  TOTAL_FILE_REAL_TIME_SEC=$((TOTAL_FILE_REAL_TIME_SEC + file_real_elapsed_sec))
  vlog_msg "CHECKPOINT" "file_done: $input_file (real=${file_real_elapsed_sec}s encode=${encode_elapsed_sec}s post=${post_elapsed_sec}s)"

  VIDEOS_PROCESSED=$((VIDEOS_PROCESSED + 1))

  if [[ "$VERBOSE" == true ]]; then
    log_msg "INFO" "Transcoded: $output_file (real=${file_real_elapsed_sec}s / $(format_seconds "$file_real_elapsed_sec"), encode=${encode_elapsed_sec}s / $(format_seconds "$encode_elapsed_sec"), post=${post_elapsed_sec}s / $(format_seconds "$post_elapsed_sec"))"
  else
    log_progress_done "$display_path" "$file_real_elapsed_sec" "$encode_elapsed_sec"
  fi

  cleanup_process_inputs "$staged_input_file" "$repacked_input_file"

  return 0
}

main() {
  local total_started_at total_ended_at total_elapsed_sec total_elapsed_fmt
  total_started_at="$(date +%s)"
  RUN_STARTED_AT="$total_started_at"

  if ! pwd >/dev/null 2>&1; then
    log_msg "WARN" "Current working directory no longer exists or is inaccessible; switching to a stable directory before continuing."
    if [[ -n "${HOME:-}" && -d "$HOME" ]]; then
      cd "$HOME" || cd /
    else
      cd /
    fi
  fi

  trap 'handle_interrupt' INT TERM
  trap 'handle_unexpected_error "$LINENO" "$BASH_COMMAND"' ERR
  trap 'cleanup_on_exit' EXIT

  parse_args "$@"
  MAIN_FLOW_STARTED=true
  check_tools

  # Auto single-file detection: if --source points to a file, treat it as single-file mode
  if [[ -z "$SINGLE_FILE" && "$SOURCE_PROVIDED" == true && -f "$SOURCE_DIR" ]]; then
    SINGLE_FILE="$SOURCE_DIR"
    SOURCE_DIR="$(dirname "$SINGLE_FILE")"
  fi

  normalize_source_dir
  normalize_dest_dir

  if path_is_within "$DEST_DIR" "$SOURCE_DIR"; then
    die "Destination must not be inside source. Choose a destination outside source: source=$SOURCE_DIR destination=$DEST_DIR"
  fi

  preflight_destination_writable "$DEST_DIR"
  normalize_staging_dir
  if [[ "$STAGE_INPUT" == true ]]; then
    log_msg "INFO" "Input staging enabled; sources will be copied to staging before encoding"
  fi
  collect_extensions

  detect_encoders
  resolve_encoder

  log_msg "INFO" "Scanning source for videos: $SOURCE_DIR"
  collect_video_files
  if [[ -z "${VIDEO_FILES[*]-}" ]]; then
    log_msg "INFO" "No video files found for extensions: $EXTENSIONS_CSV"
  else
    log_msg "INFO" "Found ${#VIDEO_FILES[@]} candidate video file(s)"
  fi

  local f
  vlog_msg "INFO" "Starting processing loop for ${#VIDEO_FILES[@]} candidate file(s)"
  for f in ${VIDEO_FILES+"${VIDEO_FILES[@]}"}; do
    vlog_msg "CHECKPOINT" "loop_file: $f"
    check_for_quit_key || true
    if [[ "$STOP_AFTER_CURRENT" == true ]]; then
      log_msg "INFO" "Stop requested; ending before next file"
      break
    fi

    local process_rc=0
    vlog_msg "INFO" "Dispatching $(rel_path_from_source "$f")"
    if process_video_file "$f"; then
      process_rc=0
    else
      process_rc=$?
    fi
    vlog_msg "CHECKPOINT" "process_rc=$process_rc file=$f"

    if [[ "$DEBUG_TIMING_LIMIT" -gt 0 ]]; then
      DEBUG_TIMING_FILES=$((DEBUG_TIMING_FILES + 1))
      if [[ "$DEBUG_TIMING_FILES" -ge "$DEBUG_TIMING_LIMIT" ]]; then
        DEBUG_TIMING_STOPPED=true
      fi
    fi

    if [[ "$DEBUG_TIMING_STOPPED" == true ]]; then
      log_msg "INFO" "Debug timing limit reached after $DEBUG_TIMING_FILES file(s); stopping before next file"
      break
    fi

    if [[ "$process_rc" -eq 0 ]]; then
      continue
    fi

    case "$process_rc" in
      2)
        if [[ "$STOP_AFTER_CURRENT" == true && "$INTERRUPT_COUNT" -gt 0 ]]; then
          log_msg "INFO" "Stopped by user after current file"
          break
        fi
        log_msg "INFO" "Continuing after unreadable/corrupted source file"
        continue
        ;;
      3)
        log_msg "ERROR" "Stopping encoding chain because destination is not writable or is out of space"
        break
        ;;
      4)
        log_msg "INFO" "Stopped by user after current file"
        break
        ;;
      *)
        if [[ "$FAIL_FAST" == true ]]; then
          log_msg "ERROR" "Stopping encoding chain due to ffmpeg failure/interruption"
          break
        fi
        log_msg "ERROR" "Continuing after ffmpeg failure because --keep-going is enabled"
        continue
        ;;
    esac
  done

  total_ended_at="$(date +%s)"
  total_elapsed_sec=$((total_ended_at - total_started_at))
  total_elapsed_fmt="$(format_seconds "$total_elapsed_sec")"

  local avg_video_time_sec=0 avg_video_time_fmt="00:00:00"
  local avg_file_real_time_sec=0 avg_file_real_time_fmt="00:00:00"
  local file_post_total_sec=0 file_post_total_fmt="00:00:00"
  local run_non_file_overhead_sec=0 run_non_file_overhead_fmt="00:00:00"
  local startup_warmup_sec=-1 startup_warmup_fmt="n/a"
  local staging_warmup_sec=-1 staging_warmup_fmt="n/a"

  if [[ "$VIDEOS_PROCESSED" -gt 0 ]]; then
    avg_video_time_sec=$((TOTAL_VIDEO_TIME_SEC / VIDEOS_PROCESSED))
    avg_video_time_fmt="$(format_seconds "$avg_video_time_sec")"
    avg_file_real_time_sec=$((TOTAL_FILE_REAL_TIME_SEC / VIDEOS_PROCESSED))
    avg_file_real_time_fmt="$(format_seconds "$avg_file_real_time_sec")"
  fi

  file_post_total_sec=$((TOTAL_FILE_REAL_TIME_SEC - TOTAL_VIDEO_TIME_SEC))
  if [[ "$file_post_total_sec" -lt 0 ]]; then
    file_post_total_sec=0
  fi
  file_post_total_fmt="$(format_seconds "$file_post_total_sec")"

  run_non_file_overhead_sec=$((total_elapsed_sec - TOTAL_FILE_REAL_TIME_SEC))
  if [[ "$run_non_file_overhead_sec" -lt 0 ]]; then
    run_non_file_overhead_sec=0
  fi
  run_non_file_overhead_fmt="$(format_seconds "$run_non_file_overhead_sec")"

  if [[ "$RUN_STARTED_AT" -gt 0 && "$FIRST_ENCODE_STARTED_AT" -gt 0 ]]; then
    startup_warmup_sec=$((FIRST_ENCODE_STARTED_AT - RUN_STARTED_AT))
    if [[ "$startup_warmup_sec" -lt 0 ]]; then
      startup_warmup_sec=0
    fi
    startup_warmup_fmt="$(format_seconds "$startup_warmup_sec")"
  fi

  if [[ "$AUTO_CREATED_STAGING_RAMDISK" == true && "$STAGING_RAMDISK_CREATED_AT" -gt 0 && "$FIRST_ENCODE_STARTED_AT" -gt 0 ]]; then
    staging_warmup_sec=$((FIRST_ENCODE_STARTED_AT - STAGING_RAMDISK_CREATED_AT))
    if [[ "$staging_warmup_sec" -lt 0 ]]; then
      staging_warmup_sec=0
    fi
    staging_warmup_fmt="$(format_seconds "$staging_warmup_sec")"
  fi

  print_final_report \
    "$total_elapsed_sec" \
    "$total_elapsed_fmt" \
    "$file_post_total_sec" \
    "$file_post_total_fmt" \
    "$run_non_file_overhead_sec" \
    "$run_non_file_overhead_fmt" \
    "$avg_file_real_time_sec" \
    "$avg_file_real_time_fmt" \
    "$avg_video_time_sec" \
    "$avg_video_time_fmt" \
    "$startup_warmup_sec" \
    "$startup_warmup_fmt" \
    "$staging_warmup_sec" \
    "$staging_warmup_fmt" \
    "$INPUT_BYTES_PROCESSED" \
    "$OUTPUT_BYTES_PROCESSED"
  FINAL_REPORT_PRINTED=true

  if [[ "$ERRORS" -gt 0 ]]; then
    exit 1
  fi
}

main "$@"