Skip to content

MediaPlayer

Data Display

Native video/audio playback with captions, posters, and source-error feedback.

Local MP4 with captionsOriginal WAV cuePoster and metadata preloadEmpty/failure states
Open preview

Responsive viewport — resize the window to explore. Direction and density apply only to this preview. Theme applies to the docs too.

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
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.

MediaPlayer props
PropType / DefaultDescription
labelstring

Default: required

Visible media name.
sourcesMediaSource[]

Default: required

Source URLs and MIME types in preference order.
kindvideo | audio

Default: video

Native element type.
posterstring

Default: —

Video poster URL.
captionsMediaCaption[]

Default: []

VTT src, language, label, and optional default flag.
preloadnone | metadata | auto

Default: metadata

Browser preload hint; it does not enable autoplay.
classNamestring

Default: —

Extend the container with semantic theme utilities.

Build with these too