GrCameraCapture

  • media
  • Медиа
  • API · 9 props

Берут, когда аватар без файлового менеджера.

import { GrCameraCapture } from '@feugene/granularity-media/components/GrCameraCapture'

Обзор

Когда брать

  • аватар без файлового менеджера — пользователь на телефоне, и «сфотографировать» быстрее, чем искать снимок в галерее;
  • документ или визитка в форме — тыловой камерой, а распознавание уже на сервере;
  • подтверждение присутствия — селфи в заявке, где важен сам факт съёмки здесь и сейчас;
  • быстрый снимок товара — карточка заполняется прямо со склада.

Когда взять другое

НужноБерите
Принять готовый файл: зона переноса, очередь, проверкиGrFileUpload / GrFormFile
Выбрать кадр из уже полученной картинкиGrImageCrop
Показать картинку во весь экран, ничего не меняяGrImageViewer

Примеры 4 у каждого — живое демо и его код

  • Фото профиля без файлового менеджера

    Фронтальная камера зеркалится в превью — так человек видит себя, — но не на снимке. aspect-ratio просит у камеры квадрат: это пожелание, а не обрезка, а output делает JPEG шириной 512px вместо полного кадра. status-change даёт каждому отказу свой следующий шаг: настройки, другое устройство, закрыть чужое приложение.

    ALProfile photoInitials for now

    Камера выключена.

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

  • Подтверждение доставки тыловой камерой

    facing="environment" берёт тыловую камеру, и её кадр не зеркалится: номер двери должен читаться. Каждый capture приносит Blob — здесь три последних снимка с их весом, который держит output. После вручения камера disabled: подтверждение окончательное.

    Proof of deliveryOrder #4815 · 12 Harbour Street, Hamburg

    Камера выключена.

    No photos yet — the parcel and the door number are enough.
  • Две стороны документа своими кнопками

    Слот controls заменяет кнопки, а не камеру: он получает статус и действия — здесь capture и stop, — а «Turn on camera», разрешение и уборка при размонтировании остаются за компонентом. Имена устройств приходят только после разрешения, поэтому список читается по start, а device-id переключает на выбранную камеру. Когда обе стороны сняты, открытый метод stop() гасит лампочку камеры.

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

    Камера выключена.

    Step 1 of 2 — the front side
    1Front sideNext
    2Back sideWaiting
  • Фото чека в окне

    Окно существует только ради камеры, а клик, который его открыл, — действие читателя: единственный случай, где auto-start уместен, — без него окно второй раз просило бы включить камеру. Снимок закрывает окно и попадает в расход.

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

API порождается из пакета · разойтись не может

Props

PropTypeпо умолчаниюОписание
facingGrCameraFacing | undefined"user"Какая камера: `user` — фронтальная, `environment` — тыловая.
deviceIdstring | undefinedundefinedКонкретное устройство, если приложение его уже выбрало.
aspectRationumber | undefinedundefinedПожелание камере: какое соотношение сторон запросить у устройства. Это **не обрезка**. Камера, которая умеет запрошенное, отдаст его сама; та, что не умеет, отдаст своё — и показан, и снят будет её настоящий кадр. Нужен ровно квадрат — это `GrImageCrop` после съёмки.
autoStartboolean | undefinedfalseВключать камеру сразу. По умолчанию `false`: запрос разрешения без действия пользователя отклоняют не глядя, а второй раз браузер уже не спросит.
mirrorboolean | undefinedundefinedЗеркалить превью. По умолчанию — только фронтальную камеру.
outputGrCameraCaptureOutput | undefinedundefinedРазмер и формат снимка. Одна сторона — вторая считается из пропорций кадра; обе — габарит, в который кадр вписывается, не искажаясь. Не задан — снимок в размер кадра камеры, JPEG.
sizeGrCameraCaptureSize | undefinedundefinedСтупень размера: элементы управления и кегль подписей. По умолчанию `md`; берётся из `GrConfigProvider`.
disabledboolean | undefinedfalseКамера недоступна: включить её и снять кадр нельзя, кнопки выключены.
ariaLabelstring | undefinedundefinedИмя группы с превью и кнопками. Не задано — из локали («Camera»).

Слоты

SlotTypeОписание
controls
{ status: GrCameraStatus; start: () => void; stop: () => voi{ status: GrCameraStatus; start: () => void; stop: () => void; capture: () => Promise<Blob | null>; }
Своя панель управления вместо встроенной.

События

EventСигнатураОписание
start(event: "start"): void—
stop(event: "stop"): void—
capture(event: "capture", blob: Blob): void—
statusChange(event: "statusChange", status: GrCameraStatus): void—

Методы

MethodsСигнатураОписание
start() => Promise<void>Запросить разрешение и включить камеру. При `disabled`, во время запроса и на живом потоке ничего не делает; отказ оседает в `status`.
stop() => voidВыключить камеру и погасить её индикатор; состояние возвращается в `idle`.
capture() => Promise<Blob | null>Снять текущий кадр целиком, без зеркала превью, в формате `output`. Вернёт `null`, если камера не живая или кадра ещё нет.
statusGrCameraStatusТекущее состояние камеры (`GrCameraStatus`): `idle`, `starting`, `live` или причина отказа.

Типы

Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.

Импорт
import type { GrCameraStatus, GrCameraCaptureConfigurableProps, GrCameraCaptureOutput } from '@feugene/granularity-media/components/GrCameraCapture'

Файлdist/types/components/shared/cameraState.d.ts

GrCameraStatustype

Состояния камеры и разбор отказов. Вынесено из компонента, потому что именно здесь живёт вся неочевидность: getUserMedia отвечает DOMException с полудюжиной имён, и от того, в какое из них попал отказ, зависит текст, который увидит пользователь. Проверить это рендером нельзя — камеры у тестового окружения нет. Лежит в components/shared/, а не у GrCameraCapture: разбор читает и сканер — через общий useCameraStream. Импорт из директории другого компонента granum считает ребром графа, и объявить его значило бы заставить потребителя, выбравшего один сканер, оплатить CSS и safelist камеры целиком.

type GrCameraStatus = 'idle'
cameraStatusFromErrorfunction

Имя DOMException → состояние. Названия заданы спецификацией, но разные браузеры выбирают из них по-разному в одной и той же ситуации: отказ пользователя — NotAllowedError везде, а вот занятое устройство Safari называет NotReadableError, Firefox — AbortError. Поэтому разбор по имени, а не по сообщению: сообщение локализовано браузером и меняется между версиями.

function cameraStatusFromError(error: unknown): GrCameraStatus;
cameraSupportedfunction

Доступен ли API вовсе. На http:// (кроме localhost) navigator.mediaDevices не существует — не «отказ», а отсутствие. Сообщение «разрешите доступ к камере» в этом случае отправляет пользователя искать настройку, которой нет.

function cameraSupported(navigatorLike: Pick<Navigator, 'mediaDevices'> | undefined): boolean;
shouldMirrorPreviewfunction

Зеркалить ли превью. Фронтальную камеру зеркалят всегда: человек видит себя как в зеркале, и незеркальное превью читается как чужое лицо. Тыловую — никогда: там в кадре мир, а не ты. **Снимок при этом не зеркалится.** Иначе текст на визитке или в документе уедет в зазеркалье — а снимают ими ровно это.

function shouldMirrorPreview(facing: 'user' | 'environment', mirror: boolean | undefined): boolean;

Файлdist/types/components/GrCameraCapture/config.d.ts

grCameraCaptureConfigconst

Кнопки состояния и съёмки — GrButton ядра, и ребро объявлено: granum подмешивает safelist и CSS только тем компонентам, что попали в селекцию, и без графа потребитель, выбравший одну камеру, получил бы кнопки без фона.

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

Файлdist/types/components/GrCameraCapture/defaults.d.ts

GrCameraCaptureConfigurablePropsinterface

Настраиваемые через GrConfigProvider пропы. Аугментация реестра идёт по месту объявления — через @feugene/granularity/composables/useGrComponentConfig, а не через реэкспорт: иначе слияние отваливается молча.

interface GrCameraCaptureConfigurableProps {
    size?: GrCameraCaptureSize;
}

Файлdist/types/components/GrCameraCapture/GrCameraCapture.vue.d.ts

GrCameraCaptureOutputinterface
interface GrCameraCaptureOutput {
    /** Ширина снимка. Одна — высота считается из пропорций кадра. */
    width?: number;
    /** Высота снимка. Вместе с `width` — габарит, в который кадр вписывается. */
    height?: number;
    /** MIME снимка: `image/jpeg` (по умолчанию), `image/png`, `image/webp`. */
    type?: string;
    /** Качество сжатия от `0` до `1` для JPEG и WebP. По умолчанию `0.92`. */
    quality?: number;
}
GrCameraCapturePropsinterface
interface GrCameraCaptureProps {
    /** Какая камера: `user` — фронтальная, `environment` — тыловая. */
    facing?: GrCameraFacing;
    /** Конкретное устройство, если приложение его уже выбрало. */
    deviceId?: string;
    /**
     * Пожелание камере: какое соотношение сторон запросить у устройства.
     *
     * Это **не обрезка**. Камера, которая умеет запрошенное, отдаст его сама;
     * та, что не умеет, отдаст своё — и показан, и снят будет её настоящий кадр.
     * Нужен ровно квадрат — это `GrImageCrop` после съёмки.
     */
    aspectRatio?: number;
    /**
     * Включать камеру сразу.
     *
     * По умолчанию `false`: запрос разрешения без действия пользователя
     * отклоняют не глядя, а второй раз браузер уже не спросит.
     */
    autoStart?: boolean;
    /** Зеркалить превью. По умолчанию — только фронтальную камеру. */
    mirror?: boolean;
    /**
     * Размер и формат снимка. Одна сторона — вторая считается из пропорций
     * кадра; обе — габарит, в который кадр вписывается, не искажаясь. Не задан —
     * снимок в размер кадра камеры, JPEG.
     */
    output?: GrCameraCaptureOutput;
    /**
     * Ступень размера: элементы управления и кегль подписей. По умолчанию `md`;
     * берётся из `GrConfigProvider`.
     */
    size?: GrCameraCaptureSize;
    /** Камера недоступна: включить её и снять кадр нельзя, кнопки выключены. */
    disabled?: boolean;
    /** Имя группы с превью и кнопками. Не задано — из локали («Camera»). */
    ariaLabel?: string;
}
GrCameraCaptureEmitsinterface
interface GrCameraCaptureEmits {
    /** Снимок готов: кадр целиком, без зеркала превью. */
    (e: 'capture', blob: Blob): void;
    /** Поток камеры пошёл — превью живое. */
    (e: 'start'): void;
    /** Камера выключена. */
    (e: 'stop'): void;
    /** Состояние камеры сменилось — значения `GrCameraStatus`. */
    (e: 'statusChange', status: GrCameraStatus): void;
}

Файлdist/types/components/GrCameraCapture/grCameraCaptureStyles.d.ts

GrCameraCaptureSizetype
type GrCameraCaptureSize = 'xs' | 'sm' | 'md' | 'lg';
GrCameraFacingtype
type GrCameraFacing = 'user' | 'environment';
grCameraCaptureSafelistconst

Safelist пуст: всё, что рисует камера, лежит целыми литералами здесь и в шаблоне, и granum извлекает это сам — модуль уезжает в общий dist/chunks/, но сборка доходит до него по графу бандла компонента.

const grCameraCaptureSafelist: string[];

Использование из прозы библиотеки

Камера не включается сама

autoStart по умолчанию выключен. Запрос разрешения, всплывший без действия пользователя, отклоняют не глядя — а второй раз браузер уже не спросит: решение запоминается для сайта целиком, и починить это из приложения нельзя. Поэтому первым делом компонент показывает кнопку, и только она запускает запрос.

У каждого отказа своя причина и свой следующий шаг

getUserMedia отвечает DOMException, и по имени видно, что именно случилось. Компонент разводит четыре исхода, потому что действия пользователя в них разные:

СостояниеЧто произошлоЧто делать пользователю
deniedотказ или политикаразрешить в настройках сайта
missingкамеры нетничего — кнопки повтора нет
busyустройство занято другим приложениемзакрыть то приложение и повторить
insecureстраница открыта не по HTTPSничего: API отсутствует

Последнее — не отказ. На http:// объекта navigator.mediaDevices не существует вовсе, и сообщение «разрешите доступ» отправило бы человека искать настройку, которой нет.

Разбор идёт по имени исключения, а не по тексту: текст локализован браузером и меняется между версиями. Имена же разные браузеры выбирают по-своему — занятое устройство Safari называет NotReadableError, Firefox — AbortError.

Кадр не подгоняется под окно

Камеры на разных устройствах отдают разные размеры и соотношения, поэтому подгонять кадр под фиксированное окно бессмысленно: на одном телефоне срезалось бы одно, на другом другое. Компонент показывает и снимает то, что отдала камера, а рамка принимает её соотношение — до первого кадра держится 4:3, чтобы место в раскладке было видно (размеры кадра до loadedmetadata — нули).

aspectRatio при этом не исчез, но означает другое: пожелание камере. Оно уходит в getUserMedia как ideal, и устройство, которое умеет запрошенное, отдаст его само. exact тут брать нельзя — он даёт OverconstrainedError, то есть состояние «камеры нет» на исправной камере с другим соотношением.

Нужен ровно квадрат независимо от устройства — это следующий шаг, а не этот компонент: GrImageCrop режет уже снятое.

output — габарит, а не точный размер

output.width без height — обычный заказ («аватар шириной 800»), и вторая сторона считается из соотношения кадра. Взятая из исходника, она растянула бы картинку: кадр 640×480 при width: 800 дал бы холст 800×480, то есть изображение, растянутое по горизонтали на четверть.

Обе стороны — тоже не буквальный размер, а рамка, в которую кадр вписывается: пропорции съёмки в этом пакете не искажаются никогда.

Превью зеркалится, снимок — нет

Фронтальную камеру показывают зеркально: человек привык видеть себя как в зеркале, и незеркальное превью читается как чужое лицо. На снимок это отражение не переносится — иначе текст на визитке или документе уехал бы в зазеркалье, а снимают тыловой камерой ровно это.

Управляется пропом mirror; без него зеркалится только facing="user".

Поток гаснет вместе с компонентом

Живой трек держит индикатор камеры включённым, даже когда компонента уже нет на экране: браузер гасит его только по stop() каждой дорожки. Поэтому размонтирование останавливает поток — не «на всякий случай», а потому что иначе у пользователя горит лампочка на странице, где камеры нет.

Границы
  • нет распознавания — ни лиц, ни кодов: это отдельная задача и отдельный вес;
  • нет записи видео. Компонент про кадр, а не про поток; запись потребовала бы MediaRecorder, форматов и управления длительностью;
  • нет выбора устройства списком. deviceId принимается пропом, а перечисление камер (enumerateDevices) остаётся за приложением: до первого разрешения имена устройств браузер всё равно не отдаёт;
  • нет обрезки. Кадр снимается целиком; вырезать из него нужное — работа GrImageCrop.