#!/bin/bash # Condense IP cam recording to motion segments using scene score time series # Usage: ipcam-condense.sh input.mkv [threshold_mult] [gap_frames] # threshold_mult: multiplier above median scene score (default 2) # gap_frames: bridge gaps narrower than this many frames (default 10) IN="$1" MULT="${2:-3}" GAPF="${3:-10}" PAD=1 OUT="${IN%.*}_condensed.mkv" TMP=$(mktemp -d) trap "rm -rf $TMP" EXIT # pass 1: extract scene score for every frame (~15s decode, no encode) ffmpeg -i "$IN" -vf "select='gte(scene\,0)',metadata=print:file=$TMP/meta.txt" -an -f null /dev/null 2>/dev/null # parse into timestamp,score pairs grep -E 'pts_time|scene_score' "$TMP/meta.txt" | paste - - \ | sed 's/.*pts_time://;s/\s*lavfi.scene_score=/,/' > "$TMP/scores.csv" NFRAMES=$(wc -l < "$TMP/scores.csv") if [ "$NFRAMES" -eq 0 ]; then echo "no frames in $IN" exit 1 fi DUR=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$IN") FPS=$(ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate -of csv=p=0 "$IN") # FPS is a fraction like 25/1 FPS_N=$(echo "$FPS" | cut -d/ -f1) FPS_D=$(echo "$FPS" | cut -d/ -f2) # pass 2: threshold, cluster, bridge gaps awk -F, -v mult="$MULT" -v gapf="$GAPF" -v pad="$PAD" -v dur="$DUR" \ -v fps_n="$FPS_N" -v fps_d="$FPS_D" ' NR == FNR { raw[NR] = $2 + 0 times[NR] = $1 + 0 next } FNR == 1 { n = NR - 1 # convolution blur: moving average over kernel_r*2+1 frames kernel_r = int(fps_n / fps_d / 2) if (kernel_r < 1) kernel_r = 1 for (i = 1; i <= n; i++) { sum = 0; cnt = 0 lo = i - kernel_r; if (lo < 1) lo = 1 hi = i + kernel_r; if (hi > n) hi = n for (j = lo; j <= hi; j++) { sum += raw[j]; cnt++ } scores[i] = sum / cnt } for (i = 1; i <= n; i++) ss[i] = scores[i] asort(ss) median = ss[int(n/2)] thresh = median * mult gap_sec = gapf * fps_d / fps_n printf "frames=%d blur=%d median=%.6f thresh=%.6f gap=%.2fs\n", n, kernel_r*2+1, median, thresh, gap_sec > "/dev/stderr" } { idx = FNR t = times[idx] sc = scores[idx] if (sc < thresh) next if (nc == 0 || t - ce[nc] > gap_sec) { nc++ cs[nc] = t; ce[nc] = t } else { ce[nc] = t } } END { if (nc == 0) { print "no motion detected" > "/dev/stderr"; exit 1 } # apply padding and clamp kept = 0 for (i = 1; i <= nc; i++) { s = cs[i] - pad; if (s < 0) s = 0 e = ce[i] + pad; if (e > dur+0) e = dur+0 kept++ sh=int(s/3600); sm=int((s-sh*3600)/60); ss2=s-sh*3600-sm*60 eh=int(e/3600); em=int((e-eh*3600)/60); es=e-eh*3600-em*60 printf "%02d:%02d:%06.3f %02d:%02d:%06.3f\n", sh, sm, ss2, eh, em, es } printf "%d segments\n", kept > "/dev/stderr" }' "$TMP/scores.csv" "$TMP/scores.csv" > "$TMP/segments.txt" NSEG=$(wc -l < "$TMP/segments.txt") if [ "$NSEG" -eq 0 ]; then echo "no motion segments in $IN" rm "$IN" echo "deleted $IN (no motion)" exit 0 fi # pass 3: VAAPI encode each segment i=0 while read -u 3 s e; do seg="$TMP/seg_$(printf '%04d' $i).mkv" ffmpeg -y -loglevel error -vaapi_device /dev/dri/renderD128 \ -ss "$s" -to "$e" -i "$IN" \ -vf "format=nv12,hwupload" -c:v h264_vaapi -qp 18 -an "$seg" echo "file '$seg'" >> "$TMP/concat.txt" i=$((i+1)) done 3< "$TMP/segments.txt" # pass 4: concatenate ffmpeg -y -loglevel error -f concat -safe 0 -i "$TMP/concat.txt" -c copy "$OUT" echo "wrote $OUT ($(du -h "$OUT" | cut -f1))" rm "$IN" echo "deleted $IN"