Documentation

<Audio> and <Video>

Sound and moving pictures, with HTML's element names and HTML's attribute names.

Why these exist

Playing media on Android means a player, a lifecycle that releases it, a data source that carries credentials, and, for sound that outlasts the screen, a service, a session, a manifest entry and permissions. <Audio> and <Video> are that, written as the elements a web developer already knows. Neither is a player UI: an <Audio> draws nothing and a <Video> draws only the picture.

<Audio src={chapter.url} playbackRate={1.5} onEnded={() => Player.next()} />
<Audio src={chapter.url} background title={chapter.name} artist={book.title} />
<Video src={post.url} poster={post.still} autoplay muted loop fit="cover" class="w-full aspect-video" />

Attributes

The names are HTML's: src, autoplay, loop, muted, poster, duration, playbackRate, onEnded, onError. A boolean attribute is bare, as in HTML.

  • src and poster follow <Image>: a rooted path is a file in static/, checked at build time; a URL is streamed; a content:// from a picker or a $folder passes through.
  • bind:paused and bind:currentTime are two-way. currentTime is fractional seconds, and assigning to it seeks.
  • playbackRate is a number; 1.5 plays at one and a half times.
  • headers takes the same map $fetch does, for media behind a credential: headers={mapOf("Authorization" to "Bearer $token")}.
  • onEnded takes a lambda. onError takes (message) => … or () => … and fires for any failed load: a missing header, a bad URL, a codec, the network.
  • <Video> adds fit, which is CSS's object-fit with the same three names as <Image>: cover, contain, fill.
An <Audio> draws nothing, so a class on one is a build error. Put the class on the element that draws. A missing src is also a build error.

It lives as long as its mount point

A player lives as long as the place it is written. In a screen it stops when you leave; in a hoisted layout it plays on while screens swap beneath it. That is the web's rule for <audio> in a page and in a persistent shell.

It is also why there is no $audio global and no queue. A playlist is an index the app keeps, in a @store(persist) if it should survive a restart, and onEnded advances it.

<Audio background>

<Audio src={chapter.url} background title={chapter.name} artist={book.title} />

background moves playback to a generated MediaSessionService: the sound survives the screen going off, the lockscreen shows a transport, and a headset's button reaches it. That one word derives the service, its manifest entry, three permissions and the media3 session artifact. An app that never writes it gets none of them.

It is an attribute, not an inference from the element being present: a service and a notification are things an app asks for. title and artist are the session metadata.

Why <Video> has no background and no controls

There is no background on a <Video> because twenty clips in a feed must not each own a service. There are no controls because a transport is markup beside it, bound with bind:paused and bind:currentTime, the way the first example above builds one.

One player exists per mounted <Video>. It is released when the element leaves composition and paused when the app loses the foreground.

media3 is added when used

The media3 dependency goes into the build only for a project that has an <Audio> or a <Video>. An app without one carries none of it.

Try it

Media.wh
import $lib.components.ui.Button

var paused = $state(true)
var position = $state(0.0)

<Column class="gap-8">
  <Audio src="https://example.com/chapter-3.mp3" bind:paused={paused} bind:currentTime={position} />
  <Text>{Math.round(position)} s</Text>
  <Button onClick={() => paused = !paused}>{paused ? "Play" : "Pause"}</Button>
  <Button onClick={() => position = 0.0}>Restart</Button>
</Column>
Kotlin waiting
Scroll into view to compile.

An <Audio> draws nothing; the transport is ordinary markup bound to it. bind:paused and bind:currentTime are state in both directions.

See Also

  • Images — the same src rules, and fit
  • $folder — a user-chosen directory to play from
  • <Document> — markup shown as authored
  • @store — where a playlist's index lives