-
Notifications
You must be signed in to change notification settings - Fork 0
limelight-recorder #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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"] |
| 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, [ | ||
| "-i", | ||
| this.cameraUrl, | ||
| "-c:v", | ||
| "copy", | ||
| this.outputFile, | ||
| ]); | ||
|
|
||
| // Logging | ||
| this.ffmpegProcess.stderr?.on("data", (data) => { | ||
| console.log(data.toString()); | ||
Check warningCode scanning / ESLint Disallow calling a value with type `any` Warning
Unsafe call of a(n) any typed value.
Check warningCode 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 }; | ||
| 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); | ||
| }); |
| 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 |
| 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" | ||
| } | ||
| } |
| 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 warningCode 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. consider moving this try catch into the |
||||||
|
|
||||||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this code is repetitive. consider using an object to descripe the |
||||||
|
|
||||||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ?
Suggested change
|
||||||
|
|
||||||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||
| 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}`); | ||
| }); |
| 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!" }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| { | ||
| "extends": [ | ||
| "//" | ||
| ], | ||
| "tasks": { | ||
| "build": { | ||
| "outputs": [ | ||
| "dist/**" | ||
| ] | ||
| } | ||
| } | ||
| } |
| 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> |
| 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" | ||
| } | ||
| } |
Check warning
Code scanning / ESLint
Disallow type assertions that narrow a type Warning