Headless React video player built on the native HTMLVideoElement. Includes play/pause, volume, mute, scrub timeline, playback rate, picture-in-picture, captions, and a fallback poster. Loom / YouTube / Vimeo URLs are auto-converted to their embed iframe.

Paste a direct MP4 / WebM URL, a share link from a service that publishes og:video (Screen Studio, Wistia, etc.), or a Loom / YouTube / Vimeo embed URL. Press Enter or blur the field to load it.
npx shadcn@latest add https://sdk-components.thesqd.com/r/video-player.jsonnpm packages
lucide-react— Play / Pause / Volume / Caption / Maximize / Speed / PIP icons.
Squad-SDK primitives
The CLI install pulls these automatically. Manual installs need to add them first.
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
const META_RE =
/<meta\s+(?:[^>]*?\s+)?property=["']([^"']+)["'][^>]*?content=["']([^"']+)["'][^>]*>/gi;
const META_RE_NAME =
/<meta\s+(?:[^>]*?\s+)?name=["']([^"']+)["'][^>]*?content=["']([^"']+)["'][^>]*>/gi;
function decode(html: string): string {
return html
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(///gi, "/");
}
function parseOg(html: string) {
const map = new Map<string, string>();
let match: RegExpExecArray | null;
for (const re of [META_RE, META_RE_NAME]) {
re.lastIndex = 0;
while ((match = re.exec(html)) !== null) {
map.set(match[1].toLowerCase(), decode(match[2]));
}
}
return {
src:
map.get("og:video:secure_url") ??
map.get("og:video") ??
map.get("twitter:player:stream") ??
null,
poster: map.get("og:image:secure_url") ?? map.get("og:image") ?? null,
};
}
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const url = searchParams.get("url");
if (!url) return NextResponse.json({ error: "Missing url" }, { status: 400 });
try {
const res = await fetch(url, {
redirect: "follow",
headers: { "user-agent": "Mozilla/5.0 (squad-ui og-video)" },
cache: "no-store",
});
if (!res.ok) {
return NextResponse.json({ error: `Upstream ${res.status}` }, { status: 502 });
}
const payload = parseOg(await res.text());
if (!payload.src) {
return NextResponse.json({ error: "No og:video tag" }, { status: 404 });
}
return NextResponse.json(payload, {
headers: { "cache-control": "public, max-age=300, s-maxage=300" },
});
} catch (err) {
return NextResponse.json(
{ error: err instanceof Error ? err.message : "fetch failed" },
{ status: 502 },
);
}
}
// Wire the API route into your player so non-direct URLs resolve to MP4.
async function resolveOgVideo(rawSrc: string) {
const res = await fetch(`/api/og-video?url=${encodeURIComponent(rawSrc)}`);
if (!res.ok) throw new Error(`Resolver failed (${res.status})`);
return (await res.json()) as { src: string; poster?: string };
}
<VideoPlayer src={anySharePageUrl} resolveSrc={resolveOgVideo} />import { VideoPlayer } from "@/components/ui/video-player";import { VideoPlayer } from "@/components/ui/video-player";
export function Example() {
return (
<VideoPlayer
src="https://example.com/video.mp4"
poster="https://example.com/poster.jpg"
/>
);
}VideoPlayer (single component — pass content as props)
├── src="" (URL of the video — mp4, webm, or HLS in Safari)
├── poster="" (fallback image shown before play and on errors)
├── tracks={[{ src, label, srclang, default, kind }]} (WebVTT captions)
├── playbackRates={[0.5, 1, 1.5, 2]} (speed menu options)
├── aspectRatio="16/9"
└── ...standard <video> attrs (autoPlay, loop, muted, …)
Built-in controls:
- Big center Play/Pause button when paused
- Bottom bar: Play/Pause, mute, volume slider, time, speed menu, captions menu, picture-in-picture, fullscreen
- Click the video surface to toggle play/pause
- Seek by scrubbing the timeline at the top of the controls bar
Paste a direct MP4 / WebM URL, a share link from a service that publishes og:video (Screen Studio, Wistia, etc.), or a Loom / YouTube / Vimeo embed URL. Press Enter or blur the field to load it.
Loom embed / share URLs are auto-converted to the embed iframe so the player just works for Loom-hosted videos. YouTube and Vimeo links are handled the same way.

Screen Studio (and any service that publishes its video URL via og:video) plays natively. The component calls resolveSrc, which hits a Next.js route that fetches the share page server-side and returns the underlying MP4.
| Prop | Type | Default | Description |
|---|---|---|---|
src | string | — | Video URL. Direct MP4 / WebM / HLS plays in the native controls bar. Loom, YouTube, and Vimeo URLs auto-convert to the host's embed iframe. Other share URLs go through `resolveSrc` if provided. |
poster | string | — | Fallback image shown before playback and on load failures. |
tracks | VideoPlayerTrack[] | — | WebVTT subtitle / caption tracks. Each `{ src, label, srclang?, default?, kind? }` becomes a `<track>` plus a captions-menu entry. |
resolveSrc | (rawSrc: string) => Promise<string | { src: string; poster?: string }> | — | Async resolver for share pages that don't expose an embed iframe (Screen Studio, Wistia, etc.). Pair it with a server route that fetches the page and parses `og:video` to return the underlying MP4. |
fallbackPoster | string | Squad brand badge | Last-resort poster shown when no `poster` is set, no resolver poster came back, and the first-frame capture failed (e.g. cross-origin without CORS). |
posterFromFirstFrame | boolean | true | Capture the first decoded frame and use it as a fallback poster when `poster` is omitted. Silently skipped for cross-origin videos without CORS — pair with `crossOrigin="anonymous"` to enable it on those. |
crossOrigin | "anonymous" | "use-credentials" | — | Forwarded to the underlying `<video>` element. Set this when you control the CDN's CORS headers and want first-frame poster capture to work on cross-origin sources. |
playbackRates | number[] | [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] | Options shown in the speed menu. |
aspectRatio | string | "16/9" | CSS `aspect-ratio` of the player surface. |
className | string | — | Override or extend the resolved root classes. |
...rest | VideoHTMLAttributes | — | Forwarded to the underlying `<video>` element — `autoPlay`, `loop`, `muted`, `crossOrigin`, etc. |