#!/bin/bash # test-sunrise.sh — smoke-test the sunrise pipeline using recent captured frames. # # Uses the last ~10 minutes of JPEGs 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/test3.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" TODAY=$(date +%Y-%m-%d) image_dir="$BASE_DIR/$CAM/$TODAY" NUM_FRAMES=60 # ~10 minutes at 10s interval # ── Collect frames ──────────────────────────────────────────────────────────── if [ ! -d "$image_dir" ]; then echo "ERROR: no frames today at $image_dir" echo " Is capture.sh running? systemctl --user status sky-cam-capture-${CAM}.service" exit 1 fi mapfile -t frames < <(find "$image_dir" -name "*.jpg" | sort | tail -n "$NUM_FRAMES") if [ "${#frames[@]}" -lt 1 ]; then echo "ERROR: no frames found in $image_dir" exit 1 fi echo "Frames: ${#frames[@]} most-recent from $image_dir" # ── 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" # ── Temp files ──────────────────────────────────────────────────────────────── 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 -f "$TMP_LIST" "$TMP_RAW" "$TMP_SPED" "$TMP_AUDIO" "$TMP_TEXT" 2>/dev/null || true; } trap cleanup EXIT # 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. # 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,fps=25" \ -c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SUNRISE" -an \ -t "$SUNRISE_TARGET_SECS" \ -y "$TMP_SPED" # ── Step 3: grab audio ──────────────────────────────────────────────────────── echo "Step 3/4: recording ${SUNRISE_TARGET_SECS}s of live audio from camera..." rtsp_var="CAM_RTSP_${CAM}" RTSP_URL="${!rtsp_var:-}" HAS_AUDIO=false if [ -n "$RTSP_URL" ]; then 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 else echo " Audio: CAM_RTSP_${CAM} not set — skipping" 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=yellow@${SUNRISE_OVERLAY_OPACITY}" DT="${DT}:fontsize=h/22:line_spacing=4" DT="${DT}:x=w-tw-18:y=h-th-18" 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)"