Open source · Built on generous free tiers

Automatic timelapse
from any HLS stream

Capture frames from any public HLS stream on a configurable schedule, store them in Cloudflare R2, and assemble them into an MP4 timelapse on demand β€” no servers to manage. Modest usage fits within the included GitHub Actions and Cloudflare R2 allowances.

How it flows
πŸ“‘
HLS streamAny public .m3u8 URL
βš™οΈ
Cloudflare Worker cronEvaluates the schedule every 15 minutes in its UTC window, Monday–Saturday
πŸ“·
GitHub Actions + capture.pyffmpeg β†’ JPEG upload
☁️
Cloudflare R2screenshots/YYYY-MM-DD/HH-MM-SS.jpg
🎬
generate.py (on demand)Frames β†’ ffmpeg β†’ MP4 β†’ R2

How it works

streamlapse is a serverless pipeline built on GitHub and Cloudflare. A Cloudflare Worker Cron Trigger runs at 00, 15, 30, and 45 minutes past each hour from 04:00 through 17:45 UTC, Monday through Saturday. It uses the schedule in wrangler.jsonc and dispatches the GitHub Actions capture workflow only during eligible Zagreb-local times. ffmpeg then pulls one JPEG frame from the HLS stream and uploads it to Cloudflare R2. Timelapse generation is a separate on-demand workflow that downloads a selected date range, creates an MP4, and uploads the result to R2.

πŸ“…

Configurable schedule

Set days, hours, timezone, and the 15-minute cadence in the Worker configuration; no repository config download is needed.

☁️

Free storage

Cloudflare R2 includes 10 GB-month of storage and free Internet egress. Storage and operations above the allowance are billed.

βš™οΈ

No servers

Scheduling runs on Cloudflare and media processing runs on standard GitHub-hosted runners, which are free for public repos.

🎬

On-demand timelapse

Trigger MP4 generation from the GitHub Actions UI with a date range, fps, and filename.

Component Role
Cloudflare Worker Evaluates the schedule every 15 minutes in the UTC trigger window and dispatches capture
GitHub Actions Runs capture and on-demand timelapse processing
Cloudflare R2 S3-compatible object storage with a free allowance and free egress
scripts/capture.py Schedule check β†’ grabs JPEG via ffmpeg β†’ uploads to R2
scripts/generate.py Downloads frames from R2 β†’ assembles MP4 β†’ uploads back

Frame path: screenshots/YYYY-MM-DD/HH-MM-SS.jpg  Β·  Video path: videos/timelapse_<from>_to_<to>.mp4

Setup

1. Fork the repo and make it public

Standard GitHub-hosted runners are free for public repositories, subject to GitHub's current Actions usage policy.

  1. Click Fork on this GitHub repo
  2. In your fork go to Settings β†’ General and confirm visibility is Public
  3. Complete the steps below, then push to main

2. Create a Cloudflare R2 bucket

  1. Log in to dash.cloudflare.com β†’ R2 Object Storage β†’ Create bucket
  2. Choose a name (e.g. my-streamlapse) and pick your region β€” EU, US, or APAC
  3. Leave all other settings at defaults and click Create bucket
πŸ’‘ After creation you land on the bucket overview page. Note the Account ID in the top-right corner β€” you'll need it to build your R2_ENDPOINT URL.

3. Create an R2 API token

  1. Click Manage R2 API Tokens β†’ Create API Token
  2. Set Permissions: Object Read & Write and restrict to your specific bucket
  3. Click Create API Token β€” copy the Access Key ID and Secret Access Key now (shown once only)
  4. Copy the Jurisdiction-specific endpoint for S3 clients β€” this is your R2_ENDPOINT

Endpoint by bucket region:

Region R2_ENDPOINT
US (default) https://<account_id>.r2.cloudflarestorage.com
EU https://<account_id>.eu.r2.cloudflarestorage.com
APAC https://<account_id>.apac.r2.cloudflarestorage.com

4. Add GitHub Secrets

In your fork: Settings β†’ Secrets and variables β†’ Actions β†’ New repository secret

Secret Value
STREAM_URL Full HLS stream URL, e.g. https://example.com/stream.m3u8
R2_ACCESS_KEY_ID Access Key ID from your R2 API token
R2_SECRET_ACCESS_KEY Secret Access Key from your R2 API token
R2_BUCKET_NAME Your R2 bucket name, e.g. my-streamlapse
R2_ENDPOINT Full S3 endpoint URL for your bucket's region

5. Configure the schedule

Edit cloudflare/scheduler/wrangler.jsonc. The Cron expression supplies the Monday-through-Saturday, 15-minute cadence and limits invocations to the UTC hours covering both CET and CEST. The variables define the local timezone and active hours, and the Worker performs the exact local-time check to stay correct across Zagreb daylight-saving changes.

cloudflare/scheduler/wrangler.jsonc
"vars": {
  "SCHEDULE_TIME_ZONE": "Europe/Zagreb",
  "SCHEDULE_START": "06:30",
  "SCHEDULE_END": "18:00"
},
"triggers": {
  "crons": ["*/15 4-17 * * MON-SAT"]
}

6. Deploy the Cloudflare scheduler

In cloudflare/scheduler/wrangler.jsonc, set GITHUB_OWNER, GITHUB_REPO, and GITHUB_REF to your fork. Create a fine-grained GitHub token restricted to that repository with Actions: Read and write, then store it as the Worker's GITHUB_TOKEN secret and deploy:

Terminal
cd cloudflare/scheduler
corepack enable
pnpm install
pnpm wrangler login
pnpm wrangler secret put GITHUB_TOKEN
pnpm deploy

For automatic future deployments, create a GitHub environment named production, restrict it to main, and add the CLOUDFLARE_API_TOKEN environment secret plus the CLOUDFLARE_ACCOUNT_ID environment variable.

7. Push and verify

After pushing, go to Actions β†’ Capture Frame β†’ Run workflow. Manual runs always capture. The optional timezone input controls the R2 timestamp and defaults to Europe/Zagreb.

Generating a timelapse

Go to Actions β†’ Generate Timelapse β†’ Run workflow, fill in the optional inputs, and click Run workflow. When the run finishes the MP4 is available as a downloadable artifact in the run summary and is also uploaded to R2 under videos/.

Input Default Description
date_from first day of current month Start date YYYY-MM-DD
date_to today End date YYYY-MM-DD
fps 24 Output video frame rate
output auto-generated Output filename, e.g. april.mp4

Dates must use exact YYYY-MM-DD form and the start date cannot be after the end date. FPS must be a positive integer. A supplied output must be a simple .mp4 basename using only letters, numbers, dots, dashes, or underscores.

Configuration reference

Capture and media settings live in config.yml at the repository root. The complete automatic schedule and repository targeting live in cloudflare/scheduler/wrangler.jsonc.

config.yml β€” full reference
capture:
  jpeg_quality: 3       # ffmpeg -q:v: 1 (best) – 31 (worst); 2–4 is a good range
  ffmpeg_timeout: 30    # seconds before giving up on the stream

storage:
  r2_prefix: 'screenshots'    # R2 key prefix for captured frames
  videos_prefix: 'videos'     # R2 key prefix for generated timelapse videos

generate:
  default_fps: 24              # output video frame rate
  video_scale: '1920:-2'       # width; -2 auto-scales height (must be divisible by 2)
Key Default Description
SCHEDULE_TIME_ZONE Europe/Zagreb IANA timezone in wrangler.jsonc
triggers.crons Mon–Sat UTC weekday, hour-window, and 15-minute cadence
SCHEDULE_START/END 06:30–18:00 Inclusive local-time window in wrangler.jsonc
capture.jpeg_quality 3 ffmpeg -q:v β€” lower is better quality and larger file size
capture.ffmpeg_timeout 30 Seconds before ffmpeg gives up on the stream
generate.default_fps 24 Frames per second for the assembled MP4
generate.video_scale 1920:-2 Output width; -2 auto-scales height preserving aspect ratio

Local development

bash
python -m venv .venv
source .venv/bin/activate      # Windows: .venv\Scripts\activate
pip install --require-hashes -r requirements.txt

# Set required environment variables
export STREAM_URL=https://example.com/stream.m3u8
export R2_ACCESS_KEY_ID=your_access_key_id
export R2_SECRET_ACCESS_KEY=your_secret_access_key
export R2_BUCKET_NAME=my-streamlapse
export R2_ENDPOINT=https://<account_id>.eu.r2.cloudflarestorage.com
export CAPTURE_TIME_ZONE=Europe/Zagreb

# Test capture (requires ffmpeg on PATH)
python scripts/capture.py

# Generate timelapse for a date range
python scripts/generate.py --date-from 2026-04-01 --date-to 2026-04-07 --fps 12