From 31136811481d1fd862fd952bd06cf241b0b2a4bd Mon Sep 17 00:00:00 2001 From: Glenn Engel Date: Sat, 18 May 2024 10:24:51 -0700 Subject: [PATCH] Allow camera selection from discovered cameras. Clean up UI. --- .gitignore | 1 + native/recorder/src/BaslerReader.cpp | 2 +- native/recorder/src/NdiReader.cpp | 106 +++++++------ native/recorder/src/RecorderAPI.cpp | 89 ++++++----- native/recorder/src/VideoController.hpp | 59 +++++--- native/recorder/src/VideoReader.hpp | 15 +- native/recorder/src/ctrecorder.cpp | 5 +- package.json | 1 + src/main/main.ts | 1 + src/main/preload.ts | 1 + src/main/util/fileops-handler.ts | 65 ++++++++ src/main/util/util-handlers.ts | 8 + src/main/util/util-preload.ts | 87 +++++++++++ src/renderer/recorder/RecorderApi.ts | 10 ++ src/renderer/recorder/RecorderConfig.tsx | 180 ++++++++++++++--------- src/renderer/recorder/RecorderData.ts | 1 + src/renderer/recorder/RecorderTypes.ts | 11 +- src/renderer/util/util.d.ts | 32 ++++ yarn.lock | 5 + 19 files changed, 502 insertions(+), 177 deletions(-) create mode 100644 src/main/util/fileops-handler.ts create mode 100644 src/main/util/util-handlers.ts create mode 100644 src/main/util/util-preload.ts create mode 100644 src/renderer/util/util.d.ts diff --git a/.gitignore b/.gitignore index f031e37..683212b 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ npm-debug.log.* *.sass.d.ts *.scss.d.ts *.mp4 +query.json diff --git a/native/recorder/src/BaslerReader.cpp b/native/recorder/src/BaslerReader.cpp index f4c99dc..0466559 100644 --- a/native/recorder/src/BaslerReader.cpp +++ b/native/recorder/src/BaslerReader.cpp @@ -226,7 +226,7 @@ class BaslerReader : public VideoReader, public CImageEventHandler { PylonTerminate(); return 0; } - virtual std::string start() override { + virtual std::string const std::string srcName) override { keepRunning = true; readerThread = std::thread([this]() { run(); }); readerThread.join(); diff --git a/native/recorder/src/NdiReader.cpp b/native/recorder/src/NdiReader.cpp index fa7194a..0d6ecb0 100644 --- a/native/recorder/src/NdiReader.cpp +++ b/native/recorder/src/NdiReader.cpp @@ -201,8 +201,9 @@ class NdiReader : public VideoReader { std::thread ndiThread; std::atomic keepRunning; std::shared_ptr frameProcessor; - NDIlib_recv_instance_t pNDI_recv; - const std::string srcName; + NDIlib_recv_instance_t pNDI_recv = nullptr; + NDIlib_find_instance_t pNDI_find = nullptr; + std::string srcName; class NdiFrame : public Frame { NDIlib_recv_instance_t pNDI_recv; @@ -222,49 +223,57 @@ class NdiReader : public VideoReader { return ""; } - std::string connect() { + std::vector getCameraList() override { + std::vector list; // Create a finder - NDIlib_find_instance_t pNDI_find = NDIlib_find_create_v2(); - if (!pNDI_find) - return "NDIlib_find_create_v2() failed"; + if (pNDI_find == nullptr) { + pNDI_find = NDIlib_find_create_v2(); + } - // Wait until there is a source + if (!pNDI_find) + return list; + + const NDIlib_source_t *p_sources = NULL; + uint32_t no_sources = 0; + while (!no_sources) { + // Wait until the sources on the network have changed + NDIlib_find_wait_for_sources(pNDI_find, 2000); + p_sources = NDIlib_find_get_current_sources(pNDI_find, &no_sources); + } - const NDIlib_source_t *p_source = NULL; + for (int src = 0; src < no_sources; src++) { + list.push_back( + CameraInfo(p_sources[src].p_ndi_name, p_sources[src].p_url_address)); + // SystemEventQueue::push("NDI", std::string("Source Found: ") + + // p_sources[src].p_ndi_name + " at " + + // p_sources[src].p_ip_address); + } - // FIXME - allow passing in srcName - while (!p_source && keepRunning.load()) { - SystemEventQueue::push("NDI", "Looking for source " + srcName); - const NDIlib_source_t *p_sources = NULL; - uint32_t no_sources = 0; - while (!no_sources) { - // Wait until the sources on the network have changed - NDIlib_find_wait_for_sources(pNDI_find, 1000 /* One second */); - p_sources = NDIlib_find_get_current_sources(pNDI_find, &no_sources); - } + return list; + }; - for (int src = 0; src < no_sources; src++) { - SystemEventQueue::push("NDI", std::string("Source Found: ") + - p_sources[src].p_ndi_name + " at " + - p_sources[src].p_ip_address); - if (std::string(p_sources[src].p_ndi_name).find(srcName) == 0) { - p_source = p_sources + src; + std::string connect() { + NDIlib_source_t p_source; + CameraInfo foundCamera; + std::vector cameras; + while (foundCamera.name == "" && keepRunning.load()) { + cameras = getCameraList(); + for (auto camera : cameras) { + // std::cout << "Found camera: " << camera.name << std::endl; + if (camera.name.find(srcName) == 0) { + foundCamera = camera; + break; } } } - if (!p_source) { + if (foundCamera.name == "") { return ""; // stop received before ndi source found } NDIlib_recv_create_v3_t recv_create; -#ifdef NDI_BGRX - recv_create.color_format = - NDIlib_recv_color_format_BGRX_BGRA; // NDIlib_recv_color_format_RGBX_RGBA; -#else recv_create.color_format = NDIlib_recv_color_format_UYVY_BGRA; -#endif // We now have at least one source, so we create a receiver to look at it. pNDI_recv = NDIlib_recv_create_v3(&recv_create); if (!pNDI_recv) @@ -272,14 +281,14 @@ class NdiReader : public VideoReader { // Connect to the source SystemEventQueue::push("NDI", std::string("Connecting to ") + - p_source->p_ndi_name + " at " + - p_source->p_ip_address); + foundCamera.name + " at " + + foundCamera.address); // Connect to our sources - NDIlib_recv_connect(pNDI_recv, p_source); - - // Destroy the NDI finder. We needed to have access to the pointers to - // p_sources[0] - NDIlib_find_destroy(pNDI_find); + p_source.p_ndi_name = foundCamera.name.c_str(); + p_source.p_url_address = foundCamera.address.c_str(); + NDIlib_recv_connect(pNDI_recv, &p_source); + foundCamera.name = ""; + cameras.clear(); return ""; } @@ -305,7 +314,8 @@ class NdiReader : public VideoReader { if (video_frame.xres && video_frame.yres) { frameCount++; if (frameCount == 1) { - break; // 1st frame often old frame cached from ndi sender. Ignore. + break; // 1st frame often old frame cached from ndi sender. + // Ignore. } if (video_frame.timestamp == NDIlib_recv_timestamp_undefined) { std::cerr << "timestamp not supported" << std::endl; @@ -378,8 +388,9 @@ class NdiReader : public VideoReader { } public: - NdiReader(const std::string srcName) : srcName(srcName) {} - std::string start() override { + NdiReader() {} + std::string start(const std::string srcName) override { + this->srcName = srcName; keepRunning = true; ndiThread = std::thread([this]() { run(); }); #ifndef _WIN32 @@ -389,11 +400,20 @@ class NdiReader : public VideoReader { }; std::string stop() override { keepRunning = false; - ndiThread.join(); + if (ndiThread.joinable()) { + ndiThread.join(); + } return ""; } + virtual ~NdiReader() override { + stop(); + + if (pNDI_find != nullptr) { + NDIlib_find_destroy(pNDI_find); + } + }; }; -std::shared_ptr createNdiReader(std::string srcName) { - return std::shared_ptr(new NdiReader(srcName)); +std::shared_ptr createNdiReader() { + return std::shared_ptr(new NdiReader()); } diff --git a/native/recorder/src/RecorderAPI.cpp b/native/recorder/src/RecorderAPI.cpp index f47073b..c28963d 100644 --- a/native/recorder/src/RecorderAPI.cpp +++ b/native/recorder/src/RecorderAPI.cpp @@ -21,7 +21,8 @@ extern "C" { #include "VideoController.hpp" using json = nlohmann::json; -std::shared_ptr recorder; +std::shared_ptr recorder = + std::shared_ptr(new VideoController("ndi")); // Utility function to clamp a value between 0 and 255 inline uint8_t clamp(int value) { @@ -91,6 +92,33 @@ convertEventsToJS(const Napi::Env &env, return jsArray; } +// Helper function to convert nlohmann::json to Napi::Object +Napi::Object ConvertJsonToNapiObject(Napi::Env env, const json &j) { + Napi::Object obj = Napi::Object::New(env); + for (auto it = j.begin(); it != j.end(); ++it) { + if (it.value().is_string()) { + const std::string s = it.value(); + obj.Set(it.key(), Napi::String::New(env, s)); + } else if (it.value().is_number_integer()) { + obj.Set(it.key(), Napi::Number::New(env, it.value())); + } else if (it.value().is_number_float()) { + obj.Set(it.key(), Napi::Number::New(env, it.value())); + } else if (it.value().is_boolean()) { + obj.Set(it.key(), Napi::Boolean::New(env, it.value())); + } else if (it.value().is_object()) { + obj.Set(it.key(), ConvertJsonToNapiObject(env, it.value())); + } else if (it.value().is_array()) { + Napi::Array arr = Napi::Array::New(env, it.value().size()); + size_t index = 0; + for (auto &el : it.value()) { + arr.Set(index++, ConvertJsonToNapiObject(env, el)); + } + obj.Set(it.key(), arr); + } + } + return obj; +} + // Define a destructor to free uint8_t buffers void FinalizeBuffer(Napi::Env env, void *data) { // Clean up memory if necessary @@ -135,11 +163,13 @@ Napi::Object nativeVideoRecorder(const Napi::CallbackInfo &info) { } auto folder = props.Get("recordingFolder").As().Utf8Value(); auto prefix = props.Get("recordingPrefix").As().Utf8Value(); + auto networkCamera = + props.Get("networkCamera").As().Utf8Value(); auto interval = props.Get("recordingInterval").As().Uint32Value(); - recorder = std::shared_ptr( - new VideoController("", "ffmpeg", folder, prefix, interval)); - auto result = recorder->start(); + + auto result = + recorder->start(networkCamera, "ffmpeg", folder, prefix, interval); if (!result.empty()) { std::cerr << "Error: " << result << std::endl; ret.Set("status", Napi::String::New(env, "Fail")); @@ -153,7 +183,26 @@ Napi::Object nativeVideoRecorder(const Napi::CallbackInfo &info) { if (recorder) { auto err = recorder->stop(); std::cerr << "Recorder stoped with status: " << err << std::endl; - recorder = nullptr; + } + return ret; + } else if (op == "get-camera-list") { + + if (recorder) { + auto cameras = recorder->getCameraList(); + Napi::Array arr = Napi::Array::New(env, cameras.size()); + size_t index = 0; + for (auto &camera : cameras) { + auto item = Napi::Object::New(env); + item.Set("name", Napi::String::New(env, camera.name)); + item.Set("address", Napi::String::New(env, camera.address)); + arr.Set(index++, item); + } + + ret.Set("cameras", arr); + ret.Set("status", Napi::String::New(env, "OK")); + } else { + ret.Set("status", Napi::String::New(env, "Fail")); + ret.Set("error", Napi::String::New(env, "No recorder running")); } return ret; } else if (op == "recording-status") { @@ -184,8 +233,8 @@ Napi::Object nativeVideoRecorder(const Napi::CallbackInfo &info) { } return ret; } else if (op == "recording-log") { - // TODO - perhaps avoid the copy and make friend class to access the list - // for serialization + // TODO - perhaps avoid the copy and make friend class to access the + // list for serialization auto list = SystemEventQueue::getEventList(); ret.Set("list", convertEventsToJS(env, list)); return ret; @@ -232,32 +281,6 @@ Napi::Object nativeVideoRecorder(const Napi::CallbackInfo &info) { return ret; } -// Helper function to convert nlohmann::json to Napi::Object -Napi::Object ConvertJsonToNapiObject(Napi::Env env, const json &j) { - Napi::Object obj = Napi::Object::New(env); - for (auto it = j.begin(); it != j.end(); ++it) { - if (it.value().is_string()) { - const std::string s = it.value(); - obj.Set(it.key(), Napi::String::New(env, s)); - } else if (it.value().is_number_integer()) { - obj.Set(it.key(), Napi::Number::New(env, it.value())); - } else if (it.value().is_number_float()) { - obj.Set(it.key(), Napi::Number::New(env, it.value())); - } else if (it.value().is_boolean()) { - obj.Set(it.key(), Napi::Boolean::New(env, it.value())); - } else if (it.value().is_object()) { - obj.Set(it.key(), ConvertJsonToNapiObject(env, it.value())); - } else if (it.value().is_array()) { - Napi::Array arr = Napi::Array::New(env, it.value().size()); - size_t index = 0; - for (auto &el : it.value()) { - arr.Set(index++, ConvertJsonToNapiObject(env, el)); - } - obj.Set(it.key(), arr); - } - } - return obj; -} Napi::ThreadSafeFunction tsfn; // Function to send message to Electron main process diff --git a/native/recorder/src/VideoController.hpp b/native/recorder/src/VideoController.hpp index 4b12ea9..13e66d2 100644 --- a/native/recorder/src/VideoController.hpp +++ b/native/recorder/src/VideoController.hpp @@ -21,11 +21,11 @@ class VideoController { }; private: - const std::string srcName; - const std::string encoder; - const std::string dir; - const std::string prefix; - const int interval; + std::string srcName; + std::string encoder; + std::string dir; + std::string prefix; + int interval; std::thread monitorThread; std::chrono::steady_clock::time_point startTime; @@ -40,11 +40,18 @@ class VideoController { StatusInfo statusInfo; public: - VideoController(const std::string srcName, const std::string encoder, - const std::string dir, const std::string prefix, - const int interval) - : srcName(srcName), encoder(encoder), dir(dir), prefix(prefix), - interval(interval), monitorThread(&VideoController::monitorLoop, this) { + VideoController(const std::string camType) + : monitorThread(&VideoController::monitorLoop, this) { + +#ifdef HAVE_BASLER + if (camType == "basler") { // basler camera + videoReader = createBaslerReader(); + } else { + videoReader = createNdiReader(); + } +#else + videoReader = createNdiReader(); +#endif } ~VideoController() { monitorStopRequested = true; @@ -53,6 +60,15 @@ class VideoController { monitorThread.join(); } } + + std::vector getCameraList() { + std::lock_guard lock(controlMutex); + if (videoReader) { + return videoReader->getCameraList(); + } + return std::vector(); + } + StatusInfo getStatus() { std::lock_guard lock(controlMutex); std::chrono::steady_clock::time_point endTime = @@ -69,8 +85,15 @@ class VideoController { return statusInfo; } - std::string start() { + std::string start(const std::string srcName, const std::string encoder, + const std::string dir, const std::string prefix, + const int interval) { std::lock_guard lock(controlMutex); + this->srcName = srcName; + this->encoder = encoder; + this->dir = dir; + this->prefix = prefix; + this->interval = interval; if (videoRecorder) { return "Video Controller already running"; } @@ -110,21 +133,11 @@ class VideoController { frameProcessor = std::shared_ptr( new FrameProcessor(dir, prefix, videoRecorder, interval)); - // auto reader = createBaslerReader(); -#ifdef HAVE_BASLER - if (srcName == "basler") { // basler camera - videoReader = createBaslerReader(); - } else { - videoReader = createNdiReader(srcName); - } -#else - videoReader = createNdiReader(srcName); -#endif retval = videoReader->open(frameProcessor); if (!retval.empty()) { return retval; } - retval = videoReader->start(); + retval = videoReader->start(srcName); if (!retval.empty()) { return retval; } @@ -175,7 +188,7 @@ class VideoController { SystemEventQueue::push("VID", "Stopping video reader..."); videoReader->stop(); - videoReader = nullptr; + // Do not null the reader since we rely on it to query cameras SystemEventQueue::push("VID", "Stopping frame processor..."); frameProcessor->stop(); diff --git a/native/recorder/src/VideoReader.hpp b/native/recorder/src/VideoReader.hpp index ac3586b..7d569c2 100644 --- a/native/recorder/src/VideoReader.hpp +++ b/native/recorder/src/VideoReader.hpp @@ -6,11 +6,22 @@ class VideoReader { public: + typedef struct CameraInfo { + std::string name; + std::string address; + CameraInfo() {} + CameraInfo(std::string name, std::string address) + : name(name), address(address) {} + } CameraInfo; + virtual std::string open(std::shared_ptr frameProcessor) = 0; - virtual std::string start() = 0; + virtual std::string start(const std::string srcName) = 0; virtual std::string stop() = 0; + virtual std::vector getCameraList() { + return std::vector(); + }; virtual ~VideoReader() {} }; std::shared_ptr createBaslerReader(); -std::shared_ptr createNdiReader(const std::string srcName); \ No newline at end of file +std::shared_ptr createNdiReader(); \ No newline at end of file diff --git a/native/recorder/src/ctrecorder.cpp b/native/recorder/src/ctrecorder.cpp index 2b84f67..8f41ac6 100644 --- a/native/recorder/src/ctrecorder.cpp +++ b/native/recorder/src/ctrecorder.cpp @@ -176,9 +176,8 @@ int main(int argc, char *argv[]) { const auto daemon = args["-daemon"] == "true"; const auto encoder = args["-encoder"]; - auto recorder = std::shared_ptr( - new VideoController(srcName, encoder, directory, prefix, interval)); - recorder->start(); + auto recorder = std::shared_ptr(new VideoController("ndi")); + recorder->start(srcName, encoder, directory, prefix, interval); auto startShutdown = [recorder]() { recorder->stop(); }; stopHandler = startShutdown; diff --git a/package.json b/package.json index 07b20d1..ae53f9c 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,7 @@ "electron-store": "^8.2.0", "electron-updater": "^6.1.8", "github-markdown-css": "^5.5.1", + "path-browserify": "^1.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^9.0.1", diff --git a/src/main/main.ts b/src/main/main.ts index 1f518cd..c2fbc88 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -18,6 +18,7 @@ import './store/store'; import './msgbus/msgbus-main'; import { stopRecording, initRecorder } from './recorder/recorder-main'; import { setMainWindow } from './mainWindow'; +import './util/fileops-handler'; class AppUpdater { constructor() { diff --git a/src/main/preload.ts b/src/main/preload.ts index b78c733..74c1822 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -1,2 +1,3 @@ import './store/store-preload'; import './msgbus/msgbus-preload'; +import './util/util-preload'; diff --git a/src/main/util/fileops-handler.ts b/src/main/util/fileops-handler.ts new file mode 100644 index 0000000..2d04259 --- /dev/null +++ b/src/main/util/fileops-handler.ts @@ -0,0 +1,65 @@ +import { BrowserWindow, OpenDialogOptions, dialog, ipcMain } from 'electron'; +import { getMainWindow } from '../mainWindow'; + +const fs = require('fs'); + +ipcMain.handle('delete-file', async (_event, filename) => { + return new Promise((resolve) => { + fs.unlink(filename, (err: NodeJS.ErrnoException | null) => { + if (err) { + resolve({ error: err.message }); + } else { + resolve({ error: '' }); + } + }); + }); +}); + +ipcMain.handle('open-file-dialog', async (/* event */) => { + const result = await dialog.showOpenDialog(getMainWindow() as BrowserWindow, { + properties: ['openFile'], + }); + + if (result.canceled) { + return { cancelled: true, filePath: '' }; + } + if (result.filePaths.length > 0) { + return { cancelled: false, filePath: result.filePaths[0] }; + } + return { cancelled: true, filePath: '' }; +}); + +ipcMain.handle('open-dir-dialog', async (_event, title, defaultPath) => { + const options: OpenDialogOptions = { + title, + defaultPath, + properties: ['openDirectory'], + }; + const result = await dialog.showOpenDialog( + getMainWindow() as BrowserWindow, + options, + ); + + if (result.canceled) { + return { cancelled: true, path: defaultPath }; + } + if (result.filePaths.length > 0) { + return { cancelled: false, path: result.filePaths[0] }; + } + return { cancelled: true, path: defaultPath }; +}); + +ipcMain.handle('get-files-in-directory', (_event, dirPath) => { + return new Promise((resolve) => { + fs.readdir( + dirPath, + (err: NodeJS.ErrnoException | null, files: string[]) => { + if (err) { + resolve({ error: err.message, files: [] }); + } else { + resolve({ error: '', files }); + } + }, + ); + }); +}); diff --git a/src/main/util/util-handlers.ts b/src/main/util/util-handlers.ts new file mode 100644 index 0000000..dc1c4ee --- /dev/null +++ b/src/main/util/util-handlers.ts @@ -0,0 +1,8 @@ +import { getMainWindow } from '../mainWindow'; + +// eslint-disable-next-line import/prefer-default-export +export const userMessage = { + info: (msg: string) => { + getMainWindow()?.webContents.send('user-message', 'info', msg); + }, +}; diff --git a/src/main/util/util-preload.ts b/src/main/util/util-preload.ts new file mode 100644 index 0000000..9972cd4 --- /dev/null +++ b/src/main/util/util-preload.ts @@ -0,0 +1,87 @@ +/** + * Support for using LapStorage in renderer + * + * Add ```import './util/util-preload';``` to preload.ts to integrate with main + */ +import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'; + +export interface CloseFileReturn { + error: string; +} + +export interface OpenFileReturn { + cancelled: boolean; + filePath: string; +} + +export interface OpenDirReturn { + cancelled: boolean; + path: string; +} + +export interface DirListReturn { + error: string; + files: string[]; +} +// Function to open the file dialog and return the selected file path as a promise +export function openFileDialog(): Promise { + return new Promise((resolve) => { + ipcRenderer + .invoke('open-file-dialog') + .then((result) => resolve(result)) + .catch(() => resolve({ cancelled: true, filePath: '' })); + }); +} + +export function deleteFile(filename: string): Promise { + return new Promise((resolve) => { + ipcRenderer + .invoke('delete-file', filename) + .then((result) => resolve(result)) + .catch((err) => resolve({ error: String(err) })); + }); +} + +export function openDirDialog( + title: string, + defaultPath: string, +): Promise { + return new Promise((resolve) => { + ipcRenderer + .invoke('open-dir-dialog', title, defaultPath) + .then((result) => resolve(result)) + .catch(() => resolve({ cancelled: true, path: defaultPath })); + }); +} + +// Function to get the files in a directory and return them as a promise +export function getFilesInDirectory(dirPath: string): Promise { + // console.log('Executing getFiles in dir preload'); + return new Promise((resolve) => { + ipcRenderer + .invoke('get-files-in-directory', dirPath) + .then((result: DirListReturn) => { + // console.log('get files in dir result=' + JSON.stringify(result)); + return resolve(result); + }) + .catch((err) => ({ error: String(err), files: [] })); + }); +} + +contextBridge.exposeInMainWorld('Util', { + onUserMessage: ( + callback: (_event: IpcRendererEvent, level: string, msg: string) => void, + ) => ipcRenderer.on('user-message', callback), + getFilesInDirectory, + openFileDialog, + openDirDialog, + deleteFile, +}); + +const appVersion = require('../../../release/app/package.json').version; + +contextBridge.exposeInMainWorld('platform', { + platform: process.platform, + pathSeparator: process.platform.includes('win') ? '\\' : '/', + appVersion, +}); diff --git a/src/renderer/recorder/RecorderApi.ts b/src/renderer/recorder/RecorderApi.ts index 5fe30bb..10606a6 100644 --- a/src/renderer/recorder/RecorderApi.ts +++ b/src/renderer/recorder/RecorderApi.ts @@ -5,6 +5,7 @@ import { RecordingLog, GrabFrameResponse, RecordingStatus, + CameraListResponse, } from './RecorderTypes'; import { getRecordingProps, @@ -49,6 +50,15 @@ export const queryRecordingLog = () => { }); }; +export const queryCameraList = () => { + return window.msgbus.sendMessage( + 'recorder', + { + op: 'get-camera-list', + }, + ); +}; + export const requestVideoFrame = async () => { window.msgbus .sendMessage('recorder', { diff --git a/src/renderer/recorder/RecorderConfig.tsx b/src/renderer/recorder/RecorderConfig.tsx index b2cce68..52e3aff 100644 --- a/src/renderer/recorder/RecorderConfig.tsx +++ b/src/renderer/recorder/RecorderConfig.tsx @@ -1,16 +1,17 @@ -import React from 'react'; -import { Button, TextField, InputAdornment, Typography } from '@mui/material'; -import RecordIcon from '@mui/icons-material/FiberManualRecord'; -import { startRecording, stopRecording } from './RecorderApi'; +import React, { useEffect } from 'react'; +import { TextField, Typography, Grid, MenuItem } from '@mui/material'; +import { UseDatum } from 'react-usedatum'; +import { queryCameraList } from './RecorderApi'; import { useRecordingStatus, useIsRecording, - useRecordingStartTime, useRecordingProps, } from './RecorderData'; import { FullSizeWindow } from '../components/FullSizeWindow'; import RGBAImageCanvas from '../components/RGBAImageCanvas'; +const { openDirDialog } = window.Util; + const RecordingError = () => { const [recordingStatus] = useRecordingStatus(); return recordingStatus.error ? ( @@ -27,48 +28,65 @@ const RecordingError = () => { ) : null; }; + +const [useCameraList, setCameraList] = UseDatum< + { name: string; address: string }[] +>([]); const RecorderConfig: React.FC = () => { const [isRecording] = useIsRecording(); const [recordingProps, setRecordingProps] = useRecordingProps(); + const [cameraList] = useCameraList(); - const handleFolderChange = (event: React.ChangeEvent) => { - setRecordingProps({ - ...recordingProps, - recordingFolder: event.target.value, - }); - }; + useEffect(() => { + if (isRecording) { + return () => {}; + } + const timer = setInterval(() => { + queryCameraList() + .then((result) => { + setCameraList(result.cameras || []); + return null; + }) + .catch((err) => console.error(err)); + }, 5000); + return () => clearInterval(timer); + }, [isRecording]); - const handlePrefixChange = (event: React.ChangeEvent) => { - setRecordingProps({ - ...recordingProps, - recordingPrefix: event.target.value, - }); + const chooseDir = () => { + openDirDialog('Choose Video Directory', recordingProps.recordingFolder) + .then((result) => { + if (!result.cancelled) { + setRecordingProps({ + ...recordingProps, + recordingFolder: result.path, + }); + } + return null; + }) + .catch((e) => console.error(e)); }; - const handleIntervalChange = (event: React.ChangeEvent) => { + const handleChange = (event: React.ChangeEvent) => { + const value = + event.target.value === 'First Camera Discovered' + ? '' + : event.target.value; setRecordingProps({ ...recordingProps, - recordingDuration: parseInt(event.target.value, 10), + [event.target.name]: value, }); }; - const toggleRecording = () => { - (isRecording ? stopRecording() : startRecording()).catch((err) => - console.error(err), - ); - }; - - // const chooseDir = () => { - // openDirDialog('Choose Video Directory', videoDir) - // .then((result) => { - // if (!result.cancelled) { - // if (result.path !== videoDir) { - // setVideoDir(result.path); - // } - // } - // }) - // .catch(); - // }; + const cameraSelectItems = [ + { name: 'First Camera Discovered', address: '1st' }, + ...cameraList, + ]; + if (!cameraSelectItems.find((c) => c.name === recordingProps.networkCamera)) { + cameraSelectItems.push({ + name: recordingProps.networkCamera, + address: '', + }); + } return (
{ }} > - - - - ), - }} - /> - - + + + + {cameraSelectItems.map((camera) => ( + + {camera.name} + + ))} + + + + + + + + + + + + {/*