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
10 changes: 7 additions & 3 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
{
"label": "dev",
"type": "shell",
"command": "./node_modules/.bin/dotenv -e .dev.env -e .public.env -e .secret.env -- 'turbo run dev --filter=${input:project}-frontend --filter=${input:project}-backend'"
"command": "./node_modules/.bin/dotenv -e .dev.env -e .public.env -e .secret.env -- 'turbo run dev --filter=${input:project}-frontend --filter=${input:project}-backend'",
"problemMatcher": []
},
{
"label": "serve",
Expand All @@ -23,7 +24,10 @@
{
"label": "deploy",
"type": "shell",
"dependsOn": ["build", "serve"],
"dependsOn": [
"build",
"serve"
],
"dependsOrder": "sequence"
},
{
Expand All @@ -42,7 +46,7 @@
"id": "project",
"description": "Full Stack Projects worth running",
"type": "pickString",
"options": ["template", "test"]
"options": ["template", "test", "gb-limelight-recorder"]
},
{
"id": "workspace",
Expand Down
5 changes: 5 additions & 0 deletions apps/gb-limelight-recorder/backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM node:20-alpine
WORKDIR /usr/src/app
COPY ./dist /usr/src/app
EXPOSE 4590
CMD ["node","bundle.js"]
62 changes: 62 additions & 0 deletions apps/gb-limelight-recorder/backend/RecordingProcess.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//בס"ד
import type { ChildProcess } from "child_process";
import { spawn } from "child_process";
import ffmpegPath from "ffmpeg-static";

class RecordingProcess {
public ffmpegProcess: ChildProcess | null = null;
public cameraUrl: string;
public outputFile: string;

// --- CONSTRUCTOR ---
public constructor(cameraUrl: string, outputFile: string) {
this.outputFile = outputFile;

this.cameraUrl =
cameraUrl === "left" ? "http://limelight-left.local:5800"
: cameraUrl === "object" ? "http://limelight-object.local:5800"
: cameraUrl === "right" ? "http://limelight.local:5800"
: cameraUrl;
}


// --- START RECORDING ---
public startRecording(): string {
if (this.ffmpegProcess) {
return "Recording already running";
}
console.log(ffmpegPath);

// Process initiations
this.ffmpegProcess = spawn(ffmpegPath as unknown as string, [

Check warning

Code scanning / ESLint

Disallow type assertions that narrow a type Warning

Unsafe type assertion: type 'string' is more narrow than the original type.
"-i",
this.cameraUrl,
"-c:v",
"copy",
this.outputFile,
]);

// Logging
this.ffmpegProcess.stderr?.on("data", (data) => {
console.log(data.toString());

Check warning

Code scanning / ESLint

Disallow calling a value with type `any` Warning

Unsafe call of a(n) any typed value.

Check warning

Code scanning / ESLint

Disallow member access on a value with type `any` Warning

Unsafe member access .toString on an any value.
});

// Send response
return "Recording started";
}

// --- STOP RECORDING ---
public stopRecording(): string {
if (!this.ffmpegProcess) {
return "No recording running";
}

this.ffmpegProcess.stdin?.write("q");
this.ffmpegProcess.stdin?.end();
this.ffmpegProcess = null;

return "Recording stopped"
}
}

export { RecordingProcess };
37 changes: 37 additions & 0 deletions apps/gb-limelight-recorder/backend/build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// בס"ד
import { build, context } from "esbuild";
import { spawn } from "child_process";

const isDev = process.env.NODE_ENV === "DEV";

const bundlePath = "dist/bundle.js";

const buildSettings = {
entryPoints: ["src/main.ts"],
outfile: "dist/bundle.js",
bundle: true,
plugins: [],
minify: true,
platform: "node",
target: ["ES2022"],
format: "cjs",
external: ["@repo/config-env"],
} satisfies Parameters<typeof build>[0];

const buildDev = async () =>
context(buildSettings)
.then(async (ctx) => ctx.watch())
.then(() => {
console.log("Starting nodemon to manage execution of bundle.js");
spawn(
"nodemon",
[bundlePath, "--watch", bundlePath, "--ext", "js", "--exec", "node"],
{ stdio: "inherit", shell: true }
);
});

const buildedProject = isDev ? buildDev() : build(buildSettings);

buildedProject.catch((error: unknown) => {
console.warn(error);
});
11 changes: 11 additions & 0 deletions apps/gb-limelight-recorder/backend/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
services:
backend:
build:
context: .
dockerfile: Dockerfile

env_file:
- ../../../.public.env
- ../../../.secret.env
ports:
- "${BACKEND_PORT}:4590" # Maps host:${FRONTEND_PORT} to container:4590
20 changes: 20 additions & 0 deletions apps/gb-limelight-recorder/backend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "gb-limelight-recorder-backend",
"version": "1.0.0",
"description": "Backend for the application",
"main": "index.js",
"scripts": {
"test": "echo Backend Test Succeeded && exit 0",
"build": "tsx build.ts",
"serve": "node dist/bundle.js",
"dev": "tsx build.ts"
},
"author": "",
"license": "ISC",
"dependencies": {
"process": "^0.11.10"
},
"devDependencies": {
"@types/node": "^24.8.1"
}
}
124 changes: 124 additions & 0 deletions apps/gb-limelight-recorder/backend/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//בס"ד
import express from "express";
import { RecordingProcess } from "./RecordingProcess.js";
import cors from "cors";
import ping from "ping";
import fs from "fs";
import path from "path";

const app = express();
const port = 5000;
app.use(cors());

Check warning

Code scanning / ESLint

Disallow calling a value with type `any` Warning

Unsafe call of a(n) error type typed value.

let ffmpegProcessLeft: RecordingProcess | null = null;
let ffmpegProcessObject: RecordingProcess | null = null;
let ffmpegProcessRight: RecordingProcess | null = null;
const USB_ROOT = "E:/"; // CHANGE if needed

function createSessionFolder(): string {
if (!fs.existsSync(USB_ROOT)) {
throw new Error("USB drive not connected");
}

const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const sessionDir = path.join(USB_ROOT, `recording-${timestamp}`);

fs.mkdirSync(sessionDir, { recursive: true });
return sessionDir;
}
Comment on lines +18 to +28
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider moving this too



// --- HELLO ---
app.get("/", (req, res) => {
console.log("GET / route hit");
res.send("Welcome to the Limelight Recorder API");
});

// --- START THE SERVER ---
app.listen(port, () => {
console.log(`Server listening on http://localhost:${port}`);
});

function startRecording() {
if (ffmpegProcessLeft || ffmpegProcessObject || ffmpegProcessRight) {
return;
}

let sessionDir: string;

try {
sessionDir = createSessionFolder();
} catch (err) {
console.error(err);
return;
}
Comment on lines +49 to +54
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider moving this try catch into the createSessionFolder function, and return undefined in the createSessionFolder, then return if sessionDir is undefined


ffmpegProcessLeft = new RecordingProcess(
"left",
path.join(sessionDir, "left.mp4")
);
ffmpegProcessLeft.startRecording();

ffmpegProcessObject = new RecordingProcess(
"object",
path.join(sessionDir, "object.mp4")
);
ffmpegProcessObject.startRecording();

ffmpegProcessRight = new RecordingProcess(
"right",
path.join(sessionDir, "right.mp4")
);
ffmpegProcessRight.startRecording();
Comment on lines +56 to +72
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this code is repetitive. consider using an object to descripe the ffmpegProcesss. Initializing it as {left: undefined, object: undefined, right: undefined}, and then iterating over the object to initialize these.


console.log(`Recording started in ${sessionDir}`);
}

function stopRecording() {
// Stop left camera
if (ffmpegProcessLeft) {
ffmpegProcessLeft.stopRecording();
ffmpegProcessLeft = null;
console.log("Stopped recording: left");
}

// Stop object camera
if (ffmpegProcessObject) {
ffmpegProcessObject.stopRecording();
ffmpegProcessObject = null;
console.log("Stopped recording: object");
}

// Stop right camera
if (ffmpegProcessRight) {
ffmpegProcessRight.stopRecording();
ffmpegProcessRight = null;
console.log("Stopped recording: right");
}
Comment on lines +79 to +97
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as iterating here

}

const oneSecond = 1000;
async function pingRobot(robotIp: string) {
const result = await ping.promise.probe(robotIp, { timeout: 10 });
return result;
}
// --- PING CAMERAS ---
setInterval(() => {
async function pingCameras () {
const robotIp = "10.45.90.2";
const isUp = await pingRobot(robotIp).then((res) => res);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Suggested change
const isUp = await pingRobot(robotIp).then((res) => res);
const isUp = await pingRobot(robotIp);


if (isUp.alive) {
console.log(`Robot at ${robotIp} is online.`);
startRecording();
}

if (!isUp.alive) {
console.log(`Robot at ${robotIp} is offline.`);
stopRecording();
}
}
pingCameras().catch(() => {
console.error("Couldnt ping cameras");
})
}, oneSecond);
Comment on lines +42 to +124
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider making this a function in another file and importing and using it, as using it in the main file is kind of a lot.

14 changes: 14 additions & 0 deletions apps/gb-limelight-recorder/backend/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// בס"ד
import express from "express";
import { apiRouter } from "./routes";

const app = express();

const defaultPort = 4590;
const port = process.env.BACKEND_PORT ?? defaultPort;

app.use("/api/v1", apiRouter);

app.listen(port, () => {
console.log(`Production server running at http://localhost:${port}`);
});
9 changes: 9 additions & 0 deletions apps/gb-limelight-recorder/backend/src/routes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// בס"ד
import { Router } from "express";
import { StatusCodes } from "http-status-codes";

export const apiRouter = Router();

apiRouter.get("/health", (req, res) => {
res.status(StatusCodes.OK).send({ message: "Healthy!" });
});
12 changes: 12 additions & 0 deletions apps/gb-limelight-recorder/backend/turbo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": [
"//"
],
"tasks": {
"build": {
"outputs": [
"dist/**"
]
}
}
}
13 changes: 13 additions & 0 deletions apps/gb-limelight-recorder/frontend/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./src/assets/greenblitz.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GreenBlitz 4590</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
32 changes: 32 additions & 0 deletions apps/gb-limelight-recorder/frontend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "gb-limelight-recorder-frontend",
"version": "1.0.0",
"description": "Frontend for the application",
"main": "index.js",
"scripts": {
"test": "echo Frontend Test Succeeded && exit 0",
"dev": "vite",
"build": "tsc -b && vite build",
"serve": " tsx start.ts",
"lint": "eslint .",
"preview": "vite preview"
},
"author": "",
"license": "ISC",
"dependencies": {
"react": "^19.1.1",
"react-dom": "^19.1.1",
"tailwindcss": "^4.1.16",
"@tailwindcss/vite": "^4.1.16"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
"@types/node": "^24.6.0",
"@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^5.0.4",
"babel-plugin-react-compiler": "^19.1.0-rc.3",
"globals": "^16.4.0",
"vite": "^7.1.7"
}
}
Loading