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