diff --git a/app/src/room/canvas.ts b/app/src/room/canvas.ts index e32f3a91..b3a4bfbb 100644 --- a/app/src/room/canvas.ts +++ b/app/src/room/canvas.ts @@ -7,9 +7,14 @@ import { GLContext } from "./gl/context"; export class Canvas { #canvas: HTMLCanvasElement; - #glContext: GLContext; - #camera: Camera; - #backgroundRenderer: BackgroundRenderer; + #glContext?: GLContext; + #camera?: Camera; + #backgroundRenderer?: BackgroundRenderer; + + // True when a WebGL2 context was successfully created. + // Some browsers (or privacy configurations) disable WebGL entirely, in which + // case we degrade gracefully instead of crashing the whole app. + readonly supported: boolean; // Use a callback to render after the background. onRender?: (now: DOMHighResTimeStamp) => void; @@ -24,14 +29,17 @@ export class Canvas { } get gl(): WebGL2RenderingContext { + if (!this.#glContext) throw new Error("WebGL2 not supported"); return this.#glContext.gl; } get glContext(): GLContext { + if (!this.#glContext) throw new Error("WebGL2 not supported"); return this.#glContext; } - get camera() { + get camera(): Camera { + if (!this.#camera) throw new Error("WebGL2 not supported"); return this.#camera; } @@ -41,10 +49,17 @@ export class Canvas { this.visible = new Signal(false); this.viewport = new Signal(Vector.create(0, 0)); - // Initialize WebGL2 context - this.#glContext = new GLContext(this.#canvas, this.viewport); - this.#camera = new Camera(); - this.#backgroundRenderer = new BackgroundRenderer(this.#glContext); + // Initialize WebGL2 context. This can fail when WebGL is disabled by the + // browser or a privacy extension; in that case we keep the app running + // without the GL-rendered background instead of throwing. + try { + this.#glContext = new GLContext(this.#canvas, this.viewport); + this.#camera = new Camera(); + this.#backgroundRenderer = new BackgroundRenderer(this.#glContext); + } catch (err) { + console.warn("WebGL2 unavailable, disabling canvas rendering", err); + } + this.supported = this.#glContext !== undefined; const resize = (entries: ResizeObserverEntry[]) => { for (const entry of entries) { @@ -68,14 +83,14 @@ export class Canvas { this.#canvas.height = newHeight; // Update WebGL viewport - this.#glContext.resize(newWidth, newHeight); + this.#glContext?.resize(newWidth, newHeight); // The internal logic ignores devicePixelRatio because we automatically scale when rendering. const viewport = Vector.create(newWidth / dpr, newHeight / dpr); this.viewport.set(viewport); // Update camera projection - this.#camera.updateOrtho(viewport); + this.#camera?.updateOrtho(viewport); // Render immediately to avoid black flicker during resize if (this.visible.peek()) { @@ -108,8 +123,10 @@ export class Canvas { resizeObserver.disconnect(); }); - // Only render the canvas when it's visible. + // Only render the canvas when it's visible (and WebGL is available). this.#signals.effect((effect) => { + if (!this.#glContext) return; + const visible = effect.get(this.visible); if (!visible) return; @@ -130,6 +147,8 @@ export class Canvas { } #render(now: DOMHighResTimeStamp) { + if (!this.#glContext || !this.#backgroundRenderer) return; + // Clear the screen this.#glContext.clear(); @@ -169,6 +188,6 @@ export class Canvas { close() { this.#signals.close(); - this.#backgroundRenderer.cleanup(); + this.#backgroundRenderer?.cleanup(); } } diff --git a/app/src/sup.tsx b/app/src/sup.tsx index 25009301..b3dc1c1a 100644 --- a/app/src/sup.tsx +++ b/app/src/sup.tsx @@ -62,11 +62,26 @@ export function Sup(props: { canvas: Canvas; room: string }): JSX.Element { return ( }> - + }> + + ); } +function WebGLUnsupported(): JSX.Element { + return ( + +

WebGL is required

+

+ This room needs WebGL to render video, but your browser has it disabled. Some browsers and privacy + extensions block WebGL by default to prevent fingerprinting. +

+

Please enable WebGL (for this site) and reload the page.

+
+ ); +} + function App(props: { connection: Moq.Connection.Reload; canvas: Canvas;