Repository navigation
Initialize CEF on the Qt main thread on macOS - #10
Merged
zackees merged 2 commits intoOct 25, 2026
Merged
Conversation
On macOS obs-browser runs CEF on Qt's main loop, and the first browser source initializes CEF from its create callback, on the calling thread. When obs-websocket creates that source, the call comes from its IO thread, CEF's UI thread is one that never pumps, and no browser source renders for the rest of the session (obs-rust/obs-studio#2387). first_source_off_main_thread.py starts the built OBS-Studio-Rust.app with a fresh, seeded configuration and obs-websocket on a test port, and requires a browser source with first-source.html to render: websocket-first (obs-websocket creates it) fails, collection (OBS creates it on the main thread while the scene collection loads; the control) passes. macOS only, local like the other real-CEF tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the Qt loop (macOS), obs_browser_initialize() ran BrowserInit() on the calling thread, and CefInitialize() makes that thread CEF's UI thread. The first browser source initializes CEF from its create callback, so when obs-websocket created it (CreateInput, on its IO thread), CEF's UI thread was one that never pumps: no browser rendered for the rest of the session (obs-rust/obs-studio#2387). obs_browser_initialize() now runs BrowserInit() on the Qt main thread and waits for it (a blocking queued call) when it is called from another thread. On the main thread, and without a Qt application, it calls it directly as before. tests/first_source_off_main_thread.py: websocket-first renders now, and the collection control still does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 of 7 tasks
zackees
approved these changes
Oct 25, 2026
zackees
left a comment
Member
There was a problem hiding this comment.
Coordinator review: approved.
- The cause and the fix match: with
ENABLE_BROWSER_QT_LOOP,CefInitialize()makes the calling thread CEF's UI thread, so the first source created from obs-websocket's IO thread left CEF on a thread that never pumps. HandingBrowserInit()to the Qt main thread when called from elsewhere, and calling it directly on the main thread or without aQCoreApplication, keeps every existing path the same. Linux and Windows are untouched. - RED → GREEN in the real app (macOS 26.7.1, CEF 7871):
websocket-firstwas black and now renders 3 of 3 times, and thecollectioncontrol passes in both. The test refuses to touch an existing config folder, which is the right call for a test that seeds~/Library/Application Support.
Note (non-blocking): BlockingQueuedConnection deadlocks if the main thread is waiting on the calling thread at that moment, or isn't running its event loop. Today obs_source_create calls create without an OBS-global lock, and obs-websocket's request thread isn't something the UI thread waits on, so I don't see a path. A first browser source created off the main thread during shutdown (after the Qt loop has stopped) would hang, though. A guard like if (QCoreApplication::closingDown()) return false; before the blocking call would cover that.
vpacher
added a commit
to vpacher/obs-studio
that referenced
this pull request
Oct 25, 2026
…hread) Moves plugins/obs-browser from the head of obs-rust/obs-browser#9 (e513629, the same tree as its merge 1412f26) to master 9aa4a60, which adds obs-rust/obs-browser#10: with the Qt loop (macOS), obs_browser_initialize() runs BrowserInit() on the Qt main thread when it is called from another thread, so a first browser source created by obs-websocket renders (obs-rust#2387). 9aa4a60 has the tree of the branch tested on macOS 26.7.1 (first source through obs-websocket renders 3 of 3, the scene-collection control too). docs/features.md gets the obs-rust#2387 line (fixed upstream bugs, sources) and the PR links for both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 25, 2026
zackees
pushed a commit
that referenced
this pull request
Oct 25, 2026
* test: RED hand-over to the Qt main thread while OBS quits obs_browser_initialize() hands BrowserInit() to the Qt main thread with a blocking queued call when it is called from another thread (#10). Once Qt's main loop has finished, nothing runs that call, so a first browser source created off the main thread while OBS quits would hang it (obs-rust/obs-studio#2391, from the review of #10). The hand-over moves unchanged into RunOnQtMainThread() (browser-qt-main-thread.hpp), with WatchQtMainLoop() called from obs_module_load() (a no-op for now). qt-main-thread.cpp, run by qt_main_thread.py against Qt Core only: while the loop runs, a worker thread's call runs on the main thread (passes); after the loop has finished, the call must return within 2 s without running (fails: it waits forever). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Do not hand over to the Qt main thread once its loop has stopped RunOnQtMainThread() waited on a blocking queued call; once Qt's main loop has finished (OBS quits), nothing runs it, so a first browser source created off the main thread then hung OBS on quit (obs-rust/obs-studio#2391, from the review of #10). WatchQtMainLoop(), called from obs_module_load() on the main thread, sets a flag on QCoreApplication::aboutToQuit. RunOnQtMainThread() no longer hands over when that flag or QCoreApplication::closingDown() is set: it queues the call without blocking and waits for it in 50 ms steps, and if the loop stops before the call has started, it cancels the call (a queued call that runs later does nothing) and returns false. The caller then leaves CEF uninitialized. tests/qt_main_thread.py: both cases pass, 20 of 20 runs. tests/first_source_off_main_thread.py on macOS: both scenarios render. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
MCPVOT
pushed a commit
to MCPVOT/obs-studio
that referenced
this pull request
Oct 25, 2026
…thread (obs-rust#2386, obs-rust#2387) (obs-rust#2390) * ci: RED the macOS browser helpers follow the app identity (obs-rust#2386) Chromium finds its helper apps by the main executable's name ("<app> Helper", "(GPU)", "(Plugin)", "(Renderer)"). obs-browser names them "OBS Helper" while the Rust brand's app is "OBS-Studio-Rust", so the GPU process fails to launch (error_code=1003) and adding a browser source crashes OBS. test_macos_browser_helpers.py checks that obs-browser's macOS CMake resolves the helpers to "<OBS_MACOS_BUNDLE_NAME> Helper" and "<OBS_MACOS_BUNDLE_ID>.helper<suffix>" for both brands (upstream keeps "OBS Helper" and com.obsproject.obs-studio.helper), and that app_identity_check.py's bundle check, which the release workflow runs on every macOS app, rejects helpers named otherwise. Two of four fail: the pinned obs-browser hard-codes the upstream names, and the bundle check does not look at the helpers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * macos: Name the browser helpers after the app (obs-rust#2386) app_identity_check.py's bundle check, which the release workflow runs on every macOS app, now checks the browser source's helper apps: when the app has any, they must be "<CFBundleExecutable> Helper", "(GPU)", "(Plugin)" and "(Renderer)", with the bundle IDs "<app id>.helper", ".gpu", ".plugin" and ".renderer". The 2026-10-23 release fails it with its "OBS Helper" apps. plugins/obs-browser moves to obs-rust/obs-browser e513629, which names the helpers and their bundle IDs after obs-studio's app identity (OBS_MACOS_BUNDLE_NAME, OBS_MACOS_BUNDLE_ID). With it, Chromium finds the GPU helper and adding a browser source no longer crashes OBS on macOS. The source still stays black until obs-rust#2387 is fixed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci: Skip the obs-browser source checks without the submodule (obs-rust#2386) The Format job checks out no submodules, so test_macos_browser_helpers.py failed there with FileNotFoundError on plugins/obs-browser/cmake/ os-macos.cmake. The two checks that read that file now skip with the reason when the submodule is not checked out. The two bundle checks still run there, and the release workflow runs app_identity_check.py on every built macOS app. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * obs-browser: Update to 9aa4a60 (macOS helper names, CEF on the main thread) Moves plugins/obs-browser from the head of obs-rust/obs-browser#9 (e513629, the same tree as its merge 1412f26) to master 9aa4a60, which adds obs-rust/obs-browser#10: with the Qt loop (macOS), obs_browser_initialize() runs BrowserInit() on the Qt main thread when it is called from another thread, so a first browser source created by obs-websocket renders (obs-rust#2387). 9aa4a60 has the tree of the branch tested on macOS 26.7.1 (first source through obs-websocket renders 3 of 3, the scene-collection control too). docs/features.md gets the obs-rust#2387 line (fixed upstream bugs, sources) and the PR links for both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs obs-rust/obs-studio#2387
What and why
On macOS obs-browser runs CEF on Qt's main loop (
ENABLE_BROWSER_QT_LOOP), andobs_browser_initialize()ranBrowserInit()on the calling thread:CefInitialize()makes that thread CEF's UI thread. The first browser source initializes CEF from itscreatecallback, so when obs-websocket created it (CreateInput, on its IO thread), CEF's UI thread was one that never pumps. Only the GPU process started, and no browser source rendered for the rest of the session, also ones recreated in the UI later.obs_browser_initialize()now runsBrowserInit()on the Qt main thread and waits for it (QMetaObject::invokeMethod(..., Qt::BlockingQueuedConnection)) when it is called from another thread. On the main thread, and without a Qt application, it calls it directly as before. Linux and Windows (their own CEF thread) are unchanged.RED → GREEN
tests/first_source_off_main_thread.py(new, macOS, local like the other real-CEF tests) starts the builtOBS-Studio-Rust.appwith a fresh, seeded configuration and obs-websocket on a test port, and requires a browser source withfirst-source.html(a page that turns green) to render. Scenarios:websocket-first(obs-websocket creates the first source) andcollection(the scene collection has it, so OBS creates it on the main thread; the control).0a4106f:collectionrenders,websocket-firststays black for 30 s.bfeba2d: both render, in 3 of 3 runs.collectionscenario; a source added in the UI goes the same way.AI note: Claude Code (Claude Opus 5.5) wrote the change, the test and this text under the submitter's direction, and ran the tests above on the submitter's machine.