From e6c45a4f27ac432e4742cde6719b40c9aadccd97 Mon Sep 17 00:00:00 2001 From: yanziz-nv Date: Mon, 15 Jun 2026 10:53:59 -0700 Subject: [PATCH 1/7] feat(webclient): warn users when browser version is below CloudXR minimum Adds checkBrowserVersion() to BrowserCapabilities.ts that detects the current browser from the user-agent and returns a warning when it is below the documented CloudXR.js minimum versions: - Meta Quest Browser: OculusBrowser < 40 (proxy for Quest OS v79 era) - Pico Browser: Chrome < 125 in the Pico UA - Desktop Chrome: Chrome < 125 Pico is detected before Quest because Pico UAs include an "OculusBrowser/7.0" compat token that would otherwise trigger the Quest path. Safari and Firefox are not warned since they work with IWER and are not officially tested respectively. The warning is non-blocking: it is injected at the top of the checkCapabilities() warnings array and surfaced as an info notice in the 2D UI, without disabling the Connect button. Co-Authored-By: Claude Sonnet 4.6 --- .../helpers/BrowserCapabilities.ts | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts b/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts index fc0841f91..26a981b48 100644 --- a/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts +++ b/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts @@ -15,6 +15,64 @@ * 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_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. +export function checkBrowserVersion(): string | 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_CHROME_MAJOR) { + return ( + `Pico Browser (Chrome ${major}) is outdated. ` + + `CloudXR requires Chrome ${MIN_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; + } + + // Desktop Chrome / Chromium. Safari and Firefox are not warned. + const chromeMatch = ua.match(/Chrome\/(\d+)\./); + if (chromeMatch) { + const major = parseInt(chromeMatch[1], 10); + if (major < MIN_CHROME_MAJOR) { + return ( + `Chrome ${major} detected. CloudXR requires Chrome ${MIN_CHROME_MAJOR} or later. ` + + `Please update your browser.` + ); + } + } + + return null; +} + interface CapabilityCheck { name: string; required: boolean; @@ -109,6 +167,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(); + if (versionWarning) { + warnings.push(versionWarning); + console.warn('Browser version warning:', versionWarning); + } + for (const capability of capabilities) { try { const result = await Promise.resolve(capability.check()); From 5987c8858c607d32c780a816a6b4b9fc5f010683 Mon Sep 17 00:00:00 2001 From: yanziz-nv Date: Mon, 15 Jun 2026 11:03:26 -0700 Subject: [PATCH 2/7] fix(webclient): exclude Edge/Opera from Chrome version check in checkBrowserVersion Chrome/(\d+) also matches Edge (Edg/) and Opera (OPR/) UAs since they include "Chrome/" as a Chromium compat token. Guard the version check with a non-Chrome-Chromium exclusion so those browsers aren't warned about an outdated Chrome version. Co-Authored-By: Claude Sonnet 4.6 --- deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts b/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts index 26a981b48..c6059d953 100644 --- a/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts +++ b/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts @@ -59,7 +59,9 @@ export function checkBrowserVersion(): string | null { } // Desktop Chrome / Chromium. Safari and Firefox are not warned. - const chromeMatch = ua.match(/Chrome\/(\d+)\./); + // Skip other Chromium-based browsers (Edge, Opera) that also include "Chrome/" in their UA. + const isNonChromeChromium = /Edg\/|OPR\/|Opera\//.test(ua); + const chromeMatch = !isNonChromeChromium && ua.match(/Chrome\/(\d+)\./); if (chromeMatch) { const major = parseInt(chromeMatch[1], 10); if (major < MIN_CHROME_MAJOR) { From 06e46101bc8b6f045470dee2449504be02325373 Mon Sep 17 00:00:00 2001 From: yanziz-nv Date: Mon, 15 Jun 2026 11:08:03 -0700 Subject: [PATCH 3/7] fix(webclient): pass emulated flag to checkBrowserVersion instead of checking globals Replace window.IWER global check and UA string heuristics with an explicit emulated parameter on checkBrowserVersion() and checkCapabilities(). App.tsx reads iwerWasLoaded from sessionStorage (already set during IWER initialisation) and passes it through, so the browser version check is skipped cleanly when running under an XR emulator without any window/any casts. Co-Authored-By: Claude Sonnet 4.6 --- .../helpers/BrowserCapabilities.ts | 32 +++++++------------ deps/cloudxr/webxr_client/src/App.tsx | 4 +-- 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts b/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts index c6059d953..74e83ad34 100644 --- a/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts +++ b/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts @@ -20,11 +20,16 @@ // 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_CHROME_MAJOR = 125; +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. -export function checkBrowserVersion(): string | null { +// 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 @@ -33,10 +38,10 @@ export function checkBrowserVersion(): string | null { const chromeMatch = ua.match(/Chrome\/(\d+)\./); if (chromeMatch) { const major = parseInt(chromeMatch[1], 10); - if (major < MIN_CHROME_MAJOR) { + if (major < MIN_PICO_CHROME_MAJOR) { return ( `Pico Browser (Chrome ${major}) is outdated. ` + - `CloudXR requires Chrome ${MIN_CHROME_MAJOR} or later. ` + + `CloudXR requires Chrome ${MIN_PICO_CHROME_MAJOR} or later. ` + `Please update your headset firmware.` ); } @@ -55,21 +60,6 @@ export function checkBrowserVersion(): string | null { `Please update your headset firmware.` ); } - return null; - } - - // Desktop Chrome / Chromium. Safari and Firefox are not warned. - // Skip other Chromium-based browsers (Edge, Opera) that also include "Chrome/" in their UA. - const isNonChromeChromium = /Edg\/|OPR\/|Opera\//.test(ua); - const chromeMatch = !isNonChromeChromium && ua.match(/Chrome\/(\d+)\./); - if (chromeMatch) { - const major = parseInt(chromeMatch[1], 10); - if (major < MIN_CHROME_MAJOR) { - return ( - `Chrome ${major} detected. CloudXR requires Chrome ${MIN_CHROME_MAJOR} or later. ` + - `Please update your browser.` - ); - } } return null; @@ -160,7 +150,7 @@ const capabilities: CapabilityCheck[] = [ }, ]; -export async function checkCapabilities(): Promise<{ +export async function checkCapabilities(emulated = false): Promise<{ success: boolean; failures: string[]; warnings: string[]; @@ -170,7 +160,7 @@ export async function checkCapabilities(): Promise<{ const requiredFailures: string[] = []; // Check browser version first so the warning appears at the top of the list. - const versionWarning = checkBrowserVersion(); + const versionWarning = checkBrowserVersion(emulated); if (versionWarning) { warnings.push(versionWarning); console.warn('Browser version warning:', versionWarning); diff --git a/deps/cloudxr/webxr_client/src/App.tsx b/deps/cloudxr/webxr_client/src/App.tsx index 54c6079b9..484c397de 100644 --- a/deps/cloudxr/webxr_client/src/App.tsx +++ b/deps/cloudxr/webxr_client/src/App.tsx @@ -229,13 +229,14 @@ function App() { // Disable button and show checking status cloudXR2DUI.setStartButtonState(true, 'CONNECT (checking capabilities)'); + 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 +256,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) { From 54c20912b060b243547bd433e53ee0f04593a6f3 Mon Sep 17 00:00:00 2001 From: yanziz-nv Date: Mon, 15 Jun 2026 11:27:52 -0700 Subject: [PATCH 4/7] fix(webclient): clarify AV1 warning message to state codec is not supported Previous wording "recommended for optimal streaming quality" didn't make it clear that AV1 was actually missing on the device. New wording explicitly states AV1 is not supported and that streaming quality may be reduced. Co-Authored-By: Claude Sonnet 4.6 --- deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts b/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts index 74e83ad34..d5642814e 100644 --- a/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts +++ b/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts @@ -146,7 +146,7 @@ const capabilities: CapabilityCheck[] = [ return false; } }, - message: 'AV1 codec support is recommended for optimal streaming quality', + message: 'AV1 codec is not supported on this device. Streaming quality may be reduced.', }, ]; From 8f20096407f7f9aba6c08a507d23962921e720ce Mon Sep 17 00:00:00 2001 From: yanziz-nv Date: Mon, 15 Jun 2026 11:29:46 -0700 Subject: [PATCH 5/7] feat(webclient): add HEVC codec support check alongside AV1 AV1 is the default but HEVC (H.265) is a selectable alternative. Add a separate optional capability check for HEVC so users see explicitly which codecs are available on their device, mirroring the existing AV1 check pattern. Co-Authored-By: Claude Sonnet 4.6 --- .../helpers/BrowserCapabilities.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts b/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts index d5642814e..2bed24e2f 100644 --- a/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts +++ b/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts @@ -148,6 +148,35 @@ const capabilities: CapabilityCheck[] = [ }, message: 'AV1 codec is not supported on this device. Streaming quality may be reduced.', }, + { + 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. Use AV1 or H.264 for streaming.', + }, ]; export async function checkCapabilities(emulated = false): Promise<{ From 705f5d49912ecc86873850e20ba71f07b0b1d45c Mon Sep 17 00:00:00 2001 From: yanziz-nv Date: Mon, 15 Jun 2026 11:44:14 -0700 Subject: [PATCH 6/7] docs(webclient): add comment explaining iwerWasLoaded sessionStorage read Co-Authored-By: Claude Sonnet 4.6 --- deps/cloudxr/webxr_client/src/App.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/deps/cloudxr/webxr_client/src/App.tsx b/deps/cloudxr/webxr_client/src/App.tsx index 484c397de..cf61bf074 100644 --- a/deps/cloudxr/webxr_client/src/App.tsx +++ b/deps/cloudxr/webxr_client/src/App.tsx @@ -229,6 +229,8 @@ 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, From 95134d65a57db4e1aa32b84996f6f88a76c3a71e Mon Sep 17 00:00:00 2001 From: yanziz-nv Date: Mon, 15 Jun 2026 11:44:53 -0700 Subject: [PATCH 7/7] fix(webclient): fix AV1/HEVC warning messages to be consistent and accurate Both messages now use the same phrasing and list H.264 as the guaranteed fallback. Previously AV1 gave no action guidance and HEVC said "use AV1" which could be wrong if AV1 is also unsupported. Co-Authored-By: Claude Sonnet 4.6 --- deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts b/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts index 2bed24e2f..dcffb4f73 100644 --- a/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts +++ b/deps/cloudxr/webxr_client/helpers/BrowserCapabilities.ts @@ -146,7 +146,7 @@ const capabilities: CapabilityCheck[] = [ return false; } }, - message: 'AV1 codec is not supported on this device. Streaming quality may be reduced.', + message: 'AV1 codec is not supported on this device. H.264 or HEVC can be selected as an alternative.', }, { name: 'HEVC Codec Support', @@ -175,7 +175,7 @@ const capabilities: CapabilityCheck[] = [ return false; } }, - message: 'HEVC (H.265) codec is not supported on this device. Use AV1 or H.264 for streaming.', + message: 'HEVC (H.265) codec is not supported on this device. H.264 or AV1 can be selected as an alternative.', }, ];