use-media-controls
Two-way reactive controls for a <video> or <audio> element.
| Package | @vizejs/composable/use-media-controls |
| Own the source | vize lib pull composable:use-media-controls |
| Runtime exports | useMediaControls |
| Gzip budget | 3072 B |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/composable
Import the entry directly. Call lifecycle-bound utilities inside <script setup> so cleanup follows the component scope; consult the runtime contract for this entry's SSR and hydration behavior.
import { useMediaControls } from "@vizejs/composable/use-media-controls";
Try it
Build custom audio controls
Drive a native audio or video element through refs for play state, time, volume and speed.
What to observe: Play starts the audio and the time advances; Jump to 0.5 s seeks; Speed toggles between 1× and 2×.
Every control is a writable ref bound to the element. src accepts a URL, a list of sources or a getter, as here with an object URL.
Compare the initial and interacted states


Copy the example
Save this complete SFC in your Vue project. Its public imports and source are the same ones executed in the preview.
<script setup lang="ts">
import { ref } from "vue";
import { useMediaControls } from "@vizejs/composable/use-media-controls";
import { useMounted } from "@vizejs/composable/use-mounted";
import { useObjectUrl } from "@vizejs/composable/use-object-url";
// A one-second silent WAV generated in the browser stands in for your audio file.
function silentWav(seconds: number, rate = 8000): Blob {
const samples = seconds * rate;
const view = new DataView(new ArrayBuffer(44 + samples));
const text = (offset: number, value: string) =>
[...value].forEach((char, index) => view.setUint8(offset + index, char.charCodeAt(0)));
text(0, "RIFF");
view.setUint32(4, 36 + samples, true);
text(8, "WAVEfmt ");
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
view.setUint16(22, 1, true);
view.setUint32(24, rate, true);
view.setUint32(28, rate, true);
view.setUint16(32, 1, true);
view.setUint16(34, 8, true);
text(36, "data");
view.setUint32(40, samples, true);
for (let index = 0; index < samples; index += 1) view.setUint8(44 + index, 128);
return new Blob([view.buffer], { type: "audio/wav" });
}
const audio = ref<HTMLAudioElement | null>(null);
// The generated file only exists in the browser, so create the element after hydration.
const mounted = useMounted();
const captions = `data:text/vtt,${encodeURIComponent("WEBVTT\n\n00:00.000 --> 00:01.000\n(silence)\n")}`;
const source = useObjectUrl(silentWav(1));
const { playing, currentTime, duration, muted, rate } = useMediaControls(audio, {
src: () => source.value ?? "",
});
</script>
<template>
<div class="composable-example">
<p>Build custom audio controls on top of a native media element.</p>
<audio v-if="mounted" ref="audio" preload="metadata">
<track kind="captions" srclang="en" label="English" :src="captions" />
</audio>
<div class="example-actions">
<button type="button" @click="() => (playing = !playing)">
{{ playing ? "Pause" : "Play" }}
</button>
<button type="button" @click="() => (muted = !muted)">{{ muted ? "Unmute" : "Mute" }}</button>
<button type="button" @click="() => (rate = rate === 1 ? 2 : 1)">Speed {{ rate }}×</button>
<button type="button" @click="() => (currentTime = 0.5)">Jump to 0.5 s</button>
</div>
<output aria-live="off">{{ currentTime.toFixed(1) }} / {{ duration.toFixed(1) }} s</output>
</div>
</template>
Runtime contract
| Utility | Category | Stability | SSR | Hydration | Cleanup | Targets | Host globals | Uses |
|---|---|---|---|---|---|---|---|---|
useMediaControls |
media | experimental | safe | stable | reactive-scope | web, desktop | window |
— |
API
useMediaControls
Two-way reactive controls for a <video> or <audio> element. playing, currentTime, volume, muted, and rate mirror the element and are writable: assigning them drives the element, while element events update them without echoing the change back. The element is the source of truth whenever a new target attaches. Read-only state covers duration, buffering, seeking, waiting, stalls, text tracks, and picture-in-picture. A rejected play() (blocked autoplay) resets playing and is exposed through error. Listeners are removed when the target changes and when the owning reactive scope stops. Server rendering: the template ref is empty, so every value keeps its neutral default (paused, time 0, volume 1) and nothing is touched.
function useMediaControls( target: MaybeRefOrGetter<MediaElementLike | null | undefined>, options: UseMediaControlsOptions = {}, ): MediaControls
const video = useTemplateRef<HTMLVideoElement>("video");
const { playing, currentTime, duration } = useMediaControls(video, {
src: [{ src: "/clip.webm", type: "video/webm" }, { src: "/clip.mp4", type: "video/mp4" }],
});
Types
TimeRangesLike
Minimal TimeRanges.
| Member | Type | Description |
|---|---|---|
length |
number |
Number of ranges. |
start |
(index: number) => number |
Start of range index in seconds. |
end |
(index: number) => number |
End of range index in seconds. |
TextTrackLike
Minimal TextTrack.
| Member | Type | Description |
|---|---|---|
id |
string |
Track identifier. |
kind |
string |
"subtitles", "captions", "descriptions", "chapters", or "metadata". |
label |
string |
Human-readable label. |
language |
string |
BCP 47 language tag. |
mode |
string |
"disabled", "hidden", or "showing". |
TextTrackListLike
Minimal TextTrackList.
| Member | Type | Description |
|---|---|---|
length |
number |
Number of tracks. |
MediaElementLike
Minimal HTMLMediaElement (<video> or <audio>).
| Member | Type | Description |
|---|---|---|
currentTime |
number |
Playback position in seconds. |
volume |
number |
Volume from 0 to 1. |
muted |
boolean |
Whether audio is muted. |
playbackRate |
number |
Playback speed multiplier. |
src |
string |
Media source URL. |
duration |
number |
Duration in seconds (NaN until metadata loaded). |
paused |
boolean |
Whether playback is paused. |
ended |
boolean |
Whether playback reached the end. |
seeking |
boolean |
Whether the element is seeking. |
buffered |
TimeRangesLike |
Buffered time ranges. |
textTracks? |
TextTrackListLike |
Text tracks (<track> children). |
play |
() => Promise<void> |
Start playback; rejects when autoplay is blocked. |
pause |
() => void |
Pause playback. |
canPlayType? |
(type: string) => string |
Whether the element can play type ("", "maybe", "probably"). |
requestPictureInPicture? |
() => Promise<unknown> |
Enter picture-in-picture (video only). |
PictureInPictureDocumentLike
Document capabilities used for picture-in-picture.
| Member | Type | Description |
|---|---|---|
pictureInPictureEnabled? |
boolean |
Whether picture-in-picture is allowed. |
pictureInPictureElement? |
unknown |
Element currently in picture-in-picture. |
exitPictureInPicture? |
() => Promise<void> |
Leave picture-in-picture. |
MediaSourceCandidate
One candidate media source.
| Member | Type | Description |
|---|---|---|
src |
string |
Source URL. |
type? |
string |
MIME type used to test playability with canPlayType. |
MediaTextTrack
Summary of one text track.
| Member | Type | Description |
|---|---|---|
index |
number |
Index in the element's textTracks. |
id |
string |
Track identifier. |
kind |
string |
Track kind. |
label |
string |
Human-readable label. |
language |
string |
BCP 47 language tag. |
mode |
string |
Current mode. |
UseMediaControlsOptions
Options for useMediaControls.
| Member | Type | Description |
|---|---|---|
src? |
MaybeRefOrGetter<string | readonly MediaSourceCandidate[] | undefined> |
Media source: a URL, or candidates of which the first playable one (per canPlayType) is used. Omit to leave the element's src untouched. |
document? |
MaybeRefOrGetter<PictureInPictureDocumentLike | null | undefined> |
Document used for picture-in-picture. |
MediaControls
Reactive state and actions returned by useMediaControls.
| Member | Type | Description |
|---|---|---|
playing |
Ref<boolean> |
Whether media is playing; assign to play or pause. |
currentTime |
Ref<number> |
Playback position in seconds; assign to seek. |
volume |
Ref<number> |
Volume from 0 to 1; assign to change it. |
muted |
Ref<boolean> |
Whether audio is muted; assign to toggle. |
rate |
Ref<number> |
Playback speed; assign to change it. |
duration |
Readonly<Ref<number>> |
Duration in seconds (0 until known). |
buffered |
Readonly<ShallowRef<readonly (readonly [number, number])[]>> |
Buffered ranges as [start, end] pairs. |
seeking |
Readonly<Ref<boolean>> |
Whether the element is seeking. |
waiting |
Readonly<Ref<boolean>> |
Whether playback waits for data. |
ended |
Readonly<Ref<boolean>> |
Whether playback reached the end. |
stalled |
Readonly<Ref<boolean>> |
Whether data delivery stalled. |
error |
Readonly<ShallowRef<unknown>> |
Most recent play() rejection (for example blocked autoplay). |
tracks |
Readonly<ShallowRef<readonly MediaTextTrack[]>> |
Text tracks of the element. |
selectedTrack |
Readonly<Ref<number>> |
Index of the showing text track, or -1. |
supportsPictureInPicture |
ComputedRef<boolean> |
Whether picture-in-picture can be requested for the element. |
isPictureInPicture |
Readonly<Ref<boolean>> |
Whether the element is in picture-in-picture. |
play |
() => Promise<boolean> |
Start playback. |
pause |
() => void |
Pause playback. |
selectTrack |
(index: number) => void |
Show text track index and hide the others; -1 hides all. |
togglePictureInPicture |
() => Promise<boolean> |
Enter or leave picture-in-picture. |