feat: integrate Bunny Stream course videos
This commit is contained in:
@@ -22,6 +22,7 @@ import { useAuth } from '../context/AuthContext';
|
||||
import { getComments, getLessonProgress, saveComment, saveLessonCompletion, saveLessonProgress, incrementViews } from '../services/db';
|
||||
import { LoginModal } from './LoginModal';
|
||||
import { MaterialCard } from './MaterialCard';
|
||||
import { courseApi, LessonPlayback } from '../services/api';
|
||||
|
||||
interface CoursePlayerModalProps {
|
||||
course: Course | null;
|
||||
@@ -38,6 +39,8 @@ export const CoursePlayerModal: React.FC<CoursePlayerModalProps> = ({ course, on
|
||||
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'playlist' | 'materials' | 'discussion'>('playlist');
|
||||
const [copiedLink, setCopiedLink] = useState(false);
|
||||
const [playback, setPlayback] = useState<LessonPlayback | null>(null);
|
||||
const [playbackError, setPlaybackError] = useState('');
|
||||
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const lastProgressSave = useRef<Record<string, number>>({});
|
||||
@@ -70,6 +73,27 @@ export const CoursePlayerModal: React.FC<CoursePlayerModalProps> = ({ course, on
|
||||
}
|
||||
}, [course, user]);
|
||||
|
||||
// The API verifies the session before returning a time-limited Bunny embed
|
||||
// URL. A video provider key is never sent to the browser.
|
||||
useEffect(() => {
|
||||
if (!course || !user || !currentLesson) {
|
||||
setPlayback(null);
|
||||
setPlaybackError('');
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
setPlayback(null);
|
||||
setPlaybackError('');
|
||||
courseApi.playback(course.id, currentLesson.id)
|
||||
.then((response) => {
|
||||
if (!cancelled) setPlayback(response.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!cancelled) setPlaybackError(error instanceof Error ? error.message : 'Não foi possível carregar este vídeo.');
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [course, user, currentLesson]);
|
||||
|
||||
// Persist completed lessons and recalculate progress
|
||||
const updateCompletedLessons = (newCompleted: string[]) => {
|
||||
setCompletedLessonIds(newCompleted);
|
||||
@@ -239,19 +263,38 @@ export const CoursePlayerModal: React.FC<CoursePlayerModalProps> = ({ course, on
|
||||
{/* Video Player Section */}
|
||||
<div className="w-full aspect-video bg-black rounded-[8px] overflow-hidden relative shadow-xl border border-white/10 group">
|
||||
{canWatch ? (
|
||||
<video
|
||||
key={currentLesson?.id}
|
||||
ref={videoRef}
|
||||
src={currentLesson?.videoUrl}
|
||||
className="w-full h-full object-contain rounded-[8px]"
|
||||
controls
|
||||
autoPlay
|
||||
onTimeUpdate={handleTimeUpdate}
|
||||
onEnded={handleVideoEnded}
|
||||
onLoadedMetadata={restoreWatchPosition}
|
||||
>
|
||||
Seu navegador não suporta a tag de vídeo.
|
||||
</video>
|
||||
playback ? (
|
||||
playback.kind === 'embed' && playback.embedUrl ? (
|
||||
<iframe
|
||||
key={currentLesson?.id}
|
||||
src={playback.embedUrl}
|
||||
title={currentLesson?.title || 'Vídeo da aula'}
|
||||
className="w-full h-full border-0 rounded-[8px]"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; picture-in-picture; fullscreen"
|
||||
allowFullScreen
|
||||
/>
|
||||
) : playback.source ? (
|
||||
<video
|
||||
key={currentLesson?.id}
|
||||
ref={videoRef}
|
||||
src={playback.source}
|
||||
className="w-full h-full object-contain rounded-[8px]"
|
||||
controls
|
||||
autoPlay
|
||||
onTimeUpdate={handleTimeUpdate}
|
||||
onEnded={handleVideoEnded}
|
||||
onLoadedMetadata={restoreWatchPosition}
|
||||
>
|
||||
Seu navegador não suporta a tag de vídeo.
|
||||
</video>
|
||||
) : (
|
||||
<div className="absolute inset-0 flex items-center justify-center text-sm text-gray-400">Vídeo indisponível.</div>
|
||||
)
|
||||
) : (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center text-center p-6 text-sm text-gray-400">
|
||||
{playbackError ? <span className="text-amber-300">{playbackError}</span> : <span>Carregando vídeo seguro...</span>}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
// Locked State
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-zinc-950/90 backdrop-blur-md z-10 text-center p-6 rounded-[8px]">
|
||||
|
||||
Reference in New Issue
Block a user