Scan, Extract & Call
Stop typing numbers manually. Point your camera at business cards, docs, or screens to extract and dial numbers instantly.
Get Scan2Call 📱The traditional approach to rendering 3D animations—such as the high-fidelity cinematic transitions found in App3DShot, including dynamic camera sweeps like the Spotlight Spin or Arc Parade—relies on heavy, expensive server-side orchestration. Typically, this involves spinning up headless Chrome instances via Puppeteer, running multiple microservices on GPU-enabled GCP or AWS machines, capturing canvas frames, and pipe-lining them to FFmpeg containers to produce an MP4 output. This design introduces severe network latencies, high cloud hosting costs, and complex scaling issues when hundreds of users request concurrent renders.
A highly efficient alternative is to shift the entire rendering and encoding pipeline directly to the client browser. By leveraging Three.js for deterministic WebGL 3D animation, the modern WebCodecs API for hardware-accelerated H.264/HEVC encoding, and a lightweight TypeScript MP4 multiplexer, we can export native 4K 60fps MP4s inside the browser without executing a single line of backend rendering code.
Implementing zero-latency, local-only media workflows requires exceptional engineering specialization. When tech leads look to hire typescript developers with deep client-side performance experience, it is because building and optimizing these browser-native, high-throughput media pipelines demands absolute precision.
A client-side high-resolution video generator requires a strict, linearly structured pipeline. We cannot rely on standard real-time rendering patterns because browser frame drops would yield a choppy, drop-frame video output. The pipeline must move through four sequential stages:
Deterministic Update: Instead of updating the 3D scene using real-time frame timings, we force the Three.js render loops, physics calculations, and mixers to step forward by exact, fixed-time slices (e.g., 16.66ms for 60fps).
Frame Capture: We bind WebGL render targets to an offscreen canvas. Once a frame is rendered, we extract the image data directly from the graphics pipeline without blocking the main browser thread.
Video Compression: The raw frame is submitted into the browser's native VideoEncoder (part of the WebCodecs API). The browser passes this frame to local hardware-accelerated encoders, outputting highly compressed H.264 or HEVC bitstream chunks asynchronously.
Multiplexing (Muxing): The raw encoded video chunks are piped into a TypeScript-based container writer. This writes the structural ISO base media boxes (e.g., ftyp, moov, mdat) required to make the output a fully valid, playable, zero-delay MP4 file.
This decentralized approach mirrors the design patterns discussed in our architectural analysis of Architecting a Local-First 3D App Mockup Engine: WebGL. By utilizing client-side hardware, we gain 100% privacy compliance, eliminate hosting costs, and provide instantaneous exports that scale naturally with the user's local hardware capability.
This design paradigm is also consistent with specialized, offline-first utilities like PDFaiGen, which emphasize local browser-based execution for zero-server data compliance.
In standard interactive web applications, animations run inside a requestAnimationFrame loop. If the CPU experiences a spike, or if thermal throttling limits the GPU, the delta time between frames fluctuates. When rendering interactive media, these dropped frames go unnoticed. However, in video production, even a single dropped frame causes visual stuttering.
To produce smooth, professional-grade videos, we must force deterministic animation steps. This means that if we are outputting a 60fps video, we must advance the animation state by exactly 1/60th of a second for every frame rendered, completely decoupled from the system's actual refresh rate.
The code below demonstrates how to override the default clock and animation mixers to update by a fixed-time step:
import * as THREE from 'three';
export class DeterministicRenderPipeline {
private renderer: THREE.WebGLRenderer;
private scene: THREE.Scene;
private camera: THREE.PerspectiveCamera;
private mixer: THREE.AnimationMixer;
private fps: number;
private frameDelta: number;
private totalFrames: number;
constructor(
renderer: THREE.WebGLRenderer,
scene: THREE.Scene,
camera: THREE.PerspectiveCamera,
mixer: THREE.AnimationMixer,
durationSeconds: number,
fps = 60
) {
this.renderer = renderer;
this.scene = scene;
this.camera = camera;
this.mixer = mixer;
this.fps = fps;
this.frameDelta = 1 / fps;
this.totalFrames = durationSeconds * fps;
}
public async executeRenderPipeline(
onFrameReady: (canvas: HTMLCanvasElement, currentFrame: number) => Promise<void>
): Promise<void> {
// Ensure renderer has correct dimensions for 4K video
this.renderer.setSize(3840, 2160, false);
for (let frameIndex = 0; frameIndex < this.totalFrames; frameIndex++) {
// Advance animation timeline deterministically
const animationTime = frameIndex * this.frameDelta;
this.mixer.setTime(animationTime);
// Render the frame to the offscreen canvas context
this.renderer.render(this.scene, this.camera);
// Capture frame data synchronously from WebGL drawing buffer
await onFrameReady(this.renderer.domElement, frameIndex);
}
}
}Extracting raw frames from a WebGL rendering context can easily become a major bottleneck. The naive method relies on canvas.toDataURL("image/png") or gl.readPixels(). These methods are highly synchronous and introduce massive CPU-GPU sync points that halt the graphics pipeline and degrade performance, especially on 4K renders.
To avoid blocking the main thread, we capture our frames using OffscreenCanvas. When creating our WebGLRenderer, we must ensure that the preserveDrawingBuffer option is set to true. Without this, the browser will clear the color buffer immediately after rendering, presenting empty frames to our encoder.
The WebCodecs VideoFrame object natively accepts an HTMLCanvasElement or OffscreenCanvas as its source, allowing us to pass the render target directly to the system's hardware encoder. This keeps the frame data on the GPU whenever possible, bypassing expensive CPU-side memory allocation.
The WebCodecs API exposes the browser's underlying hardware compression block. Let's build a TypeScript class to manage our VideoEncoder, configured for 4K video encoding at 60fps with H.264 High Profile.
To prevent massive high-resolution frame data from overwhelming memory, we must track asynchronous backpressure using the VideoEncoder.encodeQueueSize property. If the encoder queue gets saturated, we pause frame submission and wait for the hardware to process the existing chunks.
export interface EncoderConfig {
width: number;
height: number;
fps: number;
bitrateMbps: number;
}
export class WebCodecsVideoEncoder {
private encoder!: VideoEncoder;
private isConfigured = false;
private chunkCallback: (chunk: EncodedVideoChunk, metadata?: EncodedVideoChunkMetadata) => void;
constructor(
config: EncoderConfig,
onChunk: (chunk: EncodedVideoChunk, metadata?: EncodedVideoChunkMetadata) => void
) {
this.chunkCallback = onChunk;
this.initializeEncoder(config);
}
private initializeEncoder(config: EncoderConfig): void {
this.encoder = new VideoEncoder({
output: (chunk, metadata) => {
this.chunkCallback(chunk, metadata);
},
error: (err) => {
console.error("WebCodecs Encoder pipeline crash: ", err);
}
});
const codecProfile = "avc1.640034"; // H.264 High Profile Level 5.2
this.encoder.configure({
codec: codecProfile,
width: config.width,
height: config.height,
bitrate: config.bitrateMbps * 1_000_000,
framerate: config.fps,
hardwareAcceleration: "prefer-hardware",
latencyMode: "quality"
});
this.isConfigured = true;
}
public async encodeCanvasFrame(canvas: HTMLCanvasElement, frameIndex: number, fps: number): Promise<void> {
if (!this.isConfigured) throw new Error("Encoder is not initialized.");
// Manage encoder queue backpressure
while (this.encoder.encodeQueueSize > 8) {
await new Promise((resolve) => setTimeout(resolve, 5));
}
const timestampUs = Math.round((frameIndex / fps) * 1_000_000);
const frame = new VideoFrame(canvas, { timestamp: timestampUs });
// Force keyframe output at the start and every 120 frames (GOP size = 2 seconds)
const requiresKeyFrame = frameIndex % 120 === 0;
this.encoder.encode(frame, { keyFrame: requiresKeyFrame });
// Explicitly release GPU texture resources immediately
frame.close();
}
public async finalizeEncoding(): Promise<void> {
await this.encoder.flush();
this.encoder.close();
}
}The WebCodecs VideoEncoder output is a stream of raw, encoded H.264 or HEVC frames (either keyframes or delta frames). While these frames are compressed, they cannot be played directly by media players. They must first be wrapped in an container format—such as the ISO Base Media File Format (MP4).
To avoid server-side multiplexing, we can use mp4-muxer, a highly efficient, lightweight TypeScript library designed to stitch video frames into structured MP4 streams directly in memory.
The class below demonstrates how to capture chunks from our WebCodecsVideoEncoder and write them directly to a download-ready MP4 Blob:
import { Muxer, ArrayBufferTarget } from 'mp4-muxer';
export class ClientSideMP4Muxer {
private muxer: Muxer<ArrayBufferTarget>;
constructor(width: number, height: number, fps: number) {
this.muxer = new Muxer({
target: new ArrayBufferTarget(),
video: {
codec: "avc",
width: width,
height: height
},
fastStart: "fragmented" // Optimizes initial structural playability
});
}
public handleEncodedChunk(chunk: EncodedVideoChunk, metadata?: EncodedVideoChunkMetadata): void {
// Transfer raw buffer data into the container multiplexer
const buffer = new ArrayBuffer(chunk.byteLength);
chunk.copyTo(buffer);
this.muxer.addVideoChunk(buffer, chunk.type, {
duration: chunk.duration ?? undefined,
timestamp: chunk.timestamp
}, metadata);
}
public exportVideoBlob(): Blob {
this.muxer.finalize();
const buffer = this.muxer.target.buffer;
return new Blob([buffer], { type: 'video/mp4' });
}
}WebGL and WebCodecs can quickly trigger memory leaks on macOS and iOS, often resulting in WebGL context losses. To prevent this, you must aggressively release resource handles. Calling frame.close() immediately after submitting a frame to the encoder is vital. Additionally, when managing dynamic textures on the Three.js scene (such as simulated screen captures on 3D device mockups), make sure to call texture.dispose(), and ensure that WebGL renderer framebuffers are explicitly released from hardware memory once used.
A common issue when generating client-side videos is washed-out colors. This occurs because WebGL outputs colors in the sRGB linear color space, while H.264 encoders convert pixels into the YUV420p chromatic color space. If your colors look desaturated after export, update your rendering context settings to force correct color management:
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;Older integrated GPUs will struggle to output true 4K (3840 x 2160) at 60fps due to hardware bandwidth limits. To handle this gracefully, your pipeline should verify system performance beforehand. If WebCodecs configuration queries (such as VideoEncoder.isConfigured) fail, or if frame processing stalls repeatedly, fall back dynamically to 1080p (1920 x 1080) at 30fps to maintain a responsive user interface.
Moving your media processing pipeline to the client browser drastically reduces your backend attack surface. Because video assets are generated locally, sensitive assets (such as private developer designs or app screenshots) never touch your servers. This local-only architecture is a huge win for privacy and compliance, matching the offline security strategies we write about in our guide on On-Device Cardiometabolic Risk Pipelines.
For production deployments, remember that the WebCodecs API is supported in Safari 15.4+, Chrome 94+, and Edge. However, it is still hidden behind a feature flag in Firefox. Always include feature-detection routines to fall back to a cloud-based renderer if the user's browser does not support the API:
const isWebCodecsSupported = typeof VideoEncoder !== "undefined";
if (!isWebCodecsSupported) {
// Fall back gracefully to backend render processing node
}The performance metrics below illustrate the rendering speeds of a 10-second 3D animation (600 frames total, featuring high-quality reflections and post-processing) across three different hardware profiles and configurations:
Platform / Architecture | Target Resolution | Processing Duration | Estimated Server Hosting Cost |
|---|---|---|---|
Puppeteer + Node.js (GCP n1-standard-4 + T4 GPU) | 4K @ 60fps | 78 seconds | ~$0.18 per render / scaling-dependent |
Client Browser (MacBook Pro M3 Max) | 4K @ 60fps | 14 seconds | $0.00 (Zero marginal cost) |
Client Browser (Intel Core i7 + GTX 1660) | 4K @ 60fps | 39 seconds | $0.00 (Zero marginal cost) |
The standard MediaRecorder API is limited. It runs in real-time, which means if your browser drops a frame during rendering, that frame drop is permanently baked into the final video. By contrast, WebCodecs allows you to step through frames deterministically and encode them at your own pace, ensuring a perfect 60fps output even if the rendering process takes longer than real-time on slower machines.
Safari's WebGL memory limits are strict. To prevent crashes, make sure to manually call frame.close() immediately after passing a frame to VideoEncoder.encode(). Additionally, call renderer.dispose() and clear your video buffers from memory when the render completes to free up the GPU's context memory.
Washed-out colors happen because WebGL renders frames in linear sRGB space, but H.264 expects YUV420p. You can resolve this by configuring your Three.js renderer to output in sRGB color space (renderer.outputColorSpace = THREE.SRGBColorSpace) and using a high-fidelity tone mapping setting to preserve rich colors during export.
The maximum resolution depends on the user's hardware. Most modern mid-to-high-end laptops and desktops support up to 4K (3840 x 2160) and 8K encoding profiles, whereas older integrated mobile GPUs may cap out at 1080p (1920 x 1080).
Transitioning from a server-side media processing architecture to a browser-native pipeline using Three.js, WebCodecs, and client-side MP4 muxing allows web applications to output beautiful, frame-perfect 4K videos at 60fps. By utilizing the user's local hardware, companies can completely eliminate server rendering costs while keeping their users' data entirely private.
This decentralized approach is part of a larger trend toward building interactive, high-performance web tools. When engineering teams want to build and deploy complex client-side graphics systems like this, they prioritize hiring typescript developers who understand the intersection of WebGL rendering, GPU memory lifecycle management, and high-performance browser media APIs.
class DeterministicRenderLoop {
private clock: THREE.Clock;
private mixer: THREE.AnimationMixer;
private renderTarget: THREE.WebGLRenderer;
private scene: THREE.Scene;
private camera: THREE.Camera;
private fps: number;
private frameDuration: number;
private totalFrames: number;
private currentFrame: number = 0;
constructor(
renderer: THREE.WebGLRenderer,
scene: THREE.Scene,
camera: THREE.Camera,
mixer: THREE.AnimationMixer,
fps = 60,
durationSeconds = 5
) {
this.renderTarget = renderer;
this.scene = scene;
this.camera = camera;
this.mixer = mixer;
this.fps = fps;
this.frameDuration = 1 / fps;
this.totalFrames = durationSeconds * fps;
}
public async renderVideo(onFrameCaptured: (canvas: HTMLCanvasElement, frameIndex: number) => Promise) {
this.currentFrame = 0;
while (this.currentFrame < this.totalFrames) {
const elapsedTime = this.currentFrame * this.frameDuration;
this.mixer.setTime(elapsedTime);
this.renderTarget.render(this.scene, this.camera);
await onFrameCaptured(this.renderTarget.domElement, this.currentFrame);
this.currentFrame++;
}
}
}interface WebCodecsExporterConfig {
width: number;
height: number;
fps: number;
bitrate: number;
}
class WebCodecsVideoExporter {
private encoder!: VideoEncoder;
private config: WebCodecsExporterConfig;
private onChunkEncoded: (chunk: EncodedVideoChunk, metadata?: EncodedVideoChunkMetadata) => void;
constructor(
config: WebCodecsExporterConfig,
onChunkEncoded: (chunk: EncodedVideoChunk, metadata?: EncodedVideoChunkMetadata) => void
) {
this.config = config;
this.onChunkEncoded = onChunkEncoded;
this.initEncoder();
}
private initEncoder(): void {
this.encoder = new VideoEncoder({
output: (chunk, metadata) => this.onChunkEncoded(chunk, metadata),
error: (error) => console.error("WebCodecs Encoder Error:", error)
});
this.encoder.configure({
codec: "avc1.640034", // H.264 High Profile, Level 5.2
width: this.config.width,
height: this.config.height,
bitrate: this.config.bitrate,
framerate: this.config.fps,
hardwareAcceleration: "prefer-hardware",
latencyMode: "quality"
});
}
public async encodeFrame(canvas: HTMLCanvasElement, timestampUs: number): Promise {
if (this.encoder.encodeQueueSize > 5) {
await this.waitForQueueToDrain();
}
const frame = new VideoFrame(canvas, { timestamp: timestampUs });
this.encoder.encode(frame, { keyFrame: timestampUs === 0 });
frame.close();
}
private waitForQueueToDrain(): Promise {
return new Promise((resolve) => {
const interval = setInterval(() => {
if (this.encoder.encodeQueueSize <= 2) {
clearInterval(interval);
resolve();
}
}, 10);
});
}
public async finalize(): Promise {
await this.encoder.flush();
this.encoder.close();
}
}Architecting a Local-First 3D App Mockup Engine: WebGL: Explores the fundamental rendering systems of WebGL and Three.js in a client-first browser context.
Implementing SaaS Passkey Onboarding & Multi-Tenant Auth: Details robust client-side encryption and modern secure application practices using TypeScript.
On-Device Cardiometabolic Risk: Private Flutter Imagery Pipeline: Focuses on on-device processing algorithms where performance and privacy compliance are critical.
Tell us about your project and our engineers will get back to you.