Розроблення системи уроків у LMS (текст, відео, аудіо)
Система уроків — це ядро LMS. Кожен урок рендериться за своїм типом: відео з плеєром та субтитрами, текст з форматуванням та вбудованими медіа, аудіо з транскрипціями. Плюс трекинг — скільки переглянуто, коли завершено.
Типи уроків та дані
interface VideoLessonContent {
videoUrl: string; // HLS-потік або прямий mp4
duration: number; // секунди
subtitles: Array<{ lang: string; label: string; url: string }>;
chapters?: Array<{ title: string; time: number }>;
}
interface TextLessonContent {
body: string; // HTML або Markdown
estimatedMinutes: number;
attachments?: Array<{ name: string; url: string; size: number }>;
}
interface AudioLessonContent {
audioUrl: string;
duration: number;
transcript?: string; // текстова розшифровка
}
Відеоплеєр з трекингом переглядів
import ReactPlayer from 'react-player';
import { useState, useRef, useCallback } from 'react';
function VideoLesson({ lesson, enrollmentId, onComplete }) {
const playerRef = useRef<ReactPlayer>(null);
const [played, setPlayed] = useState(0);
const [completed, setCompleted] = useState(lesson.progress?.completed ?? false);
const handleProgress = useCallback(async ({ played: p }) => {
setPlayed(p);
// Зберегти кожні 10% прогресу
if (Math.floor(p * 10) > Math.floor((p - 0.01) * 10)) {
await saveProgress(enrollmentId, lesson.id, p);
}
}, []);
const handleEnded = useCallback(async () => {
if (!completed) {
setCompleted(true);
await fetch(`/api/enrollments/${enrollmentId}/lessons/${lesson.id}/complete`, {
method: 'POST',
});
onComplete?.(lesson.id);
}
}, [completed]);
// Автозавершення при 90%+ переглядів
const handleProgress2 = useCallback(({ played: p }) => {
handleProgress({ played: p });
if (p >= 0.9 && !completed) {
handleEnded();
}
}, [handleProgress, handleEnded, completed]);
return (
<div className="space-y-4">
<div className="relative aspect-video bg-black rounded-xl overflow-hidden">
<ReactPlayer
ref={playerRef}
url={lesson.content.videoUrl}
width="100%"
height="100%"
controls
onProgress={handleProgress2}
onEnded={handleEnded}
config={{
file: {
tracks: lesson.content.subtitles.map(s => ({
kind: 'subtitles',
src: s.url,
srcLang: s.lang,
label: s.label,
})),
},
}}
/>
</div>
{lesson.content.chapters?.length > 0 && (
<div>
<h3 className="font-semibold text-gray-800 mb-2">Зміст</h3>
<ul className="space-y-1">
{lesson.content.chapters.map((ch, i) => (
<li key={i}>
<button
onClick={() => playerRef.current?.seekTo(ch.time)}
className="text-sm text-blue-600 hover:underline"
>
{formatTime(ch.time)} — {ch.title}
</button>
</li>
))}
</ul>
</div>
)}
{completed && (
<div className="flex items-center gap-2 text-green-600 text-sm">
<span>✓</span> Урок завершено
</div>
)}
</div>
);
}
Завантаження та перекодування відео
Прямий MP4 погано працює для великих файлів та повільного інтернету. Використовуйте HLS:
import ffmpeg from 'fluent-ffmpeg';
async function transcodeToHLS(inputPath: string, outputDir: string): Promise<string> {
await fs.promises.mkdir(outputDir, { recursive: true });
return new Promise((resolve, reject) => {
ffmpeg(inputPath)
.outputOptions([
'-profile:v baseline',
'-hls_time 6', // 6 секунд на сегмент
'-f hls',
])
.output(path.join(outputDir, 'index.m3u8'))
.on('end', () => resolve(`${outputDir}/index.m3u8`))
.on('error', reject)
.run();
});
}
videoProcessingQueue.process(async (job) => {
const { uploadedPath, lessonId } = job.data;
const outputDir = `storage/lessons/${lessonId}/hls`;
const hlsPath = await transcodeToHLS(uploadedPath, outputDir);
const url = await uploadHLSToS3(outputDir, `lessons/${lessonId}`);
await db.lessons.update(lessonId, {
content: { videoUrl: url, status: 'ready' }
});
});
Текстовий урок з редактором
import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Image from '@tiptap/extension-image';
function TextLessonViewer({ content, onComplete }) {
const editor = useEditor({
content: content.body,
editable: false,
extensions: [StarterKit, Image],
});
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleScroll = () => {
if (containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
const scrolled = window.innerHeight - rect.bottom > 0;
if (scrolled) onComplete?.();
}
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [onComplete]);
return <EditorContent editor={editor} ref={containerRef} />;
}
Строки виконання
Відеоурок з плеєром та трекингом — 3–5 днів. Текстові/аудіоуроки з редактором та перекодуванням — 5–7 днів. Повна система уроків — 2–3 тижні.







