Advanced
Headless Usage
Build a fully custom player UI using only hooks. No ytcn components required.
When to go headless
- •When the default ytcn controls don't match your design system
- •When you need custom control layouts (e.g., controls above the video)
- •When you want to integrate with an existing media player UI
- •When you need a minimal player with only play/pause
Minimum viable headless player
The simplest possible player — just a video with a play/pause button and time display. No ytcn components used at all.
components/minimal-player.tsx
"use client"
import { useYtcnPlayer } from "@/hooks/ytcn/use-ytcn-player"
import { useThumbnail } from "@/hooks/ytcn/use-thumbnail"
import { IconPlayerPlayFilled, IconPlayerPauseFilled } from "@tabler/icons-react"
interface MinimalPlayerProps {
videoId: string
}
export function MinimalPlayer({ videoId }: MinimalPlayerProps) {
const { containerRef, playerDivRef, state, controls } =
useYtcnPlayer({ videoId })
const { thumbnailUrl } = useThumbnail(videoId)
const formatTime = (s: number) => {
const m = Math.floor(s / 60)
const sec = Math.floor(s % 60)
return `${m}:${sec.toString().padStart(2, "0")}`
}
return (
<div
ref={containerRef}
className="relative aspect-video bg-black rounded-xl overflow-hidden group"
>
{/* Video layer */}
<div ref={playerDivRef} className="absolute inset-0" />
{/* Thumbnail (shown before iframe loads) */}
{state.phase === "thumbnail" && thumbnailUrl && (
<button
onClick={controls.togglePlay}
className="absolute inset-0 z-10 cursor-pointer"
>
<img
src={thumbnailUrl}
alt="Video thumbnail"
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 flex items-center justify-center bg-black/20">
<div className="size-16 rounded-full bg-white/90 flex items-center justify-center">
<IconPlayerPlayFilled className="size-7 text-black ml-0.5" />
</div>
</div>
</button>
)}
{/* Custom controls overlay */}
<div className="absolute inset-x-0 bottom-0 p-4 bg-gradient-to-t from-black/80 to-transparent opacity-0 group-hover:opacity-100 transition-opacity z-20">
<div className="flex items-center gap-3">
<button
onClick={controls.togglePlay}
className="text-white hover:scale-110 transition-transform"
>
{state.isPlaying ? (
<IconPlayerPauseFilled className="size-5" />
) : (
<IconPlayerPlayFilled className="size-5" />
)}
</button>
<span className="text-white text-sm font-mono tabular-nums">
{formatTime(state.currentTime)} / {formatTime(state.duration)}
</span>
</div>
</div>
</div>
)
}Full custom controls
A more complete example with progress bar, volume, and speed — all built from scratch using the hook.
components/custom-player.tsx
"use client"
import { useYtcnPlayer } from "@/hooks/ytcn/use-ytcn-player"
import { useThumbnail } from "@/hooks/ytcn/use-thumbnail"
import { useRef } from "react"
export function CustomPlayer({ videoId }: { videoId: string }) {
const { containerRef, playerDivRef, state, controls } =
useYtcnPlayer({ videoId })
const { thumbnailUrl } = useThumbnail(videoId)
const progressRef = useRef<HTMLDivElement>(null)
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => {
const rect = progressRef.current?.getBoundingClientRect()
if (!rect) return
const fraction = (e.clientX - rect.left) / rect.width
controls.seekTo(fraction * state.duration)
}
const progress = state.duration > 0
? (state.currentTime / state.duration) * 100
: 0
return (
<div ref={containerRef} className="relative aspect-video bg-black rounded-xl overflow-hidden">
<div ref={playerDivRef} className="absolute inset-0" />
{/* Progress bar (bottom edge) */}
<div
ref={progressRef}
onClick={handleProgressClick}
className="absolute bottom-0 left-0 right-0 h-1 bg-white/20 cursor-pointer z-20 hover:h-2 transition-all"
>
<div
className="h-full bg-red-500 transition-[width] duration-100"
style={{ width: `${progress}%` }}
/>
</div>
{/* Controls */}
<div className="absolute bottom-2 left-4 right-4 flex items-center gap-4 z-20">
<button onClick={controls.togglePlay} className="text-white text-lg">
{state.isPlaying ? "⏸" : "▶"}
</button>
<button onClick={controls.toggleMute} className="text-white text-sm">
{state.isMuted ? "🔇" : "🔊"}
</button>
<input
type="range"
min={0}
max={100}
value={state.isMuted ? 0 : state.volume}
onChange={(e) => controls.setVolume(Number(e.target.value))}
className="w-20"
/>
<select
value={state.playbackRate}
onChange={(e) => controls.setSpeed(Number(e.target.value) as 0.75 | 1 | 1.5 | 2)}
className="bg-black/50 text-white text-xs rounded px-1 py-0.5"
>
<option value={0.75}>0.75x</option>
<option value={1}>1x</option>
<option value={1.5}>1.5x</option>
<option value={2}>2x</option>
</select>
</div>
</div>
)
}LMS integration example
A complete LMS use case — save progress, resume from saved position, mark complete when 90% watched, and prevent seeking forward in locked content.
components/lms-player.tsx
"use client"
import { useYtcnPlayer } from "@/hooks/ytcn/use-ytcn-player"
import { useRef, useCallback, useEffect } from "react"
interface LmsPlayerProps {
videoId: string
lessonId: string
onComplete: () => void
allowSeekForward?: boolean
}
export function LmsPlayer({
videoId,
lessonId,
onComplete,
allowSeekForward = false,
}: LmsPlayerProps) {
const savedTime = Number(localStorage.getItem(`lms-${lessonId}`) ?? 0)
const maxWatched = useRef(savedTime)
const completed = useRef(false)
const { containerRef, playerDivRef, state, controls } = useYtcnPlayer({
videoId,
startAt: savedTime,
onTimeUpdate: useCallback(
(current: number, duration: number) => {
// Save progress every 5 seconds
if (current - (Number(localStorage.getItem(`lms-${lessonId}`)) ?? 0) > 5) {
localStorage.setItem(`lms-${lessonId}`, String(Math.floor(current)))
}
// Track max watched position
if (current > maxWatched.current) {
maxWatched.current = current
}
// Mark complete at 90%
if (!completed.current && current / duration > 0.9) {
completed.current = true
onComplete()
}
// Prevent seeking forward (if locked)
if (!allowSeekForward && current > maxWatched.current + 2) {
controls.seekTo(maxWatched.current)
}
},
[lessonId, onComplete, allowSeekForward, controls]
),
})
return (
<div ref={containerRef} className="relative aspect-video bg-black rounded-lg overflow-hidden">
<div ref={playerDivRef} className="absolute inset-0" />
{/* Add your custom controls here */}
</div>
)
}