atem-scopes

atem-scopes

apps.detail.types.app from Stoatworks Labs' Repository

apps.detail.sections.overview

Waveform, vectorscope and histogram for a captured video source. Analysis happens in the page and video never leaves the browser. Note: buildCmd is `static:build`, NOT `build`. In this repo `npm run build` is `electron-vite build` and produces the desktop app, not the static site — it would populate out/ and leave dist/ stale or absent, and the image would either fail to copy or silently serve an old build.

atem-scopes

AI-assisted project. This codebase was created with Claude Code (Anthropic), directed and reviewed by a human author. The scope engine is verified against a built-in test pattern (see Verification); it has not been used with a real ATEM switcher, a real multiview capture, or a DeckLink card.

Video scopes arranged around a live ATEM multiview. Capture the switcher's multiview output over USB, draw a region round each window, and put a waveform, vectorscope, histogram, false colour, zebras or focus peaking on any of them — including program and preview.

Sources are named from the switcher's own multiviewer configuration, live, so re-routing a multiview window mid-show relabels the scope watching it without touching the geometry.

Two builds, one codebase

Desktop (Electron) Hosted (Cloudflare Worker)
UVC capture + every scope
Region drawing and naming
Live source names from the switcher
DeckLink capture ⚠️ unbuilt — see below

The hosted build's two gaps are properties of the browser, not features left undone:

  • The ATEM control protocol is UDP on port 9910. A page has no UDP API. WebRTC and WebTransport are negotiated transports; neither can talk to a switcher that has never heard of them. simpleVIS documents the identical wall for Art-Net and sACN.
  • A DeckLink card is reached through Blackmagic's SDK against a kernel driver. It does not enumerate as a webcam, so getUserMedia cannot see it. Blackmagic's boxes — UltraStudio Recorder 3G, the Web Presenter family — deliberately do present as UVC, and those work in the browser like any other camera. The PCIe cards do not.

So the hosted build names regions from what you type when you draw them, and the desktop build prefers the switcher's answer. Same screen, same workflow.

Components branch on window.api.capabilities and hide what the current target cannot do rather than offering a button that always fails. The backend is chosen at build time, so the hosted bundle contains no switcher code at all — verifiably: grep 9910 dist/assets/*.js finds nothing.

The thing this app is careful about

We never see the video signal. We see whatever RGB the capture path handed the browser, and that path has already made two decisions it does not report:

  1. which matrix it used to get from Y'CbCr to RGB (BT.601 or BT.709), and
  2. whether it expanded studio-range levels (16–235) to full range.

Get either wrong and the scopes are plausibly wrong rather than obviously wrong. A BT.709 signal read as BT.601 throws every vectorscope target 5.74° off its box; unexpanded studio levels put reference white at 92 IRE. Both look like a mildly misadjusted camera.

There is no way to detect this from the pixels, so it is an explicit setting the UI states, not a guess. See src/shared/colorimetry.ts.

Two related honesty points the UI surfaces per tile:

  • A multiview tile is a monitoring aid, not a measurement. It is a few hundred pixels wide, compressed over USB, and often has a label bar, tally border or audio meter burnt into it. The window inset exists to keep those out of the trace, and a tile whose window the switcher reports an overlay on is badged overlay in crop.
  • Seeded geometry is badged uncalibrated until a human has looked at it.

For an honest measurement, capture an aux out whole as a second device and scope that.

Verification

The app generates its own 75% colour bars (Start test pattern), for the same reason simpleRTA generates its own pink noise: it is the only way to check the instrument without trusting the thing you are measuring. It is self-validating — the vectorscope graticule is computed from the same matrix the shader plots with, so with 75% bars the trace must land in the boxes.

Measured by reading pixels back off the GL canvas, BT.709, full range:

75% bar Expected IRE Waveform read
Blue 5.42 5.4
Red 15.95 15.8
Magenta 21.36 21.2
Green 53.64 53.4
Cyan 59.06 58.8
Yellow 69.59 69.4
White 75.00 74.8

Differences are the row quantisation of the readback. The vectorscope trace landed on its cyan, green and blue 75% targets to within a pixel.

What that does and does not prove. It proves the maths, the shader uniforms, the graticule derivation and the full capture→texture→scope path agree end to end. It proves nothing about a real ATEM: no switcher has ever been connected, no multiview has been captured, and the mapping from multiview window index to screen position is an assumption the calibration screen exists to correct.

DeckLink

The native addon in native/decklink has never been compiled or run. The Blackmagic SDK is a free but licence-gated download and is not ours to redistribute, so it is not vendored and was not available where this was written. It builds only with DECKLINK_SDK_DIR set, and the app is fully functional without it — capabilities.decklinkCapture is false and the UI hides DeckLink entirely.

It captures native UYVY rather than letting the card convert to BGRA, deliberately: the driver does not report which matrix it used, and RGB cannot be un-converted. See native/decklink/README.md.

Video

atem-scopes

A 57-second look at it working, filmed at the hosted address and driven through the app's own controls. The picture being measured is the built-in colour-bar generator, so every scope in it can be checked against a signal whose right answer is known — including the beat where the levels are set wrong on purpose and black falls below zero.

Download

Download

v0.1.0 — prebuilt for macOS, Windows and Linux. Pick your platform:

macOS — Apple Silicon, Intel
Build Download Size
.dmg disk image atem-scopes-0.1.0.dmg 124 MB
Apple Silicon · .pkg installer atem-scopes-0.1.0-macos-arm64.pkg 116 MB
Intel · .pkg installer atem-scopes-0.1.0-macos-x64.pkg 124 MB
Windows
Build Download Size
.exe installer atem-scopes.Setup.0.1.0.exe 102 MB
Linux
Build Download Size
AppImage atem-scopes-0.1.0.AppImage 122 MB

Also in this release:

All builds, checksums and release notes: github.com/stoatworks-labs/atem-scopes/releases.

macOS builds are signed and notarised and open normally. The Windows builds are unsigned, so SmartScreen warns once.

macOS installers are Developer ID-signed and notarised by Apple, nested helper binaries included, so they open normally. Windows installers are unsigned and SmartScreen warns once: More infoRun anyway.

The hosted build needs no install: it runs in any browser with WebGL2.

Commands

npm run dev              # Electron dev
npm run preview:static   # the hosted build, in a browser — no Electron needed
npm run static:build     # hosted production build -> dist/
npm run deploy           # static:build + wrangler deploy
npm run typecheck        # node + web — run this, not a bare tsc
npm test                 # vitest
npm run build            # typecheck + electron-vite build
npm run build:mac        # / :win / :linux

Where this sits among the ATEM projects

Repo Purpose
atem-scopes (this) Measure what a switcher is putting out
animATEM Control one switcher; UVC multiview compositing for SuperSource/DVE
atem-overseer Monitor and control a fleet from one dashboard
atem-fleet-admin Provision/configure many switchers at once

atem-scopes never sends a command to a switcher. It connects, reads the multiviewer window assignments and input names, and disconnects.

This project is built on other people's work — see ATTRIBUTIONS.md.

Licence

MIT — see LICENSE.

apps.marketingCta.appInstallTitle

apps.marketingCta.appInstallDescription

apps.installHelp.stepOpen apps.installHelp.stepSearchApp apps.installHelp.stepReview apps.installHelp.stepInstall

apps.detail.sections.categories

apps.detail.sections.related

apps.detail.sections.details

apps.detail.details.repository
ghcr.io/stoatworks-labs/atem-scopes:latest
apps.detail.details.lastUpdated2026-08-10
apps.detail.details.firstSeen2026-08-07

apps.detail.sections.runtime

apps.detail.details.webui
http://[IP]:[PORT:80]/
apps.detail.details.network
bridge
apps.detail.details.shell
sh
apps.detail.details.privileged
false

apps.detail.sections.configuration

WebUI PortPorttcp

Host port for the web interface.

apps.detail.config.target
80
apps.detail.config.default
8511
apps.detail.config.value
8511