Outis d54716c054 Merge pull request #51 from outis1one/claude/fix-mvt-video-playback-NhFjs
4-seasons: fix silent exit in grey-frame filter (read + set -e)
2026-04-23 10:03:19 -04:00
2026-04-22 22:24:28 -04:00
2026-04-23 09:29:00 -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

# export XDG_RUNTIME_DIR so system schedules will show

export XDG_RUNTIME_DIR=/run/user/$(id -u)
systemctl --user list-timers 'sky-cam-sunrise*'

# Add that export to your ~/.bashrc so it's always set in your shell:

echo 'export XDG_RUNTIME_DIR=/run/user/$(id -u)' >> ~/.bashrc
source ~/.bashrc


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

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

./4-seasons.sh east 2026-04-19

Backfill multiple past dates (useful after a camera outage or a fresh install with existing images):

for d in 2026-04-15 2026-04-16 2026-04-17 2026-04-18 2026-04-19; do
    ./4-seasons.sh east "$d"
done

Replace the date list with whatever range you need. Each run produces one *-final.mp4 in the movement's output directory. Once all days for a movement are present you can build the montage manually:

./montage-mvt.sh east 2026-04-19   # date of the last day of that movement

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

Keeping sky-cam up to date

Install git

sudo apt install git

First-time clone (if you don't have the repo yet)

git clone https://github.com/outis1one/sky-cam.git
cd sky-cam

Pull the latest changes from main

After merging a pull request on GitHub, or whenever you want to update your running copy:

git pull origin main

Then re-run install if any schedules or scripts changed:

./install.sh

Check what changed since your last pull

git log --oneline origin/main ^HEAD   # commits on remote not yet on your machine
git diff HEAD origin/main             # full diff of incoming changes
git fetch origin && git status        # fetch first, then show your local state

You have local edits and want to pull anyway

Option A — save your changes first (recommended):

git stash                # temporarily shelve your local edits
git pull origin main     # pull updates
git stash pop            # re-apply your edits on top

If stash pop reports a conflict, open the file and look for the <<<<<< markers — edit to resolve, then git add <file> and git stash drop.

Option B — discard your local edits completely:

git fetch origin
git reset --hard origin/main    # WARNING: your local edits are gone permanently

Use this only when you are sure you do not need your local changes.

Check what you have changed locally

git status              # which files are modified / untracked
git diff                # show the actual changes (unstaged)
git diff --staged       # show changes already staged with git add

Look at the history

git log --oneline               # compact list of commits
git log --oneline -20           # last 20 only
git show <commit-hash>          # full diff for one commit

Switch to a specific branch (e.g. a development branch)

git fetch origin
git checkout claude/seasonal-sunrise-montage-nn268

To switch back to main:

git checkout main
git pull origin main

Undo the last commit (before pushing)

git reset HEAD~1        # undo commit, keep the file changes

Credentials and .env are never in git

.env is listed in .gitignore and will never be overwritten by a pull. sky-cam.conf is in git — if you edited it locally, a pull may conflict. Keep your machine-specific values in .env and leave sky-cam.conf for settings you want to track.

S
Description
Mirror of outis1one/sky-cam from GitHub
Readme
60 MiB
Languages
Shell 55.2%
Python 44.8%