diff --git a/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts b/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts index fc0841f91..dcffb4f73 100644 --- a/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts +++ b/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts @@ -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; @@ -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[]; @@ -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()); diff --git a/deps/cloudxr/webxr_client/src/App.tsx b/deps/cloudxr/webxr_client/src/App.tsx index 54c6079b9..cf61bf074 100644 --- a/deps/cloudxr/webxr_client/src/App.tsx +++ b/deps/cloudxr/webxr_client/src/App.tsx @@ -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'; 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); @@ -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) {