From 1f7ab8549b999670ad83e2da87419d853bc03b95 Mon Sep 17 00:00:00 2001 From: Glenn Engel Date: Mon, 13 May 2024 21:16:04 -0600 Subject: [PATCH] Account for NDI stride for odd sized windows --- native/recorder/src/FFRecorder.cpp | 2 +- native/recorder/src/NdiReader.cpp | 58 ++++++++++++++------------- native/recorder/src/RecorderAPI.cpp | 7 ++-- native/recorder/src/VideoRecorder.hpp | 1 + package.json | 2 + yarn.lock | 34 ++++++++++++++++ 6 files changed, 72 insertions(+), 32 deletions(-) diff --git a/native/recorder/src/FFRecorder.cpp b/native/recorder/src/FFRecorder.cpp index 01c0c5a..6507b1d 100644 --- a/native/recorder/src/FFRecorder.cpp +++ b/native/recorder/src/FFRecorder.cpp @@ -199,7 +199,7 @@ class FFVideoRecorder : public VideoRecorder { } std::string writeVideoFrame(FramePtr video_frame) { - int inLinesize[1] = {2 * video_frame->xres}; + int inLinesize[1] = {video_frame->stride}; if (sws_ctx == nullptr) { auto src_fmt = AV_PIX_FMT_UYVY422; switch (video_frame->pixelFormat) { diff --git a/native/recorder/src/NdiReader.cpp b/native/recorder/src/NdiReader.cpp index 552c603..50e3a1a 100644 --- a/native/recorder/src/NdiReader.cpp +++ b/native/recorder/src/NdiReader.cpp @@ -86,7 +86,7 @@ class Point { } }; -void setDigitPixels(uint32_t *screen, int digit, Point &start, int xres, +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 @@ -94,17 +94,17 @@ void setDigitPixels(uint32_t *screen, int digit, Point &start, int xres, const auto border = 4; for (size_t y = 0; y < border; y++) { for (size_t x = 0; x < digitPixels[0].size() * scale + border * 2; ++x) { - screen[(start.x + x) + (start.y + y) * xres / 2] = bg; + screen[(start.x + x) + (start.y + y) * stride / 4] = bg; screen[(start.x + x) + - (start.y + y + digitPixels.size() * scale + border) * xres / 2] = + (start.y + y + digitPixels.size() * scale + border) * stride / 4] = bg; } } for (size_t y = 0; y < digitPixels.size() * scale + 2 * border; ++y) { for (size_t x = 0; x < border; x++) { - screen[(start.x + x) + (start.y + y) * xres / 2] = bg; + screen[(start.x + x) + (start.y + y) * stride / 4] = bg; screen[(start.x + x + border + digitPixels[0].size() * scale) + - (start.y + y) * xres / 2] = bg; + (start.y + y) * stride / 4] = bg; } } const auto yOffset = border; @@ -115,7 +115,7 @@ void setDigitPixels(uint32_t *screen, int digit, Point &start, int xres, const auto pixel = digitPixels[y][x] ? fg : bg; for (size_t xExpand = 0; xExpand < scale; xExpand++) { screen[(xOffset + start.x + x * scale + xExpand) + - ((yOffset + start.y) + y * scale + yExpand) * xres / 2] = + ((yOffset + start.y) + y * scale + yExpand) * stride / 4] = pixel; } } @@ -124,35 +124,35 @@ void setDigitPixels(uint32_t *screen, int digit, Point &start, int xres, start.x += int(digitPixels[0].size() * scale + border * 2 - 2); } -void setArea(uint32_t *screen, int xres, int startX, int startY, int width, +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 * xres / 2] = color; + screen[x + y * stride / 4] = color; } } } -void overlayDigits(uint32_t *screen, int xres, Point &point, uint16_t value, +void overlayDigits(uint32_t *screen, int stride, Point &point, uint16_t value, int digits) { - // clearArea(screen, xres, point.x - scale * 2, point.y - scale * 4, + // 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, xres, timeColor, black); + setDigitPixels(screen, hundreds, point, stride, timeColor, black); } if (digits >= 2) { const auto tens = (value / 10) % 10; - setDigitPixels(screen, tens, point, xres, timeColor, black); + setDigitPixels(screen, tens, point, stride, timeColor, black); } const auto ones = value % 10; - setDigitPixels(screen, ones, point, xres, timeColor, black); + setDigitPixels(screen, ones, point, stride, timeColor, black); } -void overlayTime(uint32_t *screen, int xres, uint64_t ts100ns) { +void overlayTime(uint32_t *screen, int stride, uint64_t ts100ns) { const auto milli = (5000 + ts100ns) / 10000; // Convert utc milliseconds to time_point of system clock @@ -171,13 +171,13 @@ void overlayTime(uint32_t *screen, int xres, uint64_t ts100ns) { Point point = Point(20, 40); - overlayDigits(screen, xres, point, local_hours, 2); - setDigitPixels(screen, 10, point, xres, timeColor, black); - overlayDigits(screen, xres, point, local_minutes, 2); - setDigitPixels(screen, 10, point, xres, timeColor, black); - overlayDigits(screen, xres, point, local_secs, 2); - setDigitPixels(screen, 11, point, xres, timeColor, black); - overlayDigits(screen, xres, point, milli % 1000, 3); + 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; @@ -185,12 +185,12 @@ void overlayTime(uint32_t *screen, int xres, uint64_t ts100ns) { // << (milli / (60 * 1000)) % 60 << ":" << (milli / (1000)) % 60 // << " m=" << milli << std::endl; - setArea(screen, xres, 0, 0, 128, 3, black); + 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, xres, bit * 2, 1, 2, 1, val ? white : black); + setArea(screen, stride, bit * 2, 1, 2, 1, val ? white : black); } } @@ -302,10 +302,13 @@ class NdiReader : public VideoReader { // Video data case NDIlib_frame_type_video: { if (video_frame.xres && video_frame.yres) { + if (video_frame.timestamp == NDIlib_recv_timestamp_undefined) { + std::cerr << "timestamp not supported" << std::endl; + } // std::cout << "Video data received (" << video_frame.xres << "x" // << video_frame.yres << std::endl; - overlayTime((uint32_t *)video_frame.p_data, video_frame.xres, - video_frame.timestamp); + overlayTime((uint32_t *)video_frame.p_data, + video_frame.line_stride_in_bytes, video_frame.timestamp); auto delta = video_frame.timestamp - lastTS; if (delta == 0 || (lastTS != 0 && delta > 400000)) { // Convert 100ns ticks since 1970 to a duration, then to @@ -341,8 +344,9 @@ class NdiReader : public VideoReader { lastTS = video_frame.timestamp; auto txframe = std::make_shared(pNDI_recv, video_frame); - txframe->xres = video_frame.xres; - txframe->yres = video_frame.yres; + txframe->xres = video_frame.xres & ~1; // force even + txframe->yres = video_frame.yres & ~1; + txframe->stride = video_frame.line_stride_in_bytes; txframe->timestamp = video_frame.timestamp; txframe->data = video_frame.p_data; txframe->frame_rate_N = video_frame.frame_rate_N; diff --git a/native/recorder/src/RecorderAPI.cpp b/native/recorder/src/RecorderAPI.cpp index 53ae24d..00f746c 100644 --- a/native/recorder/src/RecorderAPI.cpp +++ b/native/recorder/src/RecorderAPI.cpp @@ -26,14 +26,13 @@ inline uint8_t clamp(int 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 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) { // Each UYVY pixel pair contains four bytes - int uyvyIndex = (y * width + x) * 2; + int uyvyIndex = y * stride + x * 2; uint8_t u = uyvyBuffer[uyvyIndex]; uint8_t y0 = uyvyBuffer[uyvyIndex + 1]; uint8_t v = uyvyBuffer[uyvyIndex + 2]; @@ -203,7 +202,7 @@ Napi::Object nativeVideoRecorder(const Napi::CallbackInfo &info) { auto bufferData = Napi::Buffer::New(env, totalBytes); uyvyToRgba(uyvy422Frame->data, bufferData.Data(), uyvy422Frame->xres, - uyvy422Frame->yres); + uyvy422Frame->yres, uyvy422Frame->stride); // Napi::Buffer napiBuffer = Napi::Buffer::New( // env, bufferData, totalBytes, FinalizeBuffer); // ret.Set("data", napiBuffer); diff --git a/native/recorder/src/VideoRecorder.hpp b/native/recorder/src/VideoRecorder.hpp index 259811f..8780376 100644 --- a/native/recorder/src/VideoRecorder.hpp +++ b/native/recorder/src/VideoRecorder.hpp @@ -13,6 +13,7 @@ class Frame { enum PixelFormat { UYVY422 = 0, RGBX = 1, BGR = 2 }; int xres; int yres; + int stride; uint8_t *data; uint64_t timestamp; int frame_rate_N; diff --git a/package.json b/package.json index 7c89e11..f9bd4a5 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "react-dom": "^18.2.0", "react-markdown": "^9.0.1", "react-markdown-css": "^1.0.2", + "react-measure": "^2.5.2", "react-router-dom": "^6.22.3", "react-usedatum": "^1.0.7", "rehype-raw": "^7.0.0", @@ -130,6 +131,7 @@ "@types/node": "20.6.2", "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", + "@types/react-measure": "^2.0.12", "@types/react-test-renderer": "^18.0.1", "@types/terser-webpack-plugin": "^5.0.4", "@types/webpack-bundle-analyzer": "^4.6.0", diff --git a/yarn.lock b/yarn.lock index 97381ab..ece422b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1026,6 +1026,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.2.0": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.5.tgz#230946857c053a36ccc66e1dd03b17dd0c4ed02c" + integrity sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.22.15", "@babel/template@^7.24.0", "@babel/template@^7.3.3": version "7.24.0" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.0.tgz#c6a524aa93a4a05d66aaf31654258fae69d87d50" @@ -2359,6 +2366,13 @@ dependencies: "@types/react" "*" +"@types/react-measure@^2.0.12": + version "2.0.12" + resolved "https://registry.yarnpkg.com/@types/react-measure/-/react-measure-2.0.12.tgz#e8ba05057357b9529aa4115064fe7ea77549f54c" + integrity sha512-Y6V11CH6bU7RhqrIdENPwEUZlPXhfXNGylMNnGwq5TAEs2wDoBA3kSVVM/EQ8u72sz5r9ja+7W8M8PIVcS841Q== + dependencies: + "@types/react" "*" + "@types/react-test-renderer@^18.0.1": version "18.0.7" resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-18.0.7.tgz#2cfe657adb3688cdf543995eceb2e062b5a68728" @@ -5832,6 +5846,11 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" +get-node-dimensions@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/get-node-dimensions/-/get-node-dimensions-1.2.1.tgz#fb7b4bb57060fb4247dd51c9d690dfbec56b0823" + integrity sha512-2MSPMu7S1iOTL+BOa6K1S62hB2zUAYNF/lV0gSVlOaacd087lc6nR1H1r0e3B1CerTo+RceOmi1iJW+vp21xcQ== + get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" @@ -9390,6 +9409,16 @@ react-markdown@^9.0.1: unist-util-visit "^5.0.0" vfile "^6.0.0" +react-measure@^2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/react-measure/-/react-measure-2.5.2.tgz#4ffc410e8b9cb836d9455a9ff18fc1f0fca67f89" + integrity sha512-M+rpbTLWJ3FD6FXvYV6YEGvQ5tMayQ3fGrZhRPHrE9bVlBYfDCLuDcgNttYfk8IqfOI03jz6cbpqMRTUclQnaA== + dependencies: + "@babel/runtime" "^7.2.0" + get-node-dimensions "^1.2.1" + prop-types "^15.6.2" + resize-observer-polyfill "^1.5.0" + react-refresh@^0.14.0: version "0.14.0" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e" @@ -9667,6 +9696,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== +resize-observer-polyfill@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-alpn@^1.0.0: version "1.2.1" resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9"