Claude dbdd59677f 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
2026-04-21 21:08:10 +00:00
2026-04-17 23:01:35 -04:00
2026-04-17 22:48:48 -04:00

sky-cam

Automated sky / timelapse camera scripts that produce:

  • Daily sunrise clip — a speed-adjusted video of the sunrise window, uploaded to Mattermost each morning
  • Four Seasons timelapse — daily clips sized to each Vivaldi movement's music duration, assembled automatically into per-movement montages (with music + attribution overlay) and a full-year video
  • Full-day timelapse — a fixed-fps timelapse of every image captured that day, kept for a configurable retention window

Quick start

1. Install dependencies

sudo apt install ffmpeg bc fonts-dejavu curl python3-pip
pip3 install suntime pytz requests
Package Purpose
ffmpeg + ffprobe Encoding, RTSP capture, audio recording, duration probing
bc Floating-point arithmetic for speed factors
fonts-dejavu Text overlays (sunrise time, attribution)
python3 + suntime pytz Astronomical sunrise calculation — pure math, no internet, works indefinitely
python3 + requests Mattermost upload

2. Download

bash <(curl -fsSL https://raw.githubusercontent.com/outis1one/sky-cam/main/bootstrap.sh)
cd sky-cam

Or with a custom install directory:

curl -fsSL https://raw.githubusercontent.com/outis1one/sky-cam/main/bootstrap.sh | bash -s -- /opt/sky-cam
cd /opt/sky-cam

3. Configure

nano sky-cam.conf

SCRIPT_DIR and BASE_DIR are auto-detected — you only need to fill in settings specific to your setup:

Setting What it is
BASE_DIR Root where camera images live — override if storing on a separate drive (BASE_DIR/<cam>/<date>/<HH-MM-SS>.jpg)
MOVIES_DIR Where finished videos are written (default: BASE_DIR/movies)
MUSIC_DIR Directory containing the 12 Vivaldi Four Seasons MP3 files
CAMERAS Space-separated list of camera names, e.g. (east north south west)
SUNRISE_CAM Which camera faces east and gets the sunrise job
LATITUDE / LONGITUDE / TIMEZONE Your location for sunrise calculation
CAPTURE_INTERVAL Seconds between captured frames (default: 10)
SCHEDULE_SUNRISE When to start the sunrise job — default 03:00, script waits internally until the capture window closes
SCHEDULE_SEASONS_<cam> When to run the Four Seasons daily clip — processes yesterday's images; runs after midnight

4. Set up credentials

./install.sh           # generates .env.example alongside systemd units
cp .env.example .env
$EDITOR .env

.env holds all sensitive values and is never committed to git:

# RTSP stream URL — one per camera (variable name matches camera name)
CAM_RTSP_east=rtsp://admin:password@192.168.1.100:554/stream1

# Mattermost upload
mattermost_url=https://your-mattermost.example.com
access_token=your-token
channel_id=your-channel-id

# Notifications (uncomment what you use)
#NTFY_URL=https://ntfy.sh/your-topic
#EMAIL_TO=you@example.com

Re-run ./install.sh any time sky-cam.conf changes (schedules, cameras, etc.).

5. Verify

# Confirm cameras are capturing
systemctl --user status sky-cam-capture-east.service
ls BASE_DIR/east/$(date +%Y-%m-%d)/   # images should appear within CAPTURE_INTERVAL seconds

# Check all timers are scheduled
systemctl --user list-timers 'sky-cam-*'

# Test sunrise calculation
python3 sunrise.py

How it works

Camera RTSP stream
        │
        ├─ capture.sh <cam>  (long-running systemd service, one per camera)
        │    ffmpeg pulls one frame every CAPTURE_INTERVAL seconds
        │    Writes: BASE_DIR/<cam>/YYYY-MM-DD/HH-MM-SS.jpg
        │    Restarts at midnight for the new date directory; auto-reconnects after 30s on loss
        │
        ├─ sunrise-audio-capture.sh  (starts at 03:00, waits internally for sunrise)
        │    Calculates today's sunrise, then records exactly SUNRISE_TARGET_SECS of audio
        │    centred on the moment of sunrise (odd second goes to post-sunrise)
        │    Writes: BASE_DIR/<cam>/YYYY-MM-DD/sunrise-audio.m4a
        │    Deleted automatically after being mixed into the sunrise video
        │
Camera JPEGs + audio
        │
        ├─ daily_sunrise_video.sh  (starts at SCHEDULE_SUNRISE, waits for capture window)
        │    Wakes at SCHEDULE_SUNRISE, sleeps until (sunrise + SUNRISE_POST_MIN)
        │    Step 1: encode raw video from sunrise-window JPEGs
        │    Step 2: speed-adjust to SUNRISE_TARGET_SECS  → saved permanently
        │    Step 3a: mix audio (camera mic → library fallback → no audio)
        │    Step 3b: burn sunrise time overlay (optional, set SUNRISE_OVERLAY_ENABLED=false to skip)
        │    Each step degrades independently — upload always fires
        │    OnSuccess → sunrise2mm.py uploads to Mattermost
        │
        ├─ 4-seasons.sh <cam>  (runs at SCHEDULE_SEASONS_<cam>, processes yesterday)
        │    Step 1: encode all of yesterday's JPEGs into raw video
        │    Step 2: speed-adjust to music_duration / days_in_movement
        │    Last day of movement → triggers montage-mvt.sh
        │         Step 1: concatenate all daily clips
        │         Step 2: speed-adjust to exactly match music  → saved permanently
        │         Step 3: mix music + fades + attribution overlay → Montage.mp4
        │         Last movement of Autumn → triggers year-end-join.sh
        │         On completion → notify with verify-mvt.sh command
        └─ verify-mvt.sh <year> <season> <mvt> <cam>  (run manually after notification)
             Review montage, approve to delete source JPEG folders

Resilience

Every pipeline saves an intermediate file before the riskiest step, so a partial failure always leaves something uploadable:

Sunrise video — four-tier fallback, upload always fires:

Audio Overlay What gets uploaded
Final video with audio + timestamp
Audio-mixed video, no timestamp — overlay failure notified
Video with timestamp, no audio — audio failure notified
Speed-only video — both failures notified

Montage: if music + overlay (step 3) fails, the speed-adjusted silent video is promoted to *-Montage.mp4year-end-join.sh still includes the movement and you get a warning notification.


Notifications

notify.sh sends alerts through any combination of:

Channel Config key(s)
ntfy NTFY_ENABLED=true, NTFY_URL=https://ntfy.sh/your-topic
Email EMAIL_ENABLED=true, EMAIL_TO=you@example.com
Mattermost text post MM_NOTIFY_ENABLED=true, MM_NOTIFY_CHANNEL_ID=<channel-id>

Notifications fire for:

  • Sunrise video ready (with audio/overlay status), upload success/failure
  • Each daily seasons clip saved
  • Montage complete (or degraded if audio/overlay failed)
  • Year-end Four Seasons video complete
  • Any step failure, with the surviving file path named

Camera audio (optional)

If your camera has a microphone, sky-cam records exactly SUNRISE_TARGET_SECS of natural-speed audio centred on the actual sunrise moment and mixes it into the timelapse video.

  1. Set AUDIO_ENABLED=true in sky-cam.conf
  2. Set CAM_RTSP_<cam> in .env for the sunrise camera
  3. Re-run ./install.sh to generate the sky-cam-audio-capture.timer

The recording is split evenly around sunrise (e.g. 5 s before + 5 s after for a 10 s clip; odd second goes to post-sunrise). It is deleted automatically after mixing.

Audio fallback library (optional)

If the camera has no mic, or audio capture fails, daily_sunrise_video.sh picks a random ambient sound from sunrise-sounds/ instead. The library is organised into 11 weather/season folders (clear-spring, rain, thunder, windy, etc.) — populate it once with:

# Get a free API key at https://freesound.org/apiv2/apply/
python3 download-sunrise-sounds.py --api-key YOUR_KEY

Default: ~275 CC-licensed 128 kbps MP3 previews (~25 per category). Re-run any time to top up:

python3 download-sunrise-sounds.py --api-key YOUR_KEY --per-category 40

Attribution data for every file is written to sunrise-sounds/manifest.json.

Audio priority order:

  1. Camera mic recording (sunrise-audio.m4a) — real ambient sound at actual sunrise
  2. Random file from sunrise-sounds/ library
  3. No audio — overlay-only video (always produced regardless)

Manual operations

Re-run today's sunrise (e.g. after a config fix):

./daily_sunrise_video.sh

Re-run a daily seasons clip for yesterday:

./4-seasons.sh east

Rebuild a movement montage (e.g. to retry audio after a failure):

./montage-mvt.sh east                   # uses today's date
./montage-mvt.sh east 2025-06-15        # specific date

Rebuild the year-end video:

./year-end-join.sh 2025 east

Check capture status:

systemctl --user status sky-cam-capture-east.service
journalctl --user -u sky-cam-capture-east.service -f

The capture watchdog runs alongside each camera and sends a notification (via notify.sh) if no new frame arrives within CAPTURE_STALE_SECS seconds, and a second notification when capture resumes. Check watchdog logs with:

journalctl --user -u sky-cam-watchdog-east.service -f

Check logs:

journalctl --user -u sky-cam-sunrise.service
journalctl --user -u sky-cam-seasons-east.service
S
Description
Mirror of outis1one/sky-cam from GitHub
Readme
60 MiB
Languages
Shell 55.2%
Python 44.8%