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 { 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize ads plugin immediately after player creation
|
hlsRef.current = hls;
|
||||||
if (typeof (player as any).ads === 'function') {
|
} else if (videoElement.canPlayType('application/vnd.apple.mpegurl')) {
|
||||||
try {
|
// For Safari that supports HLS natively
|
||||||
(player as any).ads({
|
videoElement.src = videoUrl;
|
||||||
debug: false,
|
console.log('Using native HLS support');
|
||||||
prerollTimeout: 3000,
|
} else {
|
||||||
postrollTimeout: 3000,
|
console.error('HLS is not supported in this browser');
|
||||||
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">
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user