| Requirement | Details |
|---|---|
Horizon OS version | v204 or later |
Permission | horizonos.permission.CREATE_VIRTUAL_CAMERA declared in your AndroidManifest.xml. This is a normal-level permission. No runtime prompt is needed. |
Header | #include <horizonos/virtualcamera/VirtualCamera.h> |
Cameras per app | Up to 3 at the same time |
Max frame rate | 60 fps |
CameraCharacteristics. When a consumer opens the camera and picks an output configuration, the system selects the best-matching input configuration and invokes your onStreamConfigured callback.AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM (RGBA 8888) and AHARDWAREBUFFER_FORMAT_Y8Cb8Cr8_420 (YUV 420).HZ_VIRTUAL_CAMERA_STATUS_INVALID_ARGUMENT.HzVirtualCameraManager_createHzVirtualCameraConfiguration_initHzVirtualCamera_createonStreamConfigured — A consumer opened the camera. Start rendering to the supplied ANativeWindow.onProcessCaptureRequest(optional) — Generates a per-frame notification.onStreamClosed — The consumer closed the camera. Stop rendering and release the window.HzVirtualCamera_destroy (unregisters the camera).HzVirtualCameraManager_destroy (releases resources).onStreamConfigured fires, ownership of the ANativeWindow* transfers to your app. You must:onStreamClosed fires for the same streamId.ANativeWindow_release after you stop rendering.ANativeWindow must match the width, height, and format reported by onStreamConfigured. A mismatch produces silently corrupted or black output, not a crash.pow(color, 1.0/2.2)) before writing to the surface. Without this, the output appears darker than expected.onStreamConfigured: A consumer opened the camera. You receive the ANativeWindow*, dimensions, and format. Start rendering.onStreamClosed: The consumer closed the camera. Stop rendering and call ANativeWindow_release.onProcessCaptureRequest, fires once every frame (useful for per-frame pacing or metadata)._init macro before setting any fields. The macro zeros the struct and applies defaults (cameraSource = APP_DEFINED, lensFacing = EXTERNAL, inputTimeoutMs = 1000, and so on). Skipping initialization causes undefined behavior from garbage in unset fields.HzVirtualCameraManager_create.HzVirtualCameraConfiguration_init.HzVirtualCameraInputConfiguration_init (set width, height, format, maxFps).onStreamConfigured and onStreamClosed.HzVirtualCamera_create.CameraCharacteristics.K = [[fx, s, cx], [0, fy, cy], [0, 0, 1]] describes how 3D points project onto your camera’s image plane. For a virtual camera, derive the values from your render parameters:cx, cy = principal point. Use the image center: (width/2, height/2).fx, fy = focal length in pixels: fx = (width/2) / tan(hFOV/2), fy = (height/2) / tan(vFOV/2). Use fx == fy for square pixels (the common case).s = axis skew. Almost always 0.P (OpenGL, Vulkan), pull fx = P[0][0] · width/2 and fy = P[1][1] · height/2 directly. The example below configures a 1920×1080 camera with a ~88° horizontal FOV.// Lens facing: APP_DEFINED cameras must use EXTERNAL.
// FRONT and BACK are reserved for system cameras.
config.lensFacing = HZ_VIRTUAL_CAMERA_LENS_FACING_EXTERNAL;
// Sensor orientation in degrees (default: 0).
config.sensorOrientation = HZ_VIRTUAL_CAMERA_SENSOR_ORIENTATION_0;
// Intrinsic calibration for a 1920×1080 frame, ~88° hFOV, square pixels.
HzVirtualCameraLensIntrinsicCalibration calib = {
.fx = 1000.0f, .fy = 1000.0f, // focal length in pixels
.cx = 960.0f, .cy = 540.0f, // principal point (image center)
.s = 0.0f // skew
};
config.intrinsicCalibration = &calib;
UNDEFINEDmust be set when specifying a pose, otherwise registration fails.HzPose pose = {
.orientation = {0.0f, 0.0f, 0.0f, 1.0f}, // identity quaternion
.position = {0.065f, 0.0f, 0.0f} // 65 mm right of reference
};
config.lensPose = &pose;
config.poseReference = HZ_VIRTUAL_CAMERA_LENS_POSE_REFERENCE_GYROSCOPE;
ContentResolver.config.cameraThumbnailUri = "content://com.example.myapp/camera_thumbnail";
ContentProvider that serves the image at this URI.SAMPLING_TIME). To use the presentation timestamp attached to the buffer instead, set:config.timestampSource =
HZ_VIRTUAL_CAMERA_ACQUISITION_TIMESTAMP_SOURCE_SURFACE;
config.inputTimeoutMs = 2000; // wait up to 2 seconds (default: 1000)
config.inputTimeoutStrategy =
HZ_VIRTUAL_CAMERA_INPUT_TIMEOUT_STRATEGY_REPEAT_FRAME;
| Strategy | Behavior |
|---|---|
REPEAT_FRAME_RESPECT_MIN_FPS (default) | Repeats the last frame only if waiting longer would violate the consumer’s minimum FPS constraint. Otherwise uses the configured timeout. |
REPEAT_FRAME | Ignores the consumer’s FPS range. After the timeout, repeats the last frame. |
FAIL_REQUEST | Ignores the consumer’s FPS range. After the timeout, fails the capture request. |
FAIL_REQUEST puts your app in full control of frame timing but risks unrecoverable timeouts in the camera framework if frames aren’t delivered promptly.CaptureRequest on every frame, so the producer can react to consumer-requested settings like zoom crop region, exposure compensation, focal length, or AE/AF mode. This lets the virtual camera behave like a real, controllable camera: a spectator camera that honors the recording app’s pinch-to-zoom, an avatar camera that responds to a livestream operator’s exposure controls, or a drone camera that reframes when the viewer changes focal length.config.perFrameCameraMetadataEnabled = true; config.onProcessCaptureRequestWithMetadata = onCaptureRequestWithMetadata;
void onCaptureRequestWithMetadata(int32_t streamId,
int32_t frameId,
const uint8_t* metadata,
size_t metadataSize,
void* clientData) {
if (metadata != NULL) {
// Cast to camera_metadata* (libcamera_metadata format).
// Valid only for the duration of this callback.
const camera_metadata_t* camMeta =
(const camera_metadata_t*)metadata;
// Read consumer-requested settings (zoom, exposure, and so on).
}
}
onProcessCaptureRequest and onProcessCaptureRequestWithMetadata are set, only the latter is invoked.ACameraManager_getCameraIdList (NDK) or CameraManager.getCameraIdList() (Java) to enumerate all cameras, including virtual ones. Virtual cameras appear alongside physical cameras in the list.com.meta.extra_metadata.camera_source on each camera’s CameraCharacteristics to determine whether it’s a virtual camera and what type of source it provides:| Value | Source type |
|---|---|
0 | Passthrough |
1 | Avatar |
2 | App-defined |
3 | Spatial |
com.meta.extra_metadata.camera_name: Human-readable namecom.meta.extra_metadata.camera_thumbnail: Content URI for a thumbnail image (fetch with ContentResolver)com.meta.extra_metadata.position: Left or right position on the HMD frontACameraDevice_createCaptureSession (NDK) or CameraDevice.createCaptureSession (Java) and submit capture requests as you would for any physical camera. The virtual camera service handles routing frames from the producer’s ANativeWindow to your output surfaces.HzVirtualCamera_destroy(camera); // unregisters the camera HzVirtualCameraManager_destroy(manager); // releases the manager
| Status code | Meaning | What to do |
|---|---|---|
HZ_VIRTUAL_CAMERA_STATUS_OK | Success. | No action required. |
HZ_VIRTUAL_CAMERA_STATUS_INVALID_ARGUMENT | A required parameter is null, a required callback is missing, or input configurations violate the multi-resolution rules. | Fix the configuration and retry. |
HZ_VIRTUAL_CAMERA_STATUS_SERVICE_NOT_FOUND | The virtual camera service isn’t running. | Retry after a delay, or inform the user that virtual cameras aren’t supported on this device or build. |
HZ_VIRTUAL_CAMERA_STATUS_PERMISSION_DENIED | The app doesn’t hold the required permission. | Check that horizonos.permission.CREATE_VIRTUAL_CAMERA is declared in your manifest. |
HZ_VIRTUAL_CAMERA_STATUS_INTERNAL_ERROR | An unexpected error occurred, such as a duplicate camera ID. | Log the error. If you set a custom id, try registering without one to let the system assign an ID. |