From f0659d289fe03038ada2431efc2241cf58fa47a9 Mon Sep 17 00:00:00 2001 From: Ryan Doupe Date: Fri, 16 Oct 2026 20:05:04 -0400 Subject: [PATCH 1/2] utils: Move the source screenshot render into a helper GetSourceScreenshot and SaveSourceScreenshot render the source with TakeSourceScreenshot(), which now lives in utils/SourceScreenshot.cpp as Utils::TakeSourceScreenshot(), so it can be tested on its own. No change in behaviour. Refs obs-rust/obs-studio#1141. Co-Authored-By: Claude Opus 5.5 --- CMakeLists.txt | 2 + src/requesthandler/RequestHandler_Sources.cpp | 83 +-------------- src/utils/SourceScreenshot.cpp | 100 ++++++++++++++++++ src/utils/SourceScreenshot.h | 29 +++++ 4 files changed, 134 insertions(+), 80 deletions(-) create mode 100644 src/utils/SourceScreenshot.cpp create mode 100644 src/utils/SourceScreenshot.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 72a50f33..25b85464 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -128,6 +128,8 @@ target_sources( src/utils/Obs_VolumeMeter_Helpers.h src/utils/Platform.cpp src/utils/Platform.h + src/utils/SourceScreenshot.cpp + src/utils/SourceScreenshot.h src/utils/Utils.h) configure_file(src/plugin-macros.h.in plugin-macros.generated.h) diff --git a/src/requesthandler/RequestHandler_Sources.cpp b/src/requesthandler/RequestHandler_Sources.cpp index 857a1ce9..cfc63aa6 100644 --- a/src/requesthandler/RequestHandler_Sources.cpp +++ b/src/requesthandler/RequestHandler_Sources.cpp @@ -25,84 +25,7 @@ with this program. If not, see #include "RequestHandler.h" #include "../utils/ImageFormat.h" - -QImage TakeSourceScreenshot(obs_source_t *source, bool &success, uint32_t requestedWidth = 0, uint32_t requestedHeight = 0) -{ - // Get info about the requested source - const uint32_t sourceWidth = obs_source_get_width(source); - const uint32_t sourceHeight = obs_source_get_height(source); - const double sourceAspectRatio = ((double)sourceWidth / (double)sourceHeight); - - uint32_t imgWidth = sourceWidth; - uint32_t imgHeight = sourceHeight; - - // Determine suitable image width - if (requestedWidth) { - imgWidth = requestedWidth; - - if (!requestedHeight) - imgHeight = ((double)imgWidth / sourceAspectRatio); - } - - // Determine suitable image height - if (requestedHeight) { - imgHeight = requestedHeight; - - if (!requestedWidth) - imgWidth = ((double)imgHeight * sourceAspectRatio); - } - - // Create final image texture - QImage ret(imgWidth, imgHeight, QImage::Format::Format_RGBA8888); - ret.fill(0); - - // Video image buffer - uint8_t *videoData = nullptr; - uint32_t videoLinesize = 0; - - // Enter graphics context - obs_enter_graphics(); - - gs_texrender_t *texRender = gs_texrender_create(GS_RGBA, GS_ZS_NONE); - gs_stagesurf_t *stageSurface = gs_stagesurface_create(imgWidth, imgHeight, GS_RGBA); - - success = false; - gs_texrender_reset(texRender); - if (gs_texrender_begin(texRender, imgWidth, imgHeight)) { - vec4 background; - vec4_zero(&background); - - gs_clear(GS_CLEAR_COLOR, &background, 0.0f, 0); - gs_ortho(0.0f, (float)sourceWidth, 0.0f, (float)sourceHeight, -100.0f, 100.0f); - - gs_blend_state_push(); - gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); - - obs_source_inc_showing(source); - obs_source_video_render(source); - obs_source_dec_showing(source); - - gs_blend_state_pop(); - gs_texrender_end(texRender); - - gs_stage_texture(stageSurface, gs_texrender_get_texture(texRender)); - if (gs_stagesurface_map(stageSurface, &videoData, &videoLinesize)) { - int lineSize = ret.bytesPerLine(); - for (uint y = 0; y < imgHeight; y++) { - memcpy(ret.scanLine(y), videoData + (y * videoLinesize), lineSize); - } - gs_stagesurface_unmap(stageSurface); - success = true; - } - } - - gs_stagesurface_destroy(stageSurface); - gs_texrender_destroy(texRender); - - obs_leave_graphics(); - - return ret; -} +#include "../utils/SourceScreenshot.h" bool IsImageFormatValid(std::string format) { @@ -214,7 +137,7 @@ RequestResult RequestHandler::GetSourceScreenshot(const Request &request) } bool success; - QImage renderedImage = TakeSourceScreenshot(source, success, requestedWidth, requestedHeight); + QImage renderedImage = Utils::TakeSourceScreenshot(source, success, requestedWidth, requestedHeight); if (!success) return RequestResult::Error(RequestStatus::RequestProcessingFailed, "Failed to render screenshot."); @@ -309,7 +232,7 @@ RequestResult RequestHandler::SaveSourceScreenshot(const Request &request) } bool success; - QImage renderedImage = TakeSourceScreenshot(source, success, requestedWidth, requestedHeight); + QImage renderedImage = Utils::TakeSourceScreenshot(source, success, requestedWidth, requestedHeight); if (!success) return RequestResult::Error(RequestStatus::RequestProcessingFailed, "Failed to render screenshot."); diff --git a/src/utils/SourceScreenshot.cpp b/src/utils/SourceScreenshot.cpp new file mode 100644 index 00000000..ed6a9bad --- /dev/null +++ b/src/utils/SourceScreenshot.cpp @@ -0,0 +1,100 @@ +/* +obs-websocket +Copyright (C) 2016-2021 Stephane Lepin +Copyright (C) 2020-2021 Kyle Manning + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program. If not, see +*/ + +#include + +#include "SourceScreenshot.h" + +QImage Utils::TakeSourceScreenshot(obs_source_t *source, bool &success, uint32_t requestedWidth, uint32_t requestedHeight) +{ + // Get info about the requested source + const uint32_t sourceWidth = obs_source_get_width(source); + const uint32_t sourceHeight = obs_source_get_height(source); + const double sourceAspectRatio = ((double)sourceWidth / (double)sourceHeight); + + uint32_t imgWidth = sourceWidth; + uint32_t imgHeight = sourceHeight; + + // Determine suitable image width + if (requestedWidth) { + imgWidth = requestedWidth; + + if (!requestedHeight) + imgHeight = ((double)imgWidth / sourceAspectRatio); + } + + // Determine suitable image height + if (requestedHeight) { + imgHeight = requestedHeight; + + if (!requestedWidth) + imgWidth = ((double)imgHeight * sourceAspectRatio); + } + + // Create final image texture + QImage ret(imgWidth, imgHeight, QImage::Format::Format_RGBA8888); + ret.fill(0); + + // Video image buffer + uint8_t *videoData = nullptr; + uint32_t videoLinesize = 0; + + // Enter graphics context + obs_enter_graphics(); + + gs_texrender_t *texRender = gs_texrender_create(GS_RGBA, GS_ZS_NONE); + gs_stagesurf_t *stageSurface = gs_stagesurface_create(imgWidth, imgHeight, GS_RGBA); + + success = false; + gs_texrender_reset(texRender); + if (gs_texrender_begin(texRender, imgWidth, imgHeight)) { + vec4 background; + vec4_zero(&background); + + gs_clear(GS_CLEAR_COLOR, &background, 0.0f, 0); + gs_ortho(0.0f, (float)sourceWidth, 0.0f, (float)sourceHeight, -100.0f, 100.0f); + + gs_blend_state_push(); + gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + + obs_source_inc_showing(source); + obs_source_video_render(source); + obs_source_dec_showing(source); + + gs_blend_state_pop(); + gs_texrender_end(texRender); + + gs_stage_texture(stageSurface, gs_texrender_get_texture(texRender)); + if (gs_stagesurface_map(stageSurface, &videoData, &videoLinesize)) { + int lineSize = ret.bytesPerLine(); + for (uint y = 0; y < imgHeight; y++) { + memcpy(ret.scanLine(y), videoData + (y * videoLinesize), lineSize); + } + gs_stagesurface_unmap(stageSurface); + success = true; + } + } + + gs_stagesurface_destroy(stageSurface); + gs_texrender_destroy(texRender); + + obs_leave_graphics(); + + return ret; +} diff --git a/src/utils/SourceScreenshot.h b/src/utils/SourceScreenshot.h new file mode 100644 index 00000000..a275bb95 --- /dev/null +++ b/src/utils/SourceScreenshot.h @@ -0,0 +1,29 @@ +/* +obs-websocket +Copyright (C) 2016-2021 Stephane Lepin +Copyright (C) 2020-2021 Kyle Manning + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program. If not, see +*/ + +#pragma once + +#include +#include + +namespace Utils { + // Renders the source on its own into an 8-bit RGBA image of the requested size (0: the + // source's own width or height, keeping its aspect ratio). Enters the graphics context. + QImage TakeSourceScreenshot(obs_source_t *source, bool &success, uint32_t requestedWidth = 0, uint32_t requestedHeight = 0); +} From f163a12c9d764509a18fce4a7d005d6dc8cb44b0 Mon Sep 17 00:00:00 2001 From: Ryan Doupe Date: Fri, 16 Oct 2026 20:10:12 -0400 Subject: [PATCH 2/2] utils: Encode high-bit-depth sources to sRGB in screenshots GetSourceScreenshot and SaveSourceScreenshot of a 10- or 12-bit source (P010, I010, I210, I412: HEVC Main10, VP9 profile 2, ProRes, 10-bit capture cards) came out too dark: orange (255, 96, 0) as (255, 30, 0), while the program output, the previews and recordings showed it right. libobs keeps these frames as linear float, and TakeSourceScreenshot() drew the source with linear sRGB off, so the linear values went into the 8-bit image unencoded. The source is now drawn with linear sRGB on when its colour space is not 8-bit sRGB, as scenes and the frontend previews draw it, so libobs encodes the colours to sRGB. 8-bit sources keep the direct path: their pixels and their straight (not premultiplied) alpha are unchanged. Refs obs-rust/obs-studio#1141. Co-Authored-By: Claude Opus 5.5 --- src/utils/SourceScreenshot.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/utils/SourceScreenshot.cpp b/src/utils/SourceScreenshot.cpp index ed6a9bad..80a2cddf 100644 --- a/src/utils/SourceScreenshot.cpp +++ b/src/utils/SourceScreenshot.cpp @@ -73,10 +73,22 @@ QImage Utils::TakeSourceScreenshot(obs_source_t *source, bool &success, uint32_t gs_blend_state_push(); gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO); + // libobs keeps high-bit-depth sources (P010, I010, I210, I412: HEVC Main10, ProRes, 10-bit + // capture cards) as linear float. Scenes and the frontend previews draw with linear sRGB + // on, which encodes them to sRGB in an 8-bit target; without it the screenshot is stored + // linear and comes out too dark. 8-bit sRGB sources keep the direct path, so their pixels + // and straight alpha stay as they were. + const enum gs_color_space preferredSpaces[] = {GS_CS_SRGB, GS_CS_SRGB_16F, GS_CS_709_EXTENDED}; + const enum gs_color_space sourceSpace = + obs_source_get_color_space(source, OBS_COUNTOF(preferredSpaces), preferredSpaces); + const bool previousLinearSrgb = gs_set_linear_srgb(sourceSpace != GS_CS_SRGB); + obs_source_inc_showing(source); obs_source_video_render(source); obs_source_dec_showing(source); + gs_set_linear_srgb(previousLinearSrgb); + gs_blend_state_pop(); gs_texrender_end(texRender);