Two-device setup: a display screen shows the camera feed + AI output with a QR code, and a phone scans the QR to browse and select products. Only connects to Decart when a phone is active — no credits used while idle.
A digital mirror for in-store kiosks or smart mirrors. The display creates a session and shows a QR code. A customer scans it with their phone, browses a product grid, and taps to try on. The display picks up the selection and streams the AI try-on result. Includes portrait cropping for vertical screens.
cd examples/digital-mirror
npm installcp .env.example .env.localOpen .env.local and add your Decart API key:
DECART_API_KEY=sk_your_key_hereTip: Get your API key from platform.decart.ai. See the Authentication guide for details.
npm run devOpen http://localhost:3000 on the display screen. Scan the QR code with your phone (or open /remote/XXXXXX in another browser tab).
Page loads (display)
→ Camera starts (getUserMedia, environment-facing)
→ Portrait stream created (landscape 1280×720 → portrait 720×1280)
→ Session created → QR code rendered
→ Phone scans QR → claims session
→ Display detects controller → fetch client token
→ Connect to Decart's lucy-vton-latest model (WebRTC)
→ Phone taps product → display polls → setImage(garment, prompt)
→ AI video stream shows the person wearing the garment
→ Phone taps "Done" or leaves → disconnect (stops billing)
Session lifecycle:
- Display creates a session and renders a QR code pointing to
/remote/{sessionId} - Phone scans QR, validates the session, and claims control
- Display detects the controller via polling and connects to Decart
- Phone sends product selections; display polls and calls
setImage()for each - Phone sends heartbeat pings every 5s; if pings stop for 10s, session is released
- Phone taps "Done" or navigates away → controller released → Decart disconnected
All session state is stored in-memory on the server (no database needed). Sessions expire after 30 minutes of inactivity.
| File | Purpose |
|---|---|
app/page.tsx |
Display page — camera, portrait stream, Decart connection, QR code, polling for selections |
app/remote/[sessionId]/page.tsx |
Phone controller — session claiming, product grid, heartbeat, release |
hooks/usePortraitStream.ts |
Crops landscape camera to portrait via off-screen canvas + captureStream() |
hooks/useMirrorCamera.ts |
Environment-facing camera with fallback to any camera |
hooks/useDecartRealtime.ts |
Decart WebRTC connection management |
lib/mirror-store.ts |
In-memory session store (sessions, controllers, selections) |
lib/products.ts |
Product catalog with hardcoded prompts |
app/api/mirror/*/route.ts |
6 API routes: session, claim, poll, select, ping, release |
app/api/tokens/route.ts |
Server-side Decart client token creation |
Digital mirrors typically use portrait-oriented displays (vertical screens), but webcams output landscape video (1280×720). The usePortraitStream hook transforms the camera feed before sending it to Decart:
- Creates an off-screen
<canvas>at 720×1280 (portrait) - Creates a hidden
<video>element playing the camera stream - Uses
requestAnimationFrameat 20fps to draw a center-cropped frame from the video onto the canvas - Calls
canvas.captureStream(20)to produce a newMediaStream - This portrait stream is sent to Decart instead of the raw camera stream
The math:
Camera: 1280×720 (landscape, aspect ratio 1.78)
Output: 720×1280 (portrait, aspect ratio 0.5625)
Since camera aspect (1.78) > output aspect (0.5625):
→ Camera is wider than needed → crop the sides
cropH = 720 (full height)
cropW = 720 × (720/1280) = 405
cropX = (1280 - 405) / 2 = 437 (center horizontally)
cropY = 0
Result: A 405×720 center strip from the camera, scaled to 720×1280
Add ?landscape to skip cropping and send the raw camera stream.
| Parameter | Default | Description |
|---|---|---|
?flip=false |
true (mirrored) |
Disable horizontal flip |
?landscape |
portrait crop | Skip portrait cropping, send raw landscape stream |
Edit lib/products.ts. Each product needs an id, name, image path, and prompt:
{
id: "striped-polo",
name: "Striped Polo",
image: "/products/striped-polo.jpg",
prompt: "Substitute the current top with a navy and white striped polo shirt with a slim fit",
price: 45,
}Place the garment image in public/products/. Prompts are sent directly to the model with enhance: false, so write detailed prompts for best results.
This example is intentionally simplified to focus on the two-device mirror pattern. For production you'd likely want:
- Database-backed sessions — The in-memory store (
lib/mirror-store.ts) resets on server restart and doesn't work across multiple instances. Swap the Maps with Postgres, Redis, or any key-value store. The interface is minimal so the migration is straightforward. - Image uploads and custom prompts — Let users upload their own garment photos or enter free-text style prompts from the phone. Requires an image upload endpoint (e.g. S3 or Vercel Blob) and extended selection types.
- Recording and sharing — Capture snapshots or short video clips of the try-on result and let users share via email, SMS, or QR code.
| Variable | Required | Purpose |
|---|---|---|
DECART_API_KEY |
Yes | Creates client tokens for realtime WebRTC connections |