Restored to '551aaee106c3dc6c34e10193d8683fa67e7f9bea'
Replit-Restored-To: 551aaee106
This commit is contained in:
@@ -1,81 +0,0 @@
|
||||
import { Link, useLocation } from "wouter";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Home, List, Heart, User } from "lucide-react";
|
||||
|
||||
export function Navigation() {
|
||||
const [location] = useLocation();
|
||||
|
||||
const navItems = [
|
||||
{ path: "/", label: "Home", icon: Home },
|
||||
{ path: "/playlists", label: "Playlists", icon: List },
|
||||
{ path: "/favorites", label: "Favorites", icon: Heart },
|
||||
];
|
||||
|
||||
return (
|
||||
<nav className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 sticky top-0 z-50">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
<div className="flex items-center space-x-8">
|
||||
<Link href="/">
|
||||
<div className="text-xl font-bold text-blue-600 dark:text-blue-400 cursor-pointer" data-testid="link-logo">
|
||||
VideoStream
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className="hidden md:flex items-center space-x-4">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = location === item.path;
|
||||
|
||||
return (
|
||||
<Link key={item.path} href={item.path}>
|
||||
<Button
|
||||
variant={isActive ? "default" : "ghost"}
|
||||
size="sm"
|
||||
className="flex items-center space-x-2"
|
||||
data-testid={`nav-${item.label.toLowerCase()}`}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
<span>{item.label}</span>
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<User className="w-4 h-4" />
|
||||
<span>Demo User</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile navigation */}
|
||||
<div className="md:hidden pb-4">
|
||||
<div className="flex space-x-2">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = location === item.path;
|
||||
|
||||
return (
|
||||
<Link key={item.path} href={item.path}>
|
||||
<Button
|
||||
variant={isActive ? "default" : "ghost"}
|
||||
size="sm"
|
||||
className="flex-1 flex items-center justify-center space-x-1"
|
||||
data-testid={`nav-mobile-${item.label.toLowerCase()}`}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
<span className="text-xs">{item.label}</span>
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Download, RefreshCw, Trash2 } from "lucide-react";
|
||||
import { type Video } from "@shared/schema";
|
||||
|
||||
interface ThumbnailGeneratorProps {
|
||||
video: Video;
|
||||
onThumbnailGenerated?: (thumbnailUrl: string) => void;
|
||||
}
|
||||
|
||||
export default function ThumbnailGenerator({ video, onThumbnailGenerated }: ThumbnailGeneratorProps) {
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [customTime, setCustomTime] = useState("5");
|
||||
const [generatedThumbnails, setGeneratedThumbnails] = useState<Array<{
|
||||
timestamp: string;
|
||||
url: string;
|
||||
path: string;
|
||||
}>>([]);
|
||||
|
||||
const generateThumbnail = async (timestamp: string) => {
|
||||
setIsGenerating(true);
|
||||
try {
|
||||
const response = await fetch(`/api/thumbnails/${video.id}/generate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
timestamps: [timestamp]
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.thumbnails && data.thumbnails.length > 0) {
|
||||
const newThumbnail = data.thumbnails[0];
|
||||
setGeneratedThumbnails(prev => [...prev, newThumbnail]);
|
||||
|
||||
if (onThumbnailGenerated) {
|
||||
onThumbnailGenerated(newThumbnail.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating thumbnail:', error);
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const generateMultipleThumbnails = async () => {
|
||||
setIsGenerating(true);
|
||||
try {
|
||||
const timestamps = ["5", "15", "30", "60", "90"];
|
||||
const response = await fetch(`/api/thumbnails/${video.id}/generate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
timestamps
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setGeneratedThumbnails(data.thumbnails || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating thumbnails:', error);
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadExistingThumbnails = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/thumbnails/${video.id}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setGeneratedThumbnails(data.thumbnails || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading thumbnails:', error);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
loadExistingThumbnails();
|
||||
}, [video.id]);
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-4xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<RefreshCw className="w-5 h-5" />
|
||||
Ustvarjanje Thumbnail Slik
|
||||
</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Generirajte thumbnail slike iz različnih trenutkov videa "{video.title}"
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Quick generate buttons */}
|
||||
<div className="space-y-4">
|
||||
<Label className="text-base font-medium">Hitro generiranje</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
onClick={generateMultipleThumbnails}
|
||||
disabled={isGenerating}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
{isGenerating ? "Generiranje..." : "Generiraj 5 slik (5s, 15s, 30s, 60s, 90s)"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom time input */}
|
||||
<div className="space-y-4">
|
||||
<Label className="text-base font-medium">Določi čas</Label>
|
||||
<div className="flex gap-2 max-w-sm">
|
||||
<Input
|
||||
type="number"
|
||||
value={customTime}
|
||||
onChange={(e) => setCustomTime(e.target.value)}
|
||||
placeholder="Sekunde (npr. 30)"
|
||||
min="0"
|
||||
max={video.duration || 300}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => generateThumbnail(customTime)}
|
||||
disabled={isGenerating}
|
||||
variant="outline"
|
||||
>
|
||||
Generiraj
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Vnesite čas v sekundah (0 - {video.duration || 300}s)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Generated thumbnails grid */}
|
||||
{generatedThumbnails.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<Label className="text-base font-medium">Generirane slike</Label>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{generatedThumbnails.map((thumbnail, index) => (
|
||||
<div key={index} className="relative group">
|
||||
<div className="aspect-video bg-gray-100 rounded-lg overflow-hidden">
|
||||
<img
|
||||
src={thumbnail.url}
|
||||
alt={`Thumbnail at ${thumbnail.timestamp}s`}
|
||||
className="w-full h-full object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute bottom-2 left-2 bg-black/80 px-2 py-1 rounded text-xs text-white">
|
||||
{thumbnail.timestamp}s
|
||||
</div>
|
||||
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => {
|
||||
const link = document.createElement('a');
|
||||
link.href = thumbnail.url;
|
||||
link.download = `thumbnail-${video.id}-${thumbnail.timestamp}s.jpg`;
|
||||
link.click();
|
||||
}}
|
||||
>
|
||||
<Download className="w-3 h-3" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => {
|
||||
if (onThumbnailGenerated) {
|
||||
onThumbnailGenerated(thumbnail.url);
|
||||
}
|
||||
}}
|
||||
>
|
||||
✓
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Instructions */}
|
||||
<div className="text-sm text-muted-foreground space-y-2 border-l-4 border-blue-500 pl-4">
|
||||
<p><strong>Navodila:</strong></p>
|
||||
<ul className="space-y-1 list-disc list-inside ml-2">
|
||||
<li>Kliknite "Generiraj 5 slik" za hitro ustvarjanje slik iz različnih trenutkov</li>
|
||||
<li>Uporabite "Določi čas" za ustvarjanje slike iz določene sekunde</li>
|
||||
<li>Kliknite ✓ da nastavite sliko kot glavno thumbnail</li>
|
||||
<li>Kliknite ⬇ da prenesete sliko na svoj računalnik</li>
|
||||
</ul>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Heart, Plus, List, Check } from "lucide-react";
|
||||
import { useFavorites } from "@/hooks/use-favorites";
|
||||
import { usePlaylists } from "@/hooks/use-playlists";
|
||||
import type { Video, Playlist } from "@shared/schema";
|
||||
|
||||
interface VideoActionsProps {
|
||||
video: Video;
|
||||
showLabel?: boolean;
|
||||
}
|
||||
|
||||
export function VideoActions({ video, showLabel = false }: VideoActionsProps) {
|
||||
const [isPlaylistDialogOpen, setIsPlaylistDialogOpen] = useState(false);
|
||||
const { useIsFavorited, addToFavorites, removeFromFavorites } = useFavorites();
|
||||
const { useUserPlaylists, addVideoToPlaylist } = usePlaylists();
|
||||
|
||||
const { data: favoriteStatus } = useIsFavorited(video.id);
|
||||
const isFavorited = favoriteStatus?.isFavorited || false;
|
||||
|
||||
const { data: playlists } = useUserPlaylists();
|
||||
const playlistsArray = Array.isArray(playlists) ? playlists : [];
|
||||
|
||||
const handleFavoriteToggle = () => {
|
||||
if (isFavorited) {
|
||||
removeFromFavorites(video.id);
|
||||
} else {
|
||||
addToFavorites(video.id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddToPlaylist = (playlistId: string) => {
|
||||
addVideoToPlaylist({ playlistId, videoId: video.id });
|
||||
setIsPlaylistDialogOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
{/* Favorite Button */}
|
||||
<Button
|
||||
size="sm"
|
||||
variant={isFavorited ? "default" : "outline"}
|
||||
onClick={handleFavoriteToggle}
|
||||
className={`${
|
||||
isFavorited
|
||||
? "bg-red-500 hover:bg-red-600 text-white"
|
||||
: "hover:bg-red-50 hover:text-red-500 hover:border-red-500"
|
||||
}`}
|
||||
data-testid={`button-favorite-${video.id}`}
|
||||
>
|
||||
<Heart
|
||||
className="w-4 h-4"
|
||||
fill={isFavorited ? "currentColor" : "none"}
|
||||
/>
|
||||
{showLabel && (
|
||||
<span className="ml-2">
|
||||
{isFavorited ? "Favorited" : "Add to Favorites"}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Add to Playlist Button */}
|
||||
<Dialog open={isPlaylistDialogOpen} onOpenChange={setIsPlaylistDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="hover:bg-blue-50 hover:text-blue-600 hover:border-blue-500"
|
||||
data-testid={`button-add-to-playlist-${video.id}`}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
{showLabel && <span className="ml-2">Add to Playlist</span>}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add to Playlist</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Select a playlist to add "{video.title}" to:
|
||||
</div>
|
||||
|
||||
{playlistsArray.length > 0 ? (
|
||||
<div className="grid gap-3 max-h-96 overflow-y-auto">
|
||||
{playlistsArray.map((playlist: Playlist) => (
|
||||
<Card
|
||||
key={playlist.id}
|
||||
className="cursor-pointer hover:shadow-md transition-shadow"
|
||||
onClick={() => handleAddToPlaylist(playlist.id)}
|
||||
data-testid={`card-playlist-option-${playlist.id}`}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="font-medium">{playlist.name}</h4>
|
||||
{playlist.description && (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
{playlist.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Plus className="w-5 h-5 text-gray-400" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<List className="w-12 h-12 mx-auto text-gray-400 mb-4" />
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||
You don't have any playlists yet.
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsPlaylistDialogOpen(false);
|
||||
// You could navigate to /playlists here
|
||||
}}
|
||||
data-testid="button-create-first-playlist-from-video"
|
||||
>
|
||||
Create Your First Playlist
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Play, Share2 } from "lucide-react";
|
||||
import { type Video } from "@shared/schema";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { VideoActions } from "@/components/video-actions";
|
||||
|
||||
interface VideoCardProps {
|
||||
video: Video;
|
||||
@@ -62,21 +61,6 @@ export default function VideoCard({ video, onClick, onShare }: VideoCardProps) {
|
||||
alt={video.title}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
data-testid={`img-thumbnail-${video.id}`}
|
||||
onLoad={() => {
|
||||
console.log('Thumbnail loaded successfully:', video.thumbnailUrl);
|
||||
}}
|
||||
onError={(e) => {
|
||||
console.error('Thumbnail failed to load:', video.thumbnailUrl);
|
||||
// Use a data URL SVG as fallback
|
||||
const target = e.target as HTMLImageElement;
|
||||
const fallbackSvg = `
|
||||
<svg width="400" height="225" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="400" height="225" fill="#dc2626"/>
|
||||
<text x="200" y="112" text-anchor="middle" fill="white" font-family="Arial" font-size="16">Thumbnail Error</text>
|
||||
</svg>
|
||||
`;
|
||||
target.src = 'data:image/svg+xml;base64,' + btoa(fallbackSvg);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="absolute inset-0 bg-black/20 group-hover:bg-black/40 transition-colors duration-300 flex items-center justify-center">
|
||||
@@ -122,20 +106,17 @@ export default function VideoCard({ video, onClick, onShare }: VideoCardProps) {
|
||||
{formatDate(video.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<VideoActions video={video} />
|
||||
{onShare && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleShareClick}
|
||||
className="text-bunny-muted hover:text-bunny-blue p-1 h-auto"
|
||||
data-testid={`button-share-bottom-${video.id}`}
|
||||
>
|
||||
<Share2 className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{onShare && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleShareClick}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity text-bunny-muted hover:text-bunny-blue p-1 h-auto"
|
||||
data-testid={`button-share-bottom-${video.id}`}
|
||||
>
|
||||
<Share2 className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { X, Settings } from "lucide-react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { X } from "lucide-react";
|
||||
import { type Video } from "@shared/schema";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import Hls from "hls.js";
|
||||
import ThumbnailGenerator from "./thumbnail-generator";
|
||||
|
||||
interface VideoModalProps {
|
||||
video: Video | null;
|
||||
@@ -49,7 +47,6 @@ 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 [activeTab, setActiveTab] = useState("video");
|
||||
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
@@ -71,14 +68,74 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
// Since we're now using iframe embeds for Bunny.net videos, we don't need HLS.js
|
||||
// Initialize HLS when video is available
|
||||
useEffect(() => {
|
||||
// Just log for debugging
|
||||
if (isOpen && video) {
|
||||
console.log('Video modal opened with:', video.videoUrl);
|
||||
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 if needed
|
||||
|
||||
// Cleanup when modal closes
|
||||
return () => {
|
||||
if (hlsRef.current) {
|
||||
hlsRef.current.destroy();
|
||||
@@ -127,21 +184,21 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps)
|
||||
<iframe
|
||||
src={video.videoUrl}
|
||||
className="w-full h-auto max-h-[80vh] aspect-video"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture; fullscreen;"
|
||||
allowFullScreen={true}
|
||||
allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;"
|
||||
data-testid="video-player-iframe"
|
||||
></iframe>
|
||||
frameBorder="0"
|
||||
onLoad={handleVideoPlay}
|
||||
data-testid="video-iframe"
|
||||
/>
|
||||
) : (
|
||||
<video
|
||||
ref={videoRef}
|
||||
className="w-full h-auto max-h-[80vh] aspect-video"
|
||||
className="w-full h-auto max-h-[80vh]"
|
||||
controls
|
||||
preload="metadata"
|
||||
onPlay={handleVideoPlay}
|
||||
data-testid="video-player"
|
||||
crossOrigin="anonymous"
|
||||
src={video.videoUrl}
|
||||
>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
|
||||
Reference in New Issue
Block a user