Improve video playback using HLS.js for better streaming compatibility

Refactors video player from Video.js/IMA to HLS.js for improved HLS streaming and removes videojs-contrib-ads.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 11420304-80a9-4ef2-adff-cbdaa418ffa8
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/11420304-80a9-4ef2-adff-cbdaa418ffa8/Mx2OL5A
This commit is contained in:
sebastjanartic 2025-08-05 06:23:00 +00:00
parent 799d2c8bb1
commit 99c55d99df
2 changed files with 83 additions and 112 deletions

View File

@ -3,20 +3,14 @@ import { X, Share2 } from "lucide-react";
import { type Video } from "@shared/schema"; import { type Video } from "@shared/schema";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { apiRequest } from "@/lib/queryClient"; import { apiRequest } from "@/lib/queryClient";
import videojs from "video.js"; import Hls from "hls.js";
import "videojs-contrib-ads";
import "videojs-ima";
import "video.js/dist/video-js.css";
import { import {
FacebookShareButton,
TwitterShareButton,
WhatsappShareButton,
FacebookIcon, FacebookIcon,
TwitterIcon, TwitterIcon,
WhatsappIcon WhatsappIcon
} from "react-share"; } from "react-share";
// Video.js types extend from module import // HLS.js types for video streaming
interface VideoModalProps { interface VideoModalProps {
video: Video | null; video: Video | null;
@ -59,7 +53,7 @@ function formatDate(date: Date | string): string {
export default function VideoModal({ video, isOpen, onClose }: VideoModalProps) { export default function VideoModal({ video, isOpen, onClose }: VideoModalProps) {
const videoRef = useRef<HTMLVideoElement>(null); const videoRef = useRef<HTMLVideoElement>(null);
const playerRef = useRef<any>(null); const hlsRef = useRef<Hls | null>(null);
const [showShareMenu, setShowShareMenu] = useState(false); const [showShareMenu, setShowShareMenu] = useState(false);
const [videoThumbnail, setVideoThumbnail] = useState<string | null>(null); const [videoThumbnail, setVideoThumbnail] = useState<string | null>(null);
@ -83,126 +77,99 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
}; };
}, [isOpen, onClose]); }, [isOpen, onClose]);
// Initialize Video.js when video is available // Initialize HLS when video is available
useEffect(() => { useEffect(() => {
if (isOpen && video && videoRef.current) { if (isOpen && video && videoRef.current) {
const videoElement = videoRef.current; const videoElement = videoRef.current;
// Clean up previous Video.js instance // Clean up previous HLS instance
if (playerRef.current) { if (hlsRef.current) {
try { hlsRef.current.destroy();
playerRef.current.dispose(); hlsRef.current = null;
} catch (e) {
console.log('Player cleanup error:', e);
}
playerRef.current = null;
} }
const videoUrl = video.videoUrl; const videoUrl = video.videoUrl;
console.log('Loading video with Video.js:', videoUrl); console.log('Loading video with HLS.js:', videoUrl);
try { // Check if the video URL is HLS (.m3u8)
// Initialize Video.js player without sources first if (videoUrl.includes('.m3u8')) {
const player = videojs(videoElement, { if (Hls.isSupported()) {
controls: true, // Use HLS.js for browsers that don't support HLS natively
fluid: true, const hls = new Hls({
responsive: true, debug: false,
preload: 'metadata', enableWorker: false,
html5: { lowLatencyMode: true,
hls: { backBufferLength: 90
enableLowInitialPlaylist: true, });
smoothQualityChange: true,
overrideNative: false hls.loadSource(videoUrl);
hls.attachMedia(videoElement);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
console.log('HLS manifest loaded successfully');
});
hls.on(Hls.Events.ERROR, (event, data) => {
console.error('HLS error:', data);
if (data.fatal) {
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
console.log('Network error, trying to recover...');
hls.startLoad();
break;
case Hls.ErrorTypes.MEDIA_ERROR:
console.log('Media error, trying to recover...');
hls.recoverMediaError();
break;
default:
console.log('Fatal error, destroying HLS instance...');
hls.destroy();
break;
}
} }
} });
});
hlsRef.current = hls;
// Initialize ads plugin immediately after player creation } else if (videoElement.canPlayType('application/vnd.apple.mpegurl')) {
if (typeof (player as any).ads === 'function') { // For Safari that supports HLS natively
try { videoElement.src = videoUrl;
(player as any).ads({ console.log('Using native HLS support');
debug: false, } else {
prerollTimeout: 3000, console.error('HLS is not supported in this browser');
postrollTimeout: 3000,
timeout: 5000
});
console.log('Ads plugin initialized');
} catch (error) {
console.log('Ads plugin initialization failed:', error);
}
} }
} else {
// Set source after ads plugin is ready // For regular MP4 videos
player.src({ videoElement.src = videoUrl;
src: videoUrl, console.log('Using native video support for MP4');
type: videoUrl.includes('.m3u8') ? 'application/x-mpegURL' : 'video/mp4'
});
player.ready(() => {
console.log('Video.js player ready with source');
});
// Listen for loadeddata event to capture thumbnail
player.on('loadeddata', () => {
console.log('Video data loaded, capturing thumbnail');
setTimeout(() => {
captureVideoThumbnail(player);
}, 500);
});
player.on('canplay', () => {
console.log('Video can play, attempting thumbnail capture');
captureVideoThumbnail(player);
});
player.on('error', (error: any) => {
console.error('Video.js player error:', error);
});
player.on('play', () => {
handleVideoPlay();
});
// Simple ads event listeners
player.on('adstart', () => {
console.log('Ad playback started');
});
player.on('adend', () => {
console.log('Ad playback ended');
});
player.on('aderror', (error: any) => {
console.log('Ad error:', error);
});
playerRef.current = player;
} catch (error) {
console.error('Failed to initialize Video.js player:', error);
} }
// Capture thumbnail when video loads
videoElement.addEventListener('loadeddata', () => {
console.log('Video data loaded, capturing thumbnail');
setTimeout(() => captureVideoThumbnail(), 1000);
});
videoElement.addEventListener('canplay', () => {
console.log('Video can play, capturing thumbnail');
captureVideoThumbnail();
});
} }
// Cleanup when modal closes // Cleanup when modal closes
return () => { return () => {
if (playerRef.current) { if (hlsRef.current) {
try { hlsRef.current.destroy();
playerRef.current.dispose(); hlsRef.current = null;
} catch (e) {
console.log('Player cleanup error:', e);
}
playerRef.current = null;
} }
}; };
}, [isOpen, video]); }, [isOpen, video]);
// Function to capture video thumbnail // Function to capture video thumbnail
const captureVideoThumbnail = (player: any) => { const captureVideoThumbnail = () => {
try { try {
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
const videoElement = videoRef.current;
// Get video element directly from player
const videoElement = player.el().querySelector('video');
if (videoElement && ctx && videoElement.videoWidth > 0) { if (videoElement && ctx && videoElement.videoWidth > 0) {
canvas.width = videoElement.videoWidth; canvas.width = videoElement.videoWidth;
@ -215,7 +182,7 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
} else { } else {
console.log('Video element not ready for thumbnail capture'); console.log('Video element not ready for thumbnail capture');
// Retry after a delay // Retry after a delay
setTimeout(() => captureVideoThumbnail(player), 1000); setTimeout(() => captureVideoThumbnail(), 1000);
} }
} catch (error) { } catch (error) {
console.log('Failed to capture video thumbnail:', error); console.log('Failed to capture video thumbnail:', error);
@ -295,11 +262,15 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
<div className="relative bg-black rounded-lg overflow-hidden"> <div className="relative bg-black rounded-lg overflow-hidden">
<video <video
ref={videoRef} ref={videoRef}
className="video-js vjs-default-skin w-full h-auto max-h-[80vh]" className="w-full h-auto max-h-[80vh]"
data-setup="{}" controls
preload="metadata"
onPlay={handleVideoPlay}
data-testid="video-player" data-testid="video-player"
crossOrigin="anonymous" crossOrigin="anonymous"
/> >
Your browser does not support the video tag.
</video>
{/* Video Controls and Share Menu */} {/* Video Controls and Share Menu */}
<div className="absolute top-4 right-4 flex gap-2"> <div className="absolute top-4 right-4 flex gap-2">

View File

@ -6,9 +6,9 @@ VideoStream is a fully functional video streaming platform that integrates direc
## Recent Changes (August 2025) ## Recent Changes (August 2025)
- ✅ **Video.js + VAST Plugin Architecture**: Migrated from HLS.js to Video.js with IMA SDK for professional video streaming and advertising - ✅ **HLS.js Video Streaming**: Reliable HLS.js implementation for professional video streaming with Bunny.net integration
- ✅ **Advanced Video Controls**: Professional video player with fluid responsive design and adaptive streaming - ✅ **Advanced Video Controls**: Professional video player with fluid responsive design and adaptive streaming
- ✅ **VAST Advertising Support**: Integrated videojs-contrib-ads and videojs-ima for pre-roll, mid-roll, and post-roll video advertisements with Google DoubleClick integration - ✅ **Video Controls**: Professional video player with full controls and responsive design
- ✅ **Search Functionality**: Client-side search working with proper text visibility (white background, black text) - ✅ **Search Functionality**: Client-side search working with proper text visibility (white background, black text)
- ✅ **Bunny.net Integration**: Complete integration with private video library using signed URLs for secure access - ✅ **Bunny.net Integration**: Complete integration with private video library using signed URLs for secure access
- ✅ **Error Handling**: Robust error handling with Video.js fallback mechanisms - ✅ **Error Handling**: Robust error handling with Video.js fallback mechanisms