Files
sky-cam/daily_sunrise_video.sh
Claude ed8982da0e Fix afade: bc omits leading zero for values < 1 (.850 → 0.850)
fade_dur = SUNRISE_TARGET_SECS * 0.085 = 0.850 at the default 10s target.
bc outputs .850 without a leading zero, which ffmpeg's afade filter rejects
with "Unable to parse option value .850 as duration". Pipe bc output through
sed to prepend the zero when the value starts with '.'.

https://claude.ai/code/session_01DANuzLCLhhQ7Li12RTcswY
2026-05-01 05:09:57 +00:00

578 lines
28 KiB
Bash
Executable File
Raw Permalink 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
# 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.01.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 2s8s
#
# 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: continuous ambient recording — extract the sunrise window
# Looks in AUDIO_DIR/<cam>/<date>/ for chunk files named HH-MM-SS[.m4a|-tag.m4a]
# and extracts SUNRISE_TARGET_SECS centred on sunrise_sec from the right chunk.
if [ -z "$audio_src" ]; then
_amb_dir="${AUDIO_DIR:-$BASE_DIR/audio}/$CAM_NAME/$current_date"
if [ -d "$_amb_dir" ]; then
_best_chunk=""
_best_sec=-1
while IFS= read -r _f; do
_hms=$(basename "$_f" | cut -c1-8) # first 8 chars = HH-MM-SS
IFS='-' read -r _h _m _s <<< "$_hms"
[[ "$_h" =~ ^[0-9]{2}$ ]] || continue
_cs=$(( 10#$_h * 3600 + 10#$_m * 60 + 10#$_s ))
if [ "$_cs" -le "$sunrise_sec" ] && [ "$_cs" -gt "$_best_sec" ]; then
_best_chunk="$_f"; _best_sec=$_cs
fi
done < <(find "$_amb_dir" -maxdepth 1 -name "*.m4a" 2>/dev/null | sort)
if [ -n "$_best_chunk" ]; then
_offset=$(( sunrise_sec - _best_sec - SUNRISE_TARGET_SECS / 2 ))
[ "$_offset" -lt 0 ] && _offset=0
temp_live_audio=$(mktemp --suffix=.m4a)
echo "Audio: extracting sunrise window from ambient chunk $(basename "$_best_chunk") at +${_offset}s..."
if ffmpeg -loglevel warning \
-ss "$_offset" -i "$_best_chunk" \
-t "$SUNRISE_TARGET_SECS" -vn -c:a copy \
-y "$temp_live_audio"; then
audio_src="$temp_live_audio"
echo "Audio: ambient chunk extracted OK"
else
rm -f "$temp_live_audio"; temp_live_audio=""
echo "Audio: ambient chunk extraction failed"
fi
fi
fi
fi
# 3rd 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
# 4th 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
# 5th 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 | sed 's/^\./0./')
fade_out=$(echo "scale=3; $SUNRISE_TARGET_SECS - $fade_dur" | bc | sed 's/^\./0./')
# ── 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