From 575208cdf3fa9e39736297572c3a9b1d93187e00 Mon Sep 17 00:00:00 2001 From: Nicolas Rabault Date: Thu, 19 Jun 2025 16:35:46 +0200 Subject: [PATCH] not working external phone management --- .gitignore | 20 +- package-lock.json | 462 ++++++++++++++- package.json | 4 + scripts/setup-https.sh | 91 +++ src/App.tsx | 5 + .../recording/CameraConfiguration.tsx | 373 +++++++++++- src/components/ui/QRCodeDisplay.tsx | 92 +++ src/hooks/useNetworkAddress.ts | 215 +++++++ src/pages/RemoteCamera.tsx | 546 ++++++++++++++++++ vite.config.ts | 12 + 10 files changed, 1793 insertions(+), 27 deletions(-) create mode 100755 scripts/setup-https.sh create mode 100644 src/components/ui/QRCodeDisplay.tsx create mode 100644 src/hooks/useNetworkAddress.ts create mode 100644 src/pages/RemoteCamera.tsx diff --git a/.gitignore b/.gitignore index 5fd30d7..41d9d2c 100644 --- a/.gitignore +++ b/.gitignore @@ -73,4 +73,22 @@ wheels/ # Virtual Environment venv/ -ENV/ +ENV/ + +# Generated certificates and SSL files +certs/ +*.pem +*.key +*.crt +*.cert + +# Test files and temporary scripts +test-*.html +test_*.html +test-*.ts +test_*.tsx +*test-*.ts +*test-*.tsx + +# Generated documentation and setup files +HTTPS_SETUP.md diff --git a/package-lock.json b/package-lock.json index 05edd5f..cbbcac1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "@react-three/drei": "^9.122.0", "@react-three/fiber": "^8.18.0", "@tanstack/react-query": "^5.56.2", + "@types/socket.io-client": "^1.4.36", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.0", @@ -48,13 +49,16 @@ "jszip": "^3.10.1", "lucide-react": "^0.462.0", "next-themes": "^0.3.0", + "qrcode": "^1.5.4", "react": "^18.3.1", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-hook-form": "^7.53.0", + "react-qr-code": "^2.0.16", "react-resizable-panels": "^2.1.3", "react-router-dom": "^6.26.2", "recharts": "^2.12.7", + "socket.io-client": "^4.8.1", "sonner": "^1.5.0", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", @@ -2823,6 +2827,12 @@ "win32" ] }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@swc/core": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.12.1.tgz", @@ -3231,6 +3241,12 @@ "@types/react": "*" } }, + "node_modules/@types/socket.io-client": { + "version": "1.4.36", + "resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.36.tgz", + "integrity": "sha512-ZJWjtFBeBy1kRSYpVbeGYTElf6BqPQUkXDlHHD4k/42byCN5Rh027f4yARHCink9sKAkbtGZXEAmR0ZCnc2/Ag==", + "license": "MIT" + }, "node_modules/@types/stats.js": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", @@ -3839,6 +3855,15 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -3943,6 +3968,72 @@ "url": "https://polar.sh/cva" } }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -4207,6 +4298,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decimal.js-light": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", @@ -4241,6 +4341,12 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "license": "Apache-2.0" }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -4310,6 +4416,45 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/esbuild": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", @@ -4757,6 +4902,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -5367,7 +5521,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/mz": { @@ -5510,6 +5663,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -5539,7 +5701,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5612,6 +5773,15 @@ "node": ">= 6" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/postcss": { "version": "8.5.5", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.5.tgz", @@ -5822,6 +5992,29 @@ "node": ">=6" } }, + "node_modules/qr.js": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz", + "integrity": "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==", + "license": "MIT" + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5924,6 +6117,19 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-qr-code": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.16.tgz", + "integrity": "sha512-8f54aTOo7DxYr1LB47pMeclV5SL/zSbJxkXHIS2a+QnAIa4XDVIdmzYRC+CBCJeDLSCeFHn8gHtltwvwZGJD/w==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1", + "qr.js": "0.0.0" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-reconciler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.27.0.tgz", @@ -6171,6 +6377,15 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -6180,6 +6395,12 @@ "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -6318,6 +6539,12 @@ "node": ">=10" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -6357,6 +6584,68 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/sonner": { "version": "1.7.4", "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz", @@ -7540,6 +7829,12 @@ "node": ">= 8" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -7638,6 +7933,41 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, "node_modules/yaml": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", @@ -7650,6 +7980,134 @@ "node": ">= 14.6" } }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index d8b5178..7aca637 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@react-three/drei": "^9.122.0", "@react-three/fiber": "^8.18.0", "@tanstack/react-query": "^5.56.2", + "@types/socket.io-client": "^1.4.36", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.0", @@ -51,13 +52,16 @@ "jszip": "^3.10.1", "lucide-react": "^0.462.0", "next-themes": "^0.3.0", + "qrcode": "^1.5.4", "react": "^18.3.1", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-hook-form": "^7.53.0", + "react-qr-code": "^2.0.16", "react-resizable-panels": "^2.1.3", "react-router-dom": "^6.26.2", "recharts": "^2.12.7", + "socket.io-client": "^4.8.1", "sonner": "^1.5.0", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", diff --git a/scripts/setup-https.sh b/scripts/setup-https.sh new file mode 100755 index 0000000..d399398 --- /dev/null +++ b/scripts/setup-https.sh @@ -0,0 +1,91 @@ +#!/bin/bash + +# HTTPS Setup Script for Local Development +# This script sets up HTTPS for the Vite development server using mkcert + +set -e + +echo "๐Ÿ”’ Setting up HTTPS for local development..." + +# Check if mkcert is installed +if ! command -v mkcert &> /dev/null; then + echo "โŒ mkcert is not installed" + echo "๐Ÿ“ฆ Installing mkcert..." + + # Check if Homebrew is available (macOS) + if command -v brew &> /dev/null; then + brew install mkcert + else + echo "โŒ Please install mkcert manually:" + echo " - macOS: brew install mkcert" + echo " - Linux: Follow instructions at https://github.com/FiloSottile/mkcert#installation" + echo " - Windows: choco install mkcert or download from GitHub" + exit 1 + fi +fi + +echo "โœ… mkcert is installed" + +# Install local CA +echo "๐Ÿ—๏ธ Installing local Certificate Authority..." +mkcert -install + +# Get local IP address +LOCAL_IP="" +if command -v ipconfig &> /dev/null; then + # macOS + LOCAL_IP=$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo "") +fi + +if [ -z "$LOCAL_IP" ]; then + # Linux/Unix fallback + LOCAL_IP=$(hostname -I | awk '{print $1}' 2>/dev/null || echo "") +fi + +if [ -z "$LOCAL_IP" ]; then + # Final fallback + LOCAL_IP=$(ifconfig | grep "inet " | grep -v "127.0.0.1" | head -1 | awk '{print $2}' | sed 's/addr://') +fi + +if [ -z "$LOCAL_IP" ]; then + echo "โš ๏ธ Could not detect local IP address. Using localhost only." + LOCAL_IP="localhost" +fi + +echo "๐Ÿ“ Detected local IP: $LOCAL_IP" + +# Create certs directory +mkdir -p certs + +# Generate certificates +echo "๐Ÿ”ง Generating SSL certificates..." +cd certs + +if [ "$LOCAL_IP" = "localhost" ]; then + mkcert localhost 127.0.0.1 ::1 +else + mkcert localhost 127.0.0.1 "$LOCAL_IP" ::1 +fi + +cd .. + +echo "โœ… HTTPS setup complete!" +echo "" +echo "๐Ÿ“‹ Next steps:" +echo " 1. Start the development server: npm run dev" +echo " 2. Access your app at:" +echo " - https://localhost:8080" +if [ "$LOCAL_IP" != "localhost" ]; then + echo " - https://$LOCAL_IP:8080 (for phone access)" +fi +echo "" +echo "๐Ÿ“ฑ For phone access:" +echo " 1. Make sure your phone is on the same WiFi network" +echo " 2. Open Safari/Chrome on your phone" +if [ "$LOCAL_IP" != "localhost" ]; then + echo " 3. Navigate to: https://$LOCAL_IP:8080" +fi +echo " 4. Accept the security warning (certificate is trusted locally)" +echo " 5. Camera access should now work properly" +echo "" +echo "๐Ÿ”’ The certificates are valid for 3 months and will work across devices on your local network." diff --git a/src/App.tsx b/src/App.tsx index 53b6e47..f761faa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,6 +18,7 @@ import Training from "@/pages/Training"; import ReplayDataset from "@/pages/ReplayDataset"; import EditDataset from "@/pages/EditDataset"; import Upload from "@/pages/Upload"; +import RemoteCamera from "@/pages/RemoteCamera"; import NotFound from "@/pages/NotFound"; import "./App.css"; @@ -44,6 +45,10 @@ function App() { } /> } /> } /> + } + /> } /> diff --git a/src/components/recording/CameraConfiguration.tsx b/src/components/recording/CameraConfiguration.tsx index 2fb7e6d..be05c19 100644 --- a/src/components/recording/CameraConfiguration.tsx +++ b/src/components/recording/CameraConfiguration.tsx @@ -9,9 +9,12 @@ import { SelectValue, } from "@/components/ui/select"; import { Input } from "@/components/ui/input"; -import { Camera, Plus, X, Video, VideoOff } from "lucide-react"; +import { Camera, Plus, X, Video, VideoOff, Smartphone } from "lucide-react"; import { useApi } from "@/contexts/ApiContext"; import { useToast } from "@/hooks/use-toast"; +import { useNetworkAddress } from "@/hooks/useNetworkAddress"; +import { QRCodeDisplay } from "@/components/ui/QRCodeDisplay"; +import io from "socket.io-client"; export interface CameraConfig { id: string; @@ -22,6 +25,7 @@ export interface CameraConfig { width: number; height: number; fps?: number; + session_id?: string; // For phone cameras - used to generate QR code URLs } interface CameraConfigurationProps { @@ -30,11 +34,19 @@ interface CameraConfigurationProps { releaseStreamsRef?: React.MutableRefObject<(() => void) | null>; // Ref to expose stream release function } +interface PhoneStreamData { + streamId: string; + frameData: string; + timestamp: number; + isActive: boolean; +} + interface AvailableCamera { index: number; deviceId: string; name: string; available: boolean; + isPhone?: boolean; // Special flag for phone cameras } const CameraConfiguration: React.FC = ({ @@ -44,6 +56,7 @@ const CameraConfiguration: React.FC = ({ }) => { const { baseUrl, fetchWithHeaders } = useApi(); const { toast } = useToast(); + const networkAddress = useNetworkAddress(); const [availableCameras, setAvailableCameras] = useState( [] @@ -54,12 +67,114 @@ const CameraConfiguration: React.FC = ({ const [cameraStreams, setCameraStreams] = useState>( new Map() ); + const [phoneStreams, setPhoneStreams] = useState< + Map + >(new Map()); + const socketRef = useRef | null>(null); // Fetch available cameras on component mount useEffect(() => { fetchAvailableCameras(); + setupPhoneStreamConnection(); + + return () => { + if (socketRef.current) { + socketRef.current.disconnect(); + } + }; }, []); + // Log network address detection results for testing + useEffect(() => { + if (!networkAddress.loading) { + console.log("๐ŸŒ Network Address Detection Results:"); + console.log(" ๐Ÿ“ Address for QR codes:", networkAddress.address); + console.log(" ๐Ÿ  Is localhost:", networkAddress.isLocalhost); + console.log(" ๐Ÿ“ถ Local network IP:", networkAddress.localNetworkIP); + console.log(" โŒ Error:", networkAddress.error); + + if (networkAddress.isLocalhost && networkAddress.localNetworkIP) { + console.log("โœ… Phone access URL ready:", networkAddress.address); + } else if (networkAddress.isLocalhost && !networkAddress.localNetworkIP) { + console.warn( + "โš ๏ธ Localhost detected but no network IP found. QR codes will use localhost." + ); + } else { + console.log("๐ŸŒ Using current domain/IP for phone access."); + } + } + }, [networkAddress]); + + const setupPhoneStreamConnection = () => { + const hostname = window.location.hostname; + const serverUrl = `http://${hostname}:8000`; + + console.log("๐Ÿ“ฑ Setting up phone stream connection to:", serverUrl); + + socketRef.current = io(serverUrl, { + transports: ["polling"], + timeout: 15000, + forceNew: true, + upgrade: false, + }); + + socketRef.current.on("connect", () => { + console.log("๐Ÿ“ฑ Connected to WebRTC backend for phone streams"); + }); + + socketRef.current.on("disconnect", () => { + console.log("๐Ÿ“ฑ Disconnected from WebRTC backend"); + }); + + // Listen for phone stream frames + socketRef.current.on( + "stream-frame", + (data: { + webrtcId: string; + streamId: string; + frameData: string; + timestamp: number; + }) => { + console.log("๐Ÿ“น Received phone stream frame for:", data.webrtcId); + + setPhoneStreams((prev) => { + const newMap = new Map(prev); + newMap.set(data.webrtcId, { + streamId: data.streamId, + frameData: data.frameData, + timestamp: data.timestamp, + isActive: true, + }); + return newMap; + }); + } + ); + + // Listen for stream status updates + socketRef.current.on( + "stream-started", + (data: { webrtcId: string; streamId: string; metadata: object }) => { + console.log("๐Ÿ“น Phone stream started:", data.webrtcId); + toast({ + title: "Phone Camera Connected", + description: `Stream from ${data.webrtcId} is now active`, + }); + } + ); + + socketRef.current.on("phone-connected", (data: { webrtcId: string }) => { + console.log("๐Ÿ“ฑ Phone connected to session:", data.webrtcId); + toast({ + title: "Phone Connected", + description: `Phone joined session ${data.webrtcId}`, + }); + }); + + socketRef.current.on("connect_error", (error) => { + console.error("๐Ÿ“ฑ Phone stream connection error:", error); + }); + }; + const fetchAvailableCameras = async () => { console.log("๐Ÿš€ fetchAvailableCameras() called"); setIsLoadingCameras(true); @@ -74,7 +189,18 @@ const CameraConfiguration: React.FC = ({ if (response.ok) { const data = await response.json(); console.log("๐Ÿ“ก Backend camera data received:", data); - setAvailableCameras(data.cameras || []); + + // Add phone camera to backend data and set it + const backendCameras = data.cameras || []; + const phoneCamera: AvailableCamera = { + index: -1, // Special index for phone cameras + deviceId: "phone", + name: "Phone", + available: true, + isPhone: true, + }; + console.log("๐Ÿ“ฑ Adding special Phone camera option to backend cameras"); + setAvailableCameras([...backendCameras, phoneCamera]); // Always also try browser detection to get device IDs console.log("๐Ÿ”„ Also running browser detection for device IDs..."); @@ -133,10 +259,29 @@ const CameraConfiguration: React.FC = ({ available: true, })); + // Always add the special "Phone" camera option at the end + const phoneCamera: AvailableCamera = { + index: -1, // Special index for phone cameras + deviceId: "phone", + name: "Phone", + available: true, + isPhone: true, + }; + console.log("๐ŸŽฌ Browser cameras with indices mapped:", detectedCameras); - setAvailableCameras(detectedCameras); + console.log("๐Ÿ“ฑ Adding special Phone camera option"); + setAvailableCameras([...detectedCameras, phoneCamera]); } catch (error) { console.error("Error detecting browser cameras:", error); + // Even if camera detection fails, still add the Phone option + const phoneCamera: AvailableCamera = { + index: -1, + deviceId: "phone", + name: "Phone", + available: true, + isPhone: true, + }; + setAvailableCameras([phoneCamera]); toast({ title: "Camera Detection Failed", description: @@ -146,7 +291,45 @@ const CameraConfiguration: React.FC = ({ } }; + const createPhoneSession = (cameraConfig: CameraConfig) => { + if (!socketRef.current || cameraConfig.type !== "phone") { + console.warn( + "Cannot create phone session - socket not connected or not phone camera" + ); + return; + } + + console.log( + "๐Ÿ“ฑ Creating phone session for:", + cameraConfig.name, + cameraConfig.device_id + ); + + // Emit create session event to backend + socketRef.current.emit("create_session", { + webrtcId: cameraConfig.device_id, + }); + + toast({ + title: "Phone Session Created", + description: `Waiting for phone to connect to ${cameraConfig.name}`, + }); + }; + const startCameraPreview = async (cameraConfig: CameraConfig) => { + // Phone cameras don't have real device streams + if (cameraConfig.type === "phone") { + console.log( + "๐Ÿ“ฑ Phone camera detected, skipping preview:", + cameraConfig.name + ); + toast({ + title: "Phone Camera Added", + description: `${cameraConfig.name} is ready. Use the remote camera interface to connect.`, + }); + return null; + } + try { console.log( "๐ŸŽฅ Starting camera preview for:", @@ -166,10 +349,11 @@ const CameraConfiguration: React.FC = ({ }, }; - // Only add deviceId if it's not a fallback + // Only add deviceId if it's not a fallback or phone device if ( cameraConfig.device_id && - !cameraConfig.device_id.startsWith("fallback_") + !cameraConfig.device_id.startsWith("fallback_") && + !cameraConfig.device_id.startsWith("phone_") ) { (constraints.video as MediaTrackConstraints).deviceId = { exact: cameraConfig.device_id, // Changed from 'ideal' to 'exact' @@ -313,8 +497,11 @@ const CameraConfiguration: React.FC = ({ return; } - // Check if camera is already added - if (cameras.some((cam) => cam.camera_index === cameraIndex)) { + // Check if camera is already added (skip for phone cameras as they can be added multiple times) + if ( + !selectedCamera.isPhone && + cameras.some((cam) => cam.camera_index === cameraIndex) + ) { toast({ title: "Camera Already Added", description: "This camera is already in the configuration.", @@ -323,29 +510,43 @@ const CameraConfiguration: React.FC = ({ return; } + // Generate unique session ID for phone cameras + const sessionId = selectedCamera.isPhone + ? `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + : undefined; + const newCamera: CameraConfig = { - id: `camera_${Date.now()}`, + id: `camera_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, name: cameraName.trim(), - type: "opencv", - camera_index: selectedCamera.index, - device_id: selectedCamera.deviceId, + type: selectedCamera.isPhone ? "phone" : "opencv", + camera_index: selectedCamera.isPhone ? undefined : selectedCamera.index, + device_id: selectedCamera.isPhone + ? `phone_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + : selectedCamera.deviceId, width: 640, height: 480, fps: 30, + session_id: sessionId, }; console.log("๐Ÿ†• Creating new camera config:", { name: newCamera.name, + type: newCamera.type, camera_index: newCamera.camera_index, device_id: newCamera.device_id, selectedCamera: selectedCamera, + isPhone: selectedCamera.isPhone, }); const updatedCameras = [...cameras, newCamera]; onCamerasChange(updatedCameras); - // Start preview for the new camera - await startCameraPreview(newCamera); + // For phone cameras, create WebRTC session instead of preview + if (selectedCamera.isPhone) { + createPhoneSession(newCamera); + } else { + await startCameraPreview(newCamera); + } // Reset form setSelectedCameraIndex(""); @@ -408,6 +609,33 @@ const CameraConfiguration: React.FC = ({ Camera Configuration + {/* Network Address Info */} + {!networkAddress.loading && ( +
+
+ + ๐Ÿ“ฑ Phone Access URL: + + + {networkAddress.address} + + {networkAddress.isLocalhost && networkAddress.localNetworkIP && ( + + (Network IP detected) + + )} + {networkAddress.isLocalhost && !networkAddress.localNetworkIP && ( + (Using localhost) + )} +
+ {networkAddress.error && ( +
+ Note: {networkAddress.error} +
+ )} +
+ )} + {/* Add Camera Section */}

Add Camera

@@ -432,17 +660,29 @@ const CameraConfiguration: React.FC = ({ {availableCameras.map((camera) => ( cam.camera_index === camera.index) + (!camera.isPhone && + cameras.some( + (cam) => cam.camera_index === camera.index + )) } > - {camera.name} (Index {camera.index}) - {cameras.some((cam) => cam.camera_index === camera.index) && - " (Already added)"} + {camera.isPhone ? ( + <>๐Ÿ“ฑ {camera.name} (Remote Camera) + ) : ( + <> + {camera.name} (Index {camera.index}) + {cameras.some( + (cam) => cam.camera_index === camera.index + ) && " (Already added)"} + + )} ))} @@ -487,6 +727,7 @@ const CameraConfiguration: React.FC = ({ key={camera.id} camera={camera} stream={cameraStreams.get(camera.id)} + phoneStream={phoneStreams.get(camera.device_id)} onRemove={() => removeCamera(camera.id)} onUpdate={(updates) => updateCamera(camera.id, updates)} onStartPreview={() => startCameraPreview(camera)} @@ -509,6 +750,7 @@ const CameraConfiguration: React.FC = ({ interface CameraPreviewProps { camera: CameraConfig; stream?: MediaStream; + phoneStream?: PhoneStreamData; onRemove: () => void; onUpdate: (updates: Partial) => void; onStartPreview: () => void; @@ -517,12 +759,25 @@ interface CameraPreviewProps { const CameraPreview: React.FC = ({ camera, stream, + phoneStream, onRemove, onUpdate, onStartPreview, }) => { const videoRef = useRef(null); const [isPreviewActive, setIsPreviewActive] = useState(false); + const networkAddress = useNetworkAddress(); + + // Generate QR code URL for phone cameras + const generatePhoneUrl = (camera: CameraConfig) => { + if (camera.type !== "phone" || !camera.session_id) return ""; + + const baseUrl = + networkAddress.address || + `${window.location.protocol}//${window.location.host}`; + const webrtcId = camera.device_id; + return `${baseUrl}/remote_cam/${camera.session_id}?webrtcId=${webrtcId}`; + }; // Debug logging for props console.log("CameraPreview render for:", camera.name, { @@ -572,19 +827,73 @@ const CameraPreview: React.FC = ({ }, [stream, camera.name]); useEffect(() => { - // Auto-start preview when camera is added - if (!stream && !isPreviewActive) { + // Auto-start preview when camera is added (but not for phone cameras) + if (!stream && !isPreviewActive && camera.type !== "phone") { console.log("Auto-starting preview for camera:", camera.name); onStartPreview(); } - }, [stream, isPreviewActive, onStartPreview, camera.name]); + }, [stream, isPreviewActive, onStartPreview, camera.name, camera.type]); return (
{/* Camera Preview */}
- {/* Always show the video element if we have a stream, regardless of isPreviewActive */} - {stream ? ( + {camera.type === "phone" ? ( + /* Phone Camera - Show Stream if Active, Otherwise QR Code */ +
+ {phoneStream && phoneStream.isActive ? ( + /* Live Phone Stream */ + <> + Phone Camera Stream +
+
+
+ LIVE +
+
+
+ ๐Ÿ“ฑ Phone Stream +
+ + ) : ( + /* QR Code Display */ +
+ {camera.session_id ? ( + + ) : ( +
+ + + Phone Camera + + + Generating QR code... + +
+ )} + +
+
+
+ WAITING +
+
+
+ )} +
+ ) : stream ? ( + /* Regular Camera Stream */ <>
) : ( + /* No Stream Available */
Preview not available @@ -683,8 +993,23 @@ const CameraPreview: React.FC = ({
- Type: {camera.type} | Device: {camera.device_id?.substring(0, 10)}... + Type: {camera.type} |{" "} + {camera.type === "phone" && camera.session_id ? ( + <>Session: {camera.session_id.substring(0, 12)}... + ) : ( + <>Device: {camera.device_id?.substring(0, 10)}... + )}
+ + {/* Phone Camera URL Display */} + {camera.type === "phone" && camera.session_id && ( +
+ Phone URL: +
+ {generatePhoneUrl(camera)} +
+
+ )}
); diff --git a/src/components/ui/QRCodeDisplay.tsx b/src/components/ui/QRCodeDisplay.tsx new file mode 100644 index 0000000..f9af367 --- /dev/null +++ b/src/components/ui/QRCodeDisplay.tsx @@ -0,0 +1,92 @@ +import React from "react"; +import QRCode from "react-qr-code"; +import { Copy, ExternalLink } from "lucide-react"; +import { Button } from "./button"; +import { useToast } from "@/hooks/use-toast"; + +interface QRCodeDisplayProps { + url: string; + size?: number; + title?: string; + showControls?: boolean; + showUrl?: boolean; + className?: string; +} + +export const QRCodeDisplay: React.FC = ({ + url, + size = 200, + title = "Scan QR Code", + showControls = true, + showUrl = true, + className = "", +}) => { + const { toast } = useToast(); + + const copyToClipboard = async () => { + try { + await navigator.clipboard.writeText(url); + toast({ + title: "URL Copied", + description: "Phone camera URL copied to clipboard", + }); + } catch (err) { + console.error("Failed to copy URL:", err); + toast({ + title: "Copy Failed", + description: "Could not copy URL to clipboard", + variant: "destructive", + }); + } + }; + + const openInNewTab = () => { + window.open(url, "_blank"); + }; + + return ( +
+ {/* QR Code */} +
+ +
+ + {/* Title */} +
+

{title}

+ {showUrl && ( +

{url}

+ )} +
+ + {/* Controls */} + {showControls && ( +
+ + +
+ )} +
+ ); +}; diff --git a/src/hooks/useNetworkAddress.ts b/src/hooks/useNetworkAddress.ts new file mode 100644 index 0000000..474d6db --- /dev/null +++ b/src/hooks/useNetworkAddress.ts @@ -0,0 +1,215 @@ +import { useState, useEffect } from 'react'; + +interface NetworkAddressResult { + address: string; + isLocalhost: boolean; + localNetworkIP: string | null; + loading: boolean; + error: string | null; +} + +export const useNetworkAddress = (): NetworkAddressResult => { + const [result, setResult] = useState({ + address: window.location.origin, + isLocalhost: false, + localNetworkIP: null, + loading: true, + error: null, + }); + + const detectLocalNetworkIP = async (): Promise => { + return new Promise((resolve) => { + const timeout = setTimeout(() => { + console.warn('๐ŸŒ Network IP detection timeout'); + resolve(null); + }, 3000); + + try { + // Create RTCPeerConnection to enumerate network interfaces + const pc = new RTCPeerConnection({ + iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] + }); + + pc.createDataChannel(''); + + pc.createOffer() + .then(offer => pc.setLocalDescription(offer)) + .catch(() => { + clearTimeout(timeout); + resolve(null); + }); + + pc.onicecandidate = (event) => { + if (!event.candidate) return; + + const candidate = event.candidate.candidate; + console.log('๐Ÿ” ICE Candidate:', candidate); + + // Look for IPv4 addresses that are not localhost + const ipMatch = candidate.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/); + if (ipMatch) { + const ip = ipMatch[1]; + + // Check if it's a local network IP (not localhost, not public) + if ( + ip !== '127.0.0.1' && + (ip.startsWith('192.168.') || + ip.startsWith('10.') || + ip.startsWith('172.')) + ) { + console.log('โœ… Found local network IP:', ip); + clearTimeout(timeout); + pc.close(); + resolve(ip); + return; + } + } + }; + + // Fallback: try to close and resolve after a short delay + setTimeout(() => { + if (pc.iceConnectionState !== 'closed') { + pc.close(); + } + }, 2000); + + } catch (error) { + console.error('โŒ Error detecting network IP:', error); + clearTimeout(timeout); + resolve(null); + } + }); + }; + + const detectNetworkAddress = async () => { + console.log('๐Ÿš€ Starting network address detection...'); + + const currentOrigin = window.location.origin; + const hostname = window.location.hostname; + + console.log('๐Ÿ“ Current origin:', currentOrigin); + console.log('๐Ÿ“ Current hostname:', hostname); + + const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1'; + + setResult(prev => ({ + ...prev, + isLocalhost, + loading: true, + error: null, + })); + + if (isLocalhost) { + console.log('๐Ÿ  Localhost detected, attempting to find local network IP...'); + + try { + const localIP = await detectLocalNetworkIP(); + + if (localIP) { + const port = window.location.port; + const protocol = window.location.protocol; // Will be https: if using HTTPS + const networkAddress = `${protocol}//${localIP}${port ? `:${port}` : ''}`; + + console.log('โœ… Network address computed:', networkAddress); + console.log('๐Ÿ”’ Protocol detected:', protocol); + + setResult({ + address: networkAddress, + isLocalhost: true, + localNetworkIP: localIP, + loading: false, + error: null, + }); + } else { + console.warn('โš ๏ธ Could not detect local network IP, using localhost'); + setResult({ + address: currentOrigin, + isLocalhost: true, + localNetworkIP: null, + loading: false, + error: 'Could not detect local network IP', + }); + } + } catch (error) { + console.error('โŒ Error in network detection:', error); + setResult({ + address: currentOrigin, + isLocalhost: true, + localNetworkIP: null, + loading: false, + error: error instanceof Error ? error.message : 'Unknown error', + }); + } + } else { + console.log('๐ŸŒ Using current address:', currentOrigin); + setResult({ + address: currentOrigin, + isLocalhost: false, + localNetworkIP: null, + loading: false, + error: null, + }); + } + }; + + useEffect(() => { + detectNetworkAddress(); + }, []); + + return result; +}; + +// Utility function for one-time address detection +export const getNetworkAddress = async (): Promise => { + const currentOrigin = window.location.origin; + const hostname = window.location.hostname; + + const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1'; + + if (!isLocalhost) { + return currentOrigin; + } + + // For localhost, try to get network IP + try { + const localIP = await new Promise((resolve) => { + const timeout = setTimeout(() => resolve(null), 2000); + + const pc = new RTCPeerConnection({ + iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] + }); + + pc.createDataChannel(''); + pc.createOffer().then(offer => pc.setLocalDescription(offer)); + + pc.onicecandidate = (event) => { + if (!event.candidate) return; + + const candidate = event.candidate.candidate; + const ipMatch = candidate.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/); + + if (ipMatch) { + const ip = ipMatch[1]; + if ( + ip !== '127.0.0.1' && + (ip.startsWith('192.168.') || ip.startsWith('10.') || ip.startsWith('172.')) + ) { + clearTimeout(timeout); + pc.close(); + resolve(ip); + } + } + }; + }); + + if (localIP) { + const port = window.location.port; + const protocol = window.location.protocol; // Will be https: if using HTTPS + return `${protocol}//${localIP}${port ? `:${port}` : ''}`; + } + } catch (error) { + console.error('Error detecting network IP:', error); + } + + return currentOrigin; +}; diff --git a/src/pages/RemoteCamera.tsx b/src/pages/RemoteCamera.tsx new file mode 100644 index 0000000..937cf9e --- /dev/null +++ b/src/pages/RemoteCamera.tsx @@ -0,0 +1,546 @@ +import React, { useState, useEffect, useRef, useCallback } from "react"; +import { useParams, useSearchParams } from "react-router-dom"; +import { Button } from "@/components/ui/button"; +import { + Video, + VideoOff, + Smartphone, + Wifi, + WifiOff, + Camera, + Settings, + RotateCcw, +} from "lucide-react"; +import { useToast } from "@/hooks/use-toast"; +import io from "socket.io-client"; + +const RemoteCamera: React.FC = () => { + const { sessionId } = useParams<{ sessionId: string }>(); + const [searchParams] = useSearchParams(); + const webrtcId = searchParams.get("webrtcId"); + const { toast } = useToast(); + + const [isConnected, setIsConnected] = useState(false); + const [isStreaming, setIsStreaming] = useState(false); + const [connectionStatus, setConnectionStatus] = useState< + "disconnected" | "connecting" | "connected" | "error" + >("disconnected"); + const [stream, setStream] = useState(null); + const [error, setError] = useState(null); + const [framesSent, setFramesSent] = useState(0); + const [lastFrameTime, setLastFrameTime] = useState(0); + const [fps, setFps] = useState(0); + + const videoRef = useRef(null); + const socketRef = useRef | null>(null); + const frameIntervalRef = useRef(null); + + // Debug info + const [debugInfo, setDebugInfo] = useState({ + sessionId: sessionId || "N/A", + webrtcId: webrtcId || "N/A", + serverUrl: "", + userAgent: navigator.userAgent, + }); + + useEffect(() => { + if (!sessionId || !webrtcId) { + setError("Missing session ID or WebRTC ID in URL"); + return; + } + + connectToServer(); + + return () => { + cleanup(); + }; + }, [sessionId, webrtcId]); + + // Update FPS calculation + useEffect(() => { + const interval = setInterval(() => { + const now = Date.now(); + if (lastFrameTime > 0) { + const timeDiff = (now - lastFrameTime) / 1000; + if (timeDiff > 0) { + setFps(Math.round(framesSent / timeDiff)); + } + } + }, 1000); + + return () => clearInterval(interval); + }, [framesSent, lastFrameTime]); + + const connectToServer = async () => { + if (!sessionId || !webrtcId) { + setError("Missing session ID or WebRTC ID"); + return; + } + + try { + setConnectionStatus("connecting"); + setError(null); + + // Force HTTP with polling to avoid mixed content issues + // Polling transport works even when frontend is HTTPS and backend is HTTP + const hostname = window.location.hostname; + const serverUrl = `http://${hostname}:8000`; + + setDebugInfo((prev) => ({ ...prev, serverUrl })); + + console.log("๐Ÿ“ฑ Connecting to WebRTC server:", serverUrl); + + socketRef.current = io(serverUrl, { + transports: ["polling"], // Polling only to avoid WebSocket mixed content blocking + timeout: 15000, + forceNew: true, + upgrade: false, // Prevent upgrade to WebSocket + }); + + // Set up the main event handlers + setupSocketHandlers(); + } catch (err) { + console.error("Failed to connect:", err); + setError("Failed to connect to server"); + setConnectionStatus("error"); + } + }; + + const setupSocketHandlers = () => { + if (!socketRef.current) return; + + socketRef.current.on("connect", () => { + console.log("๐Ÿ“ฑ Connected to WebRTC server"); + setIsConnected(true); + setConnectionStatus("connected"); + + // Update debug info with successful URL + const actualUrl = socketRef.current?.io?.uri || "unknown"; + setDebugInfo((prev) => ({ + ...prev, + serverUrl: `${actualUrl} (connected)`, + })); + + // Join the session + socketRef.current?.emit("join_session", { webrtcId }); + + toast({ + title: "Connected", + description: "Successfully connected to the camera system", + }); + }); + + socketRef.current.on("disconnect", (reason) => { + console.log("๐Ÿ“ฑ Disconnected from WebRTC server:", reason); + setIsConnected(false); + setConnectionStatus("disconnected"); + + toast({ + title: "Disconnected", + description: `Connection lost: ${reason}`, + variant: "destructive", + }); + }); + + socketRef.current.on("connect_error", (error) => { + console.error("๐Ÿ“ฑ Connection error:", error); + setConnectionStatus("error"); + setError(`Connection failed: ${error.message}`); + + toast({ + title: "Connection Error", + description: `Could not connect to server: ${error.message}`, + variant: "destructive", + }); + }); + + socketRef.current.on("session-joined", (data: { webrtcId: string }) => { + console.log("๐Ÿ“ฑ Joined session:", data); + toast({ + title: "Session Joined", + description: "Ready to start camera stream", + }); + }); + + socketRef.current.on("session-error", (data: { error: string }) => { + console.error("โŒ Session error:", data); + setError(data.error); + setConnectionStatus("error"); + toast({ + title: "Session Error", + description: data.error, + variant: "destructive", + }); + }); + }; + + const startStream = async () => { + try { + setError(null); + console.log("๐Ÿ“ท Requesting camera access..."); + + // Request camera permission with mobile-optimized settings + const mediaStream = await navigator.mediaDevices.getUserMedia({ + video: { + width: { ideal: 1280, min: 640, max: 1920 }, + height: { ideal: 720, min: 480, max: 1080 }, + frameRate: { ideal: 30, min: 15, max: 60 }, + facingMode: "environment", // Use back camera on mobile + }, + audio: false, + }); + + console.log("๐Ÿ“ท Camera access granted"); + setStream(mediaStream); + + if (videoRef.current) { + videoRef.current.srcObject = mediaStream; + await videoRef.current.play(); + } + + // Notify server that stream is ready + if (socketRef.current && isConnected) { + const videoTrack = mediaStream.getVideoTracks()[0]; + const settings = videoTrack.getSettings(); + + socketRef.current.emit("stream_ready", { + webrtcId, + metadata: { + width: settings.width || 1280, + height: settings.height || 720, + fps: settings.frameRate || 30, + codec: "h264", + deviceId: settings.deviceId, + facingMode: settings.facingMode, + }, + }); + + console.log("๐Ÿ“ท Stream metadata sent:", settings); + } + + setIsStreaming(true); + setFramesSent(0); + setLastFrameTime(Date.now()); + + toast({ + title: "Camera Started", + description: "Your phone camera is now streaming", + }); + + // Start frame capture + startFrameCapture(mediaStream); + } catch (err: unknown) { + console.error("Error starting camera:", err); + const errorMessage = err instanceof Error ? err.message : "Unknown error"; + setError(errorMessage); + + // Provide more helpful error messages + let userMessage = "Could not access your phone's camera"; + if (errorMessage.includes("Permission denied")) { + userMessage = + "Camera permission denied. Please allow camera access and try again."; + } else if (errorMessage.includes("NotFound")) { + userMessage = "No camera found on this device."; + } else if (errorMessage.includes("NotSupported")) { + userMessage = "Camera not supported on this device."; + } + + toast({ + title: "Camera Error", + description: userMessage, + variant: "destructive", + }); + } + }; + + const stopStream = () => { + console.log("๐Ÿ“ท Stopping camera stream..."); + + if (frameIntervalRef.current) { + clearInterval(frameIntervalRef.current); + frameIntervalRef.current = null; + } + + if (stream) { + stream.getTracks().forEach((track) => { + track.stop(); + console.log("๐Ÿ“ท Stopped track:", track.kind); + }); + setStream(null); + } + + if (videoRef.current) { + videoRef.current.srcObject = null; + } + + setIsStreaming(false); + setFramesSent(0); + setFps(0); + + toast({ + title: "Camera Stopped", + description: "Phone camera stream has been stopped", + }); + }; + + const startFrameCapture = (mediaStream: MediaStream) => { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + const video = videoRef.current; + + if (!video || !ctx) { + console.error("โŒ Video element or canvas context not available"); + return; + } + + console.log("๐Ÿ“น Starting frame capture..."); + + const sendFrame = () => { + if ( + !isStreaming || + !socketRef.current || + !isConnected || + !video.videoWidth + ) { + return; + } + + try { + // Set canvas size to match video + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + + // Draw current video frame to canvas + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + + // Convert to JPEG with compression + const frameData = canvas.toDataURL("image/jpeg", 0.6); + + // Send frame to server + socketRef.current?.emit("stream_data", { + webrtcId, + frameData, + timestamp: Date.now(), + sequence: framesSent, + metadata: { + width: canvas.width, + height: canvas.height, + }, + }); + + setFramesSent((prev) => prev + 1); + setLastFrameTime(Date.now()); + } catch (err) { + console.error("Error capturing frame:", err); + } + }; + + // Start sending frames at 10 FPS to avoid overwhelming connection + frameIntervalRef.current = setInterval(sendFrame, 100); + console.log("๐Ÿ“น Frame capture started at 10 FPS"); + }; + + const cleanup = () => { + console.log("๐Ÿงน Cleaning up..."); + stopStream(); + + if (socketRef.current) { + socketRef.current.disconnect(); + socketRef.current = null; + } + }; + + const getStatusIcon = () => { + switch (connectionStatus) { + case "connected": + return ; + case "connecting": + return ; + case "error": + return ; + default: + return ; + } + }; + + const getStatusText = () => { + switch (connectionStatus) { + case "connected": + return "Connected"; + case "connecting": + return "Connecting..."; + case "error": + return "Connection Error"; + default: + return "Disconnected"; + } + }; + + if (!sessionId || !webrtcId) { + return ( +
+
+ +

Invalid URL

+

+ Missing session ID or WebRTC ID in URL +

+

+ URL should be: + /remote_cam/<session_id>?webrtcId=<webrtc_id> +

+
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+
+ +
+

+ Remote Camera +

+

+ Session: {sessionId?.substring(0, 8)}... +

+
+
+ +
+ {getStatusIcon()} + {getStatusText()} +
+
+
+ + {/* Main Content */} +
+ {/* Video Preview */} +
+ {isStreaming ? ( +
+ + {/* Controls */} +
+ {error && ( +
+

Error:

+

{error}

+
+ )} + +
+ {!isStreaming ? ( + + ) : ( + + )} + + {!isConnected && ( + + )} +
+ + {/* Debug Information */} +
+ + + Debug Info + +
+
+ Session ID: {debugInfo.sessionId} +
+
+ WebRTC ID: {debugInfo.webrtcId} +
+
+ Server URL: {debugInfo.serverUrl} +
+
+ Status: {connectionStatus} +
+
+ Streaming: {isStreaming ? "Yes" : "No"} +
+ {stream && ( +
+ Stream Active: {stream.active ? "Yes" : "No"} +
+ )} +
+ User Agent: {debugInfo.userAgent} +
+
+
+
+
+
+ ); +}; + +export default RemoteCamera; diff --git a/vite.config.ts b/vite.config.ts index 6ef54e8..d133786 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,13 +1,25 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react-swc"; import path from "path"; +import fs from "fs"; import { componentTagger } from "lovable-tagger"; +// Check if certificates exist +const certPath = path.resolve(__dirname, "./certs/localhost+3.pem"); +const keyPath = path.resolve(__dirname, "./certs/localhost+3-key.pem"); +const certsExist = fs.existsSync(certPath) && fs.existsSync(keyPath); + // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ server: { host: "::", port: 8080, + ...(certsExist && { + https: { + key: fs.readFileSync(keyPath), + cert: fs.readFileSync(certPath), + }, + }), // Proxy removed - the React app now handles API URLs directly through the ApiContext // This provides full flexibility for localhost/ngrok switching at runtime },