diff --git a/docker-compose.yml b/docker-compose.yml index bf717c863f2fa4edc9641126970f5f402ea8be42..edaa383a6e7d66425fa9c3c08c0c2c873dd7de11 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: dockerfile: engage.dockerfile # env_file: ".env" ports: - - "8085:5173" + - "81:5173" # - "7070:7070" environment: - VITE_UPLOAD_SERVER=https://upload.ngage.lol @@ -37,7 +37,7 @@ services: volumes: - ./media:/usr/src/app/media ports: - - "3001:3001" # for upload server + - "3002:3001" # for upload server depends_on: db: condition: service_healthy diff --git a/package-lock.json b/package-lock.json index e0214aa5934bf4496f447ce404d046cd144288cc..3fced516ffdd8e80ee2891a1285005d9e9cc3aad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "react-swipeable": "^7.0.2", "routes": "^2.1.0", "save-dev": "^0.0.1-security", + "socket.io": "^4.8.1", "socket.io-client": "^4.8.1", "uuid": "^11.1.0", "vite-plugin-fs": "^1.1.0" @@ -2256,6 +2257,15 @@ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", "license": "MIT" }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -2974,6 +2984,15 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", @@ -3854,6 +3873,26 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, "node_modules/engine.io-client": { "version": "6.6.3", "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", @@ -3914,6 +3953,53 @@ "node": ">=10.0.0" } }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -7744,6 +7830,72 @@ "node": ">=10" } }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/socket.io-client": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", @@ -7806,6 +7958,23 @@ } } }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", diff --git a/package.json b/package.json index 943e12cb7c6b0baad0c0c32c7215301f2ac4726f..1303109b14ac5620ca6edf29fab5f8e8be24cb54 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "react-swipeable": "^7.0.2", "routes": "^2.1.0", "save-dev": "^0.0.1-security", + "socket.io": "^4.8.1", "socket.io-client": "^4.8.1", "uuid": "^11.1.0", "vite-plugin-fs": "^1.1.0" diff --git a/src/App.tsx b/src/App.tsx index 1537b20bc2a9334d28633063b4a59994e2f639e9..0e13dd7add502e3093b6c403fea6a42b69a08134 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -185,7 +185,7 @@ function Home() { useEffect(() => { if (currentVideo) { - console.log("Video changed to:", currentVideo.split("/").pop()); + // console.log("Video changed to:", currentVideo.split("/").pop()); getViewCount(); if (loggedIn && userID) { checkIfLiked(); @@ -417,7 +417,7 @@ function Home() { async function getViewCount() { try { - const fileName = currentVideo.split("/").pop(); + const fileName = currentVideo.substring(currentVideo.lastIndexOf("/") + 1); if (!fileName) { console.error("Error: fileName is missing."); return; @@ -427,15 +427,22 @@ function Home() { ); setViewCount(response.data.viewCount); } catch (error) { - console.error("Error fetching view count:", error); - setViewCount(0); + if (axios.isAxiosError(error)) { + console.error( + `Error fetching view count: ${error.response?.status} - ${error.response?.statusText}` + ); + } else { + console.error("Unexpected error fetching view count:", error); + } + setViewCount(0); // Default to 0 if there's an error } } async function recordView() { try { - if (viewRecorded) return; - const fileName = currentVideo.split("/").pop(); + if (viewRecorded) return; // Prevent multiple view records for the same video session + + const fileName = currentVideo.substring(currentVideo.lastIndexOf("/") + 1); if (!fileName) { console.error("Error: fileName is missing."); return; @@ -667,7 +674,7 @@ function Home() { <div className="controls"> <div className="video-stats"> <LikeButton - fileName={currentVideo ? currentVideo.split("/").pop() || "" : ""} + fileName={currentVideo ? currentVideo.substring(currentVideo.lastIndexOf("/") + 1) : ""} loggedIn={loggedIn} userId={userID} initialLikeCount={likeCount} diff --git a/src/styles/App.scss b/src/styles/App.scss index 5bedfe5c71aba2dfc64c6a411d16acc38e418049..c487c1ee85b00ee4143a70781707f8b0d2d365c9 100644 --- a/src/styles/App.scss +++ b/src/styles/App.scss @@ -308,7 +308,7 @@ body { .button.not-liked{ &:hover{ color: #f10372; - padding: 15px 20px; + padding: 10px; } } .app-container { diff --git a/src/upload.tsx b/src/upload.tsx index 022552d8e82fe036a7251e8d2c71b6c8c921eb52..376b69f3afe5ba4ef7d8008297603f492d752da7 100644 --- a/src/upload.tsx +++ b/src/upload.tsx @@ -88,4 +88,4 @@ export default Upload; // <StrictMode> // <Upload /> // </StrictMode> -// ); +// ); \ No newline at end of file diff --git a/upload-server.js b/upload-server.js index aea6657b19dc0eeb9e5a319cd529d96c4ae84a25..4b48850901d13bba90ca5008da9f314b64668a1d 100644 --- a/upload-server.js +++ b/upload-server.js @@ -6,9 +6,19 @@ import cors from "cors"; import jwt from "jsonwebtoken"; import fs from "fs"; import child_process from "child_process"; +import { Server } from "socket.io"; +import http from "http"; const app = express(); +const server = http.createServer(app); +const io = new Server(server, { + cors: { + origin: "*", // In production, restrict this to your front-end domain + methods: ["GET", "POST"], + }, +}); const port = 3001; + const { spawn } = child_process; let dbHost = "localhost"; @@ -16,9 +26,11 @@ if (process.env.DATABASE_HOST) { dbHost = process.env.DATABASE_HOST; } -app.use(express.json()); // Parse JSON bodies +app.use(express.json()); +app.use(cors()); + +import dbRequest from "./db.js"; -// Enable CORS for your React app (localhost:5173) – for dev, using wildcard. app.use( cors({ origin: "*", @@ -27,8 +39,6 @@ app.use( }) ); -import dbRequest from "./db.js"; - // Set up multer storage configuration const storage = multer.diskStorage({ destination: (req, file, cb) => { @@ -38,6 +48,7 @@ const storage = multer.diskStorage({ cb(null, Date.now() + path.extname(file.originalname)); }, }); + const upload = multer({ storage: storage }); // Middleware to authenticate JWT @@ -47,106 +58,210 @@ const authenticateToken = (req, res, next) => { if (!token) { return res.status(401).json({ message: "Unauthorized: No token provided" }); } + jwt.verify(token, "secretkey", (err, decoded) => { if (err) { return res.status(403).json({ message: "Invalid or expired token" }); } - req.user = decoded; + req.user = decoded; // Attach decoded user info to request next(); }); }; +// Socket connection handler +io.on("connection", (socket) => { + console.log("Client connected:", socket.id); + + socket.on("disconnect", () => { + console.log("Client disconnected:", socket.id); + }); +}); + // ------------------------------ // VIDEO UPLOAD & RELATED ENDPOINTS // ------------------------------ // Upload video with authentication app.post("/upload", authenticateToken, upload.single("file"), (req, res) => { - const filePath = path.join("./media", req.file.filename); - const outputPath = filePath.replace(".mp4", "trans.mp4"); - const outputFile = req.file.filename.replace(".mp4", "trans.mp4"); - if (!req.file) { return res.status(400).json({ message: "No file uploaded" }); } const { title, description } = req.body; const creatorId = req.user.userId; + const sessionId = req.body.sessionId || "unknown"; // Client should send a sessionId to identify the connection + if (!creatorId) { - fs.unlink(filePath, (err) => { - if (err) console.error("Error deleting file: ", err); - else console.log("File deleted successfully"); - }); + deleteFile(req.file.path); return res.status(400).json({ message: "Invalid creator ID" }); } if (!title) { - fs.unlink(filePath, (err) => { - if (err) console.error("Error deleting file: ", err); - else console.log("File deleted successfully"); - }); + deleteFile(req.file.path); return res .status(400) .json({ message: "Title and description are required" }); } - // Transcode media with ffmpeg - const ffmpeg = spawn("ffmpeg", [ - "-i", + const filePath = path.join("./media", req.file.filename); + const outputPath = filePath.replace(".mp4", "trans.mp4"); + const outputFile = req.file.filename.replace(".mp4", "trans.mp4"); + + // Get duration of the video to calculate progress + const ffprobe = spawn("ffprobe", [ + "-v", + "error", + "-show_entries", + "format=duration", + "-of", + "default=noprint_wrappers=1:nokey=1", filePath, - "-c:v", - "libx264", - "-preset", - "slow", - "-crf", - "22", - "-c:a", - "copy", - outputPath, ]); - ffmpeg.stderr.on("data", (data) => { - console.error(`stderr: ${data}`); + + let duration = 0; + ffprobe.stdout.on("data", (data) => { + duration = parseFloat(data.toString().trim()); + console.log(`Video duration: ${duration} seconds`); }); - ffmpeg.on("close", (code) => { - console.log(code); + + ffprobe.on("close", (code) => { if (code !== 0) { - fs.unlink(filePath, (err) => { - if (err) console.error("Error deleting file: ", err); - }); - fs.unlink(outputPath, (err) => { - if (err) console.error("Error deleting file: ", err); - }); - return res.status(400).json({ message: "Transcoding failed" }); + console.log("Could not determine video duration"); + // Continue with transcoding anyway } - fs.unlink(filePath, (err) => { - if (err) console.error("Error deleting file: ", err); - else console.log("File deleted successfully"); + + // Now start the transcoding process + const ffmpeg = spawn("ffmpeg", [ + "-i", + filePath, + "-c:v", + "libx264", + "-preset", + "slow", + "-crf", + "22", + "-c:a", + "copy", + "-progress", + "pipe:1", // Output progress information to stdout + outputPath, + ]); + + let lastProgress = 0; + + // Extract progress information from ffmpeg output + ffmpeg.stdout.on("data", (data) => { + const output = data.toString(); + const timeMatch = output.match(/time=(\d+:\d+:\d+\.\d+)/); + + if (timeMatch && duration > 0) { + const timeStr = timeMatch[1]; + const [hours, minutes, seconds] = timeStr.split(":").map(parseFloat); + const currentTime = hours * 3600 + minutes * 60 + seconds; + const progressPercent = Math.min( + Math.round((currentTime / duration) * 100), + 99 + ); + + if (progressPercent > lastProgress) { + lastProgress = progressPercent; + io.emit("transcode-progress", { + sessionId: sessionId, + progress: progressPercent, + }); + console.log(`Transcoding progress: ${progressPercent}%`); + } + } }); - }); - const db = dbRequest(dbHost); - const insertQuery = - "INSERT INTO videos (creator_id, title, description, fileName) VALUES (?, ?, ?, ?)"; - db.query( - insertQuery, - [creatorId, title, description, outputFile], - (err, result) => { - if (err) { - fs.unlink(filePath, (err) => { - if (err) console.error("Error deleting file: ", err); - else console.log("File deleted successfully"); - }); - console.error("Error inserting video into database: ", err); - db.destroy(); - return res.status(500).json({ message: "Database error", error: err }); + ffmpeg.stderr.on("data", (data) => { + // ffmpeg outputs detailed information to stderr + const output = data.toString(); + console.log(`ffmpeg stderr: ${output}`); + + // Parse progress from stderr if needed (as alternative to stdout progress) + const frameMatch = output.match(/frame=\s*(\d+)/); + const fpsMatch = output.match(/fps=\s*(\d+)/); + const timeMatch = output.match(/time=\s*(\d+:\d+:\d+\.\d+)/); + + if (timeMatch && duration > 0) { + const timeStr = timeMatch[1]; + const [hours, minutes, seconds] = timeStr.split(":").map(parseFloat); + const currentTime = hours * 3600 + minutes * 60 + seconds; + const progressPercent = Math.min( + Math.round((currentTime / duration) * 100), + 99 + ); + + if (progressPercent > lastProgress) { + lastProgress = progressPercent; + io.emit("transcode-progress", { + sessionId: sessionId, + progress: progressPercent, + }); + } } - console.log("Insert result:", result); - db.destroy(); - return res.status(200).json({ message: "File uploaded successfully!" }); - } - ); + }); + + ffmpeg.on("close", (code) => { + console.log(`Transcoding process exited with code ${code}`); + io.emit("transcode-progress", { + sessionId: sessionId, + progress: 100, + complete: true, + }); + + if (code === 0) { + // Transcoding successful - now insert into database + const db = dbRequest(dbHost); + const insertQuery = + "INSERT INTO videos (creator_id, title, description, fileName) VALUES (?, ?, ?, ?)"; + + db.query( + insertQuery, + [creatorId, title, description, outputFile], + (err, result) => { + // Delete original file regardless of DB outcome + deleteFile(filePath); + + if (err) { + console.error("Error inserting video into database: ", err); + // If DB insertion fails, also delete the transcoded file + deleteFile(outputPath); + db.destroy(); + return res + .status(500) + .json({ message: "Database error", error: err }); + } + + console.log("Insert result:", result); + db.destroy(); + return res.status(200).json({ + message: "File uploaded and transcoded successfully!", + videoId: result.insertId, + }); + } + ); + } else { + // Transcoding failed - clean up files and return error + deleteFile(filePath); + deleteFile(outputPath); + return res.status(400).json({ message: "Transcoding failed" }); + } + }); + }); }); +function deleteFile(filePath) { + fs.unlink(filePath, (err) => { + if (err) { + console.error(`Error deleting file ${filePath}: `, err); + } else { + console.log(`File ${filePath} deleted successfully`); + } + }); +} + // Get user info app.get("/user", (req, res) => { const db = dbRequest(dbHost); @@ -311,7 +426,7 @@ app.get("/get-replies", async (req, res) => { } }); -// Start the server -app.listen(port, () => { +// Use server.listen instead of app.listen to enable socket.io +server.listen(port, () => { console.log(`Upload Server is running at http://localhost:${port}`); }); \ No newline at end of file