getBody()['course_id'] ?? $_POST['course_id'] ?? 0); $title = trim((string)($request->getBody()['title'] ?? $_POST['title'] ?? '')); $seqOrder = (int)($request->getBody()['sequence_order'] ?? $_POST['sequence_order'] ?? 1); if (!$courseId || empty($title)) { $response->status(400)->json([ 'status' => 'error', 'message' => 'معرف الدورة وعنوان الدرس مطلوبان' ]); return; } // Verify Course Ownership $course = Database::selectOne("SELECT id, teacher_id FROM courses WHERE id = ? LIMIT 1", [$courseId]); if (!$course || ($course['teacher_id'] != $request->user_id && $request->role !== 'super_admin')) { $response->status(403)->json([ 'status' => 'error', 'message' => 'غير مصرح: لا تملك صلاحية التعديل على هذه الدورة' ]); return; } if (empty($_FILES['video'])) { $response->status(400)->json([ 'status' => 'error', 'message' => 'يرجى إرفاق ملف الفيديو في الطلب (key: video)' ]); return; } try { $uploadResult = VideoService::handleDirectUpload($_FILES['video'], $courseId, $title); // Insert lesson record $lessonId = Database::insert( "INSERT INTO lessons (course_id, title, sequence_order, storage_type, video_uuid, bunny_video_id, local_path, duration_seconds, is_free_preview, encoding_status) VALUES (?, ?, ?, 'api_upload', ?, '', ?, 0, 0, 'ready')", [ $courseId, $title, $seqOrder, $uploadResult['video_uuid'], $uploadResult['local_path'] ] ); $response->status(201)->json([ 'status' => 'success', 'message' => 'تم رفع وحفظ ملف الفيديو بنجاح عبر الـ API المباشر!', 'data' => array_merge($uploadResult, [ 'lesson_id' => $lessonId, 'title' => $title, 'course_id' => $courseId ]) ]); } catch (\Throwable $e) { $response->status(500)->json([ 'status' => 'error', 'message' => $e->getMessage() ]); } } /** * Create video entity on Bunny Stream * POST /api/teacher/videos/bunny-create */ public function createBunnyVideo(Request $request, Response $response): void { VideoService::ensureSchema(); $body = $request->getBody(); $title = trim((string)($body['title'] ?? 'درس جديد')); $courseId = (int)($body['course_id'] ?? 0); if (!$courseId) { $response->status(400)->json(['status' => 'error', 'message' => 'معرف الدورة مطلوب']); return; } try { $result = VideoService::createBunnyVideo($title); $response->status(201)->json([ 'status' => 'success', 'message' => 'تم إنشاء الفيديو في Bunny Stream بنجاح', 'data' => $result ]); } catch (\Throwable $e) { $response->status(500)->json([ 'status' => 'error', 'message' => $e->getMessage() ]); } } /** * Link an existing or newly created Bunny Video ID to a Course Lesson * POST /api/teacher/videos/bunny-link */ public function linkBunnyLesson(Request $request, Response $response): void { VideoService::ensureSchema(); $body = $request->getBody(); $courseId = (int)($body['course_id'] ?? 0); $title = trim((string)($body['title'] ?? '')); $bunnyVideoId = trim((string)($body['bunny_video_id'] ?? '')); $duration = (int)($body['duration_seconds'] ?? 0); $sequenceOrder = (int)($body['sequence_order'] ?? 1); if (!$courseId || empty($title) || empty($bunnyVideoId)) { $response->status(400)->json([ 'status' => 'error', 'message' => 'معرف الدورة، عنوان الدرس، ومعرف فيديو Bunny Stream مطلوبين' ]); return; } $course = Database::selectOne("SELECT id, teacher_id FROM courses WHERE id = ? LIMIT 1", [$courseId]); if (!$course || ($course['teacher_id'] != $request->user_id && $request->role !== 'super_admin')) { $response->status(403)->json([ 'status' => 'error', 'message' => 'غير مصرح: لا تملك هذه الدورة' ]); return; } $lessonId = Database::insert( "INSERT INTO lessons (course_id, title, sequence_order, storage_type, bunny_video_id, duration_seconds, is_free_preview, encoding_status) VALUES (?, ?, ?, 'bunny_stream', ?, ?, 0, 'ready')", [$courseId, $title, $sequenceOrder, $bunnyVideoId, $duration] ); $response->status(201)->json([ 'status' => 'success', 'message' => 'تم ربط درس Bunny Stream بنجاح!', 'data' => [ 'lesson_id' => $lessonId, 'bunny_video_id' => $bunnyVideoId, 'storage_type' => 'bunny_stream' ] ]); } /** * Stream Local Video via HTTP 206 Range Streaming * GET /api/videos/stream/{uuid} */ public function streamLocalVideo(Request $request, Response $response): void { $uuid = $request->getParam('uuid'); if (empty($uuid)) { $response->status(400)->json(['status' => 'error', 'message' => 'معرف الفيديو مطلوب']); return; } VideoService::streamLocalVideo($uuid); } /** * Get Lesson Playback Data with Signed DRM Tokens and Socratic Checkpoints * GET /api/lessons/{id}/playback */ public function getPlaybackData(Request $request, Response $response): void { VideoService::ensureSchema(); $lessonId = (int)$request->getParam('id'); if (!$lessonId) { $response->status(400)->json(['status' => 'error', 'message' => 'معرف الدرس مطلوب']); return; } $lesson = Database::selectOne("SELECT * FROM lessons WHERE id = ? LIMIT 1", [$lessonId]); if (!$lesson) { $response->status(404)->json(['status' => 'error', 'message' => 'الدرس غير موجود']); return; } // Fetch attached in-video Socratic Checkpoints $checkpoints = Database::select( "SELECT e.id as exam_id, e.uuid, e.title, e.timestamp_seconds, e.rewind_on_fail_seconds, e.passing_percentage FROM exams e WHERE e.lesson_id = ? AND e.scope = 'in_video_checkpoint' AND e.is_published = 1 ORDER BY e.timestamp_seconds ASC", [$lessonId] ); $storageType = $lesson['storage_type'] ?? 'bunny_stream'; $playbackInfo = []; if ($storageType === 'api_upload') { $playbackInfo = [ 'storage_type' => 'api_upload', 'stream_url' => '/api/videos/stream/' . $lesson['video_uuid'], 'video_uuid' => $lesson['video_uuid'], 'is_direct' => true ]; } else { // Bunny Stream Signed Playback $bunnyId = $lesson['bunny_video_id'] ?: 'mock-bunny-guid-2026'; $signedData = VideoService::generateBunnySignedPlayback($bunnyId, 10800); // 3-hour token $playbackInfo = array_merge(['storage_type' => 'bunny_stream'], $signedData); } $response->json([ 'status' => 'success', 'data' => [ 'lesson' => [ 'id' => (int)$lesson['id'], 'course_id' => (int)$lesson['course_id'], 'title' => $lesson['title'], 'duration_seconds' => (int)$lesson['duration_seconds'], 'is_free_preview' => (bool)$lesson['is_free_preview'], 'storage_type' => $storageType ], 'playback' => $playbackInfo, 'checkpoints' => $checkpoints ?: [] ] ]); } /** * Webhook listener for Bunny Stream encoding notifications * POST /api/webhooks/bunny */ public function handleBunnyWebhook(Request $request, Response $response): void { VideoService::ensureSchema(); $body = $request->getBody(); $videoId = trim((string)($body['VideoGuid'] ?? $body['videoId'] ?? '')); $status = (int)($body['Status'] ?? 0); // 3 = Finished/Ready, 4 = Failed if (!empty($videoId)) { $encodingStatus = ($status === 3) ? 'ready' : (($status === 4) ? 'failed' : 'processing'); Database::query( "UPDATE lessons SET encoding_status = ? WHERE bunny_video_id = ?", [$encodingStatus, $videoId] ); } $response->json(['status' => 'received']); } }