React Video Player
React Video Player Example
Build a full-featured video player using React and HLS.js with HesedVid’s streaming infrastructure.
Basic Player Component
import React, { useEffect, useRef, useState } from 'react';import Hls from 'hls.js';
interface VideoPlayerProps { src: string; poster?: string; autoPlay?: boolean; muted?: boolean; onError?: (error: any) => void;}
export function VideoPlayer({ src, poster, autoPlay = false, muted = false, onError}: VideoPlayerProps) { const videoRef = useRef<HTMLVideoElement>(null); const hlsRef = useRef<Hls | null>(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState<string | null>(null);
useEffect(() => { const video = videoRef.current; if (!video || !src) return;
// Clean up previous instance if (hlsRef.current) { hlsRef.current.destroy(); }
if (Hls.isSupported()) { const hls = new Hls({ enableWorker: true, lowLatencyMode: true, backBufferLength: 90 });
hlsRef.current = hls;
hls.on(Hls.Events.MANIFEST_PARSED, () => { setIsLoading(false); if (autoPlay) { video.play().catch(e => console.log('Autoplay failed:', e)); } });
hls.on(Hls.Events.ERROR, (event, data) => { if (data.fatal) { setError(`Error: ${data.type} - ${data.details}`); onError?.(data); } });
hls.loadSource(src); hls.attachMedia(video); } else if (video.canPlayType('application/vnd.apple.mpegurl')) { // Native HLS support (Safari) video.src = src; video.addEventListener('loadedmetadata', () => { setIsLoading(false); }); } else { setError('HLS is not supported in this browser'); }
return () => { if (hlsRef.current) { hlsRef.current.destroy(); hlsRef.current = null; } }; }, [src, autoPlay, onError]);
if (error) { return ( <div className="aspect-video bg-black flex items-center justify-center text-white"> <p>{error}</p> </div> ); }
return ( <div className="relative aspect-video bg-black"> {isLoading && ( <div className="absolute inset-0 flex items-center justify-center"> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div> </div> )} <video ref={videoRef} className="w-full h-full" controls playsInline poster={poster} muted={muted} /> </div> );}import { VideoPlayer } from './VideoPlayer';
function App() { // Public video const publicVideoUrl = 'https://worker.hesedvid.com/pubvid_AbCdEf/master.m3u8';
// Private video with signed URL const [privateVideoUrl, setPrivateVideoUrl] = useState<string | null>(null);
useEffect(() => { // Fetch signed URL for private video async function getSignedUrl() { const response = await fetch( 'https://api.hesedvid.com/v1/api/org_123/videos/vid_XyZ123/signed-url', { headers: { 'X-Api-Key': 'YOUR_API_KEY' } } ); const { body } = await response.json(); setPrivateVideoUrl(body.signed_url); }
getSignedUrl(); }, []);
return ( <div className="max-w-4xl mx-auto p-4"> <h1 className="text-2xl font-bold mb-4">HesedVid Player Demo</h1>
<div className="space-y-8"> <div> <h2 className="text-xl font-semibold mb-2">Public Video</h2> <VideoPlayer src={publicVideoUrl} poster="https://worker.hesedvid.com/t/pubvid_AbCdEf/1280x720.jpg" /> </div>
{privateVideoUrl && ( <div> <h2 className="text-xl font-semibold mb-2">Private Video</h2> <VideoPlayer src={privateVideoUrl} onError={(error) => console.error('Player error:', error)} /> </div> )} </div> </div> );}Running the Example
To run this example locally:
- Create a new React app:
npx create-react-app hesedvid-player-democd hesedvid-player-demo- Install HLS.js:
npm install hls.jsReplace
src/App.jswith the complete example code from the “Complete Example” tabUpdate the API credentials:
- Replace
YOUR_API_KEYwith your actual API key - Replace
orgIDandenvIDwith your organization and environment IDs - Replace video IDs with your actual video IDs
- Start the development server:
npm startKey Features Demonstrated
- âś… Adaptive bitrate streaming
- âś… Error handling and recovery
- âś… Loading states
- âś… Native controls
- âś… Mobile-friendly (playsinline)
- âś… Poster image support
Advanced Features
Quality Selector
Add a quality selector to manually control video quality:
interface Quality { height: number; bitrate: number; level: number;}
function QualitySelector({ hls }: { hls: Hls | null }) { const [qualities, setQualities] = useState<Quality[]>([]); const [currentLevel, setCurrentLevel] = useState(-1);
useEffect(() => { if (!hls) return;
const updateQualities = () => { const levels = hls.levels.map((level, index) => ({ height: level.height, bitrate: level.bitrate, level: index })); setQualities(levels); setCurrentLevel(hls.currentLevel); };
hls.on(Hls.Events.MANIFEST_PARSED, updateQualities); hls.on(Hls.Events.LEVEL_SWITCHED, (_, data) => { setCurrentLevel(data.level); });
return () => { hls.off(Hls.Events.MANIFEST_PARSED, updateQualities); }; }, [hls]);
const handleQualityChange = (level: number) => { if (hls) { hls.currentLevel = level; } };
return ( <select value={currentLevel} onChange={(e) => handleQualityChange(Number(e.target.value))} className="bg-black/50 text-white px-2 py-1 rounded" > <option value={-1}>Auto</option> {qualities.map((q) => ( <option key={q.level} value={q.level}> {q.height}p ({Math.round(q.bitrate / 1000)}kbps) </option> ))} </select> );}Playback Analytics
Track viewing analytics:
function useVideoAnalytics(videoRef: React.RefObject<HTMLVideoElement>) { const [analytics, setAnalytics] = useState({ watchTime: 0, bufferingTime: 0, bufferingEvents: 0, qualitySwitches: 0 });
useEffect(() => { const video = videoRef.current; if (!video) return;
let watchStartTime: number | null = null; let bufferStartTime: number | null = null;
const handlePlay = () => { watchStartTime = Date.now(); };
const handlePause = () => { if (watchStartTime) { setAnalytics(prev => ({ ...prev, watchTime: prev.watchTime + (Date.now() - watchStartTime) / 1000 })); watchStartTime = null; } };
const handleWaiting = () => { bufferStartTime = Date.now(); setAnalytics(prev => ({ ...prev, bufferingEvents: prev.bufferingEvents + 1 })); };
const handlePlaying = () => { if (bufferStartTime) { setAnalytics(prev => ({ ...prev, bufferingTime: prev.bufferingTime + (Date.now() - bufferStartTime) / 1000 })); bufferStartTime = null; } };
video.addEventListener('play', handlePlay); video.addEventListener('pause', handlePause); video.addEventListener('waiting', handleWaiting); video.addEventListener('playing', handlePlaying);
return () => { video.removeEventListener('play', handlePlay); video.removeEventListener('pause', handlePause); video.removeEventListener('waiting', handleWaiting); video.removeEventListener('playing', handlePlaying); }; }, [videoRef]);
return analytics;}Thumbnail Preview
Show preview thumbnails on hover:
function ThumbnailPreview({ videoId, duration, width = 160, height = 90}: { videoId: string; duration: number; width?: number; height?: number;}) { const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(null);
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => { const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; const percentage = x / rect.width; const timestampMs = Math.floor(percentage * duration * 1000);
// Update thumbnail URL with timestamp setThumbnailUrl( `https://worker.hesedvid.com/t/${videoId}/${width}x${height}_${timestampMs}.jpg` ); };
return ( <div className="relative h-1 bg-gray-600 cursor-pointer" onMouseMove={handleMouseMove} onMouseLeave={() => setThumbnailUrl(null)} > {thumbnailUrl && ( <div className="absolute bottom-2 left-1/2 transform -translate-x-1/2"> <img src={thumbnailUrl} alt="Preview" className="rounded shadow-lg" width={width} height={height} /> </div> )} </div> );}Performance Optimization
Preloading
Preload video metadata for faster start:
function preloadVideo(url: string) { if (Hls.isSupported()) { const hls = new Hls(); hls.loadSource(url);
// Stop loading after manifest is parsed hls.on(Hls.Events.MANIFEST_PARSED, () => { hls.stopLoad(); });
// Clean up after 30 seconds setTimeout(() => hls.destroy(), 30000); }}
// Preload next video in playlistuseEffect(() => { if (nextVideoUrl) { preloadVideo(nextVideoUrl); }}, [nextVideoUrl]);Bandwidth Optimization
Monitor and adapt to network conditions:
function useBandwidthMonitor(hls: Hls | null) { const [bandwidth, setBandwidth] = useState<number | null>(null); const [recommendation, setRecommendation] = useState<string>('auto');
useEffect(() => { if (!hls) return;
const checkBandwidth = () => { const bw = hls.bandwidthEstimate; setBandwidth(bw);
// Make quality recommendations if (bw < 1_000_000) { // < 1 Mbps setRecommendation('480p'); } else if (bw < 3_000_000) { // < 3 Mbps setRecommendation('720p'); } else { setRecommendation('1080p'); } };
hls.on(Hls.Events.FRAG_LOADED, checkBandwidth);
return () => { hls.off(Hls.Events.FRAG_LOADED, checkBandwidth); }; }, [hls]);
return { bandwidth, recommendation };}Next Steps
Add custom controls: Build a custom UI with play/pause, seek bar, volume, and fullscreen.
Implement playlists: Create continuous playback with multiple videos.
Add captions: Display WebVTT subtitles (coming soon to HesedVid).
Track analytics: Send playback events to your analytics service.