Volumetric video player for the web. Lifelike 3D content in the browser — flat screen, VR, or AR — with a single JavaScript import.
Live demos: Player · React · Three.js · PlayCanvas
- WebGPU + WASM — hardware-accelerated volumetric playback at scale
- Cross-platform — 2D, VR, and AR from one SDK; optimized for Meta Quest 3/3S, Pico 4 Ultra, Apple Vision Pro
- Adaptive streaming — seek, buffer, and switch scenes seamlessly
- Developer-first — React hooks, Three.js mesh, PlayCanvas, vanilla JS — pick your stack
- XR built-in — hand tracking, grab-to-move, spatial audio, environment relighting
- Modular — zero required deps; tree-shake what you don't need
| File | Description |
|---|---|
GraciaWebCore.js |
WASM engine — core playback runtime (zero JS dependencies) |
GraciaSDK.js |
Tree-shakeable ES module — player, Three.js, React hooks, XR |
GraciaAIO.js |
Self-contained demo bundle (includes Three.js, React, etc.) |
GraciaSDK.js loads peer dependencies on demand — only what you use. GraciaAIO.js bundles third-party libraries unmodified under their original licenses.
npm install github:gracia-labs/web-sdk # core SDK
npm install github:gracia-labs/web-sdk three # + Three.js
npm install github:gracia-labs/web-sdk playcanvas # + PlayCanvas
npm install github:gracia-labs/web-sdk react gl-matrix # + React hooks & standalone appOr add directly to package.json:
"@gracia/web-sdk": "github:gracia-labs/web-sdk"All peer dependencies are optional — install only what your integration needs:
| Peer dependency | Required for |
|---|---|
three |
SplatsMesh, XROverlay, XR controls |
playcanvas |
GraciaSplats |
react |
useGraciaPlayer, useGraciaPlaylist hooks |
gl-matrix |
GraciaApp standalone 2D+XR player |
@react-three/fiber |
XR UI panels (R3F-based) |
@react-three/uikit |
XR UI panels |
@preact/signals-core |
XR UI reactive state |
@pmndrs/pointer-events |
XR pointer events |
Each demo below is a self-contained HTML file — view the source for a complete, working example.
| Integration | Demo | Source | Description |
|---|---|---|---|
Vanilla JS — GraciaApp |
Player | index.html |
Full-featured player with camera, XR, playback controls |
| React — Hooks | React | react.html |
Declarative integration with useGraciaPlayer and useGraciaPlaylist |
Three.js — SplatsMesh |
Three.js | three.html |
Splats as a standard Three.js mesh with environment relighting |
PlayCanvas — GraciaSplats |
PlayCanvas | playcanvas.html |
Splats with depth testing and automatic shadow casting |
Gracia spawns Web Workers that depend on SharedArrayBuffer, which requires a cross-origin isolated context. Your server must send these headers on every HTML/JS response:
| Header | Value |
|---|---|
Cross-Origin-Opener-Policy |
same-origin |
Cross-Origin-Embedder-Policy |
require-corp (recommended) or credentialless |
Use require-corp for full browser support including Safari / Apple Vision Pro. With credentialless, Safari will not be isolated. Third-party resources under require-corp need Cross-Origin-Resource-Policy: cross-origin.
HTTPS required.
SharedArrayBufferneeds a secure context.localhostis exempt.
Dev server configs
Vite (vite.config.ts)
export default defineConfig({
server: { headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
} },
});Webpack (webpack.config.js)
module.exports = {
devServer: { headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
} },
};Express
app.use((req, res, next) => {
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
next();
});Production — Nginx
server {
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
location ~* \.(js|wasm|css|html)$ {
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
}
}See nginx.conf for a complete example.
GraciaWebCore.js is precompiled Emscripten glue — serve it as a static file, not processed by your bundler.
Vite — vite-plugin-static-copy:
viteStaticCopy({ targets: [{ src: 'node_modules/@gracia/web-sdk/dist/GraciaWebCore.js', dest: 'assets' }] })Webpack — copy-webpack-plugin:
new CopyPlugin({ patterns: [{ from: 'node_modules/@gracia/web-sdk/dist/GraciaWebCore.js', to: 'assets/GraciaWebCore.js' }] })Then reference it:
useGraciaPlayer({ containerRef, moduleUrl: '/assets/GraciaWebCore.js' });self.crossOriginIsolated // → trueIf false, check both headers are present (DevTools → Network → Headers) and no sub-resource is missing Cross-Origin-Resource-Policy.
| Export | Description |
|---|---|
GraciaPlayer |
Core player — play, pause, seek, render |
GraciaApp |
High-level standalone player with camera, XR, mode switching |
SplatsMesh |
Three.js Mesh subclass for scene graph integration |
GraciaSplats |
PlayCanvas integration — splats with depth testing and shadow casting |
| Hook | Description |
|---|---|
useGraciaPlayer(options) |
Manages player lifecycle, returns reactive state |
useGraciaPlaylist(gracia) |
Multi-source playlist with next/prev/goTo |
| Export | Description |
|---|---|
XROverlay |
Full XR experience — hand tracking, grab-to-move, UI panels |
ClassicControls |
Compact VR control panel |
ModernControls |
Full-featured VR control panel with mute, lock, drag-to-reposition |
| Export | Description |
|---|---|
buildApiSources(items, baseUrl) |
Resolve streaming IDs into playable sources |
fetchStreamingMetadata(baseUrl, id, token) |
Fetch metadata for a streaming source |
ENV_PRESETS / presetToLightProbe |
Environment lighting presets for AR |
EnvLighting |
Environment lighting for AR scenes |
| Mode | Key |
|---|---|
| 2D | pw |
| VR | vr |
| AR | ar |
- WebGPU —
navigator.gpurequired (support): Chrome 113+, Edge 113+, Safari 18+, Firefox Nightly (flag) - HTTPS —
SharedArrayBufferneeds a secure context (localhostexempt) - Cross-Origin Isolation — COOP + COEP headers (see above)
- WebXR — VR/AR modes only (Meta Quest Browser, etc.)
The player needs a streamingId (identifies the scene) and a view token (authorizes playback). Both can be created in your Gracia account under Settings → Api Settings.
Important. Settings → Api Settings is available only for partner Gracia accounts. If you want to become a partner, write to support at
[email protected].
Base URL: https://streaming.gracia.ai
Not recommended. Long-lived tokens are less secure — anyone with the token can watch the content. Use Option 2 whenever possible.
Create a view token in Settings → Api Settings, set the streamingIds it can access, optionally restrict to specific domains, and pass it directly to the player. No backend needed.
Only suitable for: embedded players, kiosks, demo pages where per-user access control is not needed. Always use domain whitelisting to limit exposure.
Implement an endpoint on your backend that your frontend calls when a user wants to watch content. This endpoint verifies access, requests a short-lived view token from Gracia API, and returns the tokens to the frontend. The API token never leaves your server.
Best for: pay-per-view, purchases, subscriptions.
- Create an API token in Settings → Api Settings. Store it securely on your server.
- Implement an endpoint (e.g.
POST /api/streaming-access/{contentId}) that authenticates the user, checks access, calls Gracia API to issue a view token, and returnsstreamingId+viewToken. - Your frontend calls this endpoint and passes the received tokens to the player.
┌──────────┐ ┌──────────────┐ ┌───────────────┐ ┌──────────────────┐
│ Player │ │ Your Backend │ │ Your Database │ │ Gracia Streaming │
│ (client) │ │ │ │ │ │ API │
└────┬─────┘ └──────┬───────┘ └──────┬────────┘ └────────┬─────────┘
│ │ │ │
│ 1. User wants to │ │ │
│ watch content │ │ │
│─────────────────────>│ │ │
│ │ │ │
│ 2. Your backend │ │
│ validates access │ │
│ │ │ │
│ │ 3. Issue view token │ │
│ │ POST /view-token/issue │
│ │───────────────────────────────────────────────────>│
│ │ │ │
│ │ 4. View token + tokenId │
│ │<───────────────────────────────────────────────────│
│ │ │ │
│ │ 5. Save token state │ │
│ │ {userId, tokenId} │ |
│ │───────────────────────>│ │
│ │ │ │
│ 6. Return streaming │ │ │
│ ID + view token │ │ │
│<─────────────────────│ │ │
│ │ │ │
│ 7. Pass streamingId + viewToken to player │
│ → player handles playback │
When persisting issued tokens, store at least userId and tokenId, so your backend can safely update or revoke token access later.
- Specification file:
api-streaming-view-token.openapi.yaml - To view it in Swagger Editor, open Swagger Editor and load this YAML file (or paste the API content directly).
- You can also use any other Swagger/OpenAPI-compatible editors or viewers supporting the current version of the Specification.
Thanks to Martin Valigursky for help with the PlayCanvas integration.
Proprietary — Gracia Labs. All rights reserved.