feat: complete platform operations roadmap
This commit is contained in:
@@ -24,6 +24,32 @@ import { LoginModal } from './LoginModal';
|
||||
import { MaterialCard } from './MaterialCard';
|
||||
import { courseApi, LessonPlayback } from '../services/api';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
playerjs?: {
|
||||
Player: new (element: HTMLIFrameElement) => {
|
||||
on: (event: string, listener: (data?: { seconds?: number; duration?: number }) => void) => void;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let bunnyPlayerJsPromise: Promise<void> | null = null;
|
||||
|
||||
const loadBunnyPlayerJs = () => {
|
||||
if (window.playerjs) return Promise.resolve();
|
||||
if (bunnyPlayerJsPromise) return bunnyPlayerJsPromise;
|
||||
bunnyPlayerJsPromise = new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://assets.mediadelivery.net/playerjs/player-0.1.0.min.js';
|
||||
script.async = true;
|
||||
script.onload = () => resolve();
|
||||
script.onerror = () => reject(new Error('Bunny player events could not be loaded'));
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
return bunnyPlayerJsPromise;
|
||||
};
|
||||
|
||||
interface CoursePlayerModalProps {
|
||||
course: Course | null;
|
||||
onClose: () => void;
|
||||
@@ -43,6 +69,7 @@ export const CoursePlayerModal: React.FC<CoursePlayerModalProps> = ({ course, on
|
||||
const [playbackError, setPlaybackError] = useState('');
|
||||
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const bunnyFrameRef = useRef<HTMLIFrameElement>(null);
|
||||
const lastProgressSave = useRef<Record<string, number>>({});
|
||||
|
||||
// Load course data and this learner's saved state from PostgreSQL.
|
||||
@@ -162,6 +189,31 @@ export const CoursePlayerModal: React.FC<CoursePlayerModalProps> = ({ course, on
|
||||
}
|
||||
};
|
||||
|
||||
// Bunny's iframe does not expose an HTMLVideoElement. Player.js receives
|
||||
// Bunny's trusted playback events and sends the same progress updates used
|
||||
// for external video providers.
|
||||
useEffect(() => {
|
||||
if (!playback || playback.kind !== 'embed' || !currentLesson || !user) return;
|
||||
let disposed = false;
|
||||
const lessonId = currentLesson.id;
|
||||
void loadBunnyPlayerJs().then(() => {
|
||||
if (disposed || !bunnyFrameRef.current || !window.playerjs) return;
|
||||
const player = new window.playerjs.Player(bunnyFrameRef.current);
|
||||
player.on('timeupdate', (data) => {
|
||||
const watchedSeconds = Math.floor(data?.seconds || 0);
|
||||
if (disposed || watchedSeconds <= 0 || watchedSeconds - (lastProgressSave.current[lessonId] || 0) < 30) return;
|
||||
lastProgressSave.current[lessonId] = watchedSeconds;
|
||||
setWatchedSecondsByLesson((current) => ({ ...current, [lessonId]: watchedSeconds }));
|
||||
saveLessonProgress(lessonId, watchedSeconds).catch(() => undefined);
|
||||
});
|
||||
player.on('ended', (data) => {
|
||||
if (disposed) return;
|
||||
markLessonCompleted(lessonId, Math.floor(data?.duration || data?.seconds || 0));
|
||||
});
|
||||
}).catch(() => undefined);
|
||||
return () => { disposed = true; };
|
||||
}, [playback, currentLesson, user, completedLessonIds]);
|
||||
|
||||
const restoreWatchPosition = () => {
|
||||
if (!currentLesson || !videoRef.current) return;
|
||||
const watchedSeconds = watchedSecondsByLesson[currentLesson.id] || 0;
|
||||
@@ -267,6 +319,7 @@ export const CoursePlayerModal: React.FC<CoursePlayerModalProps> = ({ course, on
|
||||
playback.kind === 'embed' && playback.embedUrl ? (
|
||||
<iframe
|
||||
key={currentLesson?.id}
|
||||
ref={bunnyFrameRef}
|
||||
src={playback.embedUrl}
|
||||
title={currentLesson?.title || 'Vídeo da aula'}
|
||||
className="w-full h-full border-0 rounded-[8px]"
|
||||
|
||||
Reference in New Issue
Block a user