ngx-scanner icon indicating copy to clipboard operation
ngx-scanner copied to clipboard

How to select the correct camera with autofocus?

Open maerni opened this issue 2 years ago • 4 comments

When we use ngx-scanner with the smartphone, we have several cameras, i.e.:

  • camera2 1, facing front
  • camera2 3, facing front
  • camera2 2, facing back
  • camera2 0, facing back

When we use "camera2 2, facing back", it does not work to scan small QR codes because the autofocus is not active.

But when we use "camera2 0, facing back", it works to scan small QR codes because the autofocus is active.

How we can find out which camera to take to have autofocus?

Do we have other possiblities to find out more informations about all available devices to select automatically the correct one?

Thanks very much.

Best regards Mike

maerni avatar Jun 23 '23 11:06 maerni

I had to write a few workarounds to get it how I want, but i don't use the autostart anymore

either with: window.navigator.mediaDevices.getUserMedia({ video: true, facingMode: 'environment' }) or window.navigator.mediaDevices.enumerateDevices();

note that with the first one you have to cleanup your streams before changing to the device, or the camera won't work. with the second you'll have to loop over all of them to get the capabillities of a device with the getCapabilities function.

stefbeysdiekeure avatar Aug 02 '23 07:08 stefbeysdiekeure

I had the same issue. My solution was to enumerate all the devices and find the one with the shortest focusDistance, as we're likely to be scanning codes close to the device (See #517). I also had to add delays because the scanner wasn't registering the device change (See #485).

The process involved:

  1. Ensure the enabled attribute is set to false on the ZXingScannerComponent
  2. Enumerate the devices.
  3. getUserMedia() for the device
  4. Enumerate the tracks
  5. getCapabilities() for each track
  6. Determine which device has the capability with the shortest focalDistance.min value
  7. Once we have the device, set it
  8. After a delay (~ 2s), ~~start~~ enable the scanner

This is my code for querying the capabilities of the devices:

export type MediaTrackCapabilitiesMap = { [key: string]: MediaTrackCapabilities[] };

interface FocusDistance {
  min: number,
  max: number,
  step: number
}

async function getCameraWithClosestFocus(): Promise<string | undefined> {
  const capabilities = await getCapabilities();
  let minFocusDistance = Number.MAX_SAFE_INTEGER;
  let bestDeviceId: string | undefined;

  Object.entries(capabilities).forEach(([id, values]) => {
    const focusDistance = getFocusDistance(values);

    if (focusDistance && focusDistance.min < minFocusDistance) {
      minFocusDistance = focusDistance.min;
      bestDeviceId = id;
    }
  });

  return bestDeviceId;
}

async function getCameras(): Promise<MediaDeviceInfo[] | undefined> {
  if (!navigator.mediaDevices.enumerateDevices) {
    return undefined;
  }

  const devices = await navigator.mediaDevices.enumerateDevices();

  return devices.filter((x) => x.kind === "videoinput");
}

function getCapability<T>(value: MediaTrackCapabilities, key: string): T | undefined {
  if (key in value) {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
    return (value as unknown as any)[key] as T | undefined;
  }

  return undefined;
}

function getCapabilities(value: string): Promise<MediaTrackCapabilities[] | undefined>;
function getCapabilities(value: MediaDeviceInfo): Promise<MediaTrackCapabilities[]>;
function getCapabilities(): Promise<MediaTrackCapabilitiesMap>;
async function getCapabilities(value: MediaDeviceInfo | string | undefined = undefined): Promise<MediaTrackCapabilitiesMap | MediaTrackCapabilities[] | undefined> {
  if (!value) {
    const devices = await getCameras();
    const resp: MediaTrackCapabilitiesMap = {};

    if (devices?.length) {
      for (const device of devices) {
        const capabilities = await getCapabilities(device);

        resp[device.deviceId] = capabilities;
      }
    }

    return resp;
  } else {
    // Single device
    const device = typeof value === "string" ? (await getCameras())?.find((x) => x.deviceId === value) : value;

    if (!device) {
      return undefined;
    }

    const media = await navigator.mediaDevices.getUserMedia({ video: device });
    const tracks = media.getTracks();
    const capabilities = tracks.map((track) => track.getCapabilities());

    tracks.forEach((track) => track.stop());

    return capabilities;
  }
}

function getFocusDistance(value: MediaTrackCapabilities[]): FocusDistance | undefined {
  return value
    .map((x) => {
      return getCapability<{ min: number, max: number, step: number }>(x, "focusDistance");
    })
    .find((x): x is FocusDistance => x !== undefined)
    ;
}

function getHasAutoFocus(value: MediaTrackCapabilities[]): boolean {
  return value.some((x) => {
    const focusMode = getCapability<string[]>(x, "focusMode");

    return focusMode?.includes("auto") || false;
  });
}

export {
  getCameraWithClosestFocus,
  getCameras,
  getCapabilities,
};

oobayly avatar Sep 01 '23 13:09 oobayly