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"]
19 changes: 19 additions & 0 deletions apps/gb-limelight-recorder/backend/PingRobot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// בס"ד
import ping from "ping";

async function pingRobot(robotIp: string) {
const result = await ping.promise.probe(robotIp, { timeout: 10 });
return result;
}

export async function pingCameras(): Promise<boolean> {
const robotIp = "10.45.90.2";
const isUp = await pingRobot(robotIp);

if (isUp.alive) {
console.log(`Robot at ${robotIp} is online.`);
return true;
}
console.log(`Robot at ${robotIp} is offline.`);
return false;
}
65 changes: 65 additions & 0 deletions apps/gb-limelight-recorder/backend/RecordingProcess.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//בס"ד
import type { ChildProcess } from "child_process";
import { spawn } from "child_process";
import { timeStamp } from "console";
import ffmpegPath from "ffmpeg-static";

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

// --- CONSTRUCTOR ---
public constructor(cameraUrl: string, outputFile: string) {
const time: Date = new Date();
this.outputFile = outputFile
+ "/timestamp_"
+ time.getHours()
+ ":"
+ time.getMinutes()
+ ".mp4";

this.cameraUrl =
cameraUrl === "right" ? "http://limelight.local:5800"
: `http://limelight-${cameraUrl}.local:5800`;
}


// --- 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());
});

// 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"
}
}
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"
}
}
88 changes: 88 additions & 0 deletions apps/gb-limelight-recorder/backend/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//בס"ד
import express from "express";
import { RecordingProcess } from "./RecordingProcess.js";
import cors from "cors";
import { pingCameras } from "./PingRobot.js";
import { useEffect } from "react";
import { join } 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.

type cameraObj = {
name: string
camURL: string
ffmpegProcess: RecordingProcess | null
};

const leftCamObj: cameraObj = {
name: "left",
camURL: "http://limelight-left.local:5800",
ffmpegProcess: null
};
const objectCamObj: cameraObj = {
name: "object",
camURL: "http://limelight-object.local:5800",
ffmpegProcess: null
};
const rightCamObj: cameraObj = {
name: "right",
camURL: "http://limelight.local:5800",
ffmpegProcess: null
};
const cameras: cameraObj[] = [leftCamObj, objectCamObj, rightCamObj];

// --- HELLO ---
app.get("/", (req, res) => {
console.log("GET / route hit");
res.send("Welcome to the Limelight Recorder API");
});
Comment on lines +37 to +40
Copy link
Contributor

Choose a reason for hiding this comment

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

consider using the built in /api/v1/health route, specified in the routes/index.ts

Copy link
Author

Choose a reason for hiding this comment

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

ha?


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

// for general use and stuff
const zero = 0;
const one = 1;
const two = 2;
const three = 3;

function startRecording() {
if (cameras[zero].ffmpegProcess
|| cameras[one].ffmpegProcess
|| cameras[two].ffmpegProcess
) {
return;
}

for (let i = 0; i < three; i++) {
cameras[i].ffmpegProcess = new RecordingProcess(
cameras[i].name,
join(process.env.USERPROFILE ?? "", "Downloads")
);
cameras[i].ffmpegProcess?.startRecording();
}
}

function stopRecording() {
for (let i = 0; i < three; i++) {
if (cameras[i].ffmpegProcess) {
cameras[i].ffmpegProcess?.stopRecording();
cameras[i].ffmpegProcess = null;
console.log(`Stopped recording: ${cameras[i].name}`);
}
}
}

const oneSecond = 1000;
useEffect(() => {
const intervalId = setInterval(() => {
pingCameras()
.catch(console.error);
}, oneSecond);

return () => {clearInterval(intervalId)};
}, []);
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