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
180 lines
7.5 KiB
Bash
Executable File
180 lines
7.5 KiB
Bash
Executable File
#!/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)"
|