Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 126 additions & 46 deletions Backend/routes/videos.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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" });
}

Expand All @@ -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" });
Expand Down
39 changes: 19 additions & 20 deletions Frontend/videoplayer-live.html
Original file line number Diff line number Diff line change
Expand Up @@ -177,29 +177,28 @@ <h2 class="text-sm font-semibold text-slate-100">
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";
}
}

Expand Down Expand Up @@ -230,8 +229,8 @@ <h2 class="text-sm font-semibold text-slate-100">
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);
Expand Down