fix: populate questions and options tables, scale AI chapters/checkpoints to video duration, open student lesson discovery
This commit is contained in:
@@ -312,10 +312,10 @@ class VideoController
|
|||||||
$lessons = Database::select(
|
$lessons = Database::select(
|
||||||
"SELECT l.id, l.course_id, l.title, l.sequence_order, l.storage_type, l.video_uuid, l.hls_url, l.thumbnail_url,
|
"SELECT l.id, l.course_id, l.title, l.sequence_order, l.storage_type, l.video_uuid, l.hls_url, l.thumbnail_url,
|
||||||
l.duration_seconds, l.is_free_preview, l.created_at,
|
l.duration_seconds, l.is_free_preview, l.created_at,
|
||||||
c.title as course_title,
|
COALESCE(c.title, 'توجيهي 2008 — المنهاج المعتمد') as course_title,
|
||||||
(SELECT COUNT(*) FROM exams WHERE lesson_id = l.id AND scope = 'in_video_checkpoint') as checkpoints_count
|
(SELECT COUNT(*) FROM exams WHERE lesson_id = l.id AND scope = 'in_video_checkpoint') as checkpoints_count
|
||||||
FROM lessons l
|
FROM lessons l
|
||||||
JOIN courses c ON l.course_id = c.id
|
LEFT JOIN courses c ON l.course_id = c.id
|
||||||
{$where}
|
{$where}
|
||||||
ORDER BY l.id DESC"
|
ORDER BY l.id DESC"
|
||||||
);
|
);
|
||||||
@@ -354,9 +354,10 @@ class VideoController
|
|||||||
|
|
||||||
$lessonId = (int)$lesson['id'];
|
$lessonId = (int)$lesson['id'];
|
||||||
|
|
||||||
// If lesson has no checkpoints yet, generate them autonomously
|
// If lesson has no checkpoints or missing questions, generate them autonomously
|
||||||
$existingCount = Database::selectOne("SELECT COUNT(*) as cnt FROM exams WHERE lesson_id = ? AND scope = 'in_video_checkpoint'", [$lessonId]);
|
$existingCount = Database::selectOne("SELECT COUNT(*) as cnt FROM exams WHERE lesson_id = ? AND scope = 'in_video_checkpoint'", [$lessonId]);
|
||||||
if (empty($existingCount['cnt'])) {
|
$existingQuestions = Database::selectOne("SELECT COUNT(*) as cnt FROM questions q JOIN exams e ON q.exam_id = e.id WHERE e.lesson_id = ?", [$lessonId]);
|
||||||
|
if (empty($existingCount['cnt']) || empty($existingQuestions['cnt'])) {
|
||||||
AiVideoAnalyzerService::processLessonAutonomously($lessonId);
|
AiVideoAnalyzerService::processLessonAutonomously($lessonId);
|
||||||
$lesson = Database::selectOne("SELECT * FROM lessons WHERE id = ? LIMIT 1", [$lessonId]);
|
$lesson = Database::selectOne("SELECT * FROM lessons WHERE id = ? LIMIT 1", [$lessonId]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,8 +42,11 @@ class AiVideoAnalyzerService
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Clear old checkpoints for this lesson if regenerating
|
// 2. Clear old checkpoints and their questions for this lesson
|
||||||
Database::query("DELETE FROM exams WHERE lesson_id = ? AND scope = 'in_video_checkpoint'", [$lessonId]);
|
$oldExams = Database::select("SELECT id FROM exams WHERE lesson_id = ? AND scope = 'in_video_checkpoint'", [$lessonId]);
|
||||||
|
foreach ($oldExams as $oe) {
|
||||||
|
Database::query("DELETE FROM exams WHERE id = ?", [$oe['id']]);
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Save Socratic Checkpoints into exams, questions, question_options
|
// 3. Save Socratic Checkpoints into exams, questions, question_options
|
||||||
if (!empty($analysisResult['socratic_checkpoints'])) {
|
if (!empty($analysisResult['socratic_checkpoints'])) {
|
||||||
@@ -57,7 +60,7 @@ class AiVideoAnalyzerService
|
|||||||
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
|
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
|
||||||
);
|
);
|
||||||
|
|
||||||
$examId = Database::insert(
|
$examId = (int)Database::insert(
|
||||||
"INSERT INTO exams (uuid, course_id, lesson_id, creator_type, scope, title, timestamp_seconds, rewind_on_fail_seconds, passing_percentage, total_points, is_mandatory, is_published)
|
"INSERT INTO exams (uuid, course_id, lesson_id, creator_type, scope, title, timestamp_seconds, rewind_on_fail_seconds, passing_percentage, total_points, is_mandatory, is_published)
|
||||||
VALUES (?, ?, ?, 'ai_adaptive', 'in_video_checkpoint', ?, ?, ?, 100.00, 10, 1, 1)",
|
VALUES (?, ?, ?, 'ai_adaptive', 'in_video_checkpoint', ?, ?, ?, 100.00, 10, 1, 1)",
|
||||||
[
|
[
|
||||||
@@ -78,7 +81,7 @@ class AiVideoAnalyzerService
|
|||||||
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
|
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
|
||||||
);
|
);
|
||||||
|
|
||||||
$qId = Database::insert(
|
$qId = (int)Database::insert(
|
||||||
"INSERT INTO questions (uuid, exam_id, question_text, question_type, bloom_taxonomy, explanation_text, points)
|
"INSERT INTO questions (uuid, exam_id, question_text, question_type, bloom_taxonomy, explanation_text, points)
|
||||||
VALUES (?, ?, ?, 'multiple_choice', 'comprehension', ?, 10)",
|
VALUES (?, ?, ?, 'multiple_choice', 'comprehension', ?, 10)",
|
||||||
[
|
[
|
||||||
@@ -90,11 +93,13 @@ class AiVideoAnalyzerService
|
|||||||
);
|
);
|
||||||
|
|
||||||
$correctIdx = (int)($cp['correct_index'] ?? 0);
|
$correctIdx = (int)($cp['correct_index'] ?? 0);
|
||||||
foreach ($cp['options'] as $idx => $optText) {
|
if (!empty($cp['options']) && is_array($cp['options'])) {
|
||||||
Database::insert(
|
foreach ($cp['options'] as $idx => $optText) {
|
||||||
"INSERT INTO question_options (question_id, option_text, is_correct) VALUES (?, ?, ?)",
|
Database::insert(
|
||||||
[$qId, $optText, ($idx === $correctIdx) ? 1 : 0]
|
"INSERT INTO question_options (question_id, option_text, is_correct) VALUES (?, ?, ?)",
|
||||||
);
|
[$qId, (string)$optText, ($idx === $correctIdx) ? 1 : 0]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
error_log("Checkpoint insert error: " . $e->getMessage());
|
error_log("Checkpoint insert error: " . $e->getMessage());
|
||||||
@@ -199,38 +204,48 @@ class AiVideoAnalyzerService
|
|||||||
private static function buildGroundedCurriculumAnalysis(string $title, int $duration, array $curriculum): array
|
private static function buildGroundedCurriculumAnalysis(string $title, int $duration, array $curriculum): array
|
||||||
{
|
{
|
||||||
$isMilitary = ($curriculum['subject'] === 'التربية الوطنية والثقافة العسكرية');
|
$isMilitary = ($curriculum['subject'] === 'التربية الوطنية والثقافة العسكرية');
|
||||||
|
$duration = max(5, $duration);
|
||||||
|
|
||||||
|
// Dynamic Time Segmentation based on real duration
|
||||||
|
if ($duration <= 45) {
|
||||||
|
// Short Video (e.g. 10s AI generated video)
|
||||||
|
$c1 = max(1, (int)round($duration * 0.3));
|
||||||
|
$c2 = max(2, (int)round($duration * 0.7));
|
||||||
|
$cp1 = max(1, (int)round($duration * 0.3));
|
||||||
|
$cp2 = max(2, (int)round($duration * 0.7));
|
||||||
|
$rewind = 3;
|
||||||
|
} else {
|
||||||
|
// Full Length Standard Lesson (e.g. 10 - 45 minutes)
|
||||||
|
$c1 = max(60, (int)round($duration * 0.25));
|
||||||
|
$c2 = max(120, (int)round($duration * 0.60));
|
||||||
|
$cp1 = max(60, (int)round($duration * 0.25));
|
||||||
|
$cp2 = max(120, (int)round($duration * 0.60));
|
||||||
|
$rewind = 45;
|
||||||
|
}
|
||||||
|
|
||||||
if ($isMilitary) {
|
if ($isMilitary) {
|
||||||
return [
|
return [
|
||||||
'timeline_chapters' => [
|
'timeline_chapters' => [
|
||||||
['start_seconds' => 0, 'end_seconds' => 150, 'title' => 'النشأة والتأسيس التاريخي', 'summary' => 'مراحل تشكيل القوات المسلحة الأردنية — الجيش العربي منذ عام 1921.'],
|
['start_seconds' => 0, 'end_seconds' => $c1, 'title' => 'النشأة والتأسيس التاريخي', 'summary' => 'مراحل تشكيل القوات المسلحة الأردنية — الجيش العربي منذ عام 1921.'],
|
||||||
['start_seconds' => 150, 'end_seconds' => 360, 'title' => 'القرار التاريخي لتعريب القيادة (1956)', 'summary' => 'الرؤية الوطنية للملك الحسين بن طلال وإنهاء الانتداب البريطاني.'],
|
['start_seconds' => $c1, 'end_seconds' => $c2, 'title' => 'القرار التاريخي لتعريب القيادة (1956)', 'summary' => 'الرؤية الوطنية للملك الحسين بن طلال وإنهاء الانتداب البريطاني.'],
|
||||||
['start_seconds' => 360, 'end_seconds' => $duration, 'title' => 'معركة الكرامة (1968) والأدوار التنموية', 'summary' => 'تحطيم أسطورة العدو، والمستشفيات الميدانية الإنسانية.']
|
['start_seconds' => $c2, 'end_seconds' => $duration, 'title' => 'معركة الكرامة (1968) والأدوار التنموية', 'summary' => 'تحطيم أسطورة العدو، والمستشفيات الميدانية الإنسانية.']
|
||||||
],
|
],
|
||||||
'socratic_checkpoints' => [
|
'socratic_checkpoints' => [
|
||||||
[
|
[
|
||||||
'timestamp_seconds' => 150,
|
'timestamp_seconds' => $cp1,
|
||||||
'question_text' => 'في أي عام تم تأسيس الجيش العربي الأردني؟',
|
'question_text' => 'في أي عام تم تأسيس الجيش العربي الأردني؟',
|
||||||
'options' => ['عام 1921 في عهد الملك المؤسس عبدالله الأول', 'عام 1956', 'عام 1968', 'عام 1946'],
|
'options' => ['عام 1921 في عهد الملك المؤسس عبدالله الأول', 'عام 1956', 'عام 1968', 'عام 1946'],
|
||||||
'correct_index' => 0,
|
'correct_index' => 0,
|
||||||
'rewind_seconds' => 45,
|
'rewind_seconds' => $rewind,
|
||||||
'explanation' => 'تأسس الجيش العربي عام 1921 مع تأسيس إمارة شرق الأردن.'
|
'explanation' => 'تأسس الجيش العربي عام 1921 مع تأسيس إمارة شرق الأردن.'
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'timestamp_seconds' => 360,
|
'timestamp_seconds' => $cp2,
|
||||||
'question_text' => 'متى تم اتخاذ القرار التاريخي بتعريب قيادة الجيش العربي؟',
|
'question_text' => 'متى تم اتخاذ القرار التاريخي بتعريب قيادة الجيش العربي؟',
|
||||||
'options' => ['1 آذار 1956 بقيادة الملك الحسين بن طلال', '21 آذار 1968', '11 نيسان 1921', '25 أيار 1946'],
|
'options' => ['1 آذار 1956 بقيادة الملك الحسين بن طلال', '21 آذار 1968', '11 نيسان 1921', '25 أيار 1946'],
|
||||||
'correct_index' => 0,
|
'correct_index' => 0,
|
||||||
'rewind_seconds' => 45,
|
'rewind_seconds' => $rewind,
|
||||||
'explanation' => 'صدر قرار تعريب القيادة التاريخي في 1 آذار 1956.'
|
'explanation' => 'صدر قرار تعريب القيادة التاريخي في 1 آذار 1956.'
|
||||||
],
|
|
||||||
[
|
|
||||||
'timestamp_seconds' => max(480, (int)($duration * 0.8)),
|
|
||||||
'question_text' => 'ما هي المعركة التاريخية التي شكلت أول نصر عسكري عربي وحطمت أسطورة الجيش الذي لا يُقهر؟',
|
|
||||||
'options' => ['معركة الكرامة الخالدة (21 آذار 1968)', 'معركة القدس 1948', 'معركة اللطرون', 'معركة باب الواد'],
|
|
||||||
'correct_index' => 0,
|
|
||||||
'rewind_seconds' => 60,
|
|
||||||
'explanation' => 'معركة الكرامة في 21 آذار 1968 هي أول نصر عسكري للجيش العربي.'
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
@@ -239,34 +254,26 @@ class AiVideoAnalyzerService
|
|||||||
// Default: Mathematics (Tawjihi 2008 Scientific)
|
// Default: Mathematics (Tawjihi 2008 Scientific)
|
||||||
return [
|
return [
|
||||||
'timeline_chapters' => [
|
'timeline_chapters' => [
|
||||||
['start_seconds' => 0, 'end_seconds' => 180, 'title' => 'مقدمة المفهوم والتمهيد الهندسي', 'summary' => 'توضيح المعنى الفيزيائي والهندسي لمفهوم المشتقة الأولى وميل المماس.'],
|
['start_seconds' => 0, 'end_seconds' => $c1, 'title' => 'مقدمة المفهوم والتمهيد الهندسي', 'summary' => 'توضيح المعنى الفيزيائي والهندسي لمفهوم المشتقة الأولى وميل المماس.'],
|
||||||
['start_seconds' => 180, 'end_seconds' => 380, 'title' => 'عرض القواعد الأساسية والاشتقاق', 'summary' => 'قواعد اشتقاق كثيرات الحدود والاقترانات المثلثية وحاصل الضرب.'],
|
['start_seconds' => $c1, 'end_seconds' => $c2, 'title' => 'عرض القواعد الأساسية والاشتقاق', 'summary' => 'قواعد اشتقاق كثيرات الحدود والاقترانات المثلثية وحاصل الضرب.'],
|
||||||
['start_seconds' => 380, 'end_seconds' => $duration, 'title' => 'حل المسائل النموذجية والأسئلة الوزارية', 'summary' => 'تطبيق القواعد على مسائل امتحانات الثانوية العامة المعتمدة.']
|
['start_seconds' => $c2, 'end_seconds' => $duration, 'title' => 'حل المسائل النموذجية والأسئلة الوزارية', 'summary' => 'تطبيق القواعد على مسائل امتحانات الثانوية العامة المعتمدة.']
|
||||||
],
|
],
|
||||||
'socratic_checkpoints' => [
|
'socratic_checkpoints' => [
|
||||||
[
|
[
|
||||||
'timestamp_seconds' => 180,
|
'timestamp_seconds' => $cp1,
|
||||||
'question_text' => 'إذا كان الاقتران f(x) = c (اقتران ثابت)، فما هي قيمة مشتقته f\'(x)؟',
|
'question_text' => 'إذا كان الاقتران f(x) = c (اقتران ثابت)، فما هي قيمة مشتقته f\'(x)؟',
|
||||||
'options' => ['f\'(x) = 0 دائماً', 'f\'(x) = c', 'f\'(x) = 1', 'f\'(x) = x'],
|
'options' => ['f\'(x) = 0 دائماً', 'f\'(x) = c', 'f\'(x) = 1', 'f\'(x) = x'],
|
||||||
'correct_index' => 0,
|
'correct_index' => 0,
|
||||||
'rewind_seconds' => 45,
|
'rewind_seconds' => $rewind,
|
||||||
'explanation' => 'مشتقة أي عدد ثابت تساوي صفراً دائماً حسب نص الكتاب المدرسي.'
|
'explanation' => 'مشتقة أي عدد ثابت تساوي صفراً دائماً حسب نص الكتاب المدرسي.'
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'timestamp_seconds' => 360,
|
'timestamp_seconds' => $cp2,
|
||||||
'question_text' => 'إذا كان f(x) = sin(3x)، فما هي قيمة المشتقة f\'(x) وفق قاعدة مشتقات الزوايا؟',
|
'question_text' => 'إذا كان f(x) = sin(3x)، فما هي قيمة المشتقة f\'(x) وفق قاعدة مشتقات الزوايا؟',
|
||||||
'options' => ['3 cos(3x) (مشتقة الزاوية ضرب مشتقة الاقتران)', 'cos(3x)', '-3 cos(3x)', '3 sin(3x)'],
|
'options' => ['3 cos(3x) (مشتقة الزاوية ضرب مشتقة الاقتران)', 'cos(3x)', '-3 cos(3x)', '3 sin(3x)'],
|
||||||
'correct_index' => 0,
|
'correct_index' => 0,
|
||||||
'rewind_seconds' => 45,
|
'rewind_seconds' => $rewind,
|
||||||
'explanation' => 'مشتقة sin(ax) هي a*cos(ax).'
|
'explanation' => 'مشتقة sin(ax) هي a*cos(ax).'
|
||||||
],
|
|
||||||
[
|
|
||||||
'timestamp_seconds' => max(480, (int)($duration * 0.8)),
|
|
||||||
'question_text' => 'ما هي مشتقة حاصل ضرب اقترانين [f(x) * g(x)]\' حسب المنهاج الوزاري؟',
|
|
||||||
'options' => ['الأول في مشتقة الثاني + الثاني في مشتقة الأول', 'مشتقة الأول في مشتقة الثاني', 'الأول في مشتقة الثاني - الثاني في مشتقة الأول', 'مجموع المشتقات فقط'],
|
|
||||||
'correct_index' => 0,
|
|
||||||
'rewind_seconds' => 60,
|
|
||||||
'explanation' => 'قاعدة مشتقة الضرب: [f*g]\' = f*g\' + g*f\'.'
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -207,8 +207,20 @@ class VideoService
|
|||||||
$segmentPattern = $hlsOutputDir . '/segment_%03d.ts';
|
$segmentPattern = $hlsOutputDir . '/segment_%03d.ts';
|
||||||
$thumbnailPath = $hlsOutputDir . '/thumbnail.jpg';
|
$thumbnailPath = $hlsOutputDir . '/thumbnail.jpg';
|
||||||
|
|
||||||
// 1. Generate Thumbnail Screenshot at 2 seconds
|
// Measure real video duration from input file
|
||||||
$thumbCmd = "{$ffmpeg} -ss 00:00:02 -i " . escapeshellarg($sourceMp4Path) . " -vframes 1 -q:v 2 " . escapeshellarg($thumbnailPath) . " -y 2>/dev/null";
|
$durationSeconds = 10;
|
||||||
|
$probeCmd = "{$ffmpeg} -i " . escapeshellarg($sourceMp4Path) . " 2>&1";
|
||||||
|
$probeOutput = @shell_exec($probeCmd);
|
||||||
|
if ($probeOutput && preg_match('/Duration:\s*(\d{2}):(\d{2}):(\d{2})/i', $probeOutput, $m)) {
|
||||||
|
$durationSeconds = ($m[1] * 3600) + ($m[2] * 60) + (int)$m[3];
|
||||||
|
}
|
||||||
|
if ($durationSeconds <= 0) {
|
||||||
|
$durationSeconds = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Generate Thumbnail Screenshot at 1 second (or 00:00:01)
|
||||||
|
$thumbTime = ($durationSeconds > 2) ? '00:00:02' : '00:00:01';
|
||||||
|
$thumbCmd = "{$ffmpeg} -ss {$thumbTime} -i " . escapeshellarg($sourceMp4Path) . " -vframes 1 -q:v 2 " . escapeshellarg($thumbnailPath) . " -y 2>/dev/null";
|
||||||
@shell_exec($thumbCmd);
|
@shell_exec($thumbCmd);
|
||||||
|
|
||||||
// 2. Generate HLS Segments & Playlist (6-second chunks for fast start)
|
// 2. Generate HLS Segments & Playlist (6-second chunks for fast start)
|
||||||
@@ -221,7 +233,7 @@ class VideoService
|
|||||||
return [
|
return [
|
||||||
'hls_url' => file_exists($playlistPath) ? $hlsUrl : null,
|
'hls_url' => file_exists($playlistPath) ? $hlsUrl : null,
|
||||||
'thumbnail_url' => file_exists($thumbnailPath) ? $thumbUrl : null,
|
'thumbnail_url' => file_exists($thumbnailPath) ? $thumbUrl : null,
|
||||||
'duration_seconds' => 600
|
'duration_seconds' => $durationSeconds
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1073,9 +1073,11 @@ class StudentPortal
|
|||||||
}
|
}
|
||||||
initWebSocket(token);
|
initWebSocket(token);
|
||||||
await checkStudentSession(token);
|
await checkStudentSession(token);
|
||||||
await loadStudentLessonsList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always load lessons list and playback
|
||||||
|
await loadStudentLessonsList();
|
||||||
|
|
||||||
// Video player time listener for Socratic checkpoint
|
// Video player time listener for Socratic checkpoint
|
||||||
const video = document.getElementById('lesson_video_player');
|
const video = document.getElementById('lesson_video_player');
|
||||||
if (video) {
|
if (video) {
|
||||||
@@ -1098,11 +1100,9 @@ class StudentPortal
|
|||||||
|
|
||||||
async function loadStudentLessonsList() {
|
async function loadStudentLessonsList() {
|
||||||
const token = localStorage.getItem('saqel_student_jwt');
|
const token = localStorage.getItem('saqel_student_jwt');
|
||||||
if (!token) return;
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/student/lessons', {
|
const headers = token ? { 'Authorization': 'Bearer ' + token } : {};
|
||||||
headers: { 'Authorization': 'Bearer ' + token }
|
const res = await fetch('/api/student/lessons', { headers });
|
||||||
});
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (res.ok && data.data && data.data.length > 0) {
|
if (res.ok && data.data && data.data.length > 0) {
|
||||||
const lessons = data.data;
|
const lessons = data.data;
|
||||||
@@ -1239,12 +1239,10 @@ class StudentPortal
|
|||||||
|
|
||||||
async function loadStudentLessonPlayback(lessonId = 0) {
|
async function loadStudentLessonPlayback(lessonId = 0) {
|
||||||
const token = localStorage.getItem('saqel_student_jwt');
|
const token = localStorage.getItem('saqel_student_jwt');
|
||||||
if (!token) return;
|
|
||||||
try {
|
try {
|
||||||
const targetUrl = lessonId > 0 ? `/api/lessons/${lessonId}/playback` : '/api/lessons/0/playback';
|
const targetUrl = lessonId > 0 ? `/api/lessons/${lessonId}/playback` : '/api/lessons/0/playback';
|
||||||
const res = await fetch(targetUrl, {
|
const headers = token ? { 'Authorization': 'Bearer ' + token } : {};
|
||||||
headers: { 'Authorization': 'Bearer ' + token }
|
const res = await fetch(targetUrl, { headers });
|
||||||
});
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (res.ok && data.status === 'success') {
|
if (res.ok && data.status === 'success') {
|
||||||
const les = data.data.lesson;
|
const les = data.data.lesson;
|
||||||
|
|||||||
@@ -76,10 +76,10 @@ $router->post('/api/teacher/lessons', [\App\Controllers\TeacherController:
|
|||||||
// Dual Video Storage & Cloudflare R2 / HLS Stream Routes (API-Driven)
|
// Dual Video Storage & Cloudflare R2 / HLS Stream Routes (API-Driven)
|
||||||
$router->post('/api/teacher/videos/upload-direct', [\App\Controllers\VideoController::class, 'uploadDirect'], [\App\Middlewares\AuthMiddleware::class]);
|
$router->post('/api/teacher/videos/upload-direct', [\App\Controllers\VideoController::class, 'uploadDirect'], [\App\Middlewares\AuthMiddleware::class]);
|
||||||
$router->post('/api/teacher/lessons/checkpoints', [\App\Controllers\VideoController::class, 'saveCheckpoint'], [\App\Middlewares\AuthMiddleware::class]);
|
$router->post('/api/teacher/lessons/checkpoints', [\App\Controllers\VideoController::class, 'saveCheckpoint'], [\App\Middlewares\AuthMiddleware::class]);
|
||||||
$router->get('/api/student/lessons', [\App\Controllers\VideoController::class, 'getStudentLessons'], [\App\Middlewares\AuthMiddleware::class]);
|
$router->get('/api/student/lessons', [\App\Controllers\VideoController::class, 'getStudentLessons']);
|
||||||
$router->get('/api/videos/stream/{uuid}', [\App\Controllers\VideoController::class, 'streamLocalVideo']);
|
$router->get('/api/videos/stream/{uuid}', [\App\Controllers\VideoController::class, 'streamLocalVideo']);
|
||||||
$router->get('/api/videos/hls/{uuid}/{file}', [\App\Controllers\VideoController::class, 'streamHls']);
|
$router->get('/api/videos/hls/{uuid}/{file}', [\App\Controllers\VideoController::class, 'streamHls']);
|
||||||
$router->get('/api/lessons/{id}/playback', [\App\Controllers\VideoController::class, 'getPlaybackData'], [\App\Middlewares\AuthMiddleware::class]);
|
$router->get('/api/lessons/{id}/playback', [\App\Controllers\VideoController::class, 'getPlaybackData']);
|
||||||
|
|
||||||
// Student & Teacher Chat Routes (API-Driven, Authenticated)
|
// Student & Teacher Chat Routes (API-Driven, Authenticated)
|
||||||
$router->get('/api/chat/conversations', [\App\Controllers\ChatController::class, 'getConversations'], [\App\Middlewares\AuthMiddleware::class]);
|
$router->get('/api/chat/conversations', [\App\Controllers\ChatController::class, 'getConversations'], [\App\Middlewares\AuthMiddleware::class]);
|
||||||
|
|||||||
Reference in New Issue
Block a user