# @ferret/media: API

Audio sources and ambient zones, video surfaces (HLS/MP4 to texture) and HTML surfaces; recording, and frame-by-frame capture on simulated time to WebM or PNG.

48 exports. Generated from the source (`packages/media/src`), so it matches the code.

## Classes

### AmbientZones

*class* · `packages/media/src/ambient.ts:16`

```ts
class AmbientZones implements Service
```

Ambient sound by zone. `map({ Yard: 'wind', Hall: 'room-tone' })` names the sound each zone plays (sounds defined in the
AudioService, on the ambient bus); when the player's zone changes the old one fades out and the new one fades in.
A zone with no entry keeps what was playing; map it to `null` for silence.

- `readonly name: "ambient"`
- `constructor(readonly opts: AmbientOptions = {})`
- `init(world: World)`
- `map(table: Record<string, string | null>): this`
- `get playing(): { zone: string; sound: string | null } | null` — The zone whose ambience is playing and the sound.
- `dispose()`

### AudioPlayer

*class* · `packages/media/src/audio-player.ts:14`

```ts
class AudioPlayer extends MediaComponent<AudioPlayerParams>
```

A sound that plays, pauses and seeks, with a queue of tracks (next, previous, repeat, shuffle): a radio, a jukebox, the
music of a place. Streams from its source, so long tracks do not sit decoded in memory. Goes through the `music` bus by
default, positioned at the entity when `spatial` is given. Controlled through the `MediaPlayer` API and reports through the
`media:*` events; nothing here is bound to a key or a button. (Short sounds that fire and are done stay `AudioService.play`.)

- `audio: HTMLAudioElement`
- `onAttach()`
- `onDetach()`

### AudioService

*class* · `packages/media/src/audio.ts:49`

```ts
class AudioService implements Service
```

Sound for a game: a master gain and four buses (music, sfx, ambient, voice) with their own volumes, buffers decoded once,
sounds that sit in the world (a panner that follows a node, the listener follows the active camera). Browsers refuse
sound before the first user action, so the context is made and resumed then (`audio:unlocked`); loops asked for earlier
start at that moment, one-shots asked for earlier are dropped. `sound:play` events { name, entity? } play registered sounds.

- `readonly name: "audio"`
- `ctx: AudioContext | null`
- `constructor(readonly opts: AudioOptions = {})`
- `init(world: World)`
- `get unlocked(): boolean`
- `unlock(): void` — Make the context and resume it. Call from a user action (the service does it on the first key, click or touch).
- `volume(bus: Bus | 'master'): number`
- `setVolume(bus: Bus | 'master', v: number): void`
- `tap(): MediaStream | null` — What the player hears (after the master gain) as a MediaStream with one audio track, for a recording. Null until the context is running: a suspended context delivers no data, and a recorder given such a track writes nothing at all.
- `busNode(bus: Bus): AudioNode | null` — The bus node for routing other sources (a video's sound) through the same volume control.
- `define(name: string, def: SoundDef): this`
- `has(name: string): boolean`
- `load(url: string): Promise<AudioBuffer>` — Decode a sound file once.
- `play(nameOrUrl: string, o: PlayOptions = {}): SoundHandle` — Play a defined sound (by name) or a file (by url). Returns a handle at once; the sound starts when decoded.
- `update()` — Called once per rendered frame.
- `_bus(b: Bus): AudioNode`
- `_register(v: Voice)`
- `_unregister(v: Voice)`
- `get activeVoices(): number`
- `dispose()`

### HtmlScreen

*class* · `packages/media/src/html.ts:120`

```ts
class HtmlScreen extends Component<HtmlScreenParams>
```

A web page drawn onto a 3D plane, as a texture: the screen lies in the world like any mesh (hidden behind walls, seen from any angle, many
of them are cheap), but it is a picture: no scripts run and nothing can be clicked. For notice boards and posters. Make it again with
`refresh()` when the page changed. Events: `html:ready` { entity }, `html:error` { entity, message }.
The page is fetched and made self-contained (see `inlinePage`), then the browser draws it through an SVG <foreignObject>.

- `schema: { readonly url: { readonly type: "string"; }; readonly html: { readonly type: "string"; }; readonly base: { readonly type: "string"; }; readonly width: { readonly type: "number"; readonly min: 0.01; }; readonly height: { readonly type: "number"; readonly min: 0.01; }; readonly pxWidth: { readonly type: "number"; readonly min: 16; readonly max: 4096; }; readonly pxHeight: { readonly type: "number"; readonly min: 16; readonly max: 4096; }; readonly pixelsPerMetre: { readonly type: "number"; readonly min: 1; readonly max: 4096; }; readonly pixelRatio: { readonly type: "number"; readonly min: 0.25; readonly max: 4; }; readonly lit: { readonly type: "boolean"; }; }`
- `screen: BABYLON.Mesh`
- `texture: BABYLON.DynamicTexture`
- `state: 'loading' | 'ready' | 'error'`
- `warnings: string[]`
- `error: string`
- `onAttach()`
- `async refresh(): Promise<void>` — Fetch the page again and redraw it. Resolves when it is on the screen (or has failed: see `state`).
- `onDetach()`

### LampSounds

*class* · `packages/media/src/lamp-sounds.ts:34`

```ts
class LampSounds
```

Positional sounds for the lamps a light budget says are near the viewer (a torch's crackle): a small pool of looping `AudioPlayer` entities that
is moved from lamp to lamp. Only the nearest few lamps sound at a time (`LightBudgetDef.sounds.budget`), so a village of hundreds of torches costs
a handful of audio elements. A player that is let go is paused and kept for the next lamp; its Web Audio graph is built once, not per lamp.
Nothing is bound to a key; the sound goes through the AudioService (bus, master volume, panner that follows the entity) like every AudioPlayer.

- `dropped: number` — starts that found no free player (the pool is at `max`)
- `constructor(readonly world: World, readonly source: LampSoundSource, readonly opts: LampSoundsOptions = {})`
- `get active(): number[]` — the lamps that sound now
- `get players(): readonly Entity[]` — the players made so far
- `playerOf(i: number): AudioPlayer | undefined` — the player of lamp `i`, if it sounds
- `apply(start: readonly number[], stop: readonly number[])`
- `dispose()`

### MediaComponent

*class* · `packages/media/src/player.ts:81`

```ts
abstract class MediaComponent<P extends MediaPlayerParams = MediaPlayerParams> extends Component<P> implements MediaPlayer
```

Shared by the media components: owns an HTMLMediaElement, routes its sound through the AudioService (bus volume, master,
positional falloff), keeps a queue, and emits on `world.events`, always with { entity }:
`media:state` { state }, `media:play`, `media:pause`, `media:ended` (the queue ran out), `media:track` { index, src },
`media:error` { message }.

- `get state(): PlayerState`
- `get playing(): boolean`
- `get currentTime(): number` — seconds; `duration` is NaN until known and Infinity for a live stream
- `get duration(): number`
- `play(): void`
- `pause(): void`
- `toggle(): void`
- `stop(): void` — pause and go back to the start
- `seek(seconds: number): void`
- `get volume(): number` — 0..1
- `set volume(v: number)` — 0..1
- `get muted(): boolean`
- `set muted(m: boolean)`
- `get rate(): number` — playback speed, 1 = normal
- `set rate(r: number)` — playback speed, 1 = normal
- `get tracks(): readonly string[]`
- `get index(): number` — index into `tracks` of the one that is current, -1 for none
- `get track(): string | null`
- `get repeat(): Repeat`
- `set repeat(r: Repeat)`
- `get shuffle(): boolean`
- `set shuffle(s: boolean)`
- `async setTracks(tracks: string[], start = 0): Promise<void>` — replace the queue and make `start` current
- `async select(index: number): Promise<void>` — make this one current (it plays if `autoplay` or it was playing)
- `async next(): Promise<boolean>` — the next track; false when the queue is at its end (and repeat is not 'all')
- `async previous(): Promise<boolean>` — back to the one before when the current is near its start (true); otherwise back to its start (false)
- `onUpdate()`

### SceneRecorder

*class* · `packages/media/src/recorder.ts:136`

```ts
class SceneRecorder implements Service
```

Records the scene: the engine's canvas through `captureStream` and `MediaRecorder`, with the sound the player hears when the
AudioService is unlocked. Results as one file (MP4 where the browser writes it, else WebM with its Duration put in).
Events on `world.events`: `record:start` { mime, audio }, `record:stop` { mime, bytes, durationMs, audio }, `record:error` { message }.
Bound to no key or button. Sound cannot join a recording that is running: a locked audio context delivers nothing, and a
recorder given such a track writes an empty file, so a recording that started before the first user action has no sound.
With software rendering and encoding (the simulators, a headless browser) the first data comes about a second after the start,
so a recording shorter than that can be empty: `stop()` then rejects with a message saying so.

- `readonly name: "recorder"`
- `constructor(readonly opts: RecorderOptions = {})`
- `init(world: World)`
- `get state(): RecorderState`
- `get elapsedMs(): number` — milliseconds recorded so far, pauses left out
- `static get supported(): boolean` — Whether this browser can record a canvas at all.
- `start(o: { fps?: number; audio?: boolean; videoBitsPerSecond?: number } = {}): { mime: string; audio: boolean }`
- `pause()`
- `resume()`
- `stop(): Promise<Recording>` — Ends the recording and resolves with the file. Rejects when the recorder failed or delivered nothing.
- `dispose()`

### VideoScreen

*class* · `packages/media/src/video.ts:17`

```ts
class VideoScreen extends MediaComponent<VideoScreenParams>
```

A video on a flat screen in the world: a plane with the video as an emissive texture. The picture starts at once (muted:
browsers allow that); the sound begins on the first user action, through the AudioService's bus so the volume control and
the positional falloff apply. Controlled through the `MediaPlayer` API (play, pause, seek, volume, a queue of sources)
and reports through the `media:*` events; nothing here is bound to a key or a button.

- `video: HTMLVideoElement`
- `screen: BABYLON.Mesh`
- `texture: BABYLON.VideoTexture`
- `onAttach()`
- `onDetach()`

### WorldText

*class* · `packages/media/src/text.ts:31`

```ts
class WorldText extends Component<WorldTextParams>
```

Text in the world: a plane with the text drawn on it, readable from the entity's +Z side (the entity's forward is the way the text faces, as an Unreal TextRender faces
its +X). Unlit, with alpha; no collision. Draws with a canvas font (default Arial), so the letter shapes are the browser's.

- `schema: { readonly text: { readonly type: "string"; readonly required: true; }; readonly size: { readonly type: "number"; readonly min: 0.001; }; readonly xScale: { readonly type: "number"; readonly min: 0.01; }; readonly yScale: { readonly type: "number"; readonly min: 0.01; }; readonly color: { readonly type: "array"; }; readonly align: { readonly type: "string"; }; readonly valign: { readonly type: "string"; }; readonly font: { readonly type: "string"; }; readonly bold: { readonly type: "boolean"; }; }`
- `plane?: BABYLON.Mesh`
- `onAttach()`
- `onDetach()`

## Functions

### attachSource

*function* · `packages/media/src/source.ts:23`

```ts
async function attachSource(media: HTMLMediaElement, src: string, hlsUrl?: string, onError?: (m: string) => void): Promise<SourceResult>
```

Point a media element (video or audio) at a source. HLS plays natively where the browser can (Safari, iOS, WPE WebKit,
the Android WebView) and through hls.js elsewhere; any other file is just set as `src`.

### captureFrames

*function* · `packages/media/src/capture.ts:174`

```ts
async function captureFrames(engine: BABYLON.AbstractEngine, scene: BABYLON.Scene, world: World | null, o: CaptureOptions = {}): Promise<Capture>
```

Captures the scene frame by frame on simulated time, not on the wall clock: before every frame the simulation moves exactly
1/fps seconds (the world's fixed steps), and the engine's frame time reads 1/fps, so animations, physics and per-frame code all
see the same steady clock however slowly a frame is drawn. Nothing is dropped and a loop closes exactly. The render loop is
held for the length of the capture and given back after; the canvas is drawn at `supersample` times the size and scaled down.
Video is encoded in the browser (WebCodecs) into WebM; pictures come as PNG. The scene's sound is not captured.

### captureSupported

*function* · `packages/media/src/capture.ts:152`

```ts
function captureSupported(): { webm: boolean; png: boolean }
```

Whether this browser can capture video files (WebCodecs); pictures need only a canvas.

### defaultMimeTypes

*function* · `packages/media/src/recorder.ts:35`

```ts
function defaultMimeTypes(audio: boolean): string[]
```

MP4 first (it plays and seeks everywhere), then WebM; with sound the codecs are named so the audio is not dropped.

### drawPage

*function* · `packages/media/src/html.ts:104`

```ts
async function drawPage(xhtml: string, ctx: CanvasRenderingContext2D, viewport: { width: number; height: number }, size: { width: number; height: number }): Promise<void>
```

Draw a self-contained page into a canvas context at the given texture size. Throws when the browser refuses (it taints or fails).

### inlinePage

*function* · `packages/media/src/html.ts:42`

```ts
async function inlinePage(html: string, base: string, get: HtmlFetch): Promise<{ xhtml: string; warnings: string[] }>
```

Make a page self-contained: stylesheets become <style>, `url(...)` in CSS and <img src> become data URIs, scripts, frames and media
are dropped. Returns the XHTML of the page and the problems (a resource that could not be fetched is left out, not fatal).

### loadHls

*function* · `packages/media/src/source.ts:5`

```ts
function loadHls(url = ((globalThis as any).FERRET_LIB ?? 'lib/ferret/') + 'hls/hls.min.js'): Promise<HlsLike | null>
```

Load hls.js once from a script URL (it sets the global `Hls`). Resolves null if it cannot be loaded.

### pageSvg

*function* · `packages/media/src/html.ts:99`

```ts
function pageSvg(xhtml: string, width: number, height: number): string
```

The page inside an SVG image of the viewport's size (the browser lays it out in a <foreignObject>).

### pickMime

*function* · `packages/media/src/recorder.ts:42`

```ts
function pickMime(types: string[], isSupported: (m: string) => boolean): string | undefined
```

The first of `types` that `isSupported` accepts, or undefined (then the browser picks its own).

### setWebmDuration

*function* · `packages/media/src/recorder.ts:76`

```ts
function setWebmDuration(data: Uint8Array, durationMs: number): Uint8Array
```

MediaRecorder writes WebM with no Duration, so players show a nonsense length and cannot seek. Puts the Duration element into
the Segment's Info. Returns the bytes unchanged when the file is not a WebM this can read, or already has a Duration.

### spatialFromUnreal

*function* · `packages/media/src/lamp-sounds.ts:103`

```ts
function spatialFromUnreal(att: { attenuation_shape_extents?: number[]; falloff_distance?: number; distance_algorithm?: string; attenuate?: boolean }): { min: number; max: number; exact: boolean } | undefined
```

Unreal's sound attenuation (Linear distance algorithm, sphere shape) as the metres of `spatial`: full volume inside the inner radius
(`attenuation_shape_extents.x`), silent after the falloff distance beyond it. Centimetres in, metres out. The panner falls off linearly,
so another algorithm (Logarithmic, Inverse) is only approximated (`exact: false`). Undefined when the sound does not attenuate.

### textOffset

*function* · `packages/media/src/text.ts:21`

```ts
function textOffset(w: number, h: number, align: WorldTextParams['align'] = 'left', valign: WorldTextParams['valign'] = 'bottom'): [number, number]
```

Where a block of text of the given measured size sits so that the anchor is at the entity's origin: the plane's centre in the reader's frame (x to the reader's right, y up).

### writeWebm

*function* · `packages/media/src/capture.ts:56`

```ts
function writeWebm(frames: EncodedFrame[], t: WebmTrack): Uint8Array
```

One WebM file from encoded video frames: header, Info with the Duration, one track, the Cues (before the clusters, so a player can
seek at once), and a cluster at every key frame (and at least every 30 s). The first frame must be a key frame.

## Interfaces

### AmbientOptions

*interface* · `packages/media/src/ambient.ts:4`

```ts
interface AmbientOptions
```

- `tag?: string` — the zones' tag for the listener: the entity whose zone picks the ambience (default "player")
- `fade?: number` — seconds of cross-fade between ambiences (default 1.5)

### AudioOptions

*interface* · `packages/media/src/audio.ts:32`

```ts
interface AudioOptions
```

- `createContext?: () => AudioContext` — make the context (tests pass a fake; default `new AudioContext()`)
- `gestureTarget?: EventTarget | false` — where to wait for the first user action; false = do not listen (the game calls `unlock()` itself)
- `fetch?: (url: string) => Promise<{ ok: boolean; arrayBuffer(): Promise<ArrayBuffer> }>` — replace fetch (tests, a Platform file API)

### AudioPlayerParams

*interface* · `packages/media/src/audio-player.ts:3`

```ts
interface AudioPlayerParams extends MediaPlayerParams
```

- `audio?: HTMLAudioElement` — an audio element you made (a MediaStream, a test double)

### Capture

*interface* · `packages/media/src/capture.ts:127`

```ts
interface Capture
```

- `format: 'webm' | 'png'`
- `mime: string` — e.g. `video/webm;codecs=vp9`, or `image/png`
- `frames: number`
- `fps: number`
- `width: number`
- `height: number`
- `durationMs: number` — simulated time covered, ms (frames / fps)
- `blob?: Blob` — the video (webm)
- `pngs?: Blob[]` — the pictures (png without `onFrame`)
- `aborted: boolean` — true when `signal` stopped it early

### CaptureOptions

*interface* · `packages/media/src/capture.ts:91`

```ts
interface CaptureOptions
```

- `fps?: number` — frames per second of the result, and the simulated time between frames (default 30)
- `seconds?: number` — how long to capture, simulated seconds (default 1); or `frames`
- `frames?: number`
- `width?: number` — size of the result (default: the canvas's render size)
- `height?: number`
- `supersample?: number` — render at this many times the size and scale down: smooth edges and fine detail (1 to 4, default 1)
- `format?: 'webm' | 'png'` — `webm`: one video file (WebCodecs); `png`: a picture per frame (default webm)
- `codecs?: ('vp9' | 'vp8' | 'av1')[]` — video codec, the first the browser encodes is used (default vp9, then vp8)
- `bitrate?: number` — bits per second (default about 0.12 bit per pixel and frame)
- `keyEvery?: number` — a key frame every this many frames (default 2 s of frames)
- `advance?: (dt: number) => void` — move the simulation by dt seconds (default: the world's `advance`, which runs its fixed steps)
- `render?: () => void` — draw one frame (default: `scene.render()`)
- `beforeFrame?: (i: number, t: number) => void` — called before frame i is advanced and drawn, at simulated time t since the start (a turntable turns the camera here)
- `onFrame?: (i: number, png: Blob) => void | Promise<void>` — with `png`: each picture as it is made (else they are returned together)
- `signal?: AbortSignal` — stops the capture between two frames; what was made so far is returned
- `grab?: (canvas: HTMLCanvasElement, width: number, height: number) => CanvasImageSource & { convertToBlob?: (o: { type: string }) => Promise<Blob> }` — replace the browser parts (tests)
- `createEncoder?: (init: VideoEncoderInit) => VideoEncoder`
- `isConfigSupported?: (c: VideoEncoderConfig) => Promise<{ supported?: boolean }>`
- `makeFrame?: (source: CanvasImageSource, init: VideoFrameInit) => VideoFrame`

### EncodedFrame

*interface* · `packages/media/src/capture.ts:35`

```ts
interface EncodedFrame
```

- `data: Uint8Array`
- `timeMs: number` — presentation time of the frame, ms
- `key: boolean`

### HtmlScreenParams

*interface* · `packages/media/src/html.ts:6`

```ts
interface HtmlScreenParams
```

- `url?: string` — the page to show (its stylesheets, images and fonts are fetched too: they need CORS, or `fetch` that gets around it)
- `html?: string` — or the page as text; `base` then resolves its relative links
- `base?: string`
- `width?: number` — size of the screen in metres (default 4 x 2.25)
- `height?: number`
- `pxWidth?: number` — the page's window in CSS pixels: the page lays out as in a browser of exactly this size. Give both, or one (the other follows the screen's shape), or none (see `pixelsPerMetre`)
- `pxHeight?: number`
- `pixelsPerMetre?: number` — when the window size is not given: window = screen size in metres x this (default 256), the larger side capped at 2048 with the shape kept
- `pixelRatio?: number` — texture size = viewport x this (default 1; 2 for sharper text on a screen seen close)
- `fetch?: HtmlFetch` — replace `fetch`
- `lit?: boolean` — lit by the scene like a poster (default false: it glows like a screen)

### LampSoundsOptions

*interface* · `packages/media/src/lamp-sounds.ts:13`

```ts
interface LampSoundsOptions
```

- `spatial?: { min?: number; max?: number }` — the sound comes from the lamp: full volume inside `min` metres, silent beyond `max` (see `spatialFromUnreal`). Default 2 / 30
- `volume?: number` — 0..1, default 1 (Unreal: the cue's volume multiplier x the component's)
- `bus?: NonNullable<MediaPlayerParams['bus']>` — default 'ambient'
- `offset?: Vec3` — metres added to a lamp's position (Unreal: the audio component's offset from the actor)
- `max?: number` — how many players at most (default: no limit, but the source's own sound budget keeps it small); a start beyond it is dropped
- `makeAudio?: () => HTMLAudioElement | undefined` — the element a player plays through (a test double); default a new `Audio`

### LampSoundSource

*interface* · `packages/media/src/lamp-sounds.ts:8`

```ts
interface LampSoundSource
```

What `LampSounds` needs of a light budget (`LightBudget` from

- `def: { items: readonly { position: Vec3; sound?: string; id?: string }[] }`
- `onSounds?: (start: number[], stop: number[]) => void`

### MediaPlayer

*interface* · `packages/media/src/player.ts:12`

```ts
interface MediaPlayer
```

What a game controls. Every media component (a video screen, an audio player) has this same surface, and the engine binds
it to nothing: no key, button or gesture. A game calls it from its own code, from a stock behaviour's wiring or from its UI.

- `readonly state: PlayerState`
- `readonly playing: boolean`
- `readonly currentTime: number` — seconds; `duration` is NaN until known and Infinity for a live stream
- `readonly duration: number`
- `volume: number` — 0..1
- `muted: boolean`
- `rate: number` — playback speed, 1 = normal
- `play(): void`
- `pause(): void`
- `toggle(): void`
- `stop(): void` — pause and go back to the start
- `seek(seconds: number): void`
- `readonly tracks: readonly string[]`
- `readonly index: number` — index into `tracks` of the one that is current, -1 for none
- `readonly track: string | null`
- `repeat: Repeat`
- `shuffle: boolean`
- `setTracks(tracks: string[], start?: number): Promise<void>` — replace the queue and make `start` current
- `select(index: number): Promise<void>` — make this one current (it plays if `autoplay` or it was playing)
- `next(): Promise<boolean>` — the next track; false when the queue is at its end (and repeat is not 'all')
- `previous(): Promise<boolean>` — back to the one before when the current is near its start (true); otherwise back to its start (false)

### MediaPlayerParams

*interface* · `packages/media/src/player.ts:48`

```ts
interface MediaPlayerParams
```

- `src?: string` — one source: a file or an HLS playlist (.m3u8)
- `tracks?: string[]` — or a queue of them
- `start?: number` — index of the track to start with
- `repeat?: Repeat`
- `loop?: boolean` — shorthand for `repeat: 'one'`
- `shuffle?: boolean`
- `volume?: number` — 0..1 (default 1)
- `muted?: boolean`
- `rate?: number`
- `bus?: Bus` — the bus its sound goes through (video 'sfx', audio 'music')
- `spatial?: { min?: number; max?: number }` — the sound comes from the entity: louder near it. min/max distances in metres
- `hlsUrl?: string` — where hls.js is, for browsers that cannot play HLS themselves (default `hls/hls.min.js` under the library folder)
- `autoplay?: boolean` — start by itself (default true; the sound waits for the first user action)
- `random?: () => number` — replaces Math.random for the shuffle (a seeded one for tests, replays)

### PlayOptions

*interface* · `packages/media/src/audio.ts:15`

```ts
interface PlayOptions
```

- `at?: BABYLON.TransformNode | BABYLON.Vector3` — where the sound comes from; a node it follows. Without it the sound is not positioned (UI, music)
- `volume?: number`
- `loop?: boolean`
- `bus?: Bus`
- `fadeIn?: number` — seconds to fade in
- `spatial?: { min?: number; max?: number }`

### RecorderOptions

*interface* · `packages/media/src/recorder.ts:6`

```ts
interface RecorderOptions
```

- `canvas?: HTMLCanvasElement` — the canvas to record (default: the engine's rendering canvas)
- `fps?: number` — frames per second asked of the capture (default 30)
- `videoBitsPerSecond?: number` — video bit rate (default: the browser's)
- `audio?: boolean` — add the sound the player hears when the audio is unlocked (default true)
- `mimeTypes?: string[]` — formats in order of preference; the first the browser can write is used
- `timeslice?: number` — ask for a data chunk this often, ms (default 1000): a long recording is not held back in the encoder
- `createRecorder?: (stream: MediaStream, o: { mimeType?: string; videoBitsPerSecond?: number }) => MediaRecorder` — replace the browser's MediaRecorder (tests)
- `isTypeSupported?: (mime: string) => boolean`

### Recording

*interface* · `packages/media/src/recorder.ts:24`

```ts
interface Recording
```

- `blob: Blob`
- `mime: string` — the container and codecs the file was written with, e.g. `video/mp4;codecs=avc1.42E01E`
- `durationMs: number`
- `bytes: number`
- `audio: boolean` — false when the audio was still locked at the start (the file has no sound) or not asked for

### SoundDef

*interface* · `packages/media/src/audio.ts:5`

```ts
interface SoundDef
```

- `url: string`
- `bus?: Bus`
- `volume?: number` — 0..1, default 1
- `loop?: boolean`
- `spatial?: { min?: number; max?: number }` — spatial: louder near the source; `min` is the distance of full volume, `max` where it is silent (metres)

### SoundHandle

*interface* · `packages/media/src/audio.ts:26`

```ts
interface SoundHandle
```

- `readonly playing: boolean`
- `setVolume(v: number, fade?: number): void`
- `stop(fade?: number): void`

### SourceResult

*interface* · `packages/media/src/source.ts:17`

```ts
interface SourceResult
```

- `mode: 'native' | 'hls.js' | 'none'`
- `destroy(): void`

### VideoScreenParams

*interface* · `packages/media/src/video.ts:3`

```ts
interface VideoScreenParams extends MediaPlayerParams
```

- `video?: HTMLVideoElement` — or a video element you made (a MediaStream, a canvas capture)
- `width?: number` — size of the screen in metres (default 4 x 2.25)
- `height?: number`

### WebmTrack

*interface* · `packages/media/src/capture.ts:42`

```ts
interface WebmTrack
```

- `codec: 'V_VP9' | 'V_VP8' | 'V_AV1'` — Matroska codec id: V_VP9, V_VP8 or V_AV1
- `width: number`
- `height: number`
- `fps: number`
- `durationMs?: number` — the length of the video, ms (default: the last frame's time plus one frame)

### WorldTextParams

*interface* · `packages/media/src/text.ts:3`

```ts
interface WorldTextParams
```

- `text: string` — the text; `\n` starts a new line
- `size?: number` — height of a letter's em box in metres (Unreal's TextRender `WorldSize` / 100)
- `xScale?: number` — extra scale across and along the lines (Unreal's `XScale`, `YScale`)
- `yScale?: number`
- `color?: number[]` — colour as [r, g, b] or [r, g, b, a], 0..1
- `align?: 'left' | 'center' | 'right'` — how the text sits on the entity's origin, as a reader sees it
- `valign?: 'top' | 'center' | 'bottom'`
- `font?: string`
- `bold?: boolean`

## Types

### Bus

*type* · `packages/media/src/audio.ts:3`

```ts
type Bus = 'music' | 'sfx' | 'ambient' | 'voice'
```

### HlsLike

*type* · `packages/media/src/source.ts:1`

```ts
type HlsLike = { isSupported(): boolean; new (cfg?: unknown): { loadSource(u: string): void; attachMedia(v: HTMLMediaElement): void; destroy(): void; on(ev: string, fn: (...a: any[]) => void): void } }
```

### HtmlFetch

*type* · `packages/media/src/html.ts:4`

```ts
type HtmlFetch = (url: string) => Promise<{ ok: boolean; status: number; text(): Promise<string>; blob(): Promise<Blob> }>
```

What `HtmlScreen` needs from `fetch` (a Platform file API or a proxy fits): the page and its pieces come through it.

### PlayerState

*type* · `packages/media/src/player.ts:5`

```ts
type PlayerState = 'idle' | 'loading' | 'playing' | 'paused' | 'ended' | 'error'
```

### RecorderState

*type* · `packages/media/src/recorder.ts:4`

```ts
type RecorderState = 'idle' | 'recording' | 'paused'
```

### Repeat

*type* · `packages/media/src/player.ts:6`

```ts
type Repeat = 'none' | 'one' | 'all'
```

