GrCameraCapture
Берут, когда аватар без файлового менеджера.
Когда брать
- аватар без файлового менеджера — пользователь на телефоне, и «сфотографировать» быстрее, чем искать снимок в галерее;
- документ или визитка в форме — тыловой камерой, а распознавание уже на сервере;
- подтверждение присутствия — селфи в заявке, где важен сам факт съёмки здесь и сейчас;
- быстрый снимок товара — карточка заполняется прямо со склада.
Когда взять другое
| Нужно | Берите |
|---|---|
| Принять готовый файл: зона переноса, очередь, проверки | GrFileUpload / GrFormFile |
| Выбрать кадр из уже полученной картинки | GrImageCrop |
| Показать картинку во весь экран, ничего не меняя | GrImageViewer |
Камера не включается сама
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.
Установка
npm i @feugene/granularity-mediaИмпорт
import { GrCameraCapture } from '@feugene/granularity-media/components/GrCameraCapture'API
API этого компонента ещё не посчитан: генератор витрины пока обходит только ядро. Пока его нет, справочник — в документации пакета.
Примеры 1
Basic
<script setup lang="ts">
import { ref, useTemplateRef } from 'vue'
import { GrBadge } from '@feugene/granularity'
import type { GrCameraStatus } from '@feugene/granularity-media'
/**
* Демо специально не включает камеру само: на странице документации это
* означало бы запрос разрешения у каждого, кто зашёл почитать.
*/
const shot = ref<string | null>(null)
const shotSize = ref<{ width: number, height: number } | null>(null)
const status = ref<GrCameraStatus>('idle')
const camera = useTemplateRef('camera')
function onCapture(blob: Blob) {
if (shot.value)
URL.revokeObjectURL(shot.value)
shot.value = URL.createObjectURL(blob)
}
function onShotLoad(event: Event) {
const img = event.target as HTMLImageElement
shotSize.value = { width: img.naturalWidth, height: img.naturalHeight }
}
</script>
<template>
<div class="grid gap-4 lg:grid-cols-2">
<!--
Соотношение сторон не задано намеренно: рамка примет его от камеры.
Зашитое число показало бы обрезанный кадр как настоящий — а камеры отдают
то 4:3, то 16:9.
-->
<GrCameraCapture
ref="camera"
:output="{ width: 800, type: 'image/jpeg', quality: 0.9 }"
@capture="onCapture"
@status-change="(value: GrCameraStatus) => (status = value)"
/>
<div class="showcase-demo-panel grid content-start gap-3 rounded-[var(--gr-radius-lg)] border p-4">
<p class="showcase-demo-text text-sm">
Состояние: <GrBadge size="sm" tone="neutral">{{ status }}</GrBadge>
</p>
<p class="showcase-demo-text text-sm">
Камера включается только по кнопке. Запрос разрешения, всплывший сам по себе,
отклоняют не глядя — а второй раз браузер уже не спросит.
</p>
<template v-if="shot">
<img
:src="shot"
alt="Снимок с камеры"
class="w-full rounded-[var(--gr-radius-md)]"
@load="onShotLoad"
>
<p v-if="shotSize" class="showcase-demo-text text-sm">
Снимок: <strong>{{ shotSize.width }} × {{ shotSize.height }}</strong> — те же пропорции,
что и у превью. Камеры отдают то 4:3, то 16:9, и кадр не подгоняется под окно: нужен
ровно квадрат — это <code>GrImageCrop</code> следующим шагом.
</p>
<p class="showcase-demo-text text-sm">
Превью фронтальной камеры зеркальное, а снимок — нет: иначе текст в кадре уехал бы
в зазеркалье.
</p>
</template>
</div>
</div>
</template>