Chapter 13: Camera Access
Updated: Sep 4, 2026
IWSDK uses the browser MediaDevices API to expose camera streams as VideoTexture objects. CameraSystem manages stream startup and cleanup, CameraSource stores configuration and output, and CameraUtils provides device and frame utilities.
The camera system works while the IWSDK world is visible, including browser-only worlds and immersive XR sessions. It stops active streams when the page or session becomes hidden.
For a manifest-based project, enable the camera feature in iwsdk.config.json:
{
"version": "iwsdk.project.v1",
"scene": "./public/scenes/main.iwsdk.scene.json",
"world": {
"xr": {
"mode": "ar"
},
"features": {
"camera": true
}
}
}
The feature registers CameraSystem and CameraSource while the world initializes.
Create an entity and add CameraSource after the world is ready:
import {
CameraFacing,
CameraSource,
CameraState,
CameraUtils,
World,
} from '@iwsdk/core';
import projectOptions from 'virtual:iwsdk-project';
const container = document.getElementById('scene-container');
if (!(container instanceof HTMLDivElement)) {
throw new Error('Missing #scene-container');
}
const world = await World.create(container, projectOptions);
const cameraEntity = world.createEntity();
cameraEntity.addComponent(CameraSource, {
facing: CameraFacing.Back,
width: 1920,
height: 1080,
frameRate: 30,
});
A browser permission prompt can appear when the system requests the stream. You can request permission and enumerate devices earlier by calling CameraUtils.getDevices() from a user-initiated application flow.
CameraSource has the following input fields:
deviceId: A camera device ID. The default empty string selects a device by facing.facing: CameraFacing.Back, CameraFacing.Front, or CameraFacing.Unknown. The default is CameraFacing.Unknown.width: The ideal video width. The default is 1920.height: The ideal video height. The default is 1080.frameRate: The ideal frame rate. The default is 30.
When facing is unknown and deviceId is empty, the system selects the first available camera. When a specific facing is requested, selection fails if no matching camera is found.
The component also contains system-managed output:
state: CameraState.Inactive, CameraState.Starting, CameraState.Active, or CameraState.Error.texture: A VideoTexture, or null until the stream is active.videoElement: The underlying HTMLVideoElement, or null until active.stream: The underlying MediaStream, or null until active.
Read the state before using the texture:
const state = cameraEntity.getValue(CameraSource, 'state');
const texture = cameraEntity.getValue(CameraSource, 'texture');
if (state === CameraState.Active && texture != null) {
material.map = texture;
material.needsUpdate = true;
}
CameraUtils.getDevices(refresh = false) requests permission when needed, enumerates video-input devices, and caches the result. Pass true to refresh the list.
const devices = await CameraUtils.getDevices();
const backCamera = CameraUtils.findByFacing(devices, CameraFacing.Back);
if (backCamera != null) {
cameraEntity.setValue(CameraSource, 'deviceId', backCamera.deviceId);
}
Each returned device has a deviceId, label, and facing value. CameraUtils.findByFacing(devices, facing) returns the first match or null.
Use CameraUtils.hasPermission() to check the browser’s camera permission state without requesting a new stream:
const hasCameraPermission = await CameraUtils.hasPermission();
A false result can also mean that the browser does not expose the relevant Permissions API query.
CameraUtils.captureFrame(entity) returns a canvas at the current video resolution when the camera is ready. It returns null before a usable video element and dimensions are available.
const canvas = CameraUtils.captureFrame(cameraEntity);
if (canvas != null) {
canvas.toBlob((blob) => {
if (blob == null) {
return;
}
// Send the Blob to application-owned storage or processing code.
}, 'image/jpeg');
}
Do not retain frame-scoped data longer than your application requires.
The camera remains inactive or enters the error state
Symptom:CameraSource.state never becomes CameraState.Active.
Solution: Confirm that world.features.camera is enabled, the page is visible, the browser supports navigator.mediaDevices, and the user granted camera permission. Check the console for the original MediaDevices error.
Symptom: Reading CameraSource.texture returns null.
Solution: Wait until CameraSource.state is CameraState.Active. The system creates the texture only after the video stream can play.
A requested facing is unavailable
Symptom: The component enters the error state after requesting a front-facing or back-facing camera.
Solution: Call CameraUtils.getDevices() and inspect the available devices. Use a returned deviceId, or use CameraFacing.Unknown to allow any camera.
Camera access works outside XR but not after the page is hidden
Symptom: The stream stops after a visibility transition.
Solution: This is expected when the world becomes hidden. The system starts inactive or failed camera sources again after the world becomes visible.