GrCameraCapture

  • media
  • media
  • API · 9 props
import { GrCameraCapture } from '@feugene/granularity-media/components/GrCameraCapture'

Overview

When to take it

  • an avatar without a file manager — the user is on a phone, and “take a photo” is faster than looking for a shot in the gallery;
  • a document or a business card in a form — with the rear camera, and the recognition already on the server;
  • a confirmation of presence — a selfie in an application, where the fact of shooting here and now matters;
  • a quick shot of a product — the card is filled in right from the warehouse.

When to take something else

NeedTake
Accept a ready file: a drop zone, a queue, checksGrFileUpload / GrFormFile
Choose a crop out of a picture already obtainedGrImageCrop
Show a picture full screen without changing anythingGrImageViewer

Examples 4 each with a live demo and its code

  • A profile photo without a file manager

    The front camera is mirrored in the preview, because that is how a person sees themselves, and not in the photo. aspect-ratio asks the camera for a square — a wish, not a crop — and output makes a 512px JPEG instead of a full-size frame. status-change gives each refusal its own next step: settings, another device, closing the other app.

    ALProfile photoInitials for now

    The camera is off.

    Square, front camera — the preview is mirrored, the photo is not.

  • Proof of delivery from the back camera

    facing="environment" takes the back camera, and its frame is not mirrored: a door number must stay readable. Every capture brings a Blob — here three of the last photos with their weight, kept small by output. Once the parcel is handed over the camera is disabled: the proof is final.

    Proof of deliveryOrder #4815 · 12 Harbour Street, Hamburg

    The camera is off.

    No photos yet — the parcel and the door number are enough.
  • Two sides of an ID card with your own buttons

    The controls slot replaces the buttons, not the camera: it gets the status and the actions — here capture and stop — while «Turn on camera», the permission and the cleanup on unmount stay with the component. Device names come only after the permission, so the list is read on start, and device-id switches to the chosen camera. When both sides are in, the exposed stop() turns the camera light off.

    Verify your identityPhotograph both sides of your ID card on a dark, flat surface.

    The camera is off.

    Step 1 of 2 — the front side
    1Front sideNext
    2Back sideWaiting
  • A receipt photo in a window

    The window exists only for the camera, and the click that opened it is the reader’s action — the one case where auto-start is right: without it the window would ask to turn the camera on a second time. The photo closes the window and lands in the expense.

    Taxi to the airportExpense report · Hamburg trip · €46.80
    Receipt missingFinance needs a photo of the receipt.

API generated from the package · cannot drift

Props

PropTypedefaultDescription
facingGrCameraFacing | undefined"user"Which camera: `user` — front-facing, `environment` — rear.
deviceIdstring | undefinedundefinedA specific device, if the application has already picked one.
aspectRationumber | undefinedundefinedA wish to the camera: which aspect ratio to request from the device. This is **not cropping**. A camera that can do what was asked returns it itself; one that cannot returns its own — and both shown and captured will be its real frame. If you need exactly a square, that is `GrImageCrop` after the capture.
autoStartboolean | undefinedfalseTurn the camera on right away. `false` by default: a permission request without a user action is dismissed out of hand, and the browser will not ask a second time.
mirrorboolean | undefinedundefinedMirror the preview. By default — the front camera only.
outputGrCameraCaptureOutput | undefinedundefinedThe size and format of the shot. One side — the other is computed from the proportions of the frame; both — a bounding box the frame fits into without distortion. Not set — a shot at the size of the camera frame, JPEG.
sizeGrCameraCaptureSize | undefinedundefinedThe size step: the controls and the type size of the captions. `md` by default; taken from `GrConfigProvider`.
disabledboolean | undefinedfalseThe camera is unavailable: it cannot be turned on and no frame can be taken; the buttons are disabled.
ariaLabelstring | undefinedundefinedThe name of the group with the preview and the buttons. Not set — from the locale ("Camera").

Slots

SlotTypeDescription
controls
{ status: GrCameraStatus; start: () => void; stop: () => voi{ status: GrCameraStatus; start: () => void; stop: () => void; capture: () => Promise<Blob | null>; }
A custom control panel instead of the built-in one.

Events

EventSignatureDescription
start(event: "start"): void—
stop(event: "stop"): void—
capture(event: "capture", blob: Blob): void—
statusChange(event: "statusChange", status: GrCameraStatus): void—

Methods

MethodsSignatureDescription
start() => Promise<void>Request permission and turn the camera on. Does nothing with `disabled`, during a request or on a live stream; a refusal settles in `status`.
stop() => voidTurn the camera off and put out its indicator; the state returns to `idle`.
capture() => Promise<Blob | null>Take the current frame in full, without the preview mirroring, in the `output` format. Returns `null` if the camera is not live or there is no frame yet.
statusGrCameraStatusThe current state of the camera (`GrCameraStatus`): `idle`, `starting`, `live` or the reason for a refusal.

Types

Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.

Import
import type { GrCameraStatus, GrCameraCaptureConfigurableProps, GrCameraCaptureOutput } from '@feugene/granularity-media/components/GrCameraCapture'

Filedist/types/components/shared/cameraState.d.ts

GrCameraStatustype
type GrCameraStatus = 'idle'
cameraStatusFromErrorfunction
function cameraStatusFromError(error: unknown): GrCameraStatus;
cameraSupportedfunction
function cameraSupported(navigatorLike: Pick<Navigator, 'mediaDevices'> | undefined): boolean;
shouldMirrorPreviewfunction
function shouldMirrorPreview(facing: 'user' | 'environment', mirror: boolean | undefined): boolean;

Filedist/types/components/GrCameraCapture/config.d.ts

grCameraCaptureConfigconst
const grCameraCaptureConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrCameraCapture">;

Filedist/types/components/GrCameraCapture/defaults.d.ts

GrCameraCaptureConfigurablePropsinterface
interface GrCameraCaptureConfigurableProps {
    size?: GrCameraCaptureSize;
}

Filedist/types/components/GrCameraCapture/GrCameraCapture.vue.d.ts

GrCameraCaptureOutputinterface
interface GrCameraCaptureOutput {
    width?: number;
    height?: number;
    type?: string;
    quality?: number;
}
GrCameraCapturePropsinterface
interface GrCameraCaptureProps {
    facing?: GrCameraFacing;
    deviceId?: string;
    aspectRatio?: number;
    autoStart?: boolean;
    mirror?: boolean;
    output?: GrCameraCaptureOutput;
    size?: GrCameraCaptureSize;
    disabled?: boolean;
    ariaLabel?: string;
}
GrCameraCaptureEmitsinterface
interface GrCameraCaptureEmits {
    (e: 'capture', blob: Blob): void;
    (e: 'start'): void;
    (e: 'stop'): void;
    (e: 'statusChange', status: GrCameraStatus): void;
}

Filedist/types/components/GrCameraCapture/grCameraCaptureStyles.d.ts

GrCameraCaptureSizetype
type GrCameraCaptureSize = 'xs' | 'sm' | 'md' | 'lg';
GrCameraFacingtype
type GrCameraFacing = 'user' | 'environment';
grCameraCaptureSafelistconst
const grCameraCaptureSafelist: string[];

Usage from the library prose

The camera does not switch itself on

autoStart is off by default. A permission request that popped up without an action from the user is declined without a glance — and a second time the browser will not ask: the decision is remembered for the site as a whole, and it cannot be fixed from the application. The component therefore shows a button first, and only it starts the request.

Every refusal has its own reason and its own next step

getUserMedia answers with a DOMException, and the name shows what exactly happened. The component separates four outcomes, because the user’s actions in them differ:

StateWhat happenedWhat the user should do
denieda refusal or a policyallow it in the settings of the site
missingthere is no cameranothing — there is no retry button
busythe device is taken by another applicationclose that application and retry
insecurethe page is not opened over HTTPSnothing: the API is absent

The last one is not a refusal. On http:// the navigator.mediaDevices object does not exist at all, and a “please allow access” message would send the person looking for a setting that is not there.

The parsing goes by the name of the exception rather than by the text: the text is localised by the browser and changes between versions. The names, though, are chosen differently by different browsers — Safari calls a busy device NotReadableError, and Firefox AbortError.

The frame is not fitted to the window

Cameras on different devices give away different sizes and ratios, so fitting the frame to a fixed window is pointless: on one phone one thing would be cut off, on another something else. The component shows and captures what the camera gave, and the frame accepts its ratio — until the first frame it holds 4:3, so that the room in the layout is visible (the sizes of the frame before loadedmetadata are zeros).

aspectRatio has not disappeared in the process, but it means something else: a wish to the camera. It goes into getUserMedia as ideal, and a device that can do what was asked for will give it by itself. exact must not be taken here — it gives an OverconstrainedError, that is, the “there is no camera” state on a working camera with a different ratio.

If exactly a square is needed regardless of the device, that is the next step rather than this component: GrImageCrop cuts what has already been shot.

output is a bounding box rather than an exact size

output.width without height is an ordinary order (“an avatar 800 wide”), and the second side is computed from the ratio of the frame. Taken from the source, it would stretch the picture: a frame of 640×480 with width: 800 would give a canvas of 800×480, that is, an image stretched horizontally by a quarter.

Both sides are not a literal size either but a frame the shot is fitted into: the proportions of the shot are never distorted in this package.

The preview is mirrored, the shot is not

A front camera is shown mirrored: a person is used to seeing themselves as in a mirror, and a non-mirrored preview reads as someone else’s face. That reflection is not carried over to the shot — otherwise the text on a business card or a document would go into the looking glass, and that is exactly what is shot with the rear camera.

It is governed by the mirror prop; without it only facing="user" is mirrored.

The stream goes out together with the component

A live track keeps the camera indicator on even when the component is no longer on the screen: the browser puts it out only on a stop() of every track. Unmounting therefore stops the stream — not “just in case” but because otherwise a lamp is lit for the user on a page where there is no camera.

Limits
  • there is no recognition — neither of faces nor of codes: that is a separate task and a separate weight;
  • there is no video recording. The component is about a frame rather than about a stream; recording would require MediaRecorder, formats and control of the duration;
  • there is no choice of a device from a list. deviceId is accepted as a prop, and the enumeration of cameras (enumerateDevices) is left to the application: before the first permission the browser does not give away the names of the devices anyway;
  • there is no cropping. The frame is captured in full; cutting what is needed out of it is the work of GrImageCrop.