Enable video playback on more browsers and fix video loading issues
Integrates HLS.js for wider browser support and fixes video playback by using iframe. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 50814a1e-92e4-4968-856f-7bc7eedf5e8f Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/50814a1e-92e4-4968-856f-7bc7eedf5e8f/tKnLVTM
This commit is contained in:
parent
b77e18915f
commit
e087324c76
@ -3,6 +3,7 @@ import { X } 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 Hls from "hls.js";
|
||||||
|
|
||||||
interface VideoModalProps {
|
interface VideoModalProps {
|
||||||
video: Video | null;
|
video: Video | null;
|
||||||
@ -45,6 +46,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 hlsRef = useRef<Hls | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleEscape = (e: KeyboardEvent) => {
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
@ -66,6 +68,82 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
|
|||||||
};
|
};
|
||||||
}, [isOpen, onClose]);
|
}, [isOpen, onClose]);
|
||||||
|
|
||||||
|
// Initialize HLS 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoUrl = video.videoUrl;
|
||||||
|
console.log('Loading video:', 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
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For regular MP4 videos
|
||||||
|
videoElement.src = videoUrl;
|
||||||
|
console.log('Using native video support for MP4');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup when modal closes
|
||||||
|
return () => {
|
||||||
|
if (hlsRef.current) {
|
||||||
|
hlsRef.current.destroy();
|
||||||
|
hlsRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isOpen, video]);
|
||||||
|
|
||||||
const handleVideoPlay = async () => {
|
const handleVideoPlay = async () => {
|
||||||
if (video) {
|
if (video) {
|
||||||
try {
|
try {
|
||||||
@ -102,17 +180,28 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="relative bg-black rounded-lg overflow-hidden">
|
<div className="relative bg-black rounded-lg overflow-hidden">
|
||||||
<video
|
{video.videoUrl.includes('iframe.mediadelivery.net') ? (
|
||||||
ref={videoRef}
|
<iframe
|
||||||
className="w-full h-auto max-h-[80vh]"
|
src={video.videoUrl}
|
||||||
controls
|
className="w-full h-auto max-h-[80vh] aspect-video"
|
||||||
preload="metadata"
|
allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;"
|
||||||
onPlay={handleVideoPlay}
|
allowFullScreen
|
||||||
data-testid="video-player"
|
onLoad={handleVideoPlay}
|
||||||
src={video.videoUrl}
|
data-testid="video-iframe"
|
||||||
>
|
/>
|
||||||
Your browser does not support the video tag.
|
) : (
|
||||||
</video>
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-6">
|
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-6">
|
||||||
<h3
|
<h3
|
||||||
|
|||||||
7
package-lock.json
generated
7
package-lock.json
generated
@ -51,6 +51,7 @@
|
|||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
"framer-motion": "^11.13.1",
|
"framer-motion": "^11.13.1",
|
||||||
|
"hls.js": "^1.6.7",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.453.0",
|
"lucide-react": "^0.453.0",
|
||||||
"memorystore": "^1.6.7",
|
"memorystore": "^1.6.7",
|
||||||
@ -5550,6 +5551,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hls.js": {
|
||||||
|
"version": "1.6.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.7.tgz",
|
||||||
|
"integrity": "sha512-QW2fnwDGKGc9DwQUGLbmMOz8G48UZK7PVNJPcOUql1b8jubKx4/eMHNP5mGqr6tYlJNDG1g10Lx2U/qPzL6zwQ==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/http-errors": {
|
"node_modules/http-errors": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||||
|
|||||||
@ -53,6 +53,7 @@
|
|||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
"framer-motion": "^11.13.1",
|
"framer-motion": "^11.13.1",
|
||||||
|
"hls.js": "^1.6.7",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.453.0",
|
"lucide-react": "^0.453.0",
|
||||||
"memorystore": "^1.6.7",
|
"memorystore": "^1.6.7",
|
||||||
|
|||||||
@ -52,13 +52,12 @@ export class BunnyService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private bunnyVideoToVideo(bunnyVideo: BunnyVideo): Video {
|
private bunnyVideoToVideo(bunnyVideo: BunnyVideo): Video {
|
||||||
// Generate thumbnail URL from Bunny CDN
|
// For private videos, use iframe embed or proxy approach
|
||||||
const thumbnailUrl = bunnyVideo.thumbnailFileName
|
// Generate thumbnail URL from CDN (thumbnails are usually public)
|
||||||
? `https://${this.hostname}/${bunnyVideo.guid}/${bunnyVideo.thumbnailFileName}`
|
const thumbnailUrl = `https://${this.hostname}/${bunnyVideo.guid}/thumbnail.jpg`;
|
||||||
: `https://${this.hostname}/${bunnyVideo.guid}/thumbnail.jpg`;
|
|
||||||
|
|
||||||
// Generate video URL for streaming
|
// For private videos, we'll use an iframe embed URL which handles authentication
|
||||||
const videoUrl = `https://${this.hostname}/${bunnyVideo.guid}/playlist.m3u8`;
|
const videoUrl = `https://iframe.mediadelivery.net/embed/${this.libraryId}/${bunnyVideo.guid}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: bunnyVideo.guid,
|
id: bunnyVideo.guid,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user