Разработка системы уроков (текст, видео, аудио) для LMS

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка системы уроков (текст, видео, аудио) для LMS
Средняя
~2-4 недели
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

Разработка системы уроков в LMS (текст, видео, аудио)

Система уроков — это ядро LMS. Каждый урок рендерится по своему типу: видео с плеером и субтитрами, текст с форматированием и встроенными медиа, аудио с транскрипцией. Плюс трекинг — сколько просмотрено, когда завершён.

Типы уроков и их данные

type LessonType = 'video' | 'text' | 'audio' | 'scorm' | 'live';

interface VideoLessonContent {
  videoUrl: string;        // HLS-поток или прямой mp4
  thumbnailUrl?: string;
  duration: number;        // секунды
  subtitles: Array<{ lang: string; label: string; url: string }>;
  chapters?: Array<{ title: string; time: number }>;
  quality?: Array<{ label: string; url: string }>;  // 360p, 720p, 1080p
}

interface TextLessonContent {
  body: string;           // HTML или Markdown
  estimatedMinutes: number;
  attachments?: Array<{ name: string; url: string; size: number }>;
}

interface AudioLessonContent {
  audioUrl: string;
  duration: number;
  transcript?: string;    // текстовая расшифровка
  thumbnailUrl?: 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);    // 0–1
  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;
      // Сохранить каждые 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 { 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',         // 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();
  });
}

// Обработка загрузки через Bull воркер
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);
  // Загрузить на S3
  const url = await uploadHLSToS3(outputDir, `lessons/${lessonId}`);

  await db.lessons.update(lessonId, {
    content: { videoUrl: url, status: 'ready' }
  });

  await job.progress(100);
});

Текстовый урок с редактором

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>
  );
}

Аудиоурок с транскрипцией

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>
  );
}

Сроки

Система уроков с видео (HLS), текстом (Tiptap) и аудио, трекингом прогресса — 1–1.5 недели.