From 74257833cf67138eaa7a7f352c47acd28b050ec8 Mon Sep 17 00:00:00 2001 From: emmr253 <159184622+emmr253@users.noreply.github.com> Date: Wed, 10 Dec 2025 17:10:43 +0200 Subject: [PATCH 1/6] dosnt run but good enough for new --- .vscode/tasks.json | 10 +- apps/gb-limelight-recorder/backend/Dockerfile | 5 + .../backend/RecordingProcess.ts | 62 ++++ apps/gb-limelight-recorder/backend/build.ts | 37 +++ .../backend/docker-compose.yml | 11 + .../backend/package.json | 20 ++ apps/gb-limelight-recorder/backend/server.ts | 105 +++++++ .../gb-limelight-recorder/backend/src/main.ts | 14 + .../backend/src/routes/index.ts | 9 + apps/gb-limelight-recorder/backend/turbo.json | 12 + apps/gb-limelight-recorder/frontend/App.css | 44 +++ apps/gb-limelight-recorder/frontend/App.tsx | 48 +++ .../gb-limelight-recorder/frontend/Dockerfile | 7 + .../frontend/LimelightTable.tsx | 74 +++++ .../frontend/docker-compose.yml | 11 + apps/gb-limelight-recorder/frontend/index.css | 68 +++++ .../gb-limelight-recorder/frontend/index.html | 13 + apps/gb-limelight-recorder/frontend/main.tsx | 11 + .../frontend/package.json | 32 ++ .../frontend/src/App.tsx | 35 +++ .../frontend/src/assets/greenblitz.svg | 38 +++ .../frontend/src/index.css | 100 +++++++ .../frontend/src/main.tsx | 11 + apps/gb-limelight-recorder/frontend/start.ts | 24 ++ .../frontend/tsconfig.app.json | 28 ++ .../frontend/tsconfig.json | 7 + .../frontend/tsconfig.node.json | 26 ++ .../gb-limelight-recorder/frontend/turbo.json | 12 + .../frontend/vite.config.ts | 18 ++ package-lock.json | 278 ++++++++++++++++-- package.json | 3 + 31 files changed, 1144 insertions(+), 29 deletions(-) create mode 100644 apps/gb-limelight-recorder/backend/Dockerfile create mode 100644 apps/gb-limelight-recorder/backend/RecordingProcess.ts create mode 100644 apps/gb-limelight-recorder/backend/build.ts create mode 100644 apps/gb-limelight-recorder/backend/docker-compose.yml create mode 100644 apps/gb-limelight-recorder/backend/package.json create mode 100644 apps/gb-limelight-recorder/backend/server.ts create mode 100644 apps/gb-limelight-recorder/backend/src/main.ts create mode 100644 apps/gb-limelight-recorder/backend/src/routes/index.ts create mode 100644 apps/gb-limelight-recorder/backend/turbo.json create mode 100644 apps/gb-limelight-recorder/frontend/App.css create mode 100644 apps/gb-limelight-recorder/frontend/App.tsx create mode 100644 apps/gb-limelight-recorder/frontend/Dockerfile create mode 100644 apps/gb-limelight-recorder/frontend/LimelightTable.tsx create mode 100644 apps/gb-limelight-recorder/frontend/docker-compose.yml create mode 100644 apps/gb-limelight-recorder/frontend/index.css create mode 100644 apps/gb-limelight-recorder/frontend/index.html create mode 100644 apps/gb-limelight-recorder/frontend/main.tsx create mode 100644 apps/gb-limelight-recorder/frontend/package.json create mode 100644 apps/gb-limelight-recorder/frontend/src/App.tsx create mode 100644 apps/gb-limelight-recorder/frontend/src/assets/greenblitz.svg create mode 100644 apps/gb-limelight-recorder/frontend/src/index.css create mode 100644 apps/gb-limelight-recorder/frontend/src/main.tsx create mode 100644 apps/gb-limelight-recorder/frontend/start.ts create mode 100644 apps/gb-limelight-recorder/frontend/tsconfig.app.json create mode 100644 apps/gb-limelight-recorder/frontend/tsconfig.json create mode 100644 apps/gb-limelight-recorder/frontend/tsconfig.node.json create mode 100644 apps/gb-limelight-recorder/frontend/turbo.json create mode 100644 apps/gb-limelight-recorder/frontend/vite.config.ts diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 2354f2e..2545297 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -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", @@ -23,7 +24,10 @@ { "label": "deploy", "type": "shell", - "dependsOn": ["build", "serve"], + "dependsOn": [ + "build", + "serve" + ], "dependsOrder": "sequence" }, { @@ -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", diff --git a/apps/gb-limelight-recorder/backend/Dockerfile b/apps/gb-limelight-recorder/backend/Dockerfile new file mode 100644 index 0000000..7f398e6 --- /dev/null +++ b/apps/gb-limelight-recorder/backend/Dockerfile @@ -0,0 +1,5 @@ +FROM node:20-alpine +WORKDIR /usr/src/app +COPY ./dist /usr/src/app +EXPOSE 4590 +CMD ["node","bundle.js"] \ No newline at end of file diff --git a/apps/gb-limelight-recorder/backend/RecordingProcess.ts b/apps/gb-limelight-recorder/backend/RecordingProcess.ts new file mode 100644 index 0000000..9ee053e --- /dev/null +++ b/apps/gb-limelight-recorder/backend/RecordingProcess.ts @@ -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.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()); + }); + + // 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 }; \ No newline at end of file diff --git a/apps/gb-limelight-recorder/backend/build.ts b/apps/gb-limelight-recorder/backend/build.ts new file mode 100644 index 0000000..3802a64 --- /dev/null +++ b/apps/gb-limelight-recorder/backend/build.ts @@ -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[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); +}); diff --git a/apps/gb-limelight-recorder/backend/docker-compose.yml b/apps/gb-limelight-recorder/backend/docker-compose.yml new file mode 100644 index 0000000..dc82686 --- /dev/null +++ b/apps/gb-limelight-recorder/backend/docker-compose.yml @@ -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 diff --git a/apps/gb-limelight-recorder/backend/package.json b/apps/gb-limelight-recorder/backend/package.json new file mode 100644 index 0000000..355ac1f --- /dev/null +++ b/apps/gb-limelight-recorder/backend/package.json @@ -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" + } +} diff --git a/apps/gb-limelight-recorder/backend/server.ts b/apps/gb-limelight-recorder/backend/server.ts new file mode 100644 index 0000000..8503d9e --- /dev/null +++ b/apps/gb-limelight-recorder/backend/server.ts @@ -0,0 +1,105 @@ +//בס"ד +import express from "express"; +import { RecordingProcess } from "./RecordingProcess.js"; +import cors from "cors"; +import ping from "ping"; + +const app = express(); +const port = 5000; +app.use(cors()); + +let ffmpegProcessLeft: RecordingProcess | null = null; +let ffmpegProcessObject: RecordingProcess | null = null; +let ffmpegProcessRight: RecordingProcess | null = null; + +// --- 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() { + // Start left camera + if (!ffmpegProcessLeft) { + ffmpegProcessLeft = new RecordingProcess( + "left", + "../test-vids/test-recording-left.mp4", + ); + ffmpegProcessLeft.startRecording(); + console.log("Started recording: left"); + } + + // Start object camera + if (!ffmpegProcessObject) { + ffmpegProcessObject = new RecordingProcess( + "object", + "../test-vids/test-recording-object.mp4", + ); + ffmpegProcessObject.startRecording(); + console.log("Started recording: object"); + } + + // Start right camera + if (!ffmpegProcessRight) { + ffmpegProcessRight = new RecordingProcess( + "right", + "../src/test-vids/test-recording-right.mp4", + ); + ffmpegProcessRight.startRecording(); + console.log("Started recording: right"); + } +} + +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"); + } +} + +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); + + 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); \ No newline at end of file diff --git a/apps/gb-limelight-recorder/backend/src/main.ts b/apps/gb-limelight-recorder/backend/src/main.ts new file mode 100644 index 0000000..9d5aaa3 --- /dev/null +++ b/apps/gb-limelight-recorder/backend/src/main.ts @@ -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}`); +}); diff --git a/apps/gb-limelight-recorder/backend/src/routes/index.ts b/apps/gb-limelight-recorder/backend/src/routes/index.ts new file mode 100644 index 0000000..b62ad50 --- /dev/null +++ b/apps/gb-limelight-recorder/backend/src/routes/index.ts @@ -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!" }); +}); \ No newline at end of file diff --git a/apps/gb-limelight-recorder/backend/turbo.json b/apps/gb-limelight-recorder/backend/turbo.json new file mode 100644 index 0000000..52e8c76 --- /dev/null +++ b/apps/gb-limelight-recorder/backend/turbo.json @@ -0,0 +1,12 @@ +{ + "extends": [ + "//" + ], + "tasks": { + "build": { + "outputs": [ + "dist/**" + ] + } + } +} diff --git a/apps/gb-limelight-recorder/frontend/App.css b/apps/gb-limelight-recorder/frontend/App.css new file mode 100644 index 0000000..08c2a34 --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/App.css @@ -0,0 +1,44 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; + align-items: center; + align-content: center; +} + +.logo { + height: 9em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #dc42d4); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/apps/gb-limelight-recorder/frontend/App.tsx b/apps/gb-limelight-recorder/frontend/App.tsx new file mode 100644 index 0000000..8ad196f --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/App.tsx @@ -0,0 +1,48 @@ +// בס"ד +import { useEffect, useState } from "react"; +import "./App.css"; +import LimelightTable from "./LimelightTable"; + +function App() { + const [message, setMessage] = useState("Loading..."); + const [robotOnline, setRobotOnline] = useState(false); + + useEffect(() => { + fetch("http://localhost:5000/") + .then((res) => res.text()) + .then(setMessage) + .catch((e) => { + setMessage("Error connecting to server"); + console.error(e); + }); + }, []); + + useEffect(() => { + const interval = setInterval(async () => { + try { + const res = await fetch("http://localhost:4590/"); + const text = await res.text(); + setMessage(text); + setRobotOnline(text.includes("Welcome")); + } catch { + setRobotOnline(false); + } + }, 2000); + return () => clearInterval(interval); + }, []); + + return ( + <> +
+ Logo +

{message}

+
+ + + ); +} + +export default App; diff --git a/apps/gb-limelight-recorder/frontend/Dockerfile b/apps/gb-limelight-recorder/frontend/Dockerfile new file mode 100644 index 0000000..d625b35 --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/Dockerfile @@ -0,0 +1,7 @@ +FROM node:20-alpine +WORKDIR /app +COPY ./start.ts /app/start.ts +COPY ./dist /app/dist +EXPOSE 443 +RUN ["npm","install","express"] +CMD ["npm","exec","--","tsx","start.ts"] \ No newline at end of file diff --git a/apps/gb-limelight-recorder/frontend/LimelightTable.tsx b/apps/gb-limelight-recorder/frontend/LimelightTable.tsx new file mode 100644 index 0000000..69f6f98 --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/LimelightTable.tsx @@ -0,0 +1,74 @@ +//בס"ד +import { useEffect } from "react"; +import type React from "react"; + +interface LimelightTableProps { + robotOnline: boolean; + cameras: boolean[]; +} + +const zero = 0; +const one = 1; + +async function doThingy(robotOnline, cameraStatus, index) { + const camera = index === zero ? "left" : index === one ? "object" : "right"; + if (robotOnline && cameraStatus) { + await fetch(`http://localhost:5000/record/start/${camera}`, { + method: "POST", + }); + } else if (!robotOnline) { + await fetch(`http://localhost:5000/record/stop/${camera}`, { + method: "POST", + }); + } +} + +const LimelightTable: React.FC = ({ + robotOnline, +}) => { + + const cameraStatuses = [false, false, false]; + + useEffect(() => { + cameraStatuses.forEach((cameraStatus, index) => { + doThingy(robotOnline, cameraStatus, index).catch(() => { + console.error("Couldnt do the thingy"); + }) + }); + }, [robotOnline]); + + return ( + <> + + + + + + + + + + + + + + + + + + + +
LeftObjectRight
limelight-left.local:5800limelight-object.local:5800limelight.local:5800
+ + + + + +
+ +
+ + ); +}; + +export default LimelightTable; diff --git a/apps/gb-limelight-recorder/frontend/docker-compose.yml b/apps/gb-limelight-recorder/frontend/docker-compose.yml new file mode 100644 index 0000000..a27c44f --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/docker-compose.yml @@ -0,0 +1,11 @@ +services: + frontend: + build: + context: . + dockerfile: Dockerfile + + env_file: + - ../../../.public.env + - ../../../.secret.env + ports: + - "${FRONTEND_PORT}:443" # Maps host:${FRONTEND_PORT} to container:443 diff --git a/apps/gb-limelight-recorder/frontend/index.css b/apps/gb-limelight-recorder/frontend/index.css new file mode 100644 index 0000000..08a3ac9 --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/index.css @@ -0,0 +1,68 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/apps/gb-limelight-recorder/frontend/index.html b/apps/gb-limelight-recorder/frontend/index.html new file mode 100644 index 0000000..de79403 --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + GreenBlitz 4590 + + +
+ + + diff --git a/apps/gb-limelight-recorder/frontend/main.tsx b/apps/gb-limelight-recorder/frontend/main.tsx new file mode 100644 index 0000000..26d2461 --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/main.tsx @@ -0,0 +1,11 @@ +// בס"ד +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/apps/gb-limelight-recorder/frontend/package.json b/apps/gb-limelight-recorder/frontend/package.json new file mode 100644 index 0000000..7534f00 --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/package.json @@ -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" + } +} diff --git a/apps/gb-limelight-recorder/frontend/src/App.tsx b/apps/gb-limelight-recorder/frontend/src/App.tsx new file mode 100644 index 0000000..6a7d882 --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/src/App.tsx @@ -0,0 +1,35 @@ +// בס"ד +import { useState, type FC } from "react"; + +const counterStartingValue = 0; +const countIncrement = 1; +const maxCountingValue = 5; +const App: FC = () => { + const [count, setCount] = useState(counterStartingValue); + + return ( +
+

GreenBlitz Full-Stack Project:

+
+ +

+ Edit src/App.tsx and save to test HMR +

+
+
+ ); +}; + +export default App; diff --git a/apps/gb-limelight-recorder/frontend/src/assets/greenblitz.svg b/apps/gb-limelight-recorder/frontend/src/assets/greenblitz.svg new file mode 100644 index 0000000..e8eb8c5 --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/src/assets/greenblitz.svg @@ -0,0 +1,38 @@ + + + + + + + + + + diff --git a/apps/gb-limelight-recorder/frontend/src/index.css b/apps/gb-limelight-recorder/frontend/src/index.css new file mode 100644 index 0000000..1817850 --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/src/index.css @@ -0,0 +1,100 @@ +@import "tailwindcss"; +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + width: 100%; + margin: 0; + padding: 0; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/apps/gb-limelight-recorder/frontend/src/main.tsx b/apps/gb-limelight-recorder/frontend/src/main.tsx new file mode 100644 index 0000000..26d2461 --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/src/main.tsx @@ -0,0 +1,11 @@ +// בס"ד +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/apps/gb-limelight-recorder/frontend/start.ts b/apps/gb-limelight-recorder/frontend/start.ts new file mode 100644 index 0000000..a99498b --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/start.ts @@ -0,0 +1,24 @@ +// בס"ד +import express from "express"; +import path from "path"; + +const app = express(); + +const defaultPort = 80; +const securePort = 443; +const port = parseInt(process.env.FRONTEND_PORT ?? defaultPort.toString()); +const protocol = port === securePort ? "https" : "http"; + +const dirname = path.dirname(__filename); +const distDirectory = path.join(dirname, "dist"); +const indexHTML = path.join(distDirectory, "index.html"); + +app.use(express.static(distDirectory)); + +app.get(/^(.*)$/, (req, res) => { + res.sendFile(indexHTML); +}); + +app.listen(port, () => { + console.log(`Production server running at ${protocol}://localhost:${port}`); +}); diff --git a/apps/gb-limelight-recorder/frontend/tsconfig.app.json b/apps/gb-limelight-recorder/frontend/tsconfig.app.json new file mode 100644 index 0000000..a9b5a59 --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/apps/gb-limelight-recorder/frontend/tsconfig.json b/apps/gb-limelight-recorder/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/apps/gb-limelight-recorder/frontend/tsconfig.node.json b/apps/gb-limelight-recorder/frontend/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/apps/gb-limelight-recorder/frontend/turbo.json b/apps/gb-limelight-recorder/frontend/turbo.json new file mode 100644 index 0000000..52e8c76 --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/turbo.json @@ -0,0 +1,12 @@ +{ + "extends": [ + "//" + ], + "tasks": { + "build": { + "outputs": [ + "dist/**" + ] + } + } +} diff --git a/apps/gb-limelight-recorder/frontend/vite.config.ts b/apps/gb-limelight-recorder/frontend/vite.config.ts new file mode 100644 index 0000000..37eb678 --- /dev/null +++ b/apps/gb-limelight-recorder/frontend/vite.config.ts @@ -0,0 +1,18 @@ +// בס"ד +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; + + +// https://vite.dev/config/ +export default defineConfig({ + server: { port: 80 }, + plugins: [ + react({ + babel: { + plugins: [["babel-plugin-react-compiler"]], + }, + }), + tailwindcss(), + ], +}); diff --git a/package-lock.json b/package-lock.json index 43dd24c..8ebb25c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,12 +12,15 @@ ], "dependencies": { "@tailwindcss/vite": "^4.1.17", + "cors": "^2.8.5", "dotenv": "^17.2.3", "dotenv-cli": "^11.0.0", "eslint-plugin-bsd": "^1.0.0", "eslint-plugin-react": "^7.37.5", "express": "^5.1.0", + "ffmpeg-static": "^5.3.0", "http-status-codes": "^2.3.0", + "ping": "^1.0.0", "tailwindcss": "^4.1.17", "tsx": "^4.21.0" }, @@ -95,6 +98,51 @@ "vite": "^7.1.7" } }, + "apps/gb-limelight-recorder/backend": { + "name": "gb-limelight-recorder-backend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "process": "^0.11.10" + }, + "devDependencies": { + "@types/node": "^24.8.1" + } + }, + "apps/gb-limelight-recorder/frontend": { + "name": "gb-limelight-recorder-frontend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@tailwindcss/vite": "^4.1.16", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "tailwindcss": "^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" + } + }, + "apps/gb-limelight-recorder/frontend/node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "apps/template/backend": { "name": "template-backend", "version": "1.0.0", @@ -441,6 +489,21 @@ "node": ">=6.9.0" } }, + "node_modules/@derhuerst/http-basic": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@derhuerst/http-basic/-/http-basic-8.2.4.tgz", + "integrity": "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==", + "license": "MIT", + "dependencies": { + "caseless": "^0.12.0", + "concat-stream": "^2.0.0", + "http-response-object": "^3.0.1", + "parse-cache-control": "^1.0.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@dotenv-run/cli": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/@dotenv-run/cli/-/cli-1.3.6.tgz", @@ -2417,6 +2480,18 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2672,23 +2747,27 @@ "license": "(MIT OR Apache-2.0)" }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/brace-expansion": { @@ -2748,6 +2827,12 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -2835,6 +2920,12 @@ ], "license": "CC-BY-4.0" }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0" + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2924,6 +3015,21 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -2970,6 +3076,19 @@ "node": ">=6.6.0" } }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cross-env": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", @@ -3261,6 +3380,15 @@ "node": ">=10.13.0" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/es-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -3944,6 +4072,22 @@ "reusify": "^1.0.4" } }, + "node_modules/ffmpeg-static": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.3.0.tgz", + "integrity": "sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg==", + "hasInstallScript": true, + "license": "GPL-3.0-or-later", + "dependencies": { + "@derhuerst/http-basic": "^8.2.0", + "env-paths": "^2.2.0", + "https-proxy-agent": "^5.0.0", + "progress": "^2.0.3" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -4109,6 +4253,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gb-limelight-recorder-backend": { + "resolved": "apps/gb-limelight-recorder/backend", + "link": true + }, + "node_modules/gb-limelight-recorder-frontend": { + "resolved": "apps/gb-limelight-recorder/frontend", + "link": true + }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -4373,22 +4525,54 @@ "node": ">= 0.8" } }, + "node_modules/http-response-object": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", + "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", + "license": "MIT", + "dependencies": { + "@types/node": "^10.0.3" + } + }, + "node_modules/http-response-object/node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "license": "MIT" + }, "node_modules/http-status-codes": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", "license": "MIT" }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ignore": { @@ -5729,6 +5913,11 @@ "node": ">=6" } }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -5791,6 +5980,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/ping": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ping/-/ping-1.0.0.tgz", + "integrity": "sha512-3dxdgGtV+7P/EVo42JhkGSomeO/0GGicSz3mI/yK+AI+VGNAOfakw5XfcbGI4IjyBY+ZZwvuRXdhnNF2uliKew==", + "license": "MIT", + "engines": { + "node": ">=22.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5835,6 +6033,15 @@ "node": ">= 0.6.0" } }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5936,22 +6143,6 @@ "node": ">= 0.10" } }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/react": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", @@ -5989,6 +6180,20 @@ "node": ">=0.10.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -6513,6 +6718,15 @@ "node": ">= 0.4" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-ts": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/string-ts/-/string-ts-2.2.1.tgz", @@ -7039,6 +7253,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -7152,6 +7372,12 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 35155bd..b4e11a7 100644 --- a/package.json +++ b/package.json @@ -37,12 +37,15 @@ ], "dependencies": { "@tailwindcss/vite": "^4.1.17", + "cors": "^2.8.5", "dotenv": "^17.2.3", "dotenv-cli": "^11.0.0", "eslint-plugin-bsd": "^1.0.0", "eslint-plugin-react": "^7.37.5", "express": "^5.1.0", + "ffmpeg-static": "^5.3.0", "http-status-codes": "^2.3.0", + "ping": "^1.0.0", "tailwindcss": "^4.1.17", "tsx": "^4.21.0" } From e231a77363f8268c1614e07c7e93ff2a1feb4326 Mon Sep 17 00:00:00 2001 From: emmr253 <159184622+emmr253@users.noreply.github.com> Date: Sun, 14 Dec 2025 18:32:16 +0200 Subject: [PATCH 2/6] runs on template and (theoretically) saves to USB drive --- .../backend/RecordingProcess.ts | 10 +-- apps/gb-limelight-recorder/backend/server.ts | 69 ++++++++++------ apps/gb-limelight-recorder/frontend/App.tsx | 48 ------------ apps/gb-limelight-recorder/frontend/index.css | 68 ---------------- apps/gb-limelight-recorder/frontend/main.tsx | 11 --- .../frontend/package.json | 2 +- .../frontend/{ => src}/App.css | 0 .../frontend/src/App.tsx | 74 +++++++++++------- .../frontend/{ => src}/Dockerfile | 0 .../frontend/{ => src}/LimelightTable.tsx | 37 +++++++-- .../frontend/src/assets/greenblitz.png | Bin 0 -> 25858 bytes .../frontend/src/assets/greenblitz.svg | 38 --------- .../frontend/{ => src}/docker-compose.yml | 0 .../frontend/src/index.css | 48 ++---------- .../frontend/src/main.tsx | 14 ++-- 15 files changed, 140 insertions(+), 279 deletions(-) delete mode 100644 apps/gb-limelight-recorder/frontend/App.tsx delete mode 100644 apps/gb-limelight-recorder/frontend/index.css delete mode 100644 apps/gb-limelight-recorder/frontend/main.tsx rename apps/gb-limelight-recorder/frontend/{ => src}/App.css (100%) rename apps/gb-limelight-recorder/frontend/{ => src}/Dockerfile (100%) rename apps/gb-limelight-recorder/frontend/{ => src}/LimelightTable.tsx (59%) create mode 100644 apps/gb-limelight-recorder/frontend/src/assets/greenblitz.png delete mode 100644 apps/gb-limelight-recorder/frontend/src/assets/greenblitz.svg rename apps/gb-limelight-recorder/frontend/{ => src}/docker-compose.yml (100%) diff --git a/apps/gb-limelight-recorder/backend/RecordingProcess.ts b/apps/gb-limelight-recorder/backend/RecordingProcess.ts index 9ee053e..b963e7a 100644 --- a/apps/gb-limelight-recorder/backend/RecordingProcess.ts +++ b/apps/gb-limelight-recorder/backend/RecordingProcess.ts @@ -9,16 +9,16 @@ class RecordingProcess { public outputFile: string; // --- CONSTRUCTOR --- - public constructor( - cameraUrl: string, - outputFile: string, - ) { - this.cameraUrl = + 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 { diff --git a/apps/gb-limelight-recorder/backend/server.ts b/apps/gb-limelight-recorder/backend/server.ts index 8503d9e..f71b214 100644 --- a/apps/gb-limelight-recorder/backend/server.ts +++ b/apps/gb-limelight-recorder/backend/server.ts @@ -3,6 +3,8 @@ 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; @@ -11,6 +13,20 @@ app.use(cors()); 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; +} + // --- HELLO --- app.get("/", (req, res) => { @@ -24,35 +40,38 @@ app.listen(port, () => { }); function startRecording() { - // Start left camera - if (!ffmpegProcessLeft) { - ffmpegProcessLeft = new RecordingProcess( - "left", - "../test-vids/test-recording-left.mp4", - ); - ffmpegProcessLeft.startRecording(); - console.log("Started recording: left"); + if (ffmpegProcessLeft || ffmpegProcessObject || ffmpegProcessRight) { + return; } - // Start object camera - if (!ffmpegProcessObject) { - ffmpegProcessObject = new RecordingProcess( - "object", - "../test-vids/test-recording-object.mp4", - ); - ffmpegProcessObject.startRecording(); - console.log("Started recording: object"); - } + let sessionDir: string; - // Start right camera - if (!ffmpegProcessRight) { - ffmpegProcessRight = new RecordingProcess( - "right", - "../src/test-vids/test-recording-right.mp4", - ); - ffmpegProcessRight.startRecording(); - console.log("Started recording: right"); + try { + sessionDir = createSessionFolder(); + } catch (err) { + console.error(err); + return; } + + 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(); + + console.log(`Recording started in ${sessionDir}`); } function stopRecording() { diff --git a/apps/gb-limelight-recorder/frontend/App.tsx b/apps/gb-limelight-recorder/frontend/App.tsx deleted file mode 100644 index 8ad196f..0000000 --- a/apps/gb-limelight-recorder/frontend/App.tsx +++ /dev/null @@ -1,48 +0,0 @@ -// בס"ד -import { useEffect, useState } from "react"; -import "./App.css"; -import LimelightTable from "./LimelightTable"; - -function App() { - const [message, setMessage] = useState("Loading..."); - const [robotOnline, setRobotOnline] = useState(false); - - useEffect(() => { - fetch("http://localhost:5000/") - .then((res) => res.text()) - .then(setMessage) - .catch((e) => { - setMessage("Error connecting to server"); - console.error(e); - }); - }, []); - - useEffect(() => { - const interval = setInterval(async () => { - try { - const res = await fetch("http://localhost:4590/"); - const text = await res.text(); - setMessage(text); - setRobotOnline(text.includes("Welcome")); - } catch { - setRobotOnline(false); - } - }, 2000); - return () => clearInterval(interval); - }, []); - - return ( - <> -
- Logo -

{message}

-
- - - ); -} - -export default App; diff --git a/apps/gb-limelight-recorder/frontend/index.css b/apps/gb-limelight-recorder/frontend/index.css deleted file mode 100644 index 08a3ac9..0000000 --- a/apps/gb-limelight-recorder/frontend/index.css +++ /dev/null @@ -1,68 +0,0 @@ -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} diff --git a/apps/gb-limelight-recorder/frontend/main.tsx b/apps/gb-limelight-recorder/frontend/main.tsx deleted file mode 100644 index 26d2461..0000000 --- a/apps/gb-limelight-recorder/frontend/main.tsx +++ /dev/null @@ -1,11 +0,0 @@ -// בס"ד -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App' - -createRoot(document.getElementById('root')!).render( - - - , -) diff --git a/apps/gb-limelight-recorder/frontend/package.json b/apps/gb-limelight-recorder/frontend/package.json index 7534f00..570f109 100644 --- a/apps/gb-limelight-recorder/frontend/package.json +++ b/apps/gb-limelight-recorder/frontend/package.json @@ -7,7 +7,7 @@ "test": "echo Frontend Test Succeeded && exit 0", "dev": "vite", "build": "tsc -b && vite build", - "serve": "tsx start.ts", + "serve": " tsx start.ts", "lint": "eslint .", "preview": "vite preview" }, diff --git a/apps/gb-limelight-recorder/frontend/App.css b/apps/gb-limelight-recorder/frontend/src/App.css similarity index 100% rename from apps/gb-limelight-recorder/frontend/App.css rename to apps/gb-limelight-recorder/frontend/src/App.css diff --git a/apps/gb-limelight-recorder/frontend/src/App.tsx b/apps/gb-limelight-recorder/frontend/src/App.tsx index 6a7d882..bc203ea 100644 --- a/apps/gb-limelight-recorder/frontend/src/App.tsx +++ b/apps/gb-limelight-recorder/frontend/src/App.tsx @@ -1,35 +1,53 @@ // בס"ד -import { useState, type FC } from "react"; +import { useEffect, useState } from "react"; +import "./App.css"; +import LimelightTable from "./LimelightTable"; -const counterStartingValue = 0; -const countIncrement = 1; -const maxCountingValue = 5; -const App: FC = () => { - const [count, setCount] = useState(counterStartingValue); +function app() { + const [message, setMessage] = useState("Loading..."); + const [isRobotOnline, setIsRobotOnline] = useState(false); + const twoSeconds = 2000; + +useEffect(() => { + void (async () => { + try { + const res = await fetch("http://localhost:5000/"); + const text = await res.text(); + setMessage(text); + } catch (e) { + setMessage("Error connecting to server"); + console.error(e); + } + })(); +}, []); + + + useEffect(() => { + const interval = setInterval(async () => { + try { + const res = await fetch("http://localhost:4590/"); + const text = await res.text(); + setMessage(text); + setIsRobotOnline(text.includes("Welcome")); + } catch { + setIsRobotOnline(false); + } + }, twoSeconds); + return () => {clearInterval(interval)}; + }, []); return ( -
-

GreenBlitz Full-Stack Project:

-
- -

- Edit src/App.tsx and save to test HMR -

+ <> +
+ Logo +

{message}

-
+ + ); -}; +} -export default App; +export default app; diff --git a/apps/gb-limelight-recorder/frontend/Dockerfile b/apps/gb-limelight-recorder/frontend/src/Dockerfile similarity index 100% rename from apps/gb-limelight-recorder/frontend/Dockerfile rename to apps/gb-limelight-recorder/frontend/src/Dockerfile diff --git a/apps/gb-limelight-recorder/frontend/LimelightTable.tsx b/apps/gb-limelight-recorder/frontend/src/LimelightTable.tsx similarity index 59% rename from apps/gb-limelight-recorder/frontend/LimelightTable.tsx rename to apps/gb-limelight-recorder/frontend/src/LimelightTable.tsx index 69f6f98..318a492 100644 --- a/apps/gb-limelight-recorder/frontend/LimelightTable.tsx +++ b/apps/gb-limelight-recorder/frontend/src/LimelightTable.tsx @@ -1,16 +1,27 @@ //בס"ד -import { useEffect } from "react"; +import { useEffect, useState, useRef } from "react"; import type React from "react"; +import type { HTMLAttributes } from "react"; interface LimelightTableProps { robotOnline: boolean; cameras: boolean[]; } +declare module "react" { + interface InputHTMLAttributes extends HTMLAttributes { + webkitdirectory?: boolean; + } +} + const zero = 0; const one = 1; -async function doThingy(robotOnline, cameraStatus, index) { +async function doThingy( + robotOnline: boolean, + cameraStatus: boolean, + index: number +) { const camera = index === zero ? "left" : index === one ? "object" : "right"; if (robotOnline && cameraStatus) { await fetch(`http://localhost:5000/record/start/${camera}`, { @@ -23,17 +34,16 @@ async function doThingy(robotOnline, cameraStatus, index) { } } -const LimelightTable: React.FC = ({ - robotOnline, -}) => { - +const LimelightTable: React.FC = ({ robotOnline }) => { const cameraStatuses = [false, false, false]; + const [fileLocation, setFileLocation] = useState(""); + const locationPickerRef = useRef(null); useEffect(() => { cameraStatuses.forEach((cameraStatus, index) => { doThingy(robotOnline, cameraStatus, index).catch(() => { console.error("Couldnt do the thingy"); - }) + }); }); }, [robotOnline]); @@ -63,7 +73,18 @@ const LimelightTable: React.FC = ({ - + +
+ +
+

Location: {fileLocation}

diff --git a/apps/gb-limelight-recorder/frontend/src/assets/greenblitz.png b/apps/gb-limelight-recorder/frontend/src/assets/greenblitz.png new file mode 100644 index 0000000000000000000000000000000000000000..b10ccc9619379012f681c374e40913504c717bca GIT binary patch literal 25858 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

}I;1NY?Lr*Wsc3c;03TzC4f21l~X&$bx5R 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 - - - - - - - - - diff --git a/apps/gb-limelight-recorder/frontend/docker-compose.yml b/apps/gb-limelight-recorder/frontend/src/docker-compose.yml similarity index 100% rename from apps/gb-limelight-recorder/frontend/docker-compose.yml rename to apps/gb-limelight-recorder/frontend/src/docker-compose.yml diff --git a/apps/gb-limelight-recorder/frontend/src/index.css b/apps/gb-limelight-recorder/frontend/src/index.css index 1817850..08a3ac9 100644 --- a/apps/gb-limelight-recorder/frontend/src/index.css +++ b/apps/gb-limelight-recorder/frontend/src/index.css @@ -1,4 +1,3 @@ -@import "tailwindcss"; :root { font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; @@ -12,45 +11,6 @@ text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - width: 100%; - margin: 0; - padding: 0; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; } a { @@ -62,6 +22,14 @@ a:hover { color: #535bf2; } +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + h1 { font-size: 3.2em; line-height: 1.1; diff --git a/apps/gb-limelight-recorder/frontend/src/main.tsx b/apps/gb-limelight-recorder/frontend/src/main.tsx index 26d2461..e37945f 100644 --- a/apps/gb-limelight-recorder/frontend/src/main.tsx +++ b/apps/gb-limelight-recorder/frontend/src/main.tsx @@ -1,11 +1,11 @@ // בס"ד -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App' +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "./index.css"; +import App from "./App"; -createRoot(document.getElementById('root')!).render( +createRoot(document.getElementById("root")!).render( - , -) + +); From e7137a046e5f62fe030aef4debed90b1df0bcf1b Mon Sep 17 00:00:00 2001 From: emmr253 <159184622+emmr253@users.noreply.github.com> Date: Wed, 24 Dec 2025 17:41:27 +0200 Subject: [PATCH 3/6] fixed eslint (i think) --- .../frontend/src/App.tsx | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/gb-limelight-recorder/frontend/src/App.tsx b/apps/gb-limelight-recorder/frontend/src/App.tsx index bc203ea..4f9548b 100644 --- a/apps/gb-limelight-recorder/frontend/src/App.tsx +++ b/apps/gb-limelight-recorder/frontend/src/App.tsx @@ -3,23 +3,23 @@ import { useEffect, useState } from "react"; import "./App.css"; import LimelightTable from "./LimelightTable"; -function app() { +const App = () => { const [message, setMessage] = useState("Loading..."); const [isRobotOnline, setIsRobotOnline] = useState(false); const twoSeconds = 2000; -useEffect(() => { - void (async () => { - try { - const res = await fetch("http://localhost:5000/"); - const text = await res.text(); - setMessage(text); - } catch (e) { - setMessage("Error connecting to server"); - console.error(e); - } - })(); -}, []); + useEffect(() => { + void (async () => { + try { + const res = await fetch("http://localhost:5000/"); + const text = await res.text(); + setMessage(text); + } catch (e) { + setMessage("Error connecting to server"); + console.error(e); + } + })(); + }, []); useEffect(() => { @@ -50,4 +50,4 @@ useEffect(() => { ); } -export default app; +export default App; From da00e95296c8e6b04a3e4732e1e8ad0806389277 Mon Sep 17 00:00:00 2001 From: emmr253 <159184622+emmr253@users.noreply.github.com> Date: Sun, 11 Jan 2026 20:20:16 +0200 Subject: [PATCH 4/6] removed some repeating parts --- .../backend/PingRobot.ts | 22 ++++ .../backend/RecordingProcess.ts | 9 +- apps/gb-limelight-recorder/backend/server.ts | 111 ++++++------------ .../frontend/src/LimelightTable.tsx | 8 +- 4 files changed, 70 insertions(+), 80 deletions(-) create mode 100644 apps/gb-limelight-recorder/backend/PingRobot.ts diff --git a/apps/gb-limelight-recorder/backend/PingRobot.ts b/apps/gb-limelight-recorder/backend/PingRobot.ts new file mode 100644 index 0000000..cf228e8 --- /dev/null +++ b/apps/gb-limelight-recorder/backend/PingRobot.ts @@ -0,0 +1,22 @@ +// בס"ד +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 { + const robotIp = "10.45.90.2"; + const isUp = await pingRobot(robotIp).then((res) => res); + + if (isUp.alive) { + console.log(`Robot at ${robotIp} is online.`); + return true; + } + console.log(`Robot at ${robotIp} is offline.`); + return false; +} + +// export default { pingCameras }; + diff --git a/apps/gb-limelight-recorder/backend/RecordingProcess.ts b/apps/gb-limelight-recorder/backend/RecordingProcess.ts index b963e7a..9c5a40d 100644 --- a/apps/gb-limelight-recorder/backend/RecordingProcess.ts +++ b/apps/gb-limelight-recorder/backend/RecordingProcess.ts @@ -1,6 +1,7 @@ //בס"ד import type { ChildProcess } from "child_process"; import { spawn } from "child_process"; +import { timeStamp } from "console"; import ffmpegPath from "ffmpeg-static"; class RecordingProcess { @@ -10,7 +11,13 @@ class RecordingProcess { // --- CONSTRUCTOR --- public constructor(cameraUrl: string, outputFile: string) { - this.outputFile = outputFile; + const time: Date = new Date(); + this.outputFile = outputFile + + "/timestamp_" + + time.getHours() + + ":" + + time.getMinutes() + + ".mp4"; this.cameraUrl = cameraUrl === "left" ? "http://limelight-left.local:5800" diff --git a/apps/gb-limelight-recorder/backend/server.ts b/apps/gb-limelight-recorder/backend/server.ts index f71b214..0dfb506 100644 --- a/apps/gb-limelight-recorder/backend/server.ts +++ b/apps/gb-limelight-recorder/backend/server.ts @@ -5,23 +5,32 @@ import cors from "cors"; import ping from "ping"; import fs from "fs"; import path from "path"; +import { pingCameras } from "./PingRobot.js"; +import { useEffect } from "react"; + const app = express(); const port = 5000; app.use(cors()); -let ffmpegProcessLeft: RecordingProcess | null = null; -let ffmpegProcessObject: RecordingProcess | null = null; -let ffmpegProcessRight: RecordingProcess | null = null; -const USB_ROOT = "E:/"; // CHANGE if needed +const ffmpegProcessLeft: RecordingProcess | null = null; +const ffmpegProcessObject: RecordingProcess | null = null; +const ffmpegProcessRight: RecordingProcess | null = null; +const ffmpegProcesses = [ + ffmpegProcessLeft as RecordingProcess | null, + ffmpegProcessObject as RecordingProcess | null, + ffmpegProcessRight as RecordingProcess | null, +]; +const cameraNames = ["left", "object", "right"]; +const usbRoot = "E:/"; // CHANGE if needed function createSessionFolder(): string { - if (!fs.existsSync(USB_ROOT)) { + if (!fs.existsSync(usbRoot)) { throw new Error("USB drive not connected"); } const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); - const sessionDir = path.join(USB_ROOT, `recording-${timestamp}`); + const sessionDir = path.join(usbRoot, `recording-${timestamp}`); fs.mkdirSync(sessionDir, { recursive: true }); return sessionDir; @@ -44,81 +53,33 @@ function startRecording() { return; } - let sessionDir: string; + const sessionDir = createSessionFolder(); - try { - sessionDir = createSessionFolder(); - } catch (err) { - console.error(err); - return; + for (let i = 0; i < ffmpegProcesses.length; i++) { + ffmpegProcesses[i] = new RecordingProcess( + cameraNames[i], + path.join(sessionDir, `${cameraNames[i]}.mp4`) + ); + ffmpegProcesses[i]?.startRecording(); } - - 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(); - - 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"); + for (let i = 0; i < ffmpegProcesses.length; i++) { + if (ffmpegProcesses[i]) { + ffmpegProcesses[i]?.stopRecording(); + ffmpegProcesses[i] = null; + console.log(`Stopped recording: ${cameraNames[i]}`); + } } } 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); - - 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); \ No newline at end of file +useEffect(() => { + const intervalId = setInterval(() => { + pingCameras() + .catch(console.error); + }, oneSecond); + + return () => {clearInterval(intervalId)}; +}, []); diff --git a/apps/gb-limelight-recorder/frontend/src/LimelightTable.tsx b/apps/gb-limelight-recorder/frontend/src/LimelightTable.tsx index 318a492..46596da 100644 --- a/apps/gb-limelight-recorder/frontend/src/LimelightTable.tsx +++ b/apps/gb-limelight-recorder/frontend/src/LimelightTable.tsx @@ -14,15 +14,15 @@ declare module "react" { } } -const zero = 0; -const one = 1; +const leftCamIndex = 0; +const rightCamIndex = 1; async function doThingy( robotOnline: boolean, cameraStatus: boolean, index: number ) { - const camera = index === zero ? "left" : index === one ? "object" : "right"; + const camera = index === leftCamIndex ? "left" : index === rightCamIndex ? "object" : "right"; if (robotOnline && cameraStatus) { await fetch(`http://localhost:5000/record/start/${camera}`, { method: "POST", @@ -79,7 +79,7 @@ const LimelightTable: React.FC = ({ robotOnline }) => { disabled={robotOnline} onClick={() => { const files = locationPickerRef.current?.files; - const file = files?.[zero]; + const file = files?.[leftCamIndex]; setFileLocation(file?.name ?? ""); }} >Save Location From 4a8b4e1fd67b2b7192bd0960fe739e65fcc64c3d Mon Sep 17 00:00:00 2001 From: emmr253 <159184622+emmr253@users.noreply.github.com> Date: Wed, 14 Jan 2026 18:33:36 +0200 Subject: [PATCH 5/6] i did a bunch of stuff just check it --- .../backend/PingRobot.ts | 7 +- .../backend/RecordingProcess.ts | 10 +-- apps/gb-limelight-recorder/backend/server.ts | 65 ++++++++++++------ .../frontend/src/App.tsx | 4 +- .../frontend/src/LimelightTable.tsx | 67 ++++++++++++------- 5 files changed, 92 insertions(+), 61 deletions(-) diff --git a/apps/gb-limelight-recorder/backend/PingRobot.ts b/apps/gb-limelight-recorder/backend/PingRobot.ts index cf228e8..f9a77f3 100644 --- a/apps/gb-limelight-recorder/backend/PingRobot.ts +++ b/apps/gb-limelight-recorder/backend/PingRobot.ts @@ -8,7 +8,7 @@ async function pingRobot(robotIp: string) { export async function pingCameras(): Promise { const robotIp = "10.45.90.2"; - const isUp = await pingRobot(robotIp).then((res) => res); + const isUp = await pingRobot(robotIp); if (isUp.alive) { console.log(`Robot at ${robotIp} is online.`); @@ -16,7 +16,4 @@ export async function pingCameras(): Promise { } console.log(`Robot at ${robotIp} is offline.`); return false; -} - -// export default { pingCameras }; - +} \ No newline at end of file diff --git a/apps/gb-limelight-recorder/backend/RecordingProcess.ts b/apps/gb-limelight-recorder/backend/RecordingProcess.ts index 9c5a40d..71b605a 100644 --- a/apps/gb-limelight-recorder/backend/RecordingProcess.ts +++ b/apps/gb-limelight-recorder/backend/RecordingProcess.ts @@ -4,7 +4,7 @@ import { spawn } from "child_process"; import { timeStamp } from "console"; import ffmpegPath from "ffmpeg-static"; -class RecordingProcess { +export class RecordingProcess { public ffmpegProcess: ChildProcess | null = null; public cameraUrl: string; public outputFile: string; @@ -20,10 +20,8 @@ class RecordingProcess { + ".mp4"; this.cameraUrl = - cameraUrl === "left" ? "http://limelight-left.local:5800" - : cameraUrl === "object" ? "http://limelight-object.local:5800" - : cameraUrl === "right" ? "http://limelight.local:5800" - : cameraUrl; + cameraUrl === "right" ? "http://limelight.local:5800" + : `http://limelight-${cameraUrl}.local:5800`; } @@ -65,5 +63,3 @@ class RecordingProcess { return "Recording stopped" } } - -export { RecordingProcess }; \ No newline at end of file diff --git a/apps/gb-limelight-recorder/backend/server.ts b/apps/gb-limelight-recorder/backend/server.ts index 0dfb506..536ecab 100644 --- a/apps/gb-limelight-recorder/backend/server.ts +++ b/apps/gb-limelight-recorder/backend/server.ts @@ -8,22 +8,35 @@ import path from "path"; import { pingCameras } from "./PingRobot.js"; import { useEffect } from "react"; - const app = express(); const port = 5000; app.use(cors()); -const ffmpegProcessLeft: RecordingProcess | null = null; -const ffmpegProcessObject: RecordingProcess | null = null; -const ffmpegProcessRight: RecordingProcess | null = null; -const ffmpegProcesses = [ - ffmpegProcessLeft as RecordingProcess | null, - ffmpegProcessObject as RecordingProcess | null, - ffmpegProcessRight as RecordingProcess | null, -]; -const cameraNames = ["left", "object", "right"]; +type cameraObj = { + name: string + camURL: string + ffmpegProcess: RecordingProcess | null +}; + const usbRoot = "E:/"; // CHANGE if needed +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]; + function createSessionFolder(): string { if (!fs.existsSync(usbRoot)) { throw new Error("USB drive not connected"); @@ -36,7 +49,6 @@ function createSessionFolder(): string { return sessionDir; } - // --- HELLO --- app.get("/", (req, res) => { console.log("GET / route hit"); @@ -48,28 +60,37 @@ 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 (ffmpegProcessLeft || ffmpegProcessObject || ffmpegProcessRight) { + if (cameras[zero].ffmpegProcess + || cameras[one].ffmpegProcess + || cameras[two].ffmpegProcess + ) { return; } const sessionDir = createSessionFolder(); - for (let i = 0; i < ffmpegProcesses.length; i++) { - ffmpegProcesses[i] = new RecordingProcess( - cameraNames[i], - path.join(sessionDir, `${cameraNames[i]}.mp4`) + for (let i = 0; i < three; i++) { + cameras[i].ffmpegProcess = new RecordingProcess( + cameras[i].name, + path.join(sessionDir, `${cameras[i].name}.mp4`) ); - ffmpegProcesses[i]?.startRecording(); + cameras[i].ffmpegProcess?.startRecording(); } } function stopRecording() { - for (let i = 0; i < ffmpegProcesses.length; i++) { - if (ffmpegProcesses[i]) { - ffmpegProcesses[i]?.stopRecording(); - ffmpegProcesses[i] = null; - console.log(`Stopped recording: ${cameraNames[i]}`); + 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}`); } } } diff --git a/apps/gb-limelight-recorder/frontend/src/App.tsx b/apps/gb-limelight-recorder/frontend/src/App.tsx index 4f9548b..3ec1a3c 100644 --- a/apps/gb-limelight-recorder/frontend/src/App.tsx +++ b/apps/gb-limelight-recorder/frontend/src/App.tsx @@ -14,9 +14,9 @@ const App = () => { const res = await fetch("http://localhost:5000/"); const text = await res.text(); setMessage(text); - } catch (e) { + } catch (error) { setMessage("Error connecting to server"); - console.error(e); + console.error(error); } })(); }, []); diff --git a/apps/gb-limelight-recorder/frontend/src/LimelightTable.tsx b/apps/gb-limelight-recorder/frontend/src/LimelightTable.tsx index 46596da..b0e6429 100644 --- a/apps/gb-limelight-recorder/frontend/src/LimelightTable.tsx +++ b/apps/gb-limelight-recorder/frontend/src/LimelightTable.tsx @@ -8,22 +8,25 @@ interface LimelightTableProps { cameras: boolean[]; } +type cameraObj = { + name: string + status: boolean + camURL: string +} + declare module "react" { interface InputHTMLAttributes extends HTMLAttributes { webkitdirectory?: boolean; } } -const leftCamIndex = 0; -const rightCamIndex = 1; - async function doThingy( robotOnline: boolean, - cameraStatus: boolean, + cameras: cameraObj[], index: number ) { - const camera = index === leftCamIndex ? "left" : index === rightCamIndex ? "object" : "right"; - if (robotOnline && cameraStatus) { + const camera = cameras[index]; + if (robotOnline && camera.status) { await fetch(`http://localhost:5000/record/start/${camera}`, { method: "POST", }); @@ -32,44 +35,58 @@ async function doThingy( method: "POST", }); } + camera.status = !camera.status; } const LimelightTable: React.FC = ({ robotOnline }) => { - const cameraStatuses = [false, false, false]; + const leftCamObj: cameraObj = { + name: "left", + status: false, + camURL: "http://limelight-left.local:5800/", + }; + const objectCamObj: cameraObj = { + name: "object", + status: false, + camURL: "http://limelight-object.local:5800/", + }; + const rightCamObj: cameraObj = { + name: "right", + status: false, + camURL: "http://limelight.local:5800/", + }; + const cameras = [leftCamObj, objectCamObj, rightCamObj]; const [fileLocation, setFileLocation] = useState(""); const locationPickerRef = useRef(null); useEffect(() => { - cameraStatuses.forEach((cameraStatus, index) => { - doThingy(robotOnline, cameraStatus, index).catch(() => { + cameras.forEach((camera, index) => { + doThingy(robotOnline, cameras, index).catch(() => { console.error("Couldnt do the thingy"); }); }); }, [robotOnline]); + // For the substring in cameras.map to capitalize first letter + const zero = 0; + const one = 1; + return ( <> - - - + {cameras.map((camera, index) => ( + + ))} - - - + {cameras.map((camera, index) => ( + + ))} - - - + {cameras.map((camera) => ( + + ))}
LeftObjectRight{camera.name.substring(zero, one).toUpperCase()}
limelight-left.local:5800limelight-object.local:5800limelight.local:5800{camera.camURL}
- - - - - -
@@ -79,7 +96,7 @@ const LimelightTable: React.FC = ({ robotOnline }) => { disabled={robotOnline} onClick={() => { const files = locationPickerRef.current?.files; - const file = files?.[leftCamIndex]; + const file = files?.[zero]; setFileLocation(file?.name ?? ""); }} >Save Location From c01f5ac4ec7649c88d8b6c7400fbc214ecce9089 Mon Sep 17 00:00:00 2001 From: emmr253 <159184622+emmr253@users.noreply.github.com> Date: Wed, 4 Feb 2026 19:49:28 +0200 Subject: [PATCH 6/6] limelight recorder done (hopefully) --- apps/gb-limelight-recorder/backend/server.ts | 22 ++------------------ 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/apps/gb-limelight-recorder/backend/server.ts b/apps/gb-limelight-recorder/backend/server.ts index 536ecab..f7cd331 100644 --- a/apps/gb-limelight-recorder/backend/server.ts +++ b/apps/gb-limelight-recorder/backend/server.ts @@ -2,11 +2,9 @@ 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"; import { pingCameras } from "./PingRobot.js"; import { useEffect } from "react"; +import { join } from "path"; const app = express(); const port = 5000; @@ -18,8 +16,6 @@ type cameraObj = { ffmpegProcess: RecordingProcess | null }; -const usbRoot = "E:/"; // CHANGE if needed - const leftCamObj: cameraObj = { name: "left", camURL: "http://limelight-left.local:5800", @@ -37,18 +33,6 @@ const rightCamObj: cameraObj = { }; const cameras: cameraObj[] = [leftCamObj, objectCamObj, rightCamObj]; -function createSessionFolder(): string { - if (!fs.existsSync(usbRoot)) { - throw new Error("USB drive not connected"); - } - - const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); - const sessionDir = path.join(usbRoot, `recording-${timestamp}`); - - fs.mkdirSync(sessionDir, { recursive: true }); - return sessionDir; -} - // --- HELLO --- app.get("/", (req, res) => { console.log("GET / route hit"); @@ -74,12 +58,10 @@ function startRecording() { return; } - const sessionDir = createSessionFolder(); - for (let i = 0; i < three; i++) { cameras[i].ffmpegProcess = new RecordingProcess( cameras[i].name, - path.join(sessionDir, `${cameras[i].name}.mp4`) + join(process.env.USERPROFILE ?? "", "Downloads") ); cameras[i].ffmpegProcess?.startRecording(); }