diff --git a/Backend/routes/videos.js b/Backend/routes/videos.js index 6b0d7ef..11a9ea5 100644 --- a/Backend/routes/videos.js +++ b/Backend/routes/videos.js @@ -14,7 +14,7 @@ const { VideoMetadata } = require("../../DataBase/schema-mongo"); const router = express.Router(); const GRIDFS_BUCKET = "videos"; -const LOCAL_VIDEOS_DIR = path.join(__dirname, "../../Videos"); +const LOCAL_VIDEOS_DIR = path.resolve(__dirname, "../../Videos"); const upload = multer({ storage: multer.diskStorage({ @@ -63,6 +63,99 @@ function inferFormat(filename) { return "mp4"; } +function parseRangeHeader(rangeHeader, fileSize) { + if (!rangeHeader || typeof rangeHeader !== "string") return null; + if (!rangeHeader.startsWith("bytes=")) return { invalid: true }; + + const [rangePart] = rangeHeader.slice(6).split(","); + if (!rangePart) return { invalid: true }; + const [rawStart, rawEnd] = rangePart.split("-"); + const hasStart = rawStart !== ""; + const hasEnd = rawEnd !== ""; + + if (!hasStart && !hasEnd) return { invalid: true }; + + let start; + let end; + + if (!hasStart) { + const suffixLength = Number.parseInt(rawEnd, 10); + if (!Number.isFinite(suffixLength) || suffixLength <= 0) return { invalid: true }; + const clamped = Math.min(suffixLength, fileSize); + start = Math.max(fileSize - clamped, 0); + end = fileSize - 1; + } else { + start = Number.parseInt(rawStart, 10); + if (!Number.isFinite(start) || start < 0) return { invalid: true }; + + if (hasEnd) { + end = Number.parseInt(rawEnd, 10); + if (!Number.isFinite(end)) return { invalid: true }; + } else { + end = fileSize - 1; + } + } + + if (start >= fileSize) return { invalid: true }; + if (end >= fileSize) end = fileSize - 1; + if (end < start) return { invalid: true }; + return { start, end }; +} + +function resolveVideoPath(localFilePath) { + if (!localFilePath || typeof localFilePath !== "string") return null; + if (path.isAbsolute(localFilePath)) { + return path.normalize(localFilePath); + } + return path.resolve(LOCAL_VIDEOS_DIR, localFilePath); +} + +async function streamFromLocalFile(req, res, localFilePath, fallbackContentType) { + const absolutePath = resolveVideoPath(localFilePath); + if (!absolutePath) return false; + + let stat; + try { + stat = await fs.promises.stat(absolutePath); + } catch (_) { + return false; + } + + if (!stat.isFile()) return false; + + const fileSize = stat.size; + const contentType = fallbackContentType || "video/mp4"; + const parsed = parseRangeHeader(req.headers.range, fileSize); + + if (parsed && parsed.invalid) { + res.status(416); + res.setHeader("Content-Range", `bytes */${fileSize}`); + return res.end(); + } + + const start = parsed ? parsed.start : 0; + const end = parsed ? parsed.end : fileSize - 1; + const contentLength = end - start + 1; + + res.status(parsed ? 206 : 200); + res.setHeader("Accept-Ranges", "bytes"); + res.setHeader("Content-Type", contentType); + res.setHeader("Content-Length", String(contentLength)); + if (parsed) { + res.setHeader("Content-Range", `bytes ${start}-${end}/${fileSize}`); + } + + const stream = fs.createReadStream(absolutePath, { start, end }); + req.on("close", () => stream.destroy()); + stream.on("error", (err) => { + console.error("Local video stream error:", err.message); + if (!res.headersSent) res.status(500).json({ error: "Stream failed" }); + else res.destroy(err); + }); + stream.pipe(res); + return true; +} + function toPlaylistItem(doc) { const plain = doc && typeof doc.toObject === "function" ? doc.toObject() : { ...doc }; const id = String(plain._id); @@ -240,6 +333,8 @@ router.get("/:id/stream", async (req, res) => { return res.status(404).json({ error: "Video not found" }); } if (!video.file_id) { + const streamed = await streamFromLocalFile(req, res, video.localFilePath, video.mimeType); + if (streamed) return; return res.status(404).json({ error: "No media file" }); } @@ -248,58 +343,43 @@ router.get("/:id/stream", async (req, res) => { const filesColl = mongoose.connection.db.collection(`${GRIDFS_BUCKET}.files`); const fileDoc = await filesColl.findOne({ _id: fileObjectId }); if (!fileDoc) { + const streamed = await streamFromLocalFile(req, res, video.localFilePath, video.mimeType); + if (streamed) return; return res.status(404).json({ error: "File missing in storage" }); } const fileSize = fileDoc.length; const contentType = video.mimeType || fileDoc.contentType || "video/mp4"; - const range = req.headers.range; - - if (range) { - const parts = range.replace(/bytes=/, "").split("-"); - const start = parseInt(parts[0], 10); - let end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1; - if (Number.isNaN(start) || start < 0 || start >= fileSize) { - res.status(416); - res.setHeader("Content-Range", `bytes */${fileSize}`); - return res.end(); - } - if (Number.isNaN(end) || end >= fileSize) end = fileSize - 1; - if (end < start) { - res.status(416); - res.setHeader("Content-Range", `bytes */${fileSize}`); - return res.end(); - } - - const chunkSize = end - start + 1; - res.status(206); + const parsed = parseRangeHeader(req.headers.range, fileSize); + if (parsed && parsed.invalid) { + res.status(416); + res.setHeader("Content-Range", `bytes */${fileSize}`); + return res.end(); + } + + const start = parsed ? parsed.start : 0; + const end = parsed ? parsed.end : fileSize - 1; + const chunkSize = end - start + 1; + + res.status(parsed ? 206 : 200); + res.setHeader("Accept-Ranges", "bytes"); + res.setHeader("Content-Length", String(chunkSize)); + res.setHeader("Content-Type", contentType); + if (parsed) { res.setHeader("Content-Range", `bytes ${start}-${end}/${fileSize}`); - res.setHeader("Accept-Ranges", "bytes"); - res.setHeader("Content-Length", String(chunkSize)); - res.setHeader("Content-Type", contentType); - - const downloadStream = bucket.openDownloadStream(fileObjectId, { - start, - end: end + 1, - }); - downloadStream.on("error", (e) => { - console.error("GridFS range stream:", e.message); - if (!res.headersSent) res.status(500).end(); - else res.destroy(e); - }); - downloadStream.pipe(res); - } else { - res.setHeader("Content-Length", String(fileSize)); - res.setHeader("Content-Type", contentType); - res.setHeader("Accept-Ranges", "bytes"); - const downloadStream = bucket.openDownloadStream(fileObjectId); - downloadStream.on("error", (e) => { - console.error("GridFS stream:", e.message); - if (!res.headersSent) res.status(500).end(); - else res.destroy(e); - }); - downloadStream.pipe(res); } + + const downloadStream = bucket.openDownloadStream(fileObjectId, { + start, + end: end + 1, + }); + req.on("close", () => downloadStream.destroy()); + downloadStream.on("error", (e) => { + console.error("GridFS stream:", e.message); + if (!res.headersSent) res.status(500).end(); + else res.destroy(e); + }); + downloadStream.pipe(res); } catch (err) { console.error("GET /api/videos/:id/stream:", err.message); if (!res.headersSent) res.status(500).json({ error: "Stream failed" }); diff --git a/Frontend/videoplayer-live.html b/Frontend/videoplayer-live.html index 73d0132..99707db 100644 --- a/Frontend/videoplayer-live.html +++ b/Frontend/videoplayer-live.html @@ -177,29 +177,28 @@

document.getElementById("dbBadge").classList.remove("hidden"); }); - function loadPlaylist() { - // In real scenario, fetch from /api/videos - // For now, use demo data from video-metadata.js if available - if (window.ACADLY_VIDEO_ENHANCEMENTS) { - playlist = [ - { - id: "video_oops_001", - title: "Object-Oriented Programming (OOPs)", - duration: 1200, - thumbnail: "https://via.placeholder.com/200x112?text=OOPs", - }, - { - id: "video_backprop_001", - title: "Backpropagation - AI's Learning Engine", - duration: 1800, - thumbnail: "https://via.placeholder.com/200x112?text=Backprop", - }, - ]; + async function loadPlaylist() { + const badge = document.getElementById("dbBadge"); + try { + const response = await fetch("/api/videos"); + if (!response.ok) { + const payload = await response.json().catch(() => ({})); + throw new Error(payload.detail || payload.error || response.statusText); + } + playlist = await response.json(); + } catch (error) { + console.error("Failed to load playlist:", error); + playlist = []; + badge.textContent = "API offline"; + badge.classList.remove("hidden"); + return; } renderPlaylist(); if (playlist.length > 0) { loadVideo(playlist[0]); + } else { + document.getElementById("videoTitle").textContent = "No published videos available"; } } @@ -230,8 +229,8 @@

document.getElementById("videoTitle").textContent = video.title; document.getElementById("videoCount").textContent = `(${playlist.length})`; - // Show demo video or placeholder - document.getElementById("player").src = `https://www.w3schools.com/html/mov_bbb.mp4`; + const player = document.getElementById("player"); + player.src = video.src || `/api/videos/${encodeURIComponent(video.id)}/stream`; // Load saved progress for this video const progress = await sync.getVideoProgress(video.id);