2026-04-19 20:32:15 -04: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 10-second 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

Prerequisites

Dependency Notes
ffmpeg + ffprobe Encoding, capture, audio recording, duration probing
python3 suntime pytz for sunrise calculation, requests for Mattermost upload — pip3 install suntime pytz requests
fonts-dejavu DejaVu fonts for text overlays — sudo apt install fonts-dejavu
bc Shell arithmetic (floating-point speed factors)
fontconfig (fc-match) Font detection for overlays — optional, falls back to hardcoded paths
IP camera with RTSP stream capture.sh pulls frames directly — no NVR software needed
Vivaldi Four Seasons audio 12 MP3 files named so that *Spring*Mvt*1*, *Summer*Mvt*2*, etc. match with find -iname

Install Python dependencies:

pip3 install suntime pytz requests

Quick start

1. 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

2. Configure

$EDITOR sky-cam.conf

Minimum settings to fill in (everything else has sensible defaults):

Setting What it is
SCRIPT_DIR Full path to this directory
BASE_DIR Root where camera images live (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 audio files
CAMERAS Space-separated list of camera names, e.g. (sunrise north)
SUNRISE_CAM Which camera faces east and gets the sunrise job
LATITUDE / LONGITUDE / TIMEZONE Your location for sunrise calculation
CAM_RTSP_<cam> RTSP stream URL for each camera, e.g. rtsp://admin:pass@192.168.1.100:554/stream1
CAPTURE_INTERVAL Seconds between captured frames (default: 10)
mattermost_url / access_token / channel_id Mattermost upload credentials

3. Install systemd timers

./install.sh           # user-level timers (~/.config/systemd/user), no root needed
# or
./install.sh --system  # system-wide (/etc/systemd/system), requires sudo

Re-run install.sh any time sky-cam.conf changes.

4. Verify

systemctl --user list-timers 'sky-cam-*'
journalctl --user -u sky-cam-sunrise.service -f

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
        │
        ├─ sunrise-audio-capture.sh  (runs at 03:00, waits for sunrise window)
        │    ffmpeg records audio-only from RTSP during sunrise window
        │    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  (runs at SCHEDULE_SUNRISE)
        │    Step 1: encode raw video from sunrise-window JPEGs
        │    Step 2: speed-adjust to SUNRISE_TARGET_SECS  → saved permanently
        │    Step 3: burn sunrise time overlay + mix camera audio (if available)
        │    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
        │
        └─ fullday-video.sh <cam>  (runs at SCHEDULE_FULLDAY_<cam>, processes yesterday)
             Encode all of yesterday's JPEGs at FULLDAY_FPS
             Delete videos older than RETENTION_DAYS

Resilience

Each pipeline saves an intermediate file before the step most likely to fail, so a partial failure leaves a recoverable artifact:

  • Sunrise: if the overlay (step 3) fails, the speed-only video is promoted to the upload target — the upload still happens and you get a notification of the overlay failure
  • 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>

You receive notifications for:

  • Sunrise: video ready, upload success/failure, overlay 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 can mix a natural-speed 10-second audio clip (birds, rain, wind — whatever was actually happening at sunrise) into the daily sunrise video.

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

The audio is recorded during the same window as the images, stored alongside them, and deleted automatically after being mixed into the final video. No audio library or AI required — it's the real sound from your camera.

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.

Priority order for sunrise audio:

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

Manual operations

Re-run today's sunrise (e.g. after fixing a font issue):

./daily_sunrise_video.sh

Re-run a daily seasons clip for a specific date:

./4-seasons.sh <cam>       # reprocesses yesterday

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

./montage-mvt.sh           # uses today's movement
./montage-mvt.sh 2025-06-15 sunrise   # use a specific reference date + camera

Rebuild the year-end video:

./year-end-join.sh 2025 sunrise

Check capture status:

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

Check logs:

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