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

# `lr-av-player`

- **Import** `import '@aceshooting/lyra-ui/components/lr-av-player.js';` (stable tag alias; registers the tag)
- **Class** `LyraAvPlayer`, also available unregistered from `@aceshooting/lyra-ui/components/media/av-player/av-player.class.js`
- **Family** `components/media/` — see `llms/index.md` for its siblings
- **Status** `stable` since `4.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** none
- **Themeable via** 16 parts, 11 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-av-player`

An audio/video player built on a native `<audio>`/`<video>` element, plus a cue transcript synced to
`currentTime`, `time-range` anchor/highlight support, an optional dependency-free waveform (peaks
in, no in-component decoding), and playback-rate control. Owns recorded-media transcript sync —
distinct from `<lr-transcript-feed>` (live captions for an in-progress voice session) and
`<lr-sequence-playback>` (a discrete sequence stepper, no native media). Adopts
`DocumentAnchorTarget` with
`anchorKinds: ['time-range']` only — no text selection is bound. The transcript virtualizes through
`<lr-virtual-list>` the same way `lr-pdf-viewer` virtualizes pages.

Removing `mime-type` consumes the absent hint safely and restores automatic video fallback unless
`kind` explicitly selects audio. Attribute removal keeps its `null` property readback; an explicit
empty hint remains empty, and a later audio MIME value restores audio detection.

A cue with no `end` extends to the next strictly chronological applicable start, or remains
open-ended when none exists. Equal starts do not end each other. The player indexes starts on cue
assignment in at most O(n log n) work and reconciles each seek/time update in O(n) work. Caller
order, first ID admission, explicit ends and overlap arbitration are retained; a paused seek uses
replacement cues immediately, even before rendering. Duration changes preserve the same
effective-start rules.

An unavailable timeline keeps its resting border and background under hover and press. Enabled
seeking retains token-driven pointer feedback.

The default document-viewer renderer advertises `time-range` anchors but not search: the generic
document payload has no cue/transcript field, so advertising search there would expose a control
whose result is always empty. Standalone `<lr-av-player>` search remains available whenever the
consumer supplies `cues` directly.

**Properties:** `src: string = ''`, `name: string = ''`, `kind?: 'audio' | 'video'`
(attribute-backed auto-detection override), `mimeType: string = ''` (attribute `mime-type`), `poster: string =
''`, `loop: boolean = false`, `muted: boolean = false`, `preload: 'none' | 'metadata' | 'auto' =
'metadata'`, `playbackRate: number = 1` (attribute `playback-rate`, reflected),
`volume: number = 1` (attribute `volume`, reflected; normalized to `0..1`),
`rates: readonly number[] = [0.75, 1, 1.25, 1.5, 2]` (attribute: false),
`cues: readonly LyraAvCue[] = []` (attribute: false), `peaks: readonly number[] = []`
(attribute: false), and `tracks: readonly LyraAvTrack[] = []` (attribute: false). Each collection is
normalized synchronously on assignment into a bounded, cloned, frozen snapshot; mutate by assigning a
new collection. The inherited
anchor-target surface is `highlights: readonly LyraHighlight[] = []` (property only; assign a new
collection to update), `activeHighlightId: string | null = null` (attribute `active-highlight-id`),
`anchor: LyraAnchor | string | null = null` (property only), and readonly
`anchorKinds: readonly LyraAnchorKind[] = ['time-range']`.
`LyraAvCue = { readonly cueId, readonly start, readonly end?, readonly text, readonly speaker? }`;
`LyraAvTrack = { readonly src, readonly kind: 'subtitles' | 'captions' | 'descriptions', readonly
srclang, readonly label, readonly default? }`. Their retained records are frozen as well as the
outer arrays. Cue IDs are trimmed and must be nonempty; the first cue for a `cueId` is retained and
blank or later duplicate records are ignored.

Only exact `kind="audio"` and `kind="video"` values override MIME auto-detection. An unrecognized
runtime or attribute value falls back to `mimeType` (`audio/*` renders audio; every other value
renders video) and never appears in `lr-load`'s `detail.kind`.

`poster` is ignored for audio and is validated with the same safe media-source allowlist as `src`
before reaching the native video element; an unsafe poster URL is omitted.

Runtime numeric input is normalized before it reaches media, canvas, or `Intl`: cue/highlight times
and waveform peaks are clamped to their valid ranges; non-finite native duration/current time
cannot leak into state or events. `rates` keeps only unique finite values in the supported
`0.0625..16` range, while always including the normalized current `playbackRate`.

**Waveform lifecycle:** with `peaks`, waveform canvas painting is gated by player visibility when
`IntersectionObserver` is available. Peak, theme, and resize changes while the player is
off-screen coalesce into one paint on re-entry; environments without that API retain eager
painting.

**Methods:** `play(): Promise<void>` proxies the native media element and preserves its native
promise/rejection (before the media mounts it returns an already-resolved promise). `pause()` and
`toggle()` proxy the native element; an internal toggle that cannot start playback renders the
error state and emits `lr-render-error`. `seek(seconds)` sets `currentTime` and forces an immediate
`lr-time-change`. `search(query)` resolves the match count; `searchNext()`/`searchPrevious()` wrap
and reveal the active match in the virtualized transcript without seeking playback; `clearSearch()`
resets the query and match state. `focus(options?)`, `blur()`, and `click()` forward to the native
`[part='media']` element, which carries `controls` and is therefore the player's primary focusable
affordance. `scrollToAnchor(target: LyraAnchor | string): Promise<boolean>` seeks to a resolved
`time-range` anchor's `start` after media metadata loads and makes an id-addressed highlight active;
unsupported or unresolved targets report `false` through the return value and `lr-anchor-result`.

**Events:** `lr-play`, `lr-pause`, `lr-load` (`detail: { duration, kind }`), `lr-time-change`
(`detail: { currentTime }`, throttled to at most 4/s while playing plus one extra per `seek()`),
`lr-rate-change` (`detail: { rate }`), `lr-cue-change`
(`detail: { readonly cueId, readonly index }`; `cueId` is `null` and `index` is `-1` when no cue is active),
`lr-highlight-activate` (`detail: { highlightId }`), `lr-anchor-result` (`detail: {
found }`), `lr-search-change` (`detail: { query, matchCount, matchCountExact, activeIndex }`;
`matchCountExact` is always `true` — `search()` matches over the already-loaded `cues` array with no
additional ceiling), and
`lr-render-error` (`detail: { error }`). The native `ended`, `error`, `loadedmetadata`, `pause`,
`play`, `timeupdate`, and `volumechange`
events are also relayed exactly once from the host as native `Event` instances. Like the original
media notifications, these relays are non-bubbling, non-composed, and non-cancelable. The richer
`lr-*` notifications above remain unchanged. The native media element's `focus`/`blur` are relayed
exactly once as owner-realm native `FocusEvent`s (bubbling and composed, preserving
`relatedTarget`). `lr-text-select` is not part of this
player's event contract: transcript rows live inside the embedded virtual list's nested shadow
root, so no selection binding is installed.

**CSS parts:** `base`, `media` (the native `<audio>`/`<video>` element), `toolbar`, `rate-select`,
`timeline` (click-to-seek and arrow-key seeking), `timeline-marker` (one per `time-range` highlight;
`data-tone`, `data-active`), `transcript` (the `<lr-virtual-list>` itself), `cue` (`aria-current`,
`data-match`, `data-active-match`), `cue-current` (added alongside `cue` on the row the playhead is
inside), `cue-match` (added alongside `cue` on a row matching the current search query),
`cue-active-match` (added alongside `cue`/`cue-match` on the row holding the current match),
`cue-time`, `cue-speaker`, `cue-text`, `error`, and `anchor-live-region` (an aria-hidden, non-live
shadow mirror of the latest anchor-jump message; the spoken copy is appended to the shared
document-level polite sink only while the viewer and its composed ancestors are exposed to the
accessibility tree).

`error` is ordinary localized visible text, not a shadow live region. A fresh post-mount native,
playback, or unsafe-source failure appends the localized message to the document's pre-mounted
`[data-lr-live-region="assertive"]` sink. An already-unsafe initial `src` remains visible but does
not interrupt on mount; identical later failures append distinct children. `[part="base"]` remains
a named `role="region"` in the unsafe-source branch and across a safe-to-unsafe transition, so the
player does not lose its landmark or accessible name when its media is replaced by the error.

Every cue-level part above is rendered into the embedded `<lr-virtual-list>`'s own shadow root and
forwarded back out through `exportparts`, so `lr-av-player::part(cue)` and friends work from a
consumer stylesheet. The three cue states are separate part _names_ rather than attribute selectors,
because Shadow Parts forbids an attribute selector after `::part()` —
`::part(cue)[aria-current='true']` is invalid CSS, so use `::part(cue-current)`. The `aria-current`
and `data-*` attributes remain on each row for semantics and scripting.

**Themeable custom properties:** `--lr-av-player-transcript-height` (default
`var(--lr-size-16rem)` — block size of the transcript pane; forwarded to the embedded
`<lr-virtual-list>`'s own `--lr-virtual-list-height`). `--lr-av-player-marker-active-color` (default
`var(--lr-color-brand)`) — the outline of the `[part='timeline-marker']` matching
`activeHighlightId`, leaving the per-tone marker fills alone. It is an inline `var()` fallback at
the point of use rather than a `:host` declaration, so it can be set on the element or on any
ancestor — `::part(timeline-marker)[data-active]` is invalid CSS (Shadow Parts forbids an attribute
selector after `::part()`), so re-pointing the shared `--lr-color-brand` token was the only previous
lever.

Each `[part='timeline-marker']`'s own background is independently overridable per tone, the same
inline-`var()`-fallback pattern as `--lr-av-player-marker-active-color` above:
`--lr-av-player-marker-bg` (default `color-mix(in srgb, var(--lr-color-brand) 35%, transparent)`) —
no (or an unrecognized) `data-tone`; `--lr-av-player-marker-success-bg` (default `color-mix(in srgb,
var(--lr-color-success) 35%, transparent)`) — `data-tone="success"`;
`--lr-av-player-marker-warning-bg` (default `color-mix(in srgb, var(--lr-color-warning) 35%,
transparent)`) — `data-tone="warning"`; `--lr-av-player-marker-danger-bg` (default `color-mix(in
srgb, var(--lr-color-danger) 35%, transparent)`) — `data-tone="danger"`; and
`--lr-av-player-marker-neutral-bg` (default `color-mix(in srgb, var(--lr-color-text) 25%,
transparent)`) — `data-tone="neutral"`. Each can be set on the element or on any ancestor without
hijacking the shared `--lr-color-success`/`-warning`/`-danger`/`-brand`/`-text` tokens used
elsewhere in the theme. `--lr-av-player-marker-fill` is the resting fill a marker actually renders,
resolved per tone from those `-bg` knobs; its hover and pressed states are color mixes taken from
that value, so setting it directly retints all three states of one marker at once — retint a whole
tone through the matching `-bg` knob instead.

Three further cue-state properties tint the transcript: `--lr-av-player-cue-hover-bg` (default
`var(--lr-color-brand-quiet)`) is the background of a hovered `cue` row, and its pressed state is a
color mix taken from that same value rather than a separate knob, so retuning it keeps its pressed
step too. `--lr-av-player-cue-current-bg` (default `var(--lr-color-brand-quiet)`) is the background
of the `cue-current` row the playhead is inside, and `--lr-av-player-cue-active-match-color`
(default `var(--lr-color-warning)`) is the outline of the `cue-active-match` row, leaving the other
matches' dashed outline on the shared warning token. All three are inline `var()` fallbacks at the
point of use rather than `:host` declarations, so any of them can be set on the element or on any
ancestor.

**RTL behavior:** surrounding controls follow the inherited direction, but the elapsed-media axis
on `[part='timeline']` stays physical left-to-right. ArrowLeft rewinds and ArrowRight advances in
both LTR and RTL.
