# VideoViewer Component

## Описание

VideoViewer - это UI-компонент для отображения видеопотока. Поддерживает два режима работы: `camera` - самостоятельный захват изображения с локальной веб-камеры через `getUserMedia` (с выбором устройства, если их несколько), и `remote` - пассивное отображение кадров (JPEG), которые передаются компоненту извне через проп `frame` (получение потока по WebSocket или другому протоколу остаётся на стороне приложения). Компонент также обрабатывает состояния загрузки, ошибок и отсутствия доступа к камере.

## Пропсы

| Название       | Тип                                              | По умолчанию          | Описание                                                                                                                                                       |
| -------------- | ------------------------------------------------ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`           | `string`                                         | `crypto.randomUUID()` | Уникальный идентификатор компонента                                                                                                                            |
| `wrapperClass` | `string`                                         | `""`                  | Дополнительные CSS-классы для внешней обёртки компонента                                                                                                       |
| `label`        | `object`                                         | `-`                   | Настройки заголовка компонента                                                                                                                                 |
| `label.name`   | `string`                                         | `""`                  | Текст заголовка над видео                                                                                                                                      |
| `label.class`  | `string`                                         | `""`                  | CSS-классы для стилизации заголовка                                                                                                                            |
| `showSelect`   | `boolean`                                        | `true`                | Показывать ли селектор источника (список доступных камер); применяется только в режиме `source="camera"` и только когда найдено больше одного устройства       |
| `source`       | `"camera" \| "remote"`                           | `"camera"`            | Режим работы: `camera` - локальный доступ к веб-камере через `getUserMedia`, `remote` - компонент только отображает кадры, переданные извне через проп `frame` |
| `streamKey`    | `string`                                         | `undefined`           | Ключ потока для приложения/конструктора - самим компонентом не используется (компонент не открывает соединений)                                                |
| `frame`        | `Uint8Array \| Blob \| null`                     | `null`                | Последний полученный кадр (JPEG) в режиме `remote` - обновляется родительским приложением при получении нового кадра                                           |
| `status`       | `"connecting" \| "live" \| "offline" \| "error"` | `undefined`           | Статус соединения в режиме `remote` - управляет индикатором поверх кадра (индикатор скрывается при `live`, если есть кадр)                                     |

## События

Компонент не принимает коллбэк-пропсы и не генерирует событий в привычном смысле. Вместо этого через `bind:this` наружу экспортируются методы для управления локальным потоком (актуальны только в режиме `source="camera"`):

| Название      | Тип                                 | Описание                                                                                                |
| ------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `getDevices`  | `() => Promise<MediaDeviceInfo[]>`  | Запрашивает список доступных видеоустройств и обновляет внутренний стор `sources`                       |
| `startStream` | `(devId?: string) => Promise<void>` | Запускает захват видео с указанного (или первого доступного) устройства и привязывает поток к `<video>` |
| `stopStream`  | `() => void`                        | Останавливает все треки текущего потока и очищает `<video>`                                             |

## Примеры

### Локальная камера

```svelte
<script>
  import * as UI from "poe-svelte-ui-lib"
</script>

<div class="h-80">
  <UI.VideoViewer label={{ name: "Веб-камера" }} />
</div>
```

### Камера без селектора источника

```svelte
<script>
  import * as UI from "poe-svelte-ui-lib"
</script>

<div class="h-80">
  <UI.VideoViewer label={{ name: "Основная камера" }} showSelect={false} />
</div>
```

### Удалённый поток (кадры приходят извне)

```svelte
<script>
  import * as UI from "poe-svelte-ui-lib"

  let frame = $state(null)
  let status = $state("connecting")

  // Приложение само открывает WebSocket/иной канал и
  // на каждый новый кадр обновляет frame и status
  const socket = new WebSocket("wss://example.com/video")
  socket.onmessage = async (event) => {
    frame = new Blob([event.data], { type: "image/jpeg" })
    status = "live"
  }
  socket.onclose = () => (status = "offline")
  socket.onerror = () => (status = "error")
</script>

<div class="h-80">
  <UI.VideoViewer label={{ name: "Удалённая камера" }} source="remote" {frame} {status} />
</div>
```

## Внутренняя архитектура

### Реактивность

- Переменная `isRemote` (`$derived`) переключает разметку и логику между режимами `camera` и `remote` на основе пропа `source`
- Переменные `videoElement`, `stream`, `error`, `isStreaming`, `sources`, `loading`, `devId` управляют состоянием локального захвата видео в режиме `camera`
- Переменная `remoteImgSrc` хранит `object URL`, построенный из пропа `frame`, для отображения в `<img>` в режиме `remote`

### Сторы и зависимости

- Используется стор `twMerge` для объединения Tailwind CSS классов
- Применяется переход `slide`-подобной анимации через `svelte/transition` в дочерних компонентах (`Select`), сама карточка использует условный рендеринг без переходов
- Используется `onMount` для инициализации списка устройств и запроса доступа к камере (только в режиме `camera`), а также для остановки потока при размонтировании
- Используется `$effect` с `setInterval` (1 сек) для периодического опроса `getDevices` и обновления списка камер (актуально при подключении/отключении устройств "на лету")
- Используется отдельный `$effect` для режима `remote`: на каждое изменение пропа `frame` создаётся новый `object URL` через `URL.createObjectURL`, а предыдущий отзывается через `URL.revokeObjectURL` (в том числе при размонтировании) - предотвращает утечки памяти

### Директивы

- `bind:this={videoElement}` для прямого доступа к DOM-элементу `<video>` и привязки к нему `MediaStream`
- Условные блоки `{#if isRemote}` / `{:else}` разделяют разметку камеры и удалённого режима
- Обработчик `onclick` на кнопке повтора для повторного запроса доступа к камере при ошибке

### Слоты

- Компонент не использует внешних слотов

## Конструктор свойств (VideoViewerProps.svelte)

### Описание

Компонент `VideoViewerProps.svelte` предоставляет визуальный интерфейс для редактирования свойств VideoViewer. Поддерживает два режима отображения: для конструктора и для редактирования.

### Пропсы конструктора

| Название           | Тип                                                                                                                                 | По умолчанию | Описание                                                |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | ------------ | ------------------------------------------------------- |
| `component`        | `UIComponent & { properties: Partial<IVideoViewerProps> }`                                                                          | `-`          | Объект компонента с его свойствами                      |
| `onPropertyChange` | `(updates: Partial<{ properties?: string \| object; name?: string; access?: string; eventHandler?: IUIComponentHandler }>) => void` | `-`          | Коллбэк для обновления свойств компонента               |
| `forConstructor`   | `boolean`                                                                                                                           | `true`       | Режим отображения (для конструктора или редактирования) |

### Особенности конструктора

- Предоставляет переключатель источника (`camera`/`remote`) через `Switch`
- Предоставляет переключатель видимости селектора устройств (`showSelect`)
- Поле для `streamKey` отображается только при выбранном режиме `source="remote"`
- В режиме редактирования (`forConstructor={false}`) дополнительно доступны идентификатор, класс обёртки и заголовок через `CommonSnippets`
- Использует систему локализации через `$T('constructor.props.*')`

## Заметки

### Адаптивность

- Компонент использует Tailwind CSS для адаптивного дизайна и заполняет доступное пространство родителя (`w-full h-full`)
- Видео и удалённое изображение растягиваются на всю область с `object-contain`, сохраняя пропорции

### Ограничения

- Режим `camera` зависит от `navigator.mediaDevices` и требует разрешения пользователя на доступ к камере; при отказе или отсутствии API отображается сообщение об ошибке
- Режим `remote` не открывает соединений самостоятельно - приложение обязано само получать кадры (например, по WebSocket) и передавать их через `frame`/`status`
- Список устройств в режиме `camera` обновляется поллингом раз в секунду, а не событием `devicechange` - обновление может быть отложено до секунды
- Селектор источника показывается только при более чем одном найденном устройстве и при `showSelect={true}`

### Производительность

- Компонент использует оптимизации Svelte для минимизации перерисовок
- Поток камеры и обработчики корректно останавливаются/отписываются при размонтировании (`stopStream`, очистка `setInterval`)
- `object URL` для кадров в режиме `remote` создаётся и отзывается на каждый кадр, чтобы избежать утечек памяти при длительной работе
