GrCodeScanner

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

Берут, когда вход по QR.

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

Обзор

Когда брать

  • вход по QR — на экране логина другого устройства, вместо ручного набора кода;
  • товар на складе — приёмка сканирует штрихкоды подряд, continuous для этого и есть;
  • оплата или ссылка из афиши — код ведёт на страницу, приложение открывает её;
  • привязка устройства — код показывает второй экран, а телефон подтверждает.

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

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

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

  • Приёмка коробок по заказу

    continuous не останавливает сканер и сообщает тот же код повторно: одинаковые коробки подряд — законные повторы, а остановка после каждой стоила бы дороже самого скана. formats сужает поиск до EAN-13 и Code 128, interval задаёт частоту разбора кадров, а код не из заказа попадает в отдельную строку.

    Receiving · PO-77120Northwind Supplies → Hamburg, dock 3In progress

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

    Desk lamp, black400638133393124 / 24
    Desk lamp, white400638133394817 / 24
    Monitor arm, single59012341234570 / 12
    Cable tray, 80 cm761234567890440 / 40
    Scan cartons in any order; repeats count.
  • Проверка билетов на входе со своей панелью

    Слот controls заменяет панель и оставляет поток на месте: разрешение, цикл кадров и уборка остаются за компонентом. Страница решает, что написано на одной большой кнопке, и называет каждое состояние словами — «Камера заблокирована, разрешите её в адресной строке» поможет волонтёру больше, чем «denied».

    Hall B · check-in1 of 320 admitted

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

    Camera off

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

Props

PropTypeпо умолчаниюОписание
facing"user" | "environment" | undefined"environment"Какая камера: `environment` — тыловая, ей и сканируют.
deviceIdstring | undefinedundefinedКонкретное устройство, если приложение его уже выбрало.
autoStartboolean | undefinedfalseВключать камеру сразу. По умолчанию `false`: запрос разрешения без действия пользователя отклоняют не глядя, а второй раз браузер уже не спросит.
sizeGrCodeScannerSize | undefinedundefinedСтупень размера: элементы управления и кегль подписей. По умолчанию `md`; берётся из `GrConfigProvider`.
disabledboolean | undefinedfalseСканер недоступен: камера не включается, кнопки выключены.
ariaLabelstring | undefinedundefinedИмя группы с превью и кнопками. Не задано — из локали («Code scanner»).
formatsreadonly string[] | undefinedundefinedСимволики: `qr_code`, `ean_13`, `code_128`, … Без списка — все, что умеет браузер.
detectorGrCodeDetector | undefinedundefinedДетектор для браузеров без `BarcodeDetector` — Safari и Firefox. Пакет своего декодера не несёт: у него нет ни одной зависимости, и ради одного компонента появилась бы самая тяжёлая. Рецепт подключения готовой библиотеки — на странице компонента.
intervalnumber | undefined250Как часто разбирать кадр, мс. Чаще — горячее устройство, реже — вялый отклик.
continuousboolean | undefinedfalseСообщать один и тот же код повторно: приёмка сканирует подряд.

Слоты

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

События

EventСигнатураОписание
start(event: "start"): void—
stop(event: "stop"): void—
statusChange(event: "statusChange", status: GrCameraStatus): void—
detect(event: "detect", codes: GrCodeResult[]): void—

Методы

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

Типы

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

Импорт
import type { GrCameraStatus, GrCodeResult, GrCodeDetector } from '@feugene/granularity-media/components/GrCodeScanner'

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

GrCameraStatustype

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

type GrCameraStatus = 'idle'

Файлdist/types/components/GrCodeScanner/codeDetection.d.ts

GrCodeResultinterface

Один распознанный код.

interface GrCodeResult {
    /** Содержимое: адрес, номер товара, текст. */
    value: string;
    /** Символика: `qr_code`, `ean_13`, `code_128`, … */
    format: string;
}
GrCodeDetectortype

Детектор кадра. Ровно та форма, в которую заворачивается любая библиотека: на вход — то, что умеет рисовать canvas, на выход — список кодов.

type GrCodeDetector = (source: CanvasImageSource) => Promise<GrCodeResult[]>;
/** Есть ли нативный `BarcodeDetector`. */
export declare function nativeDetectorSupported(scope?: typeof globalThis): boolean;
/**
 * Нативный детектор в общей форме.
 *
 * Формат приводится к нашему типу здесь, а не в компоненте: спецификация зовёт
 * содержимое `rawValue`, и это единственное место, где чужое имя видно.
 */
export declare function createNativeDetector(formats: readonly string[] | undefined, scope?: typeof globalThis): GrCodeDetector | null;
/**
 * Что сообщать наружу из очередного кадра.
 *
 * Камера отдаёт десятки кадров в секунду, и один и тот же код распознаётся в
 * каждом. Без фильтра приложение получило бы поток одинаковых событий и
 * оформило бы двадцать заказов вместо одного. Поэтому по умолчанию сообщается
 * только то, чего не было в предыдущем кадре.
 *
 * `continuous` нужен там, где сканируют подряд (приёмка на складе): там повтор
 * того же кода — законное второе событие.
 */
export declare function freshCodes(previous: readonly GrCodeResult[], next: readonly GrCodeResult[], continuous: boolean): GrCodeResult[];
nativeDetectorSupportedfunction

Есть ли нативный BarcodeDetector.

function nativeDetectorSupported(scope?: typeof globalThis): boolean;
createNativeDetectorfunction

Нативный детектор в общей форме. Формат приводится к нашему типу здесь, а не в компоненте: спецификация зовёт содержимое rawValue, и это единственное место, где чужое имя видно.

function createNativeDetector(formats: readonly string[] | undefined, scope?: typeof globalThis): GrCodeDetector | null;
freshCodesfunction

Что сообщать наружу из очередного кадра. Камера отдаёт десятки кадров в секунду, и один и тот же код распознаётся в каждом. Без фильтра приложение получило бы поток одинаковых событий и оформило бы двадцать заказов вместо одного. Поэтому по умолчанию сообщается только то, чего не было в предыдущем кадре. continuous нужен там, где сканируют подряд (приёмка на складе): там повтор того же кода — законное второе событие.

function freshCodes(previous: readonly GrCodeResult[], next: readonly GrCodeResult[], continuous: boolean): GrCodeResult[];

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

grCodeScannerConfigconst

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

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

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

GrCodeScannerConfigurablePropsinterface

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

interface GrCodeScannerConfigurableProps {
    size?: GrCodeScannerSize;
}

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

GrCodeScannerPropsinterface
interface GrCodeScannerProps {
    /** Какая камера: `environment` — тыловая, ей и сканируют. */
    facing?: 'user' | 'environment';
    /** Конкретное устройство, если приложение его уже выбрало. */
    deviceId?: string;
    /**
     * Включать камеру сразу.
     *
     * По умолчанию `false`: запрос разрешения без действия пользователя
     * отклоняют не глядя, а второй раз браузер уже не спросит.
     */
    autoStart?: boolean;
    /** Символики: `qr_code`, `ean_13`, `code_128`, … Без списка — все, что умеет браузер. */
    formats?: readonly string[];
    /**
     * Детектор для браузеров без `BarcodeDetector` — Safari и Firefox.
     *
     * Пакет своего декодера не несёт: у него нет ни одной зависимости, и ради
     * одного компонента появилась бы самая тяжёлая. Рецепт подключения готовой
     * библиотеки — на странице компонента.
     */
    detector?: GrCodeDetector;
    /** Как часто разбирать кадр, мс. Чаще — горячее устройство, реже — вялый отклик. */
    interval?: number;
    /** Сообщать один и тот же код повторно: приёмка сканирует подряд. */
    continuous?: boolean;
    /**
     * Ступень размера: элементы управления и кегль подписей. По умолчанию `md`;
     * берётся из `GrConfigProvider`.
     */
    size?: GrCodeScannerSize;
    /** Сканер недоступен: камера не включается, кнопки выключены. */
    disabled?: boolean;
    /** Имя группы с превью и кнопками. Не задано — из локали («Code scanner»). */
    ariaLabel?: string;
}
GrCodeScannerEmitsinterface
interface GrCodeScannerEmits {
    /** Распознаны коды. Без `continuous` — только те, которых не было в прошлом кадре. */
    (e: 'detect', codes: GrCodeResult[]): void;
    /** Поток камеры пошёл — сканирование началось. */
    (e: 'start'): void;
    /** Камера выключена. */
    (e: 'stop'): void;
    /** Состояние камеры сменилось — значения `GrCameraStatus`. */
    (e: 'statusChange', status: GrCameraStatus): void;
}

Файлdist/types/components/GrCodeScanner/grCodeScannerStyles.d.ts

GrCodeScannerSizetype
type GrCodeScannerSize = 'xs' | 'sm' | 'md' | 'lg';
grCodeScannerSafelistconst

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

const grCodeScannerSafelist: string[];

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

Пакет не несёт декодер

Нативный BarcodeDetector есть в Chrome и Edge, но его нет ни в Safari, ни в Firefox — то есть на iPhone его нет вовсе, а сканируют чаще всего именно телефоном. Встроить декодер в пакет нельзя: у него нет ни одной зависимости, и ради одного компонента появилась бы самая тяжёлая — притом у всех, включая тех, кто взял только кроп.

Поэтому нативный путь компонент умеет сам, а остальные браузеры закрывает детектор, который передаёт приложение:

ts
import { BrowserMultiFormatReader } from '@zxing/browser'

const reader = new BrowserMultiFormatReader()

async function detector(source) {
  const canvas = Object.assign(document.createElement('canvas'), {
    width: source.videoWidth,
    height: source.videoHeight,
  })
  canvas.getContext('2d')?.drawImage(source, 0, 0)

  const result = reader.decodeFromCanvas(canvas)

  return result ? [{ value: result.getText(), format: String(result.getBarcodeFormat()) }] : []
}
vue
<GrCodeScanner :detector="detector" />

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

Читать нечем — и это отдельное состояние: компонент говорит прямо, что браузер не умеет и что нужен detector. Предлагать «включите камеру» в этом случае значило бы отправить пользователя решать не ту задачу.

Проверяется это после монтирования: на сервере BarcodeDetector нет никогда, и решение в рендере расходилось бы с браузером при гидрации. Первый рендер везде одинаков — состояние покоя, — и сообщение «браузер не умеет читать коды» появляется только там, где это правда.

Свой слот #controls владеет действиями

Слот получает status, start и stop — тип GrCameraStatus экспортируется и из @feugene/granularity-media/components/GrCodeScanner. Когда слот передан, в рамке остаётся только текст состояния: своя кнопка «Начать» там стала бы второй под панелью приложения.

Один код в кадре — одно событие

Камера отдаёт десятки кадров в секунду, и код распознаётся в каждом. Без фильтра приложение получило бы поток одинаковых событий и оформило бы двадцать заказов вместо одного, поэтому сообщается только то, чего не было в предыдущем кадре.

Символика — часть тождества: одно и то же значение в qr_code и в ean_13 считается разными кодами, потому что и означает разное.

continuous снимает фильтр: на приёмке сканируют одинаковые упаковки подряд, и там повтор — законное второе событие.

Разбор идёт по таймеру, а не по каждому кадру

interval (250 мс по умолчанию) — размен между отзывчивостью и нагревом: разбирать каждый кадр значит держать процессор занятым непрерывно, а телефон в руке греется заметно.

Кадр, который не разобрался, — норма, а не сбой: в объектив попала рука, отражение, размытие. Такой заход молча пропускается, следующий придёт через interval.

Границы
  • нет разбора картинки из файла. Компонент работает с живым потоком; для снимка кода приложение вызывает свой детектор напрямую;
  • нет подсветки и зума. torch и zoom — расширения трека, поддержка у них разная даже внутри Chrome;
  • нет разметки найденного кода на превью. Нативный API отдаёт координаты, но чужой детектор — не обязательно, и рисовать рамку по половине источников значило бы обещать то, чего нет.