From 12d939a9066a1ded367e63bab76821f1bb575617 Mon Sep 17 00:00:00 2001 From: Glenn Engel Date: Mon, 13 Jan 2025 20:06:42 -0800 Subject: [PATCH] Reformat source. Add backlog indicator. --- .vscode/settings.json | 4 +- native/recorder/.gitignore | 1 + native/recorder/CMakeLists.txt | 18 +- native/recorder/cmake/FindNDI.cmake | 27 ++ native/recorder/package.json | 9 +- native/recorder/src/FrameProcessor.cpp | 102 ++++-- native/recorder/src/FrameProcessor.hpp | 25 +- native/recorder/src/NdiReader.cpp | 359 ++++------------------ native/recorder/src/RecorderAPI.cpp | 177 +++++++---- native/recorder/src/VideoController.hpp | 124 +++++--- native/recorder/src/VideoReader.hpp | 17 +- native/recorder/src/VideoUtils.cpp | 31 +- native/recorder/src/ctrecorder.cpp | 92 ++++-- native/recorder/yarn.lock | 149 ++++----- release/app/package.json | 2 +- release/app/yarn.lock | 6 +- src/renderer/recorder/RecorderTypes.ts | 1 + src/renderer/recorder/RecordingStatus.tsx | 17 +- yarn.lock | 4 +- 19 files changed, 561 insertions(+), 604 deletions(-) create mode 100644 native/recorder/cmake/FindNDI.cmake diff --git a/.vscode/settings.json b/.vscode/settings.json index e5c4b27..9003600 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -82,7 +82,9 @@ "__errc": "cpp", "cstdarg": "cpp", "semaphore": "cpp", - "regex": "cpp" + "regex": "cpp", + "__availability": "cpp", + "valarray": "cpp" }, "eslint.validate": [ diff --git a/native/recorder/.gitignore b/native/recorder/.gitignore index c97f95c..03de223 100644 --- a/native/recorder/.gitignore +++ b/native/recorder/.gitignore @@ -1,3 +1,4 @@ prebuilds/ *node_modules/ lib-build +build/ diff --git a/native/recorder/CMakeLists.txt b/native/recorder/CMakeLists.txt index 8ce938f..a88aecd 100644 --- a/native/recorder/CMakeLists.txt +++ b/native/recorder/CMakeLists.txt @@ -1,6 +1,9 @@ -cmake_minimum_required(VERSION 3.5) # Set minimum CMake version +cmake_minimum_required(VERSION 3.15) # Set minimum CMake version project(ctrecorder VERSION 1.0 LANGUAGES CXX) +# Add the path to the directory containing CMake modules +set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") + # Set a default build type if none was specified if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) message(STATUS "Setting build type to 'Release' as none was specified. Use cmake -DCMAKE_BUILD_TYPE=Debug for Debug.") @@ -35,10 +38,17 @@ set(SOURCE_FILES # prefer static libs if(APPLE) - set(CMAKE_FIND_LIBRARY_SUFFIXES ".a" ".dylib") + set(CMAKE_FIND_LIBRARY_SUFFIXES ".dylib" ".a") set(EXTRA_LIBS z bz2 lzma pthread) - set(FFMPEG_FOLDER "/Users/glenne/ffmpeg-built-mac") + # set(FFMPEG_FOLDER "./lib-build/ffmpeg-static-mac-arm64") + set(FFMPEG_FOLDER "/opt/homebrew") # set(USE_OPENCV OFF CACHE BOOL "Use OPENCV features" FORCE) + find_package(NDI REQUIRED) + if(NDI_FOUND) + message(STATUS "NDI found: Library=${NDI_LIBRARIES}, Include=${NDI_INCLUDE_DIR}") + else() + message(FATAL_ERROR "NDI not found!") + endif() find_library(NDI_LIBRARIES ndi REQUIRED) elseif(WIN32) message(STATUS "Win32 section") @@ -98,6 +108,7 @@ if (USE_OPENCV) endif() if (USE_FFMPEG) + message(STATUS "Adding ffmpeg libraries") add_compile_definitions(USE_FFMPEG) list(APPEND SOURCE_FILES src/FFRecorder.cpp @@ -116,6 +127,7 @@ if (USE_FFMPEG) find_library(AVUTIL_LIBRARY ${LIB_PREFIX}avutil REQUIRED) find_library(SWSCALE_LIBRARY ${LIB_PREFIX}swscale REQUIRED) find_library(SWSRESAMPLE_LIBRARY ${LIB_PREFIX}swresample REQUIRED) + set(FFMPEG_LIBS ${AVFORMAT_LIBRARY} ${AVCODEC_LIBRARY} ${AVUTIL_LIBRARY} ${SWSCALE_LIBRARY} ${SWSRESAMPLE_LIBRARY}) message(STATUS "avlibs found at ${FFMPEG_LIBS}") diff --git a/native/recorder/cmake/FindNDI.cmake b/native/recorder/cmake/FindNDI.cmake new file mode 100644 index 0000000..205acc5 --- /dev/null +++ b/native/recorder/cmake/FindNDI.cmake @@ -0,0 +1,27 @@ +# Custom FindNDI.cmake file to locate NDI SDK on macOS +set(NDI_SDK_PATH "/Library/NDI SDK for Apple") + +# Locate the NDI library +find_library(NDI_LIBRARIES + NAMES ndi + PATHS ${NDI_SDK_PATH}/lib/macOS + REQUIRED +) + +# Locate the NDI headers +find_path(NDI_INCLUDE_DIR + NAMES Processing.NDI.Lib.h + PATHS ${NDI_SDK_PATH}/include + REQUIRED +) + +# Export the found paths for external use +if(NDI_LIBRARIES AND NDI_INCLUDE_DIR) + set(NDI_FOUND TRUE) + set(NDI_LIBRARIES ${NDI_LIBRARIES}) + set(NDI_INCLUDE_DIR ${NDI_INCLUDE_DIR}) +else() + set(NDI_FOUND FALSE) +endif() + +mark_as_advanced(NDI_LIBRARIES NDI_INCLUDE_DIR) diff --git a/native/recorder/package.json b/native/recorder/package.json index 7324090..7de5459 100644 --- a/native/recorder/package.json +++ b/native/recorder/package.json @@ -1,6 +1,6 @@ { "name": "crewtimer_video_recorder", - "version": "1.0.6-module", + "version": "1.0.7-module", "description": "A node electron utility to write mp4 files for CrewTimer Video Review", "main": "index.js", "types": "index.d.ts", @@ -14,14 +14,13 @@ "upload": "prebuild --tag module --verbose --runtime napi", "build": "node-gyp rebuild", "testrebuild": "echo 'Building recorder' && mkdir -p ./release/Build &&cp ../../../../native/recorder/prebuilds/build/Release/crewtimer_video_recorder.node ./release/Build", - "build:mac-x64": "prebuild --prerelease --napi --napi-version 6 --runtime napi --arch x64 --platform darwin", "build:mac-arm": "prebuild --prerelease --napi --napi-version 6 --runtime napi --arch arm64 --platform darwin", - "build:mac" : "yarn build:mac-x64 && yarn build:mac-arm", + "build:mac": "yarn build:mac-x64 && yarn build:mac-arm", "build:win": "prebuild --napi --napi-version 6 --runtime napi --arch=x64 --platform win32", "build:ffmpeg": "bash ./scripts/build-ffmpeg.sh", - - "clean": "rm -rf node_modules yarn.lock prebuilds build" + "clean": "rm -rf node_modules yarn.lock prebuilds build", + "cleanbuild": "yarn clean && yarn install && yarn prebuild" }, "keywords": [], "author": "Glenn Engel (glenne)", diff --git a/native/recorder/src/FrameProcessor.cpp b/native/recorder/src/FrameProcessor.cpp index afe2901..df57188 100644 --- a/native/recorder/src/FrameProcessor.cpp +++ b/native/recorder/src/FrameProcessor.cpp @@ -9,7 +9,8 @@ #include "VideoUtils.hpp" using namespace std::chrono; -static int16_t getTimezoneOffset() { +static int16_t getTimezoneOffset() +{ // Get the current time in the system's local timezone auto now = std::chrono::system_clock::now(); std::time_t now_c = std::chrono::system_clock::to_time_t(now); @@ -35,21 +36,31 @@ FrameProcessor::FrameProcessor(const std::string directory, : directory(directory), prefix(prefix), cropArea(cropArea), pxCropArea(Rectangle(0, 0, 0, 0)), guide(guide), videoRecorder(videoRecorder), durationSecs(durationSecs), running(true), - processThread(&FrameProcessor::processFrames, this) { + processThread(&FrameProcessor::processFrames, this) +{ errorMessage = ""; + statusInfo.recording = false; + statusInfo.error = ""; + statusInfo.filename = ""; + statusInfo.width = 0; + statusInfo.height = 0; + statusInfo.fps = 0; + statusInfo.frameBacklog = 0; tzOffset = getTimezoneOffset(); } FrameProcessor::~FrameProcessor() { stop(); } -void FrameProcessor::stop() { +void FrameProcessor::stop() +{ errorMessage = ""; running = false; frameAvailable.notify_all(); if (processThread.joinable()) processThread.join(); - while (!frameQueue.empty()) { + while (!frameQueue.empty()) + { frameQueue.pop(); } @@ -63,15 +74,18 @@ void FrameProcessor::stop() { */ void FrameProcessor::splitFile() { splitRequested = true; } -FrameProcessor::StatusInfo FrameProcessor::getStatus() { +FrameProcessor::StatusInfo FrameProcessor::getStatus() +{ statusInfo.recording = running; statusInfo.error = errorMessage; return statusInfo; } -void FrameProcessor::addFrame(FramePtr video_frame) { +void FrameProcessor::addFrame(FramePtr video_frame) +{ std::unique_lock lock(queueMutex); - if (!running) { + if (!running) + { return; } lastFrame = video_frame; @@ -79,18 +93,22 @@ void FrameProcessor::addFrame(FramePtr video_frame) { frameAvailable.notify_one(); } -FramePtr FrameProcessor::getLastFrame() { +FramePtr FrameProcessor::getLastFrame() +{ std::unique_lock lock(queueMutex); return lastFrame; } -void FrameProcessor::writeJsonSidecarFile() { - if (frameCount == 0) { +void FrameProcessor::writeJsonSidecarFile() +{ + if (frameCount == 0) + { return; } std::ofstream jsonFile(jsonFilename); - if (!jsonFile) { + if (!jsonFile) + { errorMessage = "Error: Could not open the file '" + jsonFilename + "' for writing."; running = false; @@ -118,7 +136,8 @@ void FrameProcessor::writeJsonSidecarFile() { jsonFile.close(); } -void FrameProcessor::processFrames() { +void FrameProcessor::processFrames() +{ SystemEventQueue::push("fproc", "Starting frame processor"); int count = 0; auto start = high_resolution_clock::now(); @@ -128,17 +147,23 @@ void FrameProcessor::processFrames() { lastFPS = 0; frameCount = 0; - while (running) { + while (running) + { std::unique_lock lock(queueMutex); frameAvailable.wait(lock, - [this] { return !frameQueue.empty() || !running; }); - while (running && !frameQueue.empty()) { + [this] + { return !frameQueue.empty() || !running; }); + while (running && !frameQueue.empty()) + { auto video_frame = frameQueue.front(); frameQueue.pop(); + + statusInfo.frameBacklog = frameQueue.size(); lock.unlock(); - if (video_frame->xres == 0 || video_frame->yres == 0) { - std::cerr << "Null frame received" << std::endl; - continue; + if (video_frame->xres == 0 || video_frame->yres == 0) + { + std::cerr << "Null frame received" << std::endl; + continue; } const auto fps = (float)video_frame->frame_rate_N / (float)video_frame->frame_rate_D; @@ -156,9 +181,11 @@ void FrameProcessor::processFrames() { if (propChange || count == 0 || (useEmbeddedTimestamp && video_frame->timestamp >= nextStartTime) || - (okToSplit && splitRequested)) { + (okToSplit && splitRequested)) + { count++; - if (frameCount > 0) { + if (frameCount > 0) + { writeJsonSidecarFile(); videoRecorder->stop(); } @@ -194,12 +221,12 @@ void FrameProcessor::processFrames() { statusInfo.height = video_frame->yres; pxCropArea.x = std::round((cropArea.x * (video_frame->xres) / 4)) * 4; - int cropWidth = cropArea.width*video_frame->xres; + int cropWidth = cropArea.width * video_frame->xres; cropWidth = std::min(video_frame->xres - pxCropArea.x, cropWidth); pxCropArea.width = int(cropWidth / 4) * 4; // trunc to mult of 4 pxCropArea.y = std::round((cropArea.y * (video_frame->yres) / 4)) * 4; - int cropHeight = cropArea.height*video_frame->yres; + int cropHeight = cropArea.height * video_frame->yres; cropHeight = std::min(video_frame->yres - pxCropArea.y, cropHeight); pxCropArea.height = int(cropHeight / 4) * 4; // trunc to mult of 4 @@ -214,7 +241,8 @@ void FrameProcessor::processFrames() { directory, filename, pxCropArea.width ? pxCropArea.width : video_frame->xres, pxCropArea.height ? pxCropArea.height : video_frame->yres, fps); - if (!err.empty()) { + if (!err.empty()) + { errorMessage = err; running = false; frameCount = 0; @@ -232,27 +260,31 @@ void FrameProcessor::processFrames() { // std::cerr << "vstride: " << std::dec << video_frame->stride // << "ptr: " << std::hex << (void *)(video_frame->data) << std::dec // << std::endl; - if (pxCropArea.width && pxCropArea.height) { + if (pxCropArea.width && pxCropArea.height) + { // std::cout << "Cropping frame (" << pxCropArea.x << "," << pxCropArea.y // << ")" << pxCropArea.width << "x" << pxCropArea.height // << std::endl; cropped = cropFrame(video_frame, pxCropArea.x, pxCropArea.y, pxCropArea.width, pxCropArea.height); - if (!cropped) { - std::cout << "Crop (" << pxCropArea.x << "," << pxCropArea.y - << ")" << pxCropArea.width << "x" << pxCropArea.height << " from frame=" << video_frame->xres << "x" << video_frame->yres - << std::endl; - std::cerr << "Frame crop failed." << std::endl; - cropped = video_frame; // continue with full frame + if (!cropped) + { + std::cout << "Crop (" << pxCropArea.x << "," << pxCropArea.y + << ")" << pxCropArea.width << "x" << pxCropArea.height << " from frame=" << video_frame->xres << "x" << video_frame->yres + << std::endl; + std::cerr << "Frame crop failed." << std::endl; + cropped = video_frame; // continue with full frame } // std::cerr << " stride: " << std::dec << cropped->stride // << "ptr: " << std::hex << (void *)(cropped->data) << std::dec // << std::endl; - encodeTimestamp(cropped->data, cropped->stride, video_frame->timestamp); } + + encodeTimestamp(cropped->data, cropped->stride, video_frame->timestamp); auto err = videoRecorder->writeVideoFrame(cropped); - if (!err.empty()) { + if (!err.empty()) + { errorMessage = err; running = false; frameCount = 0; @@ -262,7 +294,8 @@ void FrameProcessor::processFrames() { frameCount++; lock.lock(); - if (frameQueue.size() > 500) { + if (frameQueue.size() > 500) + { SystemEventQueue::instance().push( "fproc", "Frame queue overflow, discarding frames"); frameQueue = std::queue(); @@ -270,7 +303,8 @@ void FrameProcessor::processFrames() { } } - if (frameCount > 0) { + if (frameCount > 0) + { writeJsonSidecarFile(); videoRecorder->stop(); } diff --git a/native/recorder/src/FrameProcessor.hpp b/native/recorder/src/FrameProcessor.hpp index 271ed0e..7857162 100644 --- a/native/recorder/src/FrameProcessor.hpp +++ b/native/recorder/src/FrameProcessor.hpp @@ -15,19 +15,23 @@ /** * Class responsible for processing video frames. */ -class FrameProcessor { +class FrameProcessor +{ public: - struct StatusInfo { + struct StatusInfo + { bool recording; std::string error; std::string filename; std::uint32_t width; std::uint32_t height; float fps; + std::uint32_t frameBacklog; }; - struct Rectangle { + struct Rectangle + { int x; int y; int width; @@ -36,7 +40,8 @@ class FrameProcessor { : x(x), y(y), width(width), height(height) {} }; - struct FRectangle { + struct FRectangle + { float x; float y; float width; @@ -45,7 +50,8 @@ class FrameProcessor { : x(x), y(y), width(width), height(height) {} }; - struct Guide { + struct Guide + { float pt1; float pt2; Guide() : pt1(0), pt2(0) {} @@ -99,13 +105,12 @@ class FrameProcessor { uint64_t durationSecs; uint64_t nextStartTime = 0; std::atomic splitRequested; - std::queue frameQueue; ///< Queue to hold the frames. - std::mutex queueMutex; ///< Mutex for synchronizing access to the frame queue. + std::queue frameQueue; ///< Queue to hold the frames. + std::mutex queueMutex; ///< Mutex for synchronizing access to the frame queue. std::condition_variable frameAvailable; ///< Condition variable to notify ///< when frames are available. - std::atomic running = - false; ///< Atomic flag to control the running state of threads. - std::thread processThread; ///< Threads for capturing and processing frames. + std::atomic running; ///< Atomic flag to control the running state of threads. + std::thread processThread; ///< Threads for capturing and processing frames. FramePtr lastFrame; // JSON file props diff --git a/native/recorder/src/NdiReader.cpp b/native/recorder/src/NdiReader.cpp index c8c48b0..c97d7aa 100644 --- a/native/recorder/src/NdiReader.cpp +++ b/native/recorder/src/NdiReader.cpp @@ -23,274 +23,48 @@ using namespace std::chrono; -// 64% cpu rx uyuv422, convert to bgr, save to disk -// 68% cpu bgrx to bgr save to disk -// 208% cpu bgrx to bgr save as mkv X264 - -/** - * Crops a region from a UYVY422 frame buffer. - * - * @param uyvyBuffer The pointer to the original UYVY422 frame buffer. - * @param frameWidth The width of the original frame in pixels. - * @param frameHeight The height of the original frame in pixels. - * @param x The x-coordinate of the top-left corner of the crop - * region. - * @param y The y-coordinate of the top-left corner of the crop - * region. - * @param width The width of the crop region in pixels. - * @param height The height of the crop region in pixels. - * @param lineStride The number of bytes in each row of the original frame - * buffer. - * @return A vector containing the cropped frame buffer in UYVY422 - * format. If the crop region is invalid, returns an empty vector. - */ -std::vector cropUYVY422Frame(const uint8_t *uyvyBuffer, int frameWidth, - int frameHeight, int x, int y, int width, - int height, int lineStride) +class NdiReader : public VideoReader { - int bytesPerPixel = - 2; // Each pixel in UYVY422 is 2 bytes (4 bytes for 2 pixels) - - // Adjust the width and height to ensure they are within frame bounds - int maxWidth = frameWidth - x; - int maxHeight = frameHeight - y; - width = std::max(0, std::min(width, maxWidth)); - height = std::max(0, std::min(height, maxHeight)); - - // Check if the requested region is valid - if (width <= 0 || height <= 0 || x < 0 || y < 0 || x >= frameWidth || - y >= frameHeight) - { - std::cerr << "Invalid crop region." << std::endl; - return {}; - } - - // Create a buffer for the cropped frame - std::vector croppedBuffer(width * height * bytesPerPixel); - - // Iterate over the rows in the cropping region - for (int row = 0; row < height; ++row) + /** A wrapper for pNDI_recv to control the lifetime. + * It needs to stay alive until all frames associated with it have been destroyed. + */ + class NdiRecv { - // Calculate the source position - int srcY = y + row; - int srcX = x * bytesPerPixel; - - // Calculate the source offset in the original buffer with the lineStride - const uint8_t *srcPtr = uyvyBuffer + srcY * lineStride + srcX; - - // Calculate the destination position in the cropped buffer - uint8_t *destPtr = croppedBuffer.data() + row * width * bytesPerPixel; - - // Copy the row into the cropped buffer - std::memcpy(destPtr, srcPtr, width * bytesPerPixel); - } - - return croppedBuffer; -} - -uint32_t uyvy422(uint8_t r, uint8_t g, uint8_t b) -{ - // https://github.com/lplassman/V4L2-to-NDI/blob/4dd5e9594acc4f154658283ee52718fa58018ac9/PixelFormatConverter.cpp - auto Y0 = (0.299f * r + 0.587f * g + 0.114f * b); - auto Y1 = (0.299f * r + 0.587f * g + 0.114f * b); - auto U = std::max(0.0f, std::min(255.0f, 0.492f * (b - Y0) + 128.0f)); - auto V = std::max(0.0f, std::min(255.0f, 0.877f * (r - Y0) + 128.0f)); - uint32_t result = - uint8_t(Y1) << 24 | uint8_t(V) << 16 | uint8_t(Y0) << 8 | uint8_t(U); - // printf("Y0=%d, Y1=%d, U=%d, V=%d, %f\n", uint8_t(Y0), uint8_t(Y1), - // uint8_t(U), uint8_t(V), - // 0.492f * (b - Y0) + 128.0f); - - // printf("(%d,%d,%d)=0x%08x\n", r, g, b, result); - return result; -} - -auto timeColor = uyvy422(0, 255, 0); -const auto black = uyvy422(0, 0, 0); -const auto red = uyvy422(255, 0, 0); -const auto green = uyvy422(0, 255, 0); -const auto white = uyvy422(255, 255, 255); -auto scale = 3; - -// Define pixel representations for each digit (0-9) -std::vector>> digits = { - // Digit 0 - {{1, 1, 1}, {1, 0, 1}, {1, 0, 1}, {1, 0, 1}, {1, 1, 1}}, - // Digit 1 - {{0, 0, 1}, {0, 0, 1}, {0, 0, 1}, {0, 0, 1}, {0, 0, 1}}, - // Digit 2 - {{1, 1, 1}, {0, 0, 1}, {1, 1, 1}, {1, 0, 0}, {1, 1, 1}}, - // Digit 3 - {{1, 1, 1}, {0, 0, 1}, {0, 1, 1}, {0, 0, 1}, {1, 1, 1}}, - // Digit 4 - {{1, 0, 1}, {1, 0, 1}, {1, 1, 1}, {0, 0, 1}, {0, 0, 1}}, - // Digit 5 - {{1, 1, 1}, {1, 0, 0}, {1, 1, 1}, {0, 0, 1}, {1, 1, 1}}, - // Digit 6 - {{1, 1, 1}, {1, 0, 0}, {1, 1, 1}, {1, 0, 1}, {1, 1, 1}}, - // Digit 7 - {{1, 1, 1}, {0, 0, 1}, {0, 0, 1}, {0, 0, 1}, {0, 0, 1}}, - // Digit 8 - {{1, 1, 1}, {1, 0, 1}, {1, 1, 1}, {1, 0, 1}, {1, 1, 1}}, - // Digit 9 - {{1, 1, 1}, {1, 0, 1}, {1, 1, 1}, {0, 0, 1}, {0, 0, 1}}, - // Colon - {{0}, {1}, {0}, {1}, {0}}, - // Period - {{0}, {0}, {0}, {0}, {1}}}; - -class Point -{ -public: - int x; - int y; + public: + NDIlib_recv_instance_t pNDI_recv; + NdiRecv(NDIlib_recv_instance_t pNDI_recv) : pNDI_recv(pNDI_recv) {}; + ~NdiRecv() + { + NDIlib_recv_connect(pNDI_recv, nullptr); + NDIlib_recv_destroy(pNDI_recv); + } + }; - // Constructor - Point(int xCoordinate, int yCoordinate) + class NdiFrame : public Frame { - x = xCoordinate; - y = yCoordinate; - } -}; + std::shared_ptr ndiRecv; + NDIlib_video_frame_v2_t ndiFrame; -void setDigitPixels(uint32_t *screen, int digit, Point &start, int stride, - uint32_t fg, uint32_t bg) -{ - std::vector> &digitPixels = - digits[digit]; // Get the pixel representation for the digit + public: + NdiFrame(std::shared_ptr ndiRecv, NDIlib_video_frame_v2_t ndiFrame) + : ndiRecv(ndiRecv), ndiFrame(ndiFrame) { - const size_t border = 4; - for (size_t y = 0; y < border; y++) - { - for (size_t x = 0; x < size_t(digitPixels[0].size() * scale + border * 2); - ++x) - { - screen[(start.x + x) + (start.y + y) * stride / 4] = bg; - screen[(start.x + x) + - (start.y + y + digitPixels.size() * scale + border) * stride / 4] = - bg; - } - } - for (size_t y = 0; y < size_t(digitPixels.size() * scale + 2 * border); ++y) - { - for (size_t x = 0; x < border; x++) - { - screen[(start.x + x) + (start.y + y) * stride / 4] = bg; - screen[(start.x + x + border + digitPixels[0].size() * scale) + - (start.y + y) * stride / 4] = bg; - } - } - const auto yOffset = border; - const auto xOffset = border; - for (size_t y = 0; y < digitPixels.size(); ++y) - { - for (int yExpand = 0; yExpand < scale; yExpand++) + }; + virtual ~NdiFrame() override { - for (size_t x = 0; x < digitPixels[y].size(); ++x) + if (ndiRecv) { - const auto pixel = digitPixels[y][x] ? fg : bg; - for (int xExpand = 0; xExpand < scale; xExpand++) - { - screen[(xOffset + start.x + x * scale + xExpand) + - ((yOffset + start.y) + y * scale + yExpand) * stride / 4] = - pixel; - } + NDIlib_recv_free_video_v2(ndiRecv->pNDI_recv, &ndiFrame); + ndiRecv = nullptr; } } - } - start.x += int(digitPixels[0].size() * scale + border * 2 - 2); -} - -void setArea(uint32_t *screen, int stride, int startX, int startY, int width, - int height, uint32_t color) -{ - for (int x = startX / 2; x < startX / 2 + width / 2; x++) - { - for (int y = startY; y < startY + height; y++) - { - screen[x + y * stride / 4] = color; - } - } -} - -void overlayDigits(uint32_t *screen, int stride, Point &point, uint16_t value, - int digits) -{ - // clearArea(screen, stride, point.x - scale * 2, point.y - scale * 4, - // digits * (3 * scale + 2 * scale) + scale*4, - // 5 * scale + scale * 6); - - if (digits >= 3) - { - const auto hundreds = (value / 100) % 10; - setDigitPixels(screen, hundreds, point, stride, timeColor, black); - } - if (digits >= 2) - { - const auto tens = (value / 10) % 10; - setDigitPixels(screen, tens, point, stride, timeColor, black); - } - - const auto ones = value % 10; - setDigitPixels(screen, ones, point, stride, timeColor, black); -} - -void overlayTime(uint32_t *screen, int stride, uint64_t ts100ns, - const std::tm *local_time) -{ - // Extract the local hours and minutes - // int local_hours = local_time->tm_hour; - // int local_minutes = local_time->tm_min; - // int local_secs = local_time->tm_sec; - // const auto milli = (5000 + ts100ns) / 10000; - - // Point point = Point(20, 40); - - // overlayDigits(screen, stride, point, local_hours, 2); - // setDigitPixels(screen, 10, point, stride, timeColor, black); - // overlayDigits(screen, stride, point, local_minutes, 2); - // setDigitPixels(screen, 10, point, stride, timeColor, black); - // overlayDigits(screen, stride, point, local_secs, 2); - // setDigitPixels(screen, 11, point, stride, timeColor, black); - // overlayDigits(screen, stride, point, milli % 1000, 3); - - // std::cout << "Current time: " << local_hours << ":" << local_minutes << ":" - // << local_secs << std::endl; - // std::cout << "Calced time: " << (milli / (60 * 60 * 1000)) % 24 << ":" - // << (milli / (60 * 1000)) % 60 << ":" << (milli / (1000)) % 60 - // << " m=" << milli << std::endl; - - setArea(screen, stride, 0, 0, 128, 3, black); - uint64_t mask = 0x8000000000000000L; - for (int bit = 0; bit < 64; bit++) - { - const bool val = (ts100ns & mask) != 0; - mask >>= 1; - setArea(screen, stride, bit * 2, 1, 2, 1, val ? white : black); - } -} - -#ifndef _WIN32 -void setThreadPriority(std::thread &thread, int policy, int priority) -{ - pthread_t threadID = thread.native_handle(); - sched_param sch_params; - sch_params.sched_priority = priority; - if (pthread_setschedparam(threadID, policy, &sch_params)) - { - std::cerr << "Failed to set Thread scheduling : " << std::strerror(errno) - << std::endl; - } -} -#endif + }; -class NdiReader : public VideoReader -{ + std::shared_ptr ndiRecv; std::thread ndiThread; std::atomic keepRunning; std::atomic scanEnabled; - std::shared_ptr frameProcessor; - NDIlib_recv_instance_t pNDI_recv = nullptr; + AddFrameFunction addFrameFunction; NDIlib_find_instance_t pNDI_find = nullptr; std::string srcName; @@ -298,25 +72,6 @@ class NdiReader : public VideoReader std::thread scanThread; std::mutex scanMutex; - class NdiFrame : public Frame - { - NDIlib_recv_instance_t pNDI_recv; - NDIlib_video_frame_v2_t ndiFrame; - - public: - NdiFrame(NDIlib_recv_instance_t pNDI_recv, NDIlib_video_frame_v2_t ndiFrame) - : pNDI_recv(pNDI_recv), ndiFrame(ndiFrame) { - - }; - virtual ~NdiFrame() override - { - if (pNDI_recv) - { - NDIlib_recv_free_video_v2(pNDI_recv, &ndiFrame); - } - } - }; - std::vector getCameraList() override { std::unique_lock lock(scanMutex); @@ -352,8 +107,7 @@ class NdiReader : public VideoReader 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); + // p_sources[src].p_ndi_name + " at " + p_sources[src].p_ip_address); } return list; @@ -392,7 +146,7 @@ class NdiReader : public VideoReader cameras = camList; for (auto camera : cameras) { - // std::cout << "Found camera: " << camera.name << std::endl; + std::cout << "Found camera: " << camera.name << " looking for " << srcName << std::endl; if (camera.name.find(srcName) == 0) { foundCamera = camera; @@ -406,16 +160,19 @@ class NdiReader : public VideoReader return ""; // stop received before ndi source found } - if (!pNDI_recv) + std::cout << "Camera found" << std::endl; + if (!ndiRecv) { + std::cout << "Connecting..." << std::endl; // Only create this once as calling destroy on it seems to segfault NDIlib_recv_create_v3_t recv_create; recv_create.color_format = NDIlib_recv_color_format_UYVY_BGRA; // We now have at least one source, so we create a receiver to look at it. - pNDI_recv = NDIlib_recv_create_v3(&recv_create); + auto pNDI_recv = NDIlib_recv_create_v3(&recv_create); if (!pNDI_recv) return "NDIlib_recv_create_v3() failed"; + ndiRecv = std::make_shared(pNDI_recv); } // Connect to the source SystemEventQueue::push("NDI", std::string("Connecting to ") + @@ -424,7 +181,7 @@ class NdiReader : public VideoReader // Connect to our sources 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); + NDIlib_recv_connect(ndiRecv->pNDI_recv, &p_source); foundCamera.name = ""; cameras.clear(); @@ -442,7 +199,7 @@ class NdiReader : public VideoReader NDIlib_video_frame_v2_t video_frame; NDIlib_audio_frame_v3_t audio_frame; - auto frameType = NDIlib_recv_capture_v3(pNDI_recv, &video_frame, nullptr, + auto frameType = NDIlib_recv_capture_v3(ndiRecv->pNDI_recv, &video_frame, nullptr, nullptr, 5000); switch (frameType) { @@ -459,6 +216,7 @@ class NdiReader : public VideoReader frameCount++; if (frameCount == 1) { + SystemEventQueue::push("NDI", std::string("Stream active")); break; // 1st frame often old frame cached from ndi sender. // Ignore. } @@ -467,7 +225,7 @@ class NdiReader : public VideoReader std::cerr << "timestamp not supported" << std::endl; } // std::cout << "Video data received (" << video_frame.xres << "x" - // << video_frame.yres << std::endl; + // << video_frame.yres << ")" << std::endl; auto ts100ns = video_frame.timestamp; const auto milli = (5000 + ts100ns) / 10000; @@ -481,9 +239,6 @@ class NdiReader : public VideoReader // Convert to local time std::tm *local_time = std::localtime(&raw_time); - overlayTime((uint32_t *)video_frame.p_data, - video_frame.line_stride_in_bytes, video_frame.timestamp, - local_time); auto delta = video_frame.timestamp - lastTS; auto msPerFrame = @@ -498,7 +253,7 @@ class NdiReader : public VideoReader } lastTS = video_frame.timestamp; - auto txframe = std::make_shared(pNDI_recv, video_frame); + auto txframe = std::make_shared(ndiRecv, video_frame); txframe->xres = video_frame.xres & ~1; // force even txframe->yres = video_frame.yres & ~1; txframe->stride = video_frame.line_stride_in_bytes; @@ -507,11 +262,14 @@ class NdiReader : public VideoReader txframe->frame_rate_N = video_frame.frame_rate_N; txframe->frame_rate_D = video_frame.frame_rate_D; txframe->pixelFormat = Frame::PixelFormat::UYVY422; - frameProcessor->addFrame(txframe); + if (addFrameFunction) + { + addFrameFunction(txframe); + } } else { - NDIlib_recv_free_video_v2(pNDI_recv, &video_frame); + NDIlib_recv_free_video_v2(ndiRecv->pNDI_recv, &video_frame); } } break; @@ -525,7 +283,7 @@ class NdiReader : public VideoReader // audio_frame.no_samples, // audio_frame.sample_rate, 400); - NDIlib_recv_free_audio_v3(pNDI_recv, &audio_frame); + NDIlib_recv_free_audio_v3(ndiRecv->pNDI_recv, &audio_frame); } break; default: @@ -543,15 +301,16 @@ class NdiReader : public VideoReader std::cout << "NDI SDK Version: " << version << std::endl; } std::string start(const std::string srcName, - std::shared_ptr frameProcessor) override + AddFrameFunction addFrameFunction) override { + if (ndiThread.joinable()) + { + stop(); + } this->srcName = srcName; - this->frameProcessor = frameProcessor; + this->addFrameFunction = addFrameFunction; keepRunning = true; ndiThread = std::thread(&NdiReader::run, this); -#ifndef _WIN32 - setThreadPriority(ndiThread, SCHED_FIFO, 10); -#endif return ""; }; std::string stop() override @@ -561,20 +320,12 @@ class NdiReader : public VideoReader { ndiThread.join(); } - if (frameProcessor) + if (addFrameFunction) { - frameProcessor = nullptr; + addFrameFunction = nullptr; } - // Destroy the receiver - if (pNDI_recv) - { - // disconnect - NDIlib_recv_connect(pNDI_recv, nullptr); - // The following causes an exception - // NDIlib_recv_destroy(pNDI_recv); - // pNDI_recv = nullptr; - } + ndiRecv = nullptr; return ""; } virtual ~NdiReader() override diff --git a/native/recorder/src/RecorderAPI.cpp b/native/recorder/src/RecorderAPI.cpp index 191f85a..058b8c4 100644 --- a/native/recorder/src/RecorderAPI.cpp +++ b/native/recorder/src/RecorderAPI.cpp @@ -10,7 +10,8 @@ #include #include -extern "C" { +extern "C" +{ #include #include #include @@ -27,17 +28,21 @@ using json = nlohmann::json; std::shared_ptr recorder; // Utility function to clamp a value between 0 and 255 -inline uint8_t clamp(int value) { +inline uint8_t clamp(int value) +{ return static_cast(std::max(0, std::min(255, value))); } // Function to convert a UYVY422 buffer to a preallocated RGBA buffer void uyvyToRgba(const uint8_t *uyvyBuffer, uint8_t *rgbaBuffer, int width, - int height, int stride) { + int height, int stride) +{ // Initialize index for the RGBA buffer int rgbaIndex = 0; - for (int y = 0; y < height; ++y) { - for (int x = 0; x < width; x += 2) { + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; x += 2) + { // Each UYVY pixel pair contains four bytes int uyvyIndex = y * stride + x * 2; uint8_t u = uyvyBuffer[uyvyIndex]; @@ -46,7 +51,8 @@ void uyvyToRgba(const uint8_t *uyvyBuffer, uint8_t *rgbaBuffer, int width, uint8_t y1 = uyvyBuffer[uyvyIndex + 3]; // Convert YUV to RGB for each pixel - auto convertYuvToRgb = [](uint8_t y, uint8_t u, uint8_t v) { + auto convertYuvToRgb = [](uint8_t y, uint8_t u, uint8_t v) + { int c = y - 16; int d = u - 128; int e = v - 128; @@ -87,10 +93,12 @@ void uyvyToRgba(const uint8_t *uyvyBuffer, uint8_t *rgbaBuffer, int width, Napi::Value convertEventsToJS(const Napi::Env &env, - const std::vector> &eventList) { + const std::vector> &eventList) +{ Napi::Array jsArray = Napi::Array::New(env); - for (size_t i = 0; i < eventList.size(); ++i) { + for (size_t i = 0; i < eventList.size(); ++i) + { Napi::Object jsEvent = Napi::Object::New(env); jsEvent.Set("tsMilli", Napi::Number::New(env, eventList[i]->tsMilli)); jsEvent.Set("subsystem", Napi::String::New(env, eventList[i]->subsystem)); @@ -103,24 +111,38 @@ convertEventsToJS(const Napi::Env &env, } // Helper function to convert nlohmann::json to Napi::Object -Napi::Object ConvertJsonToNapiObject(Napi::Env env, const json &j) { +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()) { + 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()) { + } + else if (it.value().is_number_integer()) + { obj.Set(it.key(), Napi::Number::New(env, it.value())); - } else if (it.value().is_number_float()) { + } + else if (it.value().is_number_float()) + { obj.Set(it.key(), Napi::Number::New(env, it.value())); - } else if (it.value().is_boolean()) { + } + else if (it.value().is_boolean()) + { obj.Set(it.key(), Napi::Boolean::New(env, it.value())); - } else if (it.value().is_object()) { + } + else if (it.value().is_object()) + { obj.Set(it.key(), ConvertJsonToNapiObject(env, it.value())); - } else if (it.value().is_array()) { + } + 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()) { + for (auto &el : it.value()) + { arr.Set(index++, ConvertJsonToNapiObject(env, el)); } obj.Set(it.key(), arr); @@ -130,34 +152,42 @@ Napi::Object ConvertJsonToNapiObject(Napi::Env env, const json &j) { } // Define a destructor to free uint8_t buffers -void FinalizeBuffer(Napi::Env env, void *data) { +void FinalizeBuffer(Napi::Env env, void *data) +{ // Clean up memory if necessary delete[] static_cast(data); } -Napi::Object nativeVideoRecorder(const Napi::CallbackInfo &info) { +Napi::Object nativeVideoRecorder(const Napi::CallbackInfo &info) +{ Napi::Env env = info.Env(); Napi::Object ret = Napi::Object::New(env); ret.Set("status", Napi::String::New(env, "OK")); - if (info.Length() < 1) { + if (info.Length() < 1) + { Napi::TypeError::New(env, "Wrong number of argumentps") .ThrowAsJavaScriptException(); return ret; } auto args = info[0].As(); - if (!args.Has("op")) { + if (!args.Has("op")) + { Napi::TypeError::New(env, "Missing op field").ThrowAsJavaScriptException(); return ret; } - try { + try + { auto op = args.Get("op").As().Utf8Value(); - if (!recorder) { + if (!recorder) + { recorder = std::shared_ptr(new VideoController("ndi")); } - if (op == "start-recording") { - if (!args.Has("props")) { + if (op == "start-recording") + { + if (!args.Has("props")) + { Napi::TypeError::New(env, "Missing props field") .ThrowAsJavaScriptException(); return ret; @@ -166,9 +196,11 @@ Napi::Object nativeVideoRecorder(const Napi::CallbackInfo &info) { auto props = args.Get("props").As(); std::vector prop_names = { "recordingFolder", "recordingPrefix", "recordingDuration", - "networkCamera", "cropArea", "guide"}; - for (const auto &name : prop_names) { - if (!props.Has(name.c_str())) { + "networkCamera", "cropArea", "guide"}; + for (const auto &name : prop_names) + { + if (!props.Has(name.c_str())) + { std::stringstream ss; ss << "Missing recordingProp: " << name; Napi::TypeError::New(env, ss.str()).ThrowAsJavaScriptException(); @@ -185,7 +217,8 @@ Napi::Object nativeVideoRecorder(const Napi::CallbackInfo &info) { auto cropRect = FrameProcessor::FRectangle{0, 0, 0, 0}; if (cropArea.Has("x") && cropArea.Has("y") && cropArea.Has("width") && - cropArea.Has("height")) { + cropArea.Has("height")) + { cropRect = FrameProcessor::FRectangle{ cropArea.Get("x").As().FloatValue(), cropArea.Get("y").As().FloatValue(), @@ -199,29 +232,39 @@ Napi::Object nativeVideoRecorder(const Napi::CallbackInfo &info) { auto result = recorder->start(networkCamera, "ffmpeg", folder, prefix, interval, cropRect, guide); - if (!result.empty()) { + if (!result.empty()) + { std::cerr << "Error: " << result << std::endl; ret.Set("status", Napi::String::New(env, "Fail")); ret.Set("error", Napi::String::New(env, result)); - } else { + } + else + { std::cout << "recording started" << std::endl; } return ret; - } else if (op == "stop-recording") { - if (recorder) { + } + else if (op == "stop-recording") + { + if (recorder) + { auto err = recorder->stop(); std::cerr << "Recorder stopped with status: " << err << std::endl; } return ret; - } else if (op == "get-camera-list") { + } + else if (op == "get-camera-list") + { - if (recorder) { + if (recorder) + { auto cameras = recorder->getCameraList(); Napi::Array arr = Napi::Array::New(env, cameras.size()); size_t index = 0; - for (auto &camera : cameras) { + 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)); @@ -230,18 +273,24 @@ Napi::Object nativeVideoRecorder(const Napi::CallbackInfo &info) { ret.Set("cameras", arr); ret.Set("status", Napi::String::New(env, "OK")); - } else { + } + 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") { - if (recorder) { + } + else if (op == "recording-status") + { + if (recorder) + { auto status = recorder->getStatus(); ret.Set("status", Napi::String::New(env, "OK")); ret.Set("error", Napi::String::New(env, status.error)); ret.Set("recording", Napi::Boolean::New(env, status.recording)); - if (status.recording) { + if (status.recording) + { ret.Set("recordingDuration", Napi::Number::New(env, status.recordingDuration)); @@ -261,25 +310,34 @@ Napi::Object nativeVideoRecorder(const Napi::CallbackInfo &info) { "height", Napi::Number::New(env, status.frameProcessor.height)); frameProcessor.Set("fps", Napi::Number::New(env, status.frameProcessor.fps)); + frameProcessor.Set("frameBacklog", Napi::Number::New(env, status.frameProcessor.frameBacklog)); } - } else { + } + else + { ret.Set("status", Napi::String::New(env, "OK")); } return ret; - } else if (op == "recording-log") { + } + else if (op == "recording-log") + { // 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; - } else if (op == "grab-frame") { + } + else if (op == "grab-frame") + { // grab a rgba frame from the input stream - if (!recorder) { + if (!recorder) + { return ret; } auto uyvy422Frame = recorder->getLastFrame(); - if (!uyvy422Frame) { + if (!uyvy422Frame) + { return ret; } @@ -302,10 +360,14 @@ Napi::Object nativeVideoRecorder(const Napi::CallbackInfo &info) { std::cerr << "Unrecognized op: " << op << std::endl; Napi::TypeError::New(env, "Unrecognized op field") .ThrowAsJavaScriptException(); - } catch (const std::exception &e) { + } + catch (const std::exception &e) + { // Catch standard C++ exceptions and convert them to JavaScript errors Napi::Error::New(env, e.what()).ThrowAsJavaScriptException(); - } catch (...) { + } + catch (...) + { // Catch all other types of exceptions and throw a generic JavaScript // error Napi::Error::New(env, "An unknown error occurred") @@ -319,14 +381,16 @@ Napi::ThreadSafeFunction tsfn; // Function to send message to Electron main process void SendMessageToElectron(const std::string &sender, - std::shared_ptr content) { + std::shared_ptr content) +{ uv_async_t *async = new uv_async_t; uv_loop_t *loop = uv_default_loop(); auto *data = new std::pair>(sender, content); async->data = data; - uv_async_init(loop, async, [](uv_async_t *handle) { + uv_async_init(loop, async, [](uv_async_t *handle) + { auto *data = static_cast> *>( handle->data); std::string sender = data->first; @@ -341,14 +405,14 @@ void SendMessageToElectron(const std::string &sender, }); delete data; uv_close(reinterpret_cast(handle), - [](uv_handle_t *handle) { delete handle; }); - }); + [](uv_handle_t *handle) { delete handle; }); }); uv_async_send(async); } // Initialize the ThreadSafeFunction -Napi::Value InitThreadSafeFunction(const Napi::CallbackInfo &info) { +Napi::Value InitThreadSafeFunction(const Napi::CallbackInfo &info) +{ Napi::Env env = info.Env(); Napi::Function callback = info[0].As(); tsfn = Napi::ThreadSafeFunction::New(env, callback, "NativeEmitter", 0, 1); @@ -357,7 +421,8 @@ Napi::Value InitThreadSafeFunction(const Napi::CallbackInfo &info) { std::ofstream logFile; -Napi::Value shutdownRecorder(const Napi::CallbackInfo &info) { +Napi::Value shutdownRecorder(const Napi::CallbackInfo &info) +{ Napi::Env env = info.Env(); recorder = nullptr; tsfn = Napi::ThreadSafeFunction(); @@ -365,7 +430,8 @@ Napi::Value shutdownRecorder(const Napi::CallbackInfo &info) { return env.Undefined(); } -Napi::Value setLogFile(const Napi::CallbackInfo &info) { +Napi::Value setLogFile(const Napi::CallbackInfo &info) +{ Napi::Env env = info.Env(); std::string logFilename = info[0].As(); logFile = std::ofstream(logFilename.c_str(), std::ios::app); @@ -376,7 +442,8 @@ Napi::Value setLogFile(const Napi::CallbackInfo &info) { } // Initialize the addon -Napi::Object Init(Napi::Env env, Napi::Object exports) { +Napi::Object Init(Napi::Env env, Napi::Object exports) +{ exports.Set(Napi::String::New(env, "nativeVideoRecorder"), Napi::Function::New(env, nativeVideoRecorder)); exports.Set("setNativeMessageCallback", diff --git a/native/recorder/src/VideoController.hpp b/native/recorder/src/VideoController.hpp index 03994b0..a8d81c4 100644 --- a/native/recorder/src/VideoController.hpp +++ b/native/recorder/src/VideoController.hpp @@ -12,9 +12,11 @@ #include "VideoReader.hpp" #include "VideoRecorder.hpp" -class VideoController { +class VideoController +{ public: - struct StatusInfo { + struct StatusInfo + { bool recording; std::string error; std::uint32_t recordingDuration; @@ -41,12 +43,16 @@ class VideoController { StatusInfo statusInfo; public: - VideoController(const std::string _camType) { + VideoController(const std::string _camType) + { #ifdef HAVE_BASLER - if (camType == "basler") { // basler camera + if (camType == "basler") + { // basler camera videoReader = createBaslerReader(); - } else { + } + else + { videoReader = createNdiReader(); } #else @@ -56,24 +62,29 @@ class VideoController { monitorStopRequested = false; monitorThread = std::thread(&VideoController::monitorLoop, this); } - ~VideoController() { + ~VideoController() + { monitorStopRequested = true; stop(); videoReader = nullptr; - if (monitorThread.joinable()) { + if (monitorThread.joinable()) + { monitorThread.join(); } } - std::vector getCameraList() { + std::vector getCameraList() + { std::lock_guard lock(controlMutex); - if (videoReader) { + if (videoReader) + { return videoReader->getCameraList(); } return std::vector(); } - StatusInfo getStatus() { + StatusInfo getStatus() + { std::lock_guard lock(controlMutex); std::chrono::steady_clock::time_point endTime = std::chrono::steady_clock::now(); @@ -93,7 +104,8 @@ class VideoController { const std::string dir, const std::string prefix, const int interval, const FrameProcessor::FRectangle cropArea, - const FrameProcessor::Guide guide) { + const FrameProcessor::Guide guide) + { std::lock_guard lock(controlMutex); this->srcName = srcName; this->encoder = encoder; @@ -102,37 +114,43 @@ class VideoController { this->interval = interval; statusInfo.error = ""; statusInfo.frameProcessor.error = ""; - if (videoRecorder) { + if (videoRecorder) + { return "Video Controller already running"; } status = ""; std::string retval = ""; #ifdef USE_APPLE - if (encoder == "apple") { + if (encoder == "apple") + { SystemEventQueue::push("VID", "Using Apple VideoToolbox encoder."); videoRecorder = createAppleRecorder(); } #endif #ifdef USE_OPENCV - if (encoder == "opencv") { + if (encoder == "opencv") + { SystemEventQueue::push("VID", "Using opencv encoder."); videoRecorder = createOpenCVRecorder(); // default } #endif - if (encoder == "ffmpeg") { + if (encoder == "ffmpeg") + { SystemEventQueue::push("VID", "Using ffmpeg encoder."); videoRecorder = createFfmpegRecorder(); } - if (encoder == "null") { + if (encoder == "null") + { SystemEventQueue::push("VID", "Using null encoder."); videoRecorder = createNullRecorder(); } - if (!videoRecorder) { + if (!videoRecorder) + { auto msg = "Unknown encoder type: " + encoder; SystemEventQueue::push("VID", msg); return msg; @@ -141,36 +159,42 @@ class VideoController { frameProcessor = std::shared_ptr(new FrameProcessor( dir, prefix, videoRecorder, interval, cropArea, guide)); - retval = videoReader->start(srcName, frameProcessor); - if (!retval.empty()) { + retval = videoReader->start(srcName, [this](FramePtr frame) + { this->frameProcessor->addFrame(frame); }); + if (!retval.empty()) + { return retval; } auto fpStatus = frameProcessor->getStatus(); - if (!fpStatus.recording) { + if (!fpStatus.recording) + { return fpStatus.error; } mcastListener = std::shared_ptr( new MulticastReceiver("239.215.23.42", 52342)); - mcastListener->setMessageCallback([this](const json &j) { - // std::cerr << "Received JSON: " << j.dump() << std::endl; - auto command = j.value("cmd", ""); - if (command == "split-video") { - this->frameProcessor->splitFile(); - } - // if (command == "guide-config") { - // json config = {{"pt1", 0}, {"pt2", 0}}; - // auto guide = j.value("guide", config); - // if (!guide.is_null()) { - // std::shared_ptr msg = std::make_shared(guide); - // SendMessageToElectron("guide-config", msg); - // } - // } - }); + mcastListener->setMessageCallback([this](const json &j) + { + // std::cerr << "Received JSON: " << j.dump() << std::endl; + auto command = j.value("cmd", ""); + if (command == "split-video") + { + this->frameProcessor->splitFile(); + } + // if (command == "guide-config") { + // json config = {{"pt1", 0}, {"pt2", 0}}; + // auto guide = j.value("guide", config); + // if (!guide.is_null()) { + // std::shared_ptr msg = std::make_shared(guide); + // SendMessageToElectron("guide-config", msg); + // } + // } + }); retval = mcastListener->start(); - if (!retval.empty()) { + if (!retval.empty()) + { return retval; } @@ -178,9 +202,11 @@ class VideoController { return ""; } - std::string stop() { + std::string stop() + { statusInfo.recording = false; - if (!frameProcessor) { + if (!frameProcessor) + { return ""; } SystemEventQueue::push("VID", "Shutting down..."); @@ -205,20 +231,28 @@ class VideoController { return ""; } - FramePtr getLastFrame() { - if (frameProcessor) { + FramePtr getLastFrame() + { + if (frameProcessor) + { return frameProcessor->getLastFrame(); - } else { + } + else + { return nullptr; } } - void monitorLoop() { - while (!monitorStopRequested) { + void monitorLoop() + { + while (!monitorStopRequested) + { std::this_thread::sleep_for(std::chrono::milliseconds(1000)); std::lock_guard lock(controlMutex); - if (videoRecorder && frameProcessor) { + if (videoRecorder && frameProcessor) + { statusInfo.frameProcessor = frameProcessor->getStatus(); - if (!statusInfo.frameProcessor.error.empty()) { + if (!statusInfo.frameProcessor.error.empty()) + { statusInfo.error = statusInfo.frameProcessor.error; SystemEventQueue::push("system", statusInfo.error); stop(); diff --git a/native/recorder/src/VideoReader.hpp b/native/recorder/src/VideoReader.hpp index 22ede52..88e07b1 100644 --- a/native/recorder/src/VideoReader.hpp +++ b/native/recorder/src/VideoReader.hpp @@ -1,12 +1,16 @@ #pragma once -#include "FrameProcessor.hpp" +#include #include #include +#include "VideoUtils.hpp" -class VideoReader { +class VideoReader +{ public: - typedef struct CameraInfo { + using AddFrameFunction = std::function; + typedef struct CameraInfo + { std::string name; std::string address; CameraInfo() {} @@ -15,13 +19,14 @@ class VideoReader { } CameraInfo; virtual std::string start(const std::string srcName, - std::shared_ptr frameProcessor) = 0; + AddFrameFunction addFrameFunction) = 0; virtual std::string stop() = 0; - virtual std::vector getCameraList() { + virtual std::vector getCameraList() + { return std::vector(); }; virtual ~VideoReader() {} }; std::shared_ptr createBaslerReader(); -std::shared_ptr createNdiReader(); \ No newline at end of file +std::shared_ptr createNdiReader(); diff --git a/native/recorder/src/VideoUtils.cpp b/native/recorder/src/VideoUtils.cpp index 5e83ba6..daba23e 100644 --- a/native/recorder/src/VideoUtils.cpp +++ b/native/recorder/src/VideoUtils.cpp @@ -1,13 +1,15 @@ #include #include #include +#include "VideoUtils.hpp" -#include "FrameProcessor.hpp" FramePtr cropFrame(const FramePtr &frame, int cropX, int cropY, int cropWidth, - int cropHeight) { + int cropHeight) +{ if (!frame || cropX < 0 || cropY < 0 || cropX + cropWidth > frame->xres || - cropY + cropHeight > frame->yres) { - std::cerr << "Invalid crop. frame: " << !frame << " cropX < 0:" << (cropX < 0) << " cropY < 0:" << (cropY < 0) << "cropX + cropWidth > frame->xres:" << (cropX + cropWidth > frame->xres) << " cropY + cropHeight > frame->yres:" << (cropY + cropHeight > frame->yres) << " xres: " << frame->xres << " yres: " << frame->yres << std::endl; + cropY + cropHeight > frame->yres) + { + std::cerr << "Invalid crop. frame: " << !frame << " cropX < 0:" << (cropX < 0) << " cropY < 0:" << (cropY < 0) << "cropX + cropWidth > frame->xres:" << (cropX + cropWidth > frame->xres) << " cropY + cropHeight > frame->yres:" << (cropY + cropHeight > frame->yres) << " xres: " << frame->xres << " yres: " << frame->yres << std::endl; return nullptr; // Return null if invalid crop dimensions } @@ -21,7 +23,8 @@ FramePtr cropFrame(const FramePtr &frame, int cropX, int cropY, int cropWidth, ? 2 : (frame->pixelFormat == Frame::RGBX ? 4 : 3); - for (int y = 0; y < cropHeight; ++y) { + for (int y = 0; y < cropHeight; ++y) + { uint8_t *srcPtr = frame->data + ((cropY + y) * frame->stride) + (cropX * bytesPerPixel); uint8_t *dstPtr = croppedFrame->data + (y * croppedFrame->stride); @@ -31,7 +34,8 @@ FramePtr cropFrame(const FramePtr &frame, int cropX, int cropY, int cropWidth, return croppedFrame; } -static uint32_t uyvy422(uint8_t r, uint8_t g, uint8_t b) { +static uint32_t uyvy422(uint8_t r, uint8_t g, uint8_t b) +{ // https://github.com/lplassman/V4L2-to-NDI/blob/4dd5e9594acc4f154658283ee52718fa58018ac9/PixelFormatConverter.cpp auto Y0 = (0.299f * r + 0.587f * g + 0.114f * b); auto Y1 = (0.299f * r + 0.587f * g + 0.114f * b); @@ -53,23 +57,28 @@ const static auto green = uyvy422(0, 255, 0); const static auto white = uyvy422(255, 255, 255); static void setArea(uint32_t *screen, int stride, int startX, int startY, - int width, int height, uint32_t color) { + int width, int height, uint32_t color) +{ // std::cout << "setArea" << width << "x" << height << " stride" << stride // << " ptr" << std::hex << (void *)screen << std::dec << std::endl; - for (int x = startX / 2; x < startX / 2 + width / 2; x++) { - for (int y = startY; y < startY + height; y++) { + for (int x = startX / 2; x < startX / 2 + width / 2; x++) + { + for (int y = startY; y < startY + height; y++) + { screen[x + y * stride / 4] = color; } } } -void encodeTimestamp(uint8_t *ptr, int stride, uint64_t ts100ns) { +void encodeTimestamp(uint8_t *ptr, int stride, uint64_t ts100ns) +{ // std::cout << "overlayTime" << std::endl; // return; uint32_t *screen = (uint32_t *)(ptr); setArea(screen, stride, 0, 0, 128, 3, black); uint64_t mask = 0x8000000000000000L; - for (int bit = 0; bit < 64; bit++) { + for (int bit = 0; bit < 64; bit++) + { const bool val = (ts100ns & mask) != 0; mask >>= 1; setArea(screen, stride, bit * 2, 1, 2, 1, val ? white : black); diff --git a/native/recorder/src/ctrecorder.cpp b/native/recorder/src/ctrecorder.cpp index ec07005..969fb2e 100644 --- a/native/recorder/src/ctrecorder.cpp +++ b/native/recorder/src/ctrecorder.cpp @@ -26,25 +26,29 @@ bool running; #ifdef _WIN32 void daemonize() {} #else -void daemonize() { +void daemonize() +{ pid_t pid; // Fork off the parent process pid = fork(); - if (pid < 0) { + if (pid < 0) + { exit(EXIT_FAILURE); } // If we got a good PID, exit the parent process - if (pid > 0) { + if (pid > 0) + { exit(EXIT_SUCCESS); } // At this point, we are in the child process // Create a new SID for the child process - if (setsid() < 0) { + if (setsid() < 0) + { exit(EXIT_FAILURE); } @@ -60,15 +64,23 @@ void daemonize() { } #endif std::function stopHandler = []() {}; -void signalHandler(int signal) { - if (signal == SIGINT) { - std::cout << std::endl << "SIGINT received, shutting down" << std::endl; - if (stopHandler) { - std::cout << std::endl << "Calling stop handler" << std::endl; +void signalHandler(int signal) +{ + if (signal == SIGINT) + { + std::cout << std::endl + << "SIGINT received, shutting down" << std::endl; + if (stopHandler) + { + std::cout << std::endl + << "Calling stop handler" << std::endl; stopHandler(); - std::cout << std::endl << "Exiting program" << std::endl; + std::cout << std::endl + << "Exiting program" << std::endl; exit(0); - } else { + } + else + { exit(0); } } @@ -76,11 +88,13 @@ void signalHandler(int signal) { void testopencv(); -void testrecorder(std::shared_ptr recorder) { +void testrecorder(std::shared_ptr recorder) +{ std::cout << "testrecorder start" << std::endl; recorder->openVideoStream("./", "test", 640, 480, 30); const auto image = new uint8_t[640 * 480 * 3]; - for (int pixel = 0; pixel < 640 * 480 * 3; pixel += 3) { + for (int pixel = 0; pixel < 640 * 480 * 3; pixel += 3) + { image[pixel] = pixel > 640 * 480 && pixel < 640 * 480 * 2 ? 255 : 0; image[pixel + 1] = pixel > 640 * 480 * 2 ? 255 : 0; image[pixel + 2] = 255; @@ -94,7 +108,8 @@ void testrecorder(std::shared_ptr recorder) { frame->frame_rate_N = 30; frame->frame_rate_D = 1; frame->pixelFormat = Frame::PixelFormat::BGR; - for (int i = 0; i < 30; i++) { + for (int i = 0; i < 30; i++) + { recorder->writeVideoFrame(frame); } recorder->stop(); @@ -103,7 +118,8 @@ void testrecorder(std::shared_ptr recorder) { } std::shared_ptr recorder; -int main(int argc, char *argv[]) { +int main(int argc, char *argv[]) +{ std::shared_ptr videoRecorder; std::signal(SIGINT, signalHandler); @@ -123,18 +139,21 @@ int main(int argc, char *argv[]) { std::string defaultRecorder = "null"; #ifdef USE_FFMPEG recorders += " | ffmpeg"; - if (defaultRecorder == "null") { + if (defaultRecorder == "null") + { defaultRecorder = "ffmpeg"; } #endif #ifdef USE_OPENCV - if (defaultRecorder == "null") { + if (defaultRecorder == "null") + { defaultRecorder = "opencv"; } recorders += " | opencv"; #endif #ifdef USE_APPLE - if (defaultRecorder == "null") { + if (defaultRecorder == "null") + { defaultRecorder = "apple"; } recorders += " | apple"; @@ -143,16 +162,22 @@ int main(int argc, char *argv[]) { args["-encoder"] = defaultRecorder; // Parse command-line arguments - for (int i = 1; i < argc; ++i) { + for (int i = 1; i < argc; ++i) + { std::string arg = argv[i]; if ((arg == "-encoder" || arg == "-dir" || arg == "-prefix" || arg == "-i" || arg == "-ndi" || arg == "-timeout") && - i + 1 < argc) { + i + 1 < argc) + { args[arg] = argv[++i]; // Increment 'i' to skip next argument since it's a value - } else if (arg == "-daemon" || arg == "-u") { + } + else if (arg == "-daemon" || arg == "-u") + { args[arg] = "true"; - } else { + } + else + { auto msg = "Unknown or incomplete option: " + arg; std::cerr << msg << std::endl; return -1; @@ -161,13 +186,15 @@ int main(int argc, char *argv[]) { // Check for required arguments if (args.find("-encoder") == args.end() || args.find("-dir") == args.end() || - args.find("-prefix") == args.end() || args.find("-i") == args.end()) { + args.find("-prefix") == args.end() || args.find("-i") == args.end()) + { auto msg = "Missing required arguments."; std::cerr << msg << std::endl; return -1; } - if (args["-u"] == "true") { + if (args["-u"] == "true") + { std::cout << "Usage: -encoder <" << recorders << "> -dir -prefix " " -i -ndi -timeout " @@ -177,7 +204,8 @@ int main(int argc, char *argv[]) { return -1; } - if (args["-daemon"] == "true") { + if (args["-daemon"] == "true") + { std::cout << "Running in unattended mode." << std::endl; daemonize(); } @@ -192,9 +220,10 @@ int main(int argc, char *argv[]) { recorder = std::shared_ptr(new VideoController("ndi")); recorder->start(srcName, encoder, directory, prefix, interval, - {1280 / 4, 720 / 4, 1280 / 2, 720 / 2}); + {0, 0, 1, 1}, {}); - auto startShutdown = []() { + auto startShutdown = []() + { recorder->stop(); recorder = nullptr; running = false; @@ -205,11 +234,14 @@ int main(int argc, char *argv[]) { running = true; int count = 0; - while (running) { + while (running) + { std::this_thread::sleep_for(std::chrono::milliseconds(1000)); - if (timeout != 0) { + if (timeout != 0) + { count += 1; - if (count >= timeout) { + if (count >= timeout) + { startShutdown(); break; } diff --git a/native/recorder/yarn.lock b/native/recorder/yarn.lock index b1b2e7d..e920e42 100644 --- a/native/recorder/yarn.lock +++ b/native/recorder/yarn.lock @@ -94,16 +94,16 @@ "@types/node" "*" "@types/node@*", "@types/node@^22.9.1": - version "22.10.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.1.tgz#41ffeee127b8975a05f8c4f83fb89bcb2987d766" - integrity sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ== + version "22.10.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.6.tgz#5c6795e71635876039f853cbccd59f523d9e4239" + integrity sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ== dependencies: undici-types "~6.20.0" "@types/node@^20.9.0": - version "20.17.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.17.9.tgz#5f141d4b7ee125cdee5faefe28de095398865bab" - integrity sha512-0JOXkRyLanfGPE2QRCwgxhzlBAvaRdCNMcvbd7jFfpmD4eEXll7LRwy5ymJmyeZqk7Nh7eD2LeUyQ68BbndmXw== + version "20.17.12" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.17.12.tgz#ee3b7d25a522fd95608c1b3f02921c97b93fcbd6" + integrity sha512-vo/wmBgMIiEA23A/knMfn/cf37VnuF52nZh5ZoW0GWt4e4sxNquibrMRJ7UQsA06+MBx9r/H1jsI9grYjQCQlw== dependencies: undici-types "~6.19.2" @@ -136,12 +136,10 @@ after@~0.8.1: resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" integrity sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA== -agent-base@^7.0.2, agent-base@^7.1.0, agent-base@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317" - integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== - dependencies: - debug "^4.3.4" +agent-base@^7.1.0, agent-base@^7.1.2: + version "7.1.3" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.3.tgz#29435eb821bc4194633a5b89e5bc4703bafc25a1" + integrity sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw== aggregate-error@^3.0.0: version "3.1.0" @@ -260,9 +258,9 @@ aws4@^1.8.0: integrity sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw== axios@^1.6.5: - version "1.7.8" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.8.tgz#1997b1496b394c21953e68c14aaa51b7b5de3d6e" - integrity sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw== + version "1.7.9" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a" + integrity sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw== dependencies: follow-redirects "^1.15.6" form-data "^4.0.0" @@ -279,9 +277,9 @@ balanced-match@^1.0.0: integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== bare-events@^2.2.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.5.0.tgz#305b511e262ffd8b9d5616b056464f8e1b3329cc" - integrity sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A== + version "2.5.4" + resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.5.4.tgz#16143d435e1ed9eafd1ab85f12b89b3357a41745" + integrity sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA== base64-js@^1.3.1: version "1.5.1" @@ -538,9 +536,9 @@ dashdash@^1.12.0: assert-plus "^1.0.0" debug@4, debug@^4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.4: - version "4.3.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" - integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== dependencies: ms "^2.1.3" @@ -632,9 +630,9 @@ ecc-jsbn@~0.1.1: safer-buffer "^2.1.0" electron@^33.2.0: - version "33.2.1" - resolved "https://registry.yarnpkg.com/electron/-/electron-33.2.1.tgz#d0d7bba7a7abf4f14881d0a6e03c498b301a2d5f" - integrity sha512-SG/nmSsK9Qg1p6wAW+ZfqU+AV8cmXMTIklUL18NnOKfZLlum4ZsDoVdmmmlL39ZmeCaq27dr7CgslRPahfoVJg== + version "33.3.1" + resolved "https://registry.yarnpkg.com/electron/-/electron-33.3.1.tgz#26d63b3a110c4a9db639c33633bf806f91b4f649" + integrity sha512-Z7l2bVgpdKxHQMI4i0CirBX2n+iCYKOx5mbzNM3BpOyFELwlobEXKmzCmEnwP+3EcNeIhUQyIEBFQxN06QgdIw== dependencies: "@electron/get" "^2.0.0" "@types/node" "^20.9.0" @@ -675,11 +673,9 @@ err-code@^2.0.2: integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== es-define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" - integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== - dependencies: - get-intrinsic "^1.2.4" + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== es-errors@^1.3.0: version "1.3.0" @@ -911,11 +907,6 @@ fstream@^1.0.0, fstream@^1.0.12: mkdirp ">=0.5 0" rimraf "2" -function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - gauge@^4.0.3: version "4.0.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce" @@ -974,17 +965,6 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" - integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.0" - get-stream@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" @@ -1076,11 +1056,9 @@ globalthis@^1.0.1: gopd "^1.0.1" gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== - dependencies: - get-intrinsic "^1.1.3" + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== got@^11.8.5: version "11.8.6" @@ -1124,28 +1102,11 @@ has-property-descriptors@^1.0.0: dependencies: es-define-property "^1.0.0" -has-proto@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" - integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== - -has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== - has-unicode@^2.0.0, has-unicode@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== -hasown@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" - integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== - dependencies: - function-bind "^1.1.2" - http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" @@ -1177,11 +1138,11 @@ http2-wrapper@^1.0.0-beta.5.2: resolve-alpn "^1.0.0" https-proxy-agent@^7.0.1: - version "7.0.5" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2" - integrity sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw== + version "7.0.6" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" + integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== dependencies: - agent-base "^7.0.2" + agent-base "^7.1.2" debug "4" hyperquest@~2.1.3: @@ -1615,9 +1576,9 @@ node-api-headers@^1.1.0: integrity sha512-u83U3WnRbBpWlhc0sQbpF3slHRLV/a6/OXByc+QzHcLxiDiJUWLuKGZp4/ntZUchnXGOCnCq++JUEtwb1/tyow== node-gyp@^10.0.1, node-gyp@^10.2.0: - version "10.2.0" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-10.2.0.tgz#80101c4aa4f7ab225f13fcc8daaaac4eb1a8dd86" - integrity sha512-sp3FonBAaFe4aYTcFdZUn2NYkbP7xroPGYvQmP4Nl5PxamznItBnNCgjrVTKrEfQynInMsJvZrdmqUnysCJ8rw== + version "10.3.1" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-10.3.1.tgz#1dd1a1a1c6c5c59da1a76aea06a062786b2c8a1a" + integrity sha512-Pp3nFHBThHzVtNY7U6JfPjvT/DTE8+o/4xKsLQtBoU+j2HLsGlhcfzflAoUreaJbNmYnX+LlLi0qjV8kpyO6xQ== dependencies: env-paths "^2.2.0" exponential-backoff "^3.1.1" @@ -1917,9 +1878,9 @@ proxy-from-env@^1.1.0: integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== psl@^1.1.28: - version "1.14.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.14.0.tgz#f6ccbbd63e4e663f830ca39eeea08feb3caceaaf" - integrity sha512-Syk1bnf6fRZ9wQs03AtKJHcM12cKbOLo9L8JtCCdYj5/DTsHmTyXM4BK5ouWeG2P6kZ4nmFvuNTdtaqfobCOCg== + version "1.15.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.15.0.tgz#bdace31896f1d97cec6a79e8224898ce93d974c6" + integrity sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w== dependencies: punycode "^2.3.1" @@ -2181,11 +2142,11 @@ smart-buffer@^4.2.0: integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== socks-proxy-agent@^8.0.3: - version "8.0.4" - resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz#9071dca17af95f483300316f4b063578fa0db08c" - integrity sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw== + version "8.0.5" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz#b9cdb4e7e998509d7659d689ce7697ac21645bee" + integrity sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw== dependencies: - agent-base "^7.1.1" + agent-base "^7.1.2" debug "^4.3.4" socks "^2.8.3" @@ -2225,9 +2186,9 @@ ssri@^10.0.0: minipass "^7.0.3" streamx@^2.15.0: - version "2.20.2" - resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.20.2.tgz#6a8911959d6f307c19781a1d19ecd94b5f042d78" - integrity sha512-aDGDLU+j9tJcUdPGOaHmVF1u/hhI+CsGkT02V3OKlHDV7IukOI+nTWAGkiZEKCO35rWN1wIr4tS7YFr1f4qSvA== + version "2.21.1" + resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.21.1.tgz#f02979d8395b6b637d08a589fb514498bed55845" + integrity sha512-PhP9wUnFLa+91CPy3N6tiQsK+gnYyUNuk15S3YG/zjYE7RuPeCjJngqnzpC31ow0lzBHQ+QGO4cNJnd0djYUsw== dependencies: fast-fifo "^1.3.2" queue-tick "^1.0.1" @@ -2331,9 +2292,9 @@ sumchecker@^3.0.1: debug "^4.1.0" tar-fs@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" - integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== + version "2.1.2" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.2.tgz#425f154f3404cb16cb8ff6e671d45ab2ed9596c5" + integrity sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA== dependencies: chownr "^1.1.1" mkdirp-classic "^0.5.2" @@ -2394,9 +2355,11 @@ tar@^7.4.3: yallist "^5.0.0" text-decoder@^1.1.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.1.tgz#e173f5121d97bfa3ff8723429ad5ba92e1ead67e" - integrity sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ== + version "1.2.3" + resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.3.tgz#b19da364d981b2326d5f43099c310cc80d770c65" + integrity sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA== + dependencies: + b4a "^1.6.4" through2@~0.6.3: version "0.6.5" @@ -2437,9 +2400,9 @@ type@^2.7.2: integrity sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ== typescript@^5.6.3: - version "5.7.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6" - integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg== + version "5.7.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.3.tgz#919b44a7dbb8583a9b856d162be24a54bf80073e" + integrity sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw== undici-types@~6.19.2: version "6.19.8" diff --git a/release/app/package.json b/release/app/package.json index 23ae750..557a8b3 100644 --- a/release/app/package.json +++ b/release/app/package.json @@ -1,6 +1,6 @@ { "name": "crewtimer-video-recorder", - "version": "1.0.10", + "version": "1.0.11", "description": "An NDI video recorder for use with CrewTimer Video Review", "license": "MIT", "author": { diff --git a/release/app/yarn.lock b/release/app/yarn.lock index aa421ee..c616f83 100644 --- a/release/app/yarn.lock +++ b/release/app/yarn.lock @@ -2,9 +2,9 @@ # yarn lockfile v1 -"@electron/node-gyp@git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2": +"@electron/node-gyp@https://github.com/electron/node-gyp#06b29aafb7708acef8b3669835c8a7857ebc92d2": version "10.2.0-electron.1" - resolved "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2" + resolved "https://github.com/electron/node-gyp#06b29aafb7708acef8b3669835c8a7857ebc92d2" dependencies: env-paths "^2.2.0" exponential-backoff "^3.1.1" @@ -361,7 +361,7 @@ concat-map@0.0.1: integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== crewtimer_video_recorder@../../native/recorder: - version "1.0.6-module" + version "1.0.7-module" dependencies: bindings "^1.5.0" prebuild-install "^7.1.2" diff --git a/src/renderer/recorder/RecorderTypes.ts b/src/renderer/recorder/RecorderTypes.ts index 58f681d..6c4a198 100644 --- a/src/renderer/recorder/RecorderTypes.ts +++ b/src/renderer/recorder/RecorderTypes.ts @@ -54,6 +54,7 @@ export interface RecordingStatus extends HandlerResponse { width: number; height: number; fps: number; + frameBacklog: number; }; } diff --git a/src/renderer/recorder/RecordingStatus.tsx b/src/renderer/recorder/RecordingStatus.tsx index 3f2c826..d34457e 100644 --- a/src/renderer/recorder/RecordingStatus.tsx +++ b/src/renderer/recorder/RecordingStatus.tsx @@ -46,10 +46,11 @@ const RecordingStatus: React.FC = () => { }, []); let cropText = ''; - if (cropArea.width != 1 || cropArea.height != 1) { + if (cropArea.width !== 1 || cropArea.height !== 1) { cropText = ` -> ${Math.round((cropArea.width * recordingStatus.frameProcessor.width) / 4) * 4}x${Math.round((cropArea.height * recordingStatus.frameProcessor.height) / 4) * 4}`; } + const { frameBacklog } = recordingStatus.frameProcessor; return isRecording ? ( { > {`${seconds}`} + 100 + ? '#ffff00' + : frameBacklog > 200 + ? '#ff0000' + : undefined, + color: frameBacklog > 100 ? '#000000' : undefined, + }} + >{` backlog: ${frameBacklog}`} {recordingStatus.frameProcessor.filename ? ( <> diff --git a/yarn.lock b/yarn.lock index 8a67613..52a2028 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1028,9 +1028,9 @@ optionalDependencies: global-agent "^3.0.0" -"@electron/node-gyp@git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2": +"@electron/node-gyp@https://github.com/electron/node-gyp#06b29aafb7708acef8b3669835c8a7857ebc92d2": version "10.2.0-electron.1" - resolved "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2" + resolved "https://github.com/electron/node-gyp#06b29aafb7708acef8b3669835c8a7857ebc92d2" dependencies: env-paths "^2.2.0" exponential-backoff "^3.1.1"