summaryrefslogtreecommitdiff
path: root/indra/llwebrtc/llwebrtc.cpp
diff options
context:
space:
mode:
authorErik Kundiman <erik@megapahit.org>2026-09-01 18:35:23 +0800
committerErik Kundiman <erik@megapahit.org>2026-09-04 21:58:53 +0800
commit00bb3bb6f07660bd914d29a1389342bb3060d254 (patch)
tree431f574922494d2201718f13085f17bc6bd7fb42 /indra/llwebrtc/llwebrtc.cpp
parenta8636720129952c10cf49ab80da21bf8b340b96c (diff)
parent4ef9f8f14b35ed388c294d53767a0dca0e890924 (diff)
Merge remote-tracking branch 'secondlife/release/26.4' into 26.4
Diffstat (limited to 'indra/llwebrtc/llwebrtc.cpp')
-rw-r--r--indra/llwebrtc/llwebrtc.cpp154
1 files changed, 108 insertions, 46 deletions
diff --git a/indra/llwebrtc/llwebrtc.cpp b/indra/llwebrtc/llwebrtc.cpp
index 8b77a39541..ce5f487b32 100644
--- a/indra/llwebrtc/llwebrtc.cpp
+++ b/indra/llwebrtc/llwebrtc.cpp
@@ -412,7 +412,7 @@ void LLWebRTCImpl::init()
}
-void LLWebRTCImpl::terminate()
+bool LLWebRTCImpl::terminate()
{
// Run all blocking WebRTC shutdown calls on a separate thread so that a
// hung BlockingCall cannot block the viewer shutdown indefinitely.
@@ -426,16 +426,57 @@ void LLWebRTCImpl::terminate()
std::vector<webrtc::scoped_refptr<LLWebRTCPeerConnectionImpl>> connections;
connections.swap(mPeerConnections);
+ // Explicitely unregister observers before shutting down the threads.
+ // shutdown_thread, if detached, can outlive observer.
+ mWorkerThread->BlockingCall([this]()
+ {
+#if !CM_WEBRTC
+ if (mDeviceModule)
+ {
+ mDeviceModule->SetObserver(nullptr);
+ }
+#endif
+ });
+ mVoiceDevicesObserverList.clear();
+
+ // shutdown_thread can be detached, then LLWebRTCImpl will be nulled out.
+ // Capture what's needed in lambda, don't rely on [this].
std::thread shutdown_thread(
- [this, connections = std::move(connections), done_promise]() mutable
+ [networkThread = std::move(mNetworkThread),
+ workerThread = std::move(mWorkerThread),
+ signalingThread = std::move(mSignalingThread),
+ deviceModule = std::move(mDeviceModule),
+ factory = std::move(mPeerConnectionFactory),
+ connections = std::move(connections),
+ done_promise]() mutable
{
- mWorkerThread->BlockingCall(
- [this]()
+ // Stop the capture/render devices alongside the connection teardown
+ // below rather than ahead of it. Both of these calls end in a
+ // WaitForSingleObject on a WASAPI thread with a 2s timeout apiece
+ // (AudioDeviceWindowsCore::StopRecording / StopPlayout), so blocking on
+ // them here can spend most of the shutdown budget before the
+ // connections have been touched at all -- and after an OS sleep they
+ // tend to hit the full timeout.
+ //
+ // This work has to stay on the worker thread: the device module was
+ // created there and its AudioDeviceBuffer is guarded by a sequence
+ // checker bound to that thread. Posting instead of blocking lets the
+ // signaling close below get on with its network-thread work (data
+ // channel close, transport teardown) while the device stop is still
+ // waiting on WASAPI.
+ //
+ // No explicit join is needed: Thread's task queue is FIFO and
+ // BlockingCall posts through it, so the ForceTerminate call at the end
+ // of this lambda can't run until this task has finished. Any
+ // worker-thread work the signaling close does is likewise ordered
+ // after it, so nothing sees the device module half torn down.
+ workerThread->PostTask(
+ [&deviceModule]()
{
- if (mDeviceModule)
+ if (deviceModule)
{
- mDeviceModule->ForceStopRecording();
- mDeviceModule->StopPlayout();
+ deviceModule->ForceStopRecording();
+ deviceModule->StopPlayout();
}
});
@@ -450,7 +491,7 @@ void LLWebRTCImpl::terminate()
// callback inline, and that callback calls back into the viewer's
// signaling observers. Those observers are only valid until
// llwebrtc::terminate() returns.
- mSignalingThread->BlockingCall(
+ signalingThread->BlockingCall(
[&connections]()
{
for (auto& connection : connections)
@@ -463,22 +504,29 @@ void LLWebRTCImpl::terminate()
});
// Drain anything the closes posted before dropping the factory.
- mSignalingThread->BlockingCall([]() {});
+ signalingThread->BlockingCall([]() {});
- mSignalingThread->BlockingCall([this]() {
- mPeerConnectionFactory = nullptr;
+ signalingThread->BlockingCall([&factory]() {
+ factory = nullptr;
});
- mWorkerThread->BlockingCall(
- [this]()
+ workerThread->BlockingCall(
+ [&deviceModule]()
{
- if (mDeviceModule)
+ if (deviceModule)
{
- mDeviceModule->ForceTerminate();
+ deviceModule->ForceTerminate();
}
- mDeviceModule = nullptr;
+ deviceModule = nullptr;
});
+ // Explicitly clean WebRTC threads in dependency order before signalling completion.
+ // The connections were closed and destroyed on the signaling thread, so it's safe
+ // to clean.
+ signalingThread.reset();
+ workerThread.reset();
+ networkThread.reset();
+
done_promise->set_value();
});
@@ -490,29 +538,29 @@ void LLWebRTCImpl::terminate()
" Detaching — some WebRTC resources will be leaked.";
shutdown_thread.detach();
- // Release the unique_ptrs WITHOUT joining/deleting: the detached thread
- // may still be using these thread objects.
- // The raw pointers are intentionally leaked — the process is exiting anyway
- // and our priority is saving cache and personal data.
- (void)mNetworkThread.release();
- (void)mWorkerThread.release();
- (void)mSignalingThread.release();
-
+ // Leave every member exactly as it is. The detached thread is still
+ // running the lambda above, which reads mSignalingThread, mWorkerThread,
+ // mDeviceModule and mPeerConnectionFactory through `this` -- clearing or
+ // releasing them here would pull them out from under it mid-shutdown
+ // (a null mSignalingThread is an immediate segfault at the next
+ // BlockingCall). Instead we report the failure so the caller leaks this
+ // object rather than deleting it; the process is exiting anyway and our
+ // priority is saving cache and personal data.
+ //
// mPeerConnections is already empty -- the detached thread owns the
// connections now and must be left to finish with them.
+ //
+ // The log sink is unhooked here (and deliberately not deleted, since the
+ // detached thread may still log) because the viewer-side log callback
+ // behind it doesn't outlive this call.
webrtc::LogMessage::RemoveLogToStream(mLogSink);
- return;
+ return false;
}
shutdown_thread.join();
- // The connections were closed and destroyed on the signaling thread before
- // the shutdown thread finished, so it's safe to drop the threads now.
- mNetworkThread = nullptr;
- mWorkerThread = nullptr;
- mSignalingThread = nullptr;
-
webrtc::LogMessage::RemoveLogToStream(mLogSink);
+ return true;
}
@@ -862,7 +910,9 @@ void LLWebRTCImpl::updateDevices()
void LLWebRTCImpl::OnDevicesUpdated()
{
- updateDevices();
+ // OnDevicesUpdated() is called on macOS CoreAudio's device-change callback
+ // thread. Calling updateDevices() on that thread causes a deadlock.
+ mWorkerThread->PostTask([this] { updateDevices(); });
}
@@ -1140,17 +1190,23 @@ void LLWebRTCPeerConnectionImpl::closeOnSignalingThread()
mLocalStream = nullptr;
}
mPeerConnection = nullptr;
+ }
- for (auto &observer : mSignalingObserverList)
- {
- observer->OnPeerConnectionClosed();
- }
+ // Notify unconditionally, even if there was no peer connection to close --
+ // a connection can be shut down before it ever finished initializing, and
+ // the caller is still waiting to hear that the close is done. Withholding
+ // this leaves the viewer's connection state machine parked in
+ // VOICE_STATE_WAIT_FOR_CLOSE, which has no timeout of its own.
+ for (auto &observer : mSignalingObserverList)
+ {
+ observer->OnPeerConnectionClosed();
}
- // Nothing may call back into the viewer past this point. On shutdown the
- // viewer's connection objects are torn down as soon as llwebrtc::terminate()
- // returns and they deliberately don't unset themselves as observers, so any
- // late callback would be reaching into freed memory.
+ // Nothing may call back into the viewer past this point. Connections
+ // closed while the viewer is still running unset themselves as observers
+ // when they're destroyed, but any that are left for llwebrtc::terminate()
+ // to close deliberately don't -- they're torn down as soon as it returns,
+ // so a late callback would be reaching into freed memory.
mSignalingObserverList.clear();
mDataObserverList.clear();
}
@@ -1775,20 +1831,20 @@ void LLWebRTCPeerConnectionImpl::OnStateChange()
switch (mDataChannel->state())
{
case webrtc::DataChannelInterface::kOpen:
- RTC_LOG(LS_INFO) << __FUNCTION__ << " Data Channel State Open";
+ RTC_LOG(LS_VERBOSE) << __FUNCTION__ << " Data Channel State Open";
for (auto &observer : mSignalingObserverList)
{
observer->OnDataChannelReady(this);
}
break;
case webrtc::DataChannelInterface::kConnecting:
- RTC_LOG(LS_INFO) << __FUNCTION__ << " Data Channel State Connecting";
+ RTC_LOG(LS_VERBOSE) << __FUNCTION__ << " Data Channel State Connecting";
break;
case webrtc::DataChannelInterface::kClosing:
- RTC_LOG(LS_INFO) << __FUNCTION__ << " Data Channel State closing";
+ RTC_LOG(LS_VERBOSE) << __FUNCTION__ << " Data Channel State closing";
break;
case webrtc::DataChannelInterface::kClosed:
- RTC_LOG(LS_INFO) << __FUNCTION__ << " Data Channel State closed";
+ RTC_LOG(LS_VERBOSE) << __FUNCTION__ << " Data Channel State closed";
break;
default:
break;
@@ -1947,8 +2003,14 @@ void terminate()
{
if (gWebRTCImpl)
{
- gWebRTCImpl->terminate();
- delete gWebRTCImpl;
+ if (gWebRTCImpl->terminate())
+ {
+ delete gWebRTCImpl;
+ }
+ // Otherwise shutdown timed out and was left to a detached thread that is
+ // still using this object -- and the webrtc threads it owns -- so it's
+ // intentionally leaked. Deleting it would hand that thread a freed
+ // object to finish shutting down with.
gWebRTCImpl = nullptr;
}
}