<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.
srcandposterfollow <Image>: a rooted path is a file instatic/, checked at build time; a URL is streamed; acontent://from a picker or a $folder passes through.bind:pausedandbind:currentTimeare two-way.currentTimeis fractional seconds, and assigning to it seeks.playbackRateis a number;1.5plays at one and a half times.headerstakes the same map $fetch does, for media behind a credential:headers={mapOf("Authorization" to "Bearer $token")}.onEndedtakes a lambda.onErrortakes(message) => …or() => …and fires for any failed load: a missing header, a bad URL, a codec, the network.<Video>addsfit, which is CSS'sobject-fitwith the same three names as<Image>:cover,contain,fill.
<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
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>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
srcrules, andfit - $folder — a user-chosen directory to play from
- <Document> — markup shown as authored
- @store — where a playlist's index lives