Files
sky-cam/test-sunrise.sh
T
Claude f3839ce1cf Fix overlay vertical text; test-sunrise captures fresh RTSP frames
Overlay fix (both scripts):
  Switch from text='...\n...' to drawtext's textfile= option. Writing
  each character on its own line eliminates the \n escape ambiguity in
  ffmpeg's filter-string single-quote parser, which was causing 'n' to
  appear literally instead of as a line break.

test-sunrise.sh redesign:
  Now captures NUM_FRAMES fresh RTSP frames (120s at default interval)
  at run time instead of reading pre-existing frames from the capture
  directory. The RTSP URL must be set in .env — the script fails early
  with a clear message if it is not.

https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
2026-04-21 17:14:17 +00:00

180 lines
7.5 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# test-sunrise.sh — smoke-test the sunrise pipeline using freshly captured frames.
#
# Captures NUM_FRAMES × CAPTURE_INTERVAL seconds of RTSP frames from SUNRISE_CAM,
# then runs the same 4-step pipeline as daily_sunrise_video.sh:
# 1. raw video from captured JPEGs
# 2. speed-adjust to SUNRISE_TARGET_SECS
# 3. grab SUNRISE_TARGET_SECS of live RTSP audio
# 4. mix audio + burn vertical time overlay
#
# Usage:
# ./test-sunrise.sh [output_path]
# ./test-sunrise.sh ~/drives/data2-main/test2.mp4
set -euo pipefail
SCRIPT_DIR="$(dirname "$(realpath "$0")")"
source "$SCRIPT_DIR/sky-cam.conf"
source "$SCRIPT_DIR/.env" 2>/dev/null || true
export TZ="$TIMEZONE"
ENCODE_PRESET="${ENCODE_PRESET:-slow}"
CRF_SUNRISE="${CRF_SUNRISE:-28}"
SUNRISE_TARGET_SECS="${SUNRISE_TARGET_SECS:-10}"
SUNRISE_OVERLAY_OPACITY="${SUNRISE_OVERLAY_OPACITY:-0.45}"
OUTPUT="${1:-/tmp/sunrise-test.mp4}"
CAM="$SUNRISE_CAM"
NUM_FRAMES=12 # ~2 minutes at 10s interval
# ── RTSP URL ──────────────────────────────────────────────────────────────────
rtsp_var="CAM_RTSP_${CAM}"
RTSP_URL="${!rtsp_var:-}"
if [ -z "$RTSP_URL" ]; then
echo "ERROR: CAM_RTSP_${CAM} not set in .env"
echo " Add it: CAM_RTSP_${CAM}='rtsp://user:pass@ip:554/stream'"
exit 1
fi
# ── Temp files ────────────────────────────────────────────────────────────────
TMP_FRAME_DIR=$(mktemp -d --suffix=-sunrise-frames)
TMP_LIST=$(mktemp --suffix=.txt)
TMP_RAW=$(mktemp --suffix=.mp4)
TMP_SPED=$(mktemp --suffix=.mp4)
TMP_AUDIO=$(mktemp --suffix=.m4a)
TMP_TEXT=$(mktemp --suffix=.txt)
cleanup() {
rm -rf "$TMP_FRAME_DIR"
rm -f "$TMP_LIST" "$TMP_RAW" "$TMP_SPED" "$TMP_AUDIO" "$TMP_TEXT" 2>/dev/null || true
}
trap cleanup EXIT
# ── Capture fresh frames ─────────────────────────────────────────────────────
CAPTURE_SECS=$(( NUM_FRAMES * ${CAPTURE_INTERVAL:-10} ))
echo "Capturing ${NUM_FRAMES} frames over ${CAPTURE_SECS}s from $CAM..."
ffmpeg -loglevel warning \
-rtsp_transport tcp \
-i "$RTSP_URL" \
-t "$CAPTURE_SECS" \
-vf "fps=1/${CAPTURE_INTERVAL:-10}" \
-f image2 -strftime 1 \
-q:v 2 \
"${TMP_FRAME_DIR}/%H-%M-%S.jpg" || true
mapfile -t frames < <(find "$TMP_FRAME_DIR" -name "*.jpg" | sort)
if [ "${#frames[@]}" -lt 1 ]; then
echo "ERROR: no frames captured from $CAM — check RTSP URL in .env"
exit 1
fi
echo "Captured: ${#frames[@]} frames"
# ── Mid-point frame → overlay time ───────────────────────────────────────────
mid_idx=$(( ${#frames[@]} / 2 ))
mid_frame="${frames[$mid_idx]}"
mid_hms=$(basename "$mid_frame" .jpg) # HH-MM-SS
MID_TIME=$(echo "$mid_hms" | cut -c1-5 | tr '-' ':') # HH:MM
echo "Mid-point frame: $mid_hms → overlay time: $MID_TIME"
# Write each character on its own line for drawtext's textfile option.
# Avoids \n escape ambiguity across ffmpeg filter-string escaping levels.
printf '%s\n%s\n%s\n%s\n%s' \
"${MID_TIME:0:1}" "${MID_TIME:1:1}" "${MID_TIME:2:1}" "${MID_TIME:3:1}" "${MID_TIME:4:1}" \
> "$TMP_TEXT"
# ── Font detection ────────────────────────────────────────────────────────────
FONT=""
if command -v fc-match &>/dev/null; then
FONT=$(fc-match "DejaVu Sans:style=Regular" --format="%{file}" 2>/dev/null || true)
fi
if [ -z "$FONT" ]; then
for f in /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf \
/usr/share/fonts/dejavu/DejaVuSans.ttf \
/usr/share/fonts/TTF/DejaVuSans.ttf \
/usr/share/fonts/truetype/freefont/FreeSans.ttf; do
[ -f "$f" ] && { FONT="$f"; break; }
done
fi
# ── Write concat list ─────────────────────────────────────────────────────────
# Explicit duration per frame so raw video represents real elapsed time:
# 12 frames × CAPTURE_INTERVAL seconds = ~2 minutes.
# The final entry needs a duplicate without duration (ffmpeg concat requirement).
for f in "${frames[@]}"; do
printf "file '%s'\nduration %s\n" "$f" "${CAPTURE_INTERVAL:-10}"
done >> "$TMP_LIST"
printf "file '%s'\n" "${frames[-1]}" >> "$TMP_LIST"
# ── Step 1: raw video from frames ─────────────────────────────────────────────
echo ""
echo "Step 1/4: encoding raw video from ${#frames[@]} frames..."
ffmpeg -loglevel warning \
-f concat -safe 0 -i "$TMP_LIST" \
-c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SUNRISE" -vsync 2 -an \
-y "$TMP_RAW"
raw_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$TMP_RAW")
speed=$(echo "scale=3; $raw_dur / $SUNRISE_TARGET_SECS" | bc)
echo " Raw: ${raw_dur}s speed: ${speed}x"
# ── Step 2: speed-adjust to SUNRISE_TARGET_SECS ───────────────────────────────
echo "Step 2/4: speed-adjust to ${SUNRISE_TARGET_SECS}s..."
ffmpeg -loglevel warning \
-i "$TMP_RAW" \
-vf "setpts=PTS/$speed" \
-c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SUNRISE" -an \
-t "$SUNRISE_TARGET_SECS" \
-y "$TMP_SPED"
# ── Step 3: grab audio centred on mid-point ────────────────────────────────────
echo "Step 3/4: recording ${SUNRISE_TARGET_SECS}s of live audio from camera..."
HAS_AUDIO=false
if ffmpeg -loglevel warning \
-rtsp_transport tcp \
-i "$RTSP_URL" \
-t "$SUNRISE_TARGET_SECS" \
-vn -c:a aac -b:a 64k \
-y "$TMP_AUDIO"; then
HAS_AUDIO=true
echo " Audio: captured OK"
else
echo " Audio: capture failed — continuing without audio"
fi
# ── Step 4: mix audio + burn overlay ─────────────────────────────────────────
echo "Step 4/4: mix audio + overlay → $OUTPUT"
mkdir -p "$(dirname "$OUTPUT")"
FADE_OUT=$(echo "scale=1; $SUNRISE_TARGET_SECS - 0.5" | bc)
if [ -n "$FONT" ]; then
DT="drawtext=fontfile='${FONT}':textfile='${TMP_TEXT}'"
else
DT="drawtext=textfile='${TMP_TEXT}'"
fi
DT="${DT}:fontcolor=white@${SUNRISE_OVERLAY_OPACITY}"
DT="${DT}:fontsize=h/22:line_spacing=4"
DT="${DT}:x=w-tw-18:y=(h-th)/2"
DT="${DT}:shadowcolor=black@0.55:shadowx=1:shadowy=1"
if $HAS_AUDIO; then
ffmpeg -loglevel warning \
-i "$TMP_SPED" -i "$TMP_AUDIO" \
-filter_complex \
"[0:v]${DT}[vout];\
[1:a]afade=t=in:st=0:d=0.5,afade=t=out:st=${FADE_OUT}:d=0.5[aout]" \
-map "[vout]" -map "[aout]" \
-c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SUNRISE" \
-c:a aac -b:a 128k \
-y "$OUTPUT"
else
ffmpeg -loglevel warning \
-i "$TMP_SPED" \
-vf "$DT" \
-c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SUNRISE" -an \
-y "$OUTPUT"
fi
actual=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$OUTPUT")
echo ""
echo "Done: $OUTPUT (${actual}s, overlay: $MID_TIME)"