Engineering note
Deterministic video rendering in the browser: why we never screen-record
EmlakKlip turns a land parcel into an aerial video by rendering a browser map one frame at a time. Screen recording would tie the video to the speed of the machine. Instead, every frame is computed from time, captured only when it is provably ready, and checked before it can reach a customer.
The problem with recording a screen
A screen recording captures whatever the browser happens to show. If the machine is busy, frames drop; if map tiles arrive late, the recording shows blurred or missing imagery; and two recordings of the same input are never the same. For a product whose output is the video itself, that is not acceptable.
Map libraries add a second problem. Camera animations such as flyTo and easeTo are driven by the browser’s own clock, so the motion depends on how fast the machine renders. We do not use them.
Time is the only clock
The renderer has a single time authority. For frame n at f frames per second the time is t = n / f. A timeline computes the camera state for that moment — centre, zoom, pitch and bearing — and applies it directly, with no browser animation in between. Machine speed changes how long a render takes, never what the video shows.
- Transitions between camera keyframes are eased (ease-in-out cubic at minimum). A linear camera reads as a computer animation.
- Bearing is interpolated along the shortest angle, so a turn from 350° to 10° moves 20°, not 340° the other way.
- The orbit around the parcel is the one deliberately linear segment: constant angular speed reads as calm.
- The camera never goes below terrain + 25 m, and terrain exaggeration stays between 1.00 and 1.15; higher values look cheap.
Ready means measured, not reported
Every frame follows the same order: set the camera, let the map render, wait until tiles are idle, then take the screenshot. The hard part is knowing when the map is really ready.
We measured a case where the map library reported the style, the tiles and the map as loaded — all true — while the satellite imagery hung in empty space as a few isolated tiles. The cause was a race: the camera was framed before the terrain (elevation) source had loaded, so tile coverage was computed as if there were no terrain. Moving the camera slightly repaired it by itself. Turning terrain off, or flying to the same zoom levels by hand, never reproduced it; only the first paint did.
The fix was to define readiness ourselves: style loaded and terrain source loaded and tiles loaded, then apply the framing once more before the scene is declared ready — with its own timeout, so the page never waits forever. The lesson is general: a “loaded” flag is not proof of a complete frame.
Network logs did not help either. The map library fetches tiles inside Web Workers, which neither the page’s resource timing nor the browser’s network log shows. Zero requests in the log did not mean zero requests.
The frame that looked fine to the library
Imagery tiles fade in over a few hundred milliseconds of wall-clock time. In a renderer driven by its own time, a frame captured during that fade shows washed-out imagery although every readiness signal is green. The capture gate now waits out the fade, and a separate detector scans finished frames for washed-out tiles. On fast camera turns the same place no longer shows the same content from frame to frame, so the detector compensates for motion and compares in both directions before it flags a frame.
Every frame must pass
- Prewarm. Before capture, the camera path is visited at one-second steps, so no captured frame is the first visit to a location.
- No fixed sleeps. A frame may be ready in 30 ms or in 400 ms; quality decides the wait. A frame that is not ready within its limit fails the render instead of entering the video.
- Size latch. A blank frame is a few kilobytes, a detailed one around three megabytes. Any frame under 40 KB fails the render.
- Console errors. A single error in the browser console during capture fails the render.
- Frame count, not only duration. After encoding, the file is checked for both. We learned why when a single corrupt frame was silently skipped by the encoder: the video still reported exactly 20.000000 seconds, and only the frame count — 599 instead of 600 — gave it away.
A check that cannot fail is not trusted
Each quality check is tested by forcing the defect it looks for: the renderer runs with the fix turned off, and the check must catch the exact flaw. If a check passes on a deliberately broken frame, the check is broken — and it is fixed before anything else.
A failed render deletes nothing
A render is published only after encoding, verification, quality-control stills, the review and mobile copies and the manifest have all succeeded. The new release then replaces the previous one in a single step, so the delivered video and its manifest can never belong to different renders. A failed render never removes the last good output.
What deterministic does and does not mean
Camera state and timing are deterministic: the same input produces the same camera at every frame. Pixel-identical output is not claimed — WebGL may rasterise two runs slightly differently — so our render manifests record a deterministic camera, not deterministic pixels.
Takeaways for any browser-rendered media
- Drive time yourself; never let the browser’s clock move the camera.
- Define readiness by measurement, and treat library flags as hints.
- Gate every frame on size and integrity, and count frames after encoding.
- Test each check with a negative control.
- Publish atomically, and never let a failure delete the last good result.
See the EmlakKlip case study and our geospatial engineering work.
Have a project in mind?
Send a short description. If you need a mutual NDA first, we will send ours before you share details.