Improve video playback and add advertising capabilities to the platform

Migrates from HLS.js to Video.js with IMA SDK integration for advanced video streaming and VAST advertising support.

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/azV7Mbc
This commit is contained in:
sebastjanartic
2025-08-05 06:07:48 +00:00
parent 1530837f00
commit 17b5c378fd
4 changed files with 368 additions and 133 deletions
+65 -126
View File
@@ -1,9 +1,12 @@
import { useEffect, useRef, useState } from "react";
import { X, Share2, Facebook, Twitter, MessageCircle } from "lucide-react";
import { X, Share2 } from "lucide-react";
import { type Video } from "@shared/schema";
import { Button } from "@/components/ui/button";
import { apiRequest } from "@/lib/queryClient";
import Hls from "hls.js";
import videojs from "video.js";
import "videojs-contrib-ads";
import "videojs-ima";
import "video.js/dist/video-js.css";
import {
FacebookShareButton,
TwitterShareButton,
@@ -13,21 +16,7 @@ import {
WhatsappIcon
} from "react-share";
// Google IMA SDK types
declare global {
interface Window {
google?: {
ima?: {
AdsManager: any;
AdDisplayContainer: any;
AdsLoader: any;
AdsManagerLoadedEvent: any;
AdsRequest: any;
ViewMode: any;
};
};
}
}
// Video.js types extend from module import
interface VideoModalProps {
video: Video | null;
@@ -70,9 +59,8 @@ function formatDate(date: Date | string): string {
export default function VideoModal({ video, isOpen, onClose }: VideoModalProps) {
const videoRef = useRef<HTMLVideoElement>(null);
const hlsRef = useRef<Hls | null>(null);
const playerRef = useRef<any>(null);
const [showShareMenu, setShowShareMenu] = useState(false);
const adContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
@@ -94,118 +82,79 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
};
}, [isOpen, onClose]);
// Initialize HLS when video is available
// Initialize Video.js when video is available
useEffect(() => {
if (isOpen && video && videoRef.current) {
const videoElement = videoRef.current;
// Clean up previous HLS instance
if (hlsRef.current) {
hlsRef.current.destroy();
hlsRef.current = null;
// Clean up previous Video.js instance
if (playerRef.current) {
playerRef.current.dispose();
playerRef.current = null;
}
const videoUrl = video.videoUrl;
console.log('Loading video:', videoUrl);
console.log('Loading video with Video.js:', videoUrl);
// 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: true,
enableWorker: false,
lowLatencyMode: true,
backBufferLength: 90
});
// Initialize Video.js player with ads support
const player = videojs(videoElement, {
controls: true,
fluid: true,
responsive: true,
preload: 'metadata',
html5: {
hls: {
enableLowInitialPlaylist: true,
smoothQualityChange: true,
overrideNative: true
}
},
sources: [{
src: videoUrl,
type: videoUrl.includes('.m3u8') ? 'application/x-mpegURL' : 'video/mp4'
}]
});
// Initialize ads plugin
player.ready(() => {
try {
// Initialize ads plugin
(player as any).ads();
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;
// Configure IMA plugin for VAST ads
if ((player as any).ima) {
(player as any).ima({
id: video.id,
adTagUrl: 'https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=',
contribAdsSettings: {
prerollTimeout: 1000,
postrollTimeout: 1000,
debug: true
}
}
});
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');
});
}
} catch (error) {
console.log('Ads plugin not available, continuing without ads:', error);
}
} else {
// For regular MP4 videos
videoElement.src = videoUrl;
console.log('Using native video support for MP4');
}
});
player.on('error', (error: any) => {
console.error('Video.js player error:', error);
});
playerRef.current = player;
}
// Cleanup when modal closes
return () => {
if (hlsRef.current) {
hlsRef.current.destroy();
hlsRef.current = null;
if (playerRef.current) {
playerRef.current.dispose();
playerRef.current = null;
}
};
}, [isOpen, video]);
// Google Ads Manager Integration
useEffect(() => {
if (isOpen && video && adContainerRef.current) {
// Initialize Google Ads Manager
const loadGoogleAdsManager = () => {
// Check if Google IMA SDK is loaded and properly initialized
if (window.google && window.google.ima && window.google.ima.AdDisplayContainer) {
try {
// Create ad display container
const adDisplayContainer = new window.google.ima.AdDisplayContainer(
adContainerRef.current,
videoRef.current
);
console.log('Google Ads Manager initialized for video:', video.id);
} catch (error) {
console.log('Google Ads Manager initialization skipped:', error);
}
} else {
// Load Google IMA SDK if not already loaded
if (!document.querySelector('script[src*="ima3.js"]')) {
const script = document.createElement('script');
script.src = 'https://imasdk.googleapis.com/js/sdkloader/ima3.js';
script.onload = () => {
setTimeout(loadGoogleAdsManager, 100); // Small delay to ensure full SDK load
};
script.onerror = () => {
console.log('Google IMA SDK failed to load');
};
document.head.appendChild(script);
}
}
};
loadGoogleAdsManager();
}
}, [isOpen, video]);
const handleVideoPlay = async () => {
if (video) {
@@ -278,16 +227,12 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
<div className="relative bg-black rounded-lg overflow-hidden">
<video
ref={videoRef}
className="w-full h-auto max-h-[80vh]"
controls
preload="metadata"
className="video-js vjs-default-skin w-full h-auto max-h-[80vh]"
data-setup="{}"
onPlay={handleVideoPlay}
data-testid="video-player"
crossOrigin="anonymous"
>
<source src={video.videoUrl} type="application/x-mpegURL" />
Your browser does not support the video tag.
</video>
/>
{/* Video Controls and Share Menu */}
<div className="absolute top-4 right-4 flex gap-2">
@@ -365,13 +310,7 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
</Button>
</div>
{/* Google Ads Container */}
<div
ref={adContainerRef}
className="absolute top-0 left-0 w-full h-full pointer-events-none"
style={{ zIndex: 10 }}
data-testid="ads-container"
/>
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-6">
<h3