@classytic/react-media
v0.1.2
Published
Production-ready media library for React. Video/audio playback, recording, upload, HLS streaming, captions.
Maintainers
Readme
@classytic/react-media
The engine for professional video in React.
Performance-critical, HLS-first, and 100% headless. Built for React 19.
Why another player?
Most React players are just wrappers arounddocument.querySelector('video')or heavy Web Components (like Vidstack/Mux).
This is a native React engine. It usesrequestAnimationFrameloops for 60fps UI updates,useSyncExternalStorefor tearing-free state, and React 19'suse()API for clean context consumption. It is built for developers building the next Netflix, not marketing sites hosting a YouTube embed.
✨ Features
- 🚀 React 19 Native: Built with
use(),useSyncExternalStore, and Server Components support. - ⚡ 60fps UI Sync: Custom
TimeStoreuses RAF loops to update progress bars/time displays without triggering React re-renders. - 🎨 100% Headless: No default styles. No "shadow DOM" fighting. You own every pixel.
- 📡 HLS First: Production-ready HLS support (via hls.js) baked in.
- 🎧 First-Class Audio: Full HLS audio support for podcasts and radio.
- 🧹 No Bloat: No YouTube/Vimeo/DASH adapters. Focused purely on direct file/stream playback.
📦 Installation
npm install @classytic/react-media⚡ Quick Start (Video)
import {
VideoController,
VideoRoot,
Video,
PlayButton,
TimeSlider,
} from "@classytic/react-media";
export default function Player() {
return (
// 1. Controller manages state (HLS, buffering, errors)
<VideoController src="https://stream.mux.com/YOUR_PLAYBACK_ID.m3u8">
{/* 2. Root handles layout & keyboard shortcuts */}
<VideoRoot className="relative aspect-video bg-black group">
{/* 3. The native video element (handled by controller) */}
<Video className="w-full h-full object-cover" />
{/* 4. Headless Controls (Style with Tailwind) */}
<div className="absolute bottom-0 w-full p-4 bg-gradient-to-t from-black/80">
<TimeSlider className="h-1 bg-white/30 cursor-pointer">
<div className="h-full bg-red-500 var-progress" />
</TimeSlider>
<div className="flex gap-4 mt-2">
<PlayButton className="text-white hover:text-red-500">
{({ isPlaying }) => (isPlaying ? "PAUSE" : "PLAY")}
</PlayButton>
</div>
</div>
</VideoRoot>
</VideoController>
);
}🎧 Quick Start (Audio)
Same high-performance engine, built for Audio.
import {
AudioController,
AudioPlayButton,
AudioProgress,
} from "@classytic/react-media/audio";
export default function PodcastPlayer() {
return (
<AudioController src="https://your-hls-stream/radio.m3u8">
<div className="p-4 rounded-xl bg-zinc-900 border border-zinc-800">
<div className="flex items-center gap-4">
<AudioPlayButton className="w-12 h-12 rounded-full bg-white text-black flex items-center justify-center">
{({ isPlaying }) => (isPlaying ? <PauseIcon /> : <PlayIcon />)}
</AudioPlayButton>
<AudioProgress className="flex-1 h-2 bg-zinc-800 rounded-full overflow-hidden">
{/* Direct DOM update for 60fps smoothness */}
{({ progress }) => (
<div
className="h-full bg-white transition-all duration-75"
style={{ width: `${progress}%` }}
/>
)}
</AudioProgress>
</div>
</div>
</AudioController>
);
}🏗 Architecture
We separate state into two stores to ensure the react render cycle never blocks playback UI.
| Store | Purpose | Update Frequency | Technology |
| ---------------- | -------------------------------------- | ------------------ | ------------------------------------ |
| VideoStore | Play/Pause, Buffering, Quality, Errors | Low (Event driven) | useSyncExternalStore |
| TimeStore | CurrentTime, Progress, Duration | High (4-60Hz) | requestAnimationFrame + Direct DOM |
This means your progress bar updates smoothly even if your React app is busy rendering a complex component tree.
📚 Documentation
License
MIT
