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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user