Mreyti home  ·  SDK v0.1.0 preview

Mreyti SDK integration

A Web Component that puts glasses on a shopper's face through their webcam, rendered at true physical size. Everything runs on their device. This is what you need to integrate it.

Setting up your SDK?

Start with the Mreyti SDK setup guide → — prepare your SDK, publish or host it, configure a customer website, and download a complete HTML example. This page is the detailed API reference.

Preview — read before shipping

Version 0.1.0. The API below ships and is verified to install and typecheck against a clean project. The geometry it renders — where frames sit, how temple arms are hidden behind the head, the metric scale — has never been checked against a recorded fixture. Ship it as a preview, not as a fitting tool.

01Requirements

HTTPSMandatory. getUserMedia refuses any insecure origin. localhost is exempt for development.
WebGL2For lens refraction. Without it the lens degrades to a tinted surface rather than vanishing.
BrowsersUse a current browser with camera access and WebGL2; verify your target devices before release.
Bandwidth~6.4–6.5 MiB gzipped for the external tracker assets, fetched on Start. The standalone script plus tracker is approximately 7.13 MiB; the project’s 5 MiB release budget is currently exceeded.
The most common deployment failure

On a plain HTTP origin — or behind a certificate the browser doesn't trust — the page loads, the button works, and the camera request is silently rejected. It looks like a broken widget rather than a configuration problem. See Deploying.

02Install

npm install @pykero/eyewear-vto@preview three @mediapipe/tasks-vision

three and @mediapipe/tasks-vision are peer dependencies, so a host already using three.js doesn't ship a second copy of it — three alone is ~170 kB gzipped.

ESM build48.8 KiBgzipped · deps external
Script build634.3 KiBgzipped · fully bundled

03Script quick start

Use the standalone script (IIFE) for an HTML page, CMS, or website builder that permits custom JavaScript. No framework or npm installation is needed on the receiving website. The SDK package is still named @pykero/eyewear-vto.

1. Get the SDK file

To use this repository’s current fixes, build the SDK from the repository root:

npm ci
npm run build:lib

Upload dist-lib/eyewear-vto.iife.js to your website as /vendor/mreyti/eyewear-vto.iife.js. This is a file you must deploy; the path below is not a hosted Mreyti service. Published package previews may be older than the repository.

2. Paste this into your page

<!-- Place this where the mirror should appear. -->
<eyewear-tryon sku="DEV-48" style="display:block; width:100%; max-width:640px"></eyewear-tryon>
<script defer src="/vendor/mreyti/eyewear-vto.iife.js"></script>

Load the script once per page. DEV-48 is an included sample frame, so this example works without a catalogue. Serve the page over HTTPS, or localhost for development. The widget shows its own Start button and asks for camera access only after it is pressed.

The widget JavaScript downloads when the script loads; the external tracker model and WASM download on Start. The .iife.js file includes its JavaScript dependencies. Do not substitute eyewear-vto.js: that ESM build expects a bundler or an import map resolving its dependencies.

3. Verify the embed

Check that the SDK request returns JavaScript with HTTP 200, the Start button appears, and your camera opens after permission. Close or remove the widget and check that the camera indicator turns off. Test on your target phone as well: controls at 320 px are a known unresolved layout issue.

For your products, register frame specifications and model URLs through setFrames() before starting, then select a registered SKU. Set up both tracker asset paths if you want them served from your own domain.

Where to put it

WebsiteIntegration
Plain HTMLPaste the element into the page body and load the script once.
WordPress / WooCommerceLoad the SDK through your theme’s script setup and place the element in a product template or custom HTML block. Your editor must allow the custom element.
ShopifyUpload the SDK as a theme asset, load it once in the layout, and place the element in the product section. Register your catalogue before selecting the product’s SKU.
Webflow / other buildersUse a custom HTML embed for the element and a custom-code area for the script. A plan and editor that permit JavaScript are required; verify on the published page.
React / Vue / other bundled appsUse the npm/ESM route below. Import only in the browser for server-rendered apps.

If a builder runs your embed inside an iframe, the parent must permit camera use in its Permissions Policy and iframe allow="camera" attribute. A script cannot override a host that blocks cameras or strips custom HTML. See camera security requirements.

npm / ESM SDK

import '@pykero/eyewear-vto'

await customElements.whenDefined('eyewear-tryon')
const el = document.querySelector('eyewear-tryon')
// Set your catalogue before selecting a product SKU or starting.
// el.setFrames(yourFrames)

The element markup is the same for both approaches. When calling methods from a separate script, wait for customElements.whenDefined('eyewear-tryon'); a deferred script may not have registered the element yet. Call el.stop() when hiding a modal; removing the element also releases the camera.

Open the integration playground to inspect attributes, events and catalogue selection, or try the Mreyti mirror.

React

import { useEffect, useRef } from 'react'
import '@pykero/eyewear-vto'

export function TryOn({ frames, sku }) {
  const ref = useRef(null)

  useEffect(() => {
    const el = ref.current
    if (!el) return
    el.setFrames(frames)          // property, not attribute
    const onCal = (e) => console.log('PD', e.detail.pdMm)
    el.addEventListener('vto-calibrated', onCal)
    return () => el.removeEventListener('vto-calibrated', onCal)
  }, [frames])

  return <eyewear-tryon ref={ref} sku={sku} />
}

Arrays and objects must be set as properties. React 18 and below serialise unknown attributes to strings, so the ref approach is required there.

Vue

// vite.config.js — otherwise Vue warns on every render
vue({ template: { compilerOptions: { isCustomElement: (t) => t === 'eyewear-tryon' } } })

Next.js and other SSR

The component touches window and customElements at import, so load it client-side only:

useEffect(() => { import('@pykero/eyewear-vto') }, [])

04Your catalogue

el.setFrames([
  {
    spec: {
      sku: 'ACME-52',
      name: 'Acme Round',
      lensWidthMm: 52,      // the "52" in 52□18 145
      bridgeMm: 18,         // the "18"
      templeLengthMm: 145,  // the "145"
      lensHeightMm: 42,
      rimThicknessMm: 3,
      frontWidthMm: 138,    // hinge to hinge across the front
    },
    modelUrl: '/frames/acme-52.glb',
    modelUrlLow: '/frames/acme-52-low.glb',   // optional, used past ~1.1 m
  },

  // No model yet? Still listable — renders procedurally from its millimetres.
  { sku: 'ACME-58', name: 'Acme Wide', lensWidthMm: 58, bridgeMm: 18,
    templeLengthMm: 145, lensHeightMm: 44, rimThicknessMm: 3, frontWidthMm: 148 },
])
The millimetres are the render, not metadata

The frame is drawn at the size you declare, against a head measured in the same units. Get frontWidthMm wrong and the frame is wrong on every face — consistently and invisibly, because a wrongly-sized frame still looks like a plausible pair of glasses. Take the numbers off the temple arm, not off a marketing page.

Frames are never scaled to fit the detected face. That is the whole product: a 48 mm frame that looks too narrow on a wide face is the system telling the truth.

05Preparing models

FormatglTF 2.0 — KHR_materials_transmission, Meshopt, KTX2/Basis
ScaleMillimetres. A 138 mm frame spans 138 units.
OriginCentre of the bridge, rear face of the front
Axes+X model's left, +Y up, +Z out of the face
LightingRuntime lighting only. Do not bake lighting into frame textures.
LensesRemove them — the renderer supplies lenses at CR-39 index
Budget≤40k triangles high LOD, ≤8k low

Your model is measured, and refused if it disagrees

CheckTolerance
Front width vs frontWidthMm±1.5 mm
Unit ratio — 1000×, 100×, 10× and inversesreported as a unit error, with the fix
Height vs lensHeightMm + 2·rimThicknessMm−2.5 mm
Depth vs templeLengthMm≥ 50%

A failing model falls back to the procedural frame and fires an event. It is never silently rescaled — a wrongly-scaled model is internally consistent, so rescaling would hide the fault instead of fixing it.

el.addEventListener('vto-model-rejected', (e) => {
  console.warn(e.detail.sku, e.detail.reason)
  // "Model measures 138000.0 mm across, 1000x its declared 138 mm. This is a
  //  unit error in the export, not a bad scan — glTF is metres by convention
  //  and this project's scene is millimetres."
})

Wire that into your logging. It's the difference between noticing a bad export and shipping a catalogue where one SKU is quietly the wrong size.

06Self-hosting the tracker

By default the face tracker's runtime and model come from public CDNs. Fine for evaluation, wrong for production: your try-on then depends on a third party staying up, and fires a cross-origin request the moment a shopper opens it.

mkdir -p public/vendor/mediapipe
cp -r node_modules/@mediapipe/tasks-vision/wasm public/vendor/mediapipe/
curl -o public/vendor/mediapipe/face_landmarker.task \
  https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task
<eyewear-tryon
  wasm-base="/vendor/mediapipe/wasm"
  model-url="/vendor/mediapipe/face_landmarker.task"
></eyewear-tryon>
Both or neither

A half-configured pair silently mixes a self-hosted runtime with a CDN model — the worst of both. The wasm directory holds SIMD and non-SIMD builds; the browser downloads only the variant it needs. Serve it with long-lived cache headers.

07Measurement

el.calibrate()            // shows the card outline
const pd = el.measure()   // null if the solve was rejected
el.pdMm                   // card-referenced PD in mm, or null
el.clearCalibration()     // deletes the stored measurement

The shopper holds any bank card — ISO/IEC 7810 ID-1, 85.60 × 53.98 mm — flat against their forehead and moves until it fills the on-screen outline. That gives one known length in frame, which is what makes true scale solvable at all.

Until they do it, frames are drawn against a canonical head: the right shape at an average size. Say so in your UI rather than presenting an uncalibrated session as a measurement.

08API

Methods

start()Loads the tracker and opens the camera. Returns a promise.
stop()Releases the camera and tears down the scene.
setFrames(frames)Replace the catalogue. Specs and model entries, mixed freely.
calibrate()Show the card guide.
measure()Take the measurement. Returns PD in mm, or null.
clearCalibration()Remove the stored measurement from this device.
capture(type?)A still of the try-on as a data URL.

Properties and attributes

skuCurrent SKU. Also an attribute.
framesThe catalogue, read-only.
pdMmCard-referenced PD, or null.
assetUrlsSelf-hosted tracker assets. Also wasm-base / model-url.
auto-startAttribute. Starts without a click — a bad default on a product page.

Events

EventFires whendetail
vto-readyCamera open, tracking running{ fixture }
vto-errorCamera denied, or tracker failed to load{ message }
vto-tracking-changedFace found or lost{ tracking }
vto-calibratedMeasurement accepted{ pdMm, metricScale }
vto-calibration-rejectedSolve outside plausible bounds{}
vto-model-rejectedA model failed QA; stand-in showing{ sku, reason, qa }
vto-frame-changedSKU changed{ sku }
vto-capturecapture() produced a still{ dataUrl, type }

Styling

Everything lives in a shadow root, closed to your CSS on purpose — product pages have opinionated resets. One hook is exposed:

eyewear-tryon::part(controls) { background: none; }

09Deploying

Any static host works — the build is plain files. What matters is TLS.

The repo ships a Dockerfile: a two-stage build serving dist/ from non-root nginx on port 8080, 25 MB, with a healthcheck, running on a read-only filesystem.

Behind a reverse proxy

  • Route to container port 8080. The image runs as uid 101, and unprivileged processes cannot bind ports below 1024.
  • Terminate TLS with a publicly trusted certificate. Self-signed is not enough — browsers block the camera.
  • Let's Encrypt HTTP-01 validation needs port 80 open, even though the site serves on 443.

Behind Cloudflare

Two traps that cost hours

A proxied (orange-cloud) record intercepts the ACME challenge, so your origin can never obtain a Let's Encrypt certificate. Set the record to DNS only while issuing, then re-proxy if you want.

With SSL/TLS mode Full (strict) against an untrusted origin certificate, Cloudflare returns 502 without ever fetching your working page. Give the origin a real certificate, or use mode Full. Never Flexible — it shows a padlock while the Cloudflare-to-origin hop is plain HTTP.

10Troubleshooting

SymptomCause
Page loads, camera never startsNot a secure context — HTTP, an untrusted cert, or an IP/:port URL
NotAllowedErrorPermission denied, or a Permissions-Policy header blocks camera. In an iframe you need allow="camera".
Frames look right but too small or largeUncalibrated — drawn against a canonical head
One SKU is the wrong sizeFailed dimensional QA and fell back. Listen for vto-model-rejected.
Frames render blackNo environment map, or WebGL2 unavailable — metal has no diffuse term
Two pairs of glasses visibleShopper is wearing their own. Known and accepted.
Frames drift past ~45° of yawTracking degrades there by design; pose is damped rather than allowed to swim
502 from a CDNOrigin certificate untrusted — see Deploying

11Privacy

No video frame, landmark, or derived measurement leaves the browser. No network call in this package carries anything derived from the camera. The preview downloads software and model assets; self-hosting moves those downloads to your own origin. Never send camera captures, landmarks or measurements to analytics, logging or external services.

Calibration stores a versioned scalar solve and camera intrinsics in local storage, with no video or landmarks. Reuse additionally requires the exact same live camera stream, proven only in memory; a reload or a new stream requires calibration again. Offer clearCalibration() to remove it.

Keep captures local

capture() returns a data URL to the calling page. Use it for a local preview or download only. The project prohibits uploading camera-derived data.

No face recognition, no identity matching, no embeddings — tracking only, by design and by invariant.