Security Cam
Run a scene through a cheap recorder — macroblocks, collapsed colour, crushed blacks, crawling noise
This filter re-reads the real pixels of the scene it wraps through the experimental html-in-canvas browser API. The preview above shows the true effect in Chrome 149+ with chrome://flags/#canvas-draw-element enabled (nested canvases need 152.0.7944.0+). Every other browser — Safari, Firefox, Chrome without the flag — falls back to a CSS approximation, so the page and your video still play.
Rendering is supported through the CLI, Studio, Lambda and SSR with --gl=angle, or --gl=swangle on a machine without a GPU. Chrome may change or discontinue the API; the fallback is what keeps your composition renderable if that happens.
Installation
$ pnpm dlx shadcn@latest add @remocn/security-camUsage
SecurityCam is a wrapper. Put a scene inside it and it comes back as something
that went through a cheap recorder: the picture flattens into macroblocks where
the bitrate could not keep up, colour resolution collapses to half the detail of
brightness, blacks crush, the exposure hunts slightly, and noise crawls in the
shadows.
import { SecurityCam } from "@/components/remocn/security-cam";
export const MyScene = () => (
<SecurityCam>
<Scene />
</SecurityCam>
);Why there is no fisheye here
The obvious build is a wide bow plus desaturation, and it is a trap: bending the
frame is what crt-screen does, and a filter whose identity is a warp ends up
reading as a curved display with the scanlines missing. camera-lens already
covers optics, and it has a distortion prop if you want a wide-angle bow —
nest the two and you get a wide lens feeding a cheap recorder.
What makes footage look like surveillance is not the lens, it is the codec. So that is what this is: macroblocking and chroma subsampling, the two artifacts every starved video stream has and no display ever produces.
What to reach for it for
Open cold, then cut to the truth
The strongest use. The scene plays as a recording, and on a hard cut the surveillance drops away and the real interface is underneath — the same shot, suddenly clean.
import { interpolate, useCurrentFrame } from "remotion";
import { SecurityCam } from "@/components/remocn/security-cam";
export const Opening = () => {
const frame = useCurrentFrame();
const intensity = interpolate(frame, [0, 40, 44], [1, 1, 0], {
extrapolateRight: "clamp",
});
return (
<SecurityCam intensity={intensity}>
<Scene />
</SecurityCam>
);
};Cut, do not fade. A recording that dissolves into a clean render is a crossfade; one that switches in a single frame is an edit.
Give it an OSD
The wrapper adds nothing of its own — no timestamp, no camera label, no recording dot. Those belong to your scene, and putting them inside the wrapper is what sells it: burned-in overlays get compressed along with everything else, exactly as a real recorder writes them.
<SecurityCam>
<AbsoluteFill>
<Scene />
<CameraOsd />
</AbsoluteFill>
</SecurityCam>Starve the bitrate
Drive compression and blockSize together and the feed collapses: detail
dissolves into flat squares, colour smears across blocks twice their size, and
the picture becomes something you can only just read. Good for a moment of
things going wrong.
const starve = interpolate(frame, [0, 36], [0.15, 1], {
extrapolateRight: "clamp",
});
const block = interpolate(frame, [0, 36], [6, 26], {
extrapolateRight: "clamp",
});Props
| Prop | Type | Default | Description |
|---|---|---|---|
compression | number | 0.7 | How hard the codec is squeezing. Pulls each block toward its own average, coarsens the brightness quantizer, and collapses colour onto blocks twice the size. At 0 the picture keeps its detail and only the tone and noise remain. |
blockSize | number | 10 | Macroblock edge in composition units. Colour is always sampled on blocks twice this size, which is what makes the result read as video rather than as a mosaic. |
noise | number | 0.6 | Sensor grain, weighted toward the shadows the way a small sensor at high gain behaves. Bright areas stay comparatively clean. |
intensity | number | 1 | Master amount. Read every frame, so cutting it to 0 hands the clean scene back in a single frame. At 0 the scene passes through exactly as it was. |
children | React.ReactNode | — | The scene the filter wraps and re-reads every frame. |
Notes
Colour is sampled on blocks twice the size of the brightness blocks, which is the
detail doing most of the work. Real video codecs throw away colour resolution
first because the eye barely notices — until it does, and a red badge bleeds
across a square it does not belong to. That mismatch between sharp brightness and
blocky colour is what the eye reads as "compressed", and it is a genuinely
different artifact from the sideways chroma smear of vhs-filter.
The only motion is a slow exposure pump on a 90-frame cycle and the shadow noise, which is reseeded per frame from a 64-frame clock. Both are pure functions of the frame number, so parallel renders match and nothing drifts.
In browsers without html-in-canvas the scene keeps the wash and the vignette but not the blocking. Quantizing a frame into macroblocks is not something a stylesheet can do, and a plain blur pretending to be compression would be a different artifact wearing this one's name.