All files / src/hooks useXr.ts

66.66% Statements 28/42
33.33% Branches 7/21
50% Functions 8/16
72.22% Lines 26/36

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111                      1x                       1x   1x 2x     2x         2x   2x   1x               1x         1x   1x 1x       2x 1x   1x   1x   1x 1x   1x 1x 1x       2x   2x                               2x       2x 2x                          
import { useCallback, useEffect, useMemo, useState } from 'react';
 
import { useApp } from '@playcanvas/react/hooks';
import { CameraComponent, XRSPACE_LOCALFLOOR, XRTYPE_AR, XRTYPE_VR } from 'playcanvas';
 
export type XrType = 'VR' | 'AR';
 
interface UseXrOptions {
  onError?: (error: Error) => void;
}
 
const XR_TYPES: Record<XrType, string> = {
  VR: XRTYPE_VR,
  AR: XRTYPE_AR,
};
 
/**
 * Factor PlayCanvas applies on top of the device pixel ratio to compute the headset's
 * framebuffer resolution: `app.graphicsDevice.maxPixelRatio / window.devicePixelRatio *
 * XR_FRAMEBUFFER_SCALE_FACTOR`. Splat rendering in stereo is fill-rate bound, so this (and
 * `maxPixelRatio`, which also multiplies XR resolution) is the largest lever on XR framerate.
 * Read once when the session starts and fixed for its lifetime.
 */
const XR_FRAMEBUFFER_SCALE_FACTOR = 1;
 
export const useXr = ({ onError }: UseXrOptions = {}) => {
  const app = useApp();
  // Seeded from the manager rather than defaulting to unavailable, so a caller acting in a mount
  // effect isn't told VR is unsupported while the effect below is still waiting to run.
  const [available, setAvailable] = useState<Record<XrType, boolean>>(() => ({
    VR: app.xr?.isAvailable(XRTYPE_VR) ?? false,
    AR: app.xr?.isAvailable(XRTYPE_AR) ?? false,
  }));
  // `type` is set as soon as a session is requested, so gate on `active` to ignore pending sessions.
  const [currentXrType, setCurrentXrType] = useState<string | null>(app.xr?.active ? app.xr.type : null);
 
  useEffect(() => {
    // We subscribe to the manager directly because `app` doesn't update when XR's availability changes.
    const handleAvailable = (type: string, isAvailable: boolean) => {
      if (type === XRTYPE_VR) {
        setAvailable((prev) => ({ ...prev, VR: isAvailable }));
      } else if (type === XRTYPE_AR) {
        setAvailable((prev) => ({ ...prev, AR: isAvailable }));
      }
    };
 
    setAvailable({
      VR: app.xr?.isAvailable(XRTYPE_VR) ?? false,
      AR: app.xr?.isAvailable(XRTYPE_AR) ?? false,
    });
 
    app.xr?.on('available', handleAvailable);
 
    return () => {
      app.xr?.off('available', handleAvailable);
    };
  }, [app]);
 
  useEffect(() => {
    const handleStart = () => setCurrentXrType(app.xr?.type ?? null);
    // `app.xr.type` isn't cleared until after the `end` event fires, so don't read it here.
    const handleEnd = () => setCurrentXrType(null);
 
    setCurrentXrType(app.xr?.active ? app.xr.type : null);
 
    app.xr?.on('start', handleStart);
    app.xr?.on('end', handleEnd);
 
    return () => {
      app.xr?.off('start', handleStart);
      app.xr?.off('end', handleEnd);
    };
  }, [app]);
 
  const isXrAvailable = useCallback((type: XrType) => available[type], [available]);
 
  const startXr = useCallback(
    (type: XrType) => {
      const camera = app.root.findComponent('camera') as CameraComponent;
      app.xr?.start(camera, XR_TYPES[type], XRSPACE_LOCALFLOOR, {
        framebufferScaleFactor: XR_FRAMEBUFFER_SCALE_FACTOR,
        callback: (err: Error | null) => {
          if (err) {
            onError?.(err);
            app.xr?.end();
          }
        },
      });
    },
    [app, onError]
  );
 
  const endXr = useCallback(() => {
    app.xr?.end();
  }, [app]);
 
  return useMemo(
    () => ({
      isXrAvailable,
      startXr,
      endXr,
      isCurrentlyInXr: currentXrType !== null,
      isCurrentlyInAr: currentXrType === XRTYPE_AR,
      isCurrentlyInVr: currentXrType === XRTYPE_VR,
    }),
    [isXrAvailable, startXr, endXr, currentXrType]
  );
};
 
export default useXr;