MediaPlayer
Data DisplayNative video/audio playback with captions, posters, and source-error feedback.
Responsive viewport — resize the window to explore. Direction and density apply only to this preview. Theme applies to the docs too.
"use client";
import { MediaPlayer } from "@/components/ui/media-player";
export default function MediaPlayerExample() {
return (
<div className="w-full max-w-xl space-y-7">
<MediaPlayer
label="Coastal study · original four-second film"
sources={[
{
src: "/media/component-demos/coastal-study.mp4",
type: "video/mp4",
},
]}
poster="/images/component-demos/coast.svg"
captions={[
{
src: "/media/component-demos/coastal-study.en.vtt",
language: "en",
label: "English",
default: true,
},
]}
/>
<MediaPlayer
label="Studio cue · original audio"
kind="audio"
sources={[
{ src: "/media/component-demos/studio-cue.wav", type: "audio/wav" },
]}
/>
<p className="text-xs text-muted-foreground">
Local demo files with captions. The audio is a short, soft synthesized
tone without speech. Use the browser controls to play, pause, seek, and
change volume.
</p>
</div>
);
}Usage
Provide local or trusted media sources with accurate MIME types. The native browser controls handle play, pause, seeking, volume, and supported full-screen behavior. Video supports posters and caption tracks; audio uses the native audio player. No autoplay is enabled. Supply captions for speech and appropriate transcripts or descriptions for the content. The example uses an original four-second illustration film, a soft synthesized tone, and a local VTT caption file. Codec support depends on the browser; failures have explicit feedback.
import { MediaPlayer } from "@/components/ui/media-player";Shipped as editable source in src/components/ui/media-player.tsx in both apps. Copy the implementation below when moving it to another project. Copy the complete example from Source code for all imports and state. Add "use client" when using hooks or event handlers. See installation and theming for project setup.
Component source
The complete implementation is included in both apps. It uses the React, Radix, Lucide, and shared-core dependencies already installed in Apex. Keep your project’s semantic theme tokens when reusing it.
src/components/ui/media-player.tsx
"use client";
import { useEffect, useId, useRef, useState } from "react";
import { cn } from "@dashboardpack/core/lib/utils";
export interface MediaSource {
src: string;
type: string;
}
export interface MediaCaption {
src: string;
language: string;
label: string;
default?: boolean;
}
export interface MediaPlayerProps {
label: string;
sources: MediaSource[];
kind?: "video" | "audio";
poster?: string;
captions?: MediaCaption[];
preload?: "none" | "metadata" | "auto";
className?: string;
}
export function MediaPlayer({
label,
sources,
kind = "video",
poster,
captions = [],
preload = "metadata",
className,
}: MediaPlayerProps) {
const id = useId();
const mediaRef = useRef<HTMLMediaElement | null>(null);
const sourceKey = sources
.map((source) => `${source.type}:${source.src}`)
.join("|");
const [failed, setFailed] = useState<string | null>(null);
const [failures, setFailures] = useState<{ key: string; urls: string[] }>({
key: "",
urls: [],
});
useEffect(() => {
const element = mediaRef.current;
if (!element) return;
const markFailed = () => setFailed(sourceKey);
const sourceElements = [...element.querySelectorAll("source")];
const listeners = sourceElements.map((source, index) => {
const listener = () =>
setFailures((previous) => ({
key: sourceKey,
urls: [
...new Set([
...(previous.key === sourceKey ? previous.urls : []),
String(index),
]),
],
}));
source.addEventListener("error", listener);
return () => source.removeEventListener("error", listener);
});
element.addEventListener("error", markFailed);
// Register native source errors before loading: prerendered media may fail before hydration.
element.load();
return () => {
element.removeEventListener("error", markFailed);
listeners.forEach((remove) => remove());
};
}, [sourceKey, kind]);
const media = sources.map((source, index) => (
<source
key={`${source.src}:${source.type}:${index}`}
src={source.src}
type={source.type}
/>
));
return (
<figure className={cn("min-w-0 space-y-3", className)}>
<figcaption id={id} className="text-sm font-medium">
{label}
</figcaption>
{sources.length ? (
kind === "audio" ? (
<audio
key={sourceKey}
ref={(element) => {
mediaRef.current = element;
}}
controls
preload={preload}
aria-labelledby={id}
onError={() => setFailed(sourceKey)}
className="w-full"
>
{media}Your browser does not support audio playback.
</audio>
) : (
<video
key={sourceKey}
ref={(element) => {
mediaRef.current = element;
}}
controls
playsInline
preload={preload}
poster={poster}
aria-labelledby={id}
onError={() => setFailed(sourceKey)}
className="aspect-video w-full rounded-xl border bg-black"
>
{media}
{captions.map((track) => (
<track
key={`${track.language}-${track.src}`}
kind="captions"
src={track.src}
srcLang={track.language}
label={track.label}
default={track.default}
/>
))}
Your browser does not support video playback.
</video>
)
) : (
<p className="rounded-xl border border-dashed p-5 text-sm text-muted-foreground">
No media sources available.
</p>
)}
{(failed === sourceKey ||
(failures.key === sourceKey &&
failures.urls.length === sources.length)) &&
sources.length > 0 && (
<p role="alert" className="text-sm text-destructive">
This media could not be played. Check the file format and
connection.
</p>
)}
<p className="text-xs text-muted-foreground">
Playback uses native browser controls. Media never starts automatically.
</p>
</figure>
);
}API reference
Common props and defaults. Slot-specific props are named below; native elements also accept their standard HTML attributes. The component TypeScript definitions are the complete type reference.
| Prop | Type / Default | Description |
|---|---|---|
| label | stringDefault: required | Visible media name. |
| sources | MediaSource[]Default: required | Source URLs and MIME types in preference order. |
| kind | video | audioDefault: video | Native element type. |
| poster | stringDefault: — | Video poster URL. |
| captions | MediaCaption[]Default: [] | VTT src, language, label, and optional default flag. |
| preload | none | metadata | autoDefault: metadata | Browser preload hint; it does not enable autoplay. |
| className | stringDefault: — | Extend the container with semantic theme utilities. |