Представьте: студент на слабом интернете пытается досмотреть видеоурок, но плеер тормозит, а после перемотки прогресс обнуляется — приходится пересматривать заново. Именно с такими проблемами сталкиваются многие LMS, собранные без продуманной архитектуры. Разработка системы уроков — это комплексная задача: нужно обеспечить стабильный трекинг для видео, текста и аудио, адаптивную доставку контента и отказоустойчивость. Мы разрабатываем систему уроков, где каждый тип контента имеет надёжный трекинг и адаптивную доставку. На основе опыта 50+ учебных порталов мы выделили ключевые технические проблемы: хрупкий трекинг при перемотке видео, долгая загрузка больших файлов, отсутствие синхронизации аудио с транскрипцией. Ниже — как мы их решаем.
Какие технические проблемы решает система уроков?
Трекинг прогресса видео — пользователь может мотать назад, и прогресс должен фиксировать только максимальную позицию. Мы сохраняем её каждые 10% и завершаем урок при 90%+ просмотра, а не только при окончании.
Транскодирование больших файлов — прямой MP4 тормозит на медленном интернете. Мы перегоняем видео в HLS с несколькими битрейтами (360p–1080p) и используем фоновые воркеры (Bull) для асинхронной обработки.
Текстовые уроки без отметки о прочтении — просто пролистать до конца недостаточно. Мы используем Intersection Observer для детекции 90% прокрутки и автоматически завершаем урок.
Аудио без синхронизации — слушатель теряет нить без текста. Мы выводим транскрипцию рядом с плеером и подсвечиваем текущее слово (опционально).
Разработка системы уроков: как обеспечить надёжный трекинг прогресса?
Ключевая сложность — пользователь может перематывать назад, но прогресс должен только расти. В компоненте ReactPlayer мы запоминаем максимальное значение played (0–1) и сохраняем его каждые 10%.
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 maxPlayedRef = useRef(lesson.progress?.progress ?? 0);
const handleProgress = useCallback(async ({ played: p }) => {
setPlayed(p);
if (p > maxPlayedRef.current) {
maxPlayedRef.current = p;
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]);
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>
);
}
Почему мы используем HLS вместо прямого MP4?
Прямой MP4 загружается целиком — при плохом соединении пользователь ждёт минуты. HLS режет видео на 6-секундные сегменты: плеер начинает показ сразу, переключая качество в зависимости от скорости. HLS начинает воспроизведение в 3-10 раз быстрее MP4, а в условиях слабого интернета разница достигает 15 раз. Мы транскодируем загруженное видео через ffmpeg в фоне.
Стандарт HTTP Live Streaming описывает адаптивную передачу видео и обеспечивает плавное воспроизведение.
import { createReadStream } from 'fs';
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',
'-level 3.0',
'-start_number 0',
'-hls_time 6',
'-hls_list_size 0',
'-f hls',
])
.output(path.join(outputDir, 'index.m3u8'))
.outputOptions([
'-vf scale=-2:720',
'-b:v 2500k',
])
.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`;
await job.progress(10);
const hlsPath = await transcodeToHLS(uploadedPath, outputDir);
await job.progress(80);
const url = await uploadHLSToS3(outputDir, `lessons/${lessonId}`);
await db.lessons.update(lessonId, {
content: { videoUrl: url, status: 'ready' }
});
await job.progress(100);
});
Подробнее о фоновом транскодировании
Мы используем очередь Bull для асинхронной обработки видео. После загрузки файла на сервер задача попадает в очередь, и рабочий процесс транскодирует видео в HLS через ffmpeg. Это не блокирует основной поток и позволяет масштабировать обработку на несколько воркеров. Время транскодирования зависит от размера видео: 10-минутный ролик в 1080p обрабатывается около 5 минут.Текстовые и аудиоуроки
Как работает Intersection Observer для фиксации прочтения?
Для текстовых уроков используем редактор Tiptap (на базе ProseMirror). Контент хранится в HTML или Markdown. Чтобы засчитать прочтение, отслеживаем прокрутку до конца через Intersection Observer.
import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Image from '@tiptap/extension-image';
import { Youtube } from '@tiptap/extension-youtube';
function TextLessonViewer({ content, onComplete }) {
const editor = useEditor({
content: content.body,
editable: false,
extensions: [StarterKit, Image, Youtube],
});
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) onComplete?.();
},
{ threshold: 0.9 }
);
const marker = containerRef.current?.querySelector('.lesson-end-marker');
if (marker) observer.observe(marker);
return () => observer.disconnect();
}, []);
return (
<div ref={containerRef} className="prose prose-lg max-w-none">
<EditorContent editor={editor} />
{content.attachments?.length > 0 && (
<div className="mt-8 border-t pt-6">
<h3 className="font-semibold">Материалы урока</h3>
<ul className="space-y-2 mt-3">
{content.attachments.map(att => (
<li key={att.url}>
<a href={att.url} download className="flex items-center gap-2 text-blue-600 hover:underline">
<span>📎</span>
<span>{att.name}</span>
<span className="text-gray-400 text-sm">({formatFileSize(att.size)})</span>
</a>
</li>
))}
</ul>
</div>
)}
<div className="lesson-end-marker h-1" />
</div>
);
}
Аудиоуроки с транскрипцией
Аудиоуроки проще: используем нативный audio с кастомным оформлением. Транскрипция выводится под плеером, что помогает слабослышащим и тем, кто не может включить звук.
function AudioLesson({ lesson, onComplete }) {
const audioRef = useRef<HTMLAudioElement>(null);
const [currentTime, setCurrentTime] = useState(0);
return (
<div className="space-y-6">
<div className="bg-gradient-to-br from-purple-50 to-blue-50 rounded-2xl p-8">
{lesson.content.thumbnailUrl && (
<img src={lesson.content.thumbnailUrl}
className="w-32 h-32 rounded-full mx-auto mb-6 object-cover" alt="" />
)}
<audio
ref={audioRef}
src={lesson.content.audioUrl}
controls
className="w-full"
onTimeUpdate={() => setCurrentTime(audioRef.current?.currentTime ?? 0)}
onEnded={onComplete}
/>
</div>
{lesson.content.transcript && (
<div className="prose">
<h3>Транскрипция</h3>
<p className="text-gray-700 whitespace-pre-line">
{lesson.content.transcript}
</p>
</div>
)}
</div>
);
}
Сравнение форматов
| Тип | Рендеринг | Прогресс | Хранение | Сложность |
|---|---|---|---|---|
| Видео | ReactPlayer (HLS) | 90% просмотра или конец | HLS-сегменты в S3 | Высокая (транскодинг) |
| Текст | Tiptap (HTML/MD) | 90% прокрутки | База данных (JSON) | Средняя (редактор) |
| Аудио | Нативный audio | 90% прослушивания | MP3 в S3 | Низкая |
При переходе на HLS вы экономите до 50% трафика по сравнению с MP4, а использование адаптивного битрейта снижает расходы на полосу пропускания на 30-50%.
| Характеристика | HLS | MP4 |
|---|---|---|
| Адаптивный битрейт | Да | Нет |
| Время начала воспроизведения | 1-3 секунды | 10+ секунд |
| Экономия трафика | До 50% по сравнению с MP4 | Неэффективен |
| Поддержка субтитров | Да | Только встроенные |
| Поддержка браузеров | Все современные | Все |
Процесс работы и сроки
- Аналитика — изучаем ваши требования: типы контента, необходимое количество уроков, интеграции (SCORM, SSO).
- Проектирование — рисуем архитектуру: модели данных, API-эндпоинты, компоненты фронтенда.
- Реализация — пишем код с нуля или на основе вашей CMS. Используем TypeScript, React, Next.js, Laravel (или ваш стек).
- Тестирование — проверяем трекинг на всех типах контента, эмулируем медленный интернет, тестируем загрузку больших файлов.
- Деплой — разворачиваем на вашем хостинге или нашем инфраструктуре. Даём доступ к исходному коду и документации.
Ориентировочные сроки: от 1 до 2 недель на базовую систему с видео (HLS), текстом и аудио. Если нужны дополнительные типы (SCORM, live-уроки) или сложная аналитика — срок обговаривается отдельно. Стоимость рассчитывается индивидуально. Закажите разработку системы уроков – мы подготовим индивидуальное коммерческое предложение в течение 24 часов.
Что входит и гарантии
- Исходный код системы уроков (фронтенд + бэкенд)
- API для интеграции с существующей LMS
- Документация по развёртыванию и доработке
- Консоль администрирования уроков (загрузка, редактирование, порядок)
- Поддержка после запуска: 2 недели бесплатно
Более 5 лет мы разрабатываем LMS для школ, университетов и корпоративных порталов. Все проекты сдаём в срок и сопровождаем до полной стабильности. Гарантируем, что трекинг будет работать при любых действиях пользователя (перемотка, пауза, повтор). Получите бесплатную консультацию по вашему проекту – свяжитесь с нами. Это бесплатно.







