Per-camera capture interval; ambient recorder; fix 4-seasons one-frame bug
Per-camera capture interval: CAPTURE_INTERVAL_<cam> in sky-cam.conf overrides the global value for a specific camera. capture.sh and 4-seasons.sh both respect it. Example: south at 30s saves storage; north at 5s gives smoother motion. 4-seasons.sh one-frame bug fix: Same root cause as the sunrise scripts — concat list had no 'duration' entries, collapsing all JPEG frames to timestamp 0. Fixed by writing 'duration INTERVAL' per frame. Added fps=25 to the speed-adjust step so the VFR source resamples to a standard frame rate. ambient-record.sh (new): Continuous AAC recorder for natural sounds (birds, rain, wind). Saves AMBIENT_CHUNK_SECS chunks to AMBIENT_DIR/<cam>/YYYY-MM-DD/HH-MM-SS.m4a. Reconnects automatically on stream failure. Rolling retention via AMBIENT_RETENTION_DAYS. Enabled by setting AMBIENT_ENABLED=true and listing cameras in AMBIENT_CAMS in sky-cam.conf, then re-running install.sh. https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
This commit is contained in:
+11
-5
@@ -72,6 +72,10 @@ if [ "$total_images" -lt 1 ]; then
|
|||||||
fi
|
fi
|
||||||
echo "Images found: $total_images"
|
echo "Images found: $total_images"
|
||||||
|
|
||||||
|
# ── Per-camera capture interval ───────────────────────────────────────────────
|
||||||
|
interval_var="CAPTURE_INTERVAL_${CAM_NAME}"
|
||||||
|
INTERVAL="${!interval_var:-${CAPTURE_INTERVAL:-10}}"
|
||||||
|
|
||||||
# ── Prepare output directory ──────────────────────────────────────────────────
|
# ── Prepare output directory ──────────────────────────────────────────────────
|
||||||
output_dir="$MOVIES_DIR/$CAM_NAME/$ASTRO_YEAR/$SEASON/Mvt$MVT_NUM"
|
output_dir="$MOVIES_DIR/$CAM_NAME/$ASTRO_YEAR/$SEASON/Mvt$MVT_NUM"
|
||||||
mkdir -p "$output_dir"
|
mkdir -p "$output_dir"
|
||||||
@@ -82,12 +86,14 @@ temp_video="$output_dir/${yesterday}_Mvt${MVT_NUM}-temp.mp4"
|
|||||||
cleanup() { rm -f "$temp_file" "$temp_video" 2>/dev/null || true; }
|
cleanup() { rm -f "$temp_file" "$temp_video" 2>/dev/null || true; }
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
find "$image_dir" -type f -name "*.jpg" | sort | while read -r img; do
|
mapfile -t images < <(find "$image_dir" -type f -name "*.jpg" | sort)
|
||||||
echo "file '$img'"
|
for img in "${images[@]}"; do
|
||||||
|
printf "file '%s'\nduration %s\n" "$img" "$INTERVAL"
|
||||||
done > "$temp_file"
|
done > "$temp_file"
|
||||||
|
printf "file '%s'\n" "${images[-1]}" >> "$temp_file"
|
||||||
|
|
||||||
# ── Step 1: Build raw video at default frame rate ─────────────────────────────
|
# ── Step 1: Build raw video ───────────────────────────────────────────────────
|
||||||
echo "Step 1/2: encoding raw video..."
|
echo "Step 1/2: encoding raw video from ${#images[@]} frames..."
|
||||||
ffmpeg -loglevel warning \
|
ffmpeg -loglevel warning \
|
||||||
-f concat -safe 0 -i "$temp_file" \
|
-f concat -safe 0 -i "$temp_file" \
|
||||||
-c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SEASONS_RAW" -vsync 2 -an \
|
-c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SEASONS_RAW" -vsync 2 -an \
|
||||||
@@ -103,7 +109,7 @@ echo "Step 2/2: speed factor $speed_factor..."
|
|||||||
final_file="$output_dir/${yesterday}_Mvt${MVT_NUM}-Day${DAY_OF_MVT}of${DAYS_IN_MVT}-final.mp4"
|
final_file="$output_dir/${yesterday}_Mvt${MVT_NUM}-Day${DAY_OF_MVT}of${DAYS_IN_MVT}-final.mp4"
|
||||||
if ! ffmpeg -loglevel warning \
|
if ! ffmpeg -loglevel warning \
|
||||||
-i "$temp_video" \
|
-i "$temp_video" \
|
||||||
-vf "setpts=PTS/$speed_factor" \
|
-vf "setpts=PTS/$speed_factor,fps=25" \
|
||||||
-c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SEASONS_FINAL" \
|
-c:v libx264 -pix_fmt yuv420p -preset "$ENCODE_PRESET" -crf "$CRF_SEASONS_FINAL" \
|
||||||
-t "$target_per_clip" -an \
|
-t "$target_per_clip" -an \
|
||||||
-y "$final_file"; then
|
-y "$final_file"; then
|
||||||
|
|||||||
Executable
+79
@@ -0,0 +1,79 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ambient-record.sh <cam-name> — continuous ambient audio recorder.
|
||||||
|
#
|
||||||
|
# Records RTSP audio in AMBIENT_CHUNK_SECS chunks, saves to:
|
||||||
|
# AMBIENT_DIR/<cam>/YYYY-MM-DD/HH-MM-SS.m4a
|
||||||
|
#
|
||||||
|
# Files are named by the recording start time so the collection is easy to
|
||||||
|
# browse and pick from for white-noise / nature sound playback.
|
||||||
|
#
|
||||||
|
# Runs as a permanent systemd service (Type=simple) alongside capture.sh.
|
||||||
|
# Reconnects automatically if the camera stream drops.
|
||||||
|
# Applies rolling retention (AMBIENT_RETENTION_DAYS).
|
||||||
|
#
|
||||||
|
# Camera audio is typically 8 kHz mono (phone quality) — suitable for bird
|
||||||
|
# song, rain, wind, and ambient nature sounds; check your camera's web
|
||||||
|
# interface for a higher sample rate setting if available.
|
||||||
|
|
||||||
|
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="${1:-}"
|
||||||
|
if [ -z "$CAM" ]; then
|
||||||
|
echo "Usage: $0 <camera-name>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rtsp_var="CAM_RTSP_${CAM}"
|
||||||
|
RTSP_URL="${!rtsp_var:-}"
|
||||||
|
if [ -z "$RTSP_URL" ]; then
|
||||||
|
echo "ERROR: CAM_RTSP_${CAM} not set in .env"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
CHUNK="${AMBIENT_CHUNK_SECS:-1800}"
|
||||||
|
BITRATE="${AMBIENT_BITRATE:-96k}"
|
||||||
|
RETAIN="${AMBIENT_RETENTION_DAYS:-30}"
|
||||||
|
SAVE_DIR="${AMBIENT_DIR:-$BASE_DIR/ambient}"
|
||||||
|
|
||||||
|
echo "Ambient recorder started: cam=$CAM chunk=${CHUNK}s bitrate=$BITRATE retain=${RETAIN}d"
|
||||||
|
echo "Saving to: $SAVE_DIR/$CAM/"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
today=$(date +%Y-%m-%d)
|
||||||
|
dir="$SAVE_DIR/$CAM/$today"
|
||||||
|
mkdir -p "$dir"
|
||||||
|
|
||||||
|
timestamp=$(date +%H-%M-%S)
|
||||||
|
outfile="$dir/${timestamp}.m4a"
|
||||||
|
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Recording → $outfile (${CHUNK}s)"
|
||||||
|
|
||||||
|
ffmpeg -loglevel warning \
|
||||||
|
-rtsp_transport tcp \
|
||||||
|
-i "$RTSP_URL" \
|
||||||
|
-t "$CHUNK" \
|
||||||
|
-vn \
|
||||||
|
-c:a aac -b:a "$BITRATE" \
|
||||||
|
-y "$outfile" || true
|
||||||
|
|
||||||
|
if [ ! -s "$outfile" ]; then
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$CAM] recording failed or empty — retrying in 30s"
|
||||||
|
rm -f "$outfile"
|
||||||
|
sleep 30
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
actual=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$outfile" 2>/dev/null || echo "?")
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Saved: $(basename "$outfile") (${actual}s)"
|
||||||
|
|
||||||
|
# Rolling retention
|
||||||
|
if [ "${RETAIN}" -gt 0 ]; then
|
||||||
|
deleted=$(find "$SAVE_DIR/$CAM" -name "*.m4a" -mtime +"$RETAIN" -print -delete 2>/dev/null | wc -l)
|
||||||
|
[ "$deleted" -gt 0 ] && echo "Retention: deleted $deleted file(s) older than ${RETAIN}d"
|
||||||
|
fi
|
||||||
|
done
|
||||||
+2
-1
@@ -31,7 +31,8 @@ if [ -z "$RTSP_URL" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
INTERVAL="${CAPTURE_INTERVAL:-10}"
|
interval_var="CAPTURE_INTERVAL_${CAM}"
|
||||||
|
INTERVAL="${!interval_var:-${CAPTURE_INTERVAL:-10}}"
|
||||||
echo "Starting capture: cam=$CAM interval=${INTERVAL}s"
|
echo "Starting capture: cam=$CAM interval=${INTERVAL}s"
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
|
|||||||
+32
@@ -187,6 +187,38 @@ if [ "${AUDIO_ENABLED:-false}" = "true" ]; then
|
|||||||
timers+=("sky-cam-audio-capture")
|
timers+=("sky-cam-audio-capture")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── Ambient audio recording services ─────────────────────────────────────────
|
||||||
|
# One long-running service per camera in AMBIENT_CAMS.
|
||||||
|
# Enabled alongside the capture services so install.sh restarts them on update.
|
||||||
|
if [ "${AMBIENT_ENABLED:-false}" = "true" ]; then
|
||||||
|
for cam in "${AMBIENT_CAMS[@]}"; do
|
||||||
|
rtsp_var="CAM_RTSP_${cam}"
|
||||||
|
if [ -n "${!rtsp_var:-}" ]; then
|
||||||
|
cat > "$UNIT_DIR/sky-cam-ambient-${cam}.service" <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Sky-Cam — ${cam}: ambient audio recorder
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=$SCRIPT_DIR/ambient-record.sh ${cam}
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=30
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
EOF
|
||||||
|
echo " wrote sky-cam-ambient-${cam}.service"
|
||||||
|
capture_services+=("sky-cam-ambient-${cam}")
|
||||||
|
else
|
||||||
|
echo " WARNING: CAM_RTSP_${cam} not set — skipping ambient service for ${cam}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Per-camera Four Seasons jobs ─────────────────────────────────────────────
|
# ── Per-camera Four Seasons jobs ─────────────────────────────────────────────
|
||||||
# Reads CAMERAS and SCHEDULE_SEASONS_<cam> from sky-cam.conf.
|
# Reads CAMERAS and SCHEDULE_SEASONS_<cam> from sky-cam.conf.
|
||||||
# Script receives the camera name as $1 so it knows which camera to process.
|
# Script receives the camera name as $1 so it knows which camera to process.
|
||||||
|
|||||||
@@ -287,8 +287,16 @@ MONTAGE_MUSIC_LOOPS_Spring_2=2
|
|||||||
# CAPTURE_STALE_SECS should always be at least 2× CAPTURE_INTERVAL.
|
# CAPTURE_STALE_SECS should always be at least 2× CAPTURE_INTERVAL.
|
||||||
CAPTURE_INTERVAL=10
|
CAPTURE_INTERVAL=10
|
||||||
|
|
||||||
|
# Per-camera overrides — uncomment and set to override the global value above.
|
||||||
|
# Useful when cameras serve different purposes (e.g. south records ambient audio
|
||||||
|
# so fewer frames is fine; north faces a busy scene and benefits from 5s).
|
||||||
|
#CAPTURE_INTERVAL_east=10
|
||||||
|
#CAPTURE_INTERVAL_north=10
|
||||||
|
#CAPTURE_INTERVAL_south=30
|
||||||
|
|
||||||
# Seconds without a new frame before the watchdog sends a stall notification.
|
# Seconds without a new frame before the watchdog sends a stall notification.
|
||||||
# A recovery notification fires automatically when capture resumes.
|
# A recovery notification fires automatically when capture resumes.
|
||||||
|
# Must always be at least 2× the effective CAPTURE_INTERVAL for that camera.
|
||||||
CAPTURE_STALE_SECS=30
|
CAPTURE_STALE_SECS=30
|
||||||
|
|
||||||
# ── Sunrise audio capture ──────────────────────────────────────────────────────
|
# ── Sunrise audio capture ──────────────────────────────────────────────────────
|
||||||
@@ -302,6 +310,19 @@ CAPTURE_AUDIO_BITRATE=96k # bitrate for the sunrise audio recording
|
|||||||
# Audio is recorded for exactly SUNRISE_TARGET_SECS, centred on actual sunrise:
|
# Audio is recorded for exactly SUNRISE_TARGET_SECS, centred on actual sunrise:
|
||||||
# floor(TARGET/2) seconds before, ceil(TARGET/2) after (odd second → post-sunrise).
|
# floor(TARGET/2) seconds before, ceil(TARGET/2) after (odd second → post-sunrise).
|
||||||
|
|
||||||
|
# ── Ambient audio library ──────────────────────────────────────────────────────
|
||||||
|
# Continuously records natural sounds (birds, rain, wind) from AMBIENT_CAMS to
|
||||||
|
# build a reusable audio library. Runs as a permanent systemd service alongside
|
||||||
|
# capture.sh. Camera audio is 8 kHz mono (phone quality) — enough for nature
|
||||||
|
# sounds and white-noise style playback.
|
||||||
|
#
|
||||||
|
AMBIENT_ENABLED=false
|
||||||
|
AMBIENT_CAMS=(south) # cameras to record from
|
||||||
|
AMBIENT_DIR="${BASE_DIR}/ambient" # root storage directory
|
||||||
|
AMBIENT_CHUNK_SECS=1800 # seconds per file (default 30 min)
|
||||||
|
AMBIENT_BITRATE=96k # AAC bitrate; 64–96k is plenty at 8 kHz
|
||||||
|
AMBIENT_RETENTION_DAYS=30 # delete files older than this; 0 = keep forever
|
||||||
|
|
||||||
# ── Mattermost — daily sunrise upload ─────────────────────────────────────────
|
# ── Mattermost — daily sunrise upload ─────────────────────────────────────────
|
||||||
# mattermost_url, access_token, channel_id go in .env (see bottom of this file).
|
# mattermost_url, access_token, channel_id go in .env (see bottom of this file).
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user