Skip to content

obs-websocket: Encode 10-bit sources to sRGB in screenshots - #1146

Merged
zackees merged 2 commits into
obs-rust:masterfrom
rdoupe:fix/ws-screenshot-srgb
Oct 17, 2026
Merged

zackees merged 2 commits into
obs-rust:masterfrom
rdoupe:fix/ws-screenshot-srgb

Conversation

@rdoupe

@rdoupe rdoupe commented Oct 17, 2026

Copy link
Copy Markdown
Collaborator

Fixes #1141

Merge after #1145 (the Session Duration submodule bump) (same obs-websocket master, ws#15 comes before ws#16 there), so the submodule only moves forward.

What and why

obs-websocket's GetSourceScreenshot and SaveSourceScreenshot of a 10- or 12-bit source now show the colours the program shows, instead of a much darker picture.

  • TakeSourceScreenshot() (plugins/obs-websocket/src/requesthandler/RequestHandler_Sources.cpp) rendered the source on its own into an 8-bit GS_RGBA texrender with linear sRGB off. libobs keeps I010/P010/I210/I412 frames (HEVC Main10, VP9 profile 2, ProRes, 10-bit capture cards) as a linear float texture (need_linear_output(), GS_CS_SRGB_16F). It only turns on the framebuffer's sRGB encode while linear sRGB is on, so the linear values went into the image unencoded: orange (255, 96, 0) became (255, 30, 0), mid grey 128 became 55. Scenes (render_item() in libobs/obs-scene.c) and the Properties/Filters/Interaction previews draw with gs_set_linear_srgb(true), so the program, the preview, scene screenshots and recordings were right.
  • The fork fix, utils: Encode high-bit-depth sources to sRGB in screenshots obs-websocket#16, moves the function unchanged into src/utils/SourceScreenshot.cpp (Utils::TakeSourceScreenshot()). It then draws the source with linear sRGB on when its colour space (obs_source_get_color_space() with SRGB, SRGB_16F and 709_EXTENDED preferred, the list transitions use) is not 8-bit sRGB. This PR moves the submodule to it.
  • 8-bit sources keep the direct path, so their screenshots don't change (byte-identical in the product check below). Turning linear sRGB on for them too would make libobs premultiply semi-transparent 8-bit async sources (DrawNonlinearAlpha), and the PNG would get darker edges. HDR (PQ/HLG) async sources were already tone mapped with linear sRGB forced on by libobs, so nothing changes for them.
  • Found by our release check (FFmpeg 8.1.3 and libvpx 1.17.0 validation, checks 61 and 90: ProRes, HEVC Main10 and VP9 profile 2 files looked wrong in the source screenshot only). No upstream report. The screenshot issues upstream are other bugs: Bug: "Save/GetSourceScreenshot" (Doesn't Match OBS Behaviour) obsproject/obs-websocket#1213 is about cropping, Bug: GetSourceScreenshot returns image of preview and not program when both are the same scene but preview has changes obsproject/obs-websocket#1257 preview vs program, Bug: GetSourceScreenshot not giving correct MIME type for imageFormat: jpg obsproject/obs-websocket#1298 the MIME type.

Reproduced in release v0.1.0-beta.20261014 (AppImage, headless: private Xvfb, private HOME, dbus-run-session, --multi, loopback-only network namespace). A Media Source is the only item of the program scene, playing ordinary files made by ffmpeg from one pattern (below the top strip: left half (255, 96, 0), right half (0, 96, 255)). Left patch of GetSourceScreenshot of the source / of the scene / a recording frame:

File v0.1.0-beta.20261014 This branch, C libobs lane This branch, Rust libobs lane
ProRes 422 HQ 10-bit (255, 30, 0) / (255, 96, 0) / (255, 95, 0) (255, 96, 0) / (255, 96, 0) / (255, 95, 0) (255, 96, 0) / (255, 96, 0) / (255, 95, 0)
HEVC Main10 4:2:0 (254, 30, 0) / (255, 96, 0) / (255, 95, 0) (255, 96, 0) / (255, 96, 0) / (255, 95, 0) (255, 96, 0) / (255, 96, 0) / (255, 95, 0)
ProRes 4444 10-bit, alpha (255, 30, 0) / (255, 96, 0) / (255, 95, 0), alpha 254/0 (255, 96, 0) / (255, 96, 0) / (255, 95, 0), alpha 254/0 (255, 96, 0) / (255, 96, 0) / (255, 95, 0), alpha 254/0
H.264 8-bit (control) (255, 96, 1) / (255, 96, 1) / (255, 95, 0) the same the same
VP9 alpha 8-bit (control) (255, 106, 0) / (255, 106, 0) / (255, 105, 0), alpha 255/0 the same the same

The two 8-bit source screenshots are byte-identical to the release's in both lanes (all 48000 pixels below the pattern's changing strip).

RED → GREEN

  • RED commit: 0e0dbda test/cmocka: Add failing test for websocket screenshots of 10-bit sources. It moves the submodule from master's pin f06b040 to e87c6e1, the fork PR's first commit, which moves TakeSourceScreenshot() into Utils::TakeSourceScreenshot() unchanged. test_ws_screenshot_srgb builds that file against libobs and the built OpenGL module (GRAPHICS_MODULE_PATH), with Qt Gui for QImage. It shows a flat full-range BT.709 colour from a test async source as the only item of the program scene (BGRA output), and checks the screenshot's centre pixel against the expected sRGB colour and the program output. It fails 4 of 6 cases in both lanes: p010_grey_is_srgb (55 instead of 128), p010_orange_is_srgb, i010_orange_is_srgb and i210_orange_is_srgb ((255, 30, 0) instead of (255, 96, 0), the program (255, 96, 0)). The controls nv12_orange_is_unchanged and bgra_with_alpha_keeps_straight_colour (200, 100, 50 at alpha 128 stays as sent) pass. Linux only, like the other tests that load the OpenGL module; the cases skip without a display (checked with DISPLAY unset).
  • GREEN: 1ce9f47 moves plugins/obs-websocket from e87c6e1 to d79d2c2, the fix (d79d2c2 once utils: Encode high-bit-depth sources to sRGB in screenshots obs-websocket#16 merges; forward only). 6 of 6 pass with ENABLE_RUST_LIBOBS OFF and ON. Full OBS build (frontend + obs-websocket) in both lanes. Full ctest -j8 (private Xvfb, dbus-run-session, system libobs-opengl masked, TMPDIR unset): 275/275 in both lanes, and test_ws_screenshot_srgb ran all 6 cases there. Every PipeWire/PulseAudio test ran. Skipped only open_crashes_without_obs_core (both lanes) and, in the Rust lane, test_startup_alloc_stable: its one case skips when one of its 100 obs_startup() calls fails under load, and it passed on a rerun alone. clang-format 22.1.3, gersemi and ci/test_registration.py clean.

Checklist

  • Base is master
  • I claimed the issue, or no one else has an open PR for it
  • Characterization test (if the unit had none), then RED, then GREEN commits
  • (Optional) Local gate passed and the pushed head is stamped; otherwise PR CI runs the full suite
  • Warning or flag changes (-W..., ratchets): none
  • Intentional differences from C listed in the parity test header and rust/README.md (ports): not a port

AI note: Claude Code (Claude Opus 5.5) wrote the code, the test, the commit messages and this text under the submitter's direction, and ran the tests above on the submitter's machines.

🤖 Generated with Claude Code

@rdoupe
rdoupe requested a review from zackees as a code owner October 17, 2026 01:48
@zackees
zackees added this pull request to the merge queue Oct 17, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 17, 2026
rdoupe added a commit to rdoupe/obs-studio that referenced this pull request Oct 17, 2026
Moves the submodule to the fork fix "forms: Show session durations of a
minute and longer", and on to d79d2c2, the same commit obs-rust#1146 uses, so
the two PRs agree whichever merges first (d79d2c2 adds only the
screenshot fix for obs-rust#1141 on top). The Session Duration column of the obs-websocket
settings dialog went empty after 59 seconds because QTime(0, 0, s)
rejects 60 seconds and more (upstream obsproject/obs-websocket#1208,
reported by bryce-seifert; DanielLester83 found the QTime limit in
obsproject/obs-websocket#1179). FormatSessionDuration() now builds
hh:mm:ss from the seconds count, with the hours counting on past 24 and
a clock set back showing 00:00:00.

test_ws_session_duration passes 5 of 5.

Fixes obs-rust#1116.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zackees
zackees added this pull request to the merge queue Oct 17, 2026
rdoupe and others added 2 commits October 17, 2026 01:21
…rces

GetSourceScreenshot and SaveSourceScreenshot of a 10-bit source (P010,
I010, I210: HEVC Main10, ProRes, 10-bit capture cards) come out too
dark: orange (255, 96, 0) as (255, 30, 0), mid grey 128 as 55, while
the program output shows the right colours. libobs keeps these frames
as linear float, and the screenshot draws the source into an 8-bit
target without the sRGB encode that scenes and the previews use.

Moves the submodule to the fork commit "utils: Move the source
screenshot render into a helper", which moves TakeSourceScreenshot()
unchanged into utils/SourceScreenshot.cpp. test_ws_screenshot_srgb
builds that file against libobs and the OpenGL module, shows a flat
colour as the program scene, and compares the screenshot with the
expected sRGB colour and the program output. P010 grey, P010 orange,
I010 orange and I210 orange fail; the 8-bit NV12 and half-transparent
BGRA controls pass.

Refs obs-rust#1141.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moves the submodule to the fork fix "utils: Encode high-bit-depth
sources to sRGB in screenshots". GetSourceScreenshot and
SaveSourceScreenshot of a 10- or 12-bit source came out too dark
because the source was drawn into the 8-bit image with linear sRGB
off. It is now drawn with linear sRGB on when its colour space is not
8-bit sRGB, as scenes and the previews draw it; 8-bit sources are
unchanged.

test_ws_screenshot_srgb passes 6 of 6.

Fixes obs-rust#1141.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zackees

zackees commented Oct 17, 2026

Copy link
Copy Markdown
Member

Maintainer push: rebased onto master after #1145 merged. GitHub's own rebase reported a conflict; a local rebase was clean. The two commits are unchanged, now d84fd8267 and 5ac5bcb4a. #1145 merged in its earlier form, so master's plugins/obs-websocket is 8bca4ac. This PR moves it forward to d79d2c2 (adds obs-websocket#16), so the pointer only moves forward. I'll queue it once CI is green.

@zackees
zackees force-pushed the fix/ws-screenshot-srgb branch from 1ce9f47 to 5ac5bcb Compare October 17, 2026 08:21
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 17, 2026
@zackees
zackees added this pull request to the merge queue Oct 17, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 17, 2026
@zackees
zackees added this pull request to the merge queue Oct 17, 2026
Merged via the queue into obs-rust:master with commit 6681cf6 Oct 17, 2026
25 of 26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: obs-websocket: GetSourceScreenshot/SaveSourceScreenshot of a 10-bit source (HEVC Main10, ProRes, P010) comes out too dark

2 participants