Fix one-frame bug; add --test mode to daily_sunrise_video.sh
Root cause: the concat list had no 'duration' entries, so all JPEG frames collapsed to timestamp 0 and only one frame survived encoding. Fix: add 'duration CAPTURE_INTERVAL' per entry in both scripts. Also add fps=25 to the speed-adjust step so the VFR source is resampled to a standard frame rate — without this, players see ~6fps and may drop frames to reach the declared rate. daily_sunrise_video.sh --test [MINUTES]: Skips sunrise.py lookup and capture-window wait. Uses the last MINUTES minutes of captured frames (default 2). Overlay shows the mid-point frame's timestamp. Audio grabbed live from RTSP immediately. Output: <date>-daily-sunrise-test.mp4 — no notifications or upload. Usage: ./daily_sunrise_video.sh east --test 2 https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
This commit is contained in:
+185
-105
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# daily_sunrise_video.sh — collect today's sunrise images, speed-adjust to
|
# daily_sunrise_video.sh — collect today's sunrise images, speed-adjust to
|
||||||
# SUNRISE_TARGET_SECS, burn the local sunrise time vertically on the right
|
# SUNRISE_TARGET_SECS, burn the local sunrise time vertically in the lower-right
|
||||||
# side, and write the final video. Upload is handled by a separate systemd
|
# 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
|
# service (sky-cam-sunrise-upload) triggered via OnSuccess= so the two jobs
|
||||||
# have independent log entries and failure states.
|
# have independent log entries and failure states.
|
||||||
#
|
#
|
||||||
@@ -10,60 +10,76 @@
|
|||||||
# <output_dir>/<date>-daily-sunrise-sped.mp4
|
# <output_dir>/<date>-daily-sunrise-sped.mp4
|
||||||
# Re-run this script once the issue is resolved; the sped file is deleted
|
# Re-run this script once the issue is resolved; the sped file is deleted
|
||||||
# automatically when the overlay step succeeds.
|
# 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
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(dirname "$(realpath "$0")")"
|
SCRIPT_DIR="$(dirname "$(realpath "$0")")"
|
||||||
source "$SCRIPT_DIR/sky-cam.conf"
|
source "$SCRIPT_DIR/sky-cam.conf"
|
||||||
|
source "$SCRIPT_DIR/.env" 2>/dev/null || true
|
||||||
export TZ="$TIMEZONE"
|
export TZ="$TIMEZONE"
|
||||||
|
|
||||||
CAM_NAME="${1:-$SUNRISE_CAM}"
|
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 ──────────────────────────────────────────────────────────────
|
# ── Date / paths ──────────────────────────────────────────────────────────────
|
||||||
current_date=$(date +%Y-%m-%d)
|
current_date=$(date +%Y-%m-%d)
|
||||||
output_date=$(date +%Y-%m)
|
output_date=$(date +%Y-%m)
|
||||||
|
|
||||||
image_dir="$BASE_DIR/$CAM_NAME/$current_date"
|
image_dir="$BASE_DIR/$CAM_NAME/$current_date"
|
||||||
output_dir="$MOVIES_DIR/$CAM_NAME/$output_date/sunrise-only"
|
output_dir="$MOVIES_DIR/$CAM_NAME/$output_date/sunrise-only"
|
||||||
mkdir -p "$output_dir"
|
mkdir -p "$output_dir"
|
||||||
|
|
||||||
# ── Sunrise time ──────────────────────────────────────────────────────────────
|
# ── Sunrise time (skipped in test mode) ───────────────────────────────────────
|
||||||
sunrise_time=$(python3 "$SCRIPT_DIR/sunrise.py")
|
SR_TIME=""
|
||||||
if [[ -z "$sunrise_time" ]]; then
|
sunrise_time_local=""
|
||||||
"$SCRIPT_DIR/notify.sh" "FAILED: sunrise $current_date" \
|
if ! $TEST_MODE; then
|
||||||
"Could not retrieve sunrise time from sunrise.py" || true
|
sunrise_time=$(python3 "$SCRIPT_DIR/sunrise.py")
|
||||||
echo "Error: Failed to retrieve sunrise time."
|
if [[ -z "$sunrise_time" ]]; then
|
||||||
exit 1
|
"$SCRIPT_DIR/notify.sh" "FAILED: sunrise $current_date" \
|
||||||
fi
|
"Could not retrieve sunrise time from sunrise.py" || true
|
||||||
echo "Sunrise (UTC): $sunrise_time"
|
echo "Error: Failed to retrieve sunrise time."
|
||||||
|
exit 1
|
||||||
sunrise_time_local=$(TZ="$TIMEZONE" date -d "$sunrise_time" +"%H-%M-%S")
|
fi
|
||||||
echo "Sunrise (local): $sunrise_time_local"
|
echo "Sunrise (UTC): $sunrise_time"
|
||||||
|
sunrise_time_local=$(TZ="$TIMEZONE" date -d "$sunrise_time" +"%H-%M-%S")
|
||||||
# ── Capture window ────────────────────────────────────────────────────────────
|
echo "Sunrise (local): $sunrise_time_local"
|
||||||
time_to_seconds() { IFS='-' read -r h m s <<< "$1"; echo $((10#$h * 3600 + 10#$m * 60 + 10#$s)); }
|
SR_TIME=$(echo "$sunrise_time_local" | cut -d'-' -f1,2 | tr '-' ':')
|
||||||
|
|
||||||
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)))"
|
|
||||||
|
|
||||||
# ── Wait for capture window to finish ────────────────────────────────────────
|
|
||||||
# The timer fires early (default 03:00); we sleep internally so images from
|
|
||||||
# the full window are available before we start encoding.
|
|
||||||
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 in window ──────────────────────────────────────────────────
|
# ── 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
|
if [ ! -d "$image_dir" ]; then
|
||||||
"$SCRIPT_DIR/notify.sh" "FAILED: sunrise $current_date" \
|
$TEST_MODE || "$SCRIPT_DIR/notify.sh" "FAILED: sunrise $current_date" \
|
||||||
"Image directory not found: $image_dir" || true
|
"Image directory not found: $image_dir" || true
|
||||||
echo "Error: image directory not found: $image_dir"
|
echo "Error: image directory not found: $image_dir"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -73,25 +89,47 @@ temp_list=$(mktemp --suffix=.txt)
|
|||||||
raw_video=""
|
raw_video=""
|
||||||
temp_audio=""
|
temp_audio=""
|
||||||
temp_text=""
|
temp_text=""
|
||||||
trap 'rm -f "$temp_list" "$raw_video" "$temp_audio" "$temp_text" 2>/dev/null || true' EXIT
|
temp_live_audio=""
|
||||||
|
trap 'rm -f "$temp_list" "$raw_video" "$temp_audio" "$temp_text" "$temp_live_audio" 2>/dev/null || true' EXIT
|
||||||
|
|
||||||
find "$image_dir" -type f -name "*.jpg" | sort | while read -r img; do
|
if $TEST_MODE; then
|
||||||
img_sec=$(time_to_seconds "$(basename "$img" .jpg)")
|
test_frames=$(( TEST_MINUTES * 60 / ${CAPTURE_INTERVAL:-10} ))
|
||||||
if [[ $img_sec -ge $start_sec && $img_sec -le $end_sec ]]; then
|
mapfile -t images < <(find "$image_dir" -name "*.jpg" | sort | tail -n "$test_frames")
|
||||||
echo "file '$img'"
|
echo "Test mode: last ${TEST_MINUTES} min (${#images[@]} frames)"
|
||||||
fi
|
else
|
||||||
done > "$temp_list"
|
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
|
||||||
|
|
||||||
num_images=$(wc -l < "$temp_list")
|
if [ "${#images[@]}" -lt 1 ]; then
|
||||||
if [[ $num_images -lt 1 ]]; then
|
$TEST_MODE || "$SCRIPT_DIR/notify.sh" "FAILED: sunrise $current_date" \
|
||||||
"$SCRIPT_DIR/notify.sh" "FAILED: sunrise $current_date" \
|
|
||||||
"No images found in sunrise window" || true
|
"No images found in sunrise window" || true
|
||||||
echo "No images found in sunrise window."
|
echo "No images found."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "Images in window: $num_images"
|
echo "Images: ${#images[@]}"
|
||||||
|
|
||||||
# ── Font detection (same fallback chain as montage-mvt.sh) ───────────────────
|
# 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 ────────────────────────────────────────────────────────────
|
||||||
FONT=""
|
FONT=""
|
||||||
if command -v fc-match &>/dev/null; then
|
if command -v fc-match &>/dev/null; then
|
||||||
FONT=$(fc-match "DejaVu Sans:style=Regular" --format="%{file}" 2>/dev/null || true)
|
FONT=$(fc-match "DejaVu Sans:style=Regular" --format="%{file}" 2>/dev/null || true)
|
||||||
@@ -108,12 +146,12 @@ fi
|
|||||||
|
|
||||||
# ── Step 1: Raw video from images ─────────────────────────────────────────────
|
# ── Step 1: Raw video from images ─────────────────────────────────────────────
|
||||||
raw_video=$(mktemp --suffix=.mp4)
|
raw_video=$(mktemp --suffix=.mp4)
|
||||||
echo "Step 1/3: encoding raw video..."
|
echo "Step 1/3: encoding raw video from ${#images[@]} frames..."
|
||||||
if ! ffmpeg -loglevel warning \
|
if ! ffmpeg -loglevel warning \
|
||||||
-f concat -safe 0 -i "$temp_list" \
|
-f concat -safe 0 -i "$temp_list" \
|
||||||
-c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SUNRISE" -vsync 2 -an \
|
-c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SUNRISE" -vsync 2 -an \
|
||||||
-y "$raw_video"; then
|
-y "$raw_video"; then
|
||||||
"$SCRIPT_DIR/notify.sh" "FAILED: sunrise step 1/3 (encode) $current_date" \
|
$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
|
"ffmpeg raw encode from images failed — check: journalctl -u sky-cam-sunrise.service" || true
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -122,26 +160,34 @@ 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)
|
speed_factor=$(echo "scale=3; $raw_dur / $SUNRISE_TARGET_SECS" | bc)
|
||||||
echo "Raw: ${raw_dur}s speed factor: ${speed_factor}x"
|
echo "Raw: ${raw_dur}s speed factor: ${speed_factor}x"
|
||||||
|
|
||||||
# ── Step 2: Speed-adjust — saved permanently so overlay failure is recoverable ─
|
# ── Step 2: Speed-adjust ──────────────────────────────────────────────────────
|
||||||
# Deleted automatically if step 3 succeeds.
|
# Production: saved permanently so overlay failure is recoverable.
|
||||||
sped_video="$output_dir/$current_date-daily-sunrise-sped.mp4"
|
# Test: temporary file, deleted on exit.
|
||||||
echo "Step 2/3: speed-adjust → $sped_video"
|
# 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/$current_date-daily-sunrise-sped.mp4"
|
||||||
|
echo "Step 2/3: speed-adjust → $sped_video"
|
||||||
|
fi
|
||||||
if ! ffmpeg -loglevel warning \
|
if ! ffmpeg -loglevel warning \
|
||||||
-i "$raw_video" \
|
-i "$raw_video" \
|
||||||
-vf "setpts=PTS/${speed_factor}" \
|
-vf "setpts=PTS/${speed_factor},fps=25" \
|
||||||
-c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SUNRISE" -an \
|
-c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SUNRISE" -an \
|
||||||
|
-t "$SUNRISE_TARGET_SECS" \
|
||||||
-y "$sped_video"; then
|
-y "$sped_video"; then
|
||||||
rm -f "$sped_video"
|
rm -f "$sped_video"
|
||||||
"$SCRIPT_DIR/notify.sh" "FAILED: sunrise step 2/3 (speed) $current_date" \
|
$TEST_MODE || "$SCRIPT_DIR/notify.sh" "FAILED: sunrise step 2/3 (speed) $current_date" \
|
||||||
"speed-adjust failed — no video saved" || true
|
"speed-adjust failed — no video saved" || true
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sped_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$sped_video")
|
sped_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$sped_video")
|
||||||
echo "Speed-adjusted: ${sped_dur}s (saved — overlay still pending)"
|
$TEST_MODE || echo "Speed-adjusted: ${sped_dur}s (saved — overlay still pending)"
|
||||||
|
$TEST_MODE && echo "Speed-adjusted: ${sped_dur}s"
|
||||||
|
|
||||||
# ── Build overlay filter (skipped if SUNRISE_OVERLAY_ENABLED=false) ──────────
|
# ── Build overlay filter ──────────────────────────────────────────────────────
|
||||||
SR_TIME=$(echo "$sunrise_time_local" | cut -d'-' -f1,2 | tr '-' ':')
|
|
||||||
DT=""
|
DT=""
|
||||||
if [ "${SUNRISE_OVERLAY_ENABLED:-true}" = "true" ]; then
|
if [ "${SUNRISE_OVERLAY_ENABLED:-true}" = "true" ]; then
|
||||||
temp_text=$(mktemp --suffix=.txt)
|
temp_text=$(mktemp --suffix=.txt)
|
||||||
@@ -159,40 +205,67 @@ if [ "${SUNRISE_OVERLAY_ENABLED:-true}" = "true" ]; then
|
|||||||
DT="${DT}:shadowcolor=black@0.55:shadowx=1:shadowy=1"
|
DT="${DT}:shadowcolor=black@0.55:shadowx=1:shadowy=1"
|
||||||
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:
|
|
||||||
# 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)
|
|
||||||
final_video="$output_dir/$current_date-daily-sunrise.mp4"
|
|
||||||
fade_out=$(echo "scale=1; $SUNRISE_TARGET_SECS - 0.5" | bc)
|
|
||||||
|
|
||||||
# ── Pick audio source ─────────────────────────────────────────────────────────
|
# ── Pick audio source ─────────────────────────────────────────────────────────
|
||||||
audio_src=""
|
audio_src=""
|
||||||
audio_offset="0" # cam audio is pre-centred on sunrise; library files are trimmed to fit
|
audio_offset="0"
|
||||||
|
|
||||||
cam_audio="$image_dir/sunrise-audio.m4a"
|
if $TEST_MODE; then
|
||||||
if [ "${AUDIO_ENABLED:-false}" = "true" ]; then
|
# Test: grab live RTSP audio now
|
||||||
if [ -f "$cam_audio" ]; then
|
rtsp_var="CAM_RTSP_${CAM_NAME}"
|
||||||
# Already exactly SUNRISE_TARGET_SECS, centred on sunrise — no offset needed
|
RTSP_URL="${!rtsp_var:-}"
|
||||||
audio_src="$cam_audio"
|
if [ -n "$RTSP_URL" ]; then
|
||||||
echo "Audio: camera recording (centred on sunrise)"
|
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
|
else
|
||||||
library_dir="$SCRIPT_DIR/sunrise-sounds"
|
echo "Audio: CAM_RTSP_${CAM_NAME} not set — skipping"
|
||||||
if [ -d "$library_dir" ]; then
|
fi
|
||||||
random_file=$(find "$library_dir" -name "*.mp3" | shuf -n 1 2>/dev/null || true)
|
else
|
||||||
if [ -n "$random_file" ]; then
|
cam_audio="$image_dir/sunrise-audio.m4a"
|
||||||
audio_src="$random_file"
|
if [ "${AUDIO_ENABLED:-false}" = "true" ]; then
|
||||||
echo "Audio: library fallback $(basename "$audio_src")"
|
if [ -f "$cam_audio" ]; then
|
||||||
|
audio_src="$cam_audio"
|
||||||
|
echo "Audio: camera recording (centred on sunrise)"
|
||||||
|
else
|
||||||
|
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
|
||||||
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
|
||||||
|
final_video="$output_dir/$current_date-daily-sunrise-test.mp4"
|
||||||
|
else
|
||||||
|
final_video="$output_dir/$current_date-daily-sunrise.mp4"
|
||||||
|
fi
|
||||||
|
fade_out=$(echo "scale=1; $SUNRISE_TARGET_SECS - 0.5" | bc)
|
||||||
|
|
||||||
# ── Step 3a: mix audio (video copied, not re-encoded) ────────────────────────
|
# ── Step 3a: mix audio (video copied, not re-encoded) ────────────────────────
|
||||||
work_video="$sped_video"
|
work_video="$sped_video"
|
||||||
has_audio=false
|
has_audio=false
|
||||||
@@ -213,9 +286,11 @@ if [ -n "$audio_src" ]; then
|
|||||||
else
|
else
|
||||||
rm -f "$temp_audio"; temp_audio=""
|
rm -f "$temp_audio"; temp_audio=""
|
||||||
echo "Audio mix failed — step 3b will be overlay-only"
|
echo "Audio mix failed — step 3b will be overlay-only"
|
||||||
"$SCRIPT_DIR/notify.sh" "WARNING: sunrise audio mix failed $current_date" \
|
if ! $TEST_MODE; then
|
||||||
"Audio could not be mixed — continuing with overlay only" || true
|
"$SCRIPT_DIR/notify.sh" "WARNING: sunrise audio mix failed $current_date" \
|
||||||
[ "$audio_src" = "$cam_audio" ] && rm -f "$cam_audio"
|
"Audio could not be mixed — continuing with overlay only" || true
|
||||||
|
[ "$audio_src" = "${cam_audio:-}" ] && rm -f "${cam_audio:-}"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -237,10 +312,12 @@ if [ -n "$DT" ]; then
|
|||||||
mv "$work_video" "$final_video"
|
mv "$work_video" "$final_video"
|
||||||
[ "$work_video" != "$sped_video" ] && rm -f "$sped_video"
|
[ "$work_video" != "$sped_video" ] && rm -f "$sped_video"
|
||||||
temp_audio=""
|
temp_audio=""
|
||||||
[ -f "$cam_audio" ] && rm -f "$cam_audio"
|
if ! $TEST_MODE; then
|
||||||
echo "Overlay failed — promoting ${promote_label} video as upload target"
|
[ -f "${cam_audio:-}" ] && rm -f "${cam_audio:-}"
|
||||||
"$SCRIPT_DIR/notify.sh" "WARNING: sunrise overlay failed $current_date" \
|
echo "Overlay failed — promoting ${promote_label} video as upload target"
|
||||||
"Overlay failed — uploading ${promote_label} video (no timestamp)" || true
|
"$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
|
step3b_ok=true # degraded but recoverable — upload still fires
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
@@ -253,19 +330,22 @@ fi
|
|||||||
|
|
||||||
if $step3b_ok; then
|
if $step3b_ok; then
|
||||||
actual_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$final_video")
|
actual_dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$final_video")
|
||||||
echo "Done: $final_video (${actual_dur}s, sunrise at ${SR_TIME})"
|
echo "Done: $final_video (${actual_dur}s, time: ${SR_TIME})"
|
||||||
rm -f "$sped_video" "$temp_audio" 2>/dev/null || true; temp_audio=""
|
rm -f "$sped_video" "$temp_audio" 2>/dev/null || true; temp_audio=""
|
||||||
[ -f "$cam_audio" ] && rm -f "$cam_audio"
|
|
||||||
"$SCRIPT_DIR/notify.sh" "Sunrise ready: $current_date" \
|
|
||||||
"$(basename "$final_video") — ${actual_dur}s, sunrise at ${SR_TIME}" || true
|
|
||||||
|
|
||||||
# ── Rolling retention: delete sunrise videos older than SUNRISE_RETENTION_DAYS ─
|
if ! $TEST_MODE; then
|
||||||
retain="${SUNRISE_RETENTION_DAYS:-10}"
|
[ -f "${cam_audio:-}" ] && rm -f "${cam_audio:-}"
|
||||||
if [ "$retain" -gt 0 ]; then
|
"$SCRIPT_DIR/notify.sh" "Sunrise ready: $current_date" \
|
||||||
deleted_old=$(find "$MOVIES_DIR/$CAM_NAME" \
|
"$(basename "$final_video") — ${actual_dur}s, sunrise at ${SR_TIME}" || true
|
||||||
-path "*/sunrise-only/*-daily-sunrise.mp4" \
|
|
||||||
-mtime +"$retain" -print -delete 2>/dev/null | wc -l)
|
# ── Rolling retention ─────────────────────────────────────────────────
|
||||||
[ "$deleted_old" -gt 0 ] && \
|
retain="${SUNRISE_RETENTION_DAYS:-10}"
|
||||||
echo "Retention: deleted $deleted_old sunrise video(s) older than ${retain} days"
|
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"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
+1
-1
@@ -103,7 +103,7 @@ echo " Raw: ${raw_dur}s speed: ${speed}x"
|
|||||||
echo "Step 2/4: speed-adjust to ${SUNRISE_TARGET_SECS}s..."
|
echo "Step 2/4: speed-adjust to ${SUNRISE_TARGET_SECS}s..."
|
||||||
ffmpeg -loglevel warning \
|
ffmpeg -loglevel warning \
|
||||||
-i "$TMP_RAW" \
|
-i "$TMP_RAW" \
|
||||||
-vf "setpts=PTS/$speed" \
|
-vf "setpts=PTS/$speed,fps=25" \
|
||||||
-c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SUNRISE" -an \
|
-c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SUNRISE" -an \
|
||||||
-t "$SUNRISE_TARGET_SECS" \
|
-t "$SUNRISE_TARGET_SECS" \
|
||||||
-y "$TMP_SPED"
|
-y "$TMP_SPED"
|
||||||
|
|||||||
Reference in New Issue
Block a user