feat: Implement Server-Side FFmpeg HLS Transcoding (.m3u8), HLS.js streaming, and dynamic Socratic In-Video Checkpoint Engine

This commit is contained in:
Hamza-Ayed
2026-08-28 01:30:56 +03:00
parent 8044f86798
commit 54ceab1aae
5 changed files with 420 additions and 85 deletions
+62 -30
View File
@@ -20,6 +20,9 @@ class StudentPortal
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Alexandria:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
<!-- HLS.js for Server-Side Adaptive Video Stream -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.4.12/hls.min.js"></script>
<!-- Apple-Grade Luxury Cupertino CSS -->
<style>
:root {
@@ -617,18 +620,18 @@ class StudentPortal
<span style="font-size: 11px; font-weight: 800; color: var(--accent-gold); background: rgba(245, 158, 11, 0.15); border: 1px solid rgba(245, 158, 11, 0.4); padding: 4px 14px; border-radius: 980px; margin-bottom: 14px;">
⚠️ توقف الشرح لفحص الفهم اللحظي (Socratic Active Recall)
</span>
<h3 style="font-size: 18px; font-weight: 800; color: #FFFFFF; margin-bottom: 18px; max-width: 580px;">
<h3 id="socratic_question_title" style="font-size: 18px; font-weight: 800; color: #FFFFFF; margin-bottom: 18px; max-width: 580px; text-align: center;">
إذا كان f(x) = sin(3x)، فما هي مشتقة الاقتران f'(x)؟
</h3>
<div style="width: 100%; display: flex; flex-direction: column; align-items: center;">
<button type="button" class="quiz-option-btn" onclick="handleCheckpointAnswer(this, true)">
<div id="socratic_options_container" style="width: 100%; display: flex; flex-direction: column; align-items: center; gap: 8px;">
<button type="button" class="quiz-option-btn" onclick="handleCheckpointAnswer(this, true, 45)">
أ) 3 cos(3x) (مشتقة الزاوية ضرب مشتقة الاقتران)
</button>
<button type="button" class="quiz-option-btn" onclick="handleCheckpointAnswer(this, false)">
<button type="button" class="quiz-option-btn" onclick="handleCheckpointAnswer(this, false, 45)">
ب) cos(3x)
</button>
<button type="button" class="quiz-option-btn" onclick="handleCheckpointAnswer(this, false)">
<button type="button" class="quiz-option-btn" onclick="handleCheckpointAnswer(this, false, 45)">
ج) -3 cos(3x)
</button>
</div>
@@ -839,6 +842,10 @@ class StudentPortal
}
];
let activeLessonCheckpoints = [];
let triggeredCheckpoints = new Set();
let hlsInstance = null;
document.addEventListener('DOMContentLoaded', async () => {
const token = localStorage.getItem('saqel_student_jwt');
const cachedUser = localStorage.getItem('saqel_student_user');
@@ -851,6 +858,7 @@ class StudentPortal
}
initWebSocket(token);
await checkStudentSession(token);
await loadStudentLessonPlayback(1);
}
// Video player time listener for Socratic checkpoint
@@ -862,11 +870,12 @@ class StudentPortal
const timeEl = document.getElementById('video_time_display');
if (timeEl) timeEl.textContent = `${formatTime(cur)} / ${formatTime(dur)}`;
// Trigger checkpoint at second 15 automatically once
if (cur === 15 && !checkpointTriggered) {
checkpointTriggered = true;
// Check if current timestamp hits an active Socratic checkpoint
const activeCp = activeLessonCheckpoints.find(cp => cp.timestamp_seconds === cur);
if (activeCp && !triggeredCheckpoints.has(activeCp.exam_id)) {
triggeredCheckpoints.add(activeCp.exam_id);
video.pause();
document.getElementById('socratic_quiz_modal').style.display = 'flex';
renderSocraticModal(activeCp);
}
});
}
@@ -878,19 +887,42 @@ class StudentPortal
return `${m}:${s}`;
}
function renderSocraticModal(cp) {
const modal = document.getElementById('socratic_quiz_modal');
const titleEl = document.getElementById('socratic_question_title');
const container = document.getElementById('socratic_options_container');
const feedback = document.getElementById('checkpoint_feedback');
titleEl.textContent = cp.question_text || 'سؤال فحص الفهم اللحظي:';
container.innerHTML = '';
feedback.style.display = 'none';
const letters = ['أ', 'ب', 'ج', 'د'];
cp.options.forEach((opt, idx) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'quiz-option-btn';
btn.textContent = `${letters[idx] || (idx+1)}) ${opt.text}`;
btn.onclick = () => handleCheckpointAnswer(btn, opt.is_correct, cp.rewind_on_fail_seconds || 45, cp.exam_id);
container.appendChild(btn);
});
modal.style.display = 'flex';
}
function triggerCheckpointDemo() {
const video = document.getElementById('lesson_video_player');
if (video) {
video.currentTime = 14;
triggeredCheckpoints.clear();
video.play();
checkpointTriggered = false;
}
}
function handleCheckpointAnswer(btn, isCorrect) {
function handleCheckpointAnswer(btn, isCorrect, rewindSeconds, examId) {
const feedback = document.getElementById('checkpoint_feedback');
const video = document.getElementById('lesson_video_player');
const btns = document.querySelectorAll('#socratic_quiz_modal .quiz-option-btn');
const btns = document.querySelectorAll('#socratic_options_container .quiz-option-btn');
if (isCorrect) {
btn.style.background = 'rgba(16, 185, 129, 0.25)';
@@ -903,32 +935,24 @@ class StudentPortal
setTimeout(() => {
document.getElementById('socratic_quiz_modal').style.display = 'none';
feedback.style.display = 'none';
btns.forEach(b => {
b.style.background = '';
b.style.borderColor = '';
});
if (video) video.play();
}, 1500);
}, 1400);
} else {
btn.style.background = 'rgba(239, 68, 68, 0.25)';
btn.style.borderColor = '#EF4444';
feedback.style.color = '#F87171';
feedback.style.display = 'block';
feedback.innerHTML = '⚠️ إجابة غير دقيقة. سيتم إرجاعك 45 ثانية لمراجعة الفكرة وتثبيت الفهم!';
feedback.innerHTML = `⚠️ إجابة غير دقيقة. سيتم إرجاعك ${rewindSeconds} ثانية لمراجعة الفكرة وتثبيت الفهم!`;
setTimeout(() => {
document.getElementById('socratic_quiz_modal').style.display = 'none';
feedback.style.display = 'none';
btns.forEach(b => {
b.style.background = '';
b.style.borderColor = '';
});
if (video) {
video.currentTime = Math.max(0, video.currentTime - 45);
video.currentTime = Math.max(0, video.currentTime - rewindSeconds);
triggeredCheckpoints.delete(examId);
video.play();
checkpointTriggered = false;
}
}, 2200);
}, 2000);
}
}
@@ -940,14 +964,22 @@ class StudentPortal
headers: { 'Authorization': 'Bearer ' + token }
});
const data = await res.json();
if (res.ok && data.status === 'success' && data.data?.playback) {
if (res.ok && data.status === 'success') {
const pb = data.data.playback;
const checkpoints = data.data.checkpoints || [];
activeLessonCheckpoints = checkpoints;
const video = document.getElementById('lesson_video_player');
if (video) {
if (pb.storage_type === 'api_upload' && pb.stream_url) {
video.src = pb.stream_url;
} else if (pb.hls_url) {
if (video && pb) {
if (pb.hls_url && window.Hls && Hls.isSupported()) {
if (hlsInstance) hlsInstance.destroy();
hlsInstance = new Hls({ enableWorker: true });
hlsInstance.loadSource(pb.hls_url);
hlsInstance.attachMedia(video);
} else if (pb.hls_url && video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = pb.hls_url;
} else if (pb.stream_url) {
video.src = pb.stream_url;
}
}
}