summaryrefslogtreecommitdiff
path: root/scripts/stabilize-video
blob: 0647a3680f96a018e373d429f83bfa2a2495842e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#!/usr/bin/env bash
# Stabilize shaky Fujifilm X100V .MOV clips with ffmpeg's 2-pass vidstab
# filter and encode compact 4K HEVC .mp4 output on Intel VAAPI (software
# libx265 fallback when the GPU is unavailable).
#
# Usage:
#   stabilize-video [DIR]                 # stabilize all *.MOV in DIR
#   stabilize-video ~/Videos              # specify a directory
#   stabilize-video clip1.MOV clip2.MOV   # or pass explicit files
#
# Outputs are written next to each source as `stabilised-<base>.mp4`.
# Clips that already have a stabilized output are skipped.

set -euo pipefail

VADEVICE="/dev/dri/renderD128"
QUALITY="${QUALITY:-34}"          # HEVC CQP quality (higher = smaller)
SHAKINESS="${SHAKINESS:-5}"       # vidstab shakiness 1..10
ACCURACY="${ACCURACY:-15}"        # vidstab accuracy 1..15
UNSHARP="unsharp=5:5:0.8:3:3:0.4"

# Collect the list of .MOV files to process.
_mov_files=()
if [[ $# -gt 0 ]]; then
  for arg in "$@"; do
    if [[ -d "$arg" ]]; then
      while IFS= read -r -d '' f; do
        _mov_files+=("$f")
      done < <(find "$arg" -maxdepth 1 -type f -iname '*.MOV' -print0)
    elif [[ -f "$arg" ]]; then
      _mov_files+=("$arg")
    else
      echo "stabilize-video: not a file or directory: $arg" >&2
      exit 1
    fi
  done
else
  while IFS= read -r -d '' f; do
    _mov_files+=("$f")
  done < <(find . -maxdepth 1 -type f -iname '*.MOV' -print0)
fi

if [[ ${#_mov_files[@]} -eq 0 ]]; then
  echo "stabilize-video: no .MOV files found" >&2
  exit 0
fi

# Detect whether VAAPI HEVC is usable on the configured device.
_vaapi_ok=no
if [[ -c "$VADEVICE" ]] \
  && ffmpeg -hide_banner -vaapi_device "$VADEVICE" \
       -f lavfi -i color=black:s=320x240:d=0.1 \
       -vf 'format=nv12,hwupload' \
       -c:v hevc_vaapi -f null - 2>/dev/null; then
  _vaapi_ok=yes
fi

# Pass 1: motion detection (all clips in parallel — CPU-bound, read-only).
_pass1() {
  local src="$1" trf="$2"
  ffmpeg -y -i "$src" \
    -vf "vidstabdetect=shakiness=${SHAKINESS}:accuracy=${ACCURACY}:result=${trf}" \
    -f null - 2>&1
}

# Pass 2: transform + sharpen + encode (serial — GPU is the bottleneck).
_pass2() {
  local src="$1" trf="$2" out="$3"
  local vf="vidstabtransform=input=${trf},${UNSHARP}"
  if [[ "$_vaapi_ok" == yes ]]; then
    ffmpeg -y -vaapi_device "$VADEVICE" -i "$src" \
      -map 0:v:0 -map '0:a:0?' \
      -vf "${vf},format=nv12,hwupload" \
      -c:v hevc_vaapi -rc_mode CQP -global_quality "$QUALITY" -tag:v hvc1 \
      -c:a aac -b:a 128k -movflags +faststart "$out" 2>&1
  else
    ffmpeg -y -i "$src" \
      -map 0:v:0 -map '0:a:0?' \
      -vf "$vf" \
      -c:v libx265 -preset medium -crf 28 -tag:v hvc1 \
      -c:a aac -b:a 128k -movflags +faststart "$out" 2>&1
  fi
}

_base() { basename "${1%.MOV}"; }
_outfor() { printf '%s/stabilised-%s.mp4' "$(dirname "$1")" "$(basename "${1%.MOV}")"; }

main() {
  echo "stabilize-video: ${#_mov_files[@]} clip(s) to process"
  echo "  device : $VADEVICE"
  echo "  quality: $QUALITY (vaapi: $_vaapi_ok)"

  # Build the work list, skipping clips whose output already exists.
  local -a todo=()
  local trf_dir
  trf_dir=$(mktemp -d -t stabilize-video.XXXXXX)
  trap 'rm -rf "$trf_dir"' EXIT

  for src in "${_mov_files[@]}"; do
    local out
    out=$(_outfor "$src")
    if [[ -f "$out" ]]; then
      echo "  skip (exists): $out"
      continue
    fi
    todo+=("$src")
  done

  if [[ ${#todo[@]} -eq 0 ]]; then
    echo "stabilize-video: nothing to do (all outputs present)" >&2
    exit 0
  fi

  # Pass 1 — parallel motion detection.
  echo ""
  echo "==> Pass 1: motion detection"
  local pids=()
  for src in "${todo[@]}"; do
    local base trf
    base=$(_base "$src")
    trf="${trf_dir}/${base}.trf"
    echo "  [P1] $src"
    _pass1 "$src" "$trf" | sed "s/^/    [$base P1] /" &
    pids+=("$!")
  done
  local fail=0
  for pid in "${pids[@]}"; do
    wait "$pid" || ((++fail))
  done
  if ((fail > 0)); then
    echo "stabilize-video: pass 1 failed for $fail clip(s)" >&2
    exit 1
  fi

  # Pass 2 — serial stabilize + encode.
  echo ""
  echo "==> Pass 2: stabilize + encode ($(_vaapi_label))"
  for src in "${todo[@]}"; do
    local base trf out
    base=$(_base "$src")
    trf="${trf_dir}/${base}.trf"
    out=$(_outfor "$src")
    echo "  [P2] $src -> $out"
    _pass2 "$src" "$trf" "$out" | tail -3 | sed "s/^/    [$base P2] /"
  done

  # Verify outputs decode cleanly and report size reduction.
  echo ""
  echo "==> Verify"
  for src in "${todo[@]}"; do
    local base out src_h out_h dur
    base=$(_base "$src")
    out=$(_outfor "$src")
    src_h=$(du -h "$src" | cut -f1)
    out_h=$(du -h "$out" | cut -f1)
    dur=$(ffprobe -v error -show_entries format=duration \
      -of csv=p=0 "$out")
    if ffmpeg -v error -i "$out" -f null - 2>&1; then
      printf '  %-28s %s -> %s  (%ss, decode OK)\n' \
        "$base" "$src_h" "$out_h" "$dur"
    else
      printf '  %-28s %s -> %s  DECODE ERROR\n' "$base" "$src_h" "$out_h" >&2
    fi
  done

  echo ""
  echo "stabilize-video: done"
}

_vaapi_label() {
  if [[ "$_vaapi_ok" == yes ]]; then
    echo "vaapi hevc"
  else
    echo "software libx265"
  fi
}

main "$@"