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 { Button } from "@/components/ui/button";
import { apiRequest } from "@/lib/queryClient";
import videojs from "video.js";
import "videojs-contrib-ads";
import "videojs-ima";
import "video.js/dist/video-js.css";
import Hls from "hls.js";
import {
FacebookShareButton,
TwitterShareButton,
WhatsappShareButton,
FacebookIcon,
TwitterIcon,
WhatsappIcon
} from "react-share";
// Video.js types extend from module import
// HLS.js types for video streaming
interface VideoModalProps {
video: Video | null;
@ -59,7 +53,7 @@ function formatDate(date: Date | string): string {
export default function VideoModal({ video, isOpen, onClose }: VideoModalProps) {
const videoRef = useRef<HTMLVideoElement>(null);
const playerRef = useRef<any>(null);
const hlsRef = useRef<Hls | null>(null);
const [showShareMenu, setShowShareMenu] = useState(false);
const [videoThumbnail, setVideoThumbnail] = useState<string | null>(null);
@ -83,126 +77,99 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
};
}, [isOpen, onClose]);
// Initialize Video.js when video is available
// Initialize HLS when video is available
useEffect(() => {
if (isOpen && video && videoRef.current) {
const videoElement = videoRef.current;
// Clean up previous Video.js instance
if (playerRef.current) {
try {
playerRef.current.dispose();
} catch (e) {
console.log('Player cleanup error:', e);
}
playerRef.current = null;
// Clean up previous HLS instance
if (hlsRef.current) {
hlsRef.current.destroy();
hlsRef.current = null;
}
const videoUrl = video.videoUrl;
console.log('Loading video with Video.js:', videoUrl);
console.log('Loading video with HLS.js:', videoUrl);
try {
// Initialize Video.js player without sources first
const player = videojs(videoElement, {
controls: true,
fluid: true,
responsive: true,
preload: 'metadata',
html5: {
hls: {
enableLowInitialPlaylist: true,
smoothQualityChange: true,
overrideNative: false
// Check if the video URL is HLS (.m3u8)
if (videoUrl.includes('.m3u8')) {
if (Hls.isSupported()) {
// Use HLS.js for browsers that don't support HLS natively
const hls = new Hls({
debug: false,
enableWorker: false,
lowLatencyMode: true,
backBufferLength: 90
});
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;
}
}
}
});
// Initialize ads plugin immediately after player creation
if (typeof (player as any).ads === 'function') {
try {
(player as any).ads({
debug: false,
prerollTimeout: 3000,
postrollTimeout: 3000,
timeout: 5000
});
console.log('Ads plugin initialized');
} catch (error) {
console.log('Ads plugin initialization failed:', error);
}
});
hlsRef.current = hls;
} else if (videoElement.canPlayType('application/vnd.apple.mpegurl')) {
// For Safari that supports HLS natively
videoElement.src = videoUrl;
console.log('Using native HLS support');
} else {
console.error('HLS is not supported in this browser');
}
// Set source after ads plugin is ready
player.src({
src: videoUrl,
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);
} else {
// For regular MP4 videos
videoElement.src = videoUrl;
console.log('Using native video support for MP4');
}
// 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
return () => {
if (playerRef.current) {
try {
playerRef.current.dispose();
} catch (e) {
console.log('Player cleanup error:', e);
}
playerRef.current = null;
if (hlsRef.current) {
hlsRef.current.destroy();
hlsRef.current = null;
}
};
}, [isOpen, video]);
// Function to capture video thumbnail
const captureVideoThumbnail = (player: any) => {
const captureVideoThumbnail = () => {
try {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Get video element directly from player
const videoElement = player.el().querySelector('video');
const videoElement = videoRef.current;
if (videoElement && ctx && videoElement.videoWidth > 0) {
canvas.width = videoElement.videoWidth;
@ -215,7 +182,7 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
} else {
console.log('Video element not ready for thumbnail capture');
// Retry after a delay
setTimeout(() => captureVideoThumbnail(player), 1000);
setTimeout(() => captureVideoThumbnail(), 1000);
}
} catch (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">
<video
ref={videoRef}
className="video-js vjs-default-skin w-full h-auto max-h-[80vh]"
data-setup="{}"
className="w-full h-auto max-h-[80vh]"
controls
preload="metadata"
onPlay={handleVideoPlay}
data-testid="video-player"
crossOrigin="anonymous"
/>
>
Your browser does not support the video tag.
</video>
{/* Video Controls and Share Menu */}
<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)
- ✅ **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
- ✅ **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)
- ✅ **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