ipcam-condense.sh raw
1 #!/bin/bash
2 # Condense IP cam recording to motion segments using scene score time series
3 # Usage: ipcam-condense.sh input.mkv [threshold_mult] [gap_frames]
4 # threshold_mult: multiplier above median scene score (default 2)
5 # gap_frames: bridge gaps narrower than this many frames (default 10)
6 IN="$1"
7 MULT="${2:-3}"
8 GAPF="${3:-10}"
9 PAD=1
10 OUT="${IN%.*}_condensed.mkv"
11 TMP=$(mktemp -d)
12 trap "rm -rf $TMP" EXIT
13
14 # pass 1: extract scene score for every frame (~15s decode, no encode)
15 ffmpeg -i "$IN" -vf "select='gte(scene\,0)',metadata=print:file=$TMP/meta.txt" -an -f null /dev/null 2>/dev/null
16
17 # parse into timestamp,score pairs
18 grep -E 'pts_time|scene_score' "$TMP/meta.txt" | paste - - \
19 | sed 's/.*pts_time://;s/\s*lavfi.scene_score=/,/' > "$TMP/scores.csv"
20
21 NFRAMES=$(wc -l < "$TMP/scores.csv")
22 if [ "$NFRAMES" -eq 0 ]; then
23 echo "no frames in $IN"
24 exit 1
25 fi
26
27 DUR=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$IN")
28 FPS=$(ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate -of csv=p=0 "$IN")
29 # FPS is a fraction like 25/1
30 FPS_N=$(echo "$FPS" | cut -d/ -f1)
31 FPS_D=$(echo "$FPS" | cut -d/ -f2)
32
33 # pass 2: threshold, cluster, bridge gaps
34 awk -F, -v mult="$MULT" -v gapf="$GAPF" -v pad="$PAD" -v dur="$DUR" \
35 -v fps_n="$FPS_N" -v fps_d="$FPS_D" '
36 NR == FNR {
37 raw[NR] = $2 + 0
38 times[NR] = $1 + 0
39 next
40 }
41 FNR == 1 {
42 n = NR - 1
43 # convolution blur: moving average over kernel_r*2+1 frames
44 kernel_r = int(fps_n / fps_d / 2)
45 if (kernel_r < 1) kernel_r = 1
46 for (i = 1; i <= n; i++) {
47 sum = 0; cnt = 0
48 lo = i - kernel_r; if (lo < 1) lo = 1
49 hi = i + kernel_r; if (hi > n) hi = n
50 for (j = lo; j <= hi; j++) { sum += raw[j]; cnt++ }
51 scores[i] = sum / cnt
52 }
53 for (i = 1; i <= n; i++) ss[i] = scores[i]
54 asort(ss)
55 median = ss[int(n/2)]
56 thresh = median * mult
57 gap_sec = gapf * fps_d / fps_n
58 printf "frames=%d blur=%d median=%.6f thresh=%.6f gap=%.2fs\n", n, kernel_r*2+1, median, thresh, gap_sec > "/dev/stderr"
59 }
60 {
61 idx = FNR
62 t = times[idx]
63 sc = scores[idx]
64 if (sc < thresh) next
65
66 if (nc == 0 || t - ce[nc] > gap_sec) {
67 nc++
68 cs[nc] = t; ce[nc] = t
69 } else {
70 ce[nc] = t
71 }
72 }
73 END {
74 if (nc == 0) { print "no motion detected" > "/dev/stderr"; exit 1 }
75
76 # apply padding and clamp
77 kept = 0
78 for (i = 1; i <= nc; i++) {
79 s = cs[i] - pad; if (s < 0) s = 0
80 e = ce[i] + pad; if (e > dur+0) e = dur+0
81 kept++
82 sh=int(s/3600); sm=int((s-sh*3600)/60); ss2=s-sh*3600-sm*60
83 eh=int(e/3600); em=int((e-eh*3600)/60); es=e-eh*3600-em*60
84 printf "%02d:%02d:%06.3f %02d:%02d:%06.3f\n", sh, sm, ss2, eh, em, es
85 }
86 printf "%d segments\n", kept > "/dev/stderr"
87 }' "$TMP/scores.csv" "$TMP/scores.csv" > "$TMP/segments.txt"
88
89 NSEG=$(wc -l < "$TMP/segments.txt")
90 if [ "$NSEG" -eq 0 ]; then
91 echo "no motion segments in $IN"
92 rm "$IN"
93 echo "deleted $IN (no motion)"
94 exit 0
95 fi
96
97 # pass 3: VAAPI encode each segment
98 i=0
99 while read -u 3 s e; do
100 seg="$TMP/seg_$(printf '%04d' $i).mkv"
101 ffmpeg -y -loglevel error -vaapi_device /dev/dri/renderD128 \
102 -ss "$s" -to "$e" -i "$IN" \
103 -vf "format=nv12,hwupload" -c:v h264_vaapi -qp 18 -an "$seg"
104 echo "file '$seg'" >> "$TMP/concat.txt"
105 i=$((i+1))
106 done 3< "$TMP/segments.txt"
107
108 # pass 4: concatenate
109 ffmpeg -y -loglevel error -f concat -safe 0 -i "$TMP/concat.txt" -c copy "$OUT"
110 echo "wrote $OUT ($(du -h "$OUT" | cut -f1))"
111 rm "$IN"
112 echo "deleted $IN"
113