From 86d9b2f146b3b01ff550ed8523eec9835ab316cf Mon Sep 17 00:00:00 2001 From: Vladimir Sumarov Date: Mon, 8 Jun 2026 13:29:29 -0700 Subject: [PATCH 1/4] obs-browser: re-check validity under graphics lock in OnAcceleratedPaint Bump plugins/obs-browser to 5b30996 to pull in the OnAcceleratedPaint teardown-race guard (streamlabs/obs-browser#54), which fixes the libcef crash on BrowserManagerThread seen in dual-output sessions with browser sources. Branched from 31.1.2sl19. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/obs-browser | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/obs-browser b/plugins/obs-browser index 2660d9b8243564..5b309964a4a18b 160000 --- a/plugins/obs-browser +++ b/plugins/obs-browser @@ -1 +1 @@ -Subproject commit 2660d9b82435648ee28d90735df4c5740324b547 +Subproject commit 5b309964a4a18b3546c6c70365011bd7f48c06f4 From 1424e94079a3c7ced5dd51c8a7238c5b14bd4885 Mon Sep 17 00:00:00 2001 From: Vladimir Sumarov Date: Mon, 8 Jun 2026 18:34:39 -0700 Subject: [PATCH 2/4] obs-browser: bump for GPU-process crash-limit flag Bump plugins/obs-browser 5b30996 -> 9d18953 to add --disable-gpu-process-crash-limit (streamlabs/obs-browser#54), so a transient GPU reset (TDR) no longer escalates to Chromium's fatal 'GPU process isn't usable' abort. Combines with the OnAcceleratedPaint teardown guard already in this bump. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/obs-browser | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/obs-browser b/plugins/obs-browser index 5b309964a4a18b..9d189536447f26 160000 --- a/plugins/obs-browser +++ b/plugins/obs-browser @@ -1 +1 @@ -Subproject commit 5b309964a4a18b3546c6c70365011bd7f48c06f4 +Subproject commit 9d189536447f26bbe085516146a0c6ef16a37a62 From 87ab5ba16066ee5ae714350e0b6e7f87791e720f Mon Sep 17 00:00:00 2001 From: Vladimir Sumarov Date: Tue, 9 Jun 2026 13:47:37 -0700 Subject: [PATCH 3/4] obs-browser: re-point submodule to merged #54 squash commit Bump plugins/obs-browser 9d18953 -> 4002dac, the squash-merge of streamlabs/obs-browser#54 on streamlabs (GPU-process crash-limit flag + OnAcceleratedPaint teardown guard). Replaces the pre-merge PR-branch pointer. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/obs-browser | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/obs-browser b/plugins/obs-browser index 9d189536447f26..4002dacafd564d 160000 --- a/plugins/obs-browser +++ b/plugins/obs-browser @@ -1 +1 @@ -Subproject commit 9d189536447f26bbe085516146a0c6ef16a37a62 +Subproject commit 4002dacafd564daba96b9c097169d8d86eb08129 From 17e896e16d0d9addd78451bcae020ce1ecc401ee Mon Sep 17 00:00:00 2001 From: Aleksandr Voitenko Date: Tue, 30 Jun 2026 12:45:57 +0100 Subject: [PATCH 4/4] Fix NDI 6 runtime loading on macOS (#747) * libobs-winrt: Guard WGC capture against removed graphics device (#733) * Guard DirectShow filter activation (#740) * libobs: append message/get_messages to obs_source_info (#735) * libobs: bound volmeter plane index against MAX_AV_PLANES (#737) * libobs: mix scene-item audio when item canvas is unset (#738) * libobs: Fix use-after-free of canvas view in audio thread (#736) * Add structured module load failures (#741) * Fix NDI 6 runtime loading on macOS --------- Co-authored-by: Vladimir --- .gitmodules | 2 +- deps/libdshowcapture/src | 2 +- libobs-winrt/winrt-capture.cpp | 18 +++++++- libobs/obs-audio-controls.c | 4 +- libobs/obs-canvas.c | 3 ++ libobs/obs-internal.h | 2 + libobs/obs-module.c | 82 +++++++++++++++++++++++++++++----- libobs/obs-scene.c | 9 ++-- libobs/obs-source.h | 14 +++--- libobs/obs.h | 8 ++++ plugins/obs-ndi | 2 +- 11 files changed, 120 insertions(+), 26 deletions(-) diff --git a/.gitmodules b/.gitmodules index 7c50ea0db206de..33b92489757514 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,7 +13,7 @@ [submodule "plugins/obs-ndi"] path = plugins/obs-ndi url = https://github.com/streamlabs/obs-ndi.git - branch = streamlabs-obs + branch = streamlabs [submodule "plugins/slobs-virtual-cam"] path = plugins/slobs-virtual-cam url = https://github.com/streamlabs/slobs-virtual-cam.git diff --git a/deps/libdshowcapture/src b/deps/libdshowcapture/src index b70b7af87689df..48a97c9242b701 160000 --- a/deps/libdshowcapture/src +++ b/deps/libdshowcapture/src @@ -1 +1 @@ -Subproject commit b70b7af87689dfc0496647703885d744571bd9b3 +Subproject commit 48a97c9242b701241869b1ce4ad9df17e18a3d6d diff --git a/libobs-winrt/winrt-capture.cpp b/libobs-winrt/winrt-capture.cpp index 5545f6735314c9..c431603cf948db 100644 --- a/libobs-winrt/winrt-capture.cpp +++ b/libobs-winrt/winrt-capture.cpp @@ -301,13 +301,22 @@ static void winrt_capture_device_loss_rebuild(void *device_void, void *data) return; ID3D11Device *const d3d_device = (ID3D11Device *)device_void; + if (!d3d_device || FAILED(d3d_device->GetDeviceRemovedReason())) { + blog(LOG_WARNING, "Skipping WinRT capture rebuild; graphics device unavailable or removed"); + return; + } + ComPtr dxgi_device; - if (FAILED(d3d_device->QueryInterface(&dxgi_device))) + if (FAILED(d3d_device->QueryInterface(&dxgi_device))) { blog(LOG_ERROR, "Failed to get DXGI device"); + return; + } winrt::com_ptr inspectable; - if (FAILED(CreateDirect3D11DeviceFromDXGIDevice(dxgi_device.Get(), inspectable.put()))) + if (FAILED(CreateDirect3D11DeviceFromDXGIDevice(dxgi_device.Get(), inspectable.put()))) { blog(LOG_ERROR, "Failed to get WinRT device"); + return; + } const winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice device = inspectable.as(); @@ -349,6 +358,11 @@ static struct winrt_capture *winrt_capture_init_internal(BOOL cursor, HWND windo HMONITOR monitor) try { ID3D11Device *const d3d_device = (ID3D11Device *)gs_get_device_obj(); + if (!d3d_device || FAILED(d3d_device->GetDeviceRemovedReason())) { + blog(LOG_WARNING, "Skipping WinRT capture init; graphics device unavailable or removed"); + return nullptr; + } + ComPtr dxgi_device; HRESULT hr = d3d_device->QueryInterface(&dxgi_device); diff --git a/libobs/obs-audio-controls.c b/libobs/obs-audio-controls.c index 5cdc7e195b2d65..b571f1dfe03146 100644 --- a/libobs/obs-audio-controls.c +++ b/libobs/obs-audio-controls.c @@ -418,7 +418,7 @@ static void volmeter_process_peak(obs_volmeter_t *volmeter, const struct audio_d { int nr_samples = data->frames; int channel_nr = 0; - for (int plane_nr = 0; channel_nr < nr_channels; plane_nr++) { + for (int plane_nr = 0; channel_nr < nr_channels && plane_nr < MAX_AV_PLANES; plane_nr++) { float *samples = (float *)data->data[plane_nr]; if (!samples) { continue; @@ -466,7 +466,7 @@ static void volmeter_process_magnitude(obs_volmeter_t *volmeter, const struct au size_t nr_samples = data->frames; int channel_nr = 0; - for (int plane_nr = 0; channel_nr < nr_channels; plane_nr++) { + for (int plane_nr = 0; channel_nr < nr_channels && plane_nr < MAX_AV_PLANES; plane_nr++) { float *samples = (float *)data->data[plane_nr]; if (!samples) { continue; diff --git a/libobs/obs-canvas.c b/libobs/obs-canvas.c index 6bbfc3be529b5f..fc81bc5b6a4b52 100644 --- a/libobs/obs-canvas.c +++ b/libobs/obs-canvas.c @@ -228,6 +228,9 @@ void obs_canvas_destroy(obs_canvas_t *canvas) pthread_mutex_destroy(&canvas->sources_mutex); obs_context_data_free(&canvas->context); + + /* Null mixes referencing this view before freeing it; audio_callback() reads it via mix->view. */ + obs_view_remove(&canvas->view); obs_view_free(&canvas->view); bfree(canvas); } diff --git a/libobs/obs-internal.h b/libobs/obs-internal.h index 7cd241864859e5..4788227eecb967 100644 --- a/libobs/obs-internal.h +++ b/libobs/obs-internal.h @@ -113,6 +113,8 @@ struct obs_module { char *data_path; void *module; bool loaded; + char *load_error_code; + char *load_error_message; bool (*load)(void); void (*unload)(void); diff --git a/libobs/obs-module.c b/libobs/obs-module.c index bb7482d99f6863..43a8fd603ac209 100644 --- a/libobs/obs-module.c +++ b/libobs/obs-module.c @@ -156,6 +156,18 @@ int obs_open_module(obs_module_t **module, const char *path, const char *data_pa return MODULE_SUCCESS; } +static void clear_module_load_error(obs_module_t *module) +{ + if (!module) + return; + + bfree(module->load_error_code); + module->load_error_code = NULL; + + bfree(module->load_error_message); + module->load_error_message = NULL; +} + bool obs_init_module(obs_module_t *module) { if (!module || !obs) @@ -167,14 +179,34 @@ bool obs_init_module(obs_module_t *module) profile_store_name(obs_get_profiler_name_store(), "obs_init_module(%s)", module->file); profile_start(profile_name); + clear_module_load_error(module); module->loaded = module->load(); - if (!module->loaded) - blog(LOG_WARNING, "Failed to initialize module '%s'", module->file); + if (!module->loaded) { + if (module->load_error_code && module->load_error_message) { + blog(LOG_WARNING, "Failed to initialize module '%s': %s (%s)", module->file, + module->load_error_message, module->load_error_code); + } else if (module->load_error_code) { + blog(LOG_WARNING, "Failed to initialize module '%s': %s", module->file, + module->load_error_code); + } else { + blog(LOG_WARNING, "Failed to initialize module '%s'", module->file); + } + } profile_end(profile_name); return module->loaded; } +void obs_module_set_load_error(obs_module_t *module, const char *code, const char *message) +{ + if (!module) + return; + + clear_module_load_error(module); + module->load_error_code = bstrdup(code ? code : ""); + module->load_error_message = bstrdup(message ? message : ""); +} + void obs_log_loaded_modules(void) { blog(LOG_INFO, " Loaded Modules:"); @@ -294,9 +326,25 @@ extern void get_plugin_info(const char *path, bool *is_obs_plugin, bool *can_loa struct fail_info { struct dstr fail_modules; - size_t fail_count; + DARRAY(struct obs_module_load_failure) failures; }; +static void add_module_failure(struct fail_info *fail_info, const char *module, const char *code, const char *message) +{ + if (!fail_info) + return; + + struct obs_module_load_failure failure = { + bstrdup(module ? module : ""), + bstrdup((code && *code) ? code : "MODULE_LOAD_FAILED"), + bstrdup((message && *message) ? message : "Module failed to load."), + }; + + dstr_cat(&fail_info->fail_modules, failure.module); + dstr_cat(&fail_info->fail_modules, ";"); + da_push_back(fail_info->failures, &failure); +} + static bool is_safe_module(const char *name) { if (!obs->safe_modules.num) @@ -356,18 +404,16 @@ static void load_all_callback(void *param, const struct obs_module_info2 *info) return; } - if (!obs_init_module(module)) + if (!obs_init_module(module)) { + add_module_failure(fail_info, info->name, module->load_error_code, module->load_error_message); free_module(module); + } UNUSED_PARAMETER(param); return; load_failure: - if (fail_info) { - dstr_cat(&fail_info->fail_modules, info->name); - dstr_cat(&fail_info->fail_modules, ";"); - fail_info->fail_count++; - } + add_module_failure(fail_info, info->name, NULL, NULL); } static const char *obs_load_all_modules_name = "obs_load_all_modules"; @@ -403,7 +449,8 @@ void obs_load_all_modules2(struct obs_module_failure_info *mfi) #endif profile_end(obs_load_all_modules2_name); - mfi->count = fail_info.fail_count; + mfi->count = fail_info.failures.num; + mfi->failures = fail_info.failures.array; mfi->failed_modules = strlist_split(fail_info.fail_modules.array, ';', false); dstr_free(&fail_info.fail_modules); } @@ -414,6 +461,19 @@ void obs_module_failure_info_free(struct obs_module_failure_info *mfi) bfree(mfi->failed_modules); mfi->failed_modules = NULL; } + + if (mfi->failures) { + for (size_t i = 0; i < mfi->count; i++) { + bfree(mfi->failures[i].module); + bfree(mfi->failures[i].code); + bfree(mfi->failures[i].message); + } + + bfree(mfi->failures); + mfi->failures = NULL; + } + + mfi->count = 0; } void obs_post_load_modules(void) @@ -621,6 +681,8 @@ void free_module(struct obs_module *mod) bfree(mod->mod_name); bfree(mod->bin_path); bfree(mod->data_path); + bfree(mod->load_error_code); + bfree(mod->load_error_message); bfree(mod); } diff --git a/libobs/obs-scene.c b/libobs/obs-scene.c index 0d01f89b0c3c11..5a5f371fc7781f 100644 --- a/libobs/obs-scene.c +++ b/libobs/obs-scene.c @@ -2111,9 +2111,12 @@ static bool scene_audio_render_do(void *data, uint64_t *ts_out, for (size_t canvas_idx = 0; canvas_idx < audio_output->outputs.num; canvas_idx++) { - if (item->canvas != - obs->video.canvases - .array[canvas_idx]) { + /* NULL canvas = all canvases, as in + * scene_video_render */ + if (item->canvas && + item->canvas != + obs->video.canvases + .array[canvas_idx]) { continue; } diff --git a/libobs/obs-source.h b/libobs/obs-source.h index a903406f05dbc1..e9a98db02a57e5 100644 --- a/libobs/obs-source.h +++ b/libobs/obs-source.h @@ -301,12 +301,6 @@ struct obs_source_info { /** Called when the source has been activated in the main view */ void (*activate)(void *data); - /** Called to send message to the source */ - void (*message)(void *data, obs_data_t *settings); - - /** Called to get messages from the source */ - obs_data_array_t *(*get_messages)(void *data); - /** * Called when the source has been deactivated from the main view * (no longer being played/displayed) @@ -562,6 +556,14 @@ struct obs_source_info { * @param source Source that the filter is being added to */ void (*filter_add)(void *data, obs_source_t *source); + + /* Append-only: mid-struct inserts shift later offsets and break module ABI. */ + + /** Called to send message to the source */ + void (*message)(void *data, obs_data_t *settings); + + /** Called to get messages from the source */ + obs_data_array_t *(*get_messages)(void *data); }; EXPORT void obs_register_source_s(const struct obs_source_info *info, size_t size); diff --git a/libobs/obs.h b/libobs/obs.h index b17e3077494479..f3f010cd75b93a 100644 --- a/libobs/obs.h +++ b/libobs/obs.h @@ -589,13 +589,21 @@ EXPORT void obs_add_safe_module(const char *name); /** Automatically loads all modules from module paths (convenience function) */ EXPORT void obs_load_all_modules(void); +struct obs_module_load_failure { + char *module; + char *code; + char *message; +}; + struct obs_module_failure_info { char **failed_modules; size_t count; + struct obs_module_load_failure *failures; }; EXPORT void obs_module_failure_info_free(struct obs_module_failure_info *mfi); EXPORT void obs_load_all_modules2(struct obs_module_failure_info *mfi); +EXPORT void obs_module_set_load_error(obs_module_t *module, const char *code, const char *message); /** Notifies modules that all modules have been loaded. This function should * be called after all modules have been loaded. */ diff --git a/plugins/obs-ndi b/plugins/obs-ndi index 5f2f8a8a3e2ffe..d76cf94ebb2c93 160000 --- a/plugins/obs-ndi +++ b/plugins/obs-ndi @@ -1 +1 @@ -Subproject commit 5f2f8a8a3e2ffe4ca54e9e1c965ccb2723eb93be +Subproject commit d76cf94ebb2c93cee511691875c40129e805ce78