Immersive AI Video Generator SDK
Add BuildVR's Immersive AI Video Generator to your own website. Your users upload a normal video, pick the scenes they want, and each picked scene is converted into an immersive cylindrical video (IMRS 2.2). The scenes are then stitched into one final video with the original audio.
Upload video → choose scenes → convert → check results → create final video
There are two ways to use it:
- Ready-made React components — drop in the complete tool (upload page, scene picker, player, final video) with a few lines. No third-party branding, and you can match its colours to your site.
- The JavaScript API — build your own interface on the same calls the components use.
Both run in the browser with no backend of your own and no secret key. Detected cuts are used as they are; changing a scene's fill, AI prompt or cut points is optional.
Prerequisites
The SDK works on BuildVR enterprise plans only. On other plans, every call returns upgrade_required. If you haven't subscribed yet, view enterprise pricing.
- A BuildVR enterprise plan.
- A publishable client key (
PUBkey). Itsclient_idis the only credential the SDK uses. - Your website's domain on that key's allowed domains, for example
www.example.com, plus your local development host such aslocalhost:5173.
Step 1 — Create API Credentials
After your plan is active, generate a publishable key from your profile:
https://buildvr.ai/profile/api-credentials
Manage, rotate, and revoke your API credentials from this page.
Add Allowed Domains
BuildVR only accepts requests from the browser's real Origin, which page scripts can't fake, and only if that origin is on the key's allowed list. Add every host that will load the SDK, separated by commas:
localhost:3000, localhost:5173, www.example.com
http:// or https://.Save Your Client ID
Copy the key's client ID. You'll pass it as clientId. It can safely appear in your page source, but keep it in an environment variable so you can rotate it easily. Everything users do through the SDK (uploads, conversions, credits) is billed to the account that owns the key.
NEXT_PUBLIC_VIDEO_CLIENT_ID=YOUR_CLIENT_IDStep 2 — Install the Package
npm install @buildvr/immersive-ai-video-generatoryarn add @buildvr/immersive-ai-video-generatorpnpm add @buildvr/immersive-ai-video-generatorThe React components need React 18 or later (react and react-dom). Without a bundler, load the API from a CDN:
<script src="https://unpkg.com/@buildvr/immersive-ai-video-generator@0.3"></script>
<script>
// window.SceneReview
SceneReview.init({ clientId: "YOUR_CLIENT_ID" });
</script>Changelog and release notes: npmjs.com/package/@buildvr/immersive-ai-video-generator
Step 3 — Add the Drop-in Components
Everything in one component
ImmersiveVideoGenerator gives your users the whole flow: the upload page, the job page with scene picking, conversion and the final video download, and a list of earlier jobs. It connects with clientId itself, so you don't need to call init(). Import the stylesheet once.
import { ImmersiveVideoGenerator } from "@buildvr/immersive-ai-video-generator/panel";
import "@buildvr/immersive-ai-video-generator/panel/style.css";
export default function VideoToolPage() {
return (
<ImmersiveVideoGenerator
clientId={process.env.NEXT_PUBLIC_VIDEO_CLIENT_ID}
onComplete={(job) => console.log("Final video:", job.output_url)}
/>
);
}To keep the open job in the URL so reloads and shared links work, control it yourself:
const [jobId, setJobId] = useState(new URLSearchParams(location.search).get("job"));
<ImmersiveVideoGenerator
clientId={process.env.NEXT_PUBLIC_VIDEO_CLIENT_ID}
jobId={jobId} // null = upload page
onJobChange={(id) => { // job created or opened, or "New video" clicked
setJobId(id);
history.pushState(null, "", id ? `?job=${id}` : location.pathname);
}}
/>One component per page (Next.js and other routers)
If your site has routing, use an upload page and a job page. A Next.js App Router example:
"use client";
import { useRouter } from "next/navigation";
import { CreateVideo } from "@buildvr/immersive-ai-video-generator/panel";
import "@buildvr/immersive-ai-video-generator/panel/style.css";
export default function NewVideoPage() {
const router = useRouter();
return (
<CreateVideo
clientId={process.env.NEXT_PUBLIC_VIDEO_CLIENT_ID}
onCreated={(jobId) => router.push(`/video/${jobId}`)}
/>
);
}"use client";
import { use } from "react";
import { useRouter } from "next/navigation";
import { VideoJob } from "@buildvr/immersive-ai-video-generator/panel";
import "@buildvr/immersive-ai-video-generator/panel/style.css";
export default function VideoJobPage({ params }) {
const { jobId } = use(params);
const router = useRouter();
return (
<VideoJob
clientId={process.env.NEXT_PUBLIC_VIDEO_CLIENT_ID}
jobId={jobId}
onNewVideo={() => router.push("/video")}
/>
);
}The components are already marked as client components. With React Router, Vue Router or any other router the pattern is the same: navigate in onCreated, and pass the job ID from the URL to VideoJob.
Component Props
All components accept clientId, plus optional apiBase, className and style. If your page already called SceneReview.init(), you can leave out clientId.
<ImmersiveVideoGenerator />
| Prop | Description |
|---|---|
jobId, onJobChange(jobId | null) | Optional. Control which job is open. |
onComplete(job) | Called once when the final video is ready. job.output_url is the video. |
<CreateVideo /> — the upload page
| Prop | Description |
|---|---|
onCreated(jobId) | Required. Called when the job is created, and when the user opens an earlier job from the sidebar. Navigate to the job page here. |
onOpenJobs() | Optional. Makes the “Jobs” breadcrumb a link to your jobs page. |
showJobs | Default true. Shows “Resume a job” and recent jobs in the sidebar. |
<VideoJob /> — one job, from scenes to final video
| Prop | Description |
|---|---|
jobId | Required. |
onNewVideo() | Optional. Shows a “New video” button. |
onOpenJobs() | Optional. Makes the “Jobs” breadcrumb a link. |
onComplete(job) | Called once when the final video is ready. |
<JobsList /> — recent jobs for your own jobs page
| Prop | Description |
|---|---|
onOpenJob(jobId) | Required. |
limit | Jobs per page. Default 8. |
Matching Your Brand
The components use a dark theme. Override these CSS variables with style or className:
<ImmersiveVideoGenerator
clientId={process.env.NEXT_PUBLIC_VIDEO_CLIENT_ID}
style={{ "--cyan": "#a78bfa", "--cyanStrong": "#8b5cf6", "--bg": "#0b0b12" }}
/>| Variable | Used for |
|---|---|
--cyan, --cyanStrong | Accent colour, primary buttons, focus rings |
--bg, --panel, --card, --input | Backgrounds |
--border, --text, --muted, --faint | Lines and text |
--ok, --warn, --bad | Status colours |
How a Job Works
| Step | What happens | How you know it's done |
|---|---|---|
| 1. Upload | uploadVideo() sends the file to storage and creates a job. | You get job_id. |
| 2. Detect | BuildVR finds the scene cuts and makes a preview clip and thumbnail for each scene. Usually a few minutes, depending on video length. | review.stage === "review" and review.scenes has items. |
| 3. Choose & adjust (optional edits) | Choose the scenes to keep. Optionally change a scene's fill or AI prompt, move its cuts, split it, or merge it with the next scene. | Your UI decides. |
| 4. Convert | convertScenes() queues the chosen scenes. Each one converts on its own. | Each scene goes queued → processing → completed, with an output_url. |
| 5. Check (optional) | Play scene.output_url next to scene.preview_url. Call reprocessScene() to redo one. | — |
| 6. Final video | createFinalVideo() stitches the chosen scenes in order and adds back the original audio. Scenes you didn't choose are left out and not billed. | job.status === "completed" and job.output_url is set. |
After step 6 starts, the job is locked and no more edits are possible.
Fill modes
Cylindrical video is much wider than the source, so the extra space has to be filled:
| fillMode | Result |
|---|---|
"ai" (default) | AI generative expand: new scenery that continues the shot, optionally guided by a prompt. |
"blur" | A blurred, stretched copy of the frame. |
"black" / "white" | Solid bars. |
Building Your Own UI with the API
All methods are on the default export, SceneReview. Every method except init() resolves to a result object and never throws on API errors.
import SceneReview from "@buildvr/immersive-ai-video-generator";
// 1. Connect once per page load. Throws if the key, domain or plan is wrong.
await SceneReview.init({ clientId: "YOUR_CLIENT_ID" });
// 2. Upload. Creates the job and starts scene detection.
const upload = await SceneReview.uploadVideo(file, {
name: "Beach trip",
fillMode: "ai",
targetResolution: "4k",
onProgress: ({ stage, progress }) => console.log(stage, Math.round(progress * 100) + "%"),
});
if (!upload.success || !upload.job_id) throw new Error(upload.error);
const jobId = upload.job_id;
// 3. Wait for scene detection.
let review = await waitFor(jobId, (r) => r.stage === "review" && r.scenes.length);
// 4. Choose scenes (here: all of them) and convert them.
const picked = review.scenes.map((s) => s.id);
await SceneReview.convertScenes(jobId, picked);
// 5. Wait until every picked scene has a converted output.
review = await waitFor(jobId, (r) =>
r.scenes.filter((s) => picked.includes(s.id)).every((s) => s.output_url && s.status === "completed"));
// 6. Stitch the final video and wait for it.
await SceneReview.createFinalVideo(jobId, picked);
let job;
do {
await sleep(5000);
({ job } = await SceneReview.getJob(jobId));
} while (job && !["completed", "failed"].includes(job.status));
console.log("Final video:", job.output_url);
// Helpers
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function waitFor(jobId, done) {
for (;;) {
const res = await SceneReview.getReview(jobId);
if (res.success && done(res.review)) return res.review;
await sleep(3000);
}
}Methods
| Method | Description |
|---|---|
init({ clientId, apiBase? }) | Checks the key, domain and plan. Call once before anything else. Throws on failure. |
getSession() / destroy() | Read or forget the current session, for example on sign-out. |
uploadVideo(file, options) | Uploads a File or Blob and creates the job. Options: name, fillMode, targetResolution ("4k", "5.7k", "8k" or "WIDTHxHEIGHT"), onProgress, signal. |
listJobs({ page, limit, status? }) | This account's jobs, newest first. |
getJob(jobId) | The job's overall state, including output_url for the final video. |
readVideoDuration(file) | A local video's length in seconds, or null. |
getReview(jobId) | The job and all its scenes. This is the call to poll. |
convertScenes(jobId, sceneIds, settings?) | Converts the chosen scenes and retries failed ones. settings: { [sceneId]: { fillMode?, prompt? } }. |
reprocessScene(jobId, sceneId, opts) | Converts a scene again, optionally with a new fill or prompt. Free up to free_reprocesses_left, then uses wallet credits. |
createFinalVideo(jobId, sceneIds) | Stitches the listed, converted scenes into the final video. Locks the job. |
updateSceneSettings(jobId, sceneId, { fillMode }) | Saves a scene's fill before it is converted. |
updateBoundaries / splitScene / mergeScenes | Optional cut edits, in frame numbers (review.fps frames per second). |
retryPreview(jobId, sceneId) | Retries a scene preview that failed to generate. |
The full reference, including the review and Scene data shapes and cut-editing rules, is in the package README.
Handling Errors
Every call except init() resolves to an object like this on failure:
{ success: false, status_code: 409, error: "Human-readable message", code: "machine_readable_code", ...details }| code | When | What to do |
|---|---|---|
quota_exceeded | From uploadVideo: the plan's quota is used up and the wallet can't cover the overage. | Recharge at buildvr.ai/profile/credits. |
insufficient_credits | A reprocess needs more wallet credits (402). | Recharge at buildvr.ai/profile/credits. |
not_ready_to_stitch | A chosen scene isn't converted yet. scene_ids lists which. | Convert them, or remove them from the list. |
no_scenes | Empty sceneIds. | Choose at least one scene. |
scene_converting | The scene is busy converting. | Wait, then retry. |
preview_not_ready | The preview is still updating after a cut edit. | Wait for preview_status: "ready". |
output_requires_reprocess | updateSceneSettings on a converted scene. | Use reprocessScene instead. |
discard_outputs_required | A cut edit touches converted scenes. | Confirm with the user, then retry with { confirmDiscardOutputs: true }. |
neighbour_too_short | A cut edit would leave a neighbouring scene too short. | Retry updateBoundaries with { mergeShort: true }. |
not_in_review | Scenes are still being detected, or the final video has started. | Check review.stage. |
job_not_found | Wrong ID, or the job belongs to another account (404). | — |
Account-level failures
| status_code | Meaning |
|---|---|
| 401 / 403 | Unknown or inactive clientId, or this page's domain isn't on the key's allowed list. |
400 with upgrade_required: true | The account isn't on an enterprise plan. |
Polling
There are no webhooks yet, so poll for progress. The drop-in components do this for you.
| Waiting for | Call | Suggested interval |
|---|---|---|
| Scene detection | getReview | 3 s |
| Scene conversions / preview updates | getReview | 3 s |
| Idle (user is choosing scenes) | getReview | 15 s, or stop polling |
| Final video | getJob | 3–5 s |
job.status is completed or failed. While an action is running, pause polling so an older response doesn't overwrite the newer review the action returns.Billing
- Uploads and conversions count against the plan of the account that owns the client key.
- When the plan's quota is used up, extra storage and processing minutes are paid automatically from the account's wallet credits, with no confirmation step. Calls fail with
quota_exceededorinsufficient_creditsonly when the wallet runs short. - You're billed only for scenes in the final video. Scenes left out are refunded when the final video is made.
- Each scene includes free reprocesses (
init().scene_review.free_reprocesses_per_scene). After those, reprocesses use wallet credits at the scene's length.
Troubleshooting
Add the exact host shown in the browser's address bar, including the port for local development, to the key's allowed domains.
Can I call this from my server (Node)?
No. The SDK depends on the browser's Origin header and uses XMLHttpRequest and <video> for uploads. For server-to-server work, use the Video Processing API with a secret key.
Which browsers are supported?
Current versions of Chrome, Edge, Safari and Firefox.
Next Steps
See exactly what your users will get. Open the Immersive AI Video Generator →
Rotate keys, update domain allowlists, and view usage. Open credentials dashboard →
Full API reference, release notes and version history. View on npm →
Reach our engineering team for integration support. Open a support ticket →