research/jabodetabek-cctv-live-data/analysis.md

56 lines
4.8 KiB
Markdown

# Analysis — the chain, overlaps, stacks
## The chain
```
camera owners (Dishub/bank/operator cameras)
→ operator/government stream servers (HLS: Ant Media, MediaMTX, datarhei, custom)
→ government portals publish catalogs (HTML/JSON; some embed player iframes)
→ transit.oyenza.com harvests + verifies + curates → static catalog in its bundle
→ visitor's browser plays the owner's HLS URL directly (hotlink, no proxy)
```
## Evidence per source (cross-match against oyenza's catalog)
Method: extract oyenza's view URLs from `asset-cctv.json`; extract the portal's camera identifiers from its page/JSON; compare sets exactly (case-insensitively where the portal is inconsistent).
- **DKI — 46/46.** Portal `cards` iframes carry `<camera-id>/embed.html`; oyenza lists `<camera-id>/index.fmp4.m3u8` + the same `embed.html` as "owner page". Every oyenza DKI path appears in the portal set (52 cameras at capture). Example id: `501844_JKP_DISHUB_JL.-MH.-THAMRIN-C01_CCTV-01`.
- **Toll — 190/190.** BPJT's inline JSON carries `streamhls` values; after unescaping (`\/`) and cleaning a few literal `\r\n` artifacts, **all 190** oyenza toll view URLs appear verbatim. BPJT page at capture: 1,530 records → 1,415 unique URLs across ~39 hosts.
- **Bekasi — 23/23.** oyenza's 23 stream filenames all match `list.json` `name` values exactly.
- **Bogor — 51/51.** All 51 unique detail-link IDs (`/cctv/<id>/detail`) used by oyenza appear on the BSW page (96 IDs total). Strongest link: the Alun-Alun detail page (id 82) embeds exactly `https://restreamer2.kotabogor.go.id/memfs/c07c1926-288c-46e4-a19c-9f51022edc5d.m3u8` — the URL oyenza lists for that camera.
Interpretation: oyenza is a **curated mirror**, not a re-publisher with its own pipeline to the cameras. The catalog is rebuilt and re-verified (footer "Daftar diperiksa 13 Oktober 2026"; `checkedAt` 2026-10-12T22:07:39Z).
## Selection logic (the 154 toll cams)
oyenza's toll subset = 190 views / 154 cams from 9 operators: Hutama Karya 58, Trans Bumi Serbaraja 29, Cimanggis-Cibitung 19, Cibitung-Tanjung Priok 19, JTD 8, Marga Mandalasakti 7, KKDM 6, Jakarta Lingkar Baratsatu 5, CMNP 3.
Not simple geography: 589 BPJT cams fall inside a Jabodetabek bounding box, but only ~190 views were taken. Conspicuous exclusions, with live checks:
- **Jasa Marga (~605 cams) — absent.** `jmlive.jasamarga.com` returned HTTP 200 with an **empty body** (manifest not publicly served to us). Exclusion is consistent with "dead unless probed from a permitted vantage".
- **BSD Toll — absent.** `stream.bsdtol.com` → nginx **502** at capture.
- **Waskita (`wbw`, 55) — absent** even though its streams work (playlist verified) → curation beyond pure liveness (coverage-area choice).
**Confidence:** Stream-liveness exclusions (JM, BSD) = established via probes. "Verify-then-pick + Jabodetabek coverage" selection rule = strong inference; the exact rule isn't observable from outside.
## Stream-stack fingerprints (observed live)
- `/LiveApp/streams/<id>.m3u8` → **Ant Media Server** — HK (Cloudflare-fronted), Serbaraja, CCT. 2 s TS segments.
- `server: mediamtx` → **MediaMTX** — CTP. Master playlist → `stream.m3u8`.
- `/memfs/<uuid>.m3u8` (+ `?session=` on sub-playlists) → **datarhei Restreamer** — all Bogor servers (also bsdtol, PPTR).
- `index.fmp4.m3u8` + `tracks-v1/` + `X-Sid`/`X-Run-Time` headers, `server: Streamer 26.04.1` → **Bali Tower custom** — DKI.
- nginx + `X-Powered-By: Express` packagers → JTD, KKDM. Plain nginx/Apache HLS → MMS, JORRW1, Citramarga, Bekasi.
## Playback path (from the site's own code)
- Catalog fetched at runtime: `fetch("/assets/asset-cctv-<hash>.json")`.
- Views mapped to `{label, hls, page}`; on tap: `import("/assets/lib-E4YH4VVH.js")` (hls.js) → `hls.loadSource(view.hls); hls.attachMedia(video)`; Safari falls back to native `canPlayType("application/vnd.apple.mpegurl")` → `video.src`.
- "Buka Layar penuh" opens `view.page` — the owner's page (portal or `embed.html`).
- No proxy: streams load from the owner host in the visitor's browser. Probed hosts sent `Access-Control-Allow-Origin: *`, and returned 200 even without `Origin`/`Referer` — no hotlink protection at capture.
- The site's own claim ("Video diputar langsung dari server pemilik kamera dan tidak kami simpan") matches the code path.
## Anti-overclaim notes
- The static catalog proves what the client understands (URLs, labels, owner pages). It does not prove the operator's exact checker tooling — no public repo was found; `checkedAt` + redeploy hashes = inference of a periodic probe + rebuild.
- Uptime varies: one host timed out once and answered on retry. Treat single probes as readings, not facts — timestamp them (done: Oct 13, 2026).
- Access policy can change at any time; CORS `*` today ≠ CORS `*` forever.