Skip to content

Recording

A recording is a video of the frames the engine drew. Armed, the recorder captures the stage canvas’s pixels once per engine frame, with the screen layer over them, and encodes the frames with the browser’s WebCodecs VideoEncoder as VP9 in a WebM container. What the recording holds is exactly what the renderer drew: every material, shader, shadow, fog, sprite, post-effect, and HUD operation, because the evidence is the pixels. The recorder is part of the engine, armed and disarmed through the Engine object.

recording(): boolean;
startRecording(): void;
stopRecording(): Promise<Recording>;
MemberReturnsBehavior
recording()booleanWhether frames are being captured.
startRecording()voidArms the recorder. Capture begins at the next frame.
stopRecording()Promise<Recording>Disarms the recorder, flushes the encoder, and resolves with everything captured since startRecording.

stopRecording is asynchronous because the encoder is flushed before the container is closed, and the promise resolves once the last frame’s bytes are in it. engine.destroy() while the recorder is armed discards the capture.

interface Recording {
video: Uint8Array;
width: number;
height: number;
frames: readonly RecordedFrame[];
ended: boolean;
}
FieldMeaning
videoThe WebM bytes.
width, heightThe frame size in device pixels.
framesOne entry per video frame, in order.
endedtrue when the frame bound stopped capture before stopRecording.

frames.length is the number of video frames the container holds, so a suite reads the frame count from it and a player indexes the video’s frames by it.

interface RecordedFrame {
count: number;
timeMs: number;
deltaMs: number;
}
FieldMeaning
countThe engine’s frame counter for this frame.
timeMsThe engine’s accumulated simulated time through this frame, in milliseconds.
deltaMsWhat the frame was worth, in milliseconds.

The three are the frame’s count, timeMs, and lastDeltaMs as the engine’s FrameInfo reported them, kept beside the video so a frame in the recording is addressable by the engine’s counter and by simulated time.

Capture begins at the frame after startRecording, so a recorder armed from inside update or render captures whole frames only. A frame open when stopRecording is called is dropped.

The recorder captures the frame after the scene is rendered and the screen layer drawn, and before the diagnostics overlay draws on the screen layer. In the engine’s frame order that is the step between the scene render and the overlay, so a recording holds the picture the game submitted and nothing of the overlay.

A frame is the picture the game submitted: the scene rendered through the camera plus the screen layer over it. Everything the renderer drew is in it, the background and the letterbox bars included, and the diagnostics overlay is outside it.

The recorder composes the frame itself. After the scene is rendered and the game has drawn the screen layer, it draws the stage canvas and then the screen layer into a capture canvas of the recording’s size, and hands that canvas to the encoder as a VideoFrame. The encoder discards alpha, so wherever the composed picture is transparent the frame shows black.

The recording’s size is the stage canvas’s backing store when the recorder is armed, and Recording.width and height report it. A frame whose backing store differs is drawn scaled into the capture canvas, so every frame in one recording has the same size.

Frames are encoded as VP9 by a WebCodecs VideoEncoder and written into a WebM container. The encoder emits a keyframe at least every 60 frames, and the first frame of a recording is a keyframe.

Each video frame’s timestamp is the engine’s accumulated simulated time through that frame, RecordedFrame.timeMs, in microseconds, rounded to the nearest microsecond. A frame whose simulated time equals the previous frame’s is timed one microsecond after it, so timestamps strictly increase and every frame is addressable by its own. A player decodes with a WebCodecs VideoDecoder, indexes the frames by timestamp, and seeks by decoding forward from the nearest earlier keyframe, which costs at most 60 frames of decode.

Capture stops at 3,600 frames. Recording.ended is true when that bound stopped it, the recorder stays armed until stopRecording, and every frame the recording holds is whole.

While idle the recorder costs nothing per frame.

ConditionResult
startRecording while already armedError naming the unbalanced call
stopRecording while not armedError naming the unbalanced call
startRecording where the host has no VideoEncoderError naming WebCodecs
engine.destroy() while armedThe capture is discarded and the recorder is disarmed

Recording and RecordedFrame are exported as types from @clockwyrks/simple-3d.