<!-- GENERATED by scripts/build-llms.mjs from llms/media.md — do not edit this file. -->

# `lr-video`

- **Import** `import '@aceshooting/lyra-ui/components/lr-video.js';` (stable tag alias; registers the tag)
- **Class** `LyraVideo`, also available unregistered from `@aceshooting/lyra-ui/components/media/video/video.class.js`
- **Family** `components/media/` — see `llms/index.md` for its siblings
- **Status** `experimental` since `8.0.0` — see the maturity and deprecation policy in `llms/shared.md`
- **Release history** [CHANGELOG.md](../../CHANGELOG.md); family-wide breaking-change summaries: [llms-full.txt](../../llms-full.txt)
- **Deprecations** none
- **Optional peers** `dompurify` — see `llms/peers.md`
- **Themeable via** 16 parts, 5 custom properties — see this component's own `@csspart`/`@cssprop` list below
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-video`

Experimental inline native video player with custom controls, safe declarative sources/tracks,
selectable captions, and bounded WebVTT thumbnail previews. It mirrors the public Web Awesome Video
API under the `lr-` prefix. Import the granular registration entry with
`import '@aceshooting/lyra-ui/components/media/video/video.js'`.

When progress is unavailable, the disabled native range input and its visible timeline track retain
resting paint under hover and press. Once media duration makes progress available, the track retains
its token-driven hover and press feedback.

**Properties:** `autoplay: boolean = false`, `autoplayMuted: boolean = false` (attribute
`autoplay-muted`), `autoplayOnVisible: boolean = false` (attribute `autoplay-on-visible`),
`controls: 'none' | 'standard' | 'full' = 'standard'`, `currentTime: number = 0` (attribute
`currentTime`; HTML exposes it as lowercase `currenttime`, with legacy `current-time` also
accepted), `duration: number = 0` (live/read-only in normal use), `iconLibrary: string =
'system'` (attribute `icon-library`), `loop: boolean = false`, `muted: boolean = false`, `playing:
boolean = false` (live/read-only in normal use), `poster: string = ''`, `preload: 'auto' |
'metadata' | 'none' = 'metadata'`, `src: string = ''`, `thumbnails: string = ''`, `title: string =
''`, and `volume: number = 1`. The private native `<video>` always carries `playsinline`; native
browser controls stay disabled because the selected Lyra preset owns the control surface.
`autoplayOnVisible` does not start a video merely because it is visible: it pauses a currently
playing video when it leaves view and resumes only that visibility-owned pause when it returns.
Turning the property off while such a pause is pending resumes immediately; observer rebuilds,
temporary disconnects, reconnects, and same-origin adoption preserve the pending ownership. An
explicit user/API pause or source reload/change revokes it so later visibility changes cannot
restart user-stopped media.

`controls="standard"` renders play/pause, timeline and elapsed/duration labels, volume/mute,
available captions, and capability-gated fullscreen. `controls="full"` adds playback rate and
capability-gated picture in picture. `controls="none"` removes the control bar but leaves the
poster and active caption overlays available. Fullscreen/PiP/caption affordances are feature-gated
instead of browser-name-gated and are probed through the concrete native element's current owner
realm. While the poster is visible, its button is the only exposed play action; the ordinary
control-bar play toggle is hidden until the poster is dismissed.

**Methods:** `getState(): VideoState` returns a fresh synchronous
`{ playing, currentTime, duration, volume, muted, playbackRate }` snapshot. `VideoState` is the
canonical upstream-compatible authoring type; the redundant `LyraVideoState` alias is removed in
v9;
`getVideoElement(): HTMLVideoElement | undefined` returns the private native element after mount;
`play(): Promise<void>` returns the exact native promise and preserves its rejection; `pause()`,
`togglePlay()`, `toggleMute()`, `seek(time)`, `setPlaybackRate(rate)`, and `setVolume(volume)` proxy
finite, clamped media state; `requestFullscreen()` and `exitFullscreen()` preserve the platform
promise/rejection and reject with an owner-realm `DOMException` named `NotSupportedError` when the
capability is absent. `load()` is a
Lyra extension that re-clones current light-DOM sources/tracks and restarts native resource
selection under a fresh event generation. `focus(options?)`, `blur()`, and `click()` forward to the
play/pause control (absent, and therefore a no-op, under `controls="none"`).

**Events:** native `ended`, `error`, `loadedmetadata`, `pause`, `play`, `timeupdate`, and
`volumechange`, relayed exactly once from the host as native `Event` instances. They remain
non-bubbling, non-composed, and non-cancelable. Scrubbing the custom timeline also dispatches an
immediate host `timeupdate`, before a browser's eventual native seek notification. The internal
play/pause control's `focus`/`blur` are relayed exactly once as owner-realm native `FocusEvent`s
(bubbling and composed, preserving `relatedTarget`).

**Slots:** the default slot accepts direct `<source>` and `<track>` children;
`controls-after-play`, `controls-start`, `exit-fullscreen-icon`, `fullscreen-icon`, `mute-icon`,
`pause-icon`, `play-icon`, `poster-icon`, and `volume-icon` customize the control surface. Consumer
source/track nodes remain in light DOM: the component inserts fresh private clones containing only
safe URL, source (`type`, `media`), and track (`kind`, `srclang`, `label`, `default`) attributes.
The seven `*-icon` slots are decorative glyph overrides: assigned content renders in an inert,
`aria-hidden`, pointer-transparent visual layer beside the named native button, never inside its
flat-tree descendants. An accidentally supplied link, button, or input therefore cannot create a
nested action or second keyboard stop. `controls-start` and `controls-after-play` remain ordinary
composition slots and may intentionally contain interactive controls.

A declarative host `aria-label` or `title` remains the component's overall name and is not copied to
the private native video; that element receives the localized player-purpose name. An explicitly
empty host `aria-label` is preserved exactly as an empty native-video name. Caption/subtitle tracks
selected for Lyra's custom overlay use native `mode="hidden"`, keeping cue activity available
without asking the browser to paint a duplicate native caption layer.

**CSS parts:** `base` and `video-wrapper` (aliases on the same root node), `caption`,
`caption-overlay`, `controls`, `controls-overlay`, `poster-overlay`, `poster-play-button`,
`progress`, `thumbnail`, `timeline`, `timeline-indicator`, `timeline-thumb`, `timeline-track`,
`video`, and `video-title-overlay`.

The `progress` range carries `aria-label` and points through `aria-describedby` to a localized
`{current} of {duration}` description (key `avPlayerPosition`, shared with `lr-av-player`) built
from the same locale-formatted clock times as the visible labels. This deliberately avoids relying
on `aria-valuetext`, which browsers ignore on native range semantics. Before duration is available,
the same range remains in the DOM as a disabled `0..0` control with its description intact,
avoiding a disappearing/reappearing semantic target.

**Themeable custom properties:** `--controls-background` (default
`var(--lr-color-overlay-strong)`), `--controls-color` (default
`var(--lr-color-on-strong-overlay)`), and
`--poster-play-button-background` (default `var(--lr-color-surface-overlay)`). These exact names are
kept for mechanical Web Awesome migration. Lyra also supplies
`--lr-video-poster-play-button-hover-background` (default is the existing hover color mix) and
`--lr-video-poster-play-button-hover-border-color` (default `var(--lr-color-brand)`).

Caption and playback-rate selectors remain native `<select>` controls with decorative, pointer-inert
chevrons; their option foreground and background inherit `--controls-color` and
`--controls-background`. Controls, elapsed/title text, caption text, and selectors consistently use
the semantic strong-overlay foreground. The fallback remains legible in light and dark themes.
The poster play button and its glyph instead default to `--lr-color-text` against their
`--lr-color-surface-overlay` surface. An explicit `--controls-color` retains final precedence on
both poster and overlay controls, and forced-colors mode remains UA-owned.

**RTL behavior:** surrounding controls follow the inherited direction, while the elapsed-media axis
stays physical left-to-right. Native ArrowRight advances and ArrowLeft rewinds the timeline in both
LTR and RTL.

**Thumbnail security and lifecycle:** `thumbnails` is validated before `fetch()`, read through a
256 KiB byte ceiling, and parsed up to 2,000 cues. Cue image URLs are resolved relative to the VTT
file and validated again before reaching `<img>`; `#xywh=x,y,width,height` sprite fragments are
supported. A generation token is checked after every await, and changing `thumbnails` or
disconnecting prevents an older response from painting over newer state. Invalid, oversized,
failed, or empty thumbnail files fail closed to no preview; no caught remote error text is shown.

```html
<lr-video controls="full" poster="/posters/demo.jpg" title="Product demo">
  <source src="/video/demo.webm" type="video/webm" />
  <source src="/video/demo.mp4" type="video/mp4" />
  <track
    src="/captions/demo-en.vtt"
    kind="captions"
    srclang="en"
    label="English"
    default
  />
</lr-video>
```
