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
| Need | Take |
|---|---|
| Accept a ready file: a drop zone, a queue, checks | GrFileUpload / GrFormFile |
| Choose a crop out of a picture already obtained | GrImageCrop |
| Show a picture full screen without changing anything | GrImageViewer |
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-ratioasks the camera for a square — a wish, not a crop — andoutputmakes a 512px JPEG instead of a full-size frame.status-changegives each refusal its own next step: settings, another device, closing the other app.ALProfile photoInitials for nowThe 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. Everycapturebrings aBlob— here three of the last photos with their weight, kept small byoutput. Once the parcel is handed over the camera isdisabled: 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
controlsslot replaces the buttons, not the camera: it gets the status and the actions — herecaptureandstop— 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 onstart, anddevice-idswitches to the chosen camera. When both sides are in, the exposedstop()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 side1Front sideNext2Back sideWaitingA 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-startis 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.80Receipt missingFinance needs a photo of the receipt.
API generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
facing | GrCameraFacing | undefined | "user" | Which camera: `user` — front-facing, `environment` — rear. |
deviceId | string | undefined | undefined | A specific device, if the application has already picked one. |
aspectRatio | number | undefined | undefined | A 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. |
autoStart | boolean | undefined | false | Turn 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. |
mirror | boolean | undefined | undefined | Mirror the preview. By default — the front camera only. |
output | GrCameraCaptureOutput | undefined | undefined | The 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. |
size | GrCameraCaptureSize | undefined | undefined | The size step: the controls and the type size of the captions. `md` by default; taken from `GrConfigProvider`. |
disabled | boolean | undefined | false | The camera is unavailable: it cannot be turned on and no frame can be taken; the buttons are disabled. |
ariaLabel | string | undefined | undefined | The name of the group with the preview and the buttons. Not set — from the locale ("Camera"). |
Slots
| Slot | Type | Description |
|---|---|---|
controls |
| A custom control panel instead of the built-in one. |
Events
| Event | Signature | Description |
|---|---|---|
start | (event: "start"): void | — |
stop | (event: "stop"): void | — |
capture | (event: "capture", blob: Blob): void | — |
statusChange | (event: "statusChange", status: GrCameraStatus): void | — |
Methods
| Methods | Signature | Description |
|---|---|---|
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 | () => void | Turn 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. |
status | GrCameraStatus | The 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 type { GrCameraStatus, GrCameraCaptureConfigurableProps, GrCameraCaptureOutput } from '@feugene/granularity-media/components/GrCameraCapture'Filedist/types/components/shared/cameraState.d.ts
type GrCameraStatus = 'idle'function cameraStatusFromError(error: unknown): GrCameraStatus;function cameraSupported(navigatorLike: Pick<Navigator, 'mediaDevices'> | undefined): boolean;function shouldMirrorPreview(facing: 'user' | 'environment', mirror: boolean | undefined): boolean;Filedist/types/components/GrCameraCapture/config.d.ts
const grCameraCaptureConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrCameraCapture">;Filedist/types/components/GrCameraCapture/defaults.d.ts
interface GrCameraCaptureConfigurableProps {
size?: GrCameraCaptureSize;
}Filedist/types/components/GrCameraCapture/GrCameraCapture.vue.d.ts
interface GrCameraCaptureOutput {
width?: number;
height?: number;
type?: string;
quality?: number;
}interface GrCameraCaptureProps {
facing?: GrCameraFacing;
deviceId?: string;
aspectRatio?: number;
autoStart?: boolean;
mirror?: boolean;
output?: GrCameraCaptureOutput;
size?: GrCameraCaptureSize;
disabled?: boolean;
ariaLabel?: string;
}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
type GrCameraCaptureSize = 'xs' | 'sm' | 'md' | 'lg';type GrCameraFacing = 'user' | 'environment';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:
| State | What happened | What the user should do |
|---|---|---|
denied | a refusal or a policy | allow it in the settings of the site |
missing | there is no camera | nothing — there is no retry button |
busy | the device is taken by another application | close that application and retry |
insecure | the page is not opened over HTTPS | nothing: 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.
deviceIdis 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.
Props in this example