git clone https://github.com/oculus-samples/Unity-VirtualCameraPublisher.git
VirtualCameraPublisherSample folder as a project in Unity 6, load Assets/Scenes/SampleScene.unity, and build the APK for your Meta Quest device. This sample does not support Link or Editor testing. You must deploy a full APK to test virtual camera functionality.https://github.com/oculus-samples/Unity-VirtualCameraPublisher.git?path=com.meta.xr.virtualcamerapublisher
| File / Directory | What it demonstrates | Key concepts |
|---|---|---|
Public static API for camera registration and lifecycle management | Centralized manager pattern, frame-by-frame update model | |
Observer interface for capture state callbacks | Decoupled event notification, capture lifecycle | |
Internal per-camera state management | Double-buffered RenderTextures, frame-rate throttling | |
Sample MonoBehaviour showing registration, updates, and cleanup | Multiple cameras from one physical Camera, observer implementation |
VirtualCameraManager.RegisterVirtualCamera(
"1440p Far @ 45fps", cameraId, 2560, 1440, 45, droneCamera, far);
VirtualCameraManager.RegisterVirtualCamera(
"4k Far @ 60fps", cameraId + 1, 3840, 2160, 60, droneCamera, far);
VirtualCameraManager.Update(), which must be called every frame from a MonoBehaviour. The manager handles frame-rate throttling internally, rendering each camera only when its target frame interval has elapsed. This decouples your app’s frame rate from the virtual camera feed rates, allowing 30fps, 45fps, and 60fps cameras to coexist.IVirtualCameraObserver on the same MonoBehaviour that registers the cameras. The interface declares both OnCaptureStarted(int id) and OnCaptureStopped(int id), so implement both. Hand the instance to VirtualCameraManager.AddObserver() after registration, and call VirtualCameraManager.RemoveObserver() during teardown:using Meta.VirtualCameraSystem;
using UnityEngine;
public class UnityVideoCapture : MonoBehaviour, IVirtualCameraObserver
{
private int cameraId = 1;
[SerializeField]
private Camera droneCamera;
void IVirtualCameraObserver.OnCaptureStarted(int id)
{
Debug.Log($"Capture started for camera {id}");
}
void IVirtualCameraObserver.OnCaptureStopped(int id)
{
Debug.Log($"Capture stopped for camera {id}");
}
void Start()
{
Texture2D far = Resources.Load<Texture2D>("thumbnails/far");
VirtualCameraManager.RegisterVirtualCamera(
"1440p Far @ 45fps", cameraId, 2560, 1440, 45, droneCamera, far);
VirtualCameraManager.AddObserver(this);
}
void Update()
{
VirtualCameraManager.Update();
}
void OnApplicationQuit()
{
VirtualCameraManager.RemoveObserver(this);
VirtualCameraManager.UnregisterVirtualCamera(cameraId);
}
}
VirtualCameraManager.Update() detects each capture-state transition and calls OnCaptureStarted or OnCaptureStopped on every registered observer. Use these callbacks to pause expensive app logic, reposition cameras, or adjust quality settings when a camera feed is actively consumed.horizonos.permission.CREATE_VIRTUAL_CAMERA permission, and an element declaring the minimum and target SDK versions your app supports.uses-metavr-sdk element as a child of the manifest element, and set metavr:minSdkVersion and metavr:targetSdkVersion to the versions your app supports. See Meta VR OS SDK versioning for configuration information. For example:<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:metavr="http://schemas.meta.com/metavr-sdk">
<uses-permission android:name="horizonos.permission.CREATE_VIRTUAL_CAMERA" />
<metavr:uses-metavr-sdk
metavr:minSdkVersion="81"
metavr:targetSdkVersion="81" />
...
</manifest>
horizonos.permission.CREATE_VIRTUAL_CAMERA. It does not record, stream, or cast the feed itself. A consuming app, such as Meta Camera, handles that. For what each permission covers, see Permissions.Texture2D.EncodeToJPG().