Skip to content
90 changes: 88 additions & 2 deletions deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,56 @@
* limitations under the License.
*/

// Minimum versions for CloudXR.js compatibility.
// Requirements: https://docs.nvidia.com/cloudxr-sdk/latest/requirement/cloudxrjs_req.html
// Quest OS version is not exposed in the UA; OculusBrowser 40 approximates the OS v79 era
// (browser 41.2, Nov 2025, was the first to declare OS v81 as its minimum).
const MIN_OCULUS_BROWSER_MAJOR = 40;
const MIN_PICO_CHROME_MAJOR = 125;

// Returns a warning message if the current browser is below the documented minimum
// version, or null if the version is acceptable or cannot be determined.
// Pass emulated=true when running under an XR emulator (e.g. IWER) to skip the check.
export function checkBrowserVersion(emulated = false): string | null {
if (emulated) {
return null;
}

const ua = navigator.userAgent;

// Detect Pico first — Pico UAs also include "OculusBrowser/7.0" as a compat token
// which would otherwise match the Quest check below.
if (/PicoBrowser\//.test(ua)) {
const chromeMatch = ua.match(/Chrome\/(\d+)\./);
if (chromeMatch) {
const major = parseInt(chromeMatch[1], 10);
if (major < MIN_PICO_CHROME_MAJOR) {
return (
`Pico Browser (Chrome ${major}) is outdated. ` +
`CloudXR requires Chrome ${MIN_PICO_CHROME_MAJOR} or later. ` +
`Please update your headset firmware.`
);
}
}
return null;
}

const questMatch = ua.match(/OculusBrowser\/(\d+)\./);
if (questMatch) {
const major = parseInt(questMatch[1], 10);
if (major < MIN_OCULUS_BROWSER_MAJOR) {
return (
`Meta Quest Browser version ${major} detected. ` +
`CloudXR requires Meta Quest OS v79 or later ` +
`(approximately OculusBrowser ${MIN_OCULUS_BROWSER_MAJOR}+). ` +
`Please update your headset firmware.`
);
}
}

return null;
}

interface CapabilityCheck {
name: string;
required: boolean;
Expand Down Expand Up @@ -96,11 +146,40 @@ const capabilities: CapabilityCheck[] = [
return false;
}
},
message: 'AV1 codec support is recommended for optimal streaming quality',
message: 'AV1 codec is not supported on this device. H.264 or HEVC can be selected as an alternative.',
},
{
name: 'HEVC Codec Support',
required: false,
check: async () => {
try {
if (!navigator.mediaCapabilities) {
return false;
}

const config = {
type: 'webrtc' as MediaDecodingType,
video: {
contentType: 'video/h265',
width: 1920,
height: 1080,
framerate: 60,
bitrate: 15000000,
},
};

const result = await navigator.mediaCapabilities.decodingInfo(config);
return result.supported;
} catch (error) {
console.warn('Error checking HEVC support:', error);
return false;
}
},
message: 'HEVC (H.265) codec is not supported on this device. H.264 or AV1 can be selected as an alternative.',
},
];

export async function checkCapabilities(): Promise<{
export async function checkCapabilities(emulated = false): Promise<{
success: boolean;
failures: string[];
warnings: string[];
Expand All @@ -109,6 +188,13 @@ export async function checkCapabilities(): Promise<{
const warnings: string[] = [];
const requiredFailures: string[] = [];

// Check browser version first so the warning appears at the top of the list.
const versionWarning = checkBrowserVersion(emulated);
if (versionWarning) {
warnings.push(versionWarning);
console.warn('Browser version warning:', versionWarning);
}

for (const capability of capabilities) {
try {
const result = await Promise.resolve(capability.check());
Expand Down
6 changes: 4 additions & 2 deletions deps/cloudxr/webxr_client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,13 +229,16 @@ function App() {
// Disable button and show checking status
cloudXR2DUI.setStartButtonState(true, 'CONNECT (checking capabilities)');

// Set by the IWER load effect above; passed to checkCapabilities to skip browser
// version checks that don't apply when running under a desktop XR emulator.
const iwerWasLoaded = sessionStorage.getItem('iwerWasLoaded') === 'true';
Comment thread
yanziz-nvidia marked this conversation as resolved.
let result: { success: boolean; failures: string[]; warnings: string[] } = {
success: false,
failures: [],
warnings: [],
};
try {
result = await checkCapabilities();
result = await checkCapabilities(iwerWasLoaded);
} catch (error) {
cloudXR2DUI.showStatus(`Capability check error: ${error}`, 'error');
setCapabilitiesValid(false);
Expand All @@ -255,7 +258,6 @@ function App() {
}

// Show final status message with IWER info if applicable
const iwerWasLoaded = sessionStorage.getItem('iwerWasLoaded') === 'true';
if (result.warnings.length > 0) {
cloudXR2DUI.showStatus('Performance notice:\n' + result.warnings.join('\n'), 'info');
} else if (iwerWasLoaded) {
Expand Down
Loading