Moves live RTSP capture to 2nd position so current ambient sound is preferred over a day-old recording when today's scheduled capture is missing. https://claude.ai/code/session_01DANuzLCLhhQ7Li12RTcswY
543 lines
26 KiB
Bash
Executable File
543 lines
26 KiB
Bash
Executable File
#!/bin/bash
|
||
# daily_sunrise_video.sh — collect today's sunrise images, speed-adjust to
|
||
# SUNRISE_TARGET_SECS, burn the local sunrise time vertically in the lower-right
|
||
# corner, and write the final video. Upload is handled by a separate systemd
|
||
# service (sky-cam-sunrise-upload) triggered via OnSuccess= so the two jobs
|
||
# have independent log entries and failure states.
|
||
#
|
||
# Resilience: the speed-adjusted video is saved permanently before the overlay
|
||
# step. If overlay fails, the sped video survives at:
|
||
# <output_dir>/<date>-daily-sunrise-sped.mp4
|
||
# Re-run this script once the issue is resolved; the sped file is deleted
|
||
# automatically when the overlay step succeeds.
|
||
#
|
||
# Usage:
|
||
# ./daily_sunrise_video.sh [cam_name]
|
||
# ./daily_sunrise_video.sh [cam_name] --test [MINUTES]
|
||
#
|
||
# --test MINUTES skips sunrise.py and the capture-window wait; uses the last
|
||
# MINUTES minutes of captured frames (default 2), grabs live
|
||
# RTSP audio, writes <date>-daily-sunrise-test.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"
|
||
|
||
CAM_NAME="${1:-$SUNRISE_CAM}"
|
||
TEST_MODE=false
|
||
TEST_MINUTES=2
|
||
if [[ "${2:-}" == "--test" ]]; then
|
||
TEST_MODE=true
|
||
TEST_MINUTES="${3:-2}"
|
||
fi
|
||
|
||
time_to_seconds() { IFS='-' read -r h m s <<< "$1"; echo $((10#$h * 3600 + 10#$m * 60 + 10#$s)); }
|
||
|
||
# ── Date / paths ──────────────────────────────────────────────────────────────
|
||
current_date=$(date +%Y-%m-%d)
|
||
output_date=$(date +%Y-%m)
|
||
image_dir="$BASE_DIR/$CAM_NAME/$current_date"
|
||
output_dir="$MOVIES_DIR/$CAM_NAME/$output_date/sunrise-only"
|
||
mkdir -p "$output_dir"
|
||
|
||
# ── Sunrise time (skipped in test mode) ───────────────────────────────────────
|
||
SR_TIME=""
|
||
sunrise_time_local=""
|
||
if ! $TEST_MODE; then
|
||
sunrise_time=$(python3 "$SCRIPT_DIR/sunrise.py")
|
||
if [[ -z "$sunrise_time" ]]; then
|
||
"$SCRIPT_DIR/notify.sh" "FAILED: sunrise $current_date" \
|
||
"Could not retrieve sunrise time from sunrise.py" || true
|
||
echo "Error: Failed to retrieve sunrise time."
|
||
exit 1
|
||
fi
|
||
echo "Sunrise (local): $sunrise_time"
|
||
sunrise_time_local=$(TZ="$TIMEZONE" date -d "$sunrise_time" +"%H-%M-%S")
|
||
SR_TIME=$(echo "$sunrise_time_local" | cut -d'-' -f1,2 | tr '-' ':')
|
||
fi
|
||
|
||
# ── Capture window (skipped in test mode) ─────────────────────────────────────
|
||
if ! $TEST_MODE; then
|
||
sunrise_sec=$(time_to_seconds "$sunrise_time_local")
|
||
start_sec=$((sunrise_sec - SUNRISE_PRE_MIN * 60))
|
||
end_sec=$((sunrise_sec + SUNRISE_POST_MIN * 60))
|
||
echo "Window: $((start_sec/3600)):$(printf '%02d' $(((start_sec%3600)/60))) → $((end_sec/3600)):$(printf '%02d' $(((end_sec%3600)/60)))"
|
||
midnight=$(date -d "today 00:00:00" +%s)
|
||
now_day_sec=$(( $(date +%s) - midnight ))
|
||
if [ "$now_day_sec" -lt "$end_sec" ]; then
|
||
wait_sec=$(( end_sec - now_day_sec + 30 ))
|
||
echo "Waiting ${wait_sec}s for capture window to finish (ends $(date -d "@$(( midnight + end_sec ))" '+%H:%M:%S'))..."
|
||
sleep "$wait_sec"
|
||
else
|
||
echo "Capture window already closed at $(date -d "@$(( midnight + end_sec ))" '+%H:%M:%S') — processing available images"
|
||
fi
|
||
fi
|
||
|
||
# ── Collect images ────────────────────────────────────────────────────────────
|
||
if [ ! -d "$image_dir" ]; then
|
||
$TEST_MODE || "$SCRIPT_DIR/notify.sh" "FAILED: sunrise $current_date" \
|
||
"Image directory not found: $image_dir" || true
|
||
echo "Error: image directory not found: $image_dir"
|
||
exit 1
|
||
fi
|
||
|
||
temp_list=$(mktemp --suffix=.txt)
|
||
raw_video=""
|
||
temp_audio=""
|
||
temp_text=""
|
||
temp_live_audio=""
|
||
trap 'rm -f "$temp_list" "$raw_video" "$temp_audio" "$temp_text" "$temp_live_audio" 2>/dev/null || true' EXIT
|
||
|
||
if $TEST_MODE; then
|
||
test_frames=$(( TEST_MINUTES * 60 / ${CAPTURE_INTERVAL:-10} ))
|
||
mapfile -t images < <(find "$image_dir" -name "*.jpg" | sort | tail -n "$test_frames")
|
||
echo "Test mode: last ${TEST_MINUTES} min (${#images[@]} frames)"
|
||
else
|
||
mapfile -t images < <(
|
||
find "$image_dir" -type f -name "*.jpg" | sort | while read -r img; do
|
||
img_sec=$(time_to_seconds "$(basename "$img" .jpg)")
|
||
[[ $img_sec -ge $start_sec && $img_sec -le $end_sec ]] && echo "$img"
|
||
done
|
||
)
|
||
fi
|
||
|
||
if [ "${#images[@]}" -lt 1 ]; then
|
||
$TEST_MODE || "$SCRIPT_DIR/notify.sh" "FAILED: sunrise $current_date" \
|
||
"No images found in sunrise window" || true
|
||
echo "No images found."
|
||
exit 1
|
||
fi
|
||
first_t=$(basename "${images[0]}" .jpg | tr '-' ':')
|
||
last_t=$(basename "${images[-1]}" .jpg | tr '-' ':')
|
||
echo "Images: ${#images[@]} (${first_t} → ${last_t} overlay: ${SR_TIME})"
|
||
|
||
if ! $TEST_MODE; then
|
||
last_sec=$(time_to_seconds "$(basename "${images[-1]}" .jpg)")
|
||
gap=$(( end_sec - last_sec ))
|
||
if [ "$gap" -gt $(( ${CAPTURE_INTERVAL:-10} * 3 )) ]; then
|
||
echo "WARNING: last frame is $(( gap / 60 ))min ${gap}s before window end — capture gap near sunrise"
|
||
echo " Check: systemctl --user status sky-cam-capture-${CAM_NAME}.service"
|
||
fi
|
||
fi
|
||
|
||
# Concat list with explicit frame duration so each frame represents real elapsed
|
||
# time. Without this, all frames collapse to timestamp 0 → one-frame video.
|
||
# The final entry needs a duplicate without duration (ffmpeg concat requirement).
|
||
for img in "${images[@]}"; do
|
||
printf "file '%s'\nduration %s\n" "$img" "${CAPTURE_INTERVAL:-10}"
|
||
done > "$temp_list"
|
||
printf "file '%s'\n" "${images[-1]}" >> "$temp_list"
|
||
|
||
# ── Set overlay time ──────────────────────────────────────────────────────────
|
||
if $TEST_MODE; then
|
||
mid_idx=$(( ${#images[@]} / 2 ))
|
||
mid_hms=$(basename "${images[$mid_idx]}" .jpg)
|
||
SR_TIME=$(echo "$mid_hms" | cut -c1-5 | tr '-' ':')
|
||
echo "Mid-point frame: $mid_hms → overlay time: $SR_TIME"
|
||
fi
|
||
|
||
# ── Font detection ────────────────────────────────────────────────────────────
|
||
# Picks a TrueType font file for ffmpeg's drawtext filter. drawtext needs an
|
||
# absolute path to a .ttf/.otf file — it cannot use a font *name* like Arial.
|
||
#
|
||
# To use a different font:
|
||
# 1. Find the .ttf path on this machine, e.g.:
|
||
# fc-match "Liberation Mono" --format='%{file}\n'
|
||
# find /usr/share/fonts -name '*.ttf' | grep -i mono
|
||
# 2. Either (a) hard-code it below by replacing the fc-match line with:
|
||
# FONT="/usr/share/fonts/truetype/liberation/LiberationMono-Bold.ttf"
|
||
# or (b) change the fc-match query string ("DejaVu Sans:style=Regular")
|
||
# to your preferred face, e.g. "Liberation Mono:style=Bold".
|
||
# 3. Add the chosen path to the fallback list below so the script still works
|
||
# if fc-match is missing.
|
||
# Bold faces (e.g. DejaVuSans-Bold.ttf) read better at small sizes against sky.
|
||
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
|
||
|
||
# ── Step 1: Raw video from images ─────────────────────────────────────────────
|
||
raw_video=$(mktemp --suffix=.mp4)
|
||
echo "Step 1/3: encoding raw video from ${#images[@]} frames..."
|
||
if ! ffmpeg -loglevel warning \
|
||
-f concat -safe 0 -i "$temp_list" \
|
||
-c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SUNRISE" -vsync 2 -an \
|
||
-y "$raw_video"; then
|
||
$TEST_MODE || "$SCRIPT_DIR/notify.sh" "FAILED: sunrise step 1/3 (encode) $current_date" \
|
||
"ffmpeg raw encode from images failed — check: journalctl -u sky-cam-sunrise.service" || true
|
||
exit 1
|
||
fi
|
||
|
||
raw_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$raw_video")
|
||
speed_factor=$(echo "scale=3; $raw_dur / $SUNRISE_TARGET_SECS" | bc)
|
||
echo "Raw: ${raw_dur}s speed factor: ${speed_factor}x"
|
||
|
||
# ── Step 2: Speed-adjust ──────────────────────────────────────────────────────
|
||
# Production: saved permanently so overlay failure is recoverable.
|
||
# Test: temporary file, deleted on exit.
|
||
# fps=25 forces proper frame rate so all source frames appear in the output.
|
||
if $TEST_MODE; then
|
||
sped_video=$(mktemp --suffix=-sped.mp4)
|
||
echo "Step 2/3: speed-adjust (test)..."
|
||
else
|
||
sped_video="$output_dir/$CAM_NAME-$current_date-daily-sunrise-sped.mp4"
|
||
echo "Step 2/3: speed-adjust → $sped_video"
|
||
fi
|
||
if ! ffmpeg -loglevel warning \
|
||
-i "$raw_video" \
|
||
-vf "setpts=PTS/${speed_factor},fps=25" \
|
||
-c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SUNRISE" -an \
|
||
-t "$SUNRISE_TARGET_SECS" \
|
||
-y "$sped_video"; then
|
||
rm -f "$sped_video"
|
||
$TEST_MODE || "$SCRIPT_DIR/notify.sh" "FAILED: sunrise step 2/3 (speed) $current_date" \
|
||
"speed-adjust failed — no video saved" || true
|
||
exit 1
|
||
fi
|
||
|
||
sped_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$sped_video")
|
||
$TEST_MODE || echo "Speed-adjusted: ${sped_dur}s (saved — overlay still pending)"
|
||
$TEST_MODE && echo "Speed-adjusted: ${sped_dur}s"
|
||
|
||
# ── Build overlay filter ──────────────────────────────────────────────────────
|
||
# Burns the sunrise time onto the video using ffmpeg's drawtext filter.
|
||
# Layout is VERTICAL — each character of "HH:MM" is on its own line, stacked
|
||
# top-to-bottom in the lower-right corner. This is achieved by writing each
|
||
# char to a separate line of $temp_text, then drawtext renders that file
|
||
# verbatim (newlines = line breaks).
|
||
#
|
||
# To switch to a HORIZONTAL "HH:MM" overlay, replace the printf below with:
|
||
# printf '%s' "$SR_TIME" > "$temp_text"
|
||
# and bump fontsize (e.g. h/18) since horizontal text needs less vertical room.
|
||
#
|
||
# ── Quick-edit cheatsheet for every drawtext parameter ──────────────────────
|
||
#
|
||
# FONT FILE (set above in the "Font detection" block, not here).
|
||
# drawtext requires a .ttf path, not a font name.
|
||
#
|
||
# fontcolor Text color. Accepts named colors (yellow, white, red, cyan,
|
||
# orange, lime, magenta, gray) or hex (0xFFCC00, 0xFF8800).
|
||
# The "@N" suffix is opacity 0.0–1.0; comes from
|
||
# SUNRISE_OVERLAY_OPACITY in sky-cam.conf. Examples:
|
||
# fontcolor=white@0.7 # softer white, 70% opaque
|
||
# fontcolor=0xFF8800@0.9 # warm orange, 90% opaque
|
||
#
|
||
# fontsize Pixel height of glyphs. "h/22" = video-height / 22, so it
|
||
# scales with resolution (1080p → ~49px, 720p → ~33px). Use a
|
||
# smaller divisor for BIGGER text:
|
||
# fontsize=h/30 → small fontsize=h/22 → current
|
||
# fontsize=h/18 → medium fontsize=h/14 → large
|
||
# Or set an absolute pixel size: fontsize=48
|
||
#
|
||
# line_spacing Pixels of gap between the stacked characters. Increase for
|
||
# a more spaced look (e.g. 8 or 12); 0 packs them tight.
|
||
#
|
||
# x , y Position of the text box. ffmpeg exposes:
|
||
# w = video width h = video height
|
||
# tw = text width th = text height
|
||
# Current values place the box in the BOTTOM-RIGHT with an
|
||
# 18px right margin and a bottom margin of 18px + 5% of height.
|
||
# Recipes for the other corners (keep ~18px breathing room):
|
||
# Top-left: x=18 y=18
|
||
# Top-right: x=w-tw-18 y=18
|
||
# Bottom-left: x=18 y=h-th-18-h*0.05
|
||
# Bottom-right: x=w-tw-18 y=h-th-18-h*0.15 (current)
|
||
# Centered: x=(w-tw)/2 y=(h-th)/2
|
||
# Centered top: x=(w-tw)/2 y=24
|
||
# To nudge the current spot, change the "18" margins or the
|
||
# "h*0.15" lift (15% above the bottom edge — lower to 0.05 to
|
||
# sit closer to the bottom, raise to 0.20 for more clearance).
|
||
#
|
||
# shadowcolor / shadowx / shadowy
|
||
# A 1-pixel black drop-shadow @55% opacity for legibility against
|
||
# bright sky. Set shadowx=shadowy=0 to disable. For a thicker
|
||
# outline, swap the shadow* lines for:
|
||
# :borderw=2:bordercolor=black@0.7
|
||
# (borderw = outline thickness in pixels.)
|
||
#
|
||
# Other useful drawtext options you can append with ":name=value":
|
||
# box=1:boxcolor=black@0.4:boxborderw=6 # solid background pill
|
||
# alpha='if(lt(t,1),t,1)' # 1-second fade-in
|
||
# enable='between(t,2,8)' # only show 2s–8s
|
||
#
|
||
# Full reference: https://ffmpeg.org/ffmpeg-filters.html#drawtext
|
||
DT=""
|
||
if [ "${SUNRISE_OVERLAY_ENABLED:-true}" = "true" ]; then
|
||
# Vertical layout: write each char of "HH:MM" on its own line.
|
||
# SR_TIME is exactly 5 chars (e.g. "06:42"); indices 0..4 below.
|
||
# Switch to horizontal by replacing this printf with: printf '%s' "$SR_TIME" > "$temp_text"
|
||
temp_text=$(mktemp --suffix=.txt)
|
||
printf '%s\n%s\n%s\n%s\n%s' \
|
||
"${SR_TIME:0:1}" "${SR_TIME:1:1}" "${SR_TIME:2:1}" "${SR_TIME:3:1}" "${SR_TIME:4:1}" \
|
||
> "$temp_text"
|
||
|
||
# Source the glyph file + the text. fontfile is omitted if FONT detection
|
||
# failed above, in which case ffmpeg falls back to its built-in font.
|
||
if [ -n "$FONT" ]; then
|
||
DT="drawtext=fontfile='${FONT}':textfile='${temp_text}'"
|
||
else
|
||
DT="drawtext=textfile='${temp_text}'"
|
||
fi
|
||
|
||
# Color + opacity. Change "yellow" to any color name or 0xRRGGBB.
|
||
# Opacity comes from SUNRISE_OVERLAY_OPACITY in sky-cam.conf.
|
||
DT="${DT}:fontcolor=yellow@${SUNRISE_OVERLAY_OPACITY}"
|
||
|
||
# Size + spacing. fontsize is relative to video height (h/22).
|
||
DT="${DT}:fontsize=h/22:line_spacing=4"
|
||
|
||
# Position — bottom-right corner. See the corner recipes in the comment
|
||
# block above. The "18" values are pixel margins; "h*0.15" lifts the box
|
||
# 15% of the height off the bottom to clear the lower edge.
|
||
DT="${DT}:x=w-tw-18:y=h-th-18-h*0.15"
|
||
|
||
# Drop-shadow for legibility. Set shadowx=shadowy=0 to disable, or swap
|
||
# for :borderw=2:bordercolor=black@0.7 for a thicker outline.
|
||
DT="${DT}:shadowcolor=black@0.55:shadowx=1:shadowy=1"
|
||
fi
|
||
|
||
# ── Pick audio source ─────────────────────────────────────────────────────────
|
||
audio_src=""
|
||
audio_offset="0"
|
||
|
||
if $TEST_MODE; then
|
||
# Test: grab live RTSP audio now
|
||
rtsp_var="CAM_RTSP_${CAM_NAME}"
|
||
RTSP_URL="${!rtsp_var:-}"
|
||
if [ -n "$RTSP_URL" ]; then
|
||
temp_live_audio=$(mktemp --suffix=.m4a)
|
||
echo "Test: recording ${SUNRISE_TARGET_SECS}s of live audio..."
|
||
if ffmpeg -loglevel warning \
|
||
-rtsp_transport tcp \
|
||
-i "$RTSP_URL" \
|
||
-t "$SUNRISE_TARGET_SECS" \
|
||
-vn -c:a aac -b:a 64k \
|
||
-y "$temp_live_audio"; then
|
||
audio_src="$temp_live_audio"
|
||
echo "Audio: live RTSP captured OK"
|
||
else
|
||
echo "Audio: live RTSP failed — continuing without audio"
|
||
rm -f "$temp_live_audio"; temp_live_audio=""
|
||
fi
|
||
else
|
||
echo "Audio: CAM_RTSP_${CAM_NAME} not set — skipping"
|
||
fi
|
||
else
|
||
# New path: AUDIO_DIR/sunrise/<cam>/<date>/sunrise-audio.m4a
|
||
# Fallback to old path (BASE_DIR/<cam>/<date>/sunrise-audio.m4a) for migration
|
||
cam_audio="${AUDIO_DIR:-$BASE_DIR/audio}/sunrise/$CAM_NAME/$current_date/sunrise-audio.m4a"
|
||
[ ! -f "$cam_audio" ] && cam_audio="$image_dir/sunrise-audio.m4a"
|
||
if [ "${AUDIO_ENABLED:-false}" = "true" ]; then
|
||
if [ -f "$cam_audio" ]; then
|
||
audio_src="$cam_audio"
|
||
echo "Audio: camera recording (centred on sunrise)"
|
||
fi
|
||
# 2nd fallback: live RTSP — captures current ambient sound right now
|
||
if [ -z "$audio_src" ]; then
|
||
_rtsp_var="CAM_RTSP_${CAM_NAME}"
|
||
_rtsp_url="${!_rtsp_var:-}"
|
||
if [ -n "$_rtsp_url" ]; then
|
||
temp_live_audio=$(mktemp --suffix=.m4a)
|
||
echo "Audio: no saved file — capturing ${SUNRISE_TARGET_SECS}s live from RTSP..."
|
||
if ffmpeg -loglevel warning \
|
||
-rtsp_transport tcp \
|
||
-i "$_rtsp_url" \
|
||
-t "$SUNRISE_TARGET_SECS" \
|
||
-vn -c:a aac -b:a "${CAPTURE_AUDIO_BITRATE:-96k}" \
|
||
-y "$temp_live_audio"; then
|
||
audio_src="$temp_live_audio"
|
||
echo "Audio: live RTSP captured OK"
|
||
else
|
||
rm -f "$temp_live_audio"; temp_live_audio=""
|
||
echo "Audio: live RTSP capture failed — trying yesterday"
|
||
fi
|
||
fi
|
||
fi
|
||
# 3rd fallback: yesterday's recording
|
||
if [ -z "$audio_src" ]; then
|
||
yesterday_date=$(date -d "yesterday" +%Y-%m-%d)
|
||
yday_audio="${AUDIO_DIR:-$BASE_DIR/audio}/sunrise/$CAM_NAME/$yesterday_date/sunrise-audio.m4a"
|
||
[ ! -f "$yday_audio" ] && yday_audio="$BASE_DIR/$CAM_NAME/$yesterday_date/sunrise-audio.m4a"
|
||
if [ -f "$yday_audio" ]; then
|
||
audio_src="$yday_audio"
|
||
echo "Audio: yesterday's recording ($yesterday_date)"
|
||
fi
|
||
fi
|
||
# 4th fallback: library
|
||
if [ -z "$audio_src" ]; then
|
||
library_dir="$SCRIPT_DIR/sunrise-sounds"
|
||
if [ -d "$library_dir" ]; then
|
||
random_file=$(find "$library_dir" -name "*.mp3" | shuf -n 1 2>/dev/null || true)
|
||
if [ -n "$random_file" ]; then
|
||
audio_src="$random_file"
|
||
echo "Audio: library fallback $(basename "$audio_src")"
|
||
fi
|
||
fi
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# ── Step 3: Audio mix + overlay — each independently optional/fallible ────────
|
||
# Step 3a: mixes audio into a temp copy of the sped video (video stream is
|
||
# copied, not re-encoded — fast, and lets step 3b fail independently).
|
||
# Step 3b: burns the overlay onto whatever 3a produced.
|
||
# Failure matrix → upload always fires (production); test just continues:
|
||
# audio ✓ overlay ✓ → final has audio + overlay
|
||
# audio ✓ overlay ✗ → final has audio, no overlay
|
||
# audio ✗ overlay ✓ → final has overlay, no audio
|
||
# audio ✗ overlay ✗ → sped video promoted (no audio, no overlay)
|
||
if $TEST_MODE; then
|
||
demo_dir="${DEMO_DIR:-$MOVIES_DIR/demoio}/$CAM_NAME"
|
||
mkdir -p "$demo_dir"
|
||
final_video="$demo_dir/$CAM_NAME-$current_date-daily-sunrise-test.mp4"
|
||
else
|
||
final_video="$output_dir/$CAM_NAME-$current_date-daily-sunrise.mp4"
|
||
fi
|
||
fade_dur=$(echo "scale=3; $SUNRISE_TARGET_SECS * 0.085" | bc)
|
||
fade_out=$(echo "scale=3; $SUNRISE_TARGET_SECS - $fade_dur" | bc)
|
||
|
||
# ── Step 3a: mix audio (video copied, not re-encoded) ────────────────────────
|
||
work_video="$sped_video"
|
||
has_audio=false
|
||
|
||
if [ -n "$audio_src" ]; then
|
||
temp_audio=$(mktemp --suffix=.mp4)
|
||
echo "Step 3a/3: mixing audio → temp"
|
||
if ffmpeg -loglevel warning \
|
||
-i "$sped_video" \
|
||
-ss "$audio_offset" -t "$SUNRISE_TARGET_SECS" -i "$audio_src" \
|
||
-filter_complex "[1:a]afade=t=in:st=0:d=${fade_dur},afade=t=out:st=${fade_out}:d=${fade_dur}[aout]" \
|
||
-map "0:v" -map "[aout]" \
|
||
-c:v copy -c:a aac -b:a 128k \
|
||
-y "$temp_audio"; then
|
||
work_video="$temp_audio"
|
||
has_audio=true
|
||
echo "Audio: mixed OK"
|
||
else
|
||
rm -f "$temp_audio"; temp_audio=""
|
||
# Retry once with a fresh live RTSP capture before giving up on audio
|
||
_mix_ok=false
|
||
if ! $TEST_MODE; then
|
||
_rtsp_var="CAM_RTSP_${CAM_NAME}"
|
||
_rtsp_url="${!_rtsp_var:-}"
|
||
if [ -n "$_rtsp_url" ]; then
|
||
_live_retry=$(mktemp --suffix=.m4a)
|
||
echo "Audio mix failed — retrying with live RTSP capture..."
|
||
if ffmpeg -loglevel warning \
|
||
-rtsp_transport tcp -i "$_rtsp_url" \
|
||
-t "$SUNRISE_TARGET_SECS" -vn \
|
||
-c:a aac -b:a "${CAPTURE_AUDIO_BITRATE:-96k}" \
|
||
-y "$_live_retry"; then
|
||
temp_audio=$(mktemp --suffix=.mp4)
|
||
if ffmpeg -loglevel warning \
|
||
-i "$sped_video" \
|
||
-ss "$audio_offset" -t "$SUNRISE_TARGET_SECS" -i "$_live_retry" \
|
||
-filter_complex "[1:a]afade=t=in:st=0:d=${fade_dur},afade=t=out:st=${fade_out}:d=${fade_dur}[aout]" \
|
||
-map "0:v" -map "[aout]" \
|
||
-c:v copy -c:a aac -b:a 128k \
|
||
-y "$temp_audio"; then
|
||
work_video="$temp_audio"
|
||
has_audio=true
|
||
echo "Audio: live RTSP retry mix OK"
|
||
_mix_ok=true
|
||
else
|
||
rm -f "$temp_audio"; temp_audio=""
|
||
fi
|
||
fi
|
||
rm -f "$_live_retry"
|
||
fi
|
||
fi
|
||
if ! $_mix_ok; then
|
||
echo "Audio mix failed — step 3b will be overlay-only"
|
||
if ! $TEST_MODE; then
|
||
_jlog=$(journalctl --user -u sky-cam-audio-capture.service -n 30 --no-pager 2>/dev/null \
|
||
|| journalctl -u sky-cam-audio-capture.service -n 30 --no-pager 2>/dev/null \
|
||
|| echo "(journal unavailable)")
|
||
"$SCRIPT_DIR/notify.sh" "WARNING: sunrise audio mix failed $current_date" \
|
||
"$(printf 'Audio could not be mixed — continuing with overlay only\n\naudio-capture log:\n%s' "$_jlog")" || true
|
||
[ "$audio_src" = "${cam_audio:-}" ] && rm -f "${cam_audio:-}"
|
||
fi
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# ── Step 3b: burn overlay (or promote work_video directly if disabled) ────────
|
||
if $has_audio; then audio_out_flags=(-c:a copy); else audio_out_flags=(-an); fi
|
||
|
||
step3b_ok=false
|
||
if [ -n "$DT" ]; then
|
||
echo "Step 3b/3: overlay → $final_video"
|
||
if ffmpeg -loglevel warning \
|
||
-i "$work_video" \
|
||
-vf "${DT}" \
|
||
-c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SUNRISE" \
|
||
"${audio_out_flags[@]}" \
|
||
-y "$final_video"; then
|
||
step3b_ok=true
|
||
else
|
||
if $has_audio; then promote_label="audio-mixed"; else promote_label="speed-only"; fi
|
||
mv "$work_video" "$final_video"
|
||
[ "$work_video" != "$sped_video" ] && rm -f "$sped_video"
|
||
temp_audio=""
|
||
if ! $TEST_MODE; then
|
||
[ -f "${cam_audio:-}" ] && rm -f "${cam_audio:-}"
|
||
echo "Overlay failed — promoting ${promote_label} video as upload target"
|
||
"$SCRIPT_DIR/notify.sh" "WARNING: sunrise overlay failed $current_date" \
|
||
"Overlay failed — uploading ${promote_label} video (no timestamp)" || true
|
||
fi
|
||
step3b_ok=true # degraded but recoverable — upload still fires
|
||
fi
|
||
else
|
||
echo "Step 3b/3: overlay disabled — promoting as final"
|
||
mv "$work_video" "$final_video"
|
||
[ "$work_video" != "$sped_video" ] && rm -f "$sped_video"
|
||
temp_audio=""
|
||
step3b_ok=true
|
||
fi
|
||
|
||
if $step3b_ok; then
|
||
actual_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$final_video")
|
||
echo "Done: $final_video (${actual_dur}s, time: ${SR_TIME})"
|
||
rm -f "$sped_video" "$temp_audio" 2>/dev/null || true; temp_audio=""
|
||
|
||
if $TEST_MODE; then
|
||
# ── Demo retention ────────────────────────────────────────────────────
|
||
demo_retain="${DEMO_RETENTION_DAYS:-8}"
|
||
if [ "$demo_retain" -gt 0 ]; then
|
||
deleted_demo=$(find "${DEMO_DIR:-$MOVIES_DIR/demoio}" \
|
||
-name "*-daily-sunrise-test.mp4" \
|
||
-mtime +"$demo_retain" -print -delete 2>/dev/null | wc -l)
|
||
[ "$deleted_demo" -gt 0 ] && \
|
||
echo "Demo retention: deleted $deleted_demo test file(s) older than ${demo_retain} days" || true
|
||
fi
|
||
else
|
||
[ -f "${cam_audio:-}" ] && rm -f "${cam_audio:-}" || true
|
||
"$SCRIPT_DIR/notify.sh" "Sunrise ready: $current_date" \
|
||
"$(basename "$final_video") — ${actual_dur}s, sunrise at ${SR_TIME} | $final_video" || true
|
||
|
||
# ── Rolling retention ─────────────────────────────────────────────────
|
||
retain="${SUNRISE_RETENTION_DAYS:-10}"
|
||
if [ "$retain" -gt 0 ]; then
|
||
deleted_old=$(find "$MOVIES_DIR/$CAM_NAME" \
|
||
-path "*/sunrise-only/*-daily-sunrise.mp4" \
|
||
-mtime +"$retain" -print -delete 2>/dev/null | wc -l)
|
||
[ "$deleted_old" -gt 0 ] && \
|
||
echo "Retention: deleted $deleted_old sunrise video(s) older than ${retain} days" || true
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
exit 0
|