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 side1Front sideNext2Back sideWaitingФото чека в окне
Окно существует только ради камеры, а клик, который его открыл, — действие читателя: единственный случай, где
auto-startуместен, — без него окно второй раз просило бы включить камеру. Снимок закрывает окно и попадает в расход.Taxi to the airportExpense report · Hamburg trip · €46.80Receipt missingFinance needs a photo of the receipt.
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
facing | GrCameraFacing | undefined | "user" | Какая камера: `user` — фронтальная, `environment` — тыловая. |
deviceId | string | undefined | undefined | Конкретное устройство, если приложение его уже выбрало. |
aspectRatio | number | undefined | undefined | Пожелание камере: какое соотношение сторон запросить у устройства. Это **не обрезка**. Камера, которая умеет запрошенное, отдаст его сама; та, что не умеет, отдаст своё — и показан, и снят будет её настоящий кадр. Нужен ровно квадрат — это `GrImageCrop` после съёмки. |
autoStart | boolean | undefined | false | Включать камеру сразу. По умолчанию `false`: запрос разрешения без действия пользователя отклоняют не глядя, а второй раз браузер уже не спросит. |
mirror | boolean | undefined | undefined | Зеркалить превью. По умолчанию — только фронтальную камеру. |
output | GrCameraCaptureOutput | undefined | undefined | Размер и формат снимка. Одна сторона — вторая считается из пропорций кадра; обе — габарит, в который кадр вписывается, не искажаясь. Не задан — снимок в размер кадра камеры, JPEG. |
size | GrCameraCaptureSize | undefined | undefined | Ступень размера: элементы управления и кегль подписей. По умолчанию `md`; берётся из `GrConfigProvider`. |
disabled | boolean | undefined | false | Камера недоступна: включить её и снять кадр нельзя, кнопки выключены. |
ariaLabel | string | undefined | undefined | Имя группы с превью и кнопками. Не задано — из локали («Camera»). |
Слоты
| Slot | Type | Описание |
|---|---|---|
controls |
| Своя панель управления вместо встроенной. |
События
| 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`, если камера не живая или кадра ещё нет. |
status | GrCameraStatus | Текущее состояние камеры (`GrCameraStatus`): `idle`, `starting`, `live` или причина отказа. |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrCameraStatus, GrCameraCaptureConfigurableProps, GrCameraCaptureOutput } from '@feugene/granularity-media/components/GrCameraCapture'Файлdist/types/components/shared/cameraState.d.ts
Состояния камеры и разбор отказов.
Вынесено из компонента, потому что именно здесь живёт вся неочевидность:
getUserMedia отвечает DOMException с полудюжиной имён, и от того, в какое
из них попал отказ, зависит текст, который увидит пользователь. Проверить это
рендером нельзя — камеры у тестового окружения нет.
Лежит в components/shared/, а не у GrCameraCapture: разбор читает и
сканер — через общий useCameraStream. Импорт из директории другого
компонента granum считает ребром графа, и объявить его значило бы заставить
потребителя, выбравшего один сканер, оплатить CSS и safelist камеры целиком.
type GrCameraStatus = 'idle'Имя DOMException → состояние.
Названия заданы спецификацией, но разные браузеры выбирают из них по-разному
в одной и той же ситуации: отказ пользователя — NotAllowedError везде, а
вот занятое устройство Safari называет NotReadableError, Firefox —
AbortError. Поэтому разбор по имени, а не по сообщению: сообщение
локализовано браузером и меняется между версиями.
function cameraStatusFromError(error: unknown): GrCameraStatus;Доступен ли API вовсе.
На http:// (кроме localhost) navigator.mediaDevices не существует —
не «отказ», а отсутствие. Сообщение «разрешите доступ к камере» в этом случае
отправляет пользователя искать настройку, которой нет.
function cameraSupported(navigatorLike: Pick<Navigator, 'mediaDevices'> | undefined): boolean;Зеркалить ли превью. Фронтальную камеру зеркалят всегда: человек видит себя как в зеркале, и незеркальное превью читается как чужое лицо. Тыловую — никогда: там в кадре мир, а не ты. **Снимок при этом не зеркалится.** Иначе текст на визитке или в документе уедет в зазеркалье — а снимают ими ровно это.
function shouldMirrorPreview(facing: 'user' | 'environment', mirror: boolean | undefined): boolean;Файлdist/types/components/GrCameraCapture/config.d.ts
Кнопки состояния и съёмки — GrButton ядра, и ребро объявлено: granum
подмешивает safelist и CSS только тем компонентам, что попали в селекцию, и
без графа потребитель, выбравший одну камеру, получил бы кнопки без фона.
const grCameraCaptureConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrCameraCapture">;Файлdist/types/components/GrCameraCapture/defaults.d.ts
Настраиваемые через GrConfigProvider пропы.
Аугментация реестра идёт по месту объявления — через
@feugene/granularity/composables/useGrComponentConfig, а не через реэкспорт:
иначе слияние отваливается молча.
interface GrCameraCaptureConfigurableProps {
size?: GrCameraCaptureSize;
}Файлdist/types/components/GrCameraCapture/GrCameraCapture.vue.d.ts
interface GrCameraCaptureOutput {
/** Ширина снимка. Одна — высота считается из пропорций кадра. */
width?: number;
/** Высота снимка. Вместе с `width` — габарит, в который кадр вписывается. */
height?: number;
/** MIME снимка: `image/jpeg` (по умолчанию), `image/png`, `image/webp`. */
type?: string;
/** Качество сжатия от `0` до `1` для JPEG и WebP. По умолчанию `0.92`. */
quality?: number;
}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;
}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
type GrCameraCaptureSize = 'xs' | 'sm' | 'md' | 'lg';type GrCameraFacing = 'user' | 'environment';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.
Пропы этого примера