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:
parent
799d2c8bb1
commit
99c55d99df
@ -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">
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user