Argus is a high-performance, tactical surveillance dashboard that aggregates and visualizes global open-data camera feeds. It provides real-time monitoring of ~95,000+ camera nodes across highways, landmarks, and urban centers worldwide.
- Global Surveillance Scale — Comprehensive visualization of ~95,000 nodes across 120+ international sectors.
- Geospatial Rendering — GPU-accelerated tactical mapping utilizing Deck.GL and MapLibre for seamless navigation.
- Adaptive Stream Processing — Intelligent feed management supporting low-latency HLS video and high-frequency imaging.
- Modular Ingestion Framework — Extensible Python-based pipeline for multi-source data aggregation and standardization.
Argus/
├── public/
│ └── cameras.geojson # The main camera dataset (auto-generated)
├── scripts/ # Python Data Pipeline
│ ├── scraper.py # Unified CLI runner — the only script you need
│ ├── scrapers/
│ │ ├── utils.py # Shared helpers (build_feature, log, HEADERS)
│ │ ├── global/
│ │ │ └── windy.py # Windy Webcams (73k+ global)
│ │ ├── usa/
│ │ │ ├── road511.py # Road511 multi-state (20 states)
│ │ │ ├── california/
│ │ │ │ └── caltrans.py # Caltrans CCTV
│ │ │ ├── new_york/
│ │ │ │ └── nyc_dot.py # NYC DOT
│ │ │ └── iowa/
│ │ │ └── iowa511.py # Iowa DOT (ArcGIS)
│ │ ├── canada/
│ │ │ └── bc/
│ │ │ └── drivebc.py # DriveBC
│ │ ├── asia/
│ │ │ └── singapore/
│ │ │ └── lta.py # Singapore LTA
│ │ ├── europe/
│ │ │ └── uk/
│ │ │ └── tfl_london.py# TfL London
│ │ └── oceania/
│ │ └── nz/
│ │ └── nzta.py # NZTA New Zealand
│ └── legacy/ # Retired scripts
├── src/ # React Frontend
└── .env # API Keys (git-ignored)
npm install
npm run devRequirements: Python 3.9+ and pip install requests
API Keys — create a .env file in the project root:
WINDY_API_KEY=your_key_here
VITE_WINDY_API_KEY=your_key_hereGet a free Windy key at api.windy.com — required only for the
windyplugin. All other sources need no key.
All scraping is done through a single unified engine. Run from the scripts/ directory:
cd scriptspython scraper.py --list| Goal | Command |
|---|---|
| Full global run (everything) | python scraper.py --all |
| Windy only (the big 73k run) | python scraper.py --plugins windy |
| All fast sources, skip Windy | python scraper.py --all --exclude windy |
| Specific plugins | python scraper.py --plugins drivebc tfl_london nyc_dot |
| Remove stale cameras & refresh | python scraper.py --all --replace-source |
| Nuke and rebuild from scratch | python scraper.py --all --fresh |
| Custom output path | python scraper.py --all --output ../public/cameras.geojson |
| Run plugins in parallel | python scraper.py --all --exclude windy --parallel |
Default output:
public/cameras.geojson— the React app reads directly from this file.
The road511_usa plugin supports a --states flag to target specific states. This bypasses all exclusion rules, so you can also pull states normally handled by dedicated plugins (CA, IA, NY) to check for gaps.
| Goal | Command |
|---|---|
| Refresh one state | python scraper.py --plugins road511_usa --states CO |
| Refresh multiple states | python scraper.py --plugins road511_usa --states FL WA OR |
| Pull a normally-skipped state | python scraper.py --plugins road511_usa --states CA NY |
| Re-scrape & replace stale data | python scraper.py --plugins road511_usa --states TN --replace-source |
| Mode | What it does |
|---|---|
| (default — upsert) | Loads existing data, refreshes known cameras by ID, appends new ones. Safe to re-run anytime. |
--replace-source |
Drops all cameras from the sources being run, then inserts fresh results. Use this to remove stale/offline cameras. Other sources are untouched. |
--fresh |
Ignores existing file entirely. Writes only what was just fetched. Use to fully rebuild from scratch. |
The Windy plugin runs in two phases automatically:
- Phase 1 — Scans the globe with a 20°×20° grid (162 boxes)
- Phase 2 — Any box returning ≥999 cameras is recursively subdivided into quadrants until fully drained
python scraper.py --plugins windy⚠ Takes 10–30 minutes depending on connection speed. HTTP 429 rate limits are handled automatically with a 10-second backoff.
| Plugin Alias | Source | Region | Camera Count | Live HLS? | API Key |
|---|---|---|---|---|---|
windy |
Windy Webcams | 🌍 Global | ~73,700 | ❌ Image only | ✅ Required (Free) |
road511_usa |
Road511 | 🇺🇸 United States | ~15,000 | ✅ CO, TN, DE | ❌ None |
caltrans |
Caltrans CCTV | 🇺🇸 California, USA | ~3,300 | ✅ Yes | ❌ None |
nyc_dot |
NYC TMC | 🇺🇸 New York City, USA | ~950 | ❌ Image only | ❌ None |
iowa_dot |
Iowa DOT | 🇺🇸 Iowa, USA | ~850 | ✅ Yes | ❌ None |
drivebc |
DriveBC | 🇨🇦 British Columbia, CA | ~1,040 | ❌ Image only | ❌ None |
tfl_london |
Transport for London | 🇬🇧 London, UK | ~800 | ❌ Image only | ❌ None |
singapore_lta |
Singapore LTA | 🇸🇬 Singapore | ~90 | ❌ Image only | ❌ None |
nzta |
NZTA Journeys | 🇳🇿 New Zealand | ~varies | ❌ Image only | ❌ None |
The road511_usa plugin covers the following states. States marked Live have confirmed CORS-compatible HLS streams; others are image-only.
| State | Cameras | Feed Type |
|---|---|---|
| Florida | ~3,500 | Image (JPEG) |
| Utah | ~1,500 | Image (JPEG) |
| Washington | ~1,500 | Image (JPEG) |
| Oregon | ~1,080 | Image (JPEG) |
| Colorado | ~900 | Live HLS ✅ |
| South Carolina | ~755 | Image (JPEG) |
| Indiana | ~530 | Image (JPEG) |
| Tennessee | ~668 | Live HLS ✅ |
| Arizona | ~643 | Image (JPEG) |
| Kansas | ~576 | Image (JPEG) |
| Arkansas | ~545 | Image (JPEG) |
| Ohio | ~500 | Image (JPEG) |
| Kentucky | ~362 | Image (JPEG) |
| Nebraska | ~350 | Image (JPEG) |
| Delaware | ~345 | Live HLS ✅ |
| Massachusetts | ~305 | Image (JPEG) |
| Wyoming | ~220 | Image (JPEG) |
| North Dakota | ~185 | Image (JPEG) |
| South Dakota | ~43 | Image (JPEG) |
| Montana | ~38 | Image (JPEG) |
States with no usable feed URLs (TX, NC, WI, GA, NV, PA, MI, ID, LA, MS, CT, ME, NH, WV, VT) are excluded automatically.
The engine auto-loads any plugin registered in PLUGIN_REGISTRY inside scraper.py.
Create a .py file under the appropriate region folder in scripts/scrapers/. It must export a fetch(config) function returning a list of GeoJSON Feature dicts.
# scripts/scrapers/usa/my_state/my_source.py
from scrapers.utils import log, build_feature, HEADERS
import requests
PLUGIN_META = {
"name": "My New Source",
"key_required": False,
"description": "Short description of what this scrapes",
}
def fetch(config: dict) -> list[dict]:
log("Fetching My New Source...")
features = []
try:
resp = requests.get("https://example.gov/api/cameras", headers=HEADERS,
timeout=config.get("TIMEOUT", 15))
resp.raise_for_status()
cams = resp.json()
except Exception as e:
log(f"Fetch failed: {e}", "ERROR")
return []
for cam in cams:
try:
lat = float(cam["lat"])
lon = float(cam["lon"])
if lat == 0 and lon == 0:
continue
features.append(build_feature(
cam_id = str(cam["id"]),
name = cam.get("name", "Unknown Camera"),
lat = lat,
lon = lon,
feed_url = cam.get("imageUrl", ""), # static JPEG snapshot
stream_url = cam.get("streamUrl", ""), # HLS .m3u8 (optional)
cam_type = "traffic",
city = cam.get("city", ""),
country = "US",
source = "my_new_source",
))
except Exception:
continue
log(f"My New Source: {len(features)} cameras loaded", "OK")
return features
build_featuresignature:build_feature(cam_id, name, lat, lon, feed_url, cam_type, city, country, source, stream_url="", # HLS .m3u8 URL — only set if CORS: * confirmed player_url="", # Link to a viewer page (optional) feed_type="image/jpeg", **kwargs) # Any extra properties⚠ Only set
stream_urlif you've confirmed the host returnsAccess-Control-Allow-Origin: *. Streams without CORS will fail silently in HLS.js. Test with:curl -I <stream_url>and check theAccess-Control-Allow-Originheader.
PLUGIN_REGISTRY = {
# ... existing entries ...
"my_new_source": {
"module": "scrapers.usa.my_state.my_source",
"name": "My New Source",
"key": None, # or "MY_API_KEY_ENV_VAR"
"description": "Short description shown in --list",
},
}Add to .env:
MY_API_KEY=your_key_hereReference in scraper.py's CONFIG dict:
CONFIG = {
# ...
"MY_API_KEY": os.getenv("MY_API_KEY"),
}Read in your plugin via config.get("MY_API_KEY").
cd scripts
python scraper.py --plugins my_new_sourceCreate a .env file in the project root (Argus/.env):
# Required for the Windy plugin (73k+ global cameras)
WINDY_API_KEY=your_windy_key_here
# Required for the React frontend — Windy JIT token fetching
VITE_WINDY_API_KEY=your_windy_key_hereBoth keys can be the same value.
WINDY_API_KEYis used by Python;VITE_WINDY_API_KEYis exposed to the browser by Vite (theVITE_prefix is required for Vite to pass it through to the frontend).
.envis git-ignored. Never commit API keys.
This project is for educational and open-data visualization purposes only. All camera feeds are sourced from public, non-sensitive government or commercial APIs.

